diff --git a/.envrc b/.envrc index 17a303ba34..115b68cb32 100644 --- a/.envrc +++ b/.envrc @@ -7,22 +7,13 @@ use_flake_subdir() { watch_file $file done mkdir -p "$(direnv_layout_dir)" - if [ -z "$ARTIFACTORY_PASSWORD" ] || [ "${OSS_ONLY:-}" == 1 ] || [ "${OSS_ONLY:-}" == "true" ]; then - eval "$(nix print-dev-env --profile "$(direnv_layout_dir)/flake-profile" "path:nix#oss" "$@")" - else - eval "$(nix print-dev-env --profile "$(direnv_layout_dir)/flake-profile" "path:nix" "$@")" - fi + eval "$(nix print-dev-env --profile "$(direnv_layout_dir)/flake-profile" "path:nix" "$@")" } source_envrc_private() { [[ -f .envrc.private ]] && [[ -z "$IGNORE_PRIVATE_ENVRC" ]] && source_env .envrc.private || true } -# Source .envrc.private before use_flake_subdir because it provides ARTIFACTORY_PASSWORD -# which determines whether the enterprise or OSS nix shell is used. -# This workaround can be removed once the enterprise edition is removed. -source_envrc_private - # TODO(DACH-NY/canton-network-node#3876) work around for $TMPDIR is removed. #3876 to investigate more OLD_TMPDIR=${TMPDIR-unset} @@ -42,8 +33,6 @@ source "${TOOLS_LIB}/libcli.source" source_env .envrc.validate -export ENTERPRISE_ARTIFACTORY_DOCKER_REGISTRY=digitalasset-canton-enterprise-docker.jfrog.io - # Configure docker access for cluster and integration tests ## Read current credentials function read_docker_creds() { @@ -58,13 +47,6 @@ function read_docker_creds() { fi } -function check_docker_creds() { - local docker_creds="$1" - [[ -z "$docker_creds" || "$docker_creds" == "null" || "$docker_creds" != "$ARTIFACTORY_USER:$ARTIFACTORY_PASSWORD" ]] && \ - ## And artifactory user and password are set - [[ -n "$ARTIFACTORY_USER" && -n "$ARTIFACTORY_PASSWORD" ]] -} - function check_docker_creds_ghcr() { local docker_creds="$1" [[ -z "$docker_creds" || "$docker_creds" == "null" || "$docker_creds" != "$GH_USER:$GH_TOKEN" ]] && \ @@ -77,12 +59,6 @@ if check_docker_creds_ghcr "$DOCKER_CREDS"; then echo $GH_TOKEN | docker login "$GHCR" -u "$GH_USER" --password-stdin fi -DOCKER_CREDS=$(read_docker_creds "$ENTERPRISE_ARTIFACTORY_DOCKER_REGISTRY") -if check_docker_creds "$DOCKER_CREDS"; then - echo "Logging into $ENTERPRISE_ARTIFACTORY_DOCKER_REGISTRY" - echo $ARTIFACTORY_PASSWORD | docker login "$ENTERPRISE_ARTIFACTORY_DOCKER_REGISTRY" -u "$ARTIFACTORY_USER" --password-stdin -fi - # re-export CIRCLECI_TOKEN (which we use in many places) as CIRCLECI_CLI_TOKEN (which `circleci` cli tool picks up) export CIRCLECI_CLI_TOKEN="${CIRCLECI_TOKEN}" diff --git a/.envrc.validate b/.envrc.validate index 4a73ce2303..f326e2da30 100644 --- a/.envrc.validate +++ b/.envrc.validate @@ -34,12 +34,5 @@ if [[ -z "$IGNORE_PRIVATE_ENVRC" ]]; then AUTH0_VALIDATOR_MANAGEMENT_API_CLIENT_ID \ AUTH0_VALIDATOR_MANAGEMENT_API_CLIENT_SECRET \ AUTH0_TESTS_MANAGEMENT_API_CLIENT_ID \ - AUTH0_TESTS_MANAGEMENT_API_CLIENT_SECRET \ - ARTIFACTORY_USER \ - ARTIFACTORY_PASSWORD -fi - -if [ "$IS_ENTERPRISE" != "true" ]; then - echo "" - _warning "Note: Using Canton community instead of Canton enterprise. Certain tests which rely on Enterprise features will fail locally." + AUTH0_TESTS_MANAGEMENT_API_CLIENT_SECRET fi diff --git a/.envrc.vars b/.envrc.vars index 69864a4ec1..70783df4dd 100644 --- a/.envrc.vars +++ b/.envrc.vars @@ -105,7 +105,7 @@ export SPLICE_OAUTH_SV_TEST_CLIENT_ID_VALIDATOR=bUfFRpl2tEfZBB7wzIo9iRNGTj8wMeIn export USE_GKE_GCLOUD_AUTH_PLUGIN=true # CometBFT settings -export COMETBFT_DOCKER_IMAGE="digitalasset-canton-enterprise-docker.jfrog.io/cometbft-canton-network:${COMETBFT_RELEASE_VERSION}" +export COMETBFT_DOCKER_IMAGE="europe-docker.pkg.dev/da-images/public/docker/cometbft-canton-network:${COMETBFT_RELEASE_VERSION}" #Test containers config ## Speed up runs diff --git a/.github/actions/nix/setup_nix/action.yml b/.github/actions/nix/setup_nix/action.yml index 78920272cf..9ba47061c8 100644 --- a/.github/actions/nix/setup_nix/action.yml +++ b/.github/actions/nix/setup_nix/action.yml @@ -1,19 +1,14 @@ name: "Setup Nix" description: "Setup Nix" inputs: - artifactory_user: - description: "The Artifactory user" - required: true - artifactory_password: - description: "The Artifactory password" - required: true nix_path: description: "The path to nix flake directory" required: false default: ${{ format('{0}/nix', github.repository == 'DACH-NY/canton-network-internal' && 'splice' || '.') }} target: - description: "Choose nix target: oss - restrict upstream dependencies (e.g. Canton) to OSS versions (the equivalent of OSS_ONLY=1 in local checkouts), static_tests - for static tests, default - for enterprise dependencies" - required: true + description: "Choose nix target: static_tests - for static tests, default - for full dependencies" + required: false + default: "default" cache_version: description: "Cache version" required: true @@ -37,12 +32,12 @@ runs: using: "composite" steps: - name: Validate input - if: "${{!( inputs.target == 'default' || inputs.target == 'oss' || inputs.target == 'static_tests')}}" + if: "${{!( inputs.target == 'default' || inputs.target == 'static_tests')}}" shell: bash id: validate_input run: | echo "Target invalid: ${{ inputs.target }}" - echo "Target needs to be one of: 'default', 'oss', 'static_tests'" + echo "Target needs to be one of: 'default', 'static_tests'" exit 1 - name: Compute cache Key id: cache_key @@ -60,8 +55,8 @@ runs: echo "home: $HOME" >> /tmp/nix-cache-key # important when restoring simlinks from cache, apparently echo "nix binary version: $NIX_BINARY_VERSION" >> /tmp/nix-cache-key # different nix versions might behave differently and corrupt the caches echo "target: ${{ inputs.target }}" >> /tmp/nix-cache-key - if [ "${{ inputs.target }}" != 'default' ]; then - echo "Using OSS only dependencies" + if [ "${{ inputs.target }}" == 'static_tests' ]; then + echo "Using minimal nix dependencies for static tests" fi cat /tmp/nix-cache-key cache_key=($(md5sum "/tmp/nix-cache-key")) @@ -76,11 +71,6 @@ runs: run: | set -euxo pipefail - if [[ ${{ inputs.target }} == 'default' ]]; then - echo "Must use OSS only dependencies in GitHub-hosted runners" - exit 1 - fi - echo "Latest nix cache:" wget -q "https://storage.googleapis.com/splice-nix-cache-public/${cache_key}.tar.gz" -O cache.tar.gz || true @@ -143,15 +133,6 @@ runs: sh <(curl -fsSL --retry 8 "https://releases.nixos.org/nix/nix-$NIX_BINARY_VERSION/install") --no-daemon sudo mkdir -p /etc/nix sudo chmod a+rw /etc/nix - if [[ "${{ inputs.target }}" != 'default' ]]; then - echo "Using OSS only dependencies, not setting up Artifactory credentials" - else - cat < /etc/nix/netrc - machine digitalasset.jfrog.io - login ${{ inputs.artifactory_user }} - password ${{ inputs.artifactory_password }} - EOF - fi export USER=$(whoami) echo "Running nix.sh" . ~/.nix-profile/etc/profile.d/nix.sh diff --git a/.github/actions/sbt/execute_sbt_command/action.yml b/.github/actions/sbt/execute_sbt_command/action.yml index 1bb02c8083..295450f584 100644 --- a/.github/actions/sbt/execute_sbt_command/action.yml +++ b/.github/actions/sbt/execute_sbt_command/action.yml @@ -7,12 +7,6 @@ inputs: # The caller needs to quote commands that contain spaces, e.g. "\"testOnly myTest\"". description: "The SBT command to run" required: true - artifactory_user: - description: "Artifactory user" - required: false - artifactory_password: - description: "Artifactory password" - required: false extra_env_vars: description: "Extra environment variables to set before running the SBT command" required: false @@ -51,7 +45,7 @@ runs: - name: Execute SBT command" uses: ./.github/actions/nix/run_bash_command_in_nix with: - additional_nix_args: "--keep GITHUB_ACTION_PATH --keep ARTIFACTORY_USER --keep ARTIFACTORY_PASSWORD ${{ inputs.additional_nix_args }}" + additional_nix_args: "--keep GITHUB_ACTION_PATH ${{ inputs.additional_nix_args }}" cmd: | # This might help resolve https://github.com/DACH-NY/canton-network-node/issues/8146 export PROTOCBRIDGE_NO_CLEANUP="1" @@ -91,8 +85,6 @@ runs: $GITHUB_ACTION_PATH/../../scripts/check-sbt-output.sh "sbt_output" fi } - export ARTIFACTORY_USER="${{ inputs.artifactory_user }}" - export ARTIFACTORY_PASSWORD="${{ inputs.artifactory_password }}" # Ensure that we're in the root of splice before execution pushd ${{ inputs.splice_root }} &> /dev/null diff --git a/.github/actions/tests/common_test_setup/action.yml b/.github/actions/tests/common_test_setup/action.yml index 7f38144eca..c405f8d369 100644 --- a/.github/actions/tests/common_test_setup/action.yml +++ b/.github/actions/tests/common_test_setup/action.yml @@ -16,14 +16,8 @@ inputs: description: "Whether to save the Nix cache to GCP" required: false default: "false" - artifactory_user: - description: "The Artifactory user" - required: false - artifactory_password: - description: "The Artifactory password" - required: false target: - description: "Choose nix target: oss - restrict upstream dependencies (e.g. Canton) to OSS versions (the equivalent of OSS_ONLY=1 in local checkouts), static_tests - for static tests, default - for enterprise dependencies" + description: "Choose nix target: static_tests - for static tests, default - for full dependencies" default: 'default' # type: choice # options: @@ -51,12 +45,6 @@ outputs: runs: using: "composite" steps: - - name: Validate input - if: inputs.target == 'default' && (inputs.artifactory_password == '' || inputs.artifactory_user == '') - shell: bash - run: | - echo "artifactory_user and artifactory_password must be provided if not using OSS only dependencies." - exit 1 - name: Publish test name metric uses: miguelteixeiraa/action-run-in-background@e28f036c202e9066287e6a50ce8b80749627cc7d # v1.0.0 @@ -70,8 +58,6 @@ runs: - name: Set up Nix (Self hosted) uses: ./.github/actions/nix/setup_nix with: - artifactory_user: ${{ inputs.artifactory_user }} - artifactory_password: ${{ inputs.artifactory_password }} cache_version: ${{ inputs.cache_version }} should_save: ${{ inputs.save_nix_cache }} should_save_gcp: ${{ inputs.save_nix_cache_to_gcp }} diff --git a/.github/actions/tests/scala_test/action.yml b/.github/actions/tests/scala_test/action.yml index 728593c162..ac022f3b5b 100644 --- a/.github/actions/tests/scala_test/action.yml +++ b/.github/actions/tests/scala_test/action.yml @@ -8,12 +8,6 @@ inputs: start_canton_options: description: "Options for start-canton.sh" required: true - artifactory_user: - description: "The Artifactory user" - required: true - artifactory_password: - description: "The Artifactory password" - required: true test_suite_name: description: "Name of the test suite" required: true @@ -93,10 +87,6 @@ inputs: protocol_version: description: "Synchronizer Protocol Version" required: true - oss_only: - description: "Restrict upstream dependencies (e.g. Canton) to OSS versions (the equivalent of OSS_ONLY=1 in local checkouts)" - required: false - default: "false" cache_version: description: "Cache version" required: true @@ -109,12 +99,7 @@ runs: with: test_name: ${{ inputs.test_suite_name }} with_sbt: false # we setup SBT later while canton is starting up - artifactory_user: ${{ inputs.artifactory_user }} - artifactory_password: ${{ inputs.artifactory_password }} - target: ${{ inputs.oss_only == true && 'oss' || 'default' }} - # The docs job saves the oss nix cache, here we save the non-oss one, but only in one runner to reduce contention - # TODO(#1296): When this runner stops using non-oss, move this to one that does - save_nix_cache: ${{ inputs.runner_index == 0 && inputs.test_suite_name == 'canton-enterprise' }} + target: 'default' - name: Wait for postgres uses: ./.github/actions/nix/run_bash_command_in_nix @@ -230,8 +215,6 @@ runs: uses: ./.github/actions/nix/run_bash_command_in_nix with: cmd: | - export ARTIFACTORY_USER="${{ inputs.artifactory_user }}" - export ARTIFACTORY_PASSWORD="${{ inputs.artifactory_password }}" export CIRCLE_REPOSITORY_URL="${{ github.repositoryUrl }}" export CIRCLE_SHA1="${{ github.sha }}" /usr/bin/sudo mkdir -p ~/.docker/buildx @@ -258,8 +241,6 @@ runs: uses: ./.github/actions/sbt/execute_sbt_command with: extra_env_vars: "POSTGRES_DB=postgres POSTGRES_HOST=localhost POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres INITIAL_PACKAGE_VERSIONS=${{ steps.daml_package_versions.outputs.initial_package_versions }} PROTOCOL_VERSION=${{ inputs.protocol_version }}" - artifactory_user: ${{ inputs.artifactory_user }} - artifactory_password: ${{ inputs.artifactory_password }} cmd: ${{ steps.list_tests.outputs.RUN_SPLITTED_TESTS_CMD }} additional_nix_args: "--keep GITHUB_ACTION" extra_parameters: -DAUTH0_MANAGEMENT_API_CLIENT_ID=${{ inputs.auth0_management_api_client_id }} -DAUTH0_MANAGEMENT_API_CLIENT_SECRET=${{ inputs.auth0_management_api_client_secret }} diff --git a/.github/actions/tests/skip_on_static/action.yml b/.github/actions/tests/skip_on_static/action.yml index 73e3b6f0d0..2111eb9689 100644 --- a/.github/actions/tests/skip_on_static/action.yml +++ b/.github/actions/tests/skip_on_static/action.yml @@ -24,10 +24,17 @@ runs: # as the latter is fixed when the job starts which for `env_hold` jobs # is _before_ the approval already e.g. when an external contributor # created the PR and not when the maintainer approved it after adding the static label. - pr_labels=$(curl -sSL --fail-with-body -H "Authorization: Bearer ${{ inputs.gh_token }}" \ - --retry 10 --retry-delay 10 --retry-all-errors \ - -H "Accept: application/vnd.github.v3+json" \ - "${{ github.event.pull_request.url }}" | jq '.labels') + # Write the response to a file instead of piping into jq: curl resets an + # -o output file between retries, but a pipe keeps the failed-attempt + # bodies (breaking jq) and dies once jq exits (curl error 23). + pr_json="$RUNNER_TEMP/pr.json" + curl -sSL --fail-with-body -o "$pr_json" \ + -H "Authorization: Bearer ${{ inputs.gh_token }}" \ + --retry 10 --retry-delay 10 --retry-all-errors \ + -H "Accept: application/vnd.github.v3+json" \ + "${{ github.event.pull_request.url }}" \ + || { echo "PR fetch failed; last response body:"; cat "$pr_json"; exit 1; } + pr_labels=$(jq '.labels' "$pr_json") echo "Pull request labels: $pr_labels" static_label=$(echo "$pr_labels" | jq -r '.[] | select(.name == "static") | .name' | grep -c 'static' || true) if [[ "$last_commit_msg" == *"[static]"* ]] || [[ "$static_label" -gt 0 ]]; then diff --git a/.github/runners/runner-container-hooks b/.github/runners/runner-container-hooks index 32b74210ef..8472c0f706 160000 --- a/.github/runners/runner-container-hooks +++ b/.github/runners/runner-container-hooks @@ -1 +1 @@ -Subproject commit 32b74210efb8c288ffd9e994ee6a90337ce156d3 +Subproject commit 8472c0f7060e0f2d956185eace05812c33bacbb5 diff --git a/.github/store-perf-thresholds.json b/.github/store-perf-thresholds.json index b69ca37bc3..aa2a4b14fc 100644 --- a/.github/store-perf-thresholds.json +++ b/.github/store-perf-thresholds.json @@ -1,52 +1,52 @@ { "SvDsoStoreIngestionPerformanceTest": { - "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-May-07", + "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-June-25", "splice_perf_ingestion_avg_item_time_ns": { - "max": 2500000 + "max": 3700000 }, - "_comment_total_time_ns": "our hard max is 1h, but we set it to 10m based on the past data collected on 2026-May-07 to detect the trends earlier", + "_comment_total_time_ns": "our hard max is 1h, but we set it to 10m based on the past data collected on 2026-June-25 to detect the trends earlier", "splice_perf_ingestion_total_time_ns": { "max": 600000000000 } }, "ScanStoreIngestionPerformanceTest": { - "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-May-07", + "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-June-25", "splice_perf_ingestion_avg_item_time_ns": { - "max": 2400000 + "max": 3000000 }, - "_comment_total_time_ns": "our hard max is 1h, but we set it to 10m based on the past data collected on 2026-May-07 to detect the trends earlier", + "_comment_total_time_ns": "our hard max is 1h, but we set it to 10m based on the past data collected on 2026-June-25 to detect the trends earlier", "splice_perf_ingestion_total_time_ns": { "max": 600000000000 } }, "UpdateHistoryIngestionPerformanceTest": { - "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-May-07", + "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-June-25", "splice_perf_ingestion_avg_item_time_ns": { - "max": 16800000 + "max": 27400000 }, - "_comment_total_time_ns": "our hard max is 1h, but we set it to 20m based on the past data collected on 2026-May-07 to detect the trends earlier", + "_comment_total_time_ns": "our hard max is 1h, but we set it to 20m based on the past data collected on 2026-June-25 to detect the trends earlier", "splice_perf_ingestion_total_time_ns": { "max": 1200000000000 } }, "UpdateHistoryReadPerformanceTest-getUpdate": { - "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-May-07", + "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-June-25", "splice_perf_read_avg_item_time_ns": { - "max": 753600000 + "max": 775400000 }, "_comment_total_time_ns": "hard max same as avg_item_time_ns, as we read 1 item", "splice_perf_read_total_time_ns": { - "max": 753600000 + "max": 775400000 } }, "UpdateHistoryReadPerformanceTest-encodeUpdate": { - "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-May-07", + "_comment_avg_item_time_ns": "median*(1+ noise_margin + safety_margin) of last 30d. noise_margin=(p95-median)/median, safety_margin=10%. calculated on 2026-June-25", "splice_perf_read_avg_item_time_ns": { - "max": 337000000 + "max": 397400000 }, "_comment_total_time_ns": "hard max same as avg_item_time_ns, as we read 1 item", "splice_perf_read_total_time_ns": { - "max": 337000000 + "max": 397400000 } } } diff --git a/.github/workflows/build.daml_test.yml b/.github/workflows/build.daml_test.yml index 0ef5f6828c..016ba8c4d5 100644 --- a/.github/workflows/build.daml_test.yml +++ b/.github/workflows/build.daml_test.yml @@ -34,7 +34,6 @@ jobs: with: cache_version: 8 test_name: daml_test - target: 'oss' - name: Run Daml tests if: steps.skip.outputs.skip != 'true' diff --git a/.github/workflows/build.deployment_test.yml b/.github/workflows/build.deployment_test.yml index 80f4e14e0b..e7705bb0ed 100644 --- a/.github/workflows/build.deployment_test.yml +++ b/.github/workflows/build.deployment_test.yml @@ -25,10 +25,9 @@ jobs: - name: Setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 + cache_version: 9 test_name: deployment_test with_sbt: false - target: 'oss' - name: Helm tests uses: ./.github/actions/nix/run_bash_command_in_nix diff --git a/.github/workflows/build.docs.yml b/.github/workflows/build.docs.yml index 49c5e6c996..362a4cf039 100644 --- a/.github/workflows/build.docs.yml +++ b/.github/workflows/build.docs.yml @@ -25,19 +25,16 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 + cache_version: 9 test_name: docs save_nix_cache: true save_nix_cache_to_gcp: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} - target: 'oss' upload_workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER_SPLICE }} upload_service_account: ${{ secrets.CACHE_UPLOADER_SA }} - name: Build docs uses: ./.github/actions/sbt/execute_sbt_command with: - artifactory_user: ${{ vars.ARTIFACTORY_USER }} - artifactory_password: ${{ secrets.ARTIFACTORY_PASSWORD }} # We Test/compile here as the docs job is the one that pushes to the SBT cache # as it finishes fastest and we want to ensure that it includes test sources. cmd: "Test/compile docs/bundle" @@ -51,7 +48,7 @@ jobs: - name: Post-SBT job uses: ./.github/actions/sbt/post_sbt with: - cache_version: 8 + cache_version: 9 setup_sbt_cache_hits: ${{ steps.setup.outputs.sbt_cache_hits }} - name: Report Failures on Slack & Github diff --git a/.github/workflows/build.scala_test.yml b/.github/workflows/build.scala_test.yml index 8ec6727935..5f0434d110 100644 --- a/.github/workflows/build.scala_test.yml +++ b/.github/workflows/build.scala_test.yml @@ -57,11 +57,6 @@ on: description: "Canton protocol version" type: string required: true - oss_only: - description: "Restrict upstream dependencies (e.g. Canton) to OSS versions (the equivalent of OSS_ONLY=1 in local checkouts)" - required: false - type: boolean - default: false postgres_image: description: "The image to use for PostgreSQL" required: false @@ -142,7 +137,7 @@ jobs: - name: Run Tests uses: ./.github/actions/tests/scala_test with: - cache_version: 8 + cache_version: 9 with_canton: ${{ inputs.with_canton }} start_canton_options: ${{ inputs.start_canton_options }} test_suite_name: ${{ inputs.test_name }} @@ -162,6 +157,3 @@ jobs: failure_notifications_slack_channel: ${{ secrets.FAILURE_NOTIFICATIONS_SLACK_CHANNEL }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} - artifactory_user: ${{ inputs.oss_only && '' || vars.ARTIFACTORY_USER }} - artifactory_password: ${{ inputs.oss_only && '' || secrets.ARTIFACTORY_PASSWORD }} diff --git a/.github/workflows/build.scala_test_for_compose.yml b/.github/workflows/build.scala_test_for_compose.yml index 45b45b90df..b348d7723c 100644 --- a/.github/workflows/build.scala_test_for_compose.yml +++ b/.github/workflows/build.scala_test_for_compose.yml @@ -104,10 +104,8 @@ jobs: - name: Run Tests uses: ./.github/actions/tests/scala_test with: - cache_version: 8 + cache_version: 9 start_canton_options: ${{ inputs.start_canton_options }} - artifactory_user: ${{ vars.ARTIFACTORY_USER }} - artifactory_password: ${{ secrets.ARTIFACTORY_PASSWORD }} test_suite_name: ${{ inputs.test_name }} test_names: ${{ needs.split_tests.outputs.test_names }} runner_index: ${{ matrix.runner-index }} diff --git a/.github/workflows/build.scala_test_with_cometbft.yml b/.github/workflows/build.scala_test_with_cometbft.yml index b8f738c6a1..6434e79ee7 100644 --- a/.github/workflows/build.scala_test_with_cometbft.yml +++ b/.github/workflows/build.scala_test_with_cometbft.yml @@ -120,10 +120,8 @@ jobs: - name: Run Tests uses: ./.github/actions/tests/scala_test with: - cache_version: 8 + cache_version: 9 start_canton_options: -F -w - artifactory_user: ${{ vars.ARTIFACTORY_USER }} - artifactory_password: ${{ secrets.ARTIFACTORY_PASSWORD }} test_suite_name: ${{ inputs.test_name }} test_names: ${{ needs.split_tests.outputs.test_names }} runner_index: ${{ matrix.runner-index }} diff --git a/.github/workflows/build.static_tests.yml b/.github/workflows/build.static_tests.yml index 5690f886c8..1c0a138cca 100644 --- a/.github/workflows/build.static_tests.yml +++ b/.github/workflows/build.static_tests.yml @@ -13,15 +13,9 @@ on: type: boolean required: false default: false - oss_only: - type: boolean - required: false - default: false jobs: check_canton_consistency: - # Skipped for external contributors (fork PRs) due to container registry auth constraints - if: ${{ !inputs.oss_only }} runs-on: ${{ inputs.self_hosted && 'self-hosted-k8s-x-small' || 'ubuntu-24.04' }} timeout-minutes: 15 container: @@ -37,7 +31,7 @@ jobs: - name: Setup Nix uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 + cache_version: 9 test_name: canton_consistency target: 'static_tests' @@ -49,6 +43,9 @@ jobs: CURRENT_VERSION=$(jq -r '.version' nix/canton-sources.json) echo "Validating Docker digests for Canton version: $CURRENT_VERSION" + # TODO(#6581) to be removed + export CONTAINERS_REGISTRIES_CONF=$(mktemp) + function set_value() { local key="$1" local value="$2" @@ -58,7 +55,7 @@ jobs: for img in base participant mediator sequencer; do echo "Fetching image sha256 for canton-$img..." - sha=$(skopeo inspect --override-os linux --override-arch amd64 "docker://europe-docker.pkg.dev/da-images/public-all/docker/canton-$img:${CURRENT_VERSION}" --format '{{.Digest}}') + sha=$(skopeo inspect --no-creds --override-os linux --override-arch amd64 "docker://europe-docker.pkg.dev/da-images/public-all/docker/canton-$img:${CURRENT_VERSION}" --format '{{.Digest}}') set_value "canton_${img}_image_sha256" "$sha" done @@ -90,7 +87,7 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 + cache_version: 9 test_name: static_tests target: 'static_tests' diff --git a/.github/workflows/build.ts_cli_tests.yml b/.github/workflows/build.ts_cli_tests.yml index 6b0904cffd..1ddf0f3bf9 100644 --- a/.github/workflows/build.ts_cli_tests.yml +++ b/.github/workflows/build.ts_cli_tests.yml @@ -41,9 +41,8 @@ jobs: if: steps.skip.outputs.skip != 'true' uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 + cache_version: 9 test_name: ts_cli - target: 'oss' - name: Run Token Standard CLI tests if: steps.skip.outputs.skip != 'true' diff --git a/.github/workflows/build.ui_tests.yml b/.github/workflows/build.ui_tests.yml index 33ebfda8f5..aa1b5b5855 100644 --- a/.github/workflows/build.ui_tests.yml +++ b/.github/workflows/build.ui_tests.yml @@ -41,9 +41,8 @@ jobs: if: steps.skip.outputs.skip != 'true' uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 + cache_version: 9 test_name: ui_tests - target: 'oss' - name: Run UI tests if: steps.skip.outputs.skip != 'true' @@ -63,7 +62,7 @@ jobs: if: steps.skip.outputs.skip != 'true' uses: ./.github/actions/sbt/post_sbt with: - cache_version: 8 + cache_version: 9 setup_sbt_cache_hits: ${{ steps.setup.outputs.sbt_cache_hits }} - name: Upload logs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4676256eed..5dac6847c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,11 +17,6 @@ on: type: string required: false default: "" - oss_only: - description: "Restrict upstream dependencies (e.g. Canton) to OSS versions (skips tests that do not support running in OSS)" - required: false - type: boolean - default: false workflow_dispatch: inputs: commit_sha: @@ -39,11 +34,6 @@ on: type: string required: false default: "35" - oss_only: - description: "Restrict upstream dependencies (e.g. Canton) to OSS versions (skips tests that do not support running in OSS)" - required: false - type: boolean - default: false permissions: id-token: write # Required for GCP Workload Identity for failure notifications @@ -93,7 +83,6 @@ jobs: with: commit_sha: ${{ inputs.commit_sha }} self_hosted: true - oss_only: ${{ inputs.oss_only }} deployment_test: uses: ./.github/workflows/build.deployment_test.yml @@ -118,7 +107,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_resource_intensive: @@ -132,7 +120,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_record_time_tolerance: @@ -146,7 +133,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_logical_sync_upgrade: @@ -160,7 +146,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_roll_forward_lsu: @@ -174,12 +159,10 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_with_cometbft: uses: ./.github/workflows/build.scala_test_with_cometbft.yml - if: ${{ ! inputs.oss_only }} with: runs_on: self-hosted-k8s-medium test_names_file: "test-cometbft-full-class-names.log" @@ -202,7 +185,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_wall_clock_time: @@ -219,7 +201,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_frontend_wall_clock_time: @@ -234,7 +215,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_frontend_simtime: @@ -249,12 +229,10 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} secrets: inherit scala_test_docker_compose: uses: ./.github/workflows/build.scala_test_for_compose.yml - if: ${{ ! inputs.oss_only }} with: runs_on: self-hosted-docker-large test_names_file: 'test-full-class-names-docker-based.log' @@ -268,7 +246,6 @@ jobs: scala_test_with_docker_no_canton: uses: ./.github/workflows/build.scala_test_for_compose.yml - if: ${{ ! inputs.oss_only }} with: runs_on: self-hosted-docker-large test_names_file: 'test-full-class-names-docker-no-canton.log' @@ -283,7 +260,6 @@ jobs: scala_test_with_docker_and_canton_simtime: uses: ./.github/workflows/build.scala_test_for_compose.yml - if: ${{ ! inputs.oss_only }} with: runs_on: self-hosted-docker-large test_names_file: 'test-full-class-names-sim-time-docker.log' @@ -312,22 +288,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: ${{ inputs.oss_only }} - secrets: inherit - - scala_test_canton_enterprise: - uses: ./.github/workflows/build.scala_test.yml - if: ${{ ! inputs.oss_only }} - with: - runs_on: self-hosted-k8s-large - test_names_file: 'test-full-class-names-canton-enterprise.log' - start_canton_options: -w - parallelism: 1 - test_name: canton-enterprise - with_gcp_creds: true - commit_sha: ${{ inputs.commit_sha }} - daml_base_version: ${{ inputs.daml_base_version }} - protocol_version: ${{ inputs.protocol_version }} secrets: inherit ui_tests: @@ -360,7 +320,6 @@ jobs: commit_sha: ${{ inputs.commit_sha }} daml_base_version: ${{ inputs.daml_base_version }} protocol_version: ${{ inputs.protocol_version }} - oss_only: true secrets: inherit final_result: @@ -383,7 +342,6 @@ jobs: - scala_test_with_docker_no_canton - scala_test_with_docker_and_canton_simtime - scala_test_app_upgrade - - scala_test_canton_enterprise - scala_test_without_canton - ui_tests - ts_cli_tests @@ -430,5 +388,4 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ env.JOBS }} - # Allow skipping the enterprise-only jobs - allowed-skips: scala_test_with_cometbft, scala_test_docker_compose, scala_test_local_net, scala_test_canton_enterprise + allowed-skips: scala_test_with_cometbft, scala_test_docker_compose, scala_test_local_net diff --git a/.github/workflows/bump_gha_runner_version.yml b/.github/workflows/bump_gha_runner_version.yml index abaf4fb34e..b6e3838256 100644 --- a/.github/workflows/bump_gha_runner_version.yml +++ b/.github/workflows/bump_gha_runner_version.yml @@ -27,9 +27,7 @@ jobs: - name: Set up Nix (Self hosted) uses: ./.github/actions/nix/setup_nix with: - cache_version: 8 - artifactory_user: dummy - artifactory_password: dummy + cache_version: 9 target: default - name: Check for the latest version and create a PR to splice diff --git a/.github/workflows/canton_oss_test.yml b/.github/workflows/canton_oss_test.yml deleted file mode 100644 index ee5503e713..0000000000 --- a/.github/workflows/canton_oss_test.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Integration tests against canton oss -on: - schedule: - - cron: '0 4 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - id-token: write # Required for GCP Workload Identity for failure notifications - contents: read - pull-requests: read # Required for the static tests - issues: read # Required for the static tests - actions: write # To cancel itself if not opted in - -jobs: - build: - uses: ./.github/workflows/build.yml - with: - oss_only: true - secrets: inherit diff --git a/.github/workflows/daml_compat_test.yml b/.github/workflows/daml_compat_test.yml index 3d01694abe..c051a2a4c3 100644 --- a/.github/workflows/daml_compat_test.yml +++ b/.github/workflows/daml_compat_test.yml @@ -24,7 +24,10 @@ jobs: id: get_mainnet_version run: | set -eou pipefail - version="$(curl -sSL --fail-with-body https://docs.global.canton.network.sync.global/info | jq -r '.sv.version')" + info_json="$RUNNER_TEMP/info.json" + curl -sSL --fail-with-body -o "$info_json" https://docs.global.canton.network.sync.global/info \ + || { echo "Fetch failed; response body:"; cat "$info_json"; exit 1; } + version="$(jq -r '.sv.version' "$info_json")" echo "MainNet version is $version" echo "version=$version" >> "$GITHUB_OUTPUT" @@ -33,6 +36,4 @@ jobs: uses: ./.github/workflows/build.yml with: daml_base_version: ${{ needs.get_mainnet_version.outputs.version }} - # TODO(#5433) - remove after adopted on mainnet - protocol_version: "34" secrets: inherit diff --git a/.github/workflows/monthly-schedule.yml b/.github/workflows/monthly-schedule.yml new file mode 100644 index 0000000000..b10f37d386 --- /dev/null +++ b/.github/workflows/monthly-schedule.yml @@ -0,0 +1,53 @@ +name: Monthly Schedule + +on: + workflow_dispatch: + inputs: + version: + description: "Splice version, e.g. 0.8" + required: true + type: string + + month: + description: "Month in YYYY-MM format, e.g. 2026-08" + required: true + type: string + + dry_run: + description: "Dry run only — do not modify Monday" + required: true + default: true + type: boolean + +permissions: + contents: read + +jobs: + create-schedule: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + + - name: Create monthly schedule + env: + MONDAY_API_TOKEN: ${{ secrets.MONDAY_API_TOKEN }} + MONDAY_BOARD_ID: ${{ secrets.MONDAY_BOARD_ID }} + shell: bash + run: | + ARGS=( + "${{ inputs.version }}" + "${{ inputs.month }}" + ) + + if [[ "${{ inputs.dry_run }}" == "true" ]]; then + ARGS+=("--dry-run") + fi + + python3 scripts/monthly-schedule.py "${ARGS[@]}" diff --git a/.github/workflows/performance_tests.yml b/.github/workflows/performance_tests.yml index 1e02b76c8b..caf66cf18d 100644 --- a/.github/workflows/performance_tests.yml +++ b/.github/workflows/performance_tests.yml @@ -34,9 +34,8 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 - test_name: oss - target: 'oss' + cache_version: 9 + test_name: ingestion_performance_tests # Authenticate to GCP for read access to GCS - name: Authenticate to GCP (mainnet-history-dumps) @@ -138,9 +137,8 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 - test_name: oss - target: 'oss' + cache_version: 9 + test_name: read_performance_tests # Authenticate to GCP for read access to GCS - name: Authenticate to GCP (mainnet-history-dumps) diff --git a/.github/workflows/pr_check_github_scripts.yml b/.github/workflows/pr_check_github_scripts.yml index 997641a4a2..23f6125797 100644 --- a/.github/workflows/pr_check_github_scripts.yml +++ b/.github/workflows/pr_check_github_scripts.yml @@ -15,12 +15,8 @@ jobs: - name: Set up Nix uses: ./.github/actions/nix/setup_nix with: - cache_version: 8 - artifactory_user: dummy - artifactory_password: dummy - target: oss + cache_version: 9 - name: Check github scripts uses: ./.github/actions/nix/run_bash_command_in_nix with: cmd: bash gha-scripts/scripts/check-build.sh - \ No newline at end of file diff --git a/.github/workflows/pr_non_contributors.yml b/.github/workflows/pr_non_contributors.yml index ff44bef249..4453efa321 100644 --- a/.github/workflows/pr_non_contributors.yml +++ b/.github/workflows/pr_non_contributors.yml @@ -2,7 +2,7 @@ name: CI on PRs from forks on: pull_request_target: types: [ opened, synchronize, reopened ] - branches: [ main ] # Only run on PRs with `main` as their base + branches: [ main, 'staging-*' ] # Only run on PRs with `main` or `staging` branches as their base concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }} diff --git a/.github/workflows/pr_static_checks.yml b/.github/workflows/pr_static_checks.yml index ca5b57a4b1..2a53c7ee55 100644 --- a/.github/workflows/pr_static_checks.yml +++ b/.github/workflows/pr_static_checks.yml @@ -21,4 +21,3 @@ jobs: self_hosted: false commit_sha: ${{ github.event.pull_request.head.sha }} skip_todo_check: true # runs from forks with on: pull_request run in context of the fork, so issue references will be broken - oss_only: true diff --git a/.gitignore b/.gitignore index 2c56be64b4..fae017aaa8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ _build/ **/metals.sbt **/.scala-build/* +daml/daml-ide-mono/daml/ +daml/daml-ide-mono/.vscode/ .vscode/* !.vscode/settings.json # Make sure test files are checkedin diff --git a/CANTON_CODE_CHANGES.md b/CANTON_CODE_CHANGES.md index e88bc5523c..c4621b206e 100644 --- a/CANTON_CODE_CHANGES.md +++ b/CANTON_CODE_CHANGES.md @@ -26,6 +26,7 @@ to know which and/or what changes we'll need to upstream before the switch. * `ActiveContract.loadFromByteString` made public * ``PositiveFiniteDuration` config reader and writer made public * `ProofOfOwnership` made public +* `JcePureCrypto#signBytes` overridden to expand to public ## Misc * Added support for interface filters in ledger api ACS commands. TODO (#638): This should be upstreamed. * Generalization of `MetricsFactory` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31e337f501..7bd392189e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,9 +46,9 @@ Splice maintainers may use the following GitHub issue labels to highlight issues Note that not all `good first issue`s are also `help wanted`; some may require access to infrastructure (CI, test deployments) that is not openly available. If you are planning to work on an issue please assign yourself to it (if you are able to) or leave a comment, to -avoid duplicate work across contributors. If the issue is not new, it is also a good idea to reach out to the -core contributors before working on it, to check how relevant it still is, and whether it is something worth -working on. +avoid duplicate work across contributors. + +For any contribution, first check with a maintainer on an issue (either an existing one or a new one) that this makes sense to work on. Contributions that have not gotten explicit agreement from a maintainer on an issue beforehand may be closed without further comment. ## Opening PRs @@ -322,4 +322,3 @@ grant write permissions to the main Splice repo. - CI assumes the branch for the latest release line, so after every Splice release (assuming you rebase/merge the fork's main to Splice main), you will need to pull the corresponding release-line from splice and push it to the fork. In the future we might change CI to pull the release line from main Splice rather than look for it in the fork, but as of now, it assumes it exists in the same repo on which it is running. - diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 504b28161b..43c40e7805 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -19,6 +19,7 @@ - [Troubleshooting](#troubleshooting) - [Nix Issues on MacOS](#nix-issues-on-macos) - [NPM Lock file issues](#npm-lock-file-issues) +- [Staging Branches for Minor Releases](#staging-branches-for-minor-releases) # Setting up Your Development Environment @@ -36,49 +37,6 @@ direnv: error /home/moritz/daml-projects/canton-amulet/.envrc is blocked. Run `direnv allow` to approve its content ``` 1. Run `direnv allow`. You should see a bunch of output including `direnv: using nix`. -1. (Optional) Configure artifactory credentials - A few tests rely on Enterprise canton features. To be able to run those locally, you will require access to - Digital Asset's enterprise artifactory. If you need to run those, please contact the Maintainers of this repo, - per MAINTAINERS.md. - Once you have access to artifactory, you can generate an artifactory Identity Token [here](https://digitalasset.jfrog.io/ui/admin/artifactory/user_profile). - Your username is shown at the top of the page (under "User profile: XX"). - 1. Add the following to `/etc/nix/netrc` (you might need to create that directory as root): - ``` - machine digitalasset.jfrog.io - login yourartifactoryusername - password yourartifactoryidentitytoken - ``` - 1. In addition, add your artifactory user and password to `.envrc.private` file like so: - ``` - export ARTIFACTORY_USER="yourartifactoryusername" - export ARTIFACTORY_PASSWORD="yourartifactoryidentitytoken" - ``` - Once added, reload direnv by typing `direnv reload` in your terminal. -1. (Optional) Configure artifactory credentials - troubleshooting - If you defined Artifactory access, and are getting an authorization exception, like the following: - ``` - direnv: using nix - error: unable to download 'https://digitalasset.jfrog.io/artifactory/canton-enterprise/canton-enterprise-2.7.0-snapshot.20230614.10547.0.v03419b62.tar.gz': HTTP error 401 ('Unauthorized') - ``` - 1. Check that your access token is valid by running the following sample command: - ``` - curl -vvv -L -u: "https://digitalasset.jfrog.io/artifactory/canton-enterprise/canton-enterprise-2.7.0-snapshot.20230614.10547.0.v03419b62.tar.gz" -o canton-enterprise-2.7.0-snapshot.20230614.10547.0.v03419b62.tar.gz - ``` - If the download fails, check that your access token matches what is set in [Artifactory](https://digitalasset.jfrog.io/ui/admin/artifactory/user_profile). - Also, check you have visability via the UI [here](https://digitalasset.jfrog.io/ui/repos/tree/General/canton-enterprise). - If you don't have visibility via the UI then check with the repo Maintainers. - 1. If the artifact successfully downloaded, check the access rights of the file `/etc/nix/netrc`. - If the access rights are more restrictive than `-rw-rw-r--`, update them: - ``` - chmod 664 /etc/nix/netrc - ``` - Note - `sudo` may be required to run the above command. - 1. Switch to the Splice repo directory. - 1. If the authorization exception isn't resolved, investigate further with additional logging - by running the following command at the root of the repo: - ``` - nix develop --debug --verbose path:nix - **Important:** start your IDE and other development tools from a console that has this `direnv` loaded; and thus has the proper version of all the project dependencies on its `PATH`. @@ -144,7 +102,7 @@ and upload them to the dev GHCR registry. In order to do so, you will need to fo There are a number of environment variables managed with `direnv` that are used to contain private information. This includes credentials to -a range of external services, including Auth0 and Artifactory. To keep +a range of external services, such as Auth0. To keep this private information private, they are stored in a specific file in the root of the project repository: `.envrc.private`. This file is listed in `.gitignore` to prevent accidental commit to the repository. @@ -155,12 +113,6 @@ be present in `.envrc.private` are in fact present. Missing definitions will cause a warning to be reported when `.envrc` is executed. -A list of expected environment definitions is as follows: - -* Artifactory credentials - * `ARTIFACTORY_USER`: your username at digitalasset.jfrog.io (can be seen in the top-right corner after logging in with Google SSO) - * `ARTIFACTORY_PASSWORD`: Your identity token at digitalasset.jfrog.io (can be obtained by generating an identity token in your user profile) - If you are a Splice Contributor (see CONTRIBUTOR.md) and wish to push Docker images and deploy to test clusters from your local machine, you will need also the following: @@ -522,3 +474,20 @@ Caused by: java.lang.NullPointerException: Cannot invoke "jdk.internal.platform. at java.base/jdk.internal.platform.cgroupv2.CgroupV2Subsystem.getInstance(CgroupV2Subsystem.java:80) ``` in start-canton.sh, try adding: `export ADDITIONAL_JAVA_TOOLS_OPTIONS="-XX:-UseContainerSupport"` to .envrc.private + +## Staging Branches for Minor Releases + +Daml changes, breaking API changes and new Canton protocol versions +must only be included in new minor releases. To manage changes that +are ready but cannot be merged to main until the next minor version, +create a ``staging-X.Y.0`` branch for the next minor release based off +main and merge changes for that release into that. You likely want to +regularly merge main into that to avoid it going too far out of sync. + +Note: This is intended for changes that are complete and could be +released not for incomplete changes that then may need to get backed +out again before the release in case we do not manage to finish them +completely. + +Once the time is reached where the minor should be the next weekly +release, merge the staging branch back into main. diff --git a/LATEST_RELEASE b/LATEST_RELEASE index 45a346dba8..0a1ffad4b4 100644 --- a/LATEST_RELEASE +++ b/LATEST_RELEASE @@ -1 +1 @@ -0.6.11 +0.7.4 diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 3f0d560cbe..1d878b63ef 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -13,12 +13,14 @@ ## Bumping Canton -1. Choose the Canton version you wish to upgrade to. The currently published versions on - Artifactory can be found [here](https://digitalasset.jfrog.io/ui/repos/tree/General/canton-enterprise). +1. Choose the Canton version you wish to upgrade to. 2. Update the hashes in `nix/canton-sources.json` by running: `build-tools/bump-canton.sh ` 3. In case you have also made configuration changes to Canton in `simple-topology-canton.conf`, remember to also make the corresponding changes for our cluster deployments. It is recommended to test any configuration changes on scratchnet first. +4. Make sure to run full CI with `[ci]` for a "bump canton" PR. +5. An upgrade cluster test will be requested automatically for PRs bumping the Canton version. + Feel free to skip it if you didn't make any config changes as Canton already has sufficient testing on their side. ## Bumping Daml Compiler & SDK version @@ -44,7 +46,7 @@ Initial setup: 1. Check out the [Canton **Open Source** repo](https://github.com/digital-asset/canton) 2. Define the environment variable used in the commands below using `export PATH_TO_CANTON_OSS=`. This can be added to your private env vars. -Current Canton commit: `9b95fd4b486ccd8e436c64fa703453dd8350b92f` +Current Canton commit: `2fc931e1c8c4e7743e69f966d7c1b72f2373b3ff` 1. Checkout the **current Canton commit listed above** in the Canton open source repo from above, so we can diff our current fork against this checkout. 2. Change to your checkout of the Splice repo and execute the following steps: @@ -55,13 +57,15 @@ Current Canton commit: `9b95fd4b486ccd8e436c64fa703453dd8350b92f` 4. Create a commit to ease review, `git add canton/ && git commit -s -m"Undo our changes" --no-verify` 3. Checkout the commit of the Canton OSS repo to which you have decided to upgrade in Step 1.1 1. Learn the Daml SDK version used by Canton from `head -n15 $PATH_TO_CANTON_OSS/project/project/DamlVersions.scala`. + 2. The OSS repo commit will mention a "Reference commit". In Splice repo run `scripts/search-canton-snapshot.py` with this hash. 5. Execute the following steps in your Splice repo: 1. Copy the Canton changes: `./scripts/copy-canton.sh $PATH_TO_CANTON_OSS` 2. Create a commit to ease review, `git add canton/ && git commit -s -m"Bump Canton commit" --no-verify` 3. Reapply our changes `git apply '--exclude=canton/community/app/src/test/resources/examples/*' --directory=canton --reject canton.patch`. 4. Create a commit to ease review `git add canton/ && git reset '*.rej' && git commit -s -m"Reapply our changes" --no-verify` 5. Bump the SDK/Canton versions in the following places: - 1. The current Canton commit in this `README.md` + 1. The current Canton OSS commit in this `README.md` + 2. The `canton_library_version` in `CantonDependencies.scala` to the value produced by `search-canton-snapshot.py` above 6. Create another commit, `git add -A && git reset '*.rej' && git commit -s -m"Bump Canton commit" --no-verify` 6. Check if the `protocolVersions` in our `BuildInfoKeys` in `BuildCommon.scala` needs to be bumped. - One way to do this is to run `start-canton.sh -w` with an updated Canton binary, and check `ProtocolVersion.latest` in the console. diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 255ed6a142..6909d8572d 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -7,10 +7,10 @@ See the [design document](https://docs.google.com/document/d/1rvAec6BuKx61TdJ6sY The performance tests cover three stores. The Tests ingest update data relevant to each store and measure the time taken for the ingestion process. -| Store | Description | Content | -|---------------------|-------------|-------------------| -| **`SvDsoStore`** | DSO's internal governance data | ACS contracts | -| **`ScanStore`** | DSO's public queryable data | ACS contracts | +| Store | Description | Content | +|---------------------|-------------|---------------------------------------------------| +| **`SvDsoStore`** | DSO's internal governance data | ACS contracts | +| **`ScanStore`** | DSO's public queryable data | ACS contracts and Tx Log | | **`UpdateHistory`** | DSO's append-only audit log | Updates and associated events (creates/exercises) | # How to run a test on branch diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 65307ac462..95bdd015ac 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -5,7 +5,6 @@ This guide outlines steps to resolve common issues that a contributor might enco ## Prerequisites - Install **`nix`**, **`direnv`**, and **`sbt`** as per [DEVELOPMENT.md](./DEVELOPMENT.md) -- Ensure access to JFrog Artifactory (`splice-developers` team). Contact your team lead if access is denied. ## Steps @@ -156,5 +155,4 @@ If you see **`Environment variable VERSION must be set`** or **`locale.Error: un ### Additional notes - Always run SBT commands from a terminal with `direnv` enabled to ensure the `nix` environment is correctly set up. -- If JFrog access issues persist, confirm with your team lead that all necessary team members have been added to the `splice-developers` group. - For persistent issues, consult the Splice repository’s documentation or raise an issue in the repository for further assistance. diff --git a/VERSION b/VERSION index 592e815ea9..8bd6ba8c5c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.12 +0.7.5 diff --git a/apps/app/src/main/resources/application.conf b/apps/app/src/main/resources/application.conf index ef8ce7ae4b..52931b943c 100644 --- a/apps/app/src/main/resources/application.conf +++ b/apps/app/src/main/resources/application.conf @@ -2,7 +2,8 @@ pekko.http.server.request-timeout = 38 seconds pekko.http.server.parsing.error-handler="org.lfdecentralizedtrust.splice.http.PekkoHttpParsingErrorHandler$" pekko.http.server.parsing.ignore-illegal-header-for = - [ "authorization" + [ "accept-language" + "authorization" "cookie" "origin" "proxy-authorization" diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala index fad206d8f9..7d76d69eb7 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala @@ -31,9 +31,13 @@ import org.lfdecentralizedtrust.splice.splitwell.config.{ SplitwellSynchronizerConfig, } import org.lfdecentralizedtrust.splice.sv.config.* -import org.lfdecentralizedtrust.splice.sv.{SvAppClientConfig} +import org.lfdecentralizedtrust.splice.sv.SvAppClientConfig import org.lfdecentralizedtrust.splice.sv.config.SvOnboardingConfig.FoundDso -import org.lfdecentralizedtrust.splice.util.{Codec, SpliceRateLimitConfig} +import org.lfdecentralizedtrust.splice.util.{ + Codec, + PerAttributeRateLimitConfig, + SpliceRateLimitConfig, +} import org.lfdecentralizedtrust.splice.validator.config.* import org.lfdecentralizedtrust.splice.wallet.config.{ AppRewardBeneficiaryConfig, @@ -69,8 +73,8 @@ import com.typesafe.config.{Config, ConfigRenderOptions} import com.typesafe.config.ConfigException.UnresolvedSubstitution import org.slf4j.{Logger, LoggerFactory} import pureconfig.configurable.{genericMapReader, genericMapWriter} -import pureconfig.generic.FieldCoproductHint -import pureconfig.{ConfigReader, ConfigWriter} +import pureconfig.generic.{CoproductHint, FieldCoproductHint, ProductHint} +import pureconfig.{ConfigCursor, ConfigReader, ConfigWriter} import pureconfig.error.{CannotConvert, FailureReason} import pureconfig.module.cats.{nonEmptyListReader, nonEmptyListWriter} import io.circe.parser.* @@ -427,10 +431,15 @@ object SpliceConfig { deriveReader[SpliceCachingConfigs] implicit val spliceParametersConfig: ConfigReader[SpliceParametersConfig] = deriveReader[SpliceParametersConfig] + implicit val spliceRateLimiterSimpleConfig: ConfigReader[SpliceRateLimitConfig.Simple] = + deriveReader[SpliceRateLimitConfig.Simple] + implicit val clientIpRateLimitConfig: ConfigReader[PerAttributeRateLimitConfig] = + deriveReader[PerAttributeRateLimitConfig] + implicit val spliceRateLimiterWithPerClientIpConfig + : ConfigReader[SpliceRateLimitConfig.WithPerClientIp] = + deriveReader[SpliceRateLimitConfig.WithPerClientIp] implicit val rateLimitersConfig: ConfigReader[RateLimitersConfig] = deriveReader[RateLimitersConfig] - implicit val spliceRateLimiterConfig: ConfigReader[SpliceRateLimitConfig] = - deriveReader[SpliceRateLimitConfig] implicit val enabledFeaturesConfigReader: ConfigReader[EnabledFeaturesConfig] = deriveReader[EnabledFeaturesConfig] implicit val splicePostgresConfigReader: ConfigReader[SplicePostgresConfig] = @@ -695,8 +704,34 @@ object SpliceConfig { deriveReader[AutoAcceptTransfersConfig] implicit val appRewardBeneficiaryConfigReader: ConfigReader[AppRewardBeneficiaryConfig] = deriveReader[AppRewardBeneficiaryConfig] + + implicit val rewardSharingConfigHint: FieldCoproductHint[RewardSharingConfig] = + new FieldCoproductHint[RewardSharingConfig]("type") { + override def from( + cursor: ConfigCursor, + options: Seq[String], + ): ConfigReader.Result[CoproductHint.Action] = { + cursor.asObjectCursor.flatMap { objCur => + if (objCur.atKeyOrUndefined("type").isUndefined) { + options + .find(fieldValue(_) == "built-in") + .fold(super.from(cursor, options))(opt => Right(CoproductHint.Use(objCur, opt))) + } else { + super.from(cursor, options) + } + } + } + } + + implicit val rewardSharingBuiltInReader: ConfigReader[RewardSharingConfig.BuiltIn] = + deriveReader[RewardSharingConfig.BuiltIn] + implicit val rewardSharingExternalHint: ProductHint[RewardSharingConfig.External] = + ProductHint[RewardSharingConfig.External](allowUnknownKeys = false) + implicit val rewardSharingExternalReader: ConfigReader[RewardSharingConfig.External] = + deriveReader[RewardSharingConfig.External] implicit val rewardSharingConfigReader: ConfigReader[RewardSharingConfig] = deriveReader[RewardSharingConfig] + implicit val validatorDecentralizedSynchronizerConfigReader : ConfigReader[ValidatorDecentralizedSynchronizerConfig] = deriveReader[ValidatorDecentralizedSynchronizerConfig].emap(config => { @@ -827,29 +862,33 @@ object SpliceConfig { case (Left(err), _) => Left(err) case (Right(()), (party, sharingConfig)) => for { - _ <- Either.cond( - sharingConfig.beneficiaries.forall(b => - b.percentage > 0 && b.percentage <= BigDecimal(1.0) - ), - (), - ConfigValidationFailed( - s"Reward sharing percentages for $party must be in (0.0, 1.0]" - ), - ) - _ <- Either.cond( - sharingConfig.beneficiaries.map(_.percentage).sum <= BigDecimal(1.0), - (), - ConfigValidationFailed( - s"Reward sharing percentages for $party must sum to at most 1.0" - ), - ) _ <- Either.cond( sharingConfig.batchSize > 0, (), - ConfigValidationFailed( - s"Reward sharing batchSize for $party must be positive" - ), + ConfigValidationFailed(s"Reward sharing batchSize for $party must be positive"), ) + _ <- sharingConfig match { + case RewardSharingConfig.External(_) => Right(()) + case builtIn: RewardSharingConfig.BuiltIn => + for { + _ <- Either.cond( + builtIn.beneficiaries.forall(b => + b.percentage > 0 && b.percentage <= BigDecimal(1.0) + ), + (), + ConfigValidationFailed( + s"Reward sharing percentages for $party must be in (0.0, 1.0]" + ), + ) + _ <- Either.cond( + builtIn.beneficiaries.map(_.percentage).sum <= BigDecimal(1.0), + (), + ConfigValidationFailed( + s"Reward sharing percentages for $party must sum to at most 1.0" + ), + ) + } yield () + } } yield () } } yield conf @@ -918,10 +957,15 @@ object SpliceConfig { implicit val spliceParametersConfig: ConfigWriter[SpliceParametersConfig] = deriveWriter[SpliceParametersConfig] + implicit val spliceRateLimiterSimpleConfig: ConfigWriter[SpliceRateLimitConfig.Simple] = + deriveWriter[SpliceRateLimitConfig.Simple] + implicit val clientIpRateLimitConfig: ConfigWriter[PerAttributeRateLimitConfig] = + deriveWriter[PerAttributeRateLimitConfig] + implicit val spliceRateLimiterWithPerClientIpConfig + : ConfigWriter[SpliceRateLimitConfig.WithPerClientIp] = + deriveWriter[SpliceRateLimitConfig.WithPerClientIp] implicit val rateLimitersConfig: ConfigWriter[RateLimitersConfig] = deriveWriter[RateLimitersConfig] - implicit val spliceRateLimiterConfig: ConfigWriter[SpliceRateLimitConfig] = - deriveWriter[SpliceRateLimitConfig] implicit val enabledFeaturesConfigWriter: ConfigWriter[EnabledFeaturesConfig] = deriveWriter[EnabledFeaturesConfig] @@ -1137,8 +1181,16 @@ object SpliceConfig { deriveWriter[AutoAcceptTransfersConfig] implicit val appRewardBeneficiaryConfigWriter: ConfigWriter[AppRewardBeneficiaryConfig] = deriveWriter[AppRewardBeneficiaryConfig] + + implicit val rewardSharingConfigHint: FieldCoproductHint[RewardSharingConfig] = + new FieldCoproductHint[RewardSharingConfig]("type") + implicit val rewardSharingConfigBuiltInWriter: ConfigWriter[RewardSharingConfig.BuiltIn] = + deriveWriter[RewardSharingConfig.BuiltIn] + implicit val rewardSharingConfigExternalWriter: ConfigWriter[RewardSharingConfig.External] = + deriveWriter[RewardSharingConfig.External] implicit val rewardSharingConfigWriter: ConfigWriter[RewardSharingConfig] = deriveWriter[RewardSharingConfig] + implicit val validatorDecentralizedSynchronizerConfigWriter : ConfigWriter[ValidatorDecentralizedSynchronizerConfig] = deriveWriter[ValidatorDecentralizedSynchronizerConfig] diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala index 11d3a7a6a1..8e6d76a748 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala @@ -33,6 +33,7 @@ import org.lfdecentralizedtrust.splice.http.v0.definitions.{ UpdateHistoryItemV2, } import org.lfdecentralizedtrust.splice.scan.{ScanApp, ScanAppBootstrap} +import org.lfdecentralizedtrust.splice.store.VoteResultsFilters import org.lfdecentralizedtrust.splice.scan.automation.ScanAutomationService import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient.TransferContextWithInstances @@ -367,20 +368,6 @@ abstract class ScanAppReference( httpCommand(HttpScanAppClient.GetRewardAccountingBatch(roundNumber, batchHash)) } - import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryResponseItem - import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest.SortOrder - - def listTransactions( - pageEndEventId: Option[String], - sortOrder: SortOrder, - pageSize: Int, - ): Seq[TransactionHistoryResponseItem] = - consoleEnvironment.run { - httpCommand( - HttpScanAppClient.ListTransactions(pageEndEventId, sortOrder, pageSize) - ) - } - def getAcsSnapshot(party: PartyId, recordTime: Option[Instant]): ByteString = consoleEnvironment.run { httpCommand( @@ -465,6 +452,31 @@ abstract class ScanAppReference( ) } + def getAcsSnapshotAtV2( + at: CantonTimestamp, + migrationId: Long, + recordTimeMatch: Option[definitions.AcsRequestV2.RecordTimeMatch] = Some( + definitions.AcsRequestV2.RecordTimeMatch.Exact + ), + after: Option[String] = None, + pageSize: Int = 100, + partyIds: Option[Vector[PartyId]] = None, + templates: Option[Vector[PackageQualifiedName]] = None, + ) = + consoleEnvironment.run { + httpCommand( + HttpScanAppClient.GetAcsSnapshotAtV2( + at.toInstant.atOffset(java.time.ZoneOffset.UTC), + migrationId, + recordTimeMatch, + after, + pageSize, + partyIds, + templates, + ) + ) + } + def getHoldingsStateAt( at: CantonTimestamp, migrationId: Long, @@ -846,22 +858,14 @@ abstract class ScanAppReference( @Help.Summary("List vote results") def listVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: BigInt, pageToken: Option[BigInt] = None, ): (Seq[DsoRules_CloseVoteRequestResult], Option[BigInt]) = { consoleEnvironment.run { httpCommand( HttpScanAppClient.ListVoteRequestResults( - actionName, - accepted, - requester, - effectiveFrom, - effectiveTo, + filters, limit, pageToken, ) @@ -915,6 +919,18 @@ abstract class ScanAppReference( ) } + @Help.Summary( + "Get checksums for a list of bulk storage objects (using both staging and committed objects)" + ) + def getBulkObjectChecksums( + objectKeys: Seq[String] + ): definitions.GetBulkObjectChecksumsResponse = + consoleEnvironment.run { + httpCommand( + HttpScanAppClient.GetBulkObjectChecksums(objectKeys) + ) + } + @Help.Summary("Download a bulk storage object") def bulkStorageDownload(objectKey: String, output: OutputStream)(implicit ec: ExecutionContext, diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala index 926b03bbc9..63b7eb0d47 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala @@ -23,6 +23,7 @@ import org.lfdecentralizedtrust.splice.environment.{ SpliceStatus, } import org.lfdecentralizedtrust.splice.http.v0.definitions +import org.lfdecentralizedtrust.splice.store.VoteResultsFilters import org.lfdecentralizedtrust.splice.sv.{SvApp, SvAppBootstrap, SvAppClientConfig} import org.lfdecentralizedtrust.splice.sv.admin.api.client.commands.{ HttpSvAdminAppClient, @@ -133,7 +134,7 @@ abstract class SvAppReference( @Help.Summary("Cancel a running logical synchronizer upgrade by removing its LSU announcement") def cancelLogicalSynchronizerUpgrade(): Unit = consoleEnvironment.run { - httpCommand(HttpSvAdminAppClient.CancelLogicalSynchronizerUpgrade()) + httpCommand(HttpSvOperatorAppClient.CancelLogicalSynchronizerUpgrade()) } @Help.Summary("Get identities of all domain node components") @@ -197,22 +198,14 @@ abstract class SvAppReference( } def listVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: BigInt, pageToken: Option[BigInt] = None, ): (Seq[DsoRules_CloseVoteRequestResult], Option[BigInt]) = { consoleEnvironment.run { httpCommand( HttpSvOperatorAppClient.ListVoteRequestResults( - actionName, - accepted, - requester, - effectiveFrom, - effectiveTo, + filters, limit, pageToken, ) @@ -220,6 +213,16 @@ abstract class SvAppReference( } } + def countVoteRequestResults( + filters: VoteResultsFilters + ): Long = { + consoleEnvironment.run { + httpCommand( + HttpSvOperatorAppClient.CountVoteRequestResults(filters) + ) + } + } + @Help.Summary("Cast a vote") def castVote( trackingCid: VoteRequest.ContractId, @@ -289,13 +292,10 @@ class SvAppBackendReference( def appState: SvApp.State = _appState[SvApp.State, SvApp] @Help.Summary( - "Returns the current delegate based automation. Do not keep references to the result, as this automation gets replaced whenever the DSO delegate changes." + "Returns the delegate based automation. The reference is stable for the lifetime of the app." ) - def dsoDelegateBasedAutomation: DsoDelegateBasedAutomationService = { - appState.dsoAutomation.restartDsoDelegateBasedAutomationTrigger.epochState - .getOrElse(throw new RuntimeException("LeaderBasedAutomation is not fully started up")) - .dsoDelegateBasedAutomation - } + def dsoDelegateBasedAutomation: DsoDelegateBasedAutomationService = + appState.dsoAutomation.dsoDelegateBasedAutomation @Help.Summary( "Returns the current DSO automation." diff --git a/apps/app/src/pack/examples/sv-helm/cometbft-values.yaml b/apps/app/src/pack/examples/sv-helm/cometbft-values.yaml index 785f80b028..57ffcd06d8 100644 --- a/apps/app/src/pack/examples/sv-helm/cometbft-values.yaml +++ b/apps/app/src/pack/examples/sv-helm/cometbft-values.yaml @@ -39,6 +39,11 @@ stateSync: # Note that the port number is significant in the list of rpcServers rpcServers: "https://sv.sv-2.TARGET_HOSTNAME:443/api/sv/v0/admin/domain/cometbft/json-rpc,https://sv.sv-2.TARGET_HOSTNAME:443/api/sv/v0/admin/domain/cometbft/json-rpc" +watchdog: + enabled: true + sequencerMetricsUrl: http://global-domain-SERIAL_ID-sequencer:10013/metrics + mediatorMetricsUrl: http://global-domain-SERIAL_ID-mediator:10013/metrics + # k8s affinity for all deployed pods (optional) # affinity: # nodeAffinity: diff --git a/apps/app/src/pack/examples/sv-helm/global-domain-values.yaml b/apps/app/src/pack/examples/sv-helm/global-domain-values.yaml index e1b5b1c390..a1008a5571 100644 --- a/apps/app/src/pack/examples/sv-helm/global-domain-values.yaml +++ b/apps/app/src/pack/examples/sv-helm/global-domain-values.yaml @@ -12,7 +12,7 @@ sequencer: # Enable when using CantonBFT instead of CometBFT # driver: # type: "cantonbft" - # externalAddress: "sequencer-p2p-15.SERIAL_ID.sv.YOUR_HOSTNAME" + # externalAddress: "sequencer-p2p-SERIAL_ID.sv.YOUR_HOSTNAME" # externalPort: 443 # persistence: # databaseName: sequencer_SERIAL_ID_cantonbft diff --git a/apps/app/src/pack/examples/sv-helm/kms-participant-aws-values.yaml b/apps/app/src/pack/examples/sv-helm/kms-participant-aws-values.yaml index 30ddb0440e..9a8b9d01d0 100644 --- a/apps/app/src/pack/examples/sv-helm/kms-participant-aws-values.yaml +++ b/apps/app/src/pack/examples/sv-helm/kms-participant-aws-values.yaml @@ -23,3 +23,8 @@ additionalEnvVars: secretKeyRef: name: aws-credentials key: secretAccessKey + # Session signing keys reduce KMS load and cost on KMS-backed participants by signing + # most messages with short-lived in-memory keys instead of calling the KMS each time. + # This config setting only works on KMS participants. + - name: ADDITIONAL_CONFIG_SESSION_SIGNING_KEYS + value: canton.participants.participant.crypto.session-signing-keys.enabled = true diff --git a/apps/app/src/pack/examples/sv-helm/kms-participant-gcp-values.yaml b/apps/app/src/pack/examples/sv-helm/kms-participant-gcp-values.yaml index b8598761d2..ea4b871a90 100644 --- a/apps/app/src/pack/examples/sv-helm/kms-participant-gcp-values.yaml +++ b/apps/app/src/pack/examples/sv-helm/kms-participant-gcp-values.yaml @@ -19,6 +19,11 @@ kms: additionalEnvVars: - name: GOOGLE_APPLICATION_CREDENTIALS value: "/app/gcp-credentials.json" + # Session signing keys reduce KMS load and cost on KMS-backed participants by signing + # most messages with short-lived in-memory keys instead of calling the KMS each time. + # This config setting only works on KMS participants. + - name: ADDITIONAL_CONFIG_SESSION_SIGNING_KEYS + value: canton.participants.participant.crypto.session-signing-keys.enabled = true extraVolumeMounts: - name: gcp-credentials mountPath: "/app/gcp-credentials.json" diff --git a/apps/app/src/test/resources/include/scans/_scan.conf b/apps/app/src/test/resources/include/scans/_scan.conf index 147faf2366..b623e19de3 100644 --- a/apps/app/src/test/resources/include/scans/_scan.conf +++ b/apps/app/src/test/resources/include/scans/_scan.conf @@ -31,8 +31,14 @@ getAcsSnapshot = 1 minute } rate-limiting { - default { - rate-per-second = 200 + # the global limiter caps the total request rate across all operations; disabled in + # tests so that it doesn't interfere with the per-operation limits exercised here. + global { + rate-per-second = 1 + enabled = false + per-client-ip { + enabled = false + } } rate-limiters { getAcsSnapshot.rate-per-second = 2 diff --git a/apps/app/src/test/resources/include/sequencers.conf b/apps/app/src/test/resources/include/sequencers.conf index e8bf77998b..85858a350d 100644 --- a/apps/app/src/test/resources/include/sequencers.conf +++ b/apps/app/src/test/resources/include/sequencers.conf @@ -35,6 +35,8 @@ _sequencer_reference_template { config { storage = ${_shared.storage} storage.config.properties.databaseName = "sequencer_driver" + consensus-empty-block-creation-timeout = 500.milliseconds + output-fetch-how-many-recipients = 2 } type = reference block { diff --git a/apps/app/src/test/resources/include/svs/_sv.conf b/apps/app/src/test/resources/include/svs/_sv.conf index a08a5c59c4..f13c2b775f 100644 --- a/apps/app/src/test/resources/include/svs/_sv.conf +++ b/apps/app/src/test/resources/include/svs/_sv.conf @@ -64,8 +64,14 @@ onboardSvPartyMigrationAuthorize = 5 minutes } rate-limiting { - default { - rate-per-second = 200 + # the global limiter caps the total request rate across all operations; disabled in + # tests so that it doesn't interfere with the per-operation limits exercised here. + global { + rate-per-second = 1 + enabled = false + per-client-ip { + enabled = false + } } rate-limiters { prepareValidatorOnboarding.rate-per-second = 2 diff --git a/apps/app/src/test/resources/localnet-reassign-topology.conf b/apps/app/src/test/resources/localnet-reassign-topology.conf new file mode 100644 index 0000000000..426525f88e --- /dev/null +++ b/apps/app/src/test/resources/localnet-reassign-topology.conf @@ -0,0 +1,38 @@ +include required("include/canton-basic.conf") +canton { + validator-app-clients { + userValidatorClient { + admin-api { + url = "http://wallet.localhost:2000" + } + } + providerValidatorClient { + admin-api { + url = "http://wallet.localhost:3000" + } + } + } + remote-participants { + app-provider { + ledger-api { + address = "grpc-ledger-api.localhost" + port = 3000 + } + admin-api { + address = "localhost" + port = 3902 + } + } + app-user { + ledger-api { + address = "grpc-ledger-api.localhost" + port = 2000 + } + admin-api { + address = "localhost" + port = 2902 + } + } + } +} + diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/config/SpliceConfigTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/config/SpliceConfigTest.scala index db5150864f..041b6cc1e7 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/config/SpliceConfigTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/config/SpliceConfigTest.scala @@ -87,8 +87,8 @@ class SpliceConfigTest extends AsyncWordSpec with BaseTest { } // Shared helper for RewardSharingConfig tests - private def mkSharingCfg(percentages: BigDecimal*): RewardSharingConfig = - RewardSharingConfig( + private def mkSharingCfg(percentages: BigDecimal*): RewardSharingConfig.BuiltIn = + RewardSharingConfig.BuiltIn( minTtlAfterSharing = NonNegativeFiniteDuration.ofHours(30), beneficiaries = percentages.zipWithIndex.map { case (pct, i) => AppRewardBeneficiaryConfig( @@ -165,6 +165,7 @@ class SpliceConfigTest extends AsyncWordSpec with BaseTest { s""" |canton.validator-apps.aliceValidator.reward-sharing-config-by-party = { | "alice::1220abc" = { + | type = "built-in" | beneficiaries = [$beneficiaries] | min-ttl-after-sharing = 30h | } @@ -222,6 +223,7 @@ class SpliceConfigTest extends AsyncWordSpec with BaseTest { """ |canton.validator-apps.aliceValidator.reward-sharing-config-by-party = { | "alice::1220abc" = { + | type = "built-in" | beneficiaries = [{ beneficiary = "bob::1220", percentage = 0.4 }] | min-ttl-after-sharing = 30h | batch-size = 50 @@ -238,6 +240,7 @@ class SpliceConfigTest extends AsyncWordSpec with BaseTest { """ |canton.validator-apps.aliceValidator.reward-sharing-config-by-party = { | "alice::1220abc" = { + | type = "built-in" | beneficiaries = [{ beneficiary = "bob::1220", percentage = 0.4 }] | min-ttl-after-sharing = 30h | batch-size = 0 @@ -252,5 +255,88 @@ class SpliceConfigTest extends AsyncWordSpec with BaseTest { .value .toString should include("batchSize") } + + def sharingConfigOf(cfg: SpliceConfig): RewardSharingConfig = + cfg.validatorApps.values + .flatMap(_.rewardSharingConfigByParty.get("alice::1220abc")) + .loneElement + + "accept type = external with no beneficiaries and custom batch size" in { + val overwrite = ConfigFactory.parseString( + """ + |canton.validator-apps.aliceValidator.reward-sharing-config-by-party = { + | "alice::1220abc" = { + | type = "external" + | batch-size = 500 + | } + |} + """.stripMargin + ) + val validConfig = CantonConfig.mergeConfigs(config, Seq(overwrite)) + val loaded = SpliceConfig.loadAndValidate(validConfig).value + sharingConfigOf(loaded) shouldBe RewardSharingConfig.External(batchSize = 500) + } + + "accept explicit type = built-in with beneficiaries" in { + val overwrite = ConfigFactory.parseString( + """ + |canton.validator-apps.aliceValidator.reward-sharing-config-by-party = { + | "alice::1220abc" = { + | type = "built-in" + | beneficiaries = [{ beneficiary = "bob::1220", percentage = 0.4 }] + | min-ttl-after-sharing = 30h + | } + |} + """.stripMargin + ) + val validConfig = CantonConfig.mergeConfigs(config, Seq(overwrite)) + val loaded = SpliceConfig.loadAndValidate(validConfig).value + sharingConfigOf(loaded) shouldBe a[RewardSharingConfig.BuiltIn] + } + + "reject type = external, with beneficiaries" in { + val overwrite = ConfigFactory.parseString( + """ + |canton.validator-apps.aliceValidator.reward-sharing-config-by-party = { + | "alice::1220abc" = { + | type = "external" + | beneficiaries = [{ beneficiary = "bob::1220", percentage = 0.4 }] + | } + |} + """.stripMargin + ) + val validConfig = CantonConfig.mergeConfigs(config, Seq(overwrite)) + SpliceConfig.loadAndValidate(validConfig) shouldBe a[Left[?, ?]] + } + + "reject an invalid type value" in { + val overwrite = ConfigFactory.parseString( + """ + |canton.validator-apps.aliceValidator.reward-sharing-config-by-party = { + | "alice::1220abc" = { + | type = "bogus" + | } + |} + """.stripMargin + ) + val buggyConfig = CantonConfig.mergeConfigs(config, Seq(overwrite)) + SpliceConfig.loadAndValidate(buggyConfig) shouldBe a[Left[?, ?]] + } + + "default to built-in when type is omitted (legacy config shape)" in { + val overwrite = ConfigFactory.parseString( + """ + |canton.validator-apps.aliceValidator.reward-sharing-config-by-party = { + | "alice::1220abc" = { + | beneficiaries = [{ beneficiary = "bob::1220", percentage = 0.4 }] + | min-ttl-after-sharing = 30h + | } + |} + """.stripMargin + ) + val loaded = + SpliceConfig.loadAndValidate(CantonConfig.mergeConfigs(config, Seq(overwrite))).value + sharingConfigOf(loaded) shouldBe a[RewardSharingConfig.BuiltIn] + } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala index 2b1c85b275..a6105b98fa 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala @@ -217,7 +217,7 @@ case class EnvironmentDefinition( } if ( existing.item.featureFlags - .contains(ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) + .contains(ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) ) { logger.info( s"Participant ${validator.participantClient.id} already has multi synchronizer feature flag enabled for ${sync.synchronizerId}" @@ -230,7 +230,7 @@ case class EnvironmentDefinition( validator.participantClient.id, sync.synchronizerId, featureFlags = Seq( - ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer + ParticipantTopologyFeatureFlag.EnableMultiSynchronizer ), ) } @@ -278,6 +278,17 @@ case class EnvironmentDefinition( ) } + def withReducedAmuletRulesCacheTTL( + duration: NonNegativeFiniteDuration = NonNegativeFiniteDuration.ofSeconds(1) + ): EnvironmentDefinition = + this + .addConfigTransform((_, conf) => + ConfigTransforms.updateAllValidatorAppConfigs_(c => + // Reduce the cache TTL. Otherwise alice validator takes forever to see the new amulet rules version + c.copy(scanClient = c.scanClient.setAmuletRulesCacheTimeToLive(duration)) + )(conf) + ) + /** Use exactly this setup and replace any previously existing setup. */ def withThisSetup(setup: SpliceTestConsoleEnvironment => Unit): EnvironmentDefinition = copy(setup = setup) @@ -493,6 +504,13 @@ case class EnvironmentDefinition( ) } + def withTransferCommandSupport: EnvironmentDefinition = + this.addConfigTransform((_, conf) => + ConfigTransforms.updateAllValidatorAppConfigs_( + _.copy(enableDeprecatedTransferCommandSupport = true) + )(conf) + ) + def clearConfigTransforms(): EnvironmentDefinition = copy(configTransformsWithContext = _ => Seq()) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/EventHistorySanityCheckPlugin.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/EventHistorySanityCheckPlugin.scala index 232d7113a8..c985cb6683 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/EventHistorySanityCheckPlugin.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/EventHistorySanityCheckPlugin.scala @@ -16,6 +16,7 @@ import org.lfdecentralizedtrust.splice.http.v0.definitions.UpdateHistoryReassign import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.SpliceTestConsoleEnvironment import org.scalatest.concurrent.Eventually import org.scalatest.matchers.should.Matchers +import org.scalatest.time.{Millis, Seconds, Span} import org.scalatest.{Inspectors, LoneElement} import scala.annotation.tailrec @@ -34,7 +35,16 @@ class EventHistorySanityCheckPlugin( ): Unit = { val initializedScans = environment.scans.local.filter(_.is_initialized) if (initializedScans.nonEmpty) { - compareEventHistories(initializedScans) + // getEventHistory only serves events up to min(update, verdict) ingestion cursor + // (ScanEventStore.getCurrentMigrationCap), and verdict ingestion from the mediator lags + // behind update ingestion. At teardown this can hide even long-ingested updates, such as + // the DsoRules_AddSv exercise that compareEventHistories requires to appear in the founder + // history, so retry: each attempt re-fetches the histories until the cursors catch up. + eventually(compareEventHistories(initializedScans))( + PatienceConfig(timeout = Span(20, Seconds), interval = Span(500, Millis)), + implicitly[org.scalatest.enablers.Retrying[Unit]], + implicitly[org.scalactic.source.Position], + ) } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UpdateHistorySanityCheckPlugin.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UpdateHistorySanityCheckPlugin.scala index d162d1df9f..08ec0c57a9 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UpdateHistorySanityCheckPlugin.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UpdateHistorySanityCheckPlugin.scala @@ -5,7 +5,7 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms.updateAllScanAppC import org.lfdecentralizedtrust.splice.config.SpliceConfig import org.lfdecentralizedtrust.splice.console.ScanAppBackendReference import org.lfdecentralizedtrust.splice.http.v0.definitions.DamlValueEncoding.members.CompactJson -import org.lfdecentralizedtrust.splice.http.v0.definitions.{AcsResponseV1, UpdateHistoryItemV2} +import org.lfdecentralizedtrust.splice.http.v0.definitions.{AcsResponseV2, UpdateHistoryItemV2} import org.lfdecentralizedtrust.splice.http.v0.definitions.UpdateHistoryItemV2.members import org.lfdecentralizedtrust.splice.http.v0.definitions.UpdateHistoryReassignment.Event.members as reassignmentMembers import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.SpliceTestConsoleEnvironment @@ -217,14 +217,14 @@ class UpdateHistorySanityCheckPlugin( private def getAllSnapshots( scan: ScanAppBackendReference, before: CantonTimestamp, - acc: List[AcsResponseV1], - ): List[AcsResponseV1] = { + acc: List[AcsResponseV2], + ): List[AcsResponseV2] = { val acsSnapshotPeriodHours = scanStorageConfigV1.dbAcsSnapshotPeriodHours val migrationId = scan.getMigrationId() scan.getDateOfMostRecentSnapshotBefore(before, migrationId) match { case Some(snapshotDate) => val snapshot = scan - .getAcsSnapshotAtV1( + .getAcsSnapshotAtV2( CantonTimestamp.assertFromInstant(snapshotDate.toInstant), migrationId, pageSize = 1000, diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UseToxiproxy.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UseToxiproxy.scala index 2734c2d90b..8d352abc67 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UseToxiproxy.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UseToxiproxy.scala @@ -11,6 +11,7 @@ import monocle.macros.syntax.lens.* import org.apache.pekko.http.scaladsl.model.Uri import scala.collection.mutable.Map +import scala.util.Try /** A test plugin which injects toxiproxy to certain connections, a much-simplified version of the equivalent plugin in Canton. * At the moment, we support only the SV apps' ledger api connections and the scan app's HTTP connections, but as we need to add more - we will generalize the code below. @@ -252,7 +253,11 @@ case class UseToxiproxy( override def afterEnvironmentDestroyed(config: SpliceConfig): Unit = { logger.debug("deleting all proxies. ") - proxies.foreach { case (_, p) => p.delete() } + // Delete every proxy even if one fails: leftovers in the shared toxiproxy daemon keep + // their listen ports bound and cause name conflicts for the next suite. + proxies.foreach { case (name, p) => + Try(p.delete()).failed.foreach(e => logger.warn(s"Failed to delete proxy $name", e)) + } } def disableConnectionViaProxy(connection: String): Unit = { diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala index 0ec02124a1..e0e1c85eb4 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala @@ -30,7 +30,6 @@ import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId import com.digitalasset.canton.topology.store.TimeQuery.HeadState import monocle.macros.syntax.lens.* import org.lfdecentralizedtrust.splice.console.ParticipantClientReference -import com.digitalasset.canton.config.NonNegativeFiniteDuration import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.AmuletRules_SetConfig import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.amuletrules_actionrequiringconfirmation.CRARC_SetConfig @@ -84,14 +83,7 @@ class AppUpgradeIntegrationTest // Makes the test a bit faster and easier to debug. See #11488 ConfigTransforms.useDecentralizedSynchronizerSplitwell()(config) ) - .addConfigTransform((_, conf) => - ConfigTransforms.updateAllValidatorAppConfigs_(c => - // Reduce the cache TTL so package upgrades are picked up quickly. - c.copy(scanClient = - c.scanClient.setAmuletRulesCacheTimeToLive(NonNegativeFiniteDuration.ofSeconds(1)) - ) - )(conf) - ) + .withReducedAmuletRulesCacheTTL() .addConfigTransform((_, config) => { config .focus(_.validatorApps) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AutoIgnoreUnresponsivePartiesIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AutoIgnoreUnresponsivePartiesIntegrationTest.scala index 7386391182..2a4f8aea60 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AutoIgnoreUnresponsivePartiesIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AutoIgnoreUnresponsivePartiesIntegrationTest.scala @@ -191,7 +191,7 @@ class AutoIgnoreUnresponsivePartiesIntegrationTest )( "Alice is added to the ignored parties store after mediator timeout", _ => { - sv1Backend.dsoDelegateBasedAutomation.expiredAmuletIgnoredPartiesStore.getAll should contain( + sv1Backend.dsoDelegateBasedAutomation.unavailablePartiesStore.getAll should contain( aliceParty ) }, diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BftScanConnectionIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BftScanConnectionIntegrationTest.scala index 6e60e173a5..e67b6943af 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BftScanConnectionIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BftScanConnectionIntegrationTest.scala @@ -90,8 +90,6 @@ class BftScanConnectionIntegrationTest ) or include("Encountered 4 consecutive transient failures") or include( "Failed to connect to scan of FAILED Seed URL #0 (http://localhost:5112)." - ) or include( - "Failed to read bft sequencers list from scan http://localhost:5112" )) ), ) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala index 33ea2e4f0f..636cee7873 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala @@ -4,7 +4,6 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.digitalasset.canton.admin.api.client.data.TemplateId -import com.digitalasset.canton.config.NonNegativeFiniteDuration import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.topology.{ParticipantId, PartyId} import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId @@ -75,14 +74,7 @@ class BootstrapPackageConfigIntegrationTest _.copy(initialPackageConfig = initialPackageConfig) )(config) ) - .addConfigTransform((_, conf) => - ConfigTransforms.updateAllValidatorAppConfigs_(c => - // Reduce the cache TTL. Otherwise alice validator takes forever to see the new amulet rules version - c.copy(scanClient = - c.scanClient.setAmuletRulesCacheTimeToLive(NonNegativeFiniteDuration.ofSeconds(1)) - ) - )(conf) - ) + .withReducedAmuletRulesCacheTTL() .addConfigTransform((_, config) => ConfigTransforms.useDecentralizedSynchronizerSplitwell()(config) ) @@ -485,12 +477,12 @@ class BootstrapPackageConfigIntegrationTest ) ) .filter(_.metadata.version <= bootstrapPackage.metadata.version) - expectedToBeVettedVersions.foreach { expectedVettedVersion => + forEvery(expectedToBeVettedVersions) { expectedVettedVersion => val newVettedPackage = vettingState.packages .find(_.packageId == expectedVettedVersion.packageId) - .value - newVettedPackage.validFromInclusive should ( - equal(scheduledTimeO) or equal(scheduledTime1) or equal(scheduledTime2) + .value withClue "newVettedPackage" + Seq(scheduledTimeO, scheduledTime1, scheduledTime2) should contain( + newVettedPackage.validFromInclusive ) } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundCouponIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundCouponIntegrationTest.scala index 6bb2557909..a8a1bae694 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundCouponIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundCouponIntegrationTest.scala @@ -31,6 +31,7 @@ import org.lfdecentralizedtrust.splice.wallet.store.{ } import java.time.Duration +import scala.concurrent.duration.DurationInt @org.lfdecentralizedtrust.splice.util.scalatesttags.SpliceDsoGovernance_0_1_21 class DevelopmentFundCouponIntegrationTest @@ -507,7 +508,11 @@ class DevelopmentFundCouponIntegrationTest clue( "The coupon is expired" ) { - eventually() { + // The expiry trigger cannot act before expiresAt (5s after allocation) plus the + // clockSkewAutomationDelay grace period (5s), so 10s of this budget are always + // consumed before the DsoRules_ExpireDevelopmentFundCoupon submission can even + // start; leave enough headroom for slow sequencing on loaded CI runners. + eventually(30.seconds) { aliceValidatorWalletClient .listActiveDevelopmentFundCoupons() shouldBe empty withClue "alice coupons" } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DistributedDomainIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DistributedDomainIntegrationTest.scala index 40b9e4d6f5..78281baaf3 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DistributedDomainIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DistributedDomainIntegrationTest.scala @@ -117,7 +117,7 @@ class DistributedDomainIntegrationTest // Check that things work for external validators clue("Alice can tap") { - onboardWalletUser(aliceWalletClient, aliceValidatorBackend) + eventuallySucceeds()(onboardWalletUser(aliceWalletClient, aliceValidatorBackend)) aliceWalletClient.tap(1000) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DynamicSynchronizerParamsReconciliationTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DynamicSynchronizerParamsReconciliationTimeBasedIntegrationTest.scala index 8cba8a2422..fea29008fb 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DynamicSynchronizerParamsReconciliationTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DynamicSynchronizerParamsReconciliationTimeBasedIntegrationTest.scala @@ -1,6 +1,6 @@ package org.lfdecentralizedtrust.splice.integration.tests -import com.digitalasset.canton.config.NonNegativeFiniteDuration +import com.digitalasset.canton.config.{NonNegativeFiniteDuration, PositiveFiniteDuration} import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTestWithIsolatedEnvironment import org.lfdecentralizedtrust.splice.util.{TimeTestUtil, WalletTestUtil} @@ -23,7 +23,12 @@ class DynamicSynchronizerParamsReconciliationTimeBasedIntegrationTest svApps = config.svApps + (InstanceName.tryCreate("sv1Local") -> config - .svApps(InstanceName.tryCreate(s"sv1"))) + + .svApps(InstanceName.tryCreate(s"sv1")) + .copy( + // Non-default value (Canton's default is 2min) to check that the + // reconciliation trigger applies it to the synchronizer. + setBalanceRequestSubmissionWindowSize = PositiveFiniteDuration.ofMinutes(4) + )) + (InstanceName.tryCreate("sv1") -> config .svApps(InstanceName.tryCreate(s"sv1")) @@ -60,6 +65,12 @@ class DynamicSynchronizerParamsReconciliationTimeBasedIntegrationTest .trafficControl .getOrElse(throw new RuntimeException("Traffic control parameters not found")) .freeConfirmationResponses shouldBe false + // FoundDso does not set the submission window size, so bootstrapping leaves it at Canton's default + sv1Backend.participantClient.topology.synchronizer_parameters + .get_dynamic_synchronizer_parameters(synchronizerId) + .trafficControl + .getOrElse(throw new RuntimeException("Traffic control parameters not found")) + .setBalanceRequestSubmissionWindowSize shouldBe PositiveFiniteDuration.ofMinutes(2) sv1Backend.stop() sv1LocalBackend.startSync() @@ -76,6 +87,12 @@ class DynamicSynchronizerParamsReconciliationTimeBasedIntegrationTest .trafficControl .getOrElse(throw new RuntimeException("Traffic control parameters not found")) .freeConfirmationResponses shouldBe true + // sv1Local is configured with a non-default window size which the trigger applies. + sv1Backend.participantClient.topology.synchronizer_parameters + .get_dynamic_synchronizer_parameters(synchronizerId) + .trafficControl + .getOrElse(throw new RuntimeException("Traffic control parameters not found")) + .setBalanceRequestSubmissionWindowSize shouldBe PositiveFiniteDuration.ofMinutes(4) } // We go slightly above 48h as time is not actually completely still in simtime, the microseconds still advance. diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AmuletExpiryWithOldPackageIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExpiryWithMinimalVettedPackagesIntegrationTest.scala similarity index 54% rename from apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AmuletExpiryWithOldPackageIntegrationTest.scala rename to apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExpiryWithMinimalVettedPackagesIntegrationTest.scala index ceb4cfa41a..5211596688 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AmuletExpiryWithOldPackageIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExpiryWithMinimalVettedPackagesIntegrationTest.scala @@ -5,14 +5,20 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.digitalasset.canton.config.CantonRequireTypes.InstanceName import com.digitalasset.canton.config.NonNegativeFiniteDuration +import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.logging.SuppressionRule -import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.topology.transaction.ParticipantPermission -import com.digitalasset.daml.lf.data.Ref.{PackageName, PackageVersion} +import com.digitalasset.canton.topology.{ForceFlag, ForceFlags, PartyId} +import com.digitalasset.daml.lf.data.Ref.{PackageId, PackageName, PackageVersion} +import org.lfdecentralizedtrust.splice.codegen.java.da.time.types.RelTime import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.{ AppRewardCoupon, FeaturedAppActivityMarker, } +import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.TransferPreapproval +import org.lfdecentralizedtrust.splice.codegen.java.splice.ans.{AnsEntry, AnsEntryContext} +import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.payment.{PaymentAmount, Unit} +import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.subscriptions.* import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{ ConfigurableApp, @@ -25,21 +31,16 @@ import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ SpliceTestConsoleEnvironment, } import org.lfdecentralizedtrust.splice.store.db.DbMultiDomainAcsStore -import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.{ - AdvanceOpenMiningRoundTrigger, - ExpireRewardCouponsTrigger, - ExpiredAmuletTrigger, - ExpiredLockedAmuletTrigger, - FeaturedAppActivityMarkerTrigger, - UpdateExternalPartyConfigStateTrigger, -} +import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.* import org.lfdecentralizedtrust.splice.util.* +import org.lfdecentralizedtrust.splice.validator.automation.ValidatorPackageVettingTrigger +import org.lfdecentralizedtrust.splice.wallet.automation.SubscriptionReadyForPaymentTrigger import org.slf4j.event.Level -import scala.concurrent.duration.* import java.time.Duration +import scala.concurrent.duration.* -abstract class AmuletExpiryWithOldPackageIntegrationTestBase +abstract class ExpiryWithMinimalVettedPackagesIntegrationTestBase extends IntegrationTestWithIsolatedEnvironment with WalletTestUtil with TimeTestUtil @@ -92,11 +93,17 @@ abstract class AmuletExpiryWithOldPackageIntegrationTestBase .withPausedTrigger[UpdateExternalPartyConfigStateTrigger] .withPausedTrigger[ExpireRewardCouponsTrigger] .withPausedTrigger[FeaturedAppActivityMarkerTrigger] + .withPausedTrigger[ExpireTransferPreapprovalsTrigger] + .withPausedTrigger[ExpiredAnsEntryTrigger] + .withPausedTrigger[ExpiredAnsSubscriptionTrigger] + .withPausedTrigger[ExpiredAmuletTrigger] + .withPausedTrigger[ExpiredLockedAmuletTrigger] )(c) ) .addConfigTransforms((_, c) => updateAutomationConfig(ConfigurableApp.Validator)( _.copy(enableAutomaticRewardsCollectionAndAmuletMerging = false) + .withPausedTrigger[SubscriptionReadyForPaymentTrigger] )(c) ) .addConfigTransforms((_, c) => @@ -110,7 +117,27 @@ abstract class AmuletExpiryWithOldPackageIntegrationTestBase )(c) ) - def setupAliceWithDustAmulets()(implicit env: SpliceTestConsoleEnvironment): PartyId = { + protected val danglingSubscriptionCid = new Subscription.ContractId("00" * 33 + "01") + protected val danglingSubscriptionRequestCid = + new SubscriptionRequest.ContractId("00" * 33 + "02") + + protected def createAsDso[T](signatories: PartyId*)( + update: com.daml.ledger.javaapi.data.codegen.Update[T] + )(implicit env: SpliceTestConsoleEnvironment) = { + sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands + .submitWithResult( + userId = sv1Backend.config.ledgerApiUser, + actAs = dsoParty +: signatories, + readAs = Seq.empty, + update = update, + ) + .discard + } + + protected def dsoAcs(implicit env: SpliceTestConsoleEnvironment) = + sv1Backend.participantClientWithAdminToken.ledger_api_extensions.acs + + protected def setupAliceWithDustAmulets()(implicit env: SpliceTestConsoleEnvironment): PartyId = { val synchronizerId = decentralizedSynchronizerId clue("aliceValidator has not vetted splice-amulet 0.1.17 and 0.1.18") { @@ -206,8 +233,8 @@ abstract class AmuletExpiryWithOldPackageIntegrationTestBase /** Tests that expiry triggers fall back to V1 choices when alice's validator * has only vetted minimal package versions (not splice-amulet 0.1.17+). */ -class AmuletExpiryWithMinimalPackageIntegrationTest - extends AmuletExpiryWithOldPackageIntegrationTestBase { +class AmuletExpiryV1FallbackIntegrationTest + extends ExpiryWithMinimalVettedPackagesIntegrationTestBase { "Amulet expiry falls back to V1 choices when alice's validator has not vetted splice-amulet 0.1.17" in { implicit env => @@ -235,49 +262,81 @@ class AmuletExpiryWithMinimalPackageIntegrationTest /** Tests that expiry triggers skip batches when the task's amulet preferred package version * is listed in `ignoredAmuletVersions`, adding the party to the ignored-parties store. */ -class AmuletBasedExpiryWithIgnoredPackageIntegrationTest - extends AmuletExpiryWithOldPackageIntegrationTestBase { +class ExpiryWithIgnoredAmuletVersionIntegrationTest + extends ExpiryWithMinimalVettedPackagesIntegrationTestBase { override val ignoredAmuletVersions: Set[String] = Set( DarResources.amulet_0_1_15.metadata.version.toString ) - "Triggers expiring amulet, locked amulet, and reward coupons and featured app markers skip parties when their preferred amulet package version is marked as ignored" in { + private val entryName = "alice.unverified.ans" + private val entryDescription = "expired ans entry" + + "Expiry triggers skip parties whose preferred amulet package version is ignored" in { implicit env => - val aliceParty: PartyId = setupAliceWithDustAmulets() + val alice = setupAliceWithDustAmulets() + val aliceId = alice.toProtoPrimitive + val dsoId = dsoParty.toProtoPrimitive + advanceRoundsByOneTickViaAutomation() advanceRoundsByOneTickViaAutomation() val (openRounds, _) = sv1ScanBackend.getOpenAndIssuingMiningRounds() val currentRound = openRounds.toList.headOption.value.payload.round + val now = env.environment.clock.now.toInstant + val expired = now.minus(Duration.ofSeconds(1)) - sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands - .submitWithResult( - userId = sv1Backend.config.ledgerApiUser, - actAs = Seq(dsoParty), - readAs = Seq.empty, - update = new AppRewardCoupon( - dsoParty.toProtoPrimitive, - aliceParty.toProtoPrimitive, + clue("Create dust contracts owned or referenced by alice") { + createAsDso()( + new AppRewardCoupon( + dsoId, + aliceId, false, BigDecimal(10.0).bigDecimal, currentRound, java.util.Optional.empty(), - ).create, + ).create ) - - sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands - .submitWithResult( - userId = sv1Backend.config.ledgerApiUser, - actAs = Seq(dsoParty), - readAs = Seq.empty, - update = new FeaturedAppActivityMarker( - dsoParty.toProtoPrimitive, - aliceParty.toProtoPrimitive, - aliceParty.toProtoPrimitive, - BigDecimal(1.0).bigDecimal, - ).create, + createAsDso()( + new FeaturedAppActivityMarker(dsoId, aliceId, aliceId, BigDecimal(1.0).bigDecimal).create + ) + createAsDso(alice)( + new TransferPreapproval( + dsoId, + aliceId, // receiver + aliceId, // provider + now.minus(Duration.ofHours(1)), // validFrom + now.minus(Duration.ofHours(1)), // lastRenewedAt + expired, + ).create + ) + createAsDso(alice)( + new AnsEntry(aliceId, dsoId, entryName, "", entryDescription, expired).create ) + createAsDso(alice)( + new AnsEntryContext( + dsoId, + aliceId, + entryName, + "", + entryDescription, + danglingSubscriptionRequestCid, + ).create + ) + createAsDso(alice)( + new SubscriptionIdleState( + danglingSubscriptionCid, + new SubscriptionData(aliceId, dsoId, dsoId, dsoId, entryDescription), + new SubscriptionPayData( + new PaymentAmount(BigDecimal(1.0).bigDecimal, Unit.AMULETUNIT), + new RelTime(1_000_000_000L), + new RelTime(1_000_000L), + ), + expired, // nextPaymentDueAt -> overdue + danglingSubscriptionRequestCid, + ).create + ) + } actAndCheck(timeUntilSuccess = 60.seconds)( "Advance 4 rounds and resume expiry triggers", { @@ -289,29 +348,105 @@ class AmuletBasedExpiryWithIgnoredPackageIntegrationTest sv.dsoDelegateBasedAutomation.trigger[ExpiredLockedAmuletTrigger].resume() sv.dsoDelegateBasedAutomation.trigger[ExpireRewardCouponsTrigger].resume() sv.dsoDelegateBasedAutomation.trigger[FeaturedAppActivityMarkerTrigger].resume() + sv.dsoDelegateBasedAutomation.trigger[ExpireTransferPreapprovalsTrigger].resume() + sv.dsoDelegateBasedAutomation.trigger[ExpiredAnsEntryTrigger].resume() + sv.dsoDelegateBasedAutomation.trigger[ExpiredAnsSubscriptionTrigger].resume() } }, )( - "Dust contracts remain because preferred version 0.1.15 is in ignoredAmuletVersions", + s"All dust contracts remain because alice's preferred version is in ignoredAmuletVersions", _ => { - sv1Backend.dsoDelegateBasedAutomation.expiredAmuletIgnoredPartiesStore.getAll should contain( - aliceParty - ) - aliceWalletClient.list().amulets should have length 2L withClue "amulets should remain" - aliceWalletClient - .list() - .lockedAmulets should have length 2L withClue "locked amulets should remain" - sv1Backend.participantClientWithAdminToken.ledger_api_extensions.acs - .filterJava(AppRewardCoupon.COMPANION)( - dsoParty, - co => co.data.provider == aliceParty.toProtoPrimitive, - ) should have size 1L withClue "app reward coupon should remain" - sv1Backend.participantClientWithAdminToken.ledger_api_extensions.acs - .filterJava(FeaturedAppActivityMarker.COMPANION)( - dsoParty, - co => co.data.provider == aliceParty.toProtoPrimitive, - ) should have size 1L withClue "featured app activity marker should remain" + sv1Backend.dsoDelegateBasedAutomation.unavailablePartiesStore.getAll should + contain(alice) + + aliceWalletClient.list().amulets should have length 2L withClue "amulets" + aliceWalletClient.list().lockedAmulets should have length 2L withClue "locked amulets" + + dsoAcs.filterJava(AppRewardCoupon.COMPANION)( + dsoParty, + _.data.provider == aliceId, + ) should have size 1L withClue "app reward coupon" + + dsoAcs.filterJava(FeaturedAppActivityMarker.COMPANION)( + dsoParty, + _.data.provider == aliceId, + ) should have size 1L withClue "featured app activity marker" + + dsoAcs.filterJava(TransferPreapproval.COMPANION)( + dsoParty, + _.data.receiver == aliceId, + ) should have size 1L withClue "transfer preapproval" + + dsoAcs.filterJava(AnsEntry.COMPANION)( + dsoParty, + _.data.user == aliceId, + ) should have size 1L withClue "ans entry" + + dsoAcs.filterJava(SubscriptionIdleState.COMPANION)( + dsoParty, + _.data.subscriptionData.sender == aliceId, + ) should have size 1L withClue "ans subscription" }, ) } } + +/** Tests that expiry triggers ignore parties whose participant has no vetted amulet. + * Only Amulet contracts are covered in this test, as the ignore logic is shared across expiry triggers. + */ +class ExpiryWithNoVettedAmuletVersionIntegrationTest + extends ExpiryWithMinimalVettedPackagesIntegrationTestBase { + + "Amulet expiry ignores parties with no vetted amulet version" in { implicit env => + val alice = setupAliceWithDustAmulets() + + clue("Alice unvets every amulet version") { + aliceValidatorBackend.validatorAutomation + .trigger[ValidatorPackageVettingTrigger] + .pause() + .futureValue + aliceValidatorBackend.participantClient.topology.vetted_packages.propose_delta( + aliceValidatorBackend.participantClient.id, + store = decentralizedSynchronizerId, + removes = DarResources.amulet.all.map(p => PackageId.assertFromString(p.packageId)), + force = ForceFlags(ForceFlag.AllowUnvettedDependencies), + ) + eventually() { + val vetted = getVettedPackageIds( + aliceValidatorBackend.appState.participantAdminConnection, + decentralizedSynchronizerId, + ).toSet + DarResources.amulet.all.foreach(p => vetted should not contain p.packageId) + } + } + + loggerFactory.assertLogsSeq( + SuppressionRule.forLogger[ExpiredAmuletTrigger] && SuppressionRule.Level(Level.WARN) + )( + actAndCheck(timeUntilSuccess = 60.seconds)( + "Advance 4 rounds and resume the amulet expiry trigger", { + (1 to 4).foreach(_ => advanceRoundsByOneTickViaAutomation()) + updateExternalPartyConfigStatesViaAutomation() + updateExternalPartyConfigStatesViaAutomation() + env.svs.local.foreach( + _.dsoDelegateBasedAutomation.trigger[ExpiredAmuletTrigger].resume() + ) + }, + )( + "Alice is ignored and her dust amulets are not expired", + _ => { + val ignored = sv1Backend.dsoDelegateBasedAutomation.unavailablePartiesStore.getAll + ignored should contain(alice) + ignored should not contain dsoParty + aliceWalletClient.list().amulets should have length 2L withClue "dust amulets" + }, + ), + entries => + forAtLeast(1, entries) { entry => + entry.warningMessage should include("No vetted Amulet version") + entry.warningMessage should include(alice.uid.identifier.str) + entry.warningMessage should include("ignoring 1 parties") + }, + ) + } +} diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala index a6b4b8fab9..6ab887197c 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala @@ -103,6 +103,7 @@ class ExternalPartySetupProposalIntegrationTest NonNegativeFiniteDuration.ofMillis(500) )(config), ) + .withTransferCommandSupport } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedPartyTestUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedPartyTestUtil.scala index 93aec5e955..baff747368 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedPartyTestUtil.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedPartyTestUtil.scala @@ -1,10 +1,18 @@ package org.lfdecentralizedtrust.splice.integration.tests +import com.daml.metrics.api.noop.NoOpMetricsFactory +import com.daml.metrics.api.{MetricName, MetricsContext, HistogramInventory} import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.config.{CachingConfigs, CryptoProvider, CryptoSchemeConfig} import com.digitalasset.canton.crypto.* import com.digitalasset.canton.crypto.provider.jce.JcePureCrypto import com.digitalasset.canton.crypto.v30 as cryptoProto +import com.digitalasset.canton.metrics.{ + SigningHistograms, + DecryptionMetrics, + SigningMetrics, + DecryptionHistograms, +} import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.util.HexString import com.digitalasset.canton.version.ProtocolVersion @@ -133,6 +141,20 @@ trait ExternallySignedPartyTestUtil extends TestCommon { ) } + private[this] val noOpMetricsFactory = new NoOpMetricsFactory + private[this] val histogramInventory = new HistogramInventory() + private[this] implicit val metricsContext: MetricsContext = new MetricsContext(Map.empty) + + private[this] val signingMetrics = new SigningMetrics( + new SigningHistograms(MetricName("splice-test"))(histogramInventory), + noOpMetricsFactory, + )(metricsContext) + + private[this] val decryptionMetrics = new DecryptionMetrics( + new DecryptionHistograms(MetricName("splice-test"))(histogramInventory), + noOpMetricsFactory, + )(metricsContext) + // The parameters here are just defaults so don't really matter def crypto(implicit ec: ExecutionContext) = new JcePureCrypto( CryptoProvider.Jce.symmetric.default, @@ -150,6 +172,8 @@ trait ExternallySignedPartyTestUtil extends TestCommon { CachingConfigs.defaultPublicKeyConversionCache, None, PositiveInt.tryCreate(1), + signingMetrics, + decryptionMetrics, loggerFactory, ) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala index 1517b15167..cddcb116a5 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala @@ -21,7 +21,7 @@ trait ExternallySignedTxTest override def environmentDefinition: SpliceEnvironmentDefinition = { EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName) - } + }.withTransferCommandSupport def prepareAndSubmitTransfer(keyName: String, sender: PartyId, receiver: PartyId)(implicit env: SpliceTestConsoleEnvironment diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala index f279098757..afda9edb61 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala @@ -26,6 +26,7 @@ class ExternallySignedTxsTimeBasedIntegrationTest override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition .simpleTopology1SvWithSimTime(this.getClass.getSimpleName) + .withTransferCommandSupport "Externally signed transactions can tolerate a preparation/submission skew larger than ledgerTimeRecordTimeTolerance" in { implicit env => diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LocalNetReassignIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LocalNetReassignIntegrationTest.scala new file mode 100644 index 0000000000..39c9e1560c --- /dev/null +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LocalNetReassignIntegrationTest.scala @@ -0,0 +1,196 @@ +package org.lfdecentralizedtrust.splice.integration.tests + +import com.daml.ledger.api.v2.transaction_filter.CumulativeFilter.IdentifierFilter +import com.daml.ledger.api.v2.transaction_filter.{ + CumulativeFilter, + EventFormat, + Filters, + WildcardFilter, +} +import com.digitalasset.canton.protocol.LfContractId +import com.digitalasset.canton.topology.SynchronizerId +import monocle.Monocle.toAppliedFocusOps +import org.lfdecentralizedtrust.splice.auth.AuthUtil +import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.test.dummyholding.DummyHolding +import org.lfdecentralizedtrust.splice.console.ParticipantClientReference +import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition +import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTestWithIsolatedEnvironment +import org.lfdecentralizedtrust.splice.util.JavaDecodeUtil + +import java.nio.file.Paths +import scala.jdk.CollectionConverters.* +import scala.sys.process.* + +/** Verifies the actual capability unlocked by the multi-synchronizer topology feature flag set in + * cluster/compose/localnet/conf/console/app-synchronizer.sc: reassigning a contract between the + * global synchronizer and the app-synchronizer. Without the flag, the unassignment is rejected + * with MultiSynchronizerIsNotEnabled. + * + * This spins up the docker-compose localnet with the `multi-sync` profile enabled (-M) + */ +class LocalNetReassignIntegrationTest extends IntegrationTestWithIsolatedEnvironment { + + override def environmentDefinition: SpliceEnvironmentDefinition = + EnvironmentDefinition + .fromResources(Seq("localnet-reassign-topology.conf"), this.getClass.getSimpleName) + .updateTestingConfig( + _.focus(_.participantsWithoutLapiVerification).replace( + Set( + "app-provider", + "app-user", + ) + ) + ) + .withManualStart + + // These do nothing as the clients will not actually be connected to the compose setup. + override protected def runTokenStandardCliSanityCheck: Boolean = false + override lazy val resetRequiredTopologyState = false + + // The user all localnet nodes use for their ledger API access, see + // cluster/compose/localnet/env/*-auth-on.env + private val ledgerApiUserId = "ledger-api-user" + + private val token = AuthUtil.testToken(AuthUtil.testAudience, ledgerApiUserId, "unsafe") + + private val dummyHoldingDarPath = Paths + .get( + "token-standard/examples/splice-token-test-dummy-holding/.daml/dist/splice-token-test-dummy-holding-current.dar" + ) + .toAbsolutePath + .toString + + private def withLocalNet( + )(f: FixtureParam => Any)(implicit env: FixtureParam): Unit = + try { + val ret = (Seq("build-tools/splice-localnet-compose.sh", "start") ++ Seq("-M")).! + if (ret != 0) { + fail("Failed to start docker-compose SV and validator") + } + f(env) + } finally { + (Seq("build-tools/splice-localnet-compose.sh", "stop", "-D") ++ Seq("-M")).! + } + + private def participantClient(name: String)(implicit env: FixtureParam) = { + val remoteParticipant = + env.participants.remote + .find(_.name == name) + .getOrElse(fail(s"$name participant not found")) + new ParticipantClientReference( + env, + remoteParticipant.name, + remoteParticipant.config.copy(token = Some(token)), + ) + } + + private def synchronizerId( + participant: ParticipantClientReference, + alias: String, + ): SynchronizerId = + participant.synchronizers + .list_connected() + .find(_.synchronizerAlias.unwrap == alias) + .getOrElse(fail(s"${participant.name} is not connected to $alias")) + .synchronizerId + + private def testReassignment(participantName: String, validatorClientName: String)(implicit + env: FixtureParam + ): Unit = + clue(s"Reassign a contract between global and app-synchronizer on $participantName") { + val participant = participantClient(participantName) + val party = vc(validatorClientName).copy(token = Some(token)).getValidatorPartyId() + val globalSynchronizerId = synchronizerId(participant, "global") + val appSynchronizerId = synchronizerId(participant, "app-synchronizer") + + participant.upload_dar_unless_exists(dummyHoldingDarPath) + + val createdContract = clue("Create a DummyHolding on the global synchronizer") { + val tx = participant.ledger_api_extensions.commands.submitJava( + actAs = Seq(party), + commands = new DummyHolding( + party.toProtoPrimitive, + party.toProtoPrimitive, + BigDecimal(42).bigDecimal, + ).create().commands().asScala.toSeq, + synchronizerId = Some(globalSynchronizerId), + userId = ledgerApiUserId, + ) + JavaDecodeUtil.decodeAllCreated(DummyHolding.COMPANION)(tx).loneElement + } + val contractId = createdContract.id.contractId + val lfContractId = LfContractId.assertFromString(contractId) + + def contractSynchronizerId(): Option[String] = + participant.ledger_api.state.acs + .active_contracts_of_party(party = party) + .find(_.createdEvent.value.contractId == contractId) + .map(_.synchronizerId) + + contractSynchronizerId() shouldBe Some(globalSynchronizerId.toProtoPrimitive) + + // Scope the reassignment event format to `party` + val eventFormat = + EventFormat( + filtersByParty = Map( + party.toProtoPrimitive -> Filters( + Seq( + CumulativeFilter( + IdentifierFilter.WildcardFilter( + WildcardFilter(includeCreatedEventBlob = false) + ) + ) + ) + ) + ), + filtersForAnyParty = None, + verbose = true, + ) + + def reassign(source: SynchronizerId, target: SynchronizerId): Unit = { + val unassigned = participant.ledger_api.commands + .submit_unassign_with_format( + submitter = party, + contractIds = Seq(lfContractId), + source = source, + target = target, + userId = ledgerApiUserId, + eventFormat = Some(eventFormat), + timeout = None, + ) + .unassignedWrapper + val _ = participant.ledger_api.commands.submit_assign_with_format( + submitter = party, + reassignmentId = unassigned.reassignmentId, + source = source, + target = target, + userId = ledgerApiUserId, + eventFormat = Some(eventFormat), + timeout = None, + ) + } + + actAndCheck( + "Reassign the contract to the app-synchronizer", + reassign(globalSynchronizerId, appSynchronizerId), + )( + "The contract is now assigned to the app-synchronizer", + _ => contractSynchronizerId() shouldBe Some(appSynchronizerId.toProtoPrimitive), + ) + + actAndCheck( + "Reassign the contract back to the global synchronizer", + reassign(appSynchronizerId, globalSynchronizerId), + )( + "The contract is assigned to the global synchronizer again", + _ => contractSynchronizerId() shouldBe Some(globalSynchronizerId.toProtoPrimitive), + ) + } + + "docker-compose based localnet supports reassignment between synchronizers" in { implicit env => + withLocalNet() { implicit env => + testReassignment("app-provider", "providerValidatorClient") + testReassignment("app-user", "userValidatorClient") + } + } +} diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala index 9c32e60cfe..59e3baa1f1 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala @@ -29,7 +29,6 @@ import org.lfdecentralizedtrust.splice.environment.{ SequencerAdminConnection, } import org.lfdecentralizedtrust.splice.http.v0.definitions -import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ IntegrationTest, @@ -81,12 +80,10 @@ class LsuIntegrationTest override protected def beforeAll(): Unit = { super.beforeAll() - SynchronizerUpgradeUtil.migrationDumpDir.delete() + SynchronizerUpgradeUtil.migrationDumpDir.delete(swallowIOExceptions = true) } - // always set the successor PV to 35 - // thus with the daily run with PV34 we will run a PV34 -> PV35 LSU - // otherwise we will run a PV35 -> PV35 LSU - val successorPv = ProtocolVersion.v35 + + private val successorPv = ProtocolVersion.v35 override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition @@ -112,10 +109,11 @@ class LsuIntegrationTest physicalSynchronizerExpiration = NonNegativeFiniteDuration.ofSeconds(1) ), // sv-4 is intentionally a late-joining node in this test, which means the - // sequencer spends some time catching up. This can cause the sv-app's + // sequencer spends some time catching up. This can cause the sv-4 apps' // circuit breakers to trip, which makes annoying logs and delays init. // The circuit breakers tripping a bit during catchup would be just fine - // IRL, so as a simple fix to this test we disable them for sv-4. + // IRL, so as a simple fix to this test we disable them for all sv-4 apps + // (see also the scan and validator app transforms below). circuitBreakers = if (name == "sv4") CircuitBreakersConfig.never else config.parameters.circuitBreakers, @@ -124,7 +122,7 @@ class LsuIntegrationTest } .andThen( ConfigTransforms - .updateAllScanAppConfigs { (_, config) => + .updateAllScanAppConfigs { (name, config) => config.copy( synchronizerNodes = config.synchronizerNodes.copy( successor = Some(config.synchronizerNodes.current) @@ -132,10 +130,25 @@ class LsuIntegrationTest parameters = config.parameters.copy( spliceCachingConfigs = config.parameters.spliceCachingConfigs.copy( physicalSynchronizerExpiration = NonNegativeFiniteDuration.ofSeconds(1) - ) + ), + circuitBreakers = + if (name == "sv4Scan") CircuitBreakersConfig.never + else config.parameters.circuitBreakers, ), ) } + ) + .andThen( + ConfigTransforms + .updateAllValidatorAppConfigs { (name, config) => + config.copy( + parameters = config.parameters.copy( + circuitBreakers = + if (name == "sv4Validator") CircuitBreakersConfig.never + else config.parameters.circuitBreakers + ) + ) + } )(config) }) .withBftSequencersSuccessor @@ -206,6 +219,7 @@ class LsuIntegrationTest .withSvBftSequencerConnectionDisabled() .withAmuletPrice(walletAmuletPrice) .withManualStart + .withTransferCommandSupport override def walletAmuletPrice: java.math.BigDecimal = SpliceUtil.damlDecimal(1.0) @@ -216,25 +230,96 @@ class LsuIntegrationTest "cancel a scheduled logical synchronizer upgrade" in { implicit env => initDso(includeLocal = false) startAllSync(aliceValidatorBackend, splitwellValidatorBackend) + val topologyFreezeTime = CantonTimestamp.now() - val upgradeTime = CantonTimestamp.now().plusSeconds(120) + // Use an upgrade time 1h in the future so that the upgrade nodes have enough time to be + // started, initialized and to publish their sequencer successors before we cancel. + val upgradeTime = CantonTimestamp.now().plus(Duration.ofHours(1)) - clue("Schedule logical synchronizer upgrade") { - scheduleLsu(topologyFreezeTime, upgradeTime, 1L) - } + val newSynchronizerSerial = decentralizedSynchronizerPSId.serial + NonNegativeInt.one + val successorPsid = decentralizedSynchronizerPSId.copy( + serial = newSynchronizerSerial, + protocolVersion = successorPv, + ) + val svNodesDoingTheLsu = Seq(sv1Backend, sv2Backend, sv3Backend, sv4Backend) - clue("Wait for LSU announcement to be proposed") { - waitForLsuAnnouncement() - } + withCantonSvNodes( + ( + None, + None, + None, + None, + ), + participants = false, + enableBftSequencer = true, + logSuffix = "cancel-global-synchronizer-upgrade", + )( + ProcessTestUtil.javaToolOptionsKey -> "-Xms8g -Xmx10g" + ) { + clue(s"Schedule logical synchronizer upgrade at $upgradeTime") { + scheduleLsu(topologyFreezeTime, upgradeTime, newSynchronizerSerial.value.toLong) + } - clue("Cancel LSU from all SVs") { - Seq(sv1Backend, sv2Backend, sv3Backend, sv4Backend).par.foreach { sv => - sv.cancelLogicalSynchronizerUpgrade() + clue("Wait for LSU announcement to be proposed") { + waitForLsuAnnouncement() } - } - clue("LSU announcement has been removed from topology state") { - eventually() { + clue("Upgrade nodes are started and initialized before cancelling") { + svNodesDoingTheLsu.foreach { backend => + val upgradeSequencerClient = backend.sequencerClientFor(_.successor.value) + val upgradeMediatorClient = backend.mediatorClientFor(_.successor.value) + clue(s"check ${backend.name} initialized sequencer from synchronizer predecessor") { + eventuallySucceeds(3.minutes) { + upgradeSequencerClient.physical_synchronizer_id shouldBe successorPsid + } + } + clue(s"check ${backend.name} initialized mediator") { + eventuallySucceeds(3.minutes) { + upgradeMediatorClient.health.initialized() shouldBe true + } + } + } + } + + clue("Sequencer successors were published for all upgrade nodes") { + eventually() { + val successors = + sv1Backend.participantClientWithAdminToken.topology.lsu.sequencer_successors + .list(store = Some(Synchronizer(decentralizedSynchronizerId))) + successors should have size svNodesDoingTheLsu.size.toLong + successors.map(_.item.successorPsid).toSet shouldBe Set(successorPsid) + } + } + + clue("Cancel LSU from all SVs") { + svNodesDoingTheLsu.par.foreach { sv => + sv.cancelLogicalSynchronizerUpgrade() + } + } + + clue("LSU announcement has been removed from topology state") { + eventually() { + sv1Backend.participantClientWithAdminToken.topology.lsu.announcement + .list( + store = Some(Synchronizer(decentralizedSynchronizerId)), + operation = Some(TopologyChangeOp.Replace), + ) shouldBe empty + } + } + + clue("Removal transaction exists in topology history") { + val removals = sv1Backend.participantClientWithAdminToken.topology.lsu.announcement + .list( + store = Some(Synchronizer(decentralizedSynchronizerId)), + timeQuery = TimeQuery.Range(None, None), + operation = Some(TopologyChangeOp.Remove), + ) + removals should not be empty + } + + clue("Trigger does not re-create the cancelled announcement") { + // Wait long enough for the trigger to have run multiple times + Threading.sleep(10_000) sv1Backend.participantClientWithAdminToken.topology.lsu.announcement .list( store = Some(Synchronizer(decentralizedSynchronizerId)), @@ -243,158 +328,32 @@ class LsuIntegrationTest } } - clue("Removal transaction exists in topology history") { - val removals = sv1Backend.participantClientWithAdminToken.topology.lsu.announcement - .list( - store = Some(Synchronizer(decentralizedSynchronizerId)), - timeQuery = TimeQuery.Range(None, None), - operation = Some(TopologyChangeOp.Remove), - ) - removals should not be empty - } - - clue("Trigger does not re-create the cancelled announcement") { - // Wait long enough for the trigger to have run multiple times - Threading.sleep(10_000) - sv1Backend.participantClientWithAdminToken.topology.lsu.announcement - .list( - store = Some(Synchronizer(decentralizedSynchronizerId)), - operation = Some(TopologyChangeOp.Replace), - ) shouldBe empty - } + checkSerial1Sequencers() } - "upgrade synchronizer to new physical synchronizer without downtime" in { implicit env => - val allNodes = Seq[AppBackendReference]( - sv1ScanBackend, - sv2ScanBackend, - sv3ScanBackend, - sv4ScanBackend, - sv1Backend, - sv1LocalBackend, - sv1NoLegacyLocalBackend, - sv2Backend, - sv3Backend, - sv4Backend, - sv1ValidatorBackend, - sv2ValidatorBackend, - sv3ValidatorBackend, - sv4ValidatorBackend, - ) - actAndCheck("Create some transaction history", sv1WalletClient.tap(1337))( - "Scan transaction history is recorded and wallet balance is updated", - _ => { - // buffer to account for domain fee payments - assertInRange( - sv1WalletClient.balance().unlockedQty, - (walletUsdToAmulet(1000), walletUsdToAmulet(2000)), - ) - countTapsFromScan(sv1ScanBackend, walletUsdToAmulet(1337)) shouldBe 1 - }, - ) - + def checkSerial1Sequencers()(implicit env: SpliceTestConsoleEnvironment) = { clue("All sequencers are registered") { - eventually() { + eventually(timeUntilSuccess = 1.minute) { inside(sv1ScanBackend.listDsoSequencers()) { case Seq(DomainSequencers(synchronizerId, sequencers)) => synchronizerId shouldBe decentralizedSynchronizerId - sequencers should have size 8 - sequencers.foreach { sequencer => - sequencer.serial match { - case Some(serial) => - serial shouldBe 0 - sequencer.migrationId shouldBe -1 - case None => - sequencer.migrationId shouldBe 0 - } + sequencers should have size 12 + forExactly(4, sequencers) { + _.serial.value shouldBe 0 + } + forExactly(4, sequencers) { + _.serial.value shouldBe 1 + } + forExactly(4, sequencers) { + _.serial should be(empty) } } } } + } - def onboardUserAndTapAmulet( - validatorBackend: ValidatorAppBackendReference, - walletClient: WalletAppClientReference, - tapAmount: BigDecimal = 50.0, - expectedAmulets: Range = 50 to 50, - ) = { - val walletUserParty = onboardWalletUser(walletClient, validatorBackend) - eventuallySucceeds() { - walletClient.tap(tapAmount) - } - clue(s"${validatorBackend.name} has tapped a amulet") { - checkWallet( - walletUserParty, - walletClient, - Seq((walletUsdToAmulet(expectedAmulets.start), walletUsdToAmulet(expectedAmulets.end))), - ) - } - walletUserParty - } - - def createExternalParty( - validatorBackend: ValidatorAppBackendReference, - walletClient: WalletAppClientReference, - ) = { - val onboarding @ OnboardingResult(externalParty, _, _) = - onboardExternalParty(validatorBackend) - walletClient.tap(50.0) - createTransferPreapprovalEnsuringItExists(walletClient, validatorBackend) - createAndAcceptExternalPartySetupProposal(validatorBackend, onboarding) - validatorBackend - .getExternalPartyBalance(externalParty) - .totalUnlockedCoin shouldBe "0.0000000000" - // can still fail with no preapproval depending on what scan subset is used - eventuallySucceeds() { - walletClient.transferPreapprovalSend(externalParty, 40.0, UUID.randomUUID.toString) - } - eventually() { - validatorBackend - .getExternalPartyBalance(externalParty) - .totalUnlockedCoin shouldBe "40.0000000000" - } - onboarding - } - - onboardUserAndTapAmulet(aliceValidatorBackend, aliceValidatorWalletClient) - - // account for the cancellation - val newSynchronizerSerial = decentralizedSynchronizerPSId.serial + NonNegativeInt.two - val successorPsid = decentralizedSynchronizerPSId.copy( - serial = newSynchronizerSerial, - protocolVersion = successorPv, - ) - // Upload after starting validator which connects to global - // synchronizers as upload_dar_unless_exists vets on all - // connected synchronizers. - aliceValidatorBackend.participantClient.upload_dar_unless_exists(splitwellDarPath) - val externalPartyOnboarding = clue("Create external party and transfer 40 amulet to it") { - createExternalParty(aliceValidatorBackend, aliceValidatorWalletClient) - } - - val bobValidatorWalletLocal = wc( - "bobValidatorWalletLocal" - ) - clue("Start bob validator local, onboard and tap before upgrade") { - runBobValidatorWithStandaloneParticipant("before-upgrade")( - onboardUserAndTapAmulet( - bobValidatorLocal, - bobValidatorWalletLocal, - ) - ) - } - - val lateJoiningNode = sv4Nodes - lateJoiningNode.par.foreach(_.stop()) - val topologyFreezeTime = CantonTimestamp.now() - // We need to give enough time for the new Canton instance to startup - // and finish sequencer initialization so we can then publish the sequencer announcement before the upgrade time. - val upgradeTime = CantonTimestamp.now().plusSeconds(150) - clue(s"Schedule logical synchronizer upgrade at $upgradeTime") { - scheduleLsu(topologyFreezeTime, upgradeTime, newSynchronizerSerial.value.toLong) - } - val allBackends = Seq(sv1Backend, sv2Backend, sv3Backend, sv4Backend) - val initialSvNodesDoingTheLsu = Seq(sv1Backend, sv2Backend, sv3Backend) + "upgrade synchronizer to new physical synchronizer without downtime" in { implicit env => + // start the nodes early so that the sv app can remove the existing successor physical synchronizer state withCantonSvNodes( ( None, @@ -405,8 +364,128 @@ class LsuIntegrationTest participants = false, enableBftSequencer = true, logSuffix = "global-synchronizer-upgrade", - )() { + )( + ProcessTestUtil.javaToolOptionsKey -> "-Xms8g -Xmx10g" + ) { + + val allNodes = Seq[AppBackendReference]( + sv1ScanBackend, + sv2ScanBackend, + sv3ScanBackend, + sv4ScanBackend, + sv1Backend, + sv1LocalBackend, + sv1NoLegacyLocalBackend, + sv2Backend, + sv3Backend, + sv4Backend, + sv1ValidatorBackend, + sv2ValidatorBackend, + sv3ValidatorBackend, + sv4ValidatorBackend, + ) + // restart to clear any caches + allNodes.par.foreach(_.stop()) + initDso(includeLocal = false) + startAllSync(aliceValidatorBackend, splitwellValidatorBackend) + actAndCheck("Create some transaction history", sv1WalletClient.tap(1337))( + "Wallet balance is updated", + _ => { + // buffer to account for domain fee payments + assertInRange( + sv1WalletClient.balance().unlockedQty, + (walletUsdToAmulet(1000), walletUsdToAmulet(2000)), + ) + }, + ) + + // The serial 1 sequencers are still registered at this point as we only unregister them when the successors give us back a response + // and they are still uninitialized here. + checkSerial1Sequencers() + def onboardUserAndTapAmulet( + validatorBackend: ValidatorAppBackendReference, + walletClient: WalletAppClientReference, + tapAmount: BigDecimal = 50.0, + expectedAmulets: Range = 50 to 50, + ) = { + val walletUserParty = onboardWalletUser(walletClient, validatorBackend) + eventuallySucceeds() { + walletClient.tap(tapAmount) + } + clue(s"${validatorBackend.name} has tapped a amulet") { + checkWallet( + walletUserParty, + walletClient, + Seq((walletUsdToAmulet(expectedAmulets.start), walletUsdToAmulet(expectedAmulets.end))), + ) + } + walletUserParty + } + + def createExternalParty( + validatorBackend: ValidatorAppBackendReference, + walletClient: WalletAppClientReference, + ) = { + val onboarding @ OnboardingResult(externalParty, _, _) = + onboardExternalParty(validatorBackend) + walletClient.tap(50.0) + createTransferPreapprovalEnsuringItExists(walletClient, validatorBackend) + createAndAcceptExternalPartySetupProposal(validatorBackend, onboarding) + validatorBackend + .getExternalPartyBalance(externalParty) + .totalUnlockedCoin shouldBe "0.0000000000" + // can still fail with no preapproval depending on what scan subset is used + eventuallySucceeds() { + walletClient.transferPreapprovalSend(externalParty, 40.0, UUID.randomUUID.toString) + } + eventually() { + validatorBackend + .getExternalPartyBalance(externalParty) + .totalUnlockedCoin shouldBe "40.0000000000" + } + onboarding + } + + onboardUserAndTapAmulet(aliceValidatorBackend, aliceValidatorWalletClient) + + // account for the cancellation + val newSynchronizerSerial = decentralizedSynchronizerPSId.serial + NonNegativeInt.two + val successorPsid = decentralizedSynchronizerPSId.copy( + serial = newSynchronizerSerial, + protocolVersion = successorPv, + ) + // Upload after starting validator which connects to global + // synchronizers as upload_dar_unless_exists vets on all + // connected synchronizers. + aliceValidatorBackend.participantClient.upload_dar_unless_exists(splitwellDarPath) + val externalPartyOnboarding = clue("Create external party and transfer 40 amulet to it") { + createExternalParty(aliceValidatorBackend, aliceValidatorWalletClient) + } + + val bobValidatorWalletLocal = wc( + "bobValidatorWalletLocal" + ) + clue("Start bob validator local, onboard and tap before upgrade") { + runBobValidatorWithStandaloneParticipant("before-upgrade")( + onboardUserAndTapAmulet( + bobValidatorLocal, + bobValidatorWalletLocal, + ) + ) + } + + val lateJoiningNode = sv4Nodes + lateJoiningNode.par.foreach(_.stop()) + val topologyFreezeTime = CantonTimestamp.now() + // We need to give enough time for the new Canton instance to startup + // and finish sequencer initialization so we can then publish the sequencer announcement before the upgrade time. + val upgradeTime = CantonTimestamp.now().plusSeconds(150) + clue(s"Schedule logical synchronizer upgrade at $upgradeTime") { + scheduleLsu(topologyFreezeTime, upgradeTime, newSynchronizerSerial.value.toLong) + } + val allBackends = Seq(sv1Backend, sv2Backend, sv3Backend, sv4Backend) + val initialSvNodesDoingTheLsu = Seq(sv1Backend, sv2Backend, sv3Backend) clue( "Pause traffic transfer trigger on sv2 to simulate a participant that is connected to a non initialized sequencer past upgrade tiem" ) { @@ -593,7 +672,7 @@ class LsuIntegrationTest inside(sv1ScanBackend.listDsoSequencers()) { case Seq(DomainSequencers(synchronizerId, sequencers)) => synchronizerId shouldBe decentralizedSynchronizerId - sequencers should have size 11 + sequencers should have size 12 sequencers.groupBy(_.svName).foreach { case (sv, sequencers) => clue(s"check sequencers for $sv") { forExactly(1, sequencers) { sequencer => @@ -605,6 +684,13 @@ class LsuIntegrationTest sequencer.serial.value shouldBe newSynchronizerSerial.value.toLong sequencer.migrationId shouldBe -1 } + else { + // sv4 still reports the old serial until it upgrades + forExactly(1, sequencers) { sequencer => + sequencer.serial.value shouldBe 1 + sequencer.migrationId shouldBe -1 + } + } forExactly(1, sequencers) { sequencer => sequencer.serial should be(empty) sequencer.migrationId shouldBe 0 @@ -891,16 +977,6 @@ class LsuIntegrationTest retryProvider, ) - private def countTapsFromScan(scan: ScanAppBackendReference, tapAmount: BigDecimal) = { - listTransactionsFromScan(scan).count( - _.tap.map(a => BigDecimal(a.amuletAmount)).contains(tapAmount) - ) - } - - private def listTransactionsFromScan(scan: ScanAppBackendReference) = { - scan.listTransactions(None, TransactionHistoryRequest.SortOrder.Asc, 100) - } - private def getSequencerUrlsConfiguredForTheSync( participantConnection: ParticipantClientReference, synchronizerAlias: SynchronizerAlias, diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ManualSignatureIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ManualSignatureIntegrationTest.scala index 9c6ec90f0f..60d50c974e 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ManualSignatureIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ManualSignatureIntegrationTest.scala @@ -6,7 +6,6 @@ import com.digitalasset.canton.crypto.SigningPublicKey import com.digitalasset.canton.topology.Namespace import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId import com.digitalasset.canton.topology.store.TimeQuery -import org.lfdecentralizedtrust.splice.config.ConfigTransforms.updateAllScanAppConfigs import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest import org.lfdecentralizedtrust.splice.util.WalletTestUtil @@ -25,12 +24,6 @@ class ManualSignatureIntegrationTest .withManualStart .withoutAliceValidatorConnectingToSplitwell .withSequencerConnectionsFromScanDisabled() - .addConfigTransforms((_, config) => - updateAllScanAppConfigs((_, config) => - // Sequencer is returning TRAFFIC_CONTROL_DISABLED when looking up traffic summaries. - config.copy(enableAppActivityRecordAndTrafficIngestion = false) - )(config) - ) } "synchronizer" should { diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/MultiHostValidatorOperatorIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/MultiHostValidatorOperatorIntegrationTest.scala index 163e7a43d4..277c7d057f 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/MultiHostValidatorOperatorIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/MultiHostValidatorOperatorIntegrationTest.scala @@ -1,11 +1,9 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.digitalasset.canton.topology.transaction.* -import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest import org.lfdecentralizedtrust.splice.util.WalletTestUtil -import org.lfdecentralizedtrust.splice.store.Limit import java.nio.file.Files import java.util.UUID @@ -212,19 +210,8 @@ class MultiHostValidatorOperatorIntegrationTest extends IntegrationTest with Wal )( "The send succeeds despite alice's validator being disconnected and stopped", _ => { - // Fees eat up quite a bit splitwellWalletClient.balance().unlockedQty should be(60) - // Alice's wallet is stopped, so we confirm the transaction via scan - sv1ScanBackend - .listTransactions( - None, - TransactionHistoryRequest.SortOrder.Desc, - Limit.DefaultMaxPageSize, - ) - .flatMap(_.transfer) - .filter(tf => - tf.description == transferDescription - ) should not be empty withClue "transfers splitwell to alice" + // don't check on alice's wallet as it's stopped. the transaction going through on splitwell is enough signal. }, ) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ParticipantKmsIdentitiesEnterpriseIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ParticipantKmsIdentitiesIntegrationTest.scala similarity index 99% rename from apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ParticipantKmsIdentitiesEnterpriseIntegrationTest.scala rename to apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ParticipantKmsIdentitiesIntegrationTest.scala index fd73c1fbcc..fdb541a237 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ParticipantKmsIdentitiesEnterpriseIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ParticipantKmsIdentitiesIntegrationTest.scala @@ -16,7 +16,7 @@ import org.lfdecentralizedtrust.splice.util.StandaloneCanton import java.nio.file.{Path, Paths} -class ParticipantKmsIdentitiesEnterpriseIntegrationTest +class ParticipantKmsIdentitiesIntegrationTest extends IntegrationTestWithIsolatedEnvironment with StandaloneCanton { diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ReconcileBftSequencingParametersIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ReconcileBftSequencingParametersIntegrationTest.scala index 47ec306f39..4eb222f4b8 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ReconcileBftSequencingParametersIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ReconcileBftSequencingParametersIntegrationTest.scala @@ -1,10 +1,15 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.digitalasset.canton.config.CantonRequireTypes.InstanceName -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.SequencingParameters +import com.digitalasset.canton.config.PositiveFiniteDuration +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.{ + BlacklistLeaderSelectionPolicyConfig, + SequencingParameters, +} import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest +import org.lfdecentralizedtrust.splice.sv.config.BftSequencingParameters import org.lfdecentralizedtrust.splice.util.StandaloneCanton class SvReconcileBftSequencingParametersIntegrationTest @@ -27,13 +32,25 @@ class SvReconcileBftSequencingParametersIntegrationTest (InstanceName.tryCreate("sv1Local") -> c.svApps(InstanceName.tryCreate("sv1")) .copy( - cantonBftSequencingParameters = None + cantonBftSequencingParameters = Some( + BftSequencingParameters( + pbftViewChangeTimeout = PositiveFiniteDuration.ofSeconds(5), + segmentLength = SequencingParameters.DefaultSegmentLength.length, + blacklistLeaderSelectionPolicyConfig = + SequencingParameters.DefaultLeaderSelectionPolicyConfig.copy( + howLongToBlacklist = + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear( + maximumEpochBlacklisted = Some(250L) + ) + ), + ) + ) )) ), ) .withManualStart - "SV automation can set and unset bft sequencing parameters" in { implicit env => + "SV automation can modify bft sequencing parameters" in { implicit env => withCantonSvNodes( ( Some(sv1Backend), @@ -56,12 +73,26 @@ class SvReconcileBftSequencingParametersIntegrationTest .value bftParameters.pbftViewChangeTimeout shouldBe com.digitalasset.canton.time.PositiveFiniteDuration .tryOfSeconds(5) + bftParameters.blacklistLeaderSelectionPolicyConfig.howLongToBlacklist shouldBe a[ + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Exponential + ] sv1Backend.stop() - actAndCheck("Restart with sequencing parameters unset", sv1LocalBackend.startSync())( - "sequencing parameters are unset", + actAndCheck( + "Restart with modified bft sequencing parameters", + sv1LocalBackend.startSync(), + )( + "sequencing parameters are modified", _ => { - sv1LocalBackend.participantClient.topology.sequencing_parameters - .list(decentralizedSynchronizerId) shouldBe empty + val parameters = sv1Backend.participantClient.topology.sequencing_parameters + .list(decentralizedSynchronizerId) + .loneElement + val bytes = parameters.item.payload.value + val bftParameters = SequencingParameters + .fromByteString(sv1Backend.config.localSynchronizerNodes.current.protocolVersion, bytes) + .value + bftParameters.blacklistLeaderSelectionPolicyConfig.howLongToBlacklist shouldBe a[ + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear + ] }, ) sv1LocalBackend.stop() diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala index d08ded7642..fbacabfd1a 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala @@ -31,7 +31,7 @@ class RecoverExternalPartyIntegrationTest with WalletTestUtil { override def environmentDefinition: EnvironmentDefinition = - EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName) + EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName).withTransferCommandSupport override protected lazy val sanityChecksIgnoredRootCreates = Seq( ValidatorRewardCoupon.TEMPLATE_ID_WITH_PACKAGE_ID diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuDRIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuDRIntegrationTest.scala index 7c358bf7bb..12e65d9f18 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuDRIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuDRIntegrationTest.scala @@ -85,7 +85,7 @@ class RollForwardLsuDRIntegrationTest val trafficExportTimeFile = File.newTemporaryFile() // This needs to be long enough in the future that we can start Canton and initialize for SVs. - val maxSequencingTime = CantonTimestamp.now().plusSeconds(210) + val maxSequencingTime = CantonTimestamp.now().plusSeconds(240) // 5s chosen by fair dice roll val lowerBoundSequencingTimeExclusive = maxSequencingTime.plusSeconds(5) val upgradeTime = lowerBoundSequencingTimeExclusive diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuIntegrationTest.scala index 8788c3338d..f1ed840381 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuIntegrationTest.scala @@ -18,7 +18,6 @@ import org.lfdecentralizedtrust.splice.environment.{ MediatorAdminConnection, SequencerAdminConnection, } -import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest import monocle.macros.syntax.lens.* import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest @@ -164,14 +163,13 @@ class RollForwardLsuIntegrationTest startAllSync(allNodes*) actAndCheck("Create some transaction history", sv1WalletClient.tap(1337))( - "Scan transaction history is recorded and wallet balance is updated", + "Wallet balance is updated", _ => { // buffer to account for domain fee payments assertInRange( sv1WalletClient.balance().unlockedQty, (walletUsdToAmulet(1000), walletUsdToAmulet(2000)), ) - countTapsFromScan(sv1ScanBackend, walletUsdToAmulet(1337)) shouldBe 1 }, ) @@ -429,16 +427,6 @@ class RollForwardLsuIntegrationTest retryProvider, ) - private def countTapsFromScan(scan: ScanAppBackendReference, tapAmount: BigDecimal) = { - listTransactionsFromScan(scan).count( - _.tap.map(a => BigDecimal(a.amuletAmount)).contains(tapAmount) - ) - } - - private def listTransactionsFromScan(scan: ScanAppBackendReference) = { - scan.listTransactions(None, TransactionHistoryRequest.SortOrder.Asc, 100) - } - private def getSequencerUrlSet( participantConnection: ParticipantClientReference, synchronizerAlias: SynchronizerAlias, diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanHistoryBackfillingIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanHistoryBackfillingIntegrationTest.scala index 7eb0263a7e..619432c55e 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanHistoryBackfillingIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanHistoryBackfillingIntegrationTest.scala @@ -1,6 +1,9 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.daml.ledger.javaapi.data.Transaction +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.* +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.actionrequiringconfirmation.* +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.dsorules_actionrequiringconfirmation.* import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{ ConfigurableApp, @@ -20,7 +23,11 @@ import org.lfdecentralizedtrust.splice.scan.automation.{ DeleteCorruptAcsSnapshotTrigger, ScanHistoryBackfillingTrigger, } -import org.lfdecentralizedtrust.splice.store.{PageLimit, TreeUpdateWithMigrationId} +import org.lfdecentralizedtrust.splice.store.{ + PageLimit, + TreeUpdateWithMigrationId, + VoteResultsFilters, +} import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.AdvanceOpenMiningRoundTrigger import org.lfdecentralizedtrust.splice.util.{EventId, UpdateHistoryTestUtil, WalletTestUtil} import com.digitalasset.canton.config.NonNegativeFiniteDuration @@ -29,12 +36,12 @@ import com.digitalasset.canton.data.CantonTimestamp import scala.math.BigDecimal.javaBigDecimal2bigDecimal import com.digitalasset.canton.{HasActorSystem, HasExecutionContext} import org.lfdecentralizedtrust.splice.automation.TxLogBackfillingTrigger -import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest.SortOrder import org.lfdecentralizedtrust.splice.scan.store.TxLogEntry import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.TxLogBackfillingState import org.lfdecentralizedtrust.splice.store.UpdateHistory.BackfillingState import org.scalactic.source.Position +import java.util.Optional import scala.annotation.nowarn import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* @@ -129,6 +136,39 @@ class ScanHistoryBackfillingIntegrationTest }, ) + // we create votes as they produce txlog entries that can be backfilled + actAndCheck( + "Create vote", { + val action: ActionRequiringConfirmation = + new ARC_DsoRules( + new SRARC_SetConfig( + new DsoRules_SetConfig( + sv1Backend + .getDsoInfo() + .dsoRules + .payload + .config, + Optional.empty(), + ) + ) + ) + + sv1Backend.createVoteRequest( + sv1Backend.getDsoInfo().svParty.toProtoPrimitive, + action, + "url", + "description", + sv1Backend.getDsoInfo().dsoRules.payload.config.voteRequestTimeout, + None, + ) + }, + )( + "Vote has been executed", + _ => { + sv1ScanBackend.listVoteRequestResults(VoteResultsFilters(), 100)._1 should have size (1) + }, + ) + // The current round, as seen by the given scan service (reflects the state of the scan app store) def currentRoundInScan(backend: ScanAppBackendReference): Long = backend.getLatestOpenMiningRound(CantonTimestamp.now()).contract.payload.round.number @@ -466,18 +506,9 @@ class ScanHistoryBackfillingIntegrationTest .loneElement shouldBe a[TxLogBackfillingTrigger.InitializeBackfillingTask] } - clue("TxLog based historical queries differ") { - val sv1Transactions = - sv1ScanBackend.listTransactions(None, SortOrder.Asc, 1000).map(shortDebugDescription) - val sv2Transactions = - sv2ScanBackend.listTransactions(None, SortOrder.Asc, 1000).map(shortDebugDescription) - - // We tapped 4 times before SV2 joined, and once after - sv1Transactions.size should be >= 5 withClue "SV1 txns" - sv2Transactions.size should be >= 1 withClue "SV2 txns" - sv1Transactions.size should be > sv2Transactions.size withClue "SV1 txns" - sv1Transactions should contain allElementsOf sv2Transactions withClue "sv1 transactions" - sv2Transactions should not contain sv1Transactions.headOption.value withClue "sv2 transactions" + clue("TxLog based vote result result differ") { + sv1ScanBackend.listVoteRequestResults(VoteResultsFilters(), 100)._1 should have size (1) + sv2ScanBackend.listVoteRequestResults(VoteResultsFilters(), 100)._1 should be(empty) } actAndCheck( @@ -524,14 +555,13 @@ class ScanHistoryBackfillingIntegrationTest }, ) - clue("TxLog based historical queries return same results") { - val sv1Transactions = - sv1ScanBackend.listTransactions(None, SortOrder.Asc, 1000).map(shortDebugDescription) - val sv2Transactions = - sv2ScanBackend.listTransactions(None, SortOrder.Asc, 1000).map(shortDebugDescription) - - // TODO(#666): switch to theSameElementsInOrderAs once the endpoint sorts by record time instead of row id. - sv1Transactions should contain theSameElementsAs sv2Transactions withClue "SV1/2 txns" + clue("TxLog based vote result queries return same results") { + // Not quite sure why we need the eventually given that we sync on backfilling completing but without that sv2ScanBackend can still return an empty list. + eventually() { + sv1ScanBackend.listVoteRequestResults(VoteResultsFilters(), 100)._1 shouldBe sv2ScanBackend + .listVoteRequestResults(VoteResultsFilters(), 100) + ._1 + } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala index 2bbbb82560..0bbcb18f67 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala @@ -3,9 +3,7 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.digitalasset.canton.concurrent.Threading import com.digitalasset.canton.config.NonNegativeFiniteDuration import com.digitalasset.canton.config.RequireTypes.NonNegativeInt -import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.topology.PartyId import org.apache.pekko.http.scaladsl.Http import org.apache.pekko.http.scaladsl.client.RequestBuilding.{Get, Post} import org.apache.pekko.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes} @@ -19,10 +17,6 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{ updateAutomationConfig, ConfigurableApp, } -import org.lfdecentralizedtrust.splice.http.v0.definitions.{ - TransactionHistoryRequest, - TransactionHistoryResponseItem, -} import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ IntegrationTestWithIsolatedEnvironment, @@ -82,9 +76,10 @@ class ScanIntegrationTest // used for the rate limit test rateLimiting = config.parameters.rateLimiting.copy( rateLimiters = - config.parameters.rateLimiting.rateLimiters + ("listAnsEntries" -> SpliceRateLimitConfig( - ratePerSecond = 5 - )) + config.parameters.rateLimiting.rateLimiters + ("listAnsEntries" -> SpliceRateLimitConfig + .WithPerClientIp( + ratePerSecond = 5 + )) ), ), ) @@ -166,107 +161,6 @@ class ScanIntegrationTest spliceInstanceNames.nameServiceNameAcronym should be("ANS") } - "list transaction pages in ascending and descending order" in { implicit env => - val aliceWalletUser = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) - def tapsForAlice = (t: TransactionHistoryResponseItem) => - t.tap.exists { tap => - PartyId.tryFromProtoPrimitive(tap.amuletOwner) == aliceWalletUser - } - - val nrTaps = 10 - val amuletAmounts = (1 to nrTaps).map(walletUsdToAmulet(_)) - val pageSize = nrTaps / 2 - // filtering for Alice to avoid interference by the top up taps - def collectAllTapPagesForAlice(sortOrder: TransactionHistoryRequest.SortOrder) = { - LazyList - .iterate(sv1ScanBackend.listTransactions(None, sortOrder, pageSize)) { page => - sv1ScanBackend.listTransactions(page.lastOption.map(_.eventId), sortOrder, pageSize) - } - .takeWhile(_.nonEmpty) - .foldLeft(Seq.empty[TransactionHistoryResponseItem])(_ ++ _) - .filter(tapsForAlice) - } - - def toAmuletAmounts(page: Seq[TransactionHistoryResponseItem]) = - page.flatMap(_.tap.map(t => BigDecimal(t.amuletAmount))) - - actAndCheck( - "Tap amulets for Alice", { - (1 to nrTaps).foreach { i => - aliceWalletClient.tap(BigDecimal(i)) - } - }, - )( - "Amulets should appear in Alice's wallet", - _ => { - aliceWalletClient.list().amulets should have length nrTaps.toLong - }, - ) - - eventually() { - val latestRound = - sv1ScanBackend.getLatestOpenMiningRound(CantonTimestamp.now()).contract.payload.round.number - val asc = TransactionHistoryRequest.SortOrder.Asc - val desc = TransactionHistoryRequest.SortOrder.Desc - val allPagesAsc = collectAllTapPagesForAlice(asc) - val allPagesDesc = collectAllTapPagesForAlice(desc) - allPagesAsc.map(_.round) should contain only Some( - latestRound - ) withClue "alice tap pages' rounds" - - val tapsFirstPageAscending = allPagesAsc.take(pageSize) - - toAmuletAmounts(tapsFirstPageAscending) should be( - amuletAmounts.take(pageSize) - ) - - val firstPageEndEventId = tapsFirstPageAscending.last.eventId - val tapsSecondPageAscending = allPagesAsc.slice(pageSize, pageSize + pageSize) - sv1ScanBackend - .listTransactions( - Some(firstPageEndEventId), - TransactionHistoryRequest.SortOrder.Asc, - pageSize.toInt, - ) - .filter(tapsForAlice) - - toAmuletAmounts(tapsSecondPageAscending) should be( - amuletAmounts.slice(pageSize, pageSize + pageSize) - ) - - sv1ScanBackend - .listTransactions( - Some(tapsSecondPageAscending.last.eventId), - asc, - pageSize.toInt, - ) - .filter(tapsForAlice) should be(empty) - - val tapsFirstPageDescending = allPagesDesc.take(pageSize) - toAmuletAmounts(tapsFirstPageDescending) should be( - amuletAmounts.reverse.take(pageSize) - ) - - val tapsSecondPageDescending = - allPagesDesc.slice(pageSize, pageSize + pageSize) - - sv1ScanBackend - .listTransactions( - Some(tapsSecondPageDescending.last.eventId), - TransactionHistoryRequest.SortOrder.Desc, - pageSize.toInt, - ) - .filter(tapsForAlice) should be(empty) - - toAmuletAmounts(tapsSecondPageDescending) should be( - amuletAmounts.reverse.slice(pageSize, pageSize + pageSize) - ) - toAmuletAmounts( - tapsFirstPageAscending ++ tapsSecondPageAscending - ) should be(toAmuletAmounts((tapsFirstPageDescending ++ tapsSecondPageDescending).reverse)) - } - } - "getUpdateHistory should return 400 for invalid after timestamp" in { implicit env => import env.{actorSystem, executionContext} registerHttpConnectionPoolsCleanup(env) @@ -300,10 +194,14 @@ class ScanIntegrationTest bftSequencers should have size 2 val expectedSequencerId = sv1Backend.appState.localSynchronizerNodes.current.sequencerAdminConnection.getSequencerId.futureValue - val currentSequencer = bftSequencers.find(_.url == "http://testUrl:8081").value - currentSequencer.id shouldBe expectedSequencerId - val legacySequencer = bftSequencers.find(_.url == "http://legacyUrl:8082").value - legacySequencer.id shouldBe expectedSequencerId + forExactly(1, bftSequencers) { sequencer => + sequencer.url shouldBe "http://testurl:8081" + sequencer.id shouldBe expectedSequencerId + } + forExactly(1, bftSequencers) { sequencer => + sequencer.url shouldBe "http://legacyurl:8082" + sequencer.id shouldBe expectedSequencerId + } } "respect rate limit" in { implicit env => @@ -337,7 +235,11 @@ class ScanIntegrationTest // then 5 every second // first second is 5 (full capacity) + 5 (capacity added after consumption) // then 5 every second - val maxAccepted = 30 + // The 50 calls are emitted at 10/s, so ~5s of refill gives 30 in the ideal case. Allow one + // more second of refill: throttle jitter or a slow first call stretches the window past 5s + // and lets a further batch through (seen accepting 31). This is still far below the 50 + // attempted, so the assertion keeps proving that the limiter rejects. + val maxAccepted = 35 // account for bursts in the stream used to rate limit the calls in `runRateLimited` val minAccepted = 10 results.count(identity) should (be >= minAccepted and be <= maxAccepted) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanTimeBasedIntegrationTest.scala index ea4371ab30..a38a4968a1 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanTimeBasedIntegrationTest.scala @@ -22,7 +22,7 @@ import org.lfdecentralizedtrust.splice.http.v0.definitions.DamlValueEncoding.mem import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTestWithIsolatedEnvironment import org.lfdecentralizedtrust.splice.scan.admin.http.CompactJsonScanHttpEncodings -import org.lfdecentralizedtrust.splice.scan.config.BulkStorageConfig +import org.lfdecentralizedtrust.splice.scan.config.{BulkStorageConfig, ScanStorageConfig} import org.lfdecentralizedtrust.splice.scan.config.ScanStorageConfigs.scanStorageConfigV1 import org.lfdecentralizedtrust.splice.store.{HasS3Mock, S3BucketConnectionForTests} import org.lfdecentralizedtrust.splice.store.UpdateHistory.BackfillingState @@ -71,6 +71,8 @@ class ScanTimeBasedIntegrationTest updatesPollingInterval = NonNegativeFiniteDuration.ofSeconds(5), staging = Some(s3ConfigMock("staging")), committed = Some(s3ConfigMock("committed")), + bftCheckEnabled = + false, // bft checks don't work with a single scan. The bft functionality is tested in the unit test. ), publicUrl = Some(Uri("http://foo.bar.com")), ) @@ -252,7 +254,7 @@ class ScanTimeBasedIntegrationTest .getDateOfFirstSnapshotAfter(CantonTimestamp.tryFromInstant(snapshot1.value.toInstant), 0) .value shouldBe snapshotAfter.value - val snapshotAfterData = sv1ScanBackend.getAcsSnapshotAtV1( + val snapshotAfterData = sv1ScanBackend.getAcsSnapshotAtV2( CantonTimestamp.assertFromInstant(snapshotAfter.value.toInstant), migrationId, templates = Some( @@ -269,10 +271,10 @@ class ScanTimeBasedIntegrationTest val atOrBefore = getLedgerTime // afOrBefore should return the same ACS snapshot as the exact time given by snapshotAfter - val snapshotAtOrBeforeAfterData = sv1ScanBackend.getAcsSnapshotAtV1( + val snapshotAtOrBeforeAfterData = sv1ScanBackend.getAcsSnapshotAtV2( CantonTimestamp.assertFromInstant(atOrBefore.toInstant), migrationId, - recordTimeMatch = Some(definitions.AcsRequest.RecordTimeMatch.AtOrBefore), + recordTimeMatch = Some(definitions.AcsRequestV2.RecordTimeMatch.AtOrBefore), templates = Some( Vector( PackageQualifiedName.fromJavaCodegenCompanion(Amulet.COMPANION), @@ -285,10 +287,10 @@ class ScanTimeBasedIntegrationTest snapshotAfterData shouldBe snapshotAtOrBeforeAfterData snapshotAtOrBeforeAfterData.value.recordTime shouldBe snapshotAfter.value - sv1ScanBackend.getAcsSnapshotAtV1( + sv1ScanBackend.getAcsSnapshotAtV2( CantonTimestamp.assertFromInstant(atOrBefore.toInstant), migrationId, - recordTimeMatch = Some(definitions.AcsRequest.RecordTimeMatch.Exact), + recordTimeMatch = Some(definitions.AcsRequestV2.RecordTimeMatch.Exact), templates = Some( Vector( PackageQualifiedName.fromJavaCodegenCompanion(Amulet.COMPANION), @@ -412,6 +414,10 @@ class ScanTimeBasedIntegrationTest migrationId, ownerPartyIds = Vector(aliceUserParty), recordTimeMatch = Some(definitions.HoldingsSummaryRequest.RecordTimeMatch.AtOrBefore), + // as_of_round defaults to the earliest open mining round at request time, and the + // advanceTime above lets the rounds advance, so pin it to the round the exact query + // resolved. Otherwise the holding fees derived from it differ by one round's worth. + asOfRound = holdingsSummary.map(_.computedAsOfRound), ) holdingsSummaryAtOrBefore shouldBe holdingsSummary @@ -442,7 +448,8 @@ class ScanTimeBasedIntegrationTest val endTime = getLedgerTime val lastMidnight = endTime.toInstant.truncatedTo(ChronoUnit.DAYS); val nextMidnight = lastMidnight.plus(1, ChronoUnit.DAYS) - val expectedAcsSnapshotKey = s"$lastMidnight~$nextMidnight/ACS_0.zstd" + val expectedAcsSnapshotKey = + s"$lastMidnight~$nextMidnight/${ScanStorageConfig.Encoding.CompactJson.storageKey("ACS", 0)}" val committedBucketConnection = new S3BucketConnectionForTests(s3ConfigMock("committed"), loggerFactory) @@ -470,12 +477,16 @@ class ScanTimeBasedIntegrationTest // at last midnight committedS3Objs .map(_.key()) - .filter(_.endsWith(s"~$lastMidnight/updates_0.zstd")) should not be empty + .filter( + _.endsWith( + s"~$lastMidnight/${ScanStorageConfig.Encoding.CompactJson.storageKey("updates", 0)}" + ) + ) should not be empty // Compare bulk storage data to hot storage data from scan // TODO(#4788): for now, bulk storage still uses v0, so we use that here as well val acsAtMidnightFromScan = sv1ScanBackend - .getAcsSnapshotAtV1(CantonTimestamp.assertFromInstant(lastMidnight), 0) + .getAcsSnapshotAtV2(CantonTimestamp.assertFromInstant(lastMidnight), 0) .value .createdEvents val acsObjUrl = getSnapshotResponse.objectRefs.head.url diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SplitwellFrontendIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SplitwellFrontendIntegrationTest.scala index ea28a43723..c257e5de95 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SplitwellFrontendIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SplitwellFrontendIntegrationTest.scala @@ -31,10 +31,6 @@ class SplitwellFrontendIntegrationTest EnvironmentDefinition .simpleTopology1Sv(this.getClass.getSimpleName) .withAdditionalSetup(implicit env => { - EnvironmentDefinition - .simpleTopology1Sv(this.getClass.getSimpleName) - .setup(env) - aliceValidatorBackend.participantClient.upload_dar_unless_exists(splitwellDarPath) bobValidatorBackend.participantClient.upload_dar_unless_exists(splitwellDarPath) }) @@ -178,11 +174,21 @@ class SplitwellFrontendIntegrationTest } } - eventually() { - // Check final amounts in the wallets - checkWallet(aliceUserParty, aliceWalletClient, Seq((400.0, 400))) - checkWallet(bobUserParty, bobWalletClient, Seq((39.0, 39.0))) - checkWallet(charlieUserParty, charlieWalletClient, Seq((111.0, 111.0))) + withClue("check final amounts in the wallets") { + val waitTime = 20.seconds + checkWallet( + aliceUserParty, + aliceWalletClient, + Seq((400.0, 400)), + timeUntilSuccess = waitTime, + ) + checkWallet(bobUserParty, bobWalletClient, Seq((39.0, 39.0)), timeUntilSuccess = waitTime) + checkWallet( + charlieUserParty, + charlieWalletClient, + Seq((111.0, 111.0)), + timeUntilSuccess = waitTime, + ) } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvFrontendIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvFrontendIntegrationTest.scala index c23c7114c3..20be650e0b 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvFrontendIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvFrontendIntegrationTest.scala @@ -22,6 +22,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.SpliceTestConsoleEnvironment +import org.lfdecentralizedtrust.splice.store.VoteResultsFilters import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.CloseVoteRequestTrigger import org.lfdecentralizedtrust.splice.util.SpliceUtil.defaultDsoRulesConfig import org.lfdecentralizedtrust.splice.util.* @@ -30,6 +31,7 @@ import org.openqa.selenium.support.ui.Select import org.slf4j.event.Level import scala.jdk.CollectionConverters.* +import scala.jdk.OptionConverters.* import java.util.Optional class SvFrontendIntegrationTest @@ -61,7 +63,7 @@ class SvFrontendIntegrationTest }, )( "logged in in the sv ui", - _ => find(id("app-title")).value.text should matchText("SUPER VALIDATOR OPERATIONS"), + _ => find(id("app-title")).value.text should matchText("Supervalidator Operations"), ) } } @@ -1417,9 +1419,10 @@ class SvFrontendIntegrationTest } } - "NEW UI: Grant and Revoke Featured App Right" in { implicit env => + "NEW UI: Grant, Update and Revoke Featured App Right" in { implicit env => val providerParty = sv3Backend.getDsoInfo().svParty val providerPartyId = providerParty.toProtoPrimitive + val activityWeight = BigDecimal("2.5") // First, create a Grant proposal for the provider. val grantProposalContractId = assertCreateProposal( @@ -1427,27 +1430,13 @@ class SvFrontendIntegrationTest "grant-featured-app", ) { implicit webDriver => fillOutTextField("grant-featured-app-idValue", providerPartyId) + fillOutTextField("grant-featured-app-activityWeight", activityWeight.toString) } clue("vote the grant request to execution before creating revoke request") { val grantTrackingCid = eventually() { - val voteRequest: Contract[VoteRequest.ContractId, VoteRequest] = sv1Backend - .listVoteRequests() - .find { request => - val requestCid = request.contractId.contractId - val trackingCid = - if (request.payload.trackingCid.isPresent) { - Some(request.payload.trackingCid.get.contractId) - } else { - None - } - requestCid == grantProposalContractId || trackingCid.contains(grantProposalContractId) - } - .getOrElse( - fail( - s"Could not find vote request for grant proposal contract id: $grantProposalContractId" - ) - ) + val voteRequest: Contract[VoteRequest.ContractId, VoteRequest] = + getVoteRequestForProposal(grantProposalContractId) if (voteRequest.payload.trackingCid.isPresent) voteRequest.payload.trackingCid.get else voteRequest.contractId @@ -1462,7 +1451,42 @@ class SvFrontendIntegrationTest } eventually() { - sv1ScanBackend.lookupFeaturedAppRight(providerParty) shouldBe a[Some[?]] + val featuredAppRight = sv1ScanBackend.lookupFeaturedAppRight(providerParty) + featuredAppRight shouldBe a[Some[?]] + featuredAppRight.value.payload.activityWeight.toScala.map( + BigDecimal(_) + ) shouldBe Some(activityWeight) + } + } + + val newActivityWeight = BigDecimal("3.0") + + val updateProposalContractId = assertCreateProposal( + "SRARC_UpdateFeaturedAppRight", + "update-featured-app", + ) { implicit webDriver => + fillOutTextField("update-featured-app-partyId", providerPartyId) + selectFirstMuiOption("update-featured-app-rightCid-dropdown") + fillOutTextField("update-featured-app-activityWeight", newActivityWeight.toString) + } + + clue("vote the update request to execution") { + val updateTrackingCid = eventually() { + val voteRequest = getVoteRequestForProposal(updateProposalContractId) + if (voteRequest.payload.trackingCid.isPresent) voteRequest.payload.trackingCid.get + else voteRequest.contractId + } + + eventuallySucceeds() { + sv3Backend.castVote(updateTrackingCid, isAccepted = true, "", "") + } + + eventually() { + val featuredAppRight = sv1ScanBackend.lookupFeaturedAppRight(providerParty) + featuredAppRight shouldBe a[Some[?]] + featuredAppRight.value.payload.activityWeight.toScala.map( + BigDecimal(_) + ) shouldBe Some(newActivityWeight) } } @@ -1553,7 +1577,7 @@ class SvFrontendIntegrationTest .listVoteRequests() .filter(_.payload.reason.body == "first request") shouldBe empty sv1Backend - .listVoteRequestResults(None, Some(false), None, None, None, 10) + .listVoteRequestResults(VoteResultsFilters(accepted = Some(false)), 10) ._1 .exists(_.request.reason.body == "first request") shouldBe true }, @@ -1591,7 +1615,7 @@ class SvFrontendIntegrationTest .listVoteRequests() .filter(_.payload.reason.body == "second request") shouldBe empty sv1Backend - .listVoteRequestResults(None, Some(false), None, None, None, 10) + .listVoteRequestResults(VoteResultsFilters(accepted = Some(false)), 10) ._1 .count(r => r.request.reason.body == "first request" || r.request.reason.body == "second request" @@ -1601,7 +1625,7 @@ class SvFrontendIntegrationTest // Verify ordering via backend API: most recently completed first clue("vote results are ordered by completion time descending") { - val (results, _) = sv1Backend.listVoteRequestResults(None, None, None, None, None, 10) + val (results, _) = sv1Backend.listVoteRequestResults(VoteResultsFilters(), 10) val ourResults = results.filter(r => r.request.reason.body == "first request" || r.request.reason.body == "second request" ) @@ -1613,13 +1637,13 @@ class SvFrontendIntegrationTest // Verify cursor-based pagination via backend API with limit=1 clue("pagination returns correct pages") { val (firstPage, firstPageToken) = - sv1Backend.listVoteRequestResults(None, None, None, None, None, 1) + sv1Backend.listVoteRequestResults(VoteResultsFilters(), 1) firstPage.size shouldBe 1 firstPage.head.request.reason.body shouldBe "second request" firstPageToken shouldBe defined val (secondPage, _) = - sv1Backend.listVoteRequestResults(None, None, None, None, None, 1, firstPageToken) + sv1Backend.listVoteRequestResults(VoteResultsFilters(), 1, firstPageToken) secondPage.size shouldBe 1 secondPage.head.request.reason.body shouldBe "first request" } @@ -1647,6 +1671,29 @@ class SvFrontendIntegrationTest } } + def getVoteRequestForProposal( + proposalContractId: String + )(implicit env: SpliceTestConsoleEnvironment) = { + val voteRequest: Contract[VoteRequest.ContractId, VoteRequest] = sv1Backend + .listVoteRequests() + .find { request => + val requestCid = request.contractId.contractId + val trackingCid = + if (request.payload.trackingCid.isPresent) { + Some(request.payload.trackingCid.get.contractId) + } else { + None + } + requestCid == proposalContractId || trackingCid.contains(proposalContractId) + } + .getOrElse( + fail( + s"Could not find vote request for proposal contract id: $proposalContractId" + ) + ) + voteRequest + } + def changeAction(actionName: String)(implicit webDriver: WebDriverType) = { eventually() { find( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingAddlIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingAddlIntegrationTest.scala index cfccc75f9c..bff3b008fc 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingAddlIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingAddlIntegrationTest.scala @@ -13,6 +13,7 @@ import org.lfdecentralizedtrust.splice.sv.util.{SvOnboardingToken, SvUtil} import scala.jdk.OptionConverters.* import org.lfdecentralizedtrust.splice.sv.admin.api.client.commands.HttpSvPublicAppClient.SvOnboardingStatus import org.lfdecentralizedtrust.splice.util.{SvTestUtil, WalletTestUtil} +import com.digitalasset.canton.console.CommandFailure import com.digitalasset.canton.logging.SuppressionRule import com.digitalasset.canton.topology.transaction.ParticipantPermission import org.slf4j.event.Level @@ -319,8 +320,7 @@ class SvOnboardingAddlIntegrationTest forAll(lines)(line => line.message should include("Unexpected amulet create event")) // Error emitted by every ScanTxLogParser plus the one UserWalletTxLogParser // associated with the owner of the coin. - lines should have size 2 withClue "ScanTxLogParser + UserWalletTxLogParser error" - forExactly(1, lines)(line => line.loggerName should include("sv1Scan")) + lines should have size 1 withClue "UserWalletTxLogParser error" forExactly(1, lines)(line => line.loggerName should include("sv1Validator")) }, ) @@ -350,7 +350,11 @@ class SvOnboardingAddlIntegrationTest } clue("create a amulet again with actAs = DSO") { withCommandRetryPolicy(_ => _ => false) { - assertThrowsAndLogsCommandFailures( + // Suppress at ERROR level only: background WARNs (e.g. the SV app failing to read the + // bft sequencers list while sv2's scan is unavailable) must not fail the log assertion. + loggerFactory.assertThrowsAndLogsSuppressing[CommandFailure]( + SuppressionRule.LevelAndAbove(Level.ERROR) + )( createAmulet( sv1ValidatorBackend.participantClientWithAdminToken, sv1UserId, diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingViaNonFoundingSvIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingViaNonFoundingSvIntegrationTest.scala index 52c94bd898..76029cc09f 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingViaNonFoundingSvIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingViaNonFoundingSvIntegrationTest.scala @@ -26,6 +26,8 @@ class SvOnboardingViaNonFoundingSvIntegrationTest with SvTestUtil with StandaloneCanton { + // sv1 is stopped mid-test, so neither history check is meaningful here. + override protected def runUpdateHistorySanityCheck: Boolean = false override protected def runEventHistorySanityCheck: Boolean = false override def dbsSuffix: String = "non_sv1_svs" @@ -87,6 +89,8 @@ class SvOnboardingViaNonFoundingSvIntegrationTest }(configuration) }) .withManualStart + // Prevent flakes where the topology transaction gets dropped from outbox after disconnect and we're not retrying + .withSvBftSequencerConnectionDisabled() "A new SV can: 1) onboard via a non-sv1 while sv1 is offboarded from the DSO and " + "2) bootstrap using a sequencer that is not sv1's sequencer" in { implicit env => @@ -139,12 +143,13 @@ class SvOnboardingViaNonFoundingSvIntegrationTest } } endpoints.toSet shouldBe Set( - LocalSynchronizerNode.toEndpoint( - sv1Backend.config.localSynchronizerNodes.current.sequencer.internalApi - ), + // SV BFT sequencer connections are disabled +// LocalSynchronizerNode.toEndpoint( +// sv1Backend.config.localSynchronizerNodes.current.sequencer.internalApi +// ), LocalSynchronizerNode.toEndpoint( sv2Backend.config.localSynchronizerNodes.current.sequencer.internalApi - ), + ) ) sv2Backend.participantClient.synchronizers.is_connected( decentralizedSynchronizerAlias diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala index dbc5676e16..b48d282e58 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala @@ -35,6 +35,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.amuletrules_ import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.SpliceTestConsoleEnvironment +import org.lfdecentralizedtrust.splice.store.VoteResultsFilters import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.CloseVoteRequestTrigger import org.lfdecentralizedtrust.splice.util.{Codec, TriggerTestUtil} @@ -119,10 +120,14 @@ class SvStateManagementIntegrationTest extends SvIntegrationTestBase with Trigge sv1Backend.listVoteRequests() shouldBe empty withClue "VoteRequests" sv1Backend - .listVoteRequestResults(None, Some(false), None, None, None, 1) + .listVoteRequestResults(VoteResultsFilters(accepted = Some(false)), 1) ._1 .loneElement .outcome shouldBe a[VRO_Rejected] + + sv1Backend.countVoteRequestResults( + VoteResultsFilters(accepted = Some(false)) + ) shouldBe 1L withClue "vote result count" }, ) } @@ -160,7 +165,7 @@ class SvStateManagementIntegrationTest extends SvIntegrationTestBase with Trigge _ => { sv1Backend.listVoteRequests() shouldBe empty withClue "VoteRequests" sv1Backend - .listVoteRequestResults(None, Some(false), None, None, None, 1) + .listVoteRequestResults(VoteResultsFilters(accepted = Some(false)), 1) ._1 .loneElement .outcome shouldBe a[VRO_Expired] @@ -201,7 +206,7 @@ class SvStateManagementIntegrationTest extends SvIntegrationTestBase with Trigge _ => { sv1Backend.listVoteRequests() shouldBe empty withClue "VoteRequests" sv1Backend - .listVoteRequestResults(None, Some(false), None, None, None, 1) + .listVoteRequestResults(VoteResultsFilters(accepted = Some(false)), 1) ._1 .loneElement .outcome shouldBe a[VRO_Rejected] @@ -586,7 +591,7 @@ class SvStateManagementIntegrationTest extends SvIntegrationTestBase with Trigge eventually() { val voteResult = sv1Backend - .listVoteRequestResults(None, Some(true), None, None, None, 1) + .listVoteRequestResults(VoteResultsFilters(accepted = Some(true)), 1) ._1 .headOption .value diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala index 6376cd4d42..28dc924e85 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala @@ -228,7 +228,10 @@ class SvTimeBasedOnboardingIntegrationTest }, ) - actAndCheck("one week has passed", advanceTime(JavaDuration.ofDays(8)))( + actAndCheck(timeUntilSuccess = 30.seconds)( + "one week has passed", + advanceTime(JavaDuration.ofDays(8)), + )( "the vote request is not displayed anymore", _ => { sv1Backend.listVoteRequests() shouldBe empty withClue "VoteRequests" diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedRewardCouponIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedRewardCouponIntegrationTest.scala index 71d16928a4..170b08b3fc 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedRewardCouponIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedRewardCouponIntegrationTest.scala @@ -6,7 +6,6 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{ ConfigurableApp, updateAutomationConfig, } -import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ IntegrationTest, @@ -221,38 +220,7 @@ class SvTimeBasedRewardCouponIntegrationTest ) } - clue("The claims appear in the scan history") { - eventually() { - val txs = sv1ScanBackend - .listTransactions( - None, - TransactionHistoryRequest.SortOrder.Desc, - Limit.DefaultMaxPageSize, - ) - .flatMap(_.transfer) - .filter(tf => - tf.sender.inputSvRewardAmount.nonEmpty && - Seq(sv1Party.toProtoPrimitive, aliceValidatorParty.toProtoPrimitive) - .contains(tf.sender.party) - ) - .map(tf => tf.sender.party -> tf.sender.inputSvRewardAmount.value) - .toMap - BigDecimal(txs(sv1Party.toProtoPrimitive)) should beWithin( - // The expected SV reward calculated here does not match exactly the reward calculated in daml, - // presumably because of rounding differences in the reward calculation. - BigDecimal(eachSvGetInRound0) - 0.001, - BigDecimal(eachSvGetInRound0) + 0.001, - ) - BigDecimal(txs(aliceValidatorParty.toProtoPrimitive)) should beWithin( - // The expected SV reward calculated here does not match exactly the reward calculated in daml, - // presumably because of rounding differences in the reward calculation. - BigDecimal(expectedAliceAmount) - 0.001, - BigDecimal(expectedAliceAmount) + 0.001, - ) - } - } - - clue("The claims appear in the wallet history") { + clue("The claims appear in the SV wallet history") { eventually() { val txs = withoutDevNetTopups( sv1WalletClient diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TestTokenV2SettlementIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TestTokenV2SettlementIntegrationTest.scala index d83ae7c2e9..8c99eb8ce3 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TestTokenV2SettlementIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TestTokenV2SettlementIntegrationTest.scala @@ -157,6 +157,34 @@ class TestTokenV2SettlementIntegrationTest def ttAdminValidator(implicit env: SpliceTestConsoleEnvironment) = v("testTokenValidatorLocal") + private def withValidatorsInitializedForTest[T]( + f: () => T + )(implicit env: SpliceTestConsoleEnvironment): T = { + sv1ValidatorBackend.startSync() // hosts DSO + Seq( + aliceValidatorBackend, // hosts Alice + bobValidatorBackend, // hosts Bob + splitwellValidatorBackend, // hosts the venue party + ttAdminValidator, // hosts the ttadmin + ).foreach { validatorBackend => + validatorBackend.startSync() + validatorBackend.participantClient.upload_dar_unless_exists(tokenStandardV2TestDarPath) + validatorBackend.participantClient + .upload_dar_unless_exists(testTokenV2DarPath) + } + try { + f() + } finally { + Seq( + sv1ValidatorBackend, + aliceValidatorBackend, + bobValidatorBackend, + splitwellValidatorBackend, + ttAdminValidator, + ).foreach(_.stop()) + } + } + "TestTokenV2 should be settleable" in { implicit env => initDso() withCanton( @@ -168,625 +196,640 @@ class TestTokenV2SettlementIntegrationTest "EXTRA_PARTICIPANT_ADMIN_USER" -> ttAdminValidator.config.ledgerApiUser, "EXTRA_PARTICIPANT_DB" -> dbName, ) { - sv1ValidatorBackend.startSync() // hosts DSO - Seq( - aliceValidatorBackend, // hosts Alice - bobValidatorBackend, // hosts Bob - splitwellValidatorBackend, // hosts the venue party - ttAdminValidator, // hosts the ttadmin - ).foreach { validatorBackend => - validatorBackend.startSync() - validatorBackend.participantClient.upload_dar_unless_exists(tokenStandardV2TestDarPath) - validatorBackend.participantClient - .upload_dar_unless_exists(testTokenV2DarPath) - } - val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) - val bobParty = onboardWalletUser(bobWalletClient, bobValidatorBackend) - val venueValidator = splitwellValidatorBackend - val venueParty = PartyId.tryFromProtoPrimitive(splitwellWalletClient.userStatus().party) - val ttAdminParty = ttAdminValidator.getValidatorPartyId() - val registry = new TestTokenV2Registry(ttAdminParty, ttAdminValidator) - - // Give alice some CC - aliceWalletClient.tap(1000) - val aliceCCBalanceBefore = eventually() { - val balance = aliceWalletClient.balance().unlockedQty - balance should be > BigDecimal(0) - balance - } + withValidatorsInitializedForTest { () => + val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) + val bobParty = onboardWalletUser(bobWalletClient, bobValidatorBackend) + val venueValidator = splitwellValidatorBackend + val venueParty = PartyId.tryFromProtoPrimitive(splitwellWalletClient.userStatus().party) + val ttAdminParty = ttAdminValidator.getValidatorPartyId() + val registry = new TestTokenV2Registry(ttAdminParty, ttAdminValidator) + + // Give alice some CC + aliceWalletClient.tap(1000) + val aliceCCBalanceBefore = eventually() { + val balance = aliceWalletClient.balance().unlockedQty + balance should be > BigDecimal(0) + balance + } - // make venue and ttadmin featured app parties - splitwellWalletClient.selfGrantFeaturedAppRight() - aliceValidatorWalletLocalClient.selfGrantFeaturedAppRight() - advanceRoundsByOneTickViaAutomation() - advanceRoundsByOneTickViaAutomation() - advanceRoundsByOneTickViaAutomation() - - // Create BatchingUtilityV2 contracts for Alice and Bob - val batchingUtilityIds: Map[PartyId, BatchingUtility.ContractId] = - Map(aliceValidatorBackend -> aliceParty, bobValidatorBackend -> bobParty).map { - case (validatorBackend, party) => - party -> validatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands - .submitWithResult( - userId = validatorBackend.config.ledgerApiUser, - actAs = Seq(party), - readAs = Seq(party), - update = BatchingUtilityV2.create(party.toProtoPrimitive), - ) - .contractId + // make venue and ttadmin featured app parties + splitwellWalletClient.selfGrantFeaturedAppRight() + aliceValidatorWalletLocalClient.selfGrantFeaturedAppRight() + // We need to make sure that after this block, the oldest active OpenMiningRound + // has an `openAt` time that is after the time the first OpenMiningRound was archived, + // otherwise the ingestion start approximation in + // `DbScanRewardsReferenceStore.lookupActiveOpenMiningRounds` might filter out the + // OpenMiningRound contract and AppActivityComputation won't ingest activity records. + clue("Advance rounds") { + advanceRoundsByOneTickViaAutomation() // archives round 0, open rounds are 1-3 + advanceRoundsByOneTickViaAutomation() // archives round 1, open rounds are 2-4 + advanceRoundsByOneTickViaAutomation() // archives round 2, open rounds are 3-5 + advanceRoundsByOneTickViaAutomation() // archives round 3, open rounds are 4-6 } - // Create TokenRules for ttadmin - val tokenRulesId = ttAdminValidator.participantClient.ledger_api_extensions.commands - .submitWithResult( - userId = ttAdminValidator.config.ledgerApiUser, - actAs = Seq(ttAdminParty), - readAs = Seq(ttAdminParty), - update = TokenV2Rules.create(ttAdminParty.toProtoPrimitive), - ) - .contractId - - // Call TokenRules_OfferMint to offer 100 USDC to Bob - val bobConfigAccount = new testtokenv2.accountconfig.AccountConfig( - ttAdminParty.toProtoPrimitive, - basicAccount(bobParty), - new testtokenv2.accountconfig.PartyConfig(true, true), - new testtokenv2.accountconfig.PartyConfig(false, false), - ) - val bobOfferMintAmount = 100 - ttAdminValidator.participantClient.ledger_api_extensions.commands - .submitJava( - userId = ttAdminValidator.config.ledgerApiUser, - actAs = Seq(ttAdminParty), - commands = tokenRulesId - .exerciseTokenRules_OfferMint( - basicAccount(bobParty), - BigDecimal(bobOfferMintAmount).bigDecimal, - new holdingv2.InstrumentId(ttAdminParty.toProtoPrimitive, "USDC"), - Instant.now(), - bobConfigAccount, - ) - .commands() - .asScala - .toSeq, - ) + // Create BatchingUtilityV2 contracts for Alice and Bob + val batchingUtilityIds: Map[PartyId, BatchingUtility.ContractId] = + Map(aliceValidatorBackend -> aliceParty, bobValidatorBackend -> bobParty).map { + case (validatorBackend, party) => + party -> validatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands + .submitWithResult( + userId = validatorBackend.config.ledgerApiUser, + actAs = Seq(party), + readAs = Seq(party), + update = BatchingUtilityV2.create(party.toProtoPrimitive), + ) + .contractId + } - // Bob accepts - val transferInstruction = - Contract - .fromCreatedEvent(transferinstructionv2.TransferInstruction.INTERFACE)( - CreatedEvent.fromProto( - createdEventToJavaProto( - bobValidatorBackend.participantClientWithAdminToken.ledger_api.state.acs - .of_party( - party = bobParty, - filterInterfaces = - Seq(transferinstructionv2.TransferInstruction.TEMPLATE_ID).map(templateId => - TemplateId( - templateId.getPackageId, - templateId.getModuleName, - templateId.getEntityName, - ) - ), - ) - .loneElement - .event - ) - ) + // Create TokenRules for ttadmin + val tokenRulesId = ttAdminValidator.participantClient.ledger_api_extensions.commands + .submitWithResult( + userId = ttAdminValidator.config.ledgerApiUser, + actAs = Seq(ttAdminParty), + readAs = Seq(ttAdminParty), + update = TokenV2Rules.create(ttAdminParty.toProtoPrimitive), ) - .valueOrFail("Failed to read transferinstructionv2.TransferInstruction") - val acceptContext = - registry.getContext( - transferInstruction.payload.transfer.inputHoldingCids.asScala.toSeq + .contractId + + // Call TokenRules_OfferMint to offer 100 USDC to Bob + val bobConfigAccount = new testtokenv2.accountconfig.AccountConfig( + ttAdminParty.toProtoPrimitive, + basicAccount(bobParty), + new testtokenv2.accountconfig.PartyConfig(true, true), + new testtokenv2.accountconfig.PartyConfig(false, false), ) - val transferResult = - bobValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands - .submitWithResult( - userId = bobValidatorBackend.config.ledgerApiUser, - actAs = Seq(bobParty), - readAs = Seq(bobParty), - update = transferInstruction.contractId.exerciseTransferInstruction_Accept( - java.util.List.of(bobParty.toProtoPrimitive), - new metadatav1.ExtraArgs(acceptContext.choiceContext, emptyMetadata), - ), - disclosedContracts = acceptContext.disclosedContracts, + val bobOfferMintAmount = 100 + ttAdminValidator.participantClient.ledger_api_extensions.commands + .submitJava( + userId = ttAdminValidator.config.ledgerApiUser, + actAs = Seq(ttAdminParty), + commands = tokenRulesId + .exerciseTokenRules_OfferMint( + basicAccount(bobParty), + BigDecimal(bobOfferMintAmount).bigDecimal, + new holdingv2.InstrumentId(ttAdminParty.toProtoPrimitive, "USDC"), + Instant.now(), + bobConfigAccount, + ) + .commands() + .asScala + .toSeq, ) - transferResult.exerciseResult.output match { - case completed: TransferInstructionResult_Completed => - completed.receiverHoldingCids.asScala.toSeq - case other => fail(s"Offer mint was not completed: $other") - } - // Venue creates the trade - val (createTradeTx, otcTrade) = actAndCheck( - "Venue creates OTC Trade", { - venueValidator.participantClientWithAdminToken.ledger_api_extensions.commands - .submitJava( - actAs = Seq(venueParty), - commands = new tradingappv2.OTCTrade( - venueParty.toProtoPrimitive, - Seq( - // Alice -> Bob: 100 CC - new tradingappv2.TradeLeg( - dsoParty.toProtoPrimitive, - new allocationv2.TransferLeg( - "alicetobob100CC", - basicAccount(aliceParty), - basicAccount(bobParty), - BigDecimal(100).bigDecimal, - amuletInstrumentIdName, - emptyMetadata, - ), - ), - // Bob -> Alice: 15 USDC - new tradingappv2.TradeLeg( - ttAdminParty.toProtoPrimitive, - new allocationv2.TransferLeg( - "bobtoalice15USDC", - basicAccount(bobParty), - basicAccount(aliceParty), - BigDecimal(15).bigDecimal, - usdcInstrumentName, - emptyMetadata, - ), - ), - // Alice -> Venue: 0.2 USDC - new tradingappv2.TradeLeg( - ttAdminParty.toProtoPrimitive, - new allocationv2.TransferLeg( - "alicetovenue0.2USDC", - basicAccount(aliceParty), - basicAccount(venueParty), - BigDecimal(0.2).bigDecimal, - usdcInstrumentName, - emptyMetadata, - ), - ), - ).asJava, - Instant.now(), - Instant.now().plusSeconds(60L), - java.util.Optional.of(Instant.now().plusSeconds(180L)), + // Bob accepts + val transferInstruction = eventually() { + Contract + .fromCreatedEvent(transferinstructionv2.TransferInstruction.INTERFACE)( + CreatedEvent.fromProto( + createdEventToJavaProto( + bobValidatorBackend.participantClientWithAdminToken.ledger_api.state.acs + .of_party( + party = bobParty, + filterInterfaces = + Seq(transferinstructionv2.TransferInstruction.TEMPLATE_ID).map(templateId => + TemplateId( + templateId.getPackageId, + templateId.getModuleName, + templateId.getEntityName, + ) + ), + ) + .loneElement + .event + ) ) - .create() - .commands() - .asScala - .toSeq, ) - }, - )( - "There exists a trade visible to the venue's participant", - _ => - venueValidator.participantClientWithAdminToken.ledger_api_extensions.acs - .awaitJava(tradingappv2.OTCTrade.COMPANION)( - venueParty - ), - ) + .valueOrFail("Failed to read transferinstructionv2.TransferInstruction") + } + val acceptContext = + registry.getContext( + transferInstruction.payload.transfer.inputHoldingCids.asScala.toSeq + ) + val transferResult = + bobValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands + .submitWithResult( + userId = bobValidatorBackend.config.ledgerApiUser, + actAs = Seq(bobParty), + readAs = Seq(bobParty), + update = transferInstruction.contractId.exerciseTransferInstruction_Accept( + java.util.List.of(bobParty.toProtoPrimitive), + new metadatav1.ExtraArgs(acceptContext.choiceContext, emptyMetadata), + ), + disclosedContracts = acceptContext.disclosedContracts, + ) + transferResult.exerciseResult.output match { + case completed: TransferInstructionResult_Completed => + completed.receiverHoldingCids.asScala.toSeq + case other => fail(s"Offer mint was not completed: $other") + } - val (createAllocationRequestsTx, (bobAllocationRequest, aliceAllocationRequest)) = - actAndCheck( - "Venue creates allocation requests", { + // Venue creates the trade + val (createTradeTx, otcTrade) = actAndCheck( + "Venue creates OTC Trade", { venueValidator.participantClientWithAdminToken.ledger_api_extensions.commands .submitJava( actAs = Seq(venueParty), - commands = otcTrade.id - .exerciseOTCTrade_RequestAllocations() + commands = new tradingappv2.OTCTrade( + venueParty.toProtoPrimitive, + Seq( + // Alice -> Bob: 100 CC + new tradingappv2.TradeLeg( + dsoParty.toProtoPrimitive, + new allocationv2.TransferLeg( + "alicetobob100CC", + basicAccount(aliceParty), + basicAccount(bobParty), + BigDecimal(100).bigDecimal, + amuletInstrumentIdName, + emptyMetadata, + ), + ), + // Bob -> Alice: 15 USDC + new tradingappv2.TradeLeg( + ttAdminParty.toProtoPrimitive, + new allocationv2.TransferLeg( + "bobtoalice15USDC", + basicAccount(bobParty), + basicAccount(aliceParty), + BigDecimal(15).bigDecimal, + usdcInstrumentName, + emptyMetadata, + ), + ), + // Alice -> Venue: 0.2 USDC + new tradingappv2.TradeLeg( + ttAdminParty.toProtoPrimitive, + new allocationv2.TransferLeg( + "alicetovenue0.2USDC", + basicAccount(aliceParty), + basicAccount(venueParty), + BigDecimal(0.2).bigDecimal, + usdcInstrumentName, + emptyMetadata, + ), + ), + ).asJava, + Instant.now(), + Instant.now().plusSeconds(60L), + java.util.Optional.of(Instant.now().plusSeconds(180L)), + ) + .create() .commands() .asScala .toSeq, ) }, )( - "Sender and receiver see the allocation requests", - _ => { - val bobAllocationRequest = inside( - bobWalletClient.listAllocationRequests() - ) { - case (allocationRequest: HttpWalletAppClient.TokenStandard.V2AllocationRequest) +: Nil => - allocationRequest - } - val aliceAllocationRequest = inside( - aliceWalletClient.listAllocationRequests() - ) { - case (allocationRequest: HttpWalletAppClient.TokenStandard.V2AllocationRequest) +: Nil => - allocationRequest - } - - (bobAllocationRequest, aliceAllocationRequest) - }, + "There exists a trade visible to the venue's participant", + _ => + venueValidator.participantClientWithAdminToken.ledger_api_extensions.acs + .awaitJava(tradingappv2.OTCTrade.COMPANION)( + venueParty + ), ) - val (aliceAllocationCids, aliceAllocateTx) = clue( - "Alice uses the BatchingUtilityV2 to create two allocations and accept the allocation request in a single tx" - ) { - val batchingUtility = batchingUtilityIds(aliceParty) - val aliceAmulets = aliceWalletClient - .list() - .amulets - .map(_.contract.contractId.toInterface(holdingv2.Holding.INTERFACE)) - val amuletSpec = aliceAllocationRequest.contract.payload.allocations.asScala - .filter(_.admin == dsoParty.toProtoPrimitive) - .loneElement - val usdcSpec = aliceAllocationRequest.contract.payload.allocations.asScala - .filter(_.admin == ttAdminParty.toProtoPrimitive) - .loneElement - val amuletAllocationFactory = sv1ScanBackend.getAllocationFactoryV2( - new allocationinstructionv2.AllocationFactory_Allocate( - aliceAllocationRequest.contract.payload.settlement, - amuletSpec, - aliceAllocationRequest.contract.payload.requestedAt, - aliceAmulets.asJava, - emptyExtraArgs, - java.util.List.of(aliceParty.toProtoPrimitive), - ) - ) - val usdcContext = registry.getContext(Seq.empty) - val aliceAllocateUpdate = batchingUtility - .exerciseBatchingUtility_ExecuteBatch( - new HoldingMap( - Map( - new ScopedAccount( - dsoParty.toProtoPrimitive, - basicAccount(aliceParty), - ) -> Map[String, java.util.List[holdingv2.Holding.ContractId]]( - amuletInstrumentIdName -> aliceAmulets.asJava - ).asJava, - new ScopedAccount( - ttAdminParty.toProtoPrimitive, - basicAccount(aliceParty), - ) -> Map - .empty[String, java.util.List[holdingv2.Holding.ContractId]] - .asJava, // alice has no USDC here yet - ).asJava - ), - java.util.List.of( - new TSA_AllocationFactory_AllocateV2( - new ChoiceCall[AllocationFactory_Allocate]( - new metadatav1.AnyContract.ContractId( - amuletAllocationFactory.factoryId.contractId - ), - amuletAllocationFactory.args, + val (createAllocationRequestsTx, (bobAllocationRequest, aliceAllocationRequest)) = + actAndCheck( + "Venue creates allocation requests", { + venueValidator.participantClientWithAdminToken.ledger_api_extensions.commands + .submitJava( + actAs = Seq(venueParty), + commands = otcTrade.id + .exerciseOTCTrade_RequestAllocations() + .commands() + .asScala + .toSeq, ) - ), - new TSA_AllocationFactory_AllocateV2( - new ChoiceCall[AllocationFactory_Allocate]( - new metadatav1.AnyContract.ContractId(tokenRulesId.contractId), - new allocationinstructionv2.AllocationFactory_Allocate( - aliceAllocationRequest.contract.payload.settlement, - usdcSpec, - aliceAllocationRequest.contract.payload.requestedAt, - java.util.List.of(), - new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), - java.util.List.of(aliceParty.toProtoPrimitive), + }, + )( + "Sender and receiver see the allocation requests", + _ => { + val bobAllocationRequest = inside( + bobWalletClient.listAllocationRequests() + ) { + case (allocationRequest: HttpWalletAppClient.TokenStandard.V2AllocationRequest) +: Nil => + allocationRequest + } + val aliceAllocationRequest = inside( + aliceWalletClient.listAllocationRequests() + ) { + case (allocationRequest: HttpWalletAppClient.TokenStandard.V2AllocationRequest) +: Nil => + allocationRequest + } + + (bobAllocationRequest, aliceAllocationRequest) + }, + ) + + val (aliceAllocationCids, aliceAllocateTx) = clue( + "Alice uses the BatchingUtilityV2 to create two allocations and accept the allocation request in a single tx" + ) { + // UpdateExternalPartyConfigStateTrigger might run concurrently and cause a LOCAL_VERDICT_INACTIVE_CONTRACTS + // because of the ExternalPartyConfigState being updated. + // In the real world, we expect the venue to also just retry re-fetching all contexts + eventuallySucceeds() { + val batchingUtility = batchingUtilityIds(aliceParty) + val aliceAmulets = aliceWalletClient + .list() + .amulets + .map(_.contract.contractId.toInterface(holdingv2.Holding.INTERFACE)) + val amuletSpec = aliceAllocationRequest.contract.payload.allocations.asScala + .filter(_.admin == dsoParty.toProtoPrimitive) + .loneElement + val usdcSpec = aliceAllocationRequest.contract.payload.allocations.asScala + .filter(_.admin == ttAdminParty.toProtoPrimitive) + .loneElement + val amuletAllocationFactory = sv1ScanBackend.getAllocationFactoryV2( + new allocationinstructionv2.AllocationFactory_Allocate( + aliceAllocationRequest.contract.payload.settlement, + amuletSpec, + aliceAllocationRequest.contract.payload.requestedAt, + aliceAmulets.asJava, + emptyExtraArgs, + java.util.List.of(aliceParty.toProtoPrimitive), + ) + ) + val usdcContext = registry.getContext(Seq.empty) + val aliceAllocateUpdate = batchingUtility + .exerciseBatchingUtility_ExecuteBatch( + new HoldingMap( + Map( + new ScopedAccount( + dsoParty.toProtoPrimitive, + basicAccount(aliceParty), + ) -> Map[String, java.util.List[holdingv2.Holding.ContractId]]( + amuletInstrumentIdName -> aliceAmulets.asJava + ).asJava, + new ScopedAccount( + ttAdminParty.toProtoPrimitive, + basicAccount(aliceParty), + ) -> Map + .empty[String, java.util.List[holdingv2.Holding.ContractId]] + .asJava, // alice has no USDC here yet + ).asJava + ), + java.util.List.of( + new TSA_AllocationFactory_AllocateV2( + new ChoiceCall[AllocationFactory_Allocate]( + new metadatav1.AnyContract.ContractId( + amuletAllocationFactory.factoryId.contractId + ), + amuletAllocationFactory.args, + ) ), - ) - ), - new TSA_AllocationRequest_AcceptV2( - new ChoiceCall[AllocationRequest_Accept]( - new metadatav1.AnyContract.ContractId( - aliceAllocationRequest.contract.contractId.contractId + new TSA_AllocationFactory_AllocateV2( + new ChoiceCall[AllocationFactory_Allocate]( + new metadatav1.AnyContract.ContractId(tokenRulesId.contractId), + new allocationinstructionv2.AllocationFactory_Allocate( + aliceAllocationRequest.contract.payload.settlement, + usdcSpec, + aliceAllocationRequest.contract.payload.requestedAt, + java.util.List.of(), + new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), + java.util.List.of(aliceParty.toProtoPrimitive), + ), + ) ), - new AllocationRequest_Accept( - java.util.List.of(aliceParty.toProtoPrimitive), - amuletAllocationFactory.args.extraArgs, + new TSA_AllocationRequest_AcceptV2( + new ChoiceCall[AllocationRequest_Accept]( + new metadatav1.AnyContract.ContractId( + aliceAllocationRequest.contract.contractId.contractId + ), + new AllocationRequest_Accept( + java.util.List.of(aliceParty.toProtoPrimitive), + amuletAllocationFactory.args.extraArgs, + ), + ) ), + ), + true, + ) + val aliceAllocateTx = + aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands + .submitJava( + userId = aliceValidatorBackend.config.ledgerApiUser, + actAs = Seq(aliceParty), + readAs = Seq(aliceParty), + commands = aliceAllocateUpdate.commands().asScala.toSeq, + disclosedContracts = + amuletAllocationFactory.disclosedContracts ++ usdcContext.disclosedContracts, ) - ), - ), - true, - ) - val aliceAllocateTx = - aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands - .submitJava( - userId = aliceValidatorBackend.config.ledgerApiUser, - actAs = Seq(aliceParty), - readAs = Seq(aliceParty), - commands = aliceAllocateUpdate.commands().asScala.toSeq, - disclosedContracts = - amuletAllocationFactory.disclosedContracts ++ usdcContext.disclosedContracts, - ) - val aliceAllocationCids = SpliceLedgerConnection - .decodeExerciseResult( - aliceAllocateUpdate, - aliceAllocateTx, - ) - .exerciseResult - .actionResults - .asScala - .map { - case _: TSAR_AllocationRequest_AcceptV2Result => None - case v: TSAR_AllocationInstructionResultV2 => - v.allocationInstructionResultValue.output match { - case completed: AllocationInstructionResult_Completed => - Some(completed.allocationCid) - case other => fail(s"Expected AllocationInstructionResult_Completed but got $other") + val aliceAllocationCids = SpliceLedgerConnection + .decodeExerciseResult( + aliceAllocateUpdate, + aliceAllocateTx, + ) + .exerciseResult + .actionResults + .asScala + .map { + case _: TSAR_AllocationRequest_AcceptV2Result => None + case v: TSAR_AllocationInstructionResultV2 => + v.allocationInstructionResultValue.output match { + case completed: AllocationInstructionResult_Completed => + Some(completed.allocationCid) + case other => + fail(s"Expected AllocationInstructionResult_Completed but got $other") + } + case other => + fail(s"Expected TSAR_AllocationResultV2 but got $other") } - case other => - fail(s"Expected TSAR_AllocationResultV2 but got $other") - } - .collect { case Some(cid) => cid } + .collect { case Some(cid) => cid } - (aliceAllocationCids, aliceAllocateTx) - } + (aliceAllocationCids, aliceAllocateTx) + } + } - val (bobAllocationCids, bobAllocateTx) = clue( - "Bob uses the BatchingUtilityV2 to accept the request and create two allocations in a single tx" - ) { - val batchingUtility = batchingUtilityIds(bobParty) - val amuletSpec = bobAllocationRequest.contract.payload.allocations.asScala - .filter(_.admin == dsoParty.toProtoPrimitive) - .loneElement - val usdcSpec = bobAllocationRequest.contract.payload.allocations.asScala - .filter(_.admin == ttAdminParty.toProtoPrimitive) - .loneElement - val amuletAllocationFactory = sv1ScanBackend.getAllocationFactoryV2( - new allocationinstructionv2.AllocationFactory_Allocate( - bobAllocationRequest.contract.payload.settlement, - amuletSpec, - bobAllocationRequest.contract.payload.requestedAt, - java.util.List.of(), // bob has no amulets - emptyExtraArgs, - java.util.List.of(bobParty.toProtoPrimitive), - ) - ) - val bobUsdcHoldings = getHoldings(bobParty, bobValidatorBackend) - .map(_.contractId) - .map(id => new holdingv2.Holding.ContractId(id)) - val usdcContext = registry.getContext( - bobUsdcHoldings - ) - val bobAllocateUpdate = batchingUtility - .exerciseBatchingUtility_ExecuteBatch( - new HoldingMap( - Map( - new ScopedAccount( - dsoParty.toProtoPrimitive, - basicAccount(bobParty), - ) -> Map[String, java.util.List[holdingv2.Holding.ContractId]]().asJava, - new ScopedAccount( - ttAdminParty.toProtoPrimitive, - basicAccount(bobParty), - ) -> Map[String, java.util.List[holdingv2.Holding.ContractId]]( - usdcInstrumentName -> bobUsdcHoldings.asJava - ).asJava, // alice has no USDC here yet - ).asJava - ), - java.util.List.of( - new TSA_AllocationFactory_AllocateV2( - new ChoiceCall[AllocationFactory_Allocate]( - new metadatav1.AnyContract.ContractId( - amuletAllocationFactory.factoryId.contractId - ), - amuletAllocationFactory.args, - ) - ), - new TSA_AllocationFactory_AllocateV2( - new ChoiceCall[AllocationFactory_Allocate]( - new metadatav1.AnyContract.ContractId(tokenRulesId.contractId), - new allocationinstructionv2.AllocationFactory_Allocate( - bobAllocationRequest.contract.payload.settlement, - usdcSpec, - bobAllocationRequest.contract.payload.requestedAt, - java.util.List.of(), - new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), - java.util.List.of(bobParty.toProtoPrimitive), + val (bobAllocationCids, bobAllocateTx) = clue( + "Bob uses the BatchingUtilityV2 to accept the request and create two allocations in a single tx" + ) { + // Same UpdateExternalPartyConfigStateTrigger/LOCAL_VERDICT_INACTIVE_CONTRACTS logic + // as with alice's usage of BatchingUtilityV2 above. + eventuallySucceeds() { + val batchingUtility = batchingUtilityIds(bobParty) + val amuletSpec = bobAllocationRequest.contract.payload.allocations.asScala + .filter(_.admin == dsoParty.toProtoPrimitive) + .loneElement + val usdcSpec = bobAllocationRequest.contract.payload.allocations.asScala + .filter(_.admin == ttAdminParty.toProtoPrimitive) + .loneElement + val amuletAllocationFactory = sv1ScanBackend.getAllocationFactoryV2( + new allocationinstructionv2.AllocationFactory_Allocate( + bobAllocationRequest.contract.payload.settlement, + amuletSpec, + bobAllocationRequest.contract.payload.requestedAt, + java.util.List.of(), // bob has no amulets + emptyExtraArgs, + java.util.List.of(bobParty.toProtoPrimitive), + ) + ) + val bobUsdcHoldings = getHoldings(bobParty, bobValidatorBackend) + .map(_.contractId) + .map(id => new holdingv2.Holding.ContractId(id)) + val usdcContext = registry.getContext( + bobUsdcHoldings + ) + val bobAllocateUpdate = batchingUtility + .exerciseBatchingUtility_ExecuteBatch( + new HoldingMap( + Map( + new ScopedAccount( + dsoParty.toProtoPrimitive, + basicAccount(bobParty), + ) -> Map[String, java.util.List[holdingv2.Holding.ContractId]]().asJava, + new ScopedAccount( + ttAdminParty.toProtoPrimitive, + basicAccount(bobParty), + ) -> Map[String, java.util.List[holdingv2.Holding.ContractId]]( + usdcInstrumentName -> bobUsdcHoldings.asJava + ).asJava, // alice has no USDC here yet + ).asJava + ), + java.util.List.of( + new TSA_AllocationFactory_AllocateV2( + new ChoiceCall[AllocationFactory_Allocate]( + new metadatav1.AnyContract.ContractId( + amuletAllocationFactory.factoryId.contractId + ), + amuletAllocationFactory.args, + ) ), - ) - ), - new TSA_AllocationRequest_AcceptV2( - new ChoiceCall[AllocationRequest_Accept]( - new metadatav1.AnyContract.ContractId( - bobAllocationRequest.contract.contractId.contractId + new TSA_AllocationFactory_AllocateV2( + new ChoiceCall[AllocationFactory_Allocate]( + new metadatav1.AnyContract.ContractId(tokenRulesId.contractId), + new allocationinstructionv2.AllocationFactory_Allocate( + bobAllocationRequest.contract.payload.settlement, + usdcSpec, + bobAllocationRequest.contract.payload.requestedAt, + java.util.List.of(), + new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), + java.util.List.of(bobParty.toProtoPrimitive), + ), + ) ), - new AllocationRequest_Accept( - java.util.List.of(bobParty.toProtoPrimitive), - new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), + new TSA_AllocationRequest_AcceptV2( + new ChoiceCall[AllocationRequest_Accept]( + new metadatav1.AnyContract.ContractId( + bobAllocationRequest.contract.contractId.contractId + ), + new AllocationRequest_Accept( + java.util.List.of(bobParty.toProtoPrimitive), + new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), + ), + ) ), + ), + true, + ) + val bobAllocateTx = + bobValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands + .submitJava( + userId = bobValidatorBackend.config.ledgerApiUser, + actAs = Seq(bobParty), + readAs = Seq(bobParty), + commands = bobAllocateUpdate.commands().asScala.toSeq, + disclosedContracts = + amuletAllocationFactory.disclosedContracts ++ usdcContext.disclosedContracts, ) - ), - ), - true, - ) - val bobAllocateTx = - bobValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands - .submitJava( - userId = bobValidatorBackend.config.ledgerApiUser, - actAs = Seq(bobParty), - readAs = Seq(bobParty), - commands = bobAllocateUpdate.commands().asScala.toSeq, - disclosedContracts = - amuletAllocationFactory.disclosedContracts ++ usdcContext.disclosedContracts, - ) - val bobAllocationCids = SpliceLedgerConnection - .decodeExerciseResult( - bobAllocateUpdate, - bobAllocateTx, - ) - .exerciseResult - .actionResults - .asScala - .map { - case _: TSAR_AllocationRequest_AcceptV2Result => None - case v: TSAR_AllocationInstructionResultV2 => - v.allocationInstructionResultValue.output match { - case completed: AllocationInstructionResult_Completed => - Some(completed.allocationCid) + val bobAllocationCids = SpliceLedgerConnection + .decodeExerciseResult( + bobAllocateUpdate, + bobAllocateTx, + ) + .exerciseResult + .actionResults + .asScala + .map { + case _: TSAR_AllocationRequest_AcceptV2Result => None + case v: TSAR_AllocationInstructionResultV2 => + v.allocationInstructionResultValue.output match { + case completed: AllocationInstructionResult_Completed => + Some(completed.allocationCid) + case other => + fail(s"Expected AllocationInstructionResult_Completed but got $other") + } case other => - fail(s"Expected AllocationInstructionResult_Completed but got $other") + fail(s"Expected TSAR_AllocationResultV2 but got $other") } - case other => - fail(s"Expected TSAR_AllocationResultV2 but got $other") - } - .collect { case Some(cid) => cid } - - (bobAllocationCids, bobAllocateTx) - } + .collect { case Some(cid) => cid } - val (settleTradeTx, _) = actAndCheck( - "Venue settles the trade", { - val allAllocations = { - venueValidator.participantClientWithAdminToken.ledger_api.state.acs.of_party( - party = venueParty, - filterInterfaces = Seq(allocationv2.Allocation.TEMPLATE_ID).map(templateId => - TemplateId( - templateId.getPackageId, - templateId.getModuleName, - templateId.getEntityName, - ) - ), - includeCreatedEventBlob = true, - ) + (bobAllocationCids, bobAllocateTx) } - // sanity check - (bobAllocationCids ++ aliceAllocationCids).foreach { cid => - allAllocations - .find(_.contractId == cid.contractId) - .valueOrFail(s"No allocation found for cid $cid") - } - val amuletAllocations = - allAllocations.filter(_.event.signatories.contains(dsoParty.toProtoPrimitive)) - val usdAllocations = - allAllocations.filter(_.event.signatories.contains(ttAdminParty.toProtoPrimitive)) - val settleBatch = new allocationv2.SettlementFactory_SettleBatch( - new allocationv2.SettlementInfo( - java.util.List.of(venueParty.toProtoPrimitive), - "OTCTrade", - java.util.Optional.of(new metadatav1.AnyContract.ContractId(otcTrade.id.contractId)), - emptyMetadata, - ), - transferLegsFromTrade(otcTrade).asJava, - allAllocations - .map(alloc => - new allocationv2.FinalizedAllocation( - new allocationv2.Allocation.ContractId(alloc.contractId), - java.util.List.of(), - java.util.Optional.empty[java.util.Map[String, java.math.BigDecimal]](), + } + + val (settleTradeTx, _) = actAndCheck( + "Venue settles the trade", { + // Same UpdateExternalPartyConfigStateTrigger/LOCAL_VERDICT_INACTIVE_CONTRACTS logic + // as with alice's usage of BatchingUtilityV2 above. + eventuallySucceeds() { + val allAllocations = { + venueValidator.participantClientWithAdminToken.ledger_api.state.acs.of_party( + party = venueParty, + filterInterfaces = Seq(allocationv2.Allocation.TEMPLATE_ID).map(templateId => + TemplateId( + templateId.getPackageId, + templateId.getModuleName, + templateId.getEntityName, + ) + ), + includeCreatedEventBlob = true, ) - ) - .asJava, - /*actors = */ java.util.List.of(venueParty.toProtoPrimitive), - emptyExtraArgs, - ) - val amuletContext = sv1ScanBackend.getSettlementFactoryV2(settleBatch) - val bobUsdcHoldings = getHoldings(bobParty, bobValidatorBackend) - .map(_.contractId) - .map(id => new holdingv2.Holding.ContractId(id)) - val usdcContext = registry.getContext( - bobUsdcHoldings - ) - venueValidator.participantClientWithAdminToken.ledger_api_extensions.commands - .submitJava( - actAs = Seq(venueParty), - commands = otcTrade.id - .exerciseOTCTrade_Settle( - Map[String, tradingappv2.SettlementBatch]( - dsoParty.toProtoPrimitive -> new SettlementBatchV2( - amuletAllocations - .map(alloc => new allocationv2.Allocation.ContractId(alloc.contractId)) - .asJava, + } + // sanity check + (bobAllocationCids ++ aliceAllocationCids).foreach { cid => + allAllocations + .find(_.contractId == cid.contractId) + .valueOrFail(s"No allocation found for cid $cid") + } + val amuletAllocations = + allAllocations.filter(_.event.signatories.contains(dsoParty.toProtoPrimitive)) + val usdAllocations = + allAllocations.filter(_.event.signatories.contains(ttAdminParty.toProtoPrimitive)) + val settleBatch = new allocationv2.SettlementFactory_SettleBatch( + new allocationv2.SettlementInfo( + java.util.List.of(venueParty.toProtoPrimitive), + "OTCTrade", + java.util.Optional + .of(new metadatav1.AnyContract.ContractId(otcTrade.id.contractId)), + emptyMetadata, + ), + transferLegsFromTrade(otcTrade).asJava, + allAllocations + .map(alloc => + new allocationv2.FinalizedAllocation( + new allocationv2.Allocation.ContractId(alloc.contractId), java.util.List.of(), - amuletContext.factoryId, - amuletContext.args.extraArgs, - ), - ttAdminParty.toProtoPrimitive -> new SettlementBatchV2( - usdAllocations - .map(alloc => new allocationv2.Allocation.ContractId(alloc.contractId)) - .asJava, - java.util.List.of( - new tradingappv2.MissingAllocation( - java.util.Optional.empty(), - tokenRulesId.toInterface( - allocationinstructionv2.AllocationFactory.INTERFACE - ), - new allocationinstructionv2.AllocationFactory_Allocate( - new allocationv2.SettlementInfo( - java.util.List.of(venueParty.toProtoPrimitive), - "OTCTradeProposal", - java.util.Optional.of( - new metadatav1.AnyContract.ContractId(otcTrade.id.contractId) + java.util.Optional.empty[java.util.Map[String, java.math.BigDecimal]](), + ) + ) + .asJava, + /*actors = */ java.util.List.of(venueParty.toProtoPrimitive), + emptyExtraArgs, + ) + val amuletContext = sv1ScanBackend.getSettlementFactoryV2(settleBatch) + val bobUsdcHoldings = getHoldings(bobParty, bobValidatorBackend) + .map(_.contractId) + .map(id => new holdingv2.Holding.ContractId(id)) + val usdcContext = registry.getContext( + bobUsdcHoldings + ) + venueValidator.participantClientWithAdminToken.ledger_api_extensions.commands + .submitJava( + actAs = Seq(venueParty), + commands = otcTrade.id + .exerciseOTCTrade_Settle( + Map[String, tradingappv2.SettlementBatch]( + dsoParty.toProtoPrimitive -> new SettlementBatchV2( + amuletAllocations + .map(alloc => new allocationv2.Allocation.ContractId(alloc.contractId)) + .asJava, + java.util.List.of(), + amuletContext.factoryId, + amuletContext.args.extraArgs, + ), + ttAdminParty.toProtoPrimitive -> new SettlementBatchV2( + usdAllocations + .map(alloc => new allocationv2.Allocation.ContractId(alloc.contractId)) + .asJava, + java.util.List.of( + new tradingappv2.MissingAllocation( + java.util.Optional.empty(), + tokenRulesId.toInterface( + allocationinstructionv2.AllocationFactory.INTERFACE ), - emptyMetadata, - ), - new allocationv2.AllocationSpecification( - ttAdminParty.toProtoPrimitive, - basicAccount(venueParty), - java.util.List.of( - new allocationv2.TransferLegSide( - "alicetovenue0.2USDC", - allocationv2.TransferSide.RECEIVERSIDE, - basicAccount(aliceParty), - BigDecimal(0.2).bigDecimal, - usdcInstrumentName, + new allocationinstructionv2.AllocationFactory_Allocate( + new allocationv2.SettlementInfo( + java.util.List.of(venueParty.toProtoPrimitive), + "OTCTradeProposal", + java.util.Optional.of( + new metadatav1.AnyContract.ContractId(otcTrade.id.contractId) + ), + emptyMetadata, + ), + new allocationv2.AllocationSpecification( + ttAdminParty.toProtoPrimitive, + basicAccount(venueParty), + java.util.List.of( + new allocationv2.TransferLegSide( + "alicetovenue0.2USDC", + allocationv2.TransferSide.RECEIVERSIDE, + basicAccount(aliceParty), + BigDecimal(0.2).bigDecimal, + usdcInstrumentName, + emptyMetadata, + ) + ), + java.util.Optional.empty(), + java.util.Optional.empty(), + false, emptyMetadata, - ) + ), + Instant.now(), + java.util.List.of(), + new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), + java.util.List.of(venueParty.toProtoPrimitive), ), - java.util.Optional.empty(), - java.util.Optional.empty(), - false, - emptyMetadata, - ), - Instant.now(), - java.util.List.of(), - new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), - java.util.List.of(venueParty.toProtoPrimitive), + ) ), - ) - ), - new allocationv2.SettlementFactory.ContractId(tokenRulesId.contractId), - new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), - ), - ).asJava, - java.util.List.of(), + new allocationv2.SettlementFactory.ContractId(tokenRulesId.contractId), + new metadatav1.ExtraArgs(usdcContext.choiceContext, emptyMetadata), + ), + ).asJava, + java.util.List.of(), + ) + .commands() + .asScala + .toSeq, + disclosedContracts = + usdcContext.disclosedContracts ++ amuletContext.disclosedContracts, ) - .commands() - .asScala - .toSeq, - disclosedContracts = - usdcContext.disclosedContracts ++ amuletContext.disclosedContracts, - ) - }, - )( - "The balances are updated", - _ => { - aliceWalletClient.balance().unlockedQty should be(aliceCCBalanceBefore - 100) - bobWalletClient.balance().unlockedQty should be(100) - - getUsdcBalance(bobParty, bobValidatorBackend) should be(bobOfferMintAmount - 15) - getUsdcBalance(aliceParty, aliceValidatorBackend) should be(15 - 0.2) - getUsdcBalance(venueParty, venueValidator) should be(0.2) - }, - ) + } + }, + )( + "The balances are updated", + _ => { + aliceWalletClient.balance().unlockedQty should be(aliceCCBalanceBefore - 100) + bobWalletClient.balance().unlockedQty should be(100) - val events = Seq( - createTradeTx -> "Create Trade", - createAllocationRequestsTx -> "Create Allocation Requests", - aliceAllocateTx -> "Alice Allocations", - bobAllocateTx -> "Bob Allocations", - settleTradeTx -> "Settle Trade", - ).map { case (tx, name) => - val updateId = tx.getUpdateId - name -> clue(s"Checking traffic & activity records for '$name'") { - eventually() { - inside(sv1ScanBackend.getEventById(updateId, None)) { - case Some( - item @ EventHistoryItem( - _, - Some(_), - Some(_), - Some(_), - ) - ) => - EventHistoryItem.encodeEventHistoryItem(item) + getUsdcBalance(bobParty, bobValidatorBackend) should be(bobOfferMintAmount - 15) + getUsdcBalance(aliceParty, aliceValidatorBackend) should be(15 - 0.2) + getUsdcBalance(venueParty, venueValidator) should be(0.2) + }, + ) + + val events = Seq( + createTradeTx -> "Create Trade", + createAllocationRequestsTx -> "Create Allocation Requests", + aliceAllocateTx -> "Alice Allocations", + bobAllocateTx -> "Bob Allocations", + settleTradeTx -> "Settle Trade", + ).map { case (tx, name) => + val updateId = tx.getUpdateId + name -> clue(s"Checking traffic & activity records for '$name'") { + eventually() { + inside(sv1ScanBackend.getEventById(updateId, None)) { + case Some( + item @ EventHistoryItem( + _, + Some(_), + Some(_), + Some(_), + ) + ) => + EventHistoryItem.encodeEventHistoryItem(item) + } } } } - } - val json = io.circe.JsonObject(events*) - val savePath = java.io.File.createTempFile("test_token_v2_settlement_results", ".json").toPath - Files.writeString(savePath, json.toJson.spaces2) + val json = io.circe.JsonObject(events*) + val savePath = + java.io.File.createTempFile("test_token_v2_settlement_results", ".json").toPath + Files.writeString(savePath, json.toJson.spaces2) - logger.info(s"Traffic & Activity Records results written to $savePath") + logger.info(s"Traffic & Activity Records results written to $savePath") + } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardCliTestDataTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardCliTestDataTimeBasedIntegrationTest.scala index a086e43182..bd8adda202 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardCliTestDataTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardCliTestDataTimeBasedIntegrationTest.scala @@ -144,7 +144,11 @@ class TokenStandardCliTestDataTimeBasedIntegrationTest updateAllScanAppConfigs_(config => config.copy(parameters = config.parameters.copy(rateLimiting = - RateLimitersConfig(SpliceRateLimitConfig(enabled = false, 1), Map.empty) + RateLimitersConfig( + default = SpliceRateLimitConfig.WithPerClientIp(enabled = false, 1), + rateLimiters = Map.empty, + global = SpliceRateLimitConfig.WithPerClientIp(enabled = false, 1), + ) ) ) )(config) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardTransferIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardTransferIntegrationTest.scala index 94e7fe219c..fb8da38f96 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardTransferIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardTransferIntegrationTest.scala @@ -1,7 +1,6 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.console.CommandFailure import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.{HasActorSystem, HasExecutionContext} import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.metadatav1 @@ -428,25 +427,28 @@ class TokenStandardTransferIntegrationTest trackingId, ) - assertThrows[CommandFailure]( - loggerFactory.assertLogs( - aliceWalletClient.createTokenStandardTransfer( - bobUserParty, - 10, - "not ok, resubmitted same trackingId so should be rejected", - expiration, - trackingId, - ), - _.errorMessage should include("Command submission already exists"), - ) + val createdCid = created.output match { + case members.TransferInstructionPending(value) => value.transferInstructionCid + case x => fail(s"Expected pending transfer, got $x") + } + + // Resubmitting the same trackingId is deduplicated idempotently: the accepted duplicate is + // recovered centrally and returns the original result instead of failing. + val resubmitted = aliceWalletClient.createTokenStandardTransfer( + bobUserParty, + 10, + "resubmitted with the same trackingId", + expiration, + trackingId, ) + inside(resubmitted.output) { case members.TransferInstructionPending(value) => + value.transferInstructionCid shouldBe createdCid + } + // Still exactly one transfer instruction, i.e. no duplicate was created. eventually() { inside(aliceWalletClient.listTokenStandardTransfers()) { case Seq(t) => - t.contractId.contractId should be(created.output match { - case members.TransferInstructionPending(value) => value.transferInstructionCid - case x => fail(s"Expected pending transfer, got $x") - }) + t.contractId.contractId should be(createdCid) } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardV2TransferIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardV2TransferIntegrationTest.scala index 385e16d5c7..574af2078b 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardV2TransferIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardV2TransferIntegrationTest.scala @@ -1,7 +1,6 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.console.CommandFailure import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.{HasActorSystem, HasExecutionContext} import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.transferinstructionv2.transferinstructionresult_output.TransferInstructionResult_Completed @@ -381,25 +380,28 @@ class TokenStandardV2TransferIntegrationTest trackingId, ) - assertThrows[CommandFailure]( - loggerFactory.assertLogs( - aliceWalletClient.createTokenStandardTransferV2( - bobUserParty, - 10, - "not ok, resubmitted same trackingId so should be rejected", - expiration, - trackingId, - ), - _.errorMessage should include("Command submission already exists"), - ) + val createdCid = created.output match { + case members.TransferInstructionPending(value) => value.transferInstructionCid + case x => fail(s"Expected pending transfer, got $x") + } + + // Resubmitting the same trackingId is deduplicated idempotently: the accepted duplicate is + // recovered centrally and returns the original result instead of failing. + val resubmitted = aliceWalletClient.createTokenStandardTransferV2( + bobUserParty, + 10, + "resubmitted with the same trackingId", + expiration, + trackingId, ) + inside(resubmitted.output) { case members.TransferInstructionPending(value) => + value.transferInstructionCid shouldBe createdCid + } + // Still exactly one transfer instruction, i.e. no duplicate was created. eventually() { inside(aliceWalletClient.listTokenStandardTransfers()) { case Seq(t) => - t.contractId.contractId should be(created.output match { - case members.TransferInstructionPending(value) => value.transferInstructionCid - case x => fail(s"Expected pending transfer, got $x") - }) + t.contractId.contractId should be(createdCid) } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsSvAppTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsSvAppTimeBasedIntegrationTest.scala index b88a5c992d..13a38c0fd0 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsSvAppTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsSvAppTimeBasedIntegrationTest.scala @@ -36,11 +36,13 @@ import org.lfdecentralizedtrust.splice.sv.automation.RewardMetricsTrigger import org.lfdecentralizedtrust.splice.sv.automation.confirmation.{ CalculateRewardsDryRunTrigger, CalculateRewardsTrigger, + SummarizingMiningRoundTrigger, } import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.{ ProcessRewardsDryRunTrigger, ProcessRewardsTrigger, } +import org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnection import org.lfdecentralizedtrust.splice.scan.automation.RewardComputationTrigger import org.lfdecentralizedtrust.splice.sv.config.InitialRewardConfig import org.lfdecentralizedtrust.splice.util.{ @@ -227,7 +229,7 @@ class TrafficBasedRewardsSvAppTimeBasedIntegrationTest clue("CalculateRewardsV2 contracts are also visible in scan rewards reference store") { eventually() { - val v2s = sv1ScanBackend.appState.rewardsReferenceStoreO.value + val v2s = sv1ScanBackend.appState.rewardsReferenceStore .listActiveCalculateRewardsV2() .futureValue v2s.map(c => @@ -352,6 +354,45 @@ class TrafficBasedRewardsSvAppTimeBasedIntegrationTest confirmMismatchingRootHashIsFlagged(bobParty) } + // sv2's CalculateRewardsTrigger and SummarizingMiningRoundTrigger report the + // scan URIs that formed the BFT consensus at INFO. This method captures the + // logs emitted while running the 'body' argument and asserts that sv2 + // obtained both the root-hash and the reward accounting totals for 'round' + // via BFT read from sv1 and sv4. + private def withExpectedRewardTriggersLogging[A](round: Long)( + body: => A + ): A = { + val bftReadLogs = + (SuppressionRule.forLogger[CalculateRewardsTrigger] || + SuppressionRule.forLogger[SummarizingMiningRoundTrigger]) && + SuppressionRule.LevelAndAbove(Level.INFO) + + loggerFactory.assertEventuallyLogsSeq(bftReadLogs)( + body, + logs => { + // sv3 is stopped and sv2's own scan is not part of its peer BFT connection, + // so only sv1's and sv4's scans can form the consensus. + val expectedScanUris = Set("http://localhost:5012", "http://localhost:5312") + def bftReadLogged(subject: String) = + forAtLeast(1, logs) { log => + val prefix = + s"Obtained the $subject for round $round via BFT read from scans: " + log.loggerName should include("SV=sv2") + log.message should include(prefix) + val scanUris = log.message + .substring(log.message.indexOf(prefix) + prefix.length) + .stripSuffix(".") + .split(", ") + .toSeq + scanUris.size should be(1) + forAll(scanUris)(uri => expectedScanUris should contain(uri)) + } + bftReadLogged("root-hash") + bftReadLogged("reward accounting totals") + }, + ) + } + private def metricValue( node: LocalInstanceReference, name: String, @@ -385,99 +426,101 @@ class TrafficBasedRewardsSvAppTimeBasedIntegrationTest // Pausing this ensures that the root-hash is not calculated while we advance round val sv2RewardComputation = sv2ScanBackend.automation.trigger[RewardComputationTrigger] - // Here we ensure that SV2 has done ingestion of app-activity for the round just closed - // But then its AppActivityRecordMetaT is bumped so that it cannot compute the - // root-hash for the round. - val (calculateRewardsCid, round) = setTriggersWithin( - triggersToPauseAtStart = Seq(sv2CalculateRewards, sv2RewardComputation) - ) { - val round = oldestOpenRound - doTransfer(bobParty) - // Note: we can't use advanceRoundsToNextRoundOpening here, as it blocks - // on summarizing and issuing round to complete, and here the - // summarizing round will block until the sv2 provides the round totals - // via bft read. - advanceTimeAndWaitForRoundOpening - - val (calculateRewardsCid, rootHash) = - clue( - s"Round $round just closed: its CalculateRewardsV2 exists and sv1 serves root-hash" - ) { - eventually() { - val calc = sv1Backend.appState.dsoStore - .listCalculateRewardsV2() - .futureValue - .filterNot(_.payload.dryRun) - .find(_.payload.round.number == round) - .value - val rootHash = inside(sv1ScanBackend.getRewardAccountingRootHash(round)) { - case GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk(h) => - h.rootHash + val round = oldestOpenRound + withExpectedRewardTriggersLogging(round) { + // Here we ensure that SV2 has done ingestion of app-activity for the round just closed + // But then its AppActivityRecordMetaT is bumped so that it cannot compute the + // root-hash for the round. + val calculateRewardsCid = setTriggersWithin( + triggersToPauseAtStart = Seq(sv2CalculateRewards, sv2RewardComputation) + ) { + doTransfer(bobParty) + // Note: we can't use advanceRoundsToNextRoundOpening here, as it blocks + // on summarizing and issuing round to complete, and here the + // summarizing round will block until the sv2 provides the round totals + // via bft read. + advanceTimeAndWaitForRoundOpening + + val (calculateRewardsCid, rootHash) = + clue( + s"Round $round just closed: its CalculateRewardsV2 exists and sv1 serves root-hash" + ) { + eventually() { + val calc = sv1Backend.appState.dsoStore + .listCalculateRewardsV2() + .futureValue + .filterNot(_.payload.dryRun) + .find(_.payload.round.number == round) + .value + val rootHash = inside(sv1ScanBackend.getRewardAccountingRootHash(round)) { + case GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk(h) => + h.rootHash + } + (calc.contractId, rootHash) } - (calc.contractId, rootHash) } - } - clue(s"Only sv1 and sv4 confirm round $round, so it is not yet processed") { - eventually() { - val startProcessingAction = new ARC_AmuletRules( - new CRARC_StartProcessingRewardsV2( - new AmuletRules_StartProcessingRewardsV2(calculateRewardsCid, new Hash(rootHash)) + clue(s"Only sv1 and sv4 confirm round $round, so it is not yet processed") { + eventually() { + val startProcessingAction = new ARC_AmuletRules( + new CRARC_StartProcessingRewardsV2( + new AmuletRules_StartProcessingRewardsV2(calculateRewardsCid, new Hash(rootHash)) + ) ) - ) + sv1Backend.appState.dsoStore + .listConfirmations(startProcessingAction) + .futureValue should have size 2 + } sv1Backend.appState.dsoStore - .listConfirmations(startProcessingAction) - .futureValue should have size 2 + .listOldestSummarizingMiningRounds() + .futureValue + .map(_.payload.round.number) should contain(round) } - sv1Backend.appState.dsoStore - .listOldestSummarizingMiningRounds() - .futureValue - .map(_.payload.round.number) should contain(round) - } - // This is trying to simulate AppActivityRecordMetaT's userVersion bump - // albeit in a direct way, to avoid restart of scan app, etc. - actAndCheck( - s"Reset sv2's earliest-ingested round to $round", { - val sv2Db = sv2ScanBackend.appState.storage match { - case db: DbStorage => db - case other => fail(s"Expected DbStorage") - } - implicit val closeContext: CloseContext = CloseContext(sv2Db) - sv2Db - .update_( - sqlu"""update app_activity_record_meta - set earliest_ingested_round = $round, - last_archived_round = null""", - "test.increaseAppActivityMeta_EarliestIngestedRound", - ) - .futureValueUS - }, - )( - s"sv2's own scan now answers CannotProvide for round $round", - _ => - sv2ScanBackend.getRewardAccountingRootHash(round) shouldBe - a[GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashCannotProvide], - ) + // This is trying to simulate AppActivityRecordMetaT's userVersion bump + // albeit in a direct way, to avoid restart of scan app, etc. + actAndCheck( + s"Reset sv2's earliest-ingested round to $round", { + val sv2Db = sv2ScanBackend.appState.storage match { + case db: DbStorage => db + case other => fail(s"Expected DbStorage") + } + implicit val closeContext: CloseContext = CloseContext(sv2Db) + sv2Db + .update_( + sqlu"""update app_activity_record_meta + set earliest_ingested_round = $round, + last_archived_round = null""", + "test.increaseAppActivityMeta_EarliestIngestedRound", + ) + .futureValueUS + }, + )( + s"sv2's own scan now answers CannotProvide for round $round", + _ => + sv2ScanBackend.getRewardAccountingRootHash(round) shouldBe + a[GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashCannotProvide], + ) - (calculateRewardsCid, round) - } + calculateRewardsCid + } - // setTriggersWithin has resumed sv2's CalculateRewardsTrigger. sv3 is stopped and sv2's own - // scan CannotProvide, so the deciding 3rd confirmation can only come from sv2 via bft read. - clue(s"sv2's own scan still answers CannotProvide for round $round") { - sv2ScanBackend.getRewardAccountingRootHash(round) shouldBe - a[GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashCannotProvide] - } + // setTriggersWithin has resumed sv2's CalculateRewardsTrigger. sv3 is stopped and sv2's own + // scan CannotProvide, so the deciding 3rd confirmation can only come from sv2 via bft read. + clue(s"sv2's own scan still answers CannotProvide for round $round") { + sv2ScanBackend.getRewardAccountingRootHash(round) shouldBe + a[GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashCannotProvide] + } - clue( - s"sv2 reads round $round from the sv1 and sv4, and supplies the 3rd confirmation vote" - ) { - eventually() { - sv1Backend.appState.dsoStore - .listCalculateRewardsV2() - .futureValue - .map(_.contractId) should not contain calculateRewardsCid + clue( + s"sv2 reads round $round from the sv1 and sv4, and supplies the 3rd confirmation vote" + ) { + eventually() { + sv1Backend.appState.dsoStore + .listCalculateRewardsV2() + .futureValue + .map(_.contractId) should not contain calculateRewardsCid + } } } @@ -515,15 +558,38 @@ class TrafficBasedRewardsSvAppTimeBasedIntegrationTest } } finally { otherProcessRewardsTriggers.foreach(_.resume()) - clue("Restart sv3") { - sv3ScanBackend.start() - sv3Backend.start() - sv3Backend.waitForInitialization( - timeout = NonNegativeDuration.tryFromDuration(120.seconds) - ) - sv3ScanBackend.waitForInitialization( - timeout = NonNegativeDuration.tryFromDuration(120.seconds) - ) + // On restart, sv3 catches up on the round that was processed while sv3 + // was down. The reward triggers may fire before that round + // advances. sv2's own scan still answers 'CannotProvide' for that round + // (its earliest-ingested round was bumped above), so it contributes an + // 'IgnoreResponse' to sv3's BFT reads, which 'BftScanConnection' logs at + // WARN as "The following Scan URLs disagreed with consensus". These WARNs + // are an expected consequence of the 'CannotProvide' scenario under test, + // so we suppress them (targeted to 'BftScanConnection' WARNs) to keep the + // `sbt checkErrors` log-scan gate green. + // + // The same supression happens in 'withExpectedRewardTriggersLogging' but + // + // 1. 'withExpectedRewardTriggersLogging' targets a narrow part of the try + // block and doesn't expand into this finally block, and + // 2. 'withExpectedRewardTriggersLogging' has strict expectation about the + // logs when rewards trigger fire. Here triggers may or may not fire + // -- it's a race between sv3 catching up and triggers firing. We + // can't guarantee that triggers fire => can't expect that WARNs will + // appear. So we just supress them instead of expecting them. + loggerFactory.suppress( + SuppressionRule.forLogger[BftScanConnection] && SuppressionRule.Level(Level.WARN) + ) { + clue("Restart sv3") { + sv3ScanBackend.start() + sv3Backend.start() + sv3Backend.waitForInitialization( + timeout = NonNegativeDuration.tryFromDuration(120.seconds) + ) + sv3ScanBackend.waitForInitialization( + timeout = NonNegativeDuration.tryFromDuration(120.seconds) + ) + } } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsTimeBasedIntegrationTest.scala index 48c7e1cda3..a283f2599a 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsTimeBasedIntegrationTest.scala @@ -39,6 +39,7 @@ import org.lfdecentralizedtrust.splice.sv.automation.confirmation.{ CalculateRewardsDryRunTrigger, } import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.ExpiredAmuletTransferInstructionTrigger +import org.lfdecentralizedtrust.splice.sv.automation.singlesv.ReceiveSvRewardCouponTrigger import org.lfdecentralizedtrust.splice.util.{ AmuletConfigUtil, ChoiceContextWithDisclosures, @@ -83,7 +84,7 @@ abstract class TrafficBasedRewardsTimeBasedIntegrationTestBase override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition - .simpleTopology4SvsWithSimTime(this.getClass.getSimpleName) + .simpleTopology1SvWithSimTime(this.getClass.getSimpleName) .withAdditionalSetup(implicit env => { Seq( sv1ValidatorBackend, @@ -117,6 +118,14 @@ abstract class TrafficBasedRewardsTimeBasedIntegrationTestBase .withPausedTrigger[CollectRewardsAndMergeAmuletsTrigger] )(config) ) + // Pause SV reward collection so that it does not race against + // advanceTimeAndWaitForRoundOpening in the activity block, + // which would cause "Skipped N SV rewards" warnings + .addConfigTransform((_, config) => + updateAutomationConfig(ConfigurableApp.Sv)( + _.withPausedTrigger[ReceiveSvRewardCouponTrigger] + )(config) + ) "CIP-104 reward accounting pipeline works" in { implicit env => val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) @@ -159,40 +168,9 @@ abstract class TrafficBasedRewardsTimeBasedIntegrationTestBase val calculateRewardsDryRunTriggers = activeSvs.map(_.dsoAutomation.trigger[CalculateRewardsDryRunTrigger]) - // 3 initial advances with CalculateRewardsTrigger paused but - // verdict ingestion active, so that the meta row is created and - // bootstrap rounds have activity data available. - setTriggersWithin(triggersToPauseAtStart = - calculateRewardsTriggers ++ calculateRewardsDryRunTriggers - ) { - for (round <- 1 to 3) { - advanceTimeAndWaitForRoundOpening - assertOldestOpenRound(round.toLong) - } - - clue("Bootstrap rounds have zero activity on firstSV (no featured apps yet)") { - assertZeroTotals(sv1ScanBackend, 0L to 2L) - } - - clue("All SVs report zero totals for rounds after bootstrap") { - Seq(sv1ScanBackend, sv2ScanBackend, sv3ScanBackend, sv4ScanBackend).foreach { scan => - assertZeroTotals(scan, 1L to 2L, timeout = 40.seconds) - } - } - } - - // Sequence of actions - // Open rounds | Action - // ------------+-------------------------------------- - // 3, 4 | settle id0, grant venue FAP - // 4, 5 | settle id1, grant alice FAP - // 5, 6 | settle id2, cancel venue FAP - // 6, 7 | settle id3, (total 2 DvP trades) - // 7, 8 | settle id4, (total 3 DvP trades) - // 8, 9 | no-activity - // 9, 10 | settle id5, 1 DvP + 3 direct trades - // 10, 11 | settle id6, (total 5 DvP trades) - // 11, 12 | settle id7, (round not closed) + // CalculateRewardsTrigger is paused for the entire test body so + // that we can confirm CalculateRewardsV2 contracts were created + // for each round before the triggers consume them. val ( updateId0, updateId1, @@ -204,10 +182,34 @@ abstract class TrafficBasedRewardsTimeBasedIntegrationTestBase aliceCreateId, svExpireId, ) = - pauseScanVerdictIngestionWithin(sv1ScanBackend) { - setTriggersWithin(triggersToPauseAtStart = - calculateRewardsTriggers ++ calculateRewardsDryRunTriggers - ) { + setTriggersWithin(triggersToPauseAtStart = + calculateRewardsTriggers ++ calculateRewardsDryRunTriggers + ) { + // 3 initial advances with verdict ingestion active, so that the + // meta row is created and bootstrap rounds have activity data + // available. + for (round <- 1 to 3) { + advanceRoundsToNextRoundOpening + assertOldestOpenRound(round.toLong) + } + + clue("Bootstrap rounds have zero activity on firstSV (no featured apps yet)") { + assertZeroTotals(sv1ScanBackend, 0L to 2L) + } + + // Sequence of actions + // Open rounds | Action + // ------------+-------------------------------------- + // 3, 4 | settle id0, grant venue FAP + // 4, 5 | settle id1, grant alice FAP + // 5, 6 | settle id2, cancel venue FAP + // 6, 7 | settle id3, (total 2 DvP trades) + // 7, 8 | settle id4, (total 3 DvP trades) + // 8, 9 | no-activity + // 9, 10 | settle id5, 1 DvP + 3 direct trades + // 10, 11 | settle id6, (total 5 DvP trades) + // 11, 12 | settle id7, (round not closed) + pauseScanVerdictIngestionWithin(sv1ScanBackend) { val id0 = settleTrade(aliceParty, bobParty, venueParty) grantFeaturedAppRight(splitwellWalletClient) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest.scala index 1c313bf995..613362b3e4 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest.scala @@ -28,12 +28,14 @@ import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ IntegrationTestWithIsolatedEnvironment, SpliceTestConsoleEnvironment, } -import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.ExpireRewardCouponV2Trigger +import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.{ + ExpireRewardCouponV2Trigger, + UnhideRewardCouponV2Trigger, +} import org.lfdecentralizedtrust.splice.sv.config.InitialRewardConfig import org.lfdecentralizedtrust.splice.util.{ ChoiceContextWithDisclosures, TimeTestUtil, - TriggerTestUtil, UploadablePackage, WalletTestUtil, } @@ -54,26 +56,36 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest extends IntegrationTestWithIsolatedEnvironment with HasExecutionContext with WalletTestUtil - with TriggerTestUtil with TimeTestUtil { - private val v2AmuletVersion = DarResources.amulet_0_1_19.metadata.version + // Version where V2 was introduced, or the current minimum initialization version if higher + private val minV2AmuletVersion = + Ordering[PackageVersion].max( + DarResources.amulet.minimumInitialization.metadata.version, + DarResources.amulet_0_1_19.metadata.version, + ) + + private val minV2AmuletPackageId = + DarResources.amulet.getPackageIdWithVersion(minV2AmuletVersion.toString).value - private val previousAmuletPackageId = - DarResources.amulet.others - .filter(_.metadata.version < v2AmuletVersion) - .maxBy(_.metadata.version) - .packageId + private val latestAmuletDar: DarResource = DarResources.amulet.latest private val v2CapableAmuletPackageIds: Seq[String] = DarResources.amulet.all - .filter(_.metadata.version >= v2AmuletVersion) + .filter(_.metadata.version >= minV2AmuletVersion) + .map(_.packageId) + .distinct + + private val amuletVersionsAboveOldestV2: Seq[String] = + DarResources.amulet.all + .filter(_.metadata.version > minV2AmuletVersion) .map(_.packageId) .distinct - // Set of packages alice must not vet to have wrong vetting state for v2 coupons - private val v2CapableDarsUnvettedOnAlice: Seq[DarResource] = { - val v2CapableAmuletIds = v2CapableAmuletPackageIds.toSet + // Only the latest is unvetted, as this would still cause + // ProcessRewardsTrigger to create hidden coupons + private val darsUnvettedOnAliceAtStart: Seq[DarResource] = { + val latestAmuletIds = Set(latestAmuletDar.packageId) Seq( DarResources.amulet, DarResources.amuletNameService, @@ -82,8 +94,8 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest DarResources.walletPayments, ).flatMap(_.all) .filter(d => - v2CapableAmuletIds.contains(d.packageId) || - d.dependencyPackageIds.exists(v2CapableAmuletIds.contains) + latestAmuletIds.contains(d.packageId) || + d.dependencyPackageIds.exists(latestAmuletIds.contains) ) .distinctBy(d => (d.metadata.name, d.metadata.version)) } @@ -99,7 +111,7 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest (aliceValidator -> config .validatorApps(aliceValidator) .copy( - additionalPackagesToUnvet = v2CapableDarsUnvettedOnAlice + additionalPackagesToUnvet = darsUnvettedOnAliceAtStart .groupBy(_.metadata.name) .map { case (name, resources) => name -> resources.map(_.metadata.version).toSet @@ -121,6 +133,11 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest _.withPausedTrigger[AcceptedTransferOfferTrigger] )(config) ) + .addConfigTransform((_, config) => + updateAutomationConfig(ConfigurableApp.Sv)( + _.withPausedTrigger[UnhideRewardCouponV2Trigger] + )(config) + ) .addConfigTransform((_, config) => ConfigTransforms.updateAllSvAppConfigs_(svConfig => svConfig.copy( @@ -135,7 +152,7 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest "Unhide and expire of RewardCouponV2" in { implicit env => val aliceParticipantId = aliceValidatorBackend.appState.participantAdminConnection.getParticipantId().futureValue - assertAliceVettedBelowV2(aliceParticipantId) + assertAliceVettedBelowLatest(aliceParticipantId) val (aliceParty, bobParty) = onboardAliceAndBobWithFeaturedRights() @@ -216,6 +233,10 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest clue("UnhideRewardCouponV2Trigger unhides Alice's coupons once she is re-vetted") { eventually() { + sv1Backend.dsoDelegateBasedAutomation + .trigger[UnhideRewardCouponV2Trigger] + .runOnce() + .futureValue val coupons = aliceCoupons coupons should not be empty coupons.filterNot(_.payload.providerIsObserver) shouldBe empty @@ -223,6 +244,112 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest } } + // Scenario for #6372, both providers have V2 capable versions vetted, but they lack a common vetted version. + clue( + "ProcessRewardsTrigger handles a batch where providers have jointly-incompatible vetting states" + ) { + val bobParticipantId = + bobValidatorBackend.appState.participantAdminConnection.getParticipantId().futureValue + + // Alice unvets minV2AmuletVersion; Bob keeps only minV2AmuletVersion, nothing after it. + actAndCheck( + s"Unvet $minV2AmuletPackageId on Alice and $amuletVersionsAboveOldestV2 on Bob", { + aliceValidatorBackend.participantClient.topology.vetted_packages.propose_delta( + aliceParticipantId, + removes = Seq(PackageId.assertFromString(minV2AmuletPackageId)), + force = ForceFlags(ForceFlag.AllowUnvettedDependencies), + store = TopologyStoreId.Synchronizer(decentralizedSynchronizerId), + ) + bobValidatorBackend.participantClient.topology.vetted_packages.propose_delta( + bobParticipantId, + removes = amuletVersionsAboveOldestV2.map(PackageId.assertFromString), + force = ForceFlags(ForceFlag.AllowUnvettedDependencies), + store = TopologyStoreId.Synchronizer(decentralizedSynchronizerId), + ) + }, + )( + "sv1's participant observes Alice no longer has minV2AmuletVersion vetted, and Bob's vetting is capped at minV2AmuletVersion", + _ => { + vettedPackagesOnSv1View(aliceParticipantId) should not contain + minV2AmuletPackageId + vettedPackagesOnSv1View(bobParticipantId) + .intersect(amuletVersionsAboveOldestV2) shouldBe empty + + val aliceVettedAboveMin = + vettedPackagesOnSv1View(aliceParticipantId).intersect(amuletVersionsAboveOldestV2) + val bobVettedAboveMin = + vettedPackagesOnSv1View(bobParticipantId).intersect(amuletVersionsAboveOldestV2) + aliceVettedAboveMin.intersect(bobVettedAboveMin) shouldBe empty + }, + ) + + val (round, _) = actAndCheck( + "Generate activity", { + doTransfer() + val round = oldestOpenRound + advanceRoundsToNextRoundOpening + round + }, + )( + "ProcessRewardsTrigger issues coupons for the round", + round => { + val newAliceCoupons = aliceCoupons.filter(_.payload.round.number == round) + val newBobCoupons = bobUnassignedCoupons.filter(_.payload.round.number == round) + newAliceCoupons should not be empty + newAliceCoupons.foreach(_.payload.providerIsObserver shouldBe true) + newBobCoupons should not be empty + newBobCoupons.foreach(_.payload.providerIsObserver shouldBe false) + }, + ) + + clue("UnhideRewardCouponV2Trigger can unhide Bob's coupon before he is fully re-vetted") { + eventually() { + sv1Backend.dsoDelegateBasedAutomation + .trigger[UnhideRewardCouponV2Trigger] + .runOnce() + .futureValue + val coupons = bobUnassignedCoupons.filter(_.payload.round.number == round) + coupons should not be empty + coupons.filterNot(_.payload.providerIsObserver) shouldBe empty + } + } + + clue("Restore Alice's and Bob's full vetting") { + actAndCheck( + s"Re-vet the minV2AmuletVersion package on Alice and $amuletVersionsAboveOldestV2 on Bob", { + aliceValidatorBackend.participantClient.topology.vetted_packages.propose_delta( + aliceParticipantId, + adds = Seq( + VettedPackage( + PackageId.assertFromString(minV2AmuletPackageId), + None, + None, + ) + ), + store = TopologyStoreId.Synchronizer(decentralizedSynchronizerId), + ) + bobValidatorBackend.participantClient.topology.vetted_packages.propose_delta( + bobParticipantId, + adds = amuletVersionsAboveOldestV2.map(id => + VettedPackage(PackageId.assertFromString(id), None, None) + ), + store = TopologyStoreId.Synchronizer(decentralizedSynchronizerId), + ) + }, + )( + "sv1's participant observes Alice and Bob are fully vetted again", + _ => { + v2CapableAmuletPackageIds.toSet.subsetOf( + vettedPackagesOnSv1View(aliceParticipantId).toSet + ) shouldBe true + v2CapableAmuletPackageIds.toSet.subsetOf( + vettedPackagesOnSv1View(bobParticipantId).toSet + ) shouldBe true + }, + ) + } + } + clue( "RewardCouponV2 can be assigned after vetting, even when beneficiary is offline" ) { @@ -344,22 +471,27 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest } } - private def aliceVettedPackagesOnSv1View( - aliceParticipantId: ParticipantId + private def vettedPackagesOnSv1View( + participantId: ParticipantId )(implicit env: SpliceTestConsoleEnvironment): Seq[String] = sv1ValidatorBackend.appState.participantAdminConnection - .listVettedPackages(aliceParticipantId, decentralizedSynchronizerId, AuthorizedState) + .listVettedPackages(participantId, decentralizedSynchronizerId, AuthorizedState) .futureValue .flatMap(_.mapping.packages.map(_.packageId)) - private def assertAliceVettedBelowV2( + private def assertAliceVettedBelowLatest( aliceParticipantId: ParticipantId )(implicit env: SpliceTestConsoleEnvironment): Unit = - clue("Alice's validator vets the highest amulet below the V2 but none at/above it") { + clue("Alice's validator vets the second-latest amulet version but not the latest") { eventually() { - val vetted = aliceVettedPackagesOnSv1View(aliceParticipantId) - vetted should contain(previousAmuletPackageId) - vetted.intersect(v2CapableAmuletPackageIds) shouldBe empty + val vetted = vettedPackagesOnSv1View(aliceParticipantId) + vetted should contain( + DarResources.amulet.others + .filter(_.metadata.version < latestAmuletDar.metadata.version) + .maxBy(_.metadata.version) + .packageId + ) + vetted should not contain latestAmuletDar.packageId } } @@ -402,18 +534,18 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest val aliceAdminConnection = aliceValidatorBackend.appState.participantAdminConnection aliceAdminConnection .uploadDarFiles( - v2CapableDarsUnvettedOnAlice.map(UploadablePackage.fromResource), + darsUnvettedOnAliceAtStart.map(UploadablePackage.fromResource), RetryFor.Automation, ) .futureValue aliceAdminConnection - .vetDars(decentralizedSynchronizerId, v2CapableDarsUnvettedOnAlice, None, None) + .vetDars(decentralizedSynchronizerId, darsUnvettedOnAliceAtStart, None, None) .futureValue }, )( "sv1's participant observes Alice has the correct vetting state for RewardAccountingV2", _ => - aliceVettedPackagesOnSv1View(aliceParticipantId) should contain( + vettedPackagesOnSv1View(aliceParticipantId) should contain( DarResources.amulet.latest.packageId ), ) @@ -432,7 +564,7 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest )( "sv1's participant observes Alice is in the wrong vetting state", _ => - aliceVettedPackagesOnSv1View(aliceParticipantId) + vettedPackagesOnSv1View(aliceParticipantId) .intersect(v2CapableAmuletPackageIds) shouldBe empty, ) @@ -454,9 +586,9 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest "sv1's participant observes Alice has the correct vetting state again", _ => { v2CapableAmuletPackageIds.toSet - .subsetOf(aliceVettedPackagesOnSv1View(aliceParticipantId).toSet) shouldBe true + .subsetOf(vettedPackagesOnSv1View(aliceParticipantId).toSet) shouldBe true aliceLedgerApiAmuletVersionOnSv1View(aliceParty).exists( - _ >= v2AmuletVersion + _ >= minV2AmuletVersion ) shouldBe true }, ) @@ -488,6 +620,11 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest } } + private def oldestOpenRound(implicit env: SpliceTestConsoleEnvironment): Long = { + val (openRounds, _) = sv1ScanBackend.getOpenAndIssuingMiningRounds() + openRounds.map(_.contract.payload.round.number.toLong).min + } + private def assertOldestOpenRound( expected: Long )(implicit env: SpliceTestConsoleEnvironment): Unit = { diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala index e981fbe357..d7f9b156ff 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala @@ -29,29 +29,52 @@ import org.lfdecentralizedtrust.splice.util.{ UploadablePackage, WalletTestUtil, } +import org.lfdecentralizedtrust.splice.config.ConfigTransforms.updateAllValidatorConfigs import org.lfdecentralizedtrust.splice.validator.automation.ValidatorPackageVettingTrigger import org.scalatest.concurrent.PatienceConfiguration -import scala.concurrent.duration.DurationInt +import scala.concurrent.duration.DurationInt import scala.concurrent.duration.FiniteDuration +import com.digitalasset.canton.logging.SuppressionRule +import org.lfdecentralizedtrust.splice.config.ConfigTransforms +import org.slf4j.event.Level +@org.lfdecentralizedtrust.splice.util.scalatesttags.NoDamlCompatibilityCheck class UnsupportedPackageVettingIntegrationTest extends IntegrationTest with PackageUnvettingUtil with AmuletConfigUtil with WalletTestUtil { + // Prevent failures due to: + // NO_VETTED_INTERFACE_IMPLEMENTATION_PACKAGE(9,f5ce331d): + // No vetted package for rendering the interface view for package-name 'splice-amulet' + override protected def runTokenStandardCliSanityCheck: Boolean = false + override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition .simpleTopology1Sv(this.getClass.getSimpleName) .withoutAliceValidatorConnectingToSplitwell // if other tests run before, packages that break this test might already be vetted .withNoVettedPackages(implicit env => env.validators.local.map(_.participantClient)) + .withReducedAmuletRulesCacheTTL() .addConfigTransforms((_, config) => updateAutomationConfig(ConfigurableApp.Sv)( _.withPausedTrigger[SvPackageVettingTrigger] )(config) ) + .addConfigTransforms((_, config) => + updateAllValidatorConfigs { case (name, c) => + if (name == "aliceValidator" || name == "bobValidator") { + c.copy( + automation = c.automation.withPausedTrigger[ValidatorPackageVettingTrigger] + ) + } else c + }(config) + ) + .addConfigTransforms((_, config) => + ConfigTransforms.useDecentralizedSynchronizerSplitwell()(config) + ) "Unsupported vetted packages are automatically removed by the package vetting trigger for SV and validator" in { implicit env => @@ -78,12 +101,11 @@ class UnsupportedPackageVettingIntegrationTest unsupportedDarsToVetSv, sv1Backend.dsoAutomation.trigger[SvPackageVettingTrigger], ) - // See https://github.com/DACH-NY/canton/issues/29834: set darsUnvettedByAutomation when unvetting works on non-sv validators test( aliceValidatorBackend.appState.participantAdminConnection, synchronizerId, unsupportedDarsToVetValidator, - Seq.empty, + unsupportedDarsToVetValidator, aliceValidatorBackend.validatorAutomation.trigger[ValidatorPackageVettingTrigger], ) } @@ -128,7 +150,7 @@ class UnsupportedPackageVettingIntegrationTest } } - "SVs unvet package versions above the configured PackageConfig, validators do not" in { + "SVs and validators unvet package versions above the configured PackageConfig" in { implicit env => val synchronizerId = sv1Backend.participantClient.synchronizers.list_connected().head.synchronizerId @@ -190,16 +212,82 @@ class UnsupportedPackageVettingIntegrationTest } } - clue("alice validator keeps package versions above the downgraded PackageConfig vetted") { + clue("alice validator unvets package versions above the downgraded PackageConfig") { eventually() { getVettedPackageIds( aliceValidatorBackend.appState.participantAdminConnection, synchronizerId, - ) should contain allElementsOf validatorDarsAbovePackageConfigVersion.map(_.packageId) + ) should contain noElementsOf validatorDarsAbovePackageConfigVersion.map(_.packageId) } eventually(40.seconds) { alicesTapsWithPackageId(DarResources.amulet_0_1_16.packageId) } } } + + "Unvetting amulet does not affect a validator that has splitwell depending on it" in { + implicit env => + val bobValidatorVettingTrigger = + bobValidatorBackend.validatorAutomation.trigger[ValidatorPackageVettingTrigger] + + val synchronizerId = + sv1Backend.participantClient.synchronizers.list_connected().head.synchronizerId + + val bobParticipant = bobValidatorBackend.appState.participantAdminConnection + val splitwellParticipant = splitwellValidatorBackend.appState.participantAdminConnection + + val splitwellDar = DarResources.splitwell_0_1_0 + val amuletDependency = DarResources.amulet_0_1_0 + + actAndCheck( + "bob and splitwell upload and vet splitwell-0.1.0 (which vets amulet-0.1.0 as a dependency)", { + val participants = Seq(bobParticipant, splitwellParticipant) + participants.foreach( + _.uploadDarFiles( + Seq(splitwellDar).map(UploadablePackage.fromResource), + RetryFor.Automation, + ).futureValue + ) + participants.foreach( + _.vetDars(synchronizerId, Seq(splitwellDar), None, None) + .futureValue(timeout = PatienceConfiguration.Timeout(FiniteDuration(40, "seconds"))) + ) + }, + )( + "both splitwell-0.1.0 and amulet-0.1.0 are vetted on bob's participant", + _ => { + val vettedIds = getVettedPackageIds(bobParticipant, synchronizerId) + vettedIds should contain(splitwellDar.packageId) + vettedIds should contain(amuletDependency.packageId) + }, + ) + + clue("amulet-0.1.0 is unvetted on bob") { + loggerFactory.assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.INFO))( + bobValidatorVettingTrigger.resume(), + entries => { + forAtLeast(1, entries)( + _.message should include regex "Success: dars .*48cac5ba4b6bf78df6c3a952ce05409a1d2ef39c05351074679adc0cf9cd1351.* are removed .*" + ) + }, + timeUntilSuccess = 40.seconds, + ) + } + + clue("splitwell-0.1.0 remains vetted after trigger ran") { + eventually() { + val vettedIds = getVettedPackageIds(bobParticipant, synchronizerId) + vettedIds should contain(splitwellDar.packageId) + vettedIds should not contain amuletDependency.packageId + } + } + + clue("splitwell is still usable on bob") { + onboardWalletUser(bobWalletClient, bobValidatorBackend) + eventually() { + bobSplitwellClient.createInstallRequests() should not be empty + } + } + + } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorReonboardingIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorReonboardingIntegrationTest.scala index 7d5251a6d6..821e3af09c 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorReonboardingIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorReonboardingIntegrationTest.scala @@ -274,7 +274,7 @@ class ValidatorReonboardingIntegrationTest extends ValidatorReonboardingIntegrat val lockedAmount = walletUsdToAmulet(BigDecimal(50)) actAndCheck( - "alice locks a amulet that both aliceParty and aliceValidatorWalletParty are stake holders", + "alice locks an amulet that has both aliceParty and aliceValidatorWalletParty as stakeholders", lockAmulets( aliceValidatorBackend, aliceParty, diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletBuyTrafficRequestIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletBuyTrafficRequestIntegrationTest.scala index b05a07e46e..412a5d8cb9 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletBuyTrafficRequestIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletBuyTrafficRequestIntegrationTest.scala @@ -307,6 +307,12 @@ class WalletBuyTrafficRequestIntegrationTest )) ), ) + // All 10 calls share the same tracking id, but the dedup recovery does not apply here: + // reading back an accepted duplicate needs a completed submission, which concurrent + // submissions do not have. So one call wins and the other 9 are rejected, mostly with + // SUBMISSION_ALREADY_IN_FLIGHT from the participant and some with 429 from the rate + // limiter. Duplicates of an already-completed submission are covered by the sequential + // dedup tests. successes shouldBe 1 } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletIntegrationTest.scala index f37c9c21d5..1c67b378f1 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletIntegrationTest.scala @@ -2,7 +2,6 @@ package org.lfdecentralizedtrust.splice.integration.tests import org.lfdecentralizedtrust.splice.auth.AuthUtil import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet as amuletCodegen -import org.lfdecentralizedtrust.splice.codegen.java.splice.types.Round import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.payment as walletCodegen import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.transferpreapproval.TransferPreapprovalProposal import org.lfdecentralizedtrust.splice.http.v0.definitions.TapRequest @@ -12,11 +11,7 @@ import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.BracketSynchronous.bracket import org.lfdecentralizedtrust.splice.integration.tests.WalletTxLogTestUtil import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.ContractState -import org.lfdecentralizedtrust.splice.util.{ - SpliceUtil, - WalletTestUtil, - JavaDecodeUtil as DecodeUtil, -} +import org.lfdecentralizedtrust.splice.util.{WalletTestUtil, JavaDecodeUtil as DecodeUtil} import org.lfdecentralizedtrust.splice.validator.automation.AcceptTransferPreapprovalProposalTrigger import org.lfdecentralizedtrust.splice.wallet.admin.api.client.commands.HttpWalletAppClient.CreateTransferPreapprovalResponse import org.lfdecentralizedtrust.splice.wallet.store.{ @@ -34,6 +29,8 @@ import com.digitalasset.canton.discard.Implicits.DiscardOps import org.apache.pekko.http.scaladsl.Http import org.apache.pekko.http.scaladsl.model.{HttpRequest, HttpResponse, StatusCodes} import org.apache.pekko.http.scaladsl.model.headers.{Authorization, OAuth2BearerToken} +import org.scalatest.concurrent.PatienceConfiguration +import org.scalatest.time.{Seconds, Span} import org.slf4j.event.Level import java.time.Duration @@ -58,41 +55,37 @@ class WalletIntegrationTest "A wallet" should { - // TODO (#2336): unignore this test - "tap stupid amount" ignore { implicit env => + val tapLimit = 100000000 + + s"tap $tapLimit amount" in { implicit env => import com.digitalasset.daml.lf.data.Numeric val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) val round = sv1ScanBackend.getLatestOpenMiningRound(env.environment.clock.now) val price = round.contract.payload.amuletPrice val decimalScale = Numeric.Scale.assertFromInt(10) - // We subtract one to allow some slack in back/forth conversions from CC to USD. Otherwise, - // the command gets rejected by the participant and we test nothing. - val maxDecimal = Numeric - .subtract(Numeric.maxValue(decimalScale), Numeric.assertFromBigDecimal(decimalScale, 1)) - .value val maxUsd = Numeric - .multiply(decimalScale, maxDecimal, Numeric.assertFromBigDecimal(decimalScale, price)) + .multiply( + decimalScale, + Numeric.assertFromBigDecimal(decimalScale, tapLimit), + Numeric.assertFromBigDecimal(decimalScale, price), + ) .value // Integration test that the tap goes through aliceWalletClient.tap(maxUsd) val amulet = aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs .filterJava(amuletCodegen.Amulet.COMPANION)(aliceParty, _ => true) .loneElement - // Unit test that expiry does the right thing - SpliceUtil.amuletExpiresAt(amulet.data) shouldBe new Round(Long.MaxValue) - // Test that the USD/CC conversions get us to the max Decimal value ignoring decimal points + // Test that the USD/CC conversions get us to the limit ignoring decimal points amulet.data.amount.initialAmount.setScale(0, java.math.RoundingMode.DOWN) shouldBe Numeric - .maxValue(decimalScale) + .assertFromBigDecimal(decimalScale, tapLimit) .setScale(0, java.math.RoundingMode.DOWN) } "tap deduplicates" in { implicit env => onboardWalletUser(aliceWalletClient, aliceValidatorBackend) aliceWalletClient.tap(50.0, Some("dedup-test")) - assertThrowsAndLogsCommandFailures( - aliceWalletClient.tap(50.0, Some("dedup-test")), - _.errorMessage should include("409 Conflict"), - ) + // Duplicate tap with the same command id returns the original result idempotently (200, not 409). + aliceWalletClient.tap(50.0, Some("dedup-test")) } "allow two wallet app users to connect to one wallet backend and tap" in { implicit env => @@ -225,9 +218,12 @@ class WalletIntegrationTest val tapsAfter = Range(0, 3).map(_ => Future(Try(aliceWalletClient.tap(10)))) - // Wait for all futures to complete - val successfulTaps = (tapsBefore ++ tapsAfter).map(_.futureValue).count(_.isSuccess) - if (failedAcceptF.futureValue.isSuccess) + // Wait for all futures to complete. The stale accept forces the treasury to filter + // and retry batches, so under load this can exceed the default patience. + val patience = PatienceConfiguration.Timeout(Span(60, Seconds)) + val successfulTaps = + (tapsBefore ++ tapsAfter).map(_.futureValue(patience)).count(_.isSuccess) + if (failedAcceptF.futureValue(patience).isSuccess) fail("The AcceptTransferOffer action unexpectedly succeeded") successfulTaps should be( @@ -543,10 +539,11 @@ class WalletIntegrationTest aliceWalletClient.balance().unlockedQty should be(40.0) }, ) - assertThrowsAndLogsCommandFailures( - bobWalletClient.transferPreapprovalSend(aliceUserParty, 40.0, deduplicationId), - _.errorMessage should include("409 Conflict"), - ) + // Duplicate send with same deduplication id returns the original result idempotently (200, not 409). + bobWalletClient.transferPreapprovalSend(aliceUserParty, 40.0, deduplicationId) + // Balance is unchanged — idempotent + bobWalletClient.balance().unlockedQty should be(60.0) + aliceWalletClient.balance().unlockedQty should be(40.0) clue("Preapproval sends work if provider has a featured app right") { // Feature alice validator to test a transfer with a featured preapproval provider diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletMintingDelegationTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletMintingDelegationTimeBasedIntegrationTest.scala index 1e25e56b30..8ea958b5c6 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletMintingDelegationTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletMintingDelegationTimeBasedIntegrationTest.scala @@ -51,6 +51,7 @@ class WalletMintingDelegationTimeBasedIntegrationTest // Pre-generate key pairs so external party IDs are known at config time private val sharingAppProvider = preGenerateExternalParty("sharing_app_provider") private val sharingRecipient = preGenerateExternalParty("sharing_recipient") + private val externalSharingProvider = preGenerateExternalParty("external_sharing_provider") // We create many coupons directly, so avoid running sanity checks override protected def runUpdateHistorySanityCheck: Boolean = false @@ -66,13 +67,14 @@ class WalletMintingDelegationTimeBasedIntegrationTest updateAllValidatorConfigs { case (name, c) => if (name == "aliceValidator") { c.copy( - rewardSharingConfigByParty = Map( - sharingAppProvider.partyId.toProtoPrimitive -> RewardSharingConfig( + rewardSharingConfigByParty = Map[String, RewardSharingConfig]( + sharingAppProvider.partyId.toProtoPrimitive -> RewardSharingConfig.BuiltIn( minTtlAfterSharing = NonNegativeFiniteDuration.ofHours(25), beneficiaries = Seq( AppRewardBeneficiaryConfig(sharingRecipient.partyId, BigDecimal(0.4)) ), - ) + ), + externalSharingProvider.partyId.toProtoPrimitive -> RewardSharingConfig.External(), ) ) } else c @@ -756,6 +758,106 @@ class WalletMintingDelegationTimeBasedIntegrationTest } } } + "mint already-assigned V2 coupons but hold back unassigned ones in external sharing mode" in { + implicit env => + val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) + aliceWalletClient.tap(100.0) + aliceValidatorWalletClient.tap(100.0) + + val externalSharingParty = + onboardExternalParty(aliceValidatorBackend, externalSharingProvider) + createAndAcceptExternalPartySetupProposal(aliceValidatorBackend, externalSharingParty) + + val expiresAt = env.environment.clock.now.plus(Duration.ofDays(30)).toInstant + val (_, proposalContractId) = actAndCheck( + "Create minting delegation proposal", + createMintingDelegationProposal(externalSharingParty, aliceParty, expiresAt), + )( + "Proposal is visible", + _ => { + val proposals = aliceWalletClient.listMintingDelegationProposals() + proposals.proposals should have size 1 withClue "proposals" + proposals.proposals.head.contract.contractId + }, + ) + + actAndCheck( + "Alice accepts the proposal", + aliceWalletClient.acceptMintingDelegationProposal(proposalContractId), + )( + "Delegation is created", + _ => { + val delegations = aliceWalletClient.listMintingDelegations() + delegations.delegations should have size 1 withClue "delegations" + }, + ) + + val unassignedAmount1 = BigDecimal(1000.0) + val unassignedAmount2 = BigDecimal(500.0) + val assignedAmount = BigDecimal(250.0) + + val externalPartyMintingDelegationTrigger = mintingDelegationCollectRewardsTrigger( + aliceValidatorBackend, + externalSharingParty.party, + ) + + val externalPartyWallet = aliceValidatorBackend.appState.walletManager + .valueOrFail("WalletManager is expected to be defined") + .externalPartyWalletManager + .lookupExternalPartyWallet(externalSharingParty.party) + .valueOrFail( + s"Expected ${externalSharingParty.party} to have an external party wallet" + ) + + // Pause the trigger, create two unassigned and one already-assigned V2 + // coupon, then resume. In external sharing mode the off-node automation + // owns beneficiary assignment, so the trigger must leave the unassigned + // coupons untouched while still minting the already-assigned coupon. + setTriggersWithin(triggersToPauseAtStart = Seq(externalPartyMintingDelegationTrigger)) { + actAndCheck( + "Create V2 coupons", + createRewardCouponsV2( + Seq( + (externalSharingParty.party, unassignedAmount1, None), + (externalSharingParty.party, unassignedAmount2, None), + (externalSharingParty.party, assignedAmount, Some(externalSharingParty.party)), + ) + ), + )( + "Coupons are visible in store", + _ => + externalPartyWallet.store.multiDomainAcsStore + .listContracts(RewardCouponV2.COMPANION) + .futureValue should have size 3, + ) + } + + clue("Assigned coupon is minted while unassigned coupons are held back untouched") { + eventually() { + val v2Coupons = externalPartyWallet.store.multiDomainAcsStore + .listContracts(RewardCouponV2.COMPANION) + .futureValue + v2Coupons.filter(_.payload.beneficiary.isEmpty) should have size 2 withClue + "external sharing mode must leave unassigned coupons untouched" + v2Coupons.filter(_.payload.beneficiary.isPresent) shouldBe + empty withClue "the already-assigned coupon must be minted and consumed" + } + } + + // Only the already-assigned coupon is minted; the two unassigned coupons + // are neither shared nor collected, so they do not contribute to the balance. + clue("Balance reflects only the directly minted assigned coupon") { + eventually() { + val balance = BigDecimal( + aliceValidatorBackend + .getExternalPartyBalance(externalSharingParty.party) + .totalUnlockedCoin + ) + balance shouldBe assignedAmount withClue + "external sharing mode mints only the already-assigned coupon, not the held-back unassigned ones" + } + } + } } private def collectRewardsAndMergeAmuletsTrigger( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletPaymentIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletPaymentIntegrationTest.scala index 40040b601d..8687bbf9e9 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletPaymentIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletPaymentIntegrationTest.scala @@ -252,6 +252,15 @@ class WalletPaymentIntegrationTest extends IntegrationTest with WalletTestUtil { trackingId, ) + // Wait for the offer to be ingested, so that the resubmission hits the trackingId check + // instead of racing it. A duplicate that loses the race is recovered from the ledger and + // returns the original offer rather than failing. + eventually() { + inside(aliceWalletClient.listTransferOffers()) { case Seq(t) => + t.contractId should be(offerId) + } + } + assertThrows[CommandFailure]( loggerFactory.assertLogs( aliceWalletClient.createTransferOffer( @@ -261,8 +270,8 @@ class WalletPaymentIntegrationTest extends IntegrationTest with WalletTestUtil { expiration, trackingId, ), - _.errorMessage should include("Command submission already exists").or( - include(s"Transfer offer with trackingId ${trackingId} already exists.") + _.errorMessage should include( + s"Transfer offer with trackingId ${trackingId} already exists." ), ) ) @@ -271,9 +280,6 @@ class WalletPaymentIntegrationTest extends IntegrationTest with WalletTestUtil { inside(aliceWalletClient.listTransferOffers()) { case Seq(t) => t.contractId should be(offerId) } - inside(aliceWalletClient.listTransferOffers()) { case Seq(t) => - t.contractId should be(offerId) - } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletRewardsTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletRewardsTimeBasedIntegrationTest.scala index 39852b9dde..5cabd4b0bf 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletRewardsTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletRewardsTimeBasedIntegrationTest.scala @@ -19,7 +19,10 @@ import org.lfdecentralizedtrust.splice.util.{ WalletTestUtil, } import org.lfdecentralizedtrust.splice.validator.automation.ReceiveFaucetCouponTrigger -import org.lfdecentralizedtrust.splice.wallet.automation.CollectRewardsAndMergeAmuletsTrigger +import org.lfdecentralizedtrust.splice.wallet.automation.{ + CollectRewardsAndMergeAmuletsTrigger, + RewardSharingTrigger, +} import org.lfdecentralizedtrust.splice.wallet.config.{ AppRewardBeneficiaryConfig, RewardSharingConfig, @@ -31,6 +34,11 @@ import scala.concurrent.duration.DurationInt * that the sharing trigger correctly assigns beneficiaries with the right * amounts (batching multiple coupons), that the minting trigger does not * re-assign unshared coupons, and that balances reflect the minted rewards. + * The test also verifies external sharing mode: a party configured with the + * External reward-sharing config gets no built-in sharing trigger, and its + * unassigned reward coupons are left untouched (neither shared nor collected). + * This confirms that built-in and external sharing modes co-exist in one + * environment without interfering. */ @org.lfdecentralizedtrust.splice.util.scalatesttags.SpliceAmulet_0_1_19 class WalletRewardsTimeBasedIntegrationTest @@ -54,12 +62,14 @@ class WalletRewardsTimeBasedIntegrationTest } val aliceValidatorPartyId = validatorPartyId("alice_validator_user", "aliceValidator") val bobValidatorPartyId = validatorPartyId("bob_validator_user", "bobValidator") + val splitwellValidatorPartyId = + validatorPartyId("splitwell_validator_user", "splitwellValidator") updateAllValidatorConfigs { case (name, c) => if (name == "aliceValidator") { // Alice shares 40% with bob; the implicit remainder (60%) goes to alice. c.copy( rewardSharingConfigByParty = Map( - aliceValidatorPartyId.toProtoPrimitive -> RewardSharingConfig( + aliceValidatorPartyId.toProtoPrimitive -> RewardSharingConfig.BuiltIn( minTtlAfterSharing = NonNegativeFiniteDuration.ofHours(30), beneficiaries = Seq( AppRewardBeneficiaryConfig(bobValidatorPartyId, BigDecimal(0.4)) @@ -67,6 +77,12 @@ class WalletRewardsTimeBasedIntegrationTest ) ) ) + } else if (name == "splitwellValidator") { + c.copy( + rewardSharingConfigByParty = Map( + splitwellValidatorPartyId.toProtoPrimitive -> RewardSharingConfig.External() + ) + ) } else c }(config) }) @@ -88,6 +104,24 @@ class WalletRewardsTimeBasedIntegrationTest waitForWalletUser(bobValidatorWalletClient) val aliceValidatorParty = aliceValidatorBackend.getValidatorPartyId() val bobValidatorParty = bobValidatorBackend.getValidatorPartyId() + val splitwellValidatorParty = splitwellValidatorBackend.getValidatorPartyId() + + clue( + "alice (built in) has sharing trigger; splitwell (external sharing automation) does not" + ) { + val aliceAutomation = aliceValidatorBackend + .userWalletAutomation(aliceValidatorWalletClient.config.ledgerApiUser) + .futureValue + aliceAutomation.triggers[RewardSharingTrigger] should not be empty + + eventually() { + val splitwellWallet = splitwellValidatorBackend.appState.walletManager + .valueOrFail("WalletManager is expected to be defined") + .lookupEndUserPartyWallet(splitwellValidatorParty) + .valueOrFail("Expected splitwell validator to have a wallet") + splitwellWallet.automation.triggers[RewardSharingTrigger] shouldBe empty + } + } // Tap amulet and do a transfer from alice to bob aliceWalletClient.tap(walletAmuletToUsd(50)) @@ -111,6 +145,7 @@ class WalletRewardsTimeBasedIntegrationTest val bobV2Amount = BigDecimal(1000.0) val aliceV2Amounts = Seq(BigDecimal(10.0), BigDecimal(5.0)) + val splitwellV2Amount = BigDecimal(7.0) val openRounds = eventually() { import math.Ordering.Implicits.* @@ -159,10 +194,11 @@ class WalletRewardsTimeBasedIntegrationTest // Bob (no sharing config) → his coupon stays unminted (trigger paused). // Alice (has sharing config, 2 coupons) → shared then minted, // exercising batching via additionalCoupons in AssignBeneficiaries. - clue("Create unassigned RewardCouponV2 for both validators") { + clue("Create unassigned RewardCouponV2 for all validators") { createRewardCouponsV2( Seq( - (bobValidatorParty, bobV2Amount, None) + (bobValidatorParty, bobV2Amount, None), + (splitwellValidatorParty, splitwellV2Amount, None), ) ++ aliceV2Amounts.map((aliceValidatorParty, _, None)) ) } @@ -225,6 +261,27 @@ class WalletRewardsTimeBasedIntegrationTest } } + clue("splitwell's external-sharing-mode coupon is neither shared nor collected") { + val splitwellWallet = splitwellValidatorBackend.appState.walletManager + .valueOrFail("WalletManager is expected to be defined") + .lookupEndUserPartyWallet(splitwellValidatorParty) + .valueOrFail("Expected splitwell validator to have a wallet") + eventually() { + val coupons = splitwellWallet.store.multiDomainAcsStore + .listContracts(RewardCouponV2.COMPANION) + .futureValue + .filter(_.payload.provider == splitwellValidatorParty.toProtoPrimitive) + + coupons should have size 1 withClue + "the single unassigned coupon must still be present" + + coupons.filter(_.payload.beneficiary.isPresent) shouldBe + empty withClue "external sharing mode must not assign beneficiaries" + + BigDecimal(coupons.head.payload.amount) shouldBe splitwellV2Amount + } + } + balance } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTimeBasedIntegrationTest.scala index ec75840912..d9bee318f0 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTimeBasedIntegrationTest.scala @@ -1,5 +1,7 @@ package org.lfdecentralizedtrust.splice.integration.tests +import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.transferpreapproval.TransferPreapprovalProposal +import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{ ConfigurableApp, updateAutomationConfig, @@ -10,6 +12,7 @@ import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.Integration import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.{ AnsSubscriptionRenewalPaymentTrigger, ExpiredLockedAmuletTrigger, + ExpireTransferPreapprovalsTrigger, } import org.lfdecentralizedtrust.splice.sv.automation.singlesv.ReceiveSvRewardCouponTrigger import org.lfdecentralizedtrust.splice.util.{ @@ -18,8 +21,13 @@ import org.lfdecentralizedtrust.splice.util.{ TriggerTestUtil, WalletTestUtil, } -import org.lfdecentralizedtrust.splice.validator.automation.ReceiveFaucetCouponTrigger +import org.lfdecentralizedtrust.splice.validator.automation.{ + ReceiveFaucetCouponTrigger, + RenewTransferPreapprovalTrigger, +} import org.lfdecentralizedtrust.splice.wallet.admin.api.client.commands.HttpWalletAppClient +import com.digitalasset.canton.config.NonNegativeFiniteDuration +import monocle.macros.syntax.lens.* import java.time.Duration @@ -30,6 +38,9 @@ class WalletTimeBasedIntegrationTest with SplitwellTestUtil with TriggerTestUtil { + // reduce for expiry test + private val preapprovalLifetime = NonNegativeFiniteDuration.ofMinutes(1) + override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition .simpleTopology1SvWithSimTime(this.getClass.getSimpleName) @@ -50,6 +61,11 @@ class WalletTimeBasedIntegrationTest _.withPausedTrigger[ReceiveSvRewardCouponTrigger] )(config) ) + .addConfigTransforms((_, config) => + ConfigTransforms.updateAllValidatorConfigs_( + _.focus(_.transferPreapproval.preapprovalLifetime).replace(preapprovalLifetime) + )(config) + ) "A wallet" should { @@ -220,6 +236,58 @@ class WalletTimeBasedIntegrationTest } } } + + "create a new TransferPreapproval if the existing one has expired" in { implicit env => + val aliceUserParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) + val aliceValidatorParty = aliceValidatorBackend.getValidatorPartyId() + + def alicePreapprovals = + aliceValidatorBackend + .listTransferPreapprovals() + .filter(_.payload.receiver == aliceUserParty.toProtoPrimitive) + + // disable renew and expiry automation so the expired one does not get archived. + setTriggersWithin( + triggersToPauseAtStart = Seq( + aliceValidatorBackend.validatorAutomation.trigger[RenewTransferPreapprovalTrigger] + ) ++ activeSvs.map(_.dsoDelegateBasedAutomation.trigger[ExpireTransferPreapprovalsTrigger]) + ) { + val initial = clue("Alice creates a TransferPreapproval") { + createTransferPreapprovalEnsuringItExists(aliceWalletClient, aliceValidatorBackend) + alicePreapprovals.loneElement + } + + clue("The TransferPreapproval expires without being renewed or archived") { + advanceTime(preapprovalLifetime.asJava.plusSeconds(1)) + val expired = alicePreapprovals.loneElement + expired.contract.contractId shouldBe initial.contract.contractId + expired.payload.expiresAt should be < getLedgerTime.toInstant + } + + actAndCheck( + "Alice creates another TransferPreapprovalProposal", + aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands + .submitWithResult( + userId = aliceValidatorBackend.config.ledgerApiUser, + actAs = Seq(aliceUserParty), + readAs = Seq(aliceUserParty), + update = TransferPreapprovalProposal.create( + aliceUserParty.toProtoPrimitive, + aliceValidatorParty.toProtoPrimitive, + java.util.Optional.of(dsoParty.toProtoPrimitive), + ), + ), + )( + "Validator automation creates a new TransferPreapproval", + _ => { + val fresh = alicePreapprovals + .filterNot(_.contract.contractId == initial.contract.contractId) + .loneElement + fresh.payload.expiresAt should be > getLedgerTime.toInstant + }, + ) + } + } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala index f88e98f41e..f7b02ba426 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala @@ -1388,10 +1388,10 @@ class WalletTxLogIntegrationTest logs => inside(logs) { case logLines if logLines.nonEmpty => - logLines - .filter(_.errorMessage contains ("RuntimeException")) - .foreach(_.errorMessage should include("Unexpected amulet create event")) - logLines should have size (env.scans.local.size.toLong + 1) // + 1 for UserWalletTxLog + forExactly(1, logLines) { line => + line.errorMessage should include("Unexpected amulet create event") + line.loggerName should include("DbMultiDomainAcsStore") + } }, ) @@ -1407,10 +1407,10 @@ class WalletTxLogIntegrationTest logs => inside(logs) { case logLines if logLines.nonEmpty => - logLines - .filter(_.errorMessage contains ("RuntimeException")) - .foreach(_.errorMessage should include("Unexpected amulet archive event")) - logLines should have size (env.scans.local.size.toLong + 1) // + 1 for UserWalletTxLog + forExactly(1, logLines) { line => + line.errorMessage should include("Unexpected amulet archive event") + line.loggerName should include("DbMultiDomainAcsStore") + } }, ) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/RunbookSvPreflightIntegrationTestBase.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/RunbookSvPreflightIntegrationTestBase.scala index 9425e906d0..3ff56d669b 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/RunbookSvPreflightIntegrationTestBase.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/RunbookSvPreflightIntegrationTestBase.scala @@ -52,40 +52,6 @@ abstract class RunbookSvPreflightIntegrationTestBase } } - "CometBFT is working" in { _ => - val svUiUrl = s"https://sv.sv.${sys.env("NETWORK_APPS_ADDRESS")}/"; - - withFrontEnd("sv") { implicit webDriver => - actAndCheck( - s"Logging in to SV UI at: ${svUiUrl}", { - completeAuth0LoginWithAuthorization( - svUiUrl, - svUsername, - svPassword, - () => find(id("logout-button")) should not be empty withClue "'Logout' button", - ) - - eventuallyClickOn(id("information-tab-cometBft-debug")) - }, - )( - s"We see all other SVs as peers", - _ => { - inside(find(id("comet-bft-debug-network"))) { case Some(e) => - if (isDevNet) { - forAll(Range(1, 5)) { _ => - e.text should include(s"\"moniker\": \"${getSvName(1)}\"") - } - } else { - forAll(Range(1, 2)) { _ => - e.text should include(s"\"moniker\": \"Digital-Asset-2\"") - } - } - } - }, - ) - } - } - "The SV can log in to their wallet" in { implicit env => withFrontEnd("sv") { implicit webDriver => actAndCheck( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/SvNonDevNetPreflightintegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/SvNonDevNetPreflightintegrationTest.scala index 8938f7637a..3dfd7ac233 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/SvNonDevNetPreflightintegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/SvNonDevNetPreflightintegrationTest.scala @@ -109,10 +109,6 @@ abstract class SvNonDevNetPreflightIntegrationTestBase } } - "Check health status of sv cometBft node" in { implicit env => - svClient.cometBftNodeStatus().catchingUp shouldBe false - } - "Check that there is a recent participant identities backup on GCP" in { _ => testRecentParticipantIdentitiesDump(svNamespace, IdentityDump) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/performance/tests/BaseStorePerformanceTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/performance/tests/BaseStorePerformanceTest.scala index 95a5fec347..1814c36e31 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/performance/tests/BaseStorePerformanceTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/performance/tests/BaseStorePerformanceTest.scala @@ -117,14 +117,11 @@ abstract class BaseStorePerformanceTest( case Left(err) => throw new RuntimeException(s"Failed to create storage: $err") } - /** Suppress Flyway ClassPathScanner warnings about unloadable test jars (apps-app_2.13-0.1.0-SNAPSHOT-tests.jar) - * TODO(#4790): This is a temporary workaround, w/o adding ignored logs. - */ - org.slf4j.LoggerFactory - .getLogger("org.flywaydb.core.internal.scanner.classpath.ClassPathScanner") - .asInstanceOf[ch.qos.logback.classic.Logger] - .setLevel(ch.qos.logback.classic.Level.ERROR) - + // Running unforked puts sbt's test jar on the classpath Flyway scans. + // So, unlike in forked approach, Flyway finds the jar but can't read it. + // Flyway skips it with a WARN. "Skipping unloadable jar file: ...apps-app_*-tests.jar" WARN. + // That log line is handled via an ignore pattern in project/ignore-patterns/canton_network_test_log.ignore.txt + // Migrations aren't affected — the actual migration files are read from the main resources. new DbMigrations(storage.dbConfig, false, timeouts, loggerFactory) .migrateDatabase() .map(_ => storage) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/unit/http/HttpClientProxyTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/unit/http/HttpClientProxyTest.scala index dbdae41051..a1a485d736 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/unit/http/HttpClientProxyTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/unit/http/HttpClientProxyTest.scala @@ -223,7 +223,7 @@ class HttpClientProxyTest .set("http.proxyPassword", "fail") withProperties(props) { executeRequest(serverBinding).failed.futureValue.getMessage should include( - "401 Unauthorized" + "407 Proxy Authentication Required" ) } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/FrontendLoginUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/FrontendLoginUtil.scala index 2ba07f75c9..e0fef3e7ea 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/FrontendLoginUtil.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/FrontendLoginUtil.scala @@ -124,7 +124,12 @@ trait FrontendLoginUtil extends WithAuth0Support { self: FrontendTestCommon => ) } val userPartyId = if (onboardThroughWalletUI) { - actAndCheck("onboard user", eventuallyClickOn(id("onboard-button")))( + // Onboarding creates the WalletAppInstall contract + // under CI load sequencing can take longer than the default 20s + actAndCheck(timeUntilSuccess = 40.seconds)( + "onboard user", + eventuallyClickOn(id("onboard-button")), + )( "user is onboarded", _ => { val userId = seleniumText(find(id("logged-in-user"))) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/UpdateHistoryTestUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/UpdateHistoryTestUtil.scala index db3215507a..501f34cb73 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/UpdateHistoryTestUtil.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/UpdateHistoryTestUtil.scala @@ -33,7 +33,12 @@ import org.lfdecentralizedtrust.splice.store.UpdateHistoryTestBase.{ LostInScanApi, LostInStoreIngestion, } -import org.lfdecentralizedtrust.splice.store.{PageLimit, UpdateHistory, UpdateHistoryTestBase} +import org.lfdecentralizedtrust.splice.store.{ + PageLimit, + TimestampWithMigrationId, + UpdateHistory, + UpdateHistoryTestBase, +} import org.lfdecentralizedtrust.splice.store.UpdateHistory.UpdateHistoryResponse import com.daml.ledger.api.v2.transaction_filter import com.digitalasset.canton.admin.api.client.commands.LedgerApiCommands.UpdateService.{ @@ -45,7 +50,6 @@ import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.console.LocalInstanceReference import com.digitalasset.canton.metrics.MetricValue import com.digitalasset.canton.topology.{PartyId, SynchronizerId} -import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryResponseItem import org.scalatest.Assertion import scala.jdk.CollectionConverters.* @@ -116,11 +120,11 @@ trait UpdateHistoryTestUtil extends TestCommon { val recordedUpdates = updateHistory .getAllUpdates( Some( - ( - 0L, + TimestampWithMigrationId( // The after0 argument to getUpdates() is exclusive, so we need to subtract a small value // to include the first element actualUpdates.head.update.recordTime.addMicros(-1L), + 0L, ) ), PageLimit.tryCreate(actualUpdates.size), @@ -141,11 +145,11 @@ trait UpdateHistoryTestUtil extends TestCommon { val recordedUpdates = updateHistory .getAllUpdates( Some( - ( - 0L, + TimestampWithMigrationId( // The after0 argument to getUpdates() is exclusive, so we need to subtract a small value // to include the first element actualUpdates.head.update.recordTime.addMicros(-1L), + 0L, ) ), PageLimit.tryCreate(actualUpdates.size), @@ -443,22 +447,6 @@ trait UpdateHistoryTestUtil extends TestCommon { def shortDebugDescription(u: Seq[definitions.UpdateHistoryItem]): String = { u.map(shortDebugDescription).mkString("[\n", ",\n", "\n]") } - def shortDebugDescription(u: TransactionHistoryResponseItem): String = { - // Minimal, human-readable description. - // Only contains data that is consistent across SVs (in particular, no offset). - u.transactionType match { - case TransactionHistoryResponseItem.TransactionType.members.Transfer => - s"Transfer(${u.date}, ${u.transfer.value.sender}, ${u.transfer.value.receivers - .map(r => s"${r.party} -> ${r.amount}") - .mkString(", ")})" - case TransactionHistoryResponseItem.TransactionType.members.Mint => - s"Mint(${u.date}, ${u.mint.value.amuletOwner}, ${u.mint.value.amuletAmount})" - case TransactionHistoryResponseItem.TransactionType.members.DevnetTap => - s"DevnetTap(${u.date}, ${u.tap.value.amuletOwner}, ${u.tap.value.amuletAmount})" - case TransactionHistoryResponseItem.TransactionType.members.AbortTransferInstruction => - s"AbortTransferInstruction(${u.date}, ${u.abortTransferInstruction.value.transferInstructionCid})" - } - } def dropTrailingNones(u: UpdateHistoryResponse): UpdateHistoryResponse = u.copy(update = dropTrailingNones(u.update)) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/WalletTestUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/WalletTestUtil.scala index 9bed4ecebe..1422e68567 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/WalletTestUtil.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/WalletTestUtil.scala @@ -66,11 +66,12 @@ trait WalletTestUtil extends TestCommon with AnsTestUtil { wallet: WalletAppClientReference, expectedAmountRanges: Seq[(BigDecimal, BigDecimal)], holdingFee: BigDecimal = defaultHoldingFeeAmulet.bigDecimal, + timeUntilSuccess: FiniteDuration = 10.seconds, ): Unit = clue(s"checking wallet with $expectedAmountRanges") { val expectedRatePerRound = new feesCodegen.RatePerRound( holdingFee.bigDecimal setScale 10 ) - eventually(10.seconds, 500.millis) { + eventually(timeUntilSuccess, 500.millis) { val amulets = wallet.list().amulets.sortBy(amulet => amulet.contract.payload.amount.initialAmount) amulets should have size (expectedAmountRanges.size.toLong) diff --git a/apps/common/frontend/src/__tests__/dso.test.tsx b/apps/common/frontend/src/__tests__/dso.test.tsx new file mode 100644 index 0000000000..80d0fd30d2 --- /dev/null +++ b/apps/common/frontend/src/__tests__/dso.test.tsx @@ -0,0 +1,75 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { DsoInfo, theme } from '@canton-network/splice-common-frontend'; +import { Contract } from '@canton-network/splice-common-frontend-utils'; +import { dsoInfo } from '@canton-network/splice-common-test-handlers'; +import { QueryClient, QueryClientProvider, onlineManager, useQuery } from '@tanstack/react-query'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { CometBftNodeDumpOrErrorResponse } from '@canton-network/sv-openapi'; +import { afterEach, describe, expect, test } from 'vitest'; + +import { ThemeProvider } from '@mui/material'; + +import { AmuletRules } from '@daml.js/splice-amulet/lib/Splice/AmuletRules'; +import { DsoRules } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; + +import DsoViewPrettyJSON from '../components/Dso'; + +const makeDsoInfo = (): DsoInfo => ({ + svUser: dsoInfo.sv_user, + svPartyId: dsoInfo.sv_party_id, + dsoPartyId: dsoInfo.dso_party_id, + votingThreshold: BigInt(dsoInfo.voting_threshold), + amuletRules: Contract.decodeOpenAPI(dsoInfo.amulet_rules.contract, AmuletRules), + dsoRules: Contract.decodeOpenAPI(dsoInfo.dso_rules.contract, DsoRules), + nodeStates: [], +}); + +const TestDso: React.FC = () => { + const dsoInfoQuery = useQuery({ + queryKey: ['dsoInfo'], + queryFn: async () => makeDsoInfo(), + initialData: makeDsoInfo(), + }); + const cometBftNodeDebugQuery = useQuery({ + queryKey: ['cometBftDebug'], + queryFn: async () => { + throw new Error('unreachable: query is paused while offline'); + }, + }); + return ( + + ); +}; + +describe('DsoViewPrettyJSON', () => { + afterEach(() => { + onlineManager.setOnline(true); + }); + + // With CantonBFT the cometbft debug endpoint 404s forever, so the query never + // reaches success; when it pauses (browser offline / tab backgrounded during + // retry backoff), status is 'pending' but isLoading is false and data is + // undefined. Rendering must not crash in that state. + test('does not crash when the cometBFT debug query is paused without data', () => { + window.splice_config = { + spliceInstanceNames: { amuletName: 'Amulet' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + onlineManager.setOnline(false); + + render( + + + + + + ); + + expect(screen.getByText('Super Validator Information')).toBeDefined(); + }); +}); diff --git a/apps/common/frontend/src/components/Dso.tsx b/apps/common/frontend/src/components/Dso.tsx index 5b36a6d645..772584900e 100644 --- a/apps/common/frontend/src/components/Dso.tsx +++ b/apps/common/frontend/src/components/Dso.tsx @@ -118,7 +118,7 @@ const TabPanel = (props: TabPanelProps) => { function getCometBftDebugData( cometBftNodeDebugQuery: UseQueryResult ) { - if (cometBftNodeDebugQuery.isLoading) { + if (cometBftNodeDebugQuery.isPending) { return ; } diff --git a/apps/common/frontend/src/theme/index.ts b/apps/common/frontend/src/theme/index.ts index f3dbc9f1db..686eb1c6d3 100644 --- a/apps/common/frontend/src/theme/index.ts +++ b/apps/common/frontend/src/theme/index.ts @@ -27,6 +27,7 @@ declare module '@mui/material/styles' { } interface Palette { + neutral: Palette['primary']; colors: { neutral: Record; primary: Record; @@ -36,10 +37,12 @@ declare module '@mui/material/styles' { testnet: string; devnet: string; scratchnet: string; + localnet: string; }; } // allow configuration using `createTheme` interface PaletteOptions { + neutral?: PaletteOptions['primary']; colors?: { neutral?: Record; primary?: Record; @@ -49,10 +52,17 @@ declare module '@mui/material/styles' { testnet: string; devnet: string; scratchnet: string; + localnet: string; }; } } +declare module '@mui/material/Badge' { + interface BadgePropsColorOverrides { + neutral: true; + } +} + declare module '@mui/material/Button' { interface ButtonPropsVariantOverrides { pill: true; @@ -89,6 +99,7 @@ let theme = createTheme({ testnet: '#C8F1FE', devnet: '#C6B2FF', scratchnet: '#FFFFFF', + localnet: '#BDC9DB', }, }, }); @@ -111,6 +122,13 @@ theme = createTheme(theme, { tertiary: { main: '#875CFF', }, + neutral: theme.palette.augmentColor({ + color: { + main: theme.palette.colors.neutral[25], + contrastText: '#E2E2E2', + }, + name: 'neutral', + }), warning: { main: '#FD8575', }, @@ -142,7 +160,7 @@ theme = createTheme(theme, { }, }); -// Based on the Major Third type scale: https://typescale.com/?size=16&scale=1.250&text=A%20Visual%20Type%20Scale&font=Lato&fontweight=400&bodyfont=body_font_default&bodyfontweight=400&lineheight=1.75&backgroundcolor=%23ffffff&fontcolor=%23000000&preview=false +// Based on the Major Third type scale: https://typescale.com/?size=16&scale=1.250&text=A%20Visual%20Type%20Scale&font=Inter&fontweight=400&bodyfont=body_font_default&bodyfontweight=400&lineheight=1.75&backgroundcolor=%23ffffff&fontcolor=%23000000&preview=false const TYPE_SCALE = 1.25; theme = createTheme(theme, { diff --git a/apps/common/src/main/java/com/google/common/util/concurrent/BurstyRateLimiterFactory.java b/apps/common/src/main/java/com/google/common/util/concurrent/BurstyRateLimiterFactory.java new file mode 100644 index 0000000000..593e053bc6 --- /dev/null +++ b/apps/common/src/main/java/com/google/common/util/concurrent/BurstyRateLimiterFactory.java @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.google.common.util.concurrent; + +/** + * Shim that constructs a Guava {@link RateLimiter} backed by {@code SmoothBursty} with a custom + * maximum burst duration. It lives in this package because the relevant {@code + * SmoothRateLimiter.SmoothBursty} constructor and its permit bookkeeping fields are package-private. + * + *

In contrast to {@link RateLimiter#create(double)}, the limiters created here already hold + * {@code permitsPerSecond} permits (capped by the maximum burst budget) at creation time, instead of + * starting with an empty bucket that only fills up over time. That matters for limiters that are + * created lazily (e.g. one per client IP), which would otherwise reject an initial burst that an + * already running limiter would have accepted. + */ +public final class BurstyRateLimiterFactory { + + /** + * Guava's default burst window for {@code RateLimiter.create(double)}. + */ + private static final double DEFAULT_MAX_BURST_SECONDS = 1.0; + + private BurstyRateLimiterFactory() { + } + + /** + * Creates a {@link RateLimiter} allowing {@code permitsPerSecond} permits per second, starting + * with {@code permitsPerSecond} permits already available. + */ + public static RateLimiter create(double permitsPerSecond) { + return create(permitsPerSecond, DEFAULT_MAX_BURST_SECONDS); + } + + /** + * Creates a bursty {@link RateLimiter} that sustains {@code permitsPerSecond} on average while + * allowing bursts of up to {@code permitsPerSecond * maxBurstSeconds} permits after idle periods. + * The limiter starts with one second worth of permits, i.e. {@code permitsPerSecond} permits + * (capped by the maximum burst budget), already available. + */ + public static RateLimiter create(double permitsPerSecond, double maxBurstSeconds) { + SmoothRateLimiter.SmoothBursty rateLimiter = + new SmoothRateLimiter.SmoothBursty( + RateLimiter.SleepingStopwatch.createFromSystemTimer(), maxBurstSeconds); + rateLimiter.setRate(permitsPerSecond); + synchronized (rateLimiter) { + rateLimiter.storedPermits = Math.min(permitsPerSecond, rateLimiter.maxPermits); + } + return rateLimiter; + } +} diff --git a/apps/common/src/main/openapi/common-internal.yaml b/apps/common/src/main/openapi/common-internal.yaml index c39e503c4e..f388a23fd9 100644 --- a/apps/common/src/main/openapi/common-internal.yaml +++ b/apps/common/src/main/openapi/common-internal.yaml @@ -360,6 +360,33 @@ components: Cursor for the next page of results. Pass this as `pageToken` in the request. If absent or `null`, there are no more pages. + CountVoteResultsRequest: + description: | + Filters for counting vote results. Same semantics as the corresponding + fields on `ListVoteResultsRequest`. + type: object + properties: + actionName: + type: string + accepted: + type: boolean + requester: + type: string + effectiveFrom: + type: string + effectiveTo: + type: string + + CountVoteResultsResponse: + type: object + required: + - count + properties: + count: + type: integer + format: int64 + description: Total number of vote results matching the request filters. + PreviousSvRewardWeightRequest: type: object required: diff --git a/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V073__dso_unavailable_parties.sql b/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V073__dso_unavailable_parties.sql new file mode 100644 index 0000000000..9d3d90a345 --- /dev/null +++ b/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V073__dso_unavailable_parties.sql @@ -0,0 +1,26 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- Table storing parties that the automation temporarily ignore, because of unresponsiveness or vetting errors. +-- Entries are ignored for updated_at + ignore_duration. +create table dso_unavailable_parties +( + -- the ID of the party that is unavailable + party text not null, + -- the time when the party was marked as unavailable, used for capped exponential backoff + updated_at bigint not null, + -- the duration (microseconds) to ignore the entry, used for capped exponential backoff + ignore_duration bigint not null, + -- the store ID when the party is added, used for resets + store_id bigint not null, + -- the metadata fields reserved for diagnostic/extra information + metadata jsonb, + primary key (party) +); + +-- Index for the expiry check per party +create index dso_unavailable_parties_pid_exp + on dso_unavailable_parties (party, (updated_at + ignore_duration)); + +-- Index for the efficient store cleanup +create index dso_unavailable_parties_sid on dso_unavailable_parties (store_id); diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/SpliceMetrics.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/SpliceMetrics.scala index 8ea09718a1..ca3d7c2371 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/SpliceMetrics.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/SpliceMetrics.scala @@ -5,7 +5,7 @@ package org.lfdecentralizedtrust.splice import com.daml.metrics.HealthMetrics import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory -import com.daml.metrics.api.{MetricName, MetricsContext} +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} import com.digitalasset.canton.environment.BaseMetrics import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.metrics.ActiveRequestsMetrics.GrpcServerMetricsX @@ -59,4 +59,28 @@ abstract class BaseSpliceMetrics( override def httpClientMetrics: HttpClientMetrics = new HttpClientMetrics( openTelemetryMetricsFactory ) + + override def cryptoMetrics = crypto + + private[this] val crypto = { + import com.digitalasset.canton.metrics.{ + CryptoMetrics, + DecryptionHistograms, + DecryptionMetrics, + KmsMetrics, + SigningHistograms, + SigningMetrics, + } + new CryptoMetrics( + new SigningMetrics( + new SigningHistograms(prefix)(new HistogramInventory()), + openTelemetryMetricsFactory, + ), + new DecryptionMetrics( + new DecryptionHistograms(prefix)(new HistogramInventory()), + openTelemetryMetricsFactory, + ), + Some(new KmsMetrics(prefix, openTelemetryMetricsFactory)), + ) + } } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/admin/api/HttpRequestLogger.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/admin/api/HttpRequestLogger.scala index b5514fdeb6..364d1fe413 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/admin/api/HttpRequestLogger.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/admin/api/HttpRequestLogger.scala @@ -4,12 +4,13 @@ package org.lfdecentralizedtrust.splice.admin.api import org.apache.pekko.http.scaladsl.model.{ContentTypes, HttpEntity, RemoteAddress} -import org.apache.pekko.http.scaladsl.server.{Directive0, RequestContext} +import org.apache.pekko.http.scaladsl.server.{Directive0, Directive1, RequestContext} import org.apache.pekko.http.scaladsl.server.Directives.* import com.digitalasset.canton.config.ApiLoggingConfig import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* +import org.lfdecentralizedtrust.splice.http.ClientIpDirectives object HttpRequestLogger { def apply( @@ -17,6 +18,7 @@ object HttpRequestLogger { maxPathLength: Int, maxStringLength: Int, maxMetadataSize: Int, + clientIpHeaders: Seq[String], loggerFactory: NamedLoggerFactory, )(implicit traceContext: TraceContext): Directive0 = { new HttpRequestLogger( @@ -24,6 +26,7 @@ object HttpRequestLogger { maxPathLength, maxStringLength, maxMetadataSize, + clientIpHeaders, loggerFactory, ).directive } @@ -31,12 +34,14 @@ object HttpRequestLogger { // ignores maxMethodLength and maxMessageLines def apply( loggingConfig: ApiLoggingConfig, + clientIpHeaders: Seq[String], loggerFactory: NamedLoggerFactory, )(implicit traceContext: TraceContext): Directive0 = apply( messagePayloads = loggingConfig.messagePayloads, maxPathLength = loggingConfig.maxMethodLength, maxStringLength = loggingConfig.maxStringLength, maxMetadataSize = loggingConfig.maxMetadataSize, + clientIpHeaders = clientIpHeaders, loggerFactory = loggerFactory, ) } @@ -46,6 +51,7 @@ final class HttpRequestLogger( maxPathLength: Int, maxStringLength: Int, maxMetadataSize: Int, + clientIpHeaders: Seq[String], override protected val loggerFactory: NamedLoggerFactory, ) extends NamedLogging { def createLogMessage(ctx: RequestContext, remoteAddress: RemoteAddress)( @@ -56,8 +62,14 @@ final class HttpRequestLogger( s"HTTP ${ctx.request.method.name} ${pathLimited} from (${remoteAddress}): ${message}" } + private def extractConfiguredClientIp: Directive1[RemoteAddress] = + ClientIpDirectives.extractClientIp(clientIpHeaders).flatMap { + case Some(remoteAddress) => provide(remoteAddress) + case None => extractClientIP + } + private def directive(implicit traceContext: TraceContext): Directive0 = { - extractClientIP.flatMap { remoteAddress => + extractConfiguredClientIp.flatMap { remoteAddress => extractRequestContext.flatMap { ctx => val msg = createLogMessage(ctx, remoteAddress) logger.debug(msg("received request.")) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/admin/http/HttpAdminService.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/admin/http/HttpAdminService.scala index 3494946147..04bdee6c17 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/admin/http/HttpAdminService.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/admin/http/HttpAdminService.scala @@ -36,6 +36,7 @@ object HttpAdminService { adminApi: AdminServerConfig, parameterConfig: CantonNodeParameters, apiLoggingConfig: ApiLoggingConfig, + clientIpHeaders: Seq[String], loggerFactory: NamedLoggerFactory, node: => Option[CantonNode], )(implicit @@ -49,6 +50,7 @@ object HttpAdminService { adminApi.port, parameterConfig, apiLoggingConfig, + clientIpHeaders, loggerFactory, node, ) @@ -59,6 +61,7 @@ object HttpAdminService { port: Port, parameterConfig: CantonNodeParameters, apiLoggingConfig: ApiLoggingConfig, + clientIpHeaders: Seq[String], loggerFactory: NamedLoggerFactory, node: => Option[CantonNode], )(implicit ac: ActorSystem, ec: ExecutionContext, tracer: Tracer, elc: ErrorLoggingContext) @@ -98,7 +101,7 @@ object HttpAdminService { // handleRejections (inside the logger) seals the route: rejections are // converted to HTTP responses so mapResponse sees all outcomes and logs // exactly one "Responding with status code" per request. - HttpRequestLogger(apiLoggingConfig, loggerFactory)(traceContext) { + HttpRequestLogger(apiLoggingConfig, clientIpHeaders, loggerFactory)(traceContext) { handleRejections(RejectionHandler.default) { encodeResponse( handleRejections( diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/BatchedMultiDomainExpiredContractTrigger.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/BatchedMultiDomainExpiredContractTrigger.scala index ca8402976a..e2ec3530cf 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/BatchedMultiDomainExpiredContractTrigger.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/BatchedMultiDomainExpiredContractTrigger.scala @@ -8,14 +8,14 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.daml.lf.data.Ref.PackageVersion import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import org.lfdecentralizedtrust.splice.environment.{PackageIdResolver, PackageVettingLookupService} import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.ContractState -import org.lfdecentralizedtrust.splice.store.{MultiDomainAcsStore, PageLimit} +import org.lfdecentralizedtrust.splice.store.{IgnoredPartiesStore, MultiDomainAcsStore, PageLimit} import org.lfdecentralizedtrust.splice.util.{AssignedContract, Contract} +import com.digitalasset.canton.discard.Implicits.DiscardOps import scala.concurrent.{ExecutionContext, Future} @@ -34,7 +34,7 @@ abstract class BatchedMultiDomainExpiredContractTrigger[ companion: C, vettingLookupService: PackageVettingLookupService, pkg: PackageIdResolver.Package, - stakeholders: T => Seq[PartyId], + getStakeholders: T => Seq[PartyId], )(implicit ec: ExecutionContext, mat: Materializer, @@ -44,6 +44,14 @@ abstract class BatchedMultiDomainExpiredContractTrigger[ import BatchedMultiDomainExpiredContractTrigger.Batch + protected val ignoredPartiesStore: IgnoredPartiesStore + + protected def ignorePartiesWithoutVettedAmulet( + informees: Set[PartyId], + contractIds: Seq[String], + logAsWarning: Boolean, + )(implicit tc: TraceContext): String + override final protected def listReadyTasks(now: CantonTimestamp, limit: Int)(implicit tc: TraceContext ): Future[Seq[Batch[TCid, T]]] = @@ -58,14 +66,21 @@ abstract class BatchedMultiDomainExpiredContractTrigger[ PackageIdResolver.Package.SpliceAmulet, expiredContracts, batchSize, - )(c => stakeholders(c.payload)) + )(c => getStakeholders(c.payload)) .map { _.toSeq.flatMap { - case (Some(version), contractBatches) => contractBatches.map(Batch(pkg, version, _)) + case (Some(version), contractBatches) => + contractBatches.map { contracts => + val stakeholders = contracts.flatMap(c => getStakeholders(c.payload)).toSet + Batch(pkg, version, contracts, stakeholders) + } case (None, contracts) => - logger.warn( - show"No vetted $pkg version for ${contracts.flatten.map { _.contractId.contractId }}" - ) + val stakeholders = contracts.flatten.flatMap(c => getStakeholders(c.payload)).toSet + ignorePartiesWithoutVettedAmulet( + stakeholders, + contracts.flatten.map(_.contractId.contractId), + logAsWarning = true, + ).discard Seq.empty } } @@ -92,6 +107,7 @@ object BatchedMultiDomainExpiredContractTrigger { expiredContracts: Seq[ AssignedContract[TCid, T] ], + stakeholders: Set[PartyId], ) extends PrettyPrinting { override def pretty: Pretty[this.type] = prettyOfClass( @@ -99,6 +115,7 @@ object BatchedMultiDomainExpiredContractTrigger { param("vettedVersion", _.vettedVersion), param("numExpiredContracts", _.expiredContracts.size), param("expiredContractCids", _.expiredContracts.map(_.contractId.contractId.unquoted)), + param("stakeholders", _.stakeholders), ) } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/PollingTrigger.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/PollingTrigger.scala index 12274243f7..ea48f78312 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/PollingTrigger.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/PollingTrigger.scala @@ -112,6 +112,8 @@ trait PollingTrigger extends Trigger with FlagCloseableAsync { context.metricsFactory, mc.labels, context.retryProvider, + // Built outside the RetryFor typeclass, so the automation default has to be restated. + duplicateCommandIsFatal = false, ) override def isHealthy: Boolean = pollingLoopRef diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTrigger.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTrigger.scala index 1f11182896..9e72fdf48c 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTrigger.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTrigger.scala @@ -13,12 +13,14 @@ import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import org.lfdecentralizedtrust.splice.automation.SqlIndexInitializationTrigger.IndexAction +import org.lfdecentralizedtrust.splice.store.db.AdvisoryLocks import org.lfdecentralizedtrust.splice.util.PrettyInstances.* import slick.dbio.{DBIOAction, Effect, NoStream} import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton import java.util.concurrent.atomic.AtomicReference import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future, Promise} +import scala.util.{Failure, Success} /** A trigger that asynchronously creates or drops SQL indexes at application startup. * @@ -113,12 +115,12 @@ class SqlIndexInitializationTrigger( override protected def completeTask(task: SqlIndexInitializationTrigger.Task)(implicit tc: TraceContext - ): Future[TaskOutcome] = task match { + ): Future[TaskOutcome] = (task match { case Task.ExecuteAction(IndexAction.Drop(indexName)) => logger.info(s"Dropping index $indexName") storage - .update( - sqlu"drop index concurrently if exists #$indexName", + .queryAndUpdate( + AdvisoryLocks.withDdlLock(sqlu"drop index concurrently if exists #$indexName"), "drop_" + indexName, ) .unwrap @@ -130,7 +132,7 @@ class SqlIndexInitializationTrigger( case Task.ExecuteAction(IndexAction.Create(indexName, createAction)) => logger.info(s"Creating index $indexName") storage - .update(createAction, "create_" + indexName) + .queryAndUpdate(AdvisoryLocks.withDdlLock(createAction), "create_" + indexName) .unwrap .map { _ => logger.info(s"Finished creating index $indexName") @@ -144,6 +146,13 @@ class SqlIndexInitializationTrigger( } logger.info(s"Confirmed action completed for index ${action.indexName}") Future.successful(TaskSuccess(s"Confirmed action completed for index ${action.indexName}")) + }).transform { + case Failure(e: AdvisoryLocks.FailedToAcquireLockException) => + // There was a concurrent DDL statement running. + // The action stays in `remainingActions`, so we retry it on the next poll. + logger.info(s"Skipping $task, another DDL statement was running currently", e) + Success(TaskNoop) + case other => other } } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/RateLimitersConfig.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/RateLimitersConfig.scala index 971de2ae5f..39626cd40f 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/RateLimitersConfig.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/RateLimitersConfig.scala @@ -3,11 +3,45 @@ package org.lfdecentralizedtrust.splice.config -import org.lfdecentralizedtrust.splice.util.SpliceRateLimitConfig +import org.lfdecentralizedtrust.splice.util.{PerAttributeRateLimitConfig, SpliceRateLimitConfig} case class RateLimitersConfig( - default: SpliceRateLimitConfig, - rateLimiters: Map[String, SpliceRateLimitConfig], + /** Overall rate limiter applied per operation. Used when there is no operation-specific override + * in `rateLimiters`. The embedded `perClientIp` limiter is disabled by default; enable it to + * additionally limit per client IP. + */ + default: SpliceRateLimitConfig.WithPerClientIp = + SpliceRateLimitConfig.WithPerClientIp(ratePerSecond = 200), + /** Per-operation overrides of the overall `default` rate limiter. */ + rateLimiters: Map[String, SpliceRateLimitConfig.WithPerClientIp] = Map.empty, + global: SpliceRateLimitConfig.WithPerClientIp = RateLimitersConfig.DefaultGlobal, + /** Names of the HTTP headers from which the client IP used for per-client-IP rate limiting is + * extracted, in order of precedence: the first header that is present and whose value (or, for + * comma separated lists such as `X-Forwarded-For`, whose first entry) parses as an IP literal + * is used. Set to an empty list to disable per-client-IP rate limiting. + * + * Note that the default headers are client-controlled and can hence be spoofed unless they are + * overwritten by infrastructure the client cannot bypass. In deployments with a trusted reverse + * proxy, configure the (non-spoofable) header set by that proxy instead, e.g. + * `["x-envoy-external-address"]` behind an Envoy proxy. + */ + clientIpHeaders: Seq[String] = RateLimitersConfig.DefaultClientIpHeaders, ) { - def forRateLimiter(name: String): SpliceRateLimitConfig = rateLimiters.getOrElse(name, default) + def forRateLimiter(name: String): SpliceRateLimitConfig.WithPerClientIp = + rateLimiters.getOrElse(name, default) +} + +object RateLimitersConfig { + + /** The commonly used client IP headers, in order of precedence. Both are set by clients or + * reverse proxies and are hence only trustworthy if a proxy the client cannot bypass overwrites + * them. + */ + val DefaultClientIpHeaders: Seq[String] = Seq("x-forwarded-for", "x-real-ip") + + private val DefaultGlobal: SpliceRateLimitConfig.WithPerClientIp = + SpliceRateLimitConfig.WithPerClientIp( + ratePerSecond = 200, + perClientIp = PerAttributeRateLimitConfig(), + ) } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala index f0830271d3..3ee52a5994 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala @@ -25,6 +25,7 @@ abstract class SpliceBackendConfig extends LocalNodeConfig { def participantClient: BaseParticipantClientConfig def automation: AutomationConfig + def parameters: SpliceParametersConfig } @@ -91,6 +92,7 @@ final case class EnabledFeaturesConfig( // On 3.5 we should be able to set it to false. reconnectOnSynchronizerConfigurationChange: Boolean = true, enableUnsupportedDarsUnvetting: Boolean = true, + enableValidatorDarsUnvetting: Boolean = true, ignorePartyIdWithIgnoredAmulet: Boolean = true, naiveUnresponsivePartiesAutoIgnore: Boolean = true, ) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceParametersConfig.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceParametersConfig.scala index 42c842df85..733aed37ec 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceParametersConfig.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceParametersConfig.scala @@ -6,7 +6,6 @@ package org.lfdecentralizedtrust.splice.config import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.config.* import org.lfdecentralizedtrust.splice.store.{ChoiceContextContractFetcher, HardLimit, Limit} -import org.lfdecentralizedtrust.splice.util.SpliceRateLimitConfig final case class SpliceParametersConfig( batching: BatchingConfig = BatchingConfig(), @@ -17,8 +16,7 @@ final case class SpliceParametersConfig( // Do not define any defaults on the class containing the `SpliceParametersConfig` as they'll be overwritten. // Do it instead on the app.conf file in `cluster/images/${the_app}/app.conf` customTimeouts: Map[String, NonNegativeFiniteDuration] = Map.empty, - rateLimiting: RateLimitersConfig = - RateLimitersConfig(SpliceRateLimitConfig(enabled = true, ratePerSecond = 200), Map.empty), + rateLimiting: RateLimitersConfig = RateLimitersConfig(), // Configuration for the circuit breaker for ledger API command submissions. circuitBreakers: CircuitBreakersConfig = CircuitBreakersConfig(), enabledFeatures: EnabledFeaturesConfig = EnabledFeaturesConfig(), diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala index 9184a816b0..ee0a4e30dd 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala @@ -10067,13 +10067,25 @@ object DarResources { DarResources.splitwell, ) + lazy val corePackageResources: Seq[PackageResource] = + TokenStandard.allPackageResources ++ Seq( + DarResources.amulet, + DarResources.amuletNameService, + DarResources.apiRewardAssignmentV1, + DarResources.dsoGovernance, + DarResources.utilBatchedMarkers, + DarResources.validatorLifecycle, + DarResources.wallet, + DarResources.walletPayments, + ) + lazy val pkgIdToDarResource: Map[String, DarResource] = - packageResources.view.flatMap(_.all).map(resource => resource.packageId -> resource).toMap + corePackageResources.view.flatMap(_.all).map(resource => resource.packageId -> resource).toMap // We don't index the map by PackageMetadata because that type contains some additional // fields that don't matter. lazy val pkgMetadataToDarResource: Map[(PackageName, PackageVersion), DarResource] = - packageResources.view + corePackageResources.view .flatMap(_.all) .map(resource => (resource.metadata.name, resource.metadata.version) -> resource) .toMap diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/LsuTopologyAdminConnection.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/LsuTopologyAdminConnection.scala index fbdd7bdb45..21d13a817a 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/LsuTopologyAdminConnection.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/LsuTopologyAdminConnection.scala @@ -6,7 +6,7 @@ package org.lfdecentralizedtrust.splice.environment import cats.data.EitherT import cats.implicits.catsSyntaxOptionId import com.digitalasset.canton.admin.api.client.commands.TopologyAdminCommands -import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} +import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.topology.{PhysicalSynchronizerId, SequencerId, SynchronizerId} import com.digitalasset.canton.topology.admin.grpc.{BaseQuery, TopologyStoreId} @@ -32,74 +32,58 @@ import scala.concurrent.{ExecutionContext, Future} trait LsuTopologyAdminConnection { this: TopologyAdminConnection => - def lookupSequencerSuccessors(synchronizerId: SynchronizerId, sequencerId: SequencerId)(implicit + def lookupSequencerSuccessors( + synchronizerId: SynchronizerId, + sequencerId: SequencerId, + successor: Option[PhysicalSynchronizerId], + ops: Option[TopologyChangeOp], + )(implicit tc: TraceContext, ec: ExecutionContext, ): Future[Option[TopologyResult[LsuSequencerConnectionSuccessor]]] = runCmd( TopologyAdminCommands.Read.ListLsuSequencerConnectionSuccessor( BaseQuery( - TopologyStoreId.Synchronizer(synchronizerId), + TopologyStoreId.Synchronizer(synchronizerId.logical), proposals = false, timeQuery = TimeQuery.HeadState, - ops = Some(TopologyChangeOp.Replace), + ops = ops, filterSigningKey = "", protocolVersion = None, ), sequencerId.filterString, - filterSuccessorPhysicalSynchronizerId = "", + filterSuccessorPhysicalSynchronizerId = successor.map(_.toProtoPrimitive).getOrElse(""), ) ).map(_.headOption.map(r => TopologyResult(r.context, r.item))) def ensureSequencerSuccessor( - synchronizerId: PhysicalSynchronizerId, + successorSynchronizerId: PhysicalSynchronizerId, sequencerId: SequencerId, connection: GrpcConnection, )(implicit tc: TraceContext, ec: ExecutionContext, ): Future[TopologyResult[LsuSequencerConnectionSuccessor]] = { - retryProvider.ensureThat( - RetryFor.Automation, - s"sequencer_successor_$sequencerId", - s"sequencer successor for $sequencerId is published with connection $connection", - lookupSequencerSuccessors(synchronizerId.logical, sequencerId).map { result => - result.filter(_.mapping.connection == connection).toRight(result) - }, - (previous: Option[TopologyResult[LsuSequencerConnectionSuccessor]]) => { - logger.info(s"Adding sequencer $sequencerId successor with connection $connection") - (previous match { - case Some(successor) => - proposeMapping( - synchronizerId.logical, - successor.mapping.copy(connection = connection), - successor.base.serial + PositiveInt.one, - isProposal = false, - ) - case None => - proposeMapping( - synchronizerId.logical, - LsuSequencerConnectionSuccessor(sequencerId, synchronizerId, connection), - PositiveInt.one, - isProposal = false, - ) - }).map(_ => ()) + ensureTopologyMappingO( + successorSynchronizerId.logical, + s"sequencer successor for $sequencerId with connection $connection", + _ => + EitherT + .liftF( + lookupSequencerSuccessors(successorSynchronizerId.logical, sequencerId, None, None) + ) + .subflatMap { + case Some(successor) + if successor.mapping.connection == connection && successor.mapping.successorPsid == successorSynchronizerId => + Right(successor) + case Some(existing) => Left(existing.some) + case None => Left(None) + }, + { (_: Option[TopologyMapping]) => + Right( + LsuSequencerConnectionSuccessor(sequencerId, successorSynchronizerId, connection) + ) }, - logger, - ) - } - - def removeSequencerSuccessor( - synchronizerId: SynchronizerId, - sequencerId: SequencerId, - )(implicit tc: TraceContext, ec: ExecutionContext): Future[Unit] = { - ensureTopologyMappingRemoved( - s"Remove SequencerSuccessor for $synchronizerId and sequencer $sequencerId", - synchronizerId, - lookupSequencerSuccessors( - synchronizerId, - sequencerId, - ), - proposal = true, + retryFor = RetryFor.Automation, ) } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/NodeBootstrapBase.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/NodeBootstrapBase.scala index 729411f223..bb6fa45add 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/NodeBootstrapBase.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/NodeBootstrapBase.scala @@ -7,7 +7,7 @@ import cats.data.EitherT import com.daml.nameof.NameOf.functionFullName import org.lfdecentralizedtrust.splice.SpliceMetrics import com.digitalasset.canton.concurrent.ExecutionContextIdlenessExecutorService -import com.digitalasset.canton.config.{LocalNodeConfig, ProcessingTimeout} +import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.CantonRequireTypes.InstanceName import com.digitalasset.canton.crypto.Crypto import com.digitalasset.canton.environment.{CantonNode, CantonNodeBootstrap, CantonNodeParameters} @@ -26,6 +26,7 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} import scala.concurrent.{blocking, Future} import scala.util.{Failure, Success} import org.lfdecentralizedtrust.splice.admin.http.{AdminRoutes, HttpAdminService} +import org.lfdecentralizedtrust.splice.config.SpliceBackendConfig /** Modelled after CantonNodeBootstrap */ @@ -66,7 +67,7 @@ trait NodeBootstrap[+N <: CantonNode] */ abstract class NodeBootstrapBase[ T <: CantonNode, - NodeConfig <: LocalNodeConfig, + NodeConfig <: SpliceBackendConfig, ParameterConfig <: CantonNodeParameters, ]( nodeConfig: NodeConfig, @@ -109,6 +110,7 @@ abstract class NodeBootstrapBase[ nodeConfig.adminApi, parameterConfig, parameterConfig.loggingConfig.api, + nodeConfig.parameters.rateLimiting.clientIpHeaders, loggerFactory, getNode, ) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/ParticipantAdminSynchronizerConnection.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/ParticipantAdminSynchronizerConnection.scala index 9ad03ec13b..81e3fd45d4 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/ParticipantAdminSynchronizerConnection.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/ParticipantAdminSynchronizerConnection.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.admin.api.client.data.{ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.SynchronizerAlias import com.digitalasset.canton.admin.api.client.data +import com.digitalasset.canton.logging.pretty.Pretty import com.digitalasset.canton.participant.synchronizer.SynchronizerConnectionConfig import com.digitalasset.canton.sequencing.SequencerConnectionValidation import com.digitalasset.canton.topology.{PhysicalSynchronizerId, SynchronizerId} @@ -160,6 +161,9 @@ trait ParticipantAdminSynchronizerConnection { ): Future[Unit] = runCmd(ParticipantAdminCommands.SynchronizerConnectivity.DisconnectSynchronizer(alias)) + private implicit val prettyRegisteredSynchronizer: Pretty[RegisteredSynchronizer] = + Pretty.adHocPrettyInstance + def ensureSynchronizerRegisteredWithManualConnect( config: SynchronizerConnectionConfig, retryFor: RetryFor, @@ -169,15 +173,14 @@ trait ParticipantAdminSynchronizerConnection { "manualConnect must be true when trying to register only", ) retryProvider - .ensureThat( + .ensureThatO( retryFor, "synchronizer_registered_no_handshake", s"participant registered ${config.synchronizerAlias}", - isSynchronizerRegistered(config.synchronizerAlias).map(Either.cond(_, (), ())), - (_: Unit) => registerSynchronizer(config), + listSynchronizerConnectionConfig(config.synchronizerAlias).map(_.headOption), + registerSynchronizer(config), logger, ) - .flatMap(_ => getRegisteredSynchronizer(config.synchronizerAlias)) } def ensureSynchronizerRegisteredAndConnected( @@ -191,7 +194,7 @@ trait ParticipantAdminSynchronizerConnection { retryFor, "synchronizer_registered", s"participant registered ${config.synchronizerAlias} with config $config", - lookupRegisteredSynchronizer(config.synchronizerAlias, config.synchronizerId).map { + lookupRegisteredSynchronizer(config.synchronizerAlias, config.psid).map { case Some(_) if !overwriteExistingConnection => Right(()) // We don't set the sequencer id when connecting but Canton returns it so we ignore it in the comparison here. case Some(existingConfig) @@ -210,7 +213,7 @@ trait ParticipantAdminSynchronizerConnection { case Some(_) => modifySynchronizerConnectionConfigAndReconnect( config.synchronizerAlias, - config.synchronizerId, + config.psid, reconnectOnSynchronizerConfigurationChange, _ => Some(config), ) @@ -317,12 +320,12 @@ trait ParticipantAdminSynchronizerConnection { case Some(config) => if ( registeredSynchronizer.psid.toOption - .exists(oldPsid => config.synchronizerId.exists(psid => psid != oldPsid)) + .exists(oldPsid => config.psid.exists(psid => psid != oldPsid)) ) { Future.failed( Status.INVALID_ARGUMENT .withDescription( - s"New config physical synchronizer id ${config.synchronizerId} cannot be different from the old one ${registeredSynchronizer.psid} for synchronizer $synchronizer" + s"New config physical synchronizer id ${config.psid} cannot be different from the old one ${registeredSynchronizer.psid} for synchronizer $synchronizer" ) .asRuntimeException() ) @@ -333,7 +336,7 @@ trait ParticipantAdminSynchronizerConnection { for { _ <- setSynchronizerConnectionConfig( config, - registeredSynchronizer.psid.toOption.orElse(config.synchronizerId), + registeredSynchronizer.psid.toOption.orElse(config.psid), ) } yield true } @@ -361,7 +364,7 @@ trait ParticipantAdminSynchronizerConnection { if (isSynchronizerRegistered) { modifySynchronizerConnectionConfig( config.synchronizerAlias, - config.synchronizerId, + config.psid, f, ) } else { @@ -398,7 +401,7 @@ trait ParticipantAdminSynchronizerConnection { f: SynchronizerConnectionConfig => Option[SynchronizerConnectionConfig], retryFor: RetryFor, )(implicit traceContext: TraceContext): Future[Unit] = { - require(config.synchronizerId.isDefined, "psid must be set") + require(config.psid.isDefined, "psid must be set") for { configModified <- modifyOrRegisterSynchronizerConnectionConfig( config, diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/RetryFor.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/RetryFor.scala index d0d3e8de11..6cc84f82c8 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/RetryFor.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/RetryFor.scala @@ -8,12 +8,17 @@ import scala.concurrent.duration.* /** The intended use of a retry, expressed in terms like number of retries and * backoff. Use a definition in the companion rather than constructing ''ad hoc''. + * + * `duplicateCommandIsFatal` gives up on DUPLICATE_COMMAND instead of retrying. It is set + * for client calls, where a duplicate cannot resolve within the deduplication window, and + * left off for automation, which retries and then finds its task stale. */ final case class RetryFor private ( maxRetries: Int, initialDelay: FiniteDuration, maxDelay: Duration, resetRetriesAfter: Option[FiniteDuration], + duplicateCommandIsFatal: Boolean = false, ) object RetryFor { @@ -67,6 +72,7 @@ object RetryFor { initialDelay = 100.millis, maxDelay = 1.seconds, resetRetriesAfter = None, + duplicateCommandIsFatal = true, ) /** A retry intended for client calls during the init phase, timing out slower compared to the regular client calls to allow for more contention that happens during initialization. */ @@ -75,5 +81,6 @@ object RetryFor { initialDelay = 100.millis, maxDelay = 3.seconds, resetRetriesAfter = None, + duplicateCommandIsFatal = true, ) } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/RetryProvider.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/RetryProvider.scala index cfbbd329c1..998bf52637 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/RetryProvider.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/RetryProvider.scala @@ -472,6 +472,7 @@ final class RetryProvider( "operation" -> operationId ), this, + retryConfig.duplicateCommandIsFatal, ), ) } @@ -516,6 +517,7 @@ object RetryProvider { metricsFactory: LabeledMetricsFactory, additionalMetricsLabels: Map[String, String], flagCloseable: FlagCloseable, + duplicateCommandIsFatal: Boolean, ) extends ExceptionRetryPolicy { // Additional categories that are not marked as retryable but we // can safely retry since we know there are other apps or @@ -600,9 +602,18 @@ object RetryProvider { case _ => false } + val isDuplicateCommand = errorDetails.exists { + case detail: ErrorDetails.ErrorInfoDetail => + (detail.errorCodeId: String) == "DUPLICATE_COMMAND" + case _ => false + } + errorCategory match { // Pruning errors fall under FAILED_PRECONDITION which we usually retry but there is no chance to recover from it so we instead treat it as a fatal error. case _ if isPruningError => fatalError + // Accepted duplicates are recovered centrally in SpliceLedgerConnection; a rejected + // one can never succeed within the dedup window, so client calls give up here. + case _ if isDuplicateCommand && duplicateCommandIsFatal => fatalError case Some(cat) if cat.retryable.nonEmpty || extraRetryableCategories.contains(cat) => // don't log the stack traces of transient gRPC exceptions to make the logs less noisy. val msg = @@ -803,6 +814,7 @@ object RetryProvider { metricsFactory: LabeledMetricsFactory, additionalMetricsLabels: Map[String, String], flagCloseable: FlagCloseable, + duplicateCommandIsFatal: Boolean, ): ExceptionRetryPolicy } @@ -815,6 +827,7 @@ object RetryProvider { metricsFactory: LabeledMetricsFactory, additionalMetricsLabels: Map[String, String], flagCloseable: FlagCloseable, + duplicateCommandIsFatal: Boolean, ) = a(operationName) } @@ -825,6 +838,7 @@ object RetryProvider { metricsFactory: LabeledMetricsFactory, additionalMetricsLabels: Map[String, String], flagCloseable: FlagCloseable, + duplicateCommandIsFatal: Boolean, ): RetryableError = RetryProvider.RetryableError( operationName, additionalCodes, @@ -835,6 +849,7 @@ object RetryProvider { metricsFactory, additionalMetricsLabels, flagCloseable, + duplicateCommandIsFatal, ) } @@ -846,6 +861,7 @@ object RetryProvider { metricsFactory: LabeledMetricsFactory, additionalMetricsLabels: Map[String, String], flagCloseable: FlagCloseable, + duplicateCommandIsFatal: Boolean, ): RetryableError = RetryProvider.RetryableError( operationName, Seq.empty, @@ -856,6 +872,7 @@ object RetryProvider { metricsFactory, additionalMetricsLabels, flagCloseable, + duplicateCommandIsFatal, ) } } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SequencerAdminConnection.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SequencerAdminConnection.scala index f53094feb8..70d7bd6715 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SequencerAdminConnection.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SequencerAdminConnection.scala @@ -33,15 +33,13 @@ import com.digitalasset.canton.synchronizer.sequencer.SequencerPruningStatus import com.digitalasset.canton.synchronizer.sequencer.admin.grpc.InitializeSequencerResponse import com.digitalasset.canton.time.Clock import com.digitalasset.canton.topology.{Member, NodeIdentity, PhysicalSynchronizerId, SequencerId} -import com.digitalasset.canton.topology.admin.grpc.{BaseQuery, TopologyStoreId} import com.digitalasset.canton.topology.admin.v30.{ GenesisStateV2Response, SequencerLsuStateResponse, } import com.digitalasset.canton.topology.MediatorGroup.MediatorGroupIndex import com.digitalasset.canton.topology.store.StoredTopologyTransactions.GenericStoredTopologyTransactions -import com.digitalasset.canton.topology.store.TimeQuery.Snapshot -import com.digitalasset.canton.topology.transaction.{SequencerSynchronizerState, TopologyMapping} +import com.digitalasset.canton.topology.transaction.SequencerSynchronizerState import com.digitalasset.canton.tracing.TraceContext import com.google.protobuf.ByteString import com.typesafe.config.ConfigFactory @@ -68,6 +66,7 @@ import java.nio.file.{Files, Path} import java.util.{Base64, Collections} import scala.concurrent.{ExecutionContextExecutor, Future, blocking} import scala.jdk.CollectionConverters.* +import scala.util.control.NonFatal import org.lfdecentralizedtrust.splice.store.bulk.ZstdGroupedWeight /** Connection to the subset of the Canton sequencer admin API that we rely @@ -186,25 +185,6 @@ class SequencerAdminConnection( ) } - def getTopologyTransactionsSummary(store: TopologyStoreId, now: CantonTimestamp)(implicit - traceContext: TraceContext - ): Future[Map[TopologyMapping.Code, Int]] = { - runCmd( - TopologyAdminCommands.Read.ListAllV2( - query = BaseQuery( - store = store, - proposals = false, - timeQuery = Snapshot(now), - ops = None, - filterSigningKey = "", - protocolVersion = None, - ), - filterNamespace = "", - includeMappings = Seq.empty, - ) - ).map(_.result.groupMapReduce(_.mapping.code)(_ => 1)(_ + _)) - } - def getOnboardingState(sequencerIdOrTimestamp: Either[SequencerId, CantonTimestamp])(implicit traceContext: TraceContext ): Future[ByteString] = { @@ -302,29 +282,36 @@ class SequencerAdminConnection( logger, s"$serviceName connection", ) - // stub acts the client-side proxy to get access to raw grpc commands - val stub = request.createService(channel.channel) - // bridges the gRPC response stream to a Pekko Source and converts the Protobuf ByteString to a Pekko ByteString - val source = ClientAdapter - .serverStreaming( - request - .createRequestInternal() - .getOrElse(throw new IllegalStateException("Unable to create internal request.")), - (req: OnboardingStateV2Request, obs: StreamObserver[OnboardingStateV2Response]) => - stub.onboardingStateV2(req, obs), - ) - .map { response => - val proto: ByteString = response.onboardingStateForSequencer - PekkoByteString(proto.asReadOnlyByteBuffer()) + try { + // stub acts the client-side proxy to get access to raw grpc commands + val stub = request.createService(channel.channel) + // bridges the gRPC response stream to a Pekko Source and converts the Protobuf ByteString to a Pekko ByteString + val source = ClientAdapter + .serverStreaming( + request + .createRequestInternal() + .getOrElse(throw new IllegalStateException("Unable to create internal request.")), + (req: OnboardingStateV2Request, obs: StreamObserver[OnboardingStateV2Response]) => + stub.onboardingStateV2(req, obs), + ) + .map { response => + val proto: ByteString = response.onboardingStateForSequencer + PekkoByteString(proto.asReadOnlyByteBuffer()) + } + .via( + ZstdGroupedWeight(compressionLevel = 3, minSize = chunkSize.toLong) + ) // 3 is the default zstd compression level + val storageObject = source.runWith(sink) + storageObject.onComplete { _ => + channel.close() } - .via( - ZstdGroupedWeight(compressionLevel = 3, minSize = chunkSize.toLong) - ) // 3 is the default zstd compression level - val storageObject = source.runWith(sink) - storageObject.onComplete { _ => - channel.close() + storageObject + } catch { + // a throw before the onComplete callback is registered would leak the channel + case NonFatal(e) => + channel.close() + Future.failed(e) } - storageObject } /** This is used for initializing the sequencer when the domain is first bootstrapped. @@ -544,7 +531,7 @@ class SequencerAdminConnection( getSequencerId override def isNodeInitialized()(implicit traceContext: TraceContext): Future[Boolean] = { - getStatus.map { + getStatusWithoutRetries.map { case NodeStatus.Failure(_) => false case NodeStatus.NotInitialized(_, _, _) => false case NodeStatus.Success(_) => true diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SequencerBftAdminConnection.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SequencerBftAdminConnection.scala index a9bb045b33..8b283f3da2 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SequencerBftAdminConnection.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SequencerBftAdminConnection.scala @@ -41,7 +41,7 @@ trait SequencerBftAdminConnection { def listConfiguredPeerEndpoints()(implicit tc: TraceContext - ): Future[Seq[P2PEndpoint]] = { + ): Future[Seq[(P2PEndpoint, Option[SequencerId])]] = { runCmd( SequencerBftAdminCommands.ListConfiguredEndpoints ) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SpliceLedgerClient.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SpliceLedgerClient.scala index e5ade0c2c5..1b08274706 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SpliceLedgerClient.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SpliceLedgerClient.scala @@ -64,7 +64,7 @@ class SpliceLedgerClient( ) val channel = builder.build - new LedgerClient(channel, applicationId, getToken, loggerFactory) + new LedgerClient(channel, applicationId, getToken, loggerFactory, timeouts) } private val inactiveContractsCallbacks = new AtomicReference[Seq[String => Unit]](Seq()) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SpliceLedgerConnection.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SpliceLedgerConnection.scala index 6e7f5eb089..44040cfe1e 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SpliceLedgerConnection.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/SpliceLedgerConnection.scala @@ -12,7 +12,8 @@ import com.daml.ledger.javaapi.data.codegen.{ContractId, Created, Exercised, Has import com.daml.ledger.javaapi.data.{Command, CreatedEvent, ExercisedEvent, Transaction, User} import com.digitalasset.base.error.ErrorResource import com.digitalasset.base.error.utils.ErrorDetails -import com.digitalasset.base.error.utils.ErrorDetails.ResourceInfoDetail +import com.digitalasset.base.error.utils.ErrorDetails.{ErrorInfoDetail, ResourceInfoDetail} +import io.grpc.protobuf.{StatusProto as GrpcStatusProto} import com.digitalasset.canton.SynchronizerAlias import com.digitalasset.canton.admin.api.client.data.parties.PartyDetails import com.digitalasset.canton.config.NonNegativeFiniteDuration @@ -35,7 +36,6 @@ import com.google.protobuf.field_mask.FieldMask import io.grpc.{Status, StatusRuntimeException} import org.apache.pekko.NotUsed import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.pattern.CircuitBreakerOpenException import org.apache.pekko.stream.scaladsl.{Flow, Keep, RestartSource, Sink, Source} import org.apache.pekko.stream.{KillSwitch, KillSwitches, RestartSettings} import org.lfdecentralizedtrust.splice.environment.ledger.api.{ @@ -52,6 +52,7 @@ import org.lfdecentralizedtrust.splice.util.{ ContractWithState, DisclosedContracts, SpliceCircuitBreaker, + SpliceCircuitBreakerOpenException, } import shapeless.<:!< @@ -777,6 +778,23 @@ class SpliceLedgerConnection( callCallbacksOnCompletion(result)(x => (None, x)) } + // Returns Some(completionOffset) when the exception is DUPLICATE_COMMAND with accepted=true, + // allowing callers to fetch the already-completed transaction instead of failing. + private def parseDuplicateCommandAccepted(ex: StatusRuntimeException): Option[Long] = { + val statusProto = GrpcStatusProto.fromThrowable(ex) + if (statusProto == null) None + else + ErrorDetails + .from(statusProto) + .collectFirst { + case ErrorInfoDetail(errorCodeId, metadata) + if errorCodeId == "DUPLICATE_COMMAND" && + metadata.get("accepted").contains("true") => + metadata.get("completion_offset").flatMap(co => Try(co.toLong).toOption) + } + .flatten + } + private def verifyEnoughExtraTrafficRemains( synchronizerId: SynchronizerId, commandPriority: CommandPriority, @@ -863,6 +881,7 @@ class SpliceLedgerConnection( priority: CommandPriority, deadline: Option[NonNegativeFiniteDuration] = None, preferredPackageIds: Seq[String] = Seq.empty, + recoverAcceptedDuplicates: Boolean = false, ) { private type DedupNotSpecifiedYet = CmdId =:= Any private type SynchronizerIdRequired = DomId <:< SynchronizerId @@ -873,6 +892,7 @@ class SpliceLedgerConnection( disclosedContracts: DisclosedContracts = this.disclosedContracts, deadline: Option[NonNegativeFiniteDuration] = this.deadline, preferredPackageIds: Seq[String] = this.preferredPackageIds, + recoverAcceptedDuplicates: Boolean = this.recoverAcceptedDuplicates, ): submit[C, CmdId0, DomId0] = new submit( actAs, @@ -884,8 +904,18 @@ class SpliceLedgerConnection( priority, deadline, preferredPackageIds, + recoverAcceptedDuplicates, ) + /** Read an already-accepted duplicate back from the ledger and return its result, rather + * than failing the submission. + * + * For client calls, which cannot do anything useful with the failure. Automation leaves + * this off: a trigger needs the error so that its own retry re-runs the staleness check. + */ + def recoveringAcceptedDuplicates(enabled: Boolean = true): submit[C, CmdId, DomId] = + copy(recoverAcceptedDuplicates = enabled) + def withDedup(commandId: CommandId, deduplicationOffset: Long)(implicit cid: DedupNotSpecifiedYet ): submit[C, (CommandId, Long), DomId] = @@ -990,14 +1020,29 @@ class SpliceLedgerConnection( preferredPackageIds = preferredPackageIds, ) ) - .recover { case ex: CircuitBreakerOpenException => - // Expose a bit more info and turn it into our standard exceptions + .recover { case ex: SpliceCircuitBreakerOpenException => throw Status.ABORTED .withDescription( s"Command submission aborted by circuit breaker due to too many successive failures, next attempt in ${ex.remainingDuration.toSeconds}s" ) + .withCause(ex.getCause) .asRuntimeException } + .recoverWith { + case ex: StatusRuntimeException + if recoverAcceptedDuplicates && + ex.getStatus.getCode == Status.Code.ALREADY_EXISTS => + parseDuplicateCommandAccepted(ex) match { + case Some(completionOffset) => + client.recoverFromDuplicateCommand( + waitFor, + completionOffset, + actAs.map(_.toProtoPrimitive), + ) + case None => + Future.failed(ex) + } + } )(getOffsetAndResult) @annotation.tailrec diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/StatusAdminConnection.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/StatusAdminConnection.scala index 9d5e039ffd..c37fb48304 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/StatusAdminConnection.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/StatusAdminConnection.scala @@ -15,14 +15,18 @@ trait StatusAdminConnection { type Status <: NodeStatus.Status protected def getStatusRequest: GrpcAdminCommand[?, ?, NodeStatus[Status]] - def getStatus(implicit traceContext: TraceContext): Future[NodeStatus[Status]] = + def getStatus(implicit traceContext: TraceContext): Future[NodeStatus[Status]] = { retryProvider.retryForClientCalls( "status", "Get node status", - runCmd( - getStatusRequest - ), + getStatusWithoutRetries, logger, ) + } + def getStatusWithoutRetries(implicit traceContext: TraceContext): Future[NodeStatus[Status]] = { + runCmd( + getStatusRequest + ) + } } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/ledger/api/LedgerClient.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/ledger/api/LedgerClient.scala index 6fd6f0584b..d51253eaef 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/ledger/api/LedgerClient.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/ledger/api/LedgerClient.scala @@ -27,6 +27,7 @@ import com.daml.ledger.javaapi.data.{ CreateUserResponse, ListUserRightsResponse, OffsetCheckpoint, + Transaction, User, } import com.daml.ledger.javaapi.data.codegen.ContractId @@ -37,7 +38,11 @@ import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.IngestionFilter import org.lfdecentralizedtrust.splice.util.DisclosedContracts import com.digitalasset.canton.SynchronizerAlias import com.digitalasset.canton.admin.api.client.data.parties.PartyDetails -import com.digitalasset.canton.config.NonNegativeFiniteDuration +import com.digitalasset.canton.config.{ + NonNegativeDuration, + NonNegativeFiniteDuration, + ProcessingTimeout, +} import com.digitalasset.canton.crypto.Fingerprint import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.ledger.client.GrpcChannel @@ -49,13 +54,14 @@ import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.util.ErrorUtil import com.google.protobuf.{ByteString, Duration} import com.google.protobuf.field_mask.FieldMask -import io.grpc.{Channel, StatusRuntimeException, Status as GrpcStatus} +import io.grpc.{Channel, Deadline, StatusRuntimeException, Status as GrpcStatus} import io.grpc.stub.{AbstractStub, StreamObserver} import org.apache.pekko.NotUsed import org.apache.pekko.stream.scaladsl.Source import java.io.Closeable import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ExecutionContext, Future, Promise} import scala.jdk.CollectionConverters.* @@ -99,6 +105,7 @@ private[environment] class LedgerClient( expectedTokenUser: String, getToken: () => Future[Option[AuthToken]], override protected val loggerFactory: NamedLoggerFactory, + timeouts: ProcessingTimeout, )(implicit esf: ExecutionSequencerFactory, ec: ExecutionContext, @@ -118,17 +125,23 @@ private[environment] class LedgerClient( }) } - private def withCredentialsAndTraceContext[T <: AbstractStub[T]]( - stub: T + private def withGrpcContext[T <: AbstractStub[T]]( + stub: T, + timeout: Option[NonNegativeDuration] = Some(timeouts.default), )(implicit tc: TraceContext): Future[T] = { getToken().map { token => - token.fold(stub) { token => + val authedStub = token.fold(stub) { token => checkTokenUser(token) TraceContextGrpc.addTraceContextToCallOptions( stub .withCallCredentials(new AuthCallCredentials(token.accessToken)) ) } + timeout.map(_.duration) match { + case Some(finite: FiniteDuration) => + authedStub.withDeadline(Deadline.after(finite.length, finite.unit)) + case _ => authedStub + } } } private val commandServiceStub: CommandServiceGrpc.CommandServiceStub = @@ -169,7 +182,7 @@ private[environment] class LedgerClient( ): Future[Long] = { val req = lapi.state_service.GetLedgerEndRequest() for { - stub <- withCredentialsAndTraceContext(stateServiceStub) + stub <- withGrpcContext(stateServiceStub) resp <- stub.getLedgerEnd(req) } yield resp.offset } @@ -179,7 +192,7 @@ private[environment] class LedgerClient( ): Future[Long] = { val req = lapi.state_service.GetLatestPrunedOffsetsRequest() for { - stub <- withCredentialsAndTraceContext(stateServiceStub) + stub <- withGrpcContext(stateServiceStub) resp <- stub.getLatestPrunedOffsets(req) } yield resp.participantPrunedUpToInclusive } @@ -189,7 +202,7 @@ private[environment] class LedgerClient( )(implicit tc: TraceContext): Source[lapi.state_service.GetActiveContractsResponse, NotUsed] = toSource( for { - stub <- withCredentialsAndTraceContext(stateServiceStub) + stub <- withGrpcContext(stateServiceStub, timeout = Some(timeouts.unbounded)) } yield ClientAdapter .serverStreaming(request, stub.getActiveContracts) ) @@ -198,7 +211,7 @@ private[environment] class LedgerClient( tc: TraceContext ): Future[Option[CreatedEvent]] = { (for { - stub <- withCredentialsAndTraceContext(contractServiceStub) + stub <- withGrpcContext(contractServiceStub) contract <- stub.getContract( new lapi.contract_service.GetContractRequest( contractId.contractId, @@ -216,13 +229,44 @@ private[environment] class LedgerClient( )(implicit tc: TraceContext): Source[LedgerClient.GetTreeUpdatesResponse, NotUsed] = { toSource( for { - stub <- withCredentialsAndTraceContext(updateServiceStub) + stub <- withGrpcContext( + updateServiceStub, + timeout = Some(timeouts.unbounded), + ) } yield ClientAdapter .serverStreaming(request.toProto, stub.getUpdates) .mapConcat(GetTreeUpdatesResponse.fromProto) ) } + private[environment] def getTransactionByOffset( + offset: Long, + actAs: Seq[String], + )(implicit tc: TraceContext): Future[Transaction] = { + import lapi.update_service.GetUpdateResponse.Update as U + val updateFormat = LedgerClient.ledgerEffectsUpdateFormat(actAs) + for { + stub <- withGrpcContext(updateServiceStub) + response <- stub.getUpdateByOffset( + lapi.update_service + .GetUpdateByOffsetRequest(offset = offset, updateFormat = Some(updateFormat)) + ) + } yield response.update match { + case U.Transaction(tree) => LedgerClient.lapiTreeToJavaTree(tree) + case other => + throw GrpcStatus.INTERNAL + .withDescription(s"Expected transaction at offset $offset but got $other") + .asRuntimeException() + } + } + + private[environment] def recoverFromDuplicateCommand[W]( + waitFor: SubmitAndWaitFor[W], + completionOffset: Long, + actAs: Seq[String], + )(implicit tc: TraceContext): Future[W] = + waitFor.recoverFromDuplicate(completionOffset, getTransactionByOffset(_, actAs)) + def submitAndWait[Z]( synchronizerId: String, userId: String, @@ -291,7 +335,7 @@ private[environment] class LedgerClient( ) .build() for { - stubWithCredsAndTraceContext <- withCredentialsAndTraceContext(commandServiceStub) + stubWithCredsAndTraceContext <- withGrpcContext(commandServiceStub, Some(timeouts.unbounded)) stub = deadline .map(duration => stubWithCredsAndTraceContext @@ -317,7 +361,7 @@ private[environment] class LedgerClient( tc: TraceContext, ): Future[lapi.interactive.interactive_submission_service.PrepareSubmissionResponse] = { for { - stub <- withCredentialsAndTraceContext(interactiveSubmissionServiceStub) + stub <- withGrpcContext(interactiveSubmissionServiceStub) result <- stub.prepareSubmission( lapi.interactive.interactive_submission_service.PrepareSubmissionRequest( commands = commands.map(c => lapi.commands.Command.fromJavaProto(c.toProtoCommand)), @@ -352,7 +396,7 @@ private[environment] class LedgerClient( tc: TraceContext, ): Future[lapi.interactive.interactive_submission_service.ExecuteSubmissionResponse] = for { - stub <- withCredentialsAndTraceContext(interactiveSubmissionServiceStub) + stub <- withGrpcContext(interactiveSubmissionServiceStub) result <- stub.executeSubmission( lapi.interactive.interactive_submission_service.ExecuteSubmissionRequest( preparedTransaction = Some(preparedTransaction), @@ -385,7 +429,7 @@ private[environment] class LedgerClient( def listPackages()(implicit ec: ExecutionContext, tc: TraceContext): Future[Seq[String]] = { val request = ListPackagesRequest() for { - stub <- withCredentialsAndTraceContext(packageServiceStub) + stub <- withGrpcContext(packageServiceStub) res <- stub .listPackages(request) .map(_.packageIds) @@ -398,7 +442,7 @@ private[environment] class LedgerClient( )(implicit ec: ExecutionContext, tc: TraceContext): Future[Unit] = { val request = v1User.DeleteUserRequest(userId, identityProviderId.getOrElse("")) for { - stub <- withCredentialsAndTraceContext(userManagementServiceStub) + stub <- withGrpcContext(userManagementServiceStub) res <- stub.deleteUser(request).map(_ => ()) } yield res } @@ -418,7 +462,7 @@ private[environment] class LedgerClient( identityProviderId.getOrElse(""), ) for { - stub <- withCredentialsAndTraceContext(userManagementServiceStub) + stub <- withGrpcContext(userManagementServiceStub) res <- stub.listUsers(requestBuilder) } yield ( res.users.map(v1User.User.toJavaProto), @@ -443,7 +487,7 @@ private[environment] class LedgerClient( ): Future[UserManagementServiceOuterClass.User] = { val requestBuilder = v1User.GetUserRequest(userId, identityProviderId.getOrElse("")) for { - stub <- withCredentialsAndTraceContext(userManagementServiceStub) + stub <- withGrpcContext(userManagementServiceStub) res <- stub.getUser(requestBuilder).map(u => v1User.User.toJavaProto(u.getUser)) } yield res } @@ -475,7 +519,7 @@ private[environment] class LedgerClient( )(implicit ec: ExecutionContext, tc: TraceContext): Future[Seq[PartyDetails]] = { val request = GetPartiesRequest(parties.map(_.toProtoPrimitive), "") for { - stub <- withCredentialsAndTraceContext(partyManagementServiceStub) + stub <- withGrpcContext(partyManagementServiceStub) res <- stub .getParties(request) .map(r => r.partyDetails.map(details => PartyDetails.fromProtoPartyDetails(details))) @@ -509,7 +553,7 @@ private[environment] class LedgerClient( initialRights.map(javaRightToV1Right), ) for { - stub <- withCredentialsAndTraceContext(userManagementServiceStub) + stub <- withGrpcContext(userManagementServiceStub) res <- stub .createUser(request) .map(r => CreateUserResponse.fromProto(v1User.CreateUserResponse.toJavaProto(r)).getUser) @@ -560,7 +604,7 @@ private[environment] class LedgerClient( Some(mask), ) for { - stub <- withCredentialsAndTraceContext(userManagementServiceStub) + stub <- withGrpcContext(userManagementServiceStub) res <- stub.updateUser(request) } yield res }.map(_ => ()) @@ -571,7 +615,7 @@ private[environment] class LedgerClient( ): Future[Seq[User.Right]] = { val request = v1User.ListUserRightsRequest(userId, identityProviderId.getOrElse("")) for { - stub <- withCredentialsAndTraceContext(userManagementServiceStub) + stub <- withGrpcContext(userManagementServiceStub) res <- stub .listUserRights(request) .map(r => @@ -598,7 +642,7 @@ private[environment] class LedgerClient( ) for { - stub <- withCredentialsAndTraceContext(userManagementServiceStub) + stub <- withGrpcContext(userManagementServiceStub) res <- stub.grantUserRights(request).map(_ => ()) } yield res } @@ -617,7 +661,7 @@ private[environment] class LedgerClient( "", ) for { - stub <- withCredentialsAndTraceContext(userManagementServiceStub) + stub <- withGrpcContext(userManagementServiceStub) res <- stub.revokeUserRights(request).map(_ => ()) } yield res } @@ -631,7 +675,7 @@ private[environment] class LedgerClient( command: LedgerClient.ReassignmentCommand, )(implicit traceContext: TraceContext): Future[Unit] = for { - stub <- withCredentialsAndTraceContext(commandSubmissionServiceStub) + stub <- withGrpcContext(commandSubmissionServiceStub) res <- stub .submitReassignment( LedgerClient @@ -654,7 +698,10 @@ private[environment] class LedgerClient( )(implicit tc: TraceContext): Source[CompletionStreamResponse, NotUsed] = toSource( for { - stub <- withCredentialsAndTraceContext(multidomainCompletionServiceStub) + stub <- withGrpcContext( + multidomainCompletionServiceStub, + timeout = Some(timeouts.unbounded), + ) } yield ClientAdapter.serverStreaming( lapi.command_completion_service.CompletionStreamRequest( userId = userId, @@ -674,7 +721,7 @@ private[environment] class LedgerClient( "", ) for { - stub <- withCredentialsAndTraceContext(stateServiceStub) + stub <- withGrpcContext(stateServiceStub) res <- stub.getConnectedSynchronizers(req).map { resp => resp.connectedSynchronizers.map { cd => SynchronizerAlias.tryCreate(cd.synchronizerAlias) -> SynchronizerId.tryFromString( @@ -690,7 +737,7 @@ private[environment] class LedgerClient( tc: TraceContext ): Future[Seq[identity_provider_config_service.IdentityProviderConfig]] = { for { - stub <- withCredentialsAndTraceContext(identityProviderConfigServiceStub) + stub <- withGrpcContext(identityProviderConfigServiceStub) res <- stub .listIdentityProviderConfigs( identity_provider_config_service.ListIdentityProviderConfigsRequest() @@ -706,7 +753,7 @@ private[environment] class LedgerClient( audience: String, )(implicit tc: TraceContext): Future[Unit] = { for { - stub <- withCredentialsAndTraceContext(identityProviderConfigServiceStub) + stub <- withGrpcContext(identityProviderConfigServiceStub) _ <- stub.createIdentityProviderConfig( identity_provider_config_service.CreateIdentityProviderConfigRequest( Some( @@ -729,7 +776,7 @@ private[environment] class LedgerClient( vettingAsOfTime: CantonTimestamp, )(implicit tc: TraceContext): Future[Seq[PackageReference]] = { for { - stub <- withCredentialsAndTraceContext(interactiveSubmissionServiceStub) + stub <- withGrpcContext(interactiveSubmissionServiceStub) response <- stub.getPreferredPackages( lapi.interactive.interactive_submission_service.GetPreferredPackagesRequest( packageVettingRequirements = packageRequirements.map { case (pkg, parties) => @@ -806,6 +853,10 @@ object LedgerClient { private[LedgerClient] type RawResponse private[LedgerClient] val stubSubmit: StubSubmit[RawResponse] private[LedgerClient] val mapResponse: RawResponse => Z + private[LedgerClient] def recoverFromDuplicate( + completionOffset: Long, + fetchTransaction: Long => Future[Transaction], + ): Future[Z] } private[environment] object SubmitAndWaitFor { @@ -823,7 +874,7 @@ object LedgerClient { ) .map(r => command_service.SubmitAndWaitResponse.toJavaProto(r))(ec) } - ) + )((offset, _) => Future.successful(offset)) val TransactionTree: SubmitAndWaitFor[jdata.Transaction] = impl((response: CSOC.SubmitAndWaitForTransactionResponse) => @@ -836,7 +887,7 @@ object LedgerClient { ) .map(r => command_service.SubmitAndWaitForTransactionResponse.toJavaProto(r))(ec) } - } + }((offset, fetch) => fetch(offset)) private type StubSubmit[R] = ( CommandServiceGrpc.CommandServiceStub, @@ -846,16 +897,52 @@ object LedgerClient { private[this] def impl[R, Z](mapResponse0: R => Z)( stubSubmit0: StubSubmit[R] + )( + recover0: (Long, Long => Future[jdata.Transaction]) => Future[Z] ): SubmitAndWaitFor[Z] = new SubmitAndWaitFor[Z] { type RawResponse = R override val stubSubmit = stubSubmit0 override val mapResponse = mapResponse0 + override def recoverFromDuplicate( + completionOffset: Long, + fetchTransaction: Long => Future[jdata.Transaction], + ): Future[Z] = recover0(completionOffset, fetchTransaction) } } final case class GetTreeUpdatesResponse( updateOrCheckpoint: TreeUpdateOrOffsetCheckpoint ) + private def wildcardFilter(party: String) = + party -> transaction_filter.Filters( + Seq( + transaction_filter.CumulativeFilter( + transaction_filter.CumulativeFilter.IdentifierFilter + .WildcardFilter(transaction_filter.WildcardFilter(false)) + ) + ) + ) + + private[environment] def ledgerEffectsUpdateFormat( + actAs: Seq[String] + ): transaction_filter.UpdateFormat = + transaction_filter.UpdateFormat( + includeTransactions = Some( + transaction_filter.TransactionFormat( + eventFormat = Some( + transaction_filter.EventFormat( + filtersByParty = actAs.map(wildcardFilter).toMap, + filtersForAnyParty = None, + verbose = false, + ) + ), + transactionShape = transaction_filter.TransactionShape.TRANSACTION_SHAPE_LEDGER_EFFECTS, + ) + ), + includeReassignments = None, + includeTopologyEvents = None, + ) + def lapiTreeToJavaTree( tree: lapi.transaction.Transaction ): com.daml.ledger.javaapi.data.Transaction = { diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/ClientIpDirectives.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/ClientIpDirectives.scala new file mode 100644 index 0000000000..0cea65550d --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/ClientIpDirectives.scala @@ -0,0 +1,50 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.http + +import org.apache.pekko.http.scaladsl.model.headers.`X-Real-Ip` +import org.apache.pekko.http.scaladsl.model.RemoteAddress +import org.apache.pekko.http.scaladsl.server.Directive1 +import org.apache.pekko.http.scaladsl.server.Directives.* + +object ClientIpDirectives { + + /** Extracts the address of the client the request originated from, if it can be determined. + * + * The headers in `clientIpHeaders` are tried in order and the first one that is present and + * yields an IP literal determines the address. Note that headers set by the client itself (such + * as `X-Forwarded-For` and `X-Real-Ip`) can be spoofed unless they are overwritten by a reverse + * proxy the client cannot bypass. + * + * @param clientIpHeaders + * names of the headers carrying the client IP, in order of precedence. Matched + * case-insensitively, as the configured header names are not required to be lowercase. An + * empty list disables the extraction. + */ + def extractClientIp(clientIpHeaders: Seq[String]): Directive1[Option[RemoteAddress]] = + firstDefined(clientIpHeaders.map(_.trim).filter(_.nonEmpty).map(clientIpFromHeader)*) + + private def clientIpFromHeader(headerName: String): Directive1[Option[RemoteAddress]] = + optionalHeaderValueByName(headerName).map(_.flatMap(parseFirstIpLiteral)) + + /** The value of the first directive that extracts a defined value, [[None]] if there is none. */ + private def firstDefined[A]( + directives: Directive1[Option[A]]* + ): Directive1[Option[A]] = + directives.foldRight(provide(Option.empty[A])) { (directive, fallback) => + directive.flatMap { + case defined @ Some(_) => provide(defined) + case None => fallback + } + } + + private def parseFirstIpLiteral(value: String): Option[RemoteAddress] = + value.split(',').headOption.flatMap(parseIpLiteral) + + private def parseIpLiteral(value: String): Option[RemoteAddress] = + `X-Real-Ip` + .parseFromValueString(value.trim) + .toOption + .map(_.address) +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpClient.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpClient.scala index c8eff10a11..a09cfb7fa5 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpClient.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpClient.scala @@ -17,6 +17,8 @@ import org.apache.pekko.http.scaladsl.model.{ HttpHeader, HttpRequest, HttpResponse, + MediaType, + MediaTypes, StatusCode, StatusCodes, } @@ -50,6 +52,37 @@ trait HttpClient { } object HttpClient { + private object ResponseErrorByStatus { + def unapply(resp: HttpResponse): Option[StatusCode] = + resp.status match { + case code @ (StatusCodes.ServerError(_) | StatusCodes.ClientError(_)) => Some(code) + case _ => None + } + } + + private object ResponseErrorByContentType { + private val validContentTypes: Set[MediaType] = Set( + MediaTypes.`application/json`, + MediaTypes.`application/octet-stream`, + MediaTypes.`text/plain`, + ) + + def unapply(resp: HttpResponse): Boolean = + resp.entity.contentType match { + // Responses with `NoContentType` are always considered valid + case ContentTypes.NoContentType => false + // Otherwise a response is valid if its content type is contained in `validContentTypes` + case contentType => !validContentTypes.contains(contentType.mediaType) + } + } + + private def httpFnErrors( + nonErrorStatusCode: Set[StatusCode] + ): PartialFunction[HttpResponse, Unit] = { + case ResponseErrorByStatus(code) if !nonErrorStatusCode.contains(code) => + case ResponseErrorByContentType() => + } + def createHttpFn( clientName: String, operationName: String, @@ -61,10 +94,7 @@ object HttpClient { ): HttpRequest => Future[HttpResponse] = { httpClientWithErrors( httpClient.executeRequest(clientName, operationName), - { - case code @ (StatusCodes.ServerError(_) | StatusCodes.ClientError(_)) - if !nonErrorStatusCode.contains(code) => - }, + httpFnErrors(nonErrorStatusCode), ) } @@ -91,7 +121,7 @@ object HttpClient { private def httpClientWithErrors( nextClient: HttpRequest => Future[HttpResponse], - errors: PartialFunction[StatusCode, Unit], + errors: PartialFunction[HttpResponse, Unit], )( req: HttpRequest )(implicit ec: ExecutionContext, mat: Materializer) = { @@ -102,7 +132,7 @@ object HttpClient { Future.failed[HttpResponse](error) } ) - .applyOrElse(_resp.status, (_: StatusCode) => Future.successful(_resp)) + .applyOrElse(_resp, Future.successful(_: HttpResponse)) } } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiter.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiter.scala index 2dc1c8ad1f..4cf216ee81 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiter.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiter.scala @@ -6,12 +6,16 @@ package org.lfdecentralizedtrust.splice.http import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.logging.TracedLogger -import org.apache.pekko.http.scaladsl.model.{HttpEntity, StatusCodes} -import org.apache.pekko.http.scaladsl.server.Directive0 +import org.apache.pekko.http.scaladsl.model.{HttpEntity, RemoteAddress, StatusCodes} +import org.apache.pekko.http.scaladsl.server.{Directive0, Directive1} import org.lfdecentralizedtrust.splice.config.RateLimitersConfig -import org.lfdecentralizedtrust.splice.util.{SpliceRateLimitMetrics, SpliceRateLimiter} +import org.lfdecentralizedtrust.splice.util.{ + PerAttributeRateLimiter, + SpliceRateLimiter, + SpliceRateLimitMetrics, +} -import java.time.Instant +import java.net.{Inet6Address, InetAddress} class HttpRateLimiter( config: RateLimitersConfig, @@ -19,12 +23,20 @@ class HttpRateLimiter( logger: TracedLogger, ) extends AutoCloseable { - // need to cache it as the pekko reoutes get evaluated for each request - private val rateLimiters = scala.collection.concurrent.TrieMap[String, SpliceRateLimiter]() + // need to cache it as the pekko routes get evaluated for each request + // keyed by (service, operation) as the same operation name can be used by multiple services + private val rateLimiters = + scala.collection.concurrent.TrieMap[ + (String, String), + (SpliceRateLimiter, PerAttributeRateLimiter), + ]() private val metrics = scala.collection.concurrent.TrieMap[String, SpliceRateLimitMetrics]() - def withRateLimit(service: String)(operation: String): Directive0 = { - val rateLimiterMetrics = metrics.getOrElseUpdate( + private val clientIpHeaders: Seq[String] = + config.clientIpHeaders.map(_.trim).filter(_.nonEmpty) + + private def metricsFor(service: String): SpliceRateLimitMetrics = + metrics.getOrElseUpdate( service, SpliceRateLimitMetrics(metricsFactory, logger)( MetricsContext( @@ -32,33 +44,128 @@ class HttpRateLimiter( ) ), ) - val rateLimiter = rateLimiters.getOrElseUpdate( - operation, + + private val globalRateLimiter: (SpliceRateLimiter, PerAttributeRateLimiter) = { + val globalMetrics = metricsFor(HttpRateLimiter.GlobalService) + ( new SpliceRateLimiter( - operation, - config.forRateLimiter(operation), - rateLimiterMetrics, - // the rate limiter has a cold start, to avoid the first request being rejected - // we enforce the rate limit only after 1 second - Instant.now().plusSeconds(1), + HttpRateLimiter.GlobalLimiter, + config.global, + globalMetrics, + ), + new PerAttributeRateLimiter( + HttpRateLimiter.GlobalLimiter, + HttpRateLimiter.ClientIpAttribute, + config.global, + config.global.perClientIp, + globalMetrics, + logger, ), ) + } - import org.apache.pekko.http.scaladsl.server.Directives.* - - extractRequestContext.flatMap { _ => - if (rateLimiter.markRun()) { - pass - } else { - complete( - StatusCodes.TooManyRequests, - HttpEntity( - "Too Many Requests: Server is busy, please try again later." + private def operationRateLimiter( + service: String, + operation: String, + ): (SpliceRateLimiter, PerAttributeRateLimiter) = + rateLimiters.getOrElseUpdate( + (service, operation), { + val rateLimiterMetrics = metricsFor(service) + val operationConfig = config.forRateLimiter(operation) + ( + new SpliceRateLimiter( + operation, + operationConfig, + rateLimiterMetrics, + ), + new PerAttributeRateLimiter( + operation, + HttpRateLimiter.ClientIpAttribute, + operationConfig, + operationConfig.perClientIp, + rateLimiterMetrics, + logger, ), ) + }, + ) + + def withRateLimit(service: String)(operation: String): Directive0 = { + val (globalLimiter, globalClientIpLimiter) = globalRateLimiter + val (operationLimiter, operationClientIpLimiter) = operationRateLimiter(service, operation) + + import org.apache.pekko.http.scaladsl.server.Directives.* + + HttpRateLimiter + .extractClientIpKey(clientIpHeaders) + .flatMap { clientIp => + // The per client IP limiters are checked first (and `&&` short-circuits) so that a request + // rejected because of its own client IP does not consume budget from the shared overall + // limiters. Otherwise a single abusive client could exhaust the overall limits and thereby + // deny service to all other clients. + // Within each of those two groups the narrower per operation limiter is checked before the + // global one, so that a request rejected for its operation does not consume global budget. + val allowed = + operationClientIpLimiter.markRun(clientIp) && + globalClientIpLimiter.markRun(clientIp) && + operationLimiter.markRun() && + globalLimiter.markRun() + if (allowed) { + pass + } else { + complete( + StatusCodes.TooManyRequests, + HttpEntity( + "Too Many Requests: Server is busy, please try again later." + ), + ) + } } - } } def close(): Unit = metrics.view.values.foreach(_.close()) } + +object HttpRateLimiter { + + private val ClientIpAttribute = "client_ip" + + private[splice] val GlobalLimiter = "global" + private[splice] val GlobalService = "global" + + private[splice] def extractClientIpKey( + clientIpHeaders: Seq[String] = RateLimitersConfig.DefaultClientIpHeaders + ): Directive1[Option[String]] = + ClientIpDirectives + .extractClientIp(clientIpHeaders) + .map(_.collect { case RemoteAddress.IP(ip, _) => rateLimitKey(ip) }) + + /** Single clients are typically assigned a whole IPv6 /64 (or larger) network, so limiting per + * full IPv6 address would allow a single client to trivially bypass the per client IP limit. + * IPv6 addresses are therefore grouped by their /64 prefix, IPv4 addresses are used as is. + */ + private def rateLimitKey(address: InetAddress): String = address match { + case ipv6: Inet6Address => + val bytes = ipv6.getAddress + ipv4Mapped(bytes) match { + // connections accepted on a dual stack socket can report IPv4 clients as IPv4-mapped IPv6 + // addresses (e.g. ::ffff:192.0.2.1), those must use the same key as the plain IPv4 address + case Some(ipv4) => ipv4.getHostAddress + case None => + // zero out the lower 64 bits (the interface identifier), keeping the /64 network prefix + // note that this also drops any scope/zone id, which is not meaningful for rate limiting + val prefix = bytes.take(8) ++ Array.fill[Byte](8)(0) + s"${InetAddress.getByAddress(prefix).getHostAddress}/64" + } + case ip => ip.getHostAddress + } + + /** ::ffff:0:0/96, see [[https://www.rfc-editor.org/rfc/rfc4291#section-2.5.5.2]] */ + private val Ipv4MappedPrefix: Seq[Byte] = + Seq.fill[Byte](10)(0) ++ Seq[Byte](0xff.toByte, 0xff.toByte) + + private def ipv4Mapped(bytes: Array[Byte]): Option[InetAddress] = + Option.when(bytes.length == 16 && bytes.startsWith(Ipv4MappedPrefix))( + InetAddress.getByAddress(bytes.drop(12)) + ) +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/metrics/ScanConnectionMetrics.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/metrics/ScanConnectionMetrics.scala index a4ac00a1f1..a388fab313 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/metrics/ScanConnectionMetrics.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/metrics/ScanConnectionMetrics.scala @@ -38,7 +38,9 @@ class ScanConnectionMetrics(metricsFactory: LabeledMetricsFactory) { summary = "Count of succeeded and failed requests to a scan connection", qualification = Traffic, labelsWithDescription = perConnectionLabels ++ Map( - "outcome" -> "Category of failure or success" + "outcome" -> "Category of failure or success", + "http_status" -> ("For failures, the HTTP status code of the response when available, " + + "'none' otherwise (e.g. transport-level failures). Absent for successful requests."), ), ) ) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/DbVotesStoreQueryBuilder.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/DbVotesStoreQueryBuilder.scala index bc602a8b3e..cf5227001a 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/DbVotesStoreQueryBuilder.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/DbVotesStoreQueryBuilder.scala @@ -23,47 +23,25 @@ trait DbVotesTxLogStoreQueryBuilder[TXE] with LimitHelpers with NamedLogging { - def listVoteRequestResultsQuery( - txLogTableName: String, - txLogStoreId: TxLogStoreId, + private def voteRequestResultsConditions( dbType: String3, actionNameColumnName: String, acceptedColumnName: String, requesterNameColumnName: String, - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], - limit: Limit, - after: Option[Long] = None, - ): SqlStreamingAction[Vector[ - TxLogQueries.SelectFromTxLogTableResult - ], TxLogQueries.SelectFromTxLogTableResult, Effect.Read] = { - // Sort key: the vote's effective date, falling back to the result's completedAt for non-accepted votes that have none. - val effectiveAtSortKey = - "coalesce(vote_effective_at, entry_data->'result'->>'completedAt')" - val afterCondition = after match { - case Some(a) => - Some( - // Keyset pagination past the previous page's last entry_number `a`. Lexicographical row comparison, - // expands to: effectiveAt < cursorEffectiveAt OR (effectiveAt = cursorEffectiveAt AND entry_number < a). - sql"""(#$effectiveAtSortKey, entry_number) < ((select #$effectiveAtSortKey from #$txLogTableName where store_id = $txLogStoreId and entry_number = $a), $a)""" - ) - case None => None - } - val actionNameCondition = actionName match { + filters: VoteResultsFilters, + ) = { + val actionNameCondition = filters.actionName match { case Some(actionName) => Some(sql"""#$actionNameColumnName like ${lengthLimited( s"%${lengthLimited(actionName)}%" )}""") case None => None } - val executedCondition = accepted match { + val executedCondition = filters.accepted match { case Some(accepted) => Some(sql"""#$acceptedColumnName = ${accepted}""") case None => None } - val effectivenessCondition = (effectiveFrom, effectiveTo) match { + val effectivenessCondition = (filters.effectiveFrom, filters.effectiveTo) match { case (Some(effectiveFrom), Some(effectiveTo)) => Some(sql"""vote_effective_at between ${lengthLimited( effectiveFrom @@ -76,23 +54,56 @@ trait DbVotesTxLogStoreQueryBuilder[TXE] Some(sql"""vote_effective_at < ${lengthLimited(effectiveTo)}""") case (None, None) => None } - val requesterCondition = requester match { + val requesterCondition = filters.requester match { case Some(requester) => Some(sql"""#$requesterNameColumnName like ${lengthLimited( s"%${lengthLimited(requester)}%" )}""") case None => None } - val conditions = NonEmptyList( + NonEmptyList( sql"""entry_type = ${dbType}""", List( actionNameCondition, executedCondition, requesterCondition, effectivenessCondition, - afterCondition, ).flatten, ) + } + + def listVoteRequestResultsQuery( + txLogTableName: String, + txLogStoreId: TxLogStoreId, + dbType: String3, + actionNameColumnName: String, + acceptedColumnName: String, + requesterNameColumnName: String, + filters: VoteResultsFilters, + limit: Limit, + after: Option[Long] = None, + ): SqlStreamingAction[Vector[ + TxLogQueries.SelectFromTxLogTableResult + ], TxLogQueries.SelectFromTxLogTableResult, Effect.Read] = { + // Sort key: the vote's effective date, falling back to the result's completedAt for non-accepted votes that have none. + val effectiveAtSortKey = + "coalesce(vote_effective_at, entry_data->'result'->>'completedAt')" + val afterCondition = after match { + case Some(a) => + Some( + // Keyset pagination past the previous page's last entry_number `a`. Lexicographical row comparison, + // expands to: effectiveAt < cursorEffectiveAt OR (effectiveAt = cursorEffectiveAt AND entry_number < a). + sql"""(#$effectiveAtSortKey, entry_number) < ((select #$effectiveAtSortKey from #$txLogTableName where store_id = $txLogStoreId and entry_number = $a), $a)""" + ) + case None => None + } + val conditions = voteRequestResultsConditions( + dbType, + actionNameColumnName, + acceptedColumnName, + requesterNameColumnName, + filters, + ) ++ afterCondition.toList val whereClause = conditions.reduceLeft((a, b) => (a ++ sql""" and """ ++ b).toActionBuilder) selectFromTxLogTable( @@ -103,6 +114,26 @@ trait DbVotesTxLogStoreQueryBuilder[TXE] sql"""order by #$effectiveAtSortKey desc, entry_number desc limit ${sqlLimit(limit)}""", ) } + + def countVoteRequestResultsQuery( + txLogTableName: String, + txLogStoreId: TxLogStoreId, + dbType: String3, + actionNameColumnName: String, + acceptedColumnName: String, + requesterNameColumnName: String, + filters: VoteResultsFilters, + ): SqlStreamingAction[Vector[Long], Long, Effect.Read] = { + val whereClause = voteRequestResultsConditions( + dbType, + actionNameColumnName, + acceptedColumnName, + requesterNameColumnName, + filters, + ).reduceLeft((a, b) => (a ++ sql""" and """ ++ b).toActionBuilder) + (sql"""select count(*) from #$txLogTableName where store_id = $txLogStoreId and """ ++ whereClause).toActionBuilder + .as[Long] + } } /** All column names will be unsafely interpolated, as they're expected to be constant strings. diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/HistoryMetrics.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/HistoryMetrics.scala index 1e4193c680..7bdb6fe322 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/HistoryMetrics.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/HistoryMetrics.scala @@ -438,11 +438,15 @@ class HistoryMetrics(metricsFactory: LabeledMetricsFactory)(implicit ) )(metricsContext) - def incAcsSnapshotObjects(): Unit = - objectsCount.inc()(MetricsContext("object_type" -> "ACS_snapshots")) + def incAcsSnapshotObjects(encoding: String, bucket: String): Unit = + objectsCount.inc()( + MetricsContext("object_type" -> "ACS_snapshots", "encoding" -> encoding, "bucket" -> bucket) + ) - def incUpdateObjects(): Unit = - objectsCount.inc()(MetricsContext("object_type" -> "updates")) + def incUpdateObjects(encoding: String, bucket: String): Unit = + objectsCount.inc()( + MetricsContext("object_type" -> "updates", "encoding" -> encoding, "bucket" -> bucket) + ) def incUpdatesCount(count: Int): Unit = updatesCount.inc(count.toLong)(MetricsContext.Empty) diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/IgnoredPartiesStore.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/IgnoredPartiesStore.scala similarity index 85% rename from apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/IgnoredPartiesStore.scala rename to apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/IgnoredPartiesStore.scala index de73bfed60..c082ce12e9 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/IgnoredPartiesStore.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/IgnoredPartiesStore.scala @@ -1,13 +1,14 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package org.lfdecentralizedtrust.splice.sv.store +package org.lfdecentralizedtrust.splice.store import com.digitalasset.canton.topology.PartyId import java.util.concurrent.ConcurrentHashMap import scala.jdk.CollectionConverters.* +// TODO(#6817): Remove in-memory in favor of DbUnavailablePartiesStore class IgnoredPartiesStore(initialParties: Set[PartyId]) { private val parties: ConcurrentHashMap.KeySetView[PartyId, java.lang.Boolean] = { diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/IngestionSummary.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/IngestionSummary.scala index 8e818d04ac..7ff7b168fe 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/IngestionSummary.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/IngestionSummary.scala @@ -21,7 +21,7 @@ import scala.collection.mutable final case class IngestionSummary( offset: Option[Long], synchronizerIdToRecordTime: Map[SynchronizerId, CantonTimestamp], - newAcsSize: Int, + acsSizeDiff: Int, ingestedCreatedEvents: Vector[CreatedEvent], numFilteredCreatedEvents: Int, ingestedArchivedEvents: Vector[ExercisedEvent], @@ -43,7 +43,7 @@ private[store] object IngestionSummary { private val Empty: IngestionSummary = IngestionSummary( offset = None, synchronizerIdToRecordTime = Map.empty, - newAcsSize = 0, + acsSizeDiff = 0, ingestedCreatedEvents = Vector.empty, updatedContractStates = Vector.empty, numFilteredCreatedEvents = 0, @@ -69,7 +69,7 @@ private[store] object IngestionSummary { prettyNode( "", // intentionally left empty, as that worked better in the log messages above paramIfDefined("offset", _.offset), - param("newAcsSize", _.newAcsSize), + param("acsSizeDiff", _.acsSizeDiff), param("synchronizerIdToRecordTime", _.synchronizerIdToRecordTime), paramIfNonEmpty("ingestedCreatedEvents", _.ingestedCreatedEvents), paramIfNonZero("numFilteredCreatedEvents", _.numFilteredCreatedEvents), @@ -122,12 +122,16 @@ case class MutableIngestionSummary( def toIngestionSummary( synchronizerIdToRecordTime: Map[SynchronizerId, CantonTimestamp], offset: Long, - newAcsSize: Int, + acsSizeDiff: Int, metrics: StoreMetrics, ): IngestionSummary = { // We update the metrics in here as it's the easiest way // to not miss any place that might need updating. - metrics.acsSize.updateValue(newAcsSize.toLong) + if (acsSizeDiff > 0) { + metrics.acsSizeIncrease.mark(acsSizeDiff.toLong)(MetricsContext.Empty) + } else if (acsSizeDiff < 0) { + metrics.acsSizeDecrease.mark(-acsSizeDiff.toLong)(MetricsContext.Empty) + } metrics.ingestedTxLogEntries.mark(ingestedTxLogEntries.size.toLong)(MetricsContext.Empty) metrics.eventCount.inc(this.ingestedCreatedEvents.length.toLong)( MetricsContext("event_type" -> "created") @@ -144,7 +148,7 @@ case class MutableIngestionSummary( IngestionSummary( offset = Some(offset), synchronizerIdToRecordTime = synchronizerIdToRecordTime, - newAcsSize = newAcsSize, + acsSizeDiff = acsSizeDiff, ingestedCreatedEvents = this.ingestedCreatedEvents.toVector, numFilteredCreatedEvents = this.numFilteredCreatedEvents, ingestedArchivedEvents = this.ingestedArchivedEvents.toVector, diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/MultiDomainAcsStore.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/MultiDomainAcsStore.scala index 280f64fa57..5961cc6f65 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/MultiDomainAcsStore.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/MultiDomainAcsStore.scala @@ -196,7 +196,9 @@ trait MultiDomainAcsStore extends HasIngestionSink with AutoCloseable with Named ): Future[Seq[Contract[TCid, T]]] private[splice] def listExpiredFromPayloadExpiry[C, TCid <: ContractId[T], T <: Template]( - companion: C + companion: C, + ignoredPartiesStore: Option[IgnoredPartiesStore] = None, + ignoredPartyFields: Seq[String] = Seq.empty, )(implicit companionClass: ContractCompanion[C, TCid, T] ): ListExpiredContracts[TCid, T] diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/S3BucketConnection.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/S3BucketConnection.scala index 44a8c89caa..66b0c89c50 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/S3BucketConnection.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/S3BucketConnection.scala @@ -117,27 +117,39 @@ class S3BucketConnection( def getChecksums( objectKeys: Seq[String] - )(implicit ec: ExecutionContext, as: ActorSystem): Future[Seq[ObjectKeyAndChecksum]] = { + )(implicit + ec: ExecutionContext, + as: ActorSystem, + tc: TraceContext, + ): Future[Seq[ObjectKeyAndChecksum]] = { Source(objectKeys.toList) .mapAsync(4) { key => // TODO(#3429): make this parallelism configurable readChecksum(key) - .map(checksum => ObjectKeyAndChecksum(key, checksum)) + .map(checksum => checksum.map(ObjectKeyAndChecksum(key, _))) } + .collect { case Some(obj) => obj } .runWith(Sink.seq[ObjectKeyAndChecksum]) } - private def readChecksum(key: String)(implicit ec: ExecutionContext): Future[String] = { + private def readChecksum( + key: String + )(implicit ec: ExecutionContext, tc: TraceContext): Future[Option[String]] = { val headRequest = HeadObjectRequest .builder() .bucket(bucketName) .key(key) .build() for { - head <- s3Client.headObject(headRequest).asScala - checksum = head - .metadata() - .asScala - .getOrElse("splice-checksum", throw new RuntimeException("Missing checksum metadata")) + head <- s3Client.headObject(headRequest).asScala.map(Some(_)).recover { case e => + // TODO(#3429): distinguish between "object not found" and other errors, probably want to catch only NoSuchKeyException, and throw everything else + logger + .debug(s"Failed to read checksum for object $key, object may not exist: ${e.getMessage}") + None + } + checksum = head.map( + _.metadata().asScala + .getOrElse("splice-checksum", throw new RuntimeException("Missing checksum metadata")) + ) } yield checksum } @@ -210,6 +222,14 @@ class S3BucketConnection( private val parts = TrieMap.empty[Integer, CompletedPart] private val md = MessageDigest.getInstance("SHA-256") + /** The checksum of the whole object. Computing it via a `lazy val` to support idempotent `finish()` calls. + */ + private lazy val objectChecksum: String = Base64.getEncoder.encodeToString(md.digest()) + + /** `lazy val` to ensure that multi-part upload is completed at most once. + */ + private lazy val finishResult: Future[Unit] = doFinish() + /** Call this once before uploading a new part. * The content must already be provided for checksums, but will not be uploaded yet. */ @@ -261,7 +281,12 @@ class S3BucketConnection( } } - def finish(): Future[Unit] = { + /** Completes the multi-part upload and stores the object checksum in the object's metadata. + * Idempotent, safe to call more than once (will just return the Future from the first call again). + */ + def finish(): Future[Unit] = finishResult + + private def doFinish(): Future[Unit] = { require(numParts.get() > 0) require( parts.size == numParts.get(), @@ -285,7 +310,7 @@ class S3BucketConnection( _ <- s3Client.completeMultipartUpload(completeRequest).asScala // Copy-in-place of the object to add the final checksum to its metadata - metadata = Map("splice-checksum" -> Base64.getEncoder.encodeToString(md.digest())) + metadata = Map("splice-checksum" -> objectChecksum) copyReq = CopyObjectRequest .builder() diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/StoreMetrics.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/StoreMetrics.scala index bba1e1a35f..a1a15a539d 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/StoreMetrics.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/StoreMetrics.scala @@ -59,15 +59,28 @@ class StoreMetrics(metricsFactory: LabeledMetricsFactory)(metricsContext: Metric ) ) - val acsSize: Gauge[Long] = - metricsFactory.gauge( + // we track increase and decrease separately as promql's increase doesn't work for counters that go up and down + // and delta will not handle resets to 0 after a restart properly. + val acsSizeIncrease: Meter = + metricsFactory.meter( MetricInfo( - name = prefix :+ "acs-size", - summary = "The number of active contracts in this store", + name = prefix :+ "acs-size-increase", + summary = "Counter for the number of active contracts added to the store", Traffic, - "The number of active contracts in this store. Note that this is only in the given store. The participant might have contracts we do not ingest.", - ), - 0L, + "Counter for the number of active contracts added to the store. This is _not_ an absolute value of the size, it can only be used to track changes. Note that for an individual transaction this is netted against acsSizeDecrease so ony one of the two will increase. Note that this is only in the given store. The participant might have contracts we do not ingest.", + ) + )(metricsContext) + + // we track increase and decrease separately as promql's increase doesn't work for counters that go up and down + // and delta will not handle resets to 0 after a restart properly. + val acsSizeDecrease: Meter = + metricsFactory.meter( + MetricInfo( + name = prefix :+ "acs-size-decrease", + summary = "Counter for the number of active contracts removed to the store", + Traffic, + "Counter for the number of active contracts removed from the store. This is _not_ an absolute value of the size, it can only be used to track changes. Note that for an individual transaction this is netted against acsSizeIncrease so ony one of the two will increase. Note that this is only in the given store. The participant might have contracts we do not ingest.", + ) )(metricsContext) val ingestedTxLogEntries: Meter = metricsFactory.meter( @@ -158,7 +171,6 @@ class StoreMetrics(metricsFactory: LabeledMetricsFactory)(metricsContext: Metric } override def close(): Unit = { - acsSize.close() perSynchronizerLastIngestedRecordTimeMs.values.foreach(_.close()) perSynchronizerLastSeenRecordTimeMs.values.foreach(_.close()) } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UnavailablePartiesStore.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UnavailablePartiesStore.scala new file mode 100644 index 0000000000..06acc54fd4 --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UnavailablePartiesStore.scala @@ -0,0 +1,25 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.store + +import com.digitalasset.canton.topology.PartyId +import com.digitalasset.canton.tracing.TraceContext + +import scala.concurrent.Future + +trait UnavailablePartiesStore { + + // Adds or updates parties + def addParties(parties: Seq[PartyId], nowMicros: Long)(implicit tc: TraceContext): Future[Unit] + + // Removes specific parties from the store + def removeParties(parties: Seq[PartyId])(implicit tc: TraceContext): Future[Int] + + // Removes parties from the table with matching store ID + def removePartiesUpToStoreId(storeId: Long)(implicit tc: TraceContext): Future[Int] + + // Lists parties that are being ignored + def listParties(nowMicros: Long)(implicit tc: TraceContext): Future[Seq[PartyId]] + +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UpdateHistory.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UpdateHistory.scala index 11e1e3b111..3554789a01 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UpdateHistory.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UpdateHistory.scala @@ -28,6 +28,7 @@ import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.{ } import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.{HasIngestionSink, IngestionFilter} import org.lfdecentralizedtrust.splice.store.db.{AcsJdbcTypes, AcsQueries} +import db.AsUpdateReturning.* import org.lfdecentralizedtrust.splice.util.{ Contract, DomainRecordTimeRange, @@ -39,6 +40,8 @@ import com.digitalasset.canton.config.CantonRequireTypes.String256M import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.CloseContext import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.logging.pretty.Pretty +import com.digitalasset.canton.logging.pretty.Pretty.{param, prettyOfClass} import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.topology.{ParticipantId, PartyId, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext @@ -905,14 +908,14 @@ class UpdateHistory( } private def afterFilters( - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], includeImportUpdates: Boolean, ): NonEmptyList[SQLActionBuilder] = { val gtMin = if (includeImportUpdates) ">=" else ">" afterO match { case None => NonEmptyList.of(sql"migration_id >= 0 and record_time #$gtMin ${CantonTimestamp.MinValue}") - case Some((afterMigrationId, afterRecordTime)) => + case Some(TimestampWithMigrationId(afterRecordTime, afterMigrationId)) => // This makes it so that the two queries use updt_hist_tran_hi_mi_rt_di, NonEmptyList.of( sql"migration_id = ${afterMigrationId} and record_time > ${afterRecordTime} ", @@ -1100,7 +1103,7 @@ class UpdateHistory( } def getUpdatesWithoutImportUpdates( - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], limit: Limit, )(implicit tc: TraceContext): Future[Seq[TreeUpdateWithMigrationId]] = { val filters = afterFilters(afterO, includeImportUpdates = false) @@ -1131,7 +1134,7 @@ class UpdateHistory( } def getAllUpdates( - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], limit: PageLimit, )(implicit tc: TraceContext): Future[Seq[TreeUpdateWithMigrationId]] = { val filters = afterFilters(afterO, includeImportUpdates = true) @@ -2594,6 +2597,12 @@ final case class TimestampWithMigrationId( object TimestampWithMigrationId { implicit val ordering: Ordering[TimestampWithMigrationId] = Ordering.by(x => (x.migrationId, x.timestamp)) + + implicit val prettyTimestampWithMigrationId: Pretty[TimestampWithMigrationId] = + prettyOfClass( + param("timestamp", _.timestamp), + param("migrationId", _.migrationId), + ) } final case class TreeUpdateWithMigrationId( diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/VotesStore.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/VotesStore.scala index c293f1be99..7f91f5f958 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/VotesStore.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/VotesStore.scala @@ -124,18 +124,28 @@ trait ActiveVotesStore extends AppStore with DsoRulesStore with HasAmuletRules { } +final case class VoteResultsFilters( + actionName: Option[String] = None, + accepted: Option[Boolean] = None, + requester: Option[String] = None, + effectiveFrom: Option[String] = None, + effectiveTo: Option[String] = None, +) + trait VotesStore extends ActiveVotesStore { def listVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: Limit = defaultLimit, after: Option[Long] = None, )(implicit tc: TraceContext ): Future[ResultsPage[DsoRules_CloseVoteRequestResult]] + def countVoteRequestResults( + filters: VoteResultsFilters + )(implicit + tc: TraceContext + ): Future[Long] + } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/bulk/ZstdGroupedWeight.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/bulk/ZstdGroupedWeight.scala index 66edda79fd..4d9cd8997f 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/bulk/ZstdGroupedWeight.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/bulk/ZstdGroupedWeight.scala @@ -9,6 +9,8 @@ import org.apache.pekko.stream.stage.{GraphStage, GraphStageLogic, InHandler, Ou import org.apache.pekko.stream.{Attributes, FlowShape, Inlet, Outlet} import org.apache.pekko.util.ByteString +import scala.util.control.NonFatal + /** A Pekko GraphStage that zstd-compresses a stream of bytestrings, and splits the output into zstd objects of size (minWeight + delta). * Somewhat similar to Pekko's built-in GroupedWeight, but outputs valid zstd compressed objects. */ @@ -47,15 +49,25 @@ case class ZstdGroupedWeight( val bufferAllocator = PooledByteBufAllocator.DEFAULT val tmpBuffer = bufferAllocator.directBuffer(zstdTmpBufferSize) - val tmpNioBuffer = tmpBuffer.nioBuffer(0, tmpBuffer.capacity()) - val compressingStream = - new ZstdDirectBufferCompressingStreamNoFinalizer(tmpNioBuffer, compressionLevel) + val (tmpNioBuffer, compressingStream) = + try { + val nioBuffer = tmpBuffer.nioBuffer(0, tmpBuffer.capacity()) + (nioBuffer, new ZstdDirectBufferCompressingStreamNoFinalizer(nioBuffer, compressionLevel)) + } catch { + // a failed construction never reaches close(), so release the buffer here + case NonFatal(e) => + val _ = tmpBuffer.release() + throw e + } def compress(input: ByteString): ByteString = { val inputBB = bufferAllocator.directBuffer(input.size) - inputBB.writeBytes(input.toArrayUnsafe()) - compressingStream.compress(inputBB.nioBuffer()) - inputBB.release() + try { + inputBB.writeBytes(input.toArrayUnsafe()) + compressingStream.compress(inputBB.nioBuffer()) + } finally { + val _ = inputBB.release() + } compressingStream.flush() tmpNioBuffer.flip() val result = ByteString.fromByteBuffer(tmpNioBuffer) @@ -72,8 +84,11 @@ case class ZstdGroupedWeight( } override def close(): Unit = { - compressingStream.close() - val _ = tmpBuffer.release() + try { + compressingStream.close() + } finally { + val _ = tmpBuffer.release() + } } } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsJdbcTypes.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsJdbcTypes.scala index 9eb7b7732c..4ec07e59c7 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsJdbcTypes.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsJdbcTypes.scala @@ -10,7 +10,7 @@ import com.daml.ledger.javaapi.data.codegen.{ContractId, DamlRecord, DefinedData import com.digitalasset.canton.config.CantonRequireTypes.{String2066, String3, String300} import com.digitalasset.canton.data.{CantonTimestamp, Offset} import com.digitalasset.canton.daml.lf.value.json.ApiCodecCompressed -import com.digitalasset.canton.topology.{Member, PartyId, SynchronizerId} +import com.digitalasset.canton.topology.{Member, SynchronizerId} import com.digitalasset.daml.lf.data.Ref.HexString import com.digitalasset.daml.lf.data.Time.Timestamp import com.google.protobuf.ByteString @@ -32,14 +32,12 @@ import com.digitalasset.canton.LfValue import com.digitalasset.canton.logging.ErrorLoggingContext import spray.json.{JsString, JsValue, JsonFormat, deserializationError} -import java.sql.{JDBCType, PreparedStatement, ResultSet} +import java.sql.{PreparedStatement, ResultSet} import java.io.StringWriter -trait AcsJdbcTypes { +trait AcsJdbcTypes extends JdbcTypes { import AcsJdbcTypes.JsonString - val profile: slick.jdbc.JdbcProfile - import profile.api.* protected implicit lazy val byteArrayGetResult: GetResult[Array[Byte]] = @@ -194,13 +192,6 @@ trait AcsJdbcTypes { (timestamps: Array[CantonTimestamp], pp: PositionedParameters) => longArraySetParameter(timestamps.map(_.toMicros), pp) - protected implicit lazy val stringArraySetParameter: SetParameter[Array[String]] = - (strings: Array[String], pp: PositionedParameters) => - pp.setObject( - pp.ps.getConnection.createArrayOf("text", strings.map(x => x)), - JDBCType.ARRAY.getVendorTypeNumber, - ) - protected implicit lazy val stringSeqSetParameter: SetParameter[Seq[String]] = (strings: Seq[String], pp: PositionedParameters) => stringArraySetParameter(strings.toArray, pp) @@ -208,10 +199,6 @@ trait AcsJdbcTypes { (strings: Array[String3], pp: PositionedParameters) => stringArraySetParameter(strings.map(_.str), pp) - protected implicit lazy val string2066ArraySetParameter: SetParameter[Array[String2066]] = - (strings: Array[String2066], pp: PositionedParameters) => - stringArraySetParameter(strings.map(_.str), pp) - protected implicit lazy val string2066SeqSetParameter: SetParameter[Seq[String2066]] = (strings: Seq[String2066], pp: PositionedParameters) => stringArraySetParameter(strings.map(_.str).toArray, pp) @@ -224,12 +211,6 @@ trait AcsJdbcTypes { (ids: Array[ContractId[?]], pp: PositionedParameters) => stringArraySetParameter(ids.map(_.contractId), pp) - protected implicit def partyIdGetResult[T]: GetResult[PartyId] = - GetResult.GetString.andThen(PartyId.tryFromProtoPrimitive) - - protected implicit def partyIdGetResultOption[T]: GetResult[Option[PartyId]] = - GetResult.GetStringOption.andThen(_.map(PartyId.tryFromProtoPrimitive)) - protected implicit lazy val offsetJdbcType: JdbcType[Offset] = MappedColumnType.base[Offset, String]( offset => LegacyOffset.fromLong(offset.unwrap).toHexString, @@ -273,19 +254,6 @@ trait AcsJdbcTypes { protected implicit lazy val synchronizerIdJdbcType: JdbcType[SynchronizerId] = MappedColumnType.base[SynchronizerId, String](_.toProtoPrimitive, SynchronizerId.tryFromString) - protected implicit lazy val partyIdJdbcType: JdbcType[PartyId] = - MappedColumnType.base[PartyId, String](_.toProtoPrimitive, PartyId.tryFromProtoPrimitive) - - protected implicit lazy val partyIdSetParameterOption: SetParameter[Option[PartyId]] = - (partyId: Option[PartyId], pp: PositionedParameters) => - implicitly[SetParameter[Option[String2066]]] - .apply(partyId.map(party => lengthLimited(party.toProtoPrimitive)), pp) - - protected implicit lazy val partyIdSetParameterArray: SetParameter[Array[PartyId]] = - (partyId: Array[PartyId], pp: PositionedParameters) => - implicitly[SetParameter[Array[String2066]]] - .apply(partyId.map(party => lengthLimited(party.toProtoPrimitive)), pp) - protected implicit lazy val memberIdSetParameter: SetParameter[Member] = (memberId: Member, pp: PositionedParameters) => implicitly[SetParameter[String300]].apply(memberId.toLengthLimitedString, pp) @@ -375,11 +343,6 @@ trait AcsJdbcTypes { data: DefinedDataType[?] ): Json = AcsJdbcTypes.payloadJsonFromDefinedDataType(data) - /** The DB may truncate strings of unbounded length, so it's advised to use a LengthLimitedString instead. - * We use String2066 because it's the max length of an [[com.digitalasset.canton.protocol.LfTemplateId]]. - */ - protected def lengthLimited(s: String): String2066 = String2066.tryCreate(s) - private def lengthLimitedByteString(bs: ByteString, maxLength: Int): ByteString = { require( bs.size() <= maxLength, diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsQueries.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsQueries.scala index b3c05fb390..16ce755ef4 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsQueries.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsQueries.scala @@ -6,7 +6,6 @@ package org.lfdecentralizedtrust.splice.store.db import com.daml.ledger.javaapi.data.Identifier import com.daml.ledger.javaapi.data.codegen.ContractId import com.digitalasset.canton.resource.DbStorage.Implicits.BuilderChain.toSQLActionBuilderChain -import com.digitalasset.canton.resource.DbStorage.SQLActionBuilderChain import com.digitalasset.canton.topology.{PartyId, SynchronizerId} import com.digitalasset.daml.lf.data.Time.Timestamp import com.google.protobuf.ByteString @@ -25,13 +24,11 @@ import org.lfdecentralizedtrust.splice.util.PrettyInstances.* import scalaz.{@@, Tag} import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton import slick.jdbc.canton.SQLActionBuilder -import slick.jdbc.{GetResult, PositionedResult, SetParameter} +import slick.jdbc.{GetResult, PositionedResult} import slick.dbio.Effect import slick.sql.SqlStreamingAction -import scala.reflect.ClassTag - -trait AcsQueries extends AcsJdbcTypes { +trait AcsQueries extends Queries with AcsJdbcTypes { /** @param tableName Must be SQL-safe, as it needs to be interpolated unsafely. * This is fine, as all calls to this method should use static string constants. @@ -267,39 +264,6 @@ trait AcsQueries extends AcsJdbcTypes { ) } - /** Constructions like `seq.mkString("(", ",", ")")` are dangerous because they can lead to SQL injection. - * Prefer using this instead. - */ - protected def sqlCommaSeparated( - seq: Iterable[SQLActionBuilder] - ): SQLActionBuilderChain = { - seq - .map(SQLActionBuilderChain(_)) - .reduceOption { (acc, next) => - acc ++ sql"," ++ next - } - .getOrElse(SQLActionBuilderChain(sql"")) - } - - /* - * TODO(#3900) move to use toInClause when canton fork has it: https://github.com/canton-network/splice/issues/3900 - */ - protected def inClause[V: ClassTag]( - field: String, - seq: Iterable[V], - )(implicit - arraySetParameter: SetParameter[Array[V]] - ): SQLActionBuilder = - sql" #$field = ANY(${seq.toArray[V]})" - - protected def notInClause[V: ClassTag]( - field: String, - seq: Iterable[V], - )(implicit - arraySetParameter: SetParameter[Array[V]] - ): SQLActionBuilder = - sql" NOT (#$field = ANY(${seq.toArray[V]}))" - protected def contractFromRow[C, TCId <: ContractId[?], T](companion: C)( row: AcsQueries.SelectFromAcsTableResult )(implicit diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLockIds.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLockIds.scala index ea5543b472..58d789fea4 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLockIds.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLockIds.scala @@ -16,4 +16,5 @@ object AdvisoryLockIds { private val base: Long = 0x73706c00 final val acsSnapshotDataInsert: Long = base + 1 + final val ddlStatement: Long = base + 2 } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLocks.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLocks.scala new file mode 100644 index 0000000000..1201db3c21 --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLocks.scala @@ -0,0 +1,63 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.store.db + +import slick.dbio.{DBIOAction, Effect, NoStream} +import slick.jdbc.JdbcProfile +import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton + +import scala.concurrent.ExecutionContext + +object AdvisoryLocks { + final case class FailedToAcquireLockException( + lockType: String, + lockId: Long, + ) extends RuntimeException(s"Failed to acquire $lockType advisory lock $lockId.") + + private def withLock[T, E <: Effect](lockType: String, lockId: Long)( + acquire: DBIOAction[Boolean, NoStream, Effect.Read], + onAcquired: DBIOAction[T, NoStream, E], + )(implicit ec: ExecutionContext): DBIOAction[T, NoStream, Effect.Read & E] = + acquire.flatMap(acquired => + if (acquired) onAcquired + else DBIOAction.failed(FailedToAcquireLockException(lockType, lockId)) + ) + + private def acquireSessionLock(lockId: Long): DBIOAction[Boolean, NoStream, Effect.Read] = + sql"select pg_try_advisory_lock($lockId)".as[Boolean].head + + private def releaseSessionLock(lockId: Long): DBIOAction[Boolean, NoStream, Effect.Read] = + sql"select pg_advisory_unlock($lockId)".as[Boolean].head + + /** Wraps the given action in a session-scoped advisory lock; useful for acquiring locks for + * queries like DDL that can't run in a transaction. + */ + def withSessionLock[T, E <: Effect](lockId: Long, action: DBIOAction[T, NoStream, E])(implicit + ec: ExecutionContext + ): DBIOAction[T, NoStream, Effect.Read & E] = + withLock("session-scoped", lockId)( + acquireSessionLock(lockId), + action.andFinally(releaseSessionLock(lockId)), + ).withPinnedSession + + def withDdlLock[T, E <: Effect](action: DBIOAction[T, NoStream, E])(implicit + ec: ExecutionContext + ): DBIOAction[T, NoStream, Effect.Read & E] = + withSessionLock(AdvisoryLockIds.ddlStatement, action) + + private def acquireTransactionalLock(lockId: Long): DBIOAction[Boolean, NoStream, Effect.Read] = + sql"SELECT pg_try_advisory_xact_lock($lockId)".as[Boolean].head + + /** Wraps the given action in a transactional advisory lock. */ + def withTransactionalLock[T, E <: Effect]( + profile: JdbcProfile, + lockId: Long, + action: DBIOAction[T, NoStream, E], + )(implicit + ec: ExecutionContext + ): DBIOAction[T, NoStream, Effect.Read & Effect.Transactional & E] = { + import profile.api.jdbcActionExtensionMethods + withLock("transactional", lockId)(acquireTransactionalLock(lockId), action).transactionally + } +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AsUpdateReturning.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AsUpdateReturning.scala new file mode 100644 index 0000000000..95bf108fc3 --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AsUpdateReturning.scala @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.store.db + +import slick.dbio.Effect +import slick.jdbc.GetResult +import slick.jdbc.canton.SQLActionBuilder +import slick.sql.SqlStreamingAction + +/** Syntax for running data-modifying statements that also return rows, such as + * PostgreSQL's `insert ... returning`, `insert ... on conflict do nothing returning`, + * and `delete ... returning`. + */ +private[store] object AsUpdateReturning { + + implicit class `SQLActionBuilder asUpdateReturning`(private val builder: SQLActionBuilder) + extends AnyVal { + + /** Run this statement as one that both writes and reads back rows. + * + * Neither of the combinators that come with [[slick.jdbc.canton.SQLActionBuilder]] + * fits `... returning ...` statements: + * + * - `asUpdate` yields the JDBC update count as a single `Int` and throws + * away the result set. + * - `as[R]` does decode the result set, but types the action as + * `Effect.Read` alone, which would allow its usage with unsafe + * combinators. + * + * So this simply relabels the read action as also writing by widening the + * result of `as[R]`. + */ + def asUpdateReturning[R](implicit + rconv: GetResult[R] + ): SqlStreamingAction[Vector[R], R, Effect.Read & Effect.Write] = + builder.as[R] + } +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbMultiDomainAcsStore.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbMultiDomainAcsStore.scala index c5e8825705..470d5e445b 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbMultiDomainAcsStore.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbMultiDomainAcsStore.scala @@ -37,7 +37,7 @@ import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.resource.DbStorage -import com.digitalasset.canton.topology.SynchronizerId +import com.digitalasset.canton.topology.{PartyId, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.showPretty @@ -54,6 +54,7 @@ import org.lfdecentralizedtrust.splice.store.db.AcsQueries.{ SelectFromAcsTableWithStateResult, } import org.lfdecentralizedtrust.splice.store.db.AcsTables.ContractStateRowData +import AsUpdateReturning.* import com.daml.nonempty.NonEmpty import com.digitalasset.canton.data.CantonTimestamp import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory @@ -378,9 +379,23 @@ final class DbMultiDomainAcsStore[TXE]( override private[splice] def listExpiredFromPayloadExpiry[C, TCid <: ContractId[ T - ], T <: Template](companion: C)(implicit + ], T <: Template]( + companion: C, + ignoredPartiesStore: Option[IgnoredPartiesStore] = None, + ignoredPartyFields: Seq[String] = Seq.empty, + )(implicit companionClass: ContractCompanion[C, TCid, T] ): ListExpiredContracts[TCid, T] = { (now, limit) => implicit traceContext => + val ignoredParties = ignoredPartiesStore.fold(Set.empty[PartyId])(_.getAll) + val ignoredPartiesFilter: SQLActionBuilder = + if (ignoredParties.isEmpty || ignoredPartyFields.isEmpty) sql"" + else + ignoredPartyFields.foldLeft(sql"") { (acc, field) => + (acc ++ sql" and " ++ notInClause( + s"acs.create_arguments->>'$field'", + ignoredParties, + )).toActionBuilder + } for { _ <- waitUntilAcsIngested() result <- storage @@ -390,7 +405,8 @@ final class DbMultiDomainAcsStore[TXE]( acsStoreId, domainMigrationId, companion, - additionalWhere = sql"""and acs.contract_expires_at < $now""", + additionalWhere = + (sql"""and acs.contract_expires_at < $now""" ++ ignoredPartiesFilter).toActionBuilder, orderLimit = sql"""limit ${sqlLimit(limit)}""", ), "listExpiredFromPayloadExpiry", @@ -977,21 +993,6 @@ final class DbMultiDomainAcsStore[TXE]( case Some(descriptor) => initializeDescriptor(descriptor).map(TxLogStoreId.subst) case None => Future.successful(StoreNotUsed[TxLogStoreId]()) } - - acsSizeInDb <- acsInitResult match { - case StoreHasData(acsStoreId, _) => - storage - .querySingle( - sql""" - select count(*) - from #$acsTableName - where store_id = ${acsStoreId} and migration_id = $domainMigrationId - """.as[Int].headOption, - "initialize.getAcsCount", - ) - .getOrElse(0) - case _ => FutureUnlessShutdown.pure(0) - } } yield { def initState( acsStoreId: AcsStoreId, @@ -1004,7 +1005,6 @@ final class DbMultiDomainAcsStore[TXE]( _.withInitialState( acsStoreId = acsStoreId, txLogStoreId = txLogStoreId, - acsSizeInDb = acsSizeInDb, lastIngestedOffset = lastIngestedOffset, ) ) @@ -1166,46 +1166,43 @@ final class DbMultiDomainAcsStore[TXE]( // This is fine because all clients are expected to use [[waitUntilAcsIngested()]] to avoid // reading ACS data before it has finished ingesting. _ <- clearDataForCurrentMigrationId() - acsSize <- source.runWith( - Sink.foldAsync[Int, Seq[BaseLedgerConnection.ActiveContractsItem]](0) { - case (acsSizeSoFar, batch) => - val summaryState = MutableIngestionSummary.empty - logger.debug( - s"Ingesting ACS batch with size: ${batch.size}, total ingested size so far: $acsSizeSoFar" - ) - metrics.ingestionTimePerACSBatch - .timeFuture { - ingestAcsBatch( - offset, - batch.collect { case ActiveContractsItem.ActiveContract(contract) => - contract - }, - batch.collect { case ActiveContractsItem.IncompleteUnassign(unassign) => - unassign - }, - batch.collect { case ActiveContractsItem.IncompleteAssign(assign) => assign }, - summaryState, + _ <- source.runWith( + Sink.foreachAsync[Seq[BaseLedgerConnection.ActiveContractsItem]](1) { batch => + val summaryState = MutableIngestionSummary.empty + logger.debug( + s"Ingesting ACS batch with size: ${batch.size}" + ) + metrics.ingestionTimePerACSBatch + .timeFuture { + ingestAcsBatch( + offset, + batch.collect { case ActiveContractsItem.ActiveContract(contract) => + contract + }, + batch.collect { case ActiveContractsItem.IncompleteUnassign(unassign) => + unassign + }, + batch.collect { case ActiveContractsItem.IncompleteAssign(assign) => assign }, + summaryState, + ) + } + .map { _ => + val summary = summaryState + .toIngestionSummary( + synchronizerIdToRecordTime = Map.empty, + offset = offset, + acsSizeDiff = summaryState.acsSizeDiff, + metrics = metrics, ) - } - .map { _ => - val newAcsSize = summaryState.acsSizeDiff + acsSizeSoFar - val summary = summaryState - .toIngestionSummary( - synchronizerIdToRecordTime = Map.empty, - offset = offset, - newAcsSize = newAcsSize, - metrics = metrics, - ) - handleIngestionSummary(summary) - logger.debug(show"Ingested ACS batch $summary") - newAcsSize - } + handleIngestionSummary(summary) + logger.debug(show"Ingested ACS batch $summary") + } } ) // A store is considered initialized if the last ingested offset is set // Therefore, we must do that after the ACS is ingested, // so that in case of failure the whole ACS ingestion will be retried. - _ <- markAcsIngestedAsOf(offset, acsSize) + _ <- markAcsIngestedAsOf(offset) } yield () } } @@ -1367,13 +1364,13 @@ final class DbMultiDomainAcsStore[TXE]( } } - private def markAcsIngestedAsOf(offset: Long, acsSize: Int)(implicit + private def markAcsIngestedAsOf(offset: Long)(implicit traceContext: TraceContext ): Future[Unit] = { storage.update(updateOffset(offset), "markAcsIngestedAsOf").map { _ => state .getAndUpdate( - _.withUpdate(acsSize, offset) + _.withUpdate(offset) ) .signalOffsetChanged(offset) @@ -1413,7 +1410,6 @@ final class DbMultiDomainAcsStore[TXE]( state .getAndUpdate(s => s.withUpdate( - s.acsSize + summaryState.acsSizeDiff, lastTree.getOffset, synchronizerIdToRecordTime.toMap, ) @@ -1423,7 +1419,7 @@ final class DbMultiDomainAcsStore[TXE]( summaryState.toIngestionSummary( offset = lastTree.getOffset, synchronizerIdToRecordTime = synchronizerIdToRecordTime.toMap, - newAcsSize = state.get().acsSize, + acsSizeDiff = summaryState.acsSizeDiff, metrics = metrics, ) logger.debug( @@ -1442,7 +1438,6 @@ final class DbMultiDomainAcsStore[TXE]( state .getAndUpdate(s => s.withUpdate( - s.acsSize + summaryState.acsSizeDiff, reassignment.offset, reassignmentRecordTimes, ) @@ -1452,7 +1447,7 @@ final class DbMultiDomainAcsStore[TXE]( summaryState.toIngestionSummary( synchronizerIdToRecordTime = reassignmentRecordTimes, offset = reassignment.offset, - newAcsSize = state.get().acsSize, + acsSizeDiff = summaryState.acsSizeDiff, metrics = metrics, ) logger.debug(show"Ingested reassignment $summary") @@ -1472,13 +1467,13 @@ final class DbMultiDomainAcsStore[TXE]( ) .map { _ => state - .getAndUpdate(s => s.withUpdate(s.acsSize, offset, synchronizerIdToRecordTime)) + .getAndUpdate(s => s.withUpdate(offset, synchronizerIdToRecordTime)) .signalWaiters(offset, synchronizerIdToRecordTime) val summary = MutableIngestionSummary.empty.toIngestionSummary( synchronizerIdToRecordTime = synchronizerIdToRecordTime, offset = offset, - newAcsSize = state.get().acsSize, + acsSizeDiff = 0, metrics = metrics, ) logger.debug(show"Ingested offset checkpoint $offset") @@ -2234,7 +2229,6 @@ object DbMultiDomainAcsStore { /** @param acsStoreId The primary key of this stores ACS entry in the store_descriptors table * @param txLogStoreId The primary key of this stores TxLog entry in the store_descriptors table * @param offset The last ingested offset, if any - * @param acsSize The number of active contracts in the store * @param offsetChanged A promise that is not yet completed, and will be completed the next time the offset changes * @param offsetIngestionsToSignal A map from offsets to promises. The keys are offsets that are not ingested yet. * The values are promises that are not completed, and will be completed when @@ -2246,7 +2240,6 @@ object DbMultiDomainAcsStore { acsStoreId: Option[AcsStoreId], txLogStoreId: Option[TxLogStoreId], offset: Option[Long], - acsSize: Int, offsetChanged: Promise[Unit], offsetIngestionsToSignal: SortedMap[Long, Promise[Unit]], lastIngestedRecordTimes: Map[SynchronizerId, CantonTimestamp], @@ -2255,7 +2248,6 @@ object DbMultiDomainAcsStore { def withInitialState( acsStoreId: AcsStoreId, txLogStoreId: Option[TxLogStoreId], - acsSizeInDb: Int, lastIngestedOffset: Option[Long], ): State = { assert( @@ -2268,14 +2260,12 @@ object DbMultiDomainAcsStore { this.copy( acsStoreId = Some(acsStoreId), txLogStoreId = txLogStoreId, - acsSize = acsSizeInDb, offset = lastIngestedOffset, offsetChanged = nextOffsetChanged, ) } def withUpdate( - newAcsSize: Int, newOffset: Long, recordTimes: Map[SynchronizerId, CantonTimestamp] = Map.empty, ): State = { @@ -2295,7 +2285,6 @@ object DbMultiDomainAcsStore { } } this.copy( - acsSize = newAcsSize, offset = Some(newOffset), offsetChanged = nextOffsetChanged, offsetIngestionsToSignal = offsetIngestionsToSignal.filter { case (offsetToSignal, _) => @@ -2388,7 +2377,6 @@ object DbMultiDomainAcsStore { acsStoreId = None, txLogStoreId = None, offset = None, - acsSize = 0, offsetChanged = Promise(), offsetIngestionsToSignal = SortedMap.empty, lastIngestedRecordTimes = Map.empty, diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbUnavailablePartiesStore.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbUnavailablePartiesStore.scala new file mode 100644 index 0000000000..e12f9ce6f2 --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbUnavailablePartiesStore.scala @@ -0,0 +1,121 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.store.db + +import com.digitalasset.canton.config.NonNegativeFiniteDuration +import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.lifecycle.CloseContext +import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.resource.DbStorage +import com.digitalasset.canton.topology.PartyId +import com.digitalasset.canton.tracing.TraceContext +import org.lfdecentralizedtrust.splice.store.UnavailablePartiesStore +import org.lfdecentralizedtrust.splice.util.FutureUnlessShutdownUtil.futureUnlessShutdownToFuture +import slick.jdbc.JdbcProfile +import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton + +import scala.concurrent.{ExecutionContext, Future} + +class DbUnavailablePartiesStore( + storage: DbStorage, + val storeId: Int, + baseDuration: NonNegativeFiniteDuration, + maxIgnoreDuration: NonNegativeFiniteDuration, + val loggerFactory: NamedLoggerFactory, +)(implicit + val ec: ExecutionContext, + val loggingContext: ErrorLoggingContext, + val closeContext: CloseContext, +) extends UnavailablePartiesStore + with Queries + with NamedLogging { + + val profile: JdbcProfile = storage.profile.jdbc + + private val baseMicros = baseDuration.underlying.toMicros + private val maxMicros = maxIgnoreDuration.underlying.toMicros + + /** Adds or updates parties only outside the ignore window. + * a. For new parties, it sets updated_at to now and the ignore_duration to base_duration. + * b. For existing parties, it updates updated_at to now and doubles the ignore_duration (up to max_ignore_duration) + */ + def addParties(parties: Seq[PartyId], nowMicros: Long)(implicit + tc: TraceContext + ): Future[Unit] = + if (parties.isEmpty) Future.unit + else { + val partyArray = parties.distinct.toArray + logger.debug(s"Marking ${partyArray.length} parties as unavailable at $nowMicros") + storage + .update( + sql"""insert into dso_unavailable_parties + (party, updated_at, ignore_duration, store_id) + select u.party, $nowMicros, $baseMicros, $storeId + from unnest($partyArray) as u(party) + on conflict (party) do update + set updated_at = excluded.updated_at, + ignore_duration = least( + dso_unavailable_parties.ignore_duration * 2, + $maxMicros) + where dso_unavailable_parties.updated_at + dso_unavailable_parties.ignore_duration <= excluded.updated_at + """.asUpdate, + "addParties", + ) + .map(_.discard) + } + + // Removes specific parties from the table upon successful transaction processing. + def removeParties(parties: Seq[PartyId])(implicit tc: TraceContext): Future[Int] = + if (parties.isEmpty) Future.successful(0) + else { + val partyArray = parties.distinct.toArray + storage.update( + sqlu"""delete from dso_unavailable_parties where party = any($partyArray)""", + "removeParties", + ) + } + + // Removes parties from the table with matching store ID. + def removePartiesUpToStoreId(maxStoreId: Long)(implicit tc: TraceContext): Future[Int] = + storage.update( + sqlu"""delete from dso_unavailable_parties where store_id <= $maxStoreId""", + "removePartiesUpToStoreId", + ) + + // List all parties for which updated_at + ignore_duration > now. + def listParties(nowMicros: Long)(implicit tc: TraceContext): Future[Seq[PartyId]] = + storage.query( + sql"""select party + from dso_unavailable_parties + where updated_at + ignore_duration > $nowMicros""".as[PartyId], + "listParties", + ) + +} + +object DbUnavailablePartiesStore { + def apply( + storeDescriptor: StoreDescriptor, + storage: DbStorage, + baseDuration: NonNegativeFiniteDuration, + maxIgnoreDuration: NonNegativeFiniteDuration, + loggerFactory: NamedLoggerFactory, + )(implicit + ec: ExecutionContext, + lc: ErrorLoggingContext, + cc: CloseContext, + tc: TraceContext, + ): Future[DbUnavailablePartiesStore] = + StoreDescriptorStore + .getStoreIdForDescriptor(storeDescriptor, storage) + .map(storeId => + new DbUnavailablePartiesStore( + storage, + storeId, + baseDuration, + maxIgnoreDuration, + loggerFactory, + ) + ) +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/JdbcTypes.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/JdbcTypes.scala new file mode 100644 index 0000000000..6e4b98b32d --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/JdbcTypes.scala @@ -0,0 +1,52 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.store.db + +import com.digitalasset.canton.config.CantonRequireTypes.String2066 +import com.digitalasset.canton.topology.PartyId +import slick.jdbc.* + +import java.sql.JDBCType + +trait JdbcTypes { + + val profile: slick.jdbc.JdbcProfile + import profile.api.* + + /** The DB may truncate strings of unbounded length, so it's advised to use a LengthLimitedString instead. + * We use String2066 because it's the max length of an [[com.digitalasset.canton.protocol.LfTemplateId]]. + */ + protected def lengthLimited(s: String): String2066 = String2066.tryCreate(s) + + protected implicit def partyIdGetResult[T]: GetResult[PartyId] = + GetResult.GetString.andThen(PartyId.tryFromProtoPrimitive) + + protected implicit def partyIdGetResultOption[T]: GetResult[Option[PartyId]] = + GetResult.GetStringOption.andThen(_.map(PartyId.tryFromProtoPrimitive)) + + protected implicit lazy val partyIdJdbcType: JdbcType[PartyId] = + MappedColumnType.base[PartyId, String](_.toProtoPrimitive, PartyId.tryFromProtoPrimitive) + + protected implicit lazy val partyIdSetParameterOption: SetParameter[Option[PartyId]] = + (partyId: Option[PartyId], pp: PositionedParameters) => + implicitly[SetParameter[Option[String2066]]] + .apply(partyId.map(party => lengthLimited(party.toProtoPrimitive)), pp) + + protected implicit lazy val partyIdSetParameterArray: SetParameter[Array[PartyId]] = + (partyId: Array[PartyId], pp: PositionedParameters) => + implicitly[SetParameter[Array[String2066]]] + .apply(partyId.map(party => lengthLimited(party.toProtoPrimitive)), pp) + + protected implicit lazy val stringArraySetParameter: SetParameter[Array[String]] = + (strings: Array[String], pp: PositionedParameters) => + pp.setObject( + pp.ps.getConnection.createArrayOf("text", strings.map(x => x)), + JDBCType.ARRAY.getVendorTypeNumber, + ) + + protected implicit lazy val string2066ArraySetParameter: SetParameter[Array[String2066]] = + (strings: Array[String2066], pp: PositionedParameters) => + stringArraySetParameter(strings.map(_.str), pp) + +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/Queries.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/Queries.scala new file mode 100644 index 0000000000..1a55b834e8 --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/Queries.scala @@ -0,0 +1,48 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.store.db + +import com.digitalasset.canton.resource.DbStorage.SQLActionBuilderChain +import slick.jdbc.SetParameter +import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton +import slick.jdbc.canton.SQLActionBuilder + +import scala.reflect.ClassTag + +trait Queries extends JdbcTypes { + + /** Constructions like `seq.mkString("(", ",", ")")` are dangerous because they can lead to SQL injection. + * Prefer using this instead. + */ + protected def sqlCommaSeparated( + seq: Iterable[SQLActionBuilder] + ): SQLActionBuilderChain = { + seq + .map(SQLActionBuilderChain(_)) + .reduceOption { (acc, next) => + acc ++ sql"," ++ next + } + .getOrElse(SQLActionBuilderChain(sql"")) + } + + /* + * TODO(#3900) move to use toInClause when canton fork has it: https://github.com/canton-network/splice/issues/3900 + */ + protected def inClause[V: ClassTag]( + field: String, + seq: Iterable[V], + )(implicit + arraySetParameter: SetParameter[Array[V]] + ): SQLActionBuilder = + sql" #$field = ANY(${seq.toArray[V]})" + + protected def notInClause[V: ClassTag]( + field: String, + seq: Iterable[V], + )(implicit + arraySetParameter: SetParameter[Array[V]] + ): SQLActionBuilder = + sql" NOT (#$field = ANY(${seq.toArray[V]}))" + +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/DarResourcesUtil.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/DarResourcesUtil.scala index 3cb46ac417..45eef8fabe 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/DarResourcesUtil.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/DarResourcesUtil.scala @@ -9,7 +9,7 @@ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.daml.lf.data.Ref.{PackageName, PackageVersion} import org.lfdecentralizedtrust.splice.environment.DarResource import org.lfdecentralizedtrust.splice.environment.DarResources.{ - packageResources, + corePackageResources, pkgIdToDarResource, pkgMetadataToDarResource, } @@ -18,12 +18,12 @@ object DarResourcesUtil extends NamedLogging { override protected def loggerFactory: NamedLoggerFactory = NamedLoggerFactory.root - val minimalPackageVersions: Seq[DarResource] = packageResources.flatMap(pkg => + val minimalPackageVersions: Seq[DarResource] = corePackageResources.flatMap(pkg => pkg.all.filter(p => p.metadata.version == pkg.minimumInitialization.metadata.version) ) val supportedPackageVersions: Seq[DarResource] = - packageResources.flatMap(pkg => + corePackageResources.flatMap(pkg => pkg.all.filter(p => p.metadata.version >= pkg.minimumInitialization.metadata.version) ) @@ -37,7 +37,7 @@ object DarResourcesUtil extends NamedLogging { pkgMetadataToDarResource.get((name, version)) def lookupAllPackageVersions(name: PackageName): Seq[DarResource] = - packageResources.view.flatMap(_.all).toSeq.filter(_.metadata.name == name) + corePackageResources.view.flatMap(_.all).toSeq.filter(_.metadata.name == name) // TODO(canton-network/splice#4049): remove `enableUnsupportedDarsUnvetting` once not needed anymore def getRequiredPackageVersions( @@ -61,7 +61,7 @@ object DarResourcesUtil extends NamedLogging { false } } - packageResources.view + corePackageResources.view .flatMap(_.all) .toSeq .filter(_.metadata.name == name) @@ -86,7 +86,7 @@ object DarResourcesUtil extends NamedLogging { packageConfigMap: Map[PackageName, PackageVersion], )(implicit tc: TraceContext): Seq[DarResource] = { val allSupportedVersionsPackageIds = - packageResources + corePackageResources .flatMap { pkg => val versionFromAmuletRules = packageConfigMap.getOrElse( pkg.latest.metadata.name, @@ -107,7 +107,7 @@ object DarResourcesUtil extends NamedLogging { } private def lookupMinimumPackageResource(name: PackageName): DarResource = - packageResources + corePackageResources .find(_.latest.metadata.name == name) .getOrElse(throw new NoSuchElementException(s"Could not find PackageResource for $name.")) .minimumInitialization diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/PackageVetting.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/PackageVetting.scala index 978f396b1f..7936cb0e05 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/PackageVetting.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/PackageVetting.scala @@ -152,7 +152,6 @@ class PackageVetting( .map(_ => ()) } - // See https://github.com/DACH-NY/canton/issues/29834: make it work for non-sv validators as well def unvetPackages( domainId: SynchronizerId, additionalPackagesToUnvet: Map[PackageName, Set[PackageVersion]], diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceCircuitBreaker.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceCircuitBreaker.scala index 8ca808a8f8..c6146bdb1e 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceCircuitBreaker.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceCircuitBreaker.scala @@ -21,13 +21,15 @@ import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import io.grpc.StatusRuntimeException import org.apache.pekko.actor.Scheduler -import org.apache.pekko.pattern.{CircuitBreaker, CircuitBreakerOpenException} +import org.apache.pekko.pattern.CircuitBreaker import org.lfdecentralizedtrust.splice.config.CircuitBreakerConfig import java.util.concurrent.atomic.AtomicReference +import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} +@SuppressWarnings(Array("org.wartremover.warts.Null")) class SpliceCircuitBreaker( name: String, config: CircuitBreakerConfig, @@ -40,6 +42,7 @@ class SpliceCircuitBreaker( ) extends NamedLogging { private val lastFailure: AtomicReference[Option[CantonTimestamp]] = new AtomicReference(None) + private val lastException: AtomicReference[Option[Throwable]] = new AtomicReference(None) private val errorCategoriesToIgnore: Set[ErrorCategory] = Set( InvalidIndependentOfSystemState, @@ -53,7 +56,7 @@ class SpliceCircuitBreaker( LockedContracts ) - val underlying = new CircuitBreaker( + private val underlying: CircuitBreaker = new CircuitBreaker( scheduler, maxFailures = config.maxFailures, callTimeout = config.callTimeout.underlying, @@ -63,7 +66,8 @@ class SpliceCircuitBreaker( randomFactor = config.randomFactor, ).onOpen { logger.warn( - s"Circuit breaker $name tripped after ${config.maxFailures} failures" + s"Circuit breaker $name tripped after ${config.maxFailures} failures. Attaching last failure", + lastException.get().orNull, )(TraceContext.empty) }.onHalfOpen { logger.info(s"Circuit breaker $name moving to half-open state")(TraceContext.empty) @@ -76,9 +80,10 @@ class SpliceCircuitBreaker( callAndMark(body) } else { Future.failed( - new CircuitBreakerOpenException( + new SpliceCircuitBreakerOpenException( underlying.resetTimeout, s"Circuit breaker $name is open, calls are failing fast", + lastException.get().orNull, ) ) } @@ -106,8 +111,11 @@ class SpliceCircuitBreaker( if (!isFailureIgnored(exception)) { underlying.fail() lastFailure.set(Some(clock.now)) + lastException.set(Some(exception)) } - case Success(_) => underlying.succeed() + case Success(_) => + underlying.succeed() + lastException.set(None) } } @@ -157,3 +165,9 @@ object SpliceCircuitBreaker { loggerFactory, ) } + +class SpliceCircuitBreakerOpenException( + val remainingDuration: FiniteDuration, + message: String, + cause: Throwable, +) extends RuntimeException(message, cause) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiter.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiter.scala index 6d6f66019d..c1885094dd 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiter.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiter.scala @@ -3,22 +3,31 @@ package org.lfdecentralizedtrust.splice.util +import com.daml.metrics.CacheMetrics import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory import com.daml.metrics.api.MetricQualification.Saturation import com.daml.metrics.api.{MetricHandle, MetricInfo, MetricsContext} +import com.digitalasset.canton.caching.{CaffeineCache, ConcurrentCache} import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.LifeCycle import com.digitalasset.canton.logging.TracedLogger -import com.google.common.util.concurrent.RateLimiter +import com.digitalasset.canton.tracing.TraceContext +import com.github.benmanes.caffeine.cache.{Caffeine, RemovalCause, RemovalListener} +import com.google.common.util.concurrent.{BurstyRateLimiterFactory, RateLimiter} import org.lfdecentralizedtrust.splice.environment.SpliceMetrics -import java.time.Instant +import java.time.Duration import java.util import java.util.Collections +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong import scala.concurrent.Future import scala.jdk.CollectionConverters.CollectionHasAsScala -case class SpliceRateLimitMetrics(otelFactory: LabeledMetricsFactory, logger: TracedLogger)(implicit +case class SpliceRateLimitMetrics( + otelFactory: LabeledMetricsFactory, + private val logger: TracedLogger, +)(implicit mc: MetricsContext ) extends AutoCloseable { @@ -32,6 +41,17 @@ case class SpliceRateLimitMetrics(otelFactory: LabeledMetricsFactory, logger: Tr ) ) + val unknownAttributeNotLimited: MetricHandle.Meter = otelFactory.meter( + MetricInfo( + SpliceMetrics.MetricsPrefix :+ "rate_limiting_unknown_attribute_not_limited", + "Number of requests not rate limited by a per-attribute limiter because the attribute value is unknown", + Saturation, + ) + ) + + def recordUnknownAttributeNotLimited()(implicit extraMc: MetricsContext): Unit = + unknownAttributeNotLimited.mark()(mc.merge(extraMc)) + /*we need to pass the full context when we create it to avoid duplicate values warnings*/ def recordMaxLimit(limit: Double)(implicit extraMc: MetricsContext): Unit = { val createdGauge = otelFactory.gauge[Double]( @@ -53,39 +73,116 @@ case class SpliceRateLimitMetrics(otelFactory: LabeledMetricsFactory, logger: Tr } -case class SpliceRateLimitConfig( +sealed trait SpliceRateLimitConfig { + + def enabled: Boolean + + def ratePerSecond: Double + + def sustainedRatePerSecond: Option[Double] + + def sustainedWindowSeconds: Long +} + +object SpliceRateLimitConfig { + + final case class Simple( + enabled: Boolean = true, + ratePerSecond: Double, + sustainedRatePerSecond: Option[Double] = None, + sustainedWindowSeconds: Long = SpliceRateLimiter.DefaultSustainedWindowSeconds, + ) extends SpliceRateLimitConfig + + final case class WithPerClientIp( + enabled: Boolean = true, + ratePerSecond: Double, + sustainedRatePerSecond: Option[Double] = None, + sustainedWindowSeconds: Long = SpliceRateLimiter.DefaultSustainedWindowSeconds, + perClientIp: PerAttributeRateLimitConfig = PerAttributeRateLimitConfig.Disabled, + ) extends SpliceRateLimitConfig + + def apply( + enabled: Boolean = true, + ratePerSecond: Double, + sustainedRatePerSecond: Option[Double] = None, + sustainedWindowSeconds: Long = SpliceRateLimiter.DefaultSustainedWindowSeconds, + ): Simple = + Simple(enabled, ratePerSecond, sustainedRatePerSecond, sustainedWindowSeconds) +} + +case class PerAttributeRateLimitConfig( enabled: Boolean = true, - ratePerSecond: Double, -) + limit: SpliceRateLimitConfig.Simple = PerAttributeRateLimitConfig.DefaultLimit, + maxAttributeValues: Long = 10000, +) { + + def rateLimitFor(overall: SpliceRateLimitConfig): SpliceRateLimitConfig.Simple = + limit.copy(enabled = enabled && limit.enabled && overall.enabled) +} + +object PerAttributeRateLimitConfig { + val DefaultLimit: SpliceRateLimitConfig.Simple = SpliceRateLimitConfig(ratePerSecond = 10) + val Disabled: PerAttributeRateLimitConfig = PerAttributeRateLimitConfig(enabled = false) +} +object SpliceRateLimiter { + + val GlobalLimiterType = "global" + val PerAttributeLimiterType = "per-attribute" + + val DefaultSustainedWindowSeconds: Long = 60 + + private[util] def sustainedWindow(config: SpliceRateLimitConfig): Duration = + Duration.ofSeconds(Math.max(1L, config.sustainedWindowSeconds)) +} + +// noinspection UnstableApiUsage class SpliceRateLimiter( name: String, config: SpliceRateLimitConfig, metrics: SpliceRateLimitMetrics, - enforceAfter: Instant = Instant.now(), + limiterType: String = SpliceRateLimiter.GlobalLimiterType, + extraLabels: Map[String, String] = Map.empty, + // must be disabled for the per-attribute limiters as they'd all report the same value + // and would explode the number of registered gauges + reportMaxLimit: Boolean = true, ) { - // noinspection UnstableApiUsage - private val limiter = RateLimiter.create(config.ratePerSecond) - // lazy to ensure metrics get registered only if the limiter is actually used - private lazy val rateLimiter = { - metrics - .recordMaxLimit(config.ratePerSecond)( - MetricsContext("limiter" -> name) + private val metricsContext = MetricsContext( + extraLabels ++ Map("limiter" -> name, "limiter_type" -> limiterType) + ) + + // The limiters are created with one second worth of permits already available + private val limiter: Option[RateLimiter] = + Option.when(config.enabled)(BurstyRateLimiterFactory.create(config.ratePerSecond)) + // enforces the sustained limit over the sustained window, while still allowing bursts within its budget. + private val sustainedLimiter: Option[RateLimiter] = + Option + .when(config.enabled)(config.sustainedRatePerSecond) + .flatten + .map( + BurstyRateLimiterFactory + .create(_, SpliceRateLimiter.sustainedWindow(config).toSeconds.toDouble) ) + // lazy to ensure metrics get registered only if the limiter is actually used + private lazy val rateLimiter: Option[RateLimiter] = { + if (reportMaxLimit) { + metrics + .recordMaxLimit(config.ratePerSecond)(metricsContext) + } limiter } def markRun(): Boolean = { - if (config.enabled && Instant.now().isAfter(enforceAfter)) { - val canRun = rateLimiter.tryAcquire() + if (config.enabled) { + val canRun = rateLimiter.forall(_.tryAcquire()) && sustainedLimiter.forall(_.tryAcquire()) if (canRun) { metrics.meter.mark()( - MetricsContext("result" -> "accepted", "limiter" -> name) + metricsContext.merge(MetricsContext("result" -> "accepted")) ) } else { metrics.meter.mark()( - MetricsContext("result" -> "rejected", "limiter" -> name) + metricsContext.merge(MetricsContext("result" -> "rejected")) ) } canRun @@ -105,3 +202,106 @@ class SpliceRateLimiter( } } + +class PerAttributeRateLimiter( + name: String, + attribute: String, + config: SpliceRateLimitConfig, + attributeConfig: PerAttributeRateLimitConfig, + metrics: SpliceRateLimitMetrics, + logger: TracedLogger, +) { + + private val perAttributeConfig = attributeConfig.rateLimitFor(config) + private val isEnabled = perAttributeConfig.enabled && perAttributeConfig.ratePerSecond > 0 + private val attributeLabel = Map("limiter_attribute" -> attribute) + + // evictions by size can happen for every single request (e.g. when a large number of distinct + // attribute values is seen), so the warning is throttled to avoid flooding the logs + private val lastSizeEvictionWarning = + new AtomicLong(System.nanoTime() - PerAttributeRateLimiter.EvictionWarningIntervalNanos) + + private val evictionListener: RemovalListener[String, SpliceRateLimiter] = + (key: String, _: SpliceRateLimiter, cause: RemovalCause) => { + if (cause == RemovalCause.SIZE) { + implicit val tc: TraceContext = TraceContext.empty + val message = + s"Rate limiter cache for $name (attribute '$attribute') exceeded its maximum size of " + + s"${attributeConfig.maxAttributeValues}; evicting the rate limiter for attribute value '$key'. " + + "Its rate limiting state is lost. Consider increasing max-attribute-values." + val now = System.nanoTime() + val last = lastSizeEvictionWarning.get() + if ( + now - last >= PerAttributeRateLimiter.EvictionWarningIntervalNanos && lastSizeEvictionWarning + .compareAndSet(last, now) + ) { + logger.warn(message) + } else { + logger.debug(message) + } + } + } + + // lazy so that neither the cache nor its metrics are created if the limiter is disabled + private lazy val cache: ConcurrentCache[String, SpliceRateLimiter] = CaffeineCache[ + String, + SpliceRateLimiter, + ]( + Caffeine + .newBuilder() + .maximumSize(attributeConfig.maxAttributeValues) + // Evict limiters that have not been used for a full sustained rate limiting window (the bucket + // size of the interval rate limiter): after that time an idle limiter would have refilled its + // budget anyway, so dropping it does not change the enforced rate. + .expireAfterAccess(SpliceRateLimiter.sustainedWindow(perAttributeConfig)) + .evictionListener(evictionListener), + Some(new CacheMetrics(s"$name-$attribute-rate-limiter", metrics.otelFactory)), + ) + + private lazy val reportedMaxLimit: Unit = + metrics.recordMaxLimit(perAttributeConfig.ratePerSecond)( + MetricsContext( + attributeLabel ++ Map( + "limiter" -> name, + "limiter_type" -> SpliceRateLimiter.PerAttributeLimiterType, + ) + ) + ) + + def markRun(attributeValue: Option[String]): Boolean = + if (isEnabled) attributeValue match { + case Some(value) => limiterFor(value).markRun() + case None => + metrics.recordUnknownAttributeNotLimited()( + MetricsContext( + attributeLabel ++ Map( + "limiter" -> name, + "limiter_type" -> SpliceRateLimiter.PerAttributeLimiterType, + ) + ) + ) + true + } + else true + + private def limiterFor(attributeValue: String): SpliceRateLimiter = { + reportedMaxLimit + cache.getOrAcquire( + attributeValue, + (_: String) => + new SpliceRateLimiter( + name, + perAttributeConfig, + metrics, + limiterType = SpliceRateLimiter.PerAttributeLimiterType, + extraLabels = attributeLabel, + reportMaxLimit = false, + ), + ) + } +} + +object PerAttributeRateLimiter { + + private val EvictionWarningIntervalNanos: Long = TimeUnit.MINUTES.toNanos(1) +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/TemplateJsonDecoder.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/TemplateJsonDecoder.scala index 19246976ec..71d6e2f8c0 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/TemplateJsonDecoder.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/TemplateJsonDecoder.scala @@ -18,7 +18,7 @@ import com.digitalasset.canton.ledger.api.util.LfEngineToApi import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ErrorUtil -import com.digitalasset.daml.lf.archive.{ArchivePayload, Dar, DarReader} +import com.digitalasset.daml.lf.archive.DarReader import com.digitalasset.daml.lf.data.Ref import com.digitalasset.daml.lf.data.Ref.{PackageId, QualifiedName} import com.digitalasset.daml.lf.typesig @@ -27,6 +27,7 @@ import io.circe.Json import org.lfdecentralizedtrust.splice.environment.DarResource import java.util.zip.ZipInputStream +import scala.util.Using abstract class TemplateJsonDecoder { def decodeTemplate[TCid <: ContractId[T], T]( @@ -148,12 +149,16 @@ object ResourceTemplateDecoder { if (inputStream == null) { throw new IllegalArgumentException("Resource not found: " + path) } - val dar: Dar[ArchivePayload] = DarReader - .readArchive(resource.path, new ZipInputStream(inputStream)) - .valueOr(e => - throw new IllegalArgumentException(s"Failed to read DAR at path ${resource.path}: $e") - ) - dar.all.map(a => a.pkgId -> typesig.reader.SignatureReader.readPackageSignature(a)._2).toMap + Using(new ZipInputStream(inputStream)) { zip => + DarReader + .readArchive(resource.path, zip) + .valueOr(e => + throw new IllegalArgumentException(s"Failed to read DAR at path ${resource.path}: $e") + ) + }.fold( + e => throw e, + _.all.map(a => a.pkgId -> typesig.reader.SignatureReader.readPackageSignature(a)._2).toMap, + ) }, ) } diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/admin/api/HttpRequestLoggerTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/admin/api/HttpRequestLoggerTest.scala index 2037a0cc93..0f8121f2fe 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/admin/api/HttpRequestLoggerTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/admin/api/HttpRequestLoggerTest.scala @@ -6,19 +6,22 @@ package org.lfdecentralizedtrust.splice.admin.api import com.digitalasset.canton.config.ApiLoggingConfig import com.digitalasset.canton.logging.SuppressionRule import com.digitalasset.canton.BaseTest -import org.apache.pekko.http.scaladsl.model.StatusCodes +import org.apache.pekko.http.scaladsl.model.{HttpRequest, RemoteAddress, StatusCodes} +import org.apache.pekko.http.scaladsl.model.headers.{RawHeader, `X-Forwarded-For`} import org.apache.pekko.http.scaladsl.server.{RejectionHandler, Route} import org.apache.pekko.http.scaladsl.server.Directives.* import org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest import org.scalatest.wordspec.AnyWordSpec import org.slf4j.event.Level +import org.lfdecentralizedtrust.splice.config.RateLimitersConfig class HttpRequestLoggerTest extends AnyWordSpec with BaseTest with ScalatestRouteTest { private val apiLoggingConfig = ApiLoggingConfig() - private def loggerDirective = HttpRequestLogger( + private def loggerDirective(clientIpHeaders: Seq[String]) = HttpRequestLogger( apiLoggingConfig, + clientIpHeaders, loggerFactory, ) @@ -28,8 +31,10 @@ class HttpRequestLoggerTest extends AnyWordSpec with BaseTest with ScalatestRout * all outcomes and logs exactly one "Responding with status code" per request — * whether matched or rejected. */ - private def route: Route = - loggerDirective { + private def routeFor( + clientIpHeaders: Seq[String] = RateLimitersConfig.DefaultClientIpHeaders + ): Route = + loggerDirective(clientIpHeaders) { handleRejections(RejectionHandler.default) { concat( pathPrefix("api" / "admin") { @@ -42,8 +47,44 @@ class HttpRequestLoggerTest extends AnyWordSpec with BaseTest with ScalatestRout } } + private lazy val route = routeFor() + + private def assertLoggedClientIp(request: HttpRequest, expectedClientIp: String): Unit = + loggerFactory.assertLogsSeq(SuppressionRule.Level(Level.DEBUG))( + { + request ~> routeFor(Seq("x-envoy-external-address")) ~> check { + status shouldBe StatusCodes.OK + } + }, + logEntries => + forExactly(1, logEntries) { entry => + entry.message should include("received request") + entry.message should include(s"from ($expectedClientIp)") + }, + ) + "HttpRequestLogger" should { + "prefer the configured client IP header" in { + assertLoggedClientIp( + Get("/api/app").withHeaders( + RawHeader("X-Envoy-External-Address", "5.5.5.5"), + RawHeader("X-Forwarded-For", "1.1.1.1"), + ), + "5.5.5.5", + ) + } + + "fall back to the existing client IP extraction" in { + assertLoggedClientIp( + Get("/api/app").withHeaders( + RawHeader("X-Envoy-External-Address", "not-an-ip"), + `X-Forwarded-For`(Seq(RemoteAddress(Array[Byte](2, 2, 2, 2)))), + ), + "2.2.2.2", + ) + } + "log exactly one 'received request' and one response for a matching route" in { loggerFactory.assertLogsSeq(SuppressionRule.Level(Level.DEBUG))( { @@ -84,7 +125,7 @@ class HttpRequestLoggerTest extends AnyWordSpec with BaseTest with ScalatestRout "one log entry per request when methods conflict across siblings" in { val newStyleMethodRoute: Route = - loggerDirective { + loggerDirective(RateLimitersConfig.DefaultClientIpHeaders) { handleRejections(RejectionHandler.default) { concat( pathPrefix("api" / "data") { diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTriggerStoreTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTriggerStoreTest.scala index 9afeefda60..0fbbcfe077 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTriggerStoreTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTriggerStoreTest.scala @@ -4,6 +4,7 @@ import com.daml.metrics.api.noop.NoOpMetricsFactory import org.lfdecentralizedtrust.splice.config.AutomationConfig import org.lfdecentralizedtrust.splice.environment.RetryProvider import org.lfdecentralizedtrust.splice.store.{StoreErrors, StoreTestBase} +import org.lfdecentralizedtrust.splice.store.db.AdvisoryLocksTestHelper import com.digitalasset.canton.concurrent.{FutureSupervisor, Threading} import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.SuppressionRule @@ -13,13 +14,18 @@ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.MonadUtil import com.digitalasset.canton.{FutureHelpers, HasActorSystem, HasExecutionContext} import org.lfdecentralizedtrust.splice.automation.SqlIndexInitializationTrigger.IndexAction -import org.lfdecentralizedtrust.splice.store.db.{AcsJdbcTypes, AcsTables, SplicePostgresTest} +import org.lfdecentralizedtrust.splice.store.db.{ + AcsJdbcTypes, + AcsTables, + AdvisoryLocks, + SplicePostgresTest, +} import org.slf4j.event.Level import slick.dbio.DBIOAction import slick.jdbc.{GetResult, PositionedResult} import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton -import scala.concurrent.Future +import scala.concurrent.{Future, Promise} class SqlIndexInitializationTriggerStoreTest extends StoreTestBase @@ -29,7 +35,15 @@ class SqlIndexInitializationTriggerStoreTest with SplicePostgresTest with AcsJdbcTypes with AcsTables - with FutureHelpers { + with FutureHelpers + with AdvisoryLocksTestHelper { + + private val expectedIndexNames = Seq( + "updt_hist_crea_hi_mi_ci_import_updates", + "updt_hist_tran_hi_eth", + "dso_acs_store_sid_mid_pn_tid_rbio", + "scan_txlog_store_sid_effat_en_vot", + ) "SqlIndexInitializationTrigger" should { @@ -56,12 +70,7 @@ class SqlIndexInitializationTriggerStoreTest indexNames <- listIndexNames() _ <- dumpIndexes() } yield { - indexNames should contain allElementsOf Seq( - "updt_hist_crea_hi_mi_ci_import_updates", - "updt_hist_tran_hi_eth", - "dso_acs_store_sid_mid_pn_tid_rbio", - "scan_txlog_store_sid_effat_en_vot", - ) + indexNames should contain allElementsOf expectedIndexNames indexNames should not contain "scan_txlog_store_sid_en_vot" } } @@ -342,6 +351,35 @@ class SqlIndexInitializationTriggerStoreTest tasksResult.value shouldBe empty } } + + "skip index DDL quietly while another process holds the advisory lock" in { + val trigger = SqlIndexInitializationTrigger( + storage = storage, + triggerContext = triggerContext, + indexActions = List( + IndexAction.Create( + "test_index", + sqlu"create index concurrently if not exists test_index on update_history_creates (record_time)", + ) + ), + ) + val releaseLock = Promise[Unit]() + val (lockAcquired, lockReleased) = + holdLock(AdvisoryLocks.withDdlLock, DBIOAction.unit, releaseLock.future) + + for { + _ <- lockAcquired + _ <- trigger.runOnce() + indexNamesWhileLocked <- listIndexNames() + _ = indexNamesWhileLocked should not contain "test_index" + // The contended action is still pending and succeeds once the lock is released + _ = releaseLock.success(()) + _ <- lockReleased + _ <- runTriggerUntilAllTasksDone(trigger) + indexNamesAfter <- listIndexNames() + } yield indexNamesAfter should contain("test_index") + } + } private def listIndexNames(): Future[Seq[String]] = { @@ -416,6 +454,7 @@ class SqlIndexInitializationTriggerStoreTest loggerFactory, NoOpMetricsFactory, ) + override protected def cleanDb( storage: DbStorage )(implicit traceContext: TraceContext): FutureUnlessShutdown[?] = for { diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiterTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiterTest.scala new file mode 100644 index 0000000000..da4306e880 --- /dev/null +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiterTest.scala @@ -0,0 +1,555 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.http + +import com.daml.metrics.api.testing.InMemoryMetricsFactory +import com.digitalasset.canton.BaseTest +import org.apache.pekko.http.scaladsl.model.headers.{RawHeader, `X-Forwarded-For`, `X-Real-Ip`} +import org.apache.pekko.http.scaladsl.model.{ + AttributeKeys, + HttpRequest, + RemoteAddress, + StatusCode, + StatusCodes, +} +import org.apache.pekko.http.scaladsl.server.Directives.* +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest +import org.lfdecentralizedtrust.splice.config.RateLimitersConfig +import org.lfdecentralizedtrust.splice.util.{ + PerAttributeRateLimitConfig, + SpliceRateLimitConfig, + SpliceRateLimiter, +} +import org.scalatest.wordspec.AnyWordSpec + +import java.net.InetAddress + +class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteTest { + + "clientIp" should { + + "prefer X-Forwarded-For" in { + clientIp( + HttpRequest() + .withHeaders( + `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.1.1.1"))), + `X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2"))), + ) + .withAttributes( + Map( + AttributeKeys.remoteAddress -> RemoteAddress(InetAddress.getByName("3.3.3.3")) + ) + ) + ) should be(Some("1.1.1.1")) + } + + "fall back to X-Real-Ip" in { + clientIp( + HttpRequest().withHeaders(`X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2")))) + ) should be(Some("2.2.2.2")) + } + + "ignore a non-IP value and fall back to the next header" in { + clientIp( + HttpRequest() + .withHeaders( + RawHeader("X-Forwarded-For", "evil.example.com"), + `X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2"))), + ) + ) should be(Some("2.2.2.2")) + } + + "use the first address of a comma separated header value" in { + clientIp( + HttpRequest().withHeaders(RawHeader("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3")) + ) should be(Some("1.1.1.1")) + } + + "use the configured headers in order" in { + val request = HttpRequest().withHeaders( + RawHeader("X-Envoy-External-Address", "4.4.4.4"), + `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.1.1.1"))), + `X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2"))), + ) + clientIp( + request, + clientIpHeaders = Seq("x-envoy-external-address", "x-forwarded-for"), + ) should be(Some("4.4.4.4")) + clientIp( + request, + clientIpHeaders = Seq("x-real-ip", "x-envoy-external-address"), + ) should be(Some("2.2.2.2")) + } + + "not use headers that are not configured" in { + clientIp( + HttpRequest().withHeaders( + `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.1.1.1"))), + `X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2"))), + ), + clientIpHeaders = Seq("x-envoy-external-address"), + ) should be(None) + } + + "match the configured headers case-insensitively" in { + clientIp( + HttpRequest().withHeaders(RawHeader("X-Envoy-External-Address", "4.4.4.4")), + clientIpHeaders = Seq("X-Envoy-External-Address"), + ) should be(Some("4.4.4.4")) + } + + "not extract any IP when no headers are configured" in { + clientIp( + HttpRequest().withHeaders( + `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.1.1.1"))) + ), + clientIpHeaders = Seq.empty, + ) should be(None) + } + + "not use the remote address of the transport connection" in { + // the remote address is not exposed by the server, so it must not be relied upon + clientIp( + HttpRequest().withAttributes( + Map(AttributeKeys.remoteAddress -> RemoteAddress(InetAddress.getByName("3.3.3.3"))) + ) + ) should be(None) + } + + "return None if no IP can be determined" in { + clientIp(HttpRequest()) should be(None) + clientIp( + HttpRequest().withHeaders(RawHeader("X-Forwarded-For", "not-an-ip")) + ) should be(None) + } + } + + "the client IP used for rate limiting" should { + + "use the full address for IPv4 clients" in { + clientIpOf("1.2.3.4") should be(Some("1.2.3.4")) + } + + "group IPv6 clients by their /64 prefix" in { + // the lower 64 bits (the interface identifier) are freely chosen by the client + clientIpOf("2001:db8:0:1:1:2:3:4") should be(Some("2001:db8:0:1:0:0:0:0/64")) + clientIpOf("2001:db8:0:1:ffff:ffff:ffff:ffff") should be( + clientIpOf("2001:db8:0:1:1:2:3:4") + ) + clientIpOf("2001:db8:0:1::") should be(clientIpOf("2001:db8:0:1:1:2:3:4")) + } + + "not group IPv6 clients of different /64 networks" in { + clientIpOf("2001:db8:0:2:1:2:3:4") should not be clientIpOf("2001:db8:0:1:1:2:3:4") + clientIpOf("2001:db9:0:1:1:2:3:4") should not be clientIpOf("2001:db8:0:1:1:2:3:4") + } + + "reject IPv6 addresses carrying a zone id" in { + // zone ids are only meaningful locally and are not valid in an IP literal of a header + clientIpOf("fe80::1:2:3:4%7") should be(None) + } + + "use the IPv4 address for IPv4-mapped IPv6 clients" in { + // clients behind a dual stack proxy can be reported as ::ffff:a.b.c.d, those must not end up + // in a single /64 bucket shared by all IPv4 clients + clientIpOf("::ffff:1.2.3.4") should be(Some("1.2.3.4")) + clientIpOf("::ffff:1.2.3.4") should be(clientIpOf("1.2.3.4")) + clientIpOf("::ffff:4.3.2.1") should not be clientIpOf("::ffff:1.2.3.4") + } + + "apply the same grouping to all client IP sources" in { + val expected = Some("2001:db8:0:1:0:0:0:0/64") + val address = RemoteAddress(InetAddress.getByName("2001:db8:0:1:1:2:3:4")) + clientIp( + HttpRequest().withHeaders(RawHeader("X-Envoy-External-Address", "2001:db8:0:1:1:2:3:4")), + clientIpHeaders = Seq("x-envoy-external-address"), + ) should be(expected) + clientIp( + HttpRequest().withHeaders(`X-Forwarded-For`(address)) + ) should be(expected) + clientIp( + HttpRequest().withHeaders(`X-Real-Ip`(address)) + ) should be(expected) + } + } + + "the http rate limiter" should { + + "reject requests of a client IP over the global per client IP limit" in { + // the global per client IP limiter is enabled by default + withRoutes( + globalPerClientIp = perClientIp(1) + )("testOperation") { routes => + val route = routes("testOperation") + val results = (1 to 20).map(_ => call(route, ip = Some("1.1.1.1"))) + // 1 request per second per client IP, with 1 permit available from the creation of the + // limiter plus guava's deferred payment for the next one => the rest of the burst is rejected + results.count(_ == StatusCodes.OK) should be(2) + results.count(_ == StatusCodes.TooManyRequests) should be(18) + } + } + + "not reject requests of other client IPs" in { + withRoutes( + globalPerClientIp = perClientIp(1) + )("testOperation") { routes => + val route = routes("testOperation") + (1 to 20) + .map(_ => call(route, ip = Some("1.1.1.1"))) + .count(_ == StatusCodes.TooManyRequests) should be > 0 + call(route, ip = Some("2.2.2.2")) should be(StatusCodes.OK) + } + } + + "limit IPv6 clients of the same /64 network together" in { + withRoutes( + globalPerClientIp = perClientIp(1) + )("testOperation") { routes => + val route = routes("testOperation") + // drain the budget of the /64 network + (1 to 20) + .map(_ => call(route, ip = Some("2001:db8:0:1:1:2:3:4"))) + .count(_ == StatusCodes.OK) should be > 0 + // a different address of the same /64 shares the limiter, so it is rejected + call(route, ip = Some("2001:db8:0:1:ffff:ffff:ffff:ffff")) should be( + StatusCodes.TooManyRequests + ) + // a different /64 is a different client + call(route, ip = Some("2001:db8:0:2:1:2:3:4")) should be(StatusCodes.OK) + } + } + + "not apply the per client IP limiter if no client IP is known" in { + withRoutes( + globalPerClientIp = perClientIp(1) + )("testOperation") { routes => + val route = routes("testOperation") + (1 to 20).map(_ => call(route, ip = None)) should contain only StatusCodes.OK + val results = (1 to 20).map(_ => call(route, ip = Some("1.1.1.1"))) + results.count(_ == StatusCodes.OK) should be(2) + results.count(_ == StatusCodes.TooManyRequests) should be(18) + } + } + + "not apply the per client IP limiters if no client IP headers are configured" in { + withRoutes( + globalPerClientIp = perClientIp(1), + perClientIpOverrides = Map("testOperation" -> perClientIp(1)), + clientIpHeaders = Seq.empty, + )("testOperation") { fixture => + val route = fixture("testOperation") + (1 to 20).map(_ => call(route, ip = Some("1.1.1.1"))) should contain only StatusCodes.OK + forEvery(Seq("testOperation", HttpRateLimiter.GlobalLimiter)) { limiter => + fixture.requestsRejectedBy( + limiter, + SpliceRateLimiter.PerAttributeLimiterType, + ) should be(0L) + } + } + } + + "apply the global per client IP limiter across operations" in { + // the same client IP is limited regardless of the operation + withRoutes( + globalPerClientIp = perClientIp(1) + )("operationA", "operationB") { routes => + (1 to 20) + .map(_ => call(routes("operationA"), ip = Some("1.1.1.1"))) + .count(_ == StatusCodes.OK) should be > 0 + call(routes("operationB"), ip = Some("1.1.1.1")) should be(StatusCodes.TooManyRequests) + } + } + + "apply the global overall limiter across operations" in { + withRoutes( + global = SpliceRateLimitConfig(ratePerSecond = 1), + globalPerClientIp = PerAttributeRateLimitConfig.Disabled, + )("operationA", "operationB") { routes => + // exhaust the global budget via operationA + (1 to 20).map(_ => call(routes("operationA"), ip = Some("1.1.1.1"))) + // the global limiter ignores the operation and the client IP, so operationB is rejected too + call(routes("operationB"), ip = Some("2.2.2.2")) should be(StatusCodes.TooManyRequests) + } + } + + "not apply the per operation client IP limiter by default" in { + // no per client IP limiting configured for operations => requests from a single IP are only + // bounded by the (high) overall limiters + withRoutes()("testOperation") { routes => + val route = routes("testOperation") + (1 to 20).map(_ => call(route, ip = Some("1.1.1.1"))) should contain only StatusCodes.OK + } + } + + "apply the per operation client IP limiter when enabled for an operation" in { + withRoutes( + perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)) + )("limitedOperation", "otherOperation") { routes => + val results = + (1 to 20).map(_ => call(routes("limitedOperation"), ip = Some("1.1.1.1"))) + results.count(_ == StatusCodes.OK) should be(2) + // a different operation is not affected by the per operation client IP limiter + call(routes("otherOperation"), ip = Some("1.1.1.1")) should be(StatusCodes.OK) + } + } + + "apply the per operation overall limiter" in { + withRoutes( + rateLimiters = Map("limitedOperation" -> SpliceRateLimitConfig(ratePerSecond = 1)) + )("limitedOperation", "otherOperation") { routes => + val results = (1 to 20).map(_ => call(routes("limitedOperation"), ip = Some("1.1.1.1"))) + results.count(_ == StatusCodes.TooManyRequests) should be > 0 + // a different operation uses a separate overall limiter and is not affected + call(routes("otherOperation"), ip = Some("1.1.1.1")) should be(StatusCodes.OK) + } + } + + "use separate per operation limiters for equally named operations of different services" in { + val rateLimiter = new HttpRateLimiter( + RateLimitersConfig( + default = withPerClientIp( + SpliceRateLimitConfig(ratePerSecond = 1), + PerAttributeRateLimitConfig.Disabled, + ), + rateLimiters = Map.empty, + global = withPerClientIp( + SpliceRateLimitConfig(ratePerSecond = 1000), + PerAttributeRateLimitConfig.Disabled, + ), + ), + new InMemoryMetricsFactory(), + loggerFactory.getTracedLogger(classOf[HttpRateLimiterTest]), + ) + try { + val routeV1 = + rateLimiter.withRateLimit("serviceV1")("sharedOperation")(complete(StatusCodes.OK)) + val routeV2 = + rateLimiter.withRateLimit("serviceV2")("sharedOperation")(complete(StatusCodes.OK)) + (1 to 20) + .map(_ => call(routeV1, ip = Some("1.1.1.1"))) + .count(_ == StatusCodes.TooManyRequests) should be > 0 + call(routeV2, ip = Some("1.1.1.1")) should be(StatusCodes.OK) + } finally { + rateLimiter.close() + } + } + } + + "the order in which the rate limiters are applied" should { + + // A limiter only records (and thereby only consumes budget for) the requests that actually + // reach it, as the limiters are combined with a short-circuiting `&&`. The tests below send a + // burst of requests that is rejected by one limiter and assert that the limiters which must be + // applied later only saw the requests that were accepted by the rejecting one. + val Burst = 20 + val Rejecting = SpliceRateLimitConfig(ratePerSecond = 1) + // high enough to never reject, so that the recorded requests are exactly the ones that got here + val Downstream = SpliceRateLimitConfig(ratePerSecond = 1000) + val PerAttribute = SpliceRateLimiter.PerAttributeLimiterType + val Overall = SpliceRateLimiter.GlobalLimiterType + + // Sends a burst of requests from a single client IP and returns how many were accepted. + def burst(fixture: HttpRateLimiterTest.Fixture, operation: String): Long = { + val results = (1 to Burst).map(_ => call(fixture(operation), ip = Some("1.1.1.1"))) + results.count(_ == StatusCodes.TooManyRequests) should be > 0 + results.count(_ == StatusCodes.OK).toLong + } + + def onlySawAcceptedRequests( + fixture: HttpRateLimiterTest.Fixture, + accepted: Long, + )(limiters: (String, String)*) = + forEvery(limiters) { case (limiter, limiterType) => + withClue(s"requests seen by the $limiterType limiter '$limiter': ") { + fixture.requestsSeenBy(limiter, limiterType) should be(accepted) + fixture.requestsRejectedBy(limiter, limiterType) should be(0L) + } + } + + "apply the per operation client IP limiter before the overall limiters" in { + withRoutes( + rateLimiters = Map("limitedOperation" -> Downstream), + global = Downstream, + perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)), + )("limitedOperation") { fixture => + val accepted = burst(fixture, "limitedOperation") + onlySawAcceptedRequests(fixture, accepted)( + "limitedOperation" -> Overall, + HttpRateLimiter.GlobalLimiter -> Overall, + ) + } + } + + "apply the global per client IP limiter before the overall limiters" in { + withRoutes( + rateLimiters = Map("limitedOperation" -> Downstream), + global = Downstream, + globalPerClientIp = perClientIp(1), + )("limitedOperation") { fixture => + val accepted = burst(fixture, "limitedOperation") + onlySawAcceptedRequests(fixture, accepted)( + "limitedOperation" -> Overall, + HttpRateLimiter.GlobalLimiter -> Overall, + ) + } + } + + "apply the per operation client IP limiter before the global per client IP limiter" in { + withRoutes( + globalPerClientIp = perClientIp(1000), + perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)), + )("limitedOperation") { fixture => + val accepted = burst(fixture, "limitedOperation") + onlySawAcceptedRequests(fixture, accepted)( + HttpRateLimiter.GlobalLimiter -> PerAttribute + ) + } + } + + "apply the per operation overall limiter before the global overall limiter" in { + withRoutes( + rateLimiters = Map("limitedOperation" -> Rejecting), + global = Downstream, + )("limitedOperation") { fixture => + val accepted = burst(fixture, "limitedOperation") + onlySawAcceptedRequests(fixture, accepted)( + HttpRateLimiter.GlobalLimiter -> Overall + ) + } + } + + "still reject requests that pass the per client IP limiters but exceed an overall limit" in { + withRoutes( + global = SpliceRateLimitConfig(ratePerSecond = 2), + globalPerClientIp = perClientIp(1000), + perClientIpOverrides = Map("testOperation" -> perClientIp(1000)), + )("testOperation") { fixture => + // every request is below both per client IP limits, but the overall global limit applies + val results = (1 to Burst).map(i => call(fixture("testOperation"), ip = Some(s"1.1.1.$i"))) + results.count(_ == StatusCodes.OK) should be < Burst + results.count(_ == StatusCodes.TooManyRequests) should be > 0 + } + } + } + + private def perClientIp(ratePerSecond: Double): PerAttributeRateLimitConfig = + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = ratePerSecond)) + + private def clientIp( + request: HttpRequest, + clientIpHeaders: Seq[String] = RateLimitersConfig.DefaultClientIpHeaders, + ): Option[String] = { + val route = + HttpRateLimiter.extractClientIpKey(clientIpHeaders) { extracted => + complete(extracted.getOrElse[String](HttpRateLimiterTest.NoClientIp)) + } + request ~> route ~> check { + status should be(StatusCodes.OK) + Some(responseAs[String]).filterNot(_ == HttpRateLimiterTest.NoClientIp) + } + } + + private def clientIpOf(ip: String): Option[String] = + clientIp(HttpRequest().withHeaders(RawHeader("X-Forwarded-For", ip))) + + private def call(route: Route, ip: Option[String]): StatusCode = { + val request = ip match { + case Some(value) => + Get("/") ~> addHeader(`X-Forwarded-For`(RemoteAddress(InetAddress.getByName(value)))) + case None => Get("/") + } + request ~> route ~> check(status) + } + + private def withRoutes[A]( + // high enough by default so that only the explicitly configured limiter kicks in + default: SpliceRateLimitConfig = SpliceRateLimitConfig(ratePerSecond = 1000), + rateLimiters: Map[String, SpliceRateLimitConfig] = Map.empty, + global: SpliceRateLimitConfig = SpliceRateLimitConfig(ratePerSecond = 1000), + globalPerClientIp: PerAttributeRateLimitConfig = PerAttributeRateLimitConfig.Disabled, + perClientIpOverrides: Map[String, PerAttributeRateLimitConfig] = Map.empty, + clientIpHeaders: Seq[String] = RateLimitersConfig.DefaultClientIpHeaders, + )(operations: String*)(f: HttpRateLimiterTest.Fixture => A): A = { + // Any operation with a per client IP override needs its own overall limiter entry so that the + // embedded per client IP limiter is used instead of the `default` one. + val perOperationConfigs: Map[String, SpliceRateLimitConfig.WithPerClientIp] = + (rateLimiters.keySet ++ perClientIpOverrides.keySet).map { operation => + operation -> withPerClientIp( + rateLimiters.getOrElse(operation, default), + perClientIpOverrides.getOrElse(operation, PerAttributeRateLimitConfig.Disabled), + ) + }.toMap + val metricsFactory = new InMemoryMetricsFactory() + val rateLimiter = new HttpRateLimiter( + RateLimitersConfig( + default = withPerClientIp(default, PerAttributeRateLimitConfig.Disabled), + rateLimiters = perOperationConfigs, + global = withPerClientIp(global, globalPerClientIp), + clientIpHeaders = clientIpHeaders, + ), + metricsFactory, + loggerFactory.getTracedLogger(classOf[HttpRateLimiterTest]), + ) + try { + val routes = operations.map { operation => + operation -> rateLimiter.withRateLimit("testService")(operation) { + complete(StatusCodes.OK) + } + }.toMap + f(HttpRateLimiterTest.Fixture(routes, metricsFactory)) + } finally { + rateLimiter.close() + } + } + + private def withPerClientIp( + overall: SpliceRateLimitConfig, + perClientIp: PerAttributeRateLimitConfig, + ): SpliceRateLimitConfig.WithPerClientIp = + SpliceRateLimitConfig.WithPerClientIp( + enabled = overall.enabled, + ratePerSecond = overall.ratePerSecond, + sustainedRatePerSecond = overall.sustainedRatePerSecond, + sustainedWindowSeconds = overall.sustainedWindowSeconds, + perClientIp = perClientIp, + ) +} + +object HttpRateLimiterTest { + private val NoClientIp = "" + + /** The routes of the rate limited operations together with the metrics recorded by their rate + * limiters. Requests are only recorded by the limiters they actually reach, which is what allows + * asserting on the order in which the limiters are applied. + */ + private final case class Fixture( + routes: Map[String, Route], + metricsFactory: InMemoryMetricsFactory, + ) { + + def apply(operation: String): Route = routes(operation) + + /** Number of requests recorded by the given limiter, i.e. that were actually evaluated by it. */ + def requestsSeenBy(limiter: String, limiterType: String): Long = + marks(limiter, limiterType, result = None) + + /** Number of requests the given limiter rejected. */ + def requestsRejectedBy(limiter: String, limiterType: String): Long = + marks(limiter, limiterType, result = Some("rejected")) + + private def marks(limiter: String, limiterType: String, result: Option[String]): Long = + metricsFactory.metrics.meters.values + .flatMap(_.values) + .flatMap(_.markers.toSeq) + .collect { + case (context, value) + if context.labels.get("limiter").contains(limiter) && + context.labels.get("limiter_type").contains(limiterType) && + result.forall(expected => context.labels.get("result").contains(expected)) => + value.get() + } + .sum + } +} diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/InvalidResponseContentTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/InvalidResponseContentTest.scala new file mode 100644 index 0000000000..2de16000da --- /dev/null +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/InvalidResponseContentTest.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.http + +import cats.data.EitherT +import com.digitalasset.canton.{BaseTest, HasActorSystem, HasExecutionContext} +import com.digitalasset.canton.config.NonNegativeDuration +import io.circe.syntax.* +import org.lfdecentralizedtrust.splice.auth.AuthToken +import org.lfdecentralizedtrust.splice.config.AuthTokenSourceConfig +import org.lfdecentralizedtrust.splice.http.v0.definitions.Version +import org.apache.pekko.http.scaladsl.model.* +import org.apache.pekko.stream.Materializer +import org.lfdecentralizedtrust.splice.http.v0.external.common_admin.{ + CommonAdminClient, + GetVersionResponse, + IsReadyResponse, +} +import org.scalatest.compatible.Assertion +import org.scalatest.wordspec.AnyWordSpec + +import java.time.{OffsetDateTime, ZoneOffset} +import scala.concurrent.Future +import scala.concurrent.duration.Duration + +class InvalidResponseContentTest + extends AnyWordSpec + with BaseTest + with HasActorSystem + with HasExecutionContext { + + private implicit val mat: Materializer = Materializer(actorSystem) + + private def runEndpoint[A]( + endpoint: CommonAdminClient => EitherT[Future, Either[Throwable, HttpResponse], A], + resp: ResponseEntity, + )( + expectation: Either[Either[Throwable, HttpResponse], A] => Assertion + ): Assertion = { + implicit val httpClient: HttpClient = new HttpClient { + val requestParameters = HttpClient.HttpRequestParameters(NonNegativeDuration(Duration.Zero)) + def withOverrideParameters(newParameters: HttpClient.HttpRequestParameters): HttpClient = this + def executeRequest(client: String, operation: String)( + request: HttpRequest + ): Future[HttpResponse] = Future.successful(HttpResponse(StatusCodes.OK, entity = resp)) + def getToken(authConfig: AuthTokenSourceConfig): Future[Option[AuthToken]] = + Future.successful(None) + } + + val client = CommonAdminClient.httpClient(HttpClient.createHttpFn("", ""), "http://localhost") + expectation(endpoint(client).value.futureValue) + } + + "CommonAdminClient.getVersion" should { + "include the response body in the error when the content type is invalid" in { + val testBody = "test" + runEndpoint(_.getVersion(), HttpEntity(ContentTypes.`text/html(UTF-8)`, testBody)) { + case Left(Left(err)) => err.getMessage should endWith(testBody) + case other => fail(s"expected Left(Left(throwable)), got: $other") + } + } + + "decode a well-formed application/json response" in { + runEndpoint( + _.getVersion(), + HttpEntity( + ContentTypes.`application/json`, + Version( + "1.2.3", + OffsetDateTime.of(2026, 7, 23, 0, 0, 0, 0, ZoneOffset.UTC), + ).asJson.noSpaces, + ), + ) { + case Right(_: GetVersionResponse.OK) => succeed + case other => fail(s"expected GetVersionResponse.OK, got: $other") + } + } + } + + "CommonAdminClient.isReady" should { + "handle an empty response with no content type" in { + runEndpoint(_.isReady(), HttpEntity.Empty) { + case Right(IsReadyResponse.OK) => succeed + case other => fail(s"expected IsReadyResponse.OK, got: $other") + } + } + } +} diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/DbUnavailablePartiesStoreTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/DbUnavailablePartiesStoreTest.scala new file mode 100644 index 0000000000..3d01deb390 --- /dev/null +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/DbUnavailablePartiesStoreTest.scala @@ -0,0 +1,330 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.store + +import com.digitalasset.canton.HasExecutionContext +import com.digitalasset.canton.config.NonNegativeFiniteDuration +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.resource.DbStorage +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.MonadUtil +import org.lfdecentralizedtrust.splice.store.db.{ + DbUnavailablePartiesStore, + SplicePostgresTest, + StoreDescriptor, +} +import org.scalatest.matchers.should.Matchers + +import scala.concurrent.duration.* + +class DbUnavailablePartiesStoreTest + extends StoreTestBase + with Matchers + with HasExecutionContext + with SplicePostgresTest { + + private val storeDescriptor = StoreDescriptor( + version = 1, + name = "DbUnavailablePartiesStoreTest", + party = dsoParty, + participant = mkParticipantId("participant"), + key = Map(), + ) + + private val storeDescriptor2 = storeDescriptor.copy(version = 2) + + private val baseDuration = NonNegativeFiniteDuration(1.second) + private val maxIgnoreDuration = NonNegativeFiniteDuration(4.seconds) + private val oneSecond = 1_000_000L + + private def atSeconds(seconds: Long): Long = seconds * oneSecond + + private def mkStore(descriptor: StoreDescriptor = storeDescriptor) = + DbUnavailablePartiesStore( + descriptor, + storage, + baseDuration, + maxIgnoreDuration, + loggerFactory, + ) + + "DbUnavailablePartiesStore" should { + + "addParties" should { + + "be a no-op for an empty sequence" in { + for { + store <- mkStore() + _ <- store.addParties(Seq.empty, atSeconds(0)) + parties <- store.listParties(atSeconds(0)) + } yield parties shouldBe empty + } + + "ignores new parties for the base duration" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1), userParty(2)), atSeconds(0)) + justBefore <- store.listParties(atSeconds(1) - 1) + atExpiry <- store.listParties(atSeconds(1)) + } yield { + justBefore should contain theSameElementsAs Seq(userParty(1), userParty(2)) + atExpiry shouldBe empty + } + } + + "double the ignore duration when a party is re-added later" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) // expires at 1s + _ <- store.addParties( + Seq(userParty(1)), + atSeconds(1), + ) // should bump ignore duration to 2s => expires at 3s + justBefore <- store.listParties(atSeconds(3) - 1) + atExpiry <- store.listParties(atSeconds(3)) + } yield { + justBefore should contain(userParty(1)) + atExpiry shouldBe empty + } + } + + "not double the ignore duration when a party is re-added within its window" in { + val midWindow = atSeconds(1) / 2 + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) // expires at 1s + _ <- store.addParties(Seq(userParty(1)), midWindow) // already ignored => no-op + justBefore <- store.listParties(atSeconds(1) - 1) + atExpiry <- store.listParties(atSeconds(1)) + } yield { + justBefore should contain(userParty(1)) + atExpiry shouldBe empty + } + } + + "cap the ignore duration at maxIgnoreDuration" in { + for { + store <- mkStore() + // 0s => 1s, 1s => 2s, 3s => 4s, 7s => 4s (capped), 11s => 4s (capped) => expires at 15s + _ <- MonadUtil.sequentialTraverse(Seq(0L, 1L, 3L, 7L, 11L))(n => + store.addParties(Seq(userParty(1)), atSeconds(n)) + ) + justBefore <- store.listParties(atSeconds(15) - 1) + atExpiry <- store.listParties(atSeconds(15)) + } yield { + justBefore should contain(userParty(1)) + atExpiry shouldBe empty + } + } + + "is idempotent when replayed with the same timestamp" in { + for { + store <- mkStore() + // DbStorage.update may retry the statement, which must not double the duration: + // the expiry must stay at 1s, not move to 2s + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) + justBefore <- store.listParties(atSeconds(1) - 1) + atExpiry <- store.listParties(atSeconds(1)) + } yield { + justBefore should contain(userParty(1)) + atExpiry shouldBe empty + } + } + + "restart the expiry window from the latest marking" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) // 1s => expires at 1s + _ <- store.addParties(Seq(userParty(1)), atSeconds(2)) // 2s from 2s => expires at 4s + atOldExpiry <- store.listParties(atSeconds(1)) + justBefore <- store.listParties(atSeconds(4) - 1) + atExpiry <- store.listParties(atSeconds(4)) + } yield { + atOldExpiry should contain(userParty(1)) + justBefore should contain(userParty(1)) + atExpiry shouldBe empty + } + } + + "deduplicate parties within a single call" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1), userParty(1)), atSeconds(0)) + justBefore <- store.listParties(atSeconds(1) - 1) + atExpiry <- store.listParties(atSeconds(1)) + } yield { + justBefore should contain(userParty(1)) + atExpiry shouldBe empty + } + } + + "apply the insert and the doubling branch independently within one call" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) // expires at 1s + // party 1's window has just elapsed => doubles to 2s => expires at 3s + // party 2 is new => base duration => expires at 2s + _ <- store.addParties(Seq(userParty(1), userParty(2)), atSeconds(1)) + beforeParty2Expiry <- store.listParties(atSeconds(2) - 1) + atParty2Expiry <- store.listParties(atSeconds(2)) + atParty1Expiry <- store.listParties(atSeconds(3)) + } yield { + beforeParty2Expiry should contain theSameElementsAs Seq(userParty(1), userParty(2)) + atParty2Expiry should contain theSameElementsAs Seq(userParty(1)) + atParty1Expiry shouldBe empty + } + } + + "keep the later expiry when a marking arrives out of order" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(2)) // 1s => expires at 3s + // had the stale marking landed, updated_at would rewind to 0s and the duration + // would double to 2s, expiring at 2s instead + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) + atRewoundExpiry <- store.listParties(atSeconds(2)) + atExpiry <- store.listParties(atSeconds(3)) + } yield { + atRewoundExpiry should contain(userParty(1)) + atExpiry shouldBe empty + } + } + + } + + "removeParties" should { + + "remove only the given parties" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1), userParty(2), userParty(3)), atSeconds(0)) + deleted <- store.removeParties(Seq(userParty(1), userParty(3))) + parties <- store.listParties(atSeconds(0)) + } yield { + deleted shouldBe 2 + parties should contain theSameElementsAs Seq(userParty(2)) + } + } + + "be a no-op for unknown parties" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) + deleted <- store.removeParties(Seq(userParty(99))) + parties <- store.listParties(atSeconds(0)) + } yield { + deleted shouldBe 0 + parties should contain(userParty(1)) + } + } + + "be a no-op for an empty sequence" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) + deleted <- store.removeParties(Seq.empty) + parties <- store.listParties(atSeconds(0)) + } yield { + deleted shouldBe 0 + parties should contain(userParty(1)) + } + } + + "reset the backoff, so a re-added party starts from the base duration again" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) // 1s => expires at 1s + _ <- store.addParties(Seq(userParty(1)), atSeconds(1)) // 2s => expires at 3s + deleted <- store.removeParties(Seq(userParty(1))) + afterRemoval <- store.listParties(atSeconds(1)) + _ <- store.addParties(Seq(userParty(1)), atSeconds(3)) + justBefore <- store.listParties(atSeconds(4) - 1) + atExpiry <- store.listParties(atSeconds(4)) + } yield { + deleted shouldBe 1 + afterRemoval shouldBe empty + justBefore should contain(userParty(1)) + atExpiry shouldBe empty + } + } + + "remove parties regardless of which store recorded them" in { + for { + store1 <- mkStore(storeDescriptor) + store2 <- mkStore(storeDescriptor2) + _ <- store1.addParties(Seq(userParty(1)), atSeconds(0)) + deleted <- store2.removeParties(Seq(userParty(1))) + parties <- store1.listParties(atSeconds(0)) + } yield { + deleted shouldBe 1 + parties shouldBe empty + } + } + } + + "removePartiesUpToStoreId" should { + + "remove every party recorded at or below the given store id" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1), userParty(2)), atSeconds(0)) + deleted <- store.removePartiesUpToStoreId(store.storeId.toLong) + parties <- store.listParties(atSeconds(0)) + } yield { + deleted shouldBe 2 + parties shouldBe empty + } + } + + "leave parties recorded by a later store id in place" in { + for { + store1 <- mkStore(storeDescriptor) + store2 <- mkStore(storeDescriptor2) + _ = store1.storeId should be < store2.storeId + _ <- store1.addParties(Seq(userParty(1)), atSeconds(0)) + _ <- store2.addParties(Seq(userParty(2)), atSeconds(0)) + deleted <- store1.removePartiesUpToStoreId(store1.storeId.toLong) + remaining <- store2.listParties(atSeconds(0)) + } yield { + deleted shouldBe 1 + remaining should contain theSameElementsAs Seq(userParty(2)) + } + } + } + + "listParties" should { + + "only return entries whose ignore window has not elapsed" in { + for { + store <- mkStore() + _ <- store.addParties(Seq(userParty(1)), atSeconds(0)) // expires at 1s + _ <- store.addParties(Seq(userParty(2)), atSeconds(1)) // expires at 2s + parties <- store.listParties(atSeconds(1)) + } yield { + parties should contain theSameElementsAs Seq(userParty(2)) + } + } + + "return entries recorded by any store" in { + for { + store1 <- mkStore(storeDescriptor) + store2 <- mkStore(storeDescriptor2) + _ <- store1.addParties(Seq(userParty(1)), atSeconds(0)) + _ <- store2.addParties(Seq(userParty(2)), atSeconds(0)) + parties1 <- store1.listParties(atSeconds(0)) + parties2 <- store2.listParties(atSeconds(0)) + } yield { + parties1 should contain theSameElementsAs Seq(userParty(1), userParty(2)) + parties2 should contain theSameElementsAs Seq(userParty(1), userParty(2)) + } + } + } + } + + override protected def cleanDb( + storage: DbStorage + )(implicit traceContext: TraceContext): FutureUnlessShutdown[?] = + resetAllAppTables(storage) +} diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/UpdateHistoryTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/UpdateHistoryTest.scala index 7e8cf83157..112279eb34 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/UpdateHistoryTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/UpdateHistoryTest.scala @@ -424,7 +424,10 @@ class UpdateHistoryTest extends UpdateHistoryTestBase { store .getAllUpdates( after.map { case (migrationId, recordTime) => - (migrationId, CantonTimestamp.assertFromInstant(recordTime)) + TimestampWithMigrationId( + CantonTimestamp.assertFromInstant(recordTime), + migrationId, + ) }, PageLimit.tryCreate(1), ) diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLocksTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLocksTest.scala new file mode 100644 index 0000000000..e97c0d9485 --- /dev/null +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/db/AdvisoryLocksTest.scala @@ -0,0 +1,124 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.store.db + +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.resource.DbStorage +import com.digitalasset.canton.store.db.DbTest +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.HasExecutionContext +import org.lfdecentralizedtrust.splice.store.StoreTestBase +import slick.dbio.{DBIOAction, Effect, NoStream} +import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton + +import scala.concurrent.{Future, Promise} + +trait AdvisoryLocksTestHelper { _: DbTest with StoreTestBase with HasExecutionContext => + + /** Acquires a lock by calling [[withLock]] and holds it until `release` completes. Returns a + * future that completes once the lock is held, and a future that completes once the lock has + * been released. + */ + final def holdLock( + withLock: DBIOAction[Unit, NoStream, Effect.All] => DBIOAction[Unit, NoStream, Effect.All], + action: DBIOAction[Unit, NoStream, Effect.All], + release: Future[Unit], + ): (Future[Unit], Future[Unit]) = { + val acquired = Promise[Unit]() + val released = storage.underlying + .queryAndUpdate( + withLock(action.map(_ => acquired.success(())).flatMap(_ => DBIOAction.from(release))), + "hold lock", + ) + .failOnShutdown + released.failed.foreach(acquired.tryFailure) + (acquired.future, released) + } +} + +class AdvisoryLocksTest + extends StoreTestBase + with HasExecutionContext + with SplicePostgresTest + with AcsJdbcTypes + with AcsTables + with AdvisoryLocksTestHelper { + + private val testTable = "a" + + private def commonTests( + lockType: String, + lockId: Long, + withLock: ( + Long, + DBIOAction[Unit, NoStream, Effect.All], + ) => DBIOAction[Unit, NoStream, Effect.All], + ) = { + "release the lock after running an action" in { + for { + _ <- storage.underlying + .queryAndUpdate(withLock(lockId, DBIOAction.unit), "test lock") + .failOnShutdown + lockIsFree <- lockIsFree(lockId) + } yield lockIsFree shouldBe true + } + + "release the lock after an action fails" in { + for { + failure <- storage.underlying + .queryAndUpdate( + withLock(lockId, DBIOAction.failed(new RuntimeException("error"))), + "test lock", + ) + .failOnShutdown + .failed + _ = failure shouldBe a[RuntimeException] + lockIsFree <- lockIsFree(lockId) + } yield lockIsFree shouldBe true + } + + "fail fast while another session holds the lock" in { + val releaseLock = Promise[Unit]() + val createTable = sqlu"create table #$testTable (id int)".map(_ => ()) + val (lockAcquired, lockReleased) = + holdLock(withLock(lockId, _), createTable, releaseLock.future) + for { + _ <- lockAcquired + failure <- storage.underlying + .queryAndUpdate(withLock(lockId, createTable), "contended query") + .failOnShutdown + .failed + _ = releaseLock.success(()) + _ <- lockReleased + } yield failure shouldBe AdvisoryLocks.FailedToAcquireLockException(lockType, lockId) + } + } + + "AdvisoryLocks.withSessionLock" should commonTests( + "session-scoped", + AdvisoryLockIds.ddlStatement, + AdvisoryLocks.withSessionLock, + ) + + "AdvisoryLocks.withTransactionalLock" should commonTests( + "transactional", + AdvisoryLockIds.acsSnapshotDataInsert, + AdvisoryLocks.withTransactionalLock(profile, _, _), + ) + + /** Whether [[lockId]] can be acquired, i.e. nothing is holding it. Session-scoped and + * transactional locks share one lock space, so a session-scoped check also detects a + * detects a transactional holder. + */ + private def lockIsFree(lockId: Long): Future[Boolean] = + storage.underlying + .query(AdvisoryLocks.withSessionLock(lockId, DBIOAction.successful(true)), "check lock") + .failOnShutdown + .recover { case _: AdvisoryLocks.FailedToAcquireLockException => false } + + override protected def cleanDb( + storage: DbStorage + )(implicit traceContext: TraceContext): FutureUnlessShutdown[?] = + storage.update(sqlu"drop table if exists #$testTable", s"drop $testTable") +} diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SpliceDbTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SpliceDbTest.scala index 0a1d07bac9..42c6268f59 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SpliceDbTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SpliceDbTest.scala @@ -102,7 +102,8 @@ trait SpliceDbTest extends DbTest with BeforeAndAfterAll { this: Suite => key_value_store, acs_incremental_snapshot_data_next, acs_incremental_snapshot_data_backfill, - acs_incremental_snapshot + acs_incremental_snapshot, + dso_unavailable_parties RESTART IDENTITY CASCADE""".asUpdate _ <- debugPrintPgActivity() } yield (), diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceCircuitBreakerTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceCircuitBreakerTest.scala index 55abc8ad50..6956ebd7c8 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceCircuitBreakerTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceCircuitBreakerTest.scala @@ -9,7 +9,6 @@ import com.digitalasset.canton.time.SimClock import com.digitalasset.canton.topology.PartyId import io.grpc.StatusRuntimeException import org.apache.pekko.actor.Scheduler -import org.apache.pekko.pattern.CircuitBreakerOpenException import org.lfdecentralizedtrust.splice.config.CircuitBreakerConfig import org.scalatest.concurrent.ScalaFutures import org.scalatest.time.{Millis, Seconds, Span} @@ -70,11 +69,86 @@ class SpliceCircuitBreakerTest val future3 = cb.withCircuitBreaker(Future.successful("should not reach here")) whenReady(future3.failed) { ex => - ex shouldBe a[CircuitBreakerOpenException] + ex shouldBe a[SpliceCircuitBreakerOpenException] cb.isOpen shouldBe true } } + "attach the last failure as the cause of the CircuitBreakerOpenException" in { + val cb = createCircuitBreaker() + + val lastFailure = new RuntimeException("root cause failure") + + val future1 = cb.withCircuitBreaker(Future.failed(new RuntimeException("test failure 1"))) + whenReady(future1.failed) { ex => + ex shouldBe a[RuntimeException] + cb.isClosed shouldBe true + } + loggerFactory.suppressWarnings { + val future2 = cb.withCircuitBreaker(Future.failed(lastFailure)) + whenReady(future2.failed) { ex => + ex shouldBe a[RuntimeException] + cb.isOpen shouldBe true + } + } + + val future3 = cb.withCircuitBreaker(Future.successful("should not reach here")) + whenReady(future3.failed) { ex => + ex shouldBe a[SpliceCircuitBreakerOpenException] + ex.getCause shouldBe lastFailure + ex.getCause.getMessage shouldBe "root cause failure" + } + + eventually() { + cb.isHalfOpen shouldBe true + } + + val newFailure = new RuntimeException("new root cause failure") + loggerFactory.suppressWarnings { + val future4 = cb.withCircuitBreaker(Future.failed(newFailure)) + whenReady(future4.failed) { ex => + ex shouldBe newFailure + cb.isOpen shouldBe true + } + } + val future5 = cb.withCircuitBreaker(Future.successful("should not reach here")) + whenReady(future5.failed) { ex => + ex shouldBe a[SpliceCircuitBreakerOpenException] + ex.getCause shouldBe newFailure + ex.getCause.getMessage shouldBe "new root cause failure" + } + + eventually() { + cb.isHalfOpen shouldBe true + } + val successFuture = cb.withCircuitBreaker(Future.successful("success")) + whenReady(successFuture) { result => + result shouldBe "success" + cb.isClosed shouldBe true + } + + val thirdFailure = new RuntimeException("third root cause failure") + val future6 = cb.withCircuitBreaker(Future.failed(new RuntimeException("test failure 3"))) + whenReady(future6.failed) { ex => + ex shouldBe a[RuntimeException] + cb.isClosed shouldBe true + } + loggerFactory.suppressWarnings { + val future7 = cb.withCircuitBreaker(Future.failed(thirdFailure)) + whenReady(future7.failed) { ex => + ex shouldBe thirdFailure + cb.isOpen shouldBe true + } + } + + val future8 = cb.withCircuitBreaker(Future.successful("should not reach here")) + whenReady(future8.failed) { ex => + ex shouldBe a[SpliceCircuitBreakerOpenException] + ex.getCause shouldBe thirdFailure + ex.getCause.getMessage shouldBe "third root cause failure" + } + } + "not open after failures with ignored error categories" in { val cb = createCircuitBreaker() @@ -159,7 +233,7 @@ class SpliceCircuitBreakerTest val future4 = cb.withCircuitBreaker(Future.successful("should not reach here")) whenReady(future4.failed) { ex => - ex shouldBe a[CircuitBreakerOpenException] + ex shouldBe a[SpliceCircuitBreakerOpenException] cb.isOpen shouldBe true } } diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiterTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiterTest.scala index f89db4a51c..d673e1ff0e 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiterTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiterTest.scala @@ -1,3 +1,6 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + package org.lfdecentralizedtrust.splice.util import com.daml.metrics.api.MetricsContext @@ -26,7 +29,7 @@ class SpliceRateLimiterTest "accept requests under limit" in { val elementsToRun = 100 - withRateLimiter { case (rateLimitMetrics, rateLimiter) => + withRateLimiter() { case (rateLimitMetrics, rateLimiter) => runThroughRateLimiter(rateLimiter, 9, elementsToRun).reduce(_ && _) shouldBe true rateLimitMetrics.meter.valueFilteredOnLabels( @@ -43,7 +46,7 @@ class SpliceRateLimiterTest } "reject requests that are over the limit" in { - withRateLimiter { case (rateLimitMetrics, rateLimiter) => + withRateLimiter() { case (rateLimitMetrics, rateLimiter) => val results = runThroughRateLimiter(rateLimiter, 100, 1000) val (accepted, rejected) = results.partition(identity) @@ -76,6 +79,163 @@ class SpliceRateLimiterTest } + "start with the configured rate per second worth of permits" in { + withRateLimiter(SpliceRateLimitConfig(ratePerSecond = 10)) { case (_, rateLimiter) => + // the limiter must not have to warm up first: it holds its configured rate worth of permits + // (10) right from its creation, plus guava's deferred payment for the next one + val results = Seq.fill(50)(rateLimiter.markRun()) + results.take(11) should contain only true + results.count(!_) should be > 35 + } + } + + "not create any limiter if disabled" in { + // a disabled limiter must not fail even for a rate that guava would reject + withRateLimiter(SpliceRateLimitConfig(enabled = false, ratePerSecond = 0)) { + case (_, rateLimiter) => + Seq.fill(100)(rateLimiter.markRun()) should contain only true + } + } + + } + + "the per attribute rate limiter" should { + + "gate the per attribute limit on the overall limiter being enabled" in { + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 42)) + .rateLimitFor(SpliceRateLimitConfig(ratePerSecond = 100)) + .ratePerSecond should be(42d) + + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 42)) + .rateLimitFor(SpliceRateLimitConfig(enabled = false, ratePerSecond = 100)) + .enabled should be(false) + + PerAttributeRateLimitConfig( + enabled = false, + limit = SpliceRateLimitConfig(ratePerSecond = 42), + ) + .rateLimitFor(SpliceRateLimitConfig(ratePerSecond = 100)) + .enabled should be(false) + + PerAttributeRateLimitConfig.Disabled + .rateLimitFor(SpliceRateLimitConfig(ratePerSecond = 100)) + .enabled should be(false) + } + + "limit each attribute value separately" in { + withPerAttributeRateLimiter( + SpliceRateLimitConfig(ratePerSecond = 10), + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)), + ) { case (_, perAttributeRateLimiter) => + val ip1 = Seq.fill(20)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) + ip1.count(identity) should be(2) + ip1.count(!_) should be(18) + + // a different attribute value is not affected by the limiter of the first one + perAttributeRateLimiter.markRun(Some("2.2.2.2")) should be(true) + perAttributeRateLimiter.markRun(Some("2.2.2.2")) should be(true) + perAttributeRateLimiter.markRun(Some("2.2.2.2")) should be(false) + } + } + + "not limit requests with an unknown attribute value" in { + withPerAttributeRateLimiter( + SpliceRateLimitConfig(ratePerSecond = 10), + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)), + ) { case (metrics, perAttributeRateLimiter) => + // requests without an attribute value are not rate limited here; the overall/global + // rate limiter is relied upon to bound them instead + val results = Seq.fill(20)(perAttributeRateLimiter.markRun(None)) + results.count(identity) should be(20) + + metrics.unknownAttributeNotLimited.valueFilteredOnLabels( + LabelFilter("limiter", "test"), + LabelFilter("limiter_attribute", "test_attribute"), + LabelFilter("limiter_type", SpliceRateLimiter.PerAttributeLimiterType), + ) should be(20) + + metrics.meter.valuesWithContext.keys + .flatMap(_.labels.get("limiter_attribute")) + .toSeq should be(empty) + + // requests with a known attribute value are still limited + perAttributeRateLimiter.markRun(Some("1.1.1.1")) should be(true) + } + } + + "distinguish the metrics of the per attribute limiters" in { + withPerAttributeRateLimiter( + SpliceRateLimitConfig(ratePerSecond = 10), + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)), + ) { case (metrics, perAttributeRateLimiter) => + val results = Seq.fill(20)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) + + metrics.meter.valueFilteredOnLabels( + LabelFilter("limiter", "test"), + LabelFilter("limiter_attribute", "test_attribute"), + LabelFilter("limiter_type", SpliceRateLimiter.PerAttributeLimiterType), + LabelFilter("result", "accepted"), + ) should be(results.count(identity)) + metrics.meter.valueFilteredOnLabels( + LabelFilter("limiter", "test"), + LabelFilter("limiter_attribute", "test_attribute"), + LabelFilter("limiter_type", SpliceRateLimiter.PerAttributeLimiterType), + LabelFilter("result", "rejected"), + ) should be(results.count(!_)) + // only per attribute limiters report metrics + metrics.meter.valuesWithContext.keys + .flatMap(_.labels.get("limiter_type")) + .toSet should be(Set(SpliceRateLimiter.PerAttributeLimiterType)) + } + } + + "not limit anything if disabled" in { + withPerAttributeRateLimiter( + SpliceRateLimitConfig(ratePerSecond = 10), + PerAttributeRateLimitConfig.Disabled, + ) { case (_, perAttributeRateLimiter) => + Seq.fill(100)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) should contain only true + Seq.fill(100)(perAttributeRateLimiter.markRun(None)) should contain only true + } + } + + "respect the configured rate over time" in { + withPerAttributeRateLimiter( + SpliceRateLimitConfig(ratePerSecond = 100), + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 10)), + ) { case (_, perAttributeRateLimiter) => + // 10 per second per attribute value + val results = runRateLimited(50, 100) { + if (perAttributeRateLimiter.markRun(Some("1.1.1.1"))) Future.successful(true) + else + Future.failed( + io.grpc.Status.RESOURCE_EXHAUSTED + .withDescription("Rate limit exceeded") + .asRuntimeException() + ) + }.futureValue + // roughly 2 seconds of runtime at 10 permits per second, with some slack + results.count(identity) should (be >= 5 and be <= 40) + results.count(!_) should be > 0 + } + } + + } + + "the rate limiter with a sustained limit" should { + + "throttle to the sustained rate once the burst budget is drained" in { + withRateLimiter( + SpliceRateLimitConfig(ratePerSecond = 1000, sustainedRatePerSecond = Some(10)) + ) { case (_, rateLimiter) => + val results = runRateLimited(40, 120) { + rateLimiter.runWithLimit(Future.successful(true)) + }.futureValue + // ~3 seconds of runtime at 10 permits/s, with generous slack + results.count(identity) should (be >= 10 and be <= 60) + results.count(!_) should be > 0 + } + } } private def runThroughRateLimiter( @@ -92,13 +252,36 @@ class SpliceRateLimiterTest } futureValue } - private def withRateLimiter[A](f: (SpliceRateLimitMetrics, SpliceRateLimiter) => A): A = { + private def withRateLimiter[A]( + config: SpliceRateLimitConfig = SpliceRateLimitConfig(enabled = true, ratePerSecond = 10) + )(f: (SpliceRateLimitMetrics, SpliceRateLimiter) => A): A = { val metricsFactory = new InMemoryMetricsFactory() val rateLimitMetrics = SpliceRateLimitMetrics(metricsFactory, logger)(MetricsContext.Empty) val rateLimiter = new SpliceRateLimiter( "test", - SpliceRateLimitConfig(enabled = true, 10), + config, + rateLimitMetrics, + ) + try { + f(rateLimitMetrics, rateLimiter) + } finally { + rateLimitMetrics.close() + } + } + + private def withPerAttributeRateLimiter[A]( + config: SpliceRateLimitConfig, + attributeConfig: PerAttributeRateLimitConfig, + )(f: (SpliceRateLimitMetrics, PerAttributeRateLimiter) => A): A = { + val metricsFactory = new InMemoryMetricsFactory() + val rateLimitMetrics = SpliceRateLimitMetrics(metricsFactory, logger)(MetricsContext.Empty) + val rateLimiter = new PerAttributeRateLimiter( + "test", + "test_attribute", + config, + attributeConfig, rateLimitMetrics, + logger, ) try { f(rateLimitMetrics, rateLimiter) diff --git a/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResourcesGenerator.scala b/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResourcesGenerator.scala index a24ef7f556..cd749f5548 100644 --- a/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResourcesGenerator.scala +++ b/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResourcesGenerator.scala @@ -59,6 +59,16 @@ object DarResourcesGenerator { "splice-validator-lifecycle", "splice-api-reward-assignment-v1", ) + private val topLevelPackageOrderWithoutSplitwell: Seq[String] = Seq( + "splice-amulet", + "splice-dso-governance", + "splice-util-batched-markers", + "splice-wallet", + "splice-amulet-name-service", + "splice-wallet-payments", + "splice-validator-lifecycle", + "splice-api-reward-assignment-v1", + ) private val tokenStandardProductionPackageOrder: Seq[String] = Seq( "splice-api-token-metadata-v1", "splice-api-token-holding-v1", @@ -147,14 +157,15 @@ object DarResourcesGenerator { indent(2, renderPackage(name, dars, grouped)) } ++ renderPackageResources() ++ + renderCorePackageResources() ++ Seq( """| lazy val pkgIdToDarResource: Map[String, DarResource] = - | packageResources.view.flatMap(_.all).map(resource => resource.packageId -> resource).toMap + | corePackageResources.view.flatMap(_.all).map(resource => resource.packageId -> resource).toMap | | // We don't index the map by PackageMetadata because that type contains some additional | // fields that don't matter. | lazy val pkgMetadataToDarResource: Map[(PackageName, PackageVersion), DarResource] = - | packageResources.view + | corePackageResources.view | .flatMap(_.all) | .map(resource => (resource.metadata.name, resource.metadata.version) -> resource) | .toMap @@ -177,6 +188,19 @@ object DarResourcesGenerator { "", ) + private def renderCorePackageResources(): Seq[String] = + Seq( + " lazy val corePackageResources: Seq[PackageResource] =", + " TokenStandard.allPackageResources ++ Seq(", + ) ++ + topLevelPackageOrderWithoutSplitwell.sorted.map(name => + s" DarResources.${camel(name)}," + ) ++ + Seq( + " )", + "", + ) + private def renderTokenStandard(grouped: Map[String, Seq[DarEntry]]): Seq[String] = { val production = tokenStandardProductionPackageOrder.flatMap(name => renderPackage(name, grouped.getOrElse(name, Nil), grouped) diff --git a/apps/metrics-docs/src/main/scala/org/lfdecentralizedtrust/splice/metrics/MetricsDocs.scala b/apps/metrics-docs/src/main/scala/org/lfdecentralizedtrust/splice/metrics/MetricsDocs.scala index b3c708f35c..68e96ebc2b 100644 --- a/apps/metrics-docs/src/main/scala/org/lfdecentralizedtrust/splice/metrics/MetricsDocs.scala +++ b/apps/metrics-docs/src/main/scala/org/lfdecentralizedtrust/splice/metrics/MetricsDocs.scala @@ -22,6 +22,7 @@ import org.lfdecentralizedtrust.splice.sv.automation.{ AmuletPriceMetricsTrigger, ReportSvStatusMetricsExportTrigger, RewardMetricsTrigger, + VoteRequestMetricsTrigger, } import org.lfdecentralizedtrust.splice.sv.store.db.DbSvDsoStoreMetrics import org.lfdecentralizedtrust.splice.store.{HistoryMetrics, StoreMetrics} @@ -31,7 +32,7 @@ import org.lfdecentralizedtrust.splice.sv.automation.confirmation.{ } import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.ProcessRewardsTriggerBase import org.lfdecentralizedtrust.splice.validator.metrics.TopologyMetrics -import org.lfdecentralizedtrust.splice.wallet.metrics.AmuletMetrics +import org.lfdecentralizedtrust.splice.wallet.metrics.{AmuletMetrics, TreasuryMetrics} final case class GeneratedMetrics( common: List[MetricDoc.Item], @@ -95,6 +96,7 @@ object MetricsDocs { generator.reset() // validator new AmuletMetrics(walletUserParty, generator) + new TreasuryMetrics(walletUserParty, generator, () => 0L) val topologyMetrics = new TopologyMetrics(generator) // force creation of a gauge for a dummy participant val _ = topologyMetrics.getNumPartiesPerParticipantGauge( @@ -112,6 +114,7 @@ object MetricsDocs { ) new AmuletPriceMetricsTrigger.AmuletPriceMetrics(generator) new RewardMetricsTrigger.RewardMetrics(generator) + new VoteRequestMetricsTrigger.VoteRequestMetrics(generator) new ProcessRewardsTriggerBase.ProcessRewardsMetrics(generator, true) new CalculateRewardsTriggerBase.CalculateRewardsMetrics(generator, true) new SummarizingMiningRoundTrigger.SummarizingMiningRoundMetrics(generator) diff --git a/apps/package-lock.json b/apps/package-lock.json index afa157dbfe..0493fa7e55 100644 --- a/apps/package-lock.json +++ b/apps/package-lock.json @@ -11763,9 +11763,9 @@ "license": "MIT" }, "node_modules/react-router": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", - "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", diff --git a/apps/scan/src/main/openapi/scan.yaml b/apps/scan/src/main/openapi/scan.yaml index ea914649c6..efbc94a9f4 100644 --- a/apps/scan/src/main/openapi/scan.yaml +++ b/apps/scan/src/main/openapi/scan.yaml @@ -638,10 +638,14 @@ paths: /v1/state/acs: post: + deprecated: true tags: [ external, scan ] x-jvm-package: scan operationId: "getAcsSnapshotAtV1" description: | + Deprecated. Please use /v2/state/acs instead. + The only difference with this endpoint and that one is the type of the `after`/`next_page_token` pagination token. + Returns the ACS in creation date ascending order, paged, for a given migration id and record time. Unlike /v0/state/acs, every contract is identified by an (optional) update_id (as opposed to the event ID in /v0/state/acs, which was not BFT-safe). @@ -667,6 +671,37 @@ paths: "500": $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500" + /v2/state/acs: + post: + tags: [ external, scan ] + x-jvm-package: scan + operationId: "getAcsSnapshotAtV2" + description: | + Returns the ACS in creation date ascending order, paged, for a given migration id and record time. + Unlike /v0/state/acs, every contract is identified by an (optional) update_id + (as opposed to the event ID in /v0/state/acs, which was not BFT-safe). + The update_id is the ID of the update in which the contract was created, and can be used to correlate with updates returned by /v2/updates. + For contracts created in an earlier migration ID, the update_id will be absent. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/AcsRequestV2" + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/AcsResponseV2" + "400": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400" + "404": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" + "500": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500" + /v0/state/acs/force: post: tags: [external, scan] @@ -719,10 +754,14 @@ paths: /v1/holdings/state: post: + deprecated: true tags: [external, scan] x-jvm-package: scan operationId: "getHoldingsStateAtV1" description: | + Deprecated. Please use /v2/holdings/state instead. + The only difference with this endpoint and that one is the type of the `after`/`next_page_token` pagination token. + Returns the active amulet contracts for a given migration id and record time, in creation date ascending order, paged. requestBody: required: true @@ -744,6 +783,33 @@ paths: "500": $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500" + /v2/holdings/state: + post: + tags: [external, scan] + x-jvm-package: scan + operationId: "getHoldingsStateAtV2" + description: | + Returns the active amulet contracts for a given migration id and record time, in creation date ascending order, paged. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/HoldingsStateRequestV2" + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/AcsResponseV2" + "400": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400" + "404": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" + "500": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500" + /v0/holdings/summary: post: deprecated: true @@ -1296,6 +1362,26 @@ paths: schema: "$ref": "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/ListDsoRulesVoteResultsResponse" + /v0/admin/sv/voteresults/count: + post: + tags: [internal, scan] + x-jvm-package: scan + operationId: "countVoteRequestResults" + description: Count all vote results matching the request filters. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/CountVoteResultsRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + "$ref": "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/CountVoteResultsResponse" + /v0/admin/sv/previous-sv-reward-weight: post: tags: [internal, scan] @@ -1428,36 +1514,6 @@ paths: "404": $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" - /v0/transactions: - post: - deprecated: true - tags: [deprecated] - x-jvm-package: scan - operationId: "listTransactionHistory" - description: | - **Deprecated with known bugs that will not be fixed, use /v2/updates instead**. - - Lists transactions, by default in ascending order, paged, from ledger begin or optionally starting after a provided event id. - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/TransactionHistoryRequest" - responses: - "200": - description: ok - content: - application/json: - schema: - $ref: "#/components/schemas/TransactionHistoryResponse" - "400": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400" - "404": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" - "500": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500" - /v0/updates: post: deprecated: true @@ -1790,6 +1846,30 @@ paths: "501": $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/501" + /v0/history/bulk/checksums: + post: + tags: [ internal, pre-alpha, scan ] + x-jvm-package: scan + operationId: "getBulkObjectChecksums" + description: | + **Under Development, do not use in production yet** Get checksums for bulk history objects. Searches for object_keys in both staging and committed objects. + Meant for internal use only, as part of the processing pipeline of bulk history objects. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/GetBulkObjectChecksumsRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + "$ref": "#/components/schemas/GetBulkObjectChecksumsResponse" + "501": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/501" + components: schemas: @@ -2185,97 +2265,6 @@ components: svName: description: The sequencer's operating SV name. type: string - TransactionHistoryRequest: - type: object - required: - - page_size - properties: - page_end_event_id: - type: string - description: | - Note that all transactions carry some monotonically-increasing event_id. - Omit this page_end_event_id to start reading the first page, from the beginning or the end of the ledger, depending on the sort_order column. - A subsequent request can fill the page_end_event_id with the last event_id of the TransactionHistoryResponse to continue reading in the same sort_order. - The transaction with event_id == page_end_event_id will be skipped in the next response, making it possible to continuously read pages in the same sort_order. - sort_order: - description: | - Sort order for the transactions. For ascending order, from beginning to the end of the ledger, use "asc". - For descending order, from end to beginning of the ledger, use "desc". - "asc" is used if the sort_order is omitted. - type: string - enum: - - "asc" - - "desc" - page_size: - description: | - The maximum number of transactions returned for this request. - type: integer - format: int64 - TransactionHistoryResponse: - type: object - required: - - transactions - properties: - transactions: - type: array - items: - $ref: "#/components/schemas/TransactionHistoryResponseItem" - TransactionHistoryResponseItem: - type: object - required: - - transaction_type - - event_id - - date - - domain_id - properties: - transaction_type: - description: | - Describes the type of activity that occurred. - Determines if the data for the transaction should be read - from the `transfer`, `mint`, or `tap` property. - type: string - enum: - - "transfer" - - "mint" - - "devnet_tap" - - "abort_transfer_instruction" - event_id: - description: | - The event id. - type: string - offset: - description: | - The ledger offset of the event. - Note that this field may not be the same across nodes, and therefore should not be compared between SVs. - type: string - date: - description: | - The effective date of the event. - type: string - format: date-time - domain_id: - description: | - The id of the domain through which this transaction was sequenced. - type: string - round: - description: | - The round for which this transaction was registered. - type: integer - format: int64 - transfer: - description: | - A (batch) transfer from sender to receivers. - $ref: "#/components/schemas/Transfer" - mint: - description: | - The DSO mints amulet for the cases where the DSO rules allow for that. - $ref: "#/components/schemas/AmuletAmount" - tap: - description: | - A tap creates a Amulet, only used for development purposes, and enabled only on DevNet. - $ref: "#/components/schemas/AmuletAmount" - abort_transfer_instruction: - $ref: "#/components/schemas/AbortTransferInstruction" UpdateHistoryRequestAfter: type: object required: @@ -2908,6 +2897,60 @@ components: type: string description: | Filters the ACS by contracts with these template IDs, specified as "PACKAGE_NAME:MODULE_NAME:ENTITY_NAME". + AcsRequestV2: + type: object + required: + - migration_id + - record_time + - page_size + properties: + migration_id: + type: integer + format: int64 + description: | + The migration id for which to return the ACS. + record_time: + type: string + format: date-time + description: | + The timestamp at which the contract set was active. + This needs to be an exact timestamp, i.e., + needs to correspond to a timestamp reported by `/v0/state/acs/snapshot-timestamp` if `record_time_match` is set to `exact` (which is the default). + If `record_time_match` is set to `at_or_before`, this can be any timestamp, and the most recent snapshot at or before the given `record_time` will be returned. + record_time_match: + type: string + description: | + How to match the record_time. "exact" requires the record_time to match exactly. + "at_or_before" finds the most recent snapshot at or before the given record_time. + enum: + - "exact" + - "at_or_before" + default: "exact" + after: + type: string + description: | + Pagination token for the next page of results. For this to be valid, + this must be the `next_page_token` from a prior request with identical + parameters aside from `after` and `page_size`; the response may be + invalid otherwise. + This token is opaque and not meant to be edited by users. + page_size: + description: | + The maximum number of created events returned for this request. + type: integer + format: int32 + party_ids: + type: array + items: + type: string + description: | + Filters the ACS by contracts in which these party IDs are stakeholders. + templates: + type: array + items: + type: string + description: | + Filters the ACS by contracts with these template IDs, specified as "PACKAGE_NAME:MODULE_NAME:ENTITY_NAME". HoldingsStateRequest: # subset of AcsRequest type: object required: @@ -2956,6 +2999,54 @@ components: description: | Filters by contracts in which these party_ids are the owners of the amulets. + HoldingsStateRequestV2: # subset of AcsRequestV2 + type: object + required: + - migration_id + - record_time + - page_size + - owner_party_ids + properties: + migration_id: + type: integer + format: int64 + description: | + The migration id for which to return the ACS. + record_time: + type: string + format: date-time + description: | + The timestamp at which the contract set was active. + This needs to be an exact timestamp, i.e., + needs to correspond to a timestamp reported by `/v0/state/acs/snapshot-timestamp` if `record_time_match` is set to `exact` (which is the default). + If `record_time_match` is set to `at_or_before`, this can be any timestamp, and the most recent snapshot at or before the given `record_time` will be returned. + record_time_match: + type: string + description: | + How to match the record_time. "exact" requires the record_time to match exactly. + "at_or_before" finds the most recent snapshot at or before the given record_time. + enum: + - "exact" + - "at_or_before" + default: "exact" + after: + type: string + description: | + Pagination token for the next page of results. + This token is opaque and not meant to be edited by users. + page_size: + description: | + The maximum number of created events returned for this request. + type: integer + format: int32 + owner_party_ids: + type: array + items: + type: string + minItems: 1 + description: | + Filters by contracts in which these party_ids are the owners of the amulets. + HoldingsSummaryRequest: type: object required: @@ -3110,6 +3201,36 @@ components: to the `AcsRequest` or `HoldingsStateRequest`. Will be absent when there are no more pages. + AcsResponseV2: + type: object + required: + - record_time + - migration_id + - created_events + properties: + record_time: + description: The same `record_time` as in the request. + type: string + format: date-time + migration_id: + description: The same `migration_id` as in the request. + type: integer + format: int64 + created_events: + description: | + Up to `page_size` contracts in the ACS. + `create_arguments` are always encoded as `compact_json`. + type: array + items: + $ref: "#/components/schemas/ActiveContract" + next_page_token: + type: string + description: | + When requesting the next page of results, pass this as `after` + to the `AcsRequestV2` or `HoldingsStateRequestV2`. + Will be absent when there are no more pages. + This token is opaque and not meant to be edited by users. + HoldingsSummaryResponse: type: object required: @@ -4194,6 +4315,33 @@ components: to the next `ListBulkUpdateHistoryObjectsRequest` invocation. Will be absent when there are no more pages. + GetBulkObjectChecksumsRequest: + type: object + required: + - object_keys + properties: + object_keys: + description: | + The list of keys of the bulk storage objects for which checksums are requested. + type: array + items: + type: string + + GetBulkObjectChecksumsResponse: + type: object + required: + - checksums + properties: + checksums: + description: | + The list of checksums for the requested bulk storage objects (in the same order as the object_keys). + type: array + items: + type: object + properties: + value: + type: string + BulkStorageObjectRef: type: object required: diff --git a/apps/scan/src/main/protobuf/scan_tx_log.proto b/apps/scan/src/main/protobuf/scan_tx_log.proto index 167d0b1361..34b3215631 100644 --- a/apps/scan/src/main/protobuf/scan_tx_log.proto +++ b/apps/scan/src/main/protobuf/scan_tx_log.proto @@ -9,13 +9,6 @@ import "google/protobuf/timestamp.proto"; import "google/protobuf/struct.proto"; import "scalapb/scalapb.proto"; -message PartyBalanceChange { - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string change_to_initial_amount_as_of_round_zero = 1 [(scalapb.field).type = "scala.math.BigDecimal"]; - string change_to_holding_fees_rate = 2 [(scalapb.field).type = "scala.math.BigDecimal"]; -} - message SteppedRate { option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; @@ -30,38 +23,6 @@ message SteppedRate { repeated Step steps = 2; } -message SenderAmount { - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string party = 1 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - string input_amulet_amount = 2 [(scalapb.field).type = "scala.math.BigDecimal"]; - string input_app_reward_amount = 3 [(scalapb.field).type = "scala.math.BigDecimal"]; - string input_validator_reward_amount = 4 [(scalapb.field).type = "scala.math.BigDecimal"]; - string sender_change_amount = 5 [(scalapb.field).type = "scala.math.BigDecimal"]; - string sender_change_fee = 6 [(scalapb.field).type = "scala.math.BigDecimal"]; - string sender_fee = 7 [(scalapb.field).type = "scala.math.BigDecimal"]; - string holding_fees = 8 [(scalapb.field).type = "scala.math.BigDecimal"]; - // Added after initial release, so needs to be mapped to an Option in scala - string input_sv_reward_amount = 9 [(scalapb.field).type = "Option[scala.math.BigDecimal]"]; - string input_validator_faucet_amount = 10 [(scalapb.field).type = "Option[scala.math.BigDecimal]"]; -} - -message ReceiverAmount { - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string party = 1 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - string amount = 2 [(scalapb.field).type = "scala.math.BigDecimal"]; - string receiver_fee = 3 [(scalapb.field).type = "scala.math.BigDecimal"]; -} - -message BalanceChange { - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string party = 1 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - string change_to_initial_amount_as_of_round_zero = 2 [(scalapb.field).type = "scala.math.BigDecimal"]; - string change_to_holding_fees_rate = 3 [(scalapb.field).type = "scala.math.BigDecimal"]; -} - message ErrorTxLogEntry { option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry"; option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; @@ -69,30 +30,6 @@ message ErrorTxLogEntry { string event_id = 1; } -message BalanceChangeTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - int64 round = 3; - string change_to_initial_amount_as_of_round_zero = 4 [(scalapb.field).type = "scala.math.BigDecimal"]; - string change_to_holding_fees_rate = 5 [(scalapb.field).type = "scala.math.BigDecimal"]; - map party_balance_changes = 6 [(scalapb.field).key_type = "com.digitalasset.canton.topology.PartyId"];; -} - -message ExtraTrafficPurchaseTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - int64 round = 3; - string validator = 4 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - int64 traffic_purchased = 5; - string cc_spent = 6 [(scalapb.field).type = "scala.math.BigDecimal"]; -} - message OpenMiningRoundTxLogEntry { option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry"; option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; @@ -116,98 +53,6 @@ message ClosedMiningRoundTxLogEntry { google.protobuf.Timestamp effective_at = 4 [(scalapb.field).type = "java.time.Instant"]; } -enum TransferKind { - TRANSFER_KIND_OTHER = 0; - TRANSFER_KIND_CREATE_TRANSFER_INSTRUCTION = 1; - TRANSFER_KIND_TRANSFER_INSTRUCTION_ACCEPT = 2; - TRANSFER_KIND_PREAPPROVAL_SEND = 3; -} - -message TransferTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.TransactionTxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string offset = 2; - string domain_id = 3 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - google.protobuf.Timestamp date = 4 [(scalapb.field).type = "java.time.Instant"]; - string provider = 5; // unused: reserved fields don't work well with the json en/decoding so we keep it here. - SenderAmount sender = 6; - repeated ReceiverAmount receivers = 7; - repeated BalanceChange balance_changes = 8; - int64 round = 9; - string amulet_price = 10; // Unused but our decoding infrastructure doesn't like reserved fields. - - string description = 11; - - string transfer_instruction_receiver = 12; - string transfer_instruction_amount = 13 [(scalapb.field).type = "Option[scala.math.BigDecimal]"]; - string transfer_instruction_cid = 14; - - TransferKind transfer_kind = 15; -} - -message TapTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.TransactionTxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string offset = 2; - string domain_id = 3 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - google.protobuf.Timestamp date = 4 [(scalapb.field).type = "java.time.Instant"]; - string amulet_owner = 5 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - string amulet_amount = 6 [(scalapb.field).type = "scala.math.BigDecimal"]; - int64 round = 7; - string amulet_price = 8; // Unused but our decoding infrastructure doesn't like reserved fields. -} - -message MintTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.TransactionTxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string offset = 2; - string domain_id = 3 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - google.protobuf.Timestamp date = 4 [(scalapb.field).type = "java.time.Instant"]; - string amulet_owner = 5 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - string amulet_amount = 6 [(scalapb.field).type = "scala.math.BigDecimal"]; - int64 round = 7; - string amulet_price = 8; // Unused but our decoding infrastructure doesn't like reserved fields. -} - -message SvRewardTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.RewardTxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - int64 round = 3; - string party = 4 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - string amount = 5 [(scalapb.field).type = "scala.math.BigDecimal"]; -} - -message AppRewardTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.RewardTxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - int64 round = 3; - string party = 4 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - string amount = 5 [(scalapb.field).type = "scala.math.BigDecimal"]; -} - -message ValidatorRewardTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.RewardTxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - int64 round = 3; - string party = 4 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"]; - string amount = 5 [(scalapb.field).type = "scala.math.BigDecimal"]; -} - message VoteRequestTxLogEntry { option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry"; option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; @@ -253,17 +98,3 @@ enum TransferAbortKind { TRANSFER_ABORT_KIND_WITHDRAW = 1; TRANSFER_ABORT_KIND_REJECT = 2; } - -message AbortTransferInstructionTxLogEntry { - option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.TransactionTxLogEntry"; - option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers"; - - string event_id = 1; - string offset = 2; - string domain_id = 3 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"]; - google.protobuf.Timestamp date = 4 [(scalapb.field).type = "java.time.Instant"]; - - string transfer_instruction_cid = 14; - - TransferAbortKind transfer_abort_kind = 15; -} diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanApp.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanApp.scala index 5cfe864f60..b07eb2f3b6 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanApp.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanApp.scala @@ -144,16 +144,12 @@ class ScanApp( nodeMetrics.grpcClientMetrics, retryProvider, ), - if (config.enableAppActivityRecordAndTrafficIngestion) { - Some( - new SequencerTrafficClient( - syncConfig.sequencer, - retryProvider, - nodeMetrics.grpcClientMetrics, - loggerFactory, - ) - ) - } else None, + new SequencerTrafficClient( + syncConfig.sequencer, + retryProvider, + nodeMetrics.grpcClientMetrics, + loggerFactory, + ), ) override def initialize( @@ -258,45 +254,43 @@ class ScanApp( ) kvStore <- ScanKeyValueStore(dsoParty, participantId, storage, loggerFactory) kvProvider = new ScanKeyValueProvider(kvStore, loggerFactory) - bulkStorage = (config.bulkStorage.staging, config.bulkStorage.committed).tupled.map(_ => - BulkStorage( - scanStorageConfigV1, - config.bulkStorage, - acsSnapshotStore, - updateHistory, - currentMigrationId = domainMigrationId, - kvProvider, - retryProvider.metricsFactory, - config.automation, - backoffClock = new WallClock(retryProvider.timeouts, loggerFactory), - retryProvider, - loggerFactory, - ) - ) - // Conditionally create traffic summary ingestion dependencies - appActivityRecordStoreO = - if (config.enableAppActivityRecordAndTrafficIngestion) { - Some( - new DbAppActivityRecordStore( - storage, - updateHistory, - DbAppActivityRecordStore.IngestionVersions( - AppActivityComputation.ActivityIngestionCodeVersion, - config.activityIngestionUserVersion.fold(0)(_.toInt), - ), - config.isFirstSv, - loggerFactory, - ) + bulkStorage <- (config.bulkStorage.staging, config.bulkStorage.committed).tupled.traverse(_ => + appInitStep("Initialize bulk storage") { + BulkStorage( + scanStorageConfigV1, + config.bulkStorage, + acsSnapshotStore, + updateHistory, + currentMigrationId = domainMigrationId, + kvProvider, + retryProvider.metricsFactory, + config.automation, + backoffClock = new WallClock(retryProvider.timeouts, loggerFactory), + store, + svName, + ledgerClient, + amuletAppParameters.upgradesConfig, + retryProvider, + loggerFactory, ) - } else None - appRewardsStoreO = appActivityRecordStoreO.map(appActivityRecordStore => - new DbScanAppRewardsStore( - storage, - updateHistory, - appActivityRecordStore, - config.rewardMintingAllowanceTolerance, - loggerFactory, - ) + } + ) + appActivityRecordStore = new DbAppActivityRecordStore( + storage, + updateHistory, + DbAppActivityRecordStore.IngestionVersions( + AppActivityComputation.ActivityIngestionCodeVersion, + config.activityIngestionUserVersion.fold(0)(_.toInt), + ), + config.isFirstSv, + loggerFactory, + ) + appRewardsStore = new DbScanAppRewardsStore( + storage, + updateHistory, + appActivityRecordStore, + config.rewardMintingAllowanceTolerance, + loggerFactory, ) synchronizerId <- retryProvider.getValueWithRetries( @@ -319,8 +313,8 @@ class ScanApp( loggerFactory, store, updateHistory, - appRewardsStoreO, - appActivityRecordStoreO, + appRewardsStore, + appActivityRecordStore, storage, acsSnapshotStore, serviceUserPrimaryParty, @@ -331,7 +325,7 @@ class ScanApp( scanVerdictStore = DbScanVerdictStore( storage, updateHistory, - appActivityRecordStoreO, + appActivityRecordStore, loggerFactory, )(ec) scanEventStore = new ScanEventStore( @@ -360,25 +354,24 @@ class ScanApp( dsoParty, config.spliceInstanceNames.nameServiceNameAcronym.toLowerCase(), ) - rewardsReferenceStoreO = - if (config.enableAppActivityRecordAndTrafficIngestion) { - val rewardsStore = ScanRewardsReferenceStore( - key = ScanRewardsReferenceStore.Key( - dsoParty = dsoParty, - synchronizerId = synchronizerId, - ), - storage, - loggerFactory, - retryProvider, - domainMigrationId, - participantId, - config.automation.ingestion, - config.parameters.defaultLimit, - ) - automation.registerRewardsReferenceStoreIngestion(rewardsStore) - automation.registerRewardComputationTrigger(rewardsStore) - Some(rewardsStore) - } else None + rewardsReferenceStore = { + val rewardsStore = ScanRewardsReferenceStore( + key = ScanRewardsReferenceStore.Key( + dsoParty = dsoParty, + synchronizerId = synchronizerId, + ), + storage, + loggerFactory, + retryProvider, + domainMigrationId, + participantId, + config.automation.ingestion, + config.parameters.defaultLimit, + ) + automation.registerRewardsReferenceStoreIngestion(rewardsStore) + automation.registerRewardComputationTrigger(rewardsStore) + rewardsStore + } verdictAutomation = new ScanVerdictAutomationService( config, syncNodes, @@ -390,7 +383,7 @@ class ScanApp( domainMigrationId, synchronizerId, nodeMetrics.verdictIngestion, - rewardsReferenceStoreO, + rewardsReferenceStore, ) scanHandler = new HttpScanHandler( serviceUserPrimaryParty, @@ -400,15 +393,14 @@ class ScanApp( syncService, automation, updateHistory, - appRewardsStoreO, - appActivityRecordStoreO, + appRewardsStore, + appActivityRecordStore, acsSnapshotStore, scanEventStore, bulkStorage.map(_.reader), dsoAnsResolver, config.miningRoundsCacheTimeToLiveOverride, config.enableForcedAcsSnapshots, - config.serveAppActivityRecordsAndTraffic, clock, loggerFactory, packageVersionSupport, @@ -548,7 +540,7 @@ class ScanApp( bulkStorage, verdictAutomation, scanEventStore, - rewardsReferenceStoreO, + rewardsReferenceStore, loggerFactory.getTracedLogger(ScanApp.State.getClass), timeouts, bftSequencersWithAdminConnections.map(_._1), @@ -622,7 +614,7 @@ object ScanApp { bulkStorage: Option[BulkStorage], verdictAutomation: ScanVerdictAutomationService, eventStore: ScanEventStore, - rewardsReferenceStoreO: Option[ScanRewardsReferenceStore], + rewardsReferenceStore: ScanRewardsReferenceStore, logger: TracedLogger, timeouts: ProcessingTimeout, bftSequencersAdminConnections: Seq[SequencerAdminConnection], @@ -633,20 +625,25 @@ object ScanApp { storage.isActive override def close(): Unit = { - LifeCycle.close(bftSequencersAdminConnections*)(logger) - LifeCycle.close(cleanups*)(logger) - bulkStorage.foreach(LifeCycle.close(_)(logger)) - LifeCycle.close( - automation, - verdictAutomation, - store, - storage, - synchronizerNodes.current, - participantAdminConnection, - )(logger) - synchronizerNodes.successor.foreach( - LifeCycle.close(_)(logger) - ) + // Close everything in one LifeCycle.close call: it closes every instance left to right + // even when some of them fail, whereas separate calls stop at the first failing call. + val instances: Seq[AutoCloseable] = + bftSequencersAdminConnections ++ + cleanups ++ + bulkStorage.toList ++ + Seq( + automation, + verdictAutomation, + store, + rewardsReferenceStore, + storage, + synchronizerNodes.current, + participantAdminConnection, + ) ++ + synchronizerNodes.successor.toList ++ + synchronizerNodes.legacy.toList ++ + synchronizerNodes.additionalLegacy + LifeCycle.close(instances*)(logger) } } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanSynchronizerNode.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanSynchronizerNode.scala index a118f25ccd..eaff50ea3c 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanSynchronizerNode.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanSynchronizerNode.scala @@ -8,7 +8,7 @@ import org.lfdecentralizedtrust.splice.scan.sequencer.SequencerTrafficClient final class ScanSynchronizerNode( override val sequencerAdminConnection: SequencerAdminConnection, - val sequencerTrafficClient: Option[SequencerTrafficClient], + val sequencerTrafficClient: SequencerTrafficClient, ) extends SynchronizerNode(sequencerAdminConnection) with AutoCloseable { override def close(): Unit = { diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala index c8959052c8..a8af12533b 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala @@ -37,6 +37,7 @@ import org.lfdecentralizedtrust.splice.environment.{ import org.lfdecentralizedtrust.splice.http.HttpClient import org.lfdecentralizedtrust.splice.http.v0.definitions.{ AnsEntry, + GetBulkObjectChecksumsResponse, GetDsoInfoResponse, GetRewardAccountingActivityTotalsResponse, GetRewardAccountingBatchResponse, @@ -62,7 +63,7 @@ import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAp import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient.DsoScan import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig import org.lfdecentralizedtrust.splice.scan.store.ScanStore -import org.lfdecentralizedtrust.splice.store.DsoRulesStore +import org.lfdecentralizedtrust.splice.store.{DsoRulesStore, VoteResultsFilters} import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.SourceMigrationInfo import org.lfdecentralizedtrust.splice.store.UpdateHistory.UpdateHistoryResponse import org.lfdecentralizedtrust.splice.util.{ @@ -486,11 +487,7 @@ class BftScanConnection( bftCall(_.lookupTransferPreapprovalByParty(receiver), "lookupTransferPreapprovalByParty") override def listVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: Int, pageToken: Option[BigInt] = None, )(implicit @@ -498,17 +495,23 @@ class BftScanConnection( tc: TraceContext, ): Future[(Seq[DsoRules_CloseVoteRequestResult], Option[BigInt])] = bftCall( _.listVoteRequestResults( - actionName, - accepted, - requester, - effectiveFrom, - effectiveTo, + filters, limit, pageToken, ), "listVoteRequestResults", ) + override def countVoteRequestResults( + filters: VoteResultsFilters + )(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[Long] = bftCall( + _.countVoteRequestResults(filters), + "countVoteRequestResults", + ) + override def getPreviousSvRewardWeight(svParty: String, effectiveBefore: Option[String])(implicit ec: ExecutionContext, tc: TraceContext, @@ -899,13 +902,30 @@ class BftScanConnection( endpoint: String, callConfig: BftCallConfig = BftCallConfig.default(scanList.scanConnections), consensusFailureLogLevel: Level = Level.WARN, - consensusLogConfig: BftScanConnection.ConsensusLogConfig = - BftScanConnection.ConsensusLogConfig(), shortenResponsesForLog: T => Any = identity[T], )(implicit ec: ExecutionContext, tc: TraceContext, - ): Future[T] = { + ): Future[T] = bftCallWithScanUris( + call, + endpoint, + callConfig, + consensusFailureLogLevel, + shortenResponsesForLog = shortenResponsesForLog, + ) + .map(_._1) + + private def bftCallWithScanUris[T]( + call: SingleScanConnection => Future[T], + endpoint: String, + callConfig: BftCallConfig, + consensusFailureLogLevel: Level = Level.WARN, + disagreementLogLevel: Level = Level.INFO, + shortenResponsesForLog: T => Any = identity[T], + )(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[(T, List[Uri])] = { implicit val mc: MetricsContext = MetricsContext("request" -> endpoint) val connections = scanList.scanConnections @@ -943,7 +963,7 @@ class BftScanConnection( nTargetSuccess = callConfig.targetSuccess, logger, shortenResponsesForLog, - consensusLogConfig, + disagreementLogLevel, connectionMetrics, ), logger, @@ -1008,15 +1028,21 @@ class BftScanConnection( override def getRewardAccountingActivityTotals(roundNumber: Long)(implicit ec: ExecutionContext, tc: TraceContext, - ): Future[GetRewardAccountingActivityTotalsResponse] = { + ): Future[GetRewardAccountingActivityTotalsResponse] = + getRewardAccountingActivityTotalsWithScanUris(roundNumber).map(_._1) + + def getRewardAccountingActivityTotalsWithScanUris(roundNumber: Long)(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[(GetRewardAccountingActivityTotalsResponse, List[Uri])] = { val undetermined = GetRewardAccountingActivityTotalsResponse( RewardAccountingActivityTotalsUndetermined(status = "Undetermined") ) val callConfig = BftCallConfig.default(scanList.scanConnections) - if (!callConfig.enoughAvailableScans) Future.successful(undetermined) + if (!callConfig.enoughAvailableScans) Future.successful((undetermined, Nil)) else - bftCall[RewardAccountingActivityTotalsOk]( + bftCallWithScanUris[RewardAccountingActivityTotalsOk]( call = scan => scan.getRewardAccountingActivityTotals(roundNumber).flatMap { case GetRewardAccountingActivityTotalsResponse.members @@ -1028,19 +1054,13 @@ class BftScanConnection( }, endpoint = "getRewardAccountingActivityTotals", callConfig = callConfig, - consensusLogConfig = BftScanConnection.ConsensusLogConfig( - disagreementLogLevel = Level.WARN, - onlyLogDisagreementsInSuccessResponse = true, - agreementLogLevel = Some(Level.INFO), - ), + disagreementLogLevel = Level.WARN, ) - .transform(tryTotals => - Success( - tryTotals.toOption.fold(undetermined)(ok => - GetRewardAccountingActivityTotalsResponse(ok) - ) - ) - ) + .transformWith { + case Success((totals, consensusUris)) => + Future.successful((GetRewardAccountingActivityTotalsResponse(totals), consensusUris)) + case Failure(_) => Future.successful((undetermined, Nil)) + } } /** This is special because in addition to 'Ok' we can receive @@ -1056,15 +1076,21 @@ class BftScanConnection( override def getRewardAccountingRootHash(roundNumber: Long)(implicit ec: ExecutionContext, tc: TraceContext, - ): Future[GetRewardAccountingRootHashResponse] = { + ): Future[GetRewardAccountingRootHashResponse] = + getRewardAccountingRootHashWithScanUris(roundNumber).map(_._1) + + def getRewardAccountingRootHashWithScanUris(roundNumber: Long)(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[(GetRewardAccountingRootHashResponse, List[Uri])] = { val undetermined = GetRewardAccountingRootHashResponse( RewardAccountingRootHashUndetermined(status = "Undetermined") ) val callConfig = BftCallConfig.default(scanList.scanConnections) - if (!callConfig.enoughAvailableScans) Future.successful(undetermined) + if (!callConfig.enoughAvailableScans) Future.successful((undetermined, Nil)) else - bftCall[String]( + bftCallWithScanUris[String]( call = scan => scan.getRewardAccountingRootHash(roundNumber).flatMap { case GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk(ok) => @@ -1075,25 +1101,24 @@ class BftScanConnection( }, endpoint = "getRewardAccountingRootHash", callConfig = callConfig, - consensusLogConfig = BftScanConnection.ConsensusLogConfig( - disagreementLogLevel = Level.WARN, - onlyLogDisagreementsInSuccessResponse = true, - agreementLogLevel = Some(Level.INFO), - ), + disagreementLogLevel = Level.WARN, ) - .transform(tryRootHash => - Success( - tryRootHash.toOption.fold(undetermined)(rootHash => - GetRewardAccountingRootHashResponse( - RewardAccountingRootHashOk( - status = "Ok", - roundNumber = roundNumber, - rootHash = rootHash, - ) + .transformWith { + case Success((rootHash, consensusUris)) => + Future.successful( + ( + GetRewardAccountingRootHashResponse( + RewardAccountingRootHashOk( + status = "Ok", + roundNumber = roundNumber, + rootHash = rootHash, + ) + ), + consensusUris.map(_.toString), ) ) - ) - ) + case Failure(_) => Future.successful((undetermined, Nil)) + } } /** The batch contents are verifiable via the hash, so BFT agreement across scans is not @@ -1118,6 +1143,15 @@ class BftScanConnection( ) .transform(tryBatch => Success(tryBatch.toOption)) } + + override def getBulkObjectChecksums( + objectKeys: Seq[String] + )(implicit ec: ExecutionContext, tc: TraceContext): Future[GetBulkObjectChecksumsResponse] = + bftCall( + _.getBulkObjectChecksums(objectKeys), + "getBulkObjectChecksums", + consensusFailureLogLevel = Level.DEBUG, + ) } trait HasUrl { def url: Uri @@ -1130,19 +1164,19 @@ object BftScanConnection { nTargetSuccess: Int, logger: TracedLogger, shortenResponsesForLog: T => Any = identity[T], - consensusLogConfig: ConsensusLogConfig = ConsensusLogConfig(), + disagreementLogLevel: Level = Level.INFO, connectionMetrics: Option[ScanConnectionMetrics] = None, )(implicit ec: ExecutionContext, tc: TraceContext, mc: MetricsContext = MetricsContext.Empty, - ): Future[T] = { + ): Future[(T, List[Uri])] = { require(requestFrom.nonEmpty, "At least one request must be made.") val responses = new ConcurrentHashMap[BftScanConnection.ScanResponse[T], List[Uri]]() val nResponsesDone = new AtomicInteger(0) - val finalResponse = Promise[T]() + val finalResponse = Promise[(T, List[Uri])]() requestFrom.foreach { scan => call(scan) @@ -1161,7 +1195,7 @@ object BftScanConnection { case _ => true } if (considerResponseForQuorum && agreements.size == nTargetSuccess) { // consensus has been reached - finalResponse.tryComplete(response): Unit + finalResponse.tryComplete(response.map(r => (r, agreements))): Unit } if (nResponsesDone.incrementAndGet() == requestFrom.size) { // all Scans are done @@ -1176,9 +1210,9 @@ object BftScanConnection { case Some(consensusResponse) => logDisagreements( logger, - consensusResponse, + consensusResponse.map(_._1), responses, - consensusLogConfig, + disagreementLogLevel, connectionMetrics, ) } @@ -1224,7 +1258,7 @@ object BftScanConnection { logger: TracedLogger, consensusResponse: Try[T], responses: ConcurrentHashMap[BftScanConnection.ScanResponse[T], List[Uri]], - consensusLogConfig: ConsensusLogConfig, + disagreementLogLevel: Level, connectionMetrics: Option[ScanConnectionMetrics], )(implicit ec: ExecutionContext, tc: TraceContext, mc: MetricsContext): Unit = { implicit val elc: ErrorLoggingContext = ErrorLoggingContext.fromTracedLogger(logger) @@ -1254,28 +1288,16 @@ object BftScanConnection { keyToGroupResponses(consensusResponse).foreach { consensusResponseKey => val agreeingScanUrls = responses.remove(consensusResponseKey) agreeingScanUrls.foreach(recordConsensus(_, "agree", Map.empty)) - consensusLogConfig.agreementLogLevel.foreach { level => - LoggerUtil.logAtLevel( - level, - s"Reached consensus from:\n${agreeingScanUrls.mkString("\n")}", - ) - } responses.forEach { (disagreeingResponse, scanUrls) => val extraLabels = disagreementLabels(disagreeingResponse) scanUrls.foreach(recordConsensus(_, "disagree", extraLabels)) - val shouldLog = disagreeingResponse match { - case _: SuccessfulResponse[?] => true - case _ => !consensusLogConfig.onlyLogDisagreementsInSuccessResponse - } - if (shouldLog) { - LoggerUtil.logAtLevel( - consensusLogConfig.disagreementLogLevel, - s"""The following Scan URLs disagreed with consensus: - |${scanUrls.map(url => s" $url").mkString("\n")} - |consensus response: $consensusResponse - |disagreeing response: $disagreeingResponse""".stripMargin, - ) - } + LoggerUtil.logAtLevel( + disagreementLogLevel, + s"""The following Scan URLs disagreed with consensus: + |${scanUrls.map(url => s" $url").mkString("\n")} + |consensus response: $consensusResponse + |disagreeing response: $disagreeingResponse""".stripMargin, + ) } } } @@ -1861,8 +1883,10 @@ object BftScanConnection { disableBackgroundRefresh = true, ) - // Use the temporary connection to get a consensus on the full list of scans - allScans <- Bft.getScansInDsoRules(tempBftConnection).andThen { case _ => + // Use the temporary connection to get a consensus on the full list of scans. + // Future.delegate turns a synchronous throw into a failed future, so the + // andThen cleanup always runs. + allScans <- Future.delegate(Bft.getScansInDsoRules(tempBftConnection)).andThen { case _ => tempBftConnection.close() } @@ -1919,23 +1943,29 @@ object BftScanConnection { connectionMetrics, ) - _ <- retryProvider.waitUntil( - RetryFor.WaitingOnInitDependency, - "refresh_initial_scan_list", - "Scan list is refreshed.", - scanList - .refresh(bftConnection) - .recoverWith { case NonFatal(ex) => - Future.failed( - Status.UNAVAILABLE - .withDescription("Failed to refresh scan list on init") - .withCause(ex) - .asException() - ) - } - .map(_ => ()), - loggerFactory.getTracedLogger(classOf[BftScanConnection]), - ) + _ <- retryProvider + .waitUntil( + RetryFor.WaitingOnInitDependency, + "refresh_initial_scan_list", + "Scan list is refreshed.", + scanList + .refresh(bftConnection) + .recoverWith { case NonFatal(ex) => + Future.failed( + Status.UNAVAILABLE + .withDescription("Failed to refresh scan list on init") + .withCause(ex) + .asException() + ) + } + .map(_ => ()), + loggerFactory.getTracedLogger(classOf[BftScanConnection]), + ) + .recoverWith { case NonFatal(ex) => + // do not leak the scan connections when initialization ultimately fails + bftConnection.close() + Future.failed(ex) + } } yield bftConnection case bft @ BftScanClientConfig.Bft(_, _, _, _) => @@ -1970,24 +2000,30 @@ object BftScanConnection { else { _ => Future.unit }, connectionMetrics, ) - _ <- retryProvider.waitUntil( - RetryFor.WaitingOnInitDependency, - "refresh_initial_scan_list", - "Scan list is refreshed.", - bftConnection.scanList - .asInstanceOf[AllDsoScansBft] - .refresh(bftConnection) - .recoverWith { case NonFatal(ex) => - Future.failed( - Status.UNAVAILABLE - .withDescription("Failed to refresh scan list on init") - .withCause(ex) - .asException() - ) - } - .map(_ => ()), - loggerFactory.getTracedLogger(classOf[BftScanConnection]), - ) + _ <- retryProvider + .waitUntil( + RetryFor.WaitingOnInitDependency, + "refresh_initial_scan_list", + "Scan list is refreshed.", + bftConnection.scanList + .asInstanceOf[AllDsoScansBft] + .refresh(bftConnection) + .recoverWith { case NonFatal(ex) => + Future.failed( + Status.UNAVAILABLE + .withDescription("Failed to refresh scan list on init") + .withCause(ex) + .asException() + ) + } + .map(_ => ()), + loggerFactory.getTracedLogger(classOf[BftScanConnection]), + ) + .recoverWith { case NonFatal(ex) => + // do not leak the seed scan connections when initialization ultimately fails + bftConnection.close() + Future.failed(ex) + } } yield bftConnection } } @@ -2133,12 +2169,6 @@ object BftScanConnection { extends RuntimeException(s"Scan $url has no answer to contribute to consensus") with NoStackTrace - case class ConsensusLogConfig( - disagreementLogLevel: Level = Level.INFO, - onlyLogDisagreementsInSuccessResponse: Boolean = false, - agreementLogLevel: Option[Level] = None, - ) - private sealed trait ScanResponse[+T] private case class SuccessfulResponse[+T](response: T) extends ScanResponse[T] private case class HttpFailureResponse[+T](status: StatusCode, body: Json) extends ScanResponse[T] diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala index 4bd992a4ee..bbf758cb1c 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala @@ -24,10 +24,12 @@ import org.lfdecentralizedtrust.splice.config.UpgradesConfig import org.lfdecentralizedtrust.splice.environment.* import org.lfdecentralizedtrust.splice.http.HttpClient import org.lfdecentralizedtrust.splice.http.v0.definitions.{ + GetBulkObjectChecksumsResponse, GetDsoInfoResponse, GetRewardAccountingActivityTotalsResponse, GetRewardAccountingBatchResponse, GetRewardAccountingRootHashResponse, + HoldingsSummaryRequestV1, HoldingsSummaryResponse, HoldingsSummaryResponseV1, LookupTransferCommandStatusResponse, @@ -37,6 +39,7 @@ import org.lfdecentralizedtrust.splice.scan.admin.api.client.ScanConnection.* import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient.TransferContextWithInstances import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig +import org.lfdecentralizedtrust.splice.store.VoteResultsFilters import org.lfdecentralizedtrust.splice.util.* import org.lfdecentralizedtrust.splice.util.PrettyInstances.* import com.digitalasset.canton.config.RequireTypes.NonNegativeInt @@ -54,7 +57,6 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ } import org.lfdecentralizedtrust.splice.http.v0.definitions.HoldingsSummaryRequest.RecordTimeMatch import org.lfdecentralizedtrust.splice.metrics.ScanConnectionMetrics -import org.lfdecentralizedtrust.splice.http.v0.definitions.HoldingsSummaryRequestV1 import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} import scala.jdk.OptionConverters.* @@ -310,11 +312,7 @@ trait ScanConnection ): Future[Option[ContractWithState[TransferPreapproval.ContractId, TransferPreapproval]]] def listVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: Int, pageToken: Option[BigInt] = None, )(implicit @@ -322,6 +320,13 @@ trait ScanConnection tc: TraceContext, ): Future[(Seq[DsoRules_CloseVoteRequestResult], Option[BigInt])] + def countVoteRequestResults( + filters: VoteResultsFilters + )(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[Long] + def getPreviousSvRewardWeight(svParty: String, effectiveBefore: Option[String])(implicit ec: ExecutionContext, tc: TraceContext, @@ -356,6 +361,10 @@ trait ScanConnection tc: TraceContext, ): Future[Option[GetRewardAccountingBatchResponse]] + def getBulkObjectChecksums(objectKeys: Seq[String])(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[GetBulkObjectChecksumsResponse] } object ScanConnection { diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala index 9ccffbd016..a554b15047 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala @@ -26,12 +26,14 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.round.{ import org.lfdecentralizedtrust.splice.codegen.java.splice.ans.AnsRules import org.lfdecentralizedtrust.splice.config.UpgradesConfig import org.lfdecentralizedtrust.splice.environment.{ + BaseAppConnection, HttpAppConnection, RetryProvider, SpliceLedgerClient, } import org.lfdecentralizedtrust.splice.http.HttpClient import org.lfdecentralizedtrust.splice.http.v0.definitions.{ + GetBulkObjectChecksumsResponse, GetRewardAccountingActivityTotalsResponse, GetRewardAccountingBatchResponse, GetRewardAccountingRootHashResponse, @@ -44,6 +46,7 @@ import org.lfdecentralizedtrust.splice.http.v0.definitions.{ import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.SourceMigrationInfo +import org.lfdecentralizedtrust.splice.store.VoteResultsFilters import org.lfdecentralizedtrust.splice.store.UpdateHistory.UpdateHistoryResponse import org.lfdecentralizedtrust.splice.util.{ ChoiceContextWithDisclosures, @@ -78,7 +81,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ } import io.grpc.Status import org.apache.pekko.http.scaladsl.model.{HttpHeader, Uri} -import org.lfdecentralizedtrust.splice.admin.api.client.commands.HttpCommand +import org.lfdecentralizedtrust.splice.admin.api.client.commands.{HttpCommand, HttpCommandException} import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.transferinstructionv1 import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.transferinstructionv2 import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationv1 @@ -95,7 +98,7 @@ import scala.util.{Failure, Success} * to query for the DSO party id. */ class SingleScanConnection private[client] ( - private[client] val config: ScanAppClientConfig, + val config: ScanAppClientConfig, upgradesConfig: UpgradesConfig, protected val clock: Clock, retryProvider: RetryProvider, @@ -140,9 +143,11 @@ class SingleScanConnection private[client] ( .runHttpCmd(url, command, headers) .andThen { case Failure(e) => - MetricsContext.withMetricLabels(("outcome", e.getClass.getSimpleName)) { - implicit ec2 => - metrics.callPerConnection.mark()(m.merge(ec2)) + MetricsContext.withMetricLabels( + ("outcome", e.getClass.getSimpleName), + ("http_status", SingleScanConnection.httpStatusLabel(e)), + ) { implicit ec2 => + metrics.callPerConnection.mark()(m.merge(ec2)) } timer.stop()(m) case Success(_) => @@ -567,11 +572,7 @@ class SingleScanConnection private[client] ( ) override def listVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: Int, pageToken: Option[BigInt] = None, )(implicit @@ -580,16 +581,22 @@ class SingleScanConnection private[client] ( ): Future[(Seq[DsoRules_CloseVoteRequestResult], Option[BigInt])] = runHttpCmd( config.adminApi.url, HttpScanAppClient.ListVoteRequestResults( - actionName, - accepted, - requester, - effectiveFrom, - effectiveTo, + filters, limit, pageToken, ), ) + override def countVoteRequestResults( + filters: VoteResultsFilters + )(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[Long] = runHttpCmd( + config.adminApi.url, + HttpScanAppClient.CountVoteRequestResults(filters), + ) + override def getPreviousSvRewardWeight(svParty: String, effectiveBefore: Option[String])(implicit ec: ExecutionContext, tc: TraceContext, @@ -1024,9 +1031,29 @@ class SingleScanConnection private[client] ( config.adminApi.url, HttpScanAppClient.GetRewardAccountingBatch(roundNumber, batchHash), ) + + override def getBulkObjectChecksums( + objectKeys: Seq[String] + )(implicit ec: ExecutionContext, tc: TraceContext): Future[GetBulkObjectChecksumsResponse] = + runHttpCmd( + config.adminApi.url, + HttpScanAppClient.GetBulkObjectChecksums(objectKeys), + ) } object SingleScanConnection { + + private[client] def httpStatusLabel(error: Throwable): String = + error match { + case e: BaseAppConnection.UnexpectedHttpJsonResponse => e.statusCode.intValue.toString + case e: BaseAppConnection.UnexpectedHttpMalformedJsonResponse => + e.statusCode.intValue.toString + case e: BaseAppConnection.UnexpectedHttpTextResponse => e.statusCode.intValue.toString + case e: BaseAppConnection.UnexpectedHttpNonJsonResponse => e.statusCode.intValue.toString + case e: HttpCommandException => e.status.intValue.toString + case _ => "none" + } + def withSingleScanConnection[T]( scanConfig: ScanAppClientConfig, upgradesConfig: UpgradesConfig, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala index 2ea945a677..c083776b93 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala @@ -42,6 +42,7 @@ import org.lfdecentralizedtrust.tokenstandard.{ } import org.lfdecentralizedtrust.splice.http.v0.scan.{ ForceAcsSnapshotNowResponse, + GetBulkObjectChecksumsResponse, GetDateOfFirstSnapshotAfterResponse, GetDateOfMostRecentSnapshotBeforeResponse, GetLsuResponse, @@ -53,7 +54,7 @@ import org.lfdecentralizedtrust.splice.scan.admin.http.{ ProtobufJsonScanHttpEncodings, } import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.SourceMigrationInfo -import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore +import org.lfdecentralizedtrust.splice.store.{MultiDomainAcsStore, VoteResultsFilters} import org.lfdecentralizedtrust.splice.store.UpdateHistory.UpdateHistoryResponse import org.lfdecentralizedtrust.splice.util.{ ChoiceContextWithDisclosures, @@ -843,7 +844,7 @@ object HttpScanAppClient { P2PEndpoint.fromEndpointConfig( P2PEndpointConfig( uri.authority.host.address(), - RequireTypes.Port(uri.effectivePort), + RequireTypes.Port.tryCreate(uri.effectivePort), Option.when(uri.scheme == "https")( TlsClientConfig( None, @@ -894,33 +895,6 @@ object HttpScanAppClient { final case class DsoScan(publicUrl: Uri, svName: String) - case class ListTransactions( - pageEndEventId: Option[String], - sortOrder: definitions.TransactionHistoryRequest.SortOrder, - pageSize: Int, - ) extends InternalBaseCommand[http.ListTransactionHistoryResponse, Seq[ - definitions.TransactionHistoryResponseItem - ]] { - override def submitRequest( - client: http.ScanClient, - headers: List[HttpHeader], - ): EitherT[Future, Either[ - Throwable, - HttpResponse, - ], http.ListTransactionHistoryResponse] = { - client.listTransactionHistory( - definitions - .TransactionHistoryRequest(pageEndEventId, Some(sortOrder), pageSize.toLong), - headers, - ) - } - - override def handleOk()(implicit decoder: TemplateJsonDecoder) = { - case http.ListTransactionHistoryResponse.OK(response) => - Right(response.transactions) - } - } - case class GetAcsSnapshot( party: PartyId, recordTime: Option[Instant], @@ -1097,6 +1071,48 @@ object HttpScanAppClient { } } + case class GetAcsSnapshotAtV2( + at: java.time.OffsetDateTime, + migrationId: Long, + recordTimeMatch: Option[definitions.AcsRequestV2.RecordTimeMatch], + after: Option[String] = None, + pageSize: Int = 100, + partyIds: Option[Vector[PartyId]] = None, + templates: Option[Vector[PackageQualifiedName]] = None, + ) extends InternalBaseCommand[ + http.GetAcsSnapshotAtV2Response, + Option[definitions.AcsResponseV2], + ] { + override def submitRequest( + client: ScanClient, + headers: List[HttpHeader], + ): EitherT[Future, Either[Throwable, HttpResponse], http.GetAcsSnapshotAtV2Response] = + client.getAcsSnapshotAtV2( + definitions.AcsRequestV2( + migrationId, + at, + recordTimeMatch, + after, + pageSize, + partyIds.map(_.map(_.toProtoPrimitive)), + templates.map(_.map(_.toString)), + ), + headers, + ) + + override protected def handleOk()(implicit + decoder: TemplateJsonDecoder + ): PartialFunction[http.GetAcsSnapshotAtV2Response, Either[ + String, + Option[definitions.AcsResponseV2], + ]] = { + case http.GetAcsSnapshotAtV2Response.OK(value) => + Right(Some(value)) + case http.GetAcsSnapshotAtV2Response.NotFound(_) => + Right(None) + } + } + case class GetHoldingsStateAt( at: java.time.OffsetDateTime, migrationId: Long, @@ -3086,11 +3102,7 @@ object HttpScanAppClient { } case class ListVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: BigInt, pageToken: Option[BigInt] = None, ) extends InternalBaseCommand[ @@ -3107,13 +3119,13 @@ object HttpScanAppClient { ): EitherT[Future, Either[Throwable, HttpResponse], http.ListVoteRequestResultsResponse] = client.listVoteRequestResults( body = definitions.ListVoteResultsRequest( - actionName, - accepted, - requester, - effectiveFrom, - effectiveTo, - limit, - pageToken, + filters.actionName, + filters.accepted, + requester = filters.requester, + effectiveFrom = filters.effectiveFrom, + effectiveTo = filters.effectiveTo, + limit = limit, + pageToken = pageToken, ), headers = headers, ) @@ -3135,6 +3147,35 @@ object HttpScanAppClient { } } + case class CountVoteRequestResults( + filters: VoteResultsFilters + ) extends InternalBaseCommand[ + http.CountVoteRequestResultsResponse, + Long, + ] { + + override def submitRequest( + client: ScanClient, + headers: List[HttpHeader], + ): EitherT[Future, Either[Throwable, HttpResponse], http.CountVoteRequestResultsResponse] = + client.countVoteRequestResults( + body = definitions.CountVoteResultsRequest( + filters.actionName, + filters.accepted, + requester = filters.requester, + effectiveFrom = filters.effectiveFrom, + effectiveTo = filters.effectiveTo, + ), + headers = headers, + ) + + override def handleOk()(implicit + decoder: TemplateJsonDecoder + ) = { case http.CountVoteRequestResultsResponse.OK(response) => + Right(response.count) + } + } + case class GetPreviousSvRewardWeight( svParty: String, effectiveBefore: Option[String], @@ -3346,6 +3387,32 @@ object HttpScanAppClient { } } + case class GetBulkObjectChecksums( + objectKeys: Seq[String] + ) extends InternalBaseCommand[ + http.GetBulkObjectChecksumsResponse, + definitions.GetBulkObjectChecksumsResponse, + ] { + override def submitRequest( + client: Client, + headers: List[HttpHeader], + ): EitherT[Future, Either[Throwable, HttpResponse], GetBulkObjectChecksumsResponse] = + client.getBulkObjectChecksums( + definitions.GetBulkObjectChecksumsRequest(objectKeys.toVector), + headers, + ) + + override protected def handleOk()(implicit + decoder: TemplateJsonDecoder + ): PartialFunction[GetBulkObjectChecksumsResponse, Either[ + String, + definitions.GetBulkObjectChecksumsResponse, + ]] = { + case http.GetBulkObjectChecksumsResponse.OK(response) => Right(response) + case http.GetBulkObjectChecksumsResponse.NotImplemented(err) => Left(err.error) + } + } + case class BulkStorageDownload( objectKey: String ) extends ScanStreamBaseCommand[ diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala index 160261e7d0..90d2ab9358 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala @@ -67,11 +67,15 @@ import org.lfdecentralizedtrust.splice.http.{ import org.lfdecentralizedtrust.splice.http.v0.{definitions, scan as v0} import org.lfdecentralizedtrust.splice.http.v0.definitions.{ AcsRequest, + AcsRequestV2, BatchListVotesByVoteRequestsRequest, + CountVoteResultsRequest, DamlValueEncoding, ErrorResponse, EventHistoryRequest, + GetBulkObjectChecksumsRequest, HoldingsStateRequest, + HoldingsStateRequestV2, HoldingsSummaryRequest, HoldingsSummaryRequestV1, ListBulkUpdateHistoryObjectsRequest, @@ -96,8 +100,12 @@ import org.lfdecentralizedtrust.splice.scan.store.{ ScanStore, TxLogEntry, } +import org.lfdecentralizedtrust.splice.scan.store.AppActivityStore.RoundIngestionStatus import org.lfdecentralizedtrust.splice.scan.store.bulk.BulkStorageReader -import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.QueryAcsSnapshotResult +import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.{ + QueryAcsSnapshotPaginationToken, + QueryAcsSnapshotResult, +} import org.lfdecentralizedtrust.splice.scan.store.bulk.AcsSnapshotBulkStorage.AcsSnapshotObjects import org.lfdecentralizedtrust.splice.scan.store.bulk.UpdateHistoryBulkStorage.UpdateHistoryObjectsResponse import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority @@ -106,7 +114,8 @@ import org.lfdecentralizedtrust.splice.store.{ AppStore, AppStoreWithIngestion, PageLimit, - SortOrder, + TimestampWithMigrationId, + VoteResultsFilters, VotesStore, } import org.lfdecentralizedtrust.splice.store.S3BucketConnection.ObjectKeyAndChecksum @@ -146,15 +155,14 @@ class HttpScanHandler( synchronizerNodeService: SynchronizerNodeService[ScanSynchronizerNode], protected val storeWithIngestion: AppStoreWithIngestion[ScanStore], updateHistory: UpdateHistory, - appRewardsStoreO: Option[DbScanAppRewardsStore], - appActivityStoreO: Option[AppActivityStore], + appRewardsStore: DbScanAppRewardsStore, + appActivityStore: AppActivityStore, snapshotStore: AcsSnapshotStore, eventStore: ScanEventStore, bulkStorage: Option[BulkStorageReader], dsoAnsResolver: DsoAnsResolver, miningRoundsCacheTimeToLiveOverride: Option[NonNegativeFiniteDuration], enableForcedAcsSnapshots: Boolean, - serveAppActivityRecordsAndTraffic: Boolean, clock: Clock, protected val loggerFactory: NamedLoggerFactory, protected val packageVersionSupport: PackageVersionSupport, @@ -692,33 +700,6 @@ class HttpScanHandler( } } - override def listTransactionHistory( - respond: v0.ScanResource.ListTransactionHistoryResponse.type - )( - request: definitions.TransactionHistoryRequest - )(extracted: TraceContext): Future[v0.ScanResource.ListTransactionHistoryResponse] = { - implicit val tc = extracted - withSpan(s"$workflowId.listTransactions") { _ => _ => - val pageEndEventId = - if (request.pageEndEventId.exists(_.isEmpty)) None else request.pageEndEventId - val sortOrder = request.sortOrder - .fold[SortOrder](SortOrder.Ascending) { - case definitions.TransactionHistoryRequest.SortOrder.members.Asc => SortOrder.Ascending - case definitions.TransactionHistoryRequest.SortOrder.members.Desc => SortOrder.Descending - } - - for { - txs <- store.listTransactions( - pageEndEventId, - sortOrder, - PageLimit.tryCreate(request.pageSize.intValue()), - ) - } yield definitions.TransactionHistoryResponse( - txs.map(TxLogEntry.Http.toResponseItem).toVector - ) - } - } - def getUpdateHistory( after: Option[definitions.UpdateHistoryRequestAfter] = None, pageSize: Int, @@ -730,9 +711,9 @@ class HttpScanHandler( implicit val tc: TraceContext = extracted val afterO = after.map { after => val afterRecordTime = parseTimestamp(after.afterRecordTime) - ( - after.afterMigrationId, + TimestampWithMigrationId( afterRecordTime, + after.afterMigrationId, ) } confirmBackfillingIsCompleteThen(updateHistory) { @@ -857,14 +838,11 @@ class HttpScanHandler( case Some((verdictWithViewsO, updateO)) => val verdictRowIdO = verdictWithViewsO.map { case (v, _) => v.rowId } for { - appActivityRecordO <- - if (serveAppActivityRecordsAndTraffic) - verdictRowIdO match { - case Some(rowId) => - eventStore.getAppActivityRecords(Seq(rowId)).map(_.get(rowId)) - case None => Future.successful(None) - } - else Future.successful(None) + appActivityRecordO <- verdictRowIdO match { + case Some(rowId) => + eventStore.getAppActivityRecords(Seq(rowId)).map(_.get(rowId)) + case None => Future.successful(None) + } } yield { val encodedUpdateV2 = updateO .map( @@ -879,12 +857,9 @@ class HttpScanHandler( val verdictEncoded = verdictWithViewsO.map { case (v, views) => ScanHttpEncodings.encodeVerdict(v, views) } - val trafficSummaryEncoded = - if (serveAppActivityRecordsAndTraffic) - verdictWithViewsO.flatMap { case (v, _) => - v.trafficSummaryO.map(ScanHttpEncodings.encodeTrafficSummary) - } - else None + val trafficSummaryEncoded = verdictWithViewsO.flatMap { case (v, _) => + v.trafficSummaryO.map(ScanHttpEncodings.encodeTrafficSummary) + } val appActivityRecordEncoded = appActivityRecordO.map( ScanHttpEncodings.encodeAppActivityRecord ) @@ -928,7 +903,7 @@ class HttpScanHandler( implicit val tc: TraceContext = extracted val afterO = after.map { a => val afterRecordTime = parseTimestamp(a.afterRecordTime) - (a.afterMigrationId, afterRecordTime) + TimestampWithMigrationId(afterRecordTime, a.afterMigrationId) } confirmBackfillingIsCompleteThen(updateHistory) { @@ -941,9 +916,7 @@ class HttpScanHandler( verdictRowIds = events.flatMap { case (verdictWithViewsO, _) => verdictWithViewsO.map { case (v, _) => v.rowId } } - appActivityRecordMap <- - if (serveAppActivityRecordsAndTraffic) eventStore.getAppActivityRecords(verdictRowIds) - else Future.successful(Map.empty[Long, eventStore.AppActivityRecordT]) + appActivityRecordMap <- eventStore.getAppActivityRecords(verdictRowIds) } yield events.map { case (verdictWithViewsO, updateO) => val encodedUpdateV2 = updateO .map( @@ -958,12 +931,9 @@ class HttpScanHandler( val verdictEncoded = verdictWithViewsO.map { case (v, views) => ScanHttpEncodings.encodeVerdict(v, views) } - val trafficSummaryEncoded = - if (serveAppActivityRecordsAndTraffic) - verdictWithViewsO.flatMap { case (v, _) => - v.trafficSummaryO.map(ScanHttpEncodings.encodeTrafficSummary) - } - else None + val trafficSummaryEncoded = verdictWithViewsO.flatMap { case (v, _) => + v.trafficSummaryO.map(ScanHttpEncodings.encodeTrafficSummary) + } val appActivityRecordEncoded = verdictWithViewsO.flatMap { case (v, _) => appActivityRecordMap.get(v.rowId).map(ScanHttpEncodings.encodeAppActivityRecord) } @@ -1541,19 +1511,18 @@ class HttpScanHandler( } // Shared between /v0/state/acs and /v1/state/acs. The only difference between them is in `toResponse`. - private def acsSnapshotQuery[T](request: AcsRequest, toResponse: QueryAcsSnapshotResult => T)( - implicit tc: TraceContext + private def acsSnapshotQuery[T]( + migrationId: Long, + recordTime: java.time.OffsetDateTime, + recordTimeIsAtOrBefore: Boolean, + after: Option[AcsSnapshotStore.QueryAcsSnapshotPaginationToken], + pageSize: Int, + partyIds: Option[Vector[String]], + templates: Option[Vector[String]], + toResponse: QueryAcsSnapshotResult => T, + )(implicit + tc: TraceContext ): Future[Either[String, T]] = { - val AcsRequest( - migrationId, - recordTime, - recordTimeMatch, - after, - pageSize, - partyIds, - templates, - ) = request - def exactQuery(recordTimeTs: CantonTimestamp) = snapshotStore .queryAcsSnapshot( migrationId, @@ -1578,7 +1547,7 @@ class HttpScanHandler( queryWithOptionalAtOrBefore( migrationId, recordTime, - recordTimeMatch.contains(AcsRequest.RecordTimeMatch.AtOrBefore), + recordTimeIsAtOrBefore, exactQuery, toResponse, ) @@ -1598,7 +1567,9 @@ class HttpScanHandler( event.event, ) ), - result.afterToken, + result.afterToken.map { + case QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken(after) => after + }, ) } @@ -1616,7 +1587,26 @@ class HttpScanHandler( event.event, ) ), - result.afterToken, + result.afterToken.map { + case QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken(after) => after + }, + ) + + private def toAcsV2Response(migrationId: Long, result: QueryAcsSnapshotResult)(implicit + tc: TraceContext + ) = + definitions.AcsResponseV2( + Codec.encode(result.snapshotRecordTime), + migrationId, + result.createdEventsInPage + .map(event => + CompactJsonScanHttpEncodings().javaToHttpActiveContract( + event.eventId, + event.recordTime, + event.event, + ) + ), + result.afterToken.map(_.encodeToBase64), ) override def getAcsSnapshotAt(respond: ScanResource.GetAcsSnapshotAtResponse.type)( @@ -1630,7 +1620,19 @@ class HttpScanHandler( ) withSpan(s"$workflowId.getAcsSnapshotAt") { _ => _ => - acsSnapshotQuery(body, toResponse).map { + acsSnapshotQuery( + migrationId = body.migrationId, + recordTime = body.recordTime, + recordTimeIsAtOrBefore = + body.recordTimeMatch.contains(AcsRequest.RecordTimeMatch.AtOrBefore), + after = body.after.map( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken(_) + ), + pageSize = body.pageSize, + partyIds = body.partyIds, + templates = body.templates, + toResponse = toResponse, + ).map { case Right(response) => response case Left(errorMessage) => ScanResource.GetAcsSnapshotAtResponseNotFound( @@ -1652,7 +1654,19 @@ class HttpScanHandler( } withSpan(s"$workflowId.getAcsSnapshotAtV1") { _ => _ => - acsSnapshotQuery(body, toResponse).map { + acsSnapshotQuery( + migrationId = body.migrationId, + recordTime = body.recordTime, + recordTimeIsAtOrBefore = + body.recordTimeMatch.contains(AcsRequest.RecordTimeMatch.AtOrBefore), + after = body.after.map( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken(_) + ), + pageSize = body.pageSize, + partyIds = body.partyIds, + templates = body.templates, + toResponse = toResponse, + ).map { case Right(response) => response case Left(errorMessage) => ScanResource.GetAcsSnapshotAtV1ResponseNotFound( @@ -1662,21 +1676,50 @@ class HttpScanHandler( } } + override def getAcsSnapshotAtV2(respond: ScanResource.GetAcsSnapshotAtV2Response.type)( + body: AcsRequestV2 + )(extracted: TraceContext): Future[ScanResource.GetAcsSnapshotAtV2Response] = { + implicit val tc: TraceContext = extracted + + def toResponse(result: QueryAcsSnapshotResult) = { + ScanResource.GetAcsSnapshotAtV2ResponseOK( + toAcsV2Response(body.migrationId, result) + ) + } + + withSpan(s"$workflowId.getAcsSnapshotAtV1") { _ => _ => + acsSnapshotQuery( + migrationId = body.migrationId, + recordTime = body.recordTime, + recordTimeIsAtOrBefore = + body.recordTimeMatch.contains(AcsRequestV2.RecordTimeMatch.AtOrBefore), + after = + body.after.map(AcsSnapshotStore.QueryAcsSnapshotPaginationToken.tryDecodeFromBase64), + pageSize = body.pageSize, + partyIds = body.partyIds, + templates = body.templates, + toResponse = toResponse, + ).map { + case Right(response) => response + case Left(errorMessage) => + ScanResource.GetAcsSnapshotAtV2ResponseNotFound( + ErrorResponse(errorMessage) + ) + } + } + } + private def holdingStateQuery[T]( - request: HoldingsStateRequest, + migrationId: Long, + recordTime: java.time.OffsetDateTime, + recordTimeIsAtOrBefore: Boolean, + after: Option[AcsSnapshotStore.QueryAcsSnapshotPaginationToken], + pageSize: Int, + ownerPartyIds: Vector[String], toResponse: QueryAcsSnapshotResult => T, )(implicit tc: TraceContext ): Future[Either[String, T]] = { - val HoldingsStateRequest( - migrationId, - recordTime, - recordTimeMatch, - after, - pageSize, - ownerPartyIds, - ) = request - def exactQuery(recordTimeTs: CantonTimestamp) = snapshotStore .getHoldingsState( migrationId, @@ -1689,7 +1732,7 @@ class HttpScanHandler( queryWithOptionalAtOrBefore( migrationId, recordTime, - recordTimeMatch.contains(HoldingsStateRequest.RecordTimeMatch.AtOrBefore), + recordTimeIsAtOrBefore, exactQuery, toResponse, ) @@ -1703,7 +1746,18 @@ class HttpScanHandler( ScanResource.GetHoldingsStateAtResponseOK(toAcsV0Response(body.migrationId, result)) withSpan(s"$workflowId.getHoldingsStateAt") { _ => _ => - holdingStateQuery(body, toResponse).map { + holdingStateQuery( + migrationId = body.migrationId, + recordTime = body.recordTime, + recordTimeIsAtOrBefore = + body.recordTimeMatch.contains(HoldingsStateRequest.RecordTimeMatch.AtOrBefore), + after = body.after.map( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken(_) + ), + pageSize = body.pageSize, + ownerPartyIds = body.ownerPartyIds, + toResponse = toResponse, + ).map { case Right(response) => response case Left(errorMessage) => ScanResource.GetHoldingsStateAtResponseNotFound( @@ -1721,7 +1775,18 @@ class HttpScanHandler( ScanResource.GetHoldingsStateAtV1ResponseOK(toAcsV1Response(body.migrationId, result)) withSpan(s"$workflowId.getHoldingsStateAtV1") { _ => _ => - holdingStateQuery(body, toResponse).map { + holdingStateQuery( + migrationId = body.migrationId, + recordTime = body.recordTime, + recordTimeIsAtOrBefore = + body.recordTimeMatch.contains(HoldingsStateRequest.RecordTimeMatch.AtOrBefore), + after = body.after.map( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken(_) + ), + pageSize = body.pageSize, + ownerPartyIds = body.ownerPartyIds, + toResponse, + ).map { case Right(response) => response case Left(errorMessage) => ScanResource.GetHoldingsStateAtV1ResponseNotFound( @@ -1731,6 +1796,34 @@ class HttpScanHandler( } } + override def getHoldingsStateAtV2(respond: ScanResource.GetHoldingsStateAtV2Response.type)( + body: HoldingsStateRequestV2 + )(extracted: TraceContext): Future[ScanResource.GetHoldingsStateAtV2Response] = { + implicit val tc: TraceContext = extracted + def toResponse(result: QueryAcsSnapshotResult) = + ScanResource.GetHoldingsStateAtV2ResponseOK(toAcsV2Response(body.migrationId, result)) + + withSpan(s"$workflowId.getHoldingsStateAtV1") { _ => _ => + holdingStateQuery( + migrationId = body.migrationId, + recordTime = body.recordTime, + recordTimeIsAtOrBefore = + body.recordTimeMatch.contains(HoldingsStateRequestV2.RecordTimeMatch.AtOrBefore), + after = + body.after.map(AcsSnapshotStore.QueryAcsSnapshotPaginationToken.tryDecodeFromBase64), + pageSize = body.pageSize, + ownerPartyIds = body.ownerPartyIds, + toResponse, + ).map { + case Right(response) => response + case Left(errorMessage) => + ScanResource.GetHoldingsStateAtV2ResponseNotFound( + ErrorResponse(errorMessage) + ) + } + } + } + override def getHoldingsSummaryAt(respond: ScanResource.GetHoldingsSummaryAtResponse.type)( body: HoldingsSummaryRequest )(extracted: TraceContext): Future[ScanResource.GetHoldingsSummaryAtResponse] = { @@ -2118,11 +2211,13 @@ class HttpScanHandler( val after = body.pageToken.map(_.longValue) for { page <- votesStore.listVoteRequestResults( - body.actionName, - body.accepted, - body.requester, - body.effectiveFrom, - body.effectiveTo, + VoteResultsFilters( + body.actionName, + body.accepted, + requester = body.requester, + effectiveFrom = body.effectiveFrom, + effectiveTo = body.effectiveTo, + ), limit, after, ) @@ -2149,6 +2244,29 @@ class HttpScanHandler( } } + override def countVoteRequestResults( + respond: ScanResource.CountVoteRequestResultsResponse.type + )( + body: CountVoteResultsRequest + )(extracted: TraceContext): Future[ScanResource.CountVoteRequestResultsResponse] = { + implicit val tc: TraceContext = extracted + withSpan(s"$workflowId.countVoteRequestResults") { _ => _ => + for { + count <- votesStore.countVoteRequestResults( + VoteResultsFilters( + body.actionName, + body.accepted, + requester = body.requester, + effectiveFrom = body.effectiveFrom, + effectiveTo = body.effectiveTo, + ) + ) + } yield ScanResource.CountVoteRequestResultsResponse.OK( + definitions.CountVoteResultsResponse(count) + ) + } + } + override def getPreviousSvRewardWeight( respond: ScanResource.GetPreviousSvRewardWeightResponse.type )( @@ -2499,7 +2617,7 @@ class HttpScanHandler( val entry = definitions.SynchronizerBftSequencer( psid.serial.unwrap.toLong, id.toProtoPrimitive, - bftSequencer.p2pUrl, + bftSequencer.p2pUrl.toString, ) initializedBftSequencersCache.put(idx, entry).discard Some(entry) @@ -2622,6 +2740,29 @@ class HttpScanHandler( } } + override def getBulkObjectChecksums(respond: ScanResource.GetBulkObjectChecksumsResponse.type)( + body: GetBulkObjectChecksumsRequest + )(extracted: TraceContext): Future[ScanResource.GetBulkObjectChecksumsResponse] = { + implicit val tc = extracted + withSpan(s"$workflowId.getBulkObjectChecksums") { _ => _ => + bulkStorage.fold( + Future.failed[ScanResource.GetBulkObjectChecksumsResponse]( + Status.UNIMPLEMENTED + .withDescription("Bulk storage is not configured") + .asRuntimeException() + ) + ) { bulkStorage => + bulkStorage.getObjectChecksums(body.objectKeys).map { checksums => + ScanResource.GetBulkObjectChecksumsResponse.OK( + definitions.GetBulkObjectChecksumsResponse( + checksums.map(definitions.GetBulkObjectChecksumsResponse.Checksums(_)).toVector + ) + ) + } + } + } + } + def getRollForwardLsu(respond: ScanResource.GetRollForwardLsuResponse.type)()( extracted: TraceContext ): Future[ScanResource.GetRollForwardLsuResponse] = { @@ -2707,23 +2848,14 @@ class HttpScanHandler( ] = { implicit val tc = extracted withSpan(s"$workflowId.getRewardAccountingEarliestAvailableRound") { _ => _ => - appActivityStoreO match { - case Some(appActivityStore) => - appActivityStore.earliestRoundWithCompleteAppActivity().map { - case Some(round) => - ScanResource.GetRewardAccountingEarliestAvailableRoundResponse.OK( - definitions.GetRewardAccountingEarliestAvailableRoundResponse(round) - ) - case None => - ScanResource.GetRewardAccountingEarliestAvailableRoundResponse.NotFound( - ErrorResponse("No reward accounting data available yet") - ) - } + appActivityStore.earliestRoundWithCompleteAppActivity().map { + case Some(round) => + ScanResource.GetRewardAccountingEarliestAvailableRoundResponse.OK( + definitions.GetRewardAccountingEarliestAvailableRoundResponse(round) + ) case None => - Future.successful( - ScanResource.GetRewardAccountingEarliestAvailableRoundResponse.NotFound( - ErrorResponse("Reward accounting is not enabled") - ) + ScanResource.GetRewardAccountingEarliestAvailableRoundResponse.NotFound( + ErrorResponse("No reward accounting data available yet") ) } } @@ -2746,44 +2878,36 @@ class HttpScanHandler( ) ) withSpan(s"$workflowId.getRewardAccountingActivityTotals") { _ => _ => - (appRewardsStoreO, appActivityStoreO) match { - case (Some(appRewardsStore), Some(appActivityStore)) => - appRewardsStore.getAppActivityRoundTotalByRound(roundNumber).flatMap { - case Some(activityTotal) => - appRewardsStore.getAppRewardRoundTotalByRound(roundNumber).map { - case Some(rewardTotal) => - ScanResource.GetRewardAccountingActivityTotalsResponse.OK( - definitions.GetRewardAccountingActivityTotalsResponse( - definitions.RewardAccountingActivityTotalsOk( - status = "Ok", - roundNumber = activityTotal.roundNumber, - totalAppActivityWeight = activityTotal.totalRoundAppActivityWeight, - activePartiesCount = activityTotal.activeAppProviderPartiesCount, - activityRecordsCount = activityTotal.activityRecordsCount, - totalAppRewardMintingAllowance = - rewardTotal.totalAppRewardMintingAllowance.toString, - totalAppRewardThresholded = rewardTotal.totalAppRewardThresholded.toString, - totalAppRewardUnclaimed = rewardTotal.totalAppRewardUnclaimed.toString, - rewardedAppProviderPartiesCount = - rewardTotal.rewardedAppProviderPartiesCount, - ) - ) + appRewardsStore.getAppActivityRoundTotalByRound(roundNumber).flatMap { + case Some(activityTotal) => + appRewardsStore.getAppRewardRoundTotalByRound(roundNumber).map { + case Some(rewardTotal) => + ScanResource.GetRewardAccountingActivityTotalsResponse.OK( + definitions.GetRewardAccountingActivityTotalsResponse( + definitions.RewardAccountingActivityTotalsOk( + status = "Ok", + roundNumber = activityTotal.roundNumber, + totalAppActivityWeight = activityTotal.totalRoundAppActivityWeight, + activePartiesCount = activityTotal.activeAppProviderPartiesCount, + activityRecordsCount = activityTotal.activityRecordsCount, + totalAppRewardMintingAllowance = + rewardTotal.totalAppRewardMintingAllowance.toString, + totalAppRewardThresholded = rewardTotal.totalAppRewardThresholded.toString, + totalAppRewardUnclaimed = rewardTotal.totalAppRewardUnclaimed.toString, + rewardedAppProviderPartiesCount = rewardTotal.rewardedAppProviderPartiesCount, ) - case None => - // We should never hit this, as both activity totals and round - // totals are added in a single DB Tx - undetermined - } + ) + ) case None => - appActivityStore.earliestIngestedRound().map { - case Some(earliestIngested) if roundNumber <= earliestIngested => - cannotProvide - case _ => - undetermined - } + // We should never hit this, as both activity totals and round + // totals are added in a single DB Tx + undetermined + } + case None => + appActivityStore.ingestionStatusForRound(roundNumber).map { + case RoundIngestionStatus.CannotProvide => cannotProvide + case RoundIngestionStatus.Undetermined => undetermined } - case _ => - Future.successful(cannotProvide) } } } @@ -2805,31 +2929,24 @@ class HttpScanHandler( ) ) withSpan(s"$workflowId.getRewardAccountingRootHash") { _ => _ => - (appRewardsStoreO, appActivityStoreO) match { - case (Some(appRewardsStore), Some(appActivityStore)) => - appRewardsStore.getAppRewardRootHashByRound(roundNumber).flatMap { - case Some(rootHash) => - Future.successful( - ScanResource.GetRewardAccountingRootHashResponse.OK( - definitions.GetRewardAccountingRootHashResponse( - definitions.RewardAccountingRootHashOk( - status = "Ok", - roundNumber = rootHash.roundNumber, - rootHash = rootHash.rootHash.toHex, - ) - ) + appRewardsStore.getAppRewardRootHashByRound(roundNumber).flatMap { + case Some(rootHash) => + Future.successful( + ScanResource.GetRewardAccountingRootHashResponse.OK( + definitions.GetRewardAccountingRootHashResponse( + definitions.RewardAccountingRootHashOk( + status = "Ok", + roundNumber = rootHash.roundNumber, + rootHash = rootHash.rootHash.toHex, ) ) - case None => - appActivityStore.earliestIngestedRound().map { - case Some(earliestIngested) if roundNumber <= earliestIngested => - cannotProvide - case _ => - undetermined - } + ) + ) + case None => + appActivityStore.ingestionStatusForRound(roundNumber).map { + case RoundIngestionStatus.CannotProvide => cannotProvide + case RoundIngestionStatus.Undetermined => undetermined } - case _ => - Future.successful(cannotProvide) } } } @@ -2841,50 +2958,41 @@ class HttpScanHandler( ] = { implicit val tc = extracted withSpan(s"$workflowId.getRewardAccountingBatch") { _ => _ => - appRewardsStoreO match { - case None => - Future.successful( + appRewardsStore + .lookupBatchByHash(roundNumber, DbScanAppRewardsStore.RewardHash.fromHex(batchHash)) + .map { + case None => ScanResource.GetRewardAccountingBatchResponse.NotFound( - ErrorResponse("Reward accounting is not enabled on this node") + ErrorResponse( + s"Batch not (yet) found for round $roundNumber with hash $batchHash" + ) ) - ) - case Some(appRewardsStore) => - appRewardsStore - .lookupBatchByHash(roundNumber, DbScanAppRewardsStore.RewardHash.fromHex(batchHash)) - .map { - case None => - ScanResource.GetRewardAccountingBatchResponse.NotFound( - ErrorResponse( - s"Batch not (yet) found for round $roundNumber with hash $batchHash" - ) + case Some(batch: DbScanAppRewardsStore.BatchOfBatches) => + ScanResource.GetRewardAccountingBatchResponse.OK( + definitions.GetRewardAccountingBatchResponse( + definitions.RewardAccountingBatchOfBatches( + batchType = "BatchOfBatches", + childHashes = batch.childHashes.map(_.toHex).toVector, ) - case Some(batch: DbScanAppRewardsStore.BatchOfBatches) => - ScanResource.GetRewardAccountingBatchResponse.OK( - definitions.GetRewardAccountingBatchResponse( - definitions.RewardAccountingBatchOfBatches( - batchType = "BatchOfBatches", - childHashes = batch.childHashes.map(_.toHex).toVector, - ) - ) - ) - case Some(batch: DbScanAppRewardsStore.BatchOfMintingAllowances) => - ScanResource.GetRewardAccountingBatchResponse.OK( - definitions.GetRewardAccountingBatchResponse( - definitions.RewardAccountingBatchOfMintingAllowances( - batchType = "BatchOfMintingAllowances", - mintingAllowances = batch.allowances - .map(a => - definitions.RewardAccountingMintingAllowance( - provider = a.provider, - amount = a.amount.toString, - ) - ) - .toVector, + ) + ) + case Some(batch: DbScanAppRewardsStore.BatchOfMintingAllowances) => + ScanResource.GetRewardAccountingBatchResponse.OK( + definitions.GetRewardAccountingBatchResponse( + definitions.RewardAccountingBatchOfMintingAllowances( + batchType = "BatchOfMintingAllowances", + mintingAllowances = batch.allowances + .map(a => + definitions.RewardAccountingMintingAllowance( + provider = a.provider, + amount = a.amount.toString, + ) ) - ) + .toVector, ) - } - } + ) + ) + } } } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/ScanHttpEncodings.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/ScanHttpEncodings.scala index 3c43d3695a..e8d9b20c5f 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/ScanHttpEncodings.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/ScanHttpEncodings.scala @@ -636,6 +636,12 @@ object ScanHttpEncodings { ) } + def fromDamlValueEncoding(encoding: definitions.DamlValueEncoding): ScanHttpEncodings = + encoding match { + case definitions.DamlValueEncoding.members.CompactJson => CompactJsonScanHttpEncodings() + case definitions.DamlValueEncoding.members.ProtobufJson => ProtobufJsonScanHttpEncodings + } + def encodeUpdate( update: TreeUpdateWithMigrationId, encoding: definitions.DamlValueEncoding, @@ -656,10 +662,7 @@ object ScanHttpEncodings { externalTransactionHashThresholdTime, ) } - val encodings: ScanHttpEncodings = encoding match { - case definitions.DamlValueEncoding.members.CompactJson => CompactJsonScanHttpEncodings() - case definitions.DamlValueEncoding.members.ProtobufJson => ProtobufJsonScanHttpEncodings - } + val encodings: ScanHttpEncodings = fromDamlValueEncoding(encoding) // v0 always returns the update ids as `#` prefixed,as that's the way they were encoded in canton. v1 returns it without the `#` encodings.lapiToHttpUpdate( update2, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/AcsSnapshotTriggerBase.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/AcsSnapshotTriggerBase.scala index 063596de69..affced0084 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/AcsSnapshotTriggerBase.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/AcsSnapshotTriggerBase.scala @@ -26,6 +26,7 @@ import org.apache.pekko.stream.Materializer import org.lfdecentralizedtrust.splice.scan.config.ScanStorageConfig import org.lfdecentralizedtrust.splice.store.UpdateHistory import org.lfdecentralizedtrust.splice.store.HistoryMetrics.AcsSnapshotsMetrics +import org.lfdecentralizedtrust.splice.store.db.AdvisoryLocks import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} @@ -126,7 +127,7 @@ abstract class AcsSnapshotTriggerBase( case Success(result) => snapshotMetrics.waitingForLock.updateValue(0) Success(result) - case Failure(e: AcsSnapshotStore.FailedToAcquireLockException) => + case Failure(e: AdvisoryLocks.FailedToAcquireLockException) => // It is expected that we sometimes fail to acquire the lock on the snapshot table. // The time until the lock is released is typically much larger than our task retry timeouts, // so we can just silently skip the task and try again later. diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanAutomationService.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanAutomationService.scala index e46e2bafab..6bf147e2db 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanAutomationService.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanAutomationService.scala @@ -47,8 +47,8 @@ class ScanAutomationService( protected val loggerFactory: NamedLoggerFactory, store: ScanStore, val updateHistory: UpdateHistory, - appRewardsStoreO: Option[DbScanAppRewardsStore], - appActivityStoreO: Option[AppActivityStore], + appRewardsStore: DbScanAppRewardsStore, + appActivityStore: AppActivityStore, storage: DbStorage, snapshotStore: AcsSnapshotStore, svParty: PartyId, @@ -79,10 +79,7 @@ class ScanAutomationService( def registerRewardComputationTrigger( rewardsReferenceStore: ScanRewardsReferenceStore ): Unit = - for { - appRewardsStore <- appRewardsStoreO - appActivityStore <- appActivityStoreO - } registerTrigger( + registerTrigger( new RewardComputationTrigger( appRewardsStore, appActivityStore, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanHistoryBackfillingTrigger.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanHistoryBackfillingTrigger.scala index 3876147017..22f9c15175 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanHistoryBackfillingTrigger.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanHistoryBackfillingTrigger.scala @@ -14,11 +14,7 @@ import org.lfdecentralizedtrust.splice.automation.{ import org.lfdecentralizedtrust.splice.config.UpgradesConfig import org.lfdecentralizedtrust.splice.environment.SpliceLedgerClient import org.lfdecentralizedtrust.splice.http.HttpClient -import org.lfdecentralizedtrust.splice.scan.admin.api.client.{ - BackfillingScanConnection, - BftScanConnection, -} -import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig +import org.lfdecentralizedtrust.splice.scan.admin.api.client.BackfillingScanConnection import org.lfdecentralizedtrust.splice.scan.store.ScanHistoryBackfilling.{ FoundingTransactionTreeUpdate, InitialTransactionTreeUpdate, @@ -30,17 +26,19 @@ import org.lfdecentralizedtrust.splice.store.{ HistoryMetrics, ImportUpdatesBackfilling, PageLimit, + TimestampWithMigrationId, TreeUpdateWithMigrationId, UpdateHistory, } import org.lfdecentralizedtrust.splice.util.TemplateJsonDecoder import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.lifecycle.{AsyncOrSyncCloseable, SyncCloseable} +import com.digitalasset.canton.lifecycle.{AsyncOrSyncCloseable, LifeCycle} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer +import org.lfdecentralizedtrust.splice.scan.util.PeerBftScanConnection import org.lfdecentralizedtrust.splice.store.UpdateHistory.BackfillingState import scala.concurrent.{ExecutionContextExecutor, Future, blocking} @@ -65,6 +63,17 @@ class ScanHistoryBackfillingTrigger( private val currentMigrationId = updateHistory.domainMigrationId + private val scanConnection = new PeerBftScanConnection( + store, + svName, + ledgerClient, + context.config, + upgradesConfig, + context.clock, + context.retryProvider, + loggerFactory, + ) + private val historyMetrics = new HistoryMetrics(context.metricsFactory)( MetricsContext( "current_migration_id" -> currentMigrationId.toString @@ -77,11 +86,7 @@ class ScanHistoryBackfillingTrigger( */ @SuppressWarnings(Array("org.wartremover.warts.Var")) @volatile - private var findHistoryStartAfter: Option[(Long, CantonTimestamp)] = None - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - @volatile - private var connectionVar: Option[BftScanConnection] = None + private var findHistoryStartAfter: Option[TimestampWithMigrationId] = None @SuppressWarnings(Array("org.wartremover.warts.Var")) @volatile @@ -210,7 +215,8 @@ class ScanHistoryBackfillingTrigger( PageLimit.tryCreate(batchSize), ) _ = updates.lastOption.foreach(u => - findHistoryStartAfter = Some(u.migrationId -> u.update.update.recordTime) + findHistoryStartAfter = + Some(TimestampWithMigrationId(u.update.update.recordTime, u.migrationId)) ) result <- if (updates.isEmpty) { @@ -222,34 +228,6 @@ class ScanHistoryBackfillingTrigger( } } - private def getOrCreateScanConnection()(implicit tc: TraceContext): Future[BftScanConnection] = - blocking { - mutex.exclusive { - connectionVar match { - case Some(connection) => - Future.successful(connection) - case None => - for { - connection <- BftScanConnection.peerScanConnection( - () => BftScanConnection.Bft.getPeerScansFromStore(store, svName), - ledgerClient, - // When the network is starting up, the pool of SVs is changing fast - // Using a short refresh interval to quickly pick up new SVs - scansRefreshInterval = context.config.pollingInterval, - amuletRulesCacheTimeToLive = ScanAppClientConfig.DefaultAmuletRulesCacheTimeToLive, - upgradesConfig, - context.clock, - context.retryProvider, - loggerFactory, - ) - } yield { - connectionVar = Some(connection) - connection - } - } - } - } - private def getOrCreateBackfilling( connection: BackfillingScanConnection ): ScanHistoryBackfilling = blocking { @@ -273,7 +251,7 @@ class ScanHistoryBackfillingTrigger( } private def performBackfilling()(implicit traceContext: TraceContext): Future[TaskOutcome] = for { - connection <- getOrCreateScanConnection() + connection <- scanConnection.connection backfilling = getOrCreateBackfilling(connection) outcome <- backfilling.backfill().map { case HistoryBackfilling.Outcome.MoreWorkAvailableNow(workDone) => @@ -305,7 +283,7 @@ class ScanHistoryBackfillingTrigger( private def performImportUpdatesBackfilling()(implicit traceContext: TraceContext ): Future[TaskOutcome] = for { - connection <- getOrCreateScanConnection() + connection <- scanConnection.connection backfilling = getOrCreateBackfilling(connection) outcome <- backfilling.backfillImportUpdates().map { case ImportUpdatesBackfilling.Outcome.MoreWorkAvailableNow(workDone) => @@ -327,21 +305,15 @@ class ScanHistoryBackfillingTrigger( } yield outcome override def closeAsync(): Seq[AsyncOrSyncCloseable] = { - connectionVar - .map(connection => - SyncCloseable( - "closing scan connection", - connection.close(), - ) - ) - .toList + LifeCycle.close(scanConnection)(logger) + super.closeAsync() } } object ScanHistoryBackfillingTrigger { sealed trait Task extends PrettyPrinting final case class InitializeBackfillingTask( - after: Option[(Long, CantonTimestamp)] + after: Option[TimestampWithMigrationId] ) extends Task { override def pretty: Pretty[this.type] = prettyOfClass(param("after", _.after)) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictAutomationService.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictAutomationService.scala index 4341f3e5fe..d40da689ce 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictAutomationService.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictAutomationService.scala @@ -34,7 +34,7 @@ class ScanVerdictAutomationService( migrationId: Long, synchronizerId: SynchronizerId, ingestionMetrics: ScanMediatorVerdictIngestionMetrics, - rewardsReferenceStoreO: Option[ScanRewardsReferenceStore], + rewardsReferenceStore: ScanRewardsReferenceStore, )(implicit ec: ExecutionContextExecutor, mat: Materializer, @@ -49,10 +49,8 @@ class ScanVerdictAutomationService( override def companion: AutomationServiceCompanion = ScanVerdictAutomationService - private val appActivityComputationO: Option[AppActivityComputation] = - rewardsReferenceStoreO.map { store => - new AppActivityComputation(store, loggerFactory) - } + private val appActivityComputation: AppActivityComputation = + new AppActivityComputation(rewardsReferenceStore, loggerFactory) registerService( new ScanVerdictIngestionService( @@ -63,7 +61,7 @@ class ScanVerdictAutomationService( migrationId = migrationId, synchronizerId = synchronizerId, ingestionMetrics = ingestionMetrics, - appActivityComputationO = appActivityComputationO, + appActivityComputation = appActivityComputation, backoffClock = triggerContext.pollingClock, retryProvider = triggerContext.retryProvider, loggerFactory = triggerContext.loggerFactory, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionService.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionService.scala index 302df6e015..5a8da6401b 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionService.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionService.scala @@ -74,7 +74,7 @@ class ScanVerdictIngestionService( migrationId: Long, synchronizerId: SynchronizerId, ingestionMetrics: ScanMediatorVerdictIngestionMetrics, - appActivityComputationO: Option[AppActivityComputation], + appActivityComputation: AppActivityComputation, backoffClock: Clock, override protected val retryProvider: RetryProvider, override protected val loggerFactory: NamedLoggerFactory, @@ -107,10 +107,7 @@ class ScanVerdictIngestionService( private def waitForStores(): Future[Unit] = for { _ <- store.waitUntilInitialized - _ <- appActivityComputationO match { - case Some(appActivityComputation) => appActivityComputation.waitUntilInitialized - case None => Future.unit - } + _ <- appActivityComputation.waitUntilInitialized } yield () /** When starting a fresh stream, the record time from which to start streaming */ @@ -130,7 +127,7 @@ class ScanVerdictIngestionService( streamVerdictsAndBatchWithTraffic( ingestionStart, currentMediatorClient, - synchronizerNodes.current.sequencerTrafficClient, + Some(synchronizerNodes.current.sequencerTrafficClient), ) val completedWithCompleteF = Promise[Option[v30.VerdictsResponse.Complete]]() val source = currentSource @@ -157,7 +154,7 @@ class ScanVerdictIngestionService( streamVerdictsAndBatchWithTraffic( successorIngestionStart, successorMediatorClient, - synchronizerNodes.successor.flatMap(_.sequencerTrafficClient), + synchronizerNodes.successor.map(_.sequencerTrafficClient), ) .mapMaterializedValue(_ => NotUsed) case None => @@ -243,30 +240,35 @@ class ScanVerdictIngestionService( // Compute app activity records (before DB transaction). // Records have verdictRowId = DUMMY_VERDICT_ROW_ID // the store resolves actual row_ids during insertion. - (appActivityRecords, lastArchivedRoundO) <- appActivityComputationO match { - case Some(appActivityComputation) => - for { - records <- appActivityComputation.computeActivities(summariesWithVerdicts).map { - _.flatMap { case (summary, _, recordO) => - recordO.map(summary.sequencingTime -> _) - } - } - lastArchivedRoundO <- verdicts - .map(v => CantonTimestamp.tryFromProtoTimestamp(v.getRecordTime)) - .maxOption match { - case Some(maxRecordTime) => - appActivityComputation.lookupLatestArchivedOpenMiningRound(maxRecordTime) - case None => Future.successful(None) + (appActivityRecords, firstActiveRoundO, lastArchivedRoundO) <- { + val recordTimes = + verdicts.map(v => CantonTimestamp.tryFromProtoTimestamp(v.getRecordTime)) + for { + records <- appActivityComputation.computeActivities(summariesWithVerdicts).map { + _.flatMap { case (summary, _, recordO) => + recordO.map(summary.sequencingTime -> _) } - } yield (records, lastArchivedRoundO) - case None => Future.successful((Seq.empty, None)) + } + firstActiveRoundO <- recordTimes.minOption match { + case Some(minRecordTime) => + appActivityComputation.lookupActiveOpenMiningRound(minRecordTime) + case None => Future.successful(None) + } + lastArchivedRoundO <- recordTimes.maxOption match { + case Some(maxRecordTime) => + appActivityComputation.lookupLatestArchivedOpenMiningRound(maxRecordTime) + case None => Future.successful(None) + } + } yield (records, firstActiveRoundO, lastArchivedRoundO) } _ <- ensureVerdictsHaveTrafficSummaries(verdicts, summaryByTime) _ <- store.insertVerdictsWithAppActivityRecords( items, appActivityRecords, - lastArchivedRoundO, + hasTrafficSummaries = summaryByTime.nonEmpty, + firstActiveRoundO = firstActiveRoundO, + lastArchivedRoundO = lastArchivedRoundO, ) } yield { val lastRecordTime = verdicts.lastOption @@ -354,10 +356,7 @@ class ScanVerdictIngestionService( verdicts: Seq[v30.Verdict], summaryByTime: Map[CantonTimestamp, DbScanVerdictStore.TrafficSummaryT], )(implicit tc: TraceContext): Future[Unit] = - (store.appActivityRecordStoreO match { - case None => Future.successful(None) - case Some(s) => s.startedIngestingAt - }).map { startO => + store.appActivityRecordStore.startedIngestingAt.map { startO => val missingTimes = ScanVerdictIngestionService.findMissingTrafficSummaries( verdicts.map(v => CantonTimestamp.tryFromProtoTimestamp(v.getRecordTime)), summaryByTime.keySet, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/CantonBftPeerConfig.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/CantonBftPeerConfig.scala index 6b979175a5..373ab29fb4 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/CantonBftPeerConfig.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/CantonBftPeerConfig.scala @@ -3,6 +3,8 @@ package org.lfdecentralizedtrust.splice.scan.config +import org.apache.pekko.http.scaladsl.model.Uri + case class CantonBftPeerConfig( - p2pUrl: String + p2pUrl: Uri ) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/ScanAppConfig.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/ScanAppConfig.scala index d9c87180c5..423efc8d51 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/ScanAppConfig.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/ScanAppConfig.scala @@ -47,6 +47,22 @@ final case class BulkStorageConfig( maxParallelPartUploads: Int = 4, staging: Option[S3Config] = None, committed: Option[S3Config] = None, + bftCheckEnabled: Boolean = true, + /** When enabled, the app will reset all progress markers thus force recomputing data from genesis. + * Note that this does not delete any existing data, you usually would want to do that before setting + * this flag. Also, after restarting the app once with this flag enabled, you'd want to disable it back + * to avoid having the markers reset on every restart. + * TODO(#6251): this makes sense for initial stages of testing&deploying bulk storage, in case of + * encountered issues, but will not make sense when we start pruning the data from scan. We should remove + * this before starting to prune data. + */ + debugForceStartFromGenesis: Boolean = false, + /** A list of S3 object keys that this instance should not save to the committed bucket, and instead only + * delete from staging. To be used only in extreme cases where we decide to accept a BFT disagreement, + * and have the (minority of) disagreeing instances simply skip the broken objects. + * Should typically be used in test environments only. + */ + debugObjectsToNotCommit: Seq[String] = Seq.empty, ) /** @param miningRoundsCacheTimeToLiveOverride Intended only for testing! @@ -61,8 +77,6 @@ case class ScanAppBackendConfig( synchronizerNodes: ScanSynchronizerNodesConfig, override val automation: AutomationConfig = AutomationConfig(), mediatorVerdictIngestion: MediatorVerdictIngestionConfig = MediatorVerdictIngestionConfig(), - enableAppActivityRecordAndTrafficIngestion: Boolean = true, - serveAppActivityRecordsAndTraffic: Boolean = true, isFirstSv: Boolean = false, // Max rounding error tolerated wrt actual total of minting allowances // and the per-round minting allowance from the CC whitepaper. diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/ScanStorageConfig.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/ScanStorageConfig.scala index 9eaa7a0e10..1655bc8eee 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/ScanStorageConfig.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/ScanStorageConfig.scala @@ -3,7 +3,9 @@ package org.lfdecentralizedtrust.splice.scan.config +import cats.data.NonEmptyList import com.digitalasset.canton.data.CantonTimestamp +import org.lfdecentralizedtrust.splice.http.v0.definitions import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.{ AcsSnapshot, IncrementalAcsSnapshot, @@ -11,6 +13,7 @@ import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.{ import java.time.{Duration, Instant, ZoneOffset} import java.time.temporal.{ChronoField, ChronoUnit} +import scala.util.matching.Regex /** Note that these configurations must be kept consistent between SVs, * so they are not configured via a local config file in Scan. Instead, they must be voted on. @@ -136,6 +139,26 @@ case class ScanStorageConfig( } +object ScanStorageConfig { + sealed abstract class Encoding( + val key: String, + val damlValueEncoding: definitions.DamlValueEncoding, + ) { + final def storageKey(prefix: String, index: Int): String = s"${prefix}_${key}_$index.zstd" + + final def storageKeyRegex(prefix: String): Regex = + (".*" + Regex.quote(prefix) + "_" + Regex.quote(key) + "_\\d+\\.zstd").r + } + object Encoding { + case object CompactJson + extends Encoding("compact_json", definitions.DamlValueEncoding.CompactJson) + case object ProtobufJson + extends Encoding("protobuf_json", definitions.DamlValueEncoding.ProtobufJson) + + lazy val all: NonEmptyList[Encoding] = NonEmptyList.of[Encoding](CompactJson, ProtobufJson) + } +} + object ScanStorageConfigs { val scanStorageConfigV1 = ScanStorageConfig( dbAcsSnapshotPeriodHours = 3, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/TokenStandardConfig.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/TokenStandardConfig.scala index a0630dcdea..ab0b50830f 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/TokenStandardConfig.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/config/TokenStandardConfig.scala @@ -14,6 +14,7 @@ object TokenStandardConfig { final case class SettlementConfig( maxLegs: Int = 100, maxParties: Int = 100, + maxAllocations: Int = 100, ) { def validateSettleBatch(settleBatch: allocationv2.SettlementFactory_SettleBatch): Unit = { val numTransferLegs = settleBatch.transferLegs.size() @@ -24,6 +25,9 @@ object TokenStandardConfig { .distinct .size validateNumParties(numParties) + + val numAllocations = settleBatch.allocations.size() + validateNumAllocations(numAllocations) } def validateAllocate(allocate: allocationinstructionv2.AllocationFactory_Allocate): Unit = { @@ -56,5 +60,14 @@ object TokenStandardConfig { .asRuntimeException() } } + private def validateNumAllocations(numAllocations: Int) = { + if (numAllocations > maxAllocations) { + throw io.grpc.Status.INVALID_ARGUMENT + .withDescription( + s"Too many allocations in the settle batch: $numAllocations. Maximum allowed: $maxAllocations" + ) + .asRuntimeException() + } + } } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/metrics/ScanAppMetrics.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/metrics/ScanAppMetrics.scala index c4d9af350b..57c32761fa 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/metrics/ScanAppMetrics.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/metrics/ScanAppMetrics.scala @@ -6,8 +6,8 @@ package org.lfdecentralizedtrust.splice.scan.metrics import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.DbStorageHistograms import org.lfdecentralizedtrust.splice.BaseSpliceMetrics +import com.digitalasset.canton.metrics.DbStorageHistograms import org.lfdecentralizedtrust.splice.scan.store.db.DbScanStoreMetrics /** Modelled after [[com.digitalasset.canton.synchronizer.metrics.DomainMetrics]]. diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/rewards/AppActivityComputation.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/rewards/AppActivityComputation.scala index 30b4b9ede5..2181d3fcd6 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/rewards/AppActivityComputation.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/rewards/AppActivityComputation.scala @@ -11,6 +11,7 @@ import com.digitalasset.daml.lf.data.Numeric import com.digitalasset.daml.lf.data.{assertRight as damlRight} import org.lfdecentralizedtrust.splice.scan.store.ScanRewardsReferenceStore import org.lfdecentralizedtrust.splice.scan.store.db.{DbAppActivityRecordStore, DbScanVerdictStore} +import org.lfdecentralizedtrust.splice.store.TimestampWithMigrationId import java.math.RoundingMode import scala.collection.immutable.SortedMap @@ -49,6 +50,14 @@ class AppActivityComputation( )(implicit tc: TraceContext): Future[Option[Long]] = rewardsReferenceStore.lookupLatestArchivedOpenMiningRound(asOf) + /** The OpenMiningRound round number active at asOf, if the round data has been ingested. */ + def lookupActiveOpenMiningRound( + asOf: CantonTimestamp + )(implicit tc: TraceContext): Future[Option[Long]] = + rewardsReferenceStore + .lookupActiveOpenMiningRounds(Seq(asOf)) + .map(_.get(asOf).map { case TimestampWithMigrationId(_, roundNumber) => roundNumber }) + /** Compute app activity records for a batch of verdicts. * * Records are returned with verdictRowId = DUMMY_VERDICT_ROW_ID as a placeholder. @@ -96,7 +105,7 @@ class AppActivityComputation( Future.successful((summary, verdict, None)) case (summary, verdict, true) => roundInfoByTime.get(summary.sequencingTime) match { - case Some((roundNumber, roundOpensAt)) => + case Some(TimestampWithMigrationId(roundOpensAt, roundNumber)) => for { featuredAppWeights <- rewardsReferenceStore.lookupFeaturedAppPartiesAsOf( roundOpensAt @@ -125,8 +134,12 @@ class AppActivityComputation( } case None => // Skip activity record computation as we don't have the necessary round data ingested. - // This can happen for freshly onboarded SVs, but is not - // expected to happen once the first activity record has been computed. + // This can happen for freshly onboarded SVs if the reward + // reference store does not have the data for any of the + // sequencingTime(s) in this batch. + // OTOH this cannot happen after ingestion starts because + // lookupActiveOpenMiningRounds blocks until the reference store + // has caught up to all the sequencingTime(s) in this batch. logger.debug( s"No round data found for sequencingTime=${summary.sequencingTime}, skipping activity record computation" ) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AcsSnapshotStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AcsSnapshotStore.scala index b5dfc209a0..81721f9393 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AcsSnapshotStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AcsSnapshotStore.scala @@ -8,16 +8,21 @@ import com.daml.ledger.javaapi.data.CreatedEvent import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.{Amulet, LockedAmulet} import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.{ AcsSnapshot, - FailedToAcquireLockException, IncrementalAcsSnapshot, IncrementalAcsSnapshotTable, + QueryAcsSnapshotPaginationToken, QueryAcsSnapshotResult, amuletQualifiedName, lockedAmuletQualifiedName, } import org.lfdecentralizedtrust.splice.store.UpdateHistory.SelectFromCreateEvents import org.lfdecentralizedtrust.splice.store.{HardLimit, Limit, LimitHelpers, UpdateHistory} -import org.lfdecentralizedtrust.splice.store.db.{AcsJdbcTypes, AcsQueries, AdvisoryLockIds} +import org.lfdecentralizedtrust.splice.store.db.{ + AcsJdbcTypes, + AcsQueries, + AdvisoryLockIds, + AdvisoryLocks, +} import org.lfdecentralizedtrust.splice.util.{Contract, HoldingsSummary, PackageQualifiedName} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} @@ -34,6 +39,7 @@ import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInt import slick.jdbc.canton.SQLActionBuilder import slick.jdbc.{GetResult, JdbcProfile} +import java.nio.charset.StandardCharsets import java.util.concurrent.Semaphore import scala.concurrent.{ExecutionContext, Future} @@ -220,22 +226,7 @@ class AcsSnapshotStore( private def withExclusiveSnapshotDataLock[T, E <: Effect]( action: DBIOAction[T, NoStream, E] ): DBIOAction[T, NoStream, Effect.Read & Effect.Transactional & E] = - (for { - lockResult <- sql"SELECT pg_try_advisory_xact_lock(${AdvisoryLockIds.acsSnapshotDataInsert})" - .as[Boolean] - .head - result <- lockResult match { - case true => action - // Lock conflicts can happen: - // - In production, if the application crashes while writing a snapshot and then restarts - // and tries to write another snapshot before the database has closed the connection and released the lock. - // - In production, if two triggers (ingesting and backfilling) happen to write a snapshot at the same time. - // - In testing, where multiple scan instances write to the same app database. - // In all cases, we want to fail immediately, and rely on the caller's infrastructure to retry. - case false => - DBIOAction.failed(FailedToAcquireLockException()) - } - } yield result).transactionally + AdvisoryLocks.withTransactionalLock(profile, AdvisoryLockIds.acsSnapshotDataInsert, action) def deleteSnapshot( snapshot: AcsSnapshot @@ -252,7 +243,7 @@ class AcsSnapshotStore( def queryAcsSnapshot( migrationId: Long, snapshot: CantonTimestamp, - after: Option[Long], + after: Option[QueryAcsSnapshotPaginationToken], limit: Limit, partyIds: Seq[PartyId], templates: Seq[PackageQualifiedName], @@ -278,7 +269,11 @@ class AcsSnapshotStore( ) ) begin <- after match { - case Some(value) if value < snapshot.firstRowId || value > snapshot.lastRowId => + case Some( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken( + value + ) + ) if value < snapshot.firstRowId || value > snapshot.lastRowId => Future.failed( io.grpc.Status.INVALID_ARGUMENT .withDescription( @@ -286,7 +281,12 @@ class AcsSnapshotStore( ) .asRuntimeException() ) - case Some(value) => Future.successful(value + 1) + case Some( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken( + value + ) + ) => + Future.successful(value + 1) case None => Future.successful(snapshot.firstRowId) } end = snapshot.lastRowId @@ -356,7 +356,9 @@ class AcsSnapshotStore( migrationId = migrationId, snapshotRecordTime = snapshot.snapshotRecordTime, createdEventsInPage = eventsInPage, - afterToken = afterToken, + afterToken = afterToken.map( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken(_) + ), ) } } @@ -364,7 +366,7 @@ class AcsSnapshotStore( def getHoldingsState( migrationId: Long, snapshot: CantonTimestamp, - after: Option[Long], + after: Option[QueryAcsSnapshotPaginationToken], limit: Limit, partyIds: NonEmptyVector[PartyId], )(implicit tc: TraceContext): Future[QueryAcsSnapshotResult] = { @@ -799,11 +801,6 @@ class AcsSnapshotStore( object AcsSnapshotStore { - case class FailedToAcquireLockException() - extends RuntimeException( - "Failed to acquire advisory lock for writing to the acs snapshot table." - ) - sealed trait IncrementalAcsSnapshotTable { def tableName: String } object IncrementalAcsSnapshotTable { case object Next extends IncrementalAcsSnapshotTable { @@ -922,11 +919,50 @@ object AcsSnapshotStore { ) } + sealed trait QueryAcsSnapshotPaginationToken { + def encodeToBase64: String = { + val jsonString = QueryAcsSnapshotPaginationToken.codec(this).noSpaces + java.util.Base64.getEncoder.encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)) + } + } + object QueryAcsSnapshotPaginationToken { + case class RowIdQueryAcsSnapshotPaginationToken(after: Long) + extends QueryAcsSnapshotPaginationToken + + private val codec: io.circe.Codec[QueryAcsSnapshotPaginationToken] = + io.circe.Codec + .from(io.circe.Decoder[Long], io.circe.Encoder[Long]) + .iemap[QueryAcsSnapshotPaginationToken]((token: Long) => + Right(RowIdQueryAcsSnapshotPaginationToken(token)) + ) { case RowIdQueryAcsSnapshotPaginationToken(after) => after } + + def tryDecodeFromBase64(token: String): QueryAcsSnapshotPaginationToken = { + import cats.implicits.* + + (for { + decodedString <- scala.util + .Try { + val decodedBytes = java.util.Base64.getDecoder.decode(token) + new String(decodedBytes, StandardCharsets.UTF_8) + } + .toEither + .leftMap(_ => "Failed to decode base64 token") + decoded <- io.circe.parser.decode(decodedString)(codec).leftMap(_.getMessage) + } yield decoded).fold( + msg => + throw io.grpc.Status.INVALID_ARGUMENT + .withDescription(msg) + .asRuntimeException(), + identity, + ) + } + } + case class QueryAcsSnapshotResult( migrationId: Long, snapshotRecordTime: CantonTimestamp, createdEventsInPage: Vector[SpliceCreatedEvent], - afterToken: Option[Long], + afterToken: Option[QueryAcsSnapshotPaginationToken], ) private val amuletQualifiedName = diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AppActivityStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AppActivityStore.scala index 2b72c75e71..cba770a875 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AppActivityStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AppActivityStore.scala @@ -12,21 +12,16 @@ import scala.concurrent.Future */ trait AppActivityStore { - /** Find the earliest round for which all app activity records have been ingested. + /** Ingestion status for a specific round, used by the Scan HTTP + * endpoints when no root hash or activity totals are yet stored. */ - def earliestRoundWithCompleteAppActivity()(implicit + def ingestionStatusForRound(roundNumber: Long)(implicit tc: TraceContext - ): Future[Option[Long]] + ): Future[AppActivityStore.RoundIngestionStatus] - /** The earliest round for which we have ingested app activity records. - * This round may not have all app activity records ingested. - * - * Returns None if no app activity records have been ingested. - * - * Return -1 for the first SV, if the ingestion started from beginning of round 0, - * indicating that this SV has complete data of round 0. + /** Find the earliest round for which all app activity records have been ingested. */ - def earliestIngestedRound()(implicit + def earliestRoundWithCompleteAppActivity()(implicit tc: TraceContext ): Future[Option[Long]] @@ -39,3 +34,24 @@ trait AppActivityStore { /** The record time of the first activity record in the store. */ def startedIngestingAt(implicit tc: TraceContext): Future[Option[Long]] } + +object AppActivityStore { + + /** Whether this Scan can ever be authoritative for a given round, + * or whether the answer will arrive as ingestion catches up. + */ + sealed trait RoundIngestionStatus + + object RoundIngestionStatus { + + /** Cannot compute an answer for this round from local state. + * Callers should delegate to BFT read. + */ + case object CannotProvide extends RoundIngestionStatus + + /** Do not yet have an answer but expect to have one after + * ingesting up to this round. Callers should retry. + */ + case object Undetermined extends RoundIngestionStatus + } +} diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanRewardsReferenceStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanRewardsReferenceStore.scala index 244ae565e4..9dda395ffd 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanRewardsReferenceStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanRewardsReferenceStore.scala @@ -10,7 +10,12 @@ import com.digitalasset.canton.tracing.TraceContext import com.github.blemale.scaffeine.Scaffeine import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.rewardaccountingv2.CalculateRewardsV2 import org.lfdecentralizedtrust.splice.codegen.java.splice.round.OpenMiningRound -import org.lfdecentralizedtrust.splice.store.{Limit, MultiDomainAcsStore, SynchronizerStore} +import org.lfdecentralizedtrust.splice.store.{ + Limit, + MultiDomainAcsStore, + SynchronizerStore, + TimestampWithMigrationId, +} import org.lfdecentralizedtrust.splice.util.Contract import scala.concurrent.{ExecutionContext, Future} @@ -52,7 +57,7 @@ class CachingScanRewardsReferenceStore private[splice] ( override def lookupActiveOpenMiningRounds( recordTimes: Seq[CantonTimestamp] - )(implicit tc: TraceContext): Future[Map[CantonTimestamp, (Long, CantonTimestamp)]] = + )(implicit tc: TraceContext): Future[Map[CantonTimestamp, TimestampWithMigrationId]] = store.lookupActiveOpenMiningRounds(recordTimes) override def lookupFeaturedAppPartiesAsOf( diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala index 9206db972d..dec98aef00 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala @@ -35,12 +35,11 @@ import org.lfdecentralizedtrust.splice.store.{ Limit, MiningRoundsStore, MultiDomainAcsStore, - PageLimit, ResultsPage, - SortOrder, SynchronizerStore, TxLogStore, UpdateHistory, + VoteResultsFilters, } import org.lfdecentralizedtrust.splice.util.{Contract, ContractWithState} @@ -191,17 +190,6 @@ class CachingScanStore( store.lookupTransferCommandCounterByParty, ).get(partyId) - override def listTransactions( - pageEndEventId: Option[String], - sortOrder: SortOrder, - limit: PageLimit, - )(implicit tc: TraceContext): Future[Seq[TxLogEntry.TransactionTxLogEntry]] = - store.listTransactions( - pageEndEventId, - sortOrder, - limit, - ) - override def lookupLatestTransferCommandEvents(sender: PartyId, nonce: Long, limit: Int)(implicit tc: TraceContext ): Future[Map[TransferCommand.ContractId, TransferCommandTxLogEntry]] = @@ -221,11 +209,7 @@ class CachingScanStore( ) override def listVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: Limit, after: Option[Long] = None, )(implicit tc: TraceContext): Future[ResultsPage[DsoRules_CloseVoteRequestResult]] = @@ -235,16 +219,21 @@ class CachingScanStore( store.listVoteRequestResults _ tupled, ).get( ( - actionName, - accepted, - requester, - effectiveFrom, - effectiveTo, + filters, limit, after, ) ) + override def countVoteRequestResults( + filters: VoteResultsFilters + )(implicit tc: TraceContext): Future[Long] = + getCache( + "countVoteRequestResults", + cacheConfig.voteRequests, + (f: VoteResultsFilters) => store.countVoteRequestResults(f), + ).get(filters) + override def listVoteRequestsByTrackingCid( voteRequestCids: Seq[VoteRequest.ContractId], limit: Limit, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStore.scala index b2bc6d8b23..d7d378c32b 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStore.scala @@ -10,7 +10,7 @@ import org.lfdecentralizedtrust.splice.scan.store.db.{DbAppActivityRecordStore, import org.lfdecentralizedtrust.splice.store.TreeUpdateWithMigrationId import org.lfdecentralizedtrust.splice.store.UpdateHistory import com.digitalasset.canton.data.CantonTimestamp -import org.lfdecentralizedtrust.splice.store.PageLimit +import org.lfdecentralizedtrust.splice.store.{PageLimit, TimestampWithMigrationId} import scala.collection.immutable.SortedMap import scala.concurrent.{ExecutionContext, Future} @@ -60,7 +60,7 @@ class ScanEventStore( } def getEvents( - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], currentMigrationId: Long, limit: PageLimit, )(implicit tc: TraceContext): Future[Seq[Event]] = { @@ -88,9 +88,9 @@ class ScanEventStore( verdictStore.listTransactionViews(v.rowId).map(views => v -> views) ) } yield { - val verdictEntries: Iterator[((Long, CantonTimestamp), Verdict)] = + val verdictEntries: Iterator[(TimestampWithMigrationId, Verdict)] = verdictsWithViews.iterator.map { case (v, views) => - val k = (v.migrationId, v.recordTime) + val k = TimestampWithMigrationId(v.recordTime, v.migrationId) k -> (v -> views) } @@ -100,11 +100,11 @@ class ScanEventStore( val mergedSorted = { val fromUpdates = filteredUpdates.iterator.foldLeft( SortedMap.empty[ - (Long, CantonTimestamp), + TimestampWithMigrationId, (Option[Verdict], Option[TreeUpdateWithMigrationId]), ] ) { case (acc, u) => - val k = (u.migrationId, u.update.update.recordTime) + val k = TimestampWithMigrationId(u.update.update.recordTime, u.migrationId) acc.updated(k, (None, Some(u))) } verdictEntries.foldLeft(fromUpdates) { case (acc, (k, v)) => @@ -125,10 +125,7 @@ class ScanEventStore( def getAppActivityRecords(verdictRowIds: Seq[Long])(implicit tc: TraceContext ): Future[Map[Long, AppActivityRecordT]] = - verdictStore.appActivityRecordStoreO match { - case Some(store) => store.getRecordsByVerdictRowIds(verdictRowIds) - case None => Future.successful(Map.empty) - } + verdictStore.appActivityRecordStore.getRecordsByVerdictRowIds(verdictRowIds) // Get values from in-memory refs, fallsback to DB read private def resolveCurrentMigrationCap( @@ -154,12 +151,12 @@ class ScanEventStore( // Filtering logic extracted out for unit testing object ScanEventStore { def allowF( - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], currentMigrationId: Long, currentMigrationCap: CantonTimestamp, )(mig: Long, rt: CantonTimestamp): Boolean = { afterO match { - case Some((afterMig, afterRt)) if mig == afterMig => + case Some(TimestampWithMigrationId(afterRt, afterMig)) if mig == afterMig => if (mig < currentMigrationId) rt > afterRt else rt > afterRt && rt <= currentMigrationCap case _ if mig < currentMigrationId => diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanRewardsReferenceStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanRewardsReferenceStore.scala index 505fcae739..56e7e93f8b 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanRewardsReferenceStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanRewardsReferenceStore.scala @@ -16,7 +16,12 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.round.OpenMiningRound import org.lfdecentralizedtrust.splice.config.IngestionConfig import org.lfdecentralizedtrust.splice.environment.RetryProvider import org.lfdecentralizedtrust.splice.scan.store.db.ScanRewardsReferenceTables.ScanRewardsReferenceStoreRowData -import org.lfdecentralizedtrust.splice.store.{AppStore, Limit, MultiDomainAcsStore} +import org.lfdecentralizedtrust.splice.store.{ + AppStore, + Limit, + MultiDomainAcsStore, + TimestampWithMigrationId, +} import org.lfdecentralizedtrust.splice.store.db.AcsInterfaceViewRowData import org.lfdecentralizedtrust.splice.util.{Contract, TemplateJsonDecoder} @@ -57,7 +62,7 @@ trait ScanRewardsReferenceStore extends AppStore { */ def lookupActiveOpenMiningRounds( recordTimes: Seq[CantonTimestamp] - )(implicit tc: TraceContext): Future[Map[CantonTimestamp, (Long, CantonTimestamp)]] + )(implicit tc: TraceContext): Future[Map[CantonTimestamp, TimestampWithMigrationId]] def lookupFeaturedAppPartiesAsOf( asOf: CantonTimestamp @@ -189,6 +194,17 @@ object ScanRewardsReferenceStore { round = Some(contract.payload.round.number), ) }, + mkFilter(splice.amulet.rewardaccountingv2.ProcessRewardsV2.COMPANION)( + co => co.payload.dso == dso, + versionGuard = { case (pkgVersionSupport, now) => + (tc) => pkgVersionSupport.supportsTrafficBasedAppRewards(Seq(key.dsoParty), now)(tc) + }, + ) { contract => + ScanRewardsReferenceStoreRowData( + contract = contract, + round = Some(contract.payload.round.number), + ) + }, ), interfaceFilters = Map.empty, synchronizerFilter = Some(key.synchronizerId), diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala index fbe1500723..60c9f7e649 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala @@ -30,8 +30,6 @@ import org.lfdecentralizedtrust.splice.store.{ ExternalPartyConfigStateStore, MiningRoundsStore, MultiDomainAcsStore, - PageLimit, - SortOrder, TxLogAppStore, UpdateHistory, VotesStore, @@ -215,14 +213,6 @@ trait ScanStore ]] ] - def listTransactions( - pageEndEventId: Option[String], - sortOrder: SortOrder, - limit: PageLimit, - )(implicit - tc: TraceContext - ): Future[Seq[TxLogEntry.TransactionTxLogEntry]] - def lookupLatestTransferCommandEvents( sender: PartyId, nonce: Long, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanTxLogParser.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanTxLogParser.scala index a33c651e98..47a6e1f913 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanTxLogParser.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanTxLogParser.scala @@ -9,10 +9,6 @@ import com.daml.ledger.javaapi.data.* import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.topology.{PartyId, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext -import io.grpc.Status -import org.lfdecentralizedtrust.splice.codegen.java.splice -import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.AmuletCreateSummary -import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.TransferResult import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ DsoRules_CloseVoteRequest, DsoRules_CloseVoteRequestResult, @@ -21,23 +17,13 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.externalpartyamuletru TransferCommandResultFailure, TransferCommandResultSuccess, } -import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.subscriptions as sws import org.lfdecentralizedtrust.splice.history.* -import org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.* import org.lfdecentralizedtrust.splice.store.TxLogStore import org.lfdecentralizedtrust.splice.store.events.DsoRulesCloseVoteRequest import org.lfdecentralizedtrust.splice.util.SpliceUtil.dollarsToCC -import org.lfdecentralizedtrust.splice.util.TransactionTreeExtensions.* -import org.lfdecentralizedtrust.splice.util.{ - Codec, - EventId, - ExerciseNode, - LegacyOffset, - TokenStandardMetadata, -} +import org.lfdecentralizedtrust.splice.util.{Codec, EventId, ExerciseNode} import scala.collection.immutable -import scala.jdk.OptionConverters.* import scala.jdk.CollectionConverters.* import scala.math.BigDecimal.javaBigDecimal2bigDecimal @@ -81,387 +67,6 @@ class ScanTxLogParser( exercised.getNodeId, ) exercised match { - case Transfer(node) => - State.fromTransfer(tree, exercised, synchronizerId, node) - case TransferPreapproval_Send(node) => - val state = parseTrees( - tree, - synchronizerId, - tree.getChildNodeIds(exercised).asScala.toList, - ignoreUnexpectedAmuletCreateArchive, - ) - state.setTransferPreapprovalSendFields(tree, exercised, node.argument.value.description) - case TransferPreapproval_SendV2(node) => - val receiver = (node.result.value.result.summary.balanceChanges.asScala.keySet - .diff(Set(node.argument.value.sender))) - .headOption - .getOrElse(node.argument.value.sender) - val output = new splice.amuletrules.TransferOutput( - receiver, - BigDecimal(0).bigDecimal, // receiver fee ratio is irrelevant, there are no fees - node.argument.value.amount, - java.util.Optional.empty(), // lock - java.util.Optional.empty(), // meta - ) - val state = State.fromTransferResult( - tree, - exercised, - synchronizerId, - sender = node.argument.value.sender, - outputs = Seq(output), - result = node.result.value.result, - ) - val description = - node.result.value.meta.values.asScala.get(TokenStandardMetadata.reasonMetaKey) - state.setTransferPreapprovalSendFields(tree, exercised, description.toJava) - case CreateTokenStandardTransferInstruction(node) => - // TODO(tech-debt): remove this duplication with CreateTokenStandardTransferInstructionV2 - val cid: String = node.result.value.output match { - case output: splice.api.token.transferinstructionv1.transferinstructionresult_output.TransferInstructionResult_Pending => - output.transferInstructionCid.contractId - case output => - // CreateTokenStandardTransferInstruction only matches on two-step transfers resulting in pending status. - // Single-step transfers are just parsed as the underlying transfer. - logger.warn( - s"Unexpected transfer instruction result output, expected pending but got: $output" - ) - "" - } - val state = parseTrees( - tree, - synchronizerId, - tree.getChildNodeIds(exercised).asScala.toList, - ignoreUnexpectedAmuletCreateArchive = true, - ) - val stateWithTransfer = if (state.hasTransfer) { - // We hit this for transfers before the 24h signing change that call AmuletRules_Transfer internally. - state - } else { - val txLogEntry = new TransferTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - sender = Some( - senderAmountNoFees( - node.argument.value.transfer.sender, - 0.0, // Note: Because Scan tracks the sum of locked and unlocked input and output is 0. - ) - ), - // receiver is set to the sender as the amulet is locked to them - receivers = Seq( - receiverAmountNoFees( - node.argument.value.transfer.sender, - node.argument.value.transfer.amount, - ) - ), - balanceChanges = Seq.empty, - ) - State( - txLogEntry - ) - } - stateWithTransfer.copy( - entries = stateWithTransfer.entries.map { - case e: TransferTxLogEntry => - e.copy( - description = node.argument.value.transfer.meta.values - .getOrDefault(TokenStandardMetadata.reasonMetaKey, ""), - transferInstructionReceiver = node.argument.value.transfer.receiver, - transferInstructionAmount = Some(node.argument.value.transfer.amount), - transferInstructionCid = cid, - eventId = - EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId), - transferKind = TransferKind.TRANSFER_KIND_CREATE_TRANSFER_INSTRUCTION, - ) - case e => e - } - ) - case DirectTokenStandardTransfer(node) => - // TODO(tech-debt): remove this duplication with DirectTokenStandardTransferV2 - val sender = node.argument.value.transfer.sender - val receiver = node.argument.value.transfer.receiver - val amount = node.argument.value.transfer.amount - - val senderAmount = senderAmountNoFees(sender, amount) - val state = parseTrees( - tree, - synchronizerId, - tree.getChildNodeIds(exercised).asScala.toList, - ignoreUnexpectedAmuletCreateArchive = true, - ) - if (state.hasTransfer) { - // We hit this for transfers before the 24h signing delay change that call AmuletRules_Transfer or TransferPreapproval_Send internally - // or transfers with the 24h signing delay change where sender != receiver which call into TransferPreapproval_SendV2 - state - } else { - // We hit this only when sender = receiver and the 24h signing delay change is active as then there is no TransferPreapproval_SendV2 child. - // We just parse this as a plain transfer matching the behavior before the 24h signing delay change. - val txLogEntry = new TransferTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - eventId = - EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId), - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - sender = Some(senderAmount), - receivers = Seq(receiverAmountNoFees(receiver, amount)), - balanceChanges = Seq.empty, - description = node.argument.value.transfer.meta.values - .getOrDefault(TokenStandardMetadata.reasonMetaKey, ""), - ) - State(txLogEntry) - } - case CreateTokenStandardTransferInstructionV2(node) => - // TODO(tech-debt): remove this duplication with CreateTokenStandardTransferInstruction - val admin = node.argument.value.transfer.instrumentId.admin - val cid: String = node.result.value.output match { - case output: splice.api.token.transferinstructionv2.transferinstructionresult_output.TransferInstructionResult_Pending => - output.transferInstructionCid.contractId - case output => - // CreateTokenStandardTransferInstruction only matches on two-step transfers resulting in pending status. - // Single-step transfers are just parsed as the underlying transfer. - logger.warn( - s"Unexpected transfer instruction result output, expected pending but got: $output" - ) - "" - } - val state = parseTrees( - tree, - synchronizerId, - tree.getChildNodeIds(exercised).asScala.toList, - ignoreUnexpectedAmuletCreateArchive = true, - ) - val stateWithTransfer = if (state.hasTransfer) { - // We hit this for transfers before the 24h signing change that call AmuletRules_Transfer internally. - state - } else { - val txLogEntry = new TransferTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - sender = Some( - senderAmountNoFees( - node.argument.value.transfer.sender.owner.toScala.getOrElse(admin), - 0.0, // Note: Because Scan tracks the sum of locked and unlocked input and output is 0. - ) - ), - // receiver is set to the sender as the amulet is locked to them - receivers = Seq( - receiverAmountNoFees( - node.argument.value.transfer.sender.owner.toScala.getOrElse(admin), - node.argument.value.transfer.amount, - ) - ), - balanceChanges = Seq.empty, - ) - State( - txLogEntry - ) - } - stateWithTransfer.copy( - entries = stateWithTransfer.entries.map { - case e: TransferTxLogEntry => - e.copy( - description = node.argument.value.transfer.meta.values - .getOrDefault(TokenStandardMetadata.reasonMetaKey, ""), - transferInstructionReceiver = - node.argument.value.transfer.receiver.owner.toScala.getOrElse(admin), - transferInstructionAmount = Some(node.argument.value.transfer.amount), - transferInstructionCid = cid, - eventId = - EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId), - transferKind = TransferKind.TRANSFER_KIND_CREATE_TRANSFER_INSTRUCTION, - ) - case e => e - } - ) - case DirectTokenStandardTransferV2(node) => - // TODO(tech-debt): remove this duplication with DirectTokenStandardTransfer - val admin = node.argument.value.transfer.instrumentId.admin - val sender = node.argument.value.transfer.sender.owner.toScala.getOrElse(admin) - val receiver = node.argument.value.transfer.receiver.owner.toScala.getOrElse(admin) - val amount = node.argument.value.transfer.amount - - val senderAmount = senderAmountNoFees(sender, amount) - val state = parseTrees( - tree, - synchronizerId, - tree.getChildNodeIds(exercised).asScala.toList, - ignoreUnexpectedAmuletCreateArchive = true, - ) - if (state.hasTransfer) { - // We hit this for transfers before the 24h signing delay change that call AmuletRules_Transfer or TransferPreapproval_Send internally - // or transfers with the 24h signing delay change where sender != receiver which call into TransferPreapproval_SendV2 - state - } else { - // We hit this only when sender = receiver and the 24h signing delay change is active as then there is no TransferPreapproval_SendV2 child. - // We just parse this as a plain transfer matching the behavior before the 24h signing delay change. - val txLogEntry = new TransferTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - eventId = - EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId), - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - sender = Some(senderAmount), - receivers = Seq(receiverAmountNoFees(receiver, amount)), - balanceChanges = Seq.empty, - description = node.argument.value.transfer.meta.values - .getOrDefault(TokenStandardMetadata.reasonMetaKey, ""), - ) - State(txLogEntry) - } - case TransferInstruction_Accept(node) => - val state = parseTrees( - tree, - synchronizerId, - tree.getChildNodeIds(exercised).asScala.toList, - ignoreUnexpectedAmuletCreateArchive = true, - ) - val stateWithTransfer = if (state.hasTransfer) { - // We hit this for transfers before the 24h signing change that call AmuletRules_Transfer internally. - state - } else { - val coinCid = node.result.value.output match { - case output: splice.api.token.transferinstructionv1.transferinstructionresult_output.TransferInstructionResult_Completed => - assert(output.receiverHoldingCids.size == 1) - output.receiverHoldingCids.get(0) - case output => - throw new RuntimeException( - s"Unexpected transfer instruction result output, expected completed but got: $output" - ) - } - val coin = - tree - .findCreation( - splice.amulet.Amulet.COMPANION, - new splice.amulet.Amulet.ContractId(coinCid.contractId), - ) - .getOrElse( - throw new RuntimeException( - s"The amulet contract ${coinCid} was not found in transaction ${tree.getUpdateId}" - ) - ) - val sender = node.result.value.meta.values.get(TokenStandardMetadata.senderMetaKey) - val receiver = coin.payload.owner - val amount = coin.payload.amount.initialAmount - - val txLogEntry = new TransferTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - eventId = - EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId), - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - sender = Some(senderAmountNoFees(sender, amount)), - receivers = Seq(receiverAmountNoFees(receiver, amount)), - balanceChanges = Seq.empty, - ) - State(txLogEntry) - } - stateWithTransfer.copy( - entries = stateWithTransfer.entries.map { - case e: TransferTxLogEntry => - e.copy( - transferInstructionCid = exercised.getContractId, - transferKind = TransferKind.TRANSFER_KIND_TRANSFER_INSTRUCTION_ACCEPT, - ) - case e => e - } - ) - case TransferInstruction_Withdraw(_) => - // Contrary to the wallet which tracks only unlocked amulet balance, - // scan tracks the sum of locked and unlocked balance so - // this does not actually create a change in balance. - State( - AbortTransferInstructionTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - eventId = eventId, - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - transferInstructionCid = exercised.getContractId, - transferAbortKind = TransferAbortKind.TRANSFER_ABORT_KIND_WITHDRAW, - ) - ) - case TransferInstruction_Reject(_) => - // Contrary to the wallet which tracks only unlocked amulet balance, - // scan tracks the sum of locked and unlocked balance so - // this does not actually create a change in balance. - State( - AbortTransferInstructionTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - eventId = eventId, - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - transferInstructionCid = exercised.getContractId, - transferAbortKind = TransferAbortKind.TRANSFER_ABORT_KIND_REJECT, - ) - ) - case Tap(node) => - State.fromAmuletCreateSummary( - tree, - exercised, - synchronizerId, - node.result.value.amuletSum, - TransactionType.Tap, - ) - case Mint(node) => - State.fromAmuletCreateSummary( - tree, - exercised, - synchronizerId, - node.result.value.amuletSum, - TransactionType.Mint, - ) - case AmuletRules_BuyMemberTraffic(node) => - State.fromBuyMemberTraffic(eventId, synchronizerId, node) - case AmuletRules_CreateExternalPartySetupProposal(node) => - State.fromCreateExternalPartySetupProposal(eventId, synchronizerId, node) - case AmuletRules_CreateTransferPreapproval(node) => - State.fromCreateTransferPreapproval(eventId, synchronizerId, node) - case TransferPreapproval_Renew(node) => - State.fromRenewTransferPreapproval(eventId, synchronizerId, node) - case AmuletExpire(node) => - State.empty - case AmuletExpireV2(node) => - State.empty - case LockedAmuletExpireAmulet(node) => - State.empty - case LockedAmuletExpireAmuletV2(node) => - State.empty - // We track the sum of locked/unlocked so this is a noop. - case LockedAmuletUnlock(_) => - State.empty - // We track the sum of locked/unlocked so this is a noop. - case LockedAmuletUnlockV2(_) => - State.empty - // We track the sum of locked/unlocked so this is a noop. - case LockedAmuletOwnerExpireLock(_) => - State.empty - // We track the sum of locked/unlocked so this is a noop. - case LockedAmuletOwnerExpireLockV2(_) => - State.empty - case AnsRules_CollectInitialEntryPayment(_) => - fromAnsEntryPaymentCollection( - tree, - exercised, - synchronizerId, - sws.SubscriptionInitialPayment.COMPANION, - sws.SubscriptionInitialPayment.CHOICE_SubscriptionInitialPayment_Collect, - )(_.amulet) - case AnsRules_CollectEntryRenewalPayment(_) => - fromAnsEntryPaymentCollection( - tree, - exercised, - synchronizerId, - sws.SubscriptionPayment.COMPANION, - sws.SubscriptionPayment.CHOICE_SubscriptionPayment_Collect, - )(_.amulet) - case AmuletArchive(_) => - if (!ignoreUnexpectedAmuletCreateArchive) { - throw new RuntimeException( - s"Unexpected amulet archive event for amulet ${exercised.getContractId} in transaction ${tree.getUpdateId}" - ) - } else { - State.empty - } case DsoRulesCloseVoteRequest(node) => State.fromCloseVoteRequest(eventId, node) case ExternalPartyAmuletRules_CreateTransferCommand(node) => @@ -479,78 +84,6 @@ class ScanTxLogParser( State.fromTransferCommand_Withdraw(eventId, exercised, node) case TransferCommand_Expire(node) => State.fromTransferCommand_Expire(eventId, exercised, node) - case AllocationFactoryAllocate(node) => - val state = parseTrees( - tree, - synchronizerId, - tree.getChildNodeIds(exercised).asScala.toList, - ignoreUnexpectedAmuletCreateArchive = true, - ) - if (state.hasTransfer) { - // We hit this for allocations before the 24h signing change that call AmuletRules_Transfer internally. - state - } else { - - val sender = node.argument.value.allocation.transferLeg.sender - val amount = node.argument.value.allocation.transferLeg.amount - - val txLogEntry = new TransferTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - eventId = - EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId), - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - sender = Some(senderAmountNoFees(sender, amount)), - receivers = Seq( - receiverAmountNoFees(sender, amount) - ), // This step locks to the sender which scan displays as a transfer to yourself. - balanceChanges = Seq.empty, - ) - State(txLogEntry) - } - case AllocationExecuteTransfer(node) => - val state = parseTrees( - tree, - synchronizerId, - tree.getChildNodeIds(exercised).asScala.toList, - ignoreUnexpectedAmuletCreateArchive = true, - ) - if (state.hasTransfer) { - // We hit this for allocations before the 24h signing change that call AmuletRules_Transfer internally. - state - } else { - assert(node.result.value.receiverHoldingCids.size == 1) - val coinCid = node.result.value.receiverHoldingCids.get(0) - val coin = - tree - .findCreation( - splice.amulet.Amulet.COMPANION, - new splice.amulet.Amulet.ContractId(coinCid.contractId), - ) - .getOrElse( - throw new RuntimeException( - s"The amulet contract ${coinCid} was not found in transaction ${tree.getUpdateId}" - ) - ) - val sender = node.result.value.meta.values.get(TokenStandardMetadata.senderMetaKey) - val receiver = coin.payload.owner - val amount = coin.payload.amount.initialAmount - - val txLogEntry = new TransferTxLogEntry( - offset = LegacyOffset.Api.fromLong(tree.getOffset), - eventId = - EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId), - domainId = synchronizerId, - date = Some(tree.getEffectiveAt), - sender = Some(senderAmountNoFees(sender, amount)), - receivers = Seq(receiverAmountNoFees(receiver, amount)), - balanceChanges = Seq.empty, - ) - State(txLogEntry) - } - // Token Standard V2: the Scan txlog will go away so we don't bother parsing it - case AllocationFactoryV2Allocate(_) | AllocationV2Settle(_) => - State.empty case _ => parseTrees( tree, @@ -573,22 +106,6 @@ class ScanTxLogParser( ) case ClosedMiningRoundCreate(round) => State.fromClosedMiningRoundCreate(tree, root, synchronizerId, round) - case AmuletCreate(_) => - if (!ignoreUnexpectedAmuletCreateArchive) { - throw new RuntimeException( - s"Unexpected amulet create event for amulet ${created.getContractId} in transaction ${tree.getUpdateId}" - ) - } else { - State.empty - } - case LockedAmuletCreate(_) => - if (!ignoreUnexpectedAmuletCreateArchive) { - throw new RuntimeException( - s"Unexpected locked amulet create event for amulet ${created.getContractId} in transaction ${tree.getUpdateId}" - ) - } else { - State.empty - } case _ => State.empty } @@ -597,35 +114,6 @@ class ScanTxLogParser( } } - private def fromAnsEntryPaymentCollection[Marker, Res]( - tree: Transaction, - exercised: ExercisedEvent, - synchronizerId: SynchronizerId, - paymentCollectionTemplate: codegen.ContractCompanion[?, ?, Marker], - paymentCollectionChoice: codegen.Choice[Marker, ?, Res], - )( - collectionProducedAmulet: Res => AmuletCreate.TCid - )(implicit tc: TraceContext) = { - // first child event is the initial subscription payment collected by DSO - val (paymentCollectionEvent, _) = - tree - .firstDescendantExercise(exercised, paymentCollectionTemplate, paymentCollectionChoice) - .map { case (e, pr) => (e, collectionProducedAmulet(pr)) } - .getOrElse { - sys.error( - s"Unable to find ${paymentCollectionChoice.name} in ${exercised.getChoice}" - ) - } - - val stateFromPaymentCollection = parseTree( - tree, - synchronizerId, - paymentCollectionEvent, - ignoreUnexpectedAmuletCreateArchive = false, - ) - State.empty.appended(stateFromPaymentCollection) - } - private def parseTrees( tree: Transaction, synchronizerId: SynchronizerId, @@ -670,29 +158,6 @@ object ScanTxLogParser { def appended(other: State): State = State( entries = entries.appendedAll(other.entries) ) - def hasTransfer: Boolean = - entries.exists { - case _: TransferTxLogEntry => true - case _ => false - } - - def setTransferPreapprovalSendFields( - tree: Transaction, - exercised: ExercisedEvent, - description: java.util.Optional[String], - ): State = - copy( - entries = entries.map { - case e: TransferTxLogEntry => - e.copy( - description = description.orElse(""), - eventId = - EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId), - transferKind = TransferKind.TRANSFER_KIND_PREAPPROVAL_SEND, - ) - case e => e - } - ) } private object State { @@ -709,334 +174,6 @@ object ScanTxLogParser { a.appended(b) } - private def getAmuletFromSummary( - tx: Transaction, - ccsum: AmuletCreateSummary[? <: codegen.ContractId[AmuletCreate.T]], - ) = { - val amuletCid = ccsum.amulet - tx.findCreation(AmuletCreate.companion, amuletCid) - .map(_.payload) - .getOrElse { - throw new RuntimeException( - s"The amulet contract $amuletCid referenced by AmuletCreateSummary was not found in transaction ${tx.getUpdateId}" - ) - } - } - - def fromAmuletCreateSummary( - tx: Transaction, - event: Event, - synchronizerId: SynchronizerId, - acsum: AmuletCreateSummary[? <: codegen.ContractId[AmuletCreate.T]], - activityType: TransactionType, - ): State = { - val amulet = getAmuletFromSummary(tx, acsum) - val eventId = EventId.prefixedFromUpdateIdAndNodeId(tx.getUpdateId, event.getNodeId) - val activityEntry: TransactionTxLogEntry = activityType match { - case TransactionType.Tap => - TapTxLogEntry( - offset = LegacyOffset.Api.fromLong(tx.getOffset), - eventId = eventId, - domainId = synchronizerId, - date = Some(tx.getEffectiveAt), - amuletOwner = PartyId.tryFromProtoPrimitive(amulet.owner), - amuletAmount = amulet.amount.initialAmount, - round = acsum.round.number, - ) - case TransactionType.Mint => - MintTxLogEntry( - offset = LegacyOffset.Api.fromLong(tx.getOffset), - eventId = eventId, - domainId = synchronizerId, - date = Some(tx.getEffectiveAt), - amuletOwner = PartyId.tryFromProtoPrimitive(amulet.owner), - amuletAmount = amulet.amount.initialAmount, - round = acsum.round.number, - ) - case unexpected => - throw new Exception( - s"unexpected activityType: $unexpected in fromAmuletCreateSummary" - ) - } - - State(activityEntry) - } - - private def rewardsEntriesFromTransferSummary( - sender: PartyId, - summary: splice.amuletrules.TransferSummary, - round: Long, - synchronizerId: SynchronizerId, - rootEventId: String, - ): State = { - val appRewards = summary.inputAppRewardAmount - val validatorRewards = summary.inputValidatorRewardAmount - val svRewards = summary.inputSvRewardAmount - - val appRewardEntry = - if (appRewards.compareTo(BigDecimal(0.0)) > 0) { - val entry = - AppRewardTxLogEntry( - eventId = rootEventId, - domainId = synchronizerId, - round = round, - party = sender, - amount = appRewards, - ) - State(entry) - } else { - State.empty - } - - val validatorRewardEntry = - if (validatorRewards.compareTo(BigDecimal(0.0)) > 0) { - val entry = - ValidatorRewardTxLogEntry( - eventId = rootEventId, - domainId = synchronizerId, - round = round, - party = sender, - amount = validatorRewards, - ) - State(entry) - } else { - State.empty - } - - val svRewardEntry = - if (svRewards.compareTo(BigDecimal(0.0)) > 0) { - val entry = - SvRewardTxLogEntry( - eventId = rootEventId, - domainId = synchronizerId, - round = round, - party = sender, - amount = svRewards, - ) - State(entry) - } else { - State.empty - } - - appRewardEntry.appended(validatorRewardEntry).appended(svRewardEntry) - } - - def fromTransfer( - tx: Transaction, - event: ExercisedEvent, - synchronizerId: SynchronizerId, - node: ExerciseNode[Transfer.Arg, Transfer.Res], - rootEventId: Option[String] = None, - ): State = { - State.fromTransferResult( - tx, - event, - synchronizerId, - sender = node.argument.value.transfer.sender, - outputs = node.argument.value.transfer.outputs.asScala.toSeq, - result = node.result.value, - rootEventId = rootEventId, - ) - } - - def fromTransferResult( - tx: Transaction, - event: ExercisedEvent, - synchronizerId: SynchronizerId, - sender: String, - outputs: Seq[splice.amuletrules.TransferOutput], - result: splice.amuletrules.TransferResult, - rootEventId: Option[String] = None, - ): State = { - val senderParty = Codec - .decode(Codec.Party)(sender) - .getOrElse( - throw Status.INTERNAL - .withDescription(s"Cannot decode party ID ${sender}") - .asRuntimeException() - ) - val eventId = EventId.prefixedFromUpdateIdAndNodeId(tx.getUpdateId, event.getNodeId) - val rewardEntries = - rewardsEntriesFromTransferSummary( - senderParty, - result.summary, - result.round.number, - synchronizerId, - rootEventId.getOrElse(eventId), - ) - - val activityEntry = State( - transferTxLogEntry( - tx, - event, - synchronizerId, - sender = sender, - outputs = outputs, - result = result, - ) - ) - - rewardEntries - .appended(activityEntry) - } - - private def transferTxLogEntry( - tx: Transaction, - event: Event, - synchronizerId: SynchronizerId, - sender: String, - outputs: Seq[splice.amuletrules.TransferOutput], - result: splice.amuletrules.TransferResult, - ): TransferTxLogEntry = { - val senderAmount = parseSenderAmount(sender, outputs, result) - val receiverAmounts = parseReceiverAmounts(outputs, result) - - new TransferTxLogEntry( - offset = LegacyOffset.Api.fromLong(tx.getOffset), - eventId = EventId.prefixedFromUpdateIdAndNodeId(tx.getUpdateId, event.getNodeId), - domainId = synchronizerId, - date = Some(tx.getEffectiveAt), - sender = Some(senderAmount), - receivers = receiverAmounts, - round = result.round.number, - ) - } - - def fromBuyMemberTraffic( - eventId: String, - synchronizerId: SynchronizerId, - node: ExerciseNode[AmuletRules_BuyMemberTraffic.Arg, AmuletRules_BuyMemberTraffic.Res], - ): State = { - val validatorParty = Codec - .decode(Codec.Party)(node.argument.value.provider) - .getOrElse( - throw Status.INTERNAL - .withDescription( - s"Cannot decode party ID ${node.argument.value.provider}" - ) - .asRuntimeException() - ) - val round = node.result.value.round - val trafficPurchased = node.argument.value.trafficAmount - val ccSpent = node.result.value.amuletPaid - val buyExtraTrafficEntry = ExtraTrafficPurchaseTxLogEntry( - eventId = eventId, - domainId = synchronizerId, - round = round.number, - validator = validatorParty, - trafficPurchased = trafficPurchased, - ccSpent = ccSpent, - ) - - val rewardEntries = rewardsEntriesFromTransferSummary( - validatorParty, - node.result.value.summary, - round.number, - synchronizerId, - eventId, - ) - - State(buyExtraTrafficEntry) - .appended(rewardEntries) - } - - def fromCreateExternalPartySetupProposal( - eventId: String, - synchronizerId: SynchronizerId, - node: ExerciseNode[ - AmuletRules_CreateExternalPartySetupProposal.Arg, - AmuletRules_CreateExternalPartySetupProposal.Res, - ], - ): State = { - val validatorParty = Codec - .decode(Codec.Party)(node.result.value.validator) - .getOrElse( - throw Status.INTERNAL - .withDescription( - s"Cannot decode party ID ${node.argument.value.validator}" - ) - .asRuntimeException() - ) - val transferResult = node.result.value.transferResult - fromTransferPreapprovalPurchase( - eventId, - synchronizerId, - validatorParty, - transferResult, - ) - } - - def fromCreateTransferPreapproval( - eventId: String, - synchronizerId: SynchronizerId, - node: ExerciseNode[ - AmuletRules_CreateTransferPreapproval.Arg, - AmuletRules_CreateTransferPreapproval.Res, - ], - ): State = { - val validatorParty = Codec - .decode(Codec.Party)(node.argument.value.provider) - .getOrElse( - throw Status.INTERNAL - .withDescription( - s"Cannot decode party ID ${node.argument.value.provider}" - ) - .asRuntimeException() - ) - val transferResult = node.result.value.transferResult - fromTransferPreapprovalPurchase( - eventId, - synchronizerId, - validatorParty, - transferResult, - ) - } - - def fromRenewTransferPreapproval( - eventId: String, - synchronizerId: SynchronizerId, - node: ExerciseNode[ - TransferPreapproval_Renew.Arg, - TransferPreapproval_Renew.Res, - ], - ): State = { - val validatorParty = Codec - .decode(Codec.Party)(node.result.value.provider) - .getOrElse( - throw Status.INTERNAL - .withDescription( - s"Cannot decode party ID ${node.result.value.provider}" - ) - .asRuntimeException() - ) - val transferResult = node.result.value.transferResult - fromTransferPreapprovalPurchase( - eventId, - synchronizerId, - validatorParty, - transferResult, - ) - } - - private def fromTransferPreapprovalPurchase( - eventId: String, - synchronizerId: SynchronizerId, - validatorParty: PartyId, - transferResult: TransferResult, - ) = { - val round = transferResult.round - - val rewardEntries = rewardsEntriesFromTransferSummary( - validatorParty, - transferResult.summary, - round.number, - synchronizerId, - eventId, - ) - - State.empty.appended(rewardEntries) - } - def fromOpenMiningRoundCreate( eventId: String, synchronizerId: SynchronizerId, @@ -1181,25 +318,4 @@ object ScanTxLogParser { ) } } - - private def senderAmountNoFees(party: String, amount: BigDecimal) = - SenderAmount( - party = PartyId.tryFromProtoPrimitive(party), - inputAmuletAmount = amount, - inputAppRewardAmount = BigDecimal(0.0), - inputValidatorRewardAmount = BigDecimal(0.0), - senderChangeAmount = BigDecimal(0.0), - senderChangeFee = BigDecimal(0.0), - senderFee = BigDecimal(0.0), - holdingFees = BigDecimal(0.0), - inputSvRewardAmount = None, - inputValidatorFaucetAmount = None, - ) - - private def receiverAmountNoFees(party: String, amount: BigDecimal) = - ReceiverAmount( - party = PartyId.tryFromProtoPrimitive(party), - amount = amount, - receiverFee = BigDecimal(0.0), - ) } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/TxLogEntry.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/TxLogEntry.scala index 2b17fdf90b..d51aa84ee9 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/TxLogEntry.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/TxLogEntry.scala @@ -3,21 +3,11 @@ package org.lfdecentralizedtrust.splice.scan.store -import org.lfdecentralizedtrust.splice.codegen.java.splice import org.lfdecentralizedtrust.splice.store.StoreErrors -import org.lfdecentralizedtrust.splice.util.Codec -import scala.collection.immutable -import scala.jdk.CollectionConverters.* -import scala.jdk.OptionConverters.* import java.time.Instant import org.lfdecentralizedtrust.splice.http.v0.definitions as httpDef -import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryResponseItem.TransactionType as HttpTransactionType import com.digitalasset.canton.config.CantonRequireTypes.String3 -import com.digitalasset.canton.topology.PartyId - -import java.time.ZoneOffset -import scala.math.BigDecimal.RoundingMode trait TxLogEntry extends Product with Serializable { // Scan store uses the eventId for pagination @@ -28,42 +18,33 @@ object TxLogEntry extends StoreErrors { object EntryType { val ErrorTxLogEntry = String3.tryCreate("err") - val BalanceChangeTxLogEntry = String3.tryCreate("bac") val ClosedMiningRoundTxLogEntry = String3.tryCreate("cmr") - val ExtraTrafficPurchaseTxLogEntry = String3.tryCreate("etp") val OpenMiningRoundTxLogEntry = String3.tryCreate("omr") - val AppRewardTxLogEntry = String3.tryCreate("are") - val MintTxLogEntry = String3.tryCreate("min") - val TapTxLogEntry = String3.tryCreate("tap") - val TransferTxLogEntry = String3.tryCreate("tra") - val ValidatorRewardTxLogEntry = String3.tryCreate("vre") - val SvRewardTxLogEntry = String3.tryCreate("sre") val VoteRequestTxLogEntry = String3.tryCreate("vot") val TransferCommandTxLogEntry = String3.tryCreate("trc") - val AbortTransferInstructionTxLogEntry = String3.tryCreate("ati") // The following entry types correspond to entries that were removed from `scan_tx_log.proto` // Those entries might still exist in databases, but we don't produce new ones and we don't read them. // The values are only kept for documentation purposes. val Unused_SvRewardCollectedTxLogEntry = String3.tryCreate("src") + val Unused_BalanceChangeTxLogEntry = String3.tryCreate("bac") + val Unused_ExtraTrafficPurchaseTxLogEntry = String3.tryCreate("etp") + val Unused_AppRewardTxLogEntry = String3.tryCreate("are") + val Unused_MintTxLogEntry = String3.tryCreate("min") + val Unused_TapTxLogEntry = String3.tryCreate("tap") + val Unused_TransferTxLogEntry = String3.tryCreate("tra") + val Unused_ValidatorRewardTxLogEntry = String3.tryCreate("vre") + val Unused_SvRewardTxLogEntry = String3.tryCreate("sre") + val Unused_AbortTransferInstructionTxLogEntry = String3.tryCreate("ati") } def encode(entry: TxLogEntry): (String3, String) = { import scalapb.json4s.JsonFormat val entryType = entry match { case _: ErrorTxLogEntry => EntryType.ErrorTxLogEntry - case _: BalanceChangeTxLogEntry => EntryType.BalanceChangeTxLogEntry case _: ClosedMiningRoundTxLogEntry => EntryType.ClosedMiningRoundTxLogEntry - case _: ExtraTrafficPurchaseTxLogEntry => EntryType.ExtraTrafficPurchaseTxLogEntry case _: OpenMiningRoundTxLogEntry => EntryType.OpenMiningRoundTxLogEntry - case _: AppRewardTxLogEntry => EntryType.AppRewardTxLogEntry - case _: MintTxLogEntry => EntryType.MintTxLogEntry - case _: TapTxLogEntry => EntryType.TapTxLogEntry - case _: TransferTxLogEntry => EntryType.TransferTxLogEntry - case _: ValidatorRewardTxLogEntry => EntryType.ValidatorRewardTxLogEntry - case _: SvRewardTxLogEntry => EntryType.SvRewardTxLogEntry case _: VoteRequestTxLogEntry => EntryType.VoteRequestTxLogEntry case _: TransferCommandTxLogEntry => EntryType.TransferCommandTxLogEntry - case _: AbortTransferInstructionTxLogEntry => EntryType.AbortTransferInstructionTxLogEntry case _ => throw txEncodingFailed() } val jsonValue = entry match { @@ -77,20 +58,10 @@ object TxLogEntry extends StoreErrors { try { entryType match { case EntryType.ErrorTxLogEntry => from[ErrorTxLogEntry](json) - case EntryType.BalanceChangeTxLogEntry => from[BalanceChangeTxLogEntry](json) case EntryType.ClosedMiningRoundTxLogEntry => from[ClosedMiningRoundTxLogEntry](json) - case EntryType.ExtraTrafficPurchaseTxLogEntry => from[ExtraTrafficPurchaseTxLogEntry](json) case EntryType.OpenMiningRoundTxLogEntry => from[OpenMiningRoundTxLogEntry](json) - case EntryType.AppRewardTxLogEntry => from[AppRewardTxLogEntry](json) - case EntryType.MintTxLogEntry => from[MintTxLogEntry](json) - case EntryType.TapTxLogEntry => from[TapTxLogEntry](json) - case EntryType.TransferTxLogEntry => from[TransferTxLogEntry](json) - case EntryType.ValidatorRewardTxLogEntry => from[ValidatorRewardTxLogEntry](json) - case EntryType.SvRewardTxLogEntry => from[ValidatorRewardTxLogEntry](json) case EntryType.VoteRequestTxLogEntry => from[VoteRequestTxLogEntry](json) case EntryType.TransferCommandTxLogEntry => from[TransferCommandTxLogEntry](json) - case EntryType.AbortTransferInstructionTxLogEntry => - from[AbortTransferInstructionTxLogEntry](json) case _ => throw txLogIsOfWrongType(entryType.str) } } catch { @@ -98,14 +69,6 @@ object TxLogEntry extends StoreErrors { } } - trait RewardTxLogEntry extends TxLogEntry { - def party: PartyId - - def amount: BigDecimal - - def round: Long - } - trait TransactionTxLogEntry extends TxLogEntry { def date: Option[Instant] } @@ -118,130 +81,6 @@ object TxLogEntry extends StoreErrors { val Failed = "failed" } - private def toResponse(data: SenderAmount) = httpDef.SenderAmount( - party = data.party.toProtoPrimitive, - inputAmuletAmount = Some(Codec.encode(data.inputAmuletAmount)), - inputAppRewardAmount = Some(Codec.encode(data.inputAppRewardAmount)), - inputValidatorRewardAmount = Some(Codec.encode(data.inputValidatorRewardAmount)), - inputSvRewardAmount = Some(Codec.encode(data.inputSvRewardAmount.getOrElse(BigDecimal(0)))), - inputValidatorFaucetAmount = data.inputValidatorFaucetAmount.map(fa => Codec.encode(fa)), - senderChangeAmount = Codec.encode(data.senderChangeAmount), - senderChangeFee = Codec.encode(data.senderChangeFee), - senderFee = Codec.encode(data.senderFee), - holdingFees = Codec.encode(data.holdingFees), - ) - - private def toResponse(data: ReceiverAmount) = httpDef.ReceiverAmount( - party = data.party.toProtoPrimitive, - amount = Codec.encode(data.amount), - receiverFee = Codec.encode(data.receiverFee), - ) - - private def toResponse(data: BalanceChange) = httpDef.BalanceChange( - party = data.party.toProtoPrimitive, - changeToInitialAmountAsOfRoundZero = Codec.encode(data.changeToInitialAmountAsOfRoundZero), - changeToHoldingFeesRate = Codec.encode(data.changeToHoldingFeesRate), - ) - - private def toTransferResponseItem(entry: TransferTxLogEntry) = - httpDef.TransactionHistoryResponseItem( - transactionType = HttpTransactionType.Transfer, - eventId = entry.eventId, - offset = Some(entry.offset), - domainId = entry.domainId.toProtoPrimitive, - date = java.time.OffsetDateTime - .ofInstant(entry.date.getOrElse(throw txMissingField()), ZoneOffset.UTC), - transfer = Some( - httpDef.Transfer( - sender = toResponse(entry.sender.getOrElse(throw txMissingField())), - receivers = entry.receivers.map(toResponse).toVector, - balanceChanges = entry.balanceChanges.map(toResponse).toVector, - description = Some(entry.description).filter(_.nonEmpty), - transferInstructionReceiver = - Some(entry.transferInstructionReceiver).filter(_.nonEmpty), - transferInstructionAmount = entry.transferInstructionAmount.map(Codec.encode(_)), - transferInstructionCid = Some(entry.transferInstructionCid).filter(_.nonEmpty), - transferKind = entry.transferKind match { - case TransferKind.Unrecognized(_) => None - case TransferKind.TRANSFER_KIND_OTHER => None - case TransferKind.TRANSFER_KIND_CREATE_TRANSFER_INSTRUCTION => - Some(httpDef.Transfer.TransferKind.members.CreateTransferInstruction) - case TransferKind.TRANSFER_KIND_TRANSFER_INSTRUCTION_ACCEPT => - Some(httpDef.Transfer.TransferKind.members.TransferInstructionAccept) - case TransferKind.TRANSFER_KIND_PREAPPROVAL_SEND => - Some(httpDef.Transfer.TransferKind.members.PreapprovalSend) - }, - ) - ), - round = Some(entry.round), - ) - - private def toTapResponseItem(entry: TapTxLogEntry) = httpDef.TransactionHistoryResponseItem( - transactionType = HttpTransactionType.DevnetTap, - eventId = entry.eventId, - offset = Some(entry.offset), - domainId = entry.domainId.toProtoPrimitive, - date = java.time.OffsetDateTime - .ofInstant(entry.date.getOrElse(throw txMissingField()), ZoneOffset.UTC), - tap = Some( - httpDef.AmuletAmount( - amuletOwner = entry.amuletOwner.toProtoPrimitive, - amuletAmount = Codec.encode(entry.amuletAmount), - ) - ), - round = Some(entry.round), - ) - - private def toMintResponseItem(entry: MintTxLogEntry) = httpDef.TransactionHistoryResponseItem( - transactionType = HttpTransactionType.Mint, - eventId = entry.eventId, - offset = Some(entry.offset), - domainId = entry.domainId.toProtoPrimitive, - date = java.time.OffsetDateTime - .ofInstant(entry.date.getOrElse(throw txMissingField()), ZoneOffset.UTC), - mint = Some( - httpDef.AmuletAmount( - amuletOwner = entry.amuletOwner.toProtoPrimitive, - amuletAmount = Codec.encode(entry.amuletAmount), - ) - ), - ) - - private def toAbortTransferInstructionResponseItem(entry: AbortTransferInstructionTxLogEntry) = - httpDef.TransactionHistoryResponseItem( - transactionType = HttpTransactionType.AbortTransferInstruction, - eventId = entry.eventId, - offset = Some(entry.offset), - domainId = entry.domainId.toProtoPrimitive, - date = java.time.OffsetDateTime - .ofInstant(entry.date.getOrElse(throw txMissingField()), ZoneOffset.UTC), - abortTransferInstruction = Some( - httpDef.AbortTransferInstruction( - abortKind = entry.transferAbortKind match { - case TransferAbortKind.Unrecognized(_) => - sys.error(s"Unexpected transfer abort kind: ${entry.transferAbortKind}") - case TransferAbortKind.TRANSFER_ABORT_KIND_RESERVED => - sys.error(s"Unexpected transfer abort kind: ${entry.transferAbortKind}") - case TransferAbortKind.TRANSFER_ABORT_KIND_REJECT => - httpDef.AbortTransferInstruction.AbortKind.members.Reject - case TransferAbortKind.TRANSFER_ABORT_KIND_WITHDRAW => - httpDef.AbortTransferInstruction.AbortKind.members.Withdraw - }, - transferInstructionCid = entry.transferInstructionCid, - ) - ), - ) - - def toResponseItem(entry: TransactionTxLogEntry): httpDef.TransactionHistoryResponseItem = - entry match { - case entry: TransferTxLogEntry => toTransferResponseItem(entry) - case entry: TapTxLogEntry => toTapResponseItem(entry) - case entry: MintTxLogEntry => toMintResponseItem(entry) - case entry: AbortTransferInstructionTxLogEntry => - toAbortTransferInstructionResponseItem(entry) - case _ => throw txLogIsOfWrongType(entry.getClass.getSimpleName) - } - def toResponse( status: TransferCommandTxLogEntry.Status ): httpDef.TransferCommandContractStatus = @@ -275,101 +114,4 @@ object TxLogEntry extends StoreErrors { ) } } - - sealed trait TransactionType - object TransactionType { - case object Transfer extends TransactionType - case object Mint extends TransactionType - case object Tap extends TransactionType - } - - def parseSenderAmount( - sender: String, - outputs: Seq[splice.amuletrules.TransferOutput], - res: splice.amuletrules.TransferResult, - ): SenderAmount = { - val senderFee = parseOutputAmounts(outputs, res) - .map(_.senderFee) - .sum - - SenderAmount( - party = PartyId.tryFromProtoPrimitive(sender), - inputAmuletAmount = res.summary.inputAmuletAmount, - inputAppRewardAmount = res.summary.inputAppRewardAmount, - inputValidatorRewardAmount = res.summary.inputValidatorRewardAmount, - inputSvRewardAmount = Some(res.summary.inputSvRewardAmount), - inputValidatorFaucetAmount = - res.summary.inputValidatorFaucetAmount.toScala.map(BigDecimal(_)), - senderChangeAmount = res.summary.senderChangeAmount, - senderChangeFee = res.summary.senderChangeFee, - senderFee = senderFee, - holdingFees = res.summary.holdingFees, - ) - } - - def parseReceiverAmounts( - outputs: Seq[splice.amuletrules.TransferOutput], - res: splice.amuletrules.TransferResult, - ): Seq[ReceiverAmount] = { - - // Note: the same receiver party can appear multiple times in the transfer result - // The code below merges amounts and fees for the same receiver, while preserving - // the order of receivers. - parseOutputAmounts(outputs, res) - .map(o => - new ReceiverAmount( - party = PartyId.tryFromProtoPrimitive(o.output.receiver), - amount = o.output.amount, - receiverFee = o.receiverFee, - ) - ) - .foldLeft(immutable.ListMap.empty[PartyId, ReceiverAmount])((acc, receiverAmount) => - acc.updatedWith(receiverAmount.party)(prev => - Some(prev.fold(receiverAmount) { r => - r.copy( - amount = r.amount + receiverAmount.amount, - receiverFee = r.receiverFee + receiverAmount.receiverFee, - ) - }) - ) - ) - .values - .toList - } - - /** A requested output of a transfer, together with the actual fees paid for the transfer. - * - * @param output Contains the receiver and the gross amount received (before deducting fees). - * @param senderFee Actual amount of fees paid by the sender. - * @param receiverFee Actual amount of fees paid by the receiver. - */ - private final case class OutputWithFees( - output: splice.amuletrules.TransferOutput, - senderFee: BigDecimal, - receiverFee: BigDecimal, - ) - - private def parseOutputAmounts( - outputs: Seq[splice.amuletrules.TransferOutput], - res: splice.amuletrules.TransferResult, - ): Seq[OutputWithFees] = { - assert( - outputs.size == res.summary.outputFees.size(), - "Each output should have a corresponding fee", - ) - val outputsWithFees = outputs.zip(res.summary.outputFees.asScala) - - outputsWithFees - .map { case (out, fee) => - OutputWithFees( - output = out, - senderFee = setDamlDecimalScale(BigDecimal(fee) * (BigDecimal(1) - out.receiverFeeRatio)), - receiverFee = setDamlDecimalScale(BigDecimal(fee) * out.receiverFeeRatio), - ) - } - } - - /** Returns the input number modified such that it has the same number of decimal places as a daml decimal */ - private def setDamlDecimalScale(x: BigDecimal): BigDecimal = - x.setScale(10, RoundingMode.HALF_EVEN) } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorage.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorage.scala index 3adb6e4652..57928ca3f5 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorage.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorage.scala @@ -79,6 +79,16 @@ class AcsSnapshotBulkStoragePersistentProgress( kvProvider.store.readValueAndLogOnDecodingFailure(firstSnapshotKvStoreKey).value } + def reset(implicit + tc: TraceContext, + ec: ExecutionContext, + ): Future[Unit] = { + for { + _ <- kvProvider.store.deleteKey(latestSnapshotKvStoreKey) + _ <- kvProvider.store.deleteKey(firstSnapshotKvStoreKey) + } yield {} + } + def persistLatestProcessedSnapshotTimestamp(ts: TimestampWithMigrationId)(implicit tc: TraceContext, ec: ExecutionContext, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageCommitFromStaging.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageCommitFromStaging.scala index fba378264d..7f43f33eea 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageCommitFromStaging.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageCommitFromStaging.scala @@ -10,18 +10,22 @@ import org.apache.pekko.NotUsed import org.apache.pekko.actor.ActorSystem import org.apache.pekko.stream.scaladsl.Flow import org.lfdecentralizedtrust.splice.scan.config.BulkStorageConfig +import org.lfdecentralizedtrust.splice.scan.util.PeerBftScanConnection import org.lfdecentralizedtrust.splice.store.{S3BucketConnection, TimestampWithMigrationId} -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ExecutionContextExecutor, Future} class AcsSnapshotBulkStorageCommitFromStaging( stagingS3Connection: S3BucketConnection, committedS3Connection: S3BucketConnection, bulkStorageReader: BulkStorageReader, appConfig: BulkStorageConfig, + scanConnection: PeerBftScanConnection, + onObjectCommitted: Seq[S3BucketConnection.ObjectKeyAndChecksum] => Unit, val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext) - extends AcsSnapshotBulkStorageWriter +)(implicit + ec: ExecutionContextExecutor +) extends AcsSnapshotBulkStorageWriter with NamedLogging { override def getNextSnapshotTimestampAfter( @@ -55,7 +59,9 @@ class AcsSnapshotBulkStorageCommitFromStaging( Seq.empty }, appConfig, + scanConnection, loggerFactory, + onObjectCommitted, ) } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorage.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorage.scala index 1ed4ff4b55..5f7586df12 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorage.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorage.scala @@ -4,23 +4,29 @@ package org.lfdecentralizedtrust.splice.scan.store.bulk import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory -import com.digitalasset.canton.lifecycle.{AsyncOrSyncCloseable, FlagCloseableAsync} +import com.digitalasset.canton.lifecycle.{AsyncOrSyncCloseable, FlagCloseableAsync, LifeCycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.time.Clock import com.digitalasset.canton.tracing.TraceContext import io.grpc.Status import io.opentelemetry.api.trace.Tracer import org.apache.pekko.actor.{ActorSystem, Cancellable} -import org.lfdecentralizedtrust.splice.config.{AutomationConfig, S3Config} -import org.lfdecentralizedtrust.splice.environment.RetryProvider +import org.lfdecentralizedtrust.splice.config.{AutomationConfig, S3Config, UpgradesConfig} +import org.lfdecentralizedtrust.splice.environment.{RetryProvider, SpliceLedgerClient} import org.lfdecentralizedtrust.splice.scan.config.{BulkStorageConfig, ScanStorageConfig} -import org.lfdecentralizedtrust.splice.scan.store.{AcsSnapshotStore, ScanKeyValueProvider} +import org.lfdecentralizedtrust.splice.scan.store.{ + AcsSnapshotStore, + ScanKeyValueProvider, + ScanStore, +} import org.lfdecentralizedtrust.splice.store.{HistoryMetrics, S3BucketConnection, UpdateHistory} -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ExecutionContextExecutor, Future} +import com.digitalasset.canton.discard.Implicits.DiscardOps import cats.implicits.* import org.apache.pekko.stream.scaladsl.Source import org.lfdecentralizedtrust.splice.PekkoRetryableService +import org.lfdecentralizedtrust.splice.http.HttpClient import org.lfdecentralizedtrust.splice.scan.store.bulk.BulkStorage.{ acsCommittedKvStoreKey, acsStagingKvStoreKey, @@ -28,6 +34,8 @@ import org.lfdecentralizedtrust.splice.scan.store.bulk.BulkStorage.{ updatesCommittedKvStoreKey, updatesStagingKvStoreKey, } +import org.lfdecentralizedtrust.splice.scan.util.PeerBftScanConnection +import org.lfdecentralizedtrust.splice.util.TemplateJsonDecoder import scala.concurrent.duration.* @@ -43,13 +51,19 @@ class BulkStorage( metricsFactory: LabeledMetricsFactory, automationConfig: AutomationConfig, backoffClock: Clock, + store: ScanStore, + svName: String, + ledgerClient: SpliceLedgerClient, + upgradesConfig: UpgradesConfig, override val retryProvider: RetryProvider, override val loggerFactory: NamedLoggerFactory, )(implicit actorSystem: ActorSystem, tc: TraceContext, - ec: ExecutionContext, + ec: ExecutionContextExecutor, tracer: Tracer, + httpClient: HttpClient, + templateJsonDecoder: TemplateJsonDecoder, ) extends NamedLogging with FlagCloseableAsync with RetryProvider.Has { @@ -57,6 +71,16 @@ class BulkStorage( val stagingConnection = S3BucketConnection(stagingS3Config, loggerFactory) val committedConnection = S3BucketConnection(committedS3Config, loggerFactory) val historyMetrics = HistoryMetrics(metricsFactory, currentMigrationId) + val scanConnection = new PeerBftScanConnection( + store, + svName, + ledgerClient, + automationConfig, + upgradesConfig, + backoffClock, + retryProvider, + loggerFactory, + ) val backfillingCompleteGate: Source[Boolean, Cancellable] = Source @@ -128,6 +152,17 @@ class BulkStorage( committedConnection, reader, appConfig, + scanConnection, + objs => + objs.foreach { obj => + val encoding = ScanStorageConfig.Encoding.all.toList + .collectFirst { + case enc if enc.storageKeyRegex("ACS").matches(obj.key) => + enc.key + } + .getOrElse("unknown") + historyMetrics.BulkStorage.incAcsSnapshotObjects(encoding, "committed") + }, loggerFactory, ) val acsCommitted = new AcsSnapshotBulkStorage( @@ -160,6 +195,17 @@ class BulkStorage( committedConnection, reader, appConfig, + scanConnection, + objs => + objs.foreach { obj => + val encoding = ScanStorageConfig.Encoding.all.toList + .collectFirst { + case enc if enc.storageKeyRegex("updates").matches(obj.key) => + enc.key + } + .getOrElse("unknown") + historyMetrics.BulkStorage.incUpdateObjects(encoding, "committed") + }, loggerFactory, ) val updatesCommitted = new UpdateHistoryBulkStorage( @@ -171,12 +217,34 @@ class BulkStorage( loggerFactory, ) - private val services = + // Services are only started once initialization has completed. + private lazy val services = Seq[PekkoRetryableService[?]](acsStaging, acsCommitted, updatesStaging, updatesCommitted) .map(_.asPekkoRetryingService(automationConfig, backoffClock, retryProvider)) - final override def closeAsync(): Seq[AsyncOrSyncCloseable] = + private def initialize(): Future[BulkStorage] = { + val resetAll = + if (appConfig.debugForceStartFromGenesis) { + logger.warn( + "debugForceStartFromGenesis is set to true, resetting all bulk storage progress and starting from genesis" + ) + for { + _ <- acsStagingProgress.reset + _ <- acsCommittedProgress.reset + _ <- updatesStagingProgress.reset + _ <- updatesCommittedProgress.reset + } yield () + } else Future.unit + resetAll.map { _ => + services.discard + this + } + } + + final override def closeAsync(): Seq[AsyncOrSyncCloseable] = { + LifeCycle.close(scanConnection)(logger) services.flatMap(_.closeAsync()) + } } object BulkStorage { @@ -197,14 +265,20 @@ object BulkStorage { metricsFactory: LabeledMetricsFactory, automationConfig: AutomationConfig, backoffClock: Clock, + store: ScanStore, + svName: String, + ledgerClient: SpliceLedgerClient, + upgradesConfig: UpgradesConfig, retryProvider: RetryProvider, loggerFactory: NamedLoggerFactory, )(implicit actorSystem: ActorSystem, tc: TraceContext, - ec: ExecutionContext, + ec: ExecutionContextExecutor, tracer: Tracer, - ): BulkStorage = { + httpClient: HttpClient, + templateJsonDecoder: TemplateJsonDecoder, + ): Future[BulkStorage] = { val logger = loggerFactory.getTracedLogger(classOf[BulkStorage]) (appConfig.staging, appConfig.committed).tupled.fold { @@ -225,9 +299,13 @@ object BulkStorage { metricsFactory, automationConfig, backoffClock, + store, + svName, + ledgerClient, + upgradesConfig, retryProvider, loggerFactory, - ) + ).initialize() } } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageCommitFromStaging.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageCommitFromStaging.scala index 75f18af458..5d1671aeca 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageCommitFromStaging.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageCommitFromStaging.scala @@ -6,13 +6,15 @@ package org.lfdecentralizedtrust.splice.scan.store.bulk import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext import org.apache.pekko.NotUsed -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.stream.scaladsl.{Flow, Sink, Source} +import org.apache.pekko.http.scaladsl.model.StatusCodes +import org.apache.pekko.stream.scaladsl.{Flow, Source} +import org.lfdecentralizedtrust.splice.admin.http.HttpErrorWithHttpCode import org.lfdecentralizedtrust.splice.scan.config.BulkStorageConfig +import org.lfdecentralizedtrust.splice.scan.util.PeerBftScanConnection import org.lfdecentralizedtrust.splice.store.S3BucketConnection import org.lfdecentralizedtrust.splice.store.S3BucketConnection.ObjectKeyAndChecksum -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ExecutionContextExecutor, Future} // TODO(#5884): review parallelism here. We use parallelism = 1 all over, but unsure whether that's actually necessary. @@ -21,28 +23,127 @@ class BulkStorageCommitFromStaging[T]( committedS3Connection: S3BucketConnection, getObjects: T => Future[Seq[ObjectKeyAndChecksum]], appConfig: BulkStorageConfig, + scanConnection: PeerBftScanConnection, override val loggerFactory: NamedLoggerFactory, + onObjectCommitted: Seq[ObjectKeyAndChecksum] => Unit = _ => (), )(implicit tc: TraceContext, - ec: ExecutionContext, - actorSystem: ActorSystem, + ec: ExecutionContextExecutor, ) extends NamedLogging { + private def checkBftForObjects( objects: Seq[ObjectKeyAndChecksum] ): Future[Boolean] = { logger.debug( s"Checking BFT agreement for objects: ${objects.map(_.key).mkString(", ")}" ) - Future.successful(true) + if (appConfig.bftCheckEnabled) { + for { + connection <- scanConnection.connection + bft <- connection.getBulkObjectChecksums(objects.map(_.key)).map(Some(_)).recoverWith { + case ex @ HttpErrorWithHttpCode(code, _) => + if (code == StatusCodes.BadGateway) { + logger.debug( + s"Consensus on checksums for objects ${objects.map(_.key).mkString(", ")} not reached. Assuming that this is because not all peers have processed the objects yet." + ) + Future.successful(None) + } else { + throw ex + } + } + } yield { + bft match { + case Some(bftChecksums) => + val consensusChecksums = bftChecksums.checksums.filter(_.value.isDefined) + logger.debug( + s"Consensus achieved on ${consensusChecksums.length} out of ${objects.length} objects" + ) + + if (consensusChecksums.length < objects.length) { + logger.debug( + s"Not all objects are known to the BFT peers yet. Will retry after delay." + ) + false + } else { + logger.debug( + s"All objects are known to the BFT peers. Checking if checksums match." + ) + val consensus = + bftChecksums.checksums.filter(_.value.isDefined).map(_.value) == objects.map(oc => + Some(oc.checksum) + ) + if (!consensus) { + logger.error( + s"Checksums do not match for objects ${objects.map(_.key).mkString(", ")}. My checksums are: ${objects + .map(_.checksum) + .mkString(", ")}, consensus checksums are: ${consensusChecksums.mkString(", ")}" + ) + + if (appConfig.debugObjectsToNotCommit.intersect(objects.map(_.key)).nonEmpty) { + logger.debug( + s"Some relevant objects are listed in debugObjectsToNotCommit, will ignore them for the consensus check. Ignored objects: ${appConfig.debugObjectsToNotCommit + .intersect(objects.map(_.key)) + .mkString(", ")}" + ) + val objectsWithConsensusChecksums = objects.zip(consensusChecksums) + // Filter out objects for which the key is listed in appConfig.debugObjectsToNotCommit + val unignoredObjectsWithTheirConsensusChecksums = + objectsWithConsensusChecksums.filter { case (obj, _) => + !appConfig.debugObjectsToNotCommit.contains(obj.key) + } + val unignoredObjectsWithMyChecksums = + objects.filter(obj => !appConfig.debugObjectsToNotCommit.contains(obj.key)) + // recheck consensus, but now only on the unignored objects. The comparison should be similar to val consensus above + val unignoredConsensus = + unignoredObjectsWithTheirConsensusChecksums + .filter(_._2.value.isDefined) + .map(_._2.value) == unignoredObjectsWithMyChecksums.map(oc => + Some(oc.checksum) + ) + + if (!unignoredConsensus) { + logger.error( + s"Checksums still do not match for unignored objects ${unignoredObjectsWithMyChecksums + .map(_.key) + .mkString(", ")}. Expected: ${unignoredObjectsWithMyChecksums + .map(_.checksum) + .mkString(", ")}, got: ${unignoredObjectsWithTheirConsensusChecksums.map(_._2.value).mkString(", ")}" + ) + } else { + logger.debug( + s"After ignoring objects from the config, Checksums match ${unignoredObjectsWithMyChecksums.map(_.key).mkString(", ")}. Proceeding with commit." + ) + } + unignoredConsensus + } else { + logger.trace( + s"No relevant objects are listed in debugObjectsToNotCommit, will not ignore any objects for the consensus check." + ) + consensus + } + } else { + logger.trace( + s"Checksums match for all objects ${objects.map(_.key).mkString(", ")}. Proceeding with commit." + ) + true + } + } + case None => + false + } + } + } else { + logger.trace("BFT check is disabled, skipping BFT agreement check") + Future.successful(true) + } } - // TODO(#5884): implement the BFT check private def waitForBftAgreement: Flow[ (T, Seq[ObjectKeyAndChecksum]), (T, Seq[ObjectKeyAndChecksum]), NotUsed, ] = { - Flow[(T, Seq[ObjectKeyAndChecksum])].mapAsync(parallelism = 1) { case (t, obj) => + Flow[(T, Seq[ObjectKeyAndChecksum])].flatMapConcat { case (t, obj) => Source .repeat(obj) .mapAsync(parallelism = 1)(obj => checkBftForObjects(obj).map(result => (obj, result))) @@ -60,8 +161,7 @@ class BulkStorageCommitFromStaging[T]( Source.single((obj, false)).delay(appConfig.bftRetryInterval.underlying) } .takeWhile({ case (_, bftReached) => !bftReached }, inclusive = true) - .runWith(Sink.last) - .map { case (obj, _) => (t, obj) } + .collect { case (o, true) => (t, o) } } } @@ -78,8 +178,15 @@ class BulkStorageCommitFromStaging[T]( ) Future.unit case false => - logger.debug(s"Copying object ${obj.key} from staging to committed storage") - committedS3Connection.copyObject(stagingS3Connection.bucketName, obj.key) + if (appConfig.debugObjectsToNotCommit.contains(obj.key)) { + logger.debug( + s"Object ${obj.key} is listed in debugObjectsToNotCommit, skipping copy to committed storage" + ) + Future.unit + } else { + logger.debug(s"Copying object ${obj.key} from staging to committed storage") + committedS3Connection.copyObject(stagingS3Connection.bucketName, obj.key) + } } } @@ -95,7 +202,10 @@ class BulkStorageCommitFromStaging[T]( ) Future .sequence(objs.map(copyObjectToCommitted(stagingS3Connection, committedS3Connection))) - .map(_ => (ts, objs)) + .map { _ => + onObjectCommitted(objs) + (ts, objs) + } } private def deleteFromStaging: Flow[ @@ -136,18 +246,21 @@ object BulkStorageCommitFromStaging { committedS3Connection: S3BucketConnection, getStagingObjects: T => Future[Seq[ObjectKeyAndChecksum]], appConfig: BulkStorageConfig, + scanConnection: PeerBftScanConnection, loggerFactory: NamedLoggerFactory, + onObjectCommitted: Seq[ObjectKeyAndChecksum] => Unit = _ => (), )(implicit tc: TraceContext, - ec: ExecutionContext, - actorSystem: ActorSystem, + ec: ExecutionContextExecutor, ): Flow[T, T, NotUsed] = { new BulkStorageCommitFromStaging[T]( stagingS3Connection, committedS3Connection, getStagingObjects, appConfig, + scanConnection, loggerFactory, + onObjectCommitted, ).getFlow } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageReader.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageReader.scala index de1653fb35..792824f059 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageReader.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageReader.scala @@ -3,6 +3,7 @@ package org.lfdecentralizedtrust.splice.scan.store.bulk +import cats.data.NonEmptyList import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext @@ -28,7 +29,9 @@ class BulkStorageReader( extends NamedLogging { def getCommittedObjectsForAcsSnapshotAtOrBefore( - atOrBeforeTimestamp: CantonTimestamp + atOrBeforeTimestamp: CantonTimestamp, + storageEncodings: NonEmptyList[ScanStorageConfig.Encoding] = + NonEmptyList.one(ScanStorageConfig.Encoding.CompactJson), )(implicit tc: TraceContext, ec: ExecutionContext): Future[AcsSnapshotObjects] = { for { snapshotTs <- @@ -45,7 +48,12 @@ class BulkStorageReader( ts.timestamp case Some(ts) => storageConfig.computeBulkSnapshotTimeAtOrBefore(atOrBeforeTimestamp) } - objects <- getAcsSnapshotObjects(snapshotTs, committedS3Connection, storageConfig) + objects <- getAcsSnapshotObjects( + snapshotTs, + committedS3Connection, + storageConfig, + storageEncodings, + ) } yield { objects } @@ -99,18 +107,26 @@ class BulkStorageReader( def getStagingObjectsForAcsSnapshotAt( timestamp: CantonTimestamp ): Future[AcsSnapshotObjects] = { - getAcsSnapshotObjects(timestamp, stagingS3Connection, storageConfig) + getAcsSnapshotObjects( + timestamp, + stagingS3Connection, + storageConfig, + ScanStorageConfig.Encoding.all, + ) } def getStagingObjectsForUpdateHistorySegment( segment: UpdatesSegment - ): Future[UpdateHistoryObjectsResponse] = getUpdateObjectsInSegment(segment, stagingS3Connection) + ): Future[UpdateHistoryObjectsResponse] = + getUpdateObjectsInSegment(segment, stagingS3Connection, ScanStorageConfig.Encoding.all) def getCommittedUpdatesBetweenDates( afterRecordTime: CantonTimestamp, atOrBeforeRecordTime: CantonTimestamp, limit: PageLimit, nextPageTokenO: Option[String], + storageEncodings: NonEmptyList[ScanStorageConfig.Encoding] = + NonEmptyList.one(ScanStorageConfig.Encoding.CompactJson), )(implicit tc: TraceContext, ec: ExecutionContext): Future[UpdateHistoryObjectsResponse] = getUpdatesBetweenDatesFromBucket( afterRecordTime, @@ -119,6 +135,7 @@ class BulkStorageReader( nextPageTokenO, committedS3Connection, updateHistoryCommittedProgress.readLatestProcessedSegment, + storageEncodings, ) def getStagingSegmentStartingAt( @@ -143,6 +160,19 @@ class BulkStorageReader( } } + def getObjectChecksums( + objectKeys: Seq[String] + ): Future[Seq[Option[String]]] = { + for { + committed <- committedS3Connection.getChecksums(objectKeys) + staging <- stagingS3Connection.getChecksums(objectKeys) + } yield { + objectKeys.map { key => + committed.find(_.key == key).orElse(staging.find(_.key == key)).map(_.checksum) + } + } + } + private def getSegmentStartingAt( startTimestamp: Option[CantonTimestamp] ): Future[Option[(CantonTimestamp, CantonTimestamp)]] = @@ -158,10 +188,18 @@ class BulkStorageReader( ) } + /** Checks if a key in bulk storage matches one of the given [[storageEncodings]]. */ + private def keyMatchesStorageEncodings( + prefix: String, + storageEncodings: NonEmptyList[ScanStorageConfig.Encoding], + )(key: String): Boolean = + storageEncodings.exists(_.storageKeyRegex(prefix).matches(key)) + private def getAcsSnapshotObjects( timestamp: CantonTimestamp, s3Connection: S3BucketConnection, storageConfig: ScanStorageConfig, + storageEncodings: NonEmptyList[ScanStorageConfig.Encoding], ): Future[AcsSnapshotObjects] = { for { objects <- s3Connection @@ -170,7 +208,7 @@ class BulkStorageReader( // (hence the HardLimit, just as a safety precaution). .listObjects( storageConfig.findSegmentFolderPrefixByStartTimestamp(timestamp), - _.matches(".*ACS_\\d+\\.zstd"), + keyMatchesStorageEncodings("ACS", storageEncodings), HardLimit.tryCreate(Limit.DefaultMaxPageSize), ) objectsWithChecksums <- s3Connection.getChecksums(objects) @@ -201,6 +239,7 @@ class BulkStorageReader( nextPageTokenO: Option[String], s3Connection: S3BucketConnection, readLatestProcessedSegment: => Future[Option[UpdatesSegment]], + storageEncodings: NonEmptyList[ScanStorageConfig.Encoding], )(implicit tc: TraceContext, ec: ExecutionContext): Future[UpdateHistoryObjectsResponse] = { def isFolderInRange(folder: String): Boolean = { @@ -299,7 +338,7 @@ class BulkStorageReader( if (folderLimit <= 0) { Future.successful((folderAcc, folderLimit)) } else { - getUpdateObjectsInFolder(s3Connection, folder).map { folderObjs => + getUpdateObjectsInFolder(s3Connection, folder, storageEncodings).map { folderObjs => if (folderObjs.size > folderLimit) { // Folder would exceed the limit; omit it entirely (and stop adding more by making the limit 0) if (folderAcc.isEmpty) { @@ -338,12 +377,13 @@ class BulkStorageReader( private def getUpdateObjectsInSegment( segment: UpdatesSegment, s3Connection: S3BucketConnection, + storageEncodings: NonEmptyList[ScanStorageConfig.Encoding], ): Future[UpdateHistoryObjectsResponse] = { val folder = storageConfig.getSegmentFolder( segment.fromTimestamp.timestamp, Some(segment.toTimestamp.timestamp), ) - getUpdateObjectsInFolder(s3Connection, folder) + getUpdateObjectsInFolder(s3Connection, folder, storageEncodings) .flatMap(s3Connection.getChecksums) .map { objectsWithChecksums => if (objectsWithChecksums.isEmpty) { @@ -366,9 +406,10 @@ class BulkStorageReader( private def getUpdateObjectsInFolder( s3Connection: S3BucketConnection, folder: String, + storageEncodings: NonEmptyList[ScanStorageConfig.Encoding], ): Future[Seq[String]] = s3Connection.listObjects( prefix = folder, - _.matches(".*updates_\\d+\\.zstd"), + keyMatchesStorageEncodings("updates", storageEncodings), HardLimit.tryCreate(Limit.DefaultMaxPageSize), ) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/GroupedWeightS3ObjectFlow.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/GroupedWeightS3ObjectFlow.scala index 60df59359d..9825e0e6b4 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/GroupedWeightS3ObjectFlow.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/GroupedWeightS3ObjectFlow.scala @@ -83,6 +83,14 @@ case class GroupedWeightS3ObjectFlow( @SuppressWarnings(Array("org.wartremover.warts.Var")) private var state = State.initial() + // Guards against finishing the same object twice, which can otherwise happen because finish() + // is triggered both from uploadCallback and from onUpstreamFinish. + // At the time of writing, a duplicate finish is actually harmless, as it calls + // AppendWriteObject.finish() which is idempotent. We guard against it anyway, to protect against + // future changes that would make finishing an object non-idempotent. + @SuppressWarnings(Array("org.wartremover.warts.Var")) + private var finishingObject = false + private def objectDone = state.currentObjectSize >= maxObjectSize || isClosed(in) private val uploadCallback = getAsyncCallback[Unit] { _ => @@ -111,6 +119,7 @@ case class GroupedWeightS3ObjectFlow( private val finishCallback = getAsyncCallback[Unit] { _ => logger.debug(s"Finished uploading and finalizing object ${state.currentObject.key}") + finishingObject = false push(out, state.currentObject.key) if (isClosed(in)) { logger.trace("Upstream completed, completing too.") @@ -156,13 +165,24 @@ case class GroupedWeightS3ObjectFlow( } private def finishCurrentObject(): Unit = - state.currentObject.finish().onComplete { - case Success(_) => finishCallback.invoke(()) - case Failure(ex) => failCallback.invoke(ex) + if (finishingObject) { + logger.debug( + s"Object ${state.currentObject.key} is already being finished, not finishing it again" + ) + } else { + finishingObject = true + state.currentObject.finish().onComplete { + case Success(_) => finishCallback.invoke(()) + case Failure(ex) => failCallback.invoke(ex) + } } override def onUpstreamFinish(): Unit = { - if (state.numPendingPartUploads == 0) { + if (finishingObject) { + logger.debug( + s"Upstream finished while object ${state.currentObject.key} is being finished, waiting for it to complete" + ) + } else if (state.numPendingPartUploads == 0) { if (state.currentObjectSize > 0) { logger.debug( s"Upstream finished, finishing current object ${state.currentObject.key} with size ${state.currentObjectSize}" diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/MultiEncodingBulkStorageFlow.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/MultiEncodingBulkStorageFlow.scala new file mode 100644 index 0000000000..9e1342e4c1 --- /dev/null +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/MultiEncodingBulkStorageFlow.scala @@ -0,0 +1,49 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.scan.store.bulk + +import org.apache.pekko.NotUsed +import org.apache.pekko.stream.FlowShape +import org.apache.pekko.stream.scaladsl.{Broadcast, Flow, GraphDSL, Merge} +import org.apache.pekko.util.ByteString +import org.lfdecentralizedtrust.splice.scan.config.ScanStorageConfig + +/** Uploads every chunk of a bulk storage dump once per [[ScanStorageConfig.Encoding]]. + * + * Each upstream chunk is broadcast to one branch per encoding, where it's encoded, uploaded via + * the given [[uploadFlow]] (an [[S3ZstdObjects]] flow), and counted in the metrics. The emitted + * object keys of all branches are merged into a single downstream output. + * + * Note that the `Merge` is not eager, so the resulting flow only completes once every branch has + * uploaded all of its objects. Callers can therefore rely on total completion before advancing a + * progress marker. + */ +object MultiEncodingBulkStorageFlow { + private lazy val encodings = ScanStorageConfig.Encoding.all.toList + private lazy val numEncodings = encodings.length + + def apply[A]( + encode: (A, ScanStorageConfig.Encoding) => ByteString, + uploadFlow: ScanStorageConfig.Encoding => Flow[ByteString, String, ?], + incObjects: ScanStorageConfig.Encoding => Unit, + ): Flow[A, String, NotUsed] = { + Flow.fromGraph(GraphDSL.create() { implicit b => + import GraphDSL.Implicits.* + + val broadcast = b.add(Broadcast[A](numEncodings)) + val merge = b.add(Merge[String](numEncodings)) + + encodings.zipWithIndex.foreach { case (encoding, i) => + val branch = Flow[A] + .map(encode(_, encoding)) + .via(uploadFlow(encoding)) + .wireTap(_ => incObjects(encoding)) + + broadcast.out(i) ~> branch ~> merge.in(i) + } + + FlowShape(broadcast.in, merge.out) + }) + } +} diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/SingleAcsSnapshotBulkStorage.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/SingleAcsSnapshotBulkStorage.scala index fb514a6dce..5c54c8ee1a 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/SingleAcsSnapshotBulkStorage.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/SingleAcsSnapshotBulkStorage.scala @@ -8,7 +8,7 @@ import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext import org.apache.pekko.stream.scaladsl.{Flow, Source} import org.apache.pekko.util.ByteString -import org.lfdecentralizedtrust.splice.scan.admin.http.CompactJsonScanHttpEncodings +import org.lfdecentralizedtrust.splice.scan.admin.http.ScanHttpEncodings import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore import org.lfdecentralizedtrust.splice.store.{ HistoryMetrics, @@ -16,6 +16,7 @@ import org.lfdecentralizedtrust.splice.store.{ S3BucketConnection, TimestampWithMigrationId, } +import org.lfdecentralizedtrust.splice.store.events.SpliceCreatedEvent import scala.concurrent.Future import io.circe.syntax.* @@ -32,7 +33,7 @@ object Position { case object End extends Position - final case class Index(value: Long) extends Position + final case class Index(value: AcsSnapshotStore.QueryAcsSnapshotPaginationToken) extends Position } class SingleAcsSnapshotBulkStorage( @@ -46,15 +47,10 @@ class SingleAcsSnapshotBulkStorage( )(implicit tc: TraceContext, ec: ExecutionContext) extends NamedLogging { - case class AcsSnapshotChunk( - chunkBytes: ByteString, - numContracts: Int, - ) - private def getAcsSnapshotChunk( timestamp: TimestampWithMigrationId, - after: Option[Long], - ): Future[(Position, AcsSnapshotChunk)] = { + after: Option[AcsSnapshotStore.QueryAcsSnapshotPaginationToken], + ): Future[(Position, Vector[SpliceCreatedEvent])] = { for { snapshot <- acsSnapshotStore.queryAcsSnapshot( timestamp.migrationId, @@ -64,22 +60,27 @@ class SingleAcsSnapshotBulkStorage( Seq.empty, Seq.empty, ) - } yield { - val encoded = snapshot.createdEventsInPage.map(event => - CompactJsonScanHttpEncodings() - .javaToHttpActiveContract(event.eventId, event.recordTime, event.event) - ) - val contractsStr = encoded.map(_.asJson.noSpacesSortKeys).mkString("\n") + "\n" - val contractsBytes = ByteString(contractsStr.getBytes(StandardCharsets.UTF_8)) - logger.debug( - s"Read ${encoded.length} contracts from ACS, to a bytestring of size ${contractsBytes.length} bytes" - ) - ( - snapshot.afterToken.fold(End: Position)(Index(_)), - AcsSnapshotChunk(contractsBytes, encoded.length), - ) - } + } yield ( + snapshot.afterToken.fold(End: Position)(Index(_)), + snapshot.createdEventsInPage, + ) + + } + private def encodeEvents( + events: Vector[SpliceCreatedEvent], + encoding: ScanStorageConfig.Encoding, + ): ByteString = { + val encodings = ScanHttpEncodings.fromDamlValueEncoding(encoding.damlValueEncoding) + val encoded = events.map(event => + encodings.javaToHttpActiveContract(event.eventId, event.recordTime, event.event) + ) + val contractsStr = encoded.map(_.asJson.noSpacesSortKeys).mkString("\n") + "\n" + val contractsBytes = ByteString(contractsStr.getBytes(StandardCharsets.UTF_8)) + logger.debug( + s"Read ${encoded.length} contracts from ACS, to a bytestring of size ${contractsBytes.length} bytes, with encoding ${encoding.key}" + ) + contractsBytes } private def getSource: Source[Seq[String], NotUsed] = { @@ -89,22 +90,26 @@ class SingleAcsSnapshotBulkStorage( case Index(i) => getAcsSnapshotChunk(timestamp, Some(i)).map(Some(_)) case End => Future.successful(None) } - .map(chunk => { - historyMetrics.BulkStorage.incContractsCount(chunk.numContracts) - chunk.chunkBytes + .map(events => { + historyMetrics.BulkStorage.incContractsCount(events.length) + events }) .via( - S3ZstdObjects( - storageConfig, - appConfig, - s3Connection, - { objIdx => - s"${storageConfig.getSegmentFolder(timestamp.timestamp, None)}/ACS_$objIdx.zstd" - }, - loggerFactory, + MultiEncodingBulkStorageFlow( + encodeEvents, + encoding => + S3ZstdObjects( + storageConfig, + appConfig, + s3Connection, + objIdx => + s"${storageConfig.getSegmentFolder(timestamp.timestamp, None)}/${encoding + .storageKey("ACS", objIdx)}", + loggerFactory, + ), + encoding => historyMetrics.BulkStorage.incAcsSnapshotObjects(encoding.key, "staging"), ) ) - .wireTap(_ => historyMetrics.BulkStorage.incAcsSnapshotObjects()) .fold(Seq.empty[String])(_ :+ _) } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorage.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorage.scala index a27d418a20..d0e858829e 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorage.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorage.scala @@ -67,6 +67,10 @@ class UpdateHistoryBulkStoragePersistentProgress( ) }) } + + def reset(implicit tc: TraceContext): Future[Unit] = { + kvProvider.store.deleteKey(kvStoreKey) + } } /** An abstract class for pipelines that process update history for bulk storage. diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorageCommitFromStaging.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorageCommitFromStaging.scala index 1911f9f893..7a07c2f326 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorageCommitFromStaging.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorageCommitFromStaging.scala @@ -7,21 +7,24 @@ import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext import io.grpc.{Status, StatusRuntimeException} import org.apache.pekko.NotUsed -import org.apache.pekko.actor.ActorSystem import org.apache.pekko.stream.scaladsl.Flow import org.lfdecentralizedtrust.splice.scan.config.BulkStorageConfig +import org.lfdecentralizedtrust.splice.scan.util.PeerBftScanConnection import org.lfdecentralizedtrust.splice.store.{S3BucketConnection, TimestampWithMigrationId} -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ExecutionContextExecutor, Future} class UpdateHistoryBulkStorageCommitFromStaging( stagingS3Connection: S3BucketConnection, committedS3Connection: S3BucketConnection, bulkStorageReader: BulkStorageReader, appConfig: BulkStorageConfig, + scanConnection: PeerBftScanConnection, + onObjectCommitted: Seq[S3BucketConnection.ObjectKeyAndChecksum] => Unit, val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext, actorSystem: ActorSystem) - extends UpdateHistoryBulkStorageWriter +)(implicit + ec: ExecutionContextExecutor +) extends UpdateHistoryBulkStorageWriter with NamedLogging { override def processSegmentsFlow(implicit tc: TraceContext @@ -39,7 +42,9 @@ class UpdateHistoryBulkStorageCommitFromStaging( Seq.empty }, appConfig, + scanConnection, loggerFactory, + onObjectCommitted, ) override def getNextSegmentAfter( diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistorySegmentBulkStorage.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistorySegmentBulkStorage.scala index bd0782c85a..fb8b915b96 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistorySegmentBulkStorage.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistorySegmentBulkStorage.scala @@ -3,6 +3,7 @@ package org.lfdecentralizedtrust.splice.scan.store.bulk +import cats.data.NonEmptyList import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext import org.apache.pekko.NotUsed @@ -10,7 +11,6 @@ import org.apache.pekko.stream.scaladsl.{Flow, Source} import org.lfdecentralizedtrust.splice.scan.config.{BulkStorageConfig, ScanStorageConfig} import org.apache.pekko.util.ByteString import org.apache.pekko.pattern.after -import org.lfdecentralizedtrust.splice.http.v0.definitions import org.lfdecentralizedtrust.splice.scan.admin.http.{ScanHttpEncodings, ScanJsonSupport} import org.lfdecentralizedtrust.splice.store.{ HistoryMetrics, @@ -56,17 +56,14 @@ class UpdateHistorySegmentBulkStorage( )(implicit tc: TraceContext, ec: ExecutionContext) extends NamedLogging { - case class UpdatesChunk( - updateBytes: ByteString, - numUpdates: Int, - ) - private def getUpdatesChunk( afterTs: TimestampWithMigrationId - )(implicit actorSystem: ActorSystem): Future[Option[(TimestampWithMigrationId, UpdatesChunk)]] = { + )(implicit + actorSystem: ActorSystem + ): Future[Option[(TimestampWithMigrationId, Seq[TreeUpdateWithMigrationId])]] = { for { updates <- updateHistory.getUpdatesWithoutImportUpdates( - Some((afterTs.migrationId, afterTs.timestamp)), + Some(TimestampWithMigrationId(afterTs.timestamp, afterTs.migrationId)), PageLimit.tryCreate(storageConfig.bulkDbReadChunkSize), ) updatesInSegment = updates.filter(update => @@ -85,7 +82,6 @@ class UpdateHistorySegmentBulkStorage( s"Adding ${updatesInSegment.length} updates, between record time ${updatesInSegment.headOption .map(_.update.update.recordTime)} and ${updatesInSegment.lastOption.map(_.update.update.recordTime)}" ) - val updatesBytes: ByteString = encodeUpdates(updatesInSegment) val last = updatesInSegment.lastOption.getOrElse( throw new RuntimeException("Unexpected failure") ) @@ -93,7 +89,7 @@ class UpdateHistorySegmentBulkStorage( Some( ( TimestampWithMigrationId(last.update.update.recordTime, last.migrationId), - UpdatesChunk(updatesBytes, updatesInSegment.length), + updatesInSegment, ) ) ) @@ -113,7 +109,7 @@ class UpdateHistorySegmentBulkStorage( appConfig.updatesPollingInterval.underlying, actorSystem.scheduler, ) { - Future.successful(Some((afterTs, UpdatesChunk(ByteString.empty, 0)))) + Future.successful(Some((afterTs, Nil))) } } } yield { @@ -121,11 +117,14 @@ class UpdateHistorySegmentBulkStorage( } } - private def encodeUpdates(updates: Seq[TreeUpdateWithMigrationId]) = { - val encoded = updates.map(update => + private def encodeUpdates( + updates: NonEmptyList[TreeUpdateWithMigrationId], + encoding: ScanStorageConfig.Encoding, + ): ByteString = { + val encoded = updates.toList.map(update => ScanHttpEncodings.encodeUpdateV2( update, - definitions.DamlValueEncoding.CompactJson, + encoding.damlValueEncoding, ScanHttpEncodings.V1, ) ) @@ -139,8 +138,7 @@ class UpdateHistorySegmentBulkStorage( .mkString("\n") + "\n" val updatesBytes = ByteString(updatesStr.getBytes(StandardCharsets.UTF_8)) logger.debug( - s"Read and encoded ${encoded.length} updates from DB, to a bytestring of size ${updatesBytes.length} bytes. Timestamps are ${updates.headOption - .map(_.update.update.recordTime)} to ${updates.lastOption.map(_.update.update.recordTime)}" + s"Read and encoded ${encoded.length} updates from DB, to a bytestring of size ${updatesBytes.length} bytes, with encoding ${encoding.key}. Timestamps are ${updates.head.update.update.recordTime} to ${updates.last.update.update.recordTime}" ) updatesBytes } @@ -150,30 +148,35 @@ class UpdateHistorySegmentBulkStorage( ): Source[Seq[String], NotUsed] = { Source .unfoldAsync(segment.fromTimestamp)(ts => getUpdatesChunk(ts)) - .map(chunk => { - historyMetrics.BulkStorage.incUpdatesCount(chunk.numUpdates) - chunk.updateBytes + .map(updates => { + historyMetrics.BulkStorage.incUpdatesCount(updates.length) + updates }) .via( - // We use lazyFlow, so that in the case where no updates are emitted, we don't instantiate the S3ZstdObjects at all, - // since it assumes that it gets at least one chunk to write. - Flow.lazyFlow(() => - S3ZstdObjects( - storageConfig, - appConfig, - s3Connection, - { objIdx => - s"${storageConfig.getSegmentFolder(segment.fromTimestamp.timestamp, Some(segment.toTimestamp.timestamp))}/updates_$objIdx.zstd" - }, - loggerFactory, - ) + MultiEncodingBulkStorageFlow( + (updates, encoding) => + NonEmptyList.fromFoldable(updates).fold(ByteString.empty)(encodeUpdates(_, encoding)), + encoding => + // We use lazyFlow, so that in the case where no updates are emitted, we don't instantiate the S3ZstdObjects at all, + // since it assumes that it gets at least one chunk to write. + Flow.lazyFlow(() => + S3ZstdObjects( + storageConfig, + appConfig, + s3Connection, + objIdx => + s"${storageConfig.getSegmentFolder(segment.fromTimestamp.timestamp, Some(segment.toTimestamp.timestamp))}/${encoding + .storageKey("updates", objIdx)}", + loggerFactory, + ) + ), + encoding => historyMetrics.BulkStorage.incUpdateObjects(encoding.key, "staging"), ) ) .orElse(Source.lazySource { () => logger.warn(s"No updates found in segment ${segment.fromTimestamp}-${segment.toTimestamp}") Source.empty }) - .wireTap(_ => historyMetrics.BulkStorage.incUpdateObjects()) .fold(Seq.empty[String])(_ :+ _) } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbAppActivityRecordStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbAppActivityRecordStore.scala index a60594dc25..60a3664e00 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbAppActivityRecordStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbAppActivityRecordStore.scala @@ -4,6 +4,7 @@ package org.lfdecentralizedtrust.splice.scan.store.db import org.lfdecentralizedtrust.splice.scan.store.AppActivityStore +import org.lfdecentralizedtrust.splice.scan.store.AppActivityStore.RoundIngestionStatus import org.lfdecentralizedtrust.splice.store.UpdateHistory import org.lfdecentralizedtrust.splice.util.FutureUnlessShutdownUtil.futureUnlessShutdownToFuture import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} @@ -171,7 +172,7 @@ class DbAppActivityRecordStore( * This round may not have all app activity records ingested. * Returns None if no app activity records have been ingested, ie meta row does not exist. */ - def earliestIngestedRound()(implicit + private[store] def earliestIngestedRound()(implicit tc: TraceContext ): Future[Option[Long]] = { val codeVersion = ingestionVersions.code @@ -187,6 +188,32 @@ class DbAppActivityRecordStore( ) } + override def ingestionStatusForRound(roundNumber: Long)(implicit + tc: TraceContext + ): Future[RoundIngestionStatus] = + earliestIngestedRound().map { + case Some(earliestIngested) if roundNumber <= earliestIngested => + // We should have data for this round but no root hash exists: + // a peer likely does, so delegate. + RoundIngestionStatus.CannotProvide + + case Some(_) => + // Meta row present but round is beyond our ingested boundary — + // ingestion is still catching up; retry. + RoundIngestionStatus.Undetermined + + case None if !isFirstSv => + // Late-joining Scan with no ingestion boundary of its own — + // it might seem Undetermined is right, but peers do have one, + // so we delegate. + RoundIngestionStatus.CannotProvide + + case None => + // firstSV during initial ingestion (brief startup window before + // the meta row is inserted) — retry. + RoundIngestionStatus.Undetermined + } + /** Find the latest round with complete app activity. * A round is complete once the verdict ingestion has moved passed its archival. * Returns None if no meta row exists or archival of a round has not happened yet. @@ -293,20 +320,23 @@ class DbAppActivityRecordStore( (sql""" insert into #${Tables.appActivityRecords}( history_id, verdict_row_id, round_number, app_provider_parties, app_activity_weights - ) values """ ++ values).asUpdate + ) values """ ++ values ++ sql" ON CONFLICT DO NOTHING").asUpdate } } /** Insert activity records and ensure the meta row exists. * Creates the meta row when enough information is available to * determine which rounds have complete activity, even when no - * activity records exist (e.g., no featured app providers). + * activity records exist (e.g., no featured app providers), + * but only if traffic-summaries could be obtained for this batch. * On a fresh firstSV with no archived rounds, bootstraps round 0 * as complete. */ def insertAppActivityRecordsDBIO( items: Seq[AppActivityRecordT], firstRecordTimeMicros: Long, + hasTrafficSummaries: Boolean, + firstActiveRoundO: Option[Long] = None, lastArchivedRoundO: Option[Long] = None, )(implicit tc: TraceContext): DBIO[Unit] = { val insertRecords = @@ -316,15 +346,9 @@ class DbAppActivityRecordStore( logger.info(s"Inserted ${items.size} app activity records.") } - // earliestRound: the lowest round covered by this ingestion batch. - // - From activity records when present - // - From lastArchivedRound when no featured apps produced records - // - From bootstrap (-1) on a fresh firstSV with no archived rounds - val earliestRound = items - .map(_.roundNumber) - .minOption - .orElse(lastArchivedRoundO) - .orElse(if (isFirstSv) Some(-1L) else None) + // earliestRound: the oldest round open at the earliest record_time of this batch. + // or (-1) on firstSV, as it is expected to have complete data for the first round. + val earliestRound = if (isFirstSv) Some(-1L) else firstActiveRoundO // lastArchived: the highest round archived as of this verdict batch. // - From the caller when available @@ -337,10 +361,12 @@ class DbAppActivityRecordStore( for { _ <- insertRecords ensureResult <- earliestRound match { - case Some(earliest) => + case Some(earliest) if hasTrafficSummaries => ensureMetaDBIO((firstRecordTimeMicros, earliest), lastArchived) - case None => - // No archived rounds and not firstSV — skip meta creation. + case _ => + // Either we have no rounds info and this is not firstSV, + // or we have not started obtaining the traffic summaries yet + // — skip meta creation. // A later verdict batch will create it. DBIO.successful(Resume: MetaCheckResult) } @@ -416,6 +442,7 @@ class DbAppActivityRecordStore( earliest_ingested_round, last_archived_round) values ($historyId, $codeVersion, $userVersion, $startedIngestingAt, $earliestIngestedRound, $lastArchivedRound) + ON CONFLICT DO NOTHING """.asUpdate private def updateLastArchivedRoundDBIO(round: Long) = diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanRewardsReferenceStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanRewardsReferenceStore.scala index 09313f5e42..1dd5c85fdd 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanRewardsReferenceStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanRewardsReferenceStore.scala @@ -17,7 +17,12 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.round.OpenMiningRound import org.lfdecentralizedtrust.splice.config.IngestionConfig import org.lfdecentralizedtrust.splice.environment.RetryProvider import org.lfdecentralizedtrust.splice.scan.store.ScanRewardsReferenceStore -import org.lfdecentralizedtrust.splice.store.{Limit, LimitHelpers, TcsStore} +import org.lfdecentralizedtrust.splice.store.{ + Limit, + LimitHelpers, + TcsStore, + TimestampWithMigrationId, +} import org.lfdecentralizedtrust.splice.store.db.{ AcsArchiveConfig, AcsQueries, @@ -103,7 +108,7 @@ class DbScanRewardsReferenceStore( override def lookupActiveOpenMiningRounds( recordTimes: Seq[CantonTimestamp] - )(implicit tc: TraceContext): Future[Map[CantonTimestamp, (Long, CantonTimestamp)]] = { + )(implicit tc: TraceContext): Future[Map[CantonTimestamp, TimestampWithMigrationId]] = { tcsStore.getEarliestArchivedAt().flatMap { case None => Future.successful(Map.empty) @@ -125,7 +130,10 @@ class DbScanRewardsReferenceStore( .flatMap { r => val opensAt = CantonTimestamp.assertFromInstant(r.contract.payload.opensAt) Option.when(opensAt >= ingestionStart) { - recordTime -> (r.contract.payload.round.number.toLong, opensAt) + recordTime -> TimestampWithMigrationId( + opensAt, + r.contract.payload.round.number.toLong, + ) } } }.toMap diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala index 2f69177fe4..871c2633b6 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala @@ -59,9 +59,8 @@ import org.lfdecentralizedtrust.splice.store.{ DbVotesAcsStoreQueryBuilder, DbVotesTxLogStoreQueryBuilder, Limit, - PageLimit, + VoteResultsFilters, ResultsPage, - SortOrder, TxLogStore, UpdateHistory, } @@ -78,7 +77,6 @@ import org.lfdecentralizedtrust.splice.config.IngestionConfig import org.lfdecentralizedtrust.splice.store.UpdateHistoryQueries.UpdateHistoryQueries import org.lfdecentralizedtrust.splice.store.db.AcsQueries.AcsStoreId import org.lfdecentralizedtrust.splice.store.db.TxLogQueries.TxLogStoreId -import slick.jdbc.canton.SQLActionBuilder import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* @@ -390,63 +388,6 @@ class DbScanStore( } yield contractWithStateFromRow(TransferCommandCounter.COMPANION)(row)).value } - override def listTransactions( - pageEndEventId: Option[String], - sortOrder: SortOrder, - limit: PageLimit, - )(implicit - tc: TraceContext - ): Future[Seq[TxLogEntry.TransactionTxLogEntry]] = - waitUntilAcsIngested { - val entryTypeCondition: SQLActionBuilder = inClause( - "entry_type", - List( - EntryType.TransferTxLogEntry, - EntryType.TapTxLogEntry, - EntryType.MintTxLogEntry, - EntryType.AbortTransferInstructionTxLogEntry, - ), - ) - // Literal sort order since Postgres complains when trying to bind it to a parameter - val (compareEntryNumber, orderLimit) = sortOrder match { - case SortOrder.Ascending => - (sql" > ", sql""" order by entry_number asc limit ${sqlLimit(limit)};""") - case SortOrder.Descending => - (sql" < ", sql""" order by entry_number desc limit ${sqlLimit(limit)};""") - } - - // TODO (#960): don't use the event id for pagination, use the entry number - for { - rows <- storage.query( - pageEndEventId.fold( - selectFromTxLogTable( - txLogTableName, - txLogStoreId, - where = entryTypeCondition, - orderLimit = orderLimit, - ) - )(pageEndEventId => - selectFromTxLogTable( - txLogTableName, - txLogStoreId, - where = (entryTypeCondition ++ sql" and entry_number " ++ compareEntryNumber ++ - sql"""( - select entry_number - from scan_txlog_store - where store_id = $txLogStoreId - and event_id = ${lengthLimited(pageEndEventId)} - and """ ++ entryTypeCondition ++ sql""" - )""").toActionBuilder, - orderLimit = orderLimit, - ) - ), - "listTransactions", - ) - entries = rows.map(txLogEntryFromRow[TxLogEntry.TransactionTxLogEntry](txLogConfig)) - } yield entries - - } - override def lookupFeaturedAppRight( providerPartyId: PartyId )(implicit @@ -633,11 +574,7 @@ class DbScanStore( } override def listVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: Limit, after: Option[Long] = None, )(implicit tc: TraceContext): Future[ResultsPage[DsoRules_CloseVoteRequestResult]] = { @@ -648,11 +585,7 @@ class DbScanStore( actionNameColumnName = "vote_action_name", acceptedColumnName = "vote_accepted", requesterNameColumnName = "vote_requester_name", - actionName = actionName, - accepted = accepted, - requester = requester, - effectiveFrom = effectiveFrom, - effectiveTo = effectiveTo, + filters = filters, limit = limit, after = after, ) @@ -668,6 +601,23 @@ class DbScanStore( } yield ResultsPage(recentVoteResults, afterToken) } + override def countVoteRequestResults( + filters: VoteResultsFilters + )(implicit tc: TraceContext): Future[Long] = { + val query = countVoteRequestResultsQuery( + txLogTableName = ScanTables.txLogTableName, + txLogStoreId = txLogStoreId, + dbType = EntryType.VoteRequestTxLogEntry, + actionNameColumnName = "vote_action_name", + acceptedColumnName = "vote_accepted", + requesterNameColumnName = "vote_requester_name", + filters = filters, + ) + storage + .query(query, "countVoteRequestResults") + .map(_.headOption.getOrElse(0L)) + } + override def lookupLatestSvRewardWeightChange( svParty: PartyId, effectiveBefore: Option[String], diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanVerdictStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanVerdictStore.scala index 3e5cdf0646..bd565c986e 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanVerdictStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanVerdictStore.scala @@ -25,7 +25,7 @@ import slick.dbio.DBIO import java.util.concurrent.atomic.AtomicReference import scala.concurrent.{ExecutionContext, Future} import cats.data.NonEmptyList -import org.lfdecentralizedtrust.splice.store.UpdateHistory +import org.lfdecentralizedtrust.splice.store.{TimestampWithMigrationId, UpdateHistory} import org.lfdecentralizedtrust.splice.scan.store.db.DbAppActivityRecordStore.AppActivityRecordT object DbScanVerdictStore { @@ -237,13 +237,13 @@ object DbScanVerdictStore { def apply( storage: com.digitalasset.canton.resource.DbStorage, updateHistory: UpdateHistory, - appActivityRecordStoreO: Option[DbAppActivityRecordStore], + appActivityRecordStore: DbAppActivityRecordStore, loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext): DbScanVerdictStore = new DbScanVerdictStore( storage, updateHistory, - appActivityRecordStoreO, + appActivityRecordStore, loggerFactory, ) } @@ -251,7 +251,7 @@ object DbScanVerdictStore { class DbScanVerdictStore( storage: DbStorage, updateHistory: UpdateHistory, - val appActivityRecordStoreO: Option[DbAppActivityRecordStore], + val appActivityRecordStore: DbAppActivityRecordStore, override protected val loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext @@ -479,12 +479,17 @@ class DbScanVerdictStore( * * @param items verdicts with transaction view constructors * @param appActivityRecords activity records with placeholder verdictRowIds + * @param hasTrafficSummaries whether traffic summaries were fetched for this batch + * @param firstActiveRoundO the OpenMiningRound round active at the earliest + * record time of the batch * @param lastArchivedRoundO the highest archived OpenMiningRound round as of the * max record time of the batch */ def insertVerdictsWithAppActivityRecords( items: NonEmptyList[(VerdictT, Long => Seq[TransactionViewT])], appActivityRecords: Seq[(CantonTimestamp, AppActivityRecordT)], + hasTrafficSummaries: Boolean, + firstActiveRoundO: Option[Long] = None, lastArchivedRoundO: Option[Long] = None, )(implicit tc: TraceContext): Future[Unit] = { import profile.api.jdbcActionExtensionMethods @@ -498,6 +503,8 @@ class DbScanVerdictStore( _ <- insertAppActivityRecordsDBIO( resolvedAppActivityRecords, items.head._1.recordTime.toMicros, + hasTrafficSummaries, + firstActiveRoundO, lastArchivedRoundO, ) } yield () @@ -545,23 +552,27 @@ class DbScanVerdictStore( private def insertAppActivityRecordsDBIO( items: Seq[AppActivityRecordT], firstRecordTimeMicros: Long, + hasTrafficSummaries: Boolean, + firstActiveRoundO: Option[Long], lastArchivedRoundO: Option[Long], )(implicit tc: TraceContext): DBIO[Unit] = - appActivityRecordStoreO match { - case None => DBIO.successful(()) - case Some(s) => - s.insertAppActivityRecordsDBIO(items, firstRecordTimeMicros, lastArchivedRoundO) - } + appActivityRecordStore.insertAppActivityRecordsDBIO( + items, + firstRecordTimeMicros, + hasTrafficSummaries, + firstActiveRoundO, + lastArchivedRoundO, + ) private def afterFilters( - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], includeImportUpdates: Boolean, ): NonEmptyList[SQLActionBuilder] = { val gt = if (includeImportUpdates) ">=" else ">" afterO match { case None => NonEmptyList.of(sql"migration_id >= 0 and record_time #$gt ${CantonTimestamp.MinValue}") - case Some((afterMigrationId, afterRecordTime)) => + case Some(TimestampWithMigrationId(afterRecordTime, afterMigrationId)) => NonEmptyList.of( sql"migration_id = ${afterMigrationId} and record_time > ${afterRecordTime} ", sql"migration_id > ${afterMigrationId} and record_time #$gt ${CantonTimestamp.MinValue}", @@ -604,7 +615,7 @@ class DbScanVerdictStore( } def listVerdicts( - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], includeImportUpdates: Boolean, limit: Int, )(implicit tc: TraceContext): Future[Seq[VerdictT]] = { diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala index bba1683131..de109b94bf 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala @@ -15,19 +15,10 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.actionrequir } import org.lfdecentralizedtrust.splice.codegen.java.splice.externalpartyamuletrules.TransferCommand import org.lfdecentralizedtrust.splice.scan.store.{ - AbortTransferInstructionTxLogEntry, - AppRewardTxLogEntry, - BalanceChangeTxLogEntry, ClosedMiningRoundTxLogEntry, ErrorTxLogEntry, - ExtraTrafficPurchaseTxLogEntry, - MintTxLogEntry, OpenMiningRoundTxLogEntry, - SvRewardTxLogEntry, - TapTxLogEntry, - TransferTxLogEntry, TxLogEntry, - ValidatorRewardTxLogEntry, VoteRequestTxLogEntry, TransferCommandTxLogEntry, } @@ -193,50 +184,6 @@ object ScanTables extends AcsTables { round = Some(cmr.round), closedRoundEffectiveAt = cmr.effectiveAt.map(CantonTimestamp.assertFromInstant), ) - case are: AppRewardTxLogEntry => - ScanTxLogRowData( - entry = are, - round = Some(are.round), - rewardAmount = Some(are.amount), - rewardedParty = Some(are.party), - ) - case vre: ValidatorRewardTxLogEntry => - ScanTxLogRowData( - entry = vre, - round = Some(vre.round), - rewardAmount = Some(vre.amount), - rewardedParty = Some(vre.party), - ) - case sre: SvRewardTxLogEntry => - ScanTxLogRowData( - entry = sre, - round = Some(sre.round), - rewardAmount = Some(sre.amount), - rewardedParty = Some(sre.party), - ) - case etp: ExtraTrafficPurchaseTxLogEntry => - ScanTxLogRowData( - entry = etp, - round = Some(etp.round), - extraTrafficValidator = Some(etp.validator), - extraTrafficPurchaseTrafficPurchase = Some(etp.trafficPurchased), - extraTrafficPurchaseCcSpent = Some(etp.ccSpent), - ) - case rar: TransferTxLogEntry => - ScanTxLogRowData( - entry = rar, - round = Some(rar.round), - ) - case entry: TapTxLogEntry => - ScanTxLogRowData( - entry = entry, - round = Some(entry.round), - ) - case entry: MintTxLogEntry => - ScanTxLogRowData( - entry = entry, - round = Some(entry.round), - ) case vr: VoteRequestTxLogEntry => val result = vr.result.getOrElse(throw txMissingField()) val parsedOutcome = VoteRequestOutcome.parse(result.outcome) @@ -268,20 +215,12 @@ object ScanTables extends AcsTables { entry.nonce ), ) - case entry: AbortTransferInstructionTxLogEntry => - ScanTxLogRowData( - entry = entry - ) case _ => throw txEncodingFailed() } } record match { - case _: BalanceChangeTxLogEntry => - // the balance changes are no longer indexed, or written, to the tx log table, - // See https://github.com/canton-network/splice/pull/3734 - None case entry => Some(fromEntry(entry)) } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/util/PeerBftScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/util/PeerBftScanConnection.scala new file mode 100644 index 0000000000..22208a9b08 --- /dev/null +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/util/PeerBftScanConnection.scala @@ -0,0 +1,67 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.scan.util + +import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.time.Clock +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.Mutex +import org.apache.pekko.stream.Materializer +import org.lfdecentralizedtrust.splice.config.{AutomationConfig, UpgradesConfig} +import org.lfdecentralizedtrust.splice.environment.{RetryProvider, SpliceLedgerClient} +import org.lfdecentralizedtrust.splice.http.HttpClient +import org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnection +import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig +import org.lfdecentralizedtrust.splice.scan.store.ScanStore +import org.lfdecentralizedtrust.splice.util.TemplateJsonDecoder + +import scala.concurrent.{ExecutionContextExecutor, Future} + +class PeerBftScanConnection( + store: ScanStore, + svName: String, + ledgerClient: SpliceLedgerClient, + automationConfig: AutomationConfig, + upgradesConfig: UpgradesConfig, + clock: Clock, + retryProvider: RetryProvider, + loggerFactory: NamedLoggerFactory, +)(implicit + ec: ExecutionContextExecutor, + mat: Materializer, + httpClient: HttpClient, + templateDecoder: TemplateJsonDecoder, +) extends AutoCloseable { + + private val mutex = Mutex() + + @SuppressWarnings(Array("org.wartremover.warts.Var")) + @volatile private var connectionVar: Option[Future[BftScanConnection]] = None + + def connection(implicit tc: TraceContext): Future[BftScanConnection] = mutex.exclusive { + connectionVar match { + case Some(conn) => conn + case None => + val conn = BftScanConnection.peerScanConnection( + () => BftScanConnection.Bft.getPeerScansFromStore(store, svName), + ledgerClient, + // When the network is starting up, the pool of SVs is changing fast + // Using a short refresh interval to quickly pick up new SVs + scansRefreshInterval = automationConfig.pollingInterval, + amuletRulesCacheTimeToLive = ScanAppClientConfig.DefaultAmuletRulesCacheTimeToLive, + upgradesConfig, + clock, + retryProvider, + loggerFactory, + ) + connectionVar = Some(conn) + conn + } + } + + override def close(): Unit = mutex.exclusive { + connectionVar.foreach(_.foreach(_.close())) + } + +} diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala index 889fee4c45..1879ed49bb 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala @@ -97,6 +97,7 @@ class BftScanConnectionTest when(m.config).thenReturn( ScanAppClientConfig(NetworkAppClientConfig(scanUrl(n))) ) + when(m.url).thenReturn(Uri(scanUrl(n))) m } connections.foreach { connection => @@ -1043,7 +1044,7 @@ class BftScanConnectionTest connections.tail.foreach(c => when(c.getDsoPartyId()).thenReturn(delayedSuccess)) for { - result <- BftScanConnection.executeCall(call, connections, nTargetSuccess = 1, logger) + (result, _) <- BftScanConnection.executeCall(call, connections, nTargetSuccess = 1, logger) } yield result should be(partyIdA) } @@ -1111,7 +1112,7 @@ class BftScanConnectionTest } for { - result <- BftScanConnection.executeCall( + (result, _) <- BftScanConnection.executeCall( call, connections, nTargetSuccess = 2, @@ -1158,7 +1159,7 @@ class BftScanConnectionTest makeMockFail(connections(2), notFoundFailure) for { - result <- BftScanConnection.executeCall( + (result, _) <- BftScanConnection.executeCall( call, connections, nTargetSuccess = 2, @@ -1194,7 +1195,7 @@ class BftScanConnectionTest } for { - result <- BftScanConnection.executeCall( + (result, _) <- BftScanConnection.executeCall( call, connections, nTargetSuccess = 2, @@ -1245,26 +1246,36 @@ class BftScanConnectionTest // With n=4, we query only two connections randomly, and even with // retries a single call can fail to reach consensus. - def attempt(remaining: Int): Future[GetRewardAccountingRootHashResponse] = - bft.getRewardAccountingRootHash(round).flatMap { - case ok: GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk => - Future.successful(ok) + def attempt(remaining: Int): Future[(GetRewardAccountingRootHashResponse, List[Uri])] = + bft.getRewardAccountingRootHashWithScanUris(round).flatMap { + case (ok: GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk, uris) => + Future.successful((ok, uris)) case _ if remaining > 1 => attempt(remaining - 1) case other => Future.successful(other) } + // A call that reaches consensus here always queries a third scan that + // disagrees (returns IgnoreResponse or fails), which BftScanConnection + // logs at WARN for the reward-read paths. Assert that WARN is produced + // and suppress it so it doesn't fail the `sbt checkErrors` log-scan gate. loggerFactory - .assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.INFO))( + .assertEventuallyLogsSeq(SuppressionRule.Level(Level.WARN))( attempt(100).map { resp => inside(resp) { - case GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk(ok) => + case ( + GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk(ok), + uris, + ) => ok.rootHash should be("aabb") ok.roundNumber should be(round) + uris.size should be(2) } }, logs => - logs.exists(l => - l.level == Level.INFO && l.message.contains("Reached consensus from") + logs.exists(log => + log.level == Level.WARN && log.message.contains( + "disagreed with consensus" + ) ) should be(true), ) .map(_ => succeed) @@ -1366,29 +1377,42 @@ class BftScanConnectionTest // With n=4, we query only two connections randomly, and even with // retries a single call can fail to reach consensus. - def attempt(remaining: Int): Future[GetRewardAccountingActivityTotalsResponse] = - bft.getRewardAccountingActivityTotals(round).flatMap { - case ok: GetRewardAccountingActivityTotalsResponse.members.RewardAccountingActivityTotalsOk => - Future.successful(ok) + def attempt(remaining: Int): Future[(GetRewardAccountingActivityTotalsResponse, List[Uri])] = + bft.getRewardAccountingActivityTotalsWithScanUris(round).flatMap { + case ( + ok: GetRewardAccountingActivityTotalsResponse.members.RewardAccountingActivityTotalsOk, + uris, + ) => + Future.successful((ok, uris)) case _ if remaining > 1 => attempt(remaining - 1) case other => Future.successful(other) } + // A call that reaches consensus here always queries a third scan that + // disagrees (returns IgnoreResponse or fails), which BftScanConnection + // logs at WARN for the reward-read paths. Assert that WARN is produced + // and suppress it so it doesn't fail the `sbt checkErrors` log-scan gate. loggerFactory - .assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.INFO))( + .assertEventuallyLogsSeq(SuppressionRule.Level(Level.WARN))( attempt(100).map { resp => inside(resp) { - case GetRewardAccountingActivityTotalsResponse.members - .RewardAccountingActivityTotalsOk(ok) => + case ( + GetRewardAccountingActivityTotalsResponse.members + .RewardAccountingActivityTotalsOk(ok), + uris, + ) => ok.roundNumber should be(round) ok.totalAppActivityWeight should be(100L) ok.activePartiesCount should be(10L) ok.activityRecordsCount should be(5L) + uris.size should be(2) } }, logs => - logs.exists(l => - l.level == Level.INFO && l.message.contains("Reached consensus from") + logs.exists(log => + log.level == Level.WARN && log.message.contains( + "disagreed with consensus" + ) ) should be(true), ) .map(_ => succeed) diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnectionTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnectionTest.scala new file mode 100644 index 0000000000..9852669a1c --- /dev/null +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnectionTest.scala @@ -0,0 +1,62 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.scan.admin.api.client + +import com.digitalasset.canton.BaseTest +import io.circe.Json +import org.apache.pekko.http.scaladsl.model.{HttpRequest, StatusCodes} +import org.apache.pekko.stream.StreamTcpException +import org.lfdecentralizedtrust.splice.admin.api.client.commands.HttpCommandException +import org.lfdecentralizedtrust.splice.environment.BaseAppConnection +import org.scalatest.wordspec.AnyWordSpec + +class SingleScanConnectionTest extends AnyWordSpec with BaseTest { + + "SingleScanConnection.httpStatusLabel" should { + + "extract the status code of an unexpected JSON response" in { + SingleScanConnection.httpStatusLabel( + new BaseAppConnection.UnexpectedHttpJsonResponse(StatusCodes.NotFound, Json.obj()) + ) should be("404") + } + + "extract the status code of an unexpected malformed JSON response" in { + SingleScanConnection.httpStatusLabel( + new BaseAppConnection.UnexpectedHttpMalformedJsonResponse( + StatusCodes.BadGateway, + "not json", + ) + ) should be("502") + } + + "extract the status code of an unexpected text response" in { + SingleScanConnection.httpStatusLabel( + new BaseAppConnection.UnexpectedHttpTextResponse(StatusCodes.ServiceUnavailable, "nope") + ) should be("503") + } + + "extract the status code of an unexpected non-JSON response" in { + SingleScanConnection.httpStatusLabel( + new BaseAppConnection.UnexpectedHttpNonJsonResponse(StatusCodes.InternalServerError) + ) should be("500") + } + + "extract the status code of an HttpCommandException" in { + SingleScanConnection.httpStatusLabel( + HttpCommandException( + HttpRequest(), + StatusCodes.TooManyRequests, + HttpCommandException.RawResponse("slow down"), + ) + ) should be("429") + } + + "report 'none' for failures without an HTTP status code" in { + SingleScanConnection.httpStatusLabel( + new StreamTcpException("connection refused") + ) should be("none") + SingleScanConnection.httpStatusLabel(new RuntimeException("boom")) should be("none") + } + } +} diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/config/TokenStandardConfigTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/config/TokenStandardConfigTest.scala index 4c51e7328a..8d2da10a11 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/config/TokenStandardConfigTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/config/TokenStandardConfigTest.scala @@ -37,6 +37,27 @@ class TokenStandardConfigTest extends AnyWordSpec with BaseTest { config.validateSettleBatch(settleBatch) } + "reject settle batches with too many allocations" in { + val config = + TokenStandardConfig.SettlementConfig(maxLegs = 10, maxParties = 10, maxAllocations = 1) + + val ex = the[StatusRuntimeException] thrownBy { + config.validateSettleBatch( + mkSettleBatch( + transferLegs = Seq.empty, + allocations = Seq( + mkFinalizedAllocation("alloc-1"), + mkFinalizedAllocation("alloc-2"), + ), + ) + ) + } + + ex.getStatus.getCode shouldBe Status.Code.INVALID_ARGUMENT + ex.getStatus.getDescription shouldBe + "Too many allocations in the settle batch: 2. Maximum allowed: 1" + } + "reject settle batches with too many transfer legs" in { val config = TokenStandardConfig.SettlementConfig(maxLegs = 1, maxParties = 10) @@ -112,12 +133,13 @@ class TokenStandardConfigTest extends AnyWordSpec with BaseTest { } private def mkSettleBatch( - transferLegs: Seq[allocationv2.TransferLeg] + transferLegs: Seq[allocationv2.TransferLeg], + allocations: Seq[allocationv2.FinalizedAllocation] = Seq.empty, ): allocationv2.SettlementFactory_SettleBatch = new allocationv2.SettlementFactory_SettleBatch( mkSettlementInfo(), transferLegs.asJava, - java.util.List.of(), + allocations.asJava, java.util.List.of("venue"), emptyExtraArgs, ) @@ -154,6 +176,13 @@ class TokenStandardConfigTest extends AnyWordSpec with BaseTest { ) } + private def mkFinalizedAllocation(cid: String): allocationv2.FinalizedAllocation = + new allocationv2.FinalizedAllocation( + new allocationv2.Allocation.ContractId(cid), + java.util.List.of(), + java.util.Optional.empty(), + ) + private def mkTransferLeg( transferLegId: String, sender: String, diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/rewards/AppActivityComputationTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/rewards/AppActivityComputationTest.scala index ef1bba310c..9f8c350492 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/rewards/AppActivityComputationTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/rewards/AppActivityComputationTest.scala @@ -11,6 +11,7 @@ import com.google.protobuf.timestamp.Timestamp as ProtoTimestamp import com.google.protobuf.ByteString import org.lfdecentralizedtrust.splice.scan.store.ScanRewardsReferenceStore import org.lfdecentralizedtrust.splice.scan.store.db.{DbAppActivityRecordStore, DbScanVerdictStore} +import org.lfdecentralizedtrust.splice.store.TimestampWithMigrationId import org.scalatest.wordspec.AnyWordSpec import scala.concurrent.Future @@ -219,7 +220,7 @@ class AppActivityComputationTest extends AnyWordSpec with BaseTest { val store = mock[ScanRewardsReferenceStore] when(store.lookupActiveOpenMiningRounds(any[Seq[CantonTimestamp]])(any[TraceContext])) .thenAnswer { (times: Seq[CantonTimestamp]) => - Future.successful(times.map(_ -> (0L, roundOpensAt)).toMap) + Future.successful(times.map(_ -> TimestampWithMigrationId(roundOpensAt, 0L)).toMap) } when(store.lookupFeaturedAppPartiesAsOf(any[CantonTimestamp])(any[TraceContext])) .thenReturn(Future.successful(featuredWeights)) diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbAppActivityRecordStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbAppActivityRecordStoreTest.scala index b4b57c17c0..30a9867878 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbAppActivityRecordStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbAppActivityRecordStoreTest.scala @@ -7,6 +7,7 @@ import com.digitalasset.canton.topology.SynchronizerId import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import org.lfdecentralizedtrust.splice.scan.store.AppActivityStore.RoundIngestionStatus import org.lfdecentralizedtrust.splice.scan.store.db.DbAppActivityRecordStore import org.lfdecentralizedtrust.splice.scan.store.db.DbAppActivityRecordStore.* import org.lfdecentralizedtrust.splice.scan.store.db.DbScanVerdictStore @@ -169,6 +170,8 @@ class DbAppActivityRecordStoreTest _ <- verdictStore.insertVerdictsWithAppActivityRecords( NonEmptyList.of(verdict1 -> noViews, verdict2 -> noViews), appActivityRecords, + hasTrafficSummaries = true, + firstActiveRoundO = Some(10L), lastArchivedRoundO = Some(9L), ) @@ -209,6 +212,8 @@ class DbAppActivityRecordStoreTest _ <- verdictStore.insertVerdictsWithAppActivityRecords( NonEmptyList.of(mkVerdict(verdictStore, "update-mono-1", baseTs) -> noViews), Seq(baseTs -> mkRecord(0L, 10L, Seq("app1::provider"), Seq(100L))), + hasTrafficSummaries = true, + firstActiveRoundO = Some(10L), lastArchivedRoundO = Some(9L), ) // A later batch without activity records still advances the round @@ -217,10 +222,13 @@ class DbAppActivityRecordStoreTest mkVerdict(verdictStore, "update-mono-2", baseTs.plusSeconds(1L)) -> noViews ), Seq.empty, + hasTrafficSummaries = true, + firstActiveRoundO = Some(11L), lastArchivedRoundO = Some(10L), ) meta <- appStore.lookupActivityRecordMeta(1, 0) } yield { + meta.value.earliestIngestedRound shouldBe 10L meta.value.lastArchivedRound shouldBe Some(10L) } } @@ -233,40 +241,96 @@ class DbAppActivityRecordStoreTest _ <- verdictStore.insertVerdictsWithAppActivityRecords( NonEmptyList.of(mkVerdict(verdictStore, "update-no-meta", baseTs) -> noViews), Seq.empty, - lastArchivedRoundO = Some(7L), + hasTrafficSummaries = true, + firstActiveRoundO = Some(7L), + lastArchivedRoundO = None, ) meta <- appStore.lookupActivityRecordMeta(1, 0) } yield { - // Meta row is created with earliestRound = lastArchivedRound - // because verdict ingestion is active even without activity records + // Meta row is created using the firstActiveRoundO + // even though there are no activity records meta shouldBe defined meta.value.earliestIngestedRound shouldBe 7L - meta.value.lastArchivedRound shouldBe Some(7L) + meta.value.lastArchivedRound shouldBe None } } - "insert verdicts without activity records when appActivityRecords is empty" in { + "Does not create meta row when traffic summaries are absent" in { + for { + (appStore, verdictStore) <- newStores() + baseTs = CantonTimestamp.now() + + _ <- verdictStore.insertVerdictsWithAppActivityRecords( + NonEmptyList.of(mkVerdict(verdictStore, "update-no-meta", baseTs) -> noViews), + Seq.empty, + hasTrafficSummaries = false, + lastArchivedRoundO = Some(7L), + ) + v <- verdictStore.getVerdictByUpdateId("update-no-meta") + countAfter <- countRecords() + meta <- appStore.lookupActivityRecordMeta(1, 0) + } yield { + v shouldBe defined + countAfter shouldBe 0L + meta shouldBe None + } + } + + "on a fresh firstSV, does not create meta row when traffic summaries are absent" in { + for { + (appStore, verdictStore) <- newStores(isFirstSv = true) + baseTs = CantonTimestamp.now() + + _ <- verdictStore.insertVerdictsWithAppActivityRecords( + NonEmptyList.of(mkVerdict(verdictStore, "update-firstsv-2", baseTs) -> noViews), + Seq.empty, + hasTrafficSummaries = false, + ) + // Even on firstSV, missing traffic summaries defer meta creation + // to a later batch. + metaBefore <- appStore.lookupActivityRecordMeta(1, 0) + + // A later batch with traffic summaries creates the meta row. + _ <- verdictStore.insertVerdictsWithAppActivityRecords( + NonEmptyList.of( + mkVerdict(verdictStore, "update-firstsv-3", baseTs.plusSeconds(1L)) -> noViews + ), + Seq.empty, + hasTrafficSummaries = true, + ) + metaAfter <- appStore.lookupActivityRecordMeta(1, 0) + } yield { + metaBefore shouldBe None + + metaAfter shouldBe defined + metaAfter.value.earliestIngestedRound shouldBe -1L + metaAfter.value.lastArchivedRound shouldBe Some(0L) + } + } + + "insert verdicts without activity records, when reward reference store does not have data asOf" in { for { (appStore, verdictStore) <- newStores() baseTs = CantonTimestamp.now() verdict = mkVerdict(verdictStore, "update-no-activity", baseTs) + // firstActiveRoundO is None, as reward reference store began ingestion after baseTx _ <- verdictStore.insertVerdictsWithAppActivityRecords( NonEmptyList.of(verdict -> noViews), Seq.empty, + hasTrafficSummaries = true, + firstActiveRoundO = None, + lastArchivedRoundO = None, ) v <- verdictStore.getVerdictByUpdateId("update-no-activity") countAfter <- countRecords() - // No meta row should be created when there are no activity records meta <- appStore.lookupActivityRecordMeta(1, 0) } yield { v shouldBe defined countAfter shouldBe 0L - // Non-firstSV with no lastArchivedRound: meta row is not created - // because it would have last_archived_round = NULL, making no - // rounds complete. + // Non-firstSV with no firstActiveRoundO: meta row is not created meta shouldBe None } } @@ -289,6 +353,7 @@ class DbAppActivityRecordStoreTest _ <- verdictStore.insertVerdictsWithAppActivityRecords( NonEmptyList.of(verdict1 -> noViews, verdict2 -> noViews, verdict3 -> noViews), appActivityRecords, + hasTrafficSummaries = true, ) v1 <- verdictStore.getVerdictByUpdateId("update-with-1") @@ -335,6 +400,7 @@ class DbAppActivityRecordStoreTest _ <- verdictStore.insertVerdictsWithAppActivityRecords( NonEmptyList.of(verdict -> noViews), appActivityRecords, + hasTrafficSummaries = true, ) v <- verdictStore.getVerdictByUpdateId("update-mismatch") @@ -592,6 +658,51 @@ class DbAppActivityRecordStoreTest } } + "ingestionStatusForRound" should { + + "return CannotProvide when meta row absent and isFirstSv=false" in { + for { + (store, _) <- newStore(isFirstSv = false) + result <- store.ingestionStatusForRound(5L) + } yield { + result shouldBe RoundIngestionStatus.CannotProvide + } + } + + "return Undetermined when meta row absent and isFirstSv=true" in { + for { + (store, _) <- newStore(isFirstSv = true) + result <- store.ingestionStatusForRound(5L) + } yield { + result shouldBe RoundIngestionStatus.Undetermined + } + } + + "return CannotProvide when meta row present and roundNumber <= earliestIngested" in { + for { + (store, _) <- newStore() + baseTs = CantonTimestamp.now() + _ <- store.insertActivityRecordMetaForTesting(1, 0, baseTs.toMicros, 10L, Some(11L)) + atBoundary <- store.ingestionStatusForRound(10L) + below <- store.ingestionStatusForRound(5L) + } yield { + atBoundary shouldBe RoundIngestionStatus.CannotProvide + below shouldBe RoundIngestionStatus.CannotProvide + } + } + + "return Undetermined when meta row present and roundNumber > earliestIngested" in { + for { + (store, _) <- newStore() + baseTs = CantonTimestamp.now() + _ <- store.insertActivityRecordMetaForTesting(1, 0, baseTs.toMicros, 10L, Some(11L)) + result <- store.ingestionStatusForRound(15L) + } yield { + result shouldBe RoundIngestionStatus.Undetermined + } + } + } + "lookupActivityRecordMeta" should { "return None when no meta row exists" in { @@ -1025,7 +1136,7 @@ class DbAppActivityRecordStoreTest val n = storeCounter.getAndIncrement() val participantId = mkParticipantId(s"activity-test-$n") val updateHistory = new UpdateHistory( - storage.underlying, + storage, migrationId, s"app_activity_test_$n", participantId, @@ -1038,7 +1149,7 @@ class DbAppActivityRecordStoreTest ) updateHistory.ingestionSink.initialize().map { _ => val store = new DbAppActivityRecordStore( - storage.underlying, + storage, updateHistory, versions, isFirstSv, @@ -1051,10 +1162,12 @@ class DbAppActivityRecordStoreTest /** Creates both an app activity record store and a verdict store backed by * the same UpdateHistory, for testing insertVerdictsWithAppActivityRecords. */ - private def newStores(): Future[(DbAppActivityRecordStore, DbScanVerdictStore)] = { + private def newStores( + isFirstSv: Boolean = false + ): Future[(DbAppActivityRecordStore, DbScanVerdictStore)] = { val participantId = mkParticipantId("activity-test") val updateHistory = new UpdateHistory( - storage.underlying, + storage, migrationId, "app_activity_combined_test", participantId, @@ -1070,13 +1183,13 @@ class DbAppActivityRecordStoreTest storage.underlying, updateHistory, DbAppActivityRecordStore.IngestionVersions(1, 0), - isFirstSv = false, + isFirstSv, loggerFactory, ) val verdictStore = new DbScanVerdictStore( storage.underlying, updateHistory, - Some(appStore), + appStore, loggerFactory, ) (appStore, verdictStore) diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbScanAppRewardsStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbScanAppRewardsStoreTest.scala index bdfd83565e..eabc35301b 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbScanAppRewardsStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbScanAppRewardsStoreTest.scala @@ -36,11 +36,8 @@ class DbScanAppRewardsStoreTest private val migrationId = 0L - "DbScanAppRewardsStore" should { - - // -- Test 1: Insert and read back a single row per table ---------------- - - "insert and read back app_activity_party_totals" in { + "insert and read back" should { + "app_activity_party_totals" in { for { (store, historyId) <- newStore() row = AppActivityPartyTotalT( @@ -58,7 +55,7 @@ class DbScanAppRewardsStoreTest } } - "insert and read back app_activity_round_totals" in { + "app_activity_round_totals" in { for { (store, historyId) <- newStore() row = AppActivityRoundTotalT( @@ -75,7 +72,7 @@ class DbScanAppRewardsStoreTest } } - "insert and read back app_reward_party_totals" in { + "app_reward_party_totals" in { for { (store, historyId) <- newStore() activityRow = AppActivityPartyTotalT( @@ -147,7 +144,7 @@ class DbScanAppRewardsStoreTest } } - "insert and read back app_reward_round_totals" in { + "app_reward_round_totals" in { for { (store, historyId) <- newStore() row = AppRewardRoundTotalT( @@ -165,7 +162,7 @@ class DbScanAppRewardsStoreTest } } - "insert and read back app_reward_batch_hashes" in { + "app_reward_batch_hashes" in { for { (store, historyId) <- newStore() hash = RewardHash( @@ -187,7 +184,7 @@ class DbScanAppRewardsStoreTest } } - "insert and read back app_reward_root_hashes" in { + "app_reward_root_hashes" in { for { (store, historyId) <- newStore() hash = RewardHash(Array[Byte](0xca.toByte, 0xfe.toByte, 0xba.toByte, 0xbe.toByte)) @@ -202,10 +199,10 @@ class DbScanAppRewardsStoreTest loaded.value shouldBe row } } + } - // -- Test 2: Batch inserts ---------------------------------------------- - - "batch insert multiple app_activity_party_totals and spot-check" in { + "batch insert" should { + "batch multiple app_activity_party_totals and spot-check" in { for { (store, historyId) <- newStore() rows = (0 until 10).map { i => @@ -227,7 +224,7 @@ class DbScanAppRewardsStoreTest } } - "batch insert multiple app_reward_batch_hashes and spot-check" in { + "batch multiple app_reward_batch_hashes and spot-check" in { for { (store, historyId) <- newStore() rows = (0 until 5).map { i => @@ -249,10 +246,10 @@ class DbScanAppRewardsStoreTest loaded.last shouldBe rows(4) } } + } - // -- Test 5: Duplicate key handling (reject) ---------------------------- - - "reject duplicate app_activity_party_totals on PK conflict" in { + "duplicate key handling" should { + "duplicate app_activity_party_totals on PK conflict" in { for { (store, historyId) <- newStore() row = AppActivityPartyTotalT( @@ -270,7 +267,7 @@ class DbScanAppRewardsStoreTest } } - "reject duplicate app_activity_round_totals on PK conflict" in { + "duplicate app_activity_round_totals on PK conflict" in { for { (store, historyId) <- newStore() row = AppActivityRoundTotalT( @@ -288,7 +285,7 @@ class DbScanAppRewardsStoreTest } } - "reject duplicate app_reward_root_hashes on PK conflict" in { + "duplicate app_reward_root_hashes on PK conflict" in { for { (store, historyId) <- newStore() row = AppRewardRootHashT( @@ -307,10 +304,11 @@ class DbScanAppRewardsStoreTest result.getMessage should (include("unique constraint") or include("duplicate key")) } } + } - // -- Aggregation tests --------------------------------------------------- + "aggregateActivityTotals" should { - "aggregateActivityTotals — single round, single party" in { + "single round, single party" in { for { (store, historyId) <- newStore() _ <- markRoundComplete(historyId, roundNumber) @@ -330,7 +328,7 @@ class DbScanAppRewardsStoreTest } } - "aggregateActivityTotals — multiple parties with correct GROUP BY and seq_nums" in { + "multiple parties with correct GROUP BY and seq_nums" in { for { (store, historyId) <- newStore() _ <- markRoundComplete(historyId, roundNumber) @@ -371,7 +369,7 @@ class DbScanAppRewardsStoreTest } } - "aggregateActivityTotals — empty round produces zero totals" in { + "empty round produces zero totals" in { for { (store, historyId) <- newStore() _ <- markRoundComplete(historyId, roundNumber) @@ -387,7 +385,7 @@ class DbScanAppRewardsStoreTest } } - "aggregateActivityTotals — only aggregates records from own history_id" in { + "only aggregates records from own history_id" in { for { (store1, historyId1) <- newStore() (_, historyId2) <- newStore() @@ -409,7 +407,7 @@ class DbScanAppRewardsStoreTest } } - "aggregateActivityTotals — re-run for same round raises error" in { + "re-run for same round raises error" in { for { (store, historyId) <- newStore() _ <- markRoundComplete(historyId, roundNumber) @@ -421,7 +419,7 @@ class DbScanAppRewardsStoreTest } } - "aggregateActivityTotals — rejects the first ingested round (possibly partial)" in { + "rejects the first ingested round (possibly partial)" in { for { (store, historyId) <- newStore() // roundNumber is the first ingested round, which may be partial @@ -438,7 +436,7 @@ class DbScanAppRewardsStoreTest } } - "aggregateActivityTotals — rejects round whose OpenMiningRound is not yet archived" in { + "rejects round whose OpenMiningRound is not yet archived" in { for { (store, historyId) <- newStore() _ <- insertActivityMeta( @@ -454,7 +452,7 @@ class DbScanAppRewardsStoreTest } } - "aggregateActivityTotals — succeeds for round 0 when marked complete" in { + "succeeds for round 0 when marked complete" in { for { (store, historyId) <- newStore() _ <- markRoundComplete(historyId, 0L) @@ -468,7 +466,7 @@ class DbScanAppRewardsStoreTest } } - "aggregateActivityTotals — rejects round 0 when not marked complete" in { + "rejects round 0 when not marked complete" in { for { (store, historyId) <- newStore() _ <- insertActivityRecord(historyId, 0L, Seq("alice::provider"), Seq(500L)) @@ -478,212 +476,267 @@ class DbScanAppRewardsStoreTest result.getMessage should include("Incomplete app activity for round 0") } } + } - "roundsWithComputedRewards" should { + "roundsWithComputedRewards" should { - "returns empty set for empty input" in { - for { - (store, _) <- newStore() - result <- store.roundsWithComputedRewards(Seq.empty) - } yield { - result shouldBe Set.empty - } + "returns empty set for empty input" in { + for { + (store, _) <- newStore() + result <- store.roundsWithComputedRewards(Seq.empty) + } yield { + result shouldBe Set.empty } + } - "returns correct subset" in { - for { - (store, historyId) <- newStore() - _ <- store.insertAppRewardRootHashes( - Seq( - AppRewardRootHashT(historyId, 10L, RewardHash(Array[Byte](1, 2, 3, 4))), - AppRewardRootHashT(historyId, 20L, RewardHash(Array[Byte](5, 6, 7, 8))), - AppRewardRootHashT(historyId, 30L, RewardHash(Array[Byte](9, 10, 11, 12))), - ) + "returns correct subset" in { + for { + (store, historyId) <- newStore() + _ <- store.insertAppRewardRootHashes( + Seq( + AppRewardRootHashT(historyId, 10L, RewardHash(Array[Byte](1, 2, 3, 4))), + AppRewardRootHashT(historyId, 20L, RewardHash(Array[Byte](5, 6, 7, 8))), + AppRewardRootHashT(historyId, 30L, RewardHash(Array[Byte](9, 10, 11, 12))), ) - result <- store.roundsWithComputedRewards(Seq(10L, 15L, 20L, 25L)) - } yield { - result shouldBe Set(10L, 20L) - } + ) + result <- store.roundsWithComputedRewards(Seq(10L, 15L, 20L, 25L)) + } yield { + result shouldBe Set(10L, 20L) } + } - "returns empty set when no matches" in { - for { - (store, historyId) <- newStore() - _ <- store.insertAppRewardRootHashes( - Seq( - AppRewardRootHashT(historyId, 10L, RewardHash(Array[Byte](1, 2, 3, 4))) - ) + "returns empty set when no matches" in { + for { + (store, historyId) <- newStore() + _ <- store.insertAppRewardRootHashes( + Seq( + AppRewardRootHashT(historyId, 10L, RewardHash(Array[Byte](1, 2, 3, 4))) ) - result <- store.roundsWithComputedRewards(Seq(20L, 30L)) - } yield { - result shouldBe Set.empty - } + ) + result <- store.roundsWithComputedRewards(Seq(20L, 30L)) + } yield { + result shouldBe Set.empty } + } + } + + "computeAndStoreRewards" should { + "returns correct summary counts" in { + for { + (store, historyId) <- newStore() + _ <- markRoundComplete(historyId, roundNumber) + // 3 activity records, 2 parties (alice in 2 records, bob in 2) + _ <- insertActivityRecord( + historyId, + roundNumber, + Seq("alice::provider", "bob::provider"), + Seq(3000000L, 2000000L), + ) + _ <- insertActivityRecord( + historyId, + roundNumber, + Seq("alice::provider"), + Seq(1000000L), + ) + _ <- insertActivityRecord( + historyId, + roundNumber, + Seq("bob::provider"), + Seq(500000L), + ) + summary <- store.computeAndStoreRewards( + roundNumber, + batchSize = 100, + testInputs, + ) + } yield { + summary.activePartiesCount shouldBe 2L + summary.activityRecordsCount shouldBe 4L // sum of per-party counts: alice=2 + bob=2 + summary.rewardedPartiesCount shouldBe 2L + summary.batchesCreatedCount should be >= 1L + } } - "computeAndStoreRewards" should { + "non-zero threshold excludes low-activity parties from rewards" in { + for { + (store, historyId) <- newStore() + _ <- markRoundComplete(historyId, roundNumber) + // alice has high activity, bob has low activity + _ <- insertActivityRecord( + historyId, + roundNumber, + Seq("alice::provider", "bob::provider"), + Seq(5000000L, 50000L), + ) + // totalIssuanceForFeaturedAppRewards is 0.45, and alice gets almost all of it. Only alice will therefore be above the threshold of 0.4 + nonZeroThresholdInputs = testInputs.copy( + appRewardCouponThreshold = RewardComputationInputs.fromBigDecimal(BigDecimal("0.4")) + ) + summary <- store.computeAndStoreRewards( + roundNumber, + batchSize = 100, + nonZeroThresholdInputs, + ) + rewardPartyTotals <- store.getAppRewardPartyTotalsByRound(roundNumber) + } yield { + summary.activePartiesCount shouldBe 2L + summary.rewardedPartiesCount shouldBe 1L // only alice above threshold + rewardPartyTotals should have size 1 + rewardPartyTotals.head.appProviderParty shouldBe "alice::provider" + } + } - "returns correct summary counts" in { - for { - (store, historyId) <- newStore() - _ <- markRoundComplete(historyId, roundNumber) - // 3 activity records, 2 parties (alice in 2 records, bob in 2) - _ <- insertActivityRecord( - historyId, - roundNumber, - Seq("alice::provider", "bob::provider"), - Seq(3000000L, 2000000L), - ) - _ <- insertActivityRecord( - historyId, - roundNumber, - Seq("alice::provider"), - Seq(1000000L), - ) - _ <- insertActivityRecord( - historyId, - roundNumber, - Seq("bob::provider"), - Seq(500000L), - ) - summary <- store.computeAndStoreRewards( - roundNumber, - batchSize = 100, - testInputs, - ) - } yield { - summary.activePartiesCount shouldBe 2L - summary.activityRecordsCount shouldBe 4L // sum of per-party counts: alice=2 + bob=2 - summary.rewardedPartiesCount shouldBe 2L - summary.batchesCreatedCount should be >= 1L - } + "empty round returns zero counts" in { + for { + (store, historyId) <- newStore() + _ <- markRoundComplete(historyId, roundNumber) + summary <- store.computeAndStoreRewards( + roundNumber, + batchSize = 100, + testInputs, + ) + } yield { + summary.activePartiesCount shouldBe 0L + summary.activityRecordsCount shouldBe 0L + summary.rewardedPartiesCount shouldBe 0L + summary.batchesCreatedCount shouldBe 1L // empty root batch } + } - "non-zero threshold excludes low-activity parties from rewards" in { - for { - (store, historyId) <- newStore() - _ <- markRoundComplete(historyId, roundNumber) - // alice has high activity, bob has low activity - _ <- insertActivityRecord( - historyId, - roundNumber, - Seq("alice::provider", "bob::provider"), - Seq(5000000L, 50000L), - ) - // totalIssuanceForFeaturedAppRewards is 0.45, and alice gets almost all of it. Only alice will therefore be above the threshold of 0.4 - nonZeroThresholdInputs = testInputs.copy( - appRewardCouponThreshold = RewardComputationInputs.fromBigDecimal(BigDecimal("0.4")) - ) - summary <- store.computeAndStoreRewards( - roundNumber, - batchSize = 100, - nonZeroThresholdInputs, - ) - rewardPartyTotals <- store.getAppRewardPartyTotalsByRound(roundNumber) - } yield { - summary.activePartiesCount shouldBe 2L - summary.rewardedPartiesCount shouldBe 1L // only alice above threshold - rewardPartyTotals should have size 1 - rewardPartyTotals.head.appProviderParty shouldBe "alice::provider" - } + "rejects incomplete activity" in { + for { + (store, historyId) <- newStore() + // Activity in roundNumber but no meta row marking the round complete + _ <- insertActivityRecord(historyId, roundNumber, Seq("alice::provider"), Seq(500L)) + result <- store + .computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) + .failed + } yield { + result.getMessage should include("Incomplete app activity") } + } - "empty round returns zero counts" in { - for { - (store, historyId) <- newStore() - _ <- markRoundComplete(historyId, roundNumber) - summary <- store.computeAndStoreRewards( - roundNumber, - batchSize = 100, - testInputs, - ) - } yield { - summary.activePartiesCount shouldBe 0L - summary.activityRecordsCount shouldBe 0L - summary.rewardedPartiesCount shouldBe 0L - summary.batchesCreatedCount shouldBe 1L // empty root batch - } + "produces root hash for complete round" in { + for { + (store, historyId) <- newStore() + _ <- markRoundComplete(historyId, roundNumber) + _ <- insertActivityRecord(historyId, roundNumber, Seq("alice::provider"), Seq(5000000L)) + _ <- store.computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) + rootHash <- store.getAppRewardRootHashByRound(roundNumber) + activityTotals <- store.getAppActivityRoundTotalByRound(roundNumber) + } yield { + // Pipeline completed: activity aggregated and root hash produced + rootHash shouldBe defined + rootHash.value.rootHash.size shouldBe 32 + activityTotals shouldBe defined + activityTotals.value.totalRoundAppActivityWeight shouldBe 5000000L } + } + "re-run for same round raises error" in { + for { + (store, historyId) <- newStore() + _ <- markRoundComplete(historyId, roundNumber) + _ <- insertActivityRecord(historyId, roundNumber, Seq("alice::provider"), Seq(5000000L)) + _ <- store.computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) + result <- store + .computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) + .failed + } yield { + result shouldBe a[Exception] + } } - // -- assertMintingAllowanceWithinMintingCurve tests -------------------------------- - // Tested directly with fake round totals because normal computation - // cannot trigger the assertion — the tranche formula guarantees - // totalReward <= totalIssuance. This check is a safety net for bugs. + "rolls back when reward exceeds issuance" in { + for { + // Use a negative tolerance so any positive reward triggers the assertion + (store, historyId) <- newStore(rewardMintingAllowanceTolerance = BigDecimal(-1.0)) + _ <- markRoundComplete(historyId, roundNumber) + _ <- insertActivityRecord(historyId, roundNumber, Seq("alice::provider"), Seq(5000000L)) + result <- store + .computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) + .failed + } yield { + result.getMessage should include("exceeds minting curve allowance") + } + } + } - "assertMintingAllowanceWithinMintingCurve" should { + // Tested directly with fake round totals because normal computation + // cannot trigger the assertion — the tranche formula guarantees + // totalReward <= totalIssuance. This check is a safety net for bugs. + "assertMintingAllowanceWithinMintingCurve" should { + + def mkParams(totalIssuance: BigDecimal): RewardIssuanceParams = + RewardIssuanceParams( + issuancePerFeaturedAppTraffic_CCperMB = BigDecimal(0), + threshold_CC = BigDecimal(0), + totalIssuanceForFeaturedAppRewards = totalIssuance, + unclaimedAppRewardAmount = BigDecimal(0), + ) - def mkParams(totalIssuance: BigDecimal): RewardIssuanceParams = - RewardIssuanceParams( - issuancePerFeaturedAppTraffic_CCperMB = BigDecimal(0), - threshold_CC = BigDecimal(0), - totalIssuanceForFeaturedAppRewards = totalIssuance, - unclaimedAppRewardAmount = BigDecimal(0), + def insertRoundTotal(historyId: Long, round: Long, amount: BigDecimal): Future[Unit] = + futureUnlessShutdownToFuture( + storage.underlying.queryAndUpdate( + sqlu"""insert into app_reward_round_totals + (history_id, round_number, total_app_reward_minting_allowance, + total_app_reward_thresholded, total_app_reward_unclaimed, + rewarded_app_provider_parties_count) + values ($historyId, $round, $amount, 0, 0, 1)""".map(_ => ()), + "test.insertRoundTotal", ) + ) - def insertRoundTotal(historyId: Long, round: Long, amount: BigDecimal): Future[Unit] = - futureUnlessShutdownToFuture( - storage.underlying.queryAndUpdate( - sqlu"""insert into app_reward_round_totals - (history_id, round_number, total_app_reward_minting_allowance, - total_app_reward_thresholded, total_app_reward_unclaimed, - rewarded_app_provider_parties_count) - values ($historyId, $round, $amount, 0, 0, 1)""".map(_ => ()), - "test.insertRoundTotal", + "pass when reward amount is within issuance" in { + for { + (store, historyId) <- newStore() + _ <- insertRoundTotal(historyId, roundNumber, BigDecimal(10.0)) + _ <- futureUnlessShutdownToFuture( + storage.queryAndUpdate( + store + .assertMintingAllowanceWithinMintingCurve(roundNumber, mkParams(BigDecimal(10.0))), + "test.assertMintingAllowanceWithinMintingCurve", ) ) + } yield succeed + } - "pass when reward amount is within issuance" in { - for { - (store, historyId) <- newStore() - _ <- insertRoundTotal(historyId, roundNumber, BigDecimal(10.0)) - _ <- futureUnlessShutdownToFuture( - storage.underlying.queryAndUpdate( - store - .assertMintingAllowanceWithinMintingCurve(roundNumber, mkParams(BigDecimal(10.0))), - "test.assertMintingAllowanceWithinMintingCurve", - ) + "fail when reward amount exceeds issuance by more than tolerance" in { + for { + (store, historyId) <- newStore() + // Reward exceeds issuance by 2x tolerance (0.002 > 0.001) + _ <- insertRoundTotal(historyId, roundNumber, BigDecimal(10.002)) + result <- futureUnlessShutdownToFuture( + storage.queryAndUpdate( + store.assertMintingAllowanceWithinMintingCurve( + roundNumber, + mkParams(BigDecimal(10.0)), + ), + "test.assertMintingAllowanceWithinMintingCurve", ) - } yield succeed - } - - "fail when reward amount exceeds issuance by more than tolerance" in { - for { - (store, historyId) <- newStore() - // Reward exceeds issuance by 2x tolerance (0.002 > 0.001) - _ <- insertRoundTotal(historyId, roundNumber, BigDecimal(10.002)) - result <- futureUnlessShutdownToFuture( - storage.underlying.queryAndUpdate( - store.assertMintingAllowanceWithinMintingCurve( - roundNumber, - mkParams(BigDecimal(10.0)), - ), - "test.assertMintingAllowanceWithinMintingCurve", - ) - ).failed - } yield { - result.getMessage should include("exceeds minting curve allowance") - } + ).failed + } yield { + result.getMessage should include("exceeds minting curve allowance") } + } - "pass when reward amount exceeds issuance within tolerance" in { - for { - (store, historyId) <- newStore() - _ <- insertRoundTotal(historyId, roundNumber, BigDecimal(10.0005)) - _ <- futureUnlessShutdownToFuture( - storage.underlying.queryAndUpdate( - store - .assertMintingAllowanceWithinMintingCurve(roundNumber, mkParams(BigDecimal(10.0))), - "test.assertMintingAllowanceWithinMintingCurve", - ) + "pass when reward amount exceeds issuance within tolerance" in { + for { + (store, historyId) <- newStore() + _ <- insertRoundTotal(historyId, roundNumber, BigDecimal(10.0005)) + _ <- futureUnlessShutdownToFuture( + storage.queryAndUpdate( + store + .assertMintingAllowanceWithinMintingCurve(roundNumber, mkParams(BigDecimal(10.0))), + "test.assertMintingAllowanceWithinMintingCurve", ) - } yield succeed - } + ) + } yield succeed } + } - // -- computeRewardTotals tests ------------------------------------------- + "computeRewardTotals" should { val rewardTotalsTestCases = Seq( // 5_000_000 / 1_000_000 * 2.0 = 10.0 @@ -808,74 +861,15 @@ class DbScanAppRewardsStoreTest ) rewardTotalsTestCases.foreach { tc => - s"computeRewardTotals — ${tc.description}" in { + tc.description in { RewardTotalsTests.run(tc) } } + } - // -- computeAndStoreRewards tests ------------------------------------------ - - "computeAndStoreRewards — rejects incomplete activity" in { - for { - (store, historyId) <- newStore() - // Activity in roundNumber but no meta row marking the round complete - _ <- insertActivityRecord(historyId, roundNumber, Seq("alice::provider"), Seq(500L)) - result <- store - .computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) - .failed - } yield { - result.getMessage should include("Incomplete app activity") - } - } - - "computeAndStoreRewards — produces root hash for complete round" in { - for { - (store, historyId) <- newStore() - _ <- markRoundComplete(historyId, roundNumber) - _ <- insertActivityRecord(historyId, roundNumber, Seq("alice::provider"), Seq(5000000L)) - _ <- store.computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) - rootHash <- store.getAppRewardRootHashByRound(roundNumber) - activityTotals <- store.getAppActivityRoundTotalByRound(roundNumber) - } yield { - // Pipeline completed: activity aggregated and root hash produced - rootHash shouldBe defined - rootHash.value.rootHash.size shouldBe 32 - activityTotals shouldBe defined - activityTotals.value.totalRoundAppActivityWeight shouldBe 5000000L - } - } - - "computeAndStoreRewards — re-run for same round raises error" in { - for { - (store, historyId) <- newStore() - _ <- markRoundComplete(historyId, roundNumber) - _ <- insertActivityRecord(historyId, roundNumber, Seq("alice::provider"), Seq(5000000L)) - _ <- store.computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) - result <- store - .computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) - .failed - } yield { - result shouldBe a[Exception] - } - } - - "computeAndStoreRewards — rolls back when reward exceeds issuance" in { - for { - // Use a negative tolerance so any positive reward triggers the assertion - (store, historyId) <- newStore(rewardMintingAllowanceTolerance = BigDecimal(-1.0)) - _ <- markRoundComplete(historyId, roundNumber) - _ <- insertActivityRecord(historyId, roundNumber, Seq("alice::provider"), Seq(5000000L)) - result <- store - .computeAndStoreRewards(roundNumber, batchSize = 100, inputs = testInputs) - .failed - } yield { - result.getMessage should include("exceeds minting curve allowance") - } - } - - // -- computeRewardHashes tests -------------------------------------------- + "computeRewardHashes" should { - "computeRewardHashes — 3 activity parties, 2 rewarded, batchSize=2" in { + "3 activity parties, 2 rewarded, batchSize=2" in { // 2 rewarded parties fit in 1 batch of size 2 for { (_, batchHashes, _) <- setupAndComputeHashes( @@ -891,7 +885,7 @@ class DbScanAppRewardsStoreTest } } - "computeRewardHashes — single party produces single leaf batch" in { + "single party produces single leaf batch" in { for { (_, batchHashes, _) <- setupAndComputeHashes(partyCount = 1, batchSize = 100) } yield { @@ -901,7 +895,7 @@ class DbScanAppRewardsStoreTest } } - "computeRewardHashes — 3 levels: 9 parties, batchSize=2" in { + "3 levels: 9 parties, batchSize=2" in { // batchSize=2 → level 0: 5 batches, level 1: 3, level 2: 2 for { (_, batchHashes, _) <- setupAndComputeHashes(partyCount = 9, batchSize = 2) @@ -918,7 +912,7 @@ class DbScanAppRewardsStoreTest } } - "computeRewardHashes — exact boundary: 4 parties, batchSize=2" in { + "exact boundary: 4 parties, batchSize=2" in { // batchSize=2 → level 0: 2 batches, level 1: 1 batch (aggregation stops) for { (_, batchHashes, _) <- setupAndComputeHashes(partyCount = 4, batchSize = 2) @@ -932,7 +926,7 @@ class DbScanAppRewardsStoreTest } } - "computeRewardHashes — all parties fit in one batch, no aggregation" in { + "all parties fit in one batch, no aggregation" in { // 3 parties, batchSize=100 → single leaf batch, no aggregation for { (_, batchHashes, rootHash) <- setupAndComputeHashes(partyCount = 3, batchSize = 100) @@ -947,7 +941,7 @@ class DbScanAppRewardsStoreTest } } - "computeRewardHashes — root hash exists after multi-level aggregation" in { + "root hash exists after multi-level aggregation" in { for { (store, _, _) <- setupAndComputeHashes(partyCount = 5, batchSize = 2) computed <- store.roundsWithComputedRewards(Seq(roundNumber)) @@ -960,7 +954,7 @@ class DbScanAppRewardsStoreTest ("no activity parties", 0, -1), ("activity but no rewarded parties", 1, 0), ).foreach { case (desc, parties, rewarded) => - s"computeRewardHashes — $desc produces empty root hash" in { + s"$desc produces empty root hash" in { for { (store, batchHashes, rootHash) <- setupAndComputeHashes( partyCount = parties, @@ -982,7 +976,7 @@ class DbScanAppRewardsStoreTest } } - "computeRewardHashes — re-run for same round raises error" in { + "re-run for same round raises error" in { for { (store, _, _) <- setupAndComputeHashes(partyCount = 1, batchSize = 100) result <- store.computeRewardHashes(roundNumber, batchSize = 100).failed @@ -990,10 +984,11 @@ class DbScanAppRewardsStoreTest result shouldBe a[Exception] } } + } - // -- lookupBatchByHash tests ---------------------------------------------- + "lookupBatchByHash" should { - "lookupBatchByHash — returns None for non-existent hash" in { + "returns None for non-existent hash" in { for { (store, _) <- newStore() result <- store.lookupBatchByHash( @@ -1005,7 +1000,7 @@ class DbScanAppRewardsStoreTest } } - "lookupBatchByHash — leaf batch returns MintingAllowances" in { + "leaf batch returns MintingAllowances" in { for { (store, historyId) <- newStore() _ <- store.insertAppActivityPartyTotals( @@ -1037,7 +1032,7 @@ class DbScanAppRewardsStoreTest } } - "lookupBatchByHash — internal batch returns BatchOfBatches" in { + "internal batch returns BatchOfBatches" in { for { (store, historyId) <- newStore() // 4 parties / batchSize=2 → 2 level-0 batches → 1 level-1 batch @@ -1078,7 +1073,6 @@ class DbScanAppRewardsStoreTest childHashes shouldBe level0Hashes } } - } private val verdictCounter = new java.util.concurrent.atomic.AtomicLong(1) @@ -1214,7 +1208,7 @@ class DbScanAppRewardsStoreTest val n = storeCounter.getAndIncrement() val participantId = mkParticipantId(s"rewards-test-$n") val updateHistory = new UpdateHistory( - storage.underlying, + storage, migrationId, s"app_rewards_test_$n", participantId, @@ -1344,7 +1338,7 @@ object DbScanAppRewardsStoreTest { * trafficPrice/amuletPrice = 1.0 so 1 MB of traffic = 1 CC of reward. */ val testInputs: RewardComputationInputs = { - import RewardComputationInputs.{fromBigDecimal as n} + import RewardComputationInputs.fromBigDecimal as n val tickDurationMicros = 600L * 1000000L val microsPerYear = 365L * 24 * 3600 * 1000000L val roundsPerYear = BigDecimal(microsPerYear) / BigDecimal(tickDurationMicros) diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/QueryAcsSnapshotPaginationTokenTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/QueryAcsSnapshotPaginationTokenTest.scala new file mode 100644 index 0000000000..0b5c5da666 --- /dev/null +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/QueryAcsSnapshotPaginationTokenTest.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.scan.store + +import com.digitalasset.canton.BaseTest +import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.QueryAcsSnapshotPaginationToken +import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.QueryAcsSnapshotPaginationToken.RowIdQueryAcsSnapshotPaginationToken +import org.scalatest.wordspec.AnyWordSpec +import scala.util.Try + +class QueryAcsSnapshotPaginationTokenTest extends AnyWordSpec with BaseTest { + + "RowIdQueryAcsSnapshotPaginationToken" should { + + "encode to base64 and decode back" in { + val token = RowIdQueryAcsSnapshotPaginationToken(42L) + val encoded = token.encodeToBase64 + val decoded = QueryAcsSnapshotPaginationToken.tryDecodeFromBase64(encoded) + decoded shouldBe token + } + + "produce different encoded values for different row ids" in { + val token1 = RowIdQueryAcsSnapshotPaginationToken(1L) + val token2 = RowIdQueryAcsSnapshotPaginationToken(2L) + token1.encodeToBase64 should not equal token2.encodeToBase64 + } + } + + "QueryAcsSnapshotPaginationToken.decodeFromBase64" should { + + "return Left for an invalid base64 string" in { + val result = Try(QueryAcsSnapshotPaginationToken.tryDecodeFromBase64("not-valid-base64!!!")) + result.isFailure should be(true) + } + + "return Left for valid base64 but invalid JSON content" in { + val encoded = java.util.Base64.getEncoder.encodeToString("not-a-long".getBytes("UTF-8")) + val result = Try(QueryAcsSnapshotPaginationToken.tryDecodeFromBase64(encoded)) + result.isFailure should be(true) + } + + "return Left for valid base64 with JSON object instead of long" in { + val encoded = + java.util.Base64.getEncoder.encodeToString("""{"after": 42}""".getBytes("UTF-8")) + val result = Try(QueryAcsSnapshotPaginationToken.tryDecodeFromBase64(encoded)) + result.isFailure should be(true) + } + } + +} diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStoreTest.scala index 9a0f061b3f..f066410b23 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStoreTest.scala @@ -9,9 +9,10 @@ import org.lfdecentralizedtrust.splice.store.{ HistoryMetrics, PageLimit, StoreTestBase, + TimestampWithMigrationId, UpdateHistory, } -import org.lfdecentralizedtrust.splice.scan.store.db.DbScanVerdictStore +import org.lfdecentralizedtrust.splice.scan.store.db.{DbAppActivityRecordStore, DbScanVerdictStore} import org.lfdecentralizedtrust.splice.scan.store.db.DbScanVerdictStore.{TrafficSummaryT, EnvelopeT} import org.lfdecentralizedtrust.splice.store.db.SplicePostgresTest import com.digitalasset.canton.resource.DbStorage @@ -122,14 +123,14 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl // Fetch with cursor events2 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1.minusSeconds(1))), + Some(TimestampWithMigrationId(recordTs1.minusSeconds(1), domainMigrationId)), domainMigrationId, pageLimit, ) // Fetch after recordTs1 events3 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1)), + Some(TimestampWithMigrationId(recordTs1, domainMigrationId)), domainMigrationId, pageLimit, ) @@ -169,14 +170,14 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl // Fetch with cursor events2 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1.minusSeconds(1))), + Some(TimestampWithMigrationId(recordTs1.minusSeconds(1), domainMigrationId)), domainMigrationId, pageLimit, ) // Fetch after recordTs1 events3 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1)), + Some(TimestampWithMigrationId(recordTs1, domainMigrationId)), domainMigrationId, pageLimit, ) @@ -218,14 +219,14 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl // Fetch with cursor events2 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1.minusSeconds(1))), + Some(TimestampWithMigrationId(recordTs1.minusSeconds(1), domainMigrationId)), domainMigrationId, pageLimit, ) // Fetch after recordTs1 events3 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1)), + Some(TimestampWithMigrationId(recordTs1, domainMigrationId)), domainMigrationId, pageLimit, ) @@ -267,12 +268,17 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl events <- fetchEvents(ctx1.eventStore, None, mig1, pageLimit) events2 <- fetchEvents( ctx1.eventStore, - Some((mig0, recordTs1.minusSeconds(1))), + Some(TimestampWithMigrationId(recordTs1.minusSeconds(1), mig0)), mig1, pageLimit, ) // after recordTs1 - events3 <- fetchEvents(ctx1.eventStore, Some((mig0, recordTs1)), mig1, pageLimit) + events3 <- fetchEvents( + ctx1.eventStore, + Some(TimestampWithMigrationId(recordTs1, mig0)), + mig1, + pageLimit, + ) // Fetch by id works across migrationIds e1 <- ctx1.eventStore.getEventByUpdateId(updateId1, domainMigrationId) e2 <- ctx1.eventStore.getEventByUpdateId(updateId2, domainMigrationId) @@ -429,14 +435,14 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl // Fetch with cursor events2 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1.minusSeconds(1))), + Some(TimestampWithMigrationId(recordTs1.minusSeconds(1), domainMigrationId)), domainMigrationId, pageLimit, ) // Fetch after latest verdict events3 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs2)), + Some(TimestampWithMigrationId(recordTs2, domainMigrationId)), domainMigrationId, pageLimit, ) @@ -474,14 +480,14 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl // Fetch with cursor events2 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1.minusSeconds(1))), + Some(TimestampWithMigrationId(recordTs1.minusSeconds(1), domainMigrationId)), domainMigrationId, pageLimit, ) // Fetch after latest assignment events3 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs2)), + Some(TimestampWithMigrationId(recordTs2, domainMigrationId)), domainMigrationId, pageLimit, ) @@ -518,14 +524,14 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl // Fetch with cursor events2 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1.minusSeconds(1))), + Some(TimestampWithMigrationId(recordTs1.minusSeconds(1), domainMigrationId)), domainMigrationId, pageLimit, ) // Fetch after latest verdict events3 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs2)), + Some(TimestampWithMigrationId(recordTs2, domainMigrationId)), domainMigrationId, pageLimit, ) @@ -564,14 +570,14 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl // Fetch with cursor events2 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs1.minusSeconds(1))), + Some(TimestampWithMigrationId(recordTs1.minusSeconds(1), domainMigrationId)), domainMigrationId, pageLimit, ) // Fetch after latest unassignment events3 <- fetchEvents( ctx.eventStore, - Some((domainMigrationId, recordTs2)), + Some(TimestampWithMigrationId(recordTs2, domainMigrationId)), domainMigrationId, pageLimit, ) @@ -813,7 +819,7 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl { val allow = ScanEventStore.allowF( - afterO = Some((mig0, recordTs1)), + afterO = Some(TimestampWithMigrationId(recordTs1, mig0)), currentMigrationId = mig1, currentMigrationCap = capMin, ) @@ -824,7 +830,7 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl { val allow = ScanEventStore.allowF( - afterO = Some((mig0, recordTs1)), + afterO = Some(TimestampWithMigrationId(recordTs1, mig0)), currentMigrationId = mig1, currentMigrationCap = cap3, ) @@ -838,7 +844,7 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl { val allow = ScanEventStore.allowF( - afterO = Some((mig1, recordTs2)), + afterO = Some(TimestampWithMigrationId(recordTs2, mig1)), currentMigrationId = mig1, currentMigrationCap = cap3, ) @@ -849,7 +855,7 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl { val allow = ScanEventStore.allowF( - afterO = Some((mig2, recordTs2)), + afterO = Some(TimestampWithMigrationId(recordTs2, mig2)), currentMigrationId = mig2, currentMigrationCap = cap2, ) @@ -880,7 +886,18 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl } private def newVerdictStore(updateHistory: UpdateHistory) = - new DbScanVerdictStore(storage.underlying, updateHistory, None, loggerFactory) + new DbScanVerdictStore( + storage.underlying, + updateHistory, + new DbAppActivityRecordStore( + storage.underlying, + updateHistory, + DbAppActivityRecordStore.IngestionVersions(1, 0), + false, + loggerFactory, + ), + loggerFactory, + ) private def insertUpdate( updateHistory: UpdateHistory, @@ -1031,7 +1048,7 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl private def fetchEvents( es: ScanEventStore, - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], currentMigrationId: Long, limit: PageLimit, ): Future[Seq[ScanEventStore#Event]] = { diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageCommitFromStagingTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageCommitFromStagingTest.scala index 44adcd701e..2b3b0a314b 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageCommitFromStagingTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageCommitFromStagingTest.scala @@ -52,7 +52,8 @@ class AcsSnapshotBulkStorageCommitFromStagingTest zstdCompressionLevel = 3, ) val appConfig = BulkStorageConfig( - snapshotPollingInterval = NonNegativeFiniteDuration.ofSeconds(5) + snapshotPollingInterval = NonNegativeFiniteDuration.ofSeconds(5), + bftCheckEnabled = false, // bft checks are tested elsewhere ) override val initialBuckets: Seq[String] = Seq("staging", "committed") @@ -104,6 +105,8 @@ class AcsSnapshotBulkStorageCommitFromStagingTest committedConnection, reader, appConfig, + null, // not used when bft reads are disabled + _ => (), loggerFactory, ) val commitService = { @@ -137,21 +140,25 @@ class AcsSnapshotBulkStorageCommitFromStagingTest objectCount: Int, ): Unit = { (0 until objectCount).foreach { i => - stagingConnection - .createObject( - s"${bulkStorageTestConfig.getSegmentFolder(ts(day), None)}/ACS_$i.zstd", - s"dummy acs snapshot at ${ts(day)} (object $i)".getBytes, - ) - .futureValue + ScanStorageConfig.Encoding.all.toList.foreach { encoding => + stagingConnection + .createObject( + s"${bulkStorageTestConfig.getSegmentFolder(ts(day), None)}/${encoding.storageKey("ACS", i)}", + s"dummy acs snapshot at ${ts(day)} (object $i)".getBytes, + ) + .futureValue + } } } def assertCommittedObjectsForSnapshot(day: Int, expectedCount: Int): Assertion = { - val expectedKeys = (0 until expectedCount).map { i => - s"${bulkStorageTestConfig.getSegmentFolder(ts(day), None)}/ACS_$i.zstd" + val expectedKeys = (0 until expectedCount).flatMap { i => + ScanStorageConfig.Encoding.all.toList.map { encoding => + s"${bulkStorageTestConfig.getSegmentFolder(ts(day), None)}/${encoding.storageKey("ACS", i)}" + } } reader - .getCommittedObjectsForAcsSnapshotAtOrBefore(ts(day)) + .getCommittedObjectsForAcsSnapshotAtOrBefore(ts(day), ScanStorageConfig.Encoding.all) .futureValue .objects .map(_.key) should contain theSameElementsAs @@ -215,7 +222,8 @@ class AcsSnapshotBulkStorageCommitFromStagingTest committedConnection .copyObject( "staging", - s"${bulkStorageTestConfig.getSegmentFolder(ts(4), None)}/ACS_0.zstd", + s"${bulkStorageTestConfig.getSegmentFolder(ts(4), None)}/${ScanStorageConfig.Encoding.CompactJson + .storageKey("ACS", 0)}", ) .futureValue @@ -236,12 +244,14 @@ class AcsSnapshotBulkStorageCommitFromStagingTest committedConnection .copyObject( "staging", - s"${bulkStorageTestConfig.getSegmentFolder(ts(5), None)}/ACS_$i.zstd", + s"${bulkStorageTestConfig + .getSegmentFolder(ts(5), None)}/${ScanStorageConfig.Encoding.CompactJson.storageKey("ACS", i)}", ) .futureValue stagingConnection .deleteObject( - s"${bulkStorageTestConfig.getSegmentFolder(ts(5), None)}/ACS_$i.zstd" + s"${bulkStorageTestConfig + .getSegmentFolder(ts(5), None)}/${ScanStorageConfig.Encoding.CompactJson.storageKey("ACS", i)}" ) .futureValue } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageWriterFromDbTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageWriterFromDbTest.scala index e700ce4a2d..d5768f5968 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageWriterFromDbTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/AcsSnapshotBulkStorageWriterFromDbTest.scala @@ -23,7 +23,11 @@ import org.apache.pekko.stream.scaladsl.{Sink, Source} import org.lfdecentralizedtrust.splice.config.AutomationConfig import org.lfdecentralizedtrust.splice.environment.{DarResources, RetryProvider, SpliceMetrics} import org.lfdecentralizedtrust.splice.http.v0.definitions as httpApi -import org.lfdecentralizedtrust.splice.scan.admin.http.CompactJsonScanHttpEncodings +import org.lfdecentralizedtrust.splice.scan.admin.http.{ + CompactJsonScanHttpEncodings, + ProtobufJsonScanHttpEncodings, + ScanHttpEncodings, +} import org.lfdecentralizedtrust.splice.scan.config.{BulkStorageConfig, ScanStorageConfig} import org.lfdecentralizedtrust.splice.scan.store.{ AcsSnapshotStore, @@ -50,6 +54,7 @@ import org.slf4j.event.Level import java.time.Instant import java.time.temporal.ChronoUnit +import java.util.concurrent.ConcurrentHashMap import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* import scala.concurrent.duration.* @@ -106,49 +111,86 @@ class AcsSnapshotBulkStorageWriterFromDbTest ) .map(_.createdEventsInPage) } yield { - val objectKeys = s3Objects.contents.asScala.map(_.key()).sorted - objectKeys should have length 7 - objectKeys.foreach( - _ should startWith("2026-01-02T00:00:00Z~2026-01-03T00:00:00Z/ACS_") - ) - val objectCountMetrics = metricsFactory.metrics.counters.get( - SpliceMetrics.MetricsPrefix :+ "history" :+ "bulk-storage" :+ "object-count" - ) - val numObjectsFromMetric = objectCountMetrics.value - .get(MetricsContext.Empty) - .value - .markers - .get(MetricsContext("object_type" -> "ACS_snapshots")) - .value - .get() - numObjectsFromMetric shouldBe 7 + def checkEncoding(encoding: ScanStorageConfig.Encoding) = { + /* We hard-code the expected digests to enforce that the persisted data format does not change. + These values must not be modified unless there is a conscious decision to change the persisted format, + with a migration plan for how to apply it consistently across SVs. */ + val (encodings, expectedDigests): (ScanHttpEncodings, Seq[String]) = + encoding match { + case ScanStorageConfig.Encoding.CompactJson => + ( + new CompactJsonScanHttpEncodings(identity, identity), + Seq( + "n6CV6dF9zpleq66YiXmCG96hw1BBakp1I8JjC5lf5n0=", + "bJDalSmiVKCk9QSc6sAWdahJNZQqVn51WmkFbQI6wkA=", + "noZU+He8HnCM38MujtpEle4NNGwnE7wN8z96V+HTdK0=", + "rwak+Y4JcInTiEa2yUKf8rjO3RD7ay/D2hmQG4BAa54=", + "YM7SNxHrU3xYyNOjgEqowitAvgsiX1f7tq0pCaD/OhQ=", + "Mb3D2ZOVQclMwuYEqLuTKhGqnUHCio6K61FBTXgt5Vs=", + "+5iW2M9Vz5y9sCtEWyrS3m+EUqnD50dXRVIMQAMSgBY=", + ), + ) + case ScanStorageConfig.Encoding.ProtobufJson => + ( + ProtobufJsonScanHttpEncodings, + Seq( + "NDdxcBFCRqz5hXHXqJkD9qqzc1C9t0PWZgHq2F9xsUA=", + "hluFPPWS1V2djExfppU+aiPqYx18s/qxZe83nFjthV0=", + "5D3k/XWBw/OhN5wus7XMuesHKhQwlktDFMFcO9lAI5g=", + "sov3P/ekZ1CRQUPYVMcA7tn2yO4EV+XYtlWc5NF2FP0=", + "tYYPDCgkg/s+dbJD9i6kxBHiMIKq2RB/D0+Fc5gzlAI=", + "CjQBhKAQ+KU7it/OCAkyDtKLNHJmWu2nsU4x1TcT+us=", + "ZonY8bJ5NA2b1Y/gOU2eeUF6lVsQqijKWDjP8kKWvhU=", + "G7eAcDOxoCsxpC9Qwo61IZFUFD0sZnqPf3/dolF9nXQ=", + ), + ) + } + val objectKeys = s3Objects.contents.asScala + .map(_.key()) + .sorted + .filter( + encoding.storageKeyRegex("ACS").matches + ) + objectKeys should have length expectedDigests.length.toLong + objectKeys.foreach( + _ should startWith(s"2026-01-02T00:00:00Z~2026-01-03T00:00:00Z/ACS_${encoding.key}") + ) + val objectCountMetrics = metricsFactory.metrics.counters.get( + SpliceMetrics.MetricsPrefix :+ "history" :+ "bulk-storage" :+ "object-count" + ) + val numObjectsFromMetric = objectCountMetrics.value + .get(MetricsContext.Empty) + .value + .markers + .get( + MetricsContext( + "object_type" -> "ACS_snapshots", + "encoding" -> encoding.key, + "bucket" -> "staging", + ) + ) + .value + .get() + numObjectsFromMetric shouldBe expectedDigests.length - val allContractsFromS3 = objectKeys.flatMap( - readUncompressAndDecode( - bucketConnection, - io.circe.parser.decode[httpApi.ActiveContract], + val allContractsFromS3 = objectKeys.flatMap( + readUncompressAndDecode( + bucketConnection, + io.circe.parser.decode[httpApi.ActiveContract], + ) ) - ) - allContracts.map(c => - new CompactJsonScanHttpEncodings(identity, identity) - .javaToHttpActiveContract(c.eventId, c.recordTime, c.event) - ) should contain theSameElementsInOrderAs allContractsFromS3 + allContracts.map(c => + encodings.javaToHttpActiveContract(c.eventId, c.recordTime, c.event) + ) should contain theSameElementsInOrderAs allContractsFromS3 - /* We hard-code the expected digests to enforce that the persisted data format does not change. - These values must not be modified unless there is a conscious decision to change the persisted format, - with a migration plan for how to apply it consistently across SVs. */ - bucketConnection - .getChecksums(objectKeys.toSeq) - .futureValue - .map(_.checksum) should contain theSameElementsInOrderAs Seq( - "n6CV6dF9zpleq66YiXmCG96hw1BBakp1I8JjC5lf5n0=", - "bJDalSmiVKCk9QSc6sAWdahJNZQqVn51WmkFbQI6wkA=", - "noZU+He8HnCM38MujtpEle4NNGwnE7wN8z96V+HTdK0=", - "rwak+Y4JcInTiEa2yUKf8rjO3RD7ay/D2hmQG4BAa54=", - "YM7SNxHrU3xYyNOjgEqowitAvgsiX1f7tq0pCaD/OhQ=", - "Mb3D2ZOVQclMwuYEqLuTKhGqnUHCio6K61FBTXgt5Vs=", - "+5iW2M9Vz5y9sCtEWyrS3m+EUqnD50dXRVIMQAMSgBY=", - ) + bucketConnection + .getChecksums(objectKeys.toSeq) + .futureValue + .map(_.checksum) should contain theSameElementsInOrderAs expectedDigests + } + + checkEncoding(ScanStorageConfig.Encoding.CompactJson) + checkEncoding(ScanStorageConfig.Encoding.ProtobufJson) } } @@ -223,7 +265,8 @@ class AcsSnapshotBulkStorageWriterFromDbTest reader.getCommittedObjectsForAcsSnapshotAtOrBefore(queryTs).futureValue getObjectsResult.objects.map(_.key) should contain theSameElementsInOrderAs (0 until expectedNumObjects).map(i => - s"$expectedTs~${expectedTs.add(1.days)}/ACS_$i.zstd" + s"$expectedTs~${expectedTs + .add(1.days)}/${ScanStorageConfig.Encoding.CompactJson.storageKey("ACS", i)}" ) getObjectsResult.objects.map(_.checksum).foreach { // We test elsewhere that computed and persisted checksums are correct, so here we just check that they are present and not empty @@ -312,7 +355,7 @@ class AcsSnapshotBulkStorageWriterFromDbTest store.queryAcsSnapshot( anyLong, any[CantonTimestamp], - any[Option[Long]], + any[Option[AcsSnapshotStore.QueryAcsSnapshotPaginationToken]], any[Limit], any[Seq[PartyId]], any[Seq[PackageQualifiedName]], @@ -321,14 +364,22 @@ class AcsSnapshotBulkStorageWriterFromDbTest ( migration: Long, timestamp: CantonTimestamp, - after: Option[Long], + after: Option[AcsSnapshotStore.QueryAcsSnapshotPaginationToken], limit: Limit, _: Seq[PartyId], _: Seq[PackageQualifiedName], ) => if (snapshots.contains(timestamp)) { Future { - val remaining = snapshotSize - after.getOrElse(0L) + val afterAsLong = after match { + case Some( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken + .RowIdQueryAcsSnapshotPaginationToken(value) + ) => + value + case None => 0L + } + val remaining = snapshotSize - afterAsLong val numElems = math.min(limit.limit.toLong, remaining) val result = QueryAcsSnapshotResult( migration, @@ -336,7 +387,7 @@ class AcsSnapshotBulkStorageWriterFromDbTest Vector .range(0, numElems) .map(i => { - val idx = i + after.getOrElse(0L) + val idx = i + afterAsLong val amt = amulet( partyId, BigDecimal(idx), @@ -351,7 +402,12 @@ class AcsSnapshotBulkStorageWriterFromDbTest toCreatedEvent(amt), ) }), - if (numElems < remaining) Some(after.getOrElse(0L) + numElems) else None, + if (numElems < remaining) + Some( + AcsSnapshotStore.QueryAcsSnapshotPaginationToken + .RowIdQueryAcsSnapshotPaginationToken(afterAsLong + numElems) + ) + else None, ) result } @@ -403,19 +459,12 @@ class AcsSnapshotBulkStorageWriterFromDbTest bucketConnection: S3BucketConnection ): S3BucketConnection = { val s3BucketConnectionWithErrors = Mockito.spy(bucketConnection) - var failureCount = 0 + val failedKeys = ConcurrentHashMap.newKeySet[String]() val _ = doAnswer { (invocation: InvocationOnMock) => val args = invocation.getArguments args.toList match { - case (key: String) :: _ if key.endsWith("2.zstd") => - if (failureCount < 1) { - failureCount += 1 - throw new RuntimeException(s"Simulated S3 error (#$failureCount)") - } else { - failureCount = 0 - logger.debug(s"No Simulated S3 error, resetting failureCount to 0") - invocation.callRealMethod().asInstanceOf[s3BucketConnectionWithErrors.AppendWriteObject] - } + case (key: String) :: _ if key.endsWith("2.zstd") && failedKeys.add(key) => + throw new RuntimeException(s"Simulated S3 error for $key") case _ => invocation.callRealMethod().asInstanceOf[s3BucketConnectionWithErrors.AppendWriteObject] } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageCommitFromStagingTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageCommitFromStagingTest.scala index be368bc401..639d6ac845 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageCommitFromStagingTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/BulkStorageCommitFromStagingTest.scala @@ -3,40 +3,64 @@ package org.lfdecentralizedtrust.splice.scan.store.bulk +import com.digitalasset.canton.config.NonNegativeFiniteDuration import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.SuppressionRule import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.{HasActorSystem, HasExecutionContext} +import org.apache.pekko.NotUsed +import org.apache.pekko.http.scaladsl.model.Uri +import org.lfdecentralizedtrust.splice.config.NetworkAppClientConfig +import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig +import org.lfdecentralizedtrust.splice.test.HasRetryProvider import org.slf4j.event.Level -import org.apache.pekko.stream.scaladsl.Keep +import org.apache.pekko.stream.scaladsl.{Flow, Keep} import org.apache.pekko.stream.testkit.scaladsl.{TestSink, TestSource} +import org.lfdecentralizedtrust.splice.environment.SpliceLedgerClient +import org.lfdecentralizedtrust.splice.http.HttpClient +import org.lfdecentralizedtrust.splice.http.v0.definitions.GetBulkObjectChecksumsResponse +import org.lfdecentralizedtrust.splice.scan.admin.api.client.{ + BftScanConnection, + SingleScanConnection, +} import org.lfdecentralizedtrust.splice.scan.config.BulkStorageConfig import org.lfdecentralizedtrust.splice.store.S3BucketConnection.ObjectKeyAndChecksum import org.lfdecentralizedtrust.splice.store.{HasS3Mock, StoreTestBase} import org.lfdecentralizedtrust.splice.store.db.SplicePostgresTest +import org.lfdecentralizedtrust.splice.util.TemplateJsonDecoder +import org.lfdecentralizedtrust.splice.scan.util.PeerBftScanConnection import java.security.MessageDigest import java.util.Base64 -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* +import scala.concurrent.duration.* class BulkStorageCommitFromStagingTest extends StoreTestBase with HasExecutionContext with HasActorSystem with HasS3Mock - with SplicePostgresTest { + with SplicePostgresTest + with HasRetryProvider { override val initialBuckets = Seq("staging", "committed") - val appConfig = BulkStorageConfig() + implicit val httpClient: HttpClient = null + implicit val templateJsonDecoder: TemplateJsonDecoder = null "BulkStorageCommitFromStaging" should { + val appConfig = BulkStorageConfig( + bftCheckEnabled = false + ) + "successfully move objects from staging to committed S3 bucket" in { val (stagingS3Connection, committedS3Connection, objsWithDigests) = setupTest - triggerCopyFlow(stagingS3Connection, committedS3Connection, objsWithDigests) + triggerCopyFlowAndAssertCompletion( + newCopyFlow(stagingS3Connection, committedS3Connection, objsWithDigests) + ) assertObjectsMoved(stagingS3Connection, committedS3Connection, objsWithDigests) } @@ -54,37 +78,299 @@ class BulkStorageCommitFromStagingTest loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(Level.DEBUG))( { - triggerCopyFlow(stagingS3Connection, committedS3Connection, objsWithDigests) + triggerCopyFlowAndAssertCompletion( + newCopyFlow(stagingS3Connection, committedS3Connection, objsWithDigests) + ) }, logEntries => forExactly(1, logEntries)(_.message should include("Skipping copy")), ) assertObjectsMoved(stagingS3Connection, committedS3Connection, objsWithDigests) } + + def newCopyFlow( + stagingS3Connection: S3BucketConnectionForUnitTests, + committedS3Connection: S3BucketConnectionForUnitTests, + objsWithDigests: Seq[ObjectKeyAndChecksum], + ) = { + BulkStorageCommitFromStaging[String]( + stagingS3Connection, + committedS3Connection, + _ => Future.successful(objsWithDigests), + appConfig, + null, // not used when bft reads are disabled + loggerFactory, + ) + } + } - private def triggerCopyFlow( - stagingS3Connection: S3BucketConnectionForUnitTests, - committedS3Connection: S3BucketConnectionForUnitTests, - objsWithDigests: Seq[ObjectKeyAndChecksum], - ) = { - val flow = BulkStorageCommitFromStaging[String]( - stagingS3Connection, - committedS3Connection, - _ => Future.successful(objsWithDigests), - appConfig, - loggerFactory, + "BulkStorageCommitFromStaging with BFT reads enabled" should { + val appConfig = BulkStorageConfig( + bftRetryInterval = NonNegativeFiniteDuration.ofSeconds(1) ) + "successfully move objects from staging to committed S3 bucket when there's full consensus" in { + val (stagingS3Connection, committedS3Connection, objsWithDigests) = setupTest + + val mockScanConnections = new MockScanConnections(objsWithDigests) + Seq.range(0, 7).foreach { i => + mockScanConnections.scanAgrees(i) + } + + val flow = newCopyFlow( + stagingS3Connection, + committedS3Connection, + objsWithDigests, + mockScanConnections, + ) + + triggerCopyFlowAndAssertCompletion(flow) + + assertObjectsMoved(stagingS3Connection, committedS3Connection, objsWithDigests) + } + + "wait until all objects are known to the peers, and report disagreement on consensus correctly" in { + val (stagingS3Connection, committedS3Connection, objsWithDigests) = setupTest + + val mockScanConnections = new MockScanConnections(objsWithDigests) + + val flow = newCopyFlow( + stagingS3Connection, + committedS3Connection, + objsWithDigests, + mockScanConnections, + ) + + val (pub, sub) = TestSource + .probe[String] + .via(flow) + .toMat(TestSink.probe[String])(Keep.both) + .run() + + clue("When one object is not known to the peers, the copy flow should not complete") { + Seq.range(0, 2).foreach(i => mockScanConnections.scanAgrees(i)) + Seq.range(2, 7).foreach(i => mockScanConnections.scanMissingAnObject(i, 1)) + + sub.request(1) + pub.sendNext("go") + sub.expectNoMessage(20.seconds) + + stagingS3Connection.listObjects.futureValue + .contents() + .asScala should have size objsWithDigests.size.toLong + committedS3Connection.listObjects.futureValue.contents().asScala shouldBe empty + } + + // errors on mismatching digests continue past the first clue for some time until enough scans are updated to agree on the digests, + // so we make the assertion on the logs fairly wide here to avoid the late error logs failing the log checker + loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(Level.ERROR))( + { + clue( + "Simulate a majority disagreeing with our digests, the copy flow should not complete and an error should be emitted" + ) { + Seq.range(2, 7).foreach(i => mockScanConnections.scanDisagreesOnDigest(i, 1)) + sub.expectNoMessage(20.seconds) + stagingS3Connection.listObjects.futureValue + .contents() + .asScala should have size objsWithDigests.size.toLong + committedS3Connection.listObjects.futureValue.contents().asScala shouldBe empty + + } + + clue("Enough scans do agree - the copy flow should complete successfully") { + Seq.range(2, 5).foreach(i => mockScanConnections.scanAgrees(i)) + sub.expectNext(20.seconds, "go") + assertObjectsMoved(stagingS3Connection, committedS3Connection, objsWithDigests) + } + + }, + logEntries => + forAtLeast(1, logEntries)( + _.message should include( + "Checksums do not match for objects" + ) + ), + ) + } + + "ignore digest mismatches for objects listed in debugObjectsToNotCommit and not copy them to the committed bucket" in { + val (stagingS3Connection, committedS3Connection, objsWithDigests) = setupTest + + val ignoredObject = objsWithDigests(1) + + val mockScanConnections = new MockScanConnections(objsWithDigests) + // all peers disagree with us on the digest of the ignored object only + Seq.range(0, 7).foreach(i => mockScanConnections.scanDisagreesOnDigest(i, 1)) + + val flow = newCopyFlow( + stagingS3Connection, + committedS3Connection, + objsWithDigests, + mockScanConnections, + appConfig.copy(debugObjectsToNotCommit = Seq(ignoredObject.key)), + ) + + loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(Level.ERROR))( + { + triggerCopyFlowAndAssertCompletion(flow) + }, + logEntries => + forAll(logEntries)( + _.message should include("Checksums do not match for objects") + ), + ) + + val expectedCommittedObjects = objsWithDigests.filterNot(_.key == ignoredObject.key) + + clue("All objects have been deleted from staging") { + stagingS3Connection.listObjects.futureValue.contents().asScala shouldBe empty + } + clue("Only the non-ignored objects have been copied to the committed bucket") { + committedS3Connection.listObjects.futureValue + .contents() + .asScala + .map(_.key()) should contain theSameElementsAs expectedCommittedObjects.map(_.key) + } + clue("Checksums of objects in committed S3 bucket match the expected digests") { + committedS3Connection + .getChecksums(expectedCommittedObjects.map(_.key)) + .futureValue should contain theSameElementsAs expectedCommittedObjects + } + } + + class MockScanConnections( + objsWithDigests: Seq[ObjectKeyAndChecksum] + ) { + + private val singleScanConnections: Seq[SingleScanConnection] = Seq.range(0, 7).map { i => + val mockConn = mock[SingleScanConnection] + when(mockConn.config) thenReturn ScanAppClientConfig( + NetworkAppClientConfig( + Uri(s"http://dummy-admin-$i") + ) + ) + when(mockConn.url) thenReturn Uri(s"http://scan_$i") + mockConn + } + + def scanAgrees(idx: Integer): Unit = { + when( + singleScanConnections(idx) + .getBulkObjectChecksums(any[Seq[String]])(any[ExecutionContext], any[TraceContext]) + ) + .thenReturn( + Future.successful( + new GetBulkObjectChecksumsResponse( + objsWithDigests + .map(_.checksum) + .map(digest => new GetBulkObjectChecksumsResponse.Checksums(Some(digest))) + .toVector + ) + ) + ) + () + } + + def scanDisagreesOnDigest(scanIdx: Integer, objIdx: Integer): Unit = { + when( + singleScanConnections(scanIdx) + .getBulkObjectChecksums(any[Seq[String]])(any[ExecutionContext], any[TraceContext]) + ) + .thenReturn( + Future.successful( + new GetBulkObjectChecksumsResponse( + objsWithDigests + .map(_.checksum) + .updated(objIdx, "wrong-digest") + .map(digest => new GetBulkObjectChecksumsResponse.Checksums(Some(digest))) + .toVector + ) + ) + ) + } + + def scanMissingAnObject(scanIdx: Integer, objIdx: Integer): Unit = { + when( + singleScanConnections(scanIdx) + .getBulkObjectChecksums(any[Seq[String]])(any[ExecutionContext], any[TraceContext]) + ) + .thenReturn( + Future.successful( + new GetBulkObjectChecksumsResponse( + objsWithDigests + .map(_.checksum) + .map(Some(_)) + .updated(objIdx, None) + .map(oDigest => new GetBulkObjectChecksumsResponse.Checksums(oDigest)) + .toVector + ) + ) + ) + } + + private val scanList = new BftScanConnection.AllDsoScansBft( + initialScanConnections = singleScanConnections, + initialFailedConnections = Map.empty, + connectionBuilder = _ => Future.failed(new RuntimeException("Shouldn't be refreshing!")), + scanUrlsChangedCallback = _ => Future.unit, + getScans = BftScanConnection.Bft.getScansInDsoRules, + scansRefreshInterval = NonNegativeFiniteDuration.ofDays(10), + retryProvider = testRetryProvider, + loggerFactory = loggerFactory, + ) + private val bftConnection = new BftScanConnection( + amuletLedgerClient = mock[SpliceLedgerClient], + amuletRulesCacheTimeToLive = NonNegativeFiniteDuration.ofSeconds(1), + scanList = scanList, + clock = wallClock, + retryProvider = testRetryProvider, + loggerFactory = loggerFactory, + ) + val peerBftConnection: PeerBftScanConnection = mock[PeerBftScanConnection] + when(peerBftConnection.connection(any[TraceContext])) + .thenReturn(Future.successful(bftConnection)) + } + + def newCopyFlow( + stagingS3Connection: S3BucketConnectionForUnitTests, + committedS3Connection: S3BucketConnectionForUnitTests, + objsWithDigests: Seq[ObjectKeyAndChecksum], + mockScanConnections: MockScanConnections, + config: BulkStorageConfig = appConfig, + ) = { + new BulkStorageCommitFromStaging[String]( + stagingS3Connection, + committedS3Connection, + _ => Future.successful(objsWithDigests), + config, + mockScanConnections.peerBftConnection, + loggerFactory, + ).getFlow + } + } + + private def triggerCopyFlowAndAssertCompletion( + flow: Flow[String, String, NotUsed] + ) = { val (pub, sub) = TestSource .probe[String] .via(flow) .toMat(TestSink.probe[String])(Keep.both) .run() - sub.request(1) - pub.sendNext("go") - sub.expectNext("go") + try { + sub.request(1) + pub.sendNext("go") + sub.expectNext("go") + pub.sendComplete() + sub.expectComplete() + } catch { + case ex: Throwable => + pub.sendError(ex) + sub.cancel() + throw ex + } } private def assertObjectsMoved( diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/S3UploadTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/S3UploadTest.scala index 62d6754e3e..586ef6f0fa 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/S3UploadTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/S3UploadTest.scala @@ -6,16 +6,21 @@ package org.lfdecentralizedtrust.splice.scan.store.bulk import org.apache.pekko.stream.scaladsl.Keep import org.apache.pekko.stream.testkit.scaladsl.{TestSink, TestSource} import org.apache.pekko.util.ByteString -import org.lfdecentralizedtrust.splice.store.{HasS3Mock, StoreTestBase} +import org.lfdecentralizedtrust.splice.config.S3Config +import org.lfdecentralizedtrust.splice.store.{HasS3Mock, S3BucketConnection, StoreTestBase} +import com.digitalasset.canton.logging.NamedLoggerFactory import scala.util.Random import scala.concurrent.duration.* +import scala.concurrent.{ExecutionContext, Future, Promise} import scala.jdk.CollectionConverters.* - import java.nio.ByteBuffer +import java.util.concurrent.atomic.AtomicInteger class S3UploadTest extends StoreTestBase with HasS3Mock { + private val emptyDigest = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" + "S3 multipart uploads" should { "work" in { @@ -35,6 +40,27 @@ class S3UploadTest extends StoreTestBase with HasS3Mock { new String(content.toArray, "UTF-8") shouldBe "helloworld" } } + + "not corrupt the checksum if finish() is called more than once" in { + val expectedContent = "idempotency test" + val bucketConnection = new S3BucketConnectionForUnitTests(s3ConfigMock(), loggerFactory) + val o = bucketConnection.newAppendWriteObject("finish-twice") + val part = ByteBuffer.wrap(expectedContent.getBytes("UTF-8")) + + o.prepareUploadNext(part) + for { + _ <- o.upload(1, part) + _ <- o.finish() + checksumAfterFirstFinish <- bucketConnection.getChecksums(Seq("finish-twice")) + _ <- o.finish() + checksumAfterSecondFinish <- bucketConnection.getChecksums(Seq("finish-twice")) + content <- bucketConnection.readFullObject("finish-twice") + } yield { + checksumAfterFirstFinish.map(_.checksum) should not contain emptyDigest + checksumAfterSecondFinish shouldBe checksumAfterFirstFinish + new String(content.toArray, "UTF-8") shouldBe expectedContent + } + } } "GroupedWeightS3Object" should { @@ -59,6 +85,7 @@ class S3UploadTest extends StoreTestBase with HasS3Mock { .run() val it = data.iterator + def sendBytes(n: Int) = pub.sendNext(it.getByteString(n)) @@ -105,6 +132,7 @@ class S3UploadTest extends StoreTestBase with HasS3Mock { .run() val it = data.iterator + def sendBytes(n: Int) = pub.sendNext(it.getByteString(n)) @@ -115,5 +143,84 @@ class S3UploadTest extends StoreTestBase with HasS3Mock { sub.expectError() succeed } + + "not finish an object twice when upstream completes while the object is being finished" in { + // Regression test for the race that produced correct object content with a wrong checksum: + // an object that is done by size starts being finished from uploadCallback; `state` is only + // advanced later, in the async finishCallback. Upstream completion is delivered eagerly + // (independently of demand), so onUpstreamFinish could land in that window and call finish() + // a second time on the very same object. + val bucketConnection = new GatedFinishS3Connection(s3ConfigMock(), loggerFactory) + + val (pub, sub) = TestSource + .probe[ByteString] + .via( + GroupedWeightS3ObjectFlow( + bucketConnection, + getObjectKey = i => s"race_$i", + maxObjectSize = 10L, + maxParallelPartUploads = 2, + loggerFactory, + ) + ) + .toMat(TestSink.probe[String])(Keep.both) + .run() + + sub.request(5) + // Exactly hits maxObjectSize, so the object is done by size and finish() is started + // from uploadCallback as soon as the single part upload completes. + pub.sendNext(ByteString(Random.nextBytes(10))) + + // Wait until the flow is blocked inside finish() + eventually() { + bucketConnection.finishCount.get() shouldBe 1 + } + + // Complete upstream while finish() is still in flight + pub.sendComplete() + always(durationOfSuccess = 2.seconds) { + bucketConnection.finishCount.get() shouldBe 1 + } + + bucketConnection.releaseFinish() + sub.expectNext(20.seconds) shouldBe "race_0" + sub.expectComplete() + + val checksums = bucketConnection.getChecksums(Seq("race_0")).futureValue + checksums should have size 1 + checksums.map(_.checksum) should not contain emptyDigest + succeed + } + } + + /** An S3 connection whose `finish()` blocks until [[releaseFinish]] is called, and which counts + * how many times `finish()` was invoked. + */ + private class GatedFinishS3Connection( + s3Config: S3Config, + loggerFactory: NamedLoggerFactory, + ) extends S3BucketConnectionForUnitTests(s3Config, loggerFactory) { + val finishCount = new AtomicInteger(0) + private val gate = Promise[Unit]() + + def releaseFinish(): Unit = { val _ = gate.trySuccess(()) } + + override def newAppendWriteObject( + key: String + )(implicit ec: ExecutionContext): AppendWriteObject = + new AppendWriteObjectForUnitTests(key) { + override def finish(): Future[Unit] = { + val _ = finishCount.incrementAndGet() + gate.future.flatMap(_ => super.finish()) + } + } + } + + "S3BucketConnection" should { + "Get checksums does not panic on non existing object" in { + val bucketConnection = new S3BucketConnection(s3ConfigMock(), loggerFactory) + val checksum = bucketConnection.getChecksums(Seq("non-existing-object")).futureValue + checksum shouldBe empty + } } } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorageTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorageTest.scala index 62ddcd750e..6a9eed5319 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorageTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/bulk/UpdateHistoryBulkStorageTest.scala @@ -25,7 +25,11 @@ import org.lfdecentralizedtrust.splice.config.AutomationConfig import org.lfdecentralizedtrust.splice.environment.{DarResources, RetryProvider, SpliceMetrics} import org.lfdecentralizedtrust.splice.environment.ledger.api.TransactionTreeUpdate import org.lfdecentralizedtrust.splice.http.v0.definitions.UpdateHistoryItemV2 -import org.lfdecentralizedtrust.splice.scan.admin.http.CompactJsonScanHttpEncodings +import org.lfdecentralizedtrust.splice.scan.admin.http.{ + CompactJsonScanHttpEncodings, + ProtobufJsonScanHttpEncodings, + ScanHttpEncodings, +} import org.lfdecentralizedtrust.splice.scan.config.{BulkStorageConfig, ScanStorageConfig} import org.lfdecentralizedtrust.splice.scan.store.{ScanKeyValueProvider, ScanKeyValueStore} import org.lfdecentralizedtrust.splice.store.UpdateHistory.UpdateHistoryResponse @@ -57,7 +61,8 @@ class UpdateHistoryBulkStorageTest zstdCompressionLevel = 3, ) val appConfig = BulkStorageConfig( - updatesPollingInterval = NonNegativeFiniteDuration.ofSeconds(5) + updatesPollingInterval = NonNegativeFiniteDuration.ofSeconds(5), + bftCheckEnabled = false, // bft checks are tested elsewhere ) "UpdateHistoryBulkStorage" should { @@ -104,22 +109,43 @@ class UpdateHistoryBulkStorageTest "Ingest 1000 more events. Now the last timestamp will be beyond the segment, so the source will complete and emit the object keys" ) { mockStore.mockIngestion(1000) - probe.expectNext(20.seconds) should contain theSameElementsInOrderAs Seq( - "1970-01-01T00:00:00.100Z~1970-01-01T00:00:02.300Z/updates_0.zstd", - "1970-01-01T00:00:00.100Z~1970-01-01T00:00:02.300Z/updates_1.zstd", + val expectedKeys = ScanStorageConfig.Encoding.all.toList.flatMap(e => + Seq( + s"1970-01-01T00:00:00.100Z~1970-01-01T00:00:02.300Z/${e.storageKey("updates", 0)}", + s"1970-01-01T00:00:00.100Z~1970-01-01T00:00:02.300Z/${e.storageKey("updates", 1)}", + ) + ) + val actualKeys = probe.expectNext(20.seconds) + def filterKeys(keys: Seq[String], encoding: ScanStorageConfig.Encoding) = + keys.filter(encoding.storageKeyRegex("updates").matches) + actualKeys should contain theSameElementsAs expectedKeys + // Confirm encoding-specific keys are in the correct order + ScanStorageConfig.Encoding.all.toList.foreach(e => + filterKeys(actualKeys, e) should contain theSameElementsInOrderAs filterKeys( + expectedKeys, + e, + ) ) probe.expectComplete() val objectCountMetrics = metricsFactory.metrics.counters .get(SpliceMetrics.MetricsPrefix :+ "history" :+ "bulk-storage" :+ "object-count") .value - val numObjectsFromMetric = objectCountMetrics - .get(MetricsContext.Empty) - .value - .markers - .get(MetricsContext("object_type" -> "updates")) - .value - .get() - numObjectsFromMetric shouldBe 2 + def numObjectsFromMetric(encoding: ScanStorageConfig.Encoding): Long = + objectCountMetrics + .get(MetricsContext.Empty) + .value + .markers + .get( + MetricsContext( + "object_type" -> "updates", + "encoding" -> encoding.key, + "bucket" -> "staging", + ) + ) + .value + .get() + numObjectsFromMetric(ScanStorageConfig.Encoding.CompactJson) shouldBe 2 + numObjectsFromMetric(ScanStorageConfig.Encoding.ProtobufJson) shouldBe 2 } clue("Check that the dumped content is correct") { @@ -134,27 +160,50 @@ class UpdateHistoryBulkStorageTest update.update.update.recordTime <= toTimestamp ) } yield { - val objectKeys = s3Objects.contents.asScala.map(_.key()).sorted - objectKeys should have length 2 - s3Objects.contents().get(0).size().toInt should be >= maxFileSize.toInt - val allUpdatesFromS3 = objectKeys.flatMap( - readUncompressAndDecode(bucketConnection, io.circe.parser.decode[UpdateHistoryItemV2]) - ) - allUpdatesFromS3.length shouldBe segmentUpdates.length - allUpdatesFromS3 - .map( - new CompactJsonScanHttpEncodings(identity, identity).httpToLapiUpdate - ) should contain theSameElementsInOrderAs segmentUpdates - /* We hard-code the expected digests to enforce that the persisted data format does not change. - These values must not be modified unless there is a conscious decision to change the persisted format, - with a migration plan for how to apply it consistently across SVs. */ - bucketConnection - .getChecksums(objectKeys.toSeq) - .futureValue - .map(_.checksum) should contain theSameElementsInOrderAs Seq( - "MM+DyxPP6UgpAaSCsm99j4ZAtYIK3TIrPmxFyodBrQQ=", - "2oWb5Um18xwnJTMkC4yilyrcsUADYoxtV7toJi29VsI=", - ) + def checkEncoding(encoding: ScanStorageConfig.Encoding) = { + /* We hard-code the expected digests to enforce that the persisted data format does not change. + These values must not be modified unless there is a conscious decision to change the persisted format, + with a migration plan for how to apply it consistently across SVs. */ + val (encodings, expectedDigests): (ScanHttpEncodings, Seq[String]) = encoding match { + case ScanStorageConfig.Encoding.CompactJson => + ( + new CompactJsonScanHttpEncodings(identity, identity), + Seq( + "MM+DyxPP6UgpAaSCsm99j4ZAtYIK3TIrPmxFyodBrQQ=", + "2oWb5Um18xwnJTMkC4yilyrcsUADYoxtV7toJi29VsI=", + ), + ) + case ScanStorageConfig.Encoding.ProtobufJson => + ( + ProtobufJsonScanHttpEncodings, + Seq( + "9QrYwnzkSce+GIh82uzY+1JHv4ukYC+llD0Idx1GDio=", + "pCOz8MG6Zoxup4NGnzBx48kFPm582cWn+GxWSZFyq+E=", + ), + ) + } + + val filteredS3Objects = s3Objects.contents.asScala + .filter(o => encoding.storageKeyRegex("updates").matches(o.key())) + val objectKeys = filteredS3Objects.map(_.key()).sorted + objectKeys should have length expectedDigests.length.toLong + filteredS3Objects(0).size().toInt should be >= maxFileSize.toInt + val allUpdatesFromS3 = objectKeys.flatMap( + readUncompressAndDecode(bucketConnection, io.circe.parser.decode[UpdateHistoryItemV2]) + ) + allUpdatesFromS3.length shouldBe segmentUpdates.length + allUpdatesFromS3 + .map( + encodings.httpToLapiUpdate + ) should contain theSameElementsInOrderAs segmentUpdates + bucketConnection + .getChecksums(objectKeys.toSeq) + .futureValue + .map(_.checksum) should contain theSameElementsInOrderAs expectedDigests + } + + checkEncoding(ScanStorageConfig.Encoding.CompactJson) + checkEncoding(ScanStorageConfig.Encoding.ProtobufJson) } } } @@ -382,28 +431,28 @@ class UpdateHistoryBulkStorageTest loggerFactory, ) - val d20u0 = "2015-10-20T00:00:00Z~2015-10-21T00:00:00Z/updates_0.zstd" - val d20u1 = "2015-10-20T00:00:00Z~2015-10-21T00:00:00Z/updates_1.zstd" - val d21u0 = "2015-10-21T00:00:00Z~2015-10-22T00:00:00Z/updates_0.zstd" - val d21u1 = "2015-10-21T00:00:00Z~2015-10-22T00:00:00Z/updates_1.zstd" - val d22u0 = "2015-10-22T00:00:00Z~2015-10-23T00:00:00Z/updates_0.zstd" - val d22u1 = "2015-10-22T00:00:00Z~2015-10-23T00:00:00Z/updates_1.zstd" - val d23u0 = "2015-10-23T00:00:00Z~2015-10-24T00:00:00Z/updates_0.zstd" - val d23u1 = "2015-10-23T00:00:00Z~2015-10-24T00:00:00Z/updates_1.zstd" - val d24u0 = "2015-10-24T00:00:00Z~2015-10-25T00:00:00Z/updates_0.zstd" - val d24u1 = "2015-10-24T00:00:00Z~2015-10-25T00:00:00Z/updates_1.zstd" - val allObjs = Seq( - d20u0, - d20u1, - d21u0, - d21u1, - d22u0, - d22u1, - d23u0, - d23u1, - d24u0, - d24u1, - ) + def makeObjectKeys(dates: String, prefix: String = "updates"): Seq[String] = + ScanStorageConfig.Encoding.all.toList.flatMap { encoding => + Seq(0, 1).map { i => + s"${dates}/${encoding.storageKey(prefix, i)}" + } + } + + def getCommitted(start: String, end: String, limit: Int, nextPageTokenO: Option[String]) = + reader.getCommittedUpdatesBetweenDates( + CantonTimestamp.tryFromInstant(Instant.parse(start)), + CantonTimestamp.tryFromInstant(Instant.parse(end)), + PageLimit.tryCreate(limit), + nextPageTokenO, + ScanStorageConfig.Encoding.all, + ) + + val d20 = makeObjectKeys("2015-10-20T00:00:00Z~2015-10-21T00:00:00Z") + val d21 = makeObjectKeys("2015-10-21T00:00:00Z~2015-10-22T00:00:00Z") + val d22 = makeObjectKeys("2015-10-22T00:00:00Z~2015-10-23T00:00:00Z") + val d23 = makeObjectKeys("2015-10-23T00:00:00Z~2015-10-24T00:00:00Z") + val d24 = makeObjectKeys("2015-10-24T00:00:00Z~2015-10-25T00:00:00Z") + val allObjs = d20 ++ d21 ++ d22 ++ d23 ++ d24 Future .sequence(allObjs.map { bucketConnection.createObject(_) @@ -411,108 +460,58 @@ class UpdateHistoryBulkStorageTest .futureValue // A wider range than the data - val res1 = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-10T00:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-30T00:00:00Z")), - PageLimit.tryCreate(10), - None, - ) - .futureValue - res1.objects.map(_.key) should contain theSameElementsInOrderAs Seq( - d20u0, - d20u1, - d21u0, - d21u1, - d22u0, - d22u1, - d23u0, - d23u1, - ) + val res1 = getCommitted("2015-10-10T00:00:00Z", "2015-10-30T00:00:00Z", 20, None).futureValue + res1.objects.map(_.key) should contain theSameElementsInOrderAs d20 ++ d21 ++ d22 ++ d23 res1.nextPageTokenO shouldBe Some("2015-10-23T00:00:00Z~2015-10-24T00:00:00Z/") - val res1b = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-10T00:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-30T00:00:00Z")), - PageLimit.tryCreate(10), - res1.nextPageTokenO, - ) - .futureValue + val res1b = getCommitted( + "2015-10-10T00:00:00Z", + "2015-10-30T00:00:00Z", + 20, + res1.nextPageTokenO, + ).futureValue res1b.objects.map(_.key) shouldBe empty res1b.nextPageTokenO shouldBe Some("2015-10-23T00:00:00Z~2015-10-24T00:00:00Z/") // A smaller range within the data - val res2 = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-21T16:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-21T16:00:05Z")), - PageLimit.tryCreate(10), - None, - ) - .futureValue - res2.objects.map(_.key) should contain theSameElementsInOrderAs Seq(d21u0, d21u1) + val res2 = getCommitted("2015-10-21T16:00:00Z", "2015-10-21T16:00:05Z", 20, None).futureValue + res2.objects.map(_.key) should contain theSameElementsInOrderAs d21 res2.nextPageTokenO shouldBe None // pagination - val res3 = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-01T12:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-21T16:00:05Z")), - PageLimit.tryCreate( - 3 - ), // on purpose 3 even though we expect only 2 back (since the response is always full days of updates) - None, - ) - .futureValue - res3.objects.map(_.key) should contain theSameElementsInOrderAs Seq(d20u0, d20u1) + val res3 = getCommitted( + "2015-10-01T12:00:00Z", + "2015-10-21T16:00:05Z", + 5, // on purpose 5 even though we expect only 4 back (since the response is always full days of updates) + None, + ).futureValue + res3.objects.map(_.key) should contain theSameElementsInOrderAs d20 res3.nextPageTokenO shouldBe Some("2015-10-20T00:00:00Z~2015-10-21T00:00:00Z/") - val res3b = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-01T12:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-21T16:00:05Z")), - PageLimit.tryCreate(3), - res3.nextPageTokenO, - ) - .futureValue - res3b.objects.map(_.key) should contain theSameElementsInOrderAs Seq(d21u0, d21u1) + val res3b = getCommitted( + "2015-10-01T12:00:00Z", + "2015-10-21T16:00:05Z", + 5, + res3.nextPageTokenO, + ).futureValue + res3b.objects.map(_.key) should contain theSameElementsInOrderAs d21 res3b.nextPageTokenO shouldBe None // exact match with start and end of segments - val res4 = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-21T00:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-23T00:00:00Z")), - PageLimit.tryCreate(4), - None, - ) - .futureValue + val res4 = getCommitted("2015-10-21T00:00:00Z", "2015-10-23T00:00:00Z", 8, None).futureValue res4.objects - .map(_.key) should contain theSameElementsInOrderAs Seq(d21u0, d21u1, d22u0, d22u1) + .map(_.key) should contain theSameElementsInOrderAs d21 ++ d22 res4.nextPageTokenO shouldBe None // limit too low for first folder - val ex = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-21T00:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-23T00:00:00Z")), - PageLimit.tryCreate(1), - None, - ) - .failed - .futureValue + val ex = + getCommitted("2015-10-21T00:00:00Z", "2015-10-23T00:00:00Z", 3, None).failed.futureValue ex shouldBe a[StatusRuntimeException] ex.asInstanceOf[StatusRuntimeException] .getStatus .getCode shouldBe io.grpc.Status.Code.INVALID_ARGUMENT // Test handling an empty segment: Simulate no updates in 2015-10-25 to 2015-10-26 - val d26u0 = "2015-10-26T00:00:00Z~2015-10-27T00:00:00Z/updates_0.zstd" - val d26u1 = "2015-10-26T00:00:00Z~2015-10-27T00:00:00Z/updates_1.zstd" - val moreObjs = Seq( - "2015-10-25T00:00:00Z~2015-10-26T00:00:00Z/ACS_0.zstd", - d26u0, - d26u1, - ) + val d26 = makeObjectKeys("2015-10-26T00:00:00Z~2015-10-27T00:00:00Z") + val moreObjs = makeObjectKeys("2015-10-25T00:00:00Z~2015-10-26T00:00:00Z", "ACS") ++ d26 Future .sequence(moreObjs.map { bucketConnection.createObject(_) @@ -546,25 +545,16 @@ class UpdateHistoryBulkStorageTest ) ) // Query up to the middle of the empty segment - val res5 = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-20T00:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-25T12:00:00Z")), - PageLimit.tryCreate(20), - None, - ) - .futureValue + val res5 = getCommitted("2015-10-20T00:00:00Z", "2015-10-25T12:00:00Z", 20, None).futureValue // First response contains all data, but with a next page token res5.objects.map(_.key) should contain theSameElementsInOrderAs allObjs res5.nextPageTokenO shouldBe Some("2015-10-24T00:00:00Z~2015-10-25T00:00:00Z/") - val res5b = reader - .getCommittedUpdatesBetweenDates( - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-21T00:00:00Z")), - CantonTimestamp.tryFromInstant(Instant.parse("2015-10-25T12:00:00Z")), - PageLimit.tryCreate(20), - res5.nextPageTokenO, - ) - .futureValue + val res5b = getCommitted( + "2015-10-21T00:00:00Z", + "2015-10-25T12:00:00Z", + 20, + res5.nextPageTokenO, + ).futureValue // Second page should be empty, with no nextPageToken res5b.objects.map(_.key) shouldBe empty res5b.nextPageTokenO shouldBe None @@ -596,17 +586,15 @@ class UpdateHistoryBulkStorageTest val store = mock[UpdateHistory] when( store.getUpdatesWithoutImportUpdates( - any[Option[(Long, CantonTimestamp)]], + any[Option[TimestampWithMigrationId]], any[Limit], )(any[TraceContext]) ).thenAnswer { ( - afterO: Option[(Long, CantonTimestamp)], + afterO: Option[TimestampWithMigrationId], limit: Limit, ) => - val after = afterO - .map(a => TimestampWithMigrationId(a._2, a._1)) - .getOrElse(TimestampWithMigrationId(CantonTimestamp.MinValue, 0L)) + val after = afterO.getOrElse(TimestampWithMigrationId(CantonTimestamp.MinValue, 0L)) Future.successful( data .filter(update => diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/ScanHistoryBackfillingTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/ScanHistoryBackfillingTest.scala index 4ced0ea26c..24d2739446 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/ScanHistoryBackfillingTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/ScanHistoryBackfillingTest.scala @@ -22,7 +22,7 @@ class ScanHistoryBackfillingTest extends UpdateHistoryTestBase { "ScanHistoryBackfilling" should { "backfill from one complete history" in { for { - testData <- setup() + testData <- setupTestData() // Backfill backfillingTerminated <- backfillAll( @@ -93,7 +93,7 @@ class ScanHistoryBackfillingTest extends UpdateHistoryTestBase { "backfill from one incomplete history" in { for { - testData <- setup() + testData <- setupTestData() // Backfill part 1 - at this point, the destination history has only replicated up to record time 5 backfillingTerminated1 <- backfillAll( @@ -153,7 +153,7 @@ class ScanHistoryBackfillingTest extends UpdateHistoryTestBase { destinationHistory: UpdateHistory, ) - private def setup(): Future[TestData] = { + private def setupTestData(): Future[TestData] = { val storeA0 = mkStore(domainMigrationId = 0, participantId = participant1) val storeA1 = mkStore(domainMigrationId = 1, participantId = participant1) val storeA2 = mkStore(domainMigrationId = 2, participantId = participant1) diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/AcsSnapshotStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/AcsSnapshotStoreTest.scala index 4c27a6c133..c3574b6cfd 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/AcsSnapshotStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/AcsSnapshotStoreTest.scala @@ -432,7 +432,7 @@ class AcsSnapshotStoreTest def queryRecursive( store: AcsSnapshotStore, - after: Option[Long], + after: Option[AcsSnapshotStore.QueryAcsSnapshotPaginationToken], acc: Vector[String], partyIds: Seq[PartyId], templates: Seq[PackageQualifiedName], diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala index 686f647f03..0e2ad85086 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala @@ -24,7 +24,14 @@ import org.lfdecentralizedtrust.splice.environment.ledger.api.TreeUpdateOrOffset import org.lfdecentralizedtrust.splice.environment.{DarResources, RetryProvider} import org.lfdecentralizedtrust.splice.scan.store.ScanRewardsReferenceStore import org.lfdecentralizedtrust.splice.scan.store.db.DbScanRewardsReferenceStore -import org.lfdecentralizedtrust.splice.store.{HardLimit, Limit, PageLimit, StoreTestBase, TcsStore} +import org.lfdecentralizedtrust.splice.store.{ + HardLimit, + Limit, + PageLimit, + StoreTestBase, + TcsStore, + TimestampWithMigrationId, +} import org.lfdecentralizedtrust.splice.util.{ResourceTemplateDecoder, TemplateJsonDecoder} import slick.jdbc.JdbcProfile @@ -479,10 +486,16 @@ class DbScanRewardsReferenceStoreTest result.get(ts(275)) shouldBe None // round4.opensAt before earliest archived_at result.get(ts(350)) shouldBe None // round4.opensAt before earliest archived_at result.get(ts(375)) shouldBe None // gap: round4 archived, round5 not yet open - result(ts(400)) shouldBe (5L, ts(400)) + result(ts(400)) shouldBe TimestampWithMigrationId(ts(400), 5L) result.get(ts(401)) shouldBe None // 401 was not present in request - result(ts(450)) shouldBe (5L, ts(400)) // round5 open, round6 not yet open - result(ts(550)) shouldBe (5L, ts(400)) // both open, lowest round selected + result(ts(450)) shouldBe TimestampWithMigrationId( + ts(400), + 5L, + ) // round5 open, round6 not yet open + result(ts(550)) shouldBe TimestampWithMigrationId( + ts(400), + 5L, + ) // both open, lowest round selected } } } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala index 220a8d562c..3d2271e786 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala @@ -57,7 +57,6 @@ import scala.concurrent.Future import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* import scala.math.BigDecimal.javaBigDecimal2bigDecimal -import scala.reflect.ClassTag import org.lfdecentralizedtrust.splice.config.IngestionConfig import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.IngestionSink.IngestionStart.{ InitializeAcsAtLatestOffset, @@ -332,144 +331,6 @@ abstract class ScanStoreTest } } - "listTransactions" should { - "return the most recent txs in pages" in { - val limit = 10 - val nrTransfers = 20 - val round = 1L - val now = java.time.Instant.EPOCH - val zero = BigDecimal(0) - val fakeOffset = "0" - val txs: List[TransferTxLogEntry] = (1 to nrTransfers).map { i => - TransferTxLogEntry( - offset = fakeOffset, - eventId = s"$i", - domainId = dummyDomain, - date = Some(now), - sender = Some( - SenderAmount( - user1, - BigDecimal(i), - zero, - zero, - zero, - zero, - zero, - zero, - Some(zero), - None, - ) - ), - balanceChanges = Seq(), - receivers = Seq(ReceiverAmount(user2, BigDecimal(i), zero)), - round = round, - ) - }.toList - def stripEventIdAndOffset(tx: TransferTxLogEntry) = - tx.copy(eventId = "", offset = fakeOffset) - val expectedFirstPage = txs.reverse.take(limit).toList - val expectedSecondPage = txs.reverse.drop(limit).take(limit).toList - - def transferFromTransaction( - store: ScanStore, - amuletRulesContract: Contract[ - splice.amuletrules.AmuletRules.ContractId, - splice.amuletrules.AmuletRules, - ], - tx: TransferTxLogEntry, - ) = { - val sender = tx.sender.getOrElse(throw txMissingField()) - val senderParty = sender.party - val senderAmount = sender.inputAmuletAmount - val receiverParty = tx.receivers(0).party - val receiverAmount = tx.receivers(0).amount - dummyDomain - .exercise( - contract = amuletRulesContract, - interfaceId = Some(splice.amuletrules.AmuletRules.TEMPLATE_ID_WITH_PACKAGE_ID), - choiceName = Transfer.choice.name, - choiceArgument = mkAmuletRules_Transfer( - mkTransferInputOutput( - senderParty, - senderParty, - List(mkInputAmulet()), - List(mkTransferOutput(receiverParty, receiverAmount)), - ) - ), - exerciseResult = mkTransferResultRecord( - round = round, - inputAppRewardAmount = sender.inputAppRewardAmount.toDouble, - inputAmuletAmount = senderAmount.toDouble, - inputValidatorRewardAmount = sender.inputValidatorRewardAmount.toDouble, - inputSvRewardAmount = sender.inputSvRewardAmount.fold(0.0)(_.toDouble), - balanceChanges = Map(), - amuletPrice = 1.0, - ), - )( - store.multiDomainAcsStore - ) - .map(_ => ()) - } - - for { - store <- mkStore() - amuletRulesContract = amuletRules() - _ <- txs.foldLeft(Future.successful(())) { (f, tx) => - f.flatMap { _ => - transferFromTransaction( - store, - amuletRulesContract, - tx, - ) - } - } - } yield { - val firstPageDescending = store - .listByType[TransferTxLogEntry](None, SortOrder.Descending, limit) - .futureValue - .toList - - firstPageDescending - .map(stripEventIdAndOffset) should be( - expectedFirstPage - .map(stripEventIdAndOffset) - ) - val nextPageDescending = store - .listByType[TransferTxLogEntry]( - Some(firstPageDescending.last.eventId), - SortOrder.Descending, - limit, - ) - .futureValue - .toList - - nextPageDescending - .map(stripEventIdAndOffset) should be( - expectedSecondPage - .map(stripEventIdAndOffset) - ) - - val firstPageAscending = store - .listByType[TransferTxLogEntry](None, SortOrder.Ascending, limit) - .futureValue - .toList - - firstPageAscending should be(nextPageDescending.reverse) - - val nextPageAscending = store - .listByType[TransferTxLogEntry]( - Some(firstPageAscending.last.eventId), - SortOrder.Ascending, - limit, - ) - .futureValue - .toList - - nextPageAscending should be(firstPageDescending.reverse) - } - } - } - "votes" should { "listVoteRequestResults" should { @@ -515,19 +376,11 @@ abstract class ScanStoreTest _ <- closeVoteRequest(store, 2) _ <- closeVoteRequest(store, 1) page1 <- store.listVoteRequestResults( - None, - None, - None, - None, - None, + VoteResultsFilters(), PageLimit.tryCreate(3), ) page2 <- store.listVoteRequestResults( - None, - None, - None, - None, - None, + VoteResultsFilters(), PageLimit.tryCreate(3), page1.nextPageToken, ) @@ -585,19 +438,11 @@ abstract class ScanStoreTest _ <- closeVoteRequest(store, 2) _ <- closeVoteRequest(store, 6) page1 <- store.listVoteRequestResults( - None, - None, - None, - None, - None, + VoteResultsFilters(), PageLimit.tryCreate(3), ) page2 <- store.listVoteRequestResults( - None, - None, - None, - None, - None, + VoteResultsFilters(), PageLimit.tryCreate(3), page1.nextPageToken, ) @@ -610,6 +455,60 @@ abstract class ScanStoreTest } } + "countVoteRequestResults" should { + + "count vote results matching the filters" in { + val base = Instant.parse("2024-03-01T10:00:00Z") + val accepted = Set(1, 3, 4, 6) + def sortKeyAt(n: Int) = base.plusSeconds(n.toLong) + def recordTime(n: Int) = base.plusSeconds(100L + n.toLong) + val voteRequests = (1 to 6).map { n => + voteRequest( + requester = userParty(n), + votes = Seq( + new Vote(userParty(n).toProtoPrimitive, true, new Reason("", ""), Optional.empty()) + ), + ) + } + val results = + (1 to 6).map(n => + if (accepted(n)) mkVoteRequestResult(voteRequests(n - 1), effectiveAt = sortKeyAt(n)) + else mkRejectedVoteRequestResult(voteRequests(n - 1), completedAt = sortKeyAt(n)) + ) + def closeVoteRequest(store: ScanStore, n: Int) = + dummyDomain.exercise( + contract = dsoRules(dsoParty), + interfaceId = Some(DsoRules.TEMPLATE_ID_WITH_PACKAGE_ID), + choiceName = DsoRulesCloseVoteRequest.choice.name, + choiceArgument = mkCloseVoteRequest(voteRequests(n - 1).contractId), + exerciseResult = results(n - 1).toValue, + recordTime = recordTime(n), + )(store.multiDomainAcsStore) + for { + store <- mkStore() + _ <- MonadUtil.sequentialTraverse(voteRequests)( + dummyDomain.create(_)(store.multiDomainAcsStore) + ) + _ <- MonadUtil.sequentialTraverse(1 to 6)(closeVoteRequest(store, _)) + total <- store.countVoteRequestResults(VoteResultsFilters()) + acceptedCount <- store.countVoteRequestResults( + VoteResultsFilters(accepted = Some(true)) + ) + rejectedCount <- store.countVoteRequestResults( + VoteResultsFilters(accepted = Some(false)) + ) + requesterCount <- store.countVoteRequestResults( + VoteResultsFilters(requester = Some(userParty(1).toProtoPrimitive)) + ) + } yield { + total shouldBe 6L + acceptedCount shouldBe 4L + rejectedCount shouldBe 2L + requesterCount shouldBe 1L + } + } + } + "lookupLatestSvRewardWeightChange" should { "return the weight of the latest accepted UpdateSvRewardWeight before the given time" in { @@ -1302,11 +1201,7 @@ abstract class ScanStoreTest } yield { store .listVoteRequestResults( - Some("AddSv"), - Some(true), - None, - None, - None, + VoteResultsFilters(actionName = Some("AddSv"), accepted = Some(true)), PageLimit.tryCreate(1), ) .futureValue @@ -1315,11 +1210,7 @@ abstract class ScanStoreTest .loneElement shouldBe result2 store .listVoteRequestResults( - Some("SRARC_AddSv"), - Some(false), - None, - None, - None, + VoteResultsFilters(actionName = Some("SRARC_AddSv"), accepted = Some(false)), PageLimit.tryCreate(1), ) .futureValue @@ -1328,11 +1219,7 @@ abstract class ScanStoreTest .size shouldBe (0) store .listVoteRequestResults( - None, - None, - None, - None, - None, + VoteResultsFilters(), PageLimit.tryCreate(1), ) .futureValue @@ -1341,11 +1228,9 @@ abstract class ScanStoreTest .size shouldBe (1) store .listVoteRequestResults( - None, - None, - None, - Some(Instant.now().truncatedTo(ChronoUnit.MICROS).plusSeconds(3600).toString), - None, + VoteResultsFilters(effectiveFrom = + Some(Instant.now().truncatedTo(ChronoUnit.MICROS).plusSeconds(3600).toString) + ), PageLimit.tryCreate(1), ) .futureValue @@ -1354,11 +1239,9 @@ abstract class ScanStoreTest .size shouldBe (0) store .listVoteRequestResults( - None, - None, - None, - Some(Instant.now().truncatedTo(ChronoUnit.MICROS).minusSeconds(3600).toString), - None, + VoteResultsFilters(effectiveFrom = + Some(Instant.now().truncatedTo(ChronoUnit.MICROS).minusSeconds(3600).toString) + ), PageLimit.tryCreate(1), ) .futureValue @@ -1379,20 +1262,6 @@ abstract class ScanStoreTest ): Future[UpdateHistory] private lazy val user1 = userParty(1) - private lazy val user2 = userParty(2) - - implicit class ScanStoreExt(store: ScanStore) { - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - def listByType[T](beginAfterEventId: Option[String], sortOrder: SortOrder, limit: Int)(implicit - tag: ClassTag[T] - ): Future[Seq[T]] = { - store - .listTransactions(beginAfterEventId, sortOrder, PageLimit.tryCreate(limit)) - .map(_.collect { - case c if tag.runtimeClass.isInstance(c) => c.asInstanceOf[T] - }.toSeq) - } - } } trait AmuletTransferUtil { self: StoreTestBase => def mkInputAmulet() = { @@ -2014,11 +1883,7 @@ class DbScanStoreTest // because ingestion in these store tests is simulated by directly interacting with the ingestion sink storeReingest .listVoteRequestResults( - Some("AddSv"), - Some(true), - None, - None, - None, + VoteResultsFilters(actionName = Some("AddSv"), accepted = Some(true)), PageLimit.tryCreate(1), ) .futureValue diff --git a/apps/splitwell/src/main/scala/org/lfdecentralizedtrust/splice/splitwell/metrics/SplitwellAppMetrics.scala b/apps/splitwell/src/main/scala/org/lfdecentralizedtrust/splice/splitwell/metrics/SplitwellAppMetrics.scala index 0e091cf30c..44e266e0d8 100644 --- a/apps/splitwell/src/main/scala/org/lfdecentralizedtrust/splice/splitwell/metrics/SplitwellAppMetrics.scala +++ b/apps/splitwell/src/main/scala/org/lfdecentralizedtrust/splice/splitwell/metrics/SplitwellAppMetrics.scala @@ -14,6 +14,6 @@ import com.digitalasset.canton.metrics.DbStorageHistograms */ class SplitwellAppMetrics( metricsFactory: LabeledMetricsFactory, - storageHistograms: DbStorageHistograms, + histograms: DbStorageHistograms, loggerFactory: NamedLoggerFactory, -) extends BaseSpliceMetrics("splitwell", metricsFactory, storageHistograms, loggerFactory) {} +) extends BaseSpliceMetrics("splitwell", metricsFactory, histograms, loggerFactory) {} diff --git a/apps/sv/frontend/index.html b/apps/sv/frontend/index.html index 8cf03993b2..32252600a0 100644 --- a/apps/sv/frontend/index.html +++ b/apps/sv/frontend/index.html @@ -5,16 +5,13 @@ + - + diff --git a/apps/sv/frontend/src/App.tsx b/apps/sv/frontend/src/App.tsx index 2ec65affcb..812a5385cf 100644 --- a/apps/sv/frontend/src/App.tsx +++ b/apps/sv/frontend/src/App.tsx @@ -39,6 +39,7 @@ import { useConfigPollInterval, useSvConfig } from './utils'; import { Governance } from './routes/governance'; import { VoteRequestDetails } from './routes/voteRequestDetails'; import { CreateProposal } from './routes/createProposal'; +import DelegateElection from './routes/delegateElection'; const Providers: React.FC = ({ children }) => { const config = useSvConfig(); @@ -95,6 +96,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> @@ -111,8 +113,8 @@ const App: React.FC = () => { - Super Validator Operations - + Supervalidator Operations + diff --git a/apps/sv/frontend/src/__tests__/components/copyable-identifier.test.tsx b/apps/sv/frontend/src/__tests__/components/copyable-identifier.test.tsx new file mode 100644 index 0000000000..90a415352d --- /dev/null +++ b/apps/sv/frontend/src/__tests__/components/copyable-identifier.test.tsx @@ -0,0 +1,159 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { GlobalStyles, ThemeProvider } from '@mui/material'; +import { PartyId, theme } from '@canton-network/splice-common-frontend'; +import { describe, expect, test } from 'vitest'; + +import CopyableIdentifier from '../../components/beta/CopyableIdentifier'; +import MemberIdentifier from '../../components/beta/MemberIdentifier'; +import { partyIdScrollGlobalStyles } from '../../components/beta/identifierStyles'; +import PartyIdScrollTracks from '../../components/PartyIdScrollTracks'; + +const LONG_CONTRACT_ID = + '00deadbeefcafebabe0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + +const LONG_PARTY_ID = `digital-asset-2::12200eab17c2b87a3da9f7b3b81d371ff794a4515fa3a0b258422a251d6148b031d`; + +const NarrowContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +

{children}
+); + +describe('CopyableIdentifier', () => { + test('renders the full contract ID for horizontal scrolling', () => { + render(); + + expect(screen.getByTestId('contract-id-value')).toHaveTextContent(LONG_CONTRACT_ID); + expect(screen.getByTestId('contract-id-scroll')).toHaveStyle({ overflowX: 'auto' }); + }); + + test('keeps the copy button adjacent to short identifiers', () => { + render( +
+ +
+ ); + + expect(screen.getByTestId('short-id')).toHaveStyle({ + display: 'inline-flex', + width: 'fit-content', + }); + }); + + test('compact scroll keeps #1785 scrolling at the Figma width', async () => { + render( + + + + ); + + const scroll = screen.getByTestId('contract-id-scroll'); + expect(scroll).toHaveStyle({ overflowX: 'auto', maxWidth: '270px' }); + expect(screen.getByTestId('contract-id-value')).toHaveTextContent(LONG_CONTRACT_ID); + + Object.defineProperty(scroll, 'scrollWidth', { configurable: true, value: 400 }); + Object.defineProperty(scroll, 'clientWidth', { configurable: true, value: 100 }); + Object.defineProperty(scroll, 'scrollLeft', { configurable: true, value: 0 }); + fireEvent.scroll(scroll); + + await waitFor(() => { + expect(screen.queryByTestId('contract-id-ellipsis-cue')).not.toBeInTheDocument(); + expect(screen.getByTestId('contract-id-scroll-track')).toBeInTheDocument(); + }); + }); + + test('fullWidth fills the parent and keeps scrolling', () => { + render( +
+ +
+ ); + + expect(screen.getByTestId('contract-id')).toHaveStyle({ width: '100%', display: 'flex' }); + expect(screen.getByTestId('contract-id-scroll')).toHaveStyle({ overflowX: 'auto' }); + expect(screen.getByTestId('contract-id-value')).toHaveTextContent(LONG_CONTRACT_ID); + }); + + test('trims long identifiers to the Figma ellipsis width without a narrow parent', () => { + render( + + ); + + expect(screen.getByTestId('contract-id-value')).toHaveTextContent(LONG_CONTRACT_ID); + expect(screen.getByTestId('contract-id-value')).toHaveAttribute('title', LONG_CONTRACT_ID); + expect(screen.getByTestId('contract-id-ellipsis')).toHaveStyle({ + overflow: 'hidden', + maxWidth: '270px', + }); + expect(screen.getByTestId('contract-id-value')).toHaveStyle({ textOverflow: 'ellipsis' }); + }); +}); + +describe('MemberIdentifier', () => { + test('renders the full party ID instead of an abbreviated preview', () => { + render( + + ); + + expect(screen.getByTestId('member-value')).toHaveTextContent(LONG_PARTY_ID); + expect(screen.getByTestId('member-value')).not.toHaveTextContent('...'); + expect(screen.getByTestId('member-scroll')).toHaveStyle({ overflowX: 'auto' }); + }); + + test('supports ellipsis overflow for compact layouts', () => { + render( + + + + ); + + expect(screen.getByTestId('member-value')).toHaveTextContent(LONG_PARTY_ID); + expect(screen.getByTestId('member-value')).toHaveAttribute('title', LONG_PARTY_ID); + expect(screen.getByTestId('member-ellipsis')).toHaveStyle({ overflow: 'hidden' }); + expect(screen.getByTestId('member-value')).toHaveStyle({ textOverflow: 'ellipsis' }); + expect(screen.queryByTestId('member-scroll')).not.toBeInTheDocument(); + }); +}); + +describe('common PartyId', () => { + test('does not ellipsize the party ID when SV scroll styles are applied', async () => { + render( + + + + + + + + ); + + const input = await screen.findByTestId('sv-party-id-input'); + const partyIdRoot = input.closest('.party-id'); + + expect(input).toHaveDisplayValue(LONG_PARTY_ID); + expect(input).toHaveStyle({ textOverflow: 'clip' }); + expect(partyIdRoot).toHaveClass('identifier-scroll-area'); + expect(partyIdRoot?.querySelector('.party-id-scroll-track')).not.toBeNull(); + }); +}); diff --git a/apps/sv/frontend/src/__tests__/governance/action-required-section.test.tsx b/apps/sv/frontend/src/__tests__/governance/action-required-section.test.tsx index 8a72cf3096..b5bd2e3e19 100644 --- a/apps/sv/frontend/src/__tests__/governance/action-required-section.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/action-required-section.test.tsx @@ -11,24 +11,27 @@ import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules' import { MemoryRouter } from 'react-router'; import dayjs from 'dayjs'; import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils'; +import { svPartyId, voteRequests } from '../mocks/constants'; + +const sampleContractId = voteRequests.dso_rules_vote_requests[0] + .contract_id as ContractId; const requests: ActionRequiredData[] = [ { actionName: 'Feature Application', description: 'Test description for feature application', - contractId: '2abcde123456' as ContractId, + contractId: sampleContractId, votingCloses: '2024-09-25 11:00', createdAt: '2024-09-25 11:00', - requester: 'sv1', + requester: svPartyId, }, { actionName: 'Set DSO Rules Configuration', description: 'Test description for DSO rules configuration', - contractId: '2bcde123456' as ContractId, + contractId: voteRequests.dso_rules_vote_requests[1].contract_id as ContractId, votingCloses: '2024-09-25 11:00', createdAt: '2024-09-25 11:00', - requester: 'sv2', - isYou: true, + requester: svPartyId, }, ]; @@ -76,10 +79,10 @@ describe('Action Required', () => { const actionRequired = { actionName: 'Feature Application', description: 'Test description', - contractId: '2abcde123456' as ContractId, + contractId: sampleContractId, votingCloses: closesDate, createdAt: createdDate, - requester: 'sv1', + requester: svPartyId, }; render( @@ -104,23 +107,22 @@ describe('Action Required', () => { expect(votingCloses).toBeInTheDocument(); expect(votingCloses.textContent).toBe('10 days'); - const requester = screen.getByTestId('action-required-requester-identifier-value'); - expect(requester).toBeInTheDocument(); - expect(requester.textContent).toBe(actionRequired.requester); + const submittedBy = screen.getByTestId('action-required-submitted-by-identifier-value'); + expect(submittedBy).toBeInTheDocument(); + expect(submittedBy.textContent).toBe(svPartyId); const viewDetails = screen.getByTestId('action-required-view-details'); expect(viewDetails).toBeInTheDocument(); }); - test('should render isYou badge for requests created by viewing sv', () => { + test('should render submitted by with copy button and no You badge', () => { const actionRequired = { actionName: 'Feature Application', description: 'Test description', - contractId: '2abcde123456' as ContractId, + contractId: sampleContractId, votingCloses: '2029-09-25 11:00', createdAt: '2029-09-25 11:00', - requester: 'sv1', - isYou: true, + requester: svPartyId, }; render( @@ -129,18 +131,22 @@ describe('Action Required', () => { ); - const isYou = screen.getByTestId('action-required-requester-identifier-badge'); - expect(isYou).toBeInTheDocument(); + expect( + screen.getByTestId('action-required-submitted-by-identifier-copy-button') + ).toBeInTheDocument(); + expect( + screen.queryByTestId('action-required-submitted-by-identifier-badge') + ).not.toBeInTheDocument(); }); - test('should not render isYou badge for requests created by other svs', () => { + test('should render vote proposal contract id with full value', () => { const actionRequired = { actionName: 'Feature Application', description: 'Test description', - contractId: '2abcde123456' as ContractId, + contractId: sampleContractId, votingCloses: '2029-09-25 11:00', createdAt: '2029-09-25 11:00', - requester: 'sv1', + requester: svPartyId, }; render( @@ -149,8 +155,8 @@ describe('Action Required', () => { ); - const isYou = screen.queryByTestId('action-required-requester-identifier-badge'); - - expect(isYou).not.toBeInTheDocument(); + expect(screen.getByTestId('action-required-contract-id-value').textContent).toBe( + sampleContractId + ); }); }); diff --git a/apps/sv/frontend/src/__tests__/governance/create-proposal.test.tsx b/apps/sv/frontend/src/__tests__/governance/create-proposal.test.tsx index 599a7fcbb3..d85fb78cdc 100644 --- a/apps/sv/frontend/src/__tests__/governance/create-proposal.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/create-proposal.test.tsx @@ -38,11 +38,8 @@ async function checkActionSelection(actionName: string, actionValue: string, tes const selectInput = actionDropdown.querySelector('[role="combobox"]') as HTMLElement; await user.click(selectInput); - await waitFor(async () => { - const actionToSelect = screen.getByText(actionName); - expect(actionToSelect).toBeInTheDocument(); - await user.click(actionToSelect); - }); + const actionToSelect = await screen.findByText(actionName); + await user.click(actionToSelect); const nextButton = screen.getByText('Next'); expect(nextButton).toBeInTheDocument(); @@ -92,9 +89,6 @@ describe('Create Proposal', () => { ); - const actionSelectionTitle = screen.getByText('Select an Action'); - expect(actionSelectionTitle).toBeDefined(); - const actionDropdown = screen.getByTestId('select-action'); expect(actionDropdown).toBeDefined(); @@ -194,20 +188,19 @@ describe('Create Proposal', () => { const nextButton = screen.getByText('Next'); expect(nextButton).toBeDefined(); - expect(nextButton.getAttribute('disabled')).toBeDefined(); + expect(nextButton.getAttribute('disabled')).not.toBeNull(); const actionDropdown = screen.getByTestId('select-action'); expect(actionDropdown).toBeDefined(); const selectInput = actionDropdown.querySelector('[role="combobox"]') as HTMLElement; - user.click(selectInput); + await user.click(selectInput); + + const actionToSelect = await screen.findByText('Offboard Member'); + await user.click(actionToSelect); await waitFor(() => { - const actionToSelect = screen.getByText('Offboard Member'); - expect(actionToSelect).toBeDefined(); - user.click(actionToSelect); + expect(nextButton.getAttribute('disabled')).toBeNull(); }); - - expect(nextButton.getAttribute('disabled')).toBe(''); }); }); diff --git a/apps/sv/frontend/src/__tests__/governance/forms/create-unallocated-unclaimed-activity-form.test.tsx b/apps/sv/frontend/src/__tests__/governance/forms/create-unallocated-unclaimed-activity-form.test.tsx index bb124965f5..67858a367d 100644 --- a/apps/sv/frontend/src/__tests__/governance/forms/create-unallocated-unclaimed-activity-form.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/forms/create-unallocated-unclaimed-activity-form.test.tsx @@ -10,7 +10,11 @@ import { describe, expect, test } from 'vitest'; import App from '../../../App'; import { CreateUnallocatedUnclaimedActivityRecordForm } from '../../../components/forms/CreateUnallocatedUnclaimedActivityRecordForm'; import { SvConfigProvider } from '../../../utils'; -import { PROPOSAL_SUMMARY_SUBTITLE, PROPOSAL_SUMMARY_TITLE } from '../../../utils/constants'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + PROPOSAL_REVIEW_TITLE, + PROPOSAL_SUMMARY_SUBTITLE, +} from '../../../utils/constants'; import { Wrapper } from '../../helpers'; import { svPartyId } from '../../mocks/constants'; import { server, svUrl } from '../../setup/setup'; @@ -32,7 +36,7 @@ describe('SV user can', () => { const button = screen.getByRole('button', { name: 'Log In' }); await user.click(button); - expect(await screen.findAllByDisplayValue(svPartyId)).not.toBe([]); + expect(await screen.findAllByDisplayValue(svPartyId)).not.toHaveLength(0); }); }); @@ -47,7 +51,7 @@ describe('Create Unallocated Unclaimed Activity Record Form', () => { expect( screen.getByTestId('create-unallocated-unclaimed-activity-record-form') ).toBeInTheDocument(); - expect(screen.getByText('Proposal type')).toBeInTheDocument(); + expect(screen.getByText(CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE)).toBeInTheDocument(); const actionInput = screen.getByTestId('create-unallocated-unclaimed-activity-record-action'); expect(actionInput).toBeInTheDocument(); @@ -99,7 +103,7 @@ describe('Create Unallocated Unclaimed Activity Record Form', () => { expect(submitButton).toBeInTheDocument(); await user.click(submitButton); - expect(submitButton.getAttribute('disabled')).toBeDefined(); + expect(submitButton.getAttribute('disabled')).not.toBeNull(); await expect(async () => await user.click(submitButton)).rejects.toThrowError( /Unable to perform pointer interaction/ ); @@ -368,7 +372,7 @@ describe('Create Unallocated Unclaimed Activity Record Form', () => { await user.click(submitButton); - expect(screen.getByText(PROPOSAL_SUMMARY_TITLE)).toBeInTheDocument(); + expect(screen.getByText(PROPOSAL_REVIEW_TITLE)).toBeInTheDocument(); }); test('should show error on form if submission fails', async () => { diff --git a/apps/sv/frontend/src/__tests__/governance/forms/grant-revoke-featured-app-form.test.tsx b/apps/sv/frontend/src/__tests__/governance/forms/grant-revoke-featured-app-form.test.tsx index e13c9d4aaf..aaca03db31 100644 --- a/apps/sv/frontend/src/__tests__/governance/forms/grant-revoke-featured-app-form.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/forms/grant-revoke-featured-app-form.test.tsx @@ -13,7 +13,12 @@ import dayjs from 'dayjs'; import { GrantRevokeFeaturedAppForm } from '../../../components/forms/GrantRevokeFeaturedAppForm'; import { server, svUrl } from '../../setup/setup'; import { http, HttpResponse } from 'msw'; -import { PROPOSAL_SUMMARY_SUBTITLE, PROPOSAL_SUMMARY_TITLE } from '../../../utils/constants'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + CREATE_PROPOSAL_LABEL_PROVIDER_PARTY_ID, + PROPOSAL_REVIEW_TITLE, + PROPOSAL_SUMMARY_SUBTITLE, +} from '../../../utils/constants'; describe('SV user can', () => { test('login and see the SV party ID', async () => { @@ -32,7 +37,7 @@ describe('SV user can', () => { const button = screen.getByRole('button', { name: 'Log In' }); await user.click(button); - expect(await screen.findAllByDisplayValue(svPartyId)).not.toBe([]); + expect(await screen.findAllByDisplayValue(svPartyId)).not.toHaveLength(0); }); }); @@ -45,7 +50,7 @@ describe('Grant Featured App Form', () => { ); expect(screen.getByTestId('grant-featured-app-form')).toBeInTheDocument(); - expect(screen.getByText('Proposal type')).toBeInTheDocument(); + expect(screen.getByText(CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE)).toBeInTheDocument(); const actionInput = screen.getByTestId('grant-featured-app-action'); expect(actionInput).toBeInTheDocument(); @@ -69,7 +74,7 @@ describe('Grant Featured App Form', () => { const providerInput = screen.getByTestId('grant-featured-app-idValue-title'); expect(providerInput).toBeInTheDocument(); - expect(providerInput.textContent).toBe('Provider Party ID'); + expect(providerInput.textContent).toBe(CREATE_PROPOSAL_LABEL_PROVIDER_PARTY_ID); expect(screen.getByText('Review Proposal')).toBeInTheDocument(); }); @@ -212,7 +217,142 @@ describe('Grant Featured App Form', () => { await user.click(submitButton); - expect(screen.getByText(PROPOSAL_SUMMARY_TITLE)).toBeInTheDocument(); + expect(screen.getByText(PROPOSAL_REVIEW_TITLE)).toBeInTheDocument(); + }); + + test('activity weight is optional and is sent to backend as null when left blank', async () => { + let requestBody = ''; + server.use( + http.post(`${svUrl}/v0/admin/sv/voterequest/create`, async ({ request }) => { + requestBody = await request.text(); + return HttpResponse.json({}); + }) + ); + + const user = userEvent.setup(); + + render( + + + + ); + + const activityWeightInput = screen.getByTestId('grant-featured-app-activityWeight'); + expect(activityWeightInput.getAttribute('value')).toBe(''); + + const actionInput = screen.getByTestId('grant-featured-app-action'); + const submitButton = screen.getByTestId('submit-button'); + + const summaryInput = screen.getByTestId('grant-featured-app-summary'); + await user.type(summaryInput, 'Summary of the proposal'); + + const urlInput = screen.getByTestId('grant-featured-app-url'); + await user.type(urlInput, 'https://example.com'); + + const providerInput = screen.getByTestId('grant-featured-app-idValue'); + await user.type(providerInput, 'a-party-id::1014912492'); + + await user.click(activityWeightInput); + + await user.click(actionInput); // using this to trigger the onBlur event which triggers the validation + + await waitFor(() => { + expect(screen.queryByText('Validating provider...')).not.toBeInTheDocument(); + }); + + await waitFor(async () => { + expect(submitButton).not.toBeDisabled(); + }); + + await user.click(submitButton); // review proposal + + expect(screen.getByTestId('grantRightActivityWeight-field').textContent).toBe(''); + + await user.click(submitButton); // submit proposal + + await waitFor(() => { + expect(requestBody).toContain('"activityWeight":null'); + }); + }); + + test('should send explicit activity weight to backend when provided', async () => { + let requestBody = ''; + server.use( + http.post(`${svUrl}/v0/admin/sv/voterequest/create`, async ({ request }) => { + requestBody = await request.text(); + return HttpResponse.json({}); + }) + ); + + const user = userEvent.setup(); + + render( + + + + ); + + const actionInput = screen.getByTestId('grant-featured-app-action'); + const submitButton = screen.getByTestId('submit-button'); + + const summaryInput = screen.getByTestId('grant-featured-app-summary'); + await user.type(summaryInput, 'Summary of the proposal'); + + const urlInput = screen.getByTestId('grant-featured-app-url'); + await user.type(urlInput, 'https://example.com'); + + const providerInput = screen.getByTestId('grant-featured-app-idValue'); + await user.type(providerInput, 'a-party-id::1014912492'); + + const activityWeightInput = screen.getByTestId('grant-featured-app-activityWeight'); + await user.type(activityWeightInput, '2.5'); + + await user.click(actionInput); // using this to trigger the onBlur event which triggers the validation + + await waitFor(() => { + expect(screen.queryByText('Validating provider...')).not.toBeInTheDocument(); + }); + + await waitFor(async () => { + expect(submitButton).not.toBeDisabled(); + }); + + await user.click(submitButton); // review proposal + await user.click(submitButton); // submit proposal + + await waitFor(() => { + expect(requestBody).toContain('"activityWeight":"2.5"'); + }); + }); + + test('activity weight rejects negative numbers and more than 10 decimal places', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + const activityWeightInput = screen.getByTestId('grant-featured-app-activityWeight'); + const activityWeightError = screen.getByTestId('grant-featured-app-activityWeight-error'); + + await user.type(activityWeightInput, '-1'); + await waitFor(() => { + expect(activityWeightError.textContent).toBe('Weight must be a valid non-negative number'); + }); + + await user.clear(activityWeightInput); + await user.type(activityWeightInput, '1.1234567891'); + await waitFor(() => { + expect(activityWeightError.textContent).toBe(''); + }); + + await user.clear(activityWeightInput); + await user.type(activityWeightInput, '1.12345678912'); + await waitFor(() => { + expect(activityWeightError.textContent).toBe('Weight can have at most 10 decimal places'); + }); }); }); @@ -229,7 +369,9 @@ describe('Revoke Featured App Form', () => { fireEvent.blur(partyIdInput); await waitFor(() => { - expect(screen.queryByText('Loading featured app rights...')).not.toBeInTheDocument(); + expect( + screen.queryByText('Loading Featured Application Contract IDs...') + ).not.toBeInTheDocument(); }); const rightCidDropdown = screen.getByTestId('revoke-featured-app-rightCid-dropdown'); @@ -254,7 +396,7 @@ describe('Revoke Featured App Form', () => { ); expect(screen.getByTestId('revoke-featured-app-form')).toBeInTheDocument(); - expect(screen.getByText('Proposal type')).toBeInTheDocument(); + expect(screen.getByText(CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE)).toBeInTheDocument(); const actionInput = screen.getByTestId('revoke-featured-app-action'); expect(actionInput).toBeInTheDocument(); @@ -274,7 +416,7 @@ describe('Revoke Featured App Form', () => { const partyIdTitle = screen.getByTestId('revoke-featured-app-partyId-title'); expect(partyIdTitle).toBeInTheDocument(); - expect(partyIdTitle.textContent).toBe('Provider Party ID'); + expect(partyIdTitle.textContent).toBe(CREATE_PROPOSAL_LABEL_PROVIDER_PARTY_ID); const rightCidDropdown = screen.getByTestId('revoke-featured-app-rightCid-dropdown'); expect(rightCidDropdown).toBeInTheDocument(); @@ -296,12 +438,12 @@ describe('Revoke Featured App Form', () => { await waitFor(() => { expect(screen.getByTestId('revoke-featured-app-rightCid')).toHaveTextContent( - 'No featured application rights found for this provider' + 'No Featured Application Contract IDs found for this provider' ); }); expect(screen.getByTestId('revoke-featured-app-rightCid-error')).not.toHaveTextContent( - 'No featured application rights found for this provider' + 'No Featured Application Contract IDs found for this provider' ); expect(screen.getByTestId('revoke-featured-app-rightCid-dropdown')).toBeDisabled(); @@ -407,7 +549,7 @@ describe('Revoke Featured App Form', () => { await user.click(submitButton); - expect(screen.getByText(PROPOSAL_SUMMARY_TITLE)).toBeInTheDocument(); + expect(screen.getByText(PROPOSAL_REVIEW_TITLE)).toBeInTheDocument(); expect(screen.getByTestId('revokeProviderPartyId-title').textContent).toBe('Provider Party ID'); expect(screen.getByTestId('revokeProviderPartyId-field').textContent).toBe( 'a-party-id::1014912492' diff --git a/apps/sv/frontend/src/__tests__/governance/forms/offboard-sv-form.test.tsx b/apps/sv/frontend/src/__tests__/governance/forms/offboard-sv-form.test.tsx index 0118ba544f..48723ab78f 100644 --- a/apps/sv/frontend/src/__tests__/governance/forms/offboard-sv-form.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/forms/offboard-sv-form.test.tsx @@ -13,7 +13,13 @@ import dayjs from 'dayjs'; import { OffboardSvForm } from '../../../components/forms/OffboardSvForm'; import { server, svUrl } from '../../setup/setup'; import { http, HttpResponse } from 'msw'; -import { PROPOSAL_SUMMARY_SUBTITLE, PROPOSAL_SUMMARY_TITLE } from '../../../utils/constants'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + PROPOSAL_REVIEW_TITLE, + PROPOSAL_SUMMARY_PLACEHOLDER, + PROPOSAL_SUMMARY_SUBTITLE, + URL_PLACEHOLDER, +} from '../../../utils/constants'; describe('SV user can', () => { test('login and see the SV party ID', async () => { @@ -32,7 +38,7 @@ describe('SV user can', () => { const button = screen.getByRole('button', { name: 'Log In' }); await user.click(button); - expect(await screen.findAllByDisplayValue(svPartyId)).not.toBe([]); + expect(await screen.findAllByDisplayValue(svPartyId)).not.toHaveLength(0); }); }); @@ -45,7 +51,7 @@ describe('Offboard SV Form', () => { ); expect(screen.getByTestId('offboard-sv-form')).toBeInTheDocument(); - expect(screen.getByText('Proposal type')).toBeInTheDocument(); + expect(screen.getByText(CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE)).toBeInTheDocument(); const actionInput = screen.getByTestId('offboard-sv-action'); expect(actionInput).toBeInTheDocument(); @@ -54,6 +60,7 @@ describe('Offboard SV Form', () => { const summaryInput = screen.getByTestId('offboard-sv-summary'); expect(summaryInput).toBeInTheDocument(); expect(summaryInput.getAttribute('value')).toBeNull(); + expect(summaryInput.getAttribute('placeholder')).toBe(PROPOSAL_SUMMARY_PLACEHOLDER); const summarySubtitle = screen.getByTestId('offboard-sv-summary-subtitle'); expect(summarySubtitle).toBeInTheDocument(); @@ -62,10 +69,12 @@ describe('Offboard SV Form', () => { const urlInput = screen.getByTestId('offboard-sv-url'); expect(urlInput).toBeInTheDocument(); expect(urlInput.getAttribute('value')).toBe(''); + expect(urlInput).toHaveAttribute('placeholder', URL_PLACEHOLDER); const memberInput = screen.getByTestId('offboard-sv-member-dropdown'); expect(memberInput).toBeInTheDocument(); expect(memberInput.getAttribute('value')).toBe(''); + expect(screen.getByText('Select a member')).toBeInTheDocument(); expect(screen.getByText('Review Proposal')).toBeInTheDocument(); }); @@ -85,7 +94,7 @@ describe('Offboard SV Form', () => { expect(screen.getByText('Review Proposal')).toBeInTheDocument(); await user.click(submitButton); - expect(submitButton.getAttribute('disabled')).toBeDefined(); + expect(submitButton.getAttribute('disabled')).not.toBeNull(); await expect(async () => await user.click(submitButton)).rejects.toThrowError( /Unable to perform pointer interaction/ ); @@ -109,11 +118,8 @@ describe('Offboard SV Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - expect(memberToSelect).toBeInTheDocument(); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); await user.click(actionInput); // using this to trigger the onBlur event which triggers the validation @@ -221,10 +227,8 @@ describe('Offboard SV Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); expect(screen.getByText('Review Proposal')).toBeInTheDocument(); const submitButton = screen.getByTestId('submit-button'); @@ -236,7 +240,7 @@ describe('Offboard SV Form', () => { await user.click(submitButton); - expect(screen.getByText(PROPOSAL_SUMMARY_TITLE)).toBeInTheDocument(); + expect(screen.getByText(PROPOSAL_REVIEW_TITLE)).toBeInTheDocument(); }); test('should show error on form if submission fails', async () => { @@ -265,10 +269,8 @@ describe('Offboard SV Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); expect(screen.getByText('Review Proposal')).toBeInTheDocument(); const submitButton = screen.getByTestId('submit-button'); @@ -312,10 +314,8 @@ describe('Offboard SV Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); const submitButton = screen.getByTestId('submit-button'); await user.click(actionInput); // using this to trigger the onBlur event which triggers the validation diff --git a/apps/sv/frontend/src/__tests__/governance/forms/pending-fields.test.tsx b/apps/sv/frontend/src/__tests__/governance/forms/pending-fields.test.tsx index f84059d16d..9391e48ba6 100644 --- a/apps/sv/frontend/src/__tests__/governance/forms/pending-fields.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/forms/pending-fields.test.tsx @@ -78,7 +78,7 @@ describe('DSO Pending Fields', () => { const button = screen.getByRole('button', { name: 'Log In' }); await user.click(button); - expect(await screen.findAllByDisplayValue(svPartyId)).not.toBe([]); + expect(await screen.findAllByDisplayValue(svPartyId)).not.toHaveLength(0); }); }); diff --git a/apps/sv/frontend/src/__tests__/governance/forms/set-amulet-rules-form.test.tsx b/apps/sv/frontend/src/__tests__/governance/forms/set-amulet-rules-form.test.tsx index 728ca1538c..112720c447 100644 --- a/apps/sv/frontend/src/__tests__/governance/forms/set-amulet-rules-form.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/forms/set-amulet-rules-form.test.tsx @@ -13,7 +13,11 @@ import { SetAmuletConfigRulesForm } from '../../../components/forms/SetAmuletCon import dayjs from 'dayjs'; import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils'; import { server, svUrl } from '../../setup/setup'; -import { PROPOSAL_SUMMARY_SUBTITLE, PROPOSAL_SUMMARY_TITLE } from '../../../utils/constants'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + PROPOSAL_REVIEW_TITLE, + PROPOSAL_SUMMARY_SUBTITLE, +} from '../../../utils/constants'; describe('SV user can', () => { test('login and see the SV party ID', async () => { @@ -32,7 +36,7 @@ describe('SV user can', () => { const button = screen.getByRole('button', { name: 'Log In' }); user.click(button); - expect(await screen.findAllByDisplayValue(svPartyId)).not.toBe([]); + expect(await screen.findAllByDisplayValue(svPartyId)).not.toHaveLength(0); }); }); @@ -45,7 +49,7 @@ describe('Set Amulet Config Rules Form', () => { ); expect(screen.getByTestId('set-amulet-config-rules-form')).toBeInTheDocument(); - expect(screen.getByText('Proposal type')).toBeInTheDocument(); + expect(screen.getByText(CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE)).toBeInTheDocument(); const actionInput = screen.getByTestId('set-amulet-config-rules-action'); expect(actionInput).toBeInTheDocument(); @@ -80,7 +84,11 @@ describe('Set Amulet Config Rules Form', () => { { timeout: 1000 } ); - expect(screen.getByTestId('json-diffs-details')).toBeInTheDocument(); + const jsonDiffsToggle = screen.getByTestId('json-diff-toggle'); + expect(screen.getByText('JSON')).toBeInTheDocument(); + expect(jsonDiffsToggle).toHaveTextContent('Show JSON'); + expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByTestId('json-diffs-details')).not.toBeVisible(); }); test( @@ -99,7 +107,7 @@ describe('Set Amulet Config Rules Form', () => { expect(submitButton).toBeInTheDocument(); await user.click(submitButton); - expect(submitButton.getAttribute('disabled')).toBeDefined(); + expect(submitButton.getAttribute('disabled')).not.toBeNull(); await expect(async () => await user.click(submitButton)).rejects.toThrowError( /Unable to perform pointer interaction/ ); @@ -241,6 +249,53 @@ describe('Set Amulet Config Rules Form', () => { expect(changes.length).toBe(2); }); + test('reward config minting scheme renders as a dropdown', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + // Minting scheme should render as a Select, not a TextField + const mintingField = screen.getByTestId('config-field-rewardConfigMintingVersion'); + expect(mintingField).toBeInTheDocument(); + const selectInput = mintingField.querySelector('[role="combobox"]') as HTMLElement; + expect(selectInput).toBeInTheDocument(); + + // Open dropdown and verify options + await user.click(selectInput); + expect(screen.getByText('Featured App Markers (pre CIP-104)')).toBeInTheDocument(); + expect(screen.getByText('Traffic-Based App Rewards (CIP-104)')).toBeInTheDocument(); + + // Select an option + await user.click(screen.getByText('Traffic-Based App Rewards (CIP-104)')); + + // Verify current value is shown after change + const currentValue = screen.getByTestId('config-current-value-rewardConfigMintingVersion'); + expect(currentValue).toBeInTheDocument(); + }); + + test('reward config dry-run scheme includes None option', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + const dryRunField = screen.getByTestId('config-field-rewardConfigDryRunVersion'); + expect(dryRunField).toBeInTheDocument(); + const selectInput = dryRunField.querySelector('[role="combobox"]') as HTMLElement; + expect(selectInput).toBeInTheDocument(); + + // Open dropdown and verify None option exists + await user.click(selectInput); + expect(screen.getByText('None (disabled)')).toBeInTheDocument(); + }); + test('should show proposal review page after form completion', async () => { const user = userEvent.setup(); @@ -274,7 +329,9 @@ describe('Set Amulet Config Rules Form', () => { await user.click(submitButton); - expect(screen.getByText(PROPOSAL_SUMMARY_TITLE)).toBeInTheDocument(); + expect(screen.getByText(PROPOSAL_REVIEW_TITLE)).toBeInTheDocument(); + expect(screen.queryByText('JSON')).not.toBeInTheDocument(); + expect(screen.getByTestId('json-diff-toggle')).toHaveTextContent('Show JSON'); }); test('should show error on form if submission fails', { timeout: 10000 }, async () => { @@ -376,19 +433,22 @@ describe('Set Amulet Config Rules Form', () => { const c2Input = screen.getByTestId('config-field-transferConfigTransferFeeInitialRate'); await user.type(c2Input, '9.99'); - const jsonDiffs = screen.getByText('JSON Diffs'); - expect(jsonDiffs).toBeInTheDocument(); + const jsonDiffsToggle = screen.getByTestId('json-diff-toggle'); + expect(jsonDiffsToggle).toHaveTextContent('Show JSON'); + expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'false'); - await user.click(jsonDiffs); - expect(await screen.findByTestId('config-diffs-display')).toBeInTheDocument(); + await user.click(jsonDiffsToggle); + expect(await screen.findByTestId('config-diffs-display')).toBeVisible(); + expect(jsonDiffsToggle).toHaveTextContent('Hide JSON'); + expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'true'); const reviewButton = screen.getByTestId('submit-button'); await waitFor(async () => { expect(reviewButton.getAttribute('disabled')).toBeNull(); }); - expect(jsonDiffs).toBeInTheDocument(); - await user.click(jsonDiffs); + expect(jsonDiffsToggle).toBeInTheDocument(); + await user.click(jsonDiffsToggle); expect(await screen.findByTestId('config-diffs-display')).toBeInTheDocument(); }); diff --git a/apps/sv/frontend/src/__tests__/governance/forms/set-dso-rules-form.test.tsx b/apps/sv/frontend/src/__tests__/governance/forms/set-dso-rules-form.test.tsx index 25753a34fd..e3929882c1 100644 --- a/apps/sv/frontend/src/__tests__/governance/forms/set-dso-rules-form.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/forms/set-dso-rules-form.test.tsx @@ -12,6 +12,12 @@ import { http, HttpResponse } from 'msw'; import { describe, expect, test } from 'vitest'; import App from '../../../App'; import { SetDsoConfigRulesForm } from '../../../components/forms/SetDsoConfigRulesForm'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + DATE_TIME_PLACEHOLDER, + REASON_PLACEHOLDER, + URL_PLACEHOLDER, +} from '../../../utils/constants'; import { SvConfigProvider } from '../../../utils'; import { Wrapper } from '../../helpers'; import { svPartyId } from '../../mocks/constants'; @@ -34,7 +40,7 @@ describe('SV user can', () => { const button = screen.getByRole('button', { name: 'Log In' }); await user.click(button); - expect(await screen.findAllByDisplayValue(svPartyId)).not.toBe([]); + expect(await screen.findAllByDisplayValue(svPartyId)).not.toHaveLength(0); }); }); @@ -47,7 +53,7 @@ describe('Set DSO Config Rules Form', () => { ); expect(screen.getByTestId('set-dso-config-rules-form')).toBeInTheDocument(); - expect(screen.getByText('Proposal type')).toBeInTheDocument(); + expect(screen.getByText(CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE)).toBeInTheDocument(); const actionInput = screen.getByTestId('set-dso-config-rules-action'); expect(actionInput).toBeInTheDocument(); @@ -58,10 +64,21 @@ describe('Set DSO Config Rules Form', () => { const summaryInput = screen.getByTestId('set-dso-config-rules-summary'); expect(summaryInput).toBeInTheDocument(); expect(summaryInput.getAttribute('value')).not.toBeInTheDocument(); + expect(summaryInput).toHaveAttribute('placeholder', REASON_PLACEHOLDER); const urlInput = screen.getByTestId('set-dso-config-rules-url'); expect(urlInput).toBeInTheDocument(); expect(urlInput.getAttribute('value')).toBe(''); + expect(urlInput).toHaveAttribute('placeholder', URL_PLACEHOLDER); + + expect(screen.getByTestId('set-dso-config-rules-expiry-date-field')).toHaveAttribute( + 'placeholder', + DATE_TIME_PLACEHOLDER + ); + expect(screen.getByTestId('set-dso-config-rules-effective-date-field')).toHaveAttribute( + 'placeholder', + DATE_TIME_PLACEHOLDER + ); const configLabels = screen.getAllByTestId(/config-label-/); expect(configLabels.length).toBeGreaterThan(15); @@ -73,7 +90,11 @@ describe('Set DSO Config Rules Form', () => { /Unable to find an element/ ); - expect(screen.getByTestId('json-diffs-details')).toBeInTheDocument(); + const jsonDiffsToggle = screen.getByTestId('json-diff-toggle'); + expect(screen.getByText('JSON')).toBeInTheDocument(); + expect(jsonDiffsToggle).toHaveTextContent('Show JSON'); + expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByTestId('json-diffs-details')).not.toBeVisible(); }); test('should render errors when submit button is clicked on new form', async () => { @@ -90,7 +111,7 @@ describe('Set DSO Config Rules Form', () => { expect(submitButton).toBeInTheDocument(); await user.click(submitButton); - expect(submitButton.getAttribute('disabled')).toBeDefined(); + expect(submitButton.getAttribute('disabled')).not.toBeNull(); expect(async () => await user.click(submitButton)).rejects.toThrowError( /Unable to perform pointer interaction/ ); @@ -263,7 +284,9 @@ describe('Set DSO Config Rules Form', () => { await user.click(submitButton); - expect(screen.getByText('Proposal Summary')).toBeInTheDocument(); + expect(screen.getByText('Proposal Review')).toBeInTheDocument(); + expect(screen.queryByText('JSON')).not.toBeInTheDocument(); + expect(screen.getByTestId('json-diff-toggle')).toHaveTextContent('Show JSON'); }); test('should show error on form if submission fails', async () => { @@ -374,19 +397,22 @@ describe('Set DSO Config Rules Form', () => { const c2Input = screen.getByTestId('config-field-voteCooldownTime'); await user.type(c2Input, '9999'); - const jsonDiffs = screen.getByText('JSON Diffs'); - expect(jsonDiffs).toBeInTheDocument(); + const jsonDiffsToggle = screen.getByTestId('json-diff-toggle'); + expect(jsonDiffsToggle).toHaveTextContent('Show JSON'); + expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'false'); - await user.click(jsonDiffs); - expect(await screen.findByTestId('config-diffs-display')).toBeInTheDocument(); + await user.click(jsonDiffsToggle); + expect(await screen.findByTestId('config-diffs-display')).toBeVisible(); + expect(jsonDiffsToggle).toHaveTextContent('Hide JSON'); + expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'true'); const reviewButton = screen.getByTestId('submit-button'); await waitFor(async () => { expect(reviewButton.getAttribute('disabled')).not.toBeInTheDocument(); }); - expect(jsonDiffs).toBeInTheDocument(); - await user.click(jsonDiffs); + expect(jsonDiffsToggle).toBeInTheDocument(); + await user.click(jsonDiffsToggle); expect(await screen.findByTestId('config-diffs-display')).toBeInTheDocument(); }); diff --git a/apps/sv/frontend/src/__tests__/governance/forms/update-featured-app-form.test.tsx b/apps/sv/frontend/src/__tests__/governance/forms/update-featured-app-form.test.tsx new file mode 100644 index 0000000000..a71c1b0afd --- /dev/null +++ b/apps/sv/frontend/src/__tests__/governance/forms/update-featured-app-form.test.tsx @@ -0,0 +1,315 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { describe, expect, test } from 'vitest'; +import userEvent from '@testing-library/user-event'; +import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils'; +import dayjs from 'dayjs'; +import { SvConfigProvider } from '../../../utils'; +import App from '../../../App'; +import { svPartyId } from '../../mocks/constants'; +import { Wrapper } from '../../helpers'; +import { UpdateFeaturedAppForm } from '../../../components/forms/UpdateFeaturedAppForm'; +import { server, svUrl } from '../../setup/setup'; +import { http, HttpResponse } from 'msw'; +import { PROPOSAL_SUMMARY_SUBTITLE, PROPOSAL_SUMMARY_TITLE } from '../../../utils/constants'; + +// Logs in once so the admin client has an access token for the rest of the file. +describe('SV user can', () => { + test('login and see the SV party ID', async () => { + const user = userEvent.setup(); + render( + + + + ); + + expect(await screen.findByText('Log In')).toBeInTheDocument(); + + const input = screen.getByRole('textbox'); + await user.type(input, 'sv1'); + + const button = screen.getByRole('button', { name: 'Log In' }); + await user.click(button); + + expect(await screen.findAllByDisplayValue(svPartyId)).not.toBe([]); + }); +}); + +describe('Update Featured App Form', () => { + const fillOutUpdateForm = async (user: ReturnType) => { + const summaryInput = screen.getByTestId('update-featured-app-summary'); + await user.type(summaryInput, 'Summary of the proposal'); + + const urlInput = screen.getByTestId('update-featured-app-url'); + await user.type(urlInput, 'https://example.com'); + + const partyIdInput = screen.getByTestId('update-featured-app-partyId'); + await user.type(partyIdInput, 'a-party-id::1014912492'); + fireEvent.blur(partyIdInput); + + await waitFor(() => { + expect(screen.queryByText('Loading featured app rights...')).not.toBeInTheDocument(); + }); + + const rightCidDropdown = screen.getByTestId('update-featured-app-rightCid-dropdown'); + await waitFor( + () => { + expect(rightCidDropdown).not.toBeDisabled(); + }, + { timeout: 3000 } + ); + fireEvent.change(rightCidDropdown, { target: { value: 'rightCid123' } }); + fireEvent.blur(rightCidDropdown); + await waitFor(() => { + expect(screen.getByTestId('update-featured-app-rightCid-error').textContent).toBeFalsy(); + }); + + const activityWeightInput = screen.getByTestId('update-featured-app-activityWeight'); + await user.type(activityWeightInput, '2.5'); + }; + + test('should render all Form components', () => { + render( + + + + ); + + expect(screen.getByTestId('update-featured-app-form')).toBeInTheDocument(); + expect(screen.getByText('Proposal type')).toBeInTheDocument(); + + const actionInput = screen.getByTestId('update-featured-app-action'); + expect(actionInput).toBeInTheDocument(); + expect(actionInput.textContent).toBe('Update Featured Application'); + + const summaryInput = screen.getByTestId('update-featured-app-summary'); + expect(summaryInput).toBeInTheDocument(); + expect(summaryInput.getAttribute('value')).toBeNull(); + + const summarySubtitle = screen.getByTestId('update-featured-app-summary-subtitle'); + expect(summarySubtitle).toBeInTheDocument(); + expect(summarySubtitle.textContent).toBe(PROPOSAL_SUMMARY_SUBTITLE); + + const urlInput = screen.getByTestId('update-featured-app-url'); + expect(urlInput).toBeInTheDocument(); + expect(urlInput.getAttribute('value')).toBe(''); + + const partyIdInput = screen.getByTestId('update-featured-app-partyId'); + expect(partyIdInput).toBeInTheDocument(); + expect(partyIdInput.getAttribute('value')).toBe(''); + + const partyIdTitle = screen.getByTestId('update-featured-app-partyId-title'); + expect(partyIdTitle).toBeInTheDocument(); + expect(partyIdTitle.textContent).toBe('Provider Party ID'); + + const rightCidDropdown = screen.getByTestId('update-featured-app-rightCid-dropdown'); + expect(rightCidDropdown).toBeInTheDocument(); + expect(rightCidDropdown).toBeDisabled(); + + expect(screen.getByTestId('update-featured-app-activityWeight')).toBeInTheDocument(); + + expect(screen.getByText('Review Proposal')).toBeInTheDocument(); + }); + + test('activity weight is required', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + const activityWeightInput = screen.getByTestId('update-featured-app-activityWeight'); + const actionInput = screen.getByTestId('update-featured-app-action'); + + await user.click(activityWeightInput); + await user.click(actionInput); // blur to trigger validation + + await waitFor(() => { + expect(screen.getByTestId('update-featured-app-activityWeight-error').textContent).toBe( + 'Weight is required' + ); + }); + }); + + test('should send new activity weight to backend', async () => { + let requestBody = ''; + server.use( + http.post(`${svUrl}/v0/admin/sv/voterequest/create`, async ({ request }) => { + requestBody = await request.text(); + return HttpResponse.json({}); + }) + ); + + const user = userEvent.setup(); + + render( + + + + ); + + await fillOutUpdateForm(user); + + const actionInput = screen.getByTestId('update-featured-app-action'); + await user.click(actionInput); // blur to trigger validation + + const submitButton = screen.getByTestId('submit-button'); + await waitFor(() => { + expect(submitButton).not.toBeDisabled(); + }); + + await user.click(submitButton); // review proposal + await user.click(submitButton); // submit proposal + + await waitFor(() => { + expect(requestBody).toContain('"newActivityWeight":"2.5"'); + }); + }); + + test('activity weight rejects negative numbers and more than 10 decimal places', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + const activityWeightInput = screen.getByTestId('update-featured-app-activityWeight'); + const activityWeightError = screen.getByTestId('update-featured-app-activityWeight-error'); + + await user.type(activityWeightInput, '-1'); + await waitFor(() => { + expect(activityWeightError.textContent).toBe('Weight must be a valid non-negative number'); + }); + + await user.clear(activityWeightInput); + await user.type(activityWeightInput, '1.1234567891'); + await waitFor(() => { + expect(activityWeightError.textContent).toBe(''); + }); + + await user.clear(activityWeightInput); + await user.type(activityWeightInput, '1.12345678912'); + await waitFor(() => { + expect(activityWeightError.textContent).toBe('Weight can have at most 10 decimal places'); + }); + }); + + test('communicates when the provider has no featured app rights to update', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + const partyIdInput = screen.getByTestId('update-featured-app-partyId'); + await user.type(partyIdInput, 'no-rights-party::1014912492'); + fireEvent.blur(partyIdInput); + + await waitFor(() => { + expect(screen.getByTestId('update-featured-app-rightCid')).toHaveTextContent( + 'No featured application rights found for this provider' + ); + }); + + expect(screen.getByTestId('update-featured-app-rightCid-dropdown')).toBeDisabled(); + }); + + test('expiry date must be in the future', async () => { + render( + + + + ); + + const expiryDateInput = screen.getByTestId('update-featured-app-expiry-date-field'); + expect(expiryDateInput).toBeInTheDocument(); + + const thePast = dayjs().subtract(1, 'day').format(dateTimeFormatISO); + const theFuture = dayjs().add(1, 'day').format(dateTimeFormatISO); + + fireEvent.change(expiryDateInput, { target: { value: thePast } }); + + await waitFor(() => { + expect(screen.queryByText('Expiration must be in the future')).toBeInTheDocument(); + }); + + fireEvent.change(expiryDateInput, { target: { value: theFuture } }); + + await waitFor(() => { + expect(screen.queryByText('Expiration must be in the future')).not.toBeInTheDocument(); + }); + }); + + test('effective date must be after expiry date', async () => { + render( + + + + ); + + const expiryDateInput = screen.getByTestId('update-featured-app-expiry-date-field'); + const effectiveDateInput = screen.getByTestId('update-featured-app-effective-date-field'); + + const expiryDate = dayjs().add(1, 'week'); + const effectiveDate = expiryDate.subtract(1, 'day'); + + fireEvent.change(expiryDateInput, { target: { value: expiryDate.format(dateTimeFormatISO) } }); + fireEvent.change(effectiveDateInput, { + target: { value: effectiveDate.format(dateTimeFormatISO) }, + }); + + await waitFor(() => { + expect( + screen.queryByText('Effective Date must be after expiration date') + ).toBeInTheDocument(); + }); + + const validEffectiveDate = expiryDate.add(1, 'day').format(dateTimeFormatISO); + + fireEvent.change(effectiveDateInput, { target: { value: validEffectiveDate } }); + + await waitFor(() => { + expect( + screen.queryByText('Effective Date must be after expiration date') + ).not.toBeInTheDocument(); + }); + }); + + test('should show proposal review page after form completion', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + await fillOutUpdateForm(user); + + const actionInput = screen.getByTestId('update-featured-app-action'); + await user.click(actionInput); // blur to trigger validation + + const submitButton = screen.getByTestId('submit-button'); + await waitFor(() => { + expect(submitButton).not.toBeDisabled(); + }); + + await user.click(submitButton); // review proposal + + expect(screen.getByText(PROPOSAL_SUMMARY_TITLE)).toBeInTheDocument(); + expect(screen.getByTestId('updateProviderPartyId-field').textContent).toBe( + 'a-party-id::1014912492' + ); + expect(screen.getByTestId('updateRight-field').textContent).toBe('rightCid123'); + expect(screen.getByTestId('config-change-current-value').textContent).toBe('1.0'); + expect(screen.getByTestId('config-change-new-value').textContent).toBe('2.5'); + }); +}); diff --git a/apps/sv/frontend/src/__tests__/governance/forms/update-sv-reward-weight-form-test.test.tsx b/apps/sv/frontend/src/__tests__/governance/forms/update-sv-reward-weight-form-test.test.tsx index 7728867f43..2d6892d4bb 100644 --- a/apps/sv/frontend/src/__tests__/governance/forms/update-sv-reward-weight-form-test.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/forms/update-sv-reward-weight-form-test.test.tsx @@ -13,7 +13,10 @@ import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils' import dayjs from 'dayjs'; import { server, svUrl } from '../../setup/setup'; import { http, HttpResponse } from 'msw'; -import { PROPOSAL_SUMMARY_SUBTITLE } from '../../../utils/constants'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + PROPOSAL_SUMMARY_SUBTITLE, +} from '../../../utils/constants'; describe('SV user can', () => { test('login and see the SV party ID', async () => { @@ -32,7 +35,7 @@ describe('SV user can', () => { const button = screen.getByRole('button', { name: 'Log In' }); user.click(button); - expect(await screen.findAllByDisplayValue(svPartyId)).not.toBe([]); + expect(await screen.findAllByDisplayValue(svPartyId)).not.toHaveLength(0); }); }); @@ -45,7 +48,7 @@ describe('Update Super Validator Reward Weight Form', () => { ); expect(screen.getByTestId('update-sv-reward-weight-form')).toBeInTheDocument(); - expect(screen.getByText('Proposal type')).toBeInTheDocument(); + expect(screen.getByText(CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE)).toBeInTheDocument(); const actionInput = screen.getByTestId('update-sv-reward-weight-action'); expect(actionInput).toBeInTheDocument(); @@ -86,7 +89,7 @@ describe('Update Super Validator Reward Weight Form', () => { expect(submitButton).toBeInTheDocument(); await user.click(submitButton); - expect(submitButton.getAttribute('disabled')).toBeDefined(); + expect(submitButton.getAttribute('disabled')).not.toBeNull(); await expect(async () => await user.click(submitButton)).rejects.toThrowError( /Unable to perform pointer interaction/ ); @@ -111,11 +114,8 @@ describe('Update Super Validator Reward Weight Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - expect(memberToSelect).toBeInTheDocument(); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); const weightInput = screen.getByTestId('update-sv-reward-weight-weight'); expect(weightInput).toBeInTheDocument(); @@ -123,7 +123,7 @@ describe('Update Super Validator Reward Weight Form', () => { await user.click(actionInput); // using this to trigger the onBlur event which triggers the validation - expect(submitButton.getAttribute('disabled')).toBe(null); + expect(submitButton.getAttribute('disabled')).toBeNull(); }); test('expiry date must be in the future', async () => { @@ -222,13 +222,10 @@ describe('Update Super Validator Reward Weight Form', () => { const selectInput = screen.getByRole('combobox'); const validateCurrentWeightFor = async (sv: string, weight: string) => { - await waitFor(async () => { - fireEvent.mouseDown(selectInput); - const memberToSelect = screen.getByText(sv); - expect(memberToSelect).not.toBeNull(); - await user.click(memberToSelect); - expect(await screen.findByText(`Current Weight: ${weight}`)).toBeInTheDocument(); - }); + fireEvent.mouseDown(selectInput); + const memberToSelect = await screen.findByText(sv); + await user.click(memberToSelect); + expect(await screen.findByText(`Current Weight: ${weight}`)).toBeInTheDocument(); }; await validateCurrentWeightFor('Digital-Asset-2', '0_0010'); @@ -257,11 +254,8 @@ describe('Update Super Validator Reward Weight Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - expect(memberToSelect).toBeInTheDocument(); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); expect(weightInput.getAttribute('value')).toBe(''); }); @@ -334,11 +328,8 @@ describe('Update Super Validator Reward Weight Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - expect(memberToSelect).toBeInTheDocument(); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); const weightInput = screen.getByTestId('update-sv-reward-weight-weight'); expect(weightInput).toBeInTheDocument(); @@ -383,11 +374,8 @@ describe('Update Super Validator Reward Weight Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - expect(memberToSelect).toBeInTheDocument(); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); const weightInput = screen.getByTestId('update-sv-reward-weight-weight'); expect(weightInput.getAttribute('value')).toBe(''); @@ -438,11 +426,8 @@ describe('Update Super Validator Reward Weight Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - expect(memberToSelect).toBeInTheDocument(); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); const weightInput = screen.getByTestId('update-sv-reward-weight-weight'); expect(weightInput).toBeInTheDocument(); @@ -488,10 +473,8 @@ describe('Update Super Validator Reward Weight Form', () => { const selectInput = screen.getByRole('combobox'); fireEvent.mouseDown(selectInput); - await waitFor(async () => { - const memberToSelect = screen.getByText('Digital-Asset-Eng-2'); - await user.click(memberToSelect); - }); + const memberToSelect = await screen.findByText('Digital-Asset-Eng-2'); + await user.click(memberToSelect); const weightInput = screen.getByTestId('update-sv-reward-weight-weight'); await user.type(weightInput, '0_1000'); diff --git a/apps/sv/frontend/src/__tests__/governance/governance-page.test.tsx b/apps/sv/frontend/src/__tests__/governance/governance-page.test.tsx index c6d3c1dc60..931fa7d068 100644 --- a/apps/sv/frontend/src/__tests__/governance/governance-page.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/governance-page.test.tsx @@ -1,11 +1,20 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { render, screen, within } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { describe, expect, test } from 'vitest'; import { SvConfigProvider } from '../../utils'; import userEvent from '@testing-library/user-event'; +import dayjs from 'dayjs'; +import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils'; import App from '../../App'; import { navigateToGovernancePage } from '../helpers'; +import { + activeProposalCid, + closedVoteCid, + voteResultsAmuletRules, + voteResultsDsoRules, +} from '../mocks/constants'; +import { CONTRACT_ID_VALIDATION_MESSAGE } from '../../utils/proposalSearch'; type UserEvent = ReturnType; @@ -95,6 +104,71 @@ describe('Governance Page', () => { expect(true).toBe(true); }); + test('should display total vote history count in the section badge', async () => { + const user = userEvent.setup(); + + render(); + + await navigateToGovernancePage(user); + + const expectedCount = voteResultsAmuletRules.dso_rules_vote_results + .concat(voteResultsDsoRules.dso_rules_vote_results) + .filter( + r => r.outcome.tag !== 'VRO_Accepted' || new Date(r.outcome.value.effectiveAt) < new Date() + ).length; + + const badge = await screen.findByTestId('vote-history-section-badge-count'); + await waitFor(() => expect(badge).toHaveTextContent(`${expectedCount}`)); + }); + + test('should display inflight votes count in the section badge', async () => { + const user = userEvent.setup(); + + render(); + + await navigateToGovernancePage(user); + + const badge = screen.getByTestId('inflight-proposals-section-badge-count'); + expect(badge).toHaveTextContent(''); + }); + + test('vote history details show the actual effective time for closed votes without targetEffectiveAt', async () => { + const user = userEvent.setup(); + + await login(user); + + await navigateToGovernancePage(user); + + // The first DsoRules vote result simulates an old-model accepted vote: + // no targetEffectiveAt on the request, actual effective time on the outcome. + const closedVote = voteResultsDsoRules.dso_rules_vote_results[0]; + const effectiveAt = + closedVote.outcome.tag === 'VRO_Accepted' ? closedVote.outcome.value.effectiveAt : undefined; + const expectedEffectiveAt = dayjs(effectiveAt).format(dateTimeFormatISO); + + const rows = screen.getAllByTestId('vote-history-row'); + const targetRow = rows.find( + row => + within(row).getByTestId('vote-history-row-vote-takes-effect').textContent === + expectedEffectiveAt + ); + expect(targetRow).toBeDefined(); + + await user.click(within(targetRow!).getByTestId('vote-history-row-action-name')); + + const votingInformation = await screen.findByTestId('proposal-details-voting-information'); + + const voteTakesEffectDuration = within(votingInformation).getByTestId( + 'proposal-details-vote-takes-effect-duration' + ); + expect(voteTakesEffectDuration.textContent?.trim()).not.toBe('Threshold'); + + const voteTakesEffectIso = within(votingInformation).getByTestId( + 'proposal-details-vote-takes-effect-value' + ); + expect(voteTakesEffectIso).toHaveTextContent(expectedEffectiveAt); + }); + test('click on Details link to see Proposal Details (Action Required)', async () => { const user = userEvent.setup(); @@ -109,7 +183,7 @@ describe('Governance Page', () => { await user.click(viewDetailsLink); - const proposalDetails = screen.getByTestId('proposal-details-title'); + const proposalDetails = screen.getByTestId('proposal-details-proposal-details'); expect(proposalDetails).toBeInTheDocument(); }); @@ -127,7 +201,7 @@ describe('Governance Page', () => { await user.click(viewDetailsLink); - const proposalDetails = screen.getByTestId('proposal-details-title'); + const proposalDetails = screen.getByTestId('proposal-details-proposal-details'); expect(proposalDetails).toBeInTheDocument(); const action = screen.getByTestId('proposal-details-action-value'); @@ -146,6 +220,11 @@ describe('Governance Page', () => { 'proposal-details-requester-party-id' ); expect(requesterInput).toBeInTheDocument(); + // Resolve SV display name (e.g. Digital-Asset-2) to full party ID for display + copy. + expect( + within(votingInformationSection).getByTestId('proposal-details-requester-party-id-value') + .textContent + ).toMatch(/::/); const votingClosesIso = within(votingInformationSection).getByTestId( 'proposal-details-voting-closes-value' @@ -187,4 +266,67 @@ describe('Governance Page', () => { expect(screen.getByTestId('your-vote-accept')).toBeInTheDocument(); expect(screen.getByTestId('your-vote-reject')).toBeInTheDocument(); }); + + describe('Proposal Search', () => { + test('renders search field and filters action required by full contract ID', async () => { + const user = userEvent.setup(); + await login(user); + await navigateToGovernancePage(user); + + expect(screen.getByTestId('proposal-search')).toBeInTheDocument(); + expect(screen.getAllByTestId('action-required-card')).toHaveLength(4); + + fireEvent.change(screen.getByTestId('proposal-search-input'), { + target: { value: activeProposalCid }, + }); + + await waitFor(() => { + expect(screen.getByTestId('proposal-search-clear')).toBeInTheDocument(); + expect(screen.getAllByTestId('action-required-card')).toHaveLength(1); + }); + }); + + test('filters vote history to matching full contract ID', async () => { + const user = userEvent.setup(); + await login(user); + await navigateToGovernancePage(user); + + expect(screen.getAllByTestId('vote-history-row')).toHaveLength(5); + + fireEvent.change(screen.getByTestId('proposal-search-input'), { + target: { value: closedVoteCid }, + }); + + await waitFor(() => { + expect(screen.getAllByTestId('vote-history-row')).toHaveLength(1); + }); + }); + + test('invalid contract ID shows validation and does not filter', async () => { + const user = userEvent.setup(); + await login(user); + await navigateToGovernancePage(user); + + const input = screen.getByTestId('proposal-search-input'); + fireEvent.change(input, { target: { value: 'not-a-valid-contract-id' } }); + + expect(screen.getByText(CONTRACT_ID_VALIDATION_MESSAGE)).toBeInTheDocument(); + expect(screen.getAllByTestId('action-required-card')).toHaveLength(4); + fireEvent.keyDown(input, { key: 'Enter' }); + expect(screen.getByTestId('governance-page-header')).toBeInTheDocument(); + expect(screen.queryByTestId('proposal-details-title')).not.toBeInTheDocument(); + }); + + test('contract ID search navigates to proposal details on enter', async () => { + const user = userEvent.setup(); + await login(user); + await navigateToGovernancePage(user); + + const input = screen.getByTestId('proposal-search-input'); + fireEvent.change(input, { target: { value: activeProposalCid } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(await screen.findByTestId('proposal-details-title')).toBeInTheDocument(); + }); + }); }); diff --git a/apps/sv/frontend/src/__tests__/governance/governance-sorting.test.tsx b/apps/sv/frontend/src/__tests__/governance/governance-sorting.test.tsx index 9ff8bb2236..f593de50f4 100644 --- a/apps/sv/frontend/src/__tests__/governance/governance-sorting.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/governance-sorting.test.tsx @@ -11,6 +11,7 @@ import { } from '../../components/governance/ActionRequiredSection'; import { ProposalListingSection } from '../../components/governance/ProposalListingSection'; import { ProposalListingData } from '../../utils/types'; +import { svPartyId } from '../mocks/constants'; describe('Governance Page Sorting', () => { describe('Action Required Section', () => { @@ -22,7 +23,7 @@ describe('Governance Page Sorting', () => { contractId: 'c' as ContractId, votingCloses: '2025-01-25 12:00', createdAt: '2025-01-10 12:00', - requester: 'sv1', + requester: svPartyId, }, { actionName: 'Action A - Earliest', @@ -30,7 +31,7 @@ describe('Governance Page Sorting', () => { contractId: 'a' as ContractId, votingCloses: '2025-01-15 10:00', createdAt: '2025-01-10 12:00', - requester: 'sv1', + requester: svPartyId, }, { actionName: 'Action B - Middle', @@ -38,7 +39,7 @@ describe('Governance Page Sorting', () => { contractId: 'b' as ContractId, votingCloses: '2025-01-15 18:00', createdAt: '2025-01-10 12:00', - requester: 'sv1', + requester: svPartyId, }, ]; @@ -61,7 +62,7 @@ describe('Governance Page Sorting', () => { }); }); - describe('Inflight Votes Section', () => { + describe('In-flight Proposals Section', () => { const baseData: Omit< ProposalListingData, 'actionName' | 'contractId' | 'voteTakesEffect' | 'votingThresholdDeadline' | 'voteStats' @@ -69,6 +70,7 @@ describe('Governance Page Sorting', () => { yourVote: 'accepted', status: 'In Progress', acceptanceThreshold: BigInt(11), + requester: svPartyId, }; test('should sort with Threshold items first (by votes desc, then deadline asc), then dated items by effective date asc', () => { @@ -118,7 +120,7 @@ describe('Governance Page Sorting', () => { render( { status: 'Implemented', voteStats: { accepted: 8, rejected: 2, 'no-vote': 1 }, acceptanceThreshold: BigInt(11), + requester: svPartyId, }; test('renders in backend order without client re-sorting', () => { diff --git a/apps/sv/frontend/src/__tests__/governance/proposal-details-content.test.tsx b/apps/sv/frontend/src/__tests__/governance/proposal-details-content.test.tsx index 1de4bb9c29..5604b8c83a 100644 --- a/apps/sv/frontend/src/__tests__/governance/proposal-details-content.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/proposal-details-content.test.tsx @@ -13,6 +13,7 @@ import { ProposalVote, ProposalVotingInformation, UnclaimedActivityRecordProposal, + UpdateFeatureAppProposal, UpdateSvRewardWeightProposal, } from '../../utils/types'; import userEvent from '@testing-library/user-event'; @@ -23,6 +24,17 @@ import { ProposalVoteForm } from '../../components/governance/ProposalVoteForm'; import App from '../../App'; import { svPartyId } from '../mocks/constants'; import { Wrapper } from '../helpers'; +import { + EFFECTIVE_AT_LABEL, + PROPOSAL_CREATED_LABEL, + PROPOSAL_SUMMARY_TITLE, + SUPPORTING_URL_LABEL, + THRESHOLD_DEADLINE_LABEL, + URL_PLACEHOLDER, + VOTE_PROPOSAL_CONTRACT_ID_LABEL, + VOTE_REASON_PLACEHOLDER, + VOTE_REASON_URL_PLACEHOLDER, +} from '../../utils/constants'; const voteRequest = { contractId: 'abc123' as ContractId, @@ -158,8 +170,17 @@ describe('Proposal Details Content', () => { ); - const pageTitle = screen.getByTestId('proposal-details-title'); - expect(pageTitle.textContent).toMatch(/Proposal Details/); + expect(screen.getByTestId('proposal-details-title')).toHaveTextContent('Proposal Details'); + const backToAllVotes = screen.getByTestId('proposal-details-back-to-all-votes'); + expect(backToAllVotes).toHaveTextContent('Back to all votes'); + expect(backToAllVotes).toHaveAttribute('href', '/governance/proposals'); + + const proposalDetailsSection = screen.getByTestId('proposal-details-proposal-details'); + expect(proposalDetailsSection).toBeInTheDocument(); + // Figma starts at Action — no duplicate inner "Proposal Details" heading + expect( + within(proposalDetailsSection).queryByRole('heading', { name: 'Proposal Details' }) + ).toBeNull(); const action = screen.getByTestId('proposal-details-action-value'); expect(action.textContent).toMatch(/Offboard Member/); @@ -172,12 +193,49 @@ describe('Proposal Details Content', () => { ); expect(memberInput).toBeInTheDocument(); expect(memberInput.textContent).toBe('sv2'); + expect(within(offboardSection).getByTestId('proposal-details-member-party-id')).toHaveStyle({ + width: '100%', + }); + expect( + within(offboardSection).getByTestId('proposal-details-member-party-id-scroll') + ).toHaveStyle({ overflowX: 'auto', width: '100%' }); + expect(screen.getByTestId('proposal-details-summary-label').textContent).toBe( + PROPOSAL_SUMMARY_TITLE + ); const summary = screen.getByTestId('proposal-details-summary-value'); expect(summary.textContent).toMatch(/Summary of the proposal/); + expect(screen.getByTestId('proposal-details-url-label').textContent).toBe(SUPPORTING_URL_LABEL); + const url = screen.getByTestId('proposal-details-url'); expect(url.textContent).toMatch(/https:\/\/example.com/); + expect(url).toHaveStyle({ width: '100%' }); + expect(screen.getByTestId('proposal-details-url-scroll')).toHaveStyle({ + overflowX: 'auto', + width: '100%', + }); + + // Figma Offboard details order: Action → Member → Proposal Summary → Supporting URL → Contract ID + expect(screen.getByTestId('proposal-details-contractid-label').textContent).toBe( + VOTE_PROPOSAL_CONTRACT_ID_LABEL + ); + expect(screen.getByTestId('proposal-details-contractid-id')).toHaveStyle({ width: '100%' }); + expect(screen.getByTestId('proposal-details-contractid-id-scroll')).toHaveStyle({ + overflowX: 'auto', + width: '100%', + }); + const contractIdLabel = screen.getByTestId('proposal-details-contractid-label'); + expect( + action.compareDocumentPosition(offboardSection) & Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); + expect( + offboardSection.compareDocumentPosition(summary) & Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); + expect(summary.compareDocumentPosition(url) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect( + url.compareDocumentPosition(contractIdLabel) & Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); const votingInformationSection = screen.getByTestId('proposal-details-voting-information'); expect(votingInformationSection).toBeInTheDocument(); @@ -187,6 +245,26 @@ describe('Proposal Details Content', () => { ); expect(requesterInput).toBeInTheDocument(); expect(requesterInput.textContent).toBe('sv1'); + expect( + within(votingInformationSection).getByTestId('proposal-details-requester-party-id') + ).toHaveStyle({ width: '100%' }); + expect( + within(votingInformationSection).getByTestId('proposal-details-requester-party-id-scroll') + ).toHaveStyle({ overflowX: 'auto', width: '100%' }); + + expect(screen.getByTestId('proposal-details-created-at-label').textContent).toBe( + PROPOSAL_CREATED_LABEL + ); + expect(screen.getByTestId('proposal-details-created-at-value').textContent).toBe( + '2025-01-01 13:00' + ); + + expect(screen.getByTestId('proposal-details-threshold-deadline-label').textContent).toBe( + THRESHOLD_DEADLINE_LABEL + ); + expect(screen.getByTestId('proposal-details-effective-at-label').textContent).toBe( + EFFECTIVE_AT_LABEL + ); const votingClosesIso = within(votingInformationSection).getByTestId( 'proposal-details-voting-closes-value' @@ -209,7 +287,12 @@ describe('Proposal Details Content', () => { expect(screen.getByTestId('your-vote-form')).toBeInTheDocument(); expect(screen.getByTestId('your-vote-url-input')).toBeInTheDocument(); - expect(screen.getByTestId('your-vote-reason-input')).toBeInTheDocument(); + const reasonInput = screen.getByTestId('your-vote-reason-input'); + expect(reasonInput).toBeInTheDocument(); + expect(reasonInput.getAttribute('placeholder')).toBe(VOTE_REASON_PLACEHOLDER); + expect(screen.getByTestId('your-vote-url-input').getAttribute('placeholder')).toBe( + VOTE_REASON_URL_PLACEHOLDER + ); expect(screen.getByTestId('your-vote-accept')).toBeInTheDocument(); expect(screen.getByTestId('your-vote-reject')).toBeInTheDocument(); }); @@ -285,6 +368,83 @@ describe('Proposal Details Content', () => { expect(rightContractIdValue.textContent).toMatch(/rightContractId/); }); + test('should render update featured app proposal details', async () => { + const updateFeaturedAppDetails = { + actionName: 'Update Featured Application', + action: 'SRARC_UpdateFeaturedAppRight', + proposal: { + rightContractId: 'rightCid123', + newActivityWeight: '2.5', + } as UpdateFeatureAppProposal, + } as ProposalDetails; + + render( + + + + ); + + const action = screen.getByTestId('proposal-details-action-value'); + expect(action.textContent).toMatch('Update Featured Application'); + + const updateFeaturedAppSection = screen.getByTestId( + 'proposal-details-update-feature-app-section' + ); + expect(updateFeaturedAppSection).toBeInTheDocument(); + + const updateFeaturedAppLabel = screen.getByTestId('proposal-details-update-feature-app-label'); + expect(updateFeaturedAppLabel.textContent).toMatch('Featured Application Contract ID'); + + const updateFeaturedAppValue = screen.getByTestId('proposal-details-update-feature-app-value'); + expect(updateFeaturedAppValue.textContent).toMatch('rightCid123'); + + await waitFor(() => { + const currentFeaturedAppWeight = screen.getByTestId('config-change-current-value'); + expect(currentFeaturedAppWeight.textContent).toMatch('1.0'); + }); + + const newFeaturedAppWeight = screen.getByTestId('config-change-new-value'); + expect(newFeaturedAppWeight.textContent).toMatch('2.5'); + }); + + test('should show only new weight when featured app right is not found', async () => { + const updateFeaturedAppDetails = { + actionName: 'Update Featured Application', + action: 'SRARC_UpdateFeaturedAppRight', + proposal: { + rightContractId: 'archivedRightCid', // <- not 'rightCid123', so the mock returns not-found + newActivityWeight: '2.5', + reason: 'boosting rewards', + } as UpdateFeatureAppProposal, + } as ProposalDetails; + + render( + + + + ); + + // new value still shows + await waitFor(() => { + const newFeaturedAppWeight = screen.getByTestId('config-change-new-value'); + expect(newFeaturedAppWeight.textContent).toMatch('2.5'); + }); + // ...but there's no current-value box (contract archived → currentWeight '') + expect(screen.queryByTestId('config-change-current-value')).toBeNull(); + }); + test('should render update sv reward weight proposal details', () => { const svToUpdate = 'sv2'; const updateSvRewardWeightDetails = { @@ -417,7 +577,96 @@ describe('Proposal Details Content', () => { const maxNumInputsNewValue = within(changes[1]).getByTestId('config-change-new-value'); expect(maxNumInputsNewValue.textContent).toBe('4'); - expect(screen.getByTestId('json-diffs-details')).toBeInTheDocument(); + const jsonDiffsToggle = screen.getByTestId('json-diff-toggle'); + expect(jsonDiffsToggle).toHaveTextContent('Show JSON'); + expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'false'); + expect(screen.queryByText('JSON')).not.toBeInTheDocument(); + expect(screen.getByTestId('json-diffs-details')).not.toBeVisible(); + + expect( + screen.queryByTestId('proposal-details-disabled-fields-warning') + ).not.toBeInTheDocument(); + }); + + test('should warn when disabled fields were altered in an amulet rules config proposal', () => { + const amuletRulesConfigDetails = { + actionName: 'Set Amulet Rules Config', + action: 'CRARC_SetConfig', + proposal: { + configChanges: [ + { + fieldName: 'transferConfigCreateFee', + label: 'Transfer (Create Fee)', + currentValue: '0.03', + newValue: '0.04', + }, + { + fieldName: 'decentralizedSynchronizerActiveSynchronizer', + label: 'The currently active synchronizer', + currentValue: 'global-domain::12', + newValue: 'global-domain::13', + isId: true, + disabled: true, + }, + ], + }, + } as ProposalDetails; + + render( + + + + ); + + const warning = screen.getByTestId('proposal-details-disabled-fields-warning'); + expect(warning).toBeInTheDocument(); + expect(warning.textContent).toMatch(/Disabled fields have been altered in this vote proposal/); + + const changes = screen.getAllByTestId('config-change'); + expect(changes[1]).toHaveAttribute('data-disabled', 'true'); + expect(within(changes[1]).getByTestId('config-change-disabled-label')).toHaveTextContent( + 'Disabled field' + ); + }); + + test('should warn when disabled fields were altered in a dso rules config proposal', () => { + const dsoRulesConfigDetails = { + actionName: 'Set DSO Rules Configuration', + action: 'SRARC_SetConfig', + proposal: { + configChanges: [ + { + fieldName: 'decentralizedSynchronizerActiveSynchronizerId', + label: 'Decentralized synchronizer: Active synchronizer identifier', + currentValue: 'global-domain::12', + newValue: 'global-domain::13', + isId: true, + disabled: true, + }, + ], + }, + } as ProposalDetails; + + render( + + + + ); + + expect(screen.getByTestId('proposal-details-disabled-fields-warning')).toBeInTheDocument(); + expect(screen.getByTestId('config-change-disabled-label')).toHaveTextContent('Disabled field'); }); test('should render dso rules config changes', () => { @@ -498,7 +747,11 @@ describe('Proposal Details Content', () => { ); expect(dsoNumUnclaimedRewardsThresholdNewValue.textContent).toBe('20'); - expect(screen.getByTestId('json-diffs-details')).toBeInTheDocument(); + const jsonDiffsToggle = screen.getByTestId('json-diff-toggle'); + expect(jsonDiffsToggle).toHaveTextContent('Show JSON'); + expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'false'); + expect(screen.queryByText('JSON')).not.toBeInTheDocument(); + expect(screen.getByTestId('json-diffs-details')).not.toBeVisible(); }); }); @@ -575,6 +828,14 @@ describe('Proposal Details > Votes & Voting', () => { expect(acceptedVotesTab.getAttribute('aria-selected')).toBe('false'); expect(rejectedVotesTab.getAttribute('aria-selected')).toBe('false'); expect(noVoteVotesTab.getAttribute('aria-selected')).toBe('false'); + + const voterScroll = screen.getAllByTestId('proposal-details-voter-party-id-scroll'); + expect(voterScroll.length).toBeGreaterThan(0); + expect(voterScroll[0]).toHaveStyle({ overflowX: 'auto' }); + // Votes rows fill width and scroll — no fixed 270px cap. + expect(screen.getAllByTestId('proposal-details-voter-party-id')[0]).toHaveStyle({ + width: '100%', + }); }); test('should filter votes by tabs', async () => { @@ -795,9 +1056,11 @@ describe('Proposal Details > Votes & Voting', () => { const votingFormUrlInput = within(votingForm).getByTestId('your-vote-url-input'); expect(votingFormUrlInput).toBeInTheDocument(); + expect(votingFormUrlInput).toHaveAttribute('placeholder', URL_PLACEHOLDER); const votingFormReasonInput = within(votingForm).getByTestId('your-vote-reason-input'); expect(votingFormReasonInput).toBeInTheDocument(); + expect(votingFormReasonInput).toHaveAttribute('placeholder', VOTE_REASON_PLACEHOLDER); const votingFormAccept = within(votingForm).getByTestId('your-vote-accept'); expect(votingFormAccept).toBeInTheDocument(); @@ -848,6 +1111,12 @@ describe('Proposal Details > Votes & Voting', () => { expect(acceptButton.textContent).toMatch(/Accept/); expect(rejectButton).toBeInTheDocument(); expect(rejectButton.textContent).toMatch(/Reject/); + // Figma / #6912: Reject (left) → Accept (right); primary on the right + const voteButtons = within(votingForm).getAllByRole('button'); + expect(voteButtons.map(b => b.getAttribute('data-testid'))).toEqual([ + 'your-vote-reject', + 'your-vote-accept', + ]); }); test('render success message after api returns success', async () => { @@ -901,8 +1170,10 @@ describe('Proposal Details > Votes & Voting', () => { // This is because awaiting the button click makes it very difficult for the test runner to see the loading state user.click(acceptButton); - await waitFor(async () => { - expect(acceptButton.getAttribute('disabled')).toBeDefined(); + // once submission starts, the vote buttons are unmounted (replaced by the + // "Submitting..." state and then the submission message) + await waitFor(() => { + expect(acceptButton).not.toBeInTheDocument(); }); const submissionMessage = await screen.findByTestId('submission-message'); @@ -964,8 +1235,10 @@ describe('Proposal Details > Votes & Voting', () => { // This is because awaiting the button click makes it very difficult for the test runner to see the loading state user.click(acceptButton); - await waitFor(async () => { - expect(acceptButton.getAttribute('disabled')).toBeDefined(); + // once submission starts, the vote buttons are unmounted (replaced by the + // "Submitting..." state and then the submission message) + await waitFor(() => { + expect(acceptButton).not.toBeInTheDocument(); }); const submissionMessage = await screen.findByTestId('submission-message'); @@ -1060,3 +1333,127 @@ describe('Proposal Details > Votes & Voting', () => { expect(rejectButton).not.toBeDisabled(); }); }); + +describe('Open vote request whose effectivity has passed', () => { + const pastEffectivity = { + requester: 'sv1', + requesterIsYou: true, + votingThresholdDeadline: '2024-01-01 13:00', + voteTakesEffect: '2024-01-02 13:00', + status: 'In Progress', + } as ProposalVotingInformation; + + test('shows the vote form to an SV that has not voted', () => { + const votes: ProposalVote[] = [ + { sv: 'sv1', isYou: true, vote: 'no-vote' }, + { sv: 'sv3', vote: 'accepted', reason: { url: 'https://example.com', body: 'Reason' } }, + ]; + + render( + + + + ); + + const votingForm = screen.getByTestId('your-vote-form'); + expect(votingForm).toBeInTheDocument(); + expect(within(votingForm).getByTestId('your-vote-accept')).toBeInTheDocument(); + expect(within(votingForm).getByTestId('your-vote-reject')).toBeInTheDocument(); + }); + + test('shows the change-vote control to an SV that has already voted', async () => { + const user = userEvent.setup(); + const votes: ProposalVote[] = [ + { + sv: 'sv1', + isYou: true, + vote: 'accepted', + reason: { url: 'https://example.com', body: 'Reason' }, + }, + { sv: 'sv3', vote: 'accepted', reason: { url: 'https://example.com', body: 'Reason' } }, + ]; + + render( + + + + ); + + // An SV that has voted sees the Edit control rather than the form, until it starts editing. + expect(screen.queryByTestId('your-vote-form')).not.toBeInTheDocument(); + + const editButton = screen.getByTestId('your-vote-edit-button'); + expect(editButton).toBeInTheDocument(); + + await user.click(editButton); + + const votingForm = screen.getByTestId('your-vote-form'); + expect(votingForm).toBeInTheDocument(); + expect(within(votingForm).getByTestId('your-vote-reject')).toBeInTheDocument(); + }); + + test('shows SVs that have not voted as awaiting a response', () => { + const votes: ProposalVote[] = [ + { + sv: 'sv1', + isYou: true, + vote: 'accepted', + reason: { url: 'https://example.com', body: 'Reason' }, + }, + { sv: 'sv3', vote: 'no-vote' }, + ]; + + render( + + + + ); + + const noVoteTab = screen.getByTestId('no-vote-votes-tab'); + expect(noVoteTab.textContent).toMatch(/Awaiting Response/); + expect(noVoteTab.textContent).not.toMatch(/Did not Vote/); + + const statuses = screen + .getAllByTestId('proposal-details-vote-status-value') + .map(s => s.textContent); + expect(statuses).toContain('Awaiting Response'); + expect(statuses).not.toContain('No Vote'); + }); +}); + +describe('Closed proposal', () => { + test('does not show the vote form or the change-vote control', () => { + render( + + + + ); + + expect(screen.queryByTestId('your-vote-form')).not.toBeInTheDocument(); + expect(screen.queryByTestId('your-vote-edit-button')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/sv/frontend/src/__tests__/governance/proposal-listing.test.tsx b/apps/sv/frontend/src/__tests__/governance/proposal-listing.test.tsx index 98eaac5705..21fe59d81b 100644 --- a/apps/sv/frontend/src/__tests__/governance/proposal-listing.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/proposal-listing.test.tsx @@ -7,11 +7,16 @@ import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules' import { ContractId } from '@daml/types'; import { ProposalListingData } from '../../utils/types'; import { MemoryRouter } from 'react-router'; +import { svPartyId, voteRequests } from '../mocks/constants'; + +const sampleContractId = voteRequests.dso_rules_vote_requests[0] + .contract_id as ContractId; const inflightVoteRequests: ProposalListingData[] = [ { actionName: 'Feature Application', - contractId: '2abcde123456' as ContractId, + contractId: sampleContractId, + requester: svPartyId, votingThresholdDeadline: '2025-09-25 11:00', voteTakesEffect: '2025-09-26 11:00', yourVote: 'no-vote', @@ -21,7 +26,8 @@ const inflightVoteRequests: ProposalListingData[] = [ }, { actionName: 'Set DSO Rules Configuration', - contractId: 'bcde123456' as ContractId, + contractId: voteRequests.dso_rules_vote_requests[1].contract_id as ContractId, + requester: svPartyId, votingThresholdDeadline: '2025-09-25 11:00', voteTakesEffect: '2025-09-26 11:00', yourVote: 'accepted', @@ -34,7 +40,8 @@ const inflightVoteRequests: ProposalListingData[] = [ const voteHistory: ProposalListingData[] = [ { actionName: 'Feature Application', - contractId: '2abcde123456' as ContractId, + contractId: sampleContractId, + requester: svPartyId, votingThresholdDeadline: '2025-09-25 11:00', voteTakesEffect: '2025-09-26 11:00', yourVote: 'no-vote', @@ -44,7 +51,8 @@ const voteHistory: ProposalListingData[] = [ }, { actionName: 'Set DSO Rules Configuration', - contractId: '2bcde123456' as ContractId, + contractId: voteRequests.dso_rules_vote_requests[1].contract_id as ContractId, + requester: svPartyId, votingThresholdDeadline: '2025-09-25 11:00', voteTakesEffect: '2025-09-26 11:00', yourVote: 'accepted', @@ -93,6 +101,8 @@ describe('Inflight Vote Requests', () => { const uniqueId = 'proposals-request'; const data = { actionName: 'Feature Application', + contractId: sampleContractId, + requester: svPartyId, votingThresholdDeadline: '2025-09-25 11:00', voteTakesEffect: '2025-09-26 11:00', yourVote: 'no-vote', @@ -136,10 +146,49 @@ describe('Inflight Vote Requests', () => { expect(yourVote.textContent).toMatch(/No Vote/); }); + test('should render submitted by column with full party id and copy button', () => { + const uniqueId = 'proposals-request'; + const data = { + actionName: 'Feature Application', + contractId: sampleContractId, + requester: svPartyId, + votingThresholdDeadline: '2025-09-25 11:00', + voteTakesEffect: '2025-09-26 11:00', + yourVote: 'accepted', + status: 'In Progress', + voteStats: { accepted: 0, rejected: 0, 'no-vote': 0 }, + acceptanceThreshold: BigInt(11), + } as ProposalListingData; + + render( + + + + ); + + expect(screen.getByText('SUBMITTED BY')).toBeInTheDocument(); + expect(screen.getByTestId(`${uniqueId}-row-submitted-by-identifier-value`).textContent).toBe( + svPartyId + ); + expect( + screen.getByTestId(`${uniqueId}-row-submitted-by-identifier-copy-button`) + ).toBeInTheDocument(); + expect( + screen.queryByTestId(`${uniqueId}-row-submitted-by-identifier-badge`) + ).not.toBeInTheDocument(); + }); + test('should render Accepted inflight vote request', () => { const uniqueId = 'proposals-request'; const data = { actionName: 'Feature Application', + contractId: sampleContractId, + requester: svPartyId, votingThresholdDeadline: '2025-09-25 11:00', voteTakesEffect: '2025-09-26 11:00', yourVote: 'accepted', @@ -171,6 +220,8 @@ describe('Inflight Vote Requests', () => { const uniqueId = 'proposals-request'; const data = { actionName: 'Feature Application', + contractId: sampleContractId, + requester: svPartyId, votingThresholdDeadline: '2025-09-25 11:00', voteTakesEffect: '2025-09-26 11:00', yourVote: 'rejected', @@ -255,6 +306,8 @@ describe('Vote history', () => { const uniqueId = 'vote-history'; const data = { actionName: 'Feature Application', + contractId: sampleContractId, + requester: svPartyId, votingThresholdDeadline: '2024-09-25 11:00', voteTakesEffect: '2024-09-26 11:00', yourVote: 'accepted', diff --git a/apps/sv/frontend/src/__tests__/governance/proposal-search-validation.test.ts b/apps/sv/frontend/src/__tests__/governance/proposal-search-validation.test.ts new file mode 100644 index 0000000000..ede3aa3fd0 --- /dev/null +++ b/apps/sv/frontend/src/__tests__/governance/proposal-search-validation.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from 'vitest'; +import { activeProposalCid, closedVoteCid } from '../mocks/constants'; +import { isValidContractId } from '../../utils/proposalSearch'; + +describe('isValidContractId', () => { + test.each([ + [activeProposalCid, true], + [closedVoteCid, true], + [`00${'ab'.repeat(32)}`, true], + [`01${'ab'.repeat(12)}`, true], + ['', false], + ['not-a-valid-contract-id', false], + [`00${'a'.repeat(65)}`, false], + [`00${'ab'.repeat(31)}`, false], + [`01${'a'.repeat(25)}`, false], + ['00', false], + ])('%p -> %s', (value, expected) => { + expect(isValidContractId(value)).toBe(expected); + }); +}); diff --git a/apps/sv/frontend/src/__tests__/governance/proposal-search.test.tsx b/apps/sv/frontend/src/__tests__/governance/proposal-search.test.tsx new file mode 100644 index 0000000000..3a525043b9 --- /dev/null +++ b/apps/sv/frontend/src/__tests__/governance/proposal-search.test.tsx @@ -0,0 +1,83 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, test, vi } from 'vitest'; +import { MemoryRouter, Route, Routes, useSearchParams } from 'react-router'; +import { ProposalSearch } from '../../components/governance/ProposalSearch'; +import { activeProposalCid } from '../mocks/constants'; + +const SearchHarness: React.FC<{ onSearchChange: (query: string) => void }> = ({ + onSearchChange, +}) => { + const [searchParams, setSearchParams] = useSearchParams(); + + return ( + <> + + {searchParams.get('q') ?? ''} + + + ); +}; + +describe('ProposalSearch', () => { + test('does not reset input when stale URL update arrives while typing ahead', async () => { + const onSearchChange = vi.fn(); + render( + + + } /> + + + ); + + const input = screen.getByTestId('proposal-search-input'); + fireEvent.change(input, { target: { value: 'abc' } }); + + expect(input).toHaveValue('abc'); + expect(onSearchChange).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId('set-stale-url')); + + expect(input).toHaveValue('abc'); + expect(onSearchChange).not.toHaveBeenCalled(); + }); + + test('syncs input from URL on initial load', () => { + render( + + + } /> + + + ); + + expect(screen.getByTestId('proposal-search-input')).toHaveValue('from-url'); + }); + + test('clear search removes q from the URL', () => { + const onSearchChange = vi.fn(); + render( + + + } /> + + + ); + + expect(screen.getByTestId('url-q').textContent).toBe(activeProposalCid); + + fireEvent.click(screen.getByTestId('proposal-search-clear')); + + expect(screen.getByTestId('proposal-search-input')).toHaveValue(''); + expect(screen.getByTestId('url-q').textContent).toBe(''); + expect(onSearchChange).toHaveBeenLastCalledWith(''); + }); +}); diff --git a/apps/sv/frontend/src/__tests__/governance/proposal-summary.test.tsx b/apps/sv/frontend/src/__tests__/governance/proposal-summary.test.tsx index 8922fa5003..7eca1496df 100644 --- a/apps/sv/frontend/src/__tests__/governance/proposal-summary.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/proposal-summary.test.tsx @@ -3,6 +3,13 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, test } from 'vitest'; import { ProposalSummary } from '../../components/governance/ProposalSummary'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + EFFECTIVE_AT_LABEL, + PROPOSAL_REVIEW_TITLE, + SUPPORTING_URL_LABEL, + THRESHOLD_DEADLINE_LABEL, +} from '../../utils/constants'; import { ConfigChange } from '../../utils/types'; const url = 'https://example.com'; @@ -10,6 +17,29 @@ const summary = 'Summary of the proposal'; const expiryDate = '2025-09-25 11:00'; const effectiveDate = '2025-09-26 11:00'; +/** Shared labels for the post-rebase ProposalSummary / ProposalReviewField chrome. */ +const REVIEW_LABELS = { + title: PROPOSAL_REVIEW_TITLE, + action: CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + expiryDate: THRESHOLD_DEADLINE_LABEL, + effectiveDate: EFFECTIVE_AT_LABEL, + summary: 'Proposal Summary', + url: SUPPORTING_URL_LABEL, +} as const; + +function expectCommonReviewFields(actionName: string) { + expect(screen.getByTestId('proposal-review-title').textContent).toBe(REVIEW_LABELS.title); + expect(screen.getByTestId('action-title').textContent).toBe(REVIEW_LABELS.action); + expect(screen.getByTestId('action-field').textContent).toBe(actionName); + expect(screen.getByTestId('url-title').textContent).toBe(REVIEW_LABELS.url); + expect(screen.getByTestId('url-field').textContent).toBe(url); + expect(screen.getByTestId('summary-title').textContent).toBe(REVIEW_LABELS.summary); + expect(screen.getByTestId('summary-field').textContent).toBe(summary); + expect(screen.getByTestId('expiryDate-title').textContent).toBe(REVIEW_LABELS.expiryDate); + expect(screen.getByTestId('expiryDate-field').textContent).toBe(expiryDate); + expect(screen.getByTestId('effectiveDate-title').textContent).toBe(REVIEW_LABELS.effectiveDate); +} + describe('Review Proposal Component', () => { test('should render review proposal component for offboard member', () => { const actionName = 'Offboard Member'; @@ -29,23 +59,12 @@ describe('Review Proposal Component', () => { /> ); - expect(screen.getByTestId('action-title').textContent).toBe('Action'); - expect(screen.getByTestId('action-field').textContent).toBe(actionName); - - expect(screen.getByTestId('url-title').textContent).toBe('URL'); - expect(screen.getByTestId('url-field').textContent).toBe(url); - - expect(screen.getByTestId('summary-title').textContent).toBe('Summary'); - expect(screen.getByTestId('summary-field').textContent).toBe(summary); - - expect(screen.getByTestId('expiryDate-title').textContent).toBe('Threshold Deadline'); - expect(screen.getByTestId('expiryDate-field').textContent).toBe(expiryDate); - - expect(screen.getByTestId('effectiveDate-title').textContent).toBe('Effective Date'); + expectCommonReviewFields(actionName); expect(screen.getByTestId('effectiveDate-field').textContent).toBe(effectiveDate); - expect(screen.getByTestId('offboardMember-title').textContent).toBe('Offboard Member'); - expect(screen.getByTestId('offboardMember-field').textContent).toBe(offboardMember); + expect(screen.getByTestId('offboardMember-title').textContent).toBe('Member'); + expect(screen.getByTestId('offboardMember-party-id-value').textContent).toBe(offboardMember); + expect(screen.getByTestId('offboardMember-party-id-copy-button')).toBeInTheDocument(); }); test('should render review proposal component for offboard member at Threshold', () => { @@ -66,7 +85,7 @@ describe('Review Proposal Component', () => { /> ); - expect(screen.getByTestId('effectiveDate-title').textContent).toBe('Effective Date'); + expect(screen.getByTestId('effectiveDate-title').textContent).toBe(EFFECTIVE_AT_LABEL); expect(screen.getByTestId('effectiveDate-field').textContent).toBe('Threshold'); }); @@ -93,21 +112,16 @@ describe('Review Proposal Component', () => { /> ); - expect(screen.getByTestId('action-title').textContent).toBe('Action'); - expect(screen.getByTestId('action-field').textContent).toBe(actionName); - - expect(screen.getByTestId('url-title').textContent).toBe('URL'); - expect(screen.getByTestId('url-field').textContent).toBe(url); - - expect(screen.getByTestId('summary-title').textContent).toBe('Summary'); - expect(screen.getByTestId('summary-field').textContent).toBe(summary); - - expect(screen.getByTestId('expiryDate-title').textContent).toBe('Threshold Deadline'); - expect(screen.getByTestId('expiryDate-field').textContent).toBe(expiryDate); - - expect(screen.getByTestId('effectiveDate-title').textContent).toBe('Effective Date'); + expectCommonReviewFields(actionName); expect(screen.getByTestId('effectiveDate-field').textContent).toBe(effectiveDate); + expect(screen.getByTestId('svRewardWeightMember-title').textContent).toBe('Member'); + expect(screen.getByTestId('svRewardWeightMember-party-id-value').textContent).toBe( + svRewardWeightMember + ); + expect(screen.getByTestId('svRewardWeightMember-party-id-copy-button')).toBeInTheDocument(); + + expect(screen.getByTestId('configChange-title').textContent).toBe('Proposed Changes'); expect(screen.getByTestId('config-change-field-label').textContent).toBe(title); expect(screen.getByTestId('config-change-current-value').textContent).toBe(currentWeight); expect(screen.getByTestId('config-change-new-value').textContent).toBe(svRewardWeight); @@ -116,6 +130,7 @@ describe('Review Proposal Component', () => { test('should render review proposal component for feature application', () => { const actionName = 'Feature Application'; const provider = 'Digital-Asset-Eng-2'; + const activityWeight = '2.5'; render( { effectiveDate={effectiveDate} formType="grant-right" grantRight={provider} + activityWeight={activityWeight} onEdit={() => {}} onSubmit={() => {}} /> ); - expect(screen.getByTestId('action-title').textContent).toBe('Action'); - expect(screen.getByTestId('action-field').textContent).toBe(actionName); - - expect(screen.getByTestId('url-title').textContent).toBe('URL'); - expect(screen.getByTestId('url-field').textContent).toBe(url); - - expect(screen.getByTestId('summary-title').textContent).toBe('Summary'); - expect(screen.getByTestId('summary-field').textContent).toBe(summary); - - expect(screen.getByTestId('expiryDate-title').textContent).toBe('Threshold Deadline'); - expect(screen.getByTestId('expiryDate-field').textContent).toBe(expiryDate); - - expect(screen.getByTestId('effectiveDate-title').textContent).toBe('Effective Date'); + expectCommonReviewFields(actionName); expect(screen.getByTestId('effectiveDate-field').textContent).toBe(effectiveDate); expect(screen.getByTestId('grantRight-title').textContent).toBe('Provider Party ID'); - expect(screen.getByTestId('grantRight-field').textContent).toBe(provider); + expect(screen.getByTestId('grantRight-party-id-value').textContent).toBe(provider); + expect(screen.getByTestId('grantRight-party-id-copy-button')).toBeInTheDocument(); + + expect(screen.getByTestId('grantRightActivityWeight-title').textContent).toBe( + 'Activity Weight' + ); + expect(screen.getByTestId('grantRightActivityWeight-field').textContent).toBe(activityWeight); }); test('should render review proposal component for unfeature application', () => { @@ -170,28 +180,66 @@ describe('Review Proposal Component', () => { /> ); - expect(screen.getByTestId('action-title').textContent).toBe('Action'); - expect(screen.getByTestId('action-field').textContent).toBe(actionName); + expectCommonReviewFields(actionName); + expect(screen.getByTestId('effectiveDate-field').textContent).toBe(effectiveDate); + + expect(screen.getByTestId('revokeProviderPartyId-title').textContent).toBe('Provider Party ID'); + expect(screen.getByTestId('revokeProviderPartyId-party-id-value').textContent).toBe( + providerPartyId + ); + expect(screen.getByTestId('revokeProviderPartyId-party-id-copy-button')).toBeInTheDocument(); - expect(screen.getByTestId('url-title').textContent).toBe('URL'); - expect(screen.getByTestId('url-field').textContent).toBe(url); + expect(screen.getByTestId('revokeRight-title').textContent).toBe( + 'Featured Application Contract ID' + ); + expect(screen.getByTestId('revokeRight-field').textContent).toBe(contractId); + }); - expect(screen.getByTestId('summary-title').textContent).toBe('Summary'); - expect(screen.getByTestId('summary-field').textContent).toBe(summary); + test('should render review proposal component for update feature application', () => { + const actionName = 'Update Featured Application'; + const providerPartyId = 'a-party-id::1014912492'; + const rightCid = 'bcde123456'; + const currentActivityWeight = '1.0'; + const newActivityWeight = '2.5'; - expect(screen.getByTestId('expiryDate-title').textContent).toBe('Threshold Deadline'); - expect(screen.getByTestId('expiryDate-field').textContent).toBe(expiryDate); + render( + {}} + onSubmit={() => {}} + /> + ); - expect(screen.getByTestId('effectiveDate-title').textContent).toBe('Effective Date'); + expectCommonReviewFields(actionName); expect(screen.getByTestId('effectiveDate-field').textContent).toBe(effectiveDate); - expect(screen.getByTestId('revokeProviderPartyId-title').textContent).toBe('Provider Party ID'); - expect(screen.getByTestId('revokeProviderPartyId-field').textContent).toBe(providerPartyId); + expect(screen.getByTestId('updateProviderPartyId-title').textContent).toBe('Provider Party ID'); + expect(screen.getByTestId('updateProviderPartyId-party-id-value').textContent).toBe( + providerPartyId + ); + expect(screen.getByTestId('updateProviderPartyId-party-id-copy-button')).toBeInTheDocument(); - expect(screen.getByTestId('revokeRight-title').textContent).toBe( + expect(screen.getByTestId('updateRight-title').textContent).toBe( 'Featured Application Contract ID' ); - expect(screen.getByTestId('revokeRight-field').textContent).toBe(contractId); + expect(screen.getByTestId('updateRight-field').textContent).toBe(rightCid); + + expect(screen.getByTestId('updateActivityWeight-title').textContent).toBe('Proposed Changes'); + expect(screen.getByTestId('config-change-current-value').textContent).toBe( + currentActivityWeight + ); + expect(screen.getByTestId('config-change-new-value').textContent).toBe(newActivityWeight); + + expect(screen.queryByTestId('updateReason-field')).not.toBeInTheDocument(); }); test('should render review proposal component for dso rules config', () => { @@ -228,22 +276,12 @@ describe('Review Proposal Component', () => { /> ); - expect(screen.getByTestId('action-title').textContent).toBe('Action'); - expect(screen.getByTestId('action-field').textContent).toBe(actionName); - - expect(screen.getByTestId('url-title').textContent).toBe('URL'); - expect(screen.getByTestId('url-field').textContent).toBe(url); - - expect(screen.getByTestId('summary-title').textContent).toBe('Summary'); - expect(screen.getByTestId('summary-field').textContent).toBe(summary); - - expect(screen.getByTestId('expiryDate-title').textContent).toBe('Threshold Deadline'); - expect(screen.getByTestId('expiryDate-field').textContent).toBe(expiryDate); - - expect(screen.getByTestId('effectiveDate-title').textContent).toBe('Effective Date'); + expectCommonReviewFields(actionName); expect(screen.getByTestId('effectiveDate-field').textContent).toBe(effectiveDate); - expect(screen.getByText('Proposed Changes')).toBeDefined(); + expect(screen.getByTestId('configChange-title').textContent).toBe( + 'Proposed Configuration Changes' + ); expect(screen.getByText(numThresholdTitle)).toBeDefined(); expect(screen.getByText(voteCooldownTitle)).toBeDefined(); @@ -300,22 +338,12 @@ describe('Review Proposal Component', () => { /> ); - expect(screen.getByTestId('action-title').textContent).toBe('Action'); - expect(screen.getByTestId('action-field').textContent).toBe(actionName); - - expect(screen.getByTestId('url-title').textContent).toBe('URL'); - expect(screen.getByTestId('url-field').textContent).toBe(url); - - expect(screen.getByTestId('summary-title').textContent).toBe('Summary'); - expect(screen.getByTestId('summary-field').textContent).toBe(summary); - - expect(screen.getByTestId('expiryDate-title').textContent).toBe('Threshold Deadline'); - expect(screen.getByTestId('expiryDate-field').textContent).toBe(expiryDate); - - expect(screen.getByTestId('effectiveDate-title').textContent).toBe('Effective Date'); + expectCommonReviewFields(actionName); expect(screen.getByTestId('effectiveDate-field').textContent).toBe(effectiveDate); - expect(screen.getByText('Proposed Changes')).toBeDefined(); + expect(screen.getByTestId('configChange-title').textContent).toBe( + 'Proposed Configuration Changes' + ); expect(screen.getByText(feeTitle)).toBeDefined(); expect(screen.getByText(feeRateTitle)).toBeDefined(); diff --git a/apps/sv/frontend/src/__tests__/layout/sv-top-nav.test.tsx b/apps/sv/frontend/src/__tests__/layout/sv-top-nav.test.tsx new file mode 100644 index 0000000000..4f0d14f221 --- /dev/null +++ b/apps/sv/frontend/src/__tests__/layout/sv-top-nav.test.tsx @@ -0,0 +1,35 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router'; +import { describe, expect, test, vi } from 'vitest'; + +import SvTopNav from '../../components/layout/SvTopNav'; + +const navLinks = [ + { name: 'Global Synchronizer Information', path: '/dso' }, + { name: 'Governance', path: '/governance' }, + { name: 'Teluma Price', path: '/amulet-price' }, + { name: 'Validators', path: '/validator-onboarding' }, +]; + +describe('SvTopNav', () => { + test('renders brand, centered nav cluster, and logout', () => { + render( + + + + ); + + expect(screen.getByTestId('app-title')).toHaveTextContent('Supervalidator Operations'); + expect(screen.getByTestId('sv-top-nav-links')).toBeInTheDocument(); + expect(screen.getByTestId('navlink-dso')).toBeInTheDocument(); + expect(screen.getByTestId('navlink-governance')).toBeInTheDocument(); + expect(screen.getByTestId('logout-button')).toBeInTheDocument(); + + const row = screen.getByTestId('sv-top-nav'); + expect(row).toHaveStyle({ display: 'flex' }); + expect(screen.getByTestId('sv-top-nav-spacer-start')).toBeInTheDocument(); + expect(screen.getByTestId('sv-top-nav-spacer-end')).toBeInTheDocument(); + }); +}); diff --git a/apps/sv/frontend/src/__tests__/mocks/constants.ts b/apps/sv/frontend/src/__tests__/mocks/constants.ts index 9abb24ca93..5ef3a43091 100644 --- a/apps/sv/frontend/src/__tests__/mocks/constants.ts +++ b/apps/sv/frontend/src/__tests__/mocks/constants.ts @@ -292,7 +292,8 @@ export const voteResultsDsoRules: ListDsoRulesVoteResultsResponse = { url: '', body: 'd', }, - trackingCid: null, + trackingCid: + '99f1a2cbcd5a2dc9ad2fb9d17fec183d75de19ca91f623cbd2eaaf634e8d7cb4b5ca101220b5c5c20442f608e151ca702e0c4f51341a338c5979c0547dfcc80f911061ca99', action: getDsoSetConfigAction({ new: '2200', base: '100' }), }, completedAt: '2024-10-01T22:10:01.253341Z', @@ -388,3 +389,8 @@ if (!result.ok) { } export const svPartyId = dsoInfo.sv_party_id; + +export const activeProposalCid = + '10f1a2cbcd5a2dc9ad2fb9d17fec183d75de19ca91f623cbd2eaaf634e8d7cb4b5ca101220b5c5c20442f608e151ca702e0c4f51341a338c5979c0547dfcc80f911061ca91'; +export const closedVoteCid = + '99f1a2cbcd5a2dc9ad2fb9d17fec183d75de19ca91f623cbd2eaaf634e8d7cb4b5ca101220b5c5c20442f608e151ca702e0c4f51341a338c5979c0547dfcc80f911061ca99'; diff --git a/apps/sv/frontend/src/__tests__/mocks/handlers/sv-api.ts b/apps/sv/frontend/src/__tests__/mocks/handlers/sv-api.ts index c81c8f072c..e335af9bf9 100644 --- a/apps/sv/frontend/src/__tests__/mocks/handlers/sv-api.ts +++ b/apps/sv/frontend/src/__tests__/mocks/handlers/sv-api.ts @@ -8,6 +8,8 @@ import dayjs from 'dayjs'; import { http, HttpHandler, HttpResponse, PathParams } from 'msw'; import { FeatureSupportResponse, SuccessStatusResponse } from '@canton-network/scan-openapi'; import { + CountVoteResultsRequest, + CountVoteResultsResponse, ErrorResponse, ListDsoRulesVoteRequestsResponse, ListDsoRulesVoteResultsResponse, @@ -159,6 +161,28 @@ export const buildSvMock = (svUrl: string): HttpHandler[] => [ } ), + http.post( + `${svUrl}/v0/admin/sv/voteresults/count`, + ({ request }) => { + return request.json().then(data => { + const count = voteResultsAmuletRules.dso_rules_vote_results + .concat(voteResultsDsoRules.dso_rules_vote_results) + .filter(r => { + const isAccepted = r.outcome.tag === 'VRO_Accepted'; + const acceptedMatch = + data.accepted === undefined || data.accepted === null + ? true + : data.accepted === isAccepted; + const effectiveToMatch = data.effectiveTo + ? isAccepted && dayjs(r.outcome.value.effectiveAt).isBefore(dayjs(data.effectiveTo)) + : true; + return acceptedMatch && effectiveToMatch; + }).length; + return HttpResponse.json({ count }); + }); + } + ), + http.post(`${svUrl}/v0/admin/sv/votes`, () => { return new HttpResponse(null, { status: 201 }); }), @@ -249,7 +273,7 @@ export const buildSvMock = (svUrl: string): HttpHandler[] => [ { template_id: 'featured-app-right-template-id', contract_id: 'rightCid123', - payload: {}, + payload: { activityWeight: '1.0' }, created_event_blob: '', created_at: '2026-02-26T13:00:00.000000Z', }, @@ -269,7 +293,7 @@ export const buildSvMock = (svUrl: string): HttpHandler[] => [ ? { template_id: 'featured-app-right-template-id', contract_id: 'rightCid123', - payload: { provider: 'a-party-id::1014912492' }, + payload: { provider: 'a-party-id::1014912492', activityWeight: '1.0' }, created_event_blob: '', created_at: '2026-02-26T13:00:00.000000Z', } diff --git a/apps/sv/frontend/src/__tests__/sv.test.tsx b/apps/sv/frontend/src/__tests__/sv.test.tsx index 99dc546dce..a687d97714 100644 --- a/apps/sv/frontend/src/__tests__/sv.test.tsx +++ b/apps/sv/frontend/src/__tests__/sv.test.tsx @@ -52,8 +52,8 @@ describe('SV user can', () => { const user = userEvent.setup(); render(); - expect(await screen.findByText('Validator Onboarding')).toBeDefined(); - await user.click(screen.getByText('Validator Onboarding')); + expect(await screen.findByText('Validators')).toBeDefined(); + await user.click(screen.getByText('Validators')); expect(await screen.findByText('Validator Onboarding Secrets')).toBeDefined(); }); @@ -62,8 +62,8 @@ describe('SV user can', () => { const user = userEvent.setup(); render(); - expect(await screen.findByText('Validator Onboarding')).toBeDefined(); - await user.click(screen.getByText('Validator Onboarding')); + expect(await screen.findByText('Validators')).toBeDefined(); + await user.click(screen.getByText('Validators')); const partyHintInput = screen.getByTestId('create-party-hint'); await user.type(partyHintInput, 'wrong-input'); @@ -209,7 +209,7 @@ describe('An SetConfig request', () => { ); const button = screen.getByRole('button', { name: 'Send Request to Super Validators' }); - expect(button.getAttribute('disabled')).toBeDefined(); + expect(button.getAttribute('disabled')).not.toBeNull(); }); test('displays a warning when an SV tries to modify an AmuletRules field already changed by another request', async () => { @@ -240,7 +240,7 @@ describe('An SetConfig request', () => { 'You are therefore not allowed to modify the fields: transferConfig.createFee.fee' ); const button = screen.getByTestId('create-voterequest-submit-button'); - expect(button.getAttribute('disabled')).toBeDefined(); + expect(button.getAttribute('disabled')).not.toBeNull(); }); test('disables the Proceed button in the confirmation dialog if a conflict arises after request creation', async () => { @@ -281,7 +281,10 @@ describe('An SetConfig request', () => { ); const button = screen.getByRole('button', { name: 'Proceed' }); - expect(button.getAttribute('disabled')).toBeDefined(); + // the conflict is only detected once the vote requests query re-polls (1s interval) + await waitFor(() => expect(button.getAttribute('disabled')).not.toBeNull(), { + timeout: 5000, + }); }); }); @@ -303,8 +306,8 @@ describe('An AddFutureAmuletConfigSchedule request', () => { const user = userEvent.setup(); render(); - expect(await screen.findByText('Validator Onboarding')).toBeDefined(); - await user.click(screen.getByText('Validator Onboarding')); + expect(await screen.findByText('Validators')).toBeDefined(); + await user.click(screen.getByText('Validators')); expect(await screen.findByText('Validator Licenses')).toBeDefined(); @@ -315,8 +318,8 @@ describe('An AddFutureAmuletConfigSchedule request', () => { expect(await screen.findByDisplayValue('validator::15')).toBeDefined(); // secrets - expect(await screen.queryByText('encoded_secret')).toBeDefined(); - expect(await screen.queryByText('candidate_secret')).toBeNull(); + expect(screen.queryByText('encoded_secret')).not.toBeNull(); + expect(screen.queryByText('candidate_secret')).toBeNull(); }); }); @@ -372,7 +375,7 @@ describe('SetAmuletRules', () => { (calledWithBody.action as any).value.amuletRulesAction.value.newConfig.transferConfig .transferFee.steps // the second element is gone - ).toStrictEqual(initialSteps.filter((_, i) => i !== 1)); + ).toStrictEqual(initialSteps.filter((_: unknown, i: number) => i !== 1)); }, { timeout: 20000 } ); diff --git a/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts b/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts index a0bea6e3d9..1035962057 100644 --- a/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts +++ b/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts @@ -220,19 +220,19 @@ describe('buildAmuletRulesConfigFromChanges', () => { }, { fieldName: 'rewardConfigMintingVersion', - label: 'Reward config: Minting version', + label: 'Reward config: Minting scheme', currentValue: 'RewardVersion_FeaturedAppMarkers', newValue: 'RewardVersion_TrafficBasedAppRewards', }, { fieldName: 'rewardConfigDryRunVersion', - label: 'Reward config: Dry-run version', + label: 'Reward config: Dry-run minting scheme', currentValue: '', newValue: 'RewardVersion_TrafficBasedAppRewards', }, { fieldName: 'rewardConfigBatchSize', - label: 'Reward config: Batch size', + label: 'Reward config: Merkle tree batch size', currentValue: '100', newValue: '200', }, diff --git a/apps/sv/frontend/src/__tests__/utils/getRequesterPartyId.test.ts b/apps/sv/frontend/src/__tests__/utils/getRequesterPartyId.test.ts new file mode 100644 index 0000000000..aed68de29d --- /dev/null +++ b/apps/sv/frontend/src/__tests__/utils/getRequesterPartyId.test.ts @@ -0,0 +1,31 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from 'vitest'; +import type { SvInfo } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; +import { getRequesterPartyId } from '../../utils/governance'; + +const partyId = 'digital-asset-2::1220abc'; +const svName = 'Digital-Asset-2'; + +const svs = { + entriesArray: () => [[partyId, { name: svName } as SvInfo] as [string, SvInfo]], +}; + +describe('getRequesterPartyId', () => { + test('returns requester unchanged when it is already a party id', () => { + expect(getRequesterPartyId(partyId, svs)).toBe(partyId); + }); + + test('resolves sv name to party id', () => { + expect(getRequesterPartyId(svName, svs)).toBe(partyId); + }); + + test('returns requester when svs is undefined', () => { + expect(getRequesterPartyId(svName, undefined)).toBe(svName); + }); + + test('returns requester when sv is not in svs (e.g. offboarded)', () => { + expect(getRequesterPartyId('Offboarded-SV', svs)).toBe('Offboarded-SV'); + }); +}); diff --git a/apps/sv/frontend/src/components/Layout.tsx b/apps/sv/frontend/src/components/Layout.tsx index 8c69b6b673..c119706a6f 100644 --- a/apps/sv/frontend/src/components/Layout.tsx +++ b/apps/sv/frontend/src/components/Layout.tsx @@ -1,31 +1,49 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import { - Header, - Loading, - useUserState, - useVotesHooks, -} from '@canton-network/splice-common-frontend'; +import { Loading, useUserState, useVotesHooks } from '@canton-network/splice-common-frontend'; -import { Logout } from '@mui/icons-material'; -import { Box, Button, Divider, Stack, Typography } from '@mui/material'; -import Container from '@mui/material/Container'; -import Link from '@mui/material/Link'; +import { Box, Container, GlobalStyles } from '@mui/material'; +import { useLocation } from 'react-router'; import { useFeatureSupport } from '../contexts/SvContext'; -import { useNetworkInstanceName } from '../hooks/index'; import { useSvConfig } from '../utils'; +import { partyIdScrollGlobalStyles } from './beta/identifierStyles'; +import PartyIdScrollTracks from './PartyIdScrollTracks'; +import SvNavigationShell from './layout/SvNavigationShell'; +import { SvNavLinkItem } from './layout/SvNavLink'; +import { CONTENT_MAX_WIDTH, layoutTokens, PAGE_PX } from '../theme/tokens'; +import NetworkBanner from './layout/NetworkBanner'; interface LayoutProps { children: React.ReactNode; } -const Layout: React.FC = (props: LayoutProps) => { +const pathnameToPageName = (pathname: string, amuletName: string): string => { + if (pathname.startsWith('/governance')) { + return 'Governance'; + } + if (pathname.startsWith('/validator-onboarding')) { + return 'Validators'; + } + if (pathname.startsWith('/amulet-price')) { + return `${amuletName} Price`; + } + return 'Global Synchronizer Information'; +}; + +/** Figma content-width 1583px centered — nav uses full width inside Navigation shell */ +const contentShellSx = { + maxWidth: CONTENT_MAX_WIDTH, + mx: 'auto', + px: PAGE_PX, + width: '100%', +}; + +const Layout: React.FC = ({ children }) => { const config = useSvConfig(); const { logout } = useUserState(); - const networkInstanceName = useNetworkInstanceName(); - const networkInstanceNameColor = `colors.${networkInstanceName?.toLowerCase()}`; + const location = useLocation(); const featureSupport = useFeatureSupport(); const votesHooks = useVotesHooks(); @@ -40,55 +58,31 @@ const Layout: React.FC = (props: LayoutProps) => { return ; } - const navLinks = [ - { name: 'Information', path: 'dso' }, - { name: 'Validator Onboarding', path: 'validator-onboarding' }, - { name: `${config.spliceInstanceNames.amuletName} Price`, path: 'amulet-price' }, - { name: 'Governance', path: 'governance', badgeCount: actionsPending?.length }, + const navLinks: SvNavLinkItem[] = [ + { name: 'Global Synchronizer Information', path: '/dso', alsoActiveFor: ['/'] }, + { + name: 'Governance', + path: '/governance', + end: false, + badgeCount: actionsPending?.length, + }, + { name: `${config.spliceInstanceNames.amuletName} Price`, path: '/amulet-price' }, + { name: 'Validators', path: '/validator-onboarding' }, ]; + const pageName = pathnameToPageName(location.pathname, config.spliceInstanceNames.amuletName); + return ( - - {networkInstanceName === undefined ? ( - <> - ) : ( - - - You are on {networkInstanceName} - - - )} - -
- - - - -
-
+ + + + + - - {props.children} + + + {children} + ); diff --git a/apps/sv/frontend/src/components/PartyIdScrollTracks.tsx b/apps/sv/frontend/src/components/PartyIdScrollTracks.tsx new file mode 100644 index 0000000000..0a074f93c1 --- /dev/null +++ b/apps/sv/frontend/src/components/PartyIdScrollTracks.tsx @@ -0,0 +1,107 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useLayoutEffect } from 'react'; + +import { computeScrollMetrics } from '../hooks/useHorizontalScrollMetrics'; + +const nodeContainsPartyId = (node: Node): boolean => + node instanceof HTMLElement && + (node.classList.contains('party-id') || node.querySelector('.party-id') !== null); + +const usePartyIdScrollTracks = (): void => { + useLayoutEffect(() => { + const cleanups = new Map void>(); + let scanScheduled = false; + + const detachTrack = (partyIdRoot: HTMLElement) => { + const cleanup = cleanups.get(partyIdRoot); + if (!cleanup) return; + cleanup(); + cleanups.delete(partyIdRoot); + }; + + const attachTrack = (partyIdRoot: HTMLElement) => { + if (cleanups.has(partyIdRoot)) return; + + const input = partyIdRoot.querySelector('.MuiInputBase-input.Mui-disabled'); + if (!input) return; + + partyIdRoot.classList.add('identifier-scroll-area'); + + const track = document.createElement('div'); + track.className = 'party-id-scroll-track'; + track.setAttribute('aria-hidden', 'true'); + + const thumb = document.createElement('div'); + thumb.className = 'party-id-scroll-thumb'; + track.appendChild(thumb); + partyIdRoot.appendChild(track); + + const update = () => { + const metrics = computeScrollMetrics(input); + track.style.display = metrics.canScroll ? 'block' : 'none'; + if (!metrics.canScroll) return; + + thumb.style.transform = `translateX(${metrics.thumbLeftPercent}%) scaleX(${metrics.thumbWidthPercent / 100})`; + }; + + update(); + + input.addEventListener('scroll', update, { passive: true }); + const resizeObserver = new ResizeObserver(update); + resizeObserver.observe(input); + resizeObserver.observe(partyIdRoot); + + cleanups.set(partyIdRoot, () => { + input.removeEventListener('scroll', update); + resizeObserver.disconnect(); + track.remove(); + partyIdRoot.classList.remove('identifier-scroll-area'); + }); + }; + + const scan = () => { + for (const partyIdRoot of cleanups.keys()) { + if (!document.body.contains(partyIdRoot)) { + detachTrack(partyIdRoot); + } + } + document.querySelectorAll('.party-id').forEach(attachTrack); + }; + + const scheduleScan = () => { + if (scanScheduled) return; + scanScheduled = true; + requestAnimationFrame(() => { + scanScheduled = false; + scan(); + }); + }; + + scan(); + + const mutationObserver = new MutationObserver(mutations => { + const shouldScan = mutations.some( + mutation => + mutation.removedNodes.length > 0 || + Array.from(mutation.addedNodes).some(nodeContainsPartyId) + ); + if (shouldScan) scheduleScan(); + }); + + mutationObserver.observe(document.body, { childList: true, subtree: true }); + + return () => { + mutationObserver.disconnect(); + [...cleanups.keys()].forEach(detachTrack); + }; + }, []); +}; + +const PartyIdScrollTracks: React.FC = () => { + usePartyIdScrollTracks(); + return null; +}; + +export default PartyIdScrollTracks; diff --git a/apps/sv/frontend/src/components/beta/CopyableIdentifier.tsx b/apps/sv/frontend/src/components/beta/CopyableIdentifier.tsx index c4a7cff7b1..a8b60c012a 100644 --- a/apps/sv/frontend/src/components/beta/CopyableIdentifier.tsx +++ b/apps/sv/frontend/src/components/beta/CopyableIdentifier.tsx @@ -2,51 +2,159 @@ // SPDX-License-Identifier: Apache-2.0 import { ContentCopy } from '@mui/icons-material'; import { Box, Chip, IconButton, Typography } from '@mui/material'; +import { useRef } from 'react'; + +import { useHorizontalScrollMetrics } from '../../hooks/useHorizontalScrollMetrics'; +import { + ellipsisContainerSx, + ellipsisTextSx, + IDENTIFIER_COMPACT_MAX_WIDTH_PX, + scrollContainerSx, + scrollTextSx, + scrollThumbSx, + scrollTrackSx, +} from './identifierStyles'; export type CopyableIdentifierSize = 'small' | 'large'; +export type CopyableIdentifierOverflow = 'scroll' | 'ellipsis'; interface CopyableIdentifierProps { value: string; copyValue?: string; badge?: string; size: CopyableIdentifierSize; + overflow?: CopyableIdentifierOverflow; + /** + * Caps the text slot (Figma ~270px). With `overflow="scroll"`, the ID stays + * horizontally scrollable inside the cap (#1785 + Figma width). With + * `overflow="ellipsis"`, CSS ellipsis is used instead. + */ + maxWidth?: number; + /** + * Fill the parent width: party-ID text flexes/scrolls; copy + badge stay fixed. + * Used by Votes rows (ID + reason shrink; status stays right-aligned). + */ + fullWidth?: boolean; + /** When true, only the (scrollable) value is rendered — caller places copy / badge. */ + hideCopy?: boolean; 'data-testid': string; } +/** Gap between party-ID text and copy / You accessories. */ +const IDENTIFIER_ACCESSORY_GAP = '8px'; + const CopyableIdentifier: React.FC = ({ value, copyValue, badge, size, + overflow = 'scroll', + maxWidth, + fullWidth = false, + hideCopy = false, 'data-testid': testId, -}) => ( - - - {value} - - { - e.stopPropagation(); - e.preventDefault(); - navigator.clipboard.writeText(copyValue ?? value); +}) => { + const scrollRef = useRef(null); + const metrics = useHorizontalScrollMetrics(scrollRef, [value, maxWidth, fullWidth]); + const fontSize = size === 'small' ? '14px' : '16px'; + const isEllipsis = overflow === 'ellipsis'; + const compactMaxWidth = maxWidth ?? (isEllipsis ? IDENTIFIER_COMPACT_MAX_WIDTH_PX : undefined); + const showAccessories = !hideCopy; + + return ( + - - - {badge !== undefined && } - -); + + + + {value} + + + {!isEllipsis && metrics.canScroll && ( + + + + )} + + {showAccessories && ( + <> + { + e.stopPropagation(); + e.preventDefault(); + navigator.clipboard.writeText(copyValue ?? value); + }} + > + + + {badge !== undefined && ( + + )} + + )} + + ); +}; export default CopyableIdentifier; diff --git a/apps/sv/frontend/src/components/beta/CopyableUrl.tsx b/apps/sv/frontend/src/components/beta/CopyableUrl.tsx index 827bfa9a7f..10be7a6680 100644 --- a/apps/sv/frontend/src/components/beta/CopyableUrl.tsx +++ b/apps/sv/frontend/src/components/beta/CopyableUrl.tsx @@ -4,50 +4,107 @@ import { ContentCopy } from '@mui/icons-material'; import { Box, IconButton, Link } from '@mui/material'; import { sanitizeUrl } from '@canton-network/splice-common-frontend-utils'; +import { useRef } from 'react'; +import { useHorizontalScrollMetrics } from '../../hooks/useHorizontalScrollMetrics'; import type { CopyableIdentifierSize } from './CopyableIdentifier'; +import { + scrollContainerSx, + scrollThumbSx, + scrollTrackSx, + URL_COMPACT_MAX_WIDTH_PX, +} from './identifierStyles'; interface CopyableUrlProps { url: string; size: CopyableIdentifierSize; + /** + * Fill the parent width (proposal-details section). Default keeps the compact + * Supporting URL slot used elsewhere (~346px). + */ + fullWidth?: boolean; 'data-testid': string; } -function abbreviateUrl(url: string, maxLength = 50): string { - if (url.length <= maxLength) { - return url; - } - return `${url.slice(0, maxLength)}...`; -} - -const CopyableUrl: React.FC = ({ url, size, 'data-testid': testId }) => { +const CopyableUrl: React.FC = ({ + url, + size, + fullWidth = false, + 'data-testid': testId, +}) => { const sanitizedUrl = sanitizeUrl(url); + const fontSize = size === 'small' ? '14px' : '16px'; + const scrollRef = useRef(null); + const metrics = useHorizontalScrollMetrics(scrollRef, [sanitizedUrl, fullWidth]); + const textMaxWidth = fullWidth ? '100%' : URL_COMPACT_MAX_WIDTH_PX; return ( - - + - {abbreviateUrl(sanitizedUrl)} - + + + {sanitizedUrl} + + + {metrics.canScroll && ( + + + + )} + navigator.clipboard.writeText(sanitizedUrl)} > - + ); diff --git a/apps/sv/frontend/src/components/beta/MemberIdentifier.tsx b/apps/sv/frontend/src/components/beta/MemberIdentifier.tsx index bda90aa44c..b51cbe8cba 100644 --- a/apps/sv/frontend/src/components/beta/MemberIdentifier.tsx +++ b/apps/sv/frontend/src/components/beta/MemberIdentifier.tsx @@ -2,37 +2,35 @@ // SPDX-License-Identifier: Apache-2.0 import CopyableIdentifier from './CopyableIdentifier'; -import type { CopyableIdentifierSize } from './CopyableIdentifier'; +import type { CopyableIdentifierOverflow, CopyableIdentifierSize } from './CopyableIdentifier'; interface MemberIdentifierProps { partyId: string; isYou: boolean; size: CopyableIdentifierSize; + overflow?: CopyableIdentifierOverflow; + maxWidth?: number; + fullWidth?: boolean; 'data-testid': string; } -function abbreviatePartyId(partyId: string, length = 10): string { - const [partyHint, hash] = partyId.split('::'); - if (hash === undefined) { - return partyHint; - } - - const partOfHash = hash.slice(0, length); - - return `${partyHint}::${partOfHash}...`; -} - const MemberIdentifier: React.FC = ({ partyId, isYou, size, + overflow, + maxWidth, + fullWidth, 'data-testid': testId, }) => ( ); diff --git a/apps/sv/frontend/src/components/beta/PageSectionHeader.tsx b/apps/sv/frontend/src/components/beta/PageSectionHeader.tsx index 2f84fafd2f..188ce98ae1 100644 --- a/apps/sv/frontend/src/components/beta/PageSectionHeader.tsx +++ b/apps/sv/frontend/src/components/beta/PageSectionHeader.tsx @@ -5,12 +5,14 @@ import { Badge, Box, Typography } from '@mui/material'; interface PageSectionHeaderProps { title: string; badgeCount?: number; + badgeColor?: 'warning' | 'neutral'; 'data-testid': string; } const PageSectionHeader: React.FC = ({ title, badgeCount, + badgeColor = 'neutral', 'data-testid': testId, }) => ( @@ -19,8 +21,16 @@ const PageSectionHeader: React.FC = ({ diff --git a/apps/sv/frontend/src/components/beta/identifierStyles.ts b/apps/sv/frontend/src/components/beta/identifierStyles.ts new file mode 100644 index 0000000000..ec59d01c9d --- /dev/null +++ b/apps/sv/frontend/src/components/beta/identifierStyles.ts @@ -0,0 +1,171 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { SxProps, Theme } from '@mui/material'; + +const hiddenScrollbarSx = { + scrollbarWidth: 'none', + msOverflowStyle: 'none', + '&::-webkit-scrollbar': { + display: 'none', + }, +} as const; + +export const scrollContainerSx: SxProps = { + minWidth: 0, + overflowX: 'auto', + overflowY: 'hidden', + ...hiddenScrollbarSx, +}; + +export const scrollTextSx: SxProps = { + display: 'inline-block', + whiteSpace: 'nowrap', + textOverflow: 'clip', + // Intrinsic width for overflow scroll; parent must clip (minmax(0,1fr) / overflow). + width: 'max-content', +}; + +export const ellipsisContainerSx: SxProps = { + minWidth: 0, + // Figma truncated ID text slot (e.g. Vote proposal contract id Group 461): 270px + maxWidth: 270, + width: '100%', + overflow: 'hidden', +}; + +/** Figma Group 461 truncated ID text width — also used to cap scrollable compact IDs. */ +export const IDENTIFIER_COMPACT_MAX_WIDTH_PX = 270; + +/** Figma Supporting URL value slot (`552:960`). */ +export const URL_COMPACT_MAX_WIDTH_PX = 346; + +export const ellipsisTextSx: SxProps = { + display: 'block', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + maxWidth: '100%', +}; + +export const scrollableIdentifierFieldSx: SxProps = { + fontFamily: 'Source Code Pro, monospace', + display: 'inline-block', + width: 'max-content', + minWidth: '100%', + maxWidth: '100%', + whiteSpace: 'nowrap', + textOverflow: 'clip', +}; + +const scrollableInputTextSx = { + fontFamily: 'Source Code Pro, monospace', + overflowX: 'auto', + textOverflow: 'clip', + whiteSpace: 'nowrap', + ...hiddenScrollbarSx, +} as const; + +export const scrollableTextFieldSx: SxProps = { + '& .MuiOutlinedInput-input': scrollableInputTextSx, +}; + +export const scrollableSelectFieldSx: SxProps = { + '& .MuiSelect-select': { + ...scrollableInputTextSx, + display: 'block', + width: '100%', + maxWidth: '100%', + }, +}; + +export const scrollTrackSx: SxProps = { + height: 0, + opacity: 0, + overflow: 'hidden', + mt: 0, + borderRadius: 1, + bgcolor: 'rgba(255, 255, 255, 0.12)', + position: 'relative', + flexShrink: 0, + transition: 'opacity 0.15s ease, height 0.15s ease, margin-top 0.15s ease', + '.identifier-scroll-area:hover &': { + height: 4, + opacity: 1, + mt: 0.5, + bgcolor: 'rgba(255, 255, 255, 0.18)', + }, +}; + +export const scrollThumbSx = ( + thumbLeftPercent: number, + thumbWidthPercent: number +): SxProps => ({ + position: 'absolute', + top: 0, + bottom: 0, + left: `${thumbLeftPercent}%`, + width: `${thumbWidthPercent}%`, + borderRadius: 1, + bgcolor: 'rgba(255, 255, 255, 0.35)', + transition: 'background-color 0.15s ease', + '.identifier-scroll-area:hover &': { + bgcolor: 'rgba(255, 255, 255, 0.72)', + }, +}); + +export const partyIdScrollGlobalStyles = { + '.party-id': { + position: 'relative', + minWidth: 0, + maxWidth: '100%', + }, + '.party-id .MuiInputBase-root': { + overflow: 'visible !important', + minWidth: 0, + maxWidth: '100%', + width: '100%', + }, + '.party-id .MuiInputBase-input.Mui-disabled': { + overflowX: 'auto', + textOverflow: 'clip !important', + whiteSpace: 'nowrap', + scrollbarWidth: 'none', + msOverflowStyle: 'none', + }, + '.party-id .MuiInputBase-input.Mui-disabled::-webkit-scrollbar': { + display: 'none', + }, + '.party-id-scroll-track': { + position: 'absolute', + left: 0, + right: '40px', + bottom: 0, + height: 0, + opacity: 0, + overflow: 'hidden', + borderRadius: '4px', + backgroundColor: 'rgba(255, 255, 255, 0.12)', + pointerEvents: 'none', + transition: 'opacity 0.15s ease, height 0.15s ease', + }, + '.party-id.identifier-scroll-area:hover .party-id-scroll-track': { + height: '4px', + opacity: 1, + backgroundColor: 'rgba(255, 255, 255, 0.18)', + }, + '.party-id-scroll-thumb': { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + width: '100%', + transformOrigin: 'left center', + borderRadius: '4px', + backgroundColor: 'rgba(255, 255, 255, 0.35)', + transition: 'background-color 0.15s ease', + }, + '.party-id.identifier-scroll-area:hover .party-id-scroll-thumb': { + backgroundColor: 'rgba(255, 255, 255, 0.72)', + }, +} as const; diff --git a/apps/sv/frontend/src/components/form-components/ConfigField.tsx b/apps/sv/frontend/src/components/form-components/ConfigField.tsx index 61a0216eaa..ea6dc53a57 100644 --- a/apps/sv/frontend/src/components/form-components/ConfigField.tsx +++ b/apps/sv/frontend/src/components/form-components/ConfigField.tsx @@ -2,12 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 import { Link as RouterLink } from 'react-router'; -import { Box, Divider, TextField as MuiTextField, Typography } from '@mui/material'; +import { + Box, + Divider, + FormControl, + MenuItem, + Select, + TextField as MuiTextField, + Typography, +} from '@mui/material'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import { useFieldContext } from '../../hooks/formContext'; import type { ConfigChange, PendingConfigFieldInfo } from '../../utils/types'; import { nextScheduledSynchronizerUpgradeFormat } from '@canton-network/splice-common-frontend-utils'; +import { configFieldFieldSx, configFieldInputSx } from '../../themes/fieldStyles'; +import { + CREATE_PROPOSAL_CONFIG_INPUT_WIDTH, + CREATE_PROPOSAL_FIELD_BODY_SX, +} from '../../constants/createProposalLayout'; dayjs.extend(relativeTime); @@ -55,12 +68,16 @@ export const ConfigField: React.FC = props => { const textFieldProps = { variant: 'outlined' as const, - size: 'small' as const, color: field.state.meta.isDefaultValue ? ('primary' as const) : ('secondary' as const), focused: !field.state.meta.isDefaultValue, autoComplete: 'off' as const, + sx: configFieldFieldSx, + slotProps: { + input: { + sx: configFieldInputSx, + }, + }, inputProps: { - sx: { textAlign: 'right' }, 'data-testid': `config-field-${configChange.fieldName}`, }, disabled: isDisabled, @@ -70,44 +87,113 @@ export const ConfigField: React.FC = props => { <> - - + + {configChange.label} {configChange.fieldName} + + {configChange.options ? ( + + + + ) : ( + + field.handleChange({ + fieldName: configChange.fieldName, + value: e.target.value, + }) + } + /> + )} - - - field.handleChange({ - fieldName: configChange.fieldName, - value: e.target.value, - }) - } - /> + {configChange.description && ( + + {configChange.description} + + )} {!field.state.meta.isDefaultValue && ( Current Configuration: {configChange.currentValue} @@ -140,24 +226,46 @@ export const PendingConfigDisplay: React.FC = ({ pend effectiveDate === 'Threshold' ? 'at Threshold' : dayjs(effectiveDate).fromNow(); return ( - - Pending Configuration: {pendingValue}
- This{' '} - - pending configuration - {' '} - will go into effect {effectiveText} -
+ Pending Configuration:{' '} + + {pendingValue} + +
+ + This{' '} + + pending configuration + {' '} + will go into effect {effectiveText} + +
); }; @@ -189,7 +297,7 @@ export const SynchronizerUpgradeTimeDisplay: React.FC< {`Default: ${defaultMigrationTime}`} diff --git a/apps/sv/frontend/src/components/form-components/DateField.tsx b/apps/sv/frontend/src/components/form-components/DateField.tsx index 0e0bd7ba1f..3246b66d5b 100644 --- a/apps/sv/frontend/src/components/form-components/DateField.tsx +++ b/apps/sv/frontend/src/components/form-components/DateField.tsx @@ -2,12 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 import { useMemo } from 'react'; +import { KeyboardArrowDown } from '@mui/icons-material'; import { Box, Typography } from '@mui/material'; import { DesktopDateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import dayjs, { Dayjs } from 'dayjs'; import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils'; +import { + CREATE_PROPOSAL_FIELD_HELPER_SX, + CREATE_PROPOSAL_FIELD_LABEL_SX, +} from '../../constants/createProposalLayout'; import { useFieldContext } from '../../hooks/formContext'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { datePickerFieldSx } from '../../themes/fieldStyles'; +import { DATE_TIME_PLACEHOLDER } from '../../utils/constants'; export interface DateFieldProps { title?: string; @@ -25,13 +32,13 @@ export const DateField: React.FC = props => { return ( {title && ( - + {title} )} {description && ( - + {description} )} @@ -42,17 +49,35 @@ export const DateField: React.FC = props => { format={dateTimeFormatISO} minDateTime={minDate || dayjs()} ampm={false} + onClose={() => field.handleBlur()} onChange={newDate => field.handleChange(newDate?.format(dateTimeFormatISO)!)} enableAccessibleFieldDOMStructure={false} + slots={{ + openPickerIcon: KeyboardArrowDown, + }} slotProps={{ textField: { fullWidth: true, variant: 'outlined', id: `${id}-field`, + error: !field.state.meta.isValid, helperText: field.state.meta.errors?.[0], onBlur: field.handleBlur, + sx: datePickerFieldSx, inputProps: { 'data-testid': `${id}-field`, + placeholder: DATE_TIME_PLACEHOLDER, + }, + }, + openPickerButton: { + sx: { + color: 'text.light', + marginRight: 0, + padding: 0, + cursor: 'pointer', + '& .MuiSvgIcon-root': { + fontSize: 16, + }, }, }, }} diff --git a/apps/sv/frontend/src/components/form-components/EffectiveDateField.tsx b/apps/sv/frontend/src/components/form-components/EffectiveDateField.tsx index da838653f5..5ab4d82a41 100644 --- a/apps/sv/frontend/src/components/form-components/EffectiveDateField.tsx +++ b/apps/sv/frontend/src/components/form-components/EffectiveDateField.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Box, FormControlLabel, Radio, RadioGroup, Typography } from '@mui/material'; +import { KeyboardArrowDown } from '@mui/icons-material'; import { useFieldContext } from '../../hooks/formContext'; import { DesktopDateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils'; @@ -9,6 +9,11 @@ import dayjs from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { EffectivityType } from '../../utils/types'; import React, { useMemo } from 'react'; +import { RadioSelector } from './RadioSelector'; +import { datePickerFieldSx } from '../../themes/fieldStyles'; +import { DATE_TIME_PLACEHOLDER } from '../../utils/constants'; + +const effectiveAtDisplayFormat = 'YYYY-MM-DD HH:mm'; export interface EffectiveDateFieldProps { title?: string; @@ -19,10 +24,9 @@ export interface EffectiveDateFieldProps { export const EffectiveDateField: React.FC = props => { const { initialEffectiveDate, id } = props; - const title = props.title ? props.title : 'Vote Proposal Effectivity'; - const description = props.description - ? props.description - : 'Select the date and time the proposal will take effect'; + const title = props.title ?? 'Effective At'; + const dateDescription = + props.description ?? 'Select the block at which the proposal will take effect'; const field = useFieldContext<{ type: EffectivityType; @@ -55,72 +59,75 @@ export const EffectiveDateField: React.FC = props => { }; return ( - - - {title} - - - handleTypeChange(e.target.value as EffectivityType)} - > - } - label={Date} - /> - - {currentType === 'custom' && ( - <> - - {description} - - - - { - field.handleChange({ - type: 'custom', - effectiveDate: newDate?.format(dateTimeFormatISO) || undefined, - }); - }} - enableAccessibleFieldDOMStructure={false} - slotProps={{ - textField: { - fullWidth: true, - variant: 'outlined', - id: `${id}-field`, - className: 'effective-date-field', - onBlur: field.handleBlur, - error: !field.state.meta.isValid, - helperText: field.state.meta.errors?.[0], - inputProps: { - 'data-testid': `${id}-field`, + handleTypeChange(value as EffectivityType)} + options={[ + { + value: 'custom', + label: 'Date', + description: dateDescription, + extension: + currentType === 'custom' ? ( + + field.handleBlur()} + onChange={newDate => { + field.handleChange({ + type: 'custom', + effectiveDate: newDate?.format(dateTimeFormatISO) || undefined, + }); + }} + enableAccessibleFieldDOMStructure={false} + slots={{ + openPickerIcon: KeyboardArrowDown, + }} + slotProps={{ + openPickerButton: { + sx: { + color: 'text.light', + marginRight: 0, + p: 0, + cursor: 'pointer', + '& .MuiSvgIcon-root': { + fontSize: 16, + }, + }, }, - }, - }} - /> - - - )} - - } - label={ - - Make effective at threshold - - Allow the vote proposal to take effect immediately when 2/3 vote in favor - - - } - sx={{ mt: 2 }} - /> - - + textField: { + fullWidth: true, + variant: 'outlined', + id: `${id}-field`, + className: 'effective-date-field', + error: !field.state.meta.isValid, + helperText: field.state.meta.errors?.[0], + onBlur: field.handleBlur, + sx: datePickerFieldSx, + inputProps: { + 'data-testid': `${id}-field`, + placeholder: DATE_TIME_PLACEHOLDER, + }, + }, + }} + /> + + ) : null, + }, + { + value: 'threshold', + label: 'Make effective at threshold', + description: + 'This will allow the vote proposal to take effect immediately when 2/3 vote in favor', + radioId: 'effective-at-threshold-radio', + testId: 'effective-at-threshold-radio', + }, + ]} + /> ); }; diff --git a/apps/sv/frontend/src/components/form-components/FormControls.tsx b/apps/sv/frontend/src/components/form-components/FormControls.tsx index e661a7ba86..f409fdcf3d 100644 --- a/apps/sv/frontend/src/components/form-components/FormControls.tsx +++ b/apps/sv/frontend/src/components/form-components/FormControls.tsx @@ -2,8 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 import { Box, Button } from '@mui/material'; +import { useState } from 'react'; +import { + createProposalCancelButtonSx, + createProposalSubmitButtonSx, +} from '../../constants/formButtonStyles'; import { useFormContext } from '../../hooks/formContext'; import { useNavigate } from 'react-router'; +import { CancelProposalDialog } from '../governance/CancelProposalDialog'; export interface FormControlsProps { showConfirmation?: boolean; @@ -14,51 +20,68 @@ export const FormControls: React.FC = props => { const { showConfirmation, onEdit } = props; const form = useFormContext(); const navigate = useNavigate(); + const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + const submitTitle = showConfirmation ? 'Submit Proposal' : 'Review Proposal'; const cancelTitle = showConfirmation ? 'Edit Proposal' : 'Cancel'; const handleCancel = () => { if (showConfirmation) { onEdit(); - } else { - navigate('/governance/proposals/create'); + return; } + setCancelDialogOpen(true); + }; + + const handleConfirmCancel = () => { + setCancelDialogOpen(false); + navigate('/governance/proposals'); }; return ( - - + + + [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> + - [state.canSubmit, state.isSubmitting]} - children={([canSubmit, isSubmitting]) => ( - - )} + setCancelDialogOpen(false)} + onConfirm={handleConfirmCancel} /> - + ); }; diff --git a/apps/sv/frontend/src/components/form-components/ProposalSummaryField.tsx b/apps/sv/frontend/src/components/form-components/ProposalSummaryField.tsx index f6a7a83e53..e9e0f5b873 100644 --- a/apps/sv/frontend/src/components/form-components/ProposalSummaryField.tsx +++ b/apps/sv/frontend/src/components/form-components/ProposalSummaryField.tsx @@ -2,13 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 import { Box, TextField as MuiTextField, Typography } from '@mui/material'; +import { + CREATE_PROPOSAL_FIELD_HELPER_SX, + CREATE_PROPOSAL_FIELD_LABEL_SX, +} from '../../constants/createProposalLayout'; import { useFieldContext } from '../../hooks/formContext'; import { useDsoInfos } from '../../contexts/SvContext'; import { DEFAULT_PROPOSAL_SUMMARY_MAX_LENGTH, + PROPOSAL_SUMMARY_PLACEHOLDER, PROPOSAL_SUMMARY_SUBTITLE, PROPOSAL_SUMMARY_TITLE, } from '../../utils/constants'; +import { proposalSummaryFieldSx } from '../../themes/fieldStyles'; export interface ProposalSummaryFieldProps { id: string; @@ -28,10 +34,10 @@ export const ProposalSummaryField: React.FC = props = return ( - + {title || PROPOSAL_SUMMARY_TITLE} {optional && ( - + optional )} @@ -39,34 +45,37 @@ export const ProposalSummaryField: React.FC = props = field.handleChange(e.target.value)} error={!field.state.meta.isValid} helperText={field.state.meta.errors?.[0]} + placeholder={PROPOSAL_SUMMARY_PLACEHOLDER} inputProps={{ 'data-testid': id, maxLength }} - id={id} /> - + {subtitle || PROPOSAL_SUMMARY_SUBTITLE} {currentLength}/{maxLength} diff --git a/apps/sv/frontend/src/components/form-components/ProposalTypeField.tsx b/apps/sv/frontend/src/components/form-components/ProposalTypeField.tsx index d7c0c06dd0..3b27f3fb61 100644 --- a/apps/sv/frontend/src/components/form-components/ProposalTypeField.tsx +++ b/apps/sv/frontend/src/components/form-components/ProposalTypeField.tsx @@ -2,6 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Box, Typography } from '@mui/material'; +import { + CREATE_PROPOSAL_FIELD_BODY_SX, + CREATE_PROPOSAL_FIELD_LABEL_SX, +} from '../../constants/createProposalLayout'; import { useFieldContext } from '../../hooks/formContext'; export interface ProposalTypeFieldProps { @@ -15,11 +19,16 @@ export const ProposalTypeField: React.FC = props => { return ( - + {title} - + {field.state.value} diff --git a/apps/sv/frontend/src/components/form-components/RadioSelector.tsx b/apps/sv/frontend/src/components/form-components/RadioSelector.tsx new file mode 100644 index 0000000000..d08ef4c5c0 --- /dev/null +++ b/apps/sv/frontend/src/components/form-components/RadioSelector.tsx @@ -0,0 +1,144 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Box, FormControlLabel, Radio, RadioGroup, SvgIcon, Typography } from '@mui/material'; +import type { SvgIconProps } from '@mui/material'; +import React from 'react'; +import type { Theme } from '@mui/material/styles'; +import { theme } from '@canton-network/splice-common-frontend'; + +export interface RadioSelectorOption { + value: string; + label: string; + description?: string; + radioId?: string; + testId?: string; + extension?: React.ReactNode; +} + +export interface RadioSelectorProps { + title: string; + value: string; + onChange: (value: string) => void; + options: RadioSelectorOption[]; + id: string; +} + +const sectionSx = (theme: Theme) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + alignSelf: 'stretch', + gap: theme.spacing(3), + width: '100%', +}); + +const optionLabelColumnSx = (theme: Theme) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(0.5), + width: '100%', +}); + +const optionRowSx = (theme: Theme) => ({ + alignItems: 'flex-start', + gap: theme.spacing(1), + m: 0, + ml: 0, + mr: 0, + width: '100%', + '& .MuiFormControlLabel-label': { + flex: 1, + minWidth: 0, + }, +}); + +const radioSx = { + alignSelf: 'flex-start', + p: 0, + pt: '3px', + '& .MuiSvgIcon-root': { + fontSize: 16, + }, +} as const; + +const RADIO_RING_PATH = + 'M8.00004 1.33301C4.32004 1.33301 1.33337 4.31967 1.33337 7.99967C1.33337 11.6797 4.32004 14.6663 8.00004 14.6663C11.68 14.6663 14.6667 11.6797 14.6667 7.99967C14.6667 4.31967 11.68 1.33301 8.00004 1.33301ZM8.00004 13.333C5.05337 13.333 2.66671 10.9463 2.66671 7.99967C2.66671 5.05301 5.05337 2.66634 8.00004 2.66634C10.9467 2.66634 13.3334 5.05301 13.3334 7.99967C13.3334 10.9463 10.9467 13.333 8.00004 13.333Z'; +const RADIO_DOT_PATH = + 'M7.99996 11.3337C9.84091 11.3337 11.3333 9.84127 11.3333 8.00033C11.3333 6.15938 9.84091 4.66699 7.99996 4.66699C6.15901 4.66699 4.66663 6.15938 4.66663 8.00033C4.66663 9.84127 6.15901 11.3337 7.99996 11.3337Z'; + +const RadioIcon: React.FC = ({ checked, ...props }) => ( + + + {checked && } + +); + +export const RadioSelector: React.FC = props => { + const { title, value, onChange, options, id } = props; + + return ( + + + {title} + + + onChange(e.target.value)} + sx={theme => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(3), + m: 0, + width: '100%', + })} + > + {options.map(option => ( + } + checkedIcon={} + sx={radioSx} + /> + } + label={ + + + {option.label} + + {option.description && ( + + {option.description} + + )} + {option.extension && ( + e.stopPropagation()} + onMouseDown={e => e.stopPropagation()} + > + {option.extension} + + )} + + } + sx={optionRowSx} + /> + ))} + + + ); +}; diff --git a/apps/sv/frontend/src/components/form-components/SelectField.tsx b/apps/sv/frontend/src/components/form-components/SelectField.tsx index a6513bc4d4..7585cfa542 100644 --- a/apps/sv/frontend/src/components/form-components/SelectField.tsx +++ b/apps/sv/frontend/src/components/form-components/SelectField.tsx @@ -1,6 +1,7 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { KeyboardArrowDown } from '@mui/icons-material'; import { Box, FormControl, @@ -10,8 +11,11 @@ import { SelectChangeEvent, Typography, } from '@mui/material'; +import { CREATE_PROPOSAL_FIELD_LABEL_SX } from '../../constants/createProposalLayout'; import type { FormEvent } from 'react'; import { useFieldContext } from '../../hooks/formContext'; +import { scrollableSelectFieldSx } from '../beta/identifierStyles'; +import { selectFieldSx } from '../../themes/fieldStyles'; export type Option = { key: string; value: string }; export interface SelectFieldProps { @@ -21,10 +25,11 @@ export interface SelectFieldProps { onChange?: () => void; disabled?: boolean; placeholder?: string; + scrollableIdentifier?: boolean; } export const SelectField: React.FC = props => { - const { title, options, id, disabled = false, placeholder } = props; + const { title, options, id, disabled = false, placeholder, scrollableIdentifier = false } = props; const externalOnChange = props.onChange ?? (() => {}); const field = useFieldContext(); const handleSelectValueChange = (value: string) => { @@ -32,24 +37,32 @@ export const SelectField: React.FC = props => { externalOnChange(); }; - const showPlaceholder = !!placeholder && !field.state.value; - const isError = !field.state.meta.isValid && !showPlaceholder; + const article = /^[aeiou]/i.test(title) ? 'an' : 'a'; + const resolvedPlaceholder = placeholder ?? `Select ${article} ${title.toLowerCase()}`; + const showPlaceholder = !field.state.value; + const isError = !field.state.meta.isValid && !(placeholder && showPlaceholder); return ( - + {title} - + - field.handleChange(e.target.value as string) - } - onBlur={field.handleBlur} + state.canSubmit} + children={canSubmit => ( + <> + + + + )} /> - - - state.canSubmit} - children={canSubmit => ( - <> - - - - - )} - /> - - - - + + +
); }; diff --git a/apps/sv/frontend/src/components/forms/SetAmuletConfigRulesForm.tsx b/apps/sv/frontend/src/components/forms/SetAmuletConfigRulesForm.tsx index 2cf14577aa..b41b3c73f4 100644 --- a/apps/sv/frontend/src/components/forms/SetAmuletConfigRulesForm.tsx +++ b/apps/sv/frontend/src/components/forms/SetAmuletConfigRulesForm.tsx @@ -5,7 +5,21 @@ import { ActionRequiringConfirmation, AmuletRules_ActionRequiringConfirmation, } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; -import { THRESHOLD_DEADLINE_SUBTITLE } from '../../utils/constants'; +import { + CREATE_PROPOSAL_CONFIG_ROW_DIVIDER_GAP, + CREATE_PROPOSAL_CONFIG_ROW_GAP, + CREATE_PROPOSAL_FIELD_LABEL_SX, +} from '../../constants/createProposalLayout'; +import { + CREATE_PROPOSAL_LABEL_CONFIGURATION, + CREATE_PROPOSAL_LABEL_EFFECTIVE_AT, + CREATE_PROPOSAL_LABEL_PROPOSAL_SUMMARY, + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + CREATE_PROPOSAL_LABEL_SUPPORTING_URL, + CREATE_PROPOSAL_LABEL_THRESHOLD_DEADLINE, + SUPPORTING_URL_PLACEHOLDER, + THRESHOLD_DEADLINE_SUBTITLE, +} from '../../utils/constants'; import { buildAmuletRulesPendingConfigFields, configFormDataToConfigChanges, @@ -192,8 +206,24 @@ export const SetAmuletConfigRulesForm: () => JSX.Element = () => { dsoInfoQuery ); + const jsonDiffContent = + amuletConfigToCompareWith && amuletConfigToCompareWith[1] ? ( + + ) : null; + return ( - + {showConfirmation ? ( JSX.Element = () => { form.state.values.config, allAmuletConfigChanges )} + jsonDiff={{jsonDiffContent}} onEdit={() => setShowConfirmation(false)} onSubmit={() => {}} /> @@ -218,27 +249,43 @@ export const SetAmuletConfigRulesForm: () => JSX.Element = () => { )} - {field => } + {field => ( + + )} - - - Configuration + + + {CREATE_PROPOSAL_LABEL_CONFIGURATION} - {allAmuletConfigChanges.map((change, index) => ( - + {allAmuletConfigChanges.map(change => ( + {field => ( - f.fieldName === change.fieldName - )} - /> + + f.fieldName === change.fieldName + )} + /> + )} ))} + + {jsonDiffContent} JSX.Element = () => { > {field => ( @@ -265,6 +312,7 @@ export const SetAmuletConfigRulesForm: () => JSX.Element = () => { }} children={_ => ( @@ -278,7 +326,12 @@ export const SetAmuletConfigRulesForm: () => JSX.Element = () => { onChange: ({ value }) => validateSummary(value), }} > - {field => } + {field => ( + + )} JSX.Element = () => { onChange: ({ value }) => validateUrl(value), }} > - {field => } + {field => ( + + )} )} - - {amuletConfigToCompareWith && amuletConfigToCompareWith[1] ? ( - - ) : null} - - diff --git a/apps/sv/frontend/src/components/forms/SetDsoConfigRulesForm.tsx b/apps/sv/frontend/src/components/forms/SetDsoConfigRulesForm.tsx index 86f4307ec3..2b3ab969aa 100644 --- a/apps/sv/frontend/src/components/forms/SetDsoConfigRulesForm.tsx +++ b/apps/sv/frontend/src/components/forms/SetDsoConfigRulesForm.tsx @@ -20,7 +20,21 @@ import { useAppForm } from '../../hooks/form'; import { useProposalMutation } from '../../hooks/useProposalMutation'; import { buildDsoConfigChanges } from '../../utils/buildDsoConfigChanges'; import { buildDsoRulesConfigFromChanges } from '../../utils/buildDsoRulesConfigFromChanges'; -import { THRESHOLD_DEADLINE_SUBTITLE } from '../../utils/constants'; +import { + CREATE_PROPOSAL_CONFIG_ROW_DIVIDER_GAP, + CREATE_PROPOSAL_CONFIG_ROW_GAP, + CREATE_PROPOSAL_FIELD_LABEL_SX, +} from '../../constants/createProposalLayout'; +import { + CREATE_PROPOSAL_LABEL_CONFIGURATION, + CREATE_PROPOSAL_LABEL_EFFECTIVE_AT, + CREATE_PROPOSAL_LABEL_PROPOSAL_SUMMARY, + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + CREATE_PROPOSAL_LABEL_SUPPORTING_URL, + CREATE_PROPOSAL_LABEL_THRESHOLD_DEADLINE, + SUPPORTING_URL_PLACEHOLDER, + THRESHOLD_DEADLINE_SUBTITLE, +} from '../../utils/constants'; import { buildPendingConfigFields, configFormDataToConfigChanges, @@ -210,8 +224,23 @@ export const SetDsoConfigRulesForm: () => JSX.Element = () => { dsoInfoQuery ); + const jsonDiffContent = dsoConfigToCompareWith[1] ? ( + + ) : null; + return ( - + {showConfirmation ? ( JSX.Element = () => { effectiveDate={form.state.values.common.effectiveDate.effectiveDate} formType="config-change" configFormData={changedFields} + jsonDiff={{jsonDiffContent}} onEdit={() => setShowConfirmation(false)} onSubmit={() => {}} /> @@ -233,28 +263,44 @@ export const SetDsoConfigRulesForm: () => JSX.Element = () => { )} - {field => } + {field => ( + + )} - - - Configuration + + + {CREATE_PROPOSAL_LABEL_CONFIGURATION} - {dsoConfigChanges.map((change, index) => ( - + {dsoConfigChanges.map(change => ( + {field => ( - f.fieldName === change.fieldName - )} - effectiveDate={form.state.values.common.effectiveDate.effectiveDate} - /> + + f.fieldName === change.fieldName + )} + effectiveDate={form.state.values.common.effectiveDate.effectiveDate} + /> + )} ))} + + {jsonDiffContent} JSX.Element = () => { > {field => ( @@ -281,6 +327,7 @@ export const SetDsoConfigRulesForm: () => JSX.Element = () => { }} children={_ => ( @@ -294,7 +341,12 @@ export const SetDsoConfigRulesForm: () => JSX.Element = () => { onChange: ({ value }) => validateSummary(value), }} > - {field => } + {field => ( + + )} JSX.Element = () => { onChange: ({ value }) => validateUrl(value), }} > - {field => } + {field => ( + + )} )} - - {dsoConfigToCompareWith[1] ? ( - - ) : null} - - diff --git a/apps/sv/frontend/src/components/forms/UpdateFeaturedAppForm.tsx b/apps/sv/frontend/src/components/forms/UpdateFeaturedAppForm.tsx new file mode 100644 index 0000000000..23401daa92 --- /dev/null +++ b/apps/sv/frontend/src/components/forms/UpdateFeaturedAppForm.tsx @@ -0,0 +1,277 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState, useEffect } from 'react'; +import { useSvAdminClient } from '../../contexts/SvAdminServiceContext'; +import { useDsoInfos } from '../../contexts/SvContext'; +import { useFeaturedAppRightPicker } from '../../hooks/useFeaturedAppRightPicker'; +import { useProposalMutation } from '../../hooks/useProposalMutation'; +import { UpdateFeatureAppFormData } from '../../utils/types'; +import { createProposalActions, getInitialExpiration } from '../../utils/governance'; +import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils'; +import dayjs from 'dayjs'; +import { useAppForm } from '../../hooks/form'; +import { ActionRequiringConfirmation } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; +import { ContractId } from '@daml/types'; +import { FeaturedAppRight } from '@daml.js/splice-amulet/lib/Splice/Amulet'; +import { + validateEffectiveDate, + validateExpiration, + validateExpiryEffectiveDate, + validatePartyId, + validateRequiredActivityWeight, + validateSummary, + validateUrl, +} from './formValidators'; +import { FormLayout } from './FormLayout'; +import { ProposalSummary } from '../governance/ProposalSummary'; +import { useStore } from '@tanstack/react-form'; +import { + CREATE_PROPOSAL_LABEL_THRESHOLD_DEADLINE, + DEFAULT_APP_ACTIVITY_WEIGHT, + SUPPORTING_URL_PLACEHOLDER, + THRESHOLD_DEADLINE_SUBTITLE, +} from '../../utils/constants'; +import { EffectiveDateField } from '../form-components/EffectiveDateField'; +import { ProposalSubmissionError } from '../form-components/ProposalSubmissionError'; + +export const UpdateFeaturedAppForm: React.FC = () => { + const svAdminClient = useSvAdminClient(); + const dsoInfosQuery = useDsoInfos(); + const initialExpiration = getInitialExpiration(dsoInfosQuery.data); + const initialEffectiveDate = dayjs(initialExpiration).add(1, 'day'); + const picker = useFeaturedAppRightPicker(svAdminClient); + const [showConfirmation, setShowConfirmation] = useState(false); + const mutation = useProposalMutation(); + const idPrefix = 'update-featured-app'; + + const createProposalAction = createProposalActions.find( + a => a.value === 'SRARC_UpdateFeaturedAppRight' + ); + + const defaultValues: UpdateFeatureAppFormData = { + action: createProposalAction?.name || '', + expiryDate: initialExpiration.format(dateTimeFormatISO), + effectiveDate: { + type: 'custom', + effectiveDate: initialEffectiveDate.format(dateTimeFormatISO), + }, + url: '', + summary: '', + partyId: '', + rightCid: '', + newActivityWeight: '', + }; + + const form = useAppForm({ + defaultValues, + onSubmit: async ({ value }) => { + const action: ActionRequiringConfirmation = { + tag: 'ARC_DsoRules', + value: { + dsoAction: { + tag: 'SRARC_UpdateFeaturedAppRight', + value: { + rightCid: value.rightCid as ContractId, + update: { reason: '', newActivityWeight: value.newActivityWeight }, + }, + }, + }, + }; + if (!showConfirmation) setShowConfirmation(true); + else + await mutation.mutateAsync({ formData: value, action }).catch(e => { + console.error('Failed to submit proposal', e); + }); + }, + validators: { + onChange: ({ value }) => + validateExpiryEffectiveDate({ + expiration: value.expiryDate, + effectiveDate: value.effectiveDate.effectiveDate, + }), + }, + }); + + useEffect(() => { + const currentRightCid = form.state.values.rightCid; + const hasSelectedOption = picker.rightOptions.some(o => o.value === currentRightCid); + if (hasSelectedOption) return; + + const nextRightCid = picker.rightOptions.length === 1 ? picker.rightOptions[0].value : ''; + form.setFieldValue('rightCid', nextRightCid); + }, [form, picker.rightOptions]); + + const partyId = useStore(form.store, state => state.values.partyId); + const rightCid = useStore(form.store, state => state.values.rightCid); + const currentWeight = picker.currentWeights[rightCid] ?? DEFAULT_APP_ACTIVITY_WEIGHT; + + const providerHasNoRights = + picker.providerSearched && picker.rightOptions.length === 0 && !validatePartyId(partyId); + + const requiredActivityWeightSubtitle = + "Required. Scales the app's share of traffic-based rewards"; + + return ( + <> + + {showConfirmation ? ( + setShowConfirmation(false)} + onSubmit={() => {}} + /> + ) : ( + <> + + {field => } + + + validatePartyId(value), + onChangeAsyncDebounceMs: 500, + onChangeAsync: ({ value }) => picker.loadFeaturedAppRightsAndValidate(value), + }} + > + {field => ( + { + picker.resetOptions(); + }} + /> + )} + + + picker.validateRightSelection(value), + onChange: ({ value }) => picker.validateRightSelection(value), + }} + > + {field => ( + + )} + + + validateRequiredActivityWeight(value), + onChange: ({ value }) => validateRequiredActivityWeight(value), + }} + > + {field => ( + + )} + + + validateExpiration(value), + onBlur: ({ value }) => validateExpiration(value), + }} + > + {field => ( + + )} + + + validateEffectiveDate(value), + onBlur: ({ value }) => validateEffectiveDate(value), + }} + children={_ => ( + + )} + /> + + validateSummary(value), + onChange: ({ value }) => validateSummary(value), + }} + > + {field => } + + + validateUrl(value), + onChange: ({ value }) => validateUrl(value), + }} + > + {field => ( + + )} + + + )} + + + + + setShowConfirmation(false)} + /> + + + + ); +}; diff --git a/apps/sv/frontend/src/components/forms/UpdateSvRewardWeightForm.tsx b/apps/sv/frontend/src/components/forms/UpdateSvRewardWeightForm.tsx index 8a535d255a..0667f05c20 100644 --- a/apps/sv/frontend/src/components/forms/UpdateSvRewardWeightForm.tsx +++ b/apps/sv/frontend/src/components/forms/UpdateSvRewardWeightForm.tsx @@ -17,7 +17,17 @@ import { validateUrl, validateWeight, } from './formValidators'; -import { THRESHOLD_DEADLINE_SUBTITLE } from '../../utils/constants'; +import { + CREATE_PROPOSAL_LABEL_EFFECTIVE_AT, + CREATE_PROPOSAL_LABEL_MEMBER, + CREATE_PROPOSAL_LABEL_PROPOSAL_SUMMARY, + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + CREATE_PROPOSAL_LABEL_SUPPORTING_URL, + CREATE_PROPOSAL_LABEL_THRESHOLD_DEADLINE, + CREATE_PROPOSAL_LABEL_WEIGHT, + SUPPORTING_URL_PLACEHOLDER, + THRESHOLD_DEADLINE_SUBTITLE, +} from '../../utils/constants'; import { createProposalActions, formatBasisPoints, @@ -117,7 +127,12 @@ export const UpdateSvRewardWeightForm: React.FC = _ => { return ( <> - + {showConfirmation ? ( { ) : ( <> - {field => } + {field => ( + + )} { > {field => ( form.resetField('weight')} @@ -166,7 +186,7 @@ export const UpdateSvRewardWeightForm: React.FC = _ => { > {field => ( @@ -182,7 +202,7 @@ export const UpdateSvRewardWeightForm: React.FC = _ => { > {field => ( @@ -197,6 +217,7 @@ export const UpdateSvRewardWeightForm: React.FC = _ => { }} children={_ => ( @@ -210,7 +231,12 @@ export const UpdateSvRewardWeightForm: React.FC = _ => { onChange: ({ value }) => validateSummary(value), }} > - {field => } + {field => ( + + )} { onChange: ({ value }) => validateUrl(value), }} > - {field => } + {field => ( + + )} )} diff --git a/apps/sv/frontend/src/components/forms/formValidators.ts b/apps/sv/frontend/src/components/forms/formValidators.ts index bb6b9557f2..ed7954ceba 100644 --- a/apps/sv/frontend/src/components/forms/formValidators.ts +++ b/apps/sv/frontend/src/components/forms/formValidators.ts @@ -12,6 +12,8 @@ export const urlSchema = z.string().refine(url => isValidUrl(url), { export const summarySchema = z.string().min(1, { message: 'Summary is required' }); +export const reasonSchema = z.string().min(1, { message: 'Reason is required' }); + export const svSelectionSchema = z.string().min(1, { message: 'SV is required' }); const getExpirationSchema = (errMessage: string) => { @@ -66,6 +68,31 @@ export const rewardAmountSchema = z { message: 'Amount can have at most 10 decimal places' } ); +export const requiredActivityWeightSchema = z + .string() + .min(1, { message: 'Weight is required' }) + .regex(/^\d+(\.\d+)?$/, { message: 'Weight must be a valid non-negative number' }) + .refine( + v => { + const i = v.indexOf('.'); + return i === -1 || v.length - i - 1 <= 10; + }, + { message: 'Weight can have at most 10 decimal places' } + ); + +export const activityWeightSchema = z + .string() + .refine(v => v === '' || /^\d+(\.\d+)?$/.test(v), { + message: 'Weight must be a valid non-negative number', + }) + .refine( + v => { + const dotIndex = v.indexOf('.'); + return dotIndex === -1 || v.length - dotIndex - 1 <= 10; + }, + { message: 'Weight can have at most 10 decimal places' } + ); + export const validateWeight = (value: string): string | false => { const result = svWeightSchema.safeParse(value); return result.success ? false : result.error.issues[0].message; @@ -76,6 +103,16 @@ export const validateRewardAmount = (value: string): string | false => { return result.success ? false : result.error.issues[0].message; }; +export const validateActivityWeight = (value: string): string | false => { + const result = activityWeightSchema.safeParse(value); + return result.success ? false : result.error.issues[0].message; +}; + +export const validateRequiredActivityWeight = (value: string): string | false => { + const result = requiredActivityWeightSchema.safeParse(value); + return result.success ? false : result.error.issues[0].message; +}; + export const validateSvSelection = (value: string): string | false => { const result = svSelectionSchema.safeParse(value); return result.success ? false : result.error.issues[0].message; @@ -148,6 +185,11 @@ export const validateSummary = (value: string): string | false => { return result.success ? false : result.error.issues[0].message; }; +export const validateReason = (value: string): string | false => { + const result = reasonSchema.safeParse(value); + return result.success ? false : result.error.issues[0].message; +}; + export const validateUrl = (value: string): string | false => { const result = urlSchema.safeParse(value); return result.success ? false : result.error.issues[0].message; diff --git a/apps/sv/frontend/src/components/governance/ActionRequiredSection.tsx b/apps/sv/frontend/src/components/governance/ActionRequiredSection.tsx index 89d0ff1d4b..17adc8abd5 100644 --- a/apps/sv/frontend/src/components/governance/ActionRequiredSection.tsx +++ b/apps/sv/frontend/src/components/governance/ActionRequiredSection.tsx @@ -5,7 +5,11 @@ import { ContractId } from '@daml/types'; import { East } from '@mui/icons-material'; import { Alert, Box, Stack, Typography } from '@mui/material'; import { Link as RouterLink } from 'react-router'; -import { CopyableIdentifier, MemberIdentifier, PageSectionHeader } from '../../components/beta'; +import { CopyableIdentifier, PageSectionHeader } from '../../components/beta'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + VOTE_PROPOSAL_CONTRACT_ID_LABEL, +} from '../../utils/constants'; import React from 'react'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; @@ -19,18 +23,19 @@ export interface ActionRequiredData { votingCloses: string; createdAt: string; requester: string; - isYou?: boolean; } export interface ActionRequiredProps { actionRequiredRequests: ActionRequiredData[]; + noDataMessage?: string; } -export const ActionRequiredSection: React.FC = ( - props: ActionRequiredProps -) => { - const { actionRequiredRequests } = props; +const DEFAULT_NO_DATA_MESSAGE = 'No Action Required items available'; +export const ActionRequiredSection: React.FC = ({ + actionRequiredRequests, + noDataMessage = DEFAULT_NO_DATA_MESSAGE, +}) => { // Sort by voting closes date ascending (closest deadline first) const sortedRequests = actionRequiredRequests.toSorted((a, b) => dayjs(a.votingCloses).isBefore(dayjs(b.votingCloses)) ? -1 : 1 @@ -41,13 +46,14 @@ export const ActionRequiredSection: React.FC = ( {sortedRequests.length === 0 ? ( - No Action Required items available + {noDataMessage} ) : ( sortedRequests.map((ar, index) => ( @@ -59,7 +65,6 @@ export const ActionRequiredSection: React.FC = ( contractId={ar.contractId} votingEnds={ar.votingCloses} requester={ar.requester} - isYou={ar.isYou} /> )) )} @@ -75,11 +80,13 @@ interface ActionCardProps { contractId: ContractId; votingEnds: string; requester: string; - isYou?: boolean; } +const actionRequiredGridTemplate = + 'minmax(0, 0.85fr) minmax(0, 1fr) minmax(0, 1.15fr) minmax(0, 0.75fr) minmax(0, 0.85fr) 270px auto'; + const ActionCard = (props: ActionCardProps) => { - const { action, description, createdAt, contractId, votingEnds, requester, isYou } = props; + const { action, description, createdAt, contractId, votingEnds, requester } = props; const remainingTime = dayjs(votingEnds).fromNow(true); return ( @@ -91,99 +98,87 @@ const ActionCard = (props: ActionCardProps) => { - - - - - - - {description} - - } - data-testid="action-required-description" - /> - - - - } - data-testid="action-required-contract-id-segment" - /> - - - - - - + + {description} + + } + data-testid="action-required-description" + /> + - - - - } - data-testid="action-required-requester" + } + data-testid="action-required-contract-id-segment" + /> + + + - - - - View Details - - - + } + data-testid="action-required-submitted-by" + /> + + + View Details + +
@@ -201,14 +196,25 @@ const ActionCardSegment: React.FC = ({ content, 'data-testid': testId, }) => ( - + {title} @@ -219,13 +225,13 @@ const ActionCardSegment: React.FC = ({ color="text.light" fontWeight="medium" fontSize={14} - lineHeight={2} + lineHeight="26px" data-testid={`${testId}-content`} > {content} ) : ( - content + {content} )} ); diff --git a/apps/sv/frontend/src/components/governance/CancelProposalDialog.tsx b/apps/sv/frontend/src/components/governance/CancelProposalDialog.tsx new file mode 100644 index 0000000000..60b567aac1 --- /dev/null +++ b/apps/sv/frontend/src/components/governance/CancelProposalDialog.tsx @@ -0,0 +1,96 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import { Box, Button, Dialog, Typography } from '@mui/material'; +import React from 'react'; +import { + CREATE_PROPOSAL_CARD_BG, + CREATE_PROPOSAL_DISCARD_CTA, +} from '../../constants/createProposalLayout'; +import { + createProposalCancelButtonSx, + createProposalDiscardButtonSx, +} from '../../constants/formButtonStyles'; + +export interface CancelProposalDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export const CancelProposalDialog: React.FC = ({ + open, + onClose, + onConfirm, +}) => ( + + + + + + + + Are you sure you want to cancel this form? + + + + Any information you have entered on this vote will be lost and cannot be recovered. + + + + + + + + + + +); diff --git a/apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx b/apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx index 530e0d4b1c..d1964b8435 100644 --- a/apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx +++ b/apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { Box, Typography } from '@mui/material'; -import { ConfigChange } from '../../utils/types'; import { PartyId } from '@canton-network/splice-common-frontend'; +import { CREATE_PROPOSAL_FIELD_BODY_SX } from '../../constants/createProposalLayout'; +import { ConfigChange } from '../../utils/types'; interface ConfigValuesChangesProps { changes: ConfigChange[]; @@ -12,7 +13,8 @@ interface ConfigValuesChangesProps { export const ConfigValuesChanges: React.FC = props => { const { changes, isSummaryView } = props; - const textColor = isSummaryView ? 'text.secondary' : 'text.primary'; + const textColor = isSummaryView ? undefined : 'text.primary'; + const summaryLabelSx = isSummaryView ? CREATE_PROPOSAL_FIELD_BODY_SX : undefined; return ( = props => {changes.length === 0 && ( - + No changes found. @@ -31,17 +37,41 @@ export const ConfigValuesChanges: React.FC = props => {changes.map((change, index) => ( - - {change.label} - + + + {change.label} + + {change.disabled && ( + + Disabled field + + )} + {change.currentValue && ( <> diff --git a/apps/sv/frontend/src/components/governance/InitiateProposalHeader.tsx b/apps/sv/frontend/src/components/governance/InitiateProposalHeader.tsx new file mode 100644 index 0000000000..bb5e6b1f36 --- /dev/null +++ b/apps/sv/frontend/src/components/governance/InitiateProposalHeader.tsx @@ -0,0 +1,43 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Box, Typography } from '@mui/material'; +import React from 'react'; +import { CREATE_PROPOSAL_FIELD_HELPER_SX } from '../../constants/createProposalLayout'; + +export interface InitiateProposalHeaderProps { + actionName: string; + isReviewStep?: boolean; +} + +export const InitiateProposalHeader: React.FC = ({ + actionName, + isReviewStep = false, +}) => { + if (!isReviewStep) { + return null; + } + + return ( + + + {actionName} + + + Review your proposal before submitting + + + ); +}; diff --git a/apps/sv/frontend/src/components/governance/InitiateProposalLayout.tsx b/apps/sv/frontend/src/components/governance/InitiateProposalLayout.tsx new file mode 100644 index 0000000000..68b3db78a9 --- /dev/null +++ b/apps/sv/frontend/src/components/governance/InitiateProposalLayout.tsx @@ -0,0 +1,24 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Box } from '@mui/material'; +import React from 'react'; +import { CONTENT_MAX_WIDTH } from '../../theme/tokens'; + +export interface InitiateProposalLayoutProps { + children: React.ReactNode; +} + +export const InitiateProposalLayout: React.FC = ({ children }) => ( + + {children} + +); diff --git a/apps/sv/frontend/src/components/governance/JsonDiffAccordion.tsx b/apps/sv/frontend/src/components/governance/JsonDiffAccordion.tsx index 94127165a9..0a09fcaf81 100644 --- a/apps/sv/frontend/src/components/governance/JsonDiffAccordion.tsx +++ b/apps/sv/frontend/src/components/governance/JsonDiffAccordion.tsx @@ -1,22 +1,269 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { useState } from 'react'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { Accordion, AccordionDetails, AccordionSummary, Box, Typography } from '@mui/material'; +import { Box, Collapse, Typography } from '@mui/material'; + +const JSON_DIFF_FRAME_BACKGROUND = '#363636'; + +const JSON_DIFF_VIEWPORT_MAX_HEIGHT = '320px'; + +const jsonDiffMonoSx = { + fontFamily: '"Source Code Pro", monospace', + fontSize: '14px', + fontWeight: 400, + lineHeight: '26px', + fontFeatureSettings: "'liga' off, 'clig' off", +} as const; + +const DIFF = '& [data-testid="config-diffs-display"]'; + +const jsonSectionTitleSx = { + color: 'common.white', + fontFamily: 'Inter, sans-serif', + fontSize: 12, + fontWeight: 600, + lineHeight: '22px', + textTransform: 'uppercase', + margin: 0, + display: 'block', +} as const; + +const toggleSx = { + display: 'inline-flex', + boxSizing: 'border-box', + height: '28px', + minWidth: '108px', + padding: '2px 8px', + alignItems: 'center', + justifyContent: 'center', + gap: '4px', + borderRadius: '2px', + border: '1px solid', + borderColor: 'secondary.main', + bgcolor: 'transparent', + cursor: 'pointer', + margin: 0, + minHeight: 0, + lineHeight: 0, + flexShrink: 0, +}; + +const toggleLabelSx = { + color: 'common.white', + fontFamily: 'Inter, sans-serif', + fontSize: 14, + fontWeight: 400, + lineHeight: '22px', + margin: 0, + display: 'block', +}; + +const jsonDiffFrameSx = { + display: 'flex', + padding: '12px 24px', + justifyContent: 'flex-start', + alignItems: 'stretch', + gap: '10px', + alignSelf: 'stretch', + width: '100%', + minWidth: 0, + maxWidth: '100%', + boxSizing: 'border-box', + backgroundColor: JSON_DIFF_FRAME_BACKGROUND, +} as const; + +const collapseSx = { + width: '100%', + minWidth: 0, + maxWidth: '100%', + alignSelf: 'stretch', + overflow: 'hidden', + '& .MuiCollapse-wrapperInner': { + width: '100%', + minWidth: 0, + maxWidth: '100%', + }, +} as const; + +const jsonDiffHeaderRowSx = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + alignSelf: 'stretch', + width: '100%', +} as const; + +const jsonDiffRootSx = { + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + alignSelf: 'stretch', + width: '100%', + minWidth: 0, + maxWidth: '100%', + overflow: 'hidden', +} as const; + +const jsonDiffViewportSx = { + width: '100%', + minWidth: 0, + maxHeight: JSON_DIFF_VIEWPORT_MAX_HEIGHT, + overflowY: 'auto', + overflowX: 'hidden', + + '& > div': { + width: '100%', + minWidth: 0, + maxWidth: '100%', + }, + + '& [data-testid="stringify-display"], & [data-testid="config-diffs-display"]': { + width: '100%', + }, + + '& [data-testid="stringify-display"]': { + ...jsonDiffMonoSx, + color: 'common.white', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + overflowWrap: 'break-word', + }, + + [`${DIFF} .jsondiffpatch-delta`]: { + ...jsonDiffMonoSx, + color: 'common.white', + display: 'block', + maxWidth: '100%', + boxSizing: 'border-box', + }, + + [`${DIFF} > .jsondiffpatch-delta`]: { + padding: 0, + }, + + [`${DIFF} .jsondiffpatch-delta pre`]: { + ...jsonDiffMonoSx, + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + overflowWrap: 'break-word', + }, + + // Unchanged rows inherit grey onto keys and values (do not set color on pre/property-name directly). + [`${DIFF} .jsondiffpatch-unchanged, ${DIFF} .jsondiffpatch-movedestination`]: { + color: 'gray', + }, + + [`${DIFF} .jsondiffpatch-delta ul, ${DIFF} ul.jsondiffpatch-delta`]: { + listStyleType: 'none', + padding: '0 0 0 20px', + margin: 0, + }, + + [`${DIFF} li`]: { + display: 'block', + }, + + [`${DIFF} .jsondiffpatch-added .jsondiffpatch-value pre::after, ${DIFF} .jsondiffpatch-modified .jsondiffpatch-right-value pre::after, ${DIFF} .jsondiffpatch-deleted .jsondiffpatch-value pre::after`]: + { + content: '""', + padding: 0, + }, + + [`${DIFF} li.jsondiffpatch-added:not(:last-child) > .jsondiffpatch-value::after, ${DIFF} li.jsondiffpatch-deleted:not(:last-child) > .jsondiffpatch-value::after, ${DIFF} li.jsondiffpatch-modified:not(:last-child) > .jsondiffpatch-right-value::after`]: + { + content: '","', + color: 'common.white', + padding: 0, + }, + + [`${DIFF} .jsondiffpatch-modified .jsondiffpatch-right-value`]: { + marginLeft: 0, + }, + [`${DIFF} .jsondiffpatch-modified .jsondiffpatch-right-value::before`]: { + content: '" -> "', + }, +} as const; + +interface JsonToggleButtonProps { + expanded: boolean; + onClick: () => void; +} + +const JsonToggleButton: React.FC = ({ expanded, onClick }) => ( + + + {expanded ? 'Hide JSON' : 'Show JSON'} + + + +); + +export interface JsonDiffAccordionProps { + children: React.ReactNode; + /** `form` — JSON label left, toggle right; `review` — toggle only. */ + variant?: 'form' | 'review'; +} + +export const JsonDiffAccordion: React.FC = ({ + children, + variant = 'review', +}) => { + const [expanded, setExpanded] = useState(false); + const toggle = ( + setExpanded(current => !current)} /> + ); + + const content = ( + + + {children} + + + ); -export const JsonDiffAccordion = ({ children }: { children: React.ReactNode }): JSX.Element => { return ( - - - } - aria-controls="json-diff-content" - id="json-diff-header" - > - JSON Diffs - - {children} - + + {variant === 'form' ? ( + + + JSON + + {toggle} + + ) : ( + {toggle} + )} + {content} ); }; diff --git a/apps/sv/frontend/src/components/governance/ProposalDetailsContent.tsx b/apps/sv/frontend/src/components/governance/ProposalDetailsContent.tsx index e12d28c38b..a0a1c3ff2b 100644 --- a/apps/sv/frontend/src/components/governance/ProposalDetailsContent.tsx +++ b/apps/sv/frontend/src/components/governance/ProposalDetailsContent.tsx @@ -7,8 +7,19 @@ import { VoteRequest, } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; import { ContractId } from '@daml/types'; -import { ChevronLeft, Edit } from '@mui/icons-material'; -import { Box, Button, Divider, Stack, Tab, Tabs, Typography } from '@mui/material'; +import { ChevronLeft, ContentCopy, Edit } from '@mui/icons-material'; +import { + Alert, + Box, + Button, + Chip, + Divider, + IconButton, + Stack, + Tab, + Tabs, + Typography, +} from '@mui/material'; import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; @@ -20,6 +31,7 @@ import { } from '@canton-network/splice-common-frontend'; import { Link as RouterLink } from 'react-router'; import { + ConfigChange, ProposalDetails, ProposalVote, ProposalVotingInformation, @@ -34,6 +46,22 @@ import { CreateUnallocatedUnclaimedActivityRecordSection } from './proposal-deta import { CopyableIdentifier, CopyableUrl, MemberIdentifier, VoteStats } from '../beta'; import { useQuery } from '@tanstack/react-query'; import { useSvAdminClient } from '../../contexts/SvAdminServiceContext'; +import { + DEFAULT_APP_ACTIVITY_WEIGHT, + EFFECTIVE_AT_LABEL, + PROPOSAL_CREATED_LABEL, + PROPOSAL_SUMMARY_TITLE, + SUPPORTING_URL_LABEL, + THRESHOLD_DEADLINE_LABEL, + VOTE_PROPOSAL_CONTRACT_ID_LABEL, + VOTE_REASON_SUMMARY_LABEL, + VOTE_REASON_URL_LABEL, +} from '../../utils/constants'; + +/** True when a proposal changed fields that are locked/disabled in the create UI (e.g. emergency API). */ +export function hasAlteredDisabledFields(changes: ConfigChange[]): boolean { + return changes.some(c => c.disabled && c.currentValue !== c.newValue); +} dayjs.extend(relativeTime); @@ -47,18 +75,13 @@ export interface ProposalDetailsContentProps { type VoteTab = Extract | 'all'; -const now = () => dayjs(); - export const ProposalDetailsContent: React.FC = props => { const { contractId, proposalDetails, votingInformation, votes, currentSvPartyId } = props; const votesHooks = useVotesHooks(); const dsoInfoQuery = useDsoInfos(); - const isEffective = - votingInformation.voteTakesEffect && dayjs(votingInformation.voteTakesEffect).isBefore(now()); - const isClosed = - !proposalDetails.isVoteRequest || isEffective || votingInformation.status === 'Rejected'; + const isClosed = !proposalDetails.isVoteRequest || votingInformation.status === 'Rejected'; const dsoConfigToCompareWith = useMemo(() => { if (proposalDetails.action === 'SRARC_SetConfig') { @@ -187,13 +210,15 @@ export const ProposalDetailsContent: React.FC = pro size="small" color="secondary" startIcon={} + data-testid="proposal-details-back-to-all-votes" > Back to all votes - + {/* Figma details content starts at Action — no inner section title. */} + = pro valueId="proposal-details-action-value" /> - - } - labelId="proposal-details-contractid-label" - /> - {proposalDetails.action === 'SRARC_OffboardSv' && ( )} {proposalDetails.action === 'SRARC_GrantFeaturedAppRight' && ( - + )} {proposalDetails.action === 'SRARC_RevokeFeaturedAppRight' && ( )} + {proposalDetails.action === 'SRARC_UpdateFeaturedAppRight' && ( + + )} + {proposalDetails.action === 'SRARC_UpdateSvRewardWeight' && ( = pro {proposalDetails.action === 'CRARC_SetConfig' && ( <> + {hasAlteredDisabledFields(proposalDetails.proposal.configChanges) && ( + + Disabled fields have been altered in this vote proposal. + + )} } /> - + {amuletConfigToCompareWith ? ( = pro {proposalDetails.action === 'SRARC_SetConfig' && ( <> + {hasAlteredDisabledFields(proposalDetails.proposal.configChanges) && ( + + Disabled fields have been altered in this vote proposal. + + )} } /> - + {dsoConfigToCompareWith?.[1] ? ( = pro )} } labelId="proposal-details-url-label" /> + + + } + labelId="proposal-details-contractid-label" + /> - + = pro partyId={votingInformation.requester} isYou={false} size="large" + fullWidth data-testid="proposal-details-requester-party-id" /> } /> + + @@ -331,7 +395,8 @@ export const ProposalDetailsContent: React.FC = pro /> @@ -397,7 +462,7 @@ export const ProposalDetailsContent: React.FC = pro {getFilteredVotes().map((vote, index) => ( @@ -447,7 +512,8 @@ export const ProposalDetailsContent: React.FC = pro }; interface VoteSectionProps extends PropsWithChildren { - title: string; + /** Section heading (e.g. Proposal Information). Omit when Figma has no heading above the fields. */ + title?: string; 'data-testid': string; bordered?: boolean; centered?: boolean; @@ -456,9 +522,11 @@ interface VoteSectionProps extends PropsWithChildren { const VoteSection = React.forwardRef( ({ title, children, 'data-testid': testId, bordered = false, centered = false }, ref) => ( - - {title} - + {title !== undefined && ( + + {title} + + )} void; } +/** Gap between party-ID / You and the copy icon. */ +const VOTE_ROW_ACCESSORY_GAP_PX = 8; +/** Gap between copy icon and status column. */ +const VOTE_ROW_STATUS_GAP_PX = 40; +/** Fixed copy column so every row’s copy icon shares one vertical edge. */ +const VOTE_ROW_COPY_COL_WIDTH_PX = 40; +/** Fixed status column so Accepted / Awaiting Response share the right edge. */ +const VOTE_ROW_STATUS_COL_WIDTH_PX = 170; +/** Trailing tracks (8 + copy + 40 + status) — party-ID (+ You) width is calc(100% − this). */ +const VOTE_ROW_FIXED_TRAILING_PX = + VOTE_ROW_ACCESSORY_GAP_PX + + VOTE_ROW_COPY_COL_WIDTH_PX + + VOTE_ROW_STATUS_GAP_PX + + VOTE_ROW_STATUS_COL_WIDTH_PX; + const VoteItem: React.FC = ({ voter, url, @@ -501,29 +584,104 @@ const VoteItem: React.FC = ({ <> - - - - + + + + + + {isYou && ( + + )} + {comment && ( - - {comment} - + + + {VOTE_REASON_SUMMARY_LABEL} + + + {comment} + + + )} + {url && ( + + + {VOTE_REASON_URL_LABEL} + + + )} - {url && } - + + + { + e.stopPropagation(); + e.preventDefault(); + navigator.clipboard.writeText(voter); + }} + > + + + + + = ({ data-testid="your-vote-edit-button" sx={{ fontSize: 16, + minWidth: 0, + px: 0, }} > Edit @@ -566,6 +726,7 @@ const OffboardMemberSection = ({ memberPartyId }: OffboardMemberSectionProps) => partyId={memberPartyId} isYou={false} size="large" + fullWidth data-testid="proposal-details-member-party-id" /> } @@ -576,9 +737,10 @@ const OffboardMemberSection = ({ memberPartyId }: OffboardMemberSectionProps) => interface FeatureAppSectionProps { provider: string; + activityWeight: string; } -const FeatureAppSection = ({ provider }: FeatureAppSectionProps) => { +const FeatureAppSection = ({ provider, activityWeight }: FeatureAppSectionProps) => { return ( { } labelId="proposal-details-feature-app-label" /> + ); }; @@ -649,6 +817,79 @@ const UnfeatureAppSection = ({ rightContractId }: UnfeatureAppSectionProps) => { ); }; +interface UpdateFeatureAppSectionProps { + rightContractId: string; + newActivityWeight: string; +} + +const UpdateFeatureAppSection = ({ + rightContractId, + newActivityWeight, +}: UpdateFeatureAppSectionProps) => { + const svAdminClient = useSvAdminClient(); + const providerQuery = useQuery({ + queryKey: ['featuredAppRightProviderAndWeight', rightContractId], + queryFn: async () => { + const response = await svAdminClient.lookupFeaturedAppRightByContractId(rightContractId); + const contract = response.featured_app_right; + const payload = contract?.payload as + | { provider?: string; activityWeight?: string | null } + | undefined; + return { + provider: payload?.provider ?? null, + currentWeight: contract ? (payload?.activityWeight ?? DEFAULT_APP_ACTIVITY_WEIGHT) : '', + }; + }, + }); + return ( + + {providerQuery?.data?.provider && ( + + } + labelId="proposal-details-update-feature-label" + /> + )} + + } + labelId="proposal-details-update-feature-app-label" + /> + + } + /> + + ); +}; + interface UpdateSvRewardWeightSectionProps { svToUpdate: string; currentWeight: string; @@ -673,6 +914,7 @@ const UpdateSvRewardWeightSection = ({ partyId={svToUpdate} isYou={false} size="large" + fullWidth data-testid="proposal-details-member-party-id" /> } diff --git a/apps/sv/frontend/src/components/governance/ProposalListingSection.tsx b/apps/sv/frontend/src/components/governance/ProposalListingSection.tsx index a9609f88c6..9e29b47fbe 100644 --- a/apps/sv/frontend/src/components/governance/ProposalListingSection.tsx +++ b/apps/sv/frontend/src/components/governance/ProposalListingSection.tsx @@ -18,6 +18,11 @@ import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules' import { ContractId } from '@daml/types'; import { useNavigate } from 'react-router'; import { CopyableIdentifier, PageSectionHeader, VoteStats } from '../../components/beta'; +import { + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + THRESHOLD_DEADLINE_LABEL, + VOTE_PROPOSAL_CONTRACT_ID_LABEL, +} from '../../utils/constants'; import { ProposalListingData, ProposalListingStatus, YourVoteStatus } from '../../utils/types'; import { InfoOutlined } from '@mui/icons-material'; import dayjs from 'dayjs'; @@ -31,6 +36,9 @@ interface ProposalListingSectionProps { data: ProposalListingData[]; noDataMessage: string; uniqueId: string; + badgeCount?: number; + isLoading?: boolean; + loadingMessage?: string; showThresholdDeadline?: boolean; showVoteStats?: boolean; showStatus?: boolean; @@ -58,7 +66,7 @@ const sortProposals = ( return data.toSorted((a, b) => dayjs(b.voteTakesEffect).diff(dayjs(a.voteTakesEffect))); } - // For effectiveAtAsc (Inflight Votes): + // For effectiveAtAsc (In-flight Proposals): // Threshold items first (by votes desc, then deadline asc), then dated items (by effective date asc) return data .toSorted((a, b) => dayjs(a.votingThresholdDeadline).diff(dayjs(b.votingThresholdDeadline))) @@ -66,17 +74,94 @@ const sortProposals = ( .toSorted((a, b) => getEffectiveDate(a).diff(getEffectiveDate(b))); }; -const getColumnsCount = (...shown: (boolean | undefined)[]) => 4 + shown.filter(Boolean).length; +const getColumnsCount = (...shown: (boolean | undefined)[]) => 5 + shown.filter(Boolean).length; const getGridTemplate = (columnsCount: number) => `minmax(0, 1fr) minmax(0, 0.7fr) ${'1fr '.repeat(columnsCount - 2).trim()}`; +const governanceTableHeadCellSx = { + py: '10px', + px: '16px', + fontSize: 12, + fontWeight: 600, + textTransform: 'uppercase' as const, + color: 'colors.neutral.80', + borderBottom: 'none', + display: 'flex', + alignItems: 'center', +}; + +const governanceTableBodyCellSx = { + py: '15px', + px: '16px', + borderBottom: 'none', + display: 'flex', + alignItems: 'center', + alignSelf: 'stretch', + minWidth: 0, +}; + +interface SubmittedByCellProps { + requester: string; + uniqueId: string; +} + +const identifierCellSx = { + ...governanceTableBodyCellSx, + overflow: 'visible', +}; + +const SubmittedByCell: React.FC = ({ requester, uniqueId }) => ( + + + +); + +interface TableHeaderProps { + showThresholdDeadline?: boolean; + showStatus?: boolean; + showVoteStats?: boolean; +} + +const TableHeader: React.FC = ({ + showThresholdDeadline, + showStatus, + showVoteStats, +}) => ( + <> + {CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE} + {VOTE_PROPOSAL_CONTRACT_ID_LABEL} + {showThresholdDeadline ? ( + <> + {THRESHOLD_DEADLINE_LABEL} + SUBMITTED BY + EFFECTIVE AT + + ) : ( + <> + EFFECTIVE AT + SUBMITTED BY + {showStatus && STATUS} + + )} + {showVoteStats && VOTES} + YOUR VOTE + +); + export const ProposalListingSection: React.FC = props => { const { sectionTitle, data, noDataMessage, uniqueId, + badgeCount, + isLoading, + loadingMessage = 'Searching…', showThresholdDeadline, showVoteStats, showStatus, @@ -105,23 +190,29 @@ export const ProposalListingSection: React.FC = pro return ( - + {sortedData.length === 0 && !hasNextPage ? ( - + isLoading ? ( + + ) : ( + + ) ) : ( <> - ACTION - VOTE PROPOSAL CONTRACT ID - {showThresholdDeadline && THRESHOLD DEADLINE} - EFFECTIVE AT - {showStatus && STATUS} - {showVoteStats && VOTES} - YOUR VOTE + @@ -131,6 +222,7 @@ export const ProposalListingSection: React.FC = pro actionName={vote.actionName} description={vote.description} contractId={vote.contractId} + requester={vote.requester} uniqueId={uniqueId} votingThresholdDeadline={vote.votingThresholdDeadline} voteTakesEffect={vote.voteTakesEffect} @@ -216,10 +308,33 @@ const InfoBox: React.FC = ({ info, 'data-testid': testId }) => { ); }; +interface LoadingBoxProps { + message: string; + 'data-testid': string; +} + +const LoadingBox: React.FC = ({ message, 'data-testid': testId }) => { + return ( + + + + {message} + + + ); +}; + interface VoteRowProps { actionName: string; description?: string; contractId: ContractId; + requester: string; status: ProposalListingStatus; uniqueId: string; voteStats: Record; @@ -237,6 +352,7 @@ const VoteRow: React.FC = React.memo(props => { actionName, description, contractId, + requester, status, uniqueId, voteStats, @@ -266,10 +382,24 @@ const VoteRow: React.FC = React.memo(props => { }} data-testid={`${uniqueId}-row`} > - + {actionName} @@ -277,6 +407,7 @@ const VoteRow: React.FC = React.memo(props => { = React.memo(props => { WebkitBoxOrient: 'vertical', overflow: 'hidden', textOverflow: 'ellipsis', - lineHeight: 1.4, + lineHeight: '20px', }} > {description} )} - + - {showThresholdDeadline && ( - - {votingThresholdDeadline} - - )} - - {voteTakesEffect} - - - {showStatus && ( - - {status} - + {showThresholdDeadline ? ( + <> + + {votingThresholdDeadline} + + + + {voteTakesEffect} + + + ) : ( + <> + + {voteTakesEffect} + + + {showStatus && ( + + {status} + + )} + )} {showVoteStats && ( - + = React.memo(props => { /> )} - + = ({ + id, + label, + value, + subtitle, +}) => ( + + + {label} + + + {subtitle && ( + + {subtitle} + + )} + + {typeof value === 'string' ? ( + + {value} + + ) : ( + + {value} + + )} + +); diff --git a/apps/sv/frontend/src/components/governance/ProposalSearch.tsx b/apps/sv/frontend/src/components/governance/ProposalSearch.tsx new file mode 100644 index 0000000000..6a7212d096 --- /dev/null +++ b/apps/sv/frontend/src/components/governance/ProposalSearch.tsx @@ -0,0 +1,215 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { memo, useEffect, useRef, useState } from 'react'; +import SearchIcon from '@mui/icons-material/Search'; +import { Box, InputAdornment, Link, TextField, Typography } from '@mui/material'; +import type { Theme } from '@mui/material/styles'; +import { useNavigate, useSearchParams } from 'react-router'; +import { ContractId } from '@daml/types'; +import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; +import { CONTRACT_ID_VALIDATION_MESSAGE, isValidContractId } from '../../utils/proposalSearch'; +import { + fieldDescriptionSx, + fieldSectionSx, + fieldSectionTitleSx, + singleLineFieldSx, +} from '../../themes/fieldStyles'; +import { scrollableTextFieldSx } from '../beta/identifierStyles'; + +const searchTextFieldSx = (theme: Theme) => ({ + ...(typeof singleLineFieldSx === 'function' ? singleLineFieldSx(theme) : singleLineFieldSx), + ...scrollableTextFieldSx, +}); + +function getEffectiveSearchQuery(value: string): string { + const trimmed = value.trim(); + return isValidContractId(trimmed) ? trimmed : ''; +} + +function syncSearchQuery( + value: string, + lastSyncedSearchRef: React.MutableRefObject, + onSearchChange: (query: string) => void +): void { + const effective = getEffectiveSearchQuery(value); + if (effective === lastSyncedSearchRef.current) { + return; + } + lastSyncedSearchRef.current = effective; + onSearchChange(effective); +} + +export interface ProposalSearchProps { + onSearchChange: (query: string) => void; +} + +export const ProposalSearch: React.FC = memo(function ProposalSearch({ + onSearchChange, +}) { + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const urlQuery = searchParams.get('q') ?? ''; + const [inputValue, setInputValue] = useState(urlQuery); + /** Blocks stale URL from overwriting local input while a pending write is in flight. */ + const pendingUrlValueRef = useRef(null); + const lastSyncedSearchRef = useRef(''); + const onSearchChangeRef = useRef(onSearchChange); + onSearchChangeRef.current = onSearchChange; + + useEffect(() => { + setInputValue(prev => { + if (prev === urlQuery) { + if (pendingUrlValueRef.current === urlQuery) { + pendingUrlValueRef.current = null; + } + return prev; + } + + if (pendingUrlValueRef.current !== null) { + if (urlQuery === pendingUrlValueRef.current) { + pendingUrlValueRef.current = null; + } + return prev; + } + + syncSearchQuery(urlQuery, lastSyncedSearchRef, query => { + onSearchChangeRef.current(query); + }); + return urlQuery; + }); + }, [urlQuery]); + + const syncUrl = (value: string) => { + const trimmed = value.trim(); + if (!trimmed || !isValidContractId(trimmed)) { + setSearchParams( + prev => { + if (!prev.has('q')) { + return prev; + } + const next = new URLSearchParams(prev); + next.delete('q'); + return next; + }, + { replace: true } + ); + return; + } + + pendingUrlValueRef.current = trimmed; + setSearchParams( + prev => { + if (prev.get('q') === trimmed) { + return prev; + } + const next = new URLSearchParams(prev); + next.set('q', trimmed); + return next; + }, + { replace: true } + ); + }; + + const handleChange = (value: string) => { + pendingUrlValueRef.current = value; + setInputValue(value); + syncSearchQuery(value, lastSyncedSearchRef, query => { + onSearchChangeRef.current(query); + }); + syncUrl(value); + }; + + const handleClear = () => { + pendingUrlValueRef.current = ''; + lastSyncedSearchRef.current = ''; + setInputValue(''); + onSearchChangeRef.current(''); + syncUrl(''); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key !== 'Enter') { + return; + } + + const trimmed = inputValue.trim(); + if (isValidContractId(trimmed)) { + event.preventDefault(); + syncSearchQuery(trimmed, lastSyncedSearchRef, query => { + onSearchChangeRef.current(query); + }); + syncUrl(trimmed); + navigate(`/governance/proposals/${trimmed as ContractId}`); + } + }; + + const showValidationError = inputValue.trim().length > 0 && !isValidContractId(inputValue); + + return ( + + + Search Proposals + + + handleChange(event.target.value)} + error={showValidationError} + helperText={showValidationError ? CONTRACT_ID_VALIDATION_MESSAGE : undefined} + sx={searchTextFieldSx} + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, + htmlInput: { + 'data-testid': 'proposal-search-input', + onKeyDown: handleKeyDown, + }, + }} + /> + + {isValidContractId(inputValue) && ( + + Clear search + + )} + + ); +}); + +export default ProposalSearch; diff --git a/apps/sv/frontend/src/components/governance/ProposalSummary.tsx b/apps/sv/frontend/src/components/governance/ProposalSummary.tsx index f3e8c525ae..6561204b48 100644 --- a/apps/sv/frontend/src/components/governance/ProposalSummary.tsx +++ b/apps/sv/frontend/src/components/governance/ProposalSummary.tsx @@ -2,9 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 import { Box, Typography } from '@mui/material'; -import { THRESHOLD_DEADLINE_SUBTITLE } from '../../utils/constants'; +import type { ReactNode } from 'react'; +import { MemberIdentifier } from '../beta'; +import { IDENTIFIER_COMPACT_MAX_WIDTH_PX } from '../beta/identifierStyles'; +import { + EFFECTIVE_AT_LABEL, + CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE, + PROPOSAL_REVIEW_TITLE, + THRESHOLD_DEADLINE_LABEL, + THRESHOLD_DEADLINE_SUBTITLE, +} from '../../utils/constants'; import type { ConfigChange } from '../../utils/types'; import { ConfigValuesChanges } from './ConfigValuesChanges'; +import { ProposalReviewField } from './ProposalReviewField'; + +/** Figma review party IDs: Source Code Pro 14px + copy (node 4832:4323). */ +const ReviewPartyId: React.FC<{ partyId: string; 'data-testid': string }> = ({ + partyId, + 'data-testid': testId, +}) => ( + +); interface BaseProposalSummaryProps { actionName: string; @@ -31,6 +55,7 @@ type ProposalSummaryProps = BaseProposalSummaryProps & | { formType: 'grant-right'; grantRight: string; + activityWeight: string; } | { formType: 'revoke-right'; @@ -40,6 +65,8 @@ type ProposalSummaryProps = BaseProposalSummaryProps & | { formType: 'config-change'; configFormData: ConfigChange[]; + /** Rendered under Proposed Configuration Changes (e.g. Show JSON). */ + jsonDiff?: ReactNode; } | { formType: 'create-unallocated-unclaimed-activity-record'; @@ -47,49 +74,73 @@ type ProposalSummaryProps = BaseProposalSummaryProps & amount: string; expiresAt: string; } + | { + formType: 'update-right-weight'; + providerPartyId: string; + rightCid: string; + currentActivityWeight: string; + newActivityWeight: string; + } ); export const ProposalSummary: React.FC = props => { const { formType, actionName, url, summary, expiryDate, effectiveDate } = props; return ( - - - Proposal Summary + + + {PROPOSAL_REVIEW_TITLE} - - - - - - - - + - + {/* Action-specific fields follow Action (Figma: config/member before threshold). */} + {formType === 'config-change' && ( + + } + /> + {props.jsonDiff} + + )} {formType === 'sv-reward-weight' && ( <> - + } /> - = props => { )} {formType === 'grant-right' && ( - + <> + } + /> + + )} {formType === 'revoke-right' && ( <> - + } /> - )} + {formType === 'update-right-weight' && ( + <> + + } + /> + + + } + /> + + )} + {formType === 'offboard' && ( - + + } + /> )} {formType === 'create-unallocated-unclaimed-activity-record' && ( <> - - - - - + + } + /> + + )} - - {formType === 'config-change' && ( - } - /> - )} - - - - ); -}; - -interface ProposalFieldProps { - id: string; - title: string; - subtitle?: string; - value: React.ReactNode; -} + -const ProposalField: React.FC = props => { - const { id, title, subtitle, value } = props; - return ( - - - {title} - + - - {subtitle && ( - - {subtitle} - - )} + - {typeof value === 'string' ? ( - - {value} - - ) : ( - value - )} + ); diff --git a/apps/sv/frontend/src/components/governance/ProposalVoteForm.tsx b/apps/sv/frontend/src/components/governance/ProposalVoteForm.tsx index bce3e45608..8642da06bc 100644 --- a/apps/sv/frontend/src/components/governance/ProposalVoteForm.tsx +++ b/apps/sv/frontend/src/components/governance/ProposalVoteForm.tsx @@ -9,7 +9,15 @@ import { isValidUrl } from '../../utils/validations'; import { ContractId } from '@daml/types'; import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; import { ProposalVote } from '../../utils/types'; -import { Alert, Box, Button, Stack, TextField, Typography } from '@mui/material'; +import { Alert, Box, Button, TextField, Typography } from '@mui/material'; +import { CREATE_PROPOSAL_FIELD_LABEL_SX } from '../../constants/createProposalLayout'; +import { proposalSummaryFieldSx, singleLineFieldSx } from '../../themes/fieldStyles'; +import { + VOTE_REASON_PLACEHOLDER, + VOTE_REASON_SUMMARY_LABEL, + VOTE_REASON_URL_LABEL, + VOTE_REASON_URL_PLACEHOLDER, +} from '../../utils/constants'; interface CastVoteArgs { accepted: boolean; url: string; @@ -101,39 +109,26 @@ export const ProposalVoteForm: React.FC = props => { }} children={field => { return ( - - - Reason + + + {VOTE_REASON_SUMMARY_LABEL} field.handleChange(e.target.value)} error={!field.state.meta.isValid} helperText={field.state.meta.errors?.[0]} + placeholder={VOTE_REASON_PLACEHOLDER} inputProps={{ 'data-testid': 'your-vote-reason-input' }} - sx={{ - '& .MuiFilledInput-root': { - borderRadius: 1, - paddingTop: 1, - fontFamily: 'Lato', - '&:before, &:after': { - display: 'none', - }, - }, - }} + sx={proposalSummaryFieldSx} /> - + ); }} /> @@ -154,18 +149,14 @@ export const ProposalVoteForm: React.FC = props => { }} children={field => { return ( - - - Vote Reason URL + + + {VOTE_REASON_URL_LABEL} = props => { {field.state.meta.errors?.[0]} } + placeholder={VOTE_REASON_URL_PLACEHOLDER} inputProps={{ 'data-testid': 'your-vote-url-input' }} - sx={{ - '& .MuiFilledInput-root': { - borderRadius: 1, - fontFamily: 'Lato', - '&:before, &:after': { - display: 'none', - }, - }, - '& .MuiFilledInput-input': { - paddingTop: 1.5, - paddingBottom: 1.5, - }, - }} + sx={singleLineFieldSx} /> - + ); }} /> @@ -216,27 +196,27 @@ export const ProposalVoteForm: React.FC = props => { <> )} diff --git a/apps/sv/frontend/src/components/governance/proposal-details/DetailItem.tsx b/apps/sv/frontend/src/components/governance/proposal-details/DetailItem.tsx index 25d39367f2..c49c3acb60 100644 --- a/apps/sv/frontend/src/components/governance/proposal-details/DetailItem.tsx +++ b/apps/sv/frontend/src/components/governance/proposal-details/DetailItem.tsx @@ -3,6 +3,8 @@ import { Divider, Stack, Typography } from '@mui/material'; +import { CREATE_PROPOSAL_FIELD_LABEL_SX } from '../../../constants/createProposalLayout'; + interface DetailItemProps { label: string; value: React.ReactNode; @@ -16,11 +18,8 @@ export const DetailItem: React.FC = props => { return ( diff --git a/apps/sv/frontend/src/components/layout/LogoutButton.tsx b/apps/sv/frontend/src/components/layout/LogoutButton.tsx new file mode 100644 index 0000000000..fe4912914a --- /dev/null +++ b/apps/sv/frontend/src/components/layout/LogoutButton.tsx @@ -0,0 +1,68 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import { Box, Typography } from '@mui/material'; + +import { layoutTokens, navItemTypography, NAV_PILL_PX } from '../../theme/tokens'; +import LogoutIcon from './LogoutIcon'; + +interface LogoutButtonProps { + onLogout: () => void; +} + +/** + * Figma Dev Mode — content box 66x17, 10px padding on each side, gap-2.5 (10px) + * between icon and label. Plain `Box component="button"` (matching `SvNavLink`'s + * pattern) instead of MUI `Button` — MUI's own min-height/padding/ripple defaults + * previously inflated this to ~89.78x38 despite the padding value being correct. + */ +const LogoutButton: React.FC = ({ onLogout }) => ( + + + + Logout + + +); + +export default LogoutButton; diff --git a/apps/sv/frontend/src/components/layout/LogoutIcon.tsx b/apps/sv/frontend/src/components/layout/LogoutIcon.tsx new file mode 100644 index 0000000000..d979051d7c --- /dev/null +++ b/apps/sv/frontend/src/components/layout/LogoutIcon.tsx @@ -0,0 +1,33 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import { Box } from '@mui/material'; + +/** + * Figma logout glyph — door bracket (open right) with an arrow exiting through it. + * Path data reconstructed from the true Figma vector export (CF-design-system/svgs), + * not the lossy Tailwind HTML export — the div-based export flattens this icon into + * two filled bars that don't resemble a logout glyph at all. + */ +const LogoutIcon: React.FC = () => ( + + + + + + + +); + +export default LogoutIcon; diff --git a/apps/sv/frontend/src/components/layout/NavAttentionIcon.tsx b/apps/sv/frontend/src/components/layout/NavAttentionIcon.tsx new file mode 100644 index 0000000000..960fce26b7 --- /dev/null +++ b/apps/sv/frontend/src/components/layout/NavAttentionIcon.tsx @@ -0,0 +1,31 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import { Box } from '@mui/material'; + +/** Figma nav warning icon for Delegate Election (yellow triangle + exclamation cutout). */ +const NavAttentionIcon: React.FC = () => ( + + + + + +); + +export default NavAttentionIcon; diff --git a/apps/sv/frontend/src/components/layout/NavCountBadge.tsx b/apps/sv/frontend/src/components/layout/NavCountBadge.tsx new file mode 100644 index 0000000000..9581cb8fb6 --- /dev/null +++ b/apps/sv/frontend/src/components/layout/NavCountBadge.tsx @@ -0,0 +1,46 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import { Box } from '@mui/material'; + +import { layoutTokens } from '../../theme/tokens'; + +interface NavCountBadgeProps { + count: number; + id?: string; +} + +/** Figma nav notification badge — size-5 bg-red-400 rounded-3xl, text-xs Inter. */ +const NavCountBadge: React.FC = ({ count, id }) => { + if (count <= 0) { + return null; + } + + return ( + 9 ? 0.5 : 0, + borderRadius: '24px', + bgcolor: layoutTokens.notificationBadge, + color: 'common.white', + fontFamily: '"Inter", sans-serif', + fontSize: '0.75rem', + fontWeight: 400, + lineHeight: 1, + }} + > + {count} + + ); +}; + +export default NavCountBadge; diff --git a/apps/sv/frontend/src/components/layout/NetworkBanner.tsx b/apps/sv/frontend/src/components/layout/NetworkBanner.tsx new file mode 100644 index 0000000000..9564059df7 --- /dev/null +++ b/apps/sv/frontend/src/components/layout/NetworkBanner.tsx @@ -0,0 +1,34 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import { Stack, Typography } from '@mui/material'; + +import { useNetworkInstanceName } from '../../hooks'; + +const NetworkBanner: React.FC = () => { + const networkInstanceName = useNetworkInstanceName(); + const knownColors = ['mainnet', 'testnet', 'devnet', 'scratchnet', 'localnet']; + const networkInstanceNameColor = knownColors.includes(networkInstanceName.toLowerCase()) + ? `colors.${networkInstanceName.toLowerCase()}` + : 'colors.neutral.30'; + return ( + + + You are on {networkInstanceName} + + + ); +}; + +export default NetworkBanner; diff --git a/apps/sv/frontend/src/components/layout/SvNavLink.tsx b/apps/sv/frontend/src/components/layout/SvNavLink.tsx new file mode 100644 index 0000000000..05ea8f83c4 --- /dev/null +++ b/apps/sv/frontend/src/components/layout/SvNavLink.tsx @@ -0,0 +1,82 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { NavLink, useLocation } from 'react-router'; + +import { Box } from '@mui/material'; + +import { layoutTokens, navItemTypography, NAV_PILL_PX } from '../../theme/tokens'; +import NavAttentionIcon from './NavAttentionIcon'; +import NavCountBadge from './NavCountBadge'; + +export interface SvNavLinkItem { + name: string; + path: string; + badgeCount?: number; + hasAlert?: boolean; + /** When false, nav stays active on nested paths (e.g. /governance/proposals). */ + end?: boolean; + /** Extra pathnames that should show this link as active (e.g. `/` for GSI). */ + alsoActiveFor?: string[]; +} + +interface SvNavLinkProps { + link: SvNavLinkItem; +} + +/** Figma: badge accessory uses gap-1.5 (6px), alert-icon accessory uses gap-2.5 (10px). */ +const navLinkSx = (isActive: boolean, accessoryGap: string) => ({ + display: 'inline-flex', + alignItems: 'center', + gap: accessoryGap, + p: NAV_PILL_PX, + borderRadius: '20px', + textDecoration: 'none', + whiteSpace: 'nowrap', + color: layoutTokens.lightText, + fontFamily: layoutTokens.fontUi, + fontSize: '0.875rem', + fontWeight: 700, + ...navItemTypography, + border: '2px solid transparent', + boxSizing: 'border-box', + ...(isActive && { borderColor: layoutTokens.navActiveOutline }), + '&:focus': { outline: 'none' }, + '&:focus-visible': { + outline: '2px solid', + outlineColor: layoutTokens.navActiveOutline, + outlineOffset: '2px', + }, +}); + +const SvNavLink: React.FC = ({ link }) => { + const location = useLocation(); + + return ( + + {({ isActive }) => { + const active = isActive || (link.alsoActiveFor?.includes(location.pathname) ?? false); + return ( + + {link.name} + {link.badgeCount !== undefined && link.badgeCount > 0 ? ( + + ) : null} + {link.hasAlert ? : null} + + ); + }} + + ); +}; + +export default SvNavLink; diff --git a/apps/sv/frontend/src/components/layout/SvNavigationShell.tsx b/apps/sv/frontend/src/components/layout/SvNavigationShell.tsx new file mode 100644 index 0000000000..d2a6c3974a --- /dev/null +++ b/apps/sv/frontend/src/components/layout/SvNavigationShell.tsx @@ -0,0 +1,42 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import { Box } from '@mui/material'; + +import { HEADER_PB, HEADER_PT, layoutTokens, PAGE_PX } from '../../theme/tokens'; +import SvTopNav from './SvTopNav'; +import { SvNavLinkItem } from './SvNavLink'; + +interface SvNavigationShellProps { + navLinks: SvNavLinkItem[]; + onLogout: () => void; + pageName: string; +} + +/** + * Figma "Navigation" component — network banner above the nav row. + * Dev Mode: padding-bottom 64px, background #272727. + */ +const SvNavigationShell: React.FC = ({ navLinks, onLogout, pageName }) => { + return ( + + + + + + ); +}; + +export default SvNavigationShell; diff --git a/apps/sv/frontend/src/components/layout/SvTopNav.tsx b/apps/sv/frontend/src/components/layout/SvTopNav.tsx new file mode 100644 index 0000000000..e9d5e73a7e --- /dev/null +++ b/apps/sv/frontend/src/components/layout/SvTopNav.tsx @@ -0,0 +1,120 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import { Box, Stack, Typography } from '@mui/material'; + +import { + BRAND_TITLE, + layoutTokens, + NAV_GAP, + NAV_PILL_PX, + NAV_ROW_MIN_HEIGHT, +} from '../../theme/tokens'; +import LogoutButton from './LogoutButton'; +import SvNavLink, { SvNavLinkItem } from './SvNavLink'; + +interface SvTopNavProps { + navLinks: SvNavLinkItem[]; + onLogout: () => void; +} + +/** + * Nav row: brand (left, intrinsic width) · flex spacer · nav cluster · + * flex spacer · logout (right, intrinsic width). Equal spacers center the pills + * in the gap between brand and logout — not in the full viewport. + */ +const SvTopNav: React.FC = ({ navLinks, onLogout }) => ( + + + + {BRAND_TITLE} + + + + + + + {navLinks.map(link => ( + + ))} + + + + + + + + +); + +export default SvTopNav; diff --git a/apps/sv/frontend/src/components/ui/Dropdown.tsx b/apps/sv/frontend/src/components/ui/Dropdown.tsx new file mode 100644 index 0000000000..4fb48c3521 --- /dev/null +++ b/apps/sv/frontend/src/components/ui/Dropdown.tsx @@ -0,0 +1,211 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + Box, + FormControl, + FormHelperText, + MenuItem, + Select, + SelectChangeEvent, + SxProps, + Theme, + Typography, +} from '@mui/material'; + +/** + * Source of truth: Figma Dev Mode node `3870:3442` ("Dropdown fields") and + * [#2652](https://github.com/canton-network/splice/issues/2652) reference. + * Prop surface mirrors `components.md` Input/Select: label, required, + * placeholder, helperText, hasDropdown (always true here), state. + */ + +export type DropdownState = 'default' | 'disabled' | 'error'; + +export interface DropdownOption { + value: string; + label: string; + /** Optional per-option test id (defaults to value). */ + testId?: string; +} + +export interface DropdownProps { + options: DropdownOption[]; + value: string; + onChange: (value: string) => void; + onBlur?: () => void; + label?: string; + required?: boolean; + placeholder?: string; + helperText?: string; + state?: DropdownState; + id?: string; + labelId?: string; + testId?: string; + disabled?: boolean; + error?: boolean; + fullWidth?: boolean; + renderValue?: (selected: string, options: DropdownOption[]) => React.ReactNode; + sx?: SxProps; +} + +/** Dev Mode: `background: var(--grey54, #363636)` */ +const FIELD_BG = 'var(--grey54, #363636)'; + +/** Figma "Body M" on nodes `3870:3442` / `1724:3506`: Inter 14px/400/22px. */ +const valueTextSx = { + fontFamily: "'Inter', sans-serif", + fontSize: '14px', + fontWeight: 400, + lineHeight: '22px', + color: '#E2E2E2', + fontFeatureSettings: "'liga' off, 'clig' off", +}; + +/** Empty-state placeholder node `I3870:3442;120:415`: Inter 14px, grey105. */ +const placeholderTextSx = { + ...valueTextSx, + color: '#696969', +}; + +/** Figma "FIELD H": Inter Semi Bold 12px uppercase, grey226. */ +const labelSx = { + fontFamily: "'Inter', sans-serif", + fontSize: '12px', + fontWeight: 600, + lineHeight: '22px', + textTransform: 'uppercase' as const, + color: '#E2E2E2', + mb: '8px', +}; + +const ChevronDownIcon: React.FC> = props => ( + + + +); + +const resolveLabelId = ( + labelId: string | undefined, + id: string | undefined, + label: string | undefined +): string | undefined => labelId ?? (label ? `${id ?? 'dropdown'}-label` : undefined); + +export const Dropdown: React.FC = ({ + options, + value, + onChange, + onBlur, + label, + required = false, + placeholder, + helperText, + state = 'default', + id, + labelId: labelIdProp, + testId, + disabled: disabledProp, + error: errorProp, + fullWidth = true, + renderValue, + sx, +}) => { + const isDisabled = disabledProp ?? state === 'disabled'; + const isError = errorProp ?? state === 'error'; + const resolvedId = id ?? testId ?? 'dropdown'; + const resolvedLabelId = resolveLabelId(labelIdProp, resolvedId, label); + const showLabel = Boolean(label); + + const defaultRenderValue = (selected: string) => { + if (!selected) { + return placeholder ? ( + + {placeholder} + + ) : null; + } + const option = options.find(o => o.value === selected); + return ( + + {option?.label ?? selected} + + ); + }; + + return ( + + {showLabel && ( + + {label} + {required && ( + + )} + + )} + + + + {helperText && ( + {helperText} + )} + + ); +}; diff --git a/apps/sv/frontend/src/components/votes/actions/GrantFeaturedAppRight.tsx b/apps/sv/frontend/src/components/votes/actions/GrantFeaturedAppRight.tsx index d0a65f0a15..6a8bdee801 100644 --- a/apps/sv/frontend/src/components/votes/actions/GrantFeaturedAppRight.tsx +++ b/apps/sv/frontend/src/components/votes/actions/GrantFeaturedAppRight.tsx @@ -8,12 +8,14 @@ import { FormControl, Stack, TextField, Typography } from '@mui/material'; import { ActionRequiringConfirmation } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules/module'; import { useDsoInfos } from '../../../contexts/SvContext'; +import { activityWeightToOptional } from '../../../utils/governance'; const GrantFeaturedAppRight: React.FC<{ chooseAction: (action: ActionRequiringConfirmation) => void; }> = ({ chooseAction }) => { const dsoInfosQuery = useDsoInfos(); const [provider, setProvider] = useState(''); + const [activityWeight, setActivityWeight] = useState(''); if (dsoInfosQuery.isLoading) { return ; @@ -23,19 +25,28 @@ const GrantFeaturedAppRight: React.FC<{ return

Error: {JSON.stringify(dsoInfosQuery.error)}

; } - function setProviderAction(provider: string) { - setProvider(provider); + function chooseGrantAction(provider: string, activityWeight: string) { chooseAction({ tag: 'ARC_DsoRules', value: { dsoAction: { tag: 'SRARC_GrantFeaturedAppRight', - value: { provider: provider, activityWeight: null }, + value: { provider: provider, activityWeight: activityWeightToOptional(activityWeight) }, }, }, }); } + function setProviderAction(provider: string) { + setProvider(provider); + chooseGrantAction(provider, activityWeight); + } + + function setActivityWeightAction(activityWeight: string) { + setActivityWeight(activityWeight); + chooseGrantAction(provider, activityWeight); + } + return ( Provider @@ -46,6 +57,14 @@ const GrantFeaturedAppRight: React.FC<{ value={provider} /> + Activity Weight + + setActivityWeightAction(e.target.value)} + value={activityWeight} + /> + ); }; diff --git a/apps/sv/frontend/src/constants/createProposalLayout.ts b/apps/sv/frontend/src/constants/createProposalLayout.ts new file mode 100644 index 0000000000..fcce6a2169 --- /dev/null +++ b/apps/sv/frontend/src/constants/createProposalLayout.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** Figma field column width inside the card. */ +export const CREATE_PROPOSAL_FIELD_MAX_WIDTH = 832; + +export const CREATE_PROPOSAL_CARD_BG = '#181818'; +export const CREATE_PROPOSAL_CARD_BORDER_RADIUS = '4px'; +export const CREATE_PROPOSAL_CARD_PADDING_Y = '60px'; + +/** Vertical gap between main form sections (Figma). */ +export const CREATE_PROPOSAL_SECTION_GAP = '32px'; + +/** Vertical gap between configuration rows (Figma Frame 535). */ +export const CREATE_PROPOSAL_CONFIG_ROW_GAP = '24px'; + +/** Gap from a configuration row to its divider (Figma Frame 533). */ +export const CREATE_PROPOSAL_CONFIG_ROW_DIVIDER_GAP = '14px'; + +/** Configuration value input width (Figma Frame 531). */ +export const CREATE_PROPOSAL_CONFIG_INPUT_WIDTH = '238px'; + +/** Figma Blue (Primary CTA) — enabled Review/Submit Proposal. */ +export const CREATE_PROPOSAL_PRIMARY_CTA = '#96E4FD'; + +/** Figma Yellow (Secondary CTA) — Cancel outline and JSON toggle. */ +export const CREATE_PROPOSAL_SECONDARY_CTA = '#F3FF97'; + +/** Figma disabled primary CTA surface (stone-500). */ +export const CREATE_PROPOSAL_DISABLED_CTA_BG = '#78716C'; + +/** Figma disabled primary CTA label (neutral 25%). */ +export const CREATE_PROPOSAL_DISABLED_CTA_TEXT = '#404040'; + +/** Figma coral (Warning Button / destructive) — Discard & Exit outline, error icon. */ +export const CREATE_PROPOSAL_DISCARD_CTA = '#FD8575'; + +/** Figma FIELD H — 12px Inter semibold uppercase field labels. */ +export const CREATE_PROPOSAL_FIELD_LABEL_SX = { + fontFamily: "'Inter', sans-serif", + fontSize: '12px', + fontWeight: 600, + lineHeight: '22px', + letterSpacing: 0, + textTransform: 'uppercase' as const, + color: '#E2E2E2', +}; + +/** Figma Body M — 14px field values and radio option labels. */ +export const CREATE_PROPOSAL_FIELD_BODY_SX = { + fontFamily: "'Inter', sans-serif", + fontSize: '14px', + fontWeight: 400, + lineHeight: '22px', + letterSpacing: 0, + color: '#E2E2E2', +}; + +/** Figma Body S — 12px helper / subtitle text. */ +export const CREATE_PROPOSAL_FIELD_HELPER_SX = { + fontFamily: "'Inter', sans-serif", + fontSize: '12px', + fontWeight: 400, + lineHeight: '22px', + letterSpacing: 0, + color: '#E2E2E2', +}; diff --git a/apps/sv/frontend/src/constants/formButtonStyles.ts b/apps/sv/frontend/src/constants/formButtonStyles.ts new file mode 100644 index 0000000000..d0a8c16a9e --- /dev/null +++ b/apps/sv/frontend/src/constants/formButtonStyles.ts @@ -0,0 +1,70 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CREATE_PROPOSAL_DISABLED_CTA_BG, + CREATE_PROPOSAL_DISABLED_CTA_TEXT, + CREATE_PROPOSAL_DISCARD_CTA, + CREATE_PROPOSAL_PRIMARY_CTA, + CREATE_PROPOSAL_SECONDARY_CTA, +} from './createProposalLayout'; + +const pillButtonBaseSx = { + height: '39px', + px: '16px', + py: '10px', + borderRadius: '20px', + textTransform: 'none' as const, + fontSize: '16px', + fontWeight: 500, + fontFamily: "'Inter', sans-serif", + lineHeight: 'normal', + boxShadow: 'none', + minWidth: 'unset', +}; + +/** Figma Warning/Secondary button — transparent fill, yellow outline, white label. */ +export const createProposalCancelButtonSx = { + ...pillButtonBaseSx, + bgcolor: 'transparent', + border: `1px solid ${CREATE_PROPOSAL_SECONDARY_CTA}`, + color: '#FFFFFF', + '&:hover': { + bgcolor: 'transparent', + border: `1px solid ${CREATE_PROPOSAL_SECONDARY_CTA}`, + color: CREATE_PROPOSAL_SECONDARY_CTA, + boxShadow: 'none', + }, +}; + +/** Figma Warning Button (destructive) — transparent fill, coral outline, white label. */ +export const createProposalDiscardButtonSx = { + ...pillButtonBaseSx, + bgcolor: 'transparent', + border: `1px solid ${CREATE_PROPOSAL_DISCARD_CTA}`, + color: '#FFFFFF', + '&:hover': { + bgcolor: 'transparent', + border: `1px solid ${CREATE_PROPOSAL_DISCARD_CTA}`, + color: CREATE_PROPOSAL_DISCARD_CTA, + boxShadow: 'none', + }, +}; + +/** Figma Primary button — cyan fill, black label; stone disabled state. */ +export const createProposalSubmitButtonSx = { + ...pillButtonBaseSx, + bgcolor: CREATE_PROPOSAL_PRIMARY_CTA, + color: '#000000', + border: 'none', + '&:hover': { + bgcolor: CREATE_PROPOSAL_PRIMARY_CTA, + color: '#000000', + boxShadow: 'none', + }, + '&:disabled': { + bgcolor: CREATE_PROPOSAL_DISABLED_CTA_BG, + color: CREATE_PROPOSAL_DISABLED_CTA_TEXT, + border: 'none', + }, +}; diff --git a/apps/sv/frontend/src/contexts/SvAdminServiceContext.tsx b/apps/sv/frontend/src/contexts/SvAdminServiceContext.tsx index 06934b8e27..6b4e1b8de8 100644 --- a/apps/sv/frontend/src/contexts/SvAdminServiceContext.tsx +++ b/apps/sv/frontend/src/contexts/SvAdminServiceContext.tsx @@ -10,6 +10,7 @@ import BigNumber from 'bignumber.js'; import React, { useContext, useMemo } from 'react'; import { CastVoteRequest, + CountVoteResultsResponse, createConfiguration, CreateVoteRequest, GetPartyToParticipantResponseV1, @@ -63,6 +64,10 @@ export interface SvAdminClient { accepted?: boolean, pageToken?: number ) => Promise; + countVoteRequestResults: ( + accepted?: boolean, + effectiveTo?: string + ) => Promise; getPreviousSvRewardWeight: ( svParty: string, effectiveBefore?: string @@ -163,6 +168,12 @@ export const SvAdminClientProvider: React.FC => { + return await svAdminClient.countVoteRequestResults({ accepted, effectiveTo }); + }, getPreviousSvRewardWeight: async ( svParty: string, effectiveBefore?: string diff --git a/apps/sv/frontend/src/hooks/index.ts b/apps/sv/frontend/src/hooks/index.ts index 44494fdcfd..086047c7dc 100644 --- a/apps/sv/frontend/src/hooks/index.ts +++ b/apps/sv/frontend/src/hooks/index.ts @@ -3,7 +3,7 @@ import { useAmuletPriceVotes } from './useAmuletPriceVotes'; import { useCometBftDebug } from './useCometBftDebug'; import { useInfiniteVoteRequestResults } from './useInfiniteVoteRequestResults'; -import { useListVoteRequestResult, useListDsoRulesVoteRequests } from './useListVoteRequests'; +import { useListDsoRulesVoteRequests, useListVoteRequestResult } from './useListVoteRequests'; import { useListVotes } from './useListVotes'; import { useMediatorStatus } from './useMediatorStatus'; import { useNetworkInstanceName } from './useNetworkInstanceName'; @@ -12,6 +12,7 @@ import { useSequencerStatus } from './useSequencerStatus'; import { useValidatorLicenses } from './useValidatorLicenses'; import { useValidatorOnboardings } from './useValidatorOnboardings'; import { useVoteRequest } from './useVoteRequest'; +import { useVoteRequestResultsCount } from './useVoteRequestResultsCount'; export { useAmuletPriceVotes, @@ -27,4 +28,5 @@ export { useValidatorLicenses, useValidatorOnboardings, useVoteRequest, + useVoteRequestResultsCount, }; diff --git a/apps/sv/frontend/src/hooks/useFeaturedAppRightPicker.ts b/apps/sv/frontend/src/hooks/useFeaturedAppRightPicker.ts new file mode 100644 index 0000000000..f562bcb9b1 --- /dev/null +++ b/apps/sv/frontend/src/hooks/useFeaturedAppRightPicker.ts @@ -0,0 +1,79 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useState } from 'react'; +import { Option } from '../components/form-components/SelectField'; +import { + validatePartyId, + validateRevokeFeaturedAppRight, +} from '../components/forms/formValidators'; +import { useSvAdminClient } from '../contexts/SvAdminServiceContext'; +import { DEFAULT_APP_ACTIVITY_WEIGHT } from '../utils/constants'; + +interface FeatureAppRightPicker { + rightOptions: Option[]; + currentWeights: Record; + providerSearched: boolean; + loadFeaturedAppRightsAndValidate: (value: string) => Promise; + validateRightSelection: (value: string) => string | false; + resetOptions: () => void; +} + +export const useFeaturedAppRightPicker = ( + svAdminClient: ReturnType +): FeatureAppRightPicker => { + const [rightOptions, setRightOptions] = useState([]); + const [providerSearched, setProviderSearched] = useState(false); + const [currentWeights, setCurrentWeights] = useState>({}); + + const loadFeaturedAppRightsAndValidate = async (value: string) => { + if (validatePartyId(value)) return undefined; + + try { + const response = await svAdminClient.listFeaturedAppRightsByProvider(value); + const options = response.featured_app_rights.map((contract: { contract_id: string }) => ({ + key: contract.contract_id, + value: contract.contract_id, + })); + const weights = Object.fromEntries( + response.featured_app_rights.map(c => { + const aw = (c.payload as { activityWeight?: string | null }).activityWeight; + return [c.contract_id, aw ?? DEFAULT_APP_ACTIVITY_WEIGHT]; + }) + ); + setRightOptions(options); + setCurrentWeights(weights); + setProviderSearched(true); + return undefined; + } catch { + setRightOptions([]); + setCurrentWeights({}); + setProviderSearched(false); + return 'Could not load Featured Application Contract IDs for this provider'; + } + }; + + const validateRightSelection = (value: string): string | false => { + const requiredError = validateRevokeFeaturedAppRight(value); + if (requiredError) return requiredError; + + return rightOptions.some(option => option.value === value) + ? false + : 'Select a valid contract id'; + }; + + const resetOptions = () => { + setRightOptions([]); + setCurrentWeights({}); + setProviderSearched(false); + }; + + return { + rightOptions, + currentWeights, + providerSearched, + loadFeaturedAppRightsAndValidate, + validateRightSelection, + resetOptions, + }; +}; diff --git a/apps/sv/frontend/src/hooks/useHorizontalScrollMetrics.ts b/apps/sv/frontend/src/hooks/useHorizontalScrollMetrics.ts new file mode 100644 index 0000000000..7d285d3bb6 --- /dev/null +++ b/apps/sv/frontend/src/hooks/useHorizontalScrollMetrics.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { RefObject, useLayoutEffect, useState } from 'react'; + +export interface ScrollMetrics { + canScroll: boolean; + thumbWidthPercent: number; + thumbLeftPercent: number; +} + +const emptyMetrics: ScrollMetrics = { + canScroll: false, + thumbWidthPercent: 100, + thumbLeftPercent: 0, +}; + +export const computeScrollMetrics = (el: HTMLElement): ScrollMetrics => { + const { scrollLeft, scrollWidth, clientWidth } = el; + if (scrollWidth <= clientWidth + 1) { + return emptyMetrics; + } + + const thumbWidthPercent = Math.max((clientWidth / scrollWidth) * 100, 8); + const maxLeft = 100 - thumbWidthPercent; + const scrollableDistance = scrollWidth - clientWidth; + const thumbLeftPercent = scrollableDistance > 0 ? (scrollLeft / scrollableDistance) * maxLeft : 0; + + return { + canScroll: true, + thumbWidthPercent, + thumbLeftPercent, + }; +}; + +export const useHorizontalScrollMetrics = ( + scrollRef: RefObject, + deps: unknown[] = [] +): ScrollMetrics => { + const [metrics, setMetrics] = useState(emptyMetrics); + + useLayoutEffect(() => { + const el = scrollRef.current; + if (!el) return; + + const update = () => setMetrics(computeScrollMetrics(el)); + + update(); + const observer = new ResizeObserver(update); + observer.observe(el); + el.addEventListener('scroll', update, { passive: true }); + + return () => { + observer.disconnect(); + el.removeEventListener('scroll', update); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); + + return metrics; +}; diff --git a/apps/sv/frontend/src/hooks/useListVoteRequests.tsx b/apps/sv/frontend/src/hooks/useListVoteRequests.tsx index bf48c8d821..b3b38f3a44 100644 --- a/apps/sv/frontend/src/hooks/useListVoteRequests.tsx +++ b/apps/sv/frontend/src/hooks/useListVoteRequests.tsx @@ -6,11 +6,16 @@ import { DsoRules_CloseVoteRequestResult, VoteRequest, } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules/module'; -import { Contract } from '@canton-network/splice-common-frontend-utils'; -import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import { Contract, PollingStrategy } from '@canton-network/splice-common-frontend-utils'; +import { type UseQueryResult, useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { useEffect, useRef } from 'react'; import { useSvAdminClient } from '../contexts/SvAdminServiceContext'; import { useConfigPollInterval } from '../utils'; +import { shouldContinueVoteHistorySearch } from '../utils/proposalSearch'; + +const PAGINATED_VOTE_RESULTS_QUERY_KEY = 'paginatedVoteRequestResults'; +const PAGINATED_VOTE_RESULTS_PAGE_SIZE = 500; export type ListVoteRequestResultParams = { actionName?: string; @@ -67,3 +72,136 @@ export const useListVoteRequestResult = ( retry, }); }; + +function usePaginatedVoteRequestResultsBucket( + contractId: string, + accepted: boolean, + enabled: boolean, + shouldContinueRef: React.MutableRefObject<() => boolean> +) { + const { listVoteRequestResults } = useSvAdminClient(); + const queryKey = [ + PAGINATED_VOTE_RESULTS_QUERY_KEY, + contractId, + accepted, + PAGINATED_VOTE_RESULTS_PAGE_SIZE, + ] as const; + + const { + hasNextPage, + isFetchingNextPage, + isPending, + dataUpdatedAt, + fetchNextPage, + data, + isSuccess, + } = useInfiniteQuery({ + queryKey, + queryFn: async ({ pageParam }) => { + const response = await listVoteRequestResults( + PAGINATED_VOTE_RESULTS_PAGE_SIZE, + undefined, + undefined, + undefined, + undefined, + accepted, + pageParam ?? undefined + ); + + return { + results: List(DsoRules_CloseVoteRequestResult).decoder.runWithException( + response.dso_rules_vote_results + ), + nextPageToken: response.next_page_token, + }; + }, + initialPageParam: null as number | null, + getNextPageParam: lastPage => lastPage?.nextPageToken ?? null, + enabled, + refetchInterval: PollingStrategy.NONE, + refetchOnWindowFocus: false, + }); + + useEffect(() => { + if ( + !enabled || + !shouldContinueRef.current() || + !hasNextPage || + isFetchingNextPage || + isPending + ) { + return; + } + void fetchNextPage(); + }, [ + enabled, + hasNextPage, + isFetchingNextPage, + isPending, + dataUpdatedAt, + fetchNextPage, + shouldContinueRef, + ]); + + const results = data?.pages.flatMap(page => page.results) ?? []; + + return { + results, + isNaturallyComplete: isSuccess && !hasNextPage, + hasFirstPage: (data?.pages.length ?? 0) > 0, + }; +} + +export function usePaginatedVoteRequestResultsByContractId( + enabled: boolean, + { contractId = '' }: { contractId?: string } = {} +): { + results: DsoRules_CloseVoteRequestResult[]; + isComplete: boolean; +} { + const shouldContinueRef = useRef<() => boolean>(() => false); + const fetchDisabledRef = useRef(false); + const previousLookupRef = useRef({ enabled, contractId }); + + if ( + previousLookupRef.current.enabled !== enabled || + previousLookupRef.current.contractId !== contractId + ) { + fetchDisabledRef.current = false; + previousLookupRef.current = { enabled, contractId }; + } + + const bucketEnabled = enabled && !fetchDisabledRef.current; + + const accepted = usePaginatedVoteRequestResultsBucket( + contractId, + true, + bucketEnabled, + shouldContinueRef + ); + const rejected = usePaginatedVoteRequestResultsBucket( + contractId, + false, + bucketEnabled, + shouldContinueRef + ); + const results = enabled ? [...accepted.results, ...rejected.results] : []; + + const shouldContinue = + bucketEnabled && + shouldContinueVoteHistorySearch(contractId, results, result => result.request.trackingCid); + shouldContinueRef.current = () => shouldContinue; + + const isComplete = + (accepted.isNaturallyComplete && rejected.isNaturallyComplete) || + (!shouldContinue && accepted.hasFirstPage && rejected.hasFirstPage); + + if (isComplete) { + fetchDisabledRef.current = true; + } + + return { + results, + isComplete, + }; +} diff --git a/apps/sv/frontend/src/hooks/useNetworkInstanceName.ts b/apps/sv/frontend/src/hooks/useNetworkInstanceName.ts index 8a8294b71a..06dd7c795d 100644 --- a/apps/sv/frontend/src/hooks/useNetworkInstanceName.ts +++ b/apps/sv/frontend/src/hooks/useNetworkInstanceName.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { useDsoInfos } from '../contexts/SvContext'; -export const useNetworkInstanceName: () => string | undefined = () => { +export const useNetworkInstanceName: () => string = () => { const dsoInfosQuery = useDsoInfos(); const scanUrls = dsoInfosQuery.data?.nodeStates.flatMap(nsContract => { @@ -13,11 +13,15 @@ export const useNetworkInstanceName: () => string | undefined = () => { }) as string[]; if (scanUrls === undefined) { - return undefined; + return 'Unknown Network'; } const instances = scanUrls .map(url => { + if (/\/\/localhost(?::\d+)?(?:\/|$)/.test(url)) { + return 'local'; + } + const regex = /(?<=\/\/(?:scan\.)sv-\d+\.)([a-zA-Z0-9-]+)/; return url.match(regex)?.[1]; @@ -28,22 +32,21 @@ export const useNetworkInstanceName: () => string | undefined = () => { return getNetworkName(instances[0]); } - return undefined; + return 'Unknown Network'; }; const getNetworkName = (network: string) => { - let networkName; - // NOTE: mainnet does not have the network/cluster name in the url. if (network === 'global') { - networkName = 'MainNet'; + return 'MainNet'; } else if (network === 'test') { - networkName = 'TestNet'; + return 'TestNet'; } else if (network === 'dev') { - networkName = 'DevNet'; - } else if (network?.startsWith('scratch')) { - networkName = 'ScratchNet'; + return 'DevNet'; + } else if (network === 'local') { + return 'LocalNet'; + } else if (network.startsWith('scratch')) { + return 'ScratchNet'; } - - return networkName; + return network.charAt(0).toUpperCase() + network.slice(1); }; diff --git a/apps/sv/frontend/src/hooks/useVoteRequest.tsx b/apps/sv/frontend/src/hooks/useVoteRequest.tsx index b8ccc90762..e5358a00bf 100644 --- a/apps/sv/frontend/src/hooks/useVoteRequest.tsx +++ b/apps/sv/frontend/src/hooks/useVoteRequest.tsx @@ -1,18 +1,22 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Contract } from '@canton-network/splice-common-frontend-utils'; +import { Contract, PollingStrategy } from '@canton-network/splice-common-frontend-utils'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules/module'; import { ContractId } from '@daml/types'; import { useSvAdminClient } from '../contexts/SvAdminServiceContext'; +import { useConfigPollInterval } from '../utils'; export const useVoteRequest = ( contractId: ContractId, - retry: boolean = true + retry: boolean = true, + poll: boolean = true ): UseQueryResult> => { const { lookupDsoRulesVoteRequest } = useSvAdminClient(); + const pollInterval = useConfigPollInterval(); + return useQuery({ queryKey: ['listDsoRulesVoteRequests', contractId], queryFn: async () => { @@ -20,5 +24,6 @@ export const useVoteRequest = ( return Contract.decodeOpenAPI(request.dso_rules_vote_request, VoteRequest); }, retry, + refetchInterval: poll ? pollInterval : PollingStrategy.NONE, }); }; diff --git a/apps/sv/frontend/src/hooks/useVoteRequestResultByCid.tsx b/apps/sv/frontend/src/hooks/useVoteRequestResultByCid.tsx index 48bffd44e5..98b9a6706c 100644 --- a/apps/sv/frontend/src/hooks/useVoteRequestResultByCid.tsx +++ b/apps/sv/frontend/src/hooks/useVoteRequestResultByCid.tsx @@ -2,15 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 import { ContractId } from '@daml/types'; -import { useVoteRequest } from './useVoteRequest'; import { DsoRules_CloseVoteRequestResult, VoteRequest, } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; -import { useVotesHooks } from '@canton-network/splice-common-frontend'; import { Contract } from '@canton-network/splice-common-frontend-utils'; -const QUERY_LIMIT = 500; +import { usePaginatedVoteRequestResultsByContractId } from './useListVoteRequests'; +import { useVoteRequest } from './useVoteRequest'; +import { findByContractId } from '../utils/proposalSearch'; interface UseVoteRequestResultByCidResult { voteRequest: Contract | undefined; @@ -25,54 +25,37 @@ interface UseVoteRequestResultByCidResult { export function useVoteRequestResultByCid( contractId: ContractId ): UseVoteRequestResultByCidResult { - const votesHooks = useVotesHooks(); - const voteRequestQuery = useVoteRequest(contractId, false); + const voteRequestQuery = useVoteRequest(contractId, false, false); - const voteResultsWithAcceptedQuery = (accepted: boolean) => - votesHooks.useListVoteRequestResult( - QUERY_LIMIT, - undefined, - undefined, - undefined, - undefined, - accepted, - false - ); - const acceptedResultsQuery = voteResultsWithAcceptedQuery(true); - const notAcceptedResultsQuery = voteResultsWithAcceptedQuery(false); + const hasVoteRequest = voteRequestQuery.isSuccess && voteRequestQuery.data != null; - const acceptedResult = acceptedResultsQuery.data?.find( - vr => vr.request.trackingCid === contractId - ); - const notAcceptedResult = notAcceptedResultsQuery.data?.find( - vr => vr.request.trackingCid === contractId - ); + const needsClosedVoteFetch = + (voteRequestQuery.isSuccess || voteRequestQuery.isError) && !hasVoteRequest; - const hasVoteRequest = - voteRequestQuery.isSuccess && - voteRequestQuery.data != null && - voteRequestQuery.data != undefined; + const closedVoteResults = usePaginatedVoteRequestResultsByContractId(needsClosedVoteFetch, { + contractId, + }); - const hasVoteResult = - (acceptedResultsQuery.isSuccess && acceptedResult != undefined) || - (notAcceptedResultsQuery.isSuccess && notAcceptedResult != undefined); + const voteResult = findByContractId( + closedVoteResults.results, + contractId, + result => result.request.trackingCid + ); + const hasVoteResult = voteResult !== undefined; const isPending = voteRequestQuery.isPending || - acceptedResultsQuery.isPending || - notAcceptedResultsQuery.isPending; + (needsClosedVoteFetch && !closedVoteResults.isComplete && !hasVoteResult); const isComplete = (voteRequestQuery.isSuccess || voteRequestQuery.isError) && - (acceptedResultsQuery.isSuccess || acceptedResultsQuery.isError) && - (notAcceptedResultsQuery.isSuccess || notAcceptedResultsQuery.isError); + (!needsClosedVoteFetch || closedVoteResults.isComplete || hasVoteResult); const voteRequest = voteRequestQuery.data; - const voteResult = acceptedResult || notAcceptedResult; return { - voteRequest: voteRequest, - voteResult: voteResult, + voteRequest, + voteResult, hasVoteRequest, hasVoteResult, isPending, diff --git a/apps/sv/frontend/src/hooks/useVoteRequestResultsCount.ts b/apps/sv/frontend/src/hooks/useVoteRequestResultsCount.ts new file mode 100644 index 0000000000..ca27a768fe --- /dev/null +++ b/apps/sv/frontend/src/hooks/useVoteRequestResultsCount.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useQuery, UseQueryResult } from '@tanstack/react-query'; + +import { useSvAdminClient } from '../contexts/SvAdminServiceContext'; + +export const useVoteRequestResultsCount = (): UseQueryResult => { + const { countVoteRequestResults } = useSvAdminClient(); + return useQuery({ + queryKey: ['voteRequestResultsCount'], + queryFn: async () => { + const [effective, notAccepted] = await Promise.all([ + countVoteRequestResults(true, new Date().toISOString()), + countVoteRequestResults(false), + ]); + return effective.count + notAccepted.count; + }, + }); +}; diff --git a/apps/sv/frontend/src/routes/createProposal.tsx b/apps/sv/frontend/src/routes/createProposal.tsx index b8a6c4a9c0..0bf5d364e5 100644 --- a/apps/sv/frontend/src/routes/createProposal.tsx +++ b/apps/sv/frontend/src/routes/createProposal.tsx @@ -9,11 +9,12 @@ import { OffboardSvForm } from '../components/forms/OffboardSvForm'; import { SelectAction } from '../components/forms/SelectAction'; import { SetAmuletConfigRulesForm } from '../components/forms/SetAmuletConfigRulesForm'; import { SetDsoConfigRulesForm } from '../components/forms/SetDsoConfigRulesForm'; +import { UpdateFeaturedAppForm } from '../components/forms/UpdateFeaturedAppForm'; import { UpdateSvRewardWeightForm } from '../components/forms/UpdateSvRewardWeightForm'; +import { InitiateProposalLayout } from '../components/governance/InitiateProposalLayout'; import { useDsoInfos } from '../contexts/SvContext'; import { createProposalActions } from '../utils/governance'; import type { SupportedActionTag } from '../utils/types'; -import { Box } from '@mui/material'; const ProposalForm: React.FC<{ action: SupportedActionTag }> = ({ action }) => { const dsoInfosQuery = useDsoInfos(); @@ -35,6 +36,8 @@ const ProposalForm: React.FC<{ action: SupportedActionTag }> = ({ action }) => { return ; case 'CRARC_SetConfig': return ; + case 'SRARC_UpdateFeaturedAppRight': + return ; } }; @@ -44,12 +47,12 @@ export const CreateProposal: React.FC = () => { const selectedAction = createProposalActions.find(a => a.value === action); return ( - + {selectedAction ? ( ) : ( )} - + ); }; diff --git a/apps/sv/frontend/src/routes/delegateElection.tsx b/apps/sv/frontend/src/routes/delegateElection.tsx new file mode 100644 index 0000000000..5b1e584b2a --- /dev/null +++ b/apps/sv/frontend/src/routes/delegateElection.tsx @@ -0,0 +1,14 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import { Typography } from '@mui/material'; + +/** Placeholder for #2594 layout nav — page content is a separate issue. */ +const DelegateElection: React.FC = () => ( + + Delegate Election + +); + +export default DelegateElection; diff --git a/apps/sv/frontend/src/routes/governance.tsx b/apps/sv/frontend/src/routes/governance.tsx index 0d4086d2d9..08ead0e5ee 100644 --- a/apps/sv/frontend/src/routes/governance.tsx +++ b/apps/sv/frontend/src/routes/governance.tsx @@ -2,52 +2,49 @@ // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; import { Box, Button, Stack, Typography } from '@mui/material'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { ActionRequiredSection, ActionRequiredData, } from '../components/governance/ActionRequiredSection'; +import { ProposalListingSection } from '../components/governance/ProposalListingSection'; +import ProposalSearch from '../components/governance/ProposalSearch'; import { Loading, useVotesHooks } from '@canton-network/splice-common-frontend'; import { dateTimeFormatISO } from '@canton-network/splice-common-frontend-utils'; import dayjs from 'dayjs'; import { ContractId } from '@daml/types'; -import { - ActionRequiringConfirmation, - VoteRequest, -} from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; +import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; import { useSvConfig } from '../utils'; import { PageHeader } from '../components/beta'; -import { ProposalListingSection } from '../components/governance/ProposalListingSection'; import { actionTagToTitle, + buildVoteHistoryData, computeVoteStats, computeYourVote, - getVoteResultStatus, + getGovernanceActionTag, + getRequesterPartyId, } from '../utils/governance'; +import { filterByContractId, isValidContractId } from '../utils/proposalSearch'; import { SupportedActionTag, ProposalListingData } from '../utils/types'; -import { Link as RouterLink } from 'react-router'; +import { Link as RouterLink, useSearchParams } from 'react-router'; import { InfoOutlined, WarningAmberOutlined } from '@mui/icons-material'; -import { useInfiniteVoteRequestResults } from '../hooks'; - -function getAction(action: ActionRequiringConfirmation): string { - switch (action.tag) { - case 'ARC_AmuletRules': - return action.value.amuletRulesAction.tag; - case 'ARC_DsoRules': - return action.value.dsoAction.tag; - default: - return 'Action tag not defined.'; - } -} +import { useInfiniteVoteRequestResults, useVoteRequestResultsCount } from '../hooks'; +import { usePaginatedVoteRequestResultsByContractId } from '../hooks/useListVoteRequests'; export const Governance: React.FC = () => { const svConfig = useSvConfig(); const amuletName = svConfig.spliceInstanceNames.amuletName; + const [searchParams] = useSearchParams(); + const initialSearchQuery = searchParams.get('q') ?? ''; + const [searchQuery, setSearchQuery] = useState(() => + isValidContractId(initialSearchQuery) ? initialSearchQuery.trim() : '' + ); const votesHooks = useVotesHooks(); const dsoInfosQuery = votesHooks.useDsoInfos(); const listVoteRequestsQuery = votesHooks.useListDsoRulesVoteRequests(); const voteResultsInfiniteQuery = useInfiniteVoteRequestResults(); + const voteResultsCountQuery = useVoteRequestResultsCount(); const voteRequestIds = listVoteRequestsQuery.data ? listVoteRequestsQuery.data.map(v => v.payload.trackingCid || v.contractId) @@ -56,6 +53,7 @@ export const Governance: React.FC = () => { const svPartyId = dsoInfosQuery.data?.svPartyId; const votingThreshold = dsoInfosQuery.data?.votingThreshold; + const svs = dsoInfosQuery.data?.dsoRules.payload.svs; const alreadyVotedRequestIds: Set> = useMemo(() => { return svPartyId && votesQuery.data ? new Set(votesQuery.data.filter(v => v.voter === svPartyId).map(v => v.requestCid)) @@ -67,42 +65,136 @@ export const Governance: React.FC = () => { if (!pages || !svPartyId || votingThreshold === undefined) return []; const allVoteResults = pages.flatMap(page => page.results); + return buildVoteHistoryData(allVoteResults, amuletName, svPartyId, votingThreshold, svs); + }, [voteResultsInfiniteQuery.data?.pages, amuletName, svPartyId, votingThreshold, svs]); + + const voteRequests = listVoteRequestsQuery.data; - return allVoteResults - .filter( - vr => - (vr.outcome.tag === 'VRO_Accepted' && - dayjs(vr.outcome.value.effectiveAt).isBefore(dayjs())) || - vr.outcome.tag === 'VRO_Expired' || - vr.outcome.tag === 'VRO_Rejected' - ) - .map(vr => { - const votes = vr.request.votes.entriesArray().map(e => e[1]); + const actionRequiredBase = useMemo(() => { + if (!voteRequests) { + return []; + } + + return voteRequests + .filter(v => !alreadyVotedRequestIds.has(v.payload.trackingCid || v.contractId)) + .map(vr => ({ + contractId: vr.payload.trackingCid || vr.contractId, + actionName: + actionTagToTitle(amuletName)[ + getGovernanceActionTag(vr.payload.action) as SupportedActionTag + ], + description: vr.payload.reason.body, + votingCloses: dayjs(vr.payload.voteBefore).format(dateTimeFormatISO), + createdAt: dayjs(vr.createdAt).format(dateTimeFormatISO), + requester: getRequesterPartyId(vr.payload.requester, svs), + })) as ActionRequiredData[]; + }, [voteRequests, alreadyVotedRequestIds, amuletName, svs]); + + const inflightBase = useMemo(() => { + if (!voteRequests || votingThreshold === undefined) { + return []; + } + + return voteRequests + .filter(v => alreadyVotedRequestIds.has(v.payload.trackingCid || v.contractId)) + .map(v => { + const effectiveAt = v.payload.targetEffectiveAt + ? dayjs(v.payload.targetEffectiveAt).format(dateTimeFormatISO) + : 'Threshold'; + const votes = v.payload.votes.entriesArray().map(e => e[1]); return { - contractId: vr.request.trackingCid, + contractId: v.payload.trackingCid || v.contractId, actionName: - actionTagToTitle(amuletName)[getAction(vr.request.action) as SupportedActionTag], - description: vr.request.reason.body, - votingThresholdDeadline: dayjs(vr.request.voteBefore).format(dateTimeFormatISO), - voteTakesEffect: - (vr.outcome.tag === 'VRO_Accepted' && - dayjs(vr.outcome.value.effectiveAt).format(dateTimeFormatISO)) || - dayjs(vr.completedAt).format(dateTimeFormatISO), + actionTagToTitle(amuletName)[ + getGovernanceActionTag(v.payload.action) as SupportedActionTag + ], + description: v.payload.reason.body, + votingThresholdDeadline: dayjs(v.payload.voteBefore).format(dateTimeFormatISO), + voteTakesEffect: effectiveAt, yourVote: computeYourVote(votes, svPartyId), - status: getVoteResultStatus(vr.outcome), + status: 'In Progress', voteStats: computeVoteStats(votes), acceptanceThreshold: votingThreshold, + requester: getRequesterPartyId(v.payload.requester, svs), } as ProposalListingData; }); - }, [voteResultsInfiniteQuery.data?.pages, amuletName, svPartyId, votingThreshold]); + }, [voteRequests, votingThreshold, alreadyVotedRequestIds, amuletName, svPartyId, svs]); - if ( + const isLoading = dsoInfosQuery.isPending || listVoteRequestsQuery.isPending || votesQuery.isPending || - voteResultsInfiniteQuery.isPending - ) { + voteResultsInfiniteQuery.isPending; + + const hasSearch = isValidContractId(searchQuery); + + const actionRequiredRequests = useMemo( + () => (hasSearch ? filterByContractId(actionRequiredBase, searchQuery) : actionRequiredBase), + [hasSearch, actionRequiredBase, searchQuery] + ); + + const inflightRequests = useMemo( + () => (hasSearch ? filterByContractId(inflightBase, searchQuery) : inflightBase), + [hasSearch, inflightBase, searchQuery] + ); + + const loadedVoteHistoryMatches = useMemo( + () => (hasSearch ? filterByContractId(voteHistory, searchQuery) : []), + [hasSearch, voteHistory, searchQuery] + ); + + const needsClosedVoteFetch = + !isLoading && + hasSearch && + actionRequiredRequests.length === 0 && + inflightRequests.length === 0 && + loadedVoteHistoryMatches.length === 0; + + const searchVoteResults = usePaginatedVoteRequestResultsByContractId(needsClosedVoteFetch, { + contractId: searchQuery, + }); + + const searchVoteHistoryBase = useMemo(() => { + if (!svPartyId || votingThreshold === undefined) { + return []; + } + + return buildVoteHistoryData( + searchVoteResults.results, + amuletName, + svPartyId, + votingThreshold, + svs + ); + }, [searchVoteResults.results, amuletName, svPartyId, votingThreshold, svs]); + + const filteredVoteHistory = useMemo(() => { + if (!hasSearch) { + return voteHistory; + } + + if (loadedVoteHistoryMatches.length > 0) { + return loadedVoteHistoryMatches; + } + + return filterByContractId(searchVoteHistoryBase, searchQuery); + }, [hasSearch, voteHistory, searchQuery, loadedVoteHistoryMatches, searchVoteHistoryBase]); + + const showVoteHistorySectionLoading = + needsClosedVoteFetch && !searchVoteResults.isComplete && filteredVoteHistory.length === 0; + + const hasLoadedAllVoteHistoryPages = !voteResultsInfiniteQuery.hasNextPage; + + const showEmptyState = + !isLoading && + !hasSearch && + actionRequiredRequests.length === 0 && + inflightRequests.length === 0 && + filteredVoteHistory.length === 0 && + hasLoadedAllVoteHistoryPages; + + if (isLoading) { return ; } @@ -115,45 +207,6 @@ export const Governance: React.FC = () => { return ; } - const voteRequests = listVoteRequestsQuery.data; - - const actionRequiredRequests = voteRequests - .filter(v => !alreadyVotedRequestIds.has(v.payload.trackingCid || v.contractId)) - .map(vr => { - return { - contractId: vr.payload.trackingCid || vr.contractId, - actionName: - actionTagToTitle(amuletName)[getAction(vr.payload.action) as SupportedActionTag], - description: vr.payload.reason.body, - votingCloses: dayjs(vr.payload.voteBefore).format(dateTimeFormatISO), - createdAt: dayjs(vr.createdAt).format(dateTimeFormatISO), - requester: vr.payload.requester, - isYou: vr.payload.requester === svPartyId, - } as ActionRequiredData; - }); - - const inflightRequests = voteRequests - .filter(v => alreadyVotedRequestIds.has(v.payload.trackingCid || v.contractId)) - .map(v => { - const effectiveAt = v.payload.targetEffectiveAt - ? dayjs(v.payload.targetEffectiveAt).format(dateTimeFormatISO) - : 'Threshold'; - - const votes = v.payload.votes.entriesArray().map(e => e[1]); - - return { - contractId: v.payload.trackingCid || v.contractId, - actionName: actionTagToTitle(amuletName)[getAction(v.payload.action) as SupportedActionTag], - description: v.payload.reason.body, - votingThresholdDeadline: dayjs(v.payload.voteBefore).format(dateTimeFormatISO), - voteTakesEffect: effectiveAt, - yourVote: computeYourVote(votes, svPartyId), - status: 'In Progress', - voteStats: computeVoteStats(votes), - acceptanceThreshold: dsoInfosQuery.data.votingThreshold, - } as ProposalListingData; - }); - return ( { data-testid="governance-page-header" /> - {actionRequiredRequests.length === 0 && - inflightRequests.length === 0 && - voteHistory.length === 0 && - !voteResultsInfiniteQuery.hasNextPage ? ( + + + {showEmptyState ? ( ) : ( <> - + { p.results.length > 0).length + hasSearch + ? undefined + : voteResultsInfiniteQuery.data?.pages.filter(p => p.results.length > 0).length } /> diff --git a/apps/sv/frontend/src/routes/voteRequestDetails.tsx b/apps/sv/frontend/src/routes/voteRequestDetails.tsx index 41ad05195f..5474a7d8c6 100644 --- a/apps/sv/frontend/src/routes/voteRequestDetails.tsx +++ b/apps/sv/frontend/src/routes/voteRequestDetails.tsx @@ -19,6 +19,7 @@ import { buildProposal, formatBasisPoints, getActionValue, + getRequesterPartyId, getVoteResultStatus, } from '../utils/governance'; import { useDsoInfos } from '../contexts/SvContext'; @@ -60,7 +61,7 @@ export const VoteRequestDetails: React.FC = () => { currentEffectiveAt ); - if (dsoInfosQuery.isPending && isPending) { + if (dsoInfosQuery.isPending || isPending) { return ; } @@ -73,7 +74,8 @@ export const VoteRequestDetails: React.FC = () => { } const svPartyId = dsoInfosQuery.data?.svPartyId || ''; - const allSvs = dsoInfosQuery.data?.dsoRules.payload.svs.entriesArray().map(e => e[0]) || []; + const svs = dsoInfosQuery.data?.dsoRules.payload.svs; + const allSvs = svs?.entriesArray().map(e => e[0]) || []; const amuletOrDsoAction = getActionValue(request.action); // check that amuletOrDsoAction is a supported action @@ -111,13 +113,24 @@ export const VoteRequestDetails: React.FC = () => { previousRewardWeight !== undefined ? formatBasisPoints(previousRewardWeight) : ''; } + // For closed votes the outcome carries the actual effective time. Old vote + // requests (created before targetEffectiveAt existed) decode with + // targetEffectiveAt = None, so the request alone can't tell "effective at + // threshold" apart from "effective at expiry". + const voteTakesEffect = hasVoteRequest + ? request.targetEffectiveAt + ? dayjs(request.targetEffectiveAt).format(dateTimeFormatISO) + : 'Threshold' + : voteResult?.outcome.tag === 'VRO_Accepted' + ? dayjs(voteResult.outcome.value.effectiveAt).format(dateTimeFormatISO) + : dayjs(voteResult?.completedAt).format(dateTimeFormatISO); + + const requesterPartyId = getRequesterPartyId(request.requester, svs); const votingInformation: ProposalVotingInformation = { - requester: request.requester, - requesterIsYou: request.requester === svPartyId, + requester: requesterPartyId, + requesterIsYou: requesterPartyId === svPartyId, votingThresholdDeadline: dayjs(request.voteBefore).format(dateTimeFormatISO), - voteTakesEffect: request.targetEffectiveAt - ? dayjs(request.targetEffectiveAt).format(dateTimeFormatISO) - : 'Threshold', + voteTakesEffect, status: hasVoteRequest ? 'In Progress' : getVoteResultStatus(voteResult?.outcome), }; diff --git a/apps/sv/frontend/src/theme/tokens.ts b/apps/sv/frontend/src/theme/tokens.ts new file mode 100644 index 0000000000..63245136a8 --- /dev/null +++ b/apps/sv/frontend/src/theme/tokens.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** Figma tokens from CF-design-system (tokens.md + delegate-election-2 Dev Mode). */ +export const layoutTokens = { + /** Figma surface-page — bg-neutral-800 */ + page: '#262626', + /** Figma Dev Mode Background-lighter on Navigation component */ + navBackground: '#272727', + /** Figma Dev Mode --Light-text — brand wordmark and nav labels */ + lightText: '#E2E2E2', + /** Figma red00 — Governance count badge */ + notificationBadge: '#FD8575', + /** Figma Purple (Navigation) — active nav pill border */ + navActiveOutline: '#875CFF', + navAttention: '#F3FF97', + /** Brand wordmark — Inter until a production-licensed Termina is available */ + fontBrand: '"Inter", sans-serif', + fontUi: '"Inter", sans-serif', +} as const; + +/** Figma Dev Mode — nav row horizontal padding 50px */ +export const PAGE_PX = '50px'; + +/** Figma p-2.5 — 10px padding on brand box and nav pills */ +export const NAV_PILL_PX = '10px'; + +/** Figma content max width (nav row is full width; content uses this) */ +export const CONTENT_MAX_WIDTH = 1583; + +/** Figma Dev Mode — 64px space below nav row, present on every page */ +export const HEADER_PB = 8; + +export const HEADER_PT = 3; + +/** Figma Dev Mode — fixed 60px between nav pills (not responsive). */ +export const NAV_GAP = '60px'; + +/** Figma Dev Mode — nav row height 44px */ +export const NAV_ROW_MIN_HEIGHT = 44; + +/** + * Figma Dev Mode — Inter nav/logout typography (letter spacing: 0px, 140% line-height). + * Without an explicit `letterSpacing` reset, these Box/Typography elements inherit + * MUI's default body1 letter-spacing (0.00938em) from an ancestor, rendering as a + * ~0.13-0.15px leak that's invisible in a screenshot but measurable via computed + * styles and doesn't match Figma's 0px spec. `lineHeight: 'normal'` also falls back + * to Inter's own metrics rather than the explicit 140% Figma spec, so it's pinned here. + */ +export const navItemTypography = { + fontFeatureSettings: "'liga' off, 'clig' off", + lineHeight: 1.4, + letterSpacing: 0, +} as const; + +export const BRAND_TITLE = 'Supervalidator Operations'; diff --git a/apps/sv/frontend/src/themes/fieldStyles.ts b/apps/sv/frontend/src/themes/fieldStyles.ts new file mode 100644 index 0000000000..dde77a7564 --- /dev/null +++ b/apps/sv/frontend/src/themes/fieldStyles.ts @@ -0,0 +1,329 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { SxProps, Theme } from '@mui/material/styles'; + +const fieldSurfaceBackground = '#363636'; + +export const fieldSectionTitleSx: SxProps = { + fontSize: 12, + lineHeight: '22px', + fontWeight: 600, + textTransform: 'uppercase', + color: 'common.white', +}; + +export const fieldDescriptionSx: SxProps = { + fontSize: 12, + lineHeight: '22px', + color: 'text.light', +}; + +export const fieldSectionSx: SxProps = { + display: 'flex', + flexDirection: 'column', + gap: 1, +}; + +const fieldHelperSx: SxProps = { + '& .MuiFormHelperText-root': { + mx: 0, + mt: 1, + }, +}; + +const inputTypographySx = (theme: Theme) => ({ + fontSize: theme.typography.body2.fontSize, + fontWeight: theme.typography.body2.fontWeight, + fontFamily: theme.typography.body2.fontFamily, + lineHeight: '22px', + letterSpacing: 0, + color: theme.palette.text.light, + backgroundColor: 'transparent', + boxSizing: 'border-box' as const, + WebkitBoxShadow: 'none', +}); + +const fieldOutlineSx = (theme: Theme) => ({ + '& .MuiOutlinedInput-notchedOutline, & fieldset': { + border: 'none', + borderRadius: '4px', + }, + '&.Mui-error .MuiOutlinedInput-notchedOutline, &.Mui-error fieldset': { + border: `1px solid ${theme.palette.error.main}`, + }, +}); + +const fieldAdornmentSx = { + '& .MuiInputAdornment-root': { + margin: 0, + maxHeight: '22px', + height: '22px', + alignSelf: 'center', + }, + '& .MuiInputAdornment-positionEnd': { + marginLeft: 'auto', + }, + '& .MuiInputAdornment-root .MuiIconButton-root': { + color: 'common.white', + padding: 0, + width: 16, + height: 16, + '& svg': { + fontSize: 16, + }, + }, + '& .MuiSelect-icon': { + color: 'common.white', + fontSize: 16, + top: 'calc(50% - 0.5em)', + }, +}; + +const multilineSurfaceSx = (theme: Theme) => ({ + display: 'flex', + padding: '13px 16px', + justifyContent: 'space-between', + alignItems: 'flex-start', + alignContent: 'flex-start', + flexWrap: 'wrap', + rowGap: '10px', + alignSelf: 'stretch', + backgroundColor: fieldSurfaceBackground, + borderRadius: '4px', + overflow: 'hidden', + ...fieldOutlineSx(theme), +}); + +const multilineInputSx = (theme: Theme) => ({ + ...inputTypographySx(theme), + flex: 1, + width: '100%', + minWidth: '100%', + padding: 0, +}); + +export const singleLineInputRootSx: SxProps = theme => ({ + ...inputTypographySx(theme), + display: 'flex', + padding: '13px 16px', + justifyContent: 'space-between', + alignItems: 'center', + alignContent: 'center', + flexWrap: 'wrap', + rowGap: '10px', + alignSelf: 'stretch', + minHeight: 0, + height: 'auto', + backgroundColor: fieldSurfaceBackground, + borderRadius: '4px', + overflow: 'hidden', + ...fieldOutlineSx(theme), + ...fieldAdornmentSx, + '&.MuiInputBase-root, &.MuiOutlinedInput-root, &.MuiInputBase-adornedEnd': { + minHeight: 0, + height: 'auto', + }, + // Override common MuiInputBase global `.MuiOutlinedInput-input` background (neutral[10]). + '& .MuiOutlinedInput-input, & .MuiInputBase-input, & input, & .MuiSelect-select': { + ...inputTypographySx(theme), + flex: 1, + minWidth: 0, + padding: 0, + minHeight: 0, + height: 'auto', + }, + '&.MuiInputBase-sizeSmall': { + minHeight: 0, + }, + '& .MuiOutlinedInput-inputSizeSmall': { + padding: 0, + }, +}); + +const proposalSummaryInputRootSx: SxProps = theme => ({ + ...multilineSurfaceSx(theme), + height: '130px', + '& textarea, & .MuiOutlinedInput-input': { + ...multilineInputSx(theme), + alignSelf: 'stretch', + minHeight: 0, + height: '100%', + resize: 'none', + overflow: 'auto', + }, +}); + +const configFieldInputRootSx: SxProps = theme => ({ + ...inputTypographySx(theme), + display: 'flex', + width: '238px', + maxWidth: '100%', + height: '48px', + padding: '14px 24px', + justifyContent: 'flex-end', + alignItems: 'center', + gap: '10px', + boxSizing: 'border-box', + minHeight: 0, + backgroundColor: fieldSurfaceBackground, + borderRadius: '4px', + overflow: 'hidden', + ...fieldOutlineSx(theme), + // Edited state: ConfigField sets focused when value differs from default (Figma: #F3FF97 border). + '&.Mui-focused .MuiOutlinedInput-notchedOutline, &.Mui-focused fieldset': { + border: `1px solid ${theme.palette.secondary.main}`, + }, + '&.MuiInputBase-root, &.MuiOutlinedInput-root': { + minHeight: 0, + height: '48px', + }, + '& .MuiOutlinedInput-input, & .MuiInputBase-input, & input': { + ...inputTypographySx(theme), + flex: 1, + minWidth: 0, + padding: 0, + minHeight: 0, + height: 'auto', + textAlign: 'right', + }, +}); + +/** Single-line TextField wrapper — overrides common MuiInputBase global styles. */ +export const singleLineFieldSx: SxProps = theme => ({ + ...fieldHelperSx, + '& .MuiOutlinedInput-root': + typeof singleLineInputRootSx === 'function' + ? singleLineInputRootSx(theme) + : singleLineInputRootSx, + // Figma empty-field prompt: Body M grey105 (same as Proposal Summary / Select Action). + '& .MuiOutlinedInput-input::placeholder': { + color: '#696969', + opacity: 1, + }, +}); + +/** Proposal summary — fixed 130px height. */ +export const proposalSummaryFieldSx: SxProps = theme => ({ + ...fieldHelperSx, + '& .MuiOutlinedInput-root': + typeof proposalSummaryInputRootSx === 'function' + ? proposalSummaryInputRootSx(theme) + : proposalSummaryInputRootSx, + // Figma empty-field prompt: Body M grey105 (same as Select Action placeholder). + '& .MuiOutlinedInput-input::placeholder': { + color: '#696969', + opacity: 1, + }, +}); + +/** Date picker OutlinedInput root — single row; 16px inset for chevron via root padding. */ +const datePickerInputRootSx: SxProps = theme => ({ + ...inputTypographySx(theme), + display: 'flex', + flexWrap: 'nowrap', + padding: '13px 16px', + justifyContent: 'space-between', + alignItems: 'center', + alignSelf: 'stretch', + width: '100%', + minHeight: 0, + height: 'auto', + backgroundColor: fieldSurfaceBackground, + borderRadius: '4px', + overflow: 'hidden', + ...fieldOutlineSx(theme), + ...fieldAdornmentSx, + '&.MuiInputBase-root, &.MuiOutlinedInput-root, &.MuiInputBase-adornedEnd': { + minHeight: 0, + height: 'auto', + }, + '& .MuiOutlinedInput-input, & .MuiInputBase-input, & input': { + ...inputTypographySx(theme), + flex: 1, + minWidth: 0, + padding: 0, + minHeight: 0, + height: 'auto', + }, + '& .MuiInputAdornment-positionEnd': { + marginLeft: 'auto', + marginRight: 0, + flexShrink: 0, + }, +}); + +/** Date picker TextField wrapper (helper text spacing + OutlinedInput surface). */ +export const datePickerFieldSx: SxProps = theme => ({ + ...fieldHelperSx, + width: '100%', + '& .MuiOutlinedInput-root': + typeof datePickerInputRootSx === 'function' + ? datePickerInputRootSx(theme) + : datePickerInputRootSx, +}); + +/** Date picker OutlinedInput root — apply via `slotProps.input.sx` when needed. */ +export const datePickerInputSx: SxProps = datePickerInputRootSx; + +/** Select OutlinedInput root — single row; chevron inset matches date picker. */ +const selectInputRootSx: SxProps = theme => ({ + ...inputTypographySx(theme), + display: 'flex', + flexWrap: 'nowrap', + padding: '13px 16px', + justifyContent: 'space-between', + alignItems: 'center', + alignContent: 'center', + alignSelf: 'stretch', + minHeight: 0, + height: 'auto', + backgroundColor: fieldSurfaceBackground, + borderRadius: '4px', + overflow: 'hidden', + ...fieldOutlineSx(theme), + ...fieldAdornmentSx, + '&.MuiInputBase-root, &.MuiOutlinedInput-root, &.MuiInputBase-adornedEnd': { + minHeight: 0, + height: 'auto', + }, + '& .MuiOutlinedInput-input, & .MuiInputBase-input, & input, & .MuiSelect-select': { + ...inputTypographySx(theme), + flex: 1, + minWidth: 0, + padding: 0, + minHeight: 0, + height: 'auto', + }, + '&.MuiSelect-outlined .MuiSelect-select': { + paddingRight: 0, + }, + '& .MuiSelect-icon': { + color: 'common.white', + fontSize: 16, + marginLeft: 'auto', + marginRight: 0, + flexShrink: 0, + position: 'static', + top: 'auto', + right: 'auto', + }, + '&.MuiInputBase-sizeSmall': { + minHeight: 0, + }, + '& .MuiOutlinedInput-inputSizeSmall': { + padding: 0, + }, +}); + +export const selectFieldSx: SxProps = selectInputRootSx; + +/** DSO config table TextField wrapper (helper text spacing). */ +export const configFieldFieldSx: SxProps = { + ...fieldHelperSx, + width: '238px', + maxWidth: '100%', +}; + +/** DSO config table OutlinedInput root — apply via `slotProps.input.sx`. */ +export const configFieldInputSx: SxProps = configFieldInputRootSx; diff --git a/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts b/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts index 5eb83314ea..c7b3994d66 100644 --- a/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts +++ b/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts @@ -5,6 +5,7 @@ import { AmuletConfig, PackageConfig, RewardConfig, + RewardVersion, } from '@daml.js/splice-amulet/lib/Splice/AmuletConfig'; import { Tuple2 } from '@daml.js/daml-prim-DA-Types-1.0.0/lib/DA/Types'; import { Set as DamlSet } from '@daml.js/daml-stdlib-DA-Set-Types-1.0.0/lib/DA/Set/Types'; @@ -320,6 +321,16 @@ function buildIssuanceCurveChanges( return [...initialValues, ...futureValues]; } +const rewardVersionLabels = { + RewardVersion_FeaturedAppMarkers: 'Featured App Markers (pre CIP-104)', + RewardVersion_TrafficBasedAppRewards: 'Traffic-Based App Rewards (CIP-104)', +} satisfies Record; + +const rewardVersionOptions = RewardVersion.keys.map(value => ({ + value, + label: rewardVersionLabels[value], +})); + function buildRewardConfigChanges( before: RewardConfig | null | undefined, after: RewardConfig | null | undefined @@ -327,33 +338,42 @@ function buildRewardConfigChanges( return [ { fieldName: 'rewardConfigMintingVersion', - label: 'Reward config: Minting version', + label: 'Reward config: Reward scheme', currentValue: before?.mintingVersion || '', newValue: after?.mintingVersion || '', + options: rewardVersionOptions, + description: 'Which reward scheme to use in production.', }, { fieldName: 'rewardConfigDryRunVersion', - label: 'Reward config: Dry-run version', + label: 'Reward config: Dry-run reward scheme', currentValue: before?.dryRunVersion || '', newValue: after?.dryRunVersion || '', + options: [{ value: '', label: 'None (disabled)' }, ...rewardVersionOptions], + description: + 'Which reward scheme to run in dry-run mode. Select "None (disabled)" to turn it off.', }, { fieldName: 'rewardConfigBatchSize', - label: 'Reward config: Batch size', + label: 'Reward config: Merkle tree batch size', currentValue: before?.batchSize || '', newValue: after?.batchSize || '', + description: 'Batch size for building the Merkle tree over minting allowances (default: 100)', }, { fieldName: 'rewardConfigRewardCouponTimeToLive', label: 'Reward config: Reward coupon time to live (microseconds)', currentValue: before?.rewardCouponTimeToLive.microseconds || '', newValue: after?.rewardCouponTimeToLive.microseconds || '', + description: 'Time-to-live for RewardCouponV2 contracts (default: 36 hours)', }, { fieldName: 'rewardConfigAppRewardCouponThreshold', label: 'Reward config: App reward coupon threshold ($)', currentValue: before?.appRewardCouponThreshold || '', newValue: after?.appRewardCouponThreshold || '', + description: + 'Minimum reward amount in USD below which no RewardCouponV2 is created (default: $0.50)', }, ] as ConfigChange[]; } diff --git a/apps/sv/frontend/src/utils/buildDsoConfigChanges.ts b/apps/sv/frontend/src/utils/buildDsoConfigChanges.ts index e1c9893e41..78bbd3c988 100644 --- a/apps/sv/frontend/src/utils/buildDsoConfigChanges.ts +++ b/apps/sv/frontend/src/utils/buildDsoConfigChanges.ts @@ -79,18 +79,18 @@ export function buildDsoConfigChanges( currentValue: before?.numMemberTrafficContractsThreshold || '', newValue: after?.numMemberTrafficContractsThreshold || '', }, - { - fieldName: 'actionConfirmationTimeout', - label: 'Time-To-Live for contracts representing a confirmation of an action', - currentValue: before?.actionConfirmationTimeout.microseconds || '', - newValue: after?.actionConfirmationTimeout.microseconds || '', - }, { fieldName: 'svOnboardingRequestTimeout', label: 'Time-To-Live for contracts representing an incomplete Super Validator onboarding', currentValue: before?.svOnboardingRequestTimeout.microseconds || '', newValue: after?.svOnboardingRequestTimeout.microseconds || '', }, + { + fieldName: 'actionConfirmationTimeout', + label: 'Time-To-Live for contracts representing a confirmation of an action', + currentValue: before?.actionConfirmationTimeout.microseconds || '', + newValue: after?.actionConfirmationTimeout.microseconds || '', + }, { fieldName: 'svOnboardingConfirmedTimeout', label: @@ -98,12 +98,6 @@ export function buildDsoConfigChanges( currentValue: before?.svOnboardingConfirmedTimeout.microseconds || '', newValue: after?.svOnboardingConfirmedTimeout.microseconds || '', }, - { - fieldName: 'maxTextLength', - label: 'Generic upper limit on text fields', - currentValue: before?.maxTextLength || '', - newValue: after?.maxTextLength || '', - }, { fieldName: 'voteRequestTimeout', label: 'Time-To-Live for contracts representing vote requests and votes', @@ -146,6 +140,12 @@ export function buildDsoConfigChanges( currentValue: before?.synchronizerNodeConfigLimits.cometBft.maxPubKeyLength || '', newValue: after?.synchronizerNodeConfigLimits.cometBft.maxPubKeyLength || '', }, + { + fieldName: 'maxTextLength', + label: 'Generic upper limit on text fields', + currentValue: before?.maxTextLength || '', + newValue: after?.maxTextLength || '', + }, ...buildSynchronizerMap(before?.decentralizedSynchronizer, after?.decentralizedSynchronizer), diff --git a/apps/sv/frontend/src/utils/constants.ts b/apps/sv/frontend/src/utils/constants.ts index c0b2907dca..2e96c45db7 100644 --- a/apps/sv/frontend/src/utils/constants.ts +++ b/apps/sv/frontend/src/utils/constants.ts @@ -2,7 +2,42 @@ // SPDX-License-Identifier: Apache-2.0 export const PROPOSAL_SUMMARY_TITLE = 'Proposal Summary'; +export const PROPOSAL_REVIEW_TITLE = 'Proposal Review'; export const PROPOSAL_SUMMARY_SUBTITLE = 'For CIP votes, consider copying the CIP abstract here'; +export const DATE_TIME_PLACEHOLDER = 'YYYY-MM-DD HH:MM'; +export const REASON_PLACEHOLDER = 'Add your reasoning here'; +export const URL_PLACEHOLDER = 'https://example.com'; +/** Figma initiate Proposal Summary empty prompt (Body M grey105 when empty). */ +export const PROPOSAL_SUMMARY_PLACEHOLDER = REASON_PLACEHOLDER; export const DEFAULT_PROPOSAL_SUMMARY_MAX_LENGTH = 1024; export const THRESHOLD_DEADLINE_SUBTITLE = 'Proposal remains open only if ⅔ of nodes place a vote before this date-time'; +export const DEFAULT_APP_ACTIVITY_WEIGHT = '1.0'; + +export const SUPPORTING_URL_LABEL = 'Supporting URL'; +export const SUPPORTING_URL_PLACEHOLDER = URL_PLACEHOLDER; +export const VOTE_REASON_URL_LABEL = 'Vote Reason URL'; +export const VOTE_REASON_SUMMARY_LABEL = 'Reason'; +/** Your Vote Reason empty prompt — aligned with #6910. */ +export const VOTE_REASON_PLACEHOLDER = REASON_PLACEHOLDER; +/** Your Vote URL empty prompt — aligned with #6910. */ +export const VOTE_REASON_URL_PLACEHOLDER = URL_PLACEHOLDER; +export const VOTE_PROPOSAL_CONTRACT_ID_LABEL = 'Vote proposal contract id'; +export const THRESHOLD_DEADLINE_LABEL = 'Quorum Threshold Deadline'; +export const EFFECTIVE_AT_LABEL = 'Effective At'; +export const PROPOSAL_CREATED_LABEL = 'Proposal Created'; + +/** Figma field labels for the initiate-proposal edit step (12px uppercase). */ +export const CREATE_PROPOSAL_LABEL_PROPOSAL_TYPE = 'ACTION'; // Figma FIELD H “Action” (edit + review) +export const CREATE_PROPOSAL_LABEL_THRESHOLD_DEADLINE = 'QUORUM THRESHOLD DEADLINE'; +export const CREATE_PROPOSAL_LABEL_EFFECTIVE_AT = 'EFFECTIVE AT'; +export const CREATE_PROPOSAL_LABEL_PROPOSAL_SUMMARY = 'PROPOSAL SUMMARY'; +export const CREATE_PROPOSAL_LABEL_SUPPORTING_URL = 'SUPPORTING URL'; +export const CREATE_PROPOSAL_LABEL_CONFIGURATION = 'CONFIGURATION'; +export const CREATE_PROPOSAL_LABEL_MEMBER = 'MEMBER'; +export const CREATE_PROPOSAL_LABEL_WEIGHT = 'WEIGHT'; +export const CREATE_PROPOSAL_LABEL_PROVIDER_PARTY_ID = 'PROVIDER PARTY ID'; +export const CREATE_PROPOSAL_LABEL_FEATURED_APP_CONTRACT_ID = 'FEATURED APPLICATION CONTRACT ID'; +export const CREATE_PROPOSAL_LABEL_BENEFICIARY = 'BENEFICIARY'; +export const CREATE_PROPOSAL_LABEL_AMOUNT = 'AMOUNT'; +export const CREATE_PROPOSAL_LABEL_MUST_MINT_BEFORE = 'MUST MINT BEFORE'; diff --git a/apps/sv/frontend/src/utils/governance.ts b/apps/sv/frontend/src/utils/governance.ts index 0fdf4bd4aa..92d423a5c0 100644 --- a/apps/sv/frontend/src/utils/governance.ts +++ b/apps/sv/frontend/src/utils/governance.ts @@ -5,6 +5,7 @@ import type { ActionRequiringConfirmation, AmuletRules_ActionRequiringConfirmation, DsoRules_ActionRequiringConfirmation, + DsoRules_CloseVoteRequestResult, DsoRules_SetConfig, DsoRulesConfig, SvInfo, @@ -25,9 +26,11 @@ import type { PendingConfigFieldInfo, Proposal, ProposalListingStatus, + ProposalListingData, SupportedActionTag, UnclaimedActivityRecordProposal, UnfeatureAppProposal, + UpdateFeatureAppProposal, UpdateSvRewardWeightProposal, YourVoteStatus, } from '../utils/types'; @@ -46,6 +49,7 @@ export const actionTagToTitle = (amuletName: string): Record info.name === requester); + return match?.[0] ?? requester; +} + export function computeYourVote(votes: Vote[], svPartyId: string | undefined): YourVoteStatus { if (svPartyId === undefined) { return 'no-vote'; @@ -110,6 +129,56 @@ export function computeYourVote(votes: Vote[], svPartyId: string | undefined): Y return vote ? (vote.accept ? 'accepted' : 'rejected') : 'no-vote'; } +export function getGovernanceActionTag(action: ActionRequiringConfirmation): string { + switch (action.tag) { + case 'ARC_AmuletRules': + return action.value.amuletRulesAction.tag; + case 'ARC_DsoRules': + return action.value.dsoAction.tag; + default: + return 'Action tag not defined.'; + } +} + +export function buildVoteHistoryData( + voteResults: DsoRules_CloseVoteRequestResult[], + amuletName: string, + svPartyId: string | undefined, + votingThreshold: bigint, + svs: { entriesArray(): [string, SvInfo][] } | undefined +): ProposalListingData[] { + return voteResults + .filter( + vr => + (vr.outcome.tag === 'VRO_Accepted' && + dayjs(vr.outcome.value.effectiveAt).isBefore(dayjs())) || + vr.outcome.tag === 'VRO_Expired' || + vr.outcome.tag === 'VRO_Rejected' + ) + .map(vr => { + const votes = vr.request.votes.entriesArray().map(e => e[1]); + + return { + contractId: vr.request.trackingCid, + actionName: + actionTagToTitle(amuletName)[ + getGovernanceActionTag(vr.request.action) as SupportedActionTag + ], + description: vr.request.reason.body, + votingThresholdDeadline: dayjs(vr.request.voteBefore).format(dateTimeFormatISO), + voteTakesEffect: + (vr.outcome.tag === 'VRO_Accepted' && + dayjs(vr.outcome.value.effectiveAt).format(dateTimeFormatISO)) || + dayjs(vr.completedAt).format(dateTimeFormatISO), + yourVote: computeYourVote(votes, svPartyId), + status: getVoteResultStatus(vr.outcome), + voteStats: computeVoteStats(votes), + acceptanceThreshold: votingThreshold, + requester: getRequesterPartyId(vr.request.requester, svs), + } as ProposalListingData; + }); +} + export function buildProposal(action: ActionRequiringConfirmation, dsoInfo?: DsoInfo): Proposal { if (action.tag === 'ARC_DsoRules') { const dsoAction = action.value.dsoAction; @@ -134,9 +203,17 @@ export function buildProposal(action: ActionRequiringConfirmation, dsoInfo?: Dso dsoAction.value.expiresAt ); case 'SRARC_GrantFeaturedAppRight': - return createGrantFeatureAppProposal(dsoAction.value.provider); + return createGrantFeatureAppProposal( + dsoAction.value.provider, + dsoAction.value.activityWeight ?? '' + ); case 'SRARC_RevokeFeaturedAppRight': return createRevokeFeatureAppProposal(dsoAction.value.rightCid); + case 'SRARC_UpdateFeaturedAppRight': + return createUpdateFeatureAppProposal( + dsoAction.value.rightCid, + dsoAction.value.update.newActivityWeight + ); case 'SRARC_SetConfig': return createDsoRulesConfigProposal(dsoAction.value.baseConfig, dsoAction.value.newConfig); } @@ -156,12 +233,23 @@ function createOffboardMemberProposal(memberToOffboard: string): OffBoardMemberP return { memberToOffboard }; } -function createGrantFeatureAppProposal(provider: string): FeatureAppProposal { +function createGrantFeatureAppProposal( + provider: string, + activityWeight: string +): FeatureAppProposal { return { provider: provider, + activityWeight: activityWeight, }; } +function createUpdateFeatureAppProposal( + rightContractId: string, + newActivityWeight: string +): UpdateFeatureAppProposal { + return { rightContractId, newActivityWeight }; +} + function createRevokeFeatureAppProposal(rightContractId: string): UnfeatureAppProposal { return { rightContractId: rightContractId, @@ -274,6 +362,10 @@ export function getSvRewardWeight(svs: [string, SvInfo][], svPartyId: string): s return svInfo ? svInfo[1].svRewardWeight : ''; } +export function activityWeightToOptional(weight: string): string | null { + return weight.trim() === '' ? null : weight; +} + export function buildPendingConfigFields( proposals: Contract[] | undefined ): PendingConfigFieldInfo[] { diff --git a/apps/sv/frontend/src/utils/proposalSearch.ts b/apps/sv/frontend/src/utils/proposalSearch.ts new file mode 100644 index 0000000000..65dc86c70b --- /dev/null +++ b/apps/sv/frontend/src/utils/proposalSearch.ts @@ -0,0 +1,94 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** MSW mock CIDs use this length with non-ledger prefixes (`10…`, `99…`). */ +const MOCK_TEST_CONTRACT_ID_LENGTH = 138; + +export const CONTRACT_ID_VALIDATION_MESSAGE = 'Enter a valid contract ID.'; + +/** Bounds mirror LfValue.ContractId.fromString (Value.scala) + even hex for Bytes.fromString. */ +const V1_MIN_LENGTH = 66; +const V1_MAX_LENGTH = 254; +const V2_MIN_LENGTH = 26; +const V2_MAX_LENGTH = 92; + +function isHexChar(code: number): boolean { + return (code >= 48 && code <= 57) || (code >= 97 && code <= 102) || (code >= 65 && code <= 70); +} + +function isPlausibleContractId(value: string): boolean { + const len = value.length; + if (len === 0 || (len & 1) !== 0 || len < V2_MIN_LENGTH || len > V1_MAX_LENGTH) { + return false; + } + + for (let i = 0; i < len; i++) { + if (!isHexChar(value.charCodeAt(i))) { + return false; + } + } + + if (value.charCodeAt(0) === 48 && value.charCodeAt(1) === 48) { + return len >= V1_MIN_LENGTH && len <= V1_MAX_LENGTH; + } + if (value.charCodeAt(0) === 48 && value.charCodeAt(1) === 49) { + return len >= V2_MIN_LENGTH && len <= V2_MAX_LENGTH; + } + return len === MOCK_TEST_CONTRACT_ID_LENGTH; +} + +export function isValidContractId(value: string): boolean { + return isPlausibleContractId(value.trim()); +} + +function normalizeContractIdQuery(query: string): string | null { + const trimmed = query.trim(); + if (!trimmed || !isPlausibleContractId(trimmed)) { + return null; + } + return trimmed.toLowerCase(); +} + +function matchesNormalizedContractId( + normalizedQuery: string, + contractId: string | null | undefined +): boolean { + return typeof contractId === 'string' && contractId.toLowerCase() === normalizedQuery; +} + +export function filterByContractId( + items: T[], + query: string | null | undefined +): T[] { + const normalizedQuery = query == null ? null : normalizeContractIdQuery(query); + if (normalizedQuery === null) { + return []; + } + return items.filter(item => + matchesNormalizedContractId(normalizedQuery, item.contractId as string) + ); +} + +export function findByContractId( + items: T[], + query: string, + getContractId: (item: T) => string | null | undefined +): T | undefined { + const normalizedQuery = normalizeContractIdQuery(query); + if (normalizedQuery === null) { + return undefined; + } + return items.find(item => matchesNormalizedContractId(normalizedQuery, getContractId(item))); +} + +export function shouldContinueVoteHistorySearch( + query: string, + results: T[], + getContractId: (item: T) => string | null | undefined +): boolean { + const normalizedQuery = normalizeContractIdQuery(query); + if (normalizedQuery === null) { + return false; + } + return !results.some(item => matchesNormalizedContractId(normalizedQuery, getContractId(item))); +} diff --git a/apps/sv/frontend/src/utils/types.ts b/apps/sv/frontend/src/utils/types.ts index b54b5c6797..b30b3cc598 100644 --- a/apps/sv/frontend/src/utils/types.ts +++ b/apps/sv/frontend/src/utils/types.ts @@ -22,12 +22,18 @@ export interface OffBoardMemberProposal { export interface FeatureAppProposal { provider: string; + activityWeight: string; } export interface UnfeatureAppProposal { rightContractId: string; } +export interface UpdateFeatureAppProposal { + rightContractId: string; + newActivityWeight: string; +} + export interface UnclaimedActivityRecordProposal { beneficiary: string; amount: string; @@ -57,6 +63,14 @@ export interface ConfigChange { * If the field should be disabled for editing. */ disabled?: boolean; + /** + * If set, render as a dropdown with these options instead of free text. + */ + options?: { value: string; label: string }[]; + /** + * Optional description shown as help text below the field. + */ + description?: string; } export interface UpdateSvRewardWeightProposal { @@ -85,6 +99,7 @@ export type Proposal = | UnclaimedActivityRecordProposal | AmuletRulesConfigProposal | DsoRulesConfigProposal + | UpdateFeatureAppProposal | undefined; export type ProposalActionMap = { @@ -95,6 +110,7 @@ export type ProposalActionMap = { SRARC_CreateUnallocatedUnclaimedActivityRecord: UnclaimedActivityRecordProposal; CRARC_SetConfig: AmuletRulesConfigProposal; SRARC_SetConfig: DsoRulesConfigProposal; + SRARC_UpdateFeaturedAppRight: UpdateFeatureAppProposal; // If no proposal type is defined, can use unknown or a specific type: CRARC_AddFutureAmuletConfigSchedule: unknown; }; @@ -133,7 +149,8 @@ export type SupportedActionTag = | 'SRARC_RevokeFeaturedAppRight' | 'SRARC_SetConfig' | 'SRARC_UpdateSvRewardWeight' - | 'SRARC_CreateUnallocatedUnclaimedActivityRecord'; + | 'SRARC_CreateUnallocatedUnclaimedActivityRecord' + | 'SRARC_UpdateFeaturedAppRight'; export type ProposalListingStatus = | 'Accepted' @@ -147,6 +164,7 @@ export interface ProposalListingData { contractId: ContractId; actionName: string; description?: string; + requester: string; votingThresholdDeadline: string; voteTakesEffect: string; yourVote: YourVoteStatus; @@ -198,11 +216,18 @@ export interface ProposalMutationArgs { action: ActionRequiringConfirmation; } +export interface UpdateFeatureAppFormData extends CommonProposalFormData { + partyId: string; + rightCid: string; + newActivityWeight: string; +} + export type NonConfigProposalFormData = | UpdateSvRewardWeightFormData | OffboardSvFormData | GrantRevokeFeaturedAppFormData - | CreateUnallocatedUnclaimedActivityRecordFormData; + | CreateUnallocatedUnclaimedActivityRecordFormData + | UpdateFeatureAppFormData; export type ConfigProposalFormData = SetDsoConfigCompleteFormData | SetAmuletConfigCompleteFormData; diff --git a/apps/sv/src/main/openapi/sv-internal.yaml b/apps/sv/src/main/openapi/sv-internal.yaml index 4884c71255..d271ebfed8 100644 --- a/apps/sv/src/main/openapi/sv-internal.yaml +++ b/apps/sv/src/main/openapi/sv-internal.yaml @@ -76,6 +76,7 @@ paths: get: tags: [sv] x-jvm-package: sv_public + x-external-audience: none operationId: "getCometBftNodeStatus" responses: "200": @@ -96,6 +97,7 @@ paths: post: tags: [sv] x-jvm-package: sv_public + x-external-audience: none operationId: "cometBftJsonRpcRequest" requestBody: required: true @@ -176,7 +178,7 @@ paths: /v0/admin/synchronizer/lsu/cancel: post: tags: [sv] - x-jvm-package: sv_admin + x-jvm-package: sv_operator operationId: "cancelLogicalSynchronizerUpgrade" responses: "200": @@ -229,6 +231,24 @@ paths: application/json: schema: "$ref": "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/ListDsoRulesVoteResultsResponse" + /v0/admin/sv/voteresults/count: + post: + tags: [ sv ] + x-jvm-package: sv_operator + operationId: "countVoteRequestResults" + requestBody: + required: true + content: + application/json: + schema: + "$ref": "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/CountVoteResultsRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + "$ref": "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/CountVoteResultsResponse" /v0/admin/sv/previous-sv-reward-weight: post: tags: [ sv ] @@ -375,6 +395,7 @@ paths: post: tags: [sv] x-jvm-package: sv_public + x-external-audience: validators operationId: "onboardValidator" requestBody: required: true @@ -393,6 +414,7 @@ paths: post: tags: [sv] x-jvm-package: sv_public + x-external-audience: svs operationId: "startSvOnboarding" requestBody: required: true @@ -411,6 +433,7 @@ paths: get: tags: [sv] x-jvm-package: sv_public + x-external-audience: svs operationId: "getSvOnboardingStatus" parameters: - name: "candidate_party_id_or_name" @@ -433,6 +456,7 @@ paths: post: tags: [sv] x-jvm-package: sv_public + x-external-audience: svs operationId: "onboardSvPartyMigrationAuthorize" requestBody: required: true @@ -459,6 +483,7 @@ paths: post: tags: [sv] x-jvm-package: sv_public + x-external-audience: svs operationId: "onboardSvSequencer" requestBody: required: true @@ -479,6 +504,7 @@ paths: post: tags: [sv] x-jvm-package: sv_public + x-external-audience: validators description: "faucet for validator candidates self-service" operationId: "devNetOnboardValidatorPrepare" responses: @@ -497,6 +523,7 @@ paths: tags: [sv] # TODO(DACH-NY/canton-network-internal#2106) Move to sv_operator x-jvm-package: sv_public + x-external-audience: validators operationId: "getDsoInfo" responses: "200": @@ -510,6 +537,7 @@ paths: get: tags: [sv] x-jvm-package: sv_public + x-external-audience: svs operationId: "getMigrationId" description: | Returns the synchronizer migration id this SV is currently using. diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/SvApp.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/SvApp.scala index b7fcb81919..9c26cc8655 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/SvApp.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/SvApp.scala @@ -220,6 +220,7 @@ class SvApp( localSynchronizerNodes.current.close() localSynchronizerNodes.successor.foreach(_.close()) localSynchronizerNodes.legacy.foreach(_.close()) + localSynchronizerNodes.additionalLegacy.foreach(_.close()) Future.failed(err) } ) @@ -498,6 +499,7 @@ class SvApp( timeouts, loggerFactory, amuletAppParameters.upgradesConfig, + participantAdminConnection, ) adminHandler = new HttpSvAdminHandler( @@ -742,12 +744,22 @@ object SvApp { override def closeAsync(): Seq[AsyncOrSyncCloseable] = Seq( + // One SyncCloseable per node so a failing close does not skip the others. SyncCloseable( - s"Domain connections", { - localSynchronizerNodes.current.close() - localSynchronizerNodes.successor.foreach(_.close()) - localSynchronizerNodes.legacy.foreach(_.close()) - }, + s"current domain connections", + localSynchronizerNodes.current.close(), + ), + SyncCloseable( + s"successor domain connections", + localSynchronizerNodes.successor.foreach(_.close()), + ), + SyncCloseable( + s"legacy domain connections", + localSynchronizerNodes.legacy.foreach(_.close()), + ), + SyncCloseable( + s"additional legacy domain connections", + localSynchronizerNodes.additionalLegacy.foreach(_.close()), ), SyncCloseable( s"Participant Admin connection", diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvAdminAppClient.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvAdminAppClient.scala index 6e9060523e..0f6cafa261 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvAdminAppClient.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvAdminAppClient.scala @@ -40,22 +40,4 @@ object HttpSvAdminAppClient { } } - case class CancelLogicalSynchronizerUpgrade() - extends BaseCommand[http.CancelLogicalSynchronizerUpgradeResponse, Unit] { - - override def submitRequest( - client: Client, - headers: List[HttpHeader], - ): EitherT[Future, Either[ - Throwable, - HttpResponse, - ], http.CancelLogicalSynchronizerUpgradeResponse] = - client.cancelLogicalSynchronizerUpgrade(headers = headers) - - override def handleOk()(implicit - decoder: TemplateJsonDecoder - ) = { case http.CancelLogicalSynchronizerUpgradeResponse.OK => - Right(()) - } - } } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvOperatorAppClient.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvOperatorAppClient.scala index b9f300eebf..2744a82274 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvOperatorAppClient.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvOperatorAppClient.scala @@ -19,6 +19,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.validatoronboarding a import org.lfdecentralizedtrust.splice.codegen.java.da.time.types.RelTime import org.lfdecentralizedtrust.splice.environment.SpliceStatus import org.lfdecentralizedtrust.splice.http.v0.{definitions, sv_operator as http} +import org.lfdecentralizedtrust.splice.store.VoteResultsFilters import org.lfdecentralizedtrust.splice.util.{Codec, Contract, TemplateJsonDecoder} import org.lfdecentralizedtrust.splice.sv.util.ValidatorOnboarding import com.digitalasset.canton.admin.api.client.data.NodeStatus @@ -244,11 +245,7 @@ object HttpSvOperatorAppClient { } case class ListVoteRequestResults( - actionName: Option[String], - accepted: Option[Boolean], - requester: Option[String], - effectiveFrom: Option[String], - effectiveTo: Option[String], + filters: VoteResultsFilters, limit: BigInt, pageToken: Option[BigInt] = None, ) extends BaseCommand[ @@ -265,13 +262,13 @@ object HttpSvOperatorAppClient { ): EitherT[Future, Either[Throwable, HttpResponse], http.ListVoteRequestResultsResponse] = client.listVoteRequestResults( body = definitions.ListVoteResultsRequest( - actionName, - accepted, - requester, - effectiveFrom, - effectiveTo, - limit, - pageToken, + filters.actionName, + filters.accepted, + requester = filters.requester, + effectiveFrom = filters.effectiveFrom, + effectiveTo = filters.effectiveTo, + limit = limit, + pageToken = pageToken, ), headers = headers, ) @@ -293,6 +290,35 @@ object HttpSvOperatorAppClient { } } + case class CountVoteRequestResults( + filters: VoteResultsFilters + ) extends BaseCommand[ + http.CountVoteRequestResultsResponse, + Long, + ] { + + override def submitRequest( + client: Client, + headers: List[HttpHeader], + ): EitherT[Future, Either[Throwable, HttpResponse], http.CountVoteRequestResultsResponse] = + client.countVoteRequestResults( + body = definitions.CountVoteResultsRequest( + filters.actionName, + filters.accepted, + requester = filters.requester, + effectiveFrom = filters.effectiveFrom, + effectiveTo = filters.effectiveTo, + ), + headers = headers, + ) + + override def handleOk()(implicit + decoder: TemplateJsonDecoder + ) = { case http.CountVoteRequestResultsResponse.OK(response) => + Right(response.count) + } + } + case class CastVote( trackingCid: VoteRequest.ContractId, isAccepted: Boolean, @@ -442,4 +468,23 @@ object HttpSvOperatorAppClient { Left(response.error) } } + + case class CancelLogicalSynchronizerUpgrade() + extends BaseCommand[http.CancelLogicalSynchronizerUpgradeResponse, Unit] { + + override def submitRequest( + client: Client, + headers: List[HttpHeader], + ): EitherT[Future, Either[ + Throwable, + HttpResponse, + ], http.CancelLogicalSynchronizerUpgradeResponse] = + client.cancelLogicalSynchronizerUpgrade(headers = headers) + + override def handleOk()(implicit + decoder: TemplateJsonDecoder + ) = { case http.CancelLogicalSynchronizerUpgradeResponse.OK => + Right(()) + } + } } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvAdminHandler.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvAdminHandler.scala index 7b52cf5a43..8ff0a70ae8 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvAdminHandler.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvAdminHandler.scala @@ -37,27 +37,6 @@ class HttpSvAdminHandler( protected val workflowId: String = this.getClass.getSimpleName private val dsoStore = dsoStoreWithIngestion.store - override def cancelLogicalSynchronizerUpgrade( - respond: r0.CancelLogicalSynchronizerUpgradeResponse.type - )()( - extracted: AdminUserRequest - ): Future[r0.CancelLogicalSynchronizerUpgradeResponse] = { - implicit val AdminUserRequest(traceContext) = extracted - withSpan(s"$workflowId.cancelLogicalSynchronizerUpgrade") { _ => _ => - for { - decentralizedSynchronizer <- dsoStore.getDsoRules().map(_.domain) - sequencerId <- synchronizerNodeService.sequencerAdminConnection().flatMap(_.getSequencerId) - _ <- participantAdminConnection - .removeSequencerSuccessor( - decentralizedSynchronizer, - sequencerId, - ) - _ <- participantAdminConnection - .removeLsuAnnouncement(decentralizedSynchronizer) - } yield r0.CancelLogicalSynchronizerUpgradeResponseOK - } - } - override def getSynchronizerNodeIdentitiesDump( respond: r0.GetSynchronizerNodeIdentitiesDumpResponse.type )()( diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvOperatorHandler.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvOperatorHandler.scala index 822538ca6f..5cf43e04a7 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvOperatorHandler.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvOperatorHandler.scala @@ -34,7 +34,12 @@ import org.lfdecentralizedtrust.splice.http.{ } import org.lfdecentralizedtrust.splice.scan.admin.api.client.ScanConnection import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig -import org.lfdecentralizedtrust.splice.store.{ActiveVotesStore, AppStore, AppStoreWithIngestion} +import org.lfdecentralizedtrust.splice.store.{ + ActiveVotesStore, + AppStore, + AppStoreWithIngestion, + VoteResultsFilters, +} import org.lfdecentralizedtrust.splice.sv.cometbft.CometBftClient import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig import org.lfdecentralizedtrust.splice.sv.store.{SvDsoStore, SvSvStore} @@ -57,6 +62,7 @@ class HttpSvOperatorHandler( override protected val timeouts: ProcessingTimeout, protected val loggerFactory: NamedLoggerFactory, upgradesConfig: UpgradesConfig, + participantAdminConnection: ParticipantAdminConnection, )(implicit ec: ExecutionContextExecutor, protected val tracer: Tracer, @@ -131,11 +137,13 @@ class HttpSvOperatorHandler( for { scanConnection <- scanConnectionF (voteResults, nextPageToken) <- scanConnection.listVoteRequestResults( - body.actionName, - body.accepted, - body.requester, - body.effectiveFrom, - body.effectiveTo, + VoteResultsFilters( + body.actionName, + body.accepted, + requester = body.requester, + effectiveFrom = body.effectiveFrom, + effectiveTo = body.effectiveTo, + ), body.limit.intValue, body.pageToken, ) @@ -162,6 +170,34 @@ class HttpSvOperatorHandler( } } + override def countVoteRequestResults( + respond: r0.CountVoteRequestResultsResponse.type + )( + body: definitions.CountVoteResultsRequest + )( + extracted: ActAsKnownUserRequest + ): Future[r0.CountVoteRequestResultsResponse] = { + implicit val ActAsKnownUserRequest(traceContext) = extracted + withSpan(s"$workflowId.countVoteRequestResults") { _ => _ => + for { + scanConnection <- scanConnectionF + count <- scanConnection.countVoteRequestResults( + VoteResultsFilters( + body.actionName, + body.accepted, + requester = body.requester, + effectiveFrom = body.effectiveFrom, + effectiveTo = body.effectiveTo, + ) + ) + } yield { + r0.CountVoteRequestResultsResponse.OK( + definitions.CountVoteResultsResponse(count) + ) + } + } + } + override def getPreviousSvRewardWeight( respond: r0.GetPreviousSvRewardWeightResponse.type )( @@ -562,6 +598,21 @@ class HttpSvOperatorHandler( } } + override def cancelLogicalSynchronizerUpgrade( + respond: r0.CancelLogicalSynchronizerUpgradeResponse.type + )()( + extracted: ActAsKnownUserRequest + ): Future[r0.CancelLogicalSynchronizerUpgradeResponse] = { + implicit val ActAsKnownUserRequest(traceContext) = extracted + withSpan(s"$workflowId.cancelLogicalSynchronizerUpgrade") { _ => _ => + for { + decentralizedSynchronizer <- dsoStore.getDsoRules().map(_.domain) + _ <- participantAdminConnection + .removeLsuAnnouncement(decentralizedSynchronizer) + } yield r0.CancelLogicalSynchronizerUpgradeResponseOK + } + } + private def withClientOrNotFound[T]( notFound: definitions.ErrorResponse => T )(call: CometBftClient => Future[T])(implicit tc: TraceContext) = diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvPublicHandler.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvPublicHandler.scala index 9605f13d3e..997a2df1ea 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvPublicHandler.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvPublicHandler.scala @@ -880,6 +880,7 @@ class HttpSvPublicHandler( ), deduplicationOffset = offset, ) + .recoveringAcceptedDuplicates() .yieldUnit() } .value diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/DsoDelegateBasedAutomationService.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/DsoDelegateBasedAutomationService.scala index 81df31bf99..dc9052433e 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/DsoDelegateBasedAutomationService.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/DsoDelegateBasedAutomationService.scala @@ -13,11 +13,10 @@ import org.lfdecentralizedtrust.splice.automation.AutomationServiceCompanion.{ } import org.lfdecentralizedtrust.splice.automation.{AutomationService, AutomationServiceCompanion} import org.lfdecentralizedtrust.splice.environment.RetryProvider -import org.lfdecentralizedtrust.splice.store.DomainTimeSynchronization +import org.lfdecentralizedtrust.splice.store.{DomainTimeSynchronization, IgnoredPartiesStore} import org.lfdecentralizedtrust.splice.scan.admin.api.client.{BftScanConnection, ScanConnection} import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.* import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.ExpiredAmuletAllocationTrigger -import org.lfdecentralizedtrust.splice.sv.store.IgnoredPartiesStore import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig import scala.concurrent.{ExecutionContextExecutor, Future} @@ -46,7 +45,7 @@ class DsoDelegateBasedAutomationService( : org.lfdecentralizedtrust.splice.sv.automation.DsoDelegateBasedAutomationService.type = DsoDelegateBasedAutomationService - val expiredAmuletIgnoredPartiesStore = new IgnoredPartiesStore( + val unavailablePartiesStore = new IgnoredPartiesStore( triggerContext.config.ignoredPartyIds ) @@ -69,7 +68,7 @@ class DsoDelegateBasedAutomationService( config, triggerContext, svTaskContext, - expiredAmuletIgnoredPartiesStore, + unavailablePartiesStore, ) ) registerTrigger( @@ -77,7 +76,7 @@ class DsoDelegateBasedAutomationService( config, triggerContext, svTaskContext, - expiredAmuletIgnoredPartiesStore, + unavailablePartiesStore, ) ) registerTrigger( @@ -86,7 +85,7 @@ class DsoDelegateBasedAutomationService( clock, triggerContext, svTaskContext, - expiredAmuletIgnoredPartiesStore, + unavailablePartiesStore, ) ) registerTrigger( @@ -95,7 +94,7 @@ class DsoDelegateBasedAutomationService( clock, triggerContext, svTaskContext, - expiredAmuletIgnoredPartiesStore, + unavailablePartiesStore, ) ) registerTrigger( @@ -104,7 +103,7 @@ class DsoDelegateBasedAutomationService( clock, triggerContext, svTaskContext, - expiredAmuletIgnoredPartiesStore, + unavailablePartiesStore, ) ) registerTrigger(new ExpiredSvOnboardingRequestTrigger(triggerContext, svTaskContext)) @@ -119,15 +118,36 @@ class DsoDelegateBasedAutomationService( new ExpireRewardCouponsTrigger( triggerContext, svTaskContext, - expiredAmuletIgnoredPartiesStore, + unavailablePartiesStore, config, ) ) registerTrigger(new AnsSubscriptionRenewalPaymentTrigger(triggerContext, svTaskContext)) - registerTrigger(new ExpiredAnsEntryTrigger(triggerContext, svTaskContext)) - registerTrigger(new ExpireTransferPreapprovalsTrigger(triggerContext, svTaskContext)) - registerTrigger(new ExpiredAnsSubscriptionTrigger(triggerContext, svTaskContext)) + registerTrigger( + new ExpiredAnsEntryTrigger( + triggerContext, + svTaskContext, + config, + unavailablePartiesStore, + ) + ) + registerTrigger( + new ExpireTransferPreapprovalsTrigger( + triggerContext, + svTaskContext, + config, + unavailablePartiesStore, + ) + ) + registerTrigger( + new ExpiredAnsSubscriptionTrigger( + triggerContext, + svTaskContext, + config, + unavailablePartiesStore, + ) + ) registerTrigger(new TerminatedSubscriptionTrigger(triggerContext, svTaskContext)) registerTrigger(new MergeSvRewardStateContractsTrigger(triggerContext, svTaskContext)) @@ -143,7 +163,7 @@ class DsoDelegateBasedAutomationService( triggerContext, svTaskContext, config, - expiredAmuletIgnoredPartiesStore, + unavailablePartiesStore, ) ) @@ -180,6 +200,7 @@ class DsoDelegateBasedAutomationService( config, triggerContext, svTaskContext, + unavailablePartiesStore, ) ) @@ -214,7 +235,7 @@ class DsoDelegateBasedAutomationService( object DsoDelegateBasedAutomationService extends AutomationServiceCompanion { // defined because the service isn't available immediately in sv app state, - // but created later by the restart trigger + // but created later override protected[this] def expectedTriggerClasses: Seq[TriggerClass] = Seq( aTrigger[AdvanceOpenMiningRoundTrigger], aTrigger[UpdateExternalPartyConfigStateTrigger], diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/PeriodicTopologySnapshotTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/PeriodicTopologySnapshotTrigger.scala index 038683cf44..91a336982c 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/PeriodicTopologySnapshotTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/PeriodicTopologySnapshotTrigger.scala @@ -8,7 +8,6 @@ import com.digitalasset.canton.SynchronizerAlias import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.time.Clock import com.digitalasset.canton.topology.PhysicalSynchronizerId -import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId import com.digitalasset.canton.tracing.TraceContext import io.circe.Json import io.grpc.{Status, StatusRuntimeException} @@ -153,21 +152,12 @@ class PeriodicTopologySnapshotTrigger( }, logger, ) - // list a summary of the transactions state at the time of the snapshot to validate further imports - summary <- triggerContext.retryProvider.retry( - RetryFor.Automation, - "getTopologyTransactionsSummary", - "Get topology transactions summary", - sequencerAdminConnection.getTopologyTransactionsSummary( - TopologyStoreId.Synchronizer(physicalSynchronizerId.logical), - clock.now, - ), - logger, - ) // we create a single metadata file to store the amounts of the different transactions along the sequencerId - metadataMap = summary.map(e => (e._1.code, e._2.toString)) + - ("sequencerId" -> sequencerId.toProtoPrimitive) + - ("physicalSynchronizerId" -> physicalSynchronizerId.toProtoPrimitive) + metadataMap = + Map( + "sequencerId" -> sequencerId.toProtoPrimitive, + "physicalSynchronizerId" -> physicalSynchronizerId.toProtoPrimitive, + ) metadataJson = Json .obj(metadataMap.map { case (k, v) => k -> Json.fromString(v) }.toSeq*) .spaces2 diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/SvDsoAutomationService.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/SvDsoAutomationService.scala index b67dafbebb..81ba6fdd61 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/SvDsoAutomationService.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/SvDsoAutomationService.scala @@ -7,7 +7,12 @@ import cats.implicits.catsSyntaxOptionId import com.daml.grpc.adapter.ExecutionSequencerFactory import com.digitalasset.canton.SynchronizerAlias import com.digitalasset.canton.config.ClientConfig -import com.digitalasset.canton.lifecycle.{AsyncCloseable, AsyncOrSyncCloseable} +import com.digitalasset.canton.lifecycle.{ + AsyncCloseable, + AsyncOrSyncCloseable, + LifeCycle, + SyncCloseable, +} import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.time.{Clock, WallClock} import com.digitalasset.canton.topology.SynchronizerId @@ -47,6 +52,7 @@ import org.lfdecentralizedtrust.splice.sv.automation.SvDsoAutomationService.{ LocalSequencerClientContext, } import org.lfdecentralizedtrust.splice.sv.automation.confirmation.* +import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.SvTaskBasedTrigger import org.lfdecentralizedtrust.splice.sv.automation.singlesv.* import org.lfdecentralizedtrust.splice.sv.automation.singlesv.offboarding.{ SvOffboardingMediatorTrigger, @@ -210,7 +216,10 @@ class SvDsoAutomationService( } override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = - super.closeAsync() ++ + SyncCloseable( + "dso-delegate-based-automation", + LifeCycle.close(dsoDelegateBasedAutomation)(logger), + ) +: (super.closeAsync() ++ // super.closeAsync() waits for all triggers to close, so we do not need to worry // about synchronization when closing the scan connections here. ownScanConnectionF @@ -235,7 +244,7 @@ class SvDsoAutomationService( timeouts.shutdownNetwork, ) ) - .toList + .toList) private val packageVettingService = new PackageVettingLookupService( config.packageVettingCache, @@ -251,19 +260,24 @@ class SvDsoAutomationService( // notice the absence of UpdateHistory: the history for the dso party is duplicate with Scan - private[splice] val restartDsoDelegateBasedAutomationTrigger = - new RestartDsoDelegateBasedAutomationTrigger( - triggerContext, - domainTimeSync, - dsoStore, - connection, + private[splice] val dsoDelegateBasedAutomation = + new DsoDelegateBasedAutomationService( clock, + domainTimeSync, config, - retryProvider, - packageVersionSupport, - packageVettingService, + SvTaskBasedTrigger.Context( + dsoStore, + connection, + config.delegatelessAutomationExpectedTaskDuration, + config.delegatelessAutomationExpiredRewardCouponBatchSize, + config.delegatelessAutomationExpiredRewardCouponNumBatches, + packageVersionSupport, + packageVettingService, + ), () => getOrCreateOwnScanConnection(), () => getOrCreatePeerScanConnection(), + retryProvider, + loggerFactory, ) // required for triggers that must run in sim time as well @@ -428,7 +442,7 @@ class SvDsoAutomationService( synchronizerNodeService.nodes.successor.foreach(registerTriggersForSynchronizers) } - def registerLsuTriggers() = { + def registerLsuTriggers(): Unit = { synchronizerNodeService.nodes.successor match { case Some(successorSynchronizerNode) => registerTrigger( @@ -538,7 +552,7 @@ class SvDsoAutomationService( ) ) - registerTrigger(restartDsoDelegateBasedAutomationTrigger) + dsoDelegateBasedAutomation.start() registerTrigger( new AnsSubscriptionInitialPaymentTrigger( @@ -597,6 +611,12 @@ class SvDsoAutomationService( dsoStore, ) ) + registerTrigger( + new VoteRequestMetricsTrigger( + triggerContext, + dsoStore, + ) + ) registerTrigger( new RewardMetricsTrigger( triggerContext, @@ -737,7 +757,6 @@ object SvDsoAutomationService extends AutomationServiceCompanion { aTrigger[CalculateRewardsTrigger], aTrigger[CalculateRewardsDryRunTrigger], aTrigger[ConfirmationMismatchReportTrigger], - aTrigger[RestartDsoDelegateBasedAutomationTrigger], aTrigger[AnsSubscriptionInitialPaymentTrigger], aTrigger[SvPackageVettingTrigger], aTrigger[SvOffboardingPartyToParticipantProposalTrigger], @@ -765,6 +784,7 @@ object SvDsoAutomationService extends AutomationServiceCompanion { aTrigger[FollowAmuletConversionRateFeedTrigger], aTrigger[CopyVotesTrigger], aTrigger[AmuletPriceMetricsTrigger], + aTrigger[VoteRequestMetricsTrigger], aTrigger[RewardMetricsTrigger], aTrigger[CreateBootstrapExternalPartyConfigStateInstructionTrigger], aTrigger[LsuTrigger], diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/VoteRequestMetricsTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/VoteRequestMetricsTrigger.scala new file mode 100644 index 0000000000..c7f761dcb2 --- /dev/null +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/VoteRequestMetricsTrigger.scala @@ -0,0 +1,113 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.sv.automation + +import com.daml.metrics.api.MetricHandle.{Gauge, LabeledMetricsFactory} +import com.daml.metrics.api.MetricQualification.Saturation +import com.daml.metrics.api.{MetricInfo, MetricName, MetricsContext} +import com.digitalasset.canton.lifecycle.{AsyncOrSyncCloseable, SyncCloseable} +import com.digitalasset.canton.tracing.TraceContext +import io.opentelemetry.api.trace.Tracer +import org.apache.pekko.stream.Materializer +import org.lfdecentralizedtrust.splice.automation.{PollingTrigger, TriggerContext} +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.VoteRequest +import org.lfdecentralizedtrust.splice.environment.SpliceMetrics +import org.lfdecentralizedtrust.splice.store.PageLimit +import org.lfdecentralizedtrust.splice.sv.automation.VoteRequestMetricsTrigger.{ + VoteRequestMetrics, + countByState, +} +import org.lfdecentralizedtrust.splice.sv.store.SvDsoStore +import org.lfdecentralizedtrust.splice.util.Contract + +import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.CollectionConverters.* + +class VoteRequestMetricsTrigger( + override protected val context: TriggerContext, + dsoStore: SvDsoStore, +)(implicit + override val ec: ExecutionContext, + override val tracer: Tracer, + val mat: Materializer, +) extends PollingTrigger { + + private val voteRequestMetrics = new VoteRequestMetrics(context.metricsFactory) + private val svParty = dsoStore.key.svParty.toProtoPrimitive + + override def performWorkIfAvailable()(implicit traceContext: TraceContext): Future[Boolean] = + for { + voteRequests <- dsoStore.listVoteRequests() + readyToCloseContracts <- dsoStore.listVoteRequestsReadyToBeClosed( + context.clock.now, + PageLimit.Max, + )(traceContext) + } yield { + val counts = countByState( + voteRequests, + readyToCloseContracts.map(_.contractId).toSet, + svParty, + ) + voteRequestMetrics.actionNeeded.updateValue(counts.actionNeeded) + voteRequestMetrics.inProgress.updateValue(counts.inProgress) + voteRequestMetrics.readyToClose.updateValue(counts.readyToClose) + false + } + + override def closeAsync(): Seq[AsyncOrSyncCloseable] = super + .closeAsync() + .appended(SyncCloseable("vote request metrics", voteRequestMetrics.close())) +} + +object VoteRequestMetricsTrigger { + + case class VoteRequestCounts(actionNeeded: Long, inProgress: Long, readyToClose: Long) + + def countByState( + voteRequests: Seq[Contract[VoteRequest.ContractId, VoteRequest]], + readyToCloseCids: Set[VoteRequest.ContractId], + svParty: String, + ): VoteRequestCounts = { + val (readyToClose, open) = + voteRequests.partition(request => readyToCloseCids.contains(request.contractId)) + val (inProgress, actionNeeded) = + open.partition(_.payload.votes.values().asScala.exists(_.sv == svParty)) + VoteRequestCounts( + actionNeeded = actionNeeded.size.toLong, + inProgress = inProgress.size.toLong, + readyToClose = readyToClose.size.toLong, + ) + } + + case class VoteRequestMetrics(metricsFactory: LabeledMetricsFactory) extends AutoCloseable { + + private val name: MetricName = + SpliceMetrics.MetricsPrefix :+ "sv_vote_requests" :+ "active" + + private def stateGauge(state: String): Gauge[Long] = + metricsFactory.gauge( + MetricInfo( + name, + "The number of active vote requests, split by their state relative to this SV", + Saturation, + "The state label is one of: " + + "action_needed (the request is open for voting and this SV has not voted yet), " + + "in_progress (the request is open for voting and this SV has voted), " + + "ready_to_close (the request fulfills the conditions for the closing automation " + + "to close it, e.g. its voting deadline has passed).", + ), + 0L, + )(MetricsContext.Empty.withExtraLabels("state" -> state)) + + val actionNeeded: Gauge[Long] = stateGauge("action_needed") + val inProgress: Gauge[Long] = stateGauge("in_progress") + val readyToClose: Gauge[Long] = stateGauge("ready_to_close") + + override def close(): Unit = { + actionNeeded.close() + inProgress.close() + readyToClose.close() + } + } +} diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/CalculateRewardsTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/CalculateRewardsTrigger.scala index 6682db3c36..dcb628e3be 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/CalculateRewardsTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/CalculateRewardsTrigger.scala @@ -159,10 +159,12 @@ abstract class CalculateRewardsTriggerBase( rewardMetrics.calculateRewardsRootHashBftReads.mark() for { bftScan <- getPeerBftScanConnection() - response <- bftScan.getRewardAccountingRootHash(round) + response <- bftScan.getRewardAccountingRootHashWithScanUris(round) } yield response match { - case RewardAccountingRootHashOk(ok) => - logger.info(s"Obtained the root-hash for round $round via BFT read.") + case (RewardAccountingRootHashOk(ok), scanUris) => + logger.info( + s"Obtained the root-hash for round $round via BFT read from scans: ${scanUris.mkString(", ")}." + ) new Hash(ok.rootHash) case _ => rootHashUnavailable("could not obtain root-hash via BFT read.") } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/SummarizingMiningRoundTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/SummarizingMiningRoundTrigger.scala index 98ab463033..5fdbaca608 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/SummarizingMiningRoundTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/SummarizingMiningRoundTrigger.scala @@ -238,10 +238,13 @@ class SummarizingMiningRoundTrigger( miningRoundMetrics.summarizingRoundTotalsBftReads.mark() for { bftScan <- bftScanConnectionF() - response <- bftScan.getRewardAccountingActivityTotals(round) + response <- bftScan.getRewardAccountingActivityTotalsWithScanUris(round) } yield response match { - case RewardAccountingActivityTotalsOk(ok) => - logger.info(s"Obtained the reward accounting totals for round $round via BFT read.") + case (RewardAccountingActivityTotalsOk(ok), scanUris) => + logger.info( + s"Obtained the reward accounting totals for round $round via BFT read from scans: ${scanUris + .mkString(", ")}." + ) ok case _ => totalsUnavailable("could not obtain reward accounting totals via BFT read.") } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireRewardCouponV2Trigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireRewardCouponV2Trigger.scala index 533bdab3fe..837e02f79f 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireRewardCouponV2Trigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireRewardCouponV2Trigger.scala @@ -6,24 +6,26 @@ package org.lfdecentralizedtrust.splice.sv.automation.delegatebased import org.lfdecentralizedtrust.splice.automation.* import org.lfdecentralizedtrust.splice.codegen.java.splice import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.AmuletRules_ClaimExpiredRewardsV2 -import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import scala.concurrent.{ExecutionContext, Future} -import ExpireRewardCouponV2Trigger.* +import ExpireRewardCouponV2Trigger.{Coupon, CouponCid, Task, getStakeholders} import org.lfdecentralizedtrust.splice.environment.{DarResources, PackageIdResolver} import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* class ExpireRewardCouponV2Trigger( - svConfig: SvAppBackendConfig, + override protected val svConfig: SvAppBackendConfig, override protected val context: TriggerContext, override protected val svTaskContext: SvTaskBasedTrigger.Context, + override protected val ignoredPartiesStore: IgnoredPartiesStore, )(implicit override val ec: ExecutionContext, mat: Materializer, @@ -35,13 +37,23 @@ class ExpireRewardCouponV2Trigger( splice.amulet.RewardCouponV2.COMPANION, svTaskContext.vettingLookupService, PackageIdResolver.Package.SpliceAmulet, - payload => (payload.dso +: observerParties(payload)).map(PartyId.tryFromProtoPrimitive(_)), + getStakeholders, ) - with SvTaskBasedTrigger[Task] { + with SvTaskBasedTrigger[Task] + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit tc: TraceContext + ): Future[TaskOutcome] = + completeUnlessAmuletVersionIgnored( + task.work.vettedVersion.toString, + task.work.stakeholders, + ignoreUnresponsiveParties = true, + )(completeExpiryTaskAsDsoDelegate(task, controller)) + + private def completeExpiryTaskAsDsoDelegate(task: Task, controller: String)(implicit + tc: TraceContext ): Future[TaskOutcome] = { val expiredCoupons = task.work.expiredContracts // The batch is already split by the amulet version so we skip the whole batch. @@ -54,8 +66,11 @@ class ExpireRewardCouponV2Trigger( ) } else { val cids = expiredCoupons.map(_.contractId).asJava - val expiryObservers = - expiredCoupons.flatMap(c => observerParties(c.payload)).distinct.sorted + val expiryInformees = (task.work.stakeholders - store.key.dsoParty) + .map(_.toProtoPrimitive) + .toSeq + .distinct + .sorted for { dsoRules <- store.getDsoRules() amuletRules <- store.getAmuletRules() @@ -64,7 +79,7 @@ class ExpireRewardCouponV2Trigger( amuletRules.contractId, new AmuletRules_ClaimExpiredRewardsV2( cids, - expiryObservers.asJava, + expiryInformees.asJava, ), controller, ) @@ -82,9 +97,10 @@ class ExpireRewardCouponV2Trigger( } yield TaskSuccess(s"archived ${expiredCoupons.size} expired reward coupons v2") } } + } -object ExpireRewardCouponV2Trigger { +object ExpireRewardCouponV2Trigger extends ContractStakeholders[splice.amulet.RewardCouponV2] { private type CouponCid = splice.amulet.RewardCouponV2.ContractId private type Coupon = splice.amulet.RewardCouponV2 @@ -93,7 +109,10 @@ object ExpireRewardCouponV2Trigger { BatchedMultiDomainExpiredContractTrigger.Batch[CouponCid, Coupon] ] - private def observerParties(coupon: Coupon): Seq[String] = - if (coupon.providerIsObserver) coupon.provider +: coupon.beneficiary.toScala.toList - else Seq.empty + override def informees(payload: splice.amulet.RewardCouponV2): Seq[String] = if ( + payload.providerIsObserver + ) payload.provider +: payload.beneficiary.toScala.toList + else Seq.empty + + override def dso(payload: splice.amulet.RewardCouponV2): String = payload.dso } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireRewardCouponsTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireRewardCouponsTrigger.scala index 26d51e6196..4248c82c54 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireRewardCouponsTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireRewardCouponsTrigger.scala @@ -16,18 +16,18 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.{ } import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.DsoRules import org.lfdecentralizedtrust.splice.environment.PackageIdResolver.Package.SpliceAmulet -import org.lfdecentralizedtrust.splice.sv.store.{ExpiredRewardCouponsBatch, IgnoredPartiesStore} +import org.lfdecentralizedtrust.splice.sv.store.ExpiredRewardCouponsBatch import org.lfdecentralizedtrust.splice.util.{AssignedContract, Contract} import org.lfdecentralizedtrust.splice.util.PrettyInstances.* import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.MonadUtil import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority -import org.lfdecentralizedtrust.splice.store.PageLimit +import org.lfdecentralizedtrust.splice.store.{IgnoredPartiesStore, PageLimit} +import org.lfdecentralizedtrust.splice.codegen.java.splice import java.util.Optional import scala.concurrent.{ExecutionContext, Future} @@ -35,7 +35,12 @@ import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* import scala.util.Random import ExpireRewardCouponsTrigger.Task +import org.lfdecentralizedtrust.splice.codegen.java.splice.validatorlicense.{ + ValidatorFaucetCoupon, + ValidatorLivenessActivityRecord, +} import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders class ExpireRewardCouponsTrigger( override protected val context: TriggerContext, @@ -48,7 +53,7 @@ class ExpireRewardCouponsTrigger( tracer: Tracer, ) extends PollingParallelTaskExecutionTrigger[Task] with SvTaskBasedTrigger[Task] - with IgnoredAmuletVersionGuard { + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore override protected def retrieveTasks()(implicit @@ -89,33 +94,27 @@ class ExpireRewardCouponsTrigger( SpliceAmulet, batch.validatorCoupons, svTaskContext.delegatelessAutomationExpiredRewardCouponBatchSize, - )(c => Seq(c.payload.dso, c.payload.user).map(PartyId.tryFromProtoPrimitive(_))) + )(c => ValidatorCoupons.getStakeholders(c.payload)) appCoupons <- svTaskContext.vettingLookupService.splitBatch( SpliceAmulet, batch.appCoupons, svTaskContext.delegatelessAutomationExpiredRewardCouponBatchSize, - )(c => - (Seq(c.payload.dso, c.payload.provider) ++ c.payload.beneficiary.toScala.toList) - .map(PartyId.tryFromProtoPrimitive(_)) - ) + )(c => AppRewardCoupons.getStakeholders(c.payload)) svRewardCoupons <- svTaskContext.vettingLookupService.splitBatch( SpliceAmulet, batch.svRewardCoupons, svTaskContext.delegatelessAutomationExpiredRewardCouponBatchSize, - )(c => - Seq(c.payload.dso, c.payload.sv, c.payload.beneficiary) - .map(PartyId.tryFromProtoPrimitive(_)) - ) + )(c => SvRewardCoupons.getStakeholders(c.payload)) validatorFaucets <- svTaskContext.vettingLookupService.splitBatch( SpliceAmulet, batch.validatorFaucets, svTaskContext.delegatelessAutomationExpiredRewardCouponBatchSize, - )(c => Seq(c.payload.dso, c.payload.validator).map(PartyId.tryFromProtoPrimitive(_))) + )(c => ValidatorFaucetCoupons.getStakeholders(c.payload)) validatorLivenessActivityRecords <- svTaskContext.vettingLookupService.splitBatch( SpliceAmulet, batch.validatorLivenessActivityRecords, svTaskContext.delegatelessAutomationExpiredRewardCouponBatchSize, - )(c => Seq(c.payload.dso, c.payload.validator).map(PartyId.tryFromProtoPrimitive(_))) + )(c => ValidatorLivenessActivityRecords.getStakeholders(c.payload)) } yield { val emptyBatch = ExpiredRewardCouponsBatch( closedRoundCid = batch.closedRoundCid, @@ -157,19 +156,17 @@ class ExpireRewardCouponsTrigger( override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit tc: TraceContext ): Future[TaskOutcome] = { - val informees = - (task.batch.validatorCoupons.map(_.payload.user) ++ task.batch.appCoupons.flatMap(c => - Seq(c.payload.provider) ++ c.payload.beneficiary.toScala - ) ++ - task.batch.svRewardCoupons.map(_.payload.beneficiary) ++ task.batch.validatorFaucets.map( - _.payload.validator - ) ++ task.batch.validatorLivenessActivityRecords.map(_.payload.validator)) - .map(PartyId.tryFromProtoPrimitive(_)) - .toSet - completeWithIgnoredAmuletVersionCheck( + val informees = ValidatorCoupons.getInformeesFromContracts(task.batch.validatorCoupons) ++ + AppRewardCoupons.getInformeesFromContracts(task.batch.appCoupons) ++ + SvRewardCoupons.getInformeesFromContracts(task.batch.svRewardCoupons) ++ + ValidatorFaucetCoupons.getInformeesFromContracts(task.batch.validatorFaucets) ++ + ValidatorLivenessActivityRecords.getInformeesFromContracts( + task.batch.validatorLivenessActivityRecords + ) + completeUnlessAmuletVersionIgnored( task.vettedAmuletVersion.toString, informees, - enableUnresponsivePartiesAutoIgnore = true, + ignoreUnresponsiveParties = true, )(completeExpiryTaskAsDsoDelegate(task, controller)) } @@ -318,3 +315,35 @@ object ExpireRewardCouponsTrigger { ) } } + +object ValidatorCoupons extends ContractStakeholders[splice.amulet.ValidatorRewardCoupon] { + override def informees(payload: splice.amulet.ValidatorRewardCoupon): Seq[String] = Seq( + payload.user + ) + override def dso(payload: splice.amulet.ValidatorRewardCoupon): String = payload.dso +} + +object AppRewardCoupons extends ContractStakeholders[splice.amulet.AppRewardCoupon] { + override def informees(payload: splice.amulet.AppRewardCoupon): Seq[String] = + Seq(payload.provider) ++ payload.beneficiary.toScala.toList + override def dso(payload: splice.amulet.AppRewardCoupon): String = payload.dso +} + +object SvRewardCoupons extends ContractStakeholders[splice.amulet.SvRewardCoupon] { + override def informees(payload: splice.amulet.SvRewardCoupon): Seq[String] = + Seq(payload.sv, payload.beneficiary) + override def dso(payload: splice.amulet.SvRewardCoupon): String = payload.dso +} + +object ValidatorFaucetCoupons extends ContractStakeholders[ValidatorFaucetCoupon] { + override def informees(payload: ValidatorFaucetCoupon): Seq[String] = Seq(payload.validator) + override def dso(payload: ValidatorFaucetCoupon): String = payload.dso +} + +object ValidatorLivenessActivityRecords + extends ContractStakeholders[ValidatorLivenessActivityRecord] { + override def informees(payload: ValidatorLivenessActivityRecord): Seq[String] = Seq( + payload.validator + ) + override def dso(payload: ValidatorLivenessActivityRecord): String = payload.dso +} diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireTransferPreapprovalsTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireTransferPreapprovalsTrigger.scala index 7071d559e4..ea84f04c59 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireTransferPreapprovalsTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpireTransferPreapprovalsTrigger.scala @@ -10,13 +10,19 @@ import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority +import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import java.util.Optional import scala.concurrent.{ExecutionContext, Future} +import ExpireTransferPreapprovalsTrigger.{Task, getStakeholders} +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore class ExpireTransferPreapprovalsTrigger( override protected val context: TriggerContext, override protected val svTaskContext: SvTaskBasedTrigger.Context, + override protected val svConfig: SvAppBackendConfig, + override protected val ignoredPartiesStore: IgnoredPartiesStore, )(implicit override val ec: ExecutionContext, mat: Materializer, @@ -26,30 +32,36 @@ class ExpireTransferPreapprovalsTrigger( TransferPreapproval, ]( svTaskContext.dsoStore.multiDomainAcsStore, - svTaskContext.dsoStore.listExpiredTransferPreapprovals, + svTaskContext.dsoStore.listExpiredTransferPreapprovals(Some(ignoredPartiesStore)), TransferPreapproval.COMPANION, ) with SvTaskBasedTrigger[ScheduledTaskTrigger.ReadyTask[AssignedContract[ TransferPreapproval.ContractId, TransferPreapproval, - ]]] { - type Task = ScheduledTaskTrigger.ReadyTask[ - AssignedContract[ - TransferPreapproval.ContractId, - TransferPreapproval, - ] - ] + ]]] + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore - override def completeTaskAsDsoDelegate(co: Task, controller: String)(implicit + override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit tc: TraceContext ): Future[TaskOutcome] = + completeWithVettedAmuletVersion( + getStakeholders(task.work.payload).toSet, + Seq(task.work.contractId.contractId), + )(completeExpiryTaskAsDsoDelegate(task, controller)) + + private def completeExpiryTaskAsDsoDelegate( + task: Task, + controller: String, + )(implicit + tc: TraceContext + ): Future[TaskOutcome] = { for { dsoRules <- store.getDsoRules() cmd = dsoRules.exercise( _.exerciseDsoRules_ExpireTransferPreapproval( - co.work.contractId, + task.work.contractId, Optional.of(controller), ) ) @@ -59,6 +71,21 @@ class ExpireTransferPreapprovalsTrigger( .noDedup .yieldUnit() } yield TaskSuccess( - s"Archived expired TransferPreapproval with contractId ${co.work.contractId}" + s"Archived expired TransferPreapproval with contractId ${task.work.contractId}" ) + } +} + +object ExpireTransferPreapprovalsTrigger extends ContractStakeholders[TransferPreapproval] { + type Task = ScheduledTaskTrigger.ReadyTask[ + AssignedContract[ + TransferPreapproval.ContractId, + TransferPreapproval, + ] + ] + + override def informees(payload: TransferPreapproval): Seq[String] = + Seq(payload.provider, payload.receiver) + + override def dso(payload: TransferPreapproval): String = payload.dso } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletAllocationTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletAllocationTrigger.scala index 88f8009773..038caae7ae 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletAllocationTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletAllocationTrigger.scala @@ -12,12 +12,13 @@ import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import scala.concurrent.{ExecutionContext, Future} -import ExpiredAmuletAllocationTrigger.* +import ExpiredAmuletAllocationTrigger.{Task, getStakeholders} import com.digitalasset.canton.util.MonadUtil import org.lfdecentralizedtrust.splice.environment.PackageIdResolver import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig -import org.lfdecentralizedtrust.splice.sv.store.IgnoredPartiesStore +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import scala.jdk.CollectionConverters.* @@ -41,50 +42,38 @@ class ExpiredAmuletAllocationTrigger( splice.amuletallocation.AmuletAllocation.COMPANION, svTaskContext.vettingLookupService, PackageIdResolver.Package.SpliceAmulet, - allocation => - Seq( - allocation.allocation.transferLeg.sender, - allocation.allocation.settlement.executor, - svTaskContext.dsoStore.key.dsoParty.partyId.toProtoPrimitive, - ).map(PartyId.tryFromProtoPrimitive), + getStakeholders, ) with SvTaskBasedTrigger[Task] - with IgnoredAmuletVersionGuard { + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit tc: TraceContext ): Future[TaskOutcome] = { - val informees = task.work.expiredContracts.flatMap { contract => - val sender = PartyId.tryFromProtoPrimitive(contract.payload.allocation.transferLeg.sender) - val executor = PartyId.tryFromProtoPrimitive(contract.payload.allocation.settlement.executor) - Seq(sender, executor) - }.toSet - completeWithIgnoredAmuletVersionCheck( + completeUnlessAmuletVersionIgnored( task.work.vettedVersion.toString, - informees, - enableUnresponsivePartiesAutoIgnore = true, - )(completeExpiryTaskAsDsoDelegate(task, controller, informees)) + task.work.stakeholders, + ignoreUnresponsiveParties = true, + )(completeExpiryTaskAsDsoDelegate(task, controller)) } private def completeExpiryTaskAsDsoDelegate( task: Task, controller: String, - informees: Set[PartyId], )(implicit tc: TraceContext): Future[TaskOutcome] = { - val allParties = informees + store.key.dsoParty - + val stakeholders = task.work.stakeholders for { packageSupport <- svTaskContext.packageVersionSupport.supportsExpireAmuletAllocations( - allParties.toSeq, + stakeholders.toSeq, Seq(store.key.dsoParty), clock.now, ) res <- if (!packageSupport.supported) { logger.info( - s"Skipping expiry of ${task.work.expiredContracts.size} allocations because not all parties have vetted the required Amulet package version. Parties: ${allParties + s"Skipping expiry of ${task.work.expiredContracts.size} allocations because not all parties have vetted the required Amulet package version. Parties: ${stakeholders .mkString(", ")}" ) Future.successful( @@ -180,7 +169,8 @@ class ExpiredAmuletAllocationTrigger( } } -object ExpiredAmuletAllocationTrigger { +object ExpiredAmuletAllocationTrigger + extends ContractStakeholders[splice.amuletallocation.AmuletAllocation] { type Task = ScheduledTaskTrigger.ReadyTask[ BatchedMultiDomainExpiredContractTrigger.Batch[ @@ -188,4 +178,10 @@ object ExpiredAmuletAllocationTrigger { splice.amuletallocation.AmuletAllocation, ] ] + + override def informees(payload: splice.amuletallocation.AmuletAllocation): Seq[String] = + Seq(payload.allocation.transferLeg.sender, payload.allocation.settlement.executor) + + override def dso(payload: splice.amuletallocation.AmuletAllocation): String = + payload.allocation.transferLeg.instrumentId.admin } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletAllocationV2Trigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletAllocationV2Trigger.scala index 43f68ffe8a..ed3b73c8a9 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletAllocationV2Trigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletAllocationV2Trigger.scala @@ -4,7 +4,6 @@ package org.lfdecentralizedtrust.splice.sv.automation.delegatebased import com.digitalasset.canton.time.Clock -import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer @@ -13,14 +12,15 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice import org.lfdecentralizedtrust.splice.environment.PackageIdResolver import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig -import org.lfdecentralizedtrust.splice.sv.store.IgnoredPartiesStore import org.lfdecentralizedtrust.splice.util.{ChoiceContextWithDisclosures, TokenStandardMetadata} +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* import cats.implicits.* import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.metadatav1.anyvalue.AV_Bool +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore class ExpiredAmuletAllocationV2Trigger( override protected val svConfig: SvAppBackendConfig, @@ -42,10 +42,10 @@ class ExpiredAmuletAllocationV2Trigger( splice.amuletallocationv2.AmuletAllocationV2.COMPANION, svTaskContext.vettingLookupService, PackageIdResolver.Package.SpliceAmulet, - ExpiredAmuletAllocationV2Trigger.allocationV2Stakeholders, + ExpiredAmuletAllocationV2Trigger.getStakeholders, ) with SvTaskBasedTrigger[ExpiredAmuletAllocationV2Trigger.Task] - with IgnoredAmuletVersionGuard { + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore @@ -55,32 +55,28 @@ class ExpiredAmuletAllocationV2Trigger( )(implicit tc: TraceContext ): Future[TaskOutcome] = { - val expiredStakeholders = task.work.expiredContracts.flatMap { contract => - ExpiredAmuletAllocationV2Trigger.allocationV2Stakeholders(contract.payload) - }.toSet - completeWithIgnoredAmuletVersionCheck( + completeUnlessAmuletVersionIgnored( task.work.vettedVersion.toString, - expiredStakeholders, - enableUnresponsivePartiesAutoIgnore = true, - )(completeExpiryTaskAsDsoDelegate(task, controller, expiredStakeholders)) + task.work.stakeholders, + ignoreUnresponsiveParties = true, + )(completeExpiryTaskAsDsoDelegate(task, controller)) } private def completeExpiryTaskAsDsoDelegate( task: ExpiredAmuletAllocationV2Trigger.Task, controller: String, - informees: Set[PartyId], )(implicit tc: TraceContext): Future[TaskOutcome] = { - val allParties = informees + store.key.dsoParty - + val stakeholders = task.work.stakeholders + val informees = stakeholders - store.key.dsoParty for { packageSupport <- svTaskContext.packageVersionSupport.supportsAmuletAllocationV2( - allParties.toSeq, + stakeholders.toSeq, clock.now, ) res <- if (!packageSupport.supported) { logger.info( - s"Skipping expiry of ${task.work.expiredContracts.size} allocations because not all parties have vetted the required Amulet package version. Parties: ${allParties + s"Skipping expiry of ${task.work.expiredContracts.size} allocations because not all parties have vetted the required Amulet package version. Parties: ${stakeholders .mkString(", ")}" ) Future.successful( @@ -158,7 +154,8 @@ class ExpiredAmuletAllocationV2Trigger( } -object ExpiredAmuletAllocationV2Trigger { +object ExpiredAmuletAllocationV2Trigger + extends ContractStakeholders[splice.amuletallocationv2.AmuletAllocationV2] { type Task = ScheduledTaskTrigger.ReadyTask[ BatchedMultiDomainExpiredContractTrigger.Batch[ @@ -167,9 +164,9 @@ object ExpiredAmuletAllocationV2Trigger { ] ] - private def allocationV2Stakeholders(allocation: splice.amuletallocationv2.AmuletAllocationV2) = - (Seq( - allocation.allocation.admin - ) ++ allocation.allocation.authorizer.owner.toScala.toList ++ allocation.settlement.executors.asScala) - .map(PartyId.tryFromProtoPrimitive) + override def informees(payload: splice.amuletallocationv2.AmuletAllocationV2): Seq[String] = + payload.allocation.authorizer.owner.toScala.toList ++ payload.settlement.executors.asScala + + override def dso(payload: splice.amuletallocationv2.AmuletAllocationV2): String = + payload.allocation.admin } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletTransferInstructionTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletTransferInstructionTrigger.scala index c90afbdcd0..d50197a9cd 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletTransferInstructionTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletTransferInstructionTrigger.scala @@ -12,12 +12,13 @@ import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import scala.concurrent.{ExecutionContext, Future} -import ExpiredAmuletTransferInstructionTrigger.* +import ExpiredAmuletTransferInstructionTrigger.{Task, getStakeholders} import com.digitalasset.canton.util.MonadUtil import org.lfdecentralizedtrust.splice.environment.PackageIdResolver import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig -import org.lfdecentralizedtrust.splice.sv.store.IgnoredPartiesStore +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import scala.jdk.CollectionConverters.* @@ -41,50 +42,38 @@ class ExpiredAmuletTransferInstructionTrigger( splice.amulettransferinstruction.AmuletTransferInstruction.COMPANION, svTaskContext.vettingLookupService, PackageIdResolver.Package.SpliceAmulet, - instruction => - Seq( - instruction.transfer.sender, - instruction.transfer.receiver, - svTaskContext.dsoStore.key.dsoParty.partyId.toProtoPrimitive, - ).map(PartyId.tryFromProtoPrimitive), + getStakeholders, ) with SvTaskBasedTrigger[Task] - with IgnoredAmuletVersionGuard { + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit tc: TraceContext ): Future[TaskOutcome] = { - val informees = task.work.expiredContracts - .map(c => PartyId.tryFromProtoPrimitive(c.payload.transfer.sender)) - .toSet ++ task.work.expiredContracts - .map(c => PartyId.tryFromProtoPrimitive(c.payload.transfer.receiver)) - .toSet - completeWithIgnoredAmuletVersionCheck( + completeUnlessAmuletVersionIgnored( task.work.vettedVersion.toString, - informees, - enableUnresponsivePartiesAutoIgnore = true, - )(completeExpiryTaskAsDsoDelegate(task, controller, informees)) + task.work.stakeholders, + ignoreUnresponsiveParties = true, + )(completeExpiryTaskAsDsoDelegate(task, controller)) } private def completeExpiryTaskAsDsoDelegate( task: Task, controller: String, - informees: Set[PartyId], )(implicit tc: TraceContext): Future[TaskOutcome] = { - val allParties = informees + store.key.dsoParty - + val stakeholders = task.work.stakeholders for { packageSupport <- svTaskContext.packageVersionSupport.supportsExpireTransferInstructions( - allParties.toSeq, + stakeholders.toSeq, Seq(store.key.dsoParty), clock.now, ) res <- if (!packageSupport.supported) { logger.info( - s"Skipping expiry of ${task.work.expiredContracts.size} transfer instructions because not all parties have vetted the required Amulet package version. Parties: ${allParties + s"Skipping expiry of ${task.work.expiredContracts.size} transfer instructions because not all parties have vetted the required Amulet package version. Parties: ${stakeholders .mkString(", ")}" ) Future.successful( @@ -165,7 +154,8 @@ class ExpiredAmuletTransferInstructionTrigger( } } -object ExpiredAmuletTransferInstructionTrigger { +object ExpiredAmuletTransferInstructionTrigger + extends ContractStakeholders[splice.amulettransferinstruction.AmuletTransferInstruction] { type Task = ScheduledTaskTrigger.ReadyTask[ BatchedMultiDomainExpiredContractTrigger.Batch[ @@ -173,4 +163,12 @@ object ExpiredAmuletTransferInstructionTrigger { splice.amulettransferinstruction.AmuletTransferInstruction, ] ] + + override def informees( + payload: splice.amulettransferinstruction.AmuletTransferInstruction + ): Seq[String] = Seq(payload.transfer.sender, payload.transfer.receiver) + + override def dso( + payload: splice.amulettransferinstruction.AmuletTransferInstruction + ): String = payload.transfer.instrumentId.admin } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletTrigger.scala index a62847bd17..32a1f131de 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAmuletTrigger.scala @@ -5,17 +5,17 @@ package org.lfdecentralizedtrust.splice.sv.automation.delegatebased import org.lfdecentralizedtrust.splice.automation.* import org.lfdecentralizedtrust.splice.codegen.java.splice -import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import scala.concurrent.{ExecutionContext, Future} -import ExpiredAmuletTrigger.* import org.lfdecentralizedtrust.splice.environment.PackageIdResolver import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig -import org.lfdecentralizedtrust.splice.sv.store.IgnoredPartiesStore +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders +import ExpiredAmuletTrigger.{Task, getStakeholders} +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore import java.util.Optional import scala.jdk.CollectionConverters.* @@ -40,36 +40,33 @@ class ExpiredAmuletTrigger( splice.amulet.Amulet.COMPANION, svTaskContext.vettingLookupService, PackageIdResolver.Package.SpliceAmulet, - c => Seq(c.dso, c.owner).map(PartyId.tryFromProtoPrimitive(_)), + getStakeholders, ) with SvTaskBasedTrigger[Task] - with IgnoredAmuletVersionGuard { + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit tc: TraceContext ): Future[TaskOutcome] = { - val informees = - task.work.expiredContracts.map(c => PartyId.tryFromProtoPrimitive(c.payload.owner)).toSet - completeWithIgnoredAmuletVersionCheck( + completeUnlessAmuletVersionIgnored( task.work.vettedVersion.toString, - informees, - enableUnresponsivePartiesAutoIgnore = true, - )(completeExpiryTaskAsDsoDelegate(task, controller, informees)) + task.work.stakeholders, + ignoreUnresponsiveParties = true, + )(completeExpiryTaskAsDsoDelegate(task, controller)) } private def completeExpiryTaskAsDsoDelegate( task: Task, controller: String, - informees: Set[PartyId], )(implicit tc: TraceContext ): Future[TaskOutcome] = { - val allParties = informees + store.key.dsoParty + val stakeholders = task.work.stakeholders for { dsoRules <- store.getDsoRules() supports24hSubmissionDelay <- svTaskContext.packageVersionSupport.supports24hSubmissionDelay( - allParties.toSeq, + stakeholders.toSeq, Seq(store.key.dsoParty), context.clock.now, ) @@ -131,7 +128,7 @@ class ExpiredAmuletTrigger( } } -object ExpiredAmuletTrigger { +object ExpiredAmuletTrigger extends ContractStakeholders[splice.amulet.Amulet] { type Task = ScheduledTaskTrigger.ReadyTask[ BatchedMultiDomainExpiredContractTrigger.Batch[ @@ -139,4 +136,8 @@ object ExpiredAmuletTrigger { splice.amulet.Amulet, ] ] + + override def informees(payload: splice.amulet.Amulet): Seq[String] = Seq(payload.owner) + + override def dso(payload: splice.amulet.Amulet): String = payload.dso } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAnsEntryTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAnsEntryTrigger.scala index 5b60065613..3cb66cc1c1 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAnsEntryTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAnsEntryTrigger.scala @@ -11,13 +11,19 @@ import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority +import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import java.util.Optional import scala.concurrent.{ExecutionContext, Future} +import ExpiredAnsEntryTrigger.{Task, getStakeholders} +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore class ExpiredAnsEntryTrigger( override protected val context: TriggerContext, override protected val svTaskContext: SvTaskBasedTrigger.Context, + override protected val svConfig: SvAppBackendConfig, + override protected val ignoredPartiesStore: IgnoredPartiesStore, )(implicit override val ec: ExecutionContext, mat: Materializer, @@ -27,30 +33,36 @@ class ExpiredAnsEntryTrigger( splice.ans.AnsEntry, ]( svTaskContext.dsoStore.multiDomainAcsStore, - svTaskContext.dsoStore.listExpiredAnsEntries, + svTaskContext.dsoStore.listExpiredAnsEntries(Some(ignoredPartiesStore)), splice.ans.AnsEntry.COMPANION, ) with SvTaskBasedTrigger[ScheduledTaskTrigger.ReadyTask[AssignedContract[ splice.ans.AnsEntry.ContractId, splice.ans.AnsEntry, - ]]] { - type Task = ScheduledTaskTrigger.ReadyTask[ - AssignedContract[ - splice.ans.AnsEntry.ContractId, - splice.ans.AnsEntry, - ] - ] + ]]] + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore - override def completeTaskAsDsoDelegate(co: Task, controller: String)(implicit + override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit + tc: TraceContext + ): Future[TaskOutcome] = + completeWithVettedAmuletVersion( + getStakeholders(task.work.payload).toSet, + Seq(task.work.contractId.contractId), + )(completeExpiryTaskAsDsoDelegate(task, controller)) + + private def completeExpiryTaskAsDsoDelegate( + task: Task, + controller: String, + )(implicit tc: TraceContext ): Future[TaskOutcome] = for { dsoRules <- store.getDsoRules() cmd = dsoRules.exercise( _.exerciseDsoRules_ExpireAnsEntry( - co.work.contractId, + task.work.contractId, new AnsEntry_Expire(store.key.dsoParty.toProtoPrimitive), Optional.of(controller), ) @@ -62,3 +74,16 @@ class ExpiredAnsEntryTrigger( .yieldUnit() } yield TaskSuccess("archived expired ANS entry") } + +object ExpiredAnsEntryTrigger extends ContractStakeholders[splice.ans.AnsEntry] { + type Task = ScheduledTaskTrigger.ReadyTask[ + AssignedContract[ + splice.ans.AnsEntry.ContractId, + splice.ans.AnsEntry, + ] + ] + + override def informees(payload: splice.ans.AnsEntry): Seq[String] = Seq(payload.user) + + override def dso(payload: splice.ans.AnsEntry): String = payload.dso +} diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAnsSubscriptionTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAnsSubscriptionTrigger.scala index 8f8b673493..8b6e009a08 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAnsSubscriptionTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredAnsSubscriptionTrigger.scala @@ -16,32 +16,46 @@ import io.opentelemetry.api.trace.Tracer import org.lfdecentralizedtrust.splice.codegen.java.splice.ans as ansCodegen import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.subscriptions as subsCodegen import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.subscriptions.SubscriptionIdleState_ExpireSubscription -import org.lfdecentralizedtrust.splice.store.PageLimit +import org.lfdecentralizedtrust.splice.store.{IgnoredPartiesStore, PageLimit} +import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig import org.lfdecentralizedtrust.splice.sv.store.SvDsoStore +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import org.apache.pekko.stream.Materializer import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority import java.util.Optional import scala.concurrent.{ExecutionContext, Future} +import ExpiredAnsSubscriptionTrigger.{Task, getStakeholders} class ExpiredAnsSubscriptionTrigger( override protected val context: TriggerContext, override protected val svTaskContext: SvTaskBasedTrigger.Context, + override protected val svConfig: SvAppBackendConfig, + override protected val ignoredPartiesStore: IgnoredPartiesStore, )(implicit - ec: ExecutionContext, + override val ec: ExecutionContext, mat: Materializer, tracer: Tracer, ) extends ScheduledTaskTrigger[SvDsoStore.IdleAnsSubscription] - with SvTaskBasedTrigger[ScheduledTaskTrigger.ReadyTask[SvDsoStore.IdleAnsSubscription]] { + with SvTaskBasedTrigger[ScheduledTaskTrigger.ReadyTask[SvDsoStore.IdleAnsSubscription]] + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore override protected def listReadyTasks(now: CantonTimestamp, limit: Int)(implicit tc: TraceContext ): Future[Seq[SvDsoStore.IdleAnsSubscription]] = - store.listExpiredAnsSubscriptions(now, PageLimit.tryCreate(limit)) + store.listExpiredAnsSubscriptions(now, PageLimit.tryCreate(limit), Some(ignoredPartiesStore)) - override protected def completeTaskAsDsoDelegate( - task: ScheduledTaskTrigger.ReadyTask[SvDsoStore.IdleAnsSubscription], + override protected def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit + tc: TraceContext + ): Future[TaskOutcome] = + completeWithVettedAmuletVersion( + getStakeholders(task.work.state.payload).toSet, + Seq(task.work.state.contractId.contractId), + )(completeExpiryTaskAsDsoDelegate(task, controller)) + + private def completeExpiryTaskAsDsoDelegate( + task: Task, controller: String, )(implicit tc: TraceContext): Future[TaskOutcome] = for { dsoRules <- store.getDsoRules() @@ -67,7 +81,7 @@ class ExpiredAnsSubscriptionTrigger( } yield result override protected def isStaleTask( - task: ScheduledTaskTrigger.ReadyTask[SvDsoStore.IdleAnsSubscription] + task: Task )(implicit tc: TraceContext): Future[Boolean] = (for { _ <- OptionT( @@ -86,3 +100,18 @@ class ExpiredAnsSubscriptionTrigger( ) } yield ()).isEmpty } + +object ExpiredAnsSubscriptionTrigger + extends ContractStakeholders[subsCodegen.SubscriptionIdleState] { + type Task = ScheduledTaskTrigger.ReadyTask[SvDsoStore.IdleAnsSubscription] + + override def informees(payload: subsCodegen.SubscriptionIdleState): Seq[String] = + Seq( + payload.subscriptionData.sender, + payload.subscriptionData.receiver, + payload.subscriptionData.provider, + ) + + override def dso(payload: subsCodegen.SubscriptionIdleState): String = + payload.subscriptionData.dso +} diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredLockedAmuletTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredLockedAmuletTrigger.scala index 5affd08b9a..c01e110be4 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredLockedAmuletTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ExpiredLockedAmuletTrigger.scala @@ -6,16 +6,16 @@ package org.lfdecentralizedtrust.splice.sv.automation.delegatebased import org.lfdecentralizedtrust.splice.automation.* import org.lfdecentralizedtrust.splice.codegen.java.splice import org.lfdecentralizedtrust.splice.environment.PackageIdResolver -import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer import scala.concurrent.{ExecutionContext, Future} -import ExpiredLockedAmuletTrigger.* +import ExpiredLockedAmuletTrigger.{Task, getStakeholders} import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig -import org.lfdecentralizedtrust.splice.sv.store.IgnoredPartiesStore +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import java.util.Optional import scala.jdk.CollectionConverters.* @@ -40,41 +40,31 @@ class ExpiredLockedAmuletTrigger( splice.amulet.LockedAmulet.COMPANION, svTaskContext.vettingLookupService, PackageIdResolver.Package.SpliceAmulet, - c => - (Seq(c.amulet.dso, c.amulet.owner) ++ c.lock.holders.asScala) - .map(PartyId.tryFromProtoPrimitive(_)), + getStakeholders, ) with SvTaskBasedTrigger[Task] - with IgnoredAmuletVersionGuard { + with IgnoredUnavailablePartiesGuard { private val store = svTaskContext.dsoStore override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit tc: TraceContext ): Future[TaskOutcome] = { - val informees = task.work.expiredContracts - .flatMap(c => - PartyId.tryFromProtoPrimitive( - c.payload.amulet.owner - ) +: c.payload.lock.holders.asScala.toSeq.map(PartyId.tryFromProtoPrimitive(_)) - ) - .toSet - completeWithIgnoredAmuletVersionCheck( + completeUnlessAmuletVersionIgnored( task.work.vettedVersion.toString, - informees, - enableUnresponsivePartiesAutoIgnore = true, - )(completeExpiryTaskAsDsoDelegate(task, controller, informees)) + task.work.stakeholders, + ignoreUnresponsiveParties = true, + )(completeExpiryTaskAsDsoDelegate(task, controller)) } private def completeExpiryTaskAsDsoDelegate( task: Task, controller: String, - informees: Set[PartyId], )(implicit tc: TraceContext): Future[TaskOutcome] = { - val allParties = informees + store.key.dsoParty + val stakeholders = task.work.stakeholders for { dsoRules <- store.getDsoRules() supports24hSubmissionDelay <- svTaskContext.packageVersionSupport.supports24hSubmissionDelay( - allParties.toSeq, + stakeholders.toSeq, Seq(store.key.dsoParty), context.clock.now, ) @@ -136,7 +126,7 @@ class ExpiredLockedAmuletTrigger( } } -object ExpiredLockedAmuletTrigger { +object ExpiredLockedAmuletTrigger extends ContractStakeholders[splice.amulet.LockedAmulet] { type Task = ScheduledTaskTrigger.ReadyTask[ BatchedMultiDomainExpiredContractTrigger.Batch[ @@ -144,4 +134,9 @@ object ExpiredLockedAmuletTrigger { splice.amulet.LockedAmulet, ] ] + + override def informees(payload: splice.amulet.LockedAmulet): Seq[String] = + Seq(payload.amulet.owner) ++ payload.lock.holders.asScala + + override def dso(payload: splice.amulet.LockedAmulet): String = payload.amulet.dso } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/FeaturedAppActivityMarkerTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/FeaturedAppActivityMarkerTrigger.scala index 12f7940b6f..ddcbfa2bff 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/FeaturedAppActivityMarkerTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/FeaturedAppActivityMarkerTrigger.scala @@ -25,10 +25,17 @@ import com.digitalasset.canton.util.ShowUtil.* import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* -import FeaturedAppActivityMarkerTrigger.{CrossVersionBatch, Task} +import FeaturedAppActivityMarkerTrigger.{ + CrossVersionBatch, + Task, + getInformeesFromContracts, + getStakeholders, +} +import com.digitalasset.canton.discard.Implicits.DiscardOps import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig -import org.lfdecentralizedtrust.splice.sv.store.IgnoredPartiesStore +import org.lfdecentralizedtrust.splice.sv.util.ContractStakeholders import java.util.Optional import scala.util.Random @@ -45,7 +52,7 @@ class FeaturedAppActivityMarkerTrigger( // This is a polling trigger as we usually expect to be able to batch together the conversion ) extends PollingParallelTaskExecutionTrigger[Task] with SvTaskBasedTrigger[Task] - with IgnoredAmuletVersionGuard { + with IgnoredUnavailablePartiesGuard { private val rng: Random = new Random() @@ -83,32 +90,35 @@ class FeaturedAppActivityMarkerTrigger( def splitBatchByVettingState( batch: CrossVersionBatch - )(implicit tc: TraceContext): Future[Seq[Task]] = + )(implicit tc: TraceContext): Future[Seq[Task]] = { svTaskContext.vettingLookupService .splitBatch( PackageIdResolver.Package.SpliceAmulet, batch.markers, batchSize, - )(c => - Seq(c.payload.provider, c.payload.beneficiary, c.payload.dso) - .map(PartyId.tryFromProtoPrimitive(_)) - ) + )(c => getStakeholders(c.payload)) .map { _.toSeq.flatMap { case (Some(version), markerBatches) => - markerBatches.map( + markerBatches.map { markers => Task( batch.retrievalKind, - _, + markers, version, + getInformeesFromContracts(markers), ) - ) + } case (None, markers) => - logger.warn(show"No vetted amulet version for $markers") + ignorePartiesWithoutVettedAmulet( + getInformeesFromContracts(markers.flatten), + markers.flatten.map(_.contractId.contractId), + logAsWarning = true, + ).discard Seq.empty } } + } private def retrieveBatchesBySvIndex( dsoRules: dsorules.DsoRules @@ -195,34 +205,29 @@ class FeaturedAppActivityMarkerTrigger( override def completeTaskAsDsoDelegate(task: Task, controller: String)(implicit tc: TraceContext ): Future[TaskOutcome] = { - val informees = task.markers - .flatMap(m => Seq(m.payload.provider, m.payload.beneficiary)) - .map(PartyId.tryFromProtoPrimitive) - .toSet - completeWithIgnoredAmuletVersionCheck( + completeUnlessAmuletVersionIgnored( task.vettedAmuletVersion.toString, - informees, + task.informees, // ignoring a party would mean their featured app activity markers do not get converted into rewards - enableUnresponsivePartiesAutoIgnore = false, - )(completeExpiryTaskAsDsoDelegate(task, controller, informees)) + ignoreUnresponsiveParties = false, + )(completeExpiryTaskAsDsoDelegate(task, controller)) } private def completeExpiryTaskAsDsoDelegate( task: Task, controller: String, - informees: Set[PartyId], )(implicit tc: TraceContext): Future[TaskOutcome] = { for { dsoRules <- store.getDsoRules() amuletRules <- store.getAmuletRules() now = context.clock.now openMiningRound <- store.getLatestUsableOpenMiningRound(now) - allParties = informees + PartyId.tryFromProtoPrimitive(dsoRules.payload.dso) + stakeholders = task.informees + store.key.dsoParty supportsConvertFeaturedAppActivityMarkerObservers <- if (svConfig.convertFeaturedAppActivityMarkerObservers) { svTaskContext.packageVersionSupport .supportsConvertFeaturedAppActivityMarkerObservers( - allParties.toSeq, + stakeholders.toSeq, context.clock.now, ) .map(_.supported) @@ -240,7 +245,7 @@ class FeaturedAppActivityMarkerTrigger( Option .when( supportsConvertFeaturedAppActivityMarkerObservers - )(allParties.toSeq.map(_.toProtoPrimitive).asJava) + )(stakeholders.toSeq.map(_.toProtoPrimitive).asJava) .toJava, ), Optional.of(controller), @@ -270,7 +275,8 @@ class FeaturedAppActivityMarkerTrigger( } yield markers.exists(_.isEmpty) } -object FeaturedAppActivityMarkerTrigger { +object FeaturedAppActivityMarkerTrigger + extends ContractStakeholders[amulet.FeaturedAppActivityMarker] { final case class CrossVersionBatch( retrievalKind: String, markers: Seq[ @@ -291,6 +297,7 @@ object FeaturedAppActivityMarkerTrigger { Contract[amulet.FeaturedAppActivityMarker.ContractId, amulet.FeaturedAppActivityMarker] ], vettedAmuletVersion: PackageVersion, + informees: Set[PartyId], ) extends PrettyPrinting { override def pretty: Pretty[this.type] = prettyOfClass( @@ -298,6 +305,12 @@ object FeaturedAppActivityMarkerTrigger { param("numMarkers", _.markers.size), param("vettedAmuletVersion", _.vettedAmuletVersion), param("markerCids", _.markers.map(_.contractId.contractId.unquoted)), + param("informees", _.informees), ) } + + override def informees(payload: amulet.FeaturedAppActivityMarker): Seq[String] = + Seq(payload.provider, payload.beneficiary) + + override def dso(payload: amulet.FeaturedAppActivityMarker): String = payload.dso } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredAmuletVersionGuard.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredAmuletVersionGuard.scala deleted file mode 100644 index 3e7c3ede52..0000000000 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredAmuletVersionGuard.scala +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package org.lfdecentralizedtrust.splice.sv.automation.delegatebased - -import com.digitalasset.base.error.utils.ErrorDetails -import com.digitalasset.canton.topology.PartyId -import io.grpc.StatusRuntimeException -import io.grpc.protobuf.StatusProto -import org.lfdecentralizedtrust.splice.automation.{TaskOutcome, TaskSuccess} -import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig -import org.lfdecentralizedtrust.splice.sv.store.IgnoredPartiesStore -import org.lfdecentralizedtrust.splice.util.UnresponsiveParties - -import scala.concurrent.{ExecutionContext, Future} - -trait IgnoredAmuletVersionGuard { - protected def svConfig: SvAppBackendConfig - protected def ignoredPartiesStore: IgnoredPartiesStore - protected def svTaskContext: SvTaskBasedTrigger.Context - - protected def completeWithIgnoredAmuletVersionCheck( - vettedVersion: String, - expiredOwners: Set[PartyId], - enableUnresponsivePartiesAutoIgnore: Boolean, - )( - fallback: => Future[TaskOutcome] - )(implicit ec: ExecutionContext): Future[TaskOutcome] = { - if ( - svConfig.allIgnoredAmuletVersions.contains(vettedVersion) && - svConfig.parameters.enabledFeatures.ignorePartyIdWithIgnoredAmulet - ) { - ignoredPartiesStore.addAll(expiredOwners) - Future.successful( - TaskSuccess( - s"Skipped batch with ignored version $vettedVersion: added ${expiredOwners.size} parties to ignore list: $expiredOwners" - ) - ) - } else { - val enableNaiveUnresponsivePartiesAutoIgnore = - svConfig.parameters.enabledFeatures.naiveUnresponsivePartiesAutoIgnore && enableUnresponsivePartiesAutoIgnore - fallback.recoverWith { - case ex: StatusRuntimeException if enableNaiveUnresponsivePartiesAutoIgnore => - extractUnresponsiveParties(ex) match { - case parties if parties.nonEmpty => - val partiesToIgnore = parties - svTaskContext.dsoStore.key.dsoParty.partyId - ignoredPartiesStore.addAll(partiesToIgnore) - Future.successful( - TaskSuccess( - s"Batch failed due to unresponsive parties, added ${partiesToIgnore.size} to ignore list: $partiesToIgnore" - ) - ) - case _ => Future.failed(ex) - } - } - } - } - - private def extractUnresponsiveParties(ex: StatusRuntimeException): Set[PartyId] = { - val statusProto = StatusProto.fromThrowable(ex) - val errorDetails = ErrorDetails.from(statusProto) - errorDetails - .collectFirst { case UnresponsiveParties(parties) => parties } - .getOrElse(Set.empty) - } - -} diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredUnavailablePartiesGuard.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredUnavailablePartiesGuard.scala new file mode 100644 index 0000000000..b6f001e9b0 --- /dev/null +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredUnavailablePartiesGuard.scala @@ -0,0 +1,110 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.sv.automation.delegatebased + +import com.digitalasset.base.error.utils.ErrorDetails +import com.digitalasset.canton.logging.NamedLogging +import com.digitalasset.canton.topology.PartyId +import com.digitalasset.canton.tracing.TraceContext +import io.grpc.StatusRuntimeException +import io.grpc.protobuf.StatusProto +import org.lfdecentralizedtrust.splice.automation.{TaskOutcome, TaskSuccess} +import org.lfdecentralizedtrust.splice.store.IgnoredPartiesStore +import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig +import org.lfdecentralizedtrust.splice.util.UnresponsiveParties +import org.lfdecentralizedtrust.splice.environment.PackageIdResolver +import scala.concurrent.{ExecutionContext, Future} + +trait IgnoredUnavailablePartiesGuard extends NamedLogging { + protected def svConfig: SvAppBackendConfig + protected def ignoredPartiesStore: IgnoredPartiesStore + protected def svTaskContext: SvTaskBasedTrigger.Context + + protected def completeUnlessAmuletVersionIgnored( + vettedVersion: String, + stakeholders: Set[PartyId], + ignoreUnresponsiveParties: Boolean, + )(task: => Future[TaskOutcome])(implicit ec: ExecutionContext): Future[TaskOutcome] = + if ( + svConfig.allIgnoredAmuletVersions.contains(vettedVersion) && + svConfig.parameters.enabledFeatures.ignorePartyIdWithIgnoredAmulet + ) { + val toIgnore = withoutDsoParty(stakeholders) + ignoredPartiesStore.addAll(toIgnore) + Future.successful( + TaskSuccess( + s"Skipped batch with ignored version $vettedVersion: added ${toIgnore.size} parties to ignore list: $toIgnore" + ) + ) + } else { + task.recoverWith(recoverUnresponsiveParties(ignoreUnresponsiveParties)) + } + + protected def completeWithVettedAmuletVersion( + stakeholders: Set[PartyId], + contractIds: Seq[String], + ignoreUnresponsiveParties: Boolean = true, + )(task: => Future[TaskOutcome])(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[TaskOutcome] = + svTaskContext.vettingLookupService + .lookupVettingState(stakeholders.toSeq, PackageIdResolver.Package.SpliceAmulet) + .flatMap { + case Some(vettedVersion) => + completeUnlessAmuletVersionIgnored( + vettedVersion.toString, + stakeholders, + ignoreUnresponsiveParties, + )(task) + case None => + Future.successful( + TaskSuccess(ignorePartiesWithoutVettedAmulet(stakeholders, contractIds)) + ) + } + + protected def ignorePartiesWithoutVettedAmulet( + informees: Set[PartyId], + contractIds: Seq[String], + logAsWarning: Boolean = false, + )(implicit tc: TraceContext): String = { + val toIgnore = withoutDsoParty(informees) + ignoredPartiesStore.addAll(toIgnore) + val msg = + s"No vetted Amulet version for $contractIds; ignoring ${toIgnore.size} parties: $toIgnore" + if (logAsWarning) logger.warn(msg) + msg + } + + private def recoverUnresponsiveParties( + enabled: Boolean + ): PartialFunction[Throwable, Future[TaskOutcome]] = { + case ex: StatusRuntimeException + if enabled && svConfig.parameters.enabledFeatures.naiveUnresponsivePartiesAutoIgnore => + val toIgnore = withoutDsoParty(extractUnresponsiveParties(ex)) + if (toIgnore.isEmpty) { + Future.failed(ex) + } else { + ignoredPartiesStore.addAll(toIgnore) + Future.successful( + TaskSuccess( + s"Batch failed due to unresponsive parties, added ${toIgnore.size} to ignore list: $toIgnore" + ) + ) + } + } + + // never ignore the DSO party itself: it is a stakeholder on every DSO contract + private def withoutDsoParty(parties: Set[PartyId]): Set[PartyId] = + parties - svTaskContext.dsoStore.key.dsoParty + + private def extractUnresponsiveParties(ex: StatusRuntimeException): Set[PartyId] = { + val statusProto = StatusProto.fromThrowable(ex) + val errorDetails = ErrorDetails.from(statusProto) + errorDetails + .collectFirst { case UnresponsiveParties(parties) => parties } + .getOrElse(Set.empty) + } + +} diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ProcessRewardsTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ProcessRewardsTrigger.scala index 67cb4bd29e..67131b8551 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ProcessRewardsTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/ProcessRewardsTrigger.scala @@ -20,6 +20,8 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.rewardaccounti BatchOfBatches, BatchOfMintingAllowances, } +import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.AmuletRules +import org.lfdecentralizedtrust.splice.environment.PackageIdResolver import org.lfdecentralizedtrust.splice.http.v0.definitions.{ GetRewardAccountingBatchResponse, RewardAccountingMintingAllowance, @@ -27,12 +29,13 @@ import org.lfdecentralizedtrust.splice.http.v0.definitions.{ import org.lfdecentralizedtrust.splice.scan.admin.api.client.{BftScanConnection, ScanConnection} import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority import org.lfdecentralizedtrust.splice.store.PageLimit -import org.lfdecentralizedtrust.splice.util.AssignedContract +import org.lfdecentralizedtrust.splice.util.{AmuletConfigSchedule, AssignedContract, Contract} import org.lfdecentralizedtrust.splice.util.PrettyInstances.* import com.daml.metrics.api.{MetricInfo, MetricName, MetricsContext} import com.daml.metrics.api.MetricsContext.Implicits.empty import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.daml.lf.language.Ast import io.grpc.Status import io.opentelemetry.api.trace.Tracer import org.lfdecentralizedtrust.splice.codegen.java.da.set.types.Set as DamlSet @@ -82,11 +85,16 @@ private[delegatebased] abstract class ProcessRewardsTriggerBase( val batchHash = processRewards.payload.batchHash.value val batchF = fetchBatch(round, batchHash) val dsoRulesF = store.getDsoRules() + val amuletRulesF = store.getAmuletRules() for { batch <- batchF dsoRules <- dsoRulesF + amuletRules <- amuletRulesF damlBatch = convertBatch(batch) - providersWithWrongVettingState <- determineProvidersWithWrongVettingState(batch) + providersWithWrongVettingState <- determineProvidersWithWrongVettingState( + batch, + amuletRules, + ) choiceArg = new ProcessRewardsV2_ProcessBatch( damlBatch, providersWithWrongVettingState, @@ -149,7 +157,8 @@ private[delegatebased] abstract class ProcessRewardsTriggerBase( } private def determineProvidersWithWrongVettingState( - batch: GetRewardAccountingBatchResponse + batch: GetRewardAccountingBatchResponse, + amuletRules: Contract[AmuletRules.ContractId, AmuletRules], )(implicit tc: TraceContext): Future[DamlSet[String]] = { val providers = batch match { case GetRewardAccountingBatchResponse.members.RewardAccountingBatchOfMintingAllowances( @@ -160,15 +169,42 @@ private[delegatebased] abstract class ProcessRewardsTriggerBase( Vector.empty[String] } val now = context.clock.now + + // Checking for vetting state on the active amulet version is conservative, + // but it ensures that we don't hit issues where the common version vetted by + // all providers in a batch is below the version where V2 was introduced. (see #6372) + // + // Being conservative here is OK, as this keeps our vetting state checking + // simple, while avoiding potential issues in the submission of ProcessBatch. + // And also because the UnhideRewardCouponV2Trigger would make the coupons + // visible based on the vetting state of each party. + // In practice we expect most providers to have vetted the active amulet version. + val activeAmuletVersionMetadata = Ast.PackageMetadata( + PackageIdResolver.Package.SpliceAmulet.packageName, + PackageIdResolver.readPackageVersion( + AmuletConfigSchedule(amuletRules).getConfigAsOf(now).packageConfig, + PackageIdResolver.Package.SpliceAmulet, + ), + None, + ) Future .traverse(providers) { provider => val partyId = PartyId.tryFromProtoPrimitive(provider) svTaskContext.packageVersionSupport - .supportsTrafficBasedAppRewards(Seq(partyId), now) - .map(support => provider -> support.supported) + .isPackageSupported( + Seq(PackageIdResolver.Package.SpliceAmulet -> Seq(store.key.dsoParty, partyId)), + now, + activeAmuletVersionMetadata, + ) + .map(supported => provider -> supported.supported) } .map { supportByProvider => val withWrongVettingState = supportByProvider.collect { case (provider, false) => provider } + if (withWrongVettingState.nonEmpty) { + logger.info( + s"Providers with wrong vetting state for batch: ${withWrongVettingState.mkString(", ")}" + ) + } damlSetOf(withWrongVettingState) } } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/SvTaskBasedTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/SvTaskBasedTrigger.scala index 54d6969fc5..0f42533995 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/SvTaskBasedTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/SvTaskBasedTrigger.scala @@ -46,19 +46,8 @@ trait SvTaskBasedTrigger[T <: PrettyPrinting] { )(implicit tc: TraceContext): Future[TaskOutcome] = { for { dsoRules <- store.getDsoRules() - sameEpoch = dsoRules.payload.epoch == svTaskContext.epoch svParty = store.key.svParty.toProtoPrimitive - result <- - if (sameEpoch) { - completeTaskAsAnySv(task, svParty, dsoRules) - } else { - // TODO(DACH-NY/canton-network-internal#495) Could this be busy-looping as well, if we are a polling trigger? - Future.successful( - TaskSuccess( - s"Skipping because current epoch ${dsoRules.payload.epoch} is not the same as trigger registration epoch ${svTaskContext.epoch}" - ) - ) - } + result <- completeTaskAsAnySv(task, svParty, dsoRules) } yield result } @@ -130,7 +119,6 @@ object SvTaskBasedTrigger { case class Context( dsoStore: SvDsoStore, connection: SpliceLedgerConnectionPriority => SpliceLedgerConnection, - epoch: Long, delegatelessAutomationExpectedTaskDuration: Long, delegatelessAutomationExpiredRewardCouponBatchSize: Int, delegatelessAutomationExpiredRewardCouponNumBatches: Int, diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/singlesv/ReconcileDynamicSynchronizerParametersTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/singlesv/ReconcileDynamicSynchronizerParametersTrigger.scala index f4a0cbc02a..bab6b0900c 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/singlesv/ReconcileDynamicSynchronizerParametersTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/singlesv/ReconcileDynamicSynchronizerParametersTrigger.scala @@ -196,6 +196,8 @@ class ReconcileDynamicSynchronizerParametersTrigger( maxBaseTrafficAccumulationDuration = PositiveFiniteDuration.tryOfSeconds( domainFeesConfig.baseRateTrafficLimits.burstWindow.microseconds / 1000_000 ), + setBalanceRequestSubmissionWindowSize = + PositiveFiniteDuration.fromConfig(config.setBalanceRequestSubmissionWindowSize), freeConfirmationResponses = enableFreeConfirmationResponses, ) }, diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/singlesv/RestartDsoDelegateBasedAutomationTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/singlesv/RestartDsoDelegateBasedAutomationTrigger.scala deleted file mode 100644 index 43903e171f..0000000000 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/singlesv/RestartDsoDelegateBasedAutomationTrigger.scala +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package org.lfdecentralizedtrust.splice.sv.automation.singlesv - -import org.apache.pekko.stream.Materializer -import org.lfdecentralizedtrust.splice.automation.{ - OnAssignedContractTrigger, - TaskOutcome, - TaskSuccess, - TriggerContext, -} -import org.lfdecentralizedtrust.splice.codegen.java.splice -import org.lfdecentralizedtrust.splice.environment.{ - PackageVersionSupport, - PackageVettingLookupService, - RetryProvider, - SpliceLedgerConnection, -} -import org.lfdecentralizedtrust.splice.scan.admin.api.client.{BftScanConnection, ScanConnection} -import org.lfdecentralizedtrust.splice.store.DomainTimeSynchronization -import org.lfdecentralizedtrust.splice.util.AssignedContract -import org.lfdecentralizedtrust.splice.sv.automation.DsoDelegateBasedAutomationService -import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.SvTaskBasedTrigger -import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig -import org.lfdecentralizedtrust.splice.sv.store.SvDsoStore -import com.digitalasset.canton.time.Clock -import com.digitalasset.canton.tracing.TraceContext -import io.opentelemetry.api.trace.Tracer - -import scala.concurrent.{ExecutionContextExecutor, Future, blocking} -import com.digitalasset.canton.lifecycle.RunOnClosing -import com.digitalasset.canton.lifecycle.AsyncOrSyncCloseable -import com.digitalasset.canton.lifecycle.SyncCloseable -import com.digitalasset.canton.lifecycle.LifeCycle -import com.digitalasset.canton.lifecycle.UnlessShutdown -import com.digitalasset.canton.util.ShowUtil.* -import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority - -class RestartDsoDelegateBasedAutomationTrigger( - override protected val context: TriggerContext, - domainTimeSync: DomainTimeSynchronization, - store: SvDsoStore, - connection: SpliceLedgerConnectionPriority => SpliceLedgerConnection, - clock: Clock, - config: SvAppBackendConfig, - appLevelRetryProvider: RetryProvider, - packageVersionSupport: PackageVersionSupport, - packageVettingService: PackageVettingLookupService, - getOwnScanConnection: () => Future[ScanConnection], - getPeerBftScanConnection: () => Future[BftScanConnection], -)(implicit - override val ec: ExecutionContextExecutor, - mat: Materializer, - tracer: Tracer, -) extends OnAssignedContractTrigger.Template[ - splice.dsorules.DsoRules.ContractId, - splice.dsorules.DsoRules, - ]( - store, - splice.dsorules.DsoRules.COMPANION, - ) { - type DsoRulesContract = AssignedContract[ - splice.dsorules.DsoRules.ContractId, - splice.dsorules.DsoRules, - ] - - @volatile - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var epochStateVar: Option[EpochState] = None - - private def closeRetryProvider(): Unit = - epochStateVar.foreach(epochState => LifeCycle.close(epochState.retryProvider)(logger)) - - private def closeService(): Unit = - epochStateVar.foreach(epochState => - LifeCycle.close(epochState.dsoDelegateBasedAutomation)(logger) - ) - - def epochState: Option[EpochState] = epochStateVar - - appLevelRetryProvider.runOnShutdownWithPriority_(new RunOnClosing { - override def name = s"set per-epoch retry provider as closing" - override def done = false - override def run()(implicit tc: TraceContext) = - epochStateVar.foreach(_.retryProvider.setAsClosing()) - }) - - appLevelRetryProvider.runOnOrAfterClose_(new RunOnClosing { - override def name = s"shutdown per-epoch retry provider" - override def done = false - override def run()(implicit tc: TraceContext) = closeRetryProvider() - })(TraceContext.empty) - - override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = - SyncCloseable("Per-epoch DsoDelegateBasedAutomationService", closeService()) +: super - .closeAsync() - - override def completeTask( - dsoRules: DsoRulesContract - )(implicit tc: TraceContext): Future[TaskOutcome] = Future { - blocking { - - mutex.exclusive { - val currentEpoch = dsoRules.payload.epoch - val lastKnownEpoch = epochStateVar.map(_.epoch) - - epochStateVar match { - case None => - logger.debug(s"Learned first epoch $currentEpoch") - restartAutomation(currentEpoch) - case Some(state) => - if (state.epoch != currentEpoch) { - logger.info( - show"Noticed an DsoRules epoch change (from ${state.epoch} to $currentEpoch)." - ) - logger.debug( - s"Restarting automation, as the epoch changed from ${state.epoch} to $currentEpoch" - ) - restartAutomation(currentEpoch) - } else { - TaskSuccess( - s"DsoRules changed, but the epoch stayed the same (epoch $lastKnownEpoch)" - ) - } - } - } - } - } - - private def restartAutomation(epoch: Long): TaskOutcome = { - val svTaskContext = - SvTaskBasedTrigger.Context( - store, - connection, - epoch, - config.delegatelessAutomationExpectedTaskDuration, - config.delegatelessAutomationExpiredRewardCouponBatchSize, - config.delegatelessAutomationExpiredRewardCouponNumBatches, - packageVersionSupport, - packageVettingService, - ) - - (if (appLevelRetryProvider.isClosing) { - // Avoid updating state when we are shutting down. - UnlessShutdown.AbortedDueToShutdown - } else { - closeRetryProvider() - closeService() - - val retryProvider = - RetryProvider( - loggerFactory, - timeouts, - appLevelRetryProvider.futureSupervisor, - context.metricsFactory, - ) - val dsoDelegateBasedAutomation = new DsoDelegateBasedAutomationService( - clock, - domainTimeSync, - config, - svTaskContext, - getOwnScanConnection, - getPeerBftScanConnection, - retryProvider, - loggerFactory, - ) - - epochStateVar = Some( - EpochState( - epoch, - dsoDelegateBasedAutomation, - retryProvider, - ) - ) - - // Shutdown might have been initiated concurrently with our change to the epochStateVar - if (appLevelRetryProvider.isClosing) { - logger.debug( - "Detected race between update of state and shutdown: closing down delegate-based automation again to be on the safe side." - )(TraceContext.empty) - closeRetryProvider() - closeService() - UnlessShutdown.AbortedDueToShutdown - } else { - // Delay startup of tasks until here. - // Even if right after the else, but before starting, it starts shutdown, that's okay, - // because the child RetryProvider is already scheduled for shutdown. - dsoDelegateBasedAutomation.start() - UnlessShutdown.Outcome(TaskSuccess(s"Started automation for epoch $epoch")) - } - }).onShutdown( - TaskSuccess( - s"Skipped or aborted restarting triggers for new epoch: $epoch, as we are shutting down." - ) - ) - } -} - -case class EpochState( - epoch: Long, - dsoDelegateBasedAutomation: DsoDelegateBasedAutomationService, - retryProvider: RetryProvider, -) {} diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala index 2c6661b4bc..5db47a2e22 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala @@ -454,6 +454,11 @@ case class SvAppBackendConfig( convertFeaturedAppActivityMarkerObservers: Boolean = true, // Whether to ensure that heuristic free confirmation responses get enabled on the synchronizer via the ReconcileDynamicSynchronizerConfigTrigger. enableFreeConfirmationResponses: Boolean = true, + // Target value for the setBalanceRequestSubmissionWindowSize traffic control parameter, + // applied to the synchronizer via the ReconcileDynamicSynchronizerParametersTrigger. + // The default matches Canton's current default as of 3.5.12 + setBalanceRequestSubmissionWindowSize: PositiveFiniteDuration = + PositiveFiniteDuration.ofMinutes(2), packageVettingCache: PackageVettingLookupService.CacheConfig = PackageVettingLookupService.CacheConfig(), useInternalSequencerApi: Boolean = false, @@ -461,9 +466,18 @@ case class SvAppBackendConfig( cantonBftSequencingParameters: Option[BftSequencingParameters] = Some( BftSequencingParameters( pbftViewChangeTimeout = PositiveFiniteDuration.ofSeconds(5), - segmentLength = SequencingParameters.DefaultSegmentLength.length, + // increased from default as epoch changes are synchronization points which can slow things down. + segmentLength = + PositiveLong.tryCreate(SequencingParameters.DefaultSegmentLength.length.value * 4), blacklistLeaderSelectionPolicyConfig = - SequencingParameters.DefaultLeaderSelectionPolicyConfig, + SequencingParameters.DefaultLeaderSelectionPolicyConfig.copy( + howLongToBlacklist = + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Exponential( + initialValue = 1L, + // Reduced by 4 to compensate for increased segmentLength. + maximumEpochBlacklisted = Some(250L / 4L), + ) + ), ) ), // Set to false to disable the DB-level exclusive lock that prevents two SV instances diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/lsu/LsuTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/lsu/LsuTrigger.scala index 966bd3b50a..5fbdd2c78e 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/lsu/LsuTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/lsu/LsuTrigger.scala @@ -7,7 +7,7 @@ import cats.implicits.{catsSyntaxOptionId, showInterpolator, toTraverseOps} import com.digitalasset.canton.admin.api.client.data.NodeStatus import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.topology.transaction.LsuAnnouncement +import com.digitalasset.canton.topology.transaction.{LsuAnnouncement, TopologyChangeOp} import com.digitalasset.canton.topology.PhysicalSynchronizerId import com.digitalasset.canton.tracing.TraceContext import io.opentelemetry.api.trace.Tracer @@ -211,7 +211,12 @@ class LsuTrigger( for { sequencerId <- currentSynchronizerNode.sequencerAdminConnection.getSequencerId hasNoSuccessor <- currentSynchronizerNode.sequencerAdminConnection - .lookupSequencerSuccessors(currentPsid.logical, sequencerId) + .lookupSequencerSuccessors( + announcement.successorSynchronizerId.logical, + sequencerId, + Some(announcement.successorSynchronizerId), + Some(TopologyChangeOp.Replace), + ) .map(_.isEmpty) participantPsid <- participantAdminConnection .getPhysicalSynchronizerId(currentPsid.logical) diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/SequencerBftPeerReconciler.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/SequencerBftPeerReconciler.scala index 019a1f175b..beb8cd4be7 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/SequencerBftPeerReconciler.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/SequencerBftPeerReconciler.scala @@ -78,11 +78,9 @@ abstract class SequencerBftPeerReconciler( configuredPeers <- sequencerAdminConnection .listConfiguredPeerEndpoints() peersToAdd = dsoSequencerEndpoints - .filterNot(endpoint => configuredPeers.exists(_.id == endpoint.id)) - candidatePeersToRemove = configuredPeers - .filterNot(peer => dsoSequencerEndpoints.exists(_.id == peer.id)) - peersToRemove <- computePeersToRemove( - candidatePeersToRemove, + .filterNot(endpoint => configuredPeers.map(_._1).exists(_.id == endpoint.id)) + peersToRemove = computePeersToRemove( + configuredPeers, dsoSequencersWithEndpoint, ) } yield { @@ -99,48 +97,39 @@ abstract class SequencerBftPeerReconciler( } yield result } - /** If all DSO sequencers have an associated peer endpoint advertised by scan, any configured peer - * that does not correspond to one of those endpoints is stale and safe to remove. - * - * Otherwise we cannot rely on scan alone (as some scans can be unavailable), so we cross-check the peer network status to find the - * sequencer id backing each candidate endpoint. Removal is only safe if that sequencer id is no - * longer part of the DSO sequencers, or if it is now associated with a different endpoint. If no - * sequencer id can be found for a candidate endpoint we keep it and log a warning. - */ private def computePeersToRemove( - candidatePeersToRemove: Seq[P2PEndpoint], + configuredPeers: Seq[(P2PEndpoint, Option[SequencerId])], dsoSequencersWithEndpoint: Seq[(SequencerId, Option[P2PEndpoint])], - )(implicit tc: TraceContext, ec: ExecutionContext): Future[Seq[P2PEndpoint]] = { - val allDsoSequencersHaveEndpoint = dsoSequencersWithEndpoint.forall { case (_, endpoint) => - endpoint.isDefined + ): Seq[P2PEndpoint] = { + val peersWithWrongSequencerId = configuredPeers.filter { + case (_, Some(sequencerId)) => + !dsoSequencersWithEndpoint.exists({ case (dsoSequencerId, _) => + sequencerId == dsoSequencerId + }) + case _ => false } - if (candidatePeersToRemove.isEmpty || allDsoSequencersHaveEndpoint) { - Future.successful(candidatePeersToRemove) - } else { - sequencerAdminConnection.listCurrentPeerEndpoints().map { networkStatus => - candidatePeersToRemove.filter { peer => - networkStatus.collectFirst { - case (Some(sequencerId), Some(endpointId)) if endpointId == peer.id => sequencerId - } match { - case Some(sequencerId) => - val sequencerNoLongerInDso = - !dsoSequencersWithEndpoint.exists { case (dsoSequencerId, _) => - dsoSequencerId == sequencerId - } - val sequencerMovedToDifferentEndpoint = - dsoSequencersWithEndpoint.exists { case (dsoSequencerId, endpoint) => - dsoSequencerId == sequencerId && endpoint.exists(_.id != peer.id) - } - sequencerNoLongerInDso || sequencerMovedToDifferentEndpoint - case None => - logger.warn( - s"Could not find a sequencer id for the configured peer endpoint ${peer.id} in the peer network status; not removing it to be safe." - ) - false - } - } - } + val peersWithChangedEndpoint = configuredPeers.filter { + case (peer, Some(sequencerId)) => + dsoSequencersWithEndpoint.exists({ case (dsoSequencerId, endpoint) => + sequencerId == dsoSequencerId && endpoint.exists(_.id != peer.id) + }) + case _ => false } + // we only remove connections for which we don't have a sequencer id when we have been able to query all scans to get connections. otherwise a temporary scan issue could result in us removing the peer. + val unknownPeers = + if (dsoSequencersWithEndpoint.forall { case (_, endpoint) => endpoint.isDefined }) { + configuredPeers.filter(peer => + !dsoSequencersWithEndpoint.exists { case (_, endpoint) => + endpoint.exists(_.id == peer._1.id) + } + ) + } else Seq.empty + + (peersWithWrongSequencerId ++ peersWithChangedEndpoint ++ unknownPeers) + .map( + _._1 + ) + .distinct } private def getAllBftSequencers()(implicit ec: ExecutionContext, tc: TraceContext) = { @@ -149,7 +138,8 @@ abstract class SequencerBftPeerReconciler( scan .listSvBftSequencers() .recover { case NonFatal(ex) => - logger.warn(s"Failed to read bft sequencers list from scan ${scan.url}", ex) + // not a warn because short-term failures are benign and longer-term outages should be covered by other monitoring + logger.info(s"Failed to read bft sequencers list from scan ${scan.url}", ex) Seq.empty } } @@ -161,6 +151,6 @@ object SequencerBftPeerReconciler { case class BftPeerDifference( toAdd: Seq[P2PEndpoint], toRemove: Seq[P2PEndpoint.Id], - currentPeers: Seq[P2PEndpoint], + currentPeers: Seq[(P2PEndpoint, Option[SequencerId])], ) } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/sv1/SV1Initializer.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/sv1/SV1Initializer.scala index 77b46c1545..8cc1f727d7 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/sv1/SV1Initializer.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/sv1/SV1Initializer.scala @@ -180,7 +180,7 @@ class SV1Initializer( sequencerConnectionPoolDelays = config.participantClient.sequencerConnectionPoolDelays.toInternal, ), - synchronizerId = Some(psid), + psid = Some(psid), timeTracker = SynchronizerTimeTrackerConfig( minObservationDuration = config.timeTrackerMinObservationDuration, observationLatency = config.timeTrackerObservationLatency, diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala index 14250dbba4..1de78c5c15 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala @@ -753,15 +753,20 @@ trait SvDsoStore : ListExpiredContracts[so.SvOnboardingConfirmed.ContractId, so.SvOnboardingConfirmed] = multiDomainAcsStore.listExpiredFromPayloadExpiry(so.SvOnboardingConfirmed.COMPANION) - def listExpiredAnsEntries: ListExpiredContracts[ + def listExpiredAnsEntries(ignoredPartiesStore: Option[IgnoredPartiesStore]): ListExpiredContracts[ splice.ans.AnsEntry.ContractId, splice.ans.AnsEntry, ] = - multiDomainAcsStore.listExpiredFromPayloadExpiry(splice.ans.AnsEntry.COMPANION) + multiDomainAcsStore.listExpiredFromPayloadExpiry( + splice.ans.AnsEntry.COMPANION, + ignoredPartiesStore, + ignoredPartyFields = Seq("user"), + ) def listExpiredAnsSubscriptions( now: CantonTimestamp, limit: Limit = defaultLimit, + ignoredPartiesStore: Option[IgnoredPartiesStore], )(implicit tc: TraceContext): Future[Seq[SvDsoStore.IdleAnsSubscription]] def listExpiredUnallocatedUnclaimedActivityRecord: ListExpiredContracts[ @@ -1102,12 +1107,16 @@ trait SvDsoStore Seq[Contract[splice.dsorules.Confirmation.ContractId, splice.dsorules.Confirmation]] ] - def listExpiredTransferPreapprovals: ListExpiredContracts[ + def listExpiredTransferPreapprovals( + ignoredPartiesStore: Option[IgnoredPartiesStore] + ): ListExpiredContracts[ splice.amuletrules.TransferPreapproval.ContractId, splice.amuletrules.TransferPreapproval, ] = multiDomainAcsStore.listExpiredFromPayloadExpiry( - splice.amuletrules.TransferPreapproval.COMPANION + splice.amuletrules.TransferPreapproval.COMPANION, + ignoredPartiesStore, + ignoredPartyFields = Seq("receiver", "provider"), ) def getExternalPartyAmuletRules()(implicit diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala index 06d7133d6a..301d1d387f 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala @@ -46,17 +46,13 @@ import org.lfdecentralizedtrust.splice.store.db.{ } import org.lfdecentralizedtrust.splice.store.{ DbVotesAcsStoreQueryBuilder, + IgnoredPartiesStore, IngestionSummary, Limit, LimitHelpers, MultiDomainAcsStore, } -import org.lfdecentralizedtrust.splice.sv.store.{ - AppRewardCouponsSum, - IgnoredPartiesStore, - SvDsoStore, - SvStore, -} +import org.lfdecentralizedtrust.splice.sv.store.{AppRewardCouponsSum, SvDsoStore, SvStore} import SvDsoStore.RoundBatch import com.digitalasset.canton.config.CantonRequireTypes.String2066 import org.lfdecentralizedtrust.splice.util.* @@ -146,11 +142,22 @@ class DbSvDsoStore( override def listExpiredAnsSubscriptions( now: CantonTimestamp, limit: Limit = defaultLimit, + ignoredPartiesStore: Option[IgnoredPartiesStore] = None, )(implicit tc: TraceContext): Future[Seq[SvDsoStore.IdleAnsSubscription]] = waitUntilAcsIngested { + val ignoredParties = ignoredPartiesStore.fold(Set.empty[PartyId])(_.getAll) + val ignoredPartiesFilter: SQLActionBuilder = + if (ignoredParties.nonEmpty) { + (sql" and " ++ notInClause( + "idle.create_arguments->'subscriptionData'->>'sender'", + ignoredParties, + )).toActionBuilder + } else { + sql"" + } for { joinedRows <- storage .query( - sql""" + (sql""" select idle.store_id, idle.migration_id, @@ -188,9 +195,10 @@ class DbSvDsoStore( AnsEntryContext.TEMPLATE_ID_WITH_PACKAGE_ID )} and idle.subscription_next_payment_due_at < $now + """ ++ ignoredPartiesFilter ++ sql""" order by idle.subscription_next_payment_due_at limit ${sqlLimit(limit)} - """.as[(SelectFromAcsTableResult, SelectFromAcsTableResult)], + """).toActionBuilder.as[(SelectFromAcsTableResult, SelectFromAcsTableResult)], "listExpiredAnsSubscriptions", ) } yield applyLimit("listExpiredAnsSubscriptions", limit, joinedRows).map { diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/util/ContractStakeholders.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/util/ContractStakeholders.scala new file mode 100644 index 0000000000..b7782d80ff --- /dev/null +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/util/ContractStakeholders.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.sv.util + +import com.digitalasset.canton.topology.PartyId +import org.lfdecentralizedtrust.splice.util.Contract + +trait ContractStakeholders[T] { + + def informees(payload: T): Seq[String] + + def dso(payload: T): String + + final def getStakeholders(payload: T): Seq[PartyId] = + getInformees(payload) :+ getDsoParty(payload) + + private final def getInformees(payload: T): Seq[PartyId] = + informees(payload).map(PartyId.tryFromProtoPrimitive) + + private final def getDsoParty(payload: T): PartyId = + PartyId.tryFromProtoPrimitive(dso(payload)) + + // ExpireRewardCouponTrigger and FeaturedAppActivityMarkerTrigger do not use BatchedMultiDomainExpiredContractTrigger + final def getInformeesFromContracts[TCid]( + contracts: Seq[Contract[TCid, T]] + ): Set[PartyId] = + contracts.flatMap(c => getInformees(c.payload)).toSet + +} diff --git a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala index ba343fdae2..aeb5808050 100644 --- a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala +++ b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala @@ -54,6 +54,7 @@ import org.lfdecentralizedtrust.splice.environment.{DarResources, RetryProvider} import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.QueryResult import org.lfdecentralizedtrust.splice.store.{ HardLimit, + IgnoredPartiesStore, Limit, MiningRoundsStore, PageLimit, @@ -61,7 +62,7 @@ import org.lfdecentralizedtrust.splice.store.{ } import org.lfdecentralizedtrust.splice.sv.store.SvDsoStore.{IdleAnsSubscription, RoundBatch} import org.lfdecentralizedtrust.splice.sv.store.db.DbSvDsoStore -import org.lfdecentralizedtrust.splice.sv.store.{IgnoredPartiesStore, SvDsoStore, SvStore} +import org.lfdecentralizedtrust.splice.sv.store.{SvDsoStore, SvStore} import org.lfdecentralizedtrust.splice.sv.util.SvUtil import org.lfdecentralizedtrust.splice.util.{ AssignedContract, @@ -1775,28 +1776,64 @@ abstract class SvDsoStoreTest extends StoreTestBase with HasExecutionContext { } - "listExpiredAnsSubscriptions" should { + "listExpiredAnsEntries" should { - "return all entries where subscription_next_payment_due_at < now" in { + // 1 to 3 expire at time(1..3), 4 to 6 at time(4..6); queries run at time(4). + def mkAnsEntries(range: Range) = + range.map(i => ansEntry(userParty(i), s"entry$i", expiresAt = time(i.toLong).toInstant)) + + def setupAnsEntries(store: SvDsoStore) = { + val expired = mkAnsEntries(1 to 3) + val notExpired = mkAnsEntries(4 to 6) + MonadUtil + .sequentialTraverse(expired ++ notExpired)( + dummyDomain.create(_)(store.multiDomainAcsStore) + ) + .map(_ => expired) + } + + "return all expired ans entries" in { for { store <- mkStore() - // 1 to 3 are expired, 4 to 6 are not - data = ((1 to 3).map(n => - n -> Instant.now().truncatedTo(ChronoUnit.MICROS).minusSeconds(n * 1000L) - ) ++ (4 to 6) - .map(n => n -> Instant.now().truncatedTo(ChronoUnit.MICROS).plusSeconds(n * 1000L))) - .map { case (n, nextPaymentDueAt) => - val contextContract = - ansEntryContext(n, n.toString) - val idleStateContract = - subscriptionIdleState( - n, - nextPaymentDueAt, - ) + expired <- setupAnsEntries(store) + result <- store.listExpiredAnsEntries(None)( + time(4), + PageLimit.tryCreate(100), + )(traceContext) + } yield { + result.map(_.contract) should contain theSameElementsAs expired + } + } - (contextContract, idleStateContract) - } - _ <- MonadUtil.sequentialTraverse(data) { case (contextContract, idleContract) => + "filter out ans entries whose user is ignored" in { + for { + store <- mkStore() + expired <- setupAnsEntries(store) + result <- store.listExpiredAnsEntries( + Some(new IgnoredPartiesStore(Set(userParty(1), userParty(2)))) + )( + time(4), + PageLimit.tryCreate(100), + )(traceContext) + } yield { + result.map(_.contract) should contain theSameElementsAs Seq(expired(2)) + } + } + } + + "listExpiredAnsSubscriptions" should { + + def setupExpiredSubscriptions(store: SvDsoStore) = { + // 1 to 3 are expired, 4 to 6 are not + val data = ((1 to 3).map(n => + n -> Instant.now().truncatedTo(ChronoUnit.MICROS).minusSeconds(n * 1000L) + ) ++ (4 to 6) + .map(n => n -> Instant.now().truncatedTo(ChronoUnit.MICROS).plusSeconds(n * 1000L))) + .map { case (n, nextPaymentDueAt) => + (ansEntryContext(n, n.toString), subscriptionIdleState(n, nextPaymentDueAt)) + } + MonadUtil + .sequentialTraverse(data) { case (contextContract, idleContract) => for { _ <- dummyDomain.create(contextContract, createdEventSignatories = Seq(dsoParty))( store.multiDomainAcsStore @@ -1806,6 +1843,13 @@ abstract class SvDsoStoreTest extends StoreTestBase with HasExecutionContext { ) } yield () } + .map(_ => data) + } + + "return all entries where subscription_next_payment_due_at < now" in { + for { + store <- mkStore() + data <- setupExpiredSubscriptions(store) } yield { val expected = data .take(3) @@ -1814,7 +1858,34 @@ abstract class SvDsoStoreTest extends StoreTestBase with HasExecutionContext { } .reverse store - .listExpiredAnsSubscriptions(CantonTimestamp.now(), limit = PageLimit.tryCreate(3)) + .listExpiredAnsSubscriptions( + CantonTimestamp.now(), + limit = PageLimit.tryCreate(3), + None, + ) + .futureValue should be(expected) + } + } + + "filter out subscriptions whose sender is in the ignored parties store" in { + for { + store <- mkStore() + data <- setupExpiredSubscriptions(store) + } yield { + // n=1 and n=2 are expired but their senders are ignored, only n=3 remains + val expected = data + .slice(2, 3) + .map { case (ctxContract, idleContract) => + IdleAnsSubscription(idleContract, ctxContract) + } + store + .listExpiredAnsSubscriptions( + CantonTimestamp.now(), + limit = PageLimit.tryCreate(3), + ignoredPartiesStore = Some( + new IgnoredPartiesStore(Set(userParty(1), userParty(2))) + ), + ) .futureValue should be(expected) } } @@ -1935,41 +2006,54 @@ abstract class SvDsoStoreTest extends StoreTestBase with HasExecutionContext { "listExpiredTransferPreapprovals" should { - "return all expired transfer pre-approvals" in { - val expired = (1 to 3).map(n => + def mkTransferPreapprovals(n: Range) = + n.map(i => transferPreapproval( - userParty(n), - providerParty(n), + userParty(i), + providerParty(i), time(0), - expiresAt = time(n.toLong), + expiresAt = time(i.toLong), ) ) - val notExpired = - (4 to 6).map(n => - transferPreapproval( - userParty(n), - providerParty(n), - time(0), - expiresAt = time(n.toLong), - ) + + def setupTransferPreapprovals(store: SvDsoStore) = { + val expired = mkTransferPreapprovals(1 to 3) + val notExpired = mkTransferPreapprovals(4 to 6) + MonadUtil + .sequentialTraverse(expired ++ notExpired)( + dummyDomain.create(_)(store.multiDomainAcsStore) ) + .map(_ => expired) + } + + "return all expired transfer pre-approvals" in { for { store <- mkStore() - _ <- MonadUtil.sequentialTraverse(expired ++ notExpired)( - dummyDomain.create(_)(store.multiDomainAcsStore) - ) - result <- store.listExpiredTransferPreapprovals( + expired <- setupTransferPreapprovals(store) + result <- store.listExpiredTransferPreapprovals(None)( time(4), PageLimit.tryCreate(100), - )( - traceContext - ) + )(traceContext) } yield { - val contracts = result.map(_.contract) - contracts should contain theSameElementsAs expired + result.map(_.contract) should contain theSameElementsAs expired } } + "filter out pre-approvals whose receiver or provider is ignored" in { + for { + store <- mkStore() + expired <- setupTransferPreapprovals(store) + result <- store.listExpiredTransferPreapprovals( + // n=1 is dropped via its receiver, n=2 via its provider + Some(new IgnoredPartiesStore(Set(userParty(1), providerParty(2)))) + )( + time(4), + PageLimit.tryCreate(100), + )(traceContext) + } yield { + result.map(_.contract) should contain theSameElementsAs Seq(expired(2)) + } + } } } diff --git a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/sv/automation/VoteRequestMetricsTriggerTest.scala b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/sv/automation/VoteRequestMetricsTriggerTest.scala new file mode 100644 index 0000000000..49a8d07bec --- /dev/null +++ b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/sv/automation/VoteRequestMetricsTriggerTest.scala @@ -0,0 +1,35 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.sv.automation + +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{Reason, Vote} +import org.lfdecentralizedtrust.splice.store.StoreTestBase +import org.lfdecentralizedtrust.splice.sv.automation.VoteRequestMetricsTrigger.VoteRequestCounts + +import java.util.Optional + +class VoteRequestMetricsTriggerTest extends StoreTestBase { + + "countByState" should { + "partition vote requests by their state relative to the SV" in { + val sv = userParty(1) + val otherSv = userParty(2) + def vote(svParty: com.digitalasset.canton.topology.PartyId): Vote = + new Vote(svParty.toProtoPrimitive, true, new Reason("", ""), Optional.empty()) + + val notVoted = voteRequest(requester = otherSv, votes = Seq(vote(otherSv))) + val voted = voteRequest(requester = otherSv, votes = Seq(vote(otherSv), vote(sv))) + val ownRequest = voteRequest(requester = sv, votes = Seq(vote(sv))) + // ready to close counts as such regardless of whether the SV has voted + val readyVoted = voteRequest(requester = sv, votes = Seq(vote(sv))) + val readyNotVoted = voteRequest(requester = otherSv, votes = Seq(vote(otherSv))) + + VoteRequestMetricsTrigger.countByState( + Seq(notVoted, voted, ownRequest, readyVoted, readyNotVoted), + Set(readyVoted.contractId, readyNotVoted.contractId), + sv.toProtoPrimitive, + ) shouldBe VoteRequestCounts(actionNeeded = 1, inProgress = 2, readyToClose = 2) + } + } +} diff --git a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/sv/onboarding/SequencerBftPeerReconcilerSpec.scala b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/sv/onboarding/SequencerBftPeerReconcilerSpec.scala index f6efa519cc..1b6d7b115d 100644 --- a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/sv/onboarding/SequencerBftPeerReconcilerSpec.scala +++ b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/sv/onboarding/SequencerBftPeerReconcilerSpec.scala @@ -141,8 +141,8 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host), - configuredPeer(sequencer2Host), + configuredPeer(sequencer1Host, Some(sequencer1Id)), + configuredPeer(sequencer2Host, Some(sequencer2Id)), ) ) ) @@ -177,8 +177,8 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host), - configuredPeer(sequencer2Host), + configuredPeer(sequencer1Host, Some(sequencer1Id)), + configuredPeer(sequencer2Host, Some(sequencer2Id)), ) ) ) @@ -187,7 +187,7 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR result should be(empty) } - it should "do nothing when scan doesn't contain the sequencer info but the dso state still contains it and the network status confirms the sequencer" in { + it should "do nothing when scan doesn't contain the sequencer info but the dso state still contains it and the configured peer's sequencer id confirms the sequencer" in { withConfiguredDsoSequencers( Seq( createSequencerConfig(sequencer1Id), @@ -207,17 +207,12 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host), - configuredPeer(sequencer2Host), + configuredPeer(sequencer1Host, Some(sequencer1Id)), + configuredPeer(sequencer2Host, Some(sequencer2Id)), ) ) ) - withNetworkStatus( - (Some(sequencer1Id), Some(sequencer1Host)), - (Some(sequencer2Id), Some(sequencer2Host)), - ) - val result = reconciler.diffDsoRulesWithTopology().futureValue result should be(empty) } @@ -243,7 +238,7 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host) + configuredPeer(sequencer1Host, Some(sequencer1Id)) ) ) ) @@ -277,15 +272,11 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host) + configuredPeer(sequencer1Host, Some(sequencer1Id)) ) ) ) - withNetworkStatus( - (Some(sequencer1Id), Some(sequencer1Host)) - ) - val result = reconciler.diffDsoRulesWithTopology().futureValue result should be(empty) } @@ -309,7 +300,7 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host) + configuredPeer(sequencer1Host, Some(sequencer1Id)) ) ) ) @@ -317,7 +308,7 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR reconciler.diffDsoRulesWithTopology().futureValue should be(empty) } - it should "remove a configured peer whose sequencer id is no longer in the dso, using the network status as a fallback" in { + it should "remove a configured peer whose sequencer id is no longer in the dso, using the configured peer's sequencer id as a fallback" in { withConfiguredDsoSequencers( Seq( createSequencerConfig(sequencer1Id), @@ -337,23 +328,18 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host), - configuredPeer(sequencer3Host), + configuredPeer(sequencer1Host, Some(sequencer1Id)), + configuredPeer(sequencer3Host, Some(sequencer3Id)), ) ) ) - withNetworkStatus( - (Some(sequencer1Id), Some(sequencer1Host)), - (Some(sequencer3Id), Some(sequencer3Host)), - ) - val result = reconciler.diffDsoRulesWithTopology().futureValue.loneElement result.toAdd should be(empty) result.toRemove should contain only sequencer3Host } - it should "replace a configured peer whose sequencer moved to a different endpoint, using the network status as a fallback" in { + it should "replace a configured peer whose sequencer moved to a different endpoint, using the configured peer's sequencer id as a fallback" in { withConfiguredDsoSequencers( Seq( createSequencerConfig(sequencer1Id), @@ -375,21 +361,17 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host) + configuredPeer(sequencer1Host, Some(sequencer1Id)) ) ) ) - withNetworkStatus( - (Some(sequencer1Id), Some(sequencer1Host)) - ) - val result = reconciler.diffDsoRulesWithTopology().futureValue.loneElement result.toAdd.map(_.id) should contain only newSequencer1Host result.toRemove should contain only sequencer1Host } - it should "keep a configured peer and log a warning when no sequencer id can be found for it in the network status" in { + it should "keep a configured peer when it has no sequencer id and the dso endpoints are not fully known" in { withConfiguredDsoSequencers( Seq( createSequencerConfig(sequencer1Id), @@ -409,22 +391,13 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR .thenReturn( Future.successful( Seq( - configuredPeer(sequencer1Host), + configuredPeer(sequencer1Host, Some(sequencer1Id)), configuredPeer(sequencer2Host), ) ) ) - withNetworkStatus( - (Some(sequencer1Id), Some(sequencer1Host)) - ) - - val result = loggerFactory.assertLogs( - reconciler.diffDsoRulesWithTopology().futureValue, - _.warningMessage should include( - s"Could not find a sequencer id for the configured peer endpoint ${sequencer2Host}" - ), - ) + val result = reconciler.diffDsoRulesWithTopology().futureValue result should be(empty) } @@ -507,12 +480,9 @@ class SequencerBftPeerReconcilerSpec extends AnyFlatSpec with BaseTest with HasR ) } - private def configuredPeer(host: P2PEndpoint.Id): P2PEndpoint = - BftSequencer(serialId, selfSequencerId, host.url).peerId - - private def withNetworkStatus( - entries: (Option[SequencerId], Option[P2PEndpoint.Id])* - ) = - when(sequencerAdminConnection.listCurrentPeerEndpoints()) - .thenReturn(Future.successful(entries)) + private def configuredPeer( + host: P2PEndpoint.Id, + sequencerId: Option[SequencerId] = None, + ): (P2PEndpoint, Option[SequencerId]) = + BftSequencer(serialId, selfSequencerId, host.url).peerId -> sequencerId } diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala index d9b8dfb543..f9b9173550 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala @@ -782,6 +782,7 @@ class ValidatorApp( config.parameters.enabledFeatures, config.additionalPackagesToUnvet, config.domains.global.alias, + config.enableDeprecatedTransferCommandSupport, loggerFactory, packageVersionSupport, ) diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala index 650f7dbd32..8f9bb3b47d 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala @@ -96,6 +96,16 @@ class HttpValidatorAdminHandler( )(handleRequest) } + private def requireTransferCommandSupport[T](handleRequest: => T): T = { + if (config.enableDeprecatedTransferCommandSupport) { + handleRequest + } else { + throw HttpErrorHandler.notImplemented( + "Transfer command support is disabled by default and will be removed in 0.8.0. You can temporarily enable in on 0.7.x by setting enable-deprecated-transfer-command-support=true in your validator config." + ) + } + } + def onboardUser( respond: v0.ValidatorAdminResource.OnboardUserResponse.type )( @@ -454,6 +464,7 @@ class HttpValidatorAdminHandler( BaseLedgerConnection.sanitizeUserIdToPartyString(body.userPartyId), ), DedupOffset(implicitly[Ordering[Long]].min(offsetESP, offsetTP)), + recoverAcceptedDuplicates = true, ) ), ) @@ -704,89 +715,91 @@ class HttpValidatorAdminHandler( ): Future[v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse] = { implicit val AdminUserRequest(tracedContext) = tuser requireWalletEnabled { _ => - val senderParty = PartyId.tryFromProtoPrimitive(body.senderPartyId) - val receiverParty = PartyId.tryFromProtoPrimitive(body.receiverPartyId) - for { - synchronizerId <- getAmuletRulesDomain()(tracedContext) - // This check is just to make it fail early. The actual preapproval is fixed when the automation - // executes the transfer but we want the user to get feedback during the prepare step already. - _ <- scanConnection.lookupTransferPreapprovalByParty(receiverParty).map { preapprovalO => - if (preapprovalO.isEmpty) { - throw Status.INVALID_ARGUMENT - .withDescription(s"Receiver $receiverParty does not have a TransferPreapproval") - .asRuntimeException + requireTransferCommandSupport { + val senderParty = PartyId.tryFromProtoPrimitive(body.senderPartyId) + val receiverParty = PartyId.tryFromProtoPrimitive(body.receiverPartyId) + for { + synchronizerId <- getAmuletRulesDomain()(tracedContext) + // This check is just to make it fail early. The actual preapproval is fixed when the automation + // executes the transfer but we want the user to get feedback during the prepare step already. + _ <- scanConnection.lookupTransferPreapprovalByParty(receiverParty).map { preapprovalO => + if (preapprovalO.isEmpty) { + throw Status.INVALID_ARGUMENT + .withDescription(s"Receiver $receiverParty does not have a TransferPreapproval") + .asRuntimeException + } } - } - externalPartyAmuletRules <- scanConnection.getExternalPartyAmuletRules() - supportsDescription <- packageVersionSupport - .supportsDescriptionInTransferPreapprovals( - Seq(receiverParty, senderParty, store.key.dsoParty), - clock.now, - ) - .map(_.supported) - commands = externalPartyAmuletRules.toAssignedContract - .getOrElse( - throw Status.Code.FAILED_PRECONDITION.toStatus - .withDescription( - s"ExternalPartyAmuletRules is currently inflight between synchronizers, retry until it is assigned to a synchronizer" + externalPartyAmuletRules <- scanConnection.getExternalPartyAmuletRules() + supportsDescription <- packageVersionSupport + .supportsDescriptionInTransferPreapprovals( + Seq(receiverParty, senderParty, store.key.dsoParty), + clock.now, + ) + .map(_.supported) + commands = externalPartyAmuletRules.toAssignedContract + .getOrElse( + throw Status.Code.FAILED_PRECONDITION.toStatus + .withDescription( + s"ExternalPartyAmuletRules is currently inflight between synchronizers, retry until it is assigned to a synchronizer" + ) + .asRuntimeException() + ) + .exercise( + _.exerciseExternalPartyAmuletRules_CreateTransferCommand( + senderParty.toProtoPrimitive, + receiverParty.toProtoPrimitive, + store.key.validatorParty.toProtoPrimitive, + body.amount.bigDecimal, + body.expiresAt.toInstant, + body.nonce, + Option.when(supportsDescription)(body.description).flatten.toJava, + java.util.Optional.of(store.key.dsoParty.toProtoPrimitive), ) - .asRuntimeException() - ) - .exercise( - _.exerciseExternalPartyAmuletRules_CreateTransferCommand( - senderParty.toProtoPrimitive, - receiverParty.toProtoPrimitive, - store.key.validatorParty.toProtoPrimitive, - body.amount.bigDecimal, - body.expiresAt.toInstant, - body.nonce, - Option.when(supportsDescription)(body.description).flatten.toJava, - java.util.Optional.of(store.key.dsoParty.toProtoPrimitive), + ) + .update + .commands() + .asScala + .toSeq + r <- storeWithIngestion + .connection(SpliceLedgerConnectionPriority.Medium) + .prepareSubmission( + Some(synchronizerId), + Seq(senderParty), + Seq(senderParty), + commands, + storeWithIngestion + .connection(SpliceLedgerConnectionPriority.Medium) + .disclosedContracts(externalPartyAmuletRules), + body.verboseHashing.getOrElse(false), + ) + transferCommandCid = r.preparedTransaction + .flatMap(_.transaction) + .toList + .flatMap(_.nodes) + .flatMap(n => + n.getV1.nodeType match { + case interactive.transaction.v1.interactive_submission_data.Node.NodeType + .Create(create) => + Seq(create.contractId) + case _ => Seq.empty + } + ) + .headOption + .getOrElse( + throw Status.INTERNAL + .withDescription("Failed to obtain transferCommandCid from prepared transaction") + .asRuntimeException() + ) + } yield { + v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse.OK( + definitions.PrepareTransferPreapprovalSendResponse( + Base64.getEncoder.encodeToString(r.getPreparedTransaction.toByteArray), + HexString.toHexString(r.preparedTransactionHash), + transferCommandCid, + r.hashingDetails, ) ) - .update - .commands() - .asScala - .toSeq - r <- storeWithIngestion - .connection(SpliceLedgerConnectionPriority.Medium) - .prepareSubmission( - Some(synchronizerId), - Seq(senderParty), - Seq(senderParty), - commands, - storeWithIngestion - .connection(SpliceLedgerConnectionPriority.Medium) - .disclosedContracts(externalPartyAmuletRules), - body.verboseHashing.getOrElse(false), - ) - transferCommandCid = r.preparedTransaction - .flatMap(_.transaction) - .toList - .flatMap(_.nodes) - .flatMap(n => - n.getV1.nodeType match { - case interactive.transaction.v1.interactive_submission_data.Node.NodeType - .Create(create) => - Seq(create.contractId) - case _ => Seq.empty - } - ) - .headOption - .getOrElse( - throw Status.INTERNAL - .withDescription("Failed to obtain transferCommandCid from prepared transaction") - .asRuntimeException() - ) - } yield { - v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse.OK( - definitions.PrepareTransferPreapprovalSendResponse( - Base64.getEncoder.encodeToString(r.getPreparedTransaction.toByteArray), - HexString.toHexString(r.preparedTransactionHash), - transferCommandCid, - r.hashingDetails, - ) - ) + } } } } @@ -798,15 +811,17 @@ class HttpValidatorAdminHandler( ): Future[v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponse] = { implicit val AdminUserRequest(tracedContext) = tuser requireWalletEnabled { _ => - for { - updateId <- ValidatorUtil.submitAsExternalParty( - storeWithIngestion.connection(SpliceLedgerConnectionPriority.Medium), - body.submission, - waitForOffset = false, + requireTransferCommandSupport { + for { + updateId <- ValidatorUtil.submitAsExternalParty( + storeWithIngestion.connection(SpliceLedgerConnectionPriority.Medium), + body.submission, + waitForOffset = false, + ) + } yield v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponseOK( + definitions.SubmitTransferPreapprovalSendResponse(updateId) ) - } yield v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponseOK( - definitions.SubmitTransferPreapprovalSendResponse(updateId) - ) + } } } diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/AcceptTransferPreapprovalProposalTrigger.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/AcceptTransferPreapprovalProposalTrigger.scala index 21d5e4edc9..c60f31b25a 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/AcceptTransferPreapprovalProposalTrigger.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/AcceptTransferPreapprovalProposalTrigger.scala @@ -110,11 +110,20 @@ class AcceptTransferPreapprovalProposalTrigger( for { validatorWallet <- ValidatorUtil.getValidatorWallet(store, walletManager) result <- store.lookupTransferPreapprovalByReceiverPartyWithOffset(receiverParty) flatMap { - case QueryResult(_, Some(_)) => + // Expired pre-approvals are ignored: the receiver cannot be paid through them anymore + // and they may stick around for a while until the SV automation archives them. + case QueryResult(_, Some(existing)) + if existing.payload.expiresAt.isAfter(clock.now.toInstant) => Future.successful( TaskSuccess(show"TransferPreapproval for receiver $receiverParty already exists") ) - case QueryResult(offset, None) => + case QueryResult(offset, existing) => + existing.foreach(expired => + logger.info( + s"Accepting proposal for receiver $receiverParty as its existing TransferPreapproval " + + s"${expired.contractId.contractId} expired at ${expired.payload.expiresAt}" + ) + ) validatorWallet.treasury .enqueueAmuletOperation( operation, diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ReconcileSequencerConnectionsTrigger.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ReconcileSequencerConnectionsTrigger.scala index 1326081f63..6e0c852829 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ReconcileSequencerConnectionsTrigger.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ReconcileSequencerConnectionsTrigger.scala @@ -114,7 +114,7 @@ class ReconcileSequencerConnectionsTrigger( SynchronizerConnectionConfig( alias, sequencerConnectionConfig, - synchronizerId = Some(psid), + psid = Some(psid), ), reconnectOnSynchronizerConfigurationChange, modifySequencerConnections(sequencerConnectionConfig), diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala index cac4d2676c..fb4b1e42d0 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala @@ -76,6 +76,7 @@ class ValidatorAutomationService( enabledFeatures: EnabledFeaturesConfig, additionalPackagesToUnvet: Map[PackageName, Set[PackageVersion]], globalSynchronizerAlias: SynchronizerAlias, + enableDeprecatedTransferCommandSupport: Boolean, override protected val loggerFactory: NamedLoggerFactory, packageVersionSupport: PackageVersionSupport, )(implicit @@ -196,15 +197,17 @@ class ValidatorAutomationService( ) ) - registerTrigger( - new TransferCommandSendTrigger( - triggerContext, - scanConnection, - store, - walletManager.externalPartyWalletManager, - connection(SpliceLedgerConnectionPriority.Medium), + if (enableDeprecatedTransferCommandSupport) { + registerTrigger( + new TransferCommandSendTrigger( + triggerContext, + scanConnection, + store, + walletManager.externalPartyWalletManager, + connection(SpliceLedgerConnectionPriority.Medium), + ) ) - ) + } } backupDumpConfig.foreach(config => @@ -241,6 +244,7 @@ class ValidatorAutomationService( maxVettingDelay, latestPackagesOnly, enabledFeatures.enableUnsupportedDarsUnvetting, + enabledFeatures.enableValidatorDarsUnvetting, additionalPackagesToUnvet, ) ) diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorPackageVettingTrigger.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorPackageVettingTrigger.scala index 999eb92fd3..f59d6c399f 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorPackageVettingTrigger.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorPackageVettingTrigger.scala @@ -23,6 +23,7 @@ class ValidatorPackageVettingTrigger( maxVettingDelay: NonNegativeFiniteDuration, latestPackagesOnly: Boolean, enableUnsupportedDarsUnvetting: Boolean, + enableValidatorDarsUnvetting: Boolean, additionalPackagesToUnvet: Map[PackageName, Set[PackageVersion]], )(implicit override val ec: ExecutionContext, @@ -31,7 +32,7 @@ class ValidatorPackageVettingTrigger( ValidatorPackageVettingTrigger.packages, maxVettingDelay, latestPackagesOnly, - enableUnvetting = false, // Currently only supported by SVs. + enableUnvetting = enableValidatorDarsUnvetting, enableUnsupportedDarsUnvetting, additionalPackagesToUnvet, ) { diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala index 9dde4c84da..2973fca980 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala @@ -224,6 +224,8 @@ case class ValidatorAppBackendConfig( // from running concurrently against the same database. Only disable for migration scenarios // where intentional overlap is required. instanceLockEnabled: Boolean = true, + // Enable the deprecated transfer command support, will be fully removed in 0.8.0. + enableDeprecatedTransferCommandSupport: Boolean = false, ) extends SpliceBackendConfig // TODO(DACH-NY/canton-network-node#736): fork or generalize this trait. { override val nodeTypeName: String = "validator" diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/domain/SynchronizerConnector.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/domain/SynchronizerConnector.scala index 5a1938bc44..5b6850bc76 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/domain/SynchronizerConnector.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/domain/SynchronizerConnector.scala @@ -190,7 +190,7 @@ class SynchronizerConnector( SynchronizerConnectionConfig( alias, sequencerConnections, - synchronizerId = psid, + psid = psid, timeTracker = SynchronizerTimeTrackerConfig( minObservationDuration = config.timeTrackerMinObservationDuration, observationLatency = config.timeTrackerObservationLatency, diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/ExternalPartyWalletManager.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/ExternalPartyWalletManager.scala index 19c50c6fcd..d410127388 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/ExternalPartyWalletManager.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/ExternalPartyWalletManager.scala @@ -27,6 +27,7 @@ import org.lfdecentralizedtrust.splice.wallet.store.{ExternalPartyWalletStore, W import scala.collection.concurrent.TrieMap import scala.concurrent.{blocking, ExecutionContext} +import scala.util.control.NonFatal /** Manages all services comprising an external party wallets. */ class ExternalPartyWalletManager( @@ -134,8 +135,8 @@ class ExternalPartyWalletManager( )(TraceContext.empty) externalPartyRetryProviderAndWalletService.foreach { case (externalPartyRetryProvider, walletService) => - externalPartyRetryProvider.close() - walletService.close() + try externalPartyRetryProvider.close() + finally walletService.close() } UnlessShutdown.AbortedDueToShutdown } else { @@ -160,22 +161,32 @@ class ExternalPartyWalletManager( retryProvider.futureSupervisor, retryProvider.metricsFactory, ) - val walletService = new ExternalPartyWalletService( - ledgerClient, - key, - automationConfig, - clock, - domainTimeSync, - storage, - externalPartyRetryProvider, - partyLoggerFactory, - migrationId, - participantId, - params, - scanConnection, - packageVersionSupport, - rewardSharingConfigByParty.getOrElse(externalParty.toProtoPrimitive, RewardSharingConfig()), - ) + val walletService = + try { + new ExternalPartyWalletService( + ledgerClient, + key, + automationConfig, + clock, + domainTimeSync, + storage, + externalPartyRetryProvider, + partyLoggerFactory, + migrationId, + participantId, + params, + scanConnection, + packageVersionSupport, + rewardSharingConfigByParty.getOrElse( + externalParty.toProtoPrimitive, + RewardSharingConfig.BuiltIn(), + ), + ) + } catch { + case NonFatal(e) => + externalPartyRetryProvider.close() + throw e + } (externalPartyRetryProvider, walletService) } diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/ExternalPartyWalletService.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/ExternalPartyWalletService.scala index d2c96ebd44..aa4acf27cc 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/ExternalPartyWalletService.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/ExternalPartyWalletService.scala @@ -3,7 +3,7 @@ package org.lfdecentralizedtrust.splice.wallet -import com.digitalasset.canton.lifecycle.{CloseContext, FlagCloseable} +import com.digitalasset.canton.lifecycle.{CloseContext, FlagCloseable, LifeCycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.time.Clock @@ -20,6 +20,7 @@ import org.lfdecentralizedtrust.splice.wallet.config.RewardSharingConfig import org.lfdecentralizedtrust.splice.wallet.store.ExternalPartyWalletStore import scala.concurrent.ExecutionContext +import scala.util.control.NonFatal /** A service managing the treasury, automation, and store for an external party's wallet. */ class ExternalPartyWalletService( @@ -59,23 +60,31 @@ class ExternalPartyWalletService( params.defaultLimit, ) - val automation = new ExternalPartyWalletAutomationService( - store, - ledgerClient, - automationConfig, - clock, - domainTimeSync, - retryProvider, - params, - scanConnection, - loggerFactory, - packageVersionSupport, - rewardSharingConfig, - ) + val automation = + try { + new ExternalPartyWalletAutomationService( + store, + ledgerClient, + automationConfig, + clock, + domainTimeSync, + retryProvider, + params, + scanConnection, + loggerFactory, + packageVersionSupport, + rewardSharingConfig, + ) + } catch { + // a failed construction never reaches onClosed, so close the store here + case NonFatal(e) => + store.close() + throw e + } override def onClosed(): Unit = { - automation.close() - store.close() + // LifeCycle.close closes both in order even if the first close fails. + LifeCycle.close(automation, store)(logger) super.onClosed() } } diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/UserWalletManager.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/UserWalletManager.scala index dcd52b5750..5e90a51ba6 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/UserWalletManager.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/UserWalletManager.scala @@ -37,6 +37,7 @@ import io.opentelemetry.api.trace.Tracer import scala.collection.concurrent.TrieMap import scala.concurrent.{blocking, ExecutionContext, Future} +import scala.util.control.NonFatal /** Manages all services comprising an end-user wallets. */ class UserWalletManager( @@ -185,8 +186,8 @@ class UserWalletManager( show"Detected race between adding wallet for party ${endUserParty} and shutdown: closing wallet." )(TraceContext.empty) userRetryProviderAndWalletService.foreach { case (userRetryProvider, walletService) => - userRetryProvider.close() - walletService.close() + try userRetryProvider.close() + finally walletService.close() } UnlessShutdown.AbortedDueToShutdown } else { @@ -213,29 +214,37 @@ class UserWalletManager( retryProvider.futureSupervisor, retryProvider.metricsFactory, ) - val walletService = new UserWalletService( - ledgerClient, - key, - this, - automationConfig, - clock, - domainTimeSync, - treasuryConfig, - storage, - userRetryProvider, - userLoggerFactory, - scanConnection, - packageVersionSupport, - migrationId, - participantId, - Option.when(endUserParty == store.walletKey.validatorParty)(validatorTopupConfig), - // TODO(DACH-NY/canton-network-node#12554): make it easier to configure the sweep functionality and guard better against operator errors (typos, etc.) - walletSweep.get(endUserParty.toProtoPrimitive), - autoAcceptTransfers.get(endUserParty.toProtoPrimitive), - rewardSharingConfigByParty.getOrElse(endUserParty.toProtoPrimitive, RewardSharingConfig()), - dedupDuration, - params, - ) + val walletService = + try { + new UserWalletService( + ledgerClient, + key, + this, + automationConfig, + clock, + domainTimeSync, + treasuryConfig, + storage, + userRetryProvider, + userLoggerFactory, + scanConnection, + packageVersionSupport, + migrationId, + participantId, + Option.when(endUserParty == store.walletKey.validatorParty)(validatorTopupConfig), + // TODO(DACH-NY/canton-network-node#12554): make it easier to configure the sweep functionality and guard better against operator errors (typos, etc.) + walletSweep.get(endUserParty.toProtoPrimitive), + autoAcceptTransfers.get(endUserParty.toProtoPrimitive), + rewardSharingConfigByParty + .getOrElse(endUserParty.toProtoPrimitive, RewardSharingConfig.BuiltIn()), + dedupDuration, + params, + ) + } catch { + case NonFatal(e) => + userRetryProvider.close() + throw e + } (userRetryProvider, walletService) } @@ -246,8 +255,8 @@ class UserWalletManager( .withDescription(show"No wallet service found for user party ${userParty}") .asRuntimeException() case Some((userRetryProvider, walletService)) => - userRetryProvider.close() - walletService.close() + try userRetryProvider.close() + finally walletService.close() } } diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/UserWalletService.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/UserWalletService.scala index 1b3923127f..3a8b7a316e 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/UserWalletService.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/UserWalletService.scala @@ -19,7 +19,7 @@ import org.lfdecentralizedtrust.splice.wallet.config.{ import org.lfdecentralizedtrust.splice.wallet.store.UserWalletStore import org.lfdecentralizedtrust.splice.wallet.treasury.TreasuryService import org.lfdecentralizedtrust.splice.wallet.util.ValidatorTopupConfig -import com.digitalasset.canton.lifecycle.{CloseContext, FlagCloseable} +import com.digitalasset.canton.lifecycle.{CloseContext, FlagCloseable, LifeCycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.time.Clock @@ -30,6 +30,7 @@ import org.apache.pekko.stream.Materializer import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority import scala.concurrent.ExecutionContext +import scala.util.control.NonFatal /** A service managing the treasury, automation, and store for an end-user's wallet. */ class UserWalletService( @@ -78,47 +79,62 @@ class UserWalletService( params.defaultLimit, ) - val treasury: TreasuryService = new TreasuryService( - // The treasury gets its own connection, and is required to manage waiting for the store on its own. - ledgerClient.connection( - this.getClass.getSimpleName, - loggerFactory, - SpliceCircuitBreaker( - "treasury", - params.circuitBreakers.mediumPriority, + val treasury: TreasuryService = + try { + new TreasuryService( + // The treasury gets its own connection, and is required to manage waiting for the store on its own. + ledgerClient.connection( + this.getClass.getSimpleName, + loggerFactory, + SpliceCircuitBreaker( + "treasury", + params.circuitBreakers.mediumPriority, + clock, + store.dsoPartyId, + loggerFactory, + ), + ), + treasuryConfig, clock, - store.dsoPartyId, + store, + walletManager, + retryProvider, + scanConnection, + mintUnassignedRewardCouponsV2 = rewardSharingConfig.mintUnassignedCoupons, loggerFactory, - ), - ), - treasuryConfig, - clock, - store, - walletManager, - retryProvider, - scanConnection, - mintUnassignedRewardCouponsV2 = rewardSharingConfig.beneficiaries.isEmpty, - loggerFactory, - ) + ) + } catch { + // a failed construction never reaches onClosed, so close the store here + case NonFatal(e) => + store.close() + throw e + } - val automation = new UserWalletAutomationService( - store, - treasury, - ledgerClient, - automationConfig, - clock, - domainTimeSync, - scanConnection, - retryProvider, - packageVersionSupport, - loggerFactory, - validatorTopupConfigO, - walletSweep, - autoAcceptTransfers, - rewardSharingConfig, - dedupDuration, - params, - ) + val automation: UserWalletAutomationService = + try { + new UserWalletAutomationService( + store, + treasury, + ledgerClient, + automationConfig, + clock, + domainTimeSync, + scanConnection, + retryProvider, + packageVersionSupport, + loggerFactory, + validatorTopupConfigO, + walletSweep, + autoAcceptTransfers, + rewardSharingConfig, + dedupDuration, + params, + ) + } catch { + case NonFatal(e) => + LifeCycle.close(treasury, store)(logger) + throw e + } /** The connection to use when submitting commands based on reads from the WalletStore. * The submission will wait for the store to ingest the effect of the command before completing the future. @@ -132,9 +148,8 @@ class UserWalletService( // Close treasury early, that will result in it no longer accepting new requests // but in-flight requests can complete. If we close the automation first, // a task can get stuck forever waiting for store ingestion to complete. - treasury.close() - automation.close() - store.close() + // LifeCycle.close closes all of them in order even if one of them fails. + LifeCycle.close(treasury, automation, store)(logger) super.onClosed() } } diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandler.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandler.scala index 5219aef9d8..bb5b781c4d 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandler.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandler.scala @@ -557,6 +557,11 @@ class HttpWalletHandler( Codec.tryDecodeJavaContractId(subsCodegen.SubscriptionRequest.COMPANION)( contractId ) + val commandId = CommandId( + "org.lfdecentralizedtrust.splice.wallet.acceptSubscriptionRequest", + Seq(userWallet.store.key.endUserParty), + contractId, + ) retryProvider.retryForClientCalls( "accept_subscription", "Accept subscription and make initial payment", @@ -567,6 +572,13 @@ class HttpWalletHandler( d0.AcceptSubscriptionRequestResponse( Codec.encodeContractId(outcome.contractIdValue) ), + dedupConfig = Some( + AmuletOperationDedupConfig( + commandId, + dedupDuration, + recoverAcceptedDuplicates = true, + ) + ), ), logger, ) @@ -676,6 +688,7 @@ class HttpWalletHandler( AmuletOperationDedupConfig( commandId, dedupDuration, + recoverAcceptedDuplicates = true, ) ), ) @@ -798,6 +811,7 @@ class HttpWalletHandler( ), deduplicationOffset = dedupOffset, ) + .recoveringAcceptedDuplicates() .withSynchronizerId(domain) .yieldResult() .map(_.contractId) @@ -853,6 +867,7 @@ class HttpWalletHandler( body.deduplicationId, ), dedupDuration, + recoverAcceptedDuplicates = true, ) ), ) @@ -876,6 +891,7 @@ class HttpWalletHandler( val dedupConfig = AmuletOperationDedupConfig( commandId, dedupDuration, + recoverAcceptedDuplicates = true, ) (for { result <- userWallet.treasury.enqueueTokenStandardTransferOperationV1( @@ -1061,6 +1077,7 @@ class HttpWalletHandler( val dedupConfig = AmuletOperationDedupConfig( commandId, dedupDuration, + recoverAcceptedDuplicates = true, ) (for { result <- userWallet.treasury.enqueueTokenStandardTransferOperationV2( @@ -1224,6 +1241,7 @@ class HttpWalletHandler( val dedupConfig = AmuletOperationDedupConfig( commandId, dedupDuration, + recoverAcceptedDuplicates = true, ) for { result <- userWallet.treasury.enqueueAmuletAllocationOperation( @@ -1300,6 +1318,7 @@ class HttpWalletHandler( commandId, // Overriden to be low enough (5m) that we allow the same allocation to be re-created after being withdrawn DedupDuration(com.google.protobuf.Duration.newBuilder().setSeconds(5L * 60L).build()), + recoverAcceptedDuplicates = true, ) for { result <- userWallet.treasury.enqueueAmuletAllocationOperation( @@ -1813,6 +1832,7 @@ class HttpWalletHandler( ), deduplicationConfig = dedupDuration, ) + .recoveringAcceptedDuplicates() .withDisclosedContracts( userWallet.connection .disclosedContracts(amuletRules, unclaimedDevelopmentFundCouponsToAllocate*) diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandlerUtil.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandlerUtil.scala index 72ada24586..39d4e2e4e4 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandlerUtil.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandlerUtil.scala @@ -91,6 +91,7 @@ trait HttpWalletHandlerUtil extends Spanning with NamedLogging { priority = priority, ) .withDedup(commandId, dedupConfig) + .recoveringAcceptedDuplicates() .withDisclosedContracts(disclosedContracts(userWallet.connection)) .yieldResult() } diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/MintingDelegationCollectRewardsTrigger.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/MintingDelegationCollectRewardsTrigger.scala index b2e645547d..925ef6ad08 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/MintingDelegationCollectRewardsTrigger.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/MintingDelegationCollectRewardsTrigger.scala @@ -80,6 +80,8 @@ class MintingDelegationCollectRewardsTrigger( materializer: Materializer, ) extends PollingTrigger { + import MintingDelegationCollectRewardsTrigger.* + private def externalParty = store.key.externalParty override protected def extraMetricLabels = Seq("party" -> externalParty.toString) @@ -149,6 +151,20 @@ class MintingDelegationCollectRewardsTrigger( } } + // Handling of unassigned V2 coupons (no beneficiary yet) depends on the sharing mode: + // - No sharing (no beneficiaries, not external): mint them directly to ourselves. + // - InternalSharing (beneficiaries set): hold them back, assign to the configured + // beneficiaries first, then mint; already-assigned coupons mint directly. + // - ExternalSharing: hold them back and leave them untouched, so the + // off-node automation owns their assignment; only already-assigned coupons mint here. + private val mode: SharingMode = + rewardSharingConfig match { + case RewardSharingConfig.External(_) => ExternalSharing + case builtIn: RewardSharingConfig.BuiltIn if builtIn.automateRewardSharing => + InternalSharing(builtIn) + case _: RewardSharingConfig.BuiltIn => NoSharing + } + private def performMintIfNeeded( mintInputs: MintInputs, couponsData: CouponsData, @@ -162,30 +178,29 @@ class MintingDelegationCollectRewardsTrigger( val amuletsToMerge = selectAmuletsToMerge(amulets, mintInputs.delegation) val shouldMergeAmulets = amuletsToMerge.nonEmpty - // Without sharing config, all V2 coupons are mintable directly. - // With sharing config, only assigned-to-us V2 coupons are mintable; - // unassigned ones need sharing first. - val hasBeneficiaries = rewardSharingConfig.beneficiaries.nonEmpty - val (unassignedV2, mintableV2) = - if (hasBeneficiaries) + val (unassignedV2, mintableV2) = mode match { + case NoSharing => (Seq.empty, filteredCouponsData.rewardCouponsV2) + case InternalSharing(_) | ExternalSharing => filteredCouponsData.rewardCouponsV2.partition(_.payload.beneficiary.isEmpty) - else - (Seq.empty, filteredCouponsData.rewardCouponsV2) + } val couponsToMint = filteredCouponsData.copy(rewardCouponsV2 = mintableV2) + val submission = buildMintSubmissionData(mintInputs, couponsToMint, amuletsToMerge) // Share when the TTL threshold is reached, or batch sharing with // amulet merging to reduce traffic costs by combining both in one transaction. - val shouldAssign = unassignedV2.nonEmpty && - (shouldShareNow(unassignedV2, rewardSharingConfig) || shouldMergeAmulets) - - val submission = buildMintSubmissionData(mintInputs, couponsToMint, amuletsToMerge) - if (shouldAssign) { - performAssignAndMint(submission, unassignedV2.toList, rewardSharingConfig) - } else if (couponsToMint.hasRewards || shouldMergeAmulets) { - performMint(submission) - } else { - // Nothing to do: no rewards to mint, coupons to assign, or amulets to merge - Future.successful(false) + val hasSomethingToMint = couponsToMint.hasRewards || shouldMergeAmulets + mode match { + case InternalSharing(config) => + val shouldAssignAndMint = unassignedV2.nonEmpty && (shouldShareNow( + unassignedV2, + config, + ) || shouldMergeAmulets) + if (shouldAssignAndMint) performAssignAndMint(submission, unassignedV2.toList, config) + else if (hasSomethingToMint) performMint(submission) + else Future.successful(false) + case NoSharing | ExternalSharing => + if (hasSomethingToMint) performMint(submission) + else Future.successful(false) } } @@ -213,7 +228,7 @@ class MintingDelegationCollectRewardsTrigger( private def performAssignAndMint( submission: MintSubmissionData, unassignedV2: List[Contract[RewardCouponV2.ContractId, RewardCouponV2]], - config: RewardSharingConfig, + config: RewardSharingConfig.BuiltIn, )(implicit tc: TraceContext): Future[Boolean] = { unassignedV2 match { case Nil => @@ -436,7 +451,7 @@ class MintingDelegationCollectRewardsTrigger( private def shouldShareNow( coupons: Seq[Contract[RewardCouponV2.ContractId, RewardCouponV2]], - config: RewardSharingConfig, + config: RewardSharingConfig.BuiltIn, ): Boolean = { val now = context.clock.now.toInstant val minTtl = config.minTtlAfterSharing.asJava @@ -508,3 +523,10 @@ class MintingDelegationCollectRewardsTrigger( ) } } + +object MintingDelegationCollectRewardsTrigger { + private sealed trait SharingMode + private case object NoSharing extends SharingMode + private final case class InternalSharing(config: RewardSharingConfig.BuiltIn) extends SharingMode + private case object ExternalSharing extends SharingMode +} diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/RewardSharingTrigger.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/RewardSharingTrigger.scala index c9ed70ca64..40e5dff446 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/RewardSharingTrigger.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/RewardSharingTrigger.scala @@ -32,7 +32,7 @@ import scala.jdk.CollectionConverters.* class RewardSharingTrigger( override protected val context: TriggerContext, store: UserWalletStore, - config: RewardSharingConfig, + config: RewardSharingConfig.BuiltIn, spliceLedgerConnection: SpliceLedgerConnection, )(implicit override val ec: ExecutionContext, diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/UserWalletAutomationService.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/UserWalletAutomationService.scala index 9a597b9da4..23be2f128f 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/UserWalletAutomationService.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/UserWalletAutomationService.scala @@ -177,15 +177,18 @@ class UserWalletAutomationService( ) } - if (rewardSharingConfig.beneficiaries.nonEmpty) { - registerTrigger( - new RewardSharingTrigger( - triggerContext, - store, - rewardSharingConfig, - connection(SpliceLedgerConnectionPriority.Low), + rewardSharingConfig match { + case builtIn: RewardSharingConfig.BuiltIn if builtIn.automateRewardSharing => + registerTrigger( + new RewardSharingTrigger( + triggerContext, + store, + builtIn, + connection(SpliceLedgerConnectionPriority.Low), + ) ) - ) + case _: RewardSharingConfig.BuiltIn => () + case _: RewardSharingConfig.External => () } registerTrigger( diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/config/WalletAppConfig.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/config/WalletAppConfig.scala index 75d179a8ce..2cb4f9ba35 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/config/WalletAppConfig.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/config/WalletAppConfig.scala @@ -55,28 +55,55 @@ final case class AppRewardBeneficiaryConfig( percentage: BigDecimal, ) -/** Configuration for sharing traffic-based app reward coupons with beneficiaries. - * @param minTtlAfterSharing minimum remaining coupon TTL before sharing is triggered; - * e.g., 30h means share when 30h of coupon lifetime remains (6h after creation for 36h coupons) - * @param beneficiaries parties to share rewards with and their percentages; - * the provider keeps the remainder (1.0 - sum of percentages) - * @param batchSize maximum number of coupons to share or assign per trigger run - */ -final case class RewardSharingConfig( - minTtlAfterSharing: NonNegativeFiniteDuration = NonNegativeFiniteDuration.ofHours(30), - beneficiaries: Seq[AppRewardBeneficiaryConfig] = Seq.empty, - batchSize: Int = 100, -) { - def providerRemainder: BigDecimal = BigDecimal(1.0) - beneficiaries.map(_.percentage).sum - - @VisibleForTesting - def allBeneficiaries(provider: PartyId): Seq[AppRewardBeneficiaryConfig] = { - val remainder = providerRemainder - beneficiaries ++ - (if (remainder > 0) Seq(AppRewardBeneficiaryConfig(provider, remainder)) - else Seq.empty) +/** How traffic-based app reward coupons are shared with beneficiaries. */ +sealed trait RewardSharingConfig { + def mintUnassignedCoupons: Boolean + def automateRewardSharing: Boolean + def batchSize: Int +} + +object RewardSharingConfig { + + val DefaultBatchSize: Int = 100 + + /** Beneficiary assignment for RewardCouponV2 contracts is managed by a process + * external to the validator app: the validator app must thus leave unassigned + * coupons untouched rather than assigning or minting them itself. + */ + case class External( + batchSize: Int = DefaultBatchSize + ) extends RewardSharingConfig { + override def mintUnassignedCoupons: Boolean = false + override def automateRewardSharing: Boolean = false } - def allDamlBeneficiaries(provider: PartyId): Seq[(PartyId, java.math.BigDecimal)] = - allBeneficiaries(provider).map(b => (b.beneficiary, SpliceUtil.damlDecimal(b.percentage))) + /** The node performs beneficiary assignment and minting itself. + * @param minTtlAfterSharing minimum remaining coupon TTL before sharing is triggered; + * e.g., 30h means share when 30h of coupon lifetime remains (6h after creation for 36h coupons) + * @param beneficiaries parties to share rewards with and their percentages; + * the provider keeps the remainder (1.0 - sum of percentages) + * @param batchSize maximum number of coupons to share or assign per trigger run + */ + final case class BuiltIn( + minTtlAfterSharing: NonNegativeFiniteDuration = NonNegativeFiniteDuration.ofHours(30), + beneficiaries: Seq[AppRewardBeneficiaryConfig] = Seq.empty, + batchSize: Int = DefaultBatchSize, + ) extends RewardSharingConfig { + def providerRemainder: BigDecimal = BigDecimal(1.0) - beneficiaries.map(_.percentage).sum + + @VisibleForTesting + def allBeneficiaries(provider: PartyId): Seq[AppRewardBeneficiaryConfig] = { + val remainder = providerRemainder + beneficiaries ++ + (if (remainder > 0) Seq(AppRewardBeneficiaryConfig(provider, remainder)) + else Seq.empty) + } + + def allDamlBeneficiaries(provider: PartyId): Seq[(PartyId, java.math.BigDecimal)] = + allBeneficiaries(provider).map(b => (b.beneficiary, SpliceUtil.damlDecimal(b.percentage))) + + override def mintUnassignedCoupons: Boolean = beneficiaries.isEmpty + + override def automateRewardSharing: Boolean = beneficiaries.nonEmpty + } } diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/metrics/TreasuryMetrics.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/metrics/TreasuryMetrics.scala new file mode 100644 index 0000000000..95f26e1de9 --- /dev/null +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/metrics/TreasuryMetrics.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.wallet.metrics + +import com.daml.metrics.api.MetricHandle.Gauge.CloseableGauge +import com.daml.metrics.api.MetricHandle.{LabeledMetricsFactory, Timer} +import com.daml.metrics.api.MetricQualification.{Latency, Saturation} +import com.daml.metrics.api.{MetricInfo, MetricName, MetricsContext} +import com.digitalasset.canton.topology.PartyId +import org.lfdecentralizedtrust.splice.environment.SpliceMetrics + +import java.time.Duration + +class TreasuryMetrics( + owner: PartyId, + metricsFactory: LabeledMetricsFactory, + queueSize: () => Long, +) extends AutoCloseable { + private val prefix: MetricName = SpliceMetrics.MetricsPrefix :+ "wallet" :+ "treasury" + + private val metricsContext: MetricsContext = + MetricsContext.Empty.withExtraLabels("owner" -> owner.toString) + + private val queueSizeGauge: CloseableGauge = + metricsFactory.closeableGaugeWithSupplier[Long]( + MetricInfo( + prefix :+ "queue-size", + summary = "Treasury operation queue size", + description = "The number of operations currently queued in the treasury service.", + qualification = Saturation, + ), + queueSize, + )(metricsContext) + + private val queueLatencyTimer: Timer = + metricsFactory.timer( + MetricInfo( + prefix :+ "queue-latency", + summary = "Treasury operation queueing latency", + description = + "The time an operation spent in the queue of the treasury service. Note: This is only time between enqueuing and dequeuing, it excludes actual request processing.", + qualification = Latency, + ) + )(metricsContext) + + def recordQueueLatency(latency: Duration): Unit = + queueLatencyTimer.update(latency)(MetricsContext.Empty) + + override def close(): Unit = queueSizeGauge.close() +} diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/treasury/TreasuryService.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/treasury/TreasuryService.scala index c1162d135f..2ab576f070 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/treasury/TreasuryService.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/treasury/TreasuryService.scala @@ -62,6 +62,7 @@ import org.lfdecentralizedtrust.splice.util.{ } import org.lfdecentralizedtrust.splice.wallet.UserWalletManager import org.lfdecentralizedtrust.splice.wallet.config.TreasuryConfig +import org.lfdecentralizedtrust.splice.wallet.metrics.TreasuryMetrics import org.lfdecentralizedtrust.splice.wallet.store.UserWalletStore import org.lfdecentralizedtrust.splice.wallet.treasury.TreasuryService.* import com.digitalasset.base.error.utils.ErrorDetails @@ -72,6 +73,7 @@ import com.digitalasset.canton.lifecycle.{ AsyncOrSyncCloseable, FlagCloseableAsync, RunOnClosing, + SyncCloseable, } import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.{ @@ -108,7 +110,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.{ transferinstructionv2, } -import java.time.Instant +import java.time.{Duration, Instant} import java.util.Optional import scala.concurrent.{ExecutionContext, Future, Promise} import scala.jdk.CollectionConverters.* @@ -145,61 +147,71 @@ class TreasuryService( // Setting the weight > batch size ensures they go in a batch of their own private val BatchWithOneOperation = treasuryConfig.batchSize.toLong + 1L - private val queue: BoundedSourceQueue[EnqueuedOperation] = { + private val queue: BoundedSourceQueue[QueuedOperation] = { val queue = Source - .queue[EnqueuedOperation](treasuryConfig.queueSize) - .batchWeighted[OperationBatch]( + .queue[QueuedOperation](treasuryConfig.queueSize) + .batchWeighted[QueuedBatch]( treasuryConfig.batchSize.toLong, - { - case amuletOp: EnqueuedAmuletOperation => - if (amuletOp.priority == CommandPriority.High || amuletOp.dedup.isDefined) { + queued => + queued.operation match { + case amuletOp: EnqueuedAmuletOperation => + if (amuletOp.priority == CommandPriority.High || amuletOp.dedup.isDefined) { + BatchWithOneOperation + } else 1L + case _: EnqueuedTokenStandardTransferOperationV1 => BatchWithOneOperation - } else 1L - case _: EnqueuedTokenStandardTransferOperationV1 => - BatchWithOneOperation - case _: EnqueuedTokenStandardTransferOperationV2 => - BatchWithOneOperation - case _: EnqueuedAmuletAllocationOperation => - BatchWithOneOperation - case _: EnqueuedAmuletAllocationV2Operation => - BatchWithOneOperation - }, - { - case amuletOp: EnqueuedAmuletOperation => - AmuletOperationBatch(amuletOp) - case tsOp: EnqueuedTokenStandardTransferOperationV1 => - TokenStandardOperationV1Batch(tsOp) - case tsOp: EnqueuedTokenStandardTransferOperationV2 => - TokenStandardOperationV2Batch(tsOp) - case allOp: EnqueuedAmuletAllocationOperation => - AmuletAllocationOperationBatch(allOp) - case allOp: EnqueuedAmuletAllocationV2Operation => - AmuletAllocationV2OperationBatch(allOp) - }, - ) { - case (batch: AmuletOperationBatch, operation: EnqueuedAmuletOperation) => - batch.addCOToBatch(operation) - case (_: TokenStandardOperationV1Batch, _: EnqueuedTokenStandardTransferOperationV1) | - (_: TokenStandardOperationV2Batch, _: EnqueuedTokenStandardTransferOperationV2) => - throw new IllegalStateException( - "Token standard batches cannot contain more than one element. This is a bug." - ) - case (batch, operation) => - throw new IllegalStateException( - s"Batch is ${batch.getClass.getName} while operation is ${operation.getClass.getName}. This is a bug." - ) + case _: EnqueuedTokenStandardTransferOperationV2 => + BatchWithOneOperation + case _: EnqueuedAmuletAllocationOperation => + BatchWithOneOperation + case _: EnqueuedAmuletAllocationV2Operation => + BatchWithOneOperation + }, + queued => + QueuedBatch( + queued.operation match { + case amuletOp: EnqueuedAmuletOperation => + AmuletOperationBatch(amuletOp) + case tsOp: EnqueuedTokenStandardTransferOperationV1 => + TokenStandardOperationV1Batch(tsOp) + case tsOp: EnqueuedTokenStandardTransferOperationV2 => + TokenStandardOperationV2Batch(tsOp) + case allOp: EnqueuedAmuletAllocationOperation => + AmuletAllocationOperationBatch(allOp) + case allOp: EnqueuedAmuletAllocationV2Operation => + AmuletAllocationV2OperationBatch(allOp) + }, + Vector(queued.enqueuedAt), + ), + ) { (queuedBatch, queued) => + val batch = (queuedBatch.batch, queued.operation) match { + case (batch: AmuletOperationBatch, operation: EnqueuedAmuletOperation) => + batch.addCOToBatch(operation) + case (_: TokenStandardOperationV1Batch, _: EnqueuedTokenStandardTransferOperationV1) | + (_: TokenStandardOperationV2Batch, _: EnqueuedTokenStandardTransferOperationV2) => + throw new IllegalStateException( + "Token standard batches cannot contain more than one element. This is a bug." + ) + case (batch, operation) => + throw new IllegalStateException( + s"Batch is ${batch.getClass.getName} while operation is ${operation.getClass.getName}. This is a bug." + ) + } + QueuedBatch(batch, queuedBatch.enqueuedAts :+ queued.enqueuedAt) } - // Execute the batches sequentially to avoid contention - .mapAsync(1) { - case amuletBatch: AmuletOperationBatch => filterAndExecuteBatch(amuletBatch) - case TokenStandardOperationV1Batch(operation) => - executeTokenStandardTransferOperationV1(operation) - case TokenStandardOperationV2Batch(operation) => - executeTokenStandardTransferOperationV2(operation) - case AmuletAllocationOperationBatch(operation) => - executeAmuletAllocationOperation(operation) - case AmuletAllocationV2OperationBatch(operation) => - executeAmuletAllocationV2Operation(operation) + .mapAsync(1) { queuedBatch => + recordQueueLatencies(queuedBatch) + queuedBatch.batch match { + case amuletBatch: AmuletOperationBatch => filterAndExecuteBatch(amuletBatch) + case TokenStandardOperationV1Batch(operation) => + executeTokenStandardTransferOperationV1(operation) + case TokenStandardOperationV2Batch(operation) => + executeTokenStandardTransferOperationV2(operation) + case AmuletAllocationOperationBatch(operation) => + executeAmuletAllocationOperation(operation) + case AmuletAllocationV2OperationBatch(operation) => + executeAmuletAllocationV2Operation(operation) + } } .toMat( Sink.onComplete(result0 => { @@ -218,6 +230,12 @@ class TreasuryService( queue } + private val metrics: TreasuryMetrics = new TreasuryMetrics( + userStore.key.endUserParty, + retryProvider.metricsFactory, + () => queue.size().toLong, + ) + retryProvider.runOnOrAfterClose_(new RunOnClosing { override def name: String = s"terminate amulet operation batch executor" override def done: Boolean = queueTerminationResult.isCompleted @@ -239,7 +257,8 @@ class TreasuryService( "waiting for amulet operation batch executor shutdown", queueTerminationResult.future, timeouts.shutdownShort, - ) + ), + SyncCloseable("treasury metrics", metrics.close()), ) override def isHealthy: Boolean = !queueTerminationResult.isCompleted @@ -409,7 +428,7 @@ class TreasuryService( show"Received operation (queue size before adding this: ${queue.size()}): $operation" ) queue.offer( - operation + QueuedOperation(operation, clock.now) ) match { case Enqueued => logger.debug(show"Operation $operation enqueued successfully") @@ -431,6 +450,13 @@ class TreasuryService( } } + private def recordQueueLatencies(queuedBatch: QueuedBatch): Unit = { + val now = clock.now.toInstant + queuedBatch.enqueuedAts.foreach(enqueuedAt => + metrics.recordQueueLatency(Duration.between(enqueuedAt.toInstant, now)) + ) + } + private def closingException(operation: EnqueuedOperation) = Status.UNAVAILABLE .withDescription( @@ -648,7 +674,10 @@ class TreasuryService( (offset, result) <- batch.dedup match { case None => baseSubmission.noDedup.yieldResultAndOffset() case Some(dedup) => - baseSubmission.withDedup(dedup.commandId, dedup.config).yieldResultAndOffset() + baseSubmission + .withDedup(dedup.commandId, dedup.config) + .recoveringAcceptedDuplicates(dedup.recoverAcceptedDuplicates) + .yieldResultAndOffset() } // wait for store to ingest the new amulet holdings, then return all outcomes to the callers @@ -813,7 +842,10 @@ class TreasuryService( (offset, result) <- operation.dedup match { case None => baseSubmission.noDedup.yieldResultAndOffset() case Some(dedup) => - baseSubmission.withDedup(dedup.commandId, dedup.config).yieldResultAndOffset() + baseSubmission + .withDedup(dedup.commandId, dedup.config) + .recoveringAcceptedDuplicates(dedup.recoverAcceptedDuplicates) + .yieldResultAndOffset() } _ <- userStore.signalWhenIngestedOrShutdown(offset) } yield { @@ -1443,6 +1475,11 @@ object TreasuryService { ) } + private case class QueuedOperation(operation: EnqueuedOperation, enqueuedAt: CantonTimestamp) + + /** A batch together with the times at which its operations were put on the treasury queue. */ + private case class QueuedBatch(batch: OperationBatch, enqueuedAts: Vector[CantonTimestamp]) + private sealed trait EnqueuedOperation extends PrettyPrinting { type Result val outcomePromise: Promise[Result] @@ -1559,9 +1596,14 @@ object TreasuryService { } } + /** @param recoverAcceptedDuplicates + * set by client calls, so that a duplicate of an already-accepted submission returns the + * original result. Automation leaves it off and lets its own retry handle the duplicate. + */ final case class AmuletOperationDedupConfig( commandId: CommandId, config: DedupConfig, + recoverAcceptedDuplicates: Boolean = false, ) extends PrettyPrinting { override def pretty: Pretty[AmuletOperationDedupConfig.this.type] = prettyNode("DedupConfig", param("commandId", _.commandId), param("config", _.config)) diff --git a/build-tools/artifactory_to_gcs.py b/build-tools/artifactory_to_gcs.py deleted file mode 100755 index 086baff5ba..0000000000 --- a/build-tools/artifactory_to_gcs.py +++ /dev/null @@ -1,785 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -""" -Script to copy non-snapshot versions of Docker images or Helm charts -from JFrog Artifactory to Google Cloud Storage. - -Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -SPDX-License-Identifier: Apache-2.0 -""" - -import argparse -import json -import os -import re -import subprocess -import sys -import tempfile -import time -import urllib.parse -from dataclasses import dataclass -from typing import Dict, List, Optional, Set, Tuple, Union -import logging -import requests -from google.cloud import storage -from google.api_core import exceptions as gcp_exceptions - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -# Define version regex pattern (matches semantic versioning) -VERSION_PATTERN = re.compile(r'^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$') -# Regex to match git hash versions (git- followed by a hash) -GIT_HASH_PATTERN = re.compile(r'^git-[0-9a-f]{5,40}$') - -@dataclass -class ArtifactInfo: - """Information about an artifact (Docker image or Helm chart)""" - name: str - version: str - path: str - repository: str - type: str # 'docker' or 'helm' - is_snapshot: bool = False - - def __str__(self) -> str: - return f"{self.name}:{self.version} ({self.type})" - - -def parse_args(): - """Parse command line arguments.""" - parser = argparse.ArgumentParser( - description="Copy non-snapshot Docker images or Helm charts from JFrog Artifactory to Google Cloud Storage" - ) - parser.add_argument( - "--artifactory-url", required=True, - help="JFrog Artifactory URL (e.g., https://artifactory.example.com)" - ) - parser.add_argument( - "--repository", required=True, - help="Artifactory repository name containing Docker images or Helm charts" - ) - parser.add_argument( - "--bucket", required=True, - help="Google Cloud Storage bucket name" - ) - parser.add_argument( - "--type", choices=["docker", "helm"], required=True, - help="Artifact type (docker or helm)" - ) - parser.add_argument( - "--api-key", - help="JFrog Artifactory API key (can also be set via ARTIFACTORY_API_KEY environment variable)" - ) - parser.add_argument( - "--username", - help="JFrog Artifactory username (can also be set via ARTIFACTORY_USERNAME environment variable)" - ) - parser.add_argument( - "--dry-run", action="store_true", - help="Show what would be copied without actually copying" - ) - parser.add_argument( - "--force", action="store_true", - help="Skip confirmation prompts" - ) - parser.add_argument( - "--allow-overwrite", action="store_true", - help="Allow overwriting existing files in the bucket" - ) - parser.add_argument( - "--prefix", default="", - help="Prefix to add to the destination path in the bucket" - ) - parser.add_argument( - "--max-retries", type=int, default=5, - help="Maximum number of retries for failed operations" - ) - parser.add_argument( - "--verbose", action="store_true", - help="Enable verbose logging" - ) - parser.add_argument( - "--include-path-pattern", - help="Regular expression to filter artifacts by path" - ) - parser.add_argument( - "--exclude-path-pattern", - help="Regular expression to exclude artifacts by path" - ) - parser.add_argument( - "--policy-file", default="skopeo_policy.json", - help="Path to the skopeo policy JSON file" - ) - parser.add_argument( - "--save-artifacts-file", - help="Save fetched artifacts to a file for later reuse" - ) - parser.add_argument( - "--load-artifacts-file", - help="Load artifacts from a file instead of fetching from Artifactory" - ) - parser.add_argument( - "--docker-registry-url", required=True, - help="External URL of the Docker registry (e.g., myregistry.jfrog.io), required for skopeo copy command" - ) - return parser.parse_args() - - -def check_credentials(args): - """Check and retrieve credentials for Artifactory and Google Cloud.""" - # Check Artifactory credentials - artifactory_api_key = args.api_key or os.environ.get('ARTIFACTORY_API_KEY') - artifactory_username = args.username or os.environ.get('ARTIFACTORY_USERNAME') - - if not artifactory_api_key and not artifactory_username: - logger.error("Artifactory credentials not provided. Set --api-key/--username or ARTIFACTORY_API_KEY/ARTIFACTORY_USERNAME environment variables.") - sys.exit(1) - - # Verify Google Cloud credentials are available - try: - # Attempt to initialize a client to check if credentials are available - storage.Client() - except Exception as e: - logger.error(f"Google Cloud credentials not found or invalid: {e}") - logger.error("Make sure you have set up Google Cloud credentials (run 'gcloud auth application-default login')") - sys.exit(1) - - return { - "artifactory_api_key": artifactory_api_key, - "artifactory_username": artifactory_username - } - - -def fetch_artifacts(artifactory_url: str, repository: str, artifact_type: str, credentials: Dict, - include_pattern: Optional[str] = None, exclude_pattern: Optional[str] = None) -> List[ArtifactInfo]: - """ - Fetch non-snapshot artifacts from Artifactory. - - Args: - artifactory_url: Base URL of the Artifactory instance - repository: Repository name in Artifactory - artifact_type: 'docker' or 'helm' - credentials: Dictionary containing authentication credentials - include_pattern: Optional regex pattern to include artifacts - exclude_pattern: Optional regex pattern to exclude artifacts - - Returns: - List of ArtifactInfo objects representing non-snapshot artifacts - """ - - logger.info(f"Fetching {artifact_type} artifacts from {artifactory_url}/artifactory/{repository}") - - api_url = f"{artifactory_url}/artifactory/api/storage/{repository}" - headers = {} - - # Set up authentication - if credentials.get("artifactory_api_key"): - headers["X-JFrog-Art-Api"] = credentials["artifactory_api_key"] - elif credentials.get("artifactory_username"): - auth = requests.auth.HTTPBasicAuth( - credentials["artifactory_username"], - os.environ.get("ARTIFACTORY_PASSWORD", "") - ) - else: - auth = None - - # Compile regex patterns if provided - include_regex = re.compile(include_pattern) if include_pattern else None - exclude_regex = re.compile(exclude_pattern) if exclude_pattern else None - - # For recursive traversal of the repository - artifacts = [] - - def traverse_directory(path: str, current_depth: int = 0, current_name: str = None): - """ - Traverse directory recursively with optimized strategy: - - For Docker: Only go into version-named directories, stop once manifest.json is found - - For Helm: Regular traversal looking for .tgz files - - Args: - path: Current path in Artifactory repository - current_depth: Track depth in traversal for Docker optimization - current_name: Keep track of the container/chart name while traversing - """ - url = f"{api_url}{path}" - try: - if credentials.get("artifactory_api_key"): - response = requests.get(url, headers=headers) - else: - response = requests.get(url, auth=auth) - - response.raise_for_status() - data = response.json() - - for child in data.get("children", []): - child_path = f"{path}/{child['uri']}" if path else child["uri"] - - if logger.level <= logging.DEBUG: - logger.debug(f"Examining path: {child_path}") - - # Skip if path doesn't match include pattern - if include_regex and not include_regex.search(child_path): - continue - - # Skip if path matches exclude pattern - if exclude_regex and exclude_regex.search(child_path): - continue - - # For Docker repositories, use optimized traversal strategy - if artifact_type == "docker": - if child["folder"]: - # Skip any sha256 directories - if "/sha256:" in child_path or "sha256:" in child_path or "sha256__" in child_path or GIT_HASH_PATTERN.match(child_path.split("/")[-1]): - logger.debug(f"Skipping directory: {child_path}") - continue - - # Get the folder name (the last part of the path) - folder_name = child['uri'].strip("/") - - # Check if this folder looks like a semantic version - is_version = VERSION_PATTERN.match(folder_name) is not None - is_version = is_version and GIT_HASH_PATTERN.match(folder_name) is None - - # If it's a version directory, traverse into it but remember we're in a version dir - # Otherwise, continue regular traversal without changing depth - next_depth = current_depth + 1 if is_version else current_depth - - # If we're at depth 0, this is potentially a container name folder - next_name = folder_name if current_depth == 0 else current_name - - traverse_directory(child_path, next_depth, next_name) - # For files, only consider manifest files in version directories (depth = 1) - elif current_depth == 1 and ('manifest.json' in child['uri'] or child['uri'] == 'manifest.json'): - # We found a manifest in a version directory - this is what we're looking for - logger.debug(f"Found manifest in version directory: {child_path}") - - # Extract version from path (should be the directory containing manifest.json) - # Handle paths that might have double slashes by normalizing the path first - normalized_path = child_path.replace("//", "/").strip("/") - logger.debug(f"Found normalized path: {normalized_path}") - path_parts = [part for part in normalized_path.split("/") if part] - if len(path_parts) < 2: - continue - logger.debug(f"Found normalized path: {path_parts}") - version = path_parts[-2] # Version is the directory containing manifest.json - - name = "/".join(path_parts[:-2]) - if not name: - logger.debug(f"Could not determine artifact name from path: {normalized_path}") - continue - - # Create artifact if it's a valid semantic version - if VERSION_PATTERN.match(version): - is_snapshot = "SNAPSHOT" in version or "-snapshot" in version.lower() - if not is_snapshot: - logger.debug(f"Found Docker artifact - Name: {name}, Version: {version}") - artifact = ArtifactInfo( - name=name, - version=version, - path=child_path, - repository=repository, - type=artifact_type, - is_snapshot=False - ) - logger.debug(f"Adding artifact: {artifact}") - artifacts.append(artifact) - - # For Helm repositories, use regular traversal looking for .tgz files - else: - if child["folder"]: - traverse_directory(child_path, current_depth + 1, current_name) - elif is_version_artifact(child_path, artifact_type): - logger.debug(f"Found potential helm artifact: {child_path}") - artifact = get_artifact_details(child_path, artifactory_url, repository, - artifact_type, credentials) - if artifact and not artifact.is_snapshot: - logger.debug(f"Adding helm artifact: {artifact}") - artifacts.append(artifact) - - except requests.RequestException as e: - logger.error(f"Error fetching artifacts from {url}: {e}") - - # Start traversal from the repository root - traverse_directory("") - - logger.info(f"Found {len(artifacts)} non-snapshot {artifact_type} artifacts") - return artifacts - - -def is_version_artifact(path: str, artifact_type: str) -> bool: - """ - Check if the path appears to point to a versioned artifact. - - This is a preliminary check before fetching detailed metadata. - """ - if artifact_type == "docker": - # Docker repositories typically have a manifest.json file for each tag - return path.endswith("manifest.json") - elif artifact_type == "helm": - # Helm charts typically have a .tgz extension - return path.endswith(".tgz") - - return False - - -def get_artifact_details(path: str, artifactory_url: str, repository: str, - artifact_type: str, credentials: Dict) -> Optional[ArtifactInfo]: - """ - Get detailed information about an artifact. - - Args: - path: Path to the artifact within the repository - artifactory_url: Base URL of the Artifactory instance - repository: Repository name in Artifactory - artifact_type: 'docker' or 'helm' - credentials: Dictionary containing authentication credentials - - Returns: - ArtifactInfo object if the artifact is a valid release version, None otherwise - """ - # Extract version from the path - if artifact_type == "docker": - # For Docker, extract from path like "myimage/1.2.3/manifest.json" - path_parts = path.strip("/").split("/") - if len(path_parts) < 2: - return None - - # The version should be the part before the manifest.json - potential_version = path_parts[-2] - # The name is everything except the version and manifest.json - name = "/".join(path_parts[:-2]) - - elif artifact_type == "helm": - # For Helm, extract from path like "charts/mychart-1.2.3.tgz" - if not path.endswith(".tgz"): - return None - - # Extract the filename without extension - filename = os.path.basename(path)[:-4] # Remove .tgz - - # Try to split name and version (usually separated by a hyphen) - name_version = filename.rsplit("-", 1) - if len(name_version) != 2: - return None - - name = name_version[0] - potential_version = name_version[1] - else: - return None - - # Verify this is a semantic version and not a snapshot - if not VERSION_PATTERN.match(potential_version): - return None - - is_snapshot = "SNAPSHOT" in potential_version or "-snapshot" in potential_version.lower() - - return ArtifactInfo( - name=name, - version=potential_version, - path=path, - repository=repository, - type=artifact_type, - is_snapshot=is_snapshot - ) - - -def check_destination_exists(bucket_name: str, prefix: str, repository: str) -> bool: - """ - Check if the destination folder already exists in the bucket. - - Args: - bucket_name: Name of the GCS bucket - prefix: Optional prefix to add to the destination path - repository: Repository name which will be part of the destination path - - Returns: - True if the destination folder exists and contains files, False otherwise - """ - client = storage.Client() - - try: - bucket = client.get_bucket(bucket_name) - except gcp_exceptions.NotFound: - logger.warning(f"Bucket {bucket_name} not found. Will be created if not in dry-run mode.") - return False - - # Construct the destination prefix - destination = f"{prefix}/{repository}".strip("/") - if not destination: - # If we're copying to the root of the bucket, we need to check if there are any files - blobs = list(bucket.list_blobs(max_results=1)) - return len(blobs) > 0 - - # Check if there are any objects with this prefix - blobs = list(bucket.list_blobs(prefix=destination, max_results=1)) - return len(blobs) > 0 - - -def download_artifact(artifact: ArtifactInfo, artifactory_url: str, credentials: Dict, policy_file: str, docker_registry_url: str) -> str: - """ - Download an artifact from Artifactory to a temporary file. - - Args: - artifact: ArtifactInfo object for the artifact to download - artifactory_url: Base URL of the Artifactory instance - credentials: Dictionary containing authentication credentials - policy_file: Path to the skopeo policy JSON file - docker_registry_url: External Docker registry URL (required for Docker artifacts) - - Returns: - Path to the downloaded temporary file - - Raises: - Exception: If download fails - """ - # For docker, we need to construct the download URL differently - if artifact.type == "docker": - # We need to download the Docker image using skopeo - # First create a temp directory - temp_dir = tempfile.mkdtemp() - # Construct the Docker source URL - docker_url = f"{docker_registry_url}/{artifact.name}:{artifact.version}" - - # Prepare credentials for skopeo - creds_args = [] - if credentials.get("artifactory_username"): - creds_args = ["--src-creds", f"{credentials['artifactory_username']}:{os.environ.get('ARTIFACTORY_PASSWORD', '')}"] - - # Run skopeo to download the image - cmd = [ - "skopeo", "copy", "--policy" , policy_file, "--all", - f"docker://{docker_url}", - f"dir:{temp_dir}", - *creds_args - ] - - logger.debug(f"Running command: {' '.join(cmd)}") - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode != 0: - logger.error(f"Failed to download Docker image: {result.stderr}") - raise Exception(f"Failed to download {artifact}: {result.stderr}") - - return temp_dir - - else: # Helm chart - # Construct the download URL - download_url = f"{artifactory_url}/artifactory/{artifact.repository}/{artifact.path}" - - # Prepare authentication - if credentials.get("artifactory_api_key"): - headers = {"X-JFrog-Art-Api": credentials["artifactory_api_key"]} - auth = None - else: - headers = {} - auth = requests.auth.HTTPBasicAuth( - credentials["artifactory_username"], - os.environ.get("ARTIFACTORY_PASSWORD", "") - ) - - # Download the file - response = requests.get(download_url, headers=headers, auth=auth, stream=True) - response.raise_for_status() - - # Create a temporary file and write the content - fd, temp_path = tempfile.mkstemp() - with os.fdopen(fd, 'wb') as temp_file: - for chunk in response.iter_content(chunk_size=8192): - temp_file.write(chunk) - - return temp_path - - -def upload_to_gcs(local_path: str, artifact: ArtifactInfo, bucket_name: str, - prefix: str, allow_overwrite: bool) -> bool: - """ - Upload an artifact to Google Cloud Storage. - - Args: - local_path: Path to the local file or directory to upload - artifact: ArtifactInfo object for the artifact being uploaded - bucket_name: Name of the GCS bucket - prefix: Optional prefix to add to the destination path - allow_overwrite: Whether to allow overwriting existing files - - Returns: - True if upload was successful, False otherwise - """ - client = storage.Client() - - try: - bucket = client.get_bucket(bucket_name) - except gcp_exceptions.NotFound: - logger.warning(f"Bucket {bucket_name} not found, creating it...") - bucket = client.create_bucket(bucket_name) - - # Construct the destination path - destination_base = f"{prefix}/{artifact.repository}/{artifact.name}/{artifact.version}".strip("/") - - if artifact.type == "docker": - # For Docker, we need to upload all files in the directory - success = True - for root, _, files in os.walk(local_path): - for file in files: - local_file_path = os.path.join(root, file) - rel_path = os.path.relpath(local_file_path, local_path) - destination_path = f"{destination_base}/{rel_path}" - - blob = bucket.blob(destination_path) - - # Check if the blob already exists - if blob.exists() and not allow_overwrite: - logger.warning(f"File {destination_path} already exists in bucket and overwrite not allowed") - success = False - continue - - # Upload the file - blob.upload_from_filename(local_file_path) - logger.info(f"Uploaded {rel_path} to gs://{bucket_name}/{destination_path}") - - return success - else: # Helm chart - # For Helm charts, just upload the single file - chart_name = os.path.basename(artifact.path) - destination_path = f"{destination_base}/{chart_name}" - - blob = bucket.blob(destination_path) - - # Check if the blob already exists - if blob.exists() and not allow_overwrite: - logger.warning(f"File {destination_path} already exists in bucket and overwrite not allowed") - return False - - # Upload the file - blob.upload_from_filename(local_path) - logger.info(f"Uploaded {chart_name} to gs://{bucket_name}/{destination_path}") - - return True - - -def process_artifact(artifact: ArtifactInfo, artifactory_url: str, docker_registry_url: str, bucket_name: str, - prefix: str, credentials: Dict, policy_file: str, allow_overwrite: bool, - dry_run: bool, max_retries: int) -> bool: - """ - Process a single artifact - download and upload to GCS. - - Args: - artifact: ArtifactInfo object for the artifact to process - artifactory_url: Base URL of the Artifactory instance - bucket_name: Name of the GCS bucket - prefix: Optional prefix to add to the destination path - credentials: Dictionary containing authentication credentials - policy_file: Path to the skopeo policy JSON file - allow_overwrite: Whether to allow overwriting existing files - dry_run: If True, don't actually download or upload - max_retries: Maximum number of retry attempts - - Returns: - True if successful, False otherwise - """ - if dry_run: - logger.info(f"Would copy {artifact} to gs://{bucket_name}/{prefix}/{artifact.repository}/{artifact.name}/{artifact.version}") - return True - - temp_path = None - success = False - - for attempt in range(max_retries): - try: - # Download the artifact - logger.info(f"Downloading {artifact}") - temp_path = download_artifact(artifact, artifactory_url, credentials, policy_file, docker_registry_url) - - # Upload to GCS - logger.info(f"Uploading {artifact} to gs://{bucket_name}") - success = upload_to_gcs(temp_path, artifact, bucket_name, prefix, allow_overwrite) - - if success: - break - - except Exception as e: - logger.warning(f"Attempt {attempt + 1}/{max_retries} failed: {e}") - if attempt == max_retries - 1: - logger.error(f"Failed to process {artifact} after {max_retries} attempts") - return False - - # Wait before retrying - time.sleep(2 ** attempt) # Exponential backoff - - finally: - # Clean up temporary files - if temp_path: - if artifact.type == "docker": - # Remove the directory - subprocess.run(["rm", "-rf", temp_path], check=False) - else: - # Remove the file - os.remove(temp_path) - - return success -def save_artifacts_to_file(artifacts: List[ArtifactInfo], filename: str): - """Save artifacts to a JSON file for later reuse.""" - artifacts_data = [ - { - "name": a.name, - "version": a.version, - "path": a.path, - "repository": a.repository, - "type": a.type, - "is_snapshot": a.is_snapshot - } - for a in artifacts - ] - - with open(filename, 'w') as f: - json.dump(artifacts_data, f, indent=2) - - logger.info(f"Saved {len(artifacts)} artifacts to {filename}") - - -def load_artifacts_from_file(filename: str) -> List[ArtifactInfo]: - """Load artifacts from a previously saved JSON file.""" - with open(filename, 'r') as f: - artifacts_data = json.load(f) - - artifacts = [ - ArtifactInfo( - name=item["name"], - version=item["version"], - path=item["path"], - repository=item["repository"], - type=item["type"], - is_snapshot=item.get("is_snapshot", False) - ) - for item in artifacts_data - ] - - logger.info(f"Loaded {len(artifacts)} artifacts from {filename}") - return artifacts - - -def main(): - args = parse_args() - - if args.verbose: - logger.setLevel(logging.DEBUG) - - logger.info("Starting artifact copy from JFrog Artifactory to Google Cloud Storage") - - # Check credentials - credentials = check_credentials(args) - artifacts = [] - if args.load_artifacts_file: - artifacts = load_artifacts_from_file(args.load_artifacts_file) - else: - # Fetch artifacts from Artifactory - artifacts = fetch_artifacts( - args.artifactory_url, - args.repository, - args.type, - credentials, - args.include_path_pattern, - args.exclude_path_pattern - ) - if args.save_artifacts_file: - save_artifacts_to_file(artifacts, args.save_artifacts_file) - - if not artifacts: - logger.warning(f"No non-snapshot {args.type} artifacts found in {args.repository}") - return - - # Check if destination exists - destination_exists = check_destination_exists(args.bucket, args.prefix, args.repository) - - # In dry-run mode, just print what would be copied - if args.dry_run: - print("\nThe following artifacts would be copied:") - for artifact in artifacts: - print(f" {artifact} -> gs://{args.bucket}/{args.prefix}/{args.repository}/{artifact.name}/{artifact.version}") - print(f"\nTotal: {len(artifacts)} artifacts") - - if destination_exists: - print("\nWARNING: Destination folder already exists and may contain files that would be overwritten.") - - return - - # Ask for confirmation - if not args.force: - if destination_exists and not args.allow_overwrite: - print("\nWARNING: Destination folder already exists and may contain files.") - print("Use --allow-overwrite to allow overwriting existing files.") - print("Use --force to skip this confirmation.") - response = input("\nDo you want to continue without overwriting? (yes/no): ").lower() - else: - print(f"\nAbout to copy {len(artifacts)} artifacts from {args.artifactory_url}/artifactory/{args.repository}") - print(f"to gs://{args.bucket}/{args.prefix}/{args.repository}/") - - if destination_exists and args.allow_overwrite: - print("\nWARNING: Destination folder exists and files may be overwritten.") - - print("\nArtifacts to be copied:") - for i, artifact in enumerate(artifacts[:10]): # Show first 10 - print(f" {artifact}") - - if len(artifacts) > 10: - print(f" ... and {len(artifacts) - 10} more") - - response = input("\nDo you want to continue? (yes/no): ").lower() - - if response != "yes": - print("Operation cancelled.") - return - - # Process all artifacts - success_count = 0 - failed_artifacts = [] - - for i, artifact in enumerate(artifacts, 1): - logger.info(f"Processing artifact {i}/{len(artifacts)}: {artifact}") - - success = process_artifact( - artifact, - args.artifactory_url, - args.docker_registry_url, - args.bucket, - args.prefix, - credentials, - args.policy_file, - args.allow_overwrite, - False, # Not dry-run - args.max_retries - ) - - if success: - success_count += 1 - else: - failed_artifacts.append(artifact) - - # Print summary - print("\nCopy operation completed:") - print(f" Successfully copied: {success_count}/{len(artifacts)} artifacts") - - if failed_artifacts: - print(f" Failed to copy: {len(failed_artifacts)} artifacts") - print("\nFailed artifacts:") - for artifact in failed_artifacts: - print(f" {artifact}") - - if success_count == len(artifacts): - print("\nAll artifacts copied successfully!") - else: - print("\nNot all artifacts were copied successfully. See log for details.") - sys.exit(1) - - -if __name__ == "__main__": - main() - diff --git a/build-tools/bump-canton.sh b/build-tools/bump-canton.sh index 87d05201d1..f3c01b1ef3 100755 --- a/build-tools/bump-canton.sh +++ b/build-tools/bump-canton.sh @@ -35,6 +35,6 @@ set_value oss_sha256 "$oss_nix256" for img in base participant mediator sequencer; do _info "Fetching image sha256 for canton-$img..." - sha=$(skopeo inspect --override-os linux --override-arch amd64 "docker://europe-docker.pkg.dev/da-images/public-all/docker/canton-$img:${NEW_VERSION}" --format '{{.Digest}}') + sha=$(skopeo inspect --no-creds --override-os linux --override-arch amd64 "docker://europe-docker.pkg.dev/da-images/public-all/docker/canton-$img:${NEW_VERSION}" --format '{{.Digest}}') set_value "canton_${img}_image_sha256" "$sha" done diff --git a/build-tools/cncluster b/build-tools/cncluster index 23c58d86e0..0e7a5e0114 100755 --- a/build-tools/cncluster +++ b/build-tools/cncluster @@ -312,7 +312,6 @@ declare -A subcommand_whitelist source "${TOOLS_LIB}/pulumi-helpers" source "${TOOLS_LIB}/pulumi-commands" -source "${TOOLS_LIB}/hard-domain-migration-commands" source "${TOOLS_LIB}/logical-synchronizer-upgrade-commands" source "${TOOLS_LIB}/disaster-recovery-commands" @@ -1949,7 +1948,7 @@ function subcmd_restore_node() { _info "Silencing SV report creation alerts for $sv_name for 1 hour" subcmd_silence_grafana_alerts "1 hour" "alertname=Report Creation Time Lag" "report_publisher=$sv_name" _info "Restoring sv node $node" - SPLICE_SV=$node SPLICE_MIGRATION_ID=$migration_id "$SPLICE_ROOT"/cluster/scripts/node-restore.sh $force "$node" "$migration_id" "$backup_run_id" cometbft sequencer participant mediator cn-apps + SPLICE_SV=$node SPLICE_MIGRATION_ID=$migration_id "$SPLICE_ROOT"/cluster/scripts/node-restore.sh $force "$node" "$migration_id" "$backup_run_id" cometbft cantonBft sequencer participant mediator cn-apps ;; validator1|splitwell) _info "Restoring validator node $node" @@ -2337,23 +2336,36 @@ function subcmd_psql() { local APPLICATION="$2" _info "Retrieving DB connection info from pod description..." - local DB_INIT_COMMAND - DB_INIT_COMMAND=$( + local DB_INIT_SCRIPT + DB_INIT_SCRIPT=$( kubectl get deployment \ --namespace "${NAMESPACE}" \ --selector "app=${APPLICATION}" \ -o 'jsonpath={..spec.template.spec.initContainers[0].command}' \ - | jq 'join(" ")' \ - | grep -i psql \ - | sed -nE 's/^.*(psql.?*) 2>&1.*$/\1/p' + | jq -r 'join("\n")' ) + # There can be multiple psql invocations, pick the one that contains create database. + local DB_INIT_COMMAND + DB_INIT_COMMAND=$( + grep -F 'create database' <<< "${DB_INIT_SCRIPT}" \ + | sed -nE 's/^.*(psql .*) 2>&1.*$/\1/p' \ + | tail -n 1 + ) || true + if [ -z "${DB_INIT_COMMAND}" ]; then _error "Application ${APPLICATION} in namespace ${NAMESPACE} does not have an associated database." exit 1 fi - eval "set -- $DB_INIT_COMMAND" + # The participant does not directly inline the db name so we resolve things here. + local DB_INIT_VARS + DB_INIT_VARS=$( + sed -nE "s/^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*='[^']*')[[:space:]]*$/local \1/p" <<< "${DB_INIT_SCRIPT}" + ) || true + + eval "${DB_INIT_VARS} + set -- ${DB_INIT_COMMAND}" while [ $# -gt 0 ]; do case "$1" in -h|--host) diff --git a/build-tools/copy_release_helm_charts_to_ghcr.sh b/build-tools/copy_release_helm_charts_to_ghcr.sh index db84c501dd..1012bd2521 100755 --- a/build-tools/copy_release_helm_charts_to_ghcr.sh +++ b/build-tools/copy_release_helm_charts_to_ghcr.sh @@ -3,7 +3,7 @@ # Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Copies release helm charts for app charts defined in app_charts_file from artifactory to ghcr.io +# Copies release helm charts for app charts defined in app_charts_file from dev to release in ghcr set -eou pipefail VERSION="" diff --git a/build-tools/copy_release_images_to_ghcr.sh b/build-tools/copy_release_images_to_ghcr.sh index a41574a64e..867315d822 100755 --- a/build-tools/copy_release_images_to_ghcr.sh +++ b/build-tools/copy_release_images_to_ghcr.sh @@ -3,7 +3,7 @@ # Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Copies release docker images from artifactory to ghcr.io +# Copies release docker images from dev to release to ghcr.io # skopeo is used to copy multi-arch images correctly # Note: skopeo in nix does not work, complains about policy.json, version from brew install (skopeo version 1.17.0) works fine. # There could be a fix here, though it is a super old link ¯\_(ツ)_/¯: https://github.com/NixOS/nixpkgs/commit/365d07cea0446cbdc3d2c89502ce62c1f283989b @@ -85,8 +85,6 @@ for VERSION in $VERSIONS; do TARGET_IMAGE="$DEST_REGISTRY/$IMAGE_NAME:$TAG" for i in {1..10}; do - # Some images have been copied before from Artifactory, which is not used anymore. - # Artifactory has unknown/unknown attestation manifests, which show up as unknown/unknown os/architecture manifests. There is nothing inherently wrong with this. # skopeo on nix does not bundle the policy.json file, so we need to provide it. if skopeo copy --policy "${SPLICE_ROOT}"/build-tools/skopeo_policy.json --all docker://"$SOURCE_IMAGE" docker://"$TARGET_IMAGE"; then echo "Successfully copied $SOURCE_IMAGE to $TARGET_IMAGE" diff --git a/build-tools/dar-lock-checker/src/main/scala/org/lfdecentralizedtrust/splice/build_tools/DarLockChecker.scala b/build-tools/dar-lock-checker/src/main/scala/org/lfdecentralizedtrust/splice/build_tools/DarLockChecker.scala index b8720005b3..26c2a2ba26 100644 --- a/build-tools/dar-lock-checker/src/main/scala/org/lfdecentralizedtrust/splice/build_tools/DarLockChecker.scala +++ b/build-tools/dar-lock-checker/src/main/scala/org/lfdecentralizedtrust/splice/build_tools/DarLockChecker.scala @@ -121,15 +121,7 @@ object DarLockChecker { val currentHashes = File(outputFilename).contentAsString val lockStr = getLockStr(checkedInDarMap ++ darMap) if (currentHashes != lockStr) - sys.error( - Seq( - "Error: daml lockfile is not up-to-date", - "Expected:", - lockStr, - "Actual:", - currentHashes, - ).mkString(System.lineSeparator()) - ) + sys.error(lockOutOfDateMessage(outputFilename, lockStr)) case "update" => // Check that the freshly built packages either match the // last release or have a different version number. @@ -263,6 +255,33 @@ object DarLockChecker { .sorted .mkString(System.lineSeparator()) + private[build_tools] def lockOutOfDateMessage( + currentLockFile: String, + expectedLockStr: String, + diffColor: String = if (sys.env.contains("CI")) "never" else "always", + ): String = + File.temporaryFile(prefix = "expected-dars", suffix = ".lock") { expectedFile => + val _ = expectedFile.write(expectedLockStr) + + val out = new StringBuilder("") + def appendToOut(s: String): Unit = out ++= s + System.lineSeparator() + val _ = Seq( + "diff", + s"--color=$diffColor", + "--unified=0", + s"--label=$currentLockFile", + "--label=expected-dars.lock", + currentLockFile, + expectedFile.toString, + ).!(ProcessLogger(appendToOut, appendToOut)) + + Seq( + "Error: daml lockfile is not up-to-date", + "", + out.toString, + ).mkString(System.lineSeparator()) + } + private def getCheckedInDarMap(): Map[(PackageName, PackageVersion), String] = { val checkedInDars = File("daml/dars").list(_.extension == Some(".dar")).toSeq toDarMap(readDars(checkedInDars.map(_.toString()))) diff --git a/build-tools/dar-lock-checker/src/test/scala/org/lfdecentralizedtrust/splice/build_tools/DarLockCheckerTest.scala b/build-tools/dar-lock-checker/src/test/scala/org/lfdecentralizedtrust/splice/build_tools/DarLockCheckerTest.scala index 85efab7cb2..fa503d98e0 100644 --- a/build-tools/dar-lock-checker/src/test/scala/org/lfdecentralizedtrust/splice/build_tools/DarLockCheckerTest.scala +++ b/build-tools/dar-lock-checker/src/test/scala/org/lfdecentralizedtrust/splice/build_tools/DarLockCheckerTest.scala @@ -3,6 +3,7 @@ package org.lfdecentralizedtrust.splice.build_tools +import better.files.* import com.digitalasset.daml.lf.data.Ref.{PackageName, PackageVersion} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -13,6 +14,52 @@ class DarLockCheckerTest extends AnyWordSpec with Matchers { private def ver(v: String): PackageVersion = PackageVersion.assertFromString(v) private def key(name: String, v: String) = (pkg(name), ver(v)) + "lockOutOfDateMessage" should { + def lock(entries: String*): String = entries.mkString(System.lineSeparator()) + + def messageFor(currentLockContent: String, expectedLockStr: String): String = + File.temporaryFile(prefix = "current-dars", suffix = ".lock").apply { currentFile => + val _ = currentFile.write(currentLockContent) + DarLockChecker.lockOutOfDateMessage(currentFile.toString, expectedLockStr, "never") + } + + // The `-`/`+` lines of the diff embedded in the message + def changedLines(message: String): Seq[String] = + message.linesIterator + .filterNot(line => line.startsWith("---") || line.startsWith("+++")) + .filter(line => line.startsWith("-") || line.startsWith("+")) + .toSeq + + "report only the entries whose package id changed" in { + val current = lock( + "splice-amulet 0.1.0 hash0", + "splice-amulet 0.1.1 hash1", + "splice-amulet 0.1.2 hash2", + ) + val expected = lock( + "splice-amulet 0.1.0 hash0", + "splice-amulet 0.1.1 rebuiltHash1", + "splice-amulet 0.1.2 hash2", + ) + changedLines(messageFor(current, expected)) shouldBe Seq( + "-splice-amulet 0.1.1 hash1", + "+splice-amulet 0.1.1 rebuiltHash1", + ) + } + + "report entries missing from the lock file" in { + val current = lock() + val expected = lock("splice-amulet 0.1.0 hash0") + changedLines(messageFor(current, expected)) shouldBe Seq("+splice-amulet 0.1.0 hash0") + } + + "report entries that are no longer expected" in { + val current = lock("splice-amulet 0.1.0 hash0") + val expected = lock() + changedLines(messageFor(current, expected)) shouldBe Seq("-splice-amulet 0.1.0 hash0") + } + } + "detectBumps" should { "return empty when branch and compare base match exactly" in { val branch = Map(key("splice-amulet", "0.1.18") -> "hashA") diff --git a/build-tools/lib/hard-domain-migration-commands b/build-tools/lib/hard-domain-migration-commands deleted file mode 100644 index c17efbec6c..0000000000 --- a/build-tools/lib/hard-domain-migration-commands +++ /dev/null @@ -1,137 +0,0 @@ -# -*- shell-script -*- - -subcommand_whitelist[hard_domain_migration_trigger]='Propose a (nearly immediate) hard domain migration start and vote with SVs 1-4' - -function subcmd_hard_domain_migration_trigger() { - _prompt_to_confirm - _cluster_must_exist - - subcmd_preflight_global_domain_upgrade -} - -subcommand_whitelist[update_config]='Upgrade the hard domain migration config' - -function subcmd_update_config() { - local migration_type - local migration_id - if [ "$#" -ge 2 ]; then - migration_type="$1" - migration_id="$2" - shift 2 - else - _error "Usage: $0 $SCRIPTNAME update_config [] []" - fi - local migration_config - migration_config=$(cat < [] []" - fi - git_reference="$1" - shift - migration_config+=$(cat <] [migration]" - fi - # yq doesn't like the version being unquoted so we quote it here - _update_cluster_config "\"$1\"" "synchronizerMigration.$2.version" - if [ -z "${TARGET_CLUSTER-}" ]; then - cluster_directory="$(pwd)" - else - cluster_directory="${DEPLOYMENT_DIR}/${TARGET_CLUSTER}" - fi -} - -subcommand_whitelist[update_active_version]='Upgrade the version of the active migration id' - -function subcmd_update_active_version() { - if [ "$#" -ne 1 ]; then - _error "Usage: $0 $SCRIPTNAME []" - fi - update_version "$1" "active" -} - -subcommand_whitelist[update_upgrade_version]='Upgrade the version of the upgrade migration id' - -function subcmd_update_upgrade_version() { - if [ "$#" -ne 1 ]; then - _error "Usage: $0 $SCRIPTNAME []" - fi - if [ -z "${TARGET_CLUSTER-}" ]; then - configFile="config.yaml" - else - configFile="${DEPLOYMENT_DIR}/${TARGET_CLUSTER}/config.yaml" - fi - if yq -e '.synchronizerMigration.upgrade' "$configFile" > /dev/null 2>&1 - then - update_version "$1" "upgrade" - else - echo "No upgrade version" - fi -} - -subcommand_whitelist[update_config_to_migrate]='Upgrade the cluster config.yaml to migrate cluster' - -function subcmd_update_config_to_migrate() { - local configFile - if [ -z "${TARGET_CLUSTER-}" ]; then - configFile="config.yaml" - else - configFile="${DEPLOYMENT_DIR}/${TARGET_CLUSTER}/config.yaml" - fi - - yq e 'with(.synchronizerMigration; .legacy = .active | .active = .upgrade | del(.upgrade))' -i $configFile - yq e '.synchronizerMigration.active.migratingFrom = 0' -i $configFile - yq e 'del(.synchronizerMigration.legacy.releaseReference)' -i $configFile - "${SPLICE_ROOT}/cluster/scripts/resolve-config.sh" -} - -subcommand_whitelist[update_config_to_archive_legacy_migration]='Upgrade the cluster config.yaml to archive legacy migration' - -function subcmd_update_config_to_archive_legacy_migration() { - local configFile - if [ -z "${TARGET_CLUSTER-}" ]; then - configFile="config.yaml" - else - configFile="${DEPLOYMENT_DIR}/${TARGET_CLUSTER}/config.yaml" - fi - - yq e '.synchronizerMigration.archived = [ .synchronizerMigration.legacy ] | del(.synchronizerMigration.legacy)' -i $configFile - "${SPLICE_ROOT}/cluster/scripts/resolve-config.sh" -} - -subcommand_whitelist[update_config_to_remove_migrating_from]='Upgrade the cluster config.yaml to remove migratingFrom field from active migration' - -function subcmd_update_config_to_remove_migrating_from() { - local configFile - if [ -z "${TARGET_CLUSTER-}" ]; then - configFile="config.yaml" - else - configFile="${DEPLOYMENT_DIR}/${TARGET_CLUSTER}/config.yaml" - fi - - yq e 'del(.synchronizerMigration.active.migratingFrom)' -i $configFile - "${SPLICE_ROOT}/cluster/scripts/resolve-config.sh" -} diff --git a/build-tools/lib/logical-synchronizer-upgrade-commands b/build-tools/lib/logical-synchronizer-upgrade-commands index 60a27537a6..b7d696e061 100644 --- a/build-tools/lib/logical-synchronizer-upgrade-commands +++ b/build-tools/lib/logical-synchronizer-upgrade-commands @@ -40,3 +40,108 @@ function subcmd_retire_old_physical_synchronizer() { yq e 'del(.synchronizerMigration.legacy.releaseReference)' -i $configFile "${SPLICE_ROOT}/cluster/scripts/resolve-config.sh" } + +subcommand_whitelist[update_config]='Upgrade the hard domain migration config' + +function subcmd_update_config() { + local migration_type + local migration_id + if [ "$#" -ge 2 ]; then + migration_type="$1" + migration_id="$2" + shift 2 + else + _error "Usage: $0 $SCRIPTNAME update_config [] []" + fi + local migration_config + migration_config=$(cat < [] []" + fi + git_reference="$1" + shift + migration_config+=$(cat <] [migration]" + fi + # yq doesn't like the version being unquoted so we quote it here + _update_cluster_config "\"$1\"" "synchronizerMigration.$2.version" + if [ -z "${TARGET_CLUSTER-}" ]; then + cluster_directory="$(pwd)" + else + cluster_directory="${DEPLOYMENT_DIR}/${TARGET_CLUSTER}" + fi +} + +subcommand_whitelist[update_active_version]='Upgrade the version of the active migration id' + +function subcmd_update_active_version() { + if [ "$#" -ne 1 ]; then + _error "Usage: $0 $SCRIPTNAME []" + fi + update_version "$1" "active" +} + +subcommand_whitelist[update_upgrade_version]='Upgrade the version of the upgrade migration id' + +function subcmd_update_upgrade_version() { + if [ "$#" -ne 1 ]; then + _error "Usage: $0 $SCRIPTNAME []" + fi + if [ -z "${TARGET_CLUSTER-}" ]; then + configFile="config.yaml" + else + configFile="${DEPLOYMENT_DIR}/${TARGET_CLUSTER}/config.yaml" + fi + if yq -e '.synchronizerMigration.upgrade' "$configFile" > /dev/null 2>&1 + then + update_version "$1" "upgrade" + else + echo "No upgrade version" + fi +} + +subcommand_whitelist[update_config_to_archive_legacy_migration]='Upgrade the cluster config.yaml to archive legacy migration' + +function subcmd_update_config_to_archive_legacy_migration() { + local configFile + if [ -z "${TARGET_CLUSTER-}" ]; then + configFile="config.yaml" + else + configFile="${DEPLOYMENT_DIR}/${TARGET_CLUSTER}/config.yaml" + fi + + yq e '.synchronizerMigration.archived = [ .synchronizerMigration.legacy ] | del(.synchronizerMigration.legacy)' -i $configFile + "${SPLICE_ROOT}/cluster/scripts/resolve-config.sh" +} diff --git a/build.sbt b/build.sbt index bc7e72cc19..1fc2d2e91a 100644 --- a/build.sbt +++ b/build.sbt @@ -25,28 +25,16 @@ lazy val `canton-community-participant` = BuildCommon.`canton-community-particip lazy val `canton-community-admin-api` = BuildCommon.`canton-community-admin-api` lazy val `canton-community-integration-testing` = BuildCommon.`canton-community-integration-testing` lazy val `canton-community-testing` = BuildCommon.`canton-community-testing` -lazy val `canton-blake2b` = BuildCommon.`canton-blake2b` -lazy val `canton-slick-fork` = BuildCommon.`canton-slick-fork` lazy val `canton-wartremover-extension` = BuildCommon.`canton-wartremover-extension` -lazy val `canton-wartremover-annotations` = BuildCommon.`canton-wartremover-annotations` -lazy val `canton-util-external` = BuildCommon.`canton-util-external` lazy val `canton-util-observability` = BuildCommon.`canton-util-observability` -lazy val `canton-pekko-fork` = BuildCommon.`canton-pekko-fork` -lazy val `canton-magnolify-addon` = BuildCommon.`canton-magnolify-addon` -lazy val `canton-scalatest-addon` = BuildCommon.`canton-scalatest-addon` -lazy val `canton-ledger-common` = BuildCommon.`canton-ledger-common` -lazy val `canton-ledger-api-core` = BuildCommon.`canton-ledger-api-core` lazy val `canton-ledger-api-value` = BuildCommon.`canton-ledger-api-value` lazy val `canton-ledger-json-api` = BuildCommon.`canton-ledger-json-api` -lazy val `canton-daml-adjustable-clock` = BuildCommon.`canton-daml-adjustable-clock` -lazy val `canton-daml-jwt` = BuildCommon.`canton-daml-jwt` -lazy val `canton-daml-tls` = BuildCommon.`canton-daml-tls` -lazy val `canton-base-errors` = BuildCommon.`canton-base-errors` -lazy val `canton-google-common-protos-scala` = BuildCommon.`canton-google-common-protos-scala` lazy val `canton-sequencer-driver-api` = BuildCommon.`canton-sequencer-driver-api` -lazy val `canton-kms-driver-api` = BuildCommon.`canton-kms-driver-api` lazy val `canton-community-reference-driver` = BuildCommon.`canton-community-reference-driver` lazy val `canton-observability-metrics-testing` = BuildCommon.`canton-observability-metrics-testing` +lazy val `canton-traffic-enforcement-component` = BuildCommon.`canton-traffic-enforcement-component` +lazy val `daml-lf-transaction-test-lib` = BuildCommon.`daml-lf-transaction-test-lib` +lazy val `daml-lf-data-scalacheck` = BuildCommon.`daml-lf-data-scalacheck` lazy val `splice-wartremover-extension` = Wartremover.`splice-wartremover-extension` @@ -141,17 +129,12 @@ lazy val root: Project = (project in file(".")) `canton-community-common`, `canton-community-integration-testing`, `canton-community-testing`, - `canton-blake2b`, - `canton-slick-fork`, `canton-wartremover-extension`, `canton-community-app`, `canton-community-app-base`, `canton-community-synchronizer`, `canton-community-participant`, - `canton-ledger-common`, - `canton-ledger-api-core`, `canton-ledger-api-value`, - `canton-google-common-protos-scala`, `canton-observability-metrics-testing`, pulumi, `load-tester`, @@ -166,7 +149,7 @@ lazy val root: Project = (project in file(".")) BuildCommon.sharedSettings, scalacOptions ++= Seq("-Wconf:src=src_managed/.*:silent"), // Needed to be able to resolve scalafmt snapshot versions - resolvers ++= Resolver.sonatypeOssRepos("snapshots"), + resolvers += Resolver.sonatypeCentralSnapshots, damlDarsLockCheckerFileArg := { val darFiles: Seq[File] = damlBuild.all(allDarsFilter).value.flatten val basePath = baseDirectory.value.toPath @@ -1182,6 +1165,29 @@ lazy val `splitwell-test-daml` = Compile / damlEnableJavaCodegen := false, ) +lazy val `lf-value-json` = + project + .in(file("canton-fork/lf-value-json")) + .dependsOn( + `canton-ledger-json-api`, + `daml-lf-transaction-test-lib`, + ) + .settings( + scalacOptions += "-Xsource-features:infer-override", + libraryDependencies ++= { + import CantonDependencies._ + Seq( + CantonDependencies.canton_ledger_api_core, + daml_lf_api_type_signature, + scalatest % Test, + scalacheck % Test, + scalaz_scalacheck % Test, + scalatestScalacheck % Test, + ) + }, + CantonDependencies.excludeTranscodeConflictingDependencies, + ) + lazy val `apps-common` = project .in(file("apps/common")) @@ -1189,6 +1195,7 @@ lazy val `apps-common` = `canton-community-common`, `canton-community-app` % "compile->compile;test->test", `canton-community-testing` % "test->test", + `lf-value-json`, `splice-wartremover-extension` % "compile->compile;test->test", // We include all DARs here to make sure they are available as resources. `splice-amulet-daml`, @@ -2052,9 +2059,6 @@ def mergeStrategy(oldStrategy: String => MergeStrategy): String => MergeStrategy "Log4j2Plugins.dat", ) => MergeStrategy.first - case (PathList("org", "apache", "pekko", "stream", "scaladsl", broadcasthub, _*)) - if broadcasthub.startsWith("BroadcastHub") => - MergeStrategy.first case "META-INF/versions/9/module-info.class" => MergeStrategy.discard case path if path.contains("module-info.class") => MergeStrategy.discard case PathList("org", "jline", _ @_*) => MergeStrategy.first @@ -2067,12 +2071,23 @@ def mergeStrategy(oldStrategy: String => MergeStrategy): String => MergeStrategy MergeStrategy.first case PathList("com", "google", _*) => MergeStrategy.first case PathList("io", "grpc", _*) => MergeStrategy.first + // slick-fork + case PathList("slick", "jdbc", "canton", _*) => MergeStrategy.first + case PathList("slick", "util", name) + if name.startsWith("QueryCostTracker") || name.startsWith("AsyncExecutorWith") => + MergeStrategy.first + // community-base + case PathList("com", "daml", "nonempty", name) if name.startsWith("NonEmptyUtil") => + MergeStrategy.first + // Multiple dependencies ship this GraalVM metadata with differing content. + case PathList("META-INF", "native-image", "reflect-config.json") => MergeStrategy.first // Copy-pasta from Canton (DACH-NY/canton#31788): Remove this merge strategy once zipkin exporter is removed case PathList("okhttp3", _ @_*) => MergeStrategy.first // this file comes in multiple flavors, from io.get-coursier:interface and from org.scala-lang.modules:scala-collection-compat. Since the content differs it is resolve this explicitly with this MergeStrategy. case path if path.endsWith("scala-collection-compat.properties") => MergeStrategy.first // Don't really care about the notice file so just take any. case "META-INF/FastDoubleParser-NOTICE" => MergeStrategy.first + case "META-INF/FastDoubleParser-LICENSE" => MergeStrategy.first case "META-INF/license/LICENSE.boringssl.txt" => MergeStrategy.first case path if path.endsWith("/OSGI-INF/MANIFEST.MF") => MergeStrategy.first case x => @@ -2331,7 +2346,6 @@ lazy val `apps-dar-resources-generator` = project .in(file("apps/dar-resources-generator")) .dependsOn( - `canton-util-external`, // We include all DARs here to make sure they are available as resources. `splice-amulet-daml`, `splice-amulet-name-service-daml`, @@ -2366,6 +2380,7 @@ lazy val `apps-dar-resources-generator` = Headers.ApacheDAHeaderSettings, libraryDependencies ++= Seq( Dependencies.better_files, + CantonDependencies.canton_util_external, CantonDependencies.daml_lf_archive_reader, CantonDependencies.cats, ), @@ -2384,6 +2399,7 @@ lazy val `apps-app`: Project = `canton-community-app` % "compile->compile;test->test", `canton-community-base`, `canton-community-integration-testing` % "test", + `splice-amulet-test-daml` % "test", `splice-util-featured-app-proxies-daml` % "test", // necessary for token-standard-cli to get `npm install`ed so that TokenStandardCliSanityCheckPlugin can run `apps-common-frontend`, @@ -2392,6 +2408,7 @@ lazy val `apps-app`: Project = // scalatestplus-selenium is lagging behind, it depends on selenium 4.12, // but that's fine as it's compatible with selenium 4.44 that we end up using libraryDependencies += "org.scalatestplus" %% "selenium-4-12" % "3.2.17.0" % "test", + libraryDependencies += CantonDependencies.scalatest_shouldmatchers, libraryDependencies += "org.seleniumhq.selenium" % "selenium-java" % "4.44.0" % "test", libraryDependencies += "eu.rekawek.toxiproxy" % "toxiproxy-java" % "2.1.4" % "test", libraryDependencies += auth0, @@ -2502,10 +2519,7 @@ updateTestConfigForParallelRuns := { val allTestNames = definedTests .all( - ScopeFilter(inAggregates(root), inConfigurations(Test)) -- ScopeFilter( - inProjects(`canton-ledger-api-core`), - inConfigurations(Test), - ) + ScopeFilter(inAggregates(root), inConfigurations(Test)) ) .value .flatten @@ -2615,11 +2629,6 @@ updateTestConfigForParallelRuns := { "test-cometbft-full-class-names.log", (t: String) => !isTimeBasedTest(t) && !isFrontEndTest(t) && isCometBftTest(t), ), - ( - "tests requiring Canton Enterprise", - "test-full-class-names-canton-enterprise.log", - (t: String) => isEnterpriseIntegrationTest(t), - ), ( "tests to check logical sync roll-forward upgrade", "test-full-class-names-roll-forward-lsu.log", diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressed.scala b/canton-fork/lf-value-json/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressed.scala similarity index 100% rename from canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressed.scala rename to canton-fork/lf-value-json/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressed.scala diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiValueImplicits.scala b/canton-fork/lf-value-json/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiValueImplicits.scala similarity index 100% rename from canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiValueImplicits.scala rename to canton-fork/lf-value-json/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiValueImplicits.scala diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/JsonVariant.scala b/canton-fork/lf-value-json/src/main/scala/com/digitalasset/canton/daml/lf/value/json/JsonVariant.scala similarity index 100% rename from canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/JsonVariant.scala rename to canton-fork/lf-value-json/src/main/scala/com/digitalasset/canton/daml/lf/value/json/JsonVariant.scala diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/NavigatorModelAliases.scala b/canton-fork/lf-value-json/src/main/scala/com/digitalasset/canton/daml/lf/value/json/NavigatorModelAliases.scala similarity index 100% rename from canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/NavigatorModelAliases.scala rename to canton-fork/lf-value-json/src/main/scala/com/digitalasset/canton/daml/lf/value/json/NavigatorModelAliases.scala diff --git a/canton-fork/lf-value-json/src/test/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressedSpec.scala b/canton-fork/lf-value-json/src/test/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressedSpec.scala new file mode 100644 index 0000000000..5936fa38c5 --- /dev/null +++ b/canton-fork/lf-value-json/src/test/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressedSpec.scala @@ -0,0 +1,407 @@ +// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.daml.lf.value.json + +import com.digitalasset.canton.daml.lf.value.json.NavigatorModelAliases as model +import com.digitalasset.daml.lf.data.{ImmArray, Numeric, Ref, SortedLookupList, Time} +import com.digitalasset.daml.lf.value.Value.ContractId +import com.digitalasset.daml.lf.value.test.TypedValueGenerators.{ + ValueAddend as VA, + genAddend, + genTypeAndValue, +} +import com.digitalasset.daml.lf.value.test.ValueGenerators.coidGen +import org.scalacheck.Arbitrary +import org.scalactic.source +import org.scalatest.Inside +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import shapeless.record.Record as HRecord +import shapeless.{Coproduct as HSum, HNil} +import spray.json.* + +import java.time.Instant +import scala.annotation.nowarn +import scala.util.{Success, Try} + +import ApiCodecCompressed.{apiValueToJsValue, jsValueToApiValue} + +class ApiCodecCompressedSpec + extends AnyWordSpec + with Matchers + with ScalaCheckPropertyChecks + with Inside { + + import C.typeLookup + + protected implicit val cidArb: Arbitrary[ContractId] = Arbitrary(coidGen) + + /** Serializes the API value to JSON, then parses it back to an API value */ + protected def serializeAndParse( + value: model.ApiValue, + typ: model.DamlLfType, + ): Try[model.ApiValue] = { + import ApiCodecCompressed.JsonImplicits.* + + for { + serialized <- Try(value.toJson.prettyPrint) + json <- Try(serialized.parseJson) + parsed <- Try(jsValueToApiValue(json, typ, typeLookup)) + } yield parsed + } + + protected def roundtrip(va: VA)(v: va.Inj): Option[va.Inj] = + va.prj(jsValueToApiValue(apiValueToJsValue(va.inj(v)), va.t, typeLookup)) + + protected val decimalScale = Numeric.Scale.assertFromInt(10) + + protected object C /* based on navigator DamlConstants */ { + import shapeless.syntax.singleton.* + val packageId0 = Ref.PackageId assertFromString "hash" + val moduleName0 = Ref.ModuleName assertFromString "Module" + def defRef(name: String) = + Ref.Identifier( + packageId0, + Ref.QualifiedName(moduleName0, Ref.DottedName assertFromString name), + ) + val emptyRecordId = defRef("EmptyRecord") + val (emptyRecordDDT, emptyRecordT) = VA.record(emptyRecordId, HNil) + val simpleRecordId = defRef("SimpleRecord") + val simpleRecordVariantSpec = HRecord(fA = VA.text, fB = VA.int64) + val (simpleRecordDDT, simpleRecordT) = + VA.record(simpleRecordId, simpleRecordVariantSpec) + val simpleRecordV: simpleRecordT.Inj = HRecord(fA = "foo", fB = 100L) + + val simpleVariantId = defRef("SimpleVariant") + val (simpleVariantDDT, simpleVariantT) = + VA.variant(simpleVariantId, simpleRecordVariantSpec) + val simpleVariantV = HSum[simpleVariantT.Inj](Symbol("fA") ->> "foo") + + val complexRecordId = defRef("ComplexRecord") + val (complexRecordDDT, complexRecordT) = + VA.record( + complexRecordId, + HRecord( + fText = VA.text, + fBool = VA.bool, + fDecimal = VA.numeric(decimalScale), + fUnit = VA.unit, + fInt64 = VA.int64, + fParty = VA.party, + fContractId = VA.contractId, + fListOfText = VA.list(VA.text), + fListOfUnit = VA.list(VA.unit), + fDate = VA.date, + fTimestamp = VA.timestamp, + fOptionalText = VA.optional(VA.text), + fOptionalUnit = VA.optional(VA.unit), + fOptOptText = VA.optional(VA.optional(VA.text)), + fMap = VA.map(VA.int64), + fVariant = simpleVariantT, + fRecord = simpleRecordT, + ), + ) + @nowarn("msg=dubious usage of method asInstanceOf with unit value") + val complexRecordV: complexRecordT.Inj = + HRecord( + fText = "foo", + fBool = true, + fDecimal = Numeric assertFromString "100.0000000000", + fUnit = (), + fInt64 = 100L, + fParty = Ref.Party assertFromString "BANK1", + fContractId = ContractId.assertFromString("00" + "00" * 32 + "c0"), + fListOfText = Vector("foo", "bar"), + fListOfUnit = Vector((), ()), + fDate = Time.Date assertFromString "2019-01-28", + fTimestamp = Time.Timestamp.assertFromInstant(Instant.parse("2019-01-28T12:44:33.22Z")), + fOptionalText = None, + fOptionalUnit = Some(()), + fOptOptText = Some(Some("foo")), + fMap = SortedLookupList(Map("1" -> 1L, "2" -> 2L, "3" -> 3L)), + fVariant = simpleVariantV, + fRecord = simpleRecordV, + ) + + val colorId = defRef("Color") + val (colorGD, colorGT) = + VA.enumeration(colorId, Seq("Red", "Green", "Blue") map Ref.Name.assertFromString) + + val typeLookup: NavigatorModelAliases.DamlLfTypeLookup = + Map( + emptyRecordId -> emptyRecordDDT, + simpleRecordId -> simpleRecordDDT, + simpleVariantId -> simpleVariantDDT, + complexRecordId -> complexRecordDDT, + colorId -> colorGD, + ).lift + } + + protected def mustBeOne[A](as: Seq[A]): A = as match { + case Seq(x) => x + case xs @ _ => sys.error(s"Expected exactly one element, got: $xs") + } + + import C.typeLookup + + "API compressed JSON codec" when { + + "serializing and parsing a value" should { + + "work for arbitrary reference-free types" in forAll( + genTypeAndValue(coidGen), + minSuccessful(100), + ) { case (typ, value) => + serializeAndParse(value, typ) shouldBe Success(value) + } + + "work for many, many values in raw format" in forAll(genAddend, minSuccessful(100)) { va => + import va.injshrink + implicit val arbInj: Arbitrary[va.Inj] = va.injarb + forAll(minSuccessful(20)) { (v: va.Inj) => + roundtrip(va)(v) should ===(Some(v)) + } + } + + "handle nested optionals" in { + val va = VA.optional(VA.optional(VA.int64)) + val cases = Table( + "value", + None, + Some(None), + Some(Some(42L)), + ) + forEvery(cases) { ool => + roundtrip(va)(ool) should ===(Some(ool)) + } + } + + "handle lists of optionals" in { + val va = VA.optional(VA.optional(VA.list(VA.optional(VA.optional(VA.int64))))) + import va.injshrink + implicit val arbInj: Arbitrary[va.Inj] = va.injarb + forAll(minSuccessful(1000)) { (v: va.Inj) => + roundtrip(va)(v) should ===(Some(v)) + } + } + + def cr(typ: VA)(v: typ.Inj) = + (typ, v: Any, typ.inj(v)) + + val roundtrips = Table( + ("type", "original value", "Daml value"), + cr(C.emptyRecordT)(HRecord()), + cr(C.simpleRecordT)(C.simpleRecordV), + cr(C.simpleVariantT)(C.simpleVariantV), + cr(C.complexRecordT)(C.complexRecordV), + ) + "work for records and variants" in forAll(roundtrips) { (typ, origValue, damlValue) => + typ.prj(jsValueToApiValue(apiValueToJsValue(damlValue), typ.t, typeLookup)) should ===( + Some(origValue) + ) + } + /* + "work for Tree" in { + serializeAndParse(C.treeV, C.treeTC) shouldBe Success(C.treeV) + } + "work for Enum" in { + serializeAndParse(C.redV, C.redTC) shouldBe Success(C.redV) + } + */ + } + + def cn(canonical: String, numerically: String, typ: VA)( + expected: typ.Inj, + alternates: String* + )(implicit pos: source.Position) = + (pos.lineNumber, canonical, numerically, typ, expected, alternates) + + def c(canonical: String, typ: VA)(expected: typ.Inj, alternates: String*)(implicit + pos: source.Position + ) = + cn(canonical, canonical, typ)(expected, alternates*)(pos) + + object VAs { + val ooi = VA.optional(VA.optional(VA.int64)) + val oooi = VA.optional(ooi) + } + + val numCodec = ApiCodecCompressed.copy(false, false) + + @nowarn("cat=lint-infer-any") + val successes = Table( + ("line#", "serialized", "serializedNumerically", "type", "parsed", "alternates"), + c( + "\"0000000000000000000000000000000000000000000000000000000000000000000123\"", + VA.contractId, + )( + ContractId.assertFromString( + "0000000000000000000000000000000000000000000000000000000000000000000123" + ) + ), + cn("\"42.0\"", "42.0", VA.numeric(decimalScale))( + Numeric assertFromString "42.0000000000", + "\"42\"", + "42", + "42.0", + "\"+42\"", + ), + cn("\"2000.0\"", "2000", VA.numeric(decimalScale))( + Numeric assertFromString "2000.0000000000", + "\"2000\"", + "2000", + "2e3", + ), + cn("\"0.3\"", "0.3", VA.numeric(decimalScale))( + Numeric assertFromString "0.3000000000", + "\"0.30000000000000004\"", + "0.30000000000000004", + ), + cn( + "\"9999999999999999999999999999.9999999999\"", + "9999999999999999999999999999.9999999999", + VA.numeric(decimalScale), + )(Numeric assertFromString "9999999999999999999999999999.9999999999"), + cn("\"0.1234512346\"", "0.1234512346", VA.numeric(decimalScale))( + Numeric assertFromString "0.1234512346", + "0.12345123455", + "0.12345123465", + "\"0.12345123455\"", + "\"0.12345123465\"", + ), + cn("\"0.1234512345\"", "0.1234512345", VA.numeric(decimalScale))( + Numeric assertFromString "0.1234512345", + "0.123451234549", + "0.12345123445001", + "\"0.123451234549\"", + "\"0.12345123445001\"", + ), + c("\"1990-11-09T04:30:23.123456Z\"", VA.timestamp)( + Time.Timestamp.assertFromInstant(Instant.parse("1990-11-09T04:30:23.123456Z")), + "\"1990-11-09T04:30:23.1234569Z\"", + ), + c("\"1970-01-01T00:00:00Z\"", VA.timestamp)(Time.Timestamp assertFromLong 0), + // Ensure ISO 8601 timestamps with offsets are successfully parsed by comparing to (epoch - 1 hour) + c("\"1969-12-31T23:00:00Z\"", VA.timestamp)( + Time.Timestamp.assertFromLong(-3600000000L), + "\"1970-01-01T00:00:00+01:00\"", + ), + cn("\"42\"", "42", VA.int64)(42, "\"+42\""), + cn("\"0\"", "0", VA.int64)(0, "-0", "\"+0\"", "\"-0\""), + c("\"Alice\"", VA.party)(Ref.Party assertFromString "Alice"), + c("{}", VA.unit)(()), + c("\"2019-06-18\"", VA.date)(Time.Date assertFromString "2019-06-18"), + c("\"9999-12-31\"", VA.date)(Time.Date assertFromString "9999-12-31"), + c("\"0001-01-01\"", VA.date)(Time.Date assertFromString "0001-01-01"), + c("\"abc\"", VA.text)("abc"), + c("true", VA.bool)(true), + cn("""["1", "2", "3"]""", "[1, 2, 3]", VA.list(VA.int64))(Vector(1, 2, 3)), + c("""{"a": "b", "c": "d"}""", VA.map(VA.text))(SortedLookupList(Map("a" -> "b", "c" -> "d"))), + c("""[["a", "b"], ["c", "d"]]""", VA.genMap(VA.text, VA.text))(Map("a" -> "b", "c" -> "d")), + cn("\"42\"", "42", VA.optional(VA.int64))(Some(42)), + c("null", VA.optional(VA.int64))(None), + c("null", VAs.ooi)(None), + c("[]", VAs.ooi)(Some(None), "[null]"), + cn("""["42"]""", "[42]", VAs.ooi)(Some(Some(42))), + c("null", VAs.oooi)(None), + c("[]", VAs.oooi)(Some(None), "[null]"), + c("[[]]", VAs.oooi)(Some(Some(None)), "[[null]]"), + cn("""[["42"]]""", "[[42]]", VAs.oooi)(Some(Some(Some(42)))), + cn("""{"fA": "foo", "fB": "100"}""", """{"fA": "foo", "fB": 100}""", C.simpleRecordT)( + C.simpleRecordV + ), + c("""{"tag": "fA", "value": "foo"}""", C.simpleVariantT)(C.simpleVariantV), + c("\"Green\"", C.colorGT)( + C.colorGT get Ref.Name.assertFromString("Green") getOrElse sys.error("impossible") + ), + ) + + val failures = Table( + ("JSON", "type", "errorSubstring"), + ("42.3", VA.int64, ""), + ("\"42.3\"", VA.int64, ""), + ("9223372036854775808", VA.int64, ""), + ("-9223372036854775809", VA.int64, ""), + ("\"garbage\"", VA.int64, ""), + ("\" 42 \"", VA.int64, ""), + ("\"1970-01-01T00:00:00\"", VA.timestamp, ""), + ("\"1970-01-01T00:00:00+01:00[Europe/Paris]\"", VA.timestamp, ""), + ("\"0000-01-01\"", VA.date, "Invalid date: 0000-01-01"), + ("\"9999-99-99\"", VA.date, "Invalid date: 9999-99-99"), + ("\"9999-12-32\"", VA.date, "Invalid date: 9999-12-32"), + ("\"9999-13-31\"", VA.date, "Invalid date: 9999-13-31"), + ("\"10000-01-01\"", VA.date, "Invalid date: 10000-01-01"), + ("\"1-01-01\"", VA.date, "Invalid date: 1-01-01"), + ("\"0001-02-29\"", VA.date, "Invalid date: 0001-02-29"), + ("\"not-a-date\"", VA.date, "Invalid date: not-a-date"), + ("""{"a": "b", "c": "d"}""", VA.genMap(VA.text, VA.text), ""), + ("\"\"", VA.party, "Daml-LF Party is empty"), + (List.fill(256)('a').mkString("\"", "", "\""), VA.party, "Daml-LF Party is too long"), + ) + + "dealing with particular formats" should { + "succeed in cases" in forEvery(successes) { + (_, serialized, serializedNumerically, typ, expected, alternates) => + val json = serialized.parseJson + val numJson = serializedNumerically.parseJson + val parsed = jsValueToApiValue(json, typ.t, typeLookup) + jsValueToApiValue(numJson, typ.t, typeLookup) should ===(parsed) + typ.prj(parsed) should ===(Some(expected)) + apiValueToJsValue(parsed) should ===(json) + numCodec.apiValueToJsValue(parsed) should ===(numJson) + val tAlternates = Table("alternate", alternates*) + forEvery(tAlternates) { alternate => + val aJson = alternate.parseJson + typ.prj(jsValueToApiValue(aJson, typ.t, typeLookup)) should ===(Some(expected)) + } + } + + "fail in cases" in forEvery(failures) { (serialized, typ, errorSubstring) => + val json = serialized.parseJson // we don't test *the JSON decoder* + val exception = the[DeserializationException] thrownBy { + jsValueToApiValue(json, typ.t, typeLookup) + } + exception.getMessage should include(errorSubstring) + } + } + + import com.digitalasset.daml.lf.value.Value as LfValue + import ApiCodecCompressed.JsonImplicits.* + + val bazRecord = LfValue.ValueRecord( + None, + ImmArray(Some(Ref.Name.assertFromString("baz")) -> LfValue.ValueText("text abc")), + ) + + val bazVariant = LfValue.ValueVariant( + None, + Ref.Name.assertFromString("Baz"), + bazRecord, + ) + + val quxVariant = LfValue.ValueVariant( + None, + Ref.Name.assertFromString("Qux"), + LfValue.ValueUnit, + ) + + + "dealing with LF Variant" should { + "encode Foo/Baz to JSON" in { + val writer = implicitly[spray.json.JsonWriter[LfValue]] + (writer.write( + bazVariant + ): JsValue) shouldBe ("""{"tag":"Baz", "value":{"baz":"text abc"}}""".parseJson: JsValue) + } + + "encode Foo/Qux to JSON" in { + val writer = implicitly[spray.json.JsonWriter[LfValue]] + (writer.write( + quxVariant + ): JsValue) shouldBe ("""{"tag":"Qux", "value":{}}""".parseJson: JsValue) + } + } + } +} diff --git a/canton/.proto_snapshot_image.bin.gz b/canton/.proto_snapshot_image.bin.gz index 36562efde4..e53644f1d6 100644 Binary files a/canton/.proto_snapshot_image.bin.gz and b/canton/.proto_snapshot_image.bin.gz differ diff --git a/canton/CODEOWNERS b/canton/CODEOWNERS index 5ae768d66e..bc96433495 100644 --- a/canton/CODEOWNERS +++ b/canton/CODEOWNERS @@ -48,3 +48,6 @@ CODEOWNERS @DACH-NY/canton-change-owners # Inform CN on docker image changes but does not require their approval (canton-change-owners can approve) /docker/canton/images @DACH-NY/canton-network-upstream-notifications @DACH-NY/canton-change-owners + +# Docs should be updated in the cf-docs repo instead +/docs-open @soren-da @rgugliel-da diff --git a/canton/UNRELEASED.md b/canton/UNRELEASED.md index 41efb3649e..d49c4ab685 100644 --- a/canton/UNRELEASED.md +++ b/canton/UNRELEASED.md @@ -1,3 +1,4 @@ + # Release of Canton CANTON_VERSION Canton CANTON_VERSION has been released on RELEASE_DATE. @@ -8,831 +9,54 @@ _Write summary of release_ ## What’s New -### Contract Keys - -#### Overview -Canton 3.5 introduces contract keys. Compared to a similar feature available in Canton 2.x, there are two notable -differences: - -- The keys are not unique, meaning multiple contracts may share the same key. -- Negative lookups are not validated. - -As a consequence, application developers must ensure key uniqueness through external enforcement mechanisms. -Contract keys are available from Daml-LF 2.3 onwards, which itself is available from Protocol Version 35, see below. - -#### Standard library -Daml language now supports several primitives associated with contract keys. In all cases, the contracts are returned -in the following order: -- first the contracts created within a transaction, starting with the most recent, -- then explicitly disclosed contracts, -- then contracts known to the participant in recency order. - -The following primitives are available: - -- ``lookupByKey`` - Available in prelude. It checks whether a contract with the given key exists and if yes, returns - the contract id. If multiple contracts exist, the most recently created is returned. Signature is the same as - in 2.x. -- ``fetchByKey`` - Available in prelude. It fetches the first contract id and contract data associated with the given - contract key. If multiple contracts exist, the most recently created is returned. Signature is the same as in 2.x. -- ``exerciseByKey`` - Available in prelude. Exercise a choice on the first contract associated with the given key. - Signature is the same as in 2.x. -- ``lookupNByKey`` - Available in ``DA.ContractKeys``. It looks up up to n contracts associated with the passed key. - -#### Daml Script -There are Daml Script functions - counterparts of the standard library primitives: - -- ``queryByKey`` - It looks up a contract associated with the passed key and returns its ids and data. It is of type - ``Script``, which means it must appear as top-level instruction as part of a Script. -- ``queryNByKey`` - It looks up up to n contracts associated with the passed key and returns their ids and data. - It is of type ``Script``, which means it must appear as top-level instruction as part of a Script. -- ``exerciseByKeyCmd`` - It exercises a choice on the first contract with the given key. It is of type ``Commands`` and - must therefore be wrapped by a submit operation, and can be combined with other ``Commands``. - - -#### Smart Contract Upgrades (SCU) -To support SCU upgrade for key and maintainer definitions, new guidelines have been added. At upgrade time, the recomputed -key and maintainers are verified to be identical to the upgraded contract’s original key and maintainers. If they -aren't, an upgrade error is raised and the transaction is aborted. -It is forbidden to add or remove a key definition from a template in a later version of that template. This is enforced -at package vetting time. - -#### Ledger API -Following contract-key related extensions have been made to the Ledger API - -- ``contract_key_hash`` has been added to the ``CreatedEvent`` message returned in the ``State-`` and ``UpdateService`` - responses -- ``prefetch_contract_keys`` field present in the ``Command`` and ``PrepareSubmissionRequest`` used by the ``Command-`` - ``CommandSubmission-`` and ``InteractiveSubmissionService`` have been reactivated to allow the caller to request - pre-heating the contract key cache underpinning the command interpretation. Use it when performance tests indicate - that many sequential contract key lookups adversely impact the command interpretation speed. - -#### PQS -In PQS, keys are mere metadata that can be queried like any other metadata. It is possible to query for all contracts -with a given key: - - ``` - select contract_id, payload ->> 'label' - from __contracts - where contract_key = jsonb_build_object(...) - order by created_at_ix - ``` - -### Daml-LF 2.3 - -A new version of Daml-LF is released: Daml-LF 2.3. Its main features are: - -- [`DA.Crypto.Text`](https://docs.digitalasset.com/build/3.5/reference/daml/stdlib/DA-Crypto-Text.html), - originally released in 3.4 in early access (alpha) status, is part of LF 2.3, - which means it is now marked as stable. -- Support for Contract Keys. - -#### Targeting LF 2.3 - -If you want to use new features available in the LF 2.3, select it explicitly as compilation target by setting the -`--target=2.3`, either as direct argument on the command line or as part of a `daml.yaml`: - -``` -sdk-version: 3.5.1 -name: some-name -source: daml -version: 0.0.1 -dependencies: - - daml-prim - - daml-stdlib -build-options: -- --target=2.3 -``` - -After changing the settings, the source code must be recompiled. Please note that this will cause the package id to -change, which should be accompanied by a version change. - -### Logical Synchronizer Upgrades - - -### DA BFT Beta - -DA BFT is a new ordering service as part of the synchronizer that will replace the current single-leader CometBFT ordering service on the Global Synchronizer with a parallel, multi-leader consensus architecture, enabling significantly higher transaction throughput and fault tolerance. - -As part of this release DA BFT is ready in beta form for early access testing, but not recommended for production or close-to-production testing yet. - -### Multi Synchronizer Alpha - -Multi-synchronizer support is available in early access and has to be enabled explicitly. -This feature should only be used in test environments. - -To enable contract reassignment across synchronizers, the flag `PARTICIPANT_FEATURE_FLAG_ENABLE_ALPHA_MULTI_SYNCHRONIZER` must be activated on all participants hosting a stakeholder of the contract on both the source and target synchronizers. For a synchronizer, it can be done as follows: - -``` -participant.topology.synchronizer_trust_certificates.propose( - p.id, - synchronizerId, - featureFlags = Seq(ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer), -) -``` - - -## Functional Changes - -### Party Replication - -#### Offline party replication - -Concluding an offline party replication by clearing the onboarding flag now includes two major updates -when using protocol version 35: -- Added crash resilience for ongoing clearances. -- Automatic scheduling for clearances when a participant (re)connects to the synchronizer. - -These changes apply only to the `participant.parties.import_party_acs` and -`participant.parties.clear_party_onboarding_flag` endpoints. - -Note: The replicated party ID must be included in the party ACS import call to enable automatic -scheduling. The original behaviour is retained for protocol version 34. - -#### Party replication onboarding topology event is exposed on Ledger API - -The `PartyToParticipant` topology "onboarding" state used in the process of replicating a party with existing -contracts is now visible via the Ledger API when a party onboards on a synchronizer on protocol version 35 or higher. -Starting with PV=35, the newly introduced `ParticipantAuthorizationOnboarding` Ledger API topology event signals -the beginning of party replication and transitions to `ParticipantAuthorizationAdded` once the party's ACS is fully -visible on the Ledger API. - -#### Preview: Online party replication - -- Added the file-based online party replication command `participant.parties.add_party_with_acs_async` to - be used along with `participant.parties.export_party_acs` and instead of the sequencer-channel-based - `add_party_async` command. -- The online party replication status command now returns status in a very different, "vector-status" format - rather than the old "oneof" style. This impacts the `participant.parties.get_add_party_status` command and - `com.digitalasset.canton.admin.participant.v30.PartyManagementService.GetAddPartyStatus` gRPC response type. -- The participant configuration to enable online party replication has been renamed to - `alpha-online-party-replication-support` from `unsafe-online-party-replication` for consistency with other - alpha features and to reflect that the default file-based mode is more secure not relying on sequencer - channels. -- The sequencer configuration to enable sequencer channels for online party replication has been renamed to - `unsafe-sequencer-channel-support` from `unsafe-enable-online-party-replication` for consistency and to - refer specifically to sequencer channels. - -#### Minor Improvements - -- Onboarding party submission prevention: Ensures a participant does not submit a transaction or reassignment on behalf - of an onboarding party. -- Upgraded gRPC to 1.81.0 and AWS SDK to 2.44.3 to resolve Netty 4.1.130 CVEs (CVE-2026-33870, CVE-2026-33871). - -### New Transaction Hashing Scheme v3 - -- A new hashing scheme version `HASHING_SCHEME_VERSION_V3` has been introduced that includes the transaction's `max_record_time` in the hash computation and covers the new transaction node and fields of contract keys. This new version is avaialble from Protocol Version 35. -- See the [hashing algorithm documentation](https://docs.digitalasset-staging.com/build/3.5/explanations/external-signing/external_signing_hashing_algorithm.html#summary-of-differences-between-v2-and-v3) for the updated version. -- The `max_record_time` is now enforced by all confirming participants. -- The Ledger API and Ledger JSON API prepare `InteractiveSubmissionService` has been modified to take in a specific hashing scheme version in the request. -The default hashing scheme is `HASHING_SCHEME_VERSION_V2`. Integrators are encouraged to move to `HASHING_SCHEME_VERSION_V3` for synchronizers using protocol version 35. -In particular, usage of **contract keys** requires `HASHING_SCHEME_VERSION_V3`. See the versioning [documentation](https://docs.digitalasset-staging.com/build/3.5/explanations/external-signing/external_signing_hashing_algorithm.html#hashing-scheme-version) for details. - -### Active Contracts Head Snapshot (ACHS) - -The Active Contracts Head Snapshot (ACHS) is a new optional feature that maintains a continuously updated snapshot of -the currently active contracts. When enabled, the ACHS accelerates `GetActiveContracts` (ACS) queries by allowing them -to read directly from a pre-computed snapshot rather than scanning the full event log to reconstruct the active set. - -ACHS is disabled by default. To enable it, configure the `achs-config` block under the participant's indexer settings: -``` -canton.participants..parameters.ledger-api-server.indexer.achs-config { - valid-at-distance-target = 1000000 - last-populated-distance-target = 500000 -} -``` - -The `valid-at-distance-target` controls how far behind the ledger end (in event sequential IDs) the snapshot's validity -point is maintained. The ACHS is not used for serving queries below its validity point, logging at INFO level "ACHS for -skipped since validAt (...) already surpassed requested activeAt (...)". If the `valid-at-distance-target` -value is too small, long-running ACS queries may observe the ACHS validity point -moving (mid-stream) past their requested offset, causing the stream to fall back to the slower filter tables query, logging -at INFO level "ACHS stream for fell back to filter tables from (...) since validAt (...) surpassed activeAtEventSeqId (...)". If -the value is too large, the tail portion of the ACS (between the ACHS validity point and the requested offset) must be -resolved from the filter tables, making that last segment more expensive. - -As described above, when the ACHS validity point moves or is past the requested offset, an info-level log message is -emitted indicating that the stream fell back to the filter tables. -Two corresponding metrics, `achs_skips` and `achs_midstream_fallbacks`, are available under `daml.participant.api.index` -to help operators monitor the frequency of these fallbacks and tune the `valid-at-distance-target` accordingly. - -The `last-populated-distance-target` controls the additional lag (in event -sequential IDs) for the population of ACHS in order to store only the long-lived contracts. A larger value reduces -database I/O by skipping short-lived contracts that are created and archived before they would be added to the snapshot. -However, setting it too large increases the cost of the remaining ACS tail, as more data must be fetched from the filter -tables to cover the gap between the last populated point and the ACHS validity point. - -Further tuning parameters include: -- `population-parallelism`: number of parallel threads for adding activations to the ACHS during normal operation. -- `removal-parallelism`: number of parallel threads for removing deactivated activations from the ACHS during normal operation. -- `aggregation-threshold`: minimum batch size (in event sequential IDs) before ACHS maintenance work is emitted. -- `init-parallelism`: number of parallel threads for ACHS population and removal during initialization. -- `init-aggregation-threshold`: minimum batch size (in event sequential IDs) for ACHS maintenance during initialization. -- `buffer-size`: size of the internal buffer between the indexer pipeline and the ACHS maintenance flow. - -The `deactivation_distances` histogram metric which is available under `daml.participant.api.indexer.deactivation_distances` -can help operators understand the distribution of contract lifetimes (the event sequential ID distance between a contract's -activation and its deactivation) and set an appropriate `last-populated-distance-target`. Ideally, the population distance -should be large enough so that most short-lived contracts are already deactivated and thus not added to the snapshot. - -Three gauge metrics are available under `daml.participant.api.indexer` to monitor the ACHS state: -- `achs_valid_at`: the event sequential ID at which the ACHS is currently valid. ACS queries with a requested offset - at or after this value can read directly from the ACHS. -- `achs_last_populated`: the last event sequential ID for which activations were added to the ACHS. -- `achs_last_removed`: the last event sequential ID for which deactivations were looked up and the corresponding - activations were removed from the ACHS. - -### Hardened Error Handling in Sequencer Connect Service - -We have implemented strict error sanitization and rewording for the SequencerConnectService to mitigate information leakage. -Detailed internal error messages are now redacted before being sent to clients. - -If detailed diagnostics are required in a non-production environment, sanitization can be toggled off via: - -``` -canton.monitoring.sanitize-public-error-messages = false -``` - -### Ignoring of offboarded sequencers for submission requests - -In the case where sequencers are offboarded but remain online and kept in the connectivity configuration, it was still possible that members pick them as the target for submission requests. The submission would fail, but the member would incur a delay as it requires retrying. -This has now changed, and offboarded sequencers are ignored when sending submission requests. - -#### API Changes - -The previous method of returning errors via response fields has been removed in favor of canonical gRPC error propagation. -The following fields are now obsolete: - -- `HandshakeResponse.value.failure` -- `VerifyActiveResponse.value.failure` - -Errors are now communicated strictly through `io.grpc.Status` codes to ensure a consistent and secure interface. - -Status codes have changed as follows: - -- SequencerAuthenticationService.challenge newly fails with `INVALID_ARGUMENT` (instead of `FAILED_PRECONDITION`), - if the client does not support the sequencer's protocol version. -- SequencerConnectService newly fails with `INVALID_ARGUMENT` (instead of `FAILED_PRECONDITION`) if a non-participant tries to connect. -- SequencerConnectService.registerOnboardingTopologyTransactions newly fails with `INTERNAL` (instead of `FAILED_PRECONDITIONS`) -- if there are no dynamic synchronizer parameters. -- SequencerConnectService.registerOnboardingTopologyTransactions newly fails with `FAILED_PRECONDITION` if -- the transactions cannot be added to the topology state and sanitization of error messages is enabled. - -### Mediator Crash Fault Tolerance - -The mediator is now crash fault-tolerant and guarantees that all verdicts will eventually be persisted and available on the inspection API. - -### Enhanced Reliability for `GetHighestOffsetByTimestamp` - -Previously, the `GetHighestOffsetByTimestamp` RPC and the `find_highest_offset_by_timestamp` console command could return offsets not yet synced with the participant's local cache. Furthermore, forcing a query with a future timestamp resulted in an error. - -Specific changes: -- The required state is now retrieved atomically via a consistent database snapshot. -- The endpoint now includes an internal barrier (waiting up to 10 seconds) to ensure the local Ledger API cache catches up with the database before returning the offset. -- When `force` is true, requesting a future timestamp now gracefully returns the current ledger end instead of failing. - -No migration required. - -### ACS stream continuation - -The `GetActiveContracts` stream request has been extended with an optional `stream_continuation_token` field that allows -clients to continue an interrupted ACS stream from the last element which made through. The field can be populated with -the `stream_continuation_token` field of the last response element received before the interruption, and the stream will -continue from the next element after that. - -### ACS Ledger API counting - -Introduced a new memory-efficient consoled command `participant.ledger_api.acs.count()` -to count the number of active contracts on a participant node. - -> Note: This command is currently under the Testing feature flag. - -### ACS pagination - -A new, `GetActiveContractsPage` endpoint added to State Service API. This enables the client to retrieve the ACS in -paginated form, by specifying a `max_page_size`. The pages can be accessed sequentially by using the `page_token` -field. The token can be obtained from the `GetActiveContractsPageResponse` of the last page. - -### GetUpdates stream in descending order of events - -The `GetUpdatesRequest` object has new optional parameter `descending_order`. When this parameter is `true` the events -are streamed from the newest to the oldest ones. The pages can be accessed sequentially by using the `page_token` -field. - -### GetUpdates pagination -A new `GetUpdatesPage` endpoint has been added to Update Service API. THis allows retrieval of updates in paginated -form instead of requesting the stream. - -### Improvements for `repair.add` and migration advice - -The `participant.repair.add` admin command has been revised to use the new `ImportAcs` backend, bringing significant -memory performance improvements, stricter default safety validations, and several new parameters. - -#### Important behavioral change: strict `Validation` by default - -Previously, `repair.add` implicitly accepted all injected contracts without re-evaluating their cryptographic hashes. To -prevent accidental data corruption, the command now defaults to **Validation** mode ( -`contractImportMode = ContractImportMode.Validation`). - -- **Impact:** If you have existing scripts or recovery procedures that inject manually modified, synthetic, or - inconsistent contracts (where the payload does not strictly match the `ContractId` hash), they will now fail with a - `"Failed to authenticate contract with id"` error. -- **Migration:** To bypass this cryptographic validation and restore the legacy behavior, explicitly pass the `Accept` - mode in your command call: - ```scala - participant.repair.add( - synchronizerId = mySynchronizer, - protocolVersion = myProtocolVersion, - contracts = myContracts, - contractImportMode = ContractImportMode.Accept // Bypasses strict validation - ) - ``` - -#### New parameters - -The command signature has been expanded to support several optional parameters: - -- `workflowIdPrefix`: Allows you to set a custom prefix for the generated workflow ID to easily track the repair - transactions (defaults to `import-`). -- `contractImportMode`: Choose between `Validation` (default, validates that contract IDs comply with the scheme - associated to the synchronizer where the contracts are assigned), or `Accept` the contracts as they are (if you know - what you are doing). -- `representativePackageIdOverride`: Allows you to remap or override the representative package IDs of the contracts as - they are imported. -- `excludedStakeholders`: When defined, any contract that has one or more of these parties as a stakeholder will not be - added. - -### Improved party and repair ACS imports - -We have completely overhauled the ACS import endpoints for both party replication and participant repair to be -memory-efficient streaming endpoints: - -- Console command `participant.parties.import_party_acs` -- Console command `participant.repair.import_acs` -- gRPC RPC `PartyManagementService.ImportPartyAcs` -- gRPC RPC `ParticipantRepairService.ImportAcs` +### Topic A +Template for a bigger topic +#### Background +#### Specific Changes +#### Impact and Migration -This resolves previous memory limitations, as these endpoints no longer load the entire ACS snapshot into memory at -once. +### New `GetCompletions` endpoint on the command completion service +#### Background +The command completion service only offered `CompletionStream`, which always filters by a single user and a non-empty set of parties. There was no way to stream completions across all parties. -#### Action required: Breaking API change +#### Specific Changes +A new completion streaming endpoint is introduced: `GetCompletions` (gRPC) or `/commands/command-completions` (JSON API). This endpoint offers more flexible filtering semantics compared to the existing `CompletionStream`, `/commands/completions` endpoint. It filters by `parties` only; there is no user filtering: +- A non-empty `parties` filters to those parties and requires `ReadAs` (or `ActAs`) for each. +- An empty `parties` returns completions for all parties and requires `ReadAsAnyParty` (or `ActAsAnyParty`). -The `synchronizerId` is now a **mandatory** first parameter for both the `import_party_acs` and `import_acs` console -commands as well as their analogous gRPC endpoints. You will need to update any existing scripts. +In all other respects it behaves like `CompletionStream`, which becomes deprecated. -**For `import_party_acs`:** +#### Impact and Migration +This is an additive change. `CompletionStream` is unchanged, so no migration is required. -- **Old usage:** `participant.parties.import_party_acs("canton-acs-export.gz")` -- **New usage:** `participant.parties.import_party_acs(mySynchronizerId, importFilePath = "canton-acs-export.gz")` +### Minor Improvements +- Added latency metrics for signing and decryption operations. +- Updated nix package to source from GHCR and bump dpm version to 1.0.20 for remote dar support +- Fixed a BFT ordering sequencer crash-recovery issue affecting freshly onboarded nodes. If such a node crashed and restarted while still within its onboarding start epoch, on restart it could attempt to recover the output module from a block below its durable lower bound (a block that was never stored by this node), which could leave the node stuck. The onboarding boundary block is now persisted with its BFT time to seed BFT-time computation across a restart, the node's own activation time is reconstructed when needed for crash recovery, and output-module recovery is clamped to the durable lower bound. +- LSU: improved handshake between a sequencer and its successor: the physical synchronizer id and sequencer id are now validated. -**For `import_acs`:** +### Preview Features +- preview feature -- **Old usage:** `participant.repair.import_acs("canton-acs-export.gz")` -- **New usage:** `participant.repair.import_acs(mySynchronizerId, importFilePath = "canton-acs-export.gz")` +## Bugfixes -Because of the mandatory `synchronizerId` parameter, to import a multi-synchronizer ACS snapshot, you must now call the -endpoint sequentially for each synchronizer your participant is connected to, using the exact same snapshot file. The -import process will ignore any contracts in the snapshot that are associated to a different synchronizer. +### (YY-nnn, Risk): Title -##### Details on the gRPC `ImportAcs` repair endpoint +#### Issue Description -The `ImportAcs` and `ImportAcsV2` RPCs have been consolidated, introducing the following breaking changes and migration -steps: +#### Affected Deployments -- **Endpoint removed:** `ImportAcsV2` (along with its request/response messages) is completely removed. All clients must - migrate to the standard `ImportAcs` RPC. -- **Request signature and type changes:** - - Fields `workflow_id_prefix` (2), `contract_import_mode` (3), and `representative_package_id_override` (5) in - `ImportAcsRequest` are now explicitly `optional`. - - A new `optional string synchronizer_id = 6` field was added. - - **Migration (ScalaPB):** Adding `optional` changes generated code from base types to `Option[T]`. Existing clients - will fail to compile and must be updated to wrap assigned values (e.g., `workflowIdPrefix = Some("prefix")`) and - explicitly handle reading `Option` types. -- **Behavioral change (`synchronizer_id`):** When filtering by synchronizer, mismatched contracts are now ignored. This - breaks previous logic that relied on the import strictly aborting upon a mismatch. +#### Affected Versions -##### Details on the gRPC `ImportPartyAcs` party replication endpoint +#### Impact -The `ImportPartyAcs` endpoint underwent the exact same consolidation (removing `ImportPartyAcsV2`), streaming semantics -updates, generated code changes (ScalaPB `Option[T]`), and mismatched synchronizer behavior (ignoring rather than -failing) as `ImportAcs`. +#### Symptom -**Key differences specific to `ImportPartyAcs`:** +#### Workaround -- **New capability (`party_id`):** A new `optional string party_id = 6` field was added. Providing this in the first - request of the stream enables automatic, crash-resilient scheduling of the onboarding flag clearance. If omitted, the - participant logs a warning, and the flag must be cleared manually. +#### Likeliness -### Topology-Aware Package Selection (TAPS) improvements - -Topology-Aware Package Selection (TAPS) refinement for handling inconsistent vetting states: -- The algorithm now considers a party's package vetting state only for packages required by that party in the interpreted transaction. - It starts with a minimal set of restrictions derived from the command's root nodes and progressively accumulates more restrictions over a configurable number of passes. - This iterative process increases the likelihood of finding a valid package selection set for the routing of the transaction. -- The maximum number of TAPS passes can be set at the request-level via the optional `taps_max_passes` field in `Commands` or `PrepareSubmissionRequest` messages. - If not specified, the default value is taken from the participant configuration via `participants.participant.ledger-api.topology-aware-package-selection.max-passes-default` (defaults to `3`). - A hard limit is enforced by `participants.participant.ledger-api.topology-aware-package-selection.max-passes-limit` (defaults to `4`). -- TAPS now ignores unvetted dependencies of packages that are not required for interpretation. - complying now with the support of unvetted dependencies in the Canton protocol. - -### Ledger API Improvements - -- ApiRequestLogger now also used by Ledger JSON Api. Changes: - - Redundant Request TID removed from logs. - - Additional CLI options added: `--log-access` captures API access logs in a separate file (default: `log/canton_access.log`), and `--log-access-errors` captures API access errors in a separate file (default: `log/canton_access_error.log`). - - Additional config options added: `debugInProcessRequests` logs in-process gRPC requests at DEBUG instead of TRACE, and `prefixGrpcAddresses` prefixes gRPC client addresses with `grpc:` (enabled by default). -- LedgerAPI ListKnownParties supports an optional prefix filter argument filterParty. - The respective JSON API endpoint now additionally supports `identity-provider-id` as - an optional argument, as well as `filter-party`. -- Protect the admin participant from self lock-out. It is now impossible for an admin to remove own admin rights or - delete itself. -- On Ledger API interface subscriptions, the `CreatedEvent.interface_views` now returns the ID of the package containing - the interface implementation that was used to compute the specific interface view as `InterfaceView.implementation_package_id`. -- OffsetCheckpoints are now always generated when an open-ended updates or completions stream is requested, even if there - are no updates. The checkpoint can have the same offset as the exclusive start of the stream, making checkpoints visible - even when starting from the ledger end. This enables client systems to recognize when the ledger end is advancing, - even if the stream of updates is inactive. -- Extended the set of characters allowed in user-id in the ledger api to contain brackets: `()`. - This also makes those characters accepted as part of the `sub` claims in JWT tokens. -- Functionality for managing internal and external parties has been improved, removing previous asymmetry: - - User rights can now be assigned to an external party during allocation. - - External parties can be allocated by the user themselves in the self-administration mode. - Please note that users in self-administration mode can allocate up to N parties, depending on a setting of the parameter - ``` - canton.participants..ledger-api.party-management-service.max-self-allocated-parties - ``` - By default the value of this parameter is 0. -- An IDP administrator can now only allocate parties confined to their own IDP perimeter. - - -## Performance Improvements - -### Session Signing Keys - -Session signing keys can now be used to reduce the number of calls to external KMS (Key Management Service) providers. When enabled, session signing keys are generated and cached locally for a limited duration and used for signing operations during their validity period. - -Please read the documentation on [Session Signing Keys](https://docs.digitalasset.com/operate/3.5/howtos/secure/keys/session_signing_keys.html) for details on how to enable and configure this feature. -Session signing keys are only available from Protocol Version 35 and are not enabled by default. - -### Compatible sibling views compression - -In protocol version 35, each envelope in `TransactionConfirmationRequest` contains multiple views grouped by recipients instead of one envelope per view. -Assignment and re-assignments also use this new format, but they always have one view. - -### Single Topology Transaction for External Parties - -Multiple topology transactions for external parties can now be represented with a single `PartyToParticipant` topology transaction. - -The `generateExternalPartyTopology` endpoint on the Ledger API now returns a single `PartyToParticipant` topology transaction to onboard the party. -The transaction contains signing threshold and signing keys. This effectively deprecate the usage of `PartyToKeyMapping`. -For parties with signing keys both in `PartyToParticipant` and `PartyToKeyMapping`, the keys from `PartyToParticipant` take precedence. - -Deprecated usage of `PartyToKeyMapping`. The functionality provided by `PartyToKeyMapping` is now available directly in `PartyToParticipant`. -Please use `PartyToParticipant` for new transactions. `PartyToKeyMapping` is still fully supported in this version (including existing and new transactions). -In future version, creation of new `PartyToKeyMapping` transactions may be disallowed. - -Deprecated `TopologyManagerReadService.ListAll` in favor of `ListAllV2`, which uses an inclusion -list (`include_mappings`) instead of an exclusion list (`exclude_mappings`). This avoids sending -mapping codes unknown to older servers. The console method `topology.transactions.list` now calls -`ListAllV2` by default and only falls back to `ListAll` when targeting a 3.4 node. The -`excludeMappings` and `protocolVersion` parameters of `topology.transactions.list` are deprecated; -use `filterMappings` instead. - -Deprecated `TopologyManagerReadService.ExportTopologySnapshot` and `TopologyManagerWriteService.ImportTopologySnapshot`, -along with their console counterparts `topology.transactions.export_topology_snapshot`, -`topology.transactions.import_topology_snapshot`, `topology.transactions.import_topology_snapshot_from`, -and `topology.transactions.export_identity_transactions`. -Please use the corresponding `V2` variants (`ExportTopologySnapshotV2` / `ImportTopologySnapshotV2`, -`export_topology_snapshotV2`, `import_topology_snapshotV2`, `import_topology_snapshot_fromV2`, -`export_identity_transactionsV2`) instead, which use an updated internal bytestring format. - -Deprecated `SequencerInitializationService.InitializeSequencerFromGenesisState`, -`SequencerInitializationService.InitializeSequencerFromOnboardingState`, -`SequencerAdministrationService.OnboardingState`, and -`TopologyManagerReadService.GenesisState`, along with their console counterparts -`setup.assign_from_genesis_state`, `setup.assign_from_onboarding_state`, -`setup.onboarding_state_for_sequencer`, `setup.onboarding_state_at_timestamp`, -and `topology.transactions.genesis_state`. -Please use the corresponding `V2` variants (`InitializeSequencerFromGenesisStateV2`, -`InitializeSequencerFromOnboardingStateV2`, `OnboardingStateV2`, `GenesisStateV2`, -`assign_from_genesis_stateV2`, `assign_from_onboarding_stateV2`, -`onboarding_state_for_sequencerV2`, `onboarding_state_at_timestampV2`, -`genesis_stateV2`) instead, which use an updated internal bytestring format -that enables streaming ingestion, making snapshot export and import significantly less memory-intensive. - -### Minor Performance Improvements - -- The Postgres connection tuning configuration of the indexer is now separated from the configuration of the Ledger API server - (`canton.participants..ledger-api.postgres-data-source`). - The new configuration section `canton.participants..parameters.ledger-api-server.indexer.postgres-data-source` should - be used instead to tune the indexer's Postgres connections. -- A new indexer pipeline batching strategy added under the feature flag `useWeighetdBatching`. When switched on, the - batches are created using their estimated database processing time using the `submissionBatchInsertionSize` as a limit - for individual batches -- Changed the `CompressedBatch` structure in the sequencer protocol for protocol version 35 to separately keep recipients and envelopes (from `gzip(Seq((recp1, payload1), (recp2, payload2)))` to `gzip(Seq(recp1, recp2)), Seq(gzip(payload1), gzip(payload2)))`). -- Batching configuration now allows setting different parallelism for pruning (currently only for Sequencer pruning): - New option `canton.sequencers.sequencer.parameters.batching.pruning-parallelism` (defaults to `2`) can be used - separately from the general `canton.sequencers.sequencer.parameters.batching.parallelism` setting. -- Made the config option `...topology.use-time-proofs-to-observe-effective-time` work and changed the default to `false`. - Disabling this option activates a more robust time advancement broadcast mechanism on the sequencers, - which however still does not tolerate crashes or big gaps in block sequencing times. The parameters can be configured - in the sequencer via `canton.sequencers..parameters.time-advancing-topology`. -- Additional metrics for the ACS commitment processor: `daml.participant.sync.commitments.last-incoming-received`, `daml.participant.sync.commitments.last-incoming-processed`, `daml.participant.sync.commitments.last-locally-completed`, and `daml.participant.sync.commitments.last-locally-checkpointed`. - -## Breaking Changes - -### Removal of legacy party replication repair console macros - -The original party replication method, which relied on a silent synchronizer, has been superseded by the offline party -replication process. Consequently, the obsolete repair console macros associated with the legacy approach have -been removed. - -Specifically, the following macros are no longer available: -- `step1_hold_and_store_acs` -- `step2_import_acs` - -If you previously relied on the _Silent synchronizer replication procedure_, you will need to transition to the -current offline party replication process. For details, please consult the -[Offline Party Replication documentation](https://docs.digitalasset.com/operate/3.5/howtos/operate/parties/party_replication.html#offline-party-replication) - -### Removal of deprecated, legacy ACS export and import endpoints - -The legacy repair endpoints for the ACS export and import have been removed: - -- Console command `participant.repair.export_acs_old` -- Console command `participant.repair.import_acs_old` -- gRPC rpc `ParticipantRepairService.ExportAcsOld` -- gRPC rpc `ParticipantRepairService.ImportAcsOld` - -#### Migration advice - -Use repair endpoints without the 'old' suffix: - -- Migrate to `participant.repair.export_acs` from `participant.repair.export_acs_old` -- Migrate to `participant.repair.import_acs` from `participant.repair.import_acs_old` -- Migrate to `ParticipantRepairService.ExportAcs` from `ParticipantRepairService.ExportAcsOld` -- Migrate to `ParticipantRepairService.ImportAcs` from `ParticipantRepairService.ImportAcsOld` - -Note that previously created ACS snapshots with the legacy endpoints cannot be imported with the current endpoints as -the underlying data format has completely changed. - -##### Migrating to export_acs - -The most significant change is the removal of the `timestamp` parameter, which has been replaced by a mandatory -`ledgerOffset` parameter. - -**Console parameter changes:** - -- **New mandatory parameter:** `ledgerOffset (Long)`. You must now specify the exact ledger offset for the snapshot - instead of a `timestamp`. -- **Removed parameters:** `partiesOffboarding`, `timestamp` (replaced by `ledgerOffset`), `force`. -- **Renamed parameters:** `outputFile` is now `exportFilePath` (default is `"canton-acs-export.gz"`), - `filterSynchronizerId` is now `synchronizerId`. -- **New optional parameters:** `excludedStakeholders` allows you to omit contracts that have one or more of these - parties as a stakeholder; `contractSynchronizerRenames` allows mapping contracts from one synchronizer to another - during export. - -**gRPC changes for `ExportAcsRequest`:** - -- **`parties` -> `party_ids`:** Field renamed for consistency. If left empty, the endpoint will act as a wildcard and - export the ACS for *all* parties hosted by the participant. -- **`timestamp` -> `ledger_offset` (Breaking):** You must provide an exact `int64 ledger_offset` instead of a timestamp. -- **`filter_synchronizer_id` -> `synchronizer_id`:** Field renamed for consistency. -- **Removed fields:** `force` and `parties_offboarding` have been completely removed. -- **New fields:** `contract_synchronizer_renames` and `excluded_stakeholder_ids`. - -##### Migrating to import_acs - -The import command remains largely the same in basic usage, but introduces new optional parameters for advanced -validation and overrides, alongside strict memory-efficient streaming semantics for gRPC. - -**Console parameter changes:** - -- **Renamed parameter:** `inputFile` is now `importFilePath` (default is `"canton-acs-export.gz"`). -- **New optional parameters:** `contractImportMode` governs contract validation upon import (defaults to - `ContractImportMode.Validation`); `representativePackageIdOverride` allows overriding representative package IDs - during import; `excludedStakeholders` allows omitting contracts that have one or more of these parties as a - stakeholder. - -**gRPC changes for `ImportAcsRequest`:** - -- **Streaming Semantics (Breaking):** The new endpoint requires metadata fields (like `contract_import_mode`, - `synchronizer_id`, etc.) to be populated *only* in the first request of the stream. Subsequent requests must omit - metadata and only contain the binary `acs_snapshot` chunks. -- **New mandatory fields:** `contract_import_mode` and `synchronizer_id` must be explicitly defined in the first stream - request. -- **Removed fields:** `allow_contract_id_suffix_recomputation` is completely removed. -- **New fields:** `excluded_stakeholder_ids` and `representative_package_id_override`. -- **Response update:** `ImportAcsResponse` is now a completely empty message (previously returned a contract ID - mapping). - -### Only PackageName is accepted on Ledger API - -Usage of package id for ledger queries was deprecated and now the validation will fail if used. -The impacted APIs are: - - GetUpdates - - GetUpdateByOffset - - GetUpdateById - - GetActiveContracts - - GetEventsByContractIdRequest - - SubmitAndWaitForTransaction (the optional `transaction_format`) - - SubmitAndWaitForReassignmentRequest - - ExecuteSubmissionAndWaitForTransactionRequest - -### SynchronizerId field update in Externally signed transactions - -In Protocol version 35, the `synchronizer_id` field in externally signed prepared transaction metadata -will be populated with the physical synchronizer ID of the synchronizer on which the transaction will be processed, -instead of the logical synchronizer ID, as is the case in PV 34. -Applications must ensure they do not rely on the format of the `synchronizer_id` value. - -### Changes from NonNegativeLong to Long - -Some console commands using a NonNegativeLong for the offset are changed to accept a Long instead. -Similarly, some console commands returning an offset now return a Long instead of a NonNegativeLong. -It brings consistency and allows to pass the output of `participant.ledger_api.state.end()`. - -Impacted commands: -- `participant.repair.export_acs` -- `participant.parties.find_party_max_activation_offset` -- `participant.parties.find_party_max_deactivation_offset` -- `participant.parties.find_highest_offset_by_timestamp` - -### Removal of automatic recomputation of contract ids upon ACS import - -The ability to recompute contract ids upon ACS import has been removed. - -### Removal of multi-host name resolution tooling - -Support for the multi-host name resolution was removed. -This was only used if synchronizer connectivity defined a sequencer with multiple endpoints, which is not supported with our current sequencers: -we now have multiple sequencers each with exactly one endpoint. - -### Ledger JSON API Spec Corrections - -JSON Ledger API OpenAPI/AsyncAPI spec corrections -- Fields not marked as required in the Ledger API `.proto` specification are now also optional in the OpenAPI/AsyncAPI specifications. - If your client code is using code generated using previous versions of these specifications, it may not compile or function correctly with the new version. To migrate: - - If you prefer not to update your code, continue using the previous specification versions as the JSON API server preserves backward compatibility. - - If you want to use new endpoints, features or leverage the new less strict spec, migrate to the new OpenAPI/AsyncAPI specifications as follows: - - Java clients: No changes are needed if you use the `OpenAPI Generator`. Otherwise, potentially optionality of fields should be handled appropriately for other code generators. - - TypeScript clients: Update your code to handle optional fields, using the `!` or `??` operators as appropriate. -- From Canton 3.5 onwards, OpenAPI/AsyncAPI specification files are suffixed with the Canton version (e.g., `openapi-3.5.0.yaml`). -- Canton 3.5 is compatible with OpenAPI specification files from version 3.4.0 to 3.5.0 (inclusive). - -- The Ledger JSON API server now enforces that only fields marked as required by the Ledger API OpenAPI/AsyncAPI specification are mandatory in request payloads. - -### Change from grpcurl to grpc-health-probe in all Docker images - -The tool used for health check probes changed from grpcurl to grpc-health-probe in all the docker images. - -### Minor Breaking Changes - -- The expert `keep-alive-client` configuration parameter for various client services moved to `channel.keep-alive-client`. -- We reduced the defaults for `setBalanceRequestSubmissionWindowSize` and `defaultMaxSequencingTimeOffset` - to 2 minutes. -- The default OTLP gRPC port that the Canton connects to in order to export the traces has been changed from - 4318 to 4317. This aligns the default configuration of Canton with the default configuration of the OpenTelemetry - Collector. This change affects only the users who have configured an OTLP trace export through - ``` - canton.monitoring.tracing.tracer.exporter.type=otlp - ``` -- Removed the `LastErrorsAppender` along with the Admin API endpoints `StatusService.GetLastErrors` and `StatusServiceGetLastErrorTrace`, as - well as the corresponding console commands `last_errors` and `last_error_trace`. - - -## Deprecations - -### Deprecate scope-based access tokens -- "Scope-based" access tokens, i.e. JWTs without any audience specified, have been deprecated. -- A configuration that does not specify a `target-audience` will log a warning on node startup. -- Configurations that specify both a `target-audience` and a `target-scope` are not supported in this version and will also log a warning on node startup. -- Starting Canton version 3.7, support for "scope-based" tokens will be removed entirely to enforce a valid `aud` field in every incoming JWT. -- The `scope` field will, in a future version, be repurposed to serve exclusively as an additional, optional claim for fine-grained permissions. - -### Removal of the old sequencer connection transports - -The old sequencer connections transports have been removed, and only the new sequencer connection pool remains. -Consequently, the configuration `.sequencer-client.use-new-connection-pool` has been deprecated and no longer has any effect. - -### Deprecate initial protocol version configuration - -The config key `participant.parameters.initial-protocol-version` was unused and has been marked as deprecated. - -### Configuration Deprecations - -- The configuration parameters `topology.use-new-processor` and `topology.use-new-client` have been deprecated and now default to true. Configuring those parameters to false will be ignored. -- The parameter `canton.participants..parameters.package-metadata-view.init-takes-too-long-interval` - is now ignored, and a warning will only be printed once, rather than periodically. -- The parameter `canton.participants..parameters.ledger-api-server.indexer.prepare-package-metadata-time-out-warning` - is now ignored. -- The individual JVM metric flags `classes`, `cpu`, `memoryPools`, `threads`, `gc`, and `buffers` in - `canton.monitoring.metrics.jvm-metrics` are no longer supported since the upgrade to OpenTelemetry instrumentation 2.26.0. - All standard JVM metrics (classes, cpu, memory pools, threads, garbage collector) are now always enabled when - `jvm-metrics.enabled = true`. A new `experimental` flag has been added to control experimental JVM metrics - (e.g. buffer pools). Users who previously set `buffers = true` should migrate to `experimental = true`. - See https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/16087 for details. -- The Zipkin trace exporter configuration `canton.monitoring.tracing.tracer.exporter.type=zipkin` is - deprecated following the OpenTelemetry specification deprecation of Zipkin exporters. The Zipkin exporter - will be removed in a future release. Users should migrate to the OTLP exporter. - See https://opentelemetry.io/blog/2025/deprecating-zipkin-exporters/ for details. -- Removed the feature flag `canton.sequencers..parameters.async-writer.enabled`, as async writing is now - the only supported mode. -- Changed the path for `crypto.kms.session-signing-keys` (deprecated) to `crypto.session-signing-keys` so that session signing key configuration is no longer directly tied to a KMS. However, session signing keys can still only be enabled when using a KMS provider or when running with `non-standard-config=true`. -- `package-dependency-cache` field in `caching` configuration is deprecated. It can be removed safely from node configurations. - -### Ledger JSON API package vetting endpoints - -The Ledger JSON API `v2/package-vetting` endpoint exposes list functionality on the GET method by accepting a request body. This is not recommended by the HTTP specification, hence the endpoint is deprecated. -For consistency, the POST method, used for updating the vetting state, of the same endpoint is also deprecated. - -In turn, two new endpoints are implemented to provide the same functionality: -- `v2/package-vetting/list` accepts a POST request with the same body as the deprecated GET `v2/package-vetting` endpoint and returns the list of vetted packages in the same format. -- `v2/package-vetting/update` accepts a POST request with the same body as the deprecated POST endpoint `v2/package-vetting` and returns the updated vetting state of the package in the same format. - - -### Protocol version parameter in topology list commands - -The `protocolVersion` parameter in all `.topology..list` console commands has been deprecated and will be removed in a future version. - -## Minor Improvements - -### Bugfixes - -- Fixed a mid-crash recovery issue for offline party replication and repair ACS imports. Previously, if an ACS import - was interrupted (for example by a participant node restart or crash), a subsequent recovery attempt could result in - missing contracts on the Ledger API. The recovery process now properly rolls back uncommitted partial states upon - retrying the ACS import, ensuring recovered contracts are completely synchronized across both internal storage - and the Ledger API. -- Fixed a bug where the Ledger API `PackageService.ListVettedPackages` used to return a potentially not yet - effective state of the vetted packages. Now it returns the state of vetted packages effective at the time of the request. -- Sequencer health status used to incorrectly return the synchronizer uid instead of the sequencer uid. -- Prevent Ledger API crashes after running `ParticipantRepairService.PurgeContracts` admin command. - Fixes a critical issue where using the `ParticipantRepairService.PurgeContracts` command (when multi-synchronizer support is disabled) generated malformed - Daml values for the choice argument and choice result of the `Archive` choice of the purge contract events in the Ledger API event store. - This previously caused the Ledger API streams reading the generated `Archive` events to crash. - The repair command now generates correct Daml values for the corresponding entries, that can be safely delivered by the Ledger API. -- Fixed a bug in the repair service's `changeAssignation` where only a single repair counter was allocated when reassigning multiple contracts, - violating the monotonicity expected by the indexer. - -### Ledger API Multi-Synchronizer Events Alpha Support - -Adds a new participant node parameter, `alpha-multi-synchronizer-support` (Boolean). -- **Default (`false`):** Uses standard **Create** and **Archive** events. -- **Enabled (`true`):** Uses **Assign** and **Unassign** events. - -This flag is required in multi-synchronizer environments to preserve the **reassignment counter** of a contract. -Using the default (Create events) resets this counter to zero. - -Note: Multi-synchronizer support is currently in Alpha; most Ledger API consumers may not yet be compatible with -Assign/Unassign events. Only enable this if your application specifically requires non-zero reassignment counters -and can process these event types. - -### Support for adding table settings for PostgreSQL - -Added support for adding table settings for PostgreSQL. One can use a repeatable migration (Flyway feature) in a file -provided to Canton externally. - - Use the new config `repeatable-migrations-paths` under the `canton...storage.parameters` configuration section. - - The config takes a list of directories where repeatable migration files must be placed, paths must be prefixed with `filesystem:` for Flyway to recognize them. - - Example: `canton.sequencers.sequencer1.storage.parameters.repeatable-migrations-paths = ["filesystem:community/common/src/test/resources/test_table_settings"]`. - - Only repeatable migrations are allowed in these directories: files with names starting with `R__` and ending with `.sql`. - - The files cannot be removed once added, but they can be modified (unlike the `V__` versioned schema migrations), and if modified these will be reapplied on each Canton startup. - - The files are applied in lexicographical order. - - Example use case: adding `autovacuum_*` settings to existing tables. - - Only add idempotent changes in repeatable migrations. - -### Offline root namespace key scripts - -Offline root namespace key scripts: -- Renamed `prepare-certs.sh` to `prepare-cert.sh` -- Changed `assemble-certs.sh` to automatically suffix the generated certificate with a `.cert` extension, similarly to what is being done in `prepare-cert.sh` -- Removed the `10-offline-root-namespace-init` example folder as its content is now integrated in the documented how-to: https://docs.digitalasset.com/operate/3.5/howtos/secure/keys/namespace_key.html -- Committed the buf image necessary to run the script to the repository (also available in the release artifact), making usage from the open source repo easier - -### Reliability Improvements - -- Added a field `MaxConcurrentCallsPerConnection` and corresponding default - `defaultMaxConcurrentCallsPerConnection` (set to 100000) to `ServerConfig`. - This corresponds to `max-concurrent-streams-per-connection` in the app configs, e.g., - `docker/canton/images/canton-sequencer/app.conf` and can be changed there. At present - the value for sequencers is configured to be 500 for the public API and 100 for the Admin API. -- Added network timeout and client_connection_check_interval for db operations in the Ledger API server and indexer to avoid - hanging connections for Postgres (see PostgresDataSourceConfig). The defaults are 60 seconds network timeout and - 5 seconds client_connection_check_interval for the Ledger API server, and 20 seconds network timeout and - 5 seconds client_connection_check_interval for the indexer. These values can be configured via the new configuration parameters - `canton.participants..ledger-api.postgres-data-source.network-timeout` for network timeout of the Ledger API - server and `canton.participants..parameters.ledger-api-server.indexer.postgres-data-source.client-connection-check-interval` - for the client_connection_check_interval of the indexer. -- `.replication.connection-pool.connection.client-connection-check-interval` is introduced - that allows configuring the PostgreSQL-specific `client_connection_check_interval` parameter for DB locked connections. - This is a safety mechanism to prevent hanging connections in case of network issues. The default value is 5 seconds. -- The Ledger API now enforces a maximum number of signatures per party that can be provided for external submissions. - This value defaults to 50 and can be changed at the following config path: `canton.participants..ledger-api.interactive-submission-service.maximum-number-of-signatures-per-party` -- Added a new configuration parameter `canton.participants..ledger-api.index-service.max-lookup-limit` that caps the maximum number of contracts returned by a contract key lookup per request. - The default value is 1000. -- When the AcsCommitmentProcessor is initializing, read stakeholder groups from the snapshot in batches of size - `canton.parameters.general.batching.max-stakeholder-groups-batch-size` (default 1000), rather than all at once. - This allows early termination of this initialization if the node is shutting down. -- The release version is now exposed in `NodeStatus.NotInitialized`, so the node version can be retrieved even before the node is initialized. +#### Recommendation ## Compatibility @@ -848,3 +72,9 @@ Canton has been tested against the following versions of its dependencies: |----------------------------|----------------------------| | Java Runtime | JAVA_VERSION | | Postgres | POSTGRES_VERSION | + + +## What's Coming + +We are currently working on + diff --git a/canton/VERSION b/canton/VERSION index 4bba389fca..50ebb4c654 100644 --- a/canton/VERSION +++ b/canton/VERSION @@ -1 +1 @@ -3.5.1-SNAPSHOT +3.5.7-SNAPSHOT diff --git a/canton/base/adjustable-clock/src/main/scala/com/daml/clock/AdjustableClock.scala b/canton/base/adjustable-clock/src/main/scala/com/daml/clock/AdjustableClock.scala deleted file mode 100644 index 82813de3ee..0000000000 --- a/canton/base/adjustable-clock/src/main/scala/com/daml/clock/AdjustableClock.scala +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -package com.daml.clock - -import java.time.{Clock, Duration, Instant, ZoneId} - -@SuppressWarnings(Array("org.wartremover.warts.Var")) -final case class AdjustableClock(baseClock: Clock, var offset: Duration) extends Clock { - def fastForward(by: Duration): Unit = - offset = offset.plus(by) - - def rewind(by: Duration): Unit = - offset = offset.minus(by) - - def set(to: Instant): Unit = - offset = Duration.between(baseClock.instant(), to) - - override def getZone: ZoneId = baseClock.getZone - - override def withZone(zone: ZoneId): Clock = - if (zone == baseClock.getZone) this - else AdjustableClock(baseClock.withZone(zone), offset) - - override def millis: Long = Math.addExact(baseClock.millis, offset.toMillis) - - override def instant: Instant = baseClock.instant.plus(offset) - - override def equals(obj: Any): Boolean = obj match { - case other: AdjustableClock => baseClock == other.baseClock && offset == other.offset - case _ => false - } - - override def hashCode: Int = baseClock.hashCode ^ offset.hashCode -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/AuthServiceJWTPayload.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/AuthServiceJWTPayload.scala deleted file mode 100644 index c114a97b97..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/AuthServiceJWTPayload.scala +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import io.circe.* -import io.circe.parser.* -import org.slf4j.{Logger, LoggerFactory} - -import java.time.Instant -import java.util.concurrent.atomic.AtomicBoolean -import scala.util.{Failure, Success, Try} - -/** All the JWT payloads that can be used with the JWT auth service. */ -sealed abstract class AuthServiceJWTPayload extends Product with Serializable - -/** There are two JWT token formats which are currently supported by `StandardJWTPayload`. The - * format is identified by `aud` claim. - */ -sealed trait StandardJWTTokenFormat -object StandardJWTTokenFormat { - - /** `Scope` format is for the tokens where scope field contains `daml_ledger_api` or if it - * contains a bespoke string configured through a target-scope parameter. - */ - final case object Scope extends StandardJWTTokenFormat - - /** `Audience` format is for the tokens where `aud` claim starts with - * `https://daml.com/jwt/aud/participant/` or if it contains a bespoke string configured through - * a target-audience parameter. - */ - final case object Audience extends StandardJWTTokenFormat -} - -/** Payload parsed from the standard "sub", "aud", "exp", "iss" claims as specified in - * https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 - * - * @param issuer - * The issuer of the JWT. - * - * @param userId - * The user that is authenticated by this payload. - * - * @param participantId - * If not set, then the user is authenticated for any participant node that accepts the JWT - * issuer. We expect this to be used for development only. If set then the user is authenticated - * for the given participantId. - * - * @param exp - * If set, the token is only valid before the given instant. - * @param audiences - * If non-empty and it is an audience-based token, the token is only valid for the intended - * recipients. - */ -final case class StandardJWTPayload( - issuer: Option[String], - userId: String, - participantId: Option[String], - exp: Option[Instant], - format: StandardJWTTokenFormat, - audiences: List[String], - scope: Option[String], -) extends AuthServiceJWTPayload - -/** Codec for writing and reading [[AuthServiceJWTPayload]] to and from JSON. - * - * In general: - * - All custom claims are placed in a namespace field according to the OpenID Connect standard. - * - Access tokens use a Daml-specific scope to distinguish them from other access tokens issued - * by the same issuer for different systems or APIs. - * - All fields are optional in JSON for forward/backward compatibility reasons, where - * appropriate. - * - Extra JSON fields are ignored when reading. - * - Null values and missing JSON fields map to None or a safe default value (if there is one). - */ -object AuthServiceJWTCodec { - - protected val logger: Logger = LoggerFactory.getLogger(AuthServiceJWTCodec.getClass) - - // ------------------------------------------------------------------------------------------------------------------ - // Constants used in the encoding - // ------------------------------------------------------------------------------------------------------------------ - // Unique scope for standard tokens, following the pattern of https://developers.google.com/identity/protocols/oauth2/scopes - final val scopeLedgerApiFull: String = "daml_ledger_api" - - private[this] final val audPrefix: String = "https://daml.com/jwt/aud/participant/" - private[this] final val propAud: String = "aud" - private[this] final val propIss: String = "iss" - private[this] final val propExp: String = "exp" - private[this] final val propSub: String = "sub" - private[this] final val propScope: String = "scope" - private[this] final val propScp: String = "scp" - - // ------------------------------------------------------------------------------------------------------------------ - // Encoding - // ------------------------------------------------------------------------------------------------------------------ - def writePayload: AuthServiceJWTPayload => Json = { - case v: StandardJWTPayload if v.format == StandardJWTTokenFormat.Scope => - Json.obj( - propIss -> writeOptionalString(v.issuer), - propAud -> writeOptionalString(v.participantId), - propSub -> Json.fromString(v.userId), - propExp -> writeOptionalInstant(v.exp), - propScope -> writeOptionalString(v.scope), - ) - case v: StandardJWTPayload => - Json.obj( - propIss -> writeOptionalString(v.issuer), - propAud -> (v.audiences.headOption match { - case None => Json.fromString(audPrefix + v.participantId.getOrElse("")) - case Some(_) => writeStringList(v.audiences) - }), - propSub -> Json.fromString(v.userId), - propExp -> writeOptionalInstant(v.exp), - propScope -> writeOptionalString(v.scope), - ) - } - - def writeAudienceBasedPayload: AuthServiceJWTPayload => Json = { - case v: StandardJWTPayload if v.format == StandardJWTTokenFormat.Audience => - Json.obj( - propIss -> writeOptionalString(v.issuer), - propAud -> writeStringList(v.audiences), - propSub -> Json.fromString(v.userId), - propExp -> writeOptionalInstant(v.exp), - propScope -> writeOptionalString(v.scope), - ) - case _: StandardJWTPayload => - throw new RuntimeException( - s"Could not write StandardJWTPayload of scope format as audience-based payload" - ) - } - - def writeScopeBasedPayload: AuthServiceJWTPayload => Json = { - case v: StandardJWTPayload if v.format == StandardJWTTokenFormat.Scope => - Json.obj( - propIss -> writeOptionalString(v.issuer), - propAud -> writeStringList(v.audiences), - propSub -> Json.fromString(v.userId), - propExp -> writeOptionalInstant(v.exp), - propScope -> writeOptionalString(v.scope), - ) - case _: StandardJWTPayload => - throw new RuntimeException( - s"Could not write StandardJWTPayload of audience-based format as scope payload" - ) - } - - /** Writes the given payload to a compact JSON string */ - def compactPrint( - v: AuthServiceJWTPayload, - enforceFormat: Option[StandardJWTTokenFormat] = None, - ): String = - enforceFormat match { - case Some(StandardJWTTokenFormat.Audience) => - writeAudienceBasedPayload(v).noSpaces - case Some(StandardJWTTokenFormat.Scope) => writeScopeBasedPayload(v).noSpaces - case _ => writePayload(v).noSpaces - } - - private[this] def writeOptionalString(value: Option[String]): Json = - value.fold(Json.Null)(Json.fromString) - - private[this] def writeStringList(value: List[String]): Json = - Json.fromValues(value.map(Json.fromString).toVector) - - private[this] def writeOptionalInstant(value: Option[Instant]): Json = - value.fold(Json.Null)(i => Json.fromLong(i.getEpochSecond)) - - // ------------------------------------------------------------------------------------------------------------------ - // Decoding - // ------------------------------------------------------------------------------------------------------------------ - def readAudienceBasedToken(value: Json): Either[String, AuthServiceJWTPayload] = - value.asObject - .toRight(s"Could not read ${value.spaces2} as AuthServiceJWTPayload: value is not an object") - .map { obj => - val fields = obj.toMap - StandardJWTPayload( - issuer = readOptionalString(propIss, fields), - participantId = None, - userId = readString(propSub, fields), - exp = readInstant(propExp, fields), - format = StandardJWTTokenFormat.Audience, - audiences = readOptionalStringOrArray(propAud, fields), - scope = readAndCombineScopes(fields), - ) - } - - def readScopeBasedToken(value: Json): Either[String, AuthServiceJWTPayload] = - value.asObject - .toRight(s"Could not read ${value.noSpaces} as AuthServiceJWTPayload: value is not an object") - .map { obj => - val fields = obj.toMap - StandardJWTPayload( - issuer = readOptionalString(propIss, fields), - participantId = None, - userId = readString(propSub, fields), - exp = readInstant(propExp, fields), - format = StandardJWTTokenFormat.Scope, - audiences = readOptionalStringOrArray(propAud, fields), - scope = readAndCombineScopes(fields), - ) - } - - def readFromString(value: String): Either[RuntimeException, AuthServiceJWTPayload] = - parse(value).left - .map(_.getMessage) - .flatMap(readPayload) - .left - .map(message => new RuntimeException(message)) - - private def readPayload(value: Json): Either[String, AuthServiceJWTPayload] = - value.asObject match { - case Some(obj) => - val fields = obj.toMap - // Support scope that spells 'daml_ledger_api' - val scopes = readScopes(fields) - // We're using this rather restrictive test to ensure we continue parsing all legacy sandbox tokens that - // are in use before the 2.0 release; and thereby maintain full backwards compatibility. - val audienceValue = readOptionalStringOrArray(propAud, fields) - // Tokens with audience which starts with `https://daml.com/jwt/aud/participant/${participantId}` - // where `${participantId}` is non-empty string are supported. - // As required for JWTs, additional fields can be in a token but will be ignored (including scope) - val participantAudiences = audienceValue.filter(_.startsWith(audPrefix)) - if (participantAudiences.nonEmpty) { - participantAudiences - .map(_.substring(audPrefix.length)) - .filter(_.nonEmpty) match { - case participantId :: Nil => - Right( - StandardJWTPayload( - issuer = readOptionalString(propIss, fields), - participantId = Some(participantId), - userId = readString(propSub, fields), // guarded by if-clause above - exp = readInstant(propExp, fields), - format = StandardJWTTokenFormat.Audience, - audiences = - List.empty, // we do not read or extract audience claims for ParticipantId-based tokens - scope = readAndCombineScopes(fields), - ) - ) - case Nil => - Left( - s"Could not read ${value.noSpaces} as AuthServiceJWTPayload: `aud` must include participantId value prefixed by $audPrefix" - ) - case _ => - Left( - s"Could not read ${value.noSpaces} as AuthServiceJWTPayload: `aud` must include a single participantId value prefixed by $audPrefix" - ) - } - } else if (scopes.contains(scopeLedgerApiFull)) { - // We support the tokens with scope containing `daml_ledger_api`. - // `aud` field is interpreted as the participantId and may be validated by the apis authorizer for - // conformance with actual participantId. - val participantIdE = audienceValue match { - case id :: Nil => Right(Some(id)) - case Nil => Right(None) - case _ => - Left( - s"Could not read ${value.noSpaces} as AuthServiceJWTPayload: `aud` must be empty or a single participantId." - ) - } - participantIdE - .map(participantId => - StandardJWTPayload( - issuer = readOptionalString(propIss, fields), - participantId = participantId, - userId = readString(propSub, fields), - exp = readInstant(propExp, fields), - format = StandardJWTTokenFormat.Scope, - audiences = - List.empty, // we do not read or extract audience claims for Scope-based tokens - scope = Some(scopeLedgerApiFull), - ) - ) - - } else { - Left( - s"Access token with unknown scope \"${scopes.mkString}\". Issue tokens with adjusted or no scope to get rid of this warning." - ) - } - - case None => - Left( - s"Could not read ${value.noSpaces} as AuthServiceJWTPayload: value is not an object" - ) - } - - private[this] def readOptionalString(name: String, fields: Map[String, Json]): Option[String] = - fields.get(name) match { - case None => None - case Some(j) if j.isNull => None - case Some(j) => - j.asString.orElse( - sys.error(s"Could not read ${j.spaces2} as string for $name") - ) - } - - private[this] def readString(name: String, fields: Map[String, Json]): String = - fields.get(name) match { - case Some(j) => - j.asString.getOrElse( - throw new RuntimeException(s"Could not read ${j.noSpaces} as string for $name") - ) - case _ => - throw new RuntimeException(s"Could not read value for $name") - } - - private[this] def readOptionalStringOrArray( - name: String, - fields: Map[String, Json], - ): List[String] = - fields.get(name) match { - case None => List.empty - case Some(j) if j.isNull => List.empty - case Some(j) => - j.asString - .map(List(_)) - .orElse(j.asArray.map(readStringList(name, _))) - .getOrElse(sys.error(s"Could not read ${j.spaces2} as string for $name")) - } - - private[this] def readScopes(fields: Map[String, Json]): List[String] = { - // Read the scopes from the "scope" field which contains a string with space separated entries, - // see https://datatracker.ietf.org/doc/html/rfc8693#name-scope-scopes-claim - // Otherwise, read from the "scp" field which contains a vector of entries, - // see https://ldapwiki.com/wiki/Wiki.jsp?page=Scp%20%28Scopes%29%20Claim - val scopes = fields.get(propScope).toList.flatMap(_.asString.map(_.split(" ")).toList).flatten - if (scopes.nonEmpty) scopes else readOptionalStringOrArray(propScp, fields) - } - - private[this] def readAndCombineScopes(fields: Map[String, Json]): Option[String] = { - val scopes = readScopes(fields) - scopes.headOption.fold[Option[String]](None)(_ => Some(scopes.mkString(" "))) - } - - private def readStringList(name: String, values: Vector[Json]) = - values.toList.map { j => - j.asString.getOrElse( - throw new RuntimeException(s"Could not read ${j.noSpaces} as string element for $name") - ) - } - - private[this] def readInstant(name: String, fields: Map[String, Json]): Option[Instant] = - fields.get(name) match { - case None => None - case Some(j) if j.isNull => None - case Some(j) => - j.asNumber - .flatMap(_.toLong) - .map(Instant.ofEpochSecond) - .orElse( - throw new RuntimeException(s"Could not read ${j.spaces2} as epoch seconds for $name") - ) - } - - // ------------------------------------------------------------------------------------------------------------------ - // Implicits that can be imported to write JSON - // ------------------------------------------------------------------------------------------------------------------ - - private[this] lazy val sharedWarningCodec = new JsonImplicitsWithWarning() - - def jsonImplicits(warnOnJwtScopeUsage: Boolean): AuthServiceJWTPayloadCodec = if ( - warnOnJwtScopeUsage - ) - sharedWarningCodec - else - JsonImplicits - - object JsonImplicits extends AuthServiceJWTPayloadCodec(writePayload, readPayload) - - private class JsonImplicitsWithWarning( - private val firstRead: AtomicBoolean = new AtomicBoolean(true) - ) extends AuthServiceJWTPayloadCodec( - writePayload, - json => { - val decoded = readPayload(json) - decoded.foreach { - case payload: StandardJWTPayload if payload.format == StandardJWTTokenFormat.Scope => - if (firstRead.getAndSet(false)) { - logger.warn( - "Received scope-based token. Scope-based tokens are deprecated and will be removed from use in Canton release 3.7. Please migrate to audience-based tokens." - ) - } - case _ => - } - decoded - }, - ) - - object AudienceBasedTokenJsonImplicits - extends AuthServiceJWTPayloadCodec(writeAudienceBasedPayload, readAudienceBasedToken) - - object ScopeBasedTokenJsonImplicits - extends AuthServiceJWTPayloadCodec(writeScopeBasedPayload, readScopeBasedToken) - - abstract class AuthServiceJWTPayloadCodec( - writeToken: AuthServiceJWTPayload => Json, - readToken: Json => Either[String, AuthServiceJWTPayload], - ) { - implicit val authServiceJWTPayloadEncoder: Encoder[AuthServiceJWTPayload] = - Encoder.instance(writeToken) - - implicit val authServiceJWTPayloadDecoder: Decoder[AuthServiceJWTPayload] = - Decoder.instance { c => - Try(readToken(c.value)) match { - case Failure(exception) => Left(DecodingFailure(exception.getMessage, Nil)) - case Success(Left(parsingError)) => Left(DecodingFailure(parsingError, Nil)) - case Success(Right(parsedBody)) => Right(parsedBody) - } - } - } -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/Base64.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/Base64.scala deleted file mode 100644 index 29f7abd262..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/Base64.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -private object Base64 extends WithExecuteUnsafe { - - private val defaultEncoder = java.util.Base64.getUrlEncoder - - private val encoderWithoutPadding = java.util.Base64.getUrlEncoder.withoutPadding - - private val defaultDecoder = java.util.Base64.getUrlDecoder - - def encode(bs: Array[Byte]): Either[Error, Array[Byte]] = - encode(defaultEncoder, bs) - - def encodeWithoutPadding(bs: Array[Byte]): Either[Error, Array[Byte]] = - encode(encoderWithoutPadding, bs) - - private def encode( - encoder: java.util.Base64.Encoder, - bs: Array[Byte], - ): Either[Error, Array[Byte]] = - executeUnsafe(encoder.encode(bs), Symbol("Base64.encode")) - - def decode(base64str: String): Either[Error, String] = - executeUnsafe(new String(defaultDecoder.decode(base64str)), Symbol("Base64.decode")) -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/Error.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/Error.scala deleted file mode 100644 index 9472141bf5..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/Error.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -final case class Error(what: Symbol, message: String) { - def prettyPrint: String = s"Error: $what, $message" - def within(another: Symbol): Error = Error(what = another, message = s"($prettyPrint)") -} - -final case class JwtException(error: Error) extends RuntimeException diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtDecoder.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtDecoder.scala deleted file mode 100644 index f653d65172..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtDecoder.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -object JwtDecoder extends WithExecuteUnsafe { - def decode(jwt: Jwt): Either[Error, DecodedJwt[String]] = - executeUnsafe(com.auth0.jwt.JWT.decode(jwt.value), Symbol("JwtDecoder.decode")) - .map(a => DecodedJwt(header = a.getHeader, payload = a.getPayload)) - .flatMap(base64Decode) - - private def base64Decode(jwt: DecodedJwt[String]): Either[Error, DecodedJwt[String]] = - jwt - .transform(Base64.decode) - .left - .map(_.within(Symbol("JwtDecoder.base64Decode"))) -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtFromBearerHeader.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtFromBearerHeader.scala deleted file mode 100644 index 8d69e37439..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtFromBearerHeader.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -object JwtFromBearerHeader { - private val BearerTokenRegex = "Bearer (.+)".r - - def apply(header: String): Either[Error, String] = BearerTokenRegex - .findFirstMatchIn(header) - .map(_.group(1)) - .toRight( - Error(Symbol("JwtFromBearerHeader"), "Authorization header does not use Bearer format") - ) - -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtGenerator.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtGenerator.scala deleted file mode 100644 index 1b77eef3c4..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import scala.util.Try - -object JwtGenerator { - def generate: Try[Jwt] = Try(Jwt("dummy")) -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtSigner.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtSigner.scala deleted file mode 100644 index 5fd5525d1e..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtSigner.scala +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import com.auth0.jwt.algorithms.Algorithm - -import java.nio.charset.Charset -import java.security.interfaces.{ECPrivateKey, RSAPrivateKey} - -object JwtSigner extends WithExecuteUnsafe { - - private val charset = Charset.forName("ASCII") - - object HMAC256 { - def sign(jwt: DecodedJwt[String], secret: String): Either[Error, Jwt] = - for { - base64Jwt <- base64Encode(jwt) - - algorithm <- executeUnsafe(Algorithm.HMAC256(secret), Symbol("HMAC256.sign")) - - signature <- executeUnsafe( - algorithm.sign(base64Jwt.header, base64Jwt.payload), - Symbol("HMAC256.sign"), - ) - - base64Signature <- base64Encode(signature) - - } yield Jwt( - s"${str(base64Jwt.header): String}.${str(base64Jwt.payload)}.${str(base64Signature): String}" - ) - } - - @SuppressWarnings(Array("org.wartremover.warts.Null")) - object RSA256 { - def sign(jwt: DecodedJwt[String], privateKey: RSAPrivateKey): Either[Error, Jwt] = - for { - base64Jwt <- base64Encode(jwt) - - algorithm <- executeUnsafe(Algorithm.RSA256(null, privateKey), Symbol("RSA256.sign")) - - signature <- executeUnsafe( - algorithm.sign(base64Jwt.header, base64Jwt.payload), - Symbol("RSA256.sign"), - ) - - base64Signature <- base64Encode(signature) - - } yield Jwt( - s"${str(base64Jwt.header): String}.${str(base64Jwt.payload)}.${str(base64Signature): String}" - ) - } - - object ECDSA { - def sign( - jwt: DecodedJwt[String], - privateKey: ECPrivateKey, - algorithm: ECPrivateKey => Algorithm, - ): Either[Error, Jwt] = - for { - base64Jwt <- base64Encode(jwt) - - algorithm <- executeUnsafe(algorithm(privateKey), Symbol(algorithm.getClass.getTypeName)) - - signature <- executeUnsafe( - algorithm.sign(base64Jwt.header, base64Jwt.payload), - Symbol(algorithm.getClass.getTypeName), - ) - - base64Signature <- base64Encode(signature) - - } yield Jwt( - s"${str(base64Jwt.header): String}.${str(base64Jwt.payload)}.${str(base64Signature): String}" - ) - } - - private def str(bs: Array[Byte]) = new String(bs, charset) - - private def base64Encode(a: DecodedJwt[String]): Either[Error, DecodedJwt[Array[Byte]]] = - a.transform(base64Encode) - - private def base64Encode(str: String): Either[Error, Array[Byte]] = - base64Encode(str.getBytes) - - private def base64Encode(bs: Array[Byte]): Either[Error, Array[Byte]] = - Base64 - .encodeWithoutPadding(bs) - .left - .map(_.within(Symbol("JwtSigner.base64Encode"))) -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtTimestampLeeway.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtTimestampLeeway.scala deleted file mode 100644 index fdcace356b..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtTimestampLeeway.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.interfaces.Verification - -final case class JwtTimestampLeeway( - default: Option[Long] = None, - expiresAt: Option[Long] = None, - issuedAt: Option[Long] = None, - notBefore: Option[Long] = None, -) - -trait Leeway { - def getVerifier( - algorithm: Algorithm, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - ): com.auth0.jwt.interfaces.JWTVerifier = { - def addLeeway( - verification: Verification, - jwtTimestampLeeway: JwtTimestampLeeway, - ): Verification = { - val mbOptionsActions: List[(Option[Long], (Verification, Long) => Verification)] = List( - (jwtTimestampLeeway.default, _.acceptLeeway(_)), - (jwtTimestampLeeway.expiresAt, _.acceptExpiresAt(_)), - (jwtTimestampLeeway.issuedAt, _.acceptIssuedAt(_)), - (jwtTimestampLeeway.notBefore, _.acceptNotBefore(_)), - ) - mbOptionsActions.foldLeft(verification) { - case (verifier, (None, _)) => verifier - case (verifier, (Some(value), f)) => f(verifier, value) - } - } - val defaultVerifier = JWT.require(algorithm) - val verification = jwtTimestampLeeway.fold(defaultVerifier)(addLeeway(defaultVerifier, _)) - verification.build() - } -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtVerifier.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtVerifier.scala deleted file mode 100644 index cd7481a08e..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/JwtVerifier.scala +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.interfaces.RSAKeyProvider - -import java.io.File -import java.security.interfaces.{ECPublicKey, RSAPublicKey} -import java.time.{Duration, Instant} -import scala.math.Ordered.orderingToOrdered - -abstract class JwtVerifierBase { - def verify(jwt: Jwt): Either[Error, DecodedJwt[String]] -} - -class JwtVerifier( - val verifier: com.auth0.jwt.interfaces.JWTVerifier, - val maxTokenLife: Option[Long], -) extends JwtVerifierBase - with WithExecuteUnsafe { - - def verify(jwt: Jwt): Either[Error, DecodedJwt[String]] = - // The auth0 library verification already fails if the token has expired, - // but we still need to do manual expiration checks in ongoing streams - executeUnsafe(verifier.verify(jwt.value), Symbol("JwtVerifier.verify")) - .map(a => - ( - DecodedJwt( - header = a.getHeader, - payload = a.getPayload, - ), - Option(a.getExpiresAtAsInstant()), - ) - ) - .flatMap { case (jwt, expirationOption) => - checkTokenLifeTime(jwt, expirationOption) - } - .flatMap(base64Decode) - - // Check if the expiration time is not too long and if it exists - private def checkTokenLifeTime( - jwt: DecodedJwt[String], - expirationOption: Option[Instant], - ) = { - // TODO (i27262) use TimeProvider to get current time - val currentTime = Instant.now() - expirationOption - .map { expiresAt => - val duration = Duration.ofMillis(expiresAt.toEpochMilli - currentTime.toEpochMilli) - val maxTokenLifeDuration = maxTokenLife.getOrElse(Long.MaxValue) - // We do not check for negative durations (expired token), as the JWT library already ensures that (with a leeway) - if (duration > Duration.ofMillis(maxTokenLifeDuration)) { - Left(Error(Symbol("JwtVerifier.verify"), s"token lifetime ($expiresAt) too long")) - } else { - Right(jwt) - } - } - .getOrElse( - maxTokenLife - .map(_ => Left(Error(Symbol("JwtVerifier.verify"), "token has no expiration time"))) - .getOrElse(Right(jwt)) - ) - } - - private def base64Decode(jwt: DecodedJwt[String]): Either[Error, DecodedJwt[String]] = - jwt.transform(Base64.decode).left.map(_.within(Symbol("JwtVerifier.base64Decode"))) - -} - -// HMAC256 validator factory -object HMAC256Verifier extends Leeway with WithExecuteUnsafe { - def apply( - secret: String, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - maxTokenLife: Option[Long] = None, - ): Either[Error, JwtVerifier] = - executeUnsafe( - { - val algorithm = Algorithm.HMAC256(secret) - val verifier = getVerifier(algorithm, jwtTimestampLeeway) - new JwtVerifier(verifier, maxTokenLife) - }, - Symbol("HMAC256"), - ) -} - -// ECDSA validator factory -object ECDSAVerifier extends Leeway with WithExecuteUnsafe { - def apply( - algorithm: Algorithm, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - maxTokenLife: Option[Long] = None, - ): Either[Error, JwtVerifier] = - executeUnsafe( - { - val verifier = getVerifier(algorithm, jwtTimestampLeeway) - new JwtVerifier(verifier, maxTokenLife) - }, - Symbol(algorithm.getName), - ) - - def fromCrtFile( - path: String, - algorithmPublicKey: ECPublicKey => Algorithm, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - maxTokenLife: Option[Long] = None, - ): Either[Error, JwtVerifier] = - for { - key <- KeyUtils - .readECPublicKeyFromCrt(new File(path)) - .toEither - .left - .map(e => Error(Symbol("ECDSAVerifier.fromCrtFile"), e.getMessage)) - verifier <- ECDSAVerifier(algorithmPublicKey(key), jwtTimestampLeeway, maxTokenLife) - } yield verifier -} - -// RSA256 validator factory -@SuppressWarnings(Array("org.wartremover.warts.Null")) -object RSA256Verifier extends Leeway with WithExecuteUnsafe { - def apply( - publicKey: RSAPublicKey, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - maxTokenLife: Option[Long] = None, - ): Either[Error, JwtVerifier] = - executeUnsafe( - { - val algorithm = Algorithm.RSA256(publicKey, null) - val verifier = getVerifier(algorithm, jwtTimestampLeeway) - new JwtVerifier(verifier, maxTokenLife) - }, - Symbol("RSA256"), - ) - - def apply(keyProvider: RSAKeyProvider, maxTokenLife: Option[Long]): Either[Error, JwtVerifier] = - executeUnsafe( - { - - val algorithm = Algorithm.RSA256(keyProvider) - val verifier = getVerifier(algorithm) - new JwtVerifier(verifier, maxTokenLife) - }, - (Symbol("RSA256")), - ) - - def apply( - keyProvider: RSAKeyProvider, - jwtTimestampLeeway: Option[JwtTimestampLeeway], - maxTokenLife: Option[Long], - ): Either[Error, JwtVerifier] = - executeUnsafe( - { - - val algorithm = Algorithm.RSA256(keyProvider) - val verifier = getVerifier(algorithm, jwtTimestampLeeway) - new JwtVerifier(verifier, maxTokenLife) - }, - Symbol("RSA256"), - ) - - /** Create a RSA256 validator with the key loaded from the given file. The file is assumed to be a - * X509 encoded certificate. These typically have the .crt file extension. - */ - def fromCrtFile( - path: String, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - maxTokenLife: Option[Long] = None, - ): Either[Error, JwtVerifier] = - for { - rsaKey <- KeyUtils - .readRSAPublicKeyFromCrt(new File(path)) - .toEither - .left - .map(e => Error(Symbol("RSA256Verifier.fromCrtFile"), e.getMessage)) - verifier <- RSA256Verifier.apply(rsaKey, jwtTimestampLeeway, maxTokenLife) - } yield verifier -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/KeyUtils.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/KeyUtils.scala deleted file mode 100644 index d62cfa1810..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/KeyUtils.scala +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import java.io.{File, FileInputStream} -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.security.cert.CertificateFactory -import java.security.interfaces.{ECPublicKey, RSAPrivateKey, RSAPublicKey} -import java.security.spec.PKCS8EncodedKeySpec -import java.security.{Key, KeyFactory} -import scala.reflect.{ClassTag, classTag} -import scala.util.{Try, Using} - -object KeyUtils { - private val mimeCharSet = StandardCharsets.ISO_8859_1 - - private implicit class SpecifyPublicKey[K <: Key](private val self: K) extends AnyVal { - def asSpecific[SK <: K: ClassTag]: SK = { - val Tag = classTag[SK] - self match { - case Tag(key) => key - case _ => - throw new IllegalStateException( - s"Expected a $Tag key, but got $self :${self.getClass.getName}" - ) - } - } - } - - /** Reads an RSA public key from a X509 encoded file. These usually have the .crt file extension. - */ - def readRSAPublicKeyFromCrt(file: File): Try[RSAPublicKey] = - Using(new FileInputStream(file))( - CertificateFactory - .getInstance("X.509") - .generateCertificate(_) - .getPublicKey - .asSpecific[RSAPublicKey] - ) - - /** Reads an EC public key from a X509 encoded file. These usually have the .crt file extension. - */ - def readECPublicKeyFromCrt(file: File): Try[ECPublicKey] = - Using(new FileInputStream(file))( - CertificateFactory - .getInstance("X.509") - .generateCertificate(_) - .getPublicKey - .asSpecific[ECPublicKey] - ) - - /** Reads a RSA private key from a PEM/PKCS#8 file. These usually have the .pem file extension. - */ - def readRSAPrivateKeyFromPem(file: File): Try[RSAPrivateKey] = - for { - fileContent <- Try(Files.readAllBytes(file.toPath)) - - // Remove PEM container header and footer - pemContent <- Try( - new String(fileContent, mimeCharSet) - .replaceFirst("-----BEGIN ([A-Z ])*-----\n", "") - .replaceFirst("\n-----END ([A-Z ])*-----\n", "") - .replace("\r", "") - .replace("\n", "") - ) - - // Base64-decode the PEM container content - decoded <- Base64 - .decode(pemContent) - .left - .map(e => new RuntimeException(e.prettyPrint)) - .toTry - - // Interpret the container content as PKCS#8 - key <- Try { - val kf = KeyFactory.getInstance("RSA") - val keySpec = new PKCS8EncodedKeySpec(decoded.getBytes) - kf.generatePrivate(keySpec).asSpecific[RSAPrivateKey] - } - } yield key - - /** Reads a RSA private key from a binary file (PKCS#8, DER). To generate this file from a .pem - * file, use the following command: openssl pkcs8 -topk8 -inform PEM -outform DER -in - * private-key.pem -nocrypt > private-key.der - */ - def readRSAPrivateKeyFromDer(file: File): Try[RSAPrivateKey] = - for { - fileContent <- Try(Files.readAllBytes(file.toPath)) - - // Interpret the container content as PKCS#8 - key <- Try { - val kf = KeyFactory.getInstance("RSA") - val keySpec = new PKCS8EncodedKeySpec(fileContent) - kf.generatePrivate(keySpec).asSpecific[RSAPrivateKey] - } - } yield key - - /** Generates a JWKS JSON object for the given map of KeyID->Key for RSA - * - * Note: this uses the same format as Google OAuth, see - * https://www.googleapis.com/oauth2/v3/certs - */ - def generateJwks(keys: Map[String, RSAPublicKey]): String = { - def generateKeyEntry(keyId: String, key: RSAPublicKey): String = - s""" { - | "kid": "$keyId", - | "kty": "RSA", - | "alg": "RS256", - | "use": "sig", - | "e": "${java.util.Base64.getUrlEncoder - .encodeToString(key.getPublicExponent.toByteArray)}", - | "n": "${java.util.Base64.getUrlEncoder.encodeToString(key.getModulus.toByteArray)}" - | }""".stripMargin - - s""" - |{ - | "keys": [ - |${keys.toList.map { case (keyId, key) => generateKeyEntry(keyId, key) }.mkString(",\n")} - | ] - |} - """.stripMargin - } - - /** Generates a JWKS JSON object for the given map of KeyID->Key for EC - * - * Note: this uses the same format as Google OAuth, see - * https://www.gstatic.com/iap/verify/public_key-jwk - */ - def generateECJwks(keys: Map[String, ECPublicKey]): String = { - def generateKeyEntry(keyId: String, key: ECPublicKey): String = - s""" { - | "kid": "$keyId", - | "kty": "EC", - | "alg": "ES${key.getParams.getCurve.getField.getFieldSize}", - | "use": "sig", - | "crv": "P-${key.getParams.getCurve.getField.getFieldSize}", - | "x": "${java.util.Base64.getUrlEncoder.encodeToString( - key.getW.getAffineX.toByteArray - )}", - | "y": "${java.util.Base64.getUrlEncoder.encodeToString( - key.getW.getAffineY.toByteArray - )}" - | }""".stripMargin - - s""" - |{ - | "keys": [ - |${keys.toList.map { case (keyId, key) => generateKeyEntry(keyId, key) }.mkString(",\n")} - | ] - |} - """.stripMargin - } -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/WithExecuteUnsafe.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/WithExecuteUnsafe.scala deleted file mode 100644 index 4c2af930a5..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/WithExecuteUnsafe.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import scala.util.Try - -trait WithExecuteUnsafe { - def executeUnsafe[T](f: => T, symbol: Symbol): Either[Error, T] = - Try(f).toEither.left.map(e => Error(symbol, e.getMessage)) -} diff --git a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/package.scala b/canton/base/daml-jwt/src/main/scala/com/daml/jwt/package.scala deleted file mode 100644 index 91749ef79a..0000000000 --- a/canton/base/daml-jwt/src/main/scala/com/daml/jwt/package.scala +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml - -import java.net.URI -import scala.util.Try - -package jwt { - - final case class KeyPair[A](publicKey: A, privateKey: A) - - final case class Jwt(value: String) - - final case class DecodedJwt[A](header: A, payload: A) { - def transform[B](f: A => Either[Error, B]): Either[Error, DecodedJwt[B]] = - for { - header <- f(header) - payload <- f(payload) - } yield DecodedJwt(header, payload) - } - - final case class JwksUrl(value: String) extends AnyVal { - def toURL = new URI(value).toURL - } - - object JwksUrl { - def fromString(value: String): Either[String, JwksUrl] = - Try(new URI(value).toURL).toEither.left - .map(_.getMessage) - .map(_ => JwksUrl(value)) - - def assertFromString(str: String): JwksUrl = fromString(str) match { - case Right(value) => value - case Left(err) => throw new IllegalArgumentException(err) - } - } -} diff --git a/canton/base/daml-jwt/src/test/scala/com/daml/jwt/JwtFromBearerHeaderSpec.scala b/canton/base/daml-jwt/src/test/scala/com/daml/jwt/JwtFromBearerHeaderSpec.scala deleted file mode 100644 index 78d54035ee..0000000000 --- a/canton/base/daml-jwt/src/test/scala/com/daml/jwt/JwtFromBearerHeaderSpec.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class JwtFromBearerHeaderSpec extends AnyFlatSpec with Matchers { - - it should "produce an error in case of empty string as header" in { - JwtFromBearerHeader("") shouldBe Left( - Error(Symbol("JwtFromBearerHeader"), "Authorization header does not use Bearer format") - ) - } - - it should "produce an error in case of missing Bearer header" in { - JwtFromBearerHeader("Bearer") shouldBe Left( - Error(Symbol("JwtFromBearerHeader"), "Authorization header does not use Bearer format") - ) - - JwtFromBearerHeader("Bearer ") shouldBe Left( - Error(Symbol("JwtFromBearerHeader"), "Authorization header does not use Bearer format") - ) - } - - it should "extract valid token from the header" in { - JwtFromBearerHeader("Bearer 123") shouldBe Right("123") - } - -} diff --git a/canton/base/daml-jwt/src/test/scala/com/daml/jwt/JwtTimestampLeewaySpec.scala b/canton/base/daml-jwt/src/test/scala/com/daml/jwt/JwtTimestampLeewaySpec.scala deleted file mode 100644 index bd1f6e6967..0000000000 --- a/canton/base/daml-jwt/src/test/scala/com/daml/jwt/JwtTimestampLeewaySpec.scala +++ /dev/null @@ -1,361 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.exceptions.{InvalidClaimException, TokenExpiredException} -import org.scalactic.source -import org.scalatest.matchers.should.Matchers -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.wordspec.AnyWordSpec - -import java.security.interfaces.{ECPrivateKey, ECPublicKey, RSAPrivateKey, RSAPublicKey} -import java.security.spec.ECGenParameterSpec -import java.time.temporal.ChronoUnit -import java.util.Date - -class JwtTimestampLeewaySpec extends AnyWordSpec with Matchers with TableDrivenPropertyChecks { - - import JwtTimestampLeewaySpec.* - - "Jwt" when { - forAll(verifires) { (verifierType, algorithm, jwtVerifier) => - ("using " + verifierType + " verifier") should { - - "work with a token that has not expired" in { - val now = new Date() - val token: String = JWT - .create() - .withExpiresAt(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - jwtVerifier(None).verifier - .verify(token) - } - - "work with an expired token when leeway overlaps verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withExpiresAt(oneSecondEarlierFrom(now)) - .sign(algorithm) - - val leeway1 = Some(JwtTimestampLeeway(Some(5), None, None, None)) - jwtVerifier(leeway1).verifier - .verify(token) - } - - "work with an expired token when expiresAt overlaps verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withExpiresAt(oneSecondEarlierFrom(now)) - .sign(algorithm) - - val expiresAt1 = Some(JwtTimestampLeeway(None, Some(5), None, None)) - jwtVerifier(expiresAt1).verifier - .verify(token) - } - - "fail with an expired token when leeway is off" in { - val now = new Date() - val token: String = JWT - .create() - .withExpiresAt(oneSecondEarlierFrom(now)) - .sign(algorithm) - - assertThrows[TokenExpiredException] { - jwtVerifier(None).verifier - .verify(token) - } - } - - "fail with an expired token when leeway does not overlap verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withExpiresAt(fiveSecondsEarlierFrom(now)) - .sign(algorithm) - - val leeway1 = Some(JwtTimestampLeeway(Some(1), None, None, None)) - assertThrows[TokenExpiredException] { - jwtVerifier(leeway1).verifier - .verify(token) - } - } - - "work with an expired token when leeway does not overlap verification but expiresAt does" in { - val now = new Date() - val token: String = JWT - .create() - .withExpiresAt(fiveSecondsEarlierFrom(now)) - .sign(algorithm) - - val leeway = Some(JwtTimestampLeeway(Some(1), Some(10), None, None)) - jwtVerifier(leeway).verifier - .verify(token) - } - - "fail with an expired token when leeway overlaps verification time but expiresAt does not" in { - val now = new Date() - val token: String = JWT - .create() - .withExpiresAt(fiveSecondsEarlierFrom(now)) - .sign(algorithm) - - val leeway = Some(JwtTimestampLeeway(Some(10), Some(1), None, None)) - assertThrows[TokenExpiredException] { - jwtVerifier(leeway).verifier - .verify(token) - } - } - - "work with a token issued in a past date" in { - val now = new Date() - val token: String = JWT - .create() - .withIssuedAt(fiveSecondsEarlierFrom(now)) - .sign(algorithm) - - jwtVerifier(None).verifier - .verify(token) - } - - "work with a token issued in a future date when leeway overlaps verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withIssuedAt(oneSecondLaterFrom(now)) - .sign(algorithm) - - val leeway1 = Some(JwtTimestampLeeway(Some(5), None, None, None)) - jwtVerifier(leeway1).verifier - .verify(token) - } - - "work with a token issued in a future date when issuedAt overlaps verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withIssuedAt(oneSecondLaterFrom(now)) - .sign(algorithm) - - val issuedAt1 = Some(JwtTimestampLeeway(None, None, Some(5), None)) - jwtVerifier(issuedAt1).verifier - .verify(token) - } - - "fail with a token issued in a future date when leeway is off" in { - val now = new Date() - val token: String = JWT - .create() - .withIssuedAt(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - assertThrows[InvalidClaimException] { - jwtVerifier(None).verifier - .verify(token) - } - } - - "fail with a token issued in a future date when leeway does not overlap verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withIssuedAt(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - val leeway1 = Some(JwtTimestampLeeway(Some(1), None, None, None)) - assertThrows[InvalidClaimException] { - jwtVerifier(leeway1).verifier - .verify(token) - } - } - - "work with a token issued in a future date when leeway does not overlap verification but issuedAt does" in { - val now = new Date() - val token: String = JWT - .create() - .withIssuedAt(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - val leeway = Some(JwtTimestampLeeway(Some(1), None, Some(10), None)) - jwtVerifier(leeway).verifier - .verify(token) - } - - "fail with a token issued in a future date when leeway overlaps verification time but expiresAt does not" in { - val now = new Date() - val token: String = JWT - .create() - .withIssuedAt(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - val leeway = Some(JwtTimestampLeeway(Some(10), None, Some(1), None)) - assertThrows[InvalidClaimException] { - jwtVerifier(leeway).verifier - .verify(token) - } - } - - "work with a token that can already be used" in { - val now = new Date() - val token: String = JWT - .create() - .withNotBefore(oneSecondEarlierFrom(now)) - .sign(algorithm) - - jwtVerifier(None).verifier - .verify(token) - } - - "work with a token usable in a future date when leeway overlaps verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withNotBefore(oneSecondLaterFrom(now)) - .sign(algorithm) - - val leeway1 = Some(JwtTimestampLeeway(Some(5), None, None, None)) - jwtVerifier(leeway1).verifier - .verify(token) - } - - "work with a token usable in a future date when notBefore overlaps verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withNotBefore(oneSecondLaterFrom(now)) - .sign(algorithm) - - val notBefore1 = Some(JwtTimestampLeeway(None, None, None, Some(5))) - jwtVerifier(notBefore1).verifier - .verify(token) - } - - "fail with a token usable in a future date when leeway is off" in { - val now = new Date() - val token: String = JWT - .create() - .withNotBefore(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - assertThrows[InvalidClaimException] { - jwtVerifier(None).verifier - .verify(token) - } - } - - "fail with a token usable in a future date when leeway does not overlap verification time" in { - val now = new Date() - val token: String = JWT - .create() - .withNotBefore(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - val leeway1 = Some(JwtTimestampLeeway(Some(1), None, None, None)) - assertThrows[InvalidClaimException] { - jwtVerifier(leeway1).verifier - .verify(token) - } - } - - "work with a token usable in a future date when leeway does not overlap verification but notBefore does" in { - val now = new Date() - val token: String = JWT - .create() - .withNotBefore(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - val leeway = Some(JwtTimestampLeeway(Some(1), None, None, Some(10))) - jwtVerifier(leeway).verifier - .verify(token) - } - - "fail with a token usable in a future date when leeway overlaps verification time but notBefore does not" in { - val now = new Date() - val token: String = JWT - .create() - .withNotBefore(fiveSecondsLaterFrom(now)) - .sign(algorithm) - - val leeway = Some(JwtTimestampLeeway(Some(10), None, None, Some(1))) - assertThrows[InvalidClaimException] { - jwtVerifier(leeway).verifier - .verify(token) - } - } - } - } - } -} - -object JwtTimestampLeewaySpec extends TableDrivenPropertyChecks { - - // HMAC - val secret = "secret key" - - // RSA - val kpgRSA = java.security.KeyPairGenerator.getInstance("RSA") - kpgRSA.initialize(2048) - val keyPairRSA = kpgRSA.generateKeyPair() - val privateKeyRSA = keyPairRSA.getPrivate.asInstanceOf[RSAPrivateKey] - val publicKeyRSA = keyPairRSA.getPublic.asInstanceOf[RSAPublicKey] - - // ECDSA - // 256 - val kpgECDSA256 = java.security.KeyPairGenerator.getInstance("EC") - val ecGenParameterSpec256 = new ECGenParameterSpec("secp256r1") - kpgECDSA256.initialize(ecGenParameterSpec256) - val keyPairECDSA256 = kpgECDSA256.generateKeyPair() - val privateKeyECDSA256 = keyPairECDSA256.getPrivate.asInstanceOf[ECPrivateKey] - val publicKeyECDSA256 = keyPairECDSA256.getPublic.asInstanceOf[ECPublicKey] - // 512 - val kpg512 = java.security.KeyPairGenerator.getInstance("EC") - val ecGenParameterSpec512 = new ECGenParameterSpec("secp521r1") - kpg512.initialize(ecGenParameterSpec512) - val keyPairECDSA512 = kpg512.generateKeyPair() - val privateKeyECDSA512 = keyPairECDSA512.getPrivate.asInstanceOf[ECPrivateKey] - val publicKeyECDSA512 = keyPairECDSA512.getPublic.asInstanceOf[ECPublicKey] - - val hmac256Verifier_ = HMAC256Verifier(secret, _: Option[JwtTimestampLeeway]).assertRight - val rsa256Verifier_ = RSA256Verifier(publicKeyRSA, _: Option[JwtTimestampLeeway]).assertRight - val ecdsa256Verifier_ = - ECDSAVerifier( - Algorithm.ECDSA256(publicKeyECDSA256, null), - _: Option[JwtTimestampLeeway], - ).assertRight - val ecdsa512Verifier_ = - ECDSAVerifier( - Algorithm.ECDSA512(publicKeyECDSA512, null), - _: Option[JwtTimestampLeeway], - ).assertRight - - val verifires = Table( - ("verifier name", "algorithm", "verifier"), - ("HMAC 256", Algorithm.HMAC256(secret), hmac256Verifier_), - ("RSA 256", Algorithm.RSA256(publicKeyRSA, privateKeyRSA), rsa256Verifier_), - ("ECDSA 256", Algorithm.ECDSA256(publicKeyECDSA256, privateKeyECDSA256), ecdsa256Verifier_), - ("ECDSA 512", Algorithm.ECDSA512(publicKeyECDSA512, privateKeyECDSA512), ecdsa512Verifier_), - ) - - def oneSecondEarlierFrom(date: Date): Date = - Date.from(date.toInstant.minus(1, ChronoUnit.SECONDS)) - - def oneSecondLaterFrom(date: Date): Date = - Date.from(date.toInstant.plus(1, ChronoUnit.SECONDS)) - - def fiveSecondsEarlierFrom(date: Date): Date = - Date.from(date.toInstant.minus(5, ChronoUnit.SECONDS)) - - def fiveSecondsLaterFrom(date: Date): Date = - Date.from(date.toInstant.plus(5, ChronoUnit.SECONDS)) - - private implicit final class AssertRight[A](private val ea: Either[Error, A]) extends AnyVal { - def assertRight(implicit pos: source.Position) = - ea.fold(e => org.scalatest.Assertions.fail(e.prettyPrint), identity) - } - -} diff --git a/canton/base/daml-jwt/src/test/scala/com/daml/jwt/SignatureSpec.scala b/canton/base/daml-jwt/src/test/scala/com/daml/jwt/SignatureSpec.scala deleted file mode 100644 index 16c7689c5c..0000000000 --- a/canton/base/daml-jwt/src/test/scala/com/daml/jwt/SignatureSpec.scala +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.jwt - -import com.auth0.jwt.algorithms.Algorithm -import org.scalactic.source -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import java.security.KeyPair -import java.security.interfaces.{ECPrivateKey, ECPublicKey, RSAPrivateKey, RSAPublicKey} -import java.security.spec.ECGenParameterSpec - -class SignatureSpec extends AnyWordSpec with Matchers { - import SignatureSpec.* - - "Jwt" when { - - "using HMAC256 signatures" should { - - "work with a valid secret" in { - val secret = "secret key" - val jwtHeader = """{"alg": "HS256", "typ": "JWT"}""" - val jwtPayload = """{"dummy":"dummy"}""" - val jwt = DecodedJwt[String](jwtHeader, jwtPayload) - - val signedJwt = JwtSigner.HMAC256 - .sign(jwt, secret) - .assertRight - val verifier = HMAC256Verifier(secret).assertRight - verifier - .verify(signedJwt) - .assertRight - } - - "fail with an invalid secret" in { - val secret = "secret key" - val jwtHeader = """{"alg": "HS256", "typ": "JWT"}""" - val jwtPayload = """{"dummy":"dummy"}""" - val jwt = DecodedJwt[String](jwtHeader, jwtPayload) - - val success = { - val signedJwt = JwtSigner.HMAC256 - .sign(jwt, secret) - .assertRight - val verifier = HMAC256Verifier("invalid " + secret).assertRight - verifier - .verify(signedJwt) - .swap - .left - .map(jwt => fail(s"JWT $jwt was unexpectedly verified")) - } - - success.isRight shouldBe true - } - } - - "using RSA256 signatures" should { - - "work with a valid key" in { - val kpg = java.security.KeyPairGenerator.getInstance("RSA") - kpg.initialize(2048) - val keyPair = kpg.generateKeyPair() - val privateKey = keyPair.getPrivate.asInstanceOf[RSAPrivateKey] - val publicKey = keyPair.getPublic.asInstanceOf[RSAPublicKey] - - val jwtHeader = """{"alg": "RS256", "typ": "JWT"}""" - val jwtPayload = """{"dummy":"dummy"}""" - val jwt = DecodedJwt[String](jwtHeader, jwtPayload) - - val signedJwt = JwtSigner.RSA256 - .sign(jwt, privateKey) - .assertRight - val verifier = RSA256Verifier(publicKey).assertRight - verifier - .verify(signedJwt) - .assertRight - } - - "fail with an invalid key" in { - val kpg = java.security.KeyPairGenerator.getInstance("RSA") - kpg.initialize(2048) - val keyPair1 = kpg.generateKeyPair() - val privateKey = keyPair1.getPrivate.asInstanceOf[RSAPrivateKey] - - val keyPair2 = kpg.generateKeyPair() - val publicKey = keyPair2.getPublic.asInstanceOf[RSAPublicKey] - - val jwtHeader = """{"alg": "RS256", "typ": "JWT"}""" - val jwtPayload = """{"dummy":"dummy"}""" - val jwt = DecodedJwt[String](jwtHeader, jwtPayload) - - val signedJwt = JwtSigner.RSA256 - .sign(jwt, privateKey) - .assertRight - val verifier = RSA256Verifier(publicKey).assertRight - verifier - .verify(signedJwt) - .swap - .left - .map(jwt => fail(s"JWT $jwt was unexpectedly verified")) - } - } - - "using ECDA256 signatures" should { - "work with a valid key" in { - val kpg = java.security.KeyPairGenerator.getInstance("EC") - val ecGenParameterSpec = new ECGenParameterSpec("secp256r1") - kpg.initialize(ecGenParameterSpec) - val keyPair: KeyPair = kpg.generateKeyPair() - - val privateKey = keyPair.getPrivate.asInstanceOf[ECPrivateKey] - val publicKey = keyPair.getPublic.asInstanceOf[ECPublicKey] - - val jwtHeader = """{"alg": "ES256", "typ": "JWT"}""" - val jwtPayload = """{"dummy":"dummy"}""" - val jwt = DecodedJwt[String](jwtHeader, jwtPayload) - - val signedJwt = JwtSigner.ECDSA - .sign(jwt, privateKey, Algorithm.ECDSA256(null, _)) - .assertRight - val verifier = ECDSAVerifier(Algorithm.ECDSA256(publicKey, null)).assertRight - verifier - .verify(signedJwt) - .assertRight - } - "fail with a invalid key" in { - val kpg = java.security.KeyPairGenerator.getInstance("EC") - val ecGenParameterSpec = new ECGenParameterSpec("secp256r1") - kpg.initialize(ecGenParameterSpec) - val keyPair1: KeyPair = kpg.generateKeyPair() - - val privateKey1 = keyPair1.getPrivate.asInstanceOf[ECPrivateKey] - - val keyPair2: KeyPair = kpg.generateKeyPair() - val publicKey2 = keyPair2.getPublic.asInstanceOf[ECPublicKey] - - val jwtHeader = """{"alg": "ES256", "typ": "JWT"}""" - val jwtPayload = """{"dummy":"dummy"}""" - val jwt = DecodedJwt[String](jwtHeader, jwtPayload) - val success = { - val signedJwt = JwtSigner.ECDSA - .sign(jwt, privateKey1, Algorithm.ECDSA256(null, _)) - .assertRight - val verifier = ECDSAVerifier(Algorithm.ECDSA256(publicKey2, null)).assertRight - verifier - .verify(signedJwt) - .swap - .left - .map(jwt => fail(s"JWT $jwt was unexpectedly verified")) - } - - success.isRight shouldBe true - } - } - "using ECDSA512 signatures" should { - "work with a valid key" in { - val kpg = java.security.KeyPairGenerator.getInstance("EC") - val ecGenParameterSpec = new ECGenParameterSpec("secp521r1") - kpg.initialize(ecGenParameterSpec) - val keyPair: KeyPair = kpg.generateKeyPair() - - val privateKey = keyPair.getPrivate.asInstanceOf[ECPrivateKey] - val publicKey = keyPair.getPublic.asInstanceOf[ECPublicKey] - - val jwtHeader = """{"alg": "ES512", "typ": "JWT"}""" - val jwtPayload = """{"dummy":"dummy"}""" - val jwt = DecodedJwt[String](jwtHeader, jwtPayload) - val signedJwt = JwtSigner.ECDSA - .sign(jwt, privateKey, Algorithm.ECDSA512(null, _)) - .assertRight - - val verifier = ECDSAVerifier(Algorithm.ECDSA512(publicKey, null)).assertRight - verifier - .verify(signedJwt) - .assertRight - } - "fail with a invalid key" in { - val kpg = java.security.KeyPairGenerator.getInstance("EC") - val ecGenParameterSpec = new ECGenParameterSpec("secp521r1") - kpg.initialize(ecGenParameterSpec) - val keyPair1: KeyPair = kpg.generateKeyPair() - - val privateKey1 = keyPair1.getPrivate.asInstanceOf[ECPrivateKey] - - val keyPair2: KeyPair = kpg.generateKeyPair() - val publicKey2 = keyPair2.getPublic.asInstanceOf[ECPublicKey] - - val jwtHeader = """{"alg": "ES512", "typ": "JWT"}""" - val jwtPayload = """{"dummy":"dummy"}""" - val jwt = DecodedJwt[String](jwtHeader, jwtPayload) - val success = { - val signedJwt = JwtSigner.ECDSA - .sign(jwt, privateKey1, Algorithm.ECDSA512(null, _)) - .assertRight - val verifier = ECDSAVerifier(Algorithm.ECDSA512(publicKey2, null)).assertRight - verifier - .verify(signedJwt) - .swap - .left - .map(jwt => fail(s"JWT $jwt was unexpectedly verified")) - } - - success.isRight shouldBe true - } - } - - } -} - -object SignatureSpec { - - private implicit final class AssertRight[A](private val ea: Either[Error, A]) extends AnyVal { - def assertRight(implicit pos: source.Position) = - ea.fold(e => org.scalatest.Assertions.fail(e.prettyPrint), identity) - } - -} diff --git a/canton/base/daml-tls/src/main/scala/com/daml/tls/OcspProperties.scala b/canton/base/daml-tls/src/main/scala/com/daml/tls/OcspProperties.scala deleted file mode 100644 index 7ebb278297..0000000000 --- a/canton/base/daml-tls/src/main/scala/com/daml/tls/OcspProperties.scala +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.tls - -/** Enables certificate revocation checks with OCSP. See: - * https://tersesystems.com/blog/2014/03/22/fixing-certificate-revocation/ See: - * https://www.ibm.com/support/knowledgecenter/en/SSYKE2_8.0.0/com.ibm.java.security.component.80.doc/security-component/jsse2Docs/knowndiffsun.html - */ -object OcspProperties { - - val CheckRevocationPropertySun: String = "com.sun.net.ssl.checkRevocation" - val CheckRevocationPropertyIbm: String = "com.ibm.jsse2.checkRevocation" - val EnableOcspProperty: String = "ocsp.enable" - - def enableOcsp(): Unit = { - System.setProperty(CheckRevocationPropertySun, True) - System.setProperty(CheckRevocationPropertyIbm, True) - java.security.Security.setProperty(EnableOcspProperty, True) - } - - private val True: String = "true" - -} diff --git a/canton/base/daml-tls/src/main/scala/com/daml/tls/ProtocolDisabler.scala b/canton/base/daml-tls/src/main/scala/com/daml/tls/ProtocolDisabler.scala deleted file mode 100644 index b012736f58..0000000000 --- a/canton/base/daml-tls/src/main/scala/com/daml/tls/ProtocolDisabler.scala +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.tls - -import java.security.Security - -/** Disables the unwanted legacy SSLv2Hello protocol at the JSSE level. See: - * https://www.java.com/en/configure_crypto.html#DisableTLS:~:text=Disable%20TLS%201.0%20and%20TLS%201.1 - */ -object ProtocolDisabler { - val disabledAlgorithmsProperty: String = "jdk.tls.disabledAlgorithms" - val sslV2Protocol: String = "SSLv2Hello" - - def disableSSLv2Hello(): Unit = - PropertiesUpdater(Security.getProperty, Security.setProperty) - .appendToProperty(disabledAlgorithmsProperty, sslV2Protocol) -} - -private[tls] final case class PropertiesUpdater( - getter: String => String, - setter: (String, String) => Unit, -) { - def appendToProperty(name: String, value: String): Unit = { - val property = getter(name) - val fullProperty = - property - .split(",") - .map(_.trim) - .find(_ == value) - .map(_ => property) - .getOrElse(s"$property, $value") - setter(name, fullProperty) - } -} diff --git a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfig.scala b/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfig.scala deleted file mode 100644 index 3103caf6a5..0000000000 --- a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfig.scala +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.tls - -import com.digitalasset.canton.config.{PemFile, PemFileOrString} -import io.grpc.netty.shaded.io.netty.handler.ssl.{ClientAuth, SslContext} -import org.slf4j.LoggerFactory - -sealed trait TlsConfig { - def certChainFile: PemFileOrString - def privateKeyFile: PemFile - def minimumServerProtocolVersion: Option[String] - def ciphers: Option[Seq[String]] - - def protocols: Option[Seq[String]] = - minimumServerProtocolVersion.map { minVersion => - val knownTlsVersions = - Seq( - TlsVersion.V1.version, - TlsVersion.V1_1.version, - TlsVersion.V1_2.version, - TlsVersion.V1_3.version, - ) - knownTlsVersions - .find(_ == minVersion) - .fold[Seq[String]]( - throw new IllegalArgumentException(s"Unknown TLS protocol version $minVersion") - )(versionFound => knownTlsVersions.filter(_ >= versionFound)) - } -} - -/** A wrapper for TLS server parameters supporting only server side authentication - * - * Same parameters as the more complete `TlsServerConfig` - */ -final case class BaseServerTlsConfig( - certChainFile: PemFileOrString, - privateKeyFile: PemFile, - minimumServerProtocolVersion: Option[String] = Some( - TlsServerConfig.defaultMinimumServerProtocol - ), - ciphers: Option[Seq[String]] = TlsServerConfig.defaultCiphers, -) extends TlsConfig - -/** A wrapper for TLS related server parameters supporting mutual authentication. - * - * Certificates and keys must be provided in the PEM format. It is recommended to create them with - * OpenSSL. Other formats (such as GPG) may also work, but have not been tested. - * - * @param certChainFile - * a file containing a certificate chain, containing the certificate chain from the server to the - * root CA. The certificate chain is used to authenticate the server. The order of certificates - * in the chain matters, i.e., it must start with the server certificate and end with the root - * certificate. - * @param privateKeyFile - * a file containing the server's private key. The key must not use a password. - * @param trustCollectionFile - * a file containing certificates of all nodes the server trusts. Used for client authentication. - * It depends on the enclosing configuration whether client authentication is mandatory, optional - * or unsupported. If client authentication is enabled and this parameter is absent, the - * certificates in the JVM trust store will be used instead. - * @param clientAuth - * indicates whether server requires, requests, or does not request auth from clients. Normally - * the ledger api server requires client auth under TLS, but using this setting this requirement - * can be loosened. See - * https://github.com/digital-asset/daml/commit/edd73384c427d9afe63bae9d03baa2a26f7b7f54 - * @param minimumServerProtocolVersion - * minimum supported TLS protocol. Set None (or null in config file) to default to JVM settings. - * @param ciphers - * supported ciphers. Set to None (or null in config file) to default to JVM settings. - * @param enableCertRevocationChecking - * whether to enable certificate revocation checking per - * https://tersesystems.com/blog/2014/03/22/fixing-certificate-revocation/ - */ -// Information in this ScalaDoc comment has been taken from https://grpc.io/docs/guides/auth/. -final case class TlsServerConfig( - certChainFile: PemFileOrString, - privateKeyFile: PemFile, - trustCollectionFile: Option[PemFileOrString] = None, - clientAuth: ServerAuthRequirementConfig = ServerAuthRequirementConfig.Optional, - minimumServerProtocolVersion: Option[String] = Some( - TlsServerConfig.defaultMinimumServerProtocol - ), - ciphers: Option[Seq[String]] = TlsServerConfig.defaultCiphers, - enableCertRevocationChecking: Boolean = false, -) extends TlsConfig { - lazy val clientConfig: TlsClientConfig = { - val clientCert = clientAuth match { - case ServerAuthRequirementConfig.Require(cert) => Some(cert) - case _ => None - } - TlsClientConfig(trustCollectionFile = Some(certChainFile), clientCert = clientCert) - } - - /** This is a side-effecting method. It modifies JVM TLS properties according to the TLS - * configuration. - */ - def setJvmTlsProperties(): Unit = { - if (enableCertRevocationChecking) OcspProperties.enableOcsp() - ProtocolDisabler.disableSSLv2Hello() - } - - override def protocols: Option[Seq[String]] = { - val disallowedTlsVersions = - Seq( - TlsVersion.V1.version, - TlsVersion.V1_1.version, - ) - minimumServerProtocolVersion match { - case Some(minVersion) if disallowedTlsVersions.contains(minVersion) => - throw new IllegalArgumentException(s"Unsupported TLS version: $minVersion") - case _ => - super.protocols - } - } - -} - -object TlsServerConfig { - - // default OWASP strong cipher set with broad compatibility (B list) - // https://cheatsheetseries.owasp.org/cheatsheets/TLS_Cipher_String_Cheat_Sheet.html - lazy val defaultCiphers = { - val candidates = Seq( - "TLS_AES_256_GCM_SHA384", - "TLS_CHACHA20_POLY1305_SHA256", - "TLS_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", - ) - val logger = LoggerFactory.getLogger(TlsServerConfig.getClass) - val filtered = candidates.filter { x => - io.grpc.netty.shaded.io.netty.handler.ssl.OpenSsl - .availableOpenSslCipherSuites() - .contains(x) || - io.grpc.netty.shaded.io.netty.handler.ssl.OpenSsl.availableJavaCipherSuites().contains(x) - } - if (filtered.isEmpty) { - val len = io.grpc.netty.shaded.io.netty.handler.ssl.OpenSsl - .availableOpenSslCipherSuites() - .size() + io.grpc.netty.shaded.io.netty.handler.ssl.OpenSsl - .availableJavaCipherSuites() - .size() - logger.warn( - s"All of Canton's default TLS ciphers are unsupported by your JVM (netty reports $len ciphers). Defaulting to JVM settings." - ) - if (!io.grpc.netty.shaded.io.netty.handler.ssl.OpenSsl.isAvailable) { - logger.info( - "Netty OpenSSL is not available because of an issue", - io.grpc.netty.shaded.io.netty.handler.ssl.OpenSsl.unavailabilityCause(), - ) - } - None - } else { - logger.debug( - s"Using ${filtered.length} out of ${candidates.length} Canton's default TLS ciphers" - ) - Some(filtered) - } - } - - val defaultMinimumServerProtocol = "TLSv1.2" - - /** Netty incorrectly hardcodes the report that the SSLv2Hello protocol is enabled. There is no - * way to stop it from doing it, so we just filter the netty's erroneous claim. We also make sure - * that the SSLv2Hello protocol is knocked out completely at the JSSE level through the - * ProtocolDisabler - */ - private def filterSSLv2Hello(protocols: Seq[String]): Seq[String] = - protocols.filter(_ != ProtocolDisabler.sslV2Protocol) - - def logTlsProtocolsAndCipherSuites( - sslContext: SslContext, - isServer: Boolean, - ): Unit = { - val (who, provider, logger) = - if (isServer) - ( - "Server", - SslContext.defaultServerProvider(), - LoggerFactory.getLogger(TlsServerConfig.getClass), - ) - else - ( - "Client", - SslContext.defaultClientProvider(), - LoggerFactory.getLogger(TlsClientConfig.getClass), - ) - - val tlsInfo = TlsInfo.fromSslContext(sslContext) - logger.info(s"$who TLS - enabled via $provider") - logger.debug( - s"$who TLS - supported protocols: ${filterSSLv2Hello(tlsInfo.supportedProtocols).mkString(", ")}." - ) - logger.info( - s"$who TLS - enabled protocols: ${filterSSLv2Hello(tlsInfo.enabledProtocols).mkString(", ")}." - ) - logger.debug( - s"$who TLS $who - supported cipher suites: ${tlsInfo.supportedCipherSuites.mkString(", ")}." - ) - logger.info(s"$who TLS - enabled cipher suites: ${tlsInfo.enabledCipherSuites.mkString(", ")}.") - } - -} - -/** A wrapper for TLS related client configurations - * - * @param trustCollectionFile - * a file containing certificates of all nodes the client trusts. If none is specified, defaults - * to the JVM trust store - * @param clientCert - * the client certificate - * @param enabled - * allows enabling TLS without `trustCollectionFile` or `clientCert` - */ -final case class TlsClientConfig( - trustCollectionFile: Option[PemFileOrString], - clientCert: Option[TlsClientCertificate], - enabled: Boolean = true, -) { - def withoutClientCert: TlsClientConfigOnlyTrustFile = - TlsClientConfigOnlyTrustFile( - trustCollectionFile = trustCollectionFile, - enabled = enabled, - ) -} - -/** A wrapper for TLS related client configurations without client auth support (currently public - * sequencer api) - * - * @param trustCollectionFile - * a file containing certificates of all nodes the client trusts. If none is specified, defaults - * to the JVM trust store - * @param enabled - * allows enabling TLS without `trustCollectionFile` - */ -final case class TlsClientConfigOnlyTrustFile( - trustCollectionFile: Option[PemFileOrString], - enabled: Boolean = true, -) { - def toTlsClientConfig: TlsClientConfig = TlsClientConfig( - trustCollectionFile = trustCollectionFile, - clientCert = None, - enabled = enabled, - ) -} - -final case class TlsClientCertificate(certChainFile: PemFileOrString, privateKeyFile: PemFile) - -/** Configuration on whether server requires auth, requests auth, or no auth */ -sealed trait ServerAuthRequirementConfig { - def clientAuth: ClientAuth -} -object ServerAuthRequirementConfig { - - /** A variant of [[ServerAuthRequirementConfig]] by which the server requires auth from clients */ - final case class Require(adminClient: TlsClientCertificate) extends ServerAuthRequirementConfig { - val clientAuth = ClientAuth.REQUIRE - } - - /** A variant of [[ServerAuthRequirementConfig]] by which the server merely requests auth from - * clients - */ - case object Optional extends ServerAuthRequirementConfig { - val clientAuth = ClientAuth.OPTIONAL - } - - /** A variant of [[ServerAuthRequirementConfig]] by which the server does not even request auth - * from clients - */ - case object None extends ServerAuthRequirementConfig { - val clientAuth = ClientAuth.NONE - } -} diff --git a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfiguration.scala b/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfiguration.scala deleted file mode 100644 index d8c03ce47c..0000000000 --- a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfiguration.scala +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.tls - -import com.daml.tls.TlsVersion.TlsVersion -import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext -import org.slf4j.LoggerFactory - -import java.io.File -import scala.jdk.CollectionConverters.* - -// Interacting with java libraries makes null a necessity -@SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.AsInstanceOf")) -final case class TlsConfiguration( - enabled: Boolean, - certChainFile: Option[File] = None, // mutual auth is disabled if null - privateKeyFile: Option[File] = None, - trustCollectionFile: Option[File] = None, // System default if null -) { - - private val logger = LoggerFactory.getLogger(getClass) - - /** If enabled and all required fields are present, it returns an SslContext suitable for client - * usage - */ - def client(enabledProtocols: Seq[TlsVersion] = Seq.empty): Option[SslContext] = - if (enabled) { - val enabledProtocolsNames = - if (enabledProtocols.isEmpty) - null - else - enabledProtocols.map(_.version).asJava - val sslContext = GrpcSslContexts - .forClient() - .keyManager( - certChainFile.orNull, - privateKeyFile.orNull, - ) - .trustManager(trustCollectionFile.orNull) - .protocols(enabledProtocolsNames) - .sslProvider(SslContext.defaultClientProvider()) - .build() - logTlsProtocolsAndCipherSuites(sslContext, isServer = false) - Some(sslContext) - } else None - - private[tls] def logTlsProtocolsAndCipherSuites( - sslContext: SslContext, - isServer: Boolean, - ): Unit = { - val (who, provider) = - if (isServer) - ("Server", SslContext.defaultServerProvider()) - else - ("Client", SslContext.defaultClientProvider()) - val tlsInfo = TlsInfo.fromSslContext(sslContext) - logger.info(s"$who TLS - enabled via $provider") - logger.debug( - s"$who TLS - supported protocols: ${filterSSLv2Hello(tlsInfo.supportedProtocols).mkString(", ")}." - ) - logger.info( - s"$who TLS - enabled protocols: ${filterSSLv2Hello(tlsInfo.enabledProtocols).mkString(", ")}." - ) - logger.debug( - s"$who TLS $who - supported cipher suites: ${tlsInfo.supportedCipherSuites.mkString(", ")}." - ) - logger.info(s"$who TLS - enabled cipher suites: ${tlsInfo.enabledCipherSuites.mkString(", ")}.") - } - - /** Netty incorrectly hardcodes the report that the SSLv2Hello protocol is enabled. There is no - * way to stop it from doing it, so we just filter the netty's erroneous claim. We also make sure - * that the SSLv2Hello protocol is knocked out completely at the JSSE level through the - * ProtocolDisabler - */ - private def filterSSLv2Hello(protocols: Seq[String]): Seq[String] = - protocols.filter(_ != ProtocolDisabler.sslV2Protocol) - -} - -object TlsConfiguration { - val Empty: TlsConfiguration = TlsConfiguration( - enabled = true, - certChainFile = None, - privateKeyFile = None, - trustCollectionFile = None, - ) -} diff --git a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfigurationCli.scala b/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfigurationCli.scala deleted file mode 100644 index f02d8bb57e..0000000000 --- a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsConfigurationCli.scala +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.tls - -import java.nio.file.Paths -import scala.util.Try - -object TlsConfigurationCli { - - type Setter[T, B] = (B => B, T) => T - def parse[C](parser: scopt.OptionParser[C], colSpacer: String)( - setter: Setter[C, TlsConfiguration] - ): Unit = { - def enableSet(tlsUp: TlsConfiguration => TlsConfiguration, c: C) = - setter(tlsc => tlsUp(tlsc.copy(enabled = true)), c) - - import parser.opt - - opt[String]("pem") - .optional() - .text("TLS: The pem file to be used as the private key.") - .validate(validatePath(_, "The file specified via --pem does not exist")) - .action { (path, c) => - enableSet(_.copy(privateKeyFile = Some(Paths.get(path).toFile)), c) - }: Unit - - opt[String]("crt") - .optional() - .text( - s"TLS: The crt file to be used as the cert chain.\n$colSpacer" + - s"Required for client authentication." - ) - .validate(validatePath(_, "The file specified via --crt does not exist")) - .action { (path, c) => - enableSet(_.copy(certChainFile = Some(Paths.get(path).toFile)), c) - }: Unit - - opt[String]("cacrt") - .optional() - .text("TLS: The crt file to be used as the trusted root CA.") - .validate(validatePath(_, "The file specified via --cacrt does not exist")) - .action { (path, c) => - enableSet(_.copy(trustCollectionFile = Some(Paths.get(path).toFile)), c) - }: Unit - - // allows you to enable tls without any special certs, - // i.e., tls without client auth with the default root certs. - // If any certificates are set tls is enabled implicitly and - // this is redundant. - opt[Unit]("tls") - .optional() - .text("TLS: Enable tls. This is redundant if --pem, --crt or --cacrt are set") - .action((_, c) => enableSet(identity, c)): Unit - () - } - - private def validatePath(path: String, message: String): Either[String, Unit] = { - val valid = Try(Paths.get(path).toFile.canRead).getOrElse(false) - Either.cond(valid, (), message) - } -} diff --git a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsInfo.scala b/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsInfo.scala deleted file mode 100644 index a727625154..0000000000 --- a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsInfo.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.tls - -import io.grpc.netty.shaded.io.netty.buffer.ByteBufAllocator -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext - -import javax.net.ssl.SSLEngine - -final case class TlsInfo( - enabledCipherSuites: Seq[String], - enabledProtocols: Seq[String], - supportedCipherSuites: Seq[String], - supportedProtocols: Seq[String], -) - -object TlsInfo { - def fromSslContext(sslContext: SslContext): TlsInfo = { - val engine: SSLEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT) - TlsInfo( - enabledCipherSuites = engine.getEnabledCipherSuites.toIndexedSeq, - enabledProtocols = engine.getEnabledProtocols.toIndexedSeq, - supportedCipherSuites = engine.getSupportedCipherSuites.toIndexedSeq, - supportedProtocols = engine.getSupportedProtocols.toIndexedSeq, - ) - } -} diff --git a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsVersion.scala b/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsVersion.scala deleted file mode 100644 index ef14e17738..0000000000 --- a/canton/base/daml-tls/src/main/scala/com/daml/tls/TlsVersion.scala +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.tls - -object TlsVersion { - - sealed abstract class TlsVersion(val version: String) { - override def toString: String = version - } - - case object V1 extends TlsVersion("TLSv1") - - case object V1_1 extends TlsVersion("TLSv1.1") - - case object V1_2 extends TlsVersion("TLSv1.2") - - case object V1_3 extends TlsVersion("TLSv1.3") - - val allVersions: Set[TlsVersion] = Set( - V1, - V1_1, - V1_2, - V1_3, - ) - -} diff --git a/canton/base/daml-tls/src/test/resources/gen-test-certificates.sh b/canton/base/daml-tls/src/test/resources/gen-test-certificates.sh deleted file mode 100755 index 62ce00d6d5..0000000000 --- a/canton/base/daml-tls/src/test/resources/gen-test-certificates.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -set -eoux pipefail - -mkdir test-certificates -cd test-certificates - -mkdir newcerts - -ABSOLUTE_OUT=$(pwd) -DAYS=7305 # 20 years (accounting for leap years) - -# Generate SSL config from the template -cat ../openssl-template.cnf | sed -e "s;;$ABSOLUTE_OUT;g" > openssl.cnf - -# Setup directories -touch -- index.txt -echo 1000 > serial - -function create_key { - local name=$1 - openssl genrsa -out "${name}.key" 4096 -} - -function create_pem { - local name=$1 - openssl pkey -in "${name}.key" -out "${name}.pem" -} - -function create_key_and_pem { - local name=$1 - create_key "$name" - create_pem "$name" -} - -function create_certificate { - local conf=$1 - local name=$2 - local subj=$3 - openssl req -config "$conf" \ - -key "${name}.key" \ - -new -x509 -days $DAYS -sha256 -extensions v3_ca \ - -subj "$subj" \ - -out "${name}.crt" -} - -function create_csr { - local name=$1 - local subj=$2 - local san=$3 - local conf="${4:-""}" - - args=( - -subj "$subj" - -addext "subjectAltName=${san}" - -key "${name}.pem" - -new - -out "${name}.csr" - ) - if [[ -n "$conf" ]]; then - args+=(-config "$conf") - args+=(-sha256) - fi - - openssl req "${args[@]}" -} - -function print_certificate { - local name=$1 - openssl x509 -in "${name}.crt" -text -noout -} - -# Generate Root CA private key -create_key "ca" -chmod 400 "ca.key" -# Create Root Certificate (self-signed) -create_certificate "openssl.cnf" "ca" "/CN=0.0.0.0.ca" -print_certificate "ca" - -# Generate server key, csr and crt -create_key_and_pem "server" -create_csr "server" "/CN=0.0.0.0.server" "DNS:localhost, IP:127.0.0.1" "openssl.cnf" -openssl ca -batch -config "openssl.cnf" \ - -extensions server_cert -days $DAYS -notext -md sha256 \ - -in "server.csr" \ - -out "server.crt" -chmod 444 "server.crt" - -# Encrypt server's key and dump encryption parameters to a JSON file. -# NOTE: Encryption details used to encrypt the private must be kept in sync with `test-common/files/server-pem-decryption-parameters.json` -openssl enc -aes-128-cbc -base64 \ - -in "server.pem" \ - -out "server.pem.enc" \ - -K 0034567890abcdef1234567890abcdef \ - -iv 1134567890abcdef1234567890abcdef - -# Generate Client CA private key -create_key_and_pem "client" -openssl req -new -key "client.pem" \ - -subj "/CN=0.0.0.0.client" \ - -addext "subjectAltName = DNS:localhost, IP:127.0.0.1" \ - -out "client.csr" -# Sign Client Cert -openssl ca -batch -config "openssl.cnf" \ - -extensions usr_cert -days $DAYS -notext -md sha256 \ - -in "client.csr" \ - -out "client.crt" -# Validate cert is correct -openssl verify -CAfile "ca.crt" "client.crt" - -# Generate OCSP Server private key -openssl genrsa -out "ocsp.key.pem" 4096 -# Sign OCSP Server certificate -openssl req -config "openssl.cnf" -new -sha256 \ - -subj "/CN=ocsp.127.0.0.1" \ - -key "ocsp.key.pem" \ - -out "ocsp.csr" - -openssl ca -batch -config "openssl.cnf" \ - -extensions ocsp -days $DAYS -notext -md sha256 \ - -in "ocsp.csr" \ - -out "ocsp.crt" -# Validate extensions -openssl x509 -noout -text \ - -in "ocsp.crt" - - -# Generate Client-Revoked CA private key -openssl genpkey -out "client-revoked.key" -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -create_pem "client-revoked" -create_csr "client-revoked" "/CN=0.0.0.0.clientrevoked" "DNS:localhost, IP:127.0.0.1" -# Sign Client Cert -openssl ca -batch -config "openssl.cnf" \ - -extensions usr_cert -days $DAYS -notext -md sha256 \ - -in "client-revoked.csr" \ - -out "client-revoked.crt" -# Validate cert is correct -openssl verify -CAfile "ca.crt" "client-revoked.crt" -# Revoke -openssl ca -batch -config "openssl.cnf" -revoke "client-revoked.crt" - - -## Configure alternative CA for 'invalid certificate' scenarios -NEWCERTS_ALTERNATIVE_DIR=$ABSOLUTE_OUT/newcerts_alternative - -# Generate SSL config from the template -cat ../openssl-alternative-template.cnf | sed -e "s;;$ABSOLUTE_OUT;g" > openssl-alternative.cnf - -# Setup directories -mkdir -- $NEWCERTS_ALTERNATIVE_DIR -touch -- index_alternative.txt -echo 1000 > serial_alternative - -# Generate Root Alternative CA private key -create_key "ca_alternative" -chmod 400 ca_alternative.key -create_pem "ca_alternative" -create_certificate "openssl-alternative.cnf" "ca_alternative" "/CN=0.0.0.0.ca" -print_certificate "ca_alternative" diff --git a/canton/base/daml-tls/src/test/resources/openssl-alternative-template.cnf b/canton/base/daml-tls/src/test/resources/openssl-alternative-template.cnf deleted file mode 100644 index defe81bbb6..0000000000 --- a/canton/base/daml-tls/src/test/resources/openssl-alternative-template.cnf +++ /dev/null @@ -1,90 +0,0 @@ -# OpenSSL root CA configuration file. -# Copy to `certs/root/openssl.cnf`. - -# Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - - -[ ca ] -# `man ca` -default_ca = CA_default - -[ CA_default ] -# Directory and file locations. -dir = -certs = $dir/certs_alternative -crl_dir = $dir/crl_alternative -new_certs_dir = $dir/newcerts_alternative -database = $dir/index_alternative.txt -serial = $dir/serial_alternative -RANDFILE = $dir/private/.rand_alternative - -# The root key and root certificate. -private_key = $dir/ca_alternative.key -certificate = $dir/ca_alternative.crt - -# SHA-1 is deprecated, so use SHA-2 instead. -default_md = sha256 - -name_opt = ca_default -cert_opt = ca_default -default_days = 375 -preserve = no -policy = policy_loose - -[ policy_loose ] -# Allow the CA to sign a more diverse range of certificates. -# Test-only. -# See the POLICY FORMAT section of the `ca` man page. -countryName = optional -stateOrProvinceName = optional -localityName = optional -organizationName = optional -organizationalUnitName = optional -commonName = supplied -emailAddress = optional - -[ req ] -# Options for the `req` tool (`man req`). -default_bits = 2048 -distinguished_name = req_distinguished_name -string_mask = utf8only - -# SHA-1 is deprecated, so use SHA-2 instead. -default_md = sha256 - -# Extension to add when the -x509 option is used. -x509_extensions = v3_ca - -[ req_distinguished_name ] -# See . -countryName = CH - -[ v3_ca ] -# Extensions for a typical CA (`man x509v3_config`). -subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid:always,issuer -basicConstraints = critical, CA:true - -[ usr_cert ] -# Extensions for client certificates (`man x509v3_config`). -subjectAltName = @alt_names -authorityInfoAccess = OCSP;URI:http://127.0.0.1:2560 - -[ server_cert ] -# Extensions for server certificates (`man x509v3_config`). -subjectAltName = @alt_names -authorityInfoAccess = OCSP;URI:http://127.0.0.1:2560 - -[ ocsp ] -# Extension for OCSP signing certificates (`man ocsp`). -basicConstraints = CA:FALSE -subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid,issuer -keyUsage = critical, digitalSignature -extendedKeyUsage = critical, OCSPSigning - -[ alt_names ] -DNS = localhost -IP = 127.0.0.1 - diff --git a/canton/base/daml-tls/src/test/resources/openssl-template.cnf b/canton/base/daml-tls/src/test/resources/openssl-template.cnf deleted file mode 100644 index 5b2d89eafd..0000000000 --- a/canton/base/daml-tls/src/test/resources/openssl-template.cnf +++ /dev/null @@ -1,90 +0,0 @@ -# OpenSSL root CA configuration file. -# Copy to `certs/root/openssl.cnf`. - -# Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - - -[ ca ] -# `man ca` -default_ca = CA_default - -[ CA_default ] -# Directory and file locations. -dir = -certs = $dir/certs -crl_dir = $dir/crl -new_certs_dir = $dir/newcerts -database = $dir/index.txt -serial = $dir/serial -RANDFILE = $dir/private/.rand - -# The root key and root certificate. -private_key = $dir/ca.key -certificate = $dir/ca.crt - -# SHA-1 is deprecated, so use SHA-2 instead. -default_md = sha256 - -name_opt = ca_default -cert_opt = ca_default -default_days = 375 -preserve = no -policy = policy_loose - -[ policy_loose ] -# Allow the CA to sign a more diverse range of certificates. -# Test-only. -# See the POLICY FORMAT section of the `ca` man page. -countryName = optional -stateOrProvinceName = optional -localityName = optional -organizationName = optional -organizationalUnitName = optional -commonName = supplied -emailAddress = optional - -[ req ] -# Options for the `req` tool (`man req`). -default_bits = 2048 -distinguished_name = req_distinguished_name -string_mask = utf8only - -# SHA-1 is deprecated, so use SHA-2 instead. -default_md = sha256 - -# Extension to add when the -x509 option is used. -x509_extensions = v3_ca - -[ req_distinguished_name ] -# See . -countryName = CH - -[ v3_ca ] -# Extensions for a typical CA (`man x509v3_config`). -subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid:always,issuer -basicConstraints = critical, CA:true - -[ usr_cert ] -# Extensions for client certificates (`man x509v3_config`). -subjectAltName = @alt_names -authorityInfoAccess = OCSP;URI:http://127.0.0.1:2560 - -[ server_cert ] -# Extensions for server certificates (`man x509v3_config`). -subjectAltName = @alt_names -authorityInfoAccess = OCSP;URI:http://127.0.0.1:2560 - -[ ocsp ] -# Extension for OCSP signing certificates (`man ocsp`). -basicConstraints = CA:FALSE -subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid,issuer -keyUsage = critical, digitalSignature -extendedKeyUsage = critical, OCSPSigning - -[ alt_names ] -DNS = localhost -IP = 127.0.0.1 - diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/ca.crt b/canton/base/daml-tls/src/test/resources/test-certificates/ca.crt deleted file mode 100644 index 926c567235..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/ca.crt +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFCjCCAvKgAwIBAgITZXn3DA82+xzsZ/NP8sRZpYih0DANBgkqhkiG9w0BAQsF -ADAVMRMwEQYDVQQDDAowLjAuMC4wLmNhMB4XDTI2MDMwOTA5MDU0OVoXDTQ2MDMw -OTA5MDU0OVowFTETMBEGA1UEAwwKMC4wLjAuMC5jYTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBALeRMQw6URW5X7bI027/T2XT8XFdoTop1vzUT4w2l2H/ -YOFHEHdY8WJizvwQBGW+o/EXnziRhMb5DbccGj9RUmc9hrLLsTOVPoMy/bqNU00b -A7FfoHvqC4Oqbxb2lhySLsrr9xC5Bq5nCWBRDHJfN+HAHyQMc7LJvdPhcrrtLMvA -FmUwntox53ld8kef6ykP7NICSveNwDspt0JTLytABPY2iPw2K19aG3wxh4vovLLp -HQPV1/QvUJUg/XLsNbEzNyZron3ZJ++xO7+gyAE9kS0dtahqiFU9VxmaK1A+XanI -ISkEIrpeCt6wQKr720qLG72VxpB0NF5M1h4QMyjdECQoZhmBc43N6k71/Dw6bVhx -rcqrtHHLeXF3InjhijJm4hYWoqDSdHUoVohaDdzPw2nTmuC7Ew6TkIB3ZWgE13wD -kI1Gh61voJCV/SU/KCbbMj1Is1THZAF6V3ce3DjGyQTUboVPJwTk5NjJCYFZ4BM2 -BsmfppPrbhx8P2gpbYfFtMREFkvFstTgVrXXg8TTmsrGgypulZF+g0PBTMW+9Pcn -TdZeoI6szvXkg1btF0W/G1TaM71jbXsqxRztLvd1UeS+v+58eBuER6EVzFdXpXfH -gSkwOraSi4Cxd+hcyHdvQGL70KVQt2QgEH5j9B33M7CK4+i1vsvO+N2512sJA8/X -AgMBAAGjUzBRMB0GA1UdDgQWBBQRC9xCPoxP3RgToDoK88B1gOebzzAfBgNVHSME -GDAWgBQRC9xCPoxP3RgToDoK88B1gOebzzAPBgNVHRMBAf8EBTADAQH/MA0GCSqG -SIb3DQEBCwUAA4ICAQBCBnj94FA9AvSoTg1TcXjTwDUPBUZbTMDWML+mrw/m10Es -t+/WJQyuJ20Oj3KvPdO6TaXz4rh7H3rn428ITfFP4TAZzihYVmvIl+OokoZ3AR6m -RfxevvHD9R+eR0ru0DBRCzVLMGfU2nNd0Hhry8Q/zrPpyTqQ5zGB3N/y+dPS0VqH -aP/hOJoSAMgI6bn9tRC1C3E6SGmCDm9OygYqjWn2IuAPrTz1WxTx1TB181uD6uOj -VzvLxbbbJbbIjspa16l1VfEKGTI9WrvKf9pV/zkPgm7Nd1OxUR2G71GrMYvouI7t -3Uv758muCx9Oim3nT8ptthTfgqcfgvl2OEFfA2JWtAbaBfF9rMKASd7eZTp+d5BP -x+mEm6ZD0s+Xm4b6uHaMY8CkMOEiJ5OWZ/kXq9a9j9NiQ+cpiahxskfUzSIVoXKu -gqVUDZtUWlN7rj+MXqEa+SyrhDFUXecTq4LGuwTpXYYLIpu3tN91waw9xl3kHr+w -GLB/hLP+H+ImpDAUhdMmgLM4WwD78dOltooUkIKhHyFHbk8/aniTHdO9q/YvBcn3 -RznwbyLJVoo/oLq+qdCo8/LVpQVRLkwaBIeBh6adzpkfFXbsP3zqyyPLtPZm2gk1 -wkyGwlawjV8jmhyuiHwuSanQobDR4wzhSsvxEaKBNV/DNatNozMXgpf3y6rFqg== ------END CERTIFICATE----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/ca.key b/canton/base/daml-tls/src/test/resources/test-certificates/ca.key deleted file mode 100644 index cc909763de..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/ca.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC3kTEMOlEVuV+2 -yNNu/09l0/FxXaE6Kdb81E+MNpdh/2DhRxB3WPFiYs78EARlvqPxF584kYTG+Q23 -HBo/UVJnPYayy7EzlT6DMv26jVNNGwOxX6B76guDqm8W9pYcki7K6/cQuQauZwlg -UQxyXzfhwB8kDHOyyb3T4XK67SzLwBZlMJ7aMed5XfJHn+spD+zSAkr3jcA7KbdC -Uy8rQAT2Noj8NitfWht8MYeL6Lyy6R0D1df0L1CVIP1y7DWxMzcma6J92SfvsTu/ -oMgBPZEtHbWoaohVPVcZmitQPl2pyCEpBCK6XgresECq+9tKixu9lcaQdDReTNYe -EDMo3RAkKGYZgXONzepO9fw8Om1Yca3Kq7Rxy3lxdyJ44YoyZuIWFqKg0nR1KFaI -Wg3cz8Np05rguxMOk5CAd2VoBNd8A5CNRoetb6CQlf0lPygm2zI9SLNUx2QBeld3 -Htw4xskE1G6FTycE5OTYyQmBWeATNgbJn6aT624cfD9oKW2HxbTERBZLxbLU4Fa1 -14PE05rKxoMqbpWRfoNDwUzFvvT3J03WXqCOrM715INW7RdFvxtU2jO9Y217KsUc -7S73dVHkvr/ufHgbhEehFcxXV6V3x4EpMDq2kouAsXfoXMh3b0Bi+9ClULdkIBB+ -Y/Qd9zOwiuPotb7LzvjduddrCQPP1wIDAQABAoICAA0Tb/YzYPbWZ/SDMGrANE/j -q2oDHJ0P08a1ecxH4mCOCon/SSEz1YaQ+bZDRYl9kembFzrtH9yHIifnWVpvJnYu -EALo5NwnlMkGcnrrgvjDm11g0jzM3NVAsYb4S92Ib12UWrYkvaoGjn9/aMWjdb2E -WcE9jGbfSwolaQDavqSAC281IKOqvfIm6tGZK20XBIfMSZXLLRJgqtfYkl5gQiNd -vvr/uIlaNXe3y8akXEi1bm93ATVv52Jdz029nUwTS/LoUgmf5wZwKcN4FCTgw1hs -TyPcs2mUI++yAumzLU3YAuL6AMwlU4FSrTUp9OvFytUmU1a348o6/RVSbfOmAwjb -oTJ/5ON7eUXAnJXKDXyKEKZmCDpE2zqT372+WGwodNLebJJaJtSSbutbQzJaaH59 -RTUDKtxU+g+w2g3DYqcBl9+bInBNxZZLvxkGwxUQFYomkg/O+HIO9n8mhkPV4msU -pi4Rp7Um5Qh7pSYSTTdjk+fA2kBRxPBr8g50C33tRtOBmxzO5UWuP66P5TW07shw -rX8Ij20FeUk6P/w2/hx55oyVkAeaNtgRpZ2e/dEEABqiOwDuT/Y9J/cOjYxmfQXR -RHPPzQ/xHSWwszIT2ud8Fn3DxFIRlsyEOI3fBOaUI6k7JT7fYIK+u9+V8CWTHSkr -A9VNII08GhulEuMqHjsxAoIBAQDrOcdv646X7mG7Qb7Tt4pdlnMlt1Og0Sg4OfrE -oHLhcoXaNaFBfXJbPbGr+f14rTPEMjVJvNpAIMy+Flw5xRg/rtzQ+Prf5ptPe142 -A0cmGXawwpAXE/O39Vgww3w389F/Zm0vkgw1zh+nkEPoRSne2tJp/zx0l8t1Oo+D -TUI8YaFnMaMw/26bdG8oCuverqdi+e9DvLgTI/BpU9fgQvtQfoRRuHKD6VHyu3EN -9p2xj1tSoFdAEw6TqBiLim8jQCf61vprjQEl4ISDvvlQEtKPuHzWBSev1CX9jfD1 -lwGkugy1sntHYRrEHg4PAhnoH8lwnEJdiOgX4vW2nD0KDgkxAoIBAQDHx3a85GGJ -qd6wLuQtpQsSBD2tXeKc1yfxLq6LkMtGWbVm8Y9jZpixNnZWKqinYX4iGf14cvRw -CjJnGiQw+9SCRoAorFAh9LtsYsCFjVc0UkyHC/tpH7iPF3wn/GW0AqSYSHgU/LDo -eEEKAf0HQoXegHipj1JC6WOwrsjjsBV89/QlLYm+BisTM3YZizVdim7Z2dmU0oTZ -pSBUzLt1wguBmWKM4P7gAWxoEwami/f9ALtKX663raMOi6vFbtVtd/j38Y75r59g -8KN5hM+pUP6OXIvP152AM1LoL4bAZgctn0WGHT0i+dkD+EPuiihCvbrm6bc8k6Aw -uCgBbniF/qeHAoIBABw2DclKgwuNt4Lx26UguiOHS8AWQJ1k+GHl8kFqzNxlE9Cb -5f8owHJY79okXVX/z9mT+/ggAjz2sheKLv1uuf3JRwp5Ef2QdsgNj7sCoaHcI/QH -Iaji57QEZNNUXq2HPHT/H9E4vuYlFn7OOpa81My5icrUpbeGDQy+PUAOIAeF3gm2 -VlAmEmnqiPpQdQF8CO9B8oQ5OgpxoP/A4n8vCV0aD1yNto5DiWSHPoPiM/97pgQ+ -3rf8kR78ZhU2QSYFJ8ZZyibj/mNAjiQO32/PEhnSkoZIj/3yf6vtPhz2Q2gyOBwC -j+57ou4qfJ0oL1s7lLlAxGTbapqSyc1g56ExFcECggEAOdefoltBfllHl51MBYc2 -qFXAAdUgCqck8mjAsroOkOTiZ9bi3DXVWeHZ62LYh/XrTjZWNCycZ2Acbcuc1O3k -/n8pFh2l6d7w8RW6hjCBIi396E/oz+wWRJ/ZG76xN6hYPZsl4XW0RuqiciJXP2Xc -Wqq5xOYxPBxU98/zbDEMV61269i+e91PVXTd6dFrdxTguYSgwb0eJ+HudX79e7/e -Evz1ErMGOJdQ6uUvVMNL3t36XDNN4/KLNmfsKPLJKrPEyw0Oh25459lki4ePlIdV -3KQg+uGDwZ2akI1KJ6SiyWvG9mi6zcinM8YK7SLlO0Wxuq/KlOhtRg3bz2ZL0QgF -OwKCAQEA4rxBrCTUSg7yAS6X9feRxu7s9tZ1FRy6YfqnpJ9G3it6JYiKzyRBAJWW -yN5mFrjdUrnvnL1fhyRRJvdMJtoxPgKCPYGbI5g5nS88JQlFWnCAk7Dk8966vr0Y -GeppY8slPLcJ9mkGF0l13q8EmkvYqZ/MYbj/684YJBcWzkeJRyupT8a33LQd0mx9 -oCF3fv/NLalSk8uVJs+mTm8g5SGu52CNoGZKzU+S2WCKrkl7LOcf7JlfTsS0SzDD -OXLh6aN2ZmvQ5GEFM4/ASDK40dhKR3kOUTCNKDm8rLoEbCWAZKTZ+3wUXyRGlIs3 -6I3oWvy3J+hLa7tQGtGU0M3hFfqdvA== ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.crt b/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.crt deleted file mode 100644 index fa77aef2d5..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.crt +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFCzCCAvOgAwIBAgIUPEF+QY15O7GaOMff1imD/2Boy1owDQYJKoZIhvcNAQEL -BQAwFTETMBEGA1UEAwwKMC4wLjAuMC5jYTAeFw0yNjAzMDkwOTA1NTJaFw00NjAz -MDkwOTA1NTJaMBUxEzARBgNVBAMMCjAuMC4wLjAuY2EwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCz/CFKWJy15HgNL3QUfzEqI6LweflrWGOcxrrnM+Xp -H+zbo0ScVXWyMJd4zC637CMZJmcOgZbXWqyUBkpf5De9wOKH70urYkJCpx5QWdaY -usMtUTxsbykMFwdOkpptm+wV/379KBU0+XZQWSxlR7FYKKhHAdgGWcKFjjRgQ4+j -j66XCH1Knes4ZWhYuvNWc3di8b942etxpKfHWfZJyHYff/mzkmgtBB2zMos2LXgQ -uTc2Qp5zu5l6bcTJ0OBHo8AQO3P04VLqZMNmVFW+pjAqQyq1jA1phT2nc93daWAw -NYQg0VRaJ8jabtHZOkG7q7ndBDkxcWUTOlTvi07YY2TcVoWIHGeqMmNQ03YuwtYZ -ZX/cbLzkOtGrhnHWOeXCKwvsYL76laojAtjieket8Zk1CP4lh6rXGAI2X8hWD1Ti -H4LvOfWdAjOyOZzXpElAly9qLqa5wLw9K4RSi49UAzjrUtHDkh8TfKsg/noRkAGx -cpvzqcCuQ/rPz6W/oTi0NRmlg2ePl79STbVbtW0F7vhzCjGxU1daWnCZhzR5ZV2W -fLzgxrd0BkWAyH7Xjb3nzXlStY8ICRFLORagxiPkDcJR1HbsmjVbSvGDZfdlFZhc -wE4/E/2xHTgaLWLa029dF585tAKUJ7J5UfkL5FoaUhQK0gbD1OCDdZ0zXqOmpEnO -IQIDAQABo1MwUTAdBgNVHQ4EFgQUrEoRCs52owDJH2wWPXizze8FupowHwYDVR0j -BBgwFoAUrEoRCs52owDJH2wWPXizze8FupowDwYDVR0TAQH/BAUwAwEB/zANBgkq -hkiG9w0BAQsFAAOCAgEAMsqyBmMjzwGCq5z3XDU5S6k05bLHxy0Gw5vTTO2REpdX -NmfMEXA8nvBFgPEG/4xIuiSfjnC1YQyf6zqFzO5W+SYW8LjZFXfZ5NKHQtq5/b+9 -909WNMDZF3T+YBhe9tUyhzoCAtgWPmkIWGyOvJg8qW+xQZy1ipo4+iqAPwox6INX -0raYHk+Ae0ERk+IWKkRHW6OY5vH1nCaWm5ZHXmQV7gLe3gh8VtzqxI4FRPvMc622 -3VuogmtemMHH0sBxMbzp+cP6OuP+Te4C+a2osNksPVlNwYdMMjdOgCIGHh8LTeKe -2oE9s2qnyIyBja9irYK3nA6Ifb+O2YDs9SjOrXED8qAmvv5NHLKBikXQTynSfwhh -shk3L/2qv2MjhV2Bu6iKj7mjbQiV/BNek410t4tFKEN4VUrdJ4MgFMBjmp7uiiW4 -y5MzKO1kIoM69+OIwFm1UvA6CQwdJRbcH4z1hWAUSIR2L5f2wmpTbCUUGtwM9X+n -3FUBDMNiLv/yj+Fk8bsPGyUiitQcigH255mqF8fnqJ52/ntLdolJjonD9y5Dxwg2 -02Bcmk6PIC6YiD28XMSUQnClwqDcadkzA6IjxU1ruV7EzcOpTzpGuwiMKRLWt3p0 -sr82zpL0NXOByb34KRrjgqMtIEd5UgaGVw8ytKP8TlOAfbX9as26M7SD6aR4uNY= ------END CERTIFICATE----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.key b/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.key deleted file mode 100644 index 69d8937b04..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCz/CFKWJy15HgN -L3QUfzEqI6LweflrWGOcxrrnM+XpH+zbo0ScVXWyMJd4zC637CMZJmcOgZbXWqyU -Bkpf5De9wOKH70urYkJCpx5QWdaYusMtUTxsbykMFwdOkpptm+wV/379KBU0+XZQ -WSxlR7FYKKhHAdgGWcKFjjRgQ4+jj66XCH1Knes4ZWhYuvNWc3di8b942etxpKfH -WfZJyHYff/mzkmgtBB2zMos2LXgQuTc2Qp5zu5l6bcTJ0OBHo8AQO3P04VLqZMNm -VFW+pjAqQyq1jA1phT2nc93daWAwNYQg0VRaJ8jabtHZOkG7q7ndBDkxcWUTOlTv -i07YY2TcVoWIHGeqMmNQ03YuwtYZZX/cbLzkOtGrhnHWOeXCKwvsYL76laojAtji -eket8Zk1CP4lh6rXGAI2X8hWD1TiH4LvOfWdAjOyOZzXpElAly9qLqa5wLw9K4RS -i49UAzjrUtHDkh8TfKsg/noRkAGxcpvzqcCuQ/rPz6W/oTi0NRmlg2ePl79STbVb -tW0F7vhzCjGxU1daWnCZhzR5ZV2WfLzgxrd0BkWAyH7Xjb3nzXlStY8ICRFLORag -xiPkDcJR1HbsmjVbSvGDZfdlFZhcwE4/E/2xHTgaLWLa029dF585tAKUJ7J5UfkL -5FoaUhQK0gbD1OCDdZ0zXqOmpEnOIQIDAQABAoICADCg/7GhrX2rpBAT8S4Ui4kS -JdUJb61kLU3Pl/Id4JGTMaDKRSBkKq+hV1uo9zp0T6tc591QgBvhgN1hsbik5bac -JdiKfAxrOPHfS150V3nehBt7ZnL5ucJtemjtTVIuHaR36qylps0wVZox7b/TlgP2 -wDinM+HLYLZLDLWxq16K1KY0Af4mGsabB41pvZNvESOno48YefezgC1pjhSzV878 -+c9d0E3RXeDr2BckQ9to35UGfN9SIY6ll9TbXa2dfaglGBhixM9VuEGuqZggOMVC -boistXb8LANRV/GZpgaAfxmK1Vqigy8ZYBNsHRSCum4P1RAZzjBaw1YRcpUejcJR -2zU2H3wUUs2JCsw0hngzuVH2PfySIym+7quU2zic3hwAI0sMtsLDaxOYVai0NuLm -UEn2TCn4X+9jkTl8KxpQmvHRdOnUE0xDci0n+/t99Tg5qQrh9zEabt7XyZG2k/w+ -onpFoDhnbT2gwgZznWLGVPLxmvjfzGvAtRrHw/zkfDmjq4aPTYrvi3pJ8f+bF4ki -ZaMZtOc67dMwLFQ+ulaCzseuWdv+J48c/8ZmDJvctkVLbLpzO1v8BFqc95qJoZtP -kmF7wcCYC6qd5+onb3rUPNSK902M443BxwUM57jhE2hRqw6d7VLDaTLvj705v5Lc -wkFg4nEadcDkmhwZaiNdAoIBAQDv96dyP18gw5IlNPzXqkPLB4EUCK166kkFL1IQ -ydUrqtkRXtTpHPIym+MyGweqcEyhavAaNIRd+8pky8qUnHoC0yK4L8S5+aDakHn6 -MOXQLwGlJWCWMXIKizW0SqBXBMUOxgG+tZ/E8IgiGK8oaL7UnAKyeDk30jB/SNFt -Vxyd3FoWM/tj+9P2Tl3tE+8tZEY8GFKDLj6cYLCvFPBvFZxDmDwkIlQgPxvSZpfH -SEfEIVLDbXJ8H6tdWs3it/BTUsNp0czIoUWYZ3Lq8NVwy2pCoF/FwHr43uvnaaQV -kmbZc1LetR/ICdNRl9dhGdt0APtn2zeVOHM56uTmQtgbOUADAoIBAQDAAoyTIYJ8 -yP6KvfmAJYjq7vE4SnWjYgeqrLp/uigvf3tJBkMF1GbWt7LEPtndR094Ojfrlo9E -YqKx7HA4oFeieSSmG8p5ry8DUryPwF+bZtoaXbtCttDUm9Bk7pmPaJqj45wed+xx -g3+h55OQyhVVeF5m0wanI7LjOZbSs2tuOL0bnXZ3Kc5kQ3i1iNlEUQQx3Ax1OPRr -mG46zZGt2tV4UYbZwlt6Wf+ieUwF+K8niy3wkcbSj0ujOokBbrTG0uUhZJ0mNahJ -GLjUoDrJiuMGgq0gWmEfHPwOtJnKnjRpzN3Pg5vFT40NNEBmeH0SQTMtjECiM6pw -XhJZvhddcVoLAoIBAQCWHOFVucjjcRQqTFRjATL5fg9EHg2P16GoKar3r5q3f1/c -2urrjN0BxbzBamFIQInfbEsYyYOIW2uH1Gg8wQoeXD/a4p75C+yQ7VIwrYgFzHIG -AjTCn9nMOLt2tjxJU0WGcV1EFNAGXcV7Nt/Tsnq1+4ZAaK66ebU22rGP6a+QHPkJ -7Ki1KiK1cN6W4SJ8zb6V900enL40CtRcIggqJWWkb0YoAIWJypC39M+7rT6ftYzV -jdDRTupfMt+alSA0r5Bo0QJ++0vTA9vClreeoNEvLq3awgbrSmBeWhp4GCZYnyjX -Ao0IEQVthEQKjSdYEQk0NdE2eqKyN3vLg0M2/IBLAoIBAQCVrKcGPjXorgl1/7cR -RLEhadeRqMFg9hbB3dmXnXab6AUjnIT8c2Ei+ZmZzuCn1GHHd6KFvBy8EyN+DLBC -BAOsPEDsV3eTOZMtRP6GFFCy5A8aRHyn+bh6M2fUdgYwOztojBHz0kn3BqmkfM+U -K2NL+AOmpdNm0LYu2IKQhpo1FnebojhplaY0lD/xStcKoRjjAQSZECfAA9fRcNOh -sZd8LhkvgIksrCFQ9rrjYMKw0ZajFA/nrRoqCIQqILE818MTL8osseEX8fSKksig -uOpV9eGc+bcBuOdnUzMbNg3nd3Jkw9PWkDeE1nlpJ31fkIGjfNQZXKr84uftGvg9 -uBGXAoIBAEy05wtseYHh/C7KdAj/zqR1r3nOLV7omqwR+hLxXuW3thaMr7AlsubF -t2VRUvFv1Es1/jbMk1NzZMbsjBwDkEePItUOYt8asruRRM/dPfwwTEyfkJQiC93X -y+tLpJv+hWvVO4JHeNULA8GTwiF0nwdwaPFntlDHO17iISjhtUdW+Wt/KYflQLAr -F28aXl+9H+maXQ0UX34t5OeJCrtSWw8TcwQpuwFrcg6CS4UF1n7s5h22KfC5mgIO -NVK4wb0a6b7ajWIhJuqDljywr20ny599WzTRB55CDsWAfoxWVQxqWdEDzTgL4yU0 -Nq3cDXPDkMfIHnGoTpP3wyRQxsPOLy8= ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.pem b/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.pem deleted file mode 100644 index 69d8937b04..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/ca_alternative.pem +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCz/CFKWJy15HgN -L3QUfzEqI6LweflrWGOcxrrnM+XpH+zbo0ScVXWyMJd4zC637CMZJmcOgZbXWqyU -Bkpf5De9wOKH70urYkJCpx5QWdaYusMtUTxsbykMFwdOkpptm+wV/379KBU0+XZQ -WSxlR7FYKKhHAdgGWcKFjjRgQ4+jj66XCH1Knes4ZWhYuvNWc3di8b942etxpKfH -WfZJyHYff/mzkmgtBB2zMos2LXgQuTc2Qp5zu5l6bcTJ0OBHo8AQO3P04VLqZMNm -VFW+pjAqQyq1jA1phT2nc93daWAwNYQg0VRaJ8jabtHZOkG7q7ndBDkxcWUTOlTv -i07YY2TcVoWIHGeqMmNQ03YuwtYZZX/cbLzkOtGrhnHWOeXCKwvsYL76laojAtji -eket8Zk1CP4lh6rXGAI2X8hWD1TiH4LvOfWdAjOyOZzXpElAly9qLqa5wLw9K4RS -i49UAzjrUtHDkh8TfKsg/noRkAGxcpvzqcCuQ/rPz6W/oTi0NRmlg2ePl79STbVb -tW0F7vhzCjGxU1daWnCZhzR5ZV2WfLzgxrd0BkWAyH7Xjb3nzXlStY8ICRFLORag -xiPkDcJR1HbsmjVbSvGDZfdlFZhcwE4/E/2xHTgaLWLa029dF585tAKUJ7J5UfkL -5FoaUhQK0gbD1OCDdZ0zXqOmpEnOIQIDAQABAoICADCg/7GhrX2rpBAT8S4Ui4kS -JdUJb61kLU3Pl/Id4JGTMaDKRSBkKq+hV1uo9zp0T6tc591QgBvhgN1hsbik5bac -JdiKfAxrOPHfS150V3nehBt7ZnL5ucJtemjtTVIuHaR36qylps0wVZox7b/TlgP2 -wDinM+HLYLZLDLWxq16K1KY0Af4mGsabB41pvZNvESOno48YefezgC1pjhSzV878 -+c9d0E3RXeDr2BckQ9to35UGfN9SIY6ll9TbXa2dfaglGBhixM9VuEGuqZggOMVC -boistXb8LANRV/GZpgaAfxmK1Vqigy8ZYBNsHRSCum4P1RAZzjBaw1YRcpUejcJR -2zU2H3wUUs2JCsw0hngzuVH2PfySIym+7quU2zic3hwAI0sMtsLDaxOYVai0NuLm -UEn2TCn4X+9jkTl8KxpQmvHRdOnUE0xDci0n+/t99Tg5qQrh9zEabt7XyZG2k/w+ -onpFoDhnbT2gwgZznWLGVPLxmvjfzGvAtRrHw/zkfDmjq4aPTYrvi3pJ8f+bF4ki -ZaMZtOc67dMwLFQ+ulaCzseuWdv+J48c/8ZmDJvctkVLbLpzO1v8BFqc95qJoZtP -kmF7wcCYC6qd5+onb3rUPNSK902M443BxwUM57jhE2hRqw6d7VLDaTLvj705v5Lc -wkFg4nEadcDkmhwZaiNdAoIBAQDv96dyP18gw5IlNPzXqkPLB4EUCK166kkFL1IQ -ydUrqtkRXtTpHPIym+MyGweqcEyhavAaNIRd+8pky8qUnHoC0yK4L8S5+aDakHn6 -MOXQLwGlJWCWMXIKizW0SqBXBMUOxgG+tZ/E8IgiGK8oaL7UnAKyeDk30jB/SNFt -Vxyd3FoWM/tj+9P2Tl3tE+8tZEY8GFKDLj6cYLCvFPBvFZxDmDwkIlQgPxvSZpfH -SEfEIVLDbXJ8H6tdWs3it/BTUsNp0czIoUWYZ3Lq8NVwy2pCoF/FwHr43uvnaaQV -kmbZc1LetR/ICdNRl9dhGdt0APtn2zeVOHM56uTmQtgbOUADAoIBAQDAAoyTIYJ8 -yP6KvfmAJYjq7vE4SnWjYgeqrLp/uigvf3tJBkMF1GbWt7LEPtndR094Ojfrlo9E -YqKx7HA4oFeieSSmG8p5ry8DUryPwF+bZtoaXbtCttDUm9Bk7pmPaJqj45wed+xx -g3+h55OQyhVVeF5m0wanI7LjOZbSs2tuOL0bnXZ3Kc5kQ3i1iNlEUQQx3Ax1OPRr -mG46zZGt2tV4UYbZwlt6Wf+ieUwF+K8niy3wkcbSj0ujOokBbrTG0uUhZJ0mNahJ -GLjUoDrJiuMGgq0gWmEfHPwOtJnKnjRpzN3Pg5vFT40NNEBmeH0SQTMtjECiM6pw -XhJZvhddcVoLAoIBAQCWHOFVucjjcRQqTFRjATL5fg9EHg2P16GoKar3r5q3f1/c -2urrjN0BxbzBamFIQInfbEsYyYOIW2uH1Gg8wQoeXD/a4p75C+yQ7VIwrYgFzHIG -AjTCn9nMOLt2tjxJU0WGcV1EFNAGXcV7Nt/Tsnq1+4ZAaK66ebU22rGP6a+QHPkJ -7Ki1KiK1cN6W4SJ8zb6V900enL40CtRcIggqJWWkb0YoAIWJypC39M+7rT6ftYzV -jdDRTupfMt+alSA0r5Bo0QJ++0vTA9vClreeoNEvLq3awgbrSmBeWhp4GCZYnyjX -Ao0IEQVthEQKjSdYEQk0NdE2eqKyN3vLg0M2/IBLAoIBAQCVrKcGPjXorgl1/7cR -RLEhadeRqMFg9hbB3dmXnXab6AUjnIT8c2Ei+ZmZzuCn1GHHd6KFvBy8EyN+DLBC -BAOsPEDsV3eTOZMtRP6GFFCy5A8aRHyn+bh6M2fUdgYwOztojBHz0kn3BqmkfM+U -K2NL+AOmpdNm0LYu2IKQhpo1FnebojhplaY0lD/xStcKoRjjAQSZECfAA9fRcNOh -sZd8LhkvgIksrCFQ9rrjYMKw0ZajFA/nrRoqCIQqILE818MTL8osseEX8fSKksig -uOpV9eGc+bcBuOdnUzMbNg3nd3Jkw9PWkDeE1nlpJ31fkIGjfNQZXKr84uftGvg9 -uBGXAoIBAEy05wtseYHh/C7KdAj/zqR1r3nOLV7omqwR+hLxXuW3thaMr7AlsubF -t2VRUvFv1Es1/jbMk1NzZMbsjBwDkEePItUOYt8asruRRM/dPfwwTEyfkJQiC93X -y+tLpJv+hWvVO4JHeNULA8GTwiF0nwdwaPFntlDHO17iISjhtUdW+Wt/KYflQLAr -F28aXl+9H+maXQ0UX34t5OeJCrtSWw8TcwQpuwFrcg6CS4UF1n7s5h22KfC5mgIO -NVK4wb0a6b7ajWIhJuqDljywr20ny599WzTRB55CDsWAfoxWVQxqWdEDzTgL4yU0 -Nq3cDXPDkMfIHnGoTpP3wyRQxsPOLy8= ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.crt b/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.crt deleted file mode 100644 index ef0727252a..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.crt +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIERDCCAiygAwIBAgICEAMwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UEAwwKMC4w -LjAuMC5jYTAeFw0yNjAzMDkwOTA1NTFaFw00NjAzMDkwOTA1NTFaMCAxHjAcBgNV -BAMMFTAuMC4wLjAuY2xpZW50cmV2b2tlZDCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAOVm8Z+wk+s5oB3Yuegve9H5YG+LDkTSqQsUMMx4XWpbVr7qGR+o -SxctNh4+6FNkPqsR4X7T/Wlh9EFAeQQwWJh2L4J5+Z3flnZXjt6MLS+ZlqJZwDhe -pFhSbjWmRuM/k55ikbMYhL6U4YgvMryjgrpLifu7QyNBdcM0JIPTWw8gflVFSoI4 -h+a+oEza1Z7e8wiFbe13HHjtZAcyy/4Ph+HSrMoPcIsY0xBFXiMCgkQMgO7HbmQC -eApJbLdvHVkIxk2WLt8oTgIf7EimW757QMjUIUnZ1EjHKTz3yJlNfbEdCKQVU7bK -i3wWa23zITG7xdel9EbXDuXPa4wtRX4hgd0CAwEAAaOBkjCBjzAaBgNVHREEEzAR -gglsb2NhbGhvc3SHBH8AAAEwMQYIKwYBBQUHAQEEJTAjMCEGCCsGAQUFBzABhhVo -dHRwOi8vMTI3LjAuMC4xOjI1NjAwHQYDVR0OBBYEFEMKaEm7poVdDX4c7LzcvpKa -+4zSMB8GA1UdIwQYMBaAFBEL3EI+jE/dGBOgOgrzwHWA55vPMA0GCSqGSIb3DQEB -CwUAA4ICAQA6pY1k96L/MF2sSnempg/XLH+T3UxIrSkQ0XprWKJnltQekEt8PRdr -PwA8v7fcLhpYV6PGNEucY/SwJf3sbtl/MdmmPJ/rjxI4DRA4T3XzDIYuLKi6nymu -1Usx4VdhITx6l/oLvTbgvbphjY+oUVSlwOEDHoHGWKY3WXph8et75sZDGV4KEBvD -Vds/vCznI2FfA9NoEpfzc5s+LYdw/iwJHC2z6QR8M8yetF+r8/95Tp+On6cXDHpF -Lj05HhRjgLjvQVIHL0g3W9X8krV+zJTbn9OijqJTjGIvFYuhgjdqQtL3mjMbhahP -6uaDIBotrqK+4oAu54E1u+LagacmU3p4PyBn7hnCJwo7HagqtMb4/zuTVewxVVlF -XQIlb+C0flHtynRKKRdMq0uNBaK7EwLIFNC8Cnp9UcLEH+bym3EtuKP2iepfTfCo -4C9c4PrweNS/VZdlpM3SdX6igz1nPlf+WonUJC2XyyR+Q75Veyp3VtatdFNYrDNT -QNKQVT+UYt+adogctUt+Ko8Rqoe08OtbWHjNvnrg8p3Rm1iPBI32RjkwCTPpen2r -G0dBGq1PRweeFVsHtoo2oo3LhPBlRnDX+irzrObOB3UFFFYPyYIXrviqsYDLpHKI -5eWrtjVvDABsDsOv9Cqv7vtyUOQhvcDUrcEQ/Wjr8kEp7+01EGbzxw== ------END CERTIFICATE----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.csr b/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.csr deleted file mode 100644 index f355e61383..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.csr +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICkjCCAXoCAQAwIDEeMBwGA1UEAwwVMC4wLjAuMC5jbGllbnRyZXZva2VkMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Wbxn7CT6zmgHdi56C970flg -b4sORNKpCxQwzHhdaltWvuoZH6hLFy02Hj7oU2Q+qxHhftP9aWH0QUB5BDBYmHYv -gnn5nd+WdleO3owtL5mWolnAOF6kWFJuNaZG4z+TnmKRsxiEvpThiC8yvKOCukuJ -+7tDI0F1wzQkg9NbDyB+VUVKgjiH5r6gTNrVnt7zCIVt7XcceO1kBzLL/g+H4dKs -yg9wixjTEEVeIwKCRAyA7sduZAJ4Cklst28dWQjGTZYu3yhOAh/sSKZbvntAyNQh -SdnUSMcpPPfImU19sR0IpBVTtsqLfBZrbfMhMbvF16X0RtcO5c9rjC1FfiGB3QID -AQABoC0wKwYJKoZIhvcNAQkOMR4wHDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A -AAEwDQYJKoZIhvcNAQELBQADggEBAFfiu3aa4AfqaRhUGM2RwW8fTd24TiFlmus+ -buNtOmIJ4qDXkj8obCt/aJla3Dm0hVeYiv5dw/dpkwbpkj+AkbBoHgper0VeUFW2 -0u87uRuOjk4TSQuXk4mlPzkqdkl8mKsZNCw1MVGLAjal6MI8KSbPaWFUlm4t092I -AIoy1uMtk5josWPCT2MR/AUmZxNJo0GtJHOdhb+QK63NWBE4swxVDZLTHJTvhQvr -Bo/O+mfLAYBEKrZbgBjHYWv0nJqaBLEiU6FZx60Mdx2CAY7yAe62vj9IkRg8Bi7v -Yo5xxRTROJxBhxujT6dc5GIWuwecQh4AErZyS4F1DJJyye9kgIQ= ------END CERTIFICATE REQUEST----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.key b/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.key deleted file mode 100644 index db7b34eded..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDlZvGfsJPrOaAd -2LnoL3vR+WBviw5E0qkLFDDMeF1qW1a+6hkfqEsXLTYePuhTZD6rEeF+0/1pYfRB -QHkEMFiYdi+Cefmd35Z2V47ejC0vmZaiWcA4XqRYUm41pkbjP5OeYpGzGIS+lOGI -LzK8o4K6S4n7u0MjQXXDNCSD01sPIH5VRUqCOIfmvqBM2tWe3vMIhW3tdxx47WQH -Msv+D4fh0qzKD3CLGNMQRV4jAoJEDIDux25kAngKSWy3bx1ZCMZNli7fKE4CH+xI -plu+e0DI1CFJ2dRIxyk898iZTX2xHQikFVO2yot8Fmtt8yExu8XXpfRG1w7lz2uM -LUV+IYHdAgMBAAECggEAHOU1pDa6uxNcHsZQzHVGtHEj+4jZeEPMtS+K4gZ7Rc1R -hOS522n4Y2f3fCHSY8apuiSbcb1EG0USXjG/zI5GapXgcB/rGD60lh17Sn7/phJD -jhmeA7uwGcvwQ+O45CD6+CVUUunQtqzMJ7pCOs6hxdOEAuhYkZVKPxdPTieOPb5N -9gWLziiJjUPrbqKybA8lBSRZP1PKYlL/y4LVwRGR9ubdWnlheZvs3E4bOKe0s6hC -VFKQLBprKcOzUz4kgZP09l3cxLX97vwZ4DR9RjFOaKbWEVu1pTrY3RBKoLpjNo9O -cnbfhhoL1ccPptayPA/k1DTYIgN4ZtG8pLONkNmJgQKBgQD6hJvvFsPvn516bkHb -Kvx2C93UJ6oiHiTtDlIDrOClPe9OtyPL0tk2qTGAb8ZNxLaot4nHVwcnDz4D9KyY -0nR9I1nYdFo8bjrbgOZvWDYnapyNbFRQhE30HjwTsYteFJcrpRAqOHKPh5FdVA2A -EIfJKu5szHfabGvMF+ML5+oSYQKBgQDqbAtjzVEwTWkCzy5MBT/EtSlnHCKz5JYZ -LXQ75maE2MSgjVTGJoNTVReKQWzZ3tob7UgefIMDsbzSwoc7QRFudjglllpAb9PL -5OuSoYvBc6NxvyMddl8H6HVR8mz/q9OT8K7Tj7k1DYSrbPdNv0u21WoFW67Mc91k -BcozfvxY/QKBgGTeiDrYm8QcLLhjNLkesdCGLqeB4QsjwBKDNZqtPTbLnVPAgfVL -zJIpxfjFCL+/eRyt6hgNfp/Hj1zgY66U7rqERWTWVI8Ig0JmwGIMzqFIWAjJmZlh -7FFi6yWy0z7qY4P7Tmrxl48bMvi1I0pxXXsughnkqk2UYkMnSxA8LQ+BAoGAIArQ -q72ewVlzZxzmmSGGi96vYBaqb6hpE9vwrVda4AG5LrDiy4rsHHHfG9jBhi495d9T -IkemqNScmLpaPbExLsYd+pVIlNI0QwRxZsOKFNE3oA6eQCKz7qLJln/qgi/8ZZZa -bzSP/j5wyfXE/+2L7dvR+BWGGRAlTMy1zMAowgkCgYEAgmA/FesKg6tQny2BZLCX -paYC4dHN4jLPW+kJkgDjuGxS1G2+ybTnrOQbbV2LdpSELwIr9Wtb57avEv5QJGm9 -+MsiAMtQW0UcSyP01F8gazqZ2AIiwSdAYIbYmmhQIAFJCvIikFv7GEeuXWQdW2tJ -8FXgpq8mECwYMYRkIkBiRNY= ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.pem b/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.pem deleted file mode 100644 index db7b34eded..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/client-revoked.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDlZvGfsJPrOaAd -2LnoL3vR+WBviw5E0qkLFDDMeF1qW1a+6hkfqEsXLTYePuhTZD6rEeF+0/1pYfRB -QHkEMFiYdi+Cefmd35Z2V47ejC0vmZaiWcA4XqRYUm41pkbjP5OeYpGzGIS+lOGI -LzK8o4K6S4n7u0MjQXXDNCSD01sPIH5VRUqCOIfmvqBM2tWe3vMIhW3tdxx47WQH -Msv+D4fh0qzKD3CLGNMQRV4jAoJEDIDux25kAngKSWy3bx1ZCMZNli7fKE4CH+xI -plu+e0DI1CFJ2dRIxyk898iZTX2xHQikFVO2yot8Fmtt8yExu8XXpfRG1w7lz2uM -LUV+IYHdAgMBAAECggEAHOU1pDa6uxNcHsZQzHVGtHEj+4jZeEPMtS+K4gZ7Rc1R -hOS522n4Y2f3fCHSY8apuiSbcb1EG0USXjG/zI5GapXgcB/rGD60lh17Sn7/phJD -jhmeA7uwGcvwQ+O45CD6+CVUUunQtqzMJ7pCOs6hxdOEAuhYkZVKPxdPTieOPb5N -9gWLziiJjUPrbqKybA8lBSRZP1PKYlL/y4LVwRGR9ubdWnlheZvs3E4bOKe0s6hC -VFKQLBprKcOzUz4kgZP09l3cxLX97vwZ4DR9RjFOaKbWEVu1pTrY3RBKoLpjNo9O -cnbfhhoL1ccPptayPA/k1DTYIgN4ZtG8pLONkNmJgQKBgQD6hJvvFsPvn516bkHb -Kvx2C93UJ6oiHiTtDlIDrOClPe9OtyPL0tk2qTGAb8ZNxLaot4nHVwcnDz4D9KyY -0nR9I1nYdFo8bjrbgOZvWDYnapyNbFRQhE30HjwTsYteFJcrpRAqOHKPh5FdVA2A -EIfJKu5szHfabGvMF+ML5+oSYQKBgQDqbAtjzVEwTWkCzy5MBT/EtSlnHCKz5JYZ -LXQ75maE2MSgjVTGJoNTVReKQWzZ3tob7UgefIMDsbzSwoc7QRFudjglllpAb9PL -5OuSoYvBc6NxvyMddl8H6HVR8mz/q9OT8K7Tj7k1DYSrbPdNv0u21WoFW67Mc91k -BcozfvxY/QKBgGTeiDrYm8QcLLhjNLkesdCGLqeB4QsjwBKDNZqtPTbLnVPAgfVL -zJIpxfjFCL+/eRyt6hgNfp/Hj1zgY66U7rqERWTWVI8Ig0JmwGIMzqFIWAjJmZlh -7FFi6yWy0z7qY4P7Tmrxl48bMvi1I0pxXXsughnkqk2UYkMnSxA8LQ+BAoGAIArQ -q72ewVlzZxzmmSGGi96vYBaqb6hpE9vwrVda4AG5LrDiy4rsHHHfG9jBhi495d9T -IkemqNScmLpaPbExLsYd+pVIlNI0QwRxZsOKFNE3oA6eQCKz7qLJln/qgi/8ZZZa -bzSP/j5wyfXE/+2L7dvR+BWGGRAlTMy1zMAowgkCgYEAgmA/FesKg6tQny2BZLCX -paYC4dHN4jLPW+kJkgDjuGxS1G2+ybTnrOQbbV2LdpSELwIr9Wtb57avEv5QJGm9 -+MsiAMtQW0UcSyP01F8gazqZ2AIiwSdAYIbYmmhQIAFJCvIikFv7GEeuXWQdW2tJ -8FXgpq8mECwYMYRkIkBiRNY= ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/client.crt b/canton/base/daml-tls/src/test/resources/test-certificates/client.crt deleted file mode 100644 index 0099588a3d..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/client.crt +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFPTCCAyWgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UEAwwKMC4w -LjAuMC5jYTAeFw0yNjAzMDkwOTA1NTBaFw00NjAzMDkwOTA1NTBaMBkxFzAVBgNV -BAMMDjAuMC4wLjAuY2xpZW50MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC -AgEAoA/N31uYGYzWbUU1r+mwAt5l1BxLatW02co0Ky2VQfadolvYJVuLsQmkVufa -Fww5OwiQGgvGvDHoa38i+ve3bJTgvt60B6fZT7tTzaMPx27Y9MPezZfc1efVWzC2 -MNGE4RUaxMud8iEKPzKgOO0ZA/QuWnfVM+IqjzaC8U/5KE+QiqdyeYb6jd+GypRB -41FQcJQ6YHGpYQZIHlsIliGOrWb+Ny5T61uhdC6GIQD+x2BM2Oqzfw8RWp3kDdku -hAJ3fuQ0aPU7T5rARxtFwLG1HclSInTblinQW76Cksugyma6bwhgmsAAwArahSM8 -CfiL+yxoT7YAEDFmD6O4xiUDl1EApba9WPJ/vqgIyuEAAxXiciSko8TApKgNttjV -ZixWp4Xxlu3Smz46bmk8MHdHYip/z7nAHX9ovWFvyw+dgEeJf6/N2yHRMAk460Gg -Q9LREuxd35g0Yq6gbvwRwVY1Flt5LcFfDPlsk/VxA9rFsKBMbnj4Q6QIyGX8axAI -GID4QFsEbiXRb8mhVE8zQ3o0M1BHyhieDhKhkpQ4Rf+NsuPWGY3wjU+cfYgNUQ5O -JjGJSKi73UR9m3hBcOBhVvBCupdivETR8OM3cv1HlEFrLqMQvc3esjc5+R2/xNTF -0YNljd0vE0FaDbVAi1AkaHonvhL9joAOkqoQEUPtIqKOYOsCAwEAAaOBkjCBjzAa -BgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwMQYIKwYBBQUHAQEEJTAjMCEGCCsG -AQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjI1NjAwHQYDVR0OBBYEFHG3iWviVDhy -ioqNyoeSVcp1L0OlMB8GA1UdIwQYMBaAFBEL3EI+jE/dGBOgOgrzwHWA55vPMA0G -CSqGSIb3DQEBCwUAA4ICAQBbIoHXS8dXvlOkSXUXkO3XiDiVCk+ZJmUkSD0rBbfX -WaFCX7aB/ktvhWgJ2kPMGEIahB6XfcY95weZNn8UDNsQv5YkraGOc7wHM31kYd0q -5rvN8YpJN3Kc2Cpe8KI3gnaSQJTYPO5CUvF6ZvwpiyOLlyviz+Mt6Dg+WjocUZ9p -KtNn/gNBROFFeCkKgiY/IAZQh0ik/nl0ctoJUKndhoy5opTwtwXT/T3hCkaT5/7J -J95jSJmif+5fxDvuk0suZa9vXN60SlvQ7DXXgiINv0s1kqji5I6vrfcRT7hPKmsi -GXvZTbJ7JvNM0+tLiNgHfLpVHab/2RpqS0UfRjIMqmbjJ1SQwLw238KObFrUuir0 -Z6vnzQYs5v1DFp1i2iQsJeSrZn2Q6upNauBiQeX0B1rUrHPH3VNMsh+bF5pksLc3 -B21qiwlzo21jxiEo5Mejrx5JlcQihwgkPWaK06mNBpccrUWDQj4ypDnn99S1/Irk -A1Ndobt/BPxVuGS9m8UQLxLbMnZutCdXryLiFG6CTXB9Ox8ZJDTy28GhPxQ/fmWi -AQaTJjQM4hpmUxMMO9r3PrQXM4WpD+z+ulMhJzGbocLhwwGNL6E1390SEzva0Qvb -yEATyTWWl0894P4rfgDHP/1GBBEULmG6EOOzYhd/ryGzN+FXq6+VJzGDi4WuGWKu -gw== ------END CERTIFICATE----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/client.csr b/canton/base/daml-tls/src/test/resources/test-certificates/client.csr deleted file mode 100644 index 73cebc34d0..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/client.csr +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIEizCCAnMCAQAwGTEXMBUGA1UEAwwOMC4wLjAuMC5jbGllbnQwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgD83fW5gZjNZtRTWv6bAC3mXUHEtq1bTZ -yjQrLZVB9p2iW9glW4uxCaRW59oXDDk7CJAaC8a8MehrfyL697dslOC+3rQHp9lP -u1PNow/Hbtj0w97Nl9zV59VbMLYw0YThFRrEy53yIQo/MqA47RkD9C5ad9Uz4iqP -NoLxT/koT5CKp3J5hvqN34bKlEHjUVBwlDpgcalhBkgeWwiWIY6tZv43LlPrW6F0 -LoYhAP7HYEzY6rN/DxFaneQN2S6EAnd+5DRo9TtPmsBHG0XAsbUdyVIidNuWKdBb -voKSy6DKZrpvCGCawADACtqFIzwJ+Iv7LGhPtgAQMWYPo7jGJQOXUQCltr1Y8n++ -qAjK4QADFeJyJKSjxMCkqA222NVmLFanhfGW7dKbPjpuaTwwd0diKn/PucAdf2i9 -YW/LD52AR4l/r83bIdEwCTjrQaBD0tES7F3fmDRirqBu/BHBVjUWW3ktwV8M+WyT -9XED2sWwoExuePhDpAjIZfxrEAgYgPhAWwRuJdFvyaFUTzNDejQzUEfKGJ4OEqGS -lDhF/42y49YZjfCNT5x9iA1RDk4mMYlIqLvdRH2beEFw4GFW8EK6l2K8RNHw4zdy -/UeUQWsuoxC9zd6yNzn5Hb/E1MXRg2WN3S8TQVoNtUCLUCRoeie+Ev2OgA6SqhAR -Q+0ioo5g6wIDAQABoC0wKwYJKoZIhvcNAQkOMR4wHDAaBgNVHREEEzARgglsb2Nh -bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBACsEMFTNYGsXiq1rmdcLL92V -od1UbIzmO/BM8/Y0bSPHYGyTZqOGZ0TFKYt/PEKnaxs92R8qH/Razq+HNnsouQ8/ -LE/1mvUCW/xpfkQKzbGxOrjJwzyZRvech7LbaL9nPhxSoHxlff1fN+m3LlgAqjD7 -rGefpWwo/ReP3NvnXnMS0VBFYIy4DQ8j+LnGdedog9V6fNYJFfMbVJ9Xgr5+457G -fvFkAvdoJL7Lcg1En8cj1oFdw7FKWvHZF9FAEe2ks42FTNd4VT8TLUlWw2OZeZ+U -uQQlCEm57MIX1lW70COHHFiv1BVKi8waTsM+5ntCTQNgqf2bGDmItoMC8s1Q9n67 -80bas9SallUfbuBHF/SF320cJLBvCTGiCahqk2GGMaCr0QQF5A/RzH8qq/ynCzN1 -yv8HLLw5BiBCGuWge6CkYZK4A14zO6+NZyPBIfzmAGQX3XqtGbD5rv5BefXyHL4I -T1ADYAaBwMmrgIgZ4QnHhJx4mEqC8ipp+RNad8EL4DAbw71SurEeVKtmG4OANdk8 -df/ss6NGAqjSHJJnPDuJmFucsUwYfYSA/gmlTKeo+FlaxYzcmq0QqV2NFugz7yYv -iK5LVSeucVVzOaoUzXAZ6+PLRCc7Pb+bjA/MnV/9RCG1rHilgND5NY3Yo8I56uEq -jk4mnBygvdOPP9+tHGb8 ------END CERTIFICATE REQUEST----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/client.key b/canton/base/daml-tls/src/test/resources/test-certificates/client.key deleted file mode 100644 index e35e74dde5..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/client.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCgD83fW5gZjNZt -RTWv6bAC3mXUHEtq1bTZyjQrLZVB9p2iW9glW4uxCaRW59oXDDk7CJAaC8a8Mehr -fyL697dslOC+3rQHp9lPu1PNow/Hbtj0w97Nl9zV59VbMLYw0YThFRrEy53yIQo/ -MqA47RkD9C5ad9Uz4iqPNoLxT/koT5CKp3J5hvqN34bKlEHjUVBwlDpgcalhBkge -WwiWIY6tZv43LlPrW6F0LoYhAP7HYEzY6rN/DxFaneQN2S6EAnd+5DRo9TtPmsBH -G0XAsbUdyVIidNuWKdBbvoKSy6DKZrpvCGCawADACtqFIzwJ+Iv7LGhPtgAQMWYP -o7jGJQOXUQCltr1Y8n++qAjK4QADFeJyJKSjxMCkqA222NVmLFanhfGW7dKbPjpu -aTwwd0diKn/PucAdf2i9YW/LD52AR4l/r83bIdEwCTjrQaBD0tES7F3fmDRirqBu -/BHBVjUWW3ktwV8M+WyT9XED2sWwoExuePhDpAjIZfxrEAgYgPhAWwRuJdFvyaFU -TzNDejQzUEfKGJ4OEqGSlDhF/42y49YZjfCNT5x9iA1RDk4mMYlIqLvdRH2beEFw -4GFW8EK6l2K8RNHw4zdy/UeUQWsuoxC9zd6yNzn5Hb/E1MXRg2WN3S8TQVoNtUCL -UCRoeie+Ev2OgA6SqhARQ+0ioo5g6wIDAQABAoICADrYpKFmHNzj5R/GTq8ulMnx -NvHNN0L/b96UUyq+tv3J6/l5l9VgO1LqzxMuaYWItm/IPaZ9xuKA3MjMtLEdKsAO -WjSCTyDzaldAcVaMiSwSykvuyTbACu5Pria5GXqIRTi1WCKbE6Yl1I0YkURskwJb -n0EehnFtSKVfbrPGab2SxvrbBiC86dgIIpfO+wxWZGMHdBDqKNIli5Uxdc4wZzLE -R1lFPJAun4TF6AUxRvpJIuerHjPr977fhq06uDnLaGBsCQK/ij9Bj0h1M+dczlA1 -qG9HlEW4eMSKHZpXp/VR4GAa7d1nEr+IHf/NzPCDgv9ku7L2oD5T0wiT8kZS7Gb7 -31WDgsmOjsYfZah5iJncR6S7dnw55efk6as0e7JIXefu2VqpRkM6RY9qoCM5h3IU -PqL/q+zcdGRQVxsgbBi1F/QLUyHSaniLJTVn2zSabzzYJXtuLRV5OQUC/FylsYpt -df60yUSLn5t13w+GziiWGzzVZqOqzSumlSwgalo9fMf7iEbnUiTeg3/IWD0QIVdK -D8e9RZDsaP1cNzMZ9LeHRsaKxV97tpTKbDRUqrZCPL003UxeNGGbGcksHP8rQUqY -Nc6gPfmuaREh6dl5oHLsifThtgt88AyYYtoCQbH/Neo8QtYGNT6WIHfs2q5EzSmG -sGIxDpdqXOAw4ZOJiAqFAoIBAQDTyhaagSGYhxeT3cutus7jzxd4liRyFlwpW6Wi -VAuSujkZtWsmlOnOzJFcdeySgDnGnHB2IVbGXM7MptIYXmzRZ5jDwHej9+a8EeUE -bB0IB3ciWF9942okkBmgtaNDcKfX4XyuO99luV2gjSJIMKEOcP1k5k4oVKKUcBXT -/tPgnv7I7F6+4uKkaBuYzmIQAHTQPelDbngf66u4ZFRbv2VrFOHbafTNg+aIuQk7 -riTmNKh4T9iaGNmwitA+M8E/3WAQt+Z7f5z2CZOLrtsCxbBfsxq42WQZOVLTMPec -4QaySZsaZVKIVSoxqHsNU5vfF6uVfks6w+bQ4EqFNbTX3olnAoIBAQDBeWq/mtBN -iAsgLST66ljF2dzz4NBau7s6pJKMIGTpKDwoyRqeKS/ps7QsmGOG/v7UQmidtCMb -ZeJdkDcL4KANcbrxZQwYtpSsNcrXG8iuY5i5oRQ4zdklHgrS2RiQUaZ9rj8WrP6Y -WMcw1wKfX2KkeqUTKXnsIHPUJHvOmcT53NWZCjTArImJXohlIpfa8Bt5SSgTtw+W -M3lLExbXT8mg1BFk9GwCrrEtStO7XedJA65Z18HBdRrzAAa2cBUhl+GwmA1ZM7U4 -In5A2EArcWC8o4ydN0WIUygGxEL3hWDqMgGJxOkBnZ9z5om4fAh0oNDuaH1eLWZx -MWgcuOFJr0XdAoIBAEC1pmJewh0QaavMQMc8JKqGQ+2cMlaJbm9UiS8n7sFOFNQ+ -EhcfuLBu4JsXjMjWVNDSS6pMVW8WPDlbXyYC88ynko186RE4VU9xSEApqFUjssrT -pZmZOn0hajMoTFH2wdG8G5RAxukV0f2UIb1c3ljDPSjvFUJNWb9w37p+Sc5/xeku -D2TTfdwt0wDFTkEFYqudvv/TaPCX2Ufln8DIofwtzTR1E08X1gBASQnrE2si+7aG -7u9mWF0QjL/0oD8bLJbvGYTyYMrr8iDjtBcMv+79klSYV3U9hojwlTdhqeIfizxt -aOIu40145OAVg7KOUObER8gUwxle7mX1GuQ6IlECggEBAIOKl6d6DJIKIvZXe0MV -vhGW1IEDouVtqhhu+CxJghJ8Bpr8UBqkiizsv11qppCMIpoSJKrEZPIlzUgr2yE6 -MdeyMZYaS6Upzd9gG28xNrpKgPYErIkKe86epGFCtU66FOZyZ/Z03Aycn6F3LhNJ -ePm3YTCsDHccgWQFAiAvsBrpqFgK9l7hcAlzumx4nqJ+iU9nC9aLqg9HI/qFGgx9 -9OpiBQmMnTsdptzSeS504+FinVH8PYHYqt/4bpfvkI5sg0fbuYeifl2SJiDUZpyi -01CuKdZiU+YiE562JYnAp2nTYfRXasxsbJMSdSR3QBB7nlHZvRMg7vuaydxtXs8I -WgkCggEAN2ot7s0EhiTGR4MgFEK4Wfk73wcELadGXdj+3HhN2fcbsp61nPC7P4xX -Gjtp5VOJpuiQFGTsbc40Ile9lhrxgElcF3wgTgcUkQfZgFcNjIiEqC5xLxmlhtCJ -cr8K0EnRULxIi7DyMGvt5WSgmHRLjk2HHxwyK3Jq1pMRe4OnvP1jrv7hq0vOmrY3 -IWpJbG6pD9Ne7DPge1D/JWIiAgJ33dV6aTvanKEJl6H2hMQqZbs4w4zWUFIctEf5 -nRpiuARfrA0W9zyLOqCoHDOtpf+8RY3PNTgenjtdP1eD3sbY/jLkPDQcfzm5gWwo -K/VKL1G/nMN+f5AYYVYQEbpGspWxqA== ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/client.pem b/canton/base/daml-tls/src/test/resources/test-certificates/client.pem deleted file mode 100644 index e35e74dde5..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/client.pem +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCgD83fW5gZjNZt -RTWv6bAC3mXUHEtq1bTZyjQrLZVB9p2iW9glW4uxCaRW59oXDDk7CJAaC8a8Mehr -fyL697dslOC+3rQHp9lPu1PNow/Hbtj0w97Nl9zV59VbMLYw0YThFRrEy53yIQo/ -MqA47RkD9C5ad9Uz4iqPNoLxT/koT5CKp3J5hvqN34bKlEHjUVBwlDpgcalhBkge -WwiWIY6tZv43LlPrW6F0LoYhAP7HYEzY6rN/DxFaneQN2S6EAnd+5DRo9TtPmsBH -G0XAsbUdyVIidNuWKdBbvoKSy6DKZrpvCGCawADACtqFIzwJ+Iv7LGhPtgAQMWYP -o7jGJQOXUQCltr1Y8n++qAjK4QADFeJyJKSjxMCkqA222NVmLFanhfGW7dKbPjpu -aTwwd0diKn/PucAdf2i9YW/LD52AR4l/r83bIdEwCTjrQaBD0tES7F3fmDRirqBu -/BHBVjUWW3ktwV8M+WyT9XED2sWwoExuePhDpAjIZfxrEAgYgPhAWwRuJdFvyaFU -TzNDejQzUEfKGJ4OEqGSlDhF/42y49YZjfCNT5x9iA1RDk4mMYlIqLvdRH2beEFw -4GFW8EK6l2K8RNHw4zdy/UeUQWsuoxC9zd6yNzn5Hb/E1MXRg2WN3S8TQVoNtUCL -UCRoeie+Ev2OgA6SqhARQ+0ioo5g6wIDAQABAoICADrYpKFmHNzj5R/GTq8ulMnx -NvHNN0L/b96UUyq+tv3J6/l5l9VgO1LqzxMuaYWItm/IPaZ9xuKA3MjMtLEdKsAO -WjSCTyDzaldAcVaMiSwSykvuyTbACu5Pria5GXqIRTi1WCKbE6Yl1I0YkURskwJb -n0EehnFtSKVfbrPGab2SxvrbBiC86dgIIpfO+wxWZGMHdBDqKNIli5Uxdc4wZzLE -R1lFPJAun4TF6AUxRvpJIuerHjPr977fhq06uDnLaGBsCQK/ij9Bj0h1M+dczlA1 -qG9HlEW4eMSKHZpXp/VR4GAa7d1nEr+IHf/NzPCDgv9ku7L2oD5T0wiT8kZS7Gb7 -31WDgsmOjsYfZah5iJncR6S7dnw55efk6as0e7JIXefu2VqpRkM6RY9qoCM5h3IU -PqL/q+zcdGRQVxsgbBi1F/QLUyHSaniLJTVn2zSabzzYJXtuLRV5OQUC/FylsYpt -df60yUSLn5t13w+GziiWGzzVZqOqzSumlSwgalo9fMf7iEbnUiTeg3/IWD0QIVdK -D8e9RZDsaP1cNzMZ9LeHRsaKxV97tpTKbDRUqrZCPL003UxeNGGbGcksHP8rQUqY -Nc6gPfmuaREh6dl5oHLsifThtgt88AyYYtoCQbH/Neo8QtYGNT6WIHfs2q5EzSmG -sGIxDpdqXOAw4ZOJiAqFAoIBAQDTyhaagSGYhxeT3cutus7jzxd4liRyFlwpW6Wi -VAuSujkZtWsmlOnOzJFcdeySgDnGnHB2IVbGXM7MptIYXmzRZ5jDwHej9+a8EeUE -bB0IB3ciWF9942okkBmgtaNDcKfX4XyuO99luV2gjSJIMKEOcP1k5k4oVKKUcBXT -/tPgnv7I7F6+4uKkaBuYzmIQAHTQPelDbngf66u4ZFRbv2VrFOHbafTNg+aIuQk7 -riTmNKh4T9iaGNmwitA+M8E/3WAQt+Z7f5z2CZOLrtsCxbBfsxq42WQZOVLTMPec -4QaySZsaZVKIVSoxqHsNU5vfF6uVfks6w+bQ4EqFNbTX3olnAoIBAQDBeWq/mtBN -iAsgLST66ljF2dzz4NBau7s6pJKMIGTpKDwoyRqeKS/ps7QsmGOG/v7UQmidtCMb -ZeJdkDcL4KANcbrxZQwYtpSsNcrXG8iuY5i5oRQ4zdklHgrS2RiQUaZ9rj8WrP6Y -WMcw1wKfX2KkeqUTKXnsIHPUJHvOmcT53NWZCjTArImJXohlIpfa8Bt5SSgTtw+W -M3lLExbXT8mg1BFk9GwCrrEtStO7XedJA65Z18HBdRrzAAa2cBUhl+GwmA1ZM7U4 -In5A2EArcWC8o4ydN0WIUygGxEL3hWDqMgGJxOkBnZ9z5om4fAh0oNDuaH1eLWZx -MWgcuOFJr0XdAoIBAEC1pmJewh0QaavMQMc8JKqGQ+2cMlaJbm9UiS8n7sFOFNQ+ -EhcfuLBu4JsXjMjWVNDSS6pMVW8WPDlbXyYC88ynko186RE4VU9xSEApqFUjssrT -pZmZOn0hajMoTFH2wdG8G5RAxukV0f2UIb1c3ljDPSjvFUJNWb9w37p+Sc5/xeku -D2TTfdwt0wDFTkEFYqudvv/TaPCX2Ufln8DIofwtzTR1E08X1gBASQnrE2si+7aG -7u9mWF0QjL/0oD8bLJbvGYTyYMrr8iDjtBcMv+79klSYV3U9hojwlTdhqeIfizxt -aOIu40145OAVg7KOUObER8gUwxle7mX1GuQ6IlECggEBAIOKl6d6DJIKIvZXe0MV -vhGW1IEDouVtqhhu+CxJghJ8Bpr8UBqkiizsv11qppCMIpoSJKrEZPIlzUgr2yE6 -MdeyMZYaS6Upzd9gG28xNrpKgPYErIkKe86epGFCtU66FOZyZ/Z03Aycn6F3LhNJ -ePm3YTCsDHccgWQFAiAvsBrpqFgK9l7hcAlzumx4nqJ+iU9nC9aLqg9HI/qFGgx9 -9OpiBQmMnTsdptzSeS504+FinVH8PYHYqt/4bpfvkI5sg0fbuYeifl2SJiDUZpyi -01CuKdZiU+YiE562JYnAp2nTYfRXasxsbJMSdSR3QBB7nlHZvRMg7vuaydxtXs8I -WgkCggEAN2ot7s0EhiTGR4MgFEK4Wfk73wcELadGXdj+3HhN2fcbsp61nPC7P4xX -Gjtp5VOJpuiQFGTsbc40Ile9lhrxgElcF3wgTgcUkQfZgFcNjIiEqC5xLxmlhtCJ -cr8K0EnRULxIi7DyMGvt5WSgmHRLjk2HHxwyK3Jq1pMRe4OnvP1jrv7hq0vOmrY3 -IWpJbG6pD9Ne7DPge1D/JWIiAgJ33dV6aTvanKEJl6H2hMQqZbs4w4zWUFIctEf5 -nRpiuARfrA0W9zyLOqCoHDOtpf+8RY3PNTgenjtdP1eD3sbY/jLkPDQcfzm5gWwo -K/VKL1G/nMN+f5AYYVYQEbpGspWxqA== ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/index.txt b/canton/base/daml-tls/src/test/resources/test-certificates/index.txt deleted file mode 100644 index dd9513e956..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/index.txt +++ /dev/null @@ -1,4 +0,0 @@ -V 460309090549Z 1000 unknown /CN=0.0.0.0.server -V 460309090550Z 1001 unknown /CN=0.0.0.0.client -V 460309090551Z 1002 unknown /CN=ocsp.127.0.0.1 -R 460309090551Z 260309090551Z 1003 unknown /CN=0.0.0.0.clientrevoked diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.crt b/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.crt deleted file mode 100644 index 1e05194cb4..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.crt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFHzCCAwegAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UEAwwKMC4w -LjAuMC5jYTAeFw0yNjAzMDkwOTA1NTFaFw00NjAzMDkwOTA1NTFaMBkxFzAVBgNV -BAMMDm9jc3AuMTI3LjAuMC4xMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC -AgEAnNiotR1w2EzZ5Mg+YOWgIzbeazaDn6EJL6nSWvPeMhv06auUAWvsxL0C7k00 -8dZBSmUqZtJ3RD794CKfy9G5XW1UmrnHuZi82agq0T/ZLoedO/1UMB0L32op+17h -GzmoBE1gJ4xBGdFnr+CkBnrBnY/dKiFZXYq6pDbgqen2nnYWd28KwGtp6XbcJg+U -ZBozumFA7RrXau818JQGIjHLe1ObUxWQgQzun1Vmmt1P7/SbtOr+hsbYNronJweA -zTtSwOYRIdX9v97qACb3biAWTO5ohLCoturYMcYu7uWP/sBpotEVAfETgDlHJJ1t -x2nZ/q252eyp9B8iDK1gZ1/zNMSCdLqLf3C+B99HZOsE3tmfx7t/903Bz0sjNoUk -cxyB/HVYrBxkLQEi1jFJ8ioEZil/4cAGmGGzH/+/YIKWUulL0QR2KCrreTB7XPma -qFQ3Ywle8MVNjL4u583usoSmI8dAZlhwtQ/a7PkiVZsYoalsWHlCGHghH2V+tNsu -uyO2Tigj0fHFSRBhgN6aqLUAHl/Or7G1eBGWKeRjFIk4Eq9m7f1hwuDqsTmI0tmq -1M6+csaYqUDFFvTPEXtAaIKu0rWmWaEjpCPL/aRfYYj9Gw9oDuVPiicBSmiqnkDW -zCiHTKNINRh2fMS2kIzClKfONWn5zCCONGTTVVkdn7oqr6kCAwEAAaN1MHMwCQYD -VR0TBAIwADAdBgNVHQ4EFgQUDYUoN4/EBPNk0g0464tU2Y/2eyMwHwYDVR0jBBgw -FoAUEQvcQj6MT90YE6A6CvPAdYDnm88wDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB -/wQMMAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4ICAQBC+oV7E8J9KzyvW3nf -/60KYn/p8qigKKSlrApcIGVrASCr63M8RBOTuLG/CQaexcuZru7OwV1smVjJ4KrK -f42WVW8G471YVRpOTNDiGGg/5M3LJQyt/9SdZFfxXQT6xKI3iP64RuyR4ybtwvoj -0J6RIB6GUVTP6lnIE2lqoP49bzZ0vQ+R/YlNdgDeIQWU2EzhuoThcbMIho8RCcP4 -MoOyAV+4BiM2TMtS2mrddmQWlO7CuulMHN+ukvV622ZXGUhsZbpV2qrNs21AcE48 -1zSLqlnaFWogJHDz235hg4oaAbtdThWM0WCQkbJ5/ihcHKmfZPswr/fsbcoP/PBb -7G+0VDsQ84RusCMW4+cIxIyhp1P2DPAJ3tM/TinTn5rIi7aNt/4iFDFTdmpKW29R -TZwfixOzQAZwj/lPmQPFt02LsUmuzFpz5NBoHyu7c8vMLBfsK6NwnPNrnmmEDq/m -8t5M8UWBa1f4MUppwel4VOgnsNlWPIOItPDnmFZsQW7BW419XJLuIEF64O9rAJTm -W+XJcBz5QuUM9nKx4TUPaYKxz7Jw6hZTRmrc0tStOvZjUzD4xtcQ/HfkFXCRV7KL -s0wjSy4+rjSuauPf7kFLb/urAbvVNkNSvLXOMrYUTwubaDkabQdeHbA3Exu7K65V -PX8TGvc72Mj3rdzVksqip80KBA== ------END CERTIFICATE----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.csr b/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.csr deleted file mode 100644 index 549fca9a75..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.csr +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIEXjCCAkYCAQAwGTEXMBUGA1UEAwwOb2NzcC4xMjcuMC4wLjEwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCc2Ki1HXDYTNnkyD5g5aAjNt5rNoOfoQkv -qdJa894yG/Tpq5QBa+zEvQLuTTTx1kFKZSpm0ndEPv3gIp/L0bldbVSauce5mLzZ -qCrRP9kuh507/VQwHQvfain7XuEbOagETWAnjEEZ0Wev4KQGesGdj90qIVldirqk -NuCp6faedhZ3bwrAa2npdtwmD5RkGjO6YUDtGtdq7zXwlAYiMct7U5tTFZCBDO6f -VWaa3U/v9Ju06v6Gxtg2uicnB4DNO1LA5hEh1f2/3uoAJvduIBZM7miEsKi26tgx -xi7u5Y/+wGmi0RUB8ROAOUcknW3Hadn+rbnZ7Kn0HyIMrWBnX/M0xIJ0uot/cL4H -30dk6wTe2Z/Hu3/3TcHPSyM2hSRzHIH8dVisHGQtASLWMUnyKgRmKX/hwAaYYbMf -/79ggpZS6UvRBHYoKut5MHtc+ZqoVDdjCV7wxU2Mvi7nze6yhKYjx0BmWHC1D9rs -+SJVmxihqWxYeUIYeCEfZX602y67I7ZOKCPR8cVJEGGA3pqotQAeX86vsbV4EZYp -5GMUiTgSr2bt/WHC4OqxOYjS2arUzr5yxpipQMUW9M8Re0Bogq7StaZZoSOkI8v9 -pF9hiP0bD2gO5U+KJwFKaKqeQNbMKIdMo0g1GHZ8xLaQjMKUp841afnMII40ZNNV -WR2fuiqvqQIDAQABoAAwDQYJKoZIhvcNAQELBQADggIBABNVAQIWTUzG8GvYOPvl -9e0PgRDJ3juh1Xw81VqVYoSLZ5m7AxPgEQ/DpSrzhHMue5nyjCwI8ARMmJ81IXB4 -vE2HeXy8Q/dU79eN+tZpy39Ku5p6UMwTyR7Kd2sgQB5HsNjA4ydMS0WpBuN4gTJX -hn/12Fjy/cFfeA+cCyO3PZN8dKI79CVV7Z4X8FiIVFnUxKPxFgGVli8pkYOPnpUs -TeeIOgIGgicnqmegJlSxvsNU69pJbTLqDQT3vntxiSlsvt4ISEnoFYGXx7amO7Is -oEiak+wVMmiHxMDRCcttVxEurG73hDUAsu91spZvXspIgKotq4TsZraTxSK634uf -V+4ZvYiFFNGptJfw+0u8qe9nD4xXt4jDdWZCGCdizk3Ly+7bmqYkdg9LWAS1mRap -Bo0HUVXiOctj5jd7t2t7XWLKhAaZuUH3ckFLCFWEuAmHgkRvzFzl1Z/FUh+AgwHs -JaB8gYHsO6b+JgNhDyrEo8dYygW6GTGKtjMb9pXUCxHkcputxxCnV6MY/Sn2iF3M -KZzJJumtvVN7siuBPmM86xMiK6lKLy+3PVO2GshstMz7+s0ixKUWt9wbTdFHY+aJ -jZjAmg6L6GipGNWO9aUvC2RLlUq5SiFtsItz5gvBlb2c9DCAtF7w2mAeDkr9P433 -mMfsWMlmiifna0CpZS6PIdBf ------END CERTIFICATE REQUEST----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.key.pem b/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.key.pem deleted file mode 100644 index d5504e683f..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/ocsp.key.pem +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCc2Ki1HXDYTNnk -yD5g5aAjNt5rNoOfoQkvqdJa894yG/Tpq5QBa+zEvQLuTTTx1kFKZSpm0ndEPv3g -Ip/L0bldbVSauce5mLzZqCrRP9kuh507/VQwHQvfain7XuEbOagETWAnjEEZ0Wev -4KQGesGdj90qIVldirqkNuCp6faedhZ3bwrAa2npdtwmD5RkGjO6YUDtGtdq7zXw -lAYiMct7U5tTFZCBDO6fVWaa3U/v9Ju06v6Gxtg2uicnB4DNO1LA5hEh1f2/3uoA -JvduIBZM7miEsKi26tgxxi7u5Y/+wGmi0RUB8ROAOUcknW3Hadn+rbnZ7Kn0HyIM -rWBnX/M0xIJ0uot/cL4H30dk6wTe2Z/Hu3/3TcHPSyM2hSRzHIH8dVisHGQtASLW -MUnyKgRmKX/hwAaYYbMf/79ggpZS6UvRBHYoKut5MHtc+ZqoVDdjCV7wxU2Mvi7n -ze6yhKYjx0BmWHC1D9rs+SJVmxihqWxYeUIYeCEfZX602y67I7ZOKCPR8cVJEGGA -3pqotQAeX86vsbV4EZYp5GMUiTgSr2bt/WHC4OqxOYjS2arUzr5yxpipQMUW9M8R -e0Bogq7StaZZoSOkI8v9pF9hiP0bD2gO5U+KJwFKaKqeQNbMKIdMo0g1GHZ8xLaQ -jMKUp841afnMII40ZNNVWR2fuiqvqQIDAQABAoICAADodjT3xBcPkes4llVnWzyk -CC53QnI1BzLHe6XdsX++Ov/zyhP+87FQW+ajhY9y3xCW7bwbqPtWlxmbcRg93/Dl -pQdThkDWoZNr8PAigLBFixwS6uPlzev8Fyu/RoeDZg5GyD/sNhELnzPwgePULM2h -/e/qiyBoKu/rS2qHzTFy0c16hTbqNJfOstuC8B4wF5HiA5EFsqUJ9eKPy+eaFVqd -2qUWJl/oR0xP2oganJmYiw8AlXEKPKv8v4eC5M0ywusU53GkSCxOPbviQsrYY2du -2SXUcCd2pNXcA0uf7rEQEs70FSmNmaK5wO+6N0skwu78uTAnYRxb0o2ud2/+FZAs -5pdjDq6o0vNmsc38ltwW3I3kg4EamwvB7toII/n9FYrlug0oCrhYCyxEGIqyA/4Q -Xvuho7hP3/7EzwLhTtRBCJsvJJu0wJ3m+Sywa6/OBkFRPHM9PjN+b1twcV5j/MJp -xFoN6zaKLMuapQetMxGddu3TDQVLhQ6QfRFY3V5CJC2Q6d6V5RoqG8xu0DQftxx+ -ehxsu5nJVDxvdxFLdaeF4XHQoyLtdA0N55kHnzGrlDnsXHeiprN4xvdddQGhRR8r -bbOsEV09Wlv2JmPnW+65Fsd52XqgPmzSusmif/uypfhOqYeFCC84K1Qb512uFiTe -VP7stZkr1H4Lwl9BidfbAoIBAQDNnthalSZZKJNF1LcBCfGqTSr9sdUnRWMgLR+U -8W8Naiwm8t3ZIGVbjlPrzCSCByo5s2ShscAi6DgD5KthcrPoLAxRqjOpJy/8RKlL -2UMFe7ulmmh9tcPciXyN88t3k4p+c1YRHHHV+tZUmRGCZhur247MxVZm16v6qifp -xCy5azap0Mtv1CtvbX1TwuGNA261pG95YzPsv5l2ydEt6eR0GcR59uxGZvIej9+V -mveFhqJJ70J8e3aQATugm+uwN5h+R8HU6zAWJMrX4+lVcpM7pYJZG4Fvsgggq/lQ -w0d8zuCUMi9gtCPp0Z/pWuOSRbPHJlcNur7WiRnLJJ5/ugbrAoIBAQDDRowHlKeJ -JVqTtC96SjAko1qZn0P+RAVd5+sjrdr9lNrXjcUrSskhMjUsXxbZbXewSurHoLrv -aS8dw504sM0OFvreyvePLD4P9g3EFCEE/mAIPfh2XqNROrE3mRQRdfpSHr75r96J -V70g7fG0fkfea9+C969bIz9An6u9XsutcZ4qccb1Xzwbno6wvflLiTjAq8HlNhI2 -AIdxfT4548Uil7cYU8Asf+05GhtrJdrYF6+0X39JpyV7UZ5zL/1gEAMDD//VEi0+ -ehD0DYH42SFwSnV+qV89Yu+aVpPgRVAwNp9gmjhoyMt4+i/uG2Sj/VAw3aoOMPym -UDuk8S72Ema7AoIBACPrwQ4qCjX/MaM8YvAKha5jpsQFIiCnLYb8ewzxFJ56UtZH -DVyWHT7eYWjLFAGnMMg9v7HDtLYkbUy40PvIJwfDUi+eFUJStz5R3GWEwgGeOQzY -KbL0YxAO8LR3x67PFp26lmmM15IzLOhAiV+HaQQf6hV5z/tNaBz1Rt2+yLVntnf6 -IuAZZ7EipodQf0i8NcYDwPmqy/JrEms/HIzx1Fg3/Cv2aBZwe5G9PqkLFny8JXPX -GX1XsEG+BWWsd4MkYz6EWKdSoubrbIhEp4mm53O/GUNp27Xgmyzh/j8OrlluZqtd -DQvOzItXdKE2oJT4Gk0LNKAQZLBj6R6k+g+ENxsCggEBAJl7vp5Kui/ymzHTAyo0 -W4jAmHp8kx2r6yf3HHtrr992e1fHR30Sh+m+o8qfZr846kU3bTfA3o1EeFMFqHKh -jFqRt1cb4t62oIU9GW7Pf3CEW4i+5KZtFiLHZflUKVDcEQcwwVxRXjFHNBFI1JzQ -UL7CvOgcs8iHeLTbi0oJ0z8dOXsU+JNt6jmvM2o5t1MmpOPTXFBDYqbtXYfXL+yi -WRUqeC/3y+luF0+1dG2QUGVeoKy+DrceWDiIW2kgBB/YFfX3Qlb/tW4Q1XhtkYW9 -bCntbFJE3+XI+JivigVRlYh/tl9geAvsHvmUc83L3BuFA92UrJAA8uPp1id7pjSl -G/cCggEAJkhI/EAGuwyTmwoRviO7xgKR9pVNazb/XkyQ+TmG0JxxdOZ6u6NeuyS5 -cOGZd4j2iZuLoRlTedB7cFwZdTcUhRXXHBk4jJc5ZJR421PXIhABPSWhF3yF6Aav -Ns0K5f4cU94FYF6DrPWkCeIdQb8CG766mYWksSAYskaTFrebvMXp08tGGXxuGOw7 -tG8s6LnnwOSxEE1V0iEGUw+GFw07d81FZyb+V+AUUWvyvi9vvrZzkmiR3X7sqbtr -tIHIvxKtSQf1tBipMNJP/aMS2qqDwGDLHE3yq9dwPYBVxnON7H++egNXjLDIeodR -0eQ1uoTEB9a+nFYybjrKuWkXqzVtDw== ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/server.crt b/canton/base/daml-tls/src/test/resources/test-certificates/server.crt deleted file mode 100644 index 7cc82aff9b..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/server.crt +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFPTCCAyWgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UEAwwKMC4w -LjAuMC5jYTAeFw0yNjAzMDkwOTA1NDlaFw00NjAzMDkwOTA1NDlaMBkxFzAVBgNV -BAMMDjAuMC4wLjAuc2VydmVyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC -AgEAqMiaoKUIV5JGtOWWXv1EzYoscsq9NG9TjtQWU5+tZpziEw1A6Ynk/z5EV7JQ -48s0vHpNvjxvXCU9qfJ98FkA7C7duaOKog3NVxRmMUBeqsWEjPJ/tDujconCK++1 -ta3x0HadZzUuoJo0sY53jKRzjG7baIWzNxSHGMcFl+CBlvPDqGwKdJUBMz5x+E9L -MBV4PmRY1+HAP8qA+CxZ/20yCVwwZoSMKQfm0cpbw+fZl+OwHag9AovfE1hb5ase -62U4fa/1Hi95J7UHEKnod6OKeLtl/GcipFhUa+zJcbVRL8YxnHlkXyccQOlF/LSL -TN4XmdArXQmfs/2DoCXNs1E4cLSavrYfiqXEZAmn1ZEWOPzaGflIQFssnHUl0ujD -QxiORHLL3C3uPskW5q07j7Zc0WzhlLYXi/et651zGObXvAPzoD9bW02Cyjgjotiz -IzNfhwbwitAMnYzUZNlChWMUucW6XKZ1jutXkn1B9wbrysjyFOHy3KDRAY6dxaFY -jSnLMUAO66k8NOFZMZS1a1EFicoCPxsksy76jNR1Fqd/Hukhme/7lQF/6OAk6rex -gLb8o2/5oqYGoa7LExOXXGJyg0lIDZTvWAI6yuOpB9Dn1cBfWjHSWsgXTRkMRUdK -WWh6P00ohvMu5MW651QqRYMJs0TrTUSPKq87yz6hm6EJ6pMCAwEAAaOBkjCBjzAa -BgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwMQYIKwYBBQUHAQEEJTAjMCEGCCsG -AQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjI1NjAwHQYDVR0OBBYEFLHq2VPY2pw/ -ZEg8d2+K5OpzP7zzMB8GA1UdIwQYMBaAFBEL3EI+jE/dGBOgOgrzwHWA55vPMA0G -CSqGSIb3DQEBCwUAA4ICAQBEC3Ziaslu4qQi/67tA4/cDVzJv5f0deDZ2DiVc5pn -DUZ+JMxArpHnL5aNZKvJ+/lQZC0pkoMoJzdaypRUhe8gsX7wn3jd6UJ175/B0u1q -6d3/kD02uKD2EVYTXAfYHCfWXdAD+AETeJENvOzz3OEg2LLTFlllmiMPnZHumnKR -O2x7giX72ixd/q/Or327svzqp/rMPWuMe0Sg4M0J6oSu6v+Rr0iY2pWT7VTG3E8v -cpVQgXmmIfImLcTfKlhQK46PTG2JV+kLu4t1S/zr3xVpxTrOc2vDDGIk2xiJMWhl -ds4t6RWkF9w/7ENNhhs0iWEq3Q1p+rTHnXxQx73xf8yUrH5NbrBR5vn2t9DMBelz -K+oKBdA3dJWr1RJz8yk5xX9b1zub8FvyMZEZ0N2iNmFCk5baDzMmyRWVO/NIOBy3 -pwOhtqdR4tHBOX8eq+/1EdRs7wLdfR7UnzH+WwqtqFCpT0iEX6Q1RWx/VcFjEIFW -Qah6J/NNlNKKZZrbzltxQTmZhiuI7JHA1mQNtFEm5V7S1rQm2KSAD2ijVFcrd4ti -GVORnaddkez+P8OlVfy37se0bVtVNAe5zWH0zfIkgDjedqXDQOrr3pMrtp/sF9km -am9VdStdKxHXWiuEivk5CFCUP1ZQkhlSFc3G+/3mpPag9sRNsdffjq/4Rni+bWHT -kg== ------END CERTIFICATE----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/server.csr b/canton/base/daml-tls/src/test/resources/test-certificates/server.csr deleted file mode 100644 index e9a3b57680..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/server.csr +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIEizCCAnMCAQAwGTEXMBUGA1UEAwwOMC4wLjAuMC5zZXJ2ZXIwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCoyJqgpQhXkka05ZZe/UTNiixyyr00b1OO -1BZTn61mnOITDUDpieT/PkRXslDjyzS8ek2+PG9cJT2p8n3wWQDsLt25o4qiDc1X -FGYxQF6qxYSM8n+0O6NyicIr77W1rfHQdp1nNS6gmjSxjneMpHOMbttohbM3FIcY -xwWX4IGW88OobAp0lQEzPnH4T0swFXg+ZFjX4cA/yoD4LFn/bTIJXDBmhIwpB+bR -ylvD59mX47AdqD0Ci98TWFvlqx7rZTh9r/UeL3kntQcQqeh3o4p4u2X8ZyKkWFRr -7MlxtVEvxjGceWRfJxxA6UX8tItM3heZ0CtdCZ+z/YOgJc2zUThwtJq+th+KpcRk -CafVkRY4/NoZ+UhAWyycdSXS6MNDGI5EcsvcLe4+yRbmrTuPtlzRbOGUtheL963r -nXMY5te8A/OgP1tbTYLKOCOi2LMjM1+HBvCK0AydjNRk2UKFYxS5xbpcpnWO61eS -fUH3BuvKyPIU4fLcoNEBjp3FoViNKcsxQA7rqTw04VkxlLVrUQWJygI/GySzLvqM -1HUWp38e6SGZ7/uVAX/o4CTqt7GAtvyjb/mipgahrssTE5dcYnKDSUgNlO9YAjrK -46kH0OfVwF9aMdJayBdNGQxFR0pZaHo/TSiG8y7kxbrnVCpFgwmzROtNRI8qrzvL -PqGboQnqkwIDAQABoC0wKwYJKoZIhvcNAQkOMR4wHDAaBgNVHREEEzARgglsb2Nh -bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAHp7IZPzcIuUAYuayqd0nyTO -e/nh63RlAW4buR9rJfbMpt9YKqommoz6AAt+cBRdr62xnSAksaMX6OwaFXoYhZ6O -0fb5KoQ8GqTgo802yU5HA487zsEpZ+TByBti0rOF8nLEmtAqgRazxjpPmRIV8GUi -BXNumzMui/8v28KB43Vqum7LE0tS+W2x+h8ab5c8BwR0ctoqGsXDYrIDEVp09nFA -0AROemGbqMDJEkyHWGfuRWYUXWQS0Vhf1pZY8FwSSLtbjIVckezw5x57Hg1/Wyl3 -Z4awZs2KywSXw1v9Q8w3dJqTMNn07qnrN1xe7Y0w0wF4Ab7AAtGz5VVFElav1nqh -9OwO2TQRbrqUovEZMbwc0AhCUFCs4nS62lM0CeHppBmJbvG5WooJKN+7T7fbz2k4 -4WO5SqGlr3esbzob3cm4sYS/Sip1MieTMAPoOpptg7ig59D8y9yiZfYSSMQs2h34 -QP8AG52emcc7qZ7hcujZYdQyC2NDA1WiYxsArGNR+cccznnI7ir61NQN5oxpbPHu -8s8wxOPduj1OgV9EPC9FvlVUse9dNXpsnMherq7CKD0qdODwv3mIi/lYrkdImd/r -kU3FUK2h090SE4+NNrBZLhqmV7CDYQZbWvYgJAijfJJ1F6suCPV/hvTUV6f4LncV -C8yNip6UYnuE9bHKnKgn ------END CERTIFICATE REQUEST----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/server.key b/canton/base/daml-tls/src/test/resources/test-certificates/server.key deleted file mode 100644 index 1c7641a2dc..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/server.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCoyJqgpQhXkka0 -5ZZe/UTNiixyyr00b1OO1BZTn61mnOITDUDpieT/PkRXslDjyzS8ek2+PG9cJT2p -8n3wWQDsLt25o4qiDc1XFGYxQF6qxYSM8n+0O6NyicIr77W1rfHQdp1nNS6gmjSx -jneMpHOMbttohbM3FIcYxwWX4IGW88OobAp0lQEzPnH4T0swFXg+ZFjX4cA/yoD4 -LFn/bTIJXDBmhIwpB+bRylvD59mX47AdqD0Ci98TWFvlqx7rZTh9r/UeL3kntQcQ -qeh3o4p4u2X8ZyKkWFRr7MlxtVEvxjGceWRfJxxA6UX8tItM3heZ0CtdCZ+z/YOg -Jc2zUThwtJq+th+KpcRkCafVkRY4/NoZ+UhAWyycdSXS6MNDGI5EcsvcLe4+yRbm -rTuPtlzRbOGUtheL963rnXMY5te8A/OgP1tbTYLKOCOi2LMjM1+HBvCK0AydjNRk -2UKFYxS5xbpcpnWO61eSfUH3BuvKyPIU4fLcoNEBjp3FoViNKcsxQA7rqTw04Vkx -lLVrUQWJygI/GySzLvqM1HUWp38e6SGZ7/uVAX/o4CTqt7GAtvyjb/mipgahrssT -E5dcYnKDSUgNlO9YAjrK46kH0OfVwF9aMdJayBdNGQxFR0pZaHo/TSiG8y7kxbrn -VCpFgwmzROtNRI8qrzvLPqGboQnqkwIDAQABAoICAAvFHXSpaDOvJCt2PS5iQVuH -UK0v8nzHF0/E/h+WaYGtMV4Rv7PpeqTKrfpi7sghLLbIspuFsbWpH9aLmKOw7o20 -gPVymvbNDpjizR45y8A4kOiWpTgngTDkvBEooLEgMFY7XKOiA56N0lyKv6R4+APU -MTcQknGmBqZq2TrWRu3aEYwGU2j+K7zBPzJeg92WXwcUJAE99r7xZgf9vnLqUExz -IimWrFgqJbr907i4INDk6axPxfK5Ap/h5Vqr5wvpnZm03QYzWk1eDuA8cP/IJvO0 -txp1rhFBhaBBvWnRXULUcojjtY+lyw8Yaq8s1JqnfC7XjkaT0WVm/uL6mpBwJVwo -I5OcyS/gtdvgEyAkfA9cdCGOipoCrMNZlt/2ME7bPmrQ0sreWU1pAgHmpXRzTmLX -nBEtL+yCeA8smvomLLx+q1HN/HH3ZhNQAvhQMk0HxV2e7zVR1PcON/JYyd5++vuk -UXN8bwnOATfne0FCYzKVQZdgBDmBnHmUBLSn5ZHLnu6eePQ7lNnb3qpfSY4OwHri -pdMBgCmaoJWMntr3WZQCwR86GrPo/TsfA/fbMXeJw3dSF9s5TiZAIGRb8MFZOwlI -4GvqpVsxh7ZhvlO+HwRSbMUx4jsKA37UCww593clFhAqXRimEgO0WuDKfYMrTXLU -H/MltYKmhGUX2U6LpWv9AoIBAQDrBmXrrubIHIrKGUEcrqa/zK7zNKVzxPy1sLDQ -U5f5By4hARJCD6vqRHTfXc3sKReWj0lapA4/EgDpPn1JGOn0K1I50M40f2P7RZmU -NAITD5FXHeQo17gpVKLAUq+SjlYEk9hgcQ2b9GcJ0N0dFbYTYafPKL8TbC950UEl -cjfGK3FGdIItaKrbAtG6qRoFWRtNTJo61PZK8gNtjdk/8IZl66IhnA9oyopTvEDB -mfMsh0z6vOi7UzUcxED/hRW7GBp8n1ILxaAxhSIMyLLPka2va1H0/+xT5CSQ15fX -pDJxH7Q4kScPV9G4aOWJszx+gw1maWbhJXsnh67c00ktkamdAoIBAQC32MsBMm9i -lfTK/R0/ZK0pmMiwyyMItZY5Y/+59dnYsxC5IBIlOD1t79RrjGrIgLOXeilN48gg -ABoOR3rsM5NGCzd6Gs/zmhjdvyAfM9wjn4H6uaA37OA0z18FWOxGnbaYBUOJERX9 -zLB1Mocd7twsbbUyVHjsOD/tjbIct4M1XtoVZF+mp6BabLE+rxkMWJGaB/8xOciE -MvL/kcYqAyADlNv2jw88A37ReI0t0ZE5lygx+K19sUraMdAzse6PlL+ln5qylvw1 -Cd/W9TxmDnW8JAMVujWHTJa/6SdOaWp2Q4QjfHCVJd9uh9IB+l0naF09sw6Ucg76 -mRrwpEKoRoXvAoIBADdo08CkBJ7rM8GAALzdZEzV2e1W3ScVKys+1ADZpHu+uJ5p -XT+b6EEcEJ3UxMbHzVrev0GSahVuji5vYCRKrmW1jPY6h1MTMaQ8/X9WX6LUycRL -NM54RV/hJ1nGZMRdsGP1406heL7q+Rv0VR0VUE7zeBe8GIhgujSzI/PLIOEkvtkI -gORpx9ZIrN+gHJVkM66ev8HdEZALzMkYZz7O2zDOQ/Q+kdd34PLq8C10uUMnRvva -a9g2PtBgaJ6VZtEsVppJCZPi7Oif5a4z5wxO4S75U2I6sCH7KK/JVHAvswC+o8Ou -XRId90DN4kf7rOh6xz783Jd6ObXRJGXFnnwS2nUCggEBAJblguINomy5hj2h+QKS -QKrWs0qgwLWiTZX3ZOseRlrdxokcMKiJ+6mjYqePjgnZ0Q4wf3xlzFmvm7NCqjr6 -rhOcm0FGho58WZKQD+5sY3B9uHKxLzcX3oKZ/nTmIrBnPHqUP3VeJ72z5tWazloc -Nmzk8wdHXCUOmjIBzkJRwYjpvHg4dWXVSgFOG0DG+PkwLPnPPH8L3W/cXaS04oCT -JhCqesxpaWipucp2dvI3g0pQrfYuFUAIZ9alci0sCxGgVpi7Yn7Y/FMQMsA9cwCV -N79fk4Se3NabX1RPKyrpzS8ahXeW6NUSS8xuAEoNU65Hs9gzgnEHGRJI3lrLh8aU -c9MCggEAUBqoy6Q7awI3qLeDvYF/RiD7YzAEXLUnJyquaWeYiR0O15f5dXE8alN1 -rP5jHLV5lieAU2L/OLO3/8SrcPd8dRWrnUco5FCVCuu+enNgFGNl0sQ1O7hCLI1Q -blMGa3ChIju1USvxRpGZSUOADOF5j06sco3E2Fn7xixXswtD0t2SntI0g7QtFoH1 -2S3BlI7B3tYk+H80CWwjVXNvgYePA7db5BQ8fndAKc/KV9qCNadrxPwW6seVNMti -6luBdn4bRxd9k4BZk5NvjFkIe3hndIoL4WMBK5TcC3HHKRJXVVoz6CwDJd5+MZsH -i1mipiuno3v+EVRtyKuDVDuWG/S+0w== ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/server.pem b/canton/base/daml-tls/src/test/resources/test-certificates/server.pem deleted file mode 100644 index 1c7641a2dc..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/server.pem +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCoyJqgpQhXkka0 -5ZZe/UTNiixyyr00b1OO1BZTn61mnOITDUDpieT/PkRXslDjyzS8ek2+PG9cJT2p -8n3wWQDsLt25o4qiDc1XFGYxQF6qxYSM8n+0O6NyicIr77W1rfHQdp1nNS6gmjSx -jneMpHOMbttohbM3FIcYxwWX4IGW88OobAp0lQEzPnH4T0swFXg+ZFjX4cA/yoD4 -LFn/bTIJXDBmhIwpB+bRylvD59mX47AdqD0Ci98TWFvlqx7rZTh9r/UeL3kntQcQ -qeh3o4p4u2X8ZyKkWFRr7MlxtVEvxjGceWRfJxxA6UX8tItM3heZ0CtdCZ+z/YOg -Jc2zUThwtJq+th+KpcRkCafVkRY4/NoZ+UhAWyycdSXS6MNDGI5EcsvcLe4+yRbm -rTuPtlzRbOGUtheL963rnXMY5te8A/OgP1tbTYLKOCOi2LMjM1+HBvCK0AydjNRk -2UKFYxS5xbpcpnWO61eSfUH3BuvKyPIU4fLcoNEBjp3FoViNKcsxQA7rqTw04Vkx -lLVrUQWJygI/GySzLvqM1HUWp38e6SGZ7/uVAX/o4CTqt7GAtvyjb/mipgahrssT -E5dcYnKDSUgNlO9YAjrK46kH0OfVwF9aMdJayBdNGQxFR0pZaHo/TSiG8y7kxbrn -VCpFgwmzROtNRI8qrzvLPqGboQnqkwIDAQABAoICAAvFHXSpaDOvJCt2PS5iQVuH -UK0v8nzHF0/E/h+WaYGtMV4Rv7PpeqTKrfpi7sghLLbIspuFsbWpH9aLmKOw7o20 -gPVymvbNDpjizR45y8A4kOiWpTgngTDkvBEooLEgMFY7XKOiA56N0lyKv6R4+APU -MTcQknGmBqZq2TrWRu3aEYwGU2j+K7zBPzJeg92WXwcUJAE99r7xZgf9vnLqUExz -IimWrFgqJbr907i4INDk6axPxfK5Ap/h5Vqr5wvpnZm03QYzWk1eDuA8cP/IJvO0 -txp1rhFBhaBBvWnRXULUcojjtY+lyw8Yaq8s1JqnfC7XjkaT0WVm/uL6mpBwJVwo -I5OcyS/gtdvgEyAkfA9cdCGOipoCrMNZlt/2ME7bPmrQ0sreWU1pAgHmpXRzTmLX -nBEtL+yCeA8smvomLLx+q1HN/HH3ZhNQAvhQMk0HxV2e7zVR1PcON/JYyd5++vuk -UXN8bwnOATfne0FCYzKVQZdgBDmBnHmUBLSn5ZHLnu6eePQ7lNnb3qpfSY4OwHri -pdMBgCmaoJWMntr3WZQCwR86GrPo/TsfA/fbMXeJw3dSF9s5TiZAIGRb8MFZOwlI -4GvqpVsxh7ZhvlO+HwRSbMUx4jsKA37UCww593clFhAqXRimEgO0WuDKfYMrTXLU -H/MltYKmhGUX2U6LpWv9AoIBAQDrBmXrrubIHIrKGUEcrqa/zK7zNKVzxPy1sLDQ -U5f5By4hARJCD6vqRHTfXc3sKReWj0lapA4/EgDpPn1JGOn0K1I50M40f2P7RZmU -NAITD5FXHeQo17gpVKLAUq+SjlYEk9hgcQ2b9GcJ0N0dFbYTYafPKL8TbC950UEl -cjfGK3FGdIItaKrbAtG6qRoFWRtNTJo61PZK8gNtjdk/8IZl66IhnA9oyopTvEDB -mfMsh0z6vOi7UzUcxED/hRW7GBp8n1ILxaAxhSIMyLLPka2va1H0/+xT5CSQ15fX -pDJxH7Q4kScPV9G4aOWJszx+gw1maWbhJXsnh67c00ktkamdAoIBAQC32MsBMm9i -lfTK/R0/ZK0pmMiwyyMItZY5Y/+59dnYsxC5IBIlOD1t79RrjGrIgLOXeilN48gg -ABoOR3rsM5NGCzd6Gs/zmhjdvyAfM9wjn4H6uaA37OA0z18FWOxGnbaYBUOJERX9 -zLB1Mocd7twsbbUyVHjsOD/tjbIct4M1XtoVZF+mp6BabLE+rxkMWJGaB/8xOciE -MvL/kcYqAyADlNv2jw88A37ReI0t0ZE5lygx+K19sUraMdAzse6PlL+ln5qylvw1 -Cd/W9TxmDnW8JAMVujWHTJa/6SdOaWp2Q4QjfHCVJd9uh9IB+l0naF09sw6Ucg76 -mRrwpEKoRoXvAoIBADdo08CkBJ7rM8GAALzdZEzV2e1W3ScVKys+1ADZpHu+uJ5p -XT+b6EEcEJ3UxMbHzVrev0GSahVuji5vYCRKrmW1jPY6h1MTMaQ8/X9WX6LUycRL -NM54RV/hJ1nGZMRdsGP1406heL7q+Rv0VR0VUE7zeBe8GIhgujSzI/PLIOEkvtkI -gORpx9ZIrN+gHJVkM66ev8HdEZALzMkYZz7O2zDOQ/Q+kdd34PLq8C10uUMnRvva -a9g2PtBgaJ6VZtEsVppJCZPi7Oif5a4z5wxO4S75U2I6sCH7KK/JVHAvswC+o8Ou -XRId90DN4kf7rOh6xz783Jd6ObXRJGXFnnwS2nUCggEBAJblguINomy5hj2h+QKS -QKrWs0qgwLWiTZX3ZOseRlrdxokcMKiJ+6mjYqePjgnZ0Q4wf3xlzFmvm7NCqjr6 -rhOcm0FGho58WZKQD+5sY3B9uHKxLzcX3oKZ/nTmIrBnPHqUP3VeJ72z5tWazloc -Nmzk8wdHXCUOmjIBzkJRwYjpvHg4dWXVSgFOG0DG+PkwLPnPPH8L3W/cXaS04oCT -JhCqesxpaWipucp2dvI3g0pQrfYuFUAIZ9alci0sCxGgVpi7Yn7Y/FMQMsA9cwCV -N79fk4Se3NabX1RPKyrpzS8ahXeW6NUSS8xuAEoNU65Hs9gzgnEHGRJI3lrLh8aU -c9MCggEAUBqoy6Q7awI3qLeDvYF/RiD7YzAEXLUnJyquaWeYiR0O15f5dXE8alN1 -rP5jHLV5lieAU2L/OLO3/8SrcPd8dRWrnUco5FCVCuu+enNgFGNl0sQ1O7hCLI1Q -blMGa3ChIju1USvxRpGZSUOADOF5j06sco3E2Fn7xixXswtD0t2SntI0g7QtFoH1 -2S3BlI7B3tYk+H80CWwjVXNvgYePA7db5BQ8fndAKc/KV9qCNadrxPwW6seVNMti -6luBdn4bRxd9k4BZk5NvjFkIe3hndIoL4WMBK5TcC3HHKRJXVVoz6CwDJd5+MZsH -i1mipiuno3v+EVRtyKuDVDuWG/S+0w== ------END PRIVATE KEY----- diff --git a/canton/base/daml-tls/src/test/resources/test-certificates/server.pem.enc b/canton/base/daml-tls/src/test/resources/test-certificates/server.pem.enc deleted file mode 100644 index 3bddeb1d27..0000000000 --- a/canton/base/daml-tls/src/test/resources/test-certificates/server.pem.enc +++ /dev/null @@ -1,69 +0,0 @@ -bfDHw5FZ7KzJNXlmtygpD6OJLYgIjk/pDczHuR50vQBx8jeYMZM8UyaYzXJBqbhj -eWHbms8dpzmGvwijRbmJ1GRr+tTIKP1vP77z28dbRYalhfu3iuAasY4DOXwnQwz4 -BWhBruUXOGNz7Wh61s84C8VK7yGnbWkonbZ9qbUGQayIp23pIb9RJHwAhHnyeJ/e -t7Uismc7i3khNFY2fyOpWju2Rw7Lpm8fAppVf24eZNNyLh9t4KkSMulWcfJaQMi5 -cVgMn7vpKQmaS9XZpGjbX5jgZY2idEpaS8iaizi1qrmLvy+KSnje1ML8yJ8WXx6i -A8+uno1jZo/jkxS8Kg4mkrWx/1yX0WyE/rhP08RV5zZ05CIQsFwo3Z2wS+L9w3uG -Dq09oX+1xZ2FJjfpWjtzLcmeunUd8EwAGSGuUmo66l0c+f5yBMsBveNX5HIK1Mfl -HUxxpRtKqMiilNSuCyzEqT/k2L9AuK8pfaLAmOpCFCFBXD0poxABaZJ1HpZUdaMj -/UUm5LIrlDbljS6hD4PlNiZTkysSkaowzn9II+M1KjklzMiwwrZDQolrHe049tjJ -8PwXJG6QbF2A3KZQQqs67+I1o20JCc/Pue4nui8kUOZni63QGZFBGDbpLTZrVxC7 -el+HsEE8/ttahgu5468DI/1olsxyibqhhIgDFh38fzlpj2oANzTfdfLB098zfwc8 -5SDSvYeKVML/gtsZzeOU1wVao6hK/LJi+XyfoRxteVZsgGLwqS6mueZ8Jkm4oCQv -7rfsS6YcTt0hT5ty7JWzUl6HgSAYTheopc9a9ubmu+0fPlt6DjV3a1xs6edXyWLy -t8uczfqwBMcUW8MP23iK3t9J+chO9yZP+fUTauXo8rQgdeub7tDrTdnAYZayK37z -sBWnBpIuWA2aBewHmJ2dikSk3tp9bG8OUShGyrbp8sHd1WCVs6xD8s/+Ub9A1edp -wC9/9TCXUwgZ2cCXE1AKr5RrEpqNqsZhCbtxcU4iCPFuFomkHeminDb3qxUO5fni -Tu/8isIJvm6HZ6na+FEhKIz9ahOgVHITqPcJg0v4+T94Sd0SR8nLy8MQC5jNGshl -/1lh8jK5HFdtOQjYf0gMp1Q7q5GC0au6GTqU7ne2NYuYW6E7+qvhhpzhCU0x6o9t -P23bEVMEG4rR5hhWRwg6RnBLqAC79IcNd2XJvVwV73vNN70rXSnhD2SlaKRDUpix -bqaZ4gNGanRlWQWr7v0UcjMAtEu8iAWTa/mQ+QaK89qRS1poITgKIi5WHonRJKFt -BI4loVWrltFLCJGvOZ09NNC54rfdI0YOcK5YmkRI8IeorcBL6f+7X0EOW7BrTHUN -4IX7fnJdpcuiGaRJXb4SxM3taU8aCy/NlqP0fnvscom+Er5syg14EVAcy3UH1W29 -uHa+ok+GpDOcChR7+fj9YS41Ceu85gvF02ci0pq06UNw6YVAPNqWxXnYIXXaTWva -YP3UmUPSiUxwGCnW6rvvRiG6PSC9ywI2n7SwJPrJ3oSrfNAw9yCPmq/JJlvtuyAJ -VA0WrL5v5Ds68tWCkIOIrQN3iOzIp2w6SgeIjAeDF/Lc+g1ZlLjO8mamN4D9xDZx -V25FxhxcBlsR5GeEm07qnbpluP1tde5AtVPIiHVTKMIV2oyCjjYtS8xNygDumLE7 -nKlKYg6sn6vq5gC1lR+NZGQrpMC/OG8PXBzVQnGTRDHF5DtNxI0qMs4hbp5/nEbb -SlqJ82NBvocMaBUXz3T/faWXcUK8Y1Qw6m6gnhtpwvs6VFq33GAlW4IJteliaRdx -fR1b96Ngkc3QbE349m0t8dZR/HFkQFCGsZrv+927ZkjCrM8Jv1cuhjSdoD667ELd -kGhRYCpI8FgIqIKWijgbuoo4lWlc/vnw8oQl6QV3eGrw10ryPGRWDGkzSXNTgU30 -MH/O5MkWxd0s5CBif0vZS74uzfa+8BlNGg0JgpNe3UfDM3/o6hKrsuFtymcGwIOE -SEDLUmtWw/cYQBFka5MIa16LKx/v1a4ZKmM70iAWHV/hLgqCtw4qj6eAgKN+n4YV -tBNLiA/VKgjNZTR/gTjanHXjmf04VNVe1otyRcqoCGwPmhHmryuq5vpWYO8A1i2u -KpFz1nr7JtcN6/Swkfmal+1Go7YMRBunk8MRR3w7XQSwg2FN0YBDD0mEPEjkAoo+ -5pHob395YvysEHHM1J55z2oIHkPz1KfGeAjQhcG/rj0MyXNnAudD7wvRL72d6Mev -Jdqen5609FvUs9oYxeWIg2OtA8y6AslzM83/TmzIdbN5OszH8h+XF6nMdIvHeYtF -Klf31GYvmiKKva7Z3bjczi3VeWOImVwtcEwiOWfLLDXz4m5CrUILXY66CZCsf0va -y5pv/2es0GMINJmrtFEPW8Lix5z52BKIkG3mTuLo5dIdxHVu8/NsUhmbimQL/D7j -GJj1uK4XOcCTyE+WrWqBgD5JLgia2fwWnWUnm4m5coN5REseo0Vq61JhOlTN7edq -PQswvD1y52M0eOlRbAaHlNQW+m3+HLoi+b7axQEaG/EEUExZZ1w+fM07fd8VEoYb -JtQKeDF/YDgA0Iuwfvpy07rPvJCLennALxPeqjqNJTT9hUgI1xrFA2ze30KsV/Ml -HD8zFAl81o6dqFBxU9uhYnTkLttu2u5eo7YpEQHEogFhmq+Z4HU72teU211GIgTF -vLJGAA8JmpHibZubpqYb/9mNjGM20/0gtLrnjMLMVewNKuxE+arrXFrRNHP9M8cD -BWBiGu7SVPhXjPiQ0i2XMNA/tgxbeTgYLi10s1nNpL/xSAA0x8hJ+sZ7nqD2yXl/ -Hg+Wpnb7ngJZLnknx8fFOSLZVxbHiovsvapnoH0mKal8375sSiajbErqAN9ZtpO5 -/veoFpjTa5mCzYkGkPNJcq0tPyvBekOkm6atr3o7j5pnLJBjycjKzsvfbRBTjNGD -x8q917VUdpv8Js/kpIlC0yJtA2t78P5syvYM/ODmDV6k2e8PdBcbGpSpcutGKO/1 -rMXdSHJqz/FmW7XoTnxPbwA9NcPJmnzsvr9sFw8TVIvCcTNaj+uwQA053RpDurtl -lFP4mBXg2s0Rk7LXvk+grM+8ebml9+bjhYXWQJqOLBfH03PhYoyeuliSCaqZ0Yos -46DBA1VMecqg5ZvoJzQqKjiNTA1/Jhft6K2EMeWWdJxTTnkCiMn/DnNYqI6/u7ru -X6fKdCCbL7Vt4g7ozy7Vu+Fkq/w1qEQKyJuy6Itso2JOajQbFDZEDO/BsbPgWNX3 -+GTlMwT2mTjJsoiKR7ikhg1tNpZWJhNTpwumiqP5xHPumKudP7LU+3fjOerFjEnW -Ar7c+9JsZGM0Gl4EU0jC4NLdd0XR0FgYtkykD1YVvQtyoyCj5LoCAEHi/dIPKnlY -pEvWqWfXrOct4K08yoAeiFy3Wm/5fkgUF/h5HmYZx1wRQqIJ15Rvc7wKUob6DjVB -4RPJUvmbDgdJo2LTCmyoNI0oRf+BxvxCrlBWXzOah65sgLMsdOC9+cyODfyiSFh/ -0UhZOwMCCob1sIYH3sTO3i55/0TUznWc9Q5Qh0fqaLA5nhJ+CBl0lQdks0yN6LDO -45kcnZgJ8h1PPKI8i9kYCXoakQmZiELszmVQYTdpGPR9JyPFEcsI1oiabVlI6TIL -tbXGvUxGRkisoGELdEDHkUKf9rglpE8l92VpOBPZ9hEZ9LxpBJ2SaC410ZanJW8i -fQV52EYBetYGZgu9r8oxQVCUaMofbPpNSJ6r5eRDLzDN4/SGxHVMECZaem3K8yte -rbyQ+SYW2RxM/yZeawnFjvdtzrweGg/cHeAkOlUQc+rzFpAueeOf+UAC1EEXo6pt -Prg+gB45bKC/QAwOGFjwmJR6A4vhk0i7EXl7cda9H4TQgTynNPoXLEUzh5UB9cxP -QOkrZZZendajtZMWX864WeOuLuEZKfKBjSzNiil3EMXWj+0P8qKsCiHvZmjmiL9h -KncdWNyrQKyyXg7x3uW3XV4a/hm+WMQ+OKtZOBO10Ew4i/ZgbKN3qyzpf70zna6I -/BmfXFgbB7B7fu2gMd1kzgEHE/fMVmvdppMEXUdAwLwusQrBln1h/gvFM1zq8s2N -nJ6J0aPCoCtibd3v3IXfmJOmIAZLpa1geVePL2uiUCtEu0PhNRwGS+Xsl6krErE+ -Jq/aDC/A1uKEmvQ/LgATYbmeOSLb+d/K1WDDdAYzRg2qBpDSjR8610H0pulHBP1o -NNrPX2iAXC2tHgoAF6LEG0DwR7Zh8NbWSft1efxPVyOZFGItcE4KG6GzPFyAocqE -xcjiyUYhyJRHX4tagV8IrP6coxH5dlugpqrxg4G1+otNQAV4mE5j1ODDfFBtPYpN -JAozyXqPiZBHGvXlz5takA== diff --git a/canton/base/daml-tls/src/test/scala/com/daml/tls/ProtocolDisablerTest.scala b/canton/base/daml-tls/src/test/scala/com/daml/tls/ProtocolDisablerTest.scala deleted file mode 100644 index 8d678e8130..0000000000 --- a/canton/base/daml-tls/src/test/scala/com/daml/tls/ProtocolDisablerTest.scala +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.tls - -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import java.security.Security -import scala.collection.mutable - -class ProtocolDisablerTest extends AnyWordSpec with Matchers { - - "properties updater" should { - - // given - val hello = "SSLv2Hello" - val noHello = "SSLv3, RC4, MD5withRSA, DH keySize < 1024, EC keySize < 224" - val helloAtTheEnd = s"$noHello, $hello" - val helloAtTheBeginning = s"$hello, $noHello" - val state: mutable.Map[String, String] = mutable.Map.empty - val updater = PropertiesUpdater(state(_), state(_) = _) - - "add element if it doesn't exist yet" in { - // when - state("without") = noHello - updater.appendToProperty("without", hello) - // then - state("without") shouldBe helloAtTheEnd - } - - "do nothing if the element exists already" in { - // when - state("atTheEnd") = helloAtTheEnd - state("atTheBeginning") = helloAtTheBeginning - updater.appendToProperty("atTheEnd", hello) - updater.appendToProperty("atTheBeginning", hello) - // then - state("atTheEnd") shouldBe helloAtTheEnd - state("atTheBeginning") shouldBe helloAtTheBeginning - } - - "add the element if it already exists as substring of another element" in { - // when - val helloAsSubstring = - "SSLv3, RC4, MD5withRSA, DH keySize < 1024, EC keySize < 224, SSLv2Hello Hello" - state("asSubstring") = helloAsSubstring - updater.appendToProperty("asSubstring", hello) - // then - state("asSubstring") shouldBe s"$helloAsSubstring, $hello" - } - } - - "protocol disabler" should { - // given - def disabledProtocols = Security.getProperty(ProtocolDisabler.disabledAlgorithmsProperty) - "disable hello protocol if it is enabled" in { - // when - val startingValue = disabledProtocols - val expected = ProtocolDisabler.sslV2Protocol.r.findFirstIn(startingValue) match { - case None => - ProtocolDisabler.disableSSLv2Hello() - s"$startingValue, ${ProtocolDisabler.sslV2Protocol}" - case Some(_) => - startingValue - } - val endingValue = disabledProtocols - - // then - endingValue shouldBe expected - } - } - -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/Alarm.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/Alarm.scala deleted file mode 100644 index b669afbaf3..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/Alarm.scala +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.digitalasset.base.error.ErrorCategory.{SecurityAlert, UnredactedSecurityAlert} -import io.grpc.StatusRuntimeException - -/** An alarm indicates that a different node is behaving maliciously. Alarms include situations - * where an attack has been mitigated successfully. Alarms are security relevant events that need - * to be logged in a standardized way for monitoring and auditing. - */ -abstract class AlarmErrorCode(id: String, redactDetails: Boolean = true)(implicit - parent: ErrorClass -) extends ErrorCode(id, if (redactDetails) SecurityAlert else UnredactedSecurityAlert) { - implicit override val code: AlarmErrorCode = this - -} - -trait BaseAlarm extends BaseError { - override def code: AlarmErrorCode - - override def context: Map[String, String] = - super.context ++ BaseError.extractContext(this) - - /** Report the alarm to the logger. */ - def report()(implicit logger: BaseErrorLogger): Unit = logWithContext() - - /** Reports the alarm to the logger. - * - * @return - * this alarm - */ - def reported()(implicit logger: BaseErrorLogger): this.type = { - report() - this - } - - def asGrpcError(implicit logger: BaseErrorLogger): StatusRuntimeException = - ErrorCode.asGrpcError(this)(logger) -} - -abstract class Alarm( - override val cause: String, - override val throwableO: Option[Throwable] = None, -)(implicit - override val code: AlarmErrorCode -) extends BaseAlarm diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/BaseError.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/BaseError.scala deleted file mode 100644 index a4e69bf19d..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/BaseError.scala +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.google.rpc.status.Status as ProtoStatus -import org.slf4j.event.Level - -/** The main error interface for everything that should be logged and notified. - * - * There are two ways to communicate an error to the user: write it into a log or send it as a - * string. In most cases, we'll do both: log the error appropriately locally and communicate it to - * the user by failing the API call with an error string. - */ -trait BaseError extends LocationMixin { - - /** Optional override for the log level used when this error is logged. If defined, this level - * will be used instead of the default level associated with the error code. - */ - def overrideLogLevel: Option[Level] = None - - /** The error code, usually passed in as implicit where the error class is defined */ - def code: ErrorCode - - /** A human readable string indicating the error */ - def cause: String - - /** An optional argument to log exceptions - * - * If you want to log an exception as part of your error, then use the following example: - * - * {{{ - * object MyCode extends ErrorCode(id="SUPER_DUPER_ERROR") { - * case class MyError(someString: String, throwable: Throwable) extends SomeInternalError( - * cause = "Something failed with an exception bla", - * throwableO = Some(throwable) - * ) - * } - * }}} - * - * NOTE: This throwable's details are not included the exception communicated to the gRPC clients - * so if you want them communicated, you need to explicitly add them to the e.g. context map or - * cause string. - */ - def throwableO: Option[Throwable] = None - - /** The context (declared fields) of this error - */ - def context: Map[String, String] = Map() - - /** The resources related to this error - * - * We return the set of resources via com.google.rpc.ResourceInfo. Override this method in order - * to return resource information via com.google.rpc.Status - */ - def resources: Seq[(ErrorResource, String)] = Seq() - - def logWithContext(extra: Map[String, String] = Map())(implicit - errorLoggingContext: BaseErrorLogger - ): Unit = - errorLoggingContext.logError(this, extra, overrideLogLevel) - - /** Returns retryability information of this particular error - * - * In some cases, error instances would like to provide custom retry intervals. This can be - * achieved by locally overriding this method. - * - * Do not use this to change the contract of the error categories. Non-retryable errors shouldn't - * be made retryable. Only use it for adjusting the retry intervals. - */ - def retryable: Option[ErrorCategoryRetry] = code.category.retryable - - /** Controls whether a `definite_answer` error detail is added to the gRPC status code */ - def definiteAnswerO: Option[Boolean] = None - - def rpcStatus()(implicit - loggingContext: BaseErrorLogger - ): com.google.rpc.status.Status = - ProtoStatus.fromJavaProto(ErrorCode.asGrpcStatus(this)) - -} - -trait LocationMixin { - - /** Contains the location where the error has been created. */ - val location: Option[String] = { - val stack = Thread.currentThread().getStackTrace - val thisClassName = this.getClass.getName - val idx = stack.indexWhere(_.getClassName == thisClassName) - if (idx != -1 && (idx + 1) < stack.length) { - val stackTraceElement = stack(idx + 1) - Some(s"${stackTraceElement.getFileName}:${stackTraceElement.getLineNumber}") - } else None - } -} - -object BaseError { - object RedactedMessage { - val Prefix = "An error occurred. Please contact the operator and inquire about the request" - private val regex = s"$Prefix (.+) with tid (.+)".r - - def apply(correlationId: Option[String] = None, traceId: Option[String] = None): String = - s"$Prefix ${correlationId.getOrElse("")} with tid ${traceId - .getOrElse("")}" - - def unapply(msg: String): Option[(Option[String], Option[String])] = - msg match { - case regex(corrIdO, tIdO) => - val checkedCorrelationId = Option(corrIdO).filter(_ != "") - val checkedTraceId = Option(tIdO).filter(_ != "") - Some(checkedCorrelationId -> checkedTraceId) - case _ => None - } - } - - val isRedactedMessage: String => Boolean = - _.startsWith(RedactedMessage.Prefix) - - private val ignoreFields = - Set( - "cause", - "throwable", - "loggingContext", - "definiteAnswer", - "representativeProtocolVersion", - "companionObj", - "orderedErrors", - ) - - def extractContext[D](obj: D): Map[String, String] = - obj.getClass.getDeclaredFields - .filterNot(x => ignoreFields.contains(x.getName) || x.getName.startsWith("_")) - .map { field => - field.setAccessible(true) - (field.getName, field.get(obj).toString) - } - .toMap -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/BaseErrorLogger.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/BaseErrorLogger.scala deleted file mode 100644 index dd570a1e97..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/BaseErrorLogger.scala +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import org.slf4j.event.Level - -trait BaseErrorLogger { - def logError( - err: BaseError, - extra: Map[String, String], - overrideLogLevel: Option[Level] = None, - ): Unit - def correlationId: Option[String] - def traceId: Option[String] - def properties: Map[String, String] - - // Error construction warnings/errors - def warn(message: => String): Unit - def error(message: => String, throwable: Throwable): Unit -} -object NoBaseLogging - extends NoBaseLogging(properties = Map.empty, correlationId = None, traceId = None) {} - -class NoBaseLogging( - val properties: Map[String, String], - val correlationId: Option[String], - val traceId: Option[String] = None, -) extends BaseErrorLogger { - override def logError( - err: BaseError, - extra: Map[String, String], - overrideLogLevel: Option[Level] = None, - ): Unit = () - override def warn(message: => String): Unit = () - override def error(message: => String, throwable: Throwable): Unit = () -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/DamlError.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/DamlError.scala deleted file mode 100644 index 46984becdf..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/DamlError.scala +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.google.rpc.Status -import io.grpc.StatusRuntimeException - -abstract class ContextualizedDamlError( - override val cause: String, - override val throwableO: Option[Throwable] = None, - extraContext: Map[String, Any] = Map(), -)(implicit - override val code: ErrorCode, - val logger: BaseErrorLogger, -) extends BaseError - with RpcError - with LogOnCreation { - - // Automatically log the error on generation - override def logOnCreation: Boolean = true - - def logError(): Unit = logWithContext()(logger) - - def asGrpcStatus: Status = - ErrorCode.asGrpcStatus(this)(logger) - - def asGrpcError: StatusRuntimeException = - ErrorCode.asGrpcError(this)(logger) - - override def context: Map[String, String] = - super.context ++ extraContext.view.mapValues(_.toString) - - def correlationId: Option[String] = logger.correlationId - - def traceId: Option[String] = logger.traceId -} - -/** @param definiteAnswer - * Determines the value of the `definite_answer` key in the error details - */ -class DamlErrorWithDefiniteAnswer( - override val cause: String, - override val throwableO: Option[Throwable] = None, - val definiteAnswer: Boolean = false, - extraContext: Map[String, Any] = Map(), -)(implicit - override val code: ErrorCode, - loggingContext: BaseErrorLogger, -) extends ContextualizedDamlError( - cause = cause, - throwableO = throwableO, - extraContext = extraContext, - ) { - - final override def definiteAnswerO: Option[Boolean] = Some(definiteAnswer) - -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorCategory.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorCategory.scala deleted file mode 100644 index 7509e48628..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorCategory.scala +++ /dev/null @@ -1,449 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import io.grpc.Status.Code -import org.slf4j.event.Level - -import scala.concurrent.duration.* - -/** Standard error categories - * - * Ideally, all products will return errors with appropriate error codes. Every such error code is - * associated with an error category that defines how the error will appear in the log file and on - * the api level. - */ -sealed trait ErrorCategory extends Product with Serializable { - - /** The Grpc code use to signal this error (in case it is signalled via API) */ - def grpcCode: Option[Code] - - /** The log level used to log this error on the server side */ - def logLevel: Level - - /** Default retryability information for this error category */ - def retryable: Option[ErrorCategoryRetry] - - /** If true, error details should not be emitted on the api, typically for security reasons. Must - * be true for authentication errors, permission errors and internal errors by OWASP - * recommendations. - */ - def redactDetails: Boolean - - /** Int representation of this error category */ - def asInt: Int - - /** Rank used to order severity (internal only) */ - def rank: Int -} - -object ErrorCategory { - - val all: Seq[ErrorCategory] = - Seq( - TransientServerFailure, - ContentionOnSharedResources, - DeadlineExceededRequestStateUnknown, - SystemInternalAssumptionViolated, - AuthInterceptorInvalidAuthenticationCredentials, - InsufficientPermission, - // UnredactedSecurityAlert comes before SecurityAlert with the same int representation so that - // find by int returns the unredacted one (redacted security alerts will never be found because they are redacted!) - UnredactedSecurityAlert, - SecurityAlert, - InvalidIndependentOfSystemState, - InvalidGivenCurrentSystemStateOther, - InvalidGivenCurrentSystemStateResourceExists, - InvalidGivenCurrentSystemStateResourceMissing, - InvalidGivenCurrentSystemStateSeekAfterEnd, - BackgroundProcessDegradationWarning, - InternalUnsupportedOperation, - ) - - def fromInt(ii: Int): Option[ErrorCategory] = all.find(_.asInt == ii) - - abstract class ErrorCategoryImpl( - val grpcCode: Option[Code], - val logLevel: Level, - val retryable: Option[ErrorCategoryRetry], - val redactDetails: Boolean, - val asInt: Int, - val rank: Int, - ) - - /** Service is temporarily unavailable - */ - @Description("""One of the services required to process the request was not available. - |The request might or might not have been processed, as the server aborted the request while it was being processed. - |Note that for requests that change the state of the system, this error may be returned - |even if the request has completed successfully.""") - @RetryStrategy("Retry quickly in load balancer.") - @Resolution( - "Expectation: transient failure that should be handled by retrying the request with appropriate backoff." - ) - case object TransientServerFailure - extends ErrorCategoryImpl( - grpcCode = Some(Code.UNAVAILABLE), - logLevel = Level.INFO, - retryable = Some(ErrorCategoryRetry(1.second)), - redactDetails = false, - asInt = 1, - rank = 3, - ) - with ErrorCategory - - /** Failure due to contention on some resources - */ - @Description( - """The request could not be processed due to shared processing resources - |(e.g. locks or rate limits that replenish quickly) being occupied. - |If the resource is known (i.e. locked contract), it will be included as a resource info. (Not known - |resource contentions are e.g. overloaded networks where we just observe timeouts, but can’t pin-point the cause).""" - ) - @RetryStrategy("Retry quickly (indefinitely or limited), but do not retry in load balancer.") - @Resolution("""Expectation: this is processing-flow level contention that should be handled by - |retrying the request with appropriate backoff.""") - case object ContentionOnSharedResources - extends ErrorCategoryImpl( - grpcCode = Some(Code.ABORTED), - logLevel = Level.INFO, - retryable = Some(ErrorCategoryRetry(1.second)), - redactDetails = false, - asInt = 2, - rank = 3, - ) - with ErrorCategory - - /** Request completion not observed within a pre-defined window - */ - @Description("""The request might not have been processed, as its deadline expired before its - |completion was signalled. Note that for requests that change the state of the - |system, this error may be returned even if the request has completed successfully. - |Note that known and well-defined timeouts are signalled as - |[[ContentionOnSharedResources]], while this category indicates that the - |state of the request is unknown.""") - @RetryStrategy("Retry for a limited number of times with deduplication.") - @Resolution( - """Expectation: the deadline might have been exceeded due to transient resource - |congestion or due to a timeout in the request processing pipeline being too low. - |The transient errors might be solved by the application retrying. - |The non-transient errors will require operator intervention to change the timeouts.""" - ) - case object DeadlineExceededRequestStateUnknown - extends ErrorCategoryImpl( - grpcCode = Some(Code.DEADLINE_EXCEEDED), - logLevel = Level.INFO, - retryable = Some(ErrorCategoryRetry(1.second)), - redactDetails = false, - asInt = 3, - rank = 3, - ) - with ErrorCategory - - /** Some internal error - */ - @Description( - "Request processing failed due to a violation of system internal invariants. This error is exposed on the API with grpc-status INTERNAL without any details for security reasons" - ) - @RetryStrategy("Retry after operator intervention.") - @Resolution( - """Expectation: this is due to a bug in the implementation or data corruption in the systems databases. - |Resolution will require operator intervention, and potentially vendor support.""" - ) - case object SystemInternalAssumptionViolated - extends ErrorCategoryImpl( - grpcCode = Some(Code.INTERNAL), - logLevel = Level.ERROR, - retryable = None, - redactDetails = true, - asInt = 4, - rank = 1, - ) - with ErrorCategory - - @Description( - """A potential attack or a faulty peer component has been detected. - |This error is exposed on the API with grpc-status INVALID_ARGUMENT with unredacted details.""" - ) - @RetryStrategy("Errors in this category are non-retryable.") - @Resolution( - """Expectation: this can be a severe issue that requires operator attention or intervention, and - |potentially vendor support. It means that the system has detected invalid information that can be attributed - |to either faulty or malicious manipulation of data coming from a peer source.""" - ) - case object UnredactedSecurityAlert - extends ErrorCategoryImpl( - grpcCode = Some(Code.INVALID_ARGUMENT), - logLevel = Level.WARN, - retryable = None, - redactDetails = false, - asInt = 5, - rank = 2, - ) - with ErrorCategory - - @Description( - """A potential attack or a faulty peer component has been detected. - |This error is exposed on the API with grpc-status INVALID_ARGUMENT without any details for security reasons.""" - ) - @RetryStrategy("Errors in this category are non-retryable.") - @Resolution( - """Expectation: this can be a severe issue that requires operator attention or intervention, and - |potentially vendor support. It means that the system has detected invalid information that can be attributed - |to either faulty or malicious manipulation of data coming from a peer source.""" - ) - case object SecurityAlert - extends ErrorCategoryImpl( - grpcCode = Some(Code.INVALID_ARGUMENT), - logLevel = Level.WARN, - retryable = None, - redactDetails = true, - asInt = 5, - rank = 2, - ) - with ErrorCategory - - /** Client is not authenticated properly - */ - @Description( - """The request does not have valid authentication credentials for the operation. This error is exposed on the API with grpc-status UNAUTHENTICATED without any details for security reasons""" - ) - @RetryStrategy("""Retry after application operator intervention.""") - @Resolution( - """Expectation: this is an application bug, application misconfiguration or ledger-level - |misconfiguration. Resolution requires application and/or ledger operator intervention.""" - ) - case object AuthInterceptorInvalidAuthenticationCredentials - extends ErrorCategoryImpl( - grpcCode = Some(Code.UNAUTHENTICATED), - logLevel = Level.WARN, - retryable = None, - redactDetails = true, - asInt = 6, - rank = 2, - ) - with ErrorCategory - - /** Client does not have appropriate permissions - */ - @Description( - """The caller does not have permission to execute the specified operation. This error is exposed on the API with grpc-status PERMISSION_DENIED without any details for security reasons""" - ) - @RetryStrategy("""Retry after application operator intervention.""") - @Resolution( - """Expectation: this is an application bug or application misconfiguration. Resolution requires - |application operator intervention.""" - ) - case object InsufficientPermission - extends ErrorCategoryImpl( - grpcCode = Some(Code.PERMISSION_DENIED), - logLevel = Level.WARN, - retryable = None, - redactDetails = true, - asInt = 7, - rank = 2, - ) - with ErrorCategory - - /** A request which is never going to be valid - */ - @Description("""The request is invalid independent of the state of the system.""") - @RetryStrategy("""Retry after application operator intervention.""") - @Resolution( - """Expectation: this is an application bug or ledger-level misconfiguration (e.g. request size limits). - |Resolution requires application and/or ledger operator intervention.""" - ) - case object InvalidIndependentOfSystemState - extends ErrorCategoryImpl( - grpcCode = Some(Code.INVALID_ARGUMENT), - logLevel = Level.INFO, - retryable = None, - redactDetails = false, - asInt = 8, - rank = 3, - ) - with ErrorCategory - - /** A failure due to the current system state - */ - @Description( - """The mutable state of the system does not satisfy the preconditions required to execute the request. - |We consider the whole Daml ledger including ledger config, parties, packages, users and command - |deduplication to be mutable system state. Thus all Daml interpretation errors are reported - |as this error or one of its specializations.""" - ) - @RetryStrategy("""Retry after application operator intervention.""") - @Resolution("""ALREADY_EXISTS and NOT_FOUND are special cases for the existence and non-existence of well-defined - |entities within the system state; e.g., a .dalf package, contracts ids, contract keys, or a - |transaction at an offset. OUT_OF_RANGE is a special case for reading past a range. Violations of the - |Daml ledger model always result in these kinds of errors. Expectation: this is due to - |application-level bugs, misconfiguration or contention on application-visible resources; and might be - |resolved by retrying later, or after changing the state of the system. Handling these errors requires - |an application-specific strategy and/or operator intervention.""") - case object InvalidGivenCurrentSystemStateOther - extends ErrorCategoryImpl( - grpcCode = Some(Code.FAILED_PRECONDITION), - logLevel = Level.INFO, - retryable = None, - redactDetails = false, - asInt = 9, - rank = 3, - ) - with ErrorCategory - - /** A failure due to a resource already existing in the current system state - */ - @Description("""Special type of InvalidGivenCurrentSystemState referring to a well-defined - |resource.""") - @RetryStrategy( - """Inspect resource failure and retry after resource failure has been resolved (depends on type of - |resource and application).""" - ) - @Resolution("""Same as [[InvalidGivenCurrentSystemStateOther]].""") - case object InvalidGivenCurrentSystemStateResourceExists - extends ErrorCategoryImpl( - grpcCode = Some(Code.ALREADY_EXISTS), - logLevel = Level.INFO, - retryable = None, - redactDetails = false, - asInt = 10, - rank = 3, - ) - with ErrorCategory - - /** A failure due to a resource not existing in the current system state - */ - @Description("""Special type of InvalidGivenCurrentSystemState referring to a well-defined - |resource.""") - @RetryStrategy( - """Inspect resource failure and retry after resource failure has been resolved (depends on type of - |resource and application).""" - ) - @Resolution("""Same as [[InvalidGivenCurrentSystemStateOther]].""") - case object InvalidGivenCurrentSystemStateResourceMissing - extends ErrorCategoryImpl( - grpcCode = Some(Code.NOT_FOUND), - logLevel = Level.INFO, - retryable = None, - redactDetails = false, - asInt = 11, - rank = 3, - ) - with ErrorCategory - - /** A failure due to requesting a resource using a parameter value that falls beyond the current - * upper bound (or 'end') defined by the system's state. - */ - @Description( - """The request failed because it resulted in an operation beyond the current upper bound (or 'end') - |defined by the system's state. For example, supplying a ledger offset which is larger than the current - |ledger end, or a record time that is in the future.""" - ) - @RetryStrategy( - """Wait and retry. For example, retry a limited number of times with potentially increasing backoff. - |Hint: Inspect the retryable value of the error code to decide on the particular retry duration.""" - ) - @Resolution( - """Resolution can occur naturally as the system progresses. The requested operation may become valid - |eventually once the system's state has advanced further. For example, when new ledger entries are added. - |If however the situation does not resolve as expected, operator intervention may be required.""" - ) - case object InvalidGivenCurrentSystemStateSeekAfterEnd - extends ErrorCategoryImpl( - grpcCode = Some(Code.OUT_OF_RANGE), - logLevel = Level.INFO, - retryable = Some(ErrorCategoryRetry(1.second)), - redactDetails = false, - asInt = 12, - rank = 3, - ) - with ErrorCategory - - /** Background daemon notifying about observed degradation - */ - @Description( - """This error category is used internally to signal to the system operator an internal degradation.""" - ) - @RetryStrategy("""Not an API error, therefore not retryable.""") - @Resolution("""Inspect details of the specific error for more information.""") - case object BackgroundProcessDegradationWarning - extends ErrorCategoryImpl( - grpcCode = None, // should not be used on the API level - logLevel = Level.WARN, - retryable = None, - redactDetails = false, - asInt = 13, - rank = 2, - ) - with ErrorCategory - - @Description( - """This error category is used to signal that an unimplemented code-path has been triggered by a client or participant operator request. This error is exposed on the API with grpc-status UNIMPLEMENTED without any details for security reasons""" - ) - @RetryStrategy("""Errors in this category are non-retryable.""") - @Resolution( - """This error is caused by a ledger-level misconfiguration or by an implementation bug. - |Resolution requires node operator intervention.""" - ) - case object InternalUnsupportedOperation - extends ErrorCategoryImpl( - grpcCode = Some(Code.UNIMPLEMENTED), - logLevel = Level.ERROR, - retryable = None, - redactDetails = true, - asInt = 14, - rank = 1, - ) - with ErrorCategory - - implicit val orderingErrorType: Ordering[ErrorCategory] = Ordering.by[ErrorCategory, Int](_.rank) - - /** Special error category that isn't included in [[ErrorCategory.all]] and is meant purely for - * overriding the error category text in the generated error code documentation. It is expected - * that this category is replaced by an existing category from ErrorCategory.all in the error - * code implementation of [[RpcError.code]] when the actual error is instantiated. as such, all - * methods in this Error Category are errors - */ - final case class OverrideDocStringErrorCategory(overrideText: String) extends ErrorCategory { - private def unimplemented = throw new IllegalArgumentException( - "Attempted to use OverrideDocStringErrorCategory in thrown error. This should be replaced at construction" - ) - override def grpcCode: Option[Code] = unimplemented - override def logLevel: Level = unimplemented - override def retryable: Option[ErrorCategoryRetry] = unimplemented - override def redactDetails: Boolean = unimplemented - override def asInt: Int = unimplemented - override def rank: Int = unimplemented - } - - /** Generic error category class meant to be used as a data container for information deserialized - * from gRPC statuses (see [[com.digitalasset.base.error.utils.DecodedCantonError]]). - * - * Note: Do NOT use this class for adding error category information to error code instances but - * instead re-use existing ones or define new ones (see [[ErrorCategory.all]]) - */ - final case class GenericErrorCategory( - override val grpcCode: Option[Code], - override val logLevel: Level, - override val retryable: Option[ErrorCategoryRetry], - override val redactDetails: Boolean, - override val asInt: Int, - override val rank: Int, - ) extends ErrorCategoryImpl( - grpcCode = grpcCode, - logLevel = logLevel, - retryable = retryable, - redactDetails = redactDetails, - asInt = asInt, - rank = rank, - ) - with ErrorCategory -} - -/** Default retryability information - * - * Every error category has a default retryability classification. An error code may adjust the - * retry duration. - */ -final case class ErrorCategoryRetry(duration: FiniteDuration) diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorClass.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorClass.scala deleted file mode 100644 index b6b0052465..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorClass.scala +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -/** A component of [[ErrorClass]] - * - * @param docName - * The name that will appear in the generated documentation for the grouping. - * @param fullClassName - * Full class name of the corresponding [[ErrorGroup]]. - */ -final case class Grouping( - docName: String, - fullClassName: String, -) { - require( - docName.trim.nonEmpty, - s"Grouping.docName must be non empty and must contain not only whitespace characters, but was: |$docName|!", - ) -} - -/** Used to hierarchically structure error codes in the official documentation. - */ -final case class ErrorClass(groupings: List[Grouping]) { - def extend(grouping: Grouping): ErrorClass = - ErrorClass(groupings :+ grouping) -} - -object ErrorClass { - def root(): ErrorClass = ErrorClass(Nil) -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorCode.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorCode.scala deleted file mode 100644 index a1e25c9d67..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorCode.scala +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.google.rpc.Status -import io.grpc.Status.Code -import io.grpc.StatusRuntimeException -import io.grpc.protobuf.StatusProto -import org.slf4j.event.Level - -import scala.annotation.StaticAnnotation -import scala.util.control.{NoStackTrace, NonFatal} - -import SerializableErrorCodeComponents.validateTraceIdAndCorrelationId - -/** Error Code Definition - * - * We want to support our users and our developers with good error codes. Therefore, every error - * that an API returns should refer to a documented error code and provide some context - * information. - * - * Every error code is uniquely identified using an error-id of max 63 CAPITALIZED_WITH_UNDERSCORES - * characters - * - * Errors are organised according to ErrorGroups. And we separate the error code definition (using - * a singleton by virtue of using objects to express the nested hierarchy) and the actual error. - * - * Please note that there is some implicit argument passing involved in the example below: - * - * {{{ - * object SyncServiceErrors extends ParticipantErrorGroup { - * object ConnectionErrors extends ErrorGroup { - * object SynchronizerUnavailable extends ErrorCode(id="SYNCHRONIZER_UNAVAILABLE", ..) { - * case class ActualError(someContext: Val) extends BaseError with SyncServiceError - * // this error will actually be referring to the same error code! - * case class OtherError(otherContext: Val) extends BaseError with SyncServiceError - * } - * } - * object HandshakeErrors extends ErrorGroup { - * ... - * } - * } - * }}} - */ -abstract class ErrorCode(val id: String, val category: ErrorCategory)(implicit - val parent: ErrorClass -) { - - require(id.nonEmpty, "error-id must be non empty") - require(id.length < 64, s"error-id is too long: $id") - require(id.forall(c => c.isUpper || c == '_' || c.isDigit), s"Invalid characters in error-id $id") - - implicit val code: ErrorCode = this - - /** The machine readable error code string, uniquely identifiable by the error id, error category - * and correlation id. e.g. NOT_CONNECTED_TO_ANY_SYNCHRONIZER(2,ABC234) - */ - def codeStr(correlationId: Option[String]): String = - ErrorCodeMsg.codeStr(code.id, category.asInt, correlationId) - - /** @return - * message including error category id, error code id, correlation id and cause - */ - def toMsg(cause: => String, correlationId: Option[String], limit: Option[Int]): String = { - val truncatedCause = limit match { - case Some(maxLength) if (cause.length > maxLength) => cause.take(maxLength) + "..." - case _ => cause - } - ErrorCodeMsg(id, category.asInt, correlationId, truncatedCause) - } - - /** Log level of the error code - * - * Generally, the log level is defined by the error category. In rare cases, it might be - * overridden by the error code. - */ - def logLevel: Level = category.logLevel - - /** True if this error may appear on the API */ - protected def exposedViaApi: Boolean = category.grpcCode.nonEmpty - - /** The error conveyance doc string provides a statement about the form this error will be - * returned to the user - */ - def errorConveyanceDocString: Option[String] = { - val loggedAs = s"This error is logged with log-level $logLevel on the server side" - val apiLevel = (category.grpcCode, exposedViaApi) match { - case (Some(grpcCode), true) => - if (category.redactDetails) - s". It is exposed on the API with grpc-status $grpcCode without any details for security reasons." - else - s" and exposed on the API with grpc-status $grpcCode including a detailed error message." - case _ => "." - } - Some(loggedAs ++ apiLevel) - } -} - -object ErrorCodeMsg { - private val ErrorCodeMsgRegex = """([A-Z_]+)\((\d+),(.+?)\): (.*)""".r - - def apply( - errorCodeId: String, - errorCategoryInt: Int, - maybeCorrelationId: Option[String], - cause: String, - ): String = - s"${codeStr(errorCodeId, errorCategoryInt, maybeCorrelationId)}: $cause" - - def codeStr( - errorCodeId: String, - errorCategoryInt: Int, - maybeCorrelationId: Option[String], - ): String = s"$errorCodeId($errorCategoryInt,${maybeCorrelationId.getOrElse("0").take(8)})" - - def extract(errorCodeMsg: String): Either[String, (String, Int, String, String)] = - errorCodeMsg match { - case ErrorCodeMsgRegex(errorCodeId, errorCategoryIdIntAsString, corrId, cause) => - Right((errorCodeId, errorCategoryIdIntAsString.toInt, corrId, cause)) - case other => Left(s"Could not extract error code constituents from $other") - } -} - -object ErrorCode { - - /** Maximum size (in bytes) of the [[com.google.rpc.Status]] proto that a self-service error code - * can be serialized into. - * - * We choose this value with the following considerations: - * - The serialized error Status proto is packed into a [[io.grpc.Metadata]] together with the - * error description, which is then enriched with additional gRPC-internal entries and - * converted into HTTP2 headers for transmission. - * - The default maximum gRPC metadata size is 8KB (for both clients and servers). We MUST - * ensure that we don't exceed this value for error-returning gRPC metadata, otherwise - * INTERNAL errors may be reported on both the client and server. - * - The error description is packed twice in the serialized metadata (see - * [[ErrorCode.asGrpcError]] and how it creates a [[io.grpc.StatusRuntimeException]]). - * - * Conservatively we allow a buffer of > 3KB for gRPC and gRPC->HTTP2 internals overhead. - * (considering a [[MaxErrorContentBytes]] maximum Status proto size and limit the cause - * description size). - * - * Note: instead of increasing this value, consider limiting better the error contents. - */ - val MaxErrorContentBytes = 4096 - - def asGrpcError(err: BaseError)(implicit - loggingContext: BaseErrorLogger - ): StatusRuntimeException = { - val status = asGrpcStatus(err)(loggingContext) - // Builder methods for metadata are not exposed, so going route via creating an exception - val e = StatusProto.toStatusRuntimeException(status) - // Stripping stacktrace - err match { - case loc: LogOnCreation if loc.logOnCreation => - new ErrorCode.LoggedApiException(e.getStatus, e.getTrailers) - case _ => - new ErrorCode.ApiException(e.getStatus, e.getTrailers) - } - } - - def asGrpcStatus(err: BaseError)(implicit loggingContext: BaseErrorLogger): Status = - asGrpcStatus(err, MaxErrorContentBytes) - - private[error] def asGrpcStatus(err: BaseError, maxSerializedErrorSize: Int)(implicit - loggingContext: BaseErrorLogger - ): Status = - try - SerializableErrorCodeComponents( - errorCode = err.code, - loggingContext = loggingContext, - rawCorrelationId = loggingContext.correlationId, - rawTraceId = loggingContext.traceId, - cause = err.cause, - definiteAnswer = err.definiteAnswerO, - errorResources = err.resources, - contextMap = err.context ++ loggingContext.properties, - retryableInfo = err.retryable.map(_.duration), - ).toStatusProto(maxSerializedErrorSize) - catch { - case NonFatal(e) => - val (traceId, correlationId) = - validateTraceIdAndCorrelationId(loggingContext.traceId, loggingContext.correlationId) - - loggingContext.error(s"Error building gRPC status for error $err", e) - com.google.rpc.Status - .newBuilder() - .setCode(Code.INTERNAL.value()) - .setMessage( - BaseError.RedactedMessage(correlationId = correlationId, traceId = traceId) - ) - .build() - } - - class ApiException(status: io.grpc.Status, metadata: io.grpc.Metadata) - extends StatusRuntimeException(status, metadata) - with NoStackTrace - - /** Exception that has already been logged. - */ - class LoggedApiException(status: io.grpc.Status, metadata: io.grpc.Metadata) - extends ApiException(status, metadata) -} - -// Use these annotations to add more information to the documentation for an error on the website -final case class Explanation(explanation: String) extends StaticAnnotation -final case class Resolution(resolution: String) extends StaticAnnotation -final case class Description(description: String) extends StaticAnnotation -final case class RetryStrategy(retryStrategy: String) extends StaticAnnotation diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorGroup.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorGroup.scala deleted file mode 100644 index 1f92d91607..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorGroup.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -abstract class ErrorGroup()(implicit parent: ErrorClass) { - private val simpleClassName: String = getClass.getSimpleName.replace("$", "") - val fullClassName: String = getClass.getName - - implicit val errorClass: ErrorClass = - parent.extend(Grouping(docName = simpleClassName, fullClassName = fullClassName)) -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorResource.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorResource.scala deleted file mode 100644 index 3228f3ac05..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/ErrorResource.scala +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -/** Type of error resource - * - * Some errors are linked to a specific resource such as a contract id or a package id. In such - * cases, we include the resource identifier as part of the error message. This enum allows an - * error to provide identifiers of a resource - */ -final case class ErrorResource(asString: String) { - def nullable: ErrorResource = ErrorResource(s"NULLABLE_$asString") -} - -object ErrorResource { - lazy val ContractId: ErrorResource = ErrorResource("CONTRACT_ID") - lazy val ContractIds: ErrorResource = ErrorResource("CONTRACT_IDS") - lazy val ContractKey: ErrorResource = ErrorResource("CONTRACT_KEY") - lazy val ContractArg: ErrorResource = ErrorResource("CONTRACT_ARG") - lazy val CryptoValue: ErrorResource = ErrorResource("CRYPTO_VALUE") - lazy val TransactionId: ErrorResource = ErrorResource("TRANSACTION_ID") - lazy val UpdateId: ErrorResource = ErrorResource("UPDATE_ID") - lazy val DalfPackage: ErrorResource = ErrorResource("PACKAGE") - lazy val TemplateId: ErrorResource = ErrorResource("TEMPLATE_ID") - lazy val InterfaceId: ErrorResource = ErrorResource("INTERFACE_ID") - lazy val PackageName: ErrorResource = ErrorResource("PACKAGE_NAME") - lazy val CommandId: ErrorResource = ErrorResource("COMMAND_ID") - lazy val Party: ErrorResource = ErrorResource("PARTY") - lazy val Parties: ErrorResource = ErrorResource("PARTIES") - lazy val User: ErrorResource = ErrorResource("USER") - lazy val IdentityProviderConfig: ErrorResource = ErrorResource("IDENTITY_PROVIDER_CONFIG") - lazy val ContractKeyHash: ErrorResource = ErrorResource("CONTRACT_KEY_HASH") - lazy val ExceptionValue: ErrorResource = ErrorResource("EXCEPTION_VALUE") - lazy val ExceptionType: ErrorResource = ErrorResource("EXCEPTION_TYPE") - lazy val ExceptionText: ErrorResource = ErrorResource("EXCEPTION_TEXT") - lazy val DevErrorType: ErrorResource = ErrorResource("DEV_ERROR_TYPE") - lazy val SynchronizerId: ErrorResource = ErrorResource("SYNCHRONIZER_ID") - lazy val SynchronizerAlias: ErrorResource = ErrorResource("SYNCHRONIZER_ALIAS") - lazy val Offset: ErrorResource = ErrorResource("OFFSET") - lazy val ExpectedType: ErrorResource = ErrorResource("EXPECTED_TYPE") - lazy val FieldIndex: ErrorResource = ErrorResource("FIELD_INDEX") - - lazy val all: Seq[ErrorResource] = Seq( - CommandId, - ContractArg, - ContractId, - ContractIds, - ContractKey, - ContractKeyHash, - CryptoValue, - DalfPackage, - DevErrorType, - ExceptionText, - ExceptionType, - ExceptionValue, - ExpectedType, - FieldIndex, - IdentityProviderConfig, - InterfaceId, - Offset, - PackageName, - Parties, - Party, - SynchronizerAlias, - SynchronizerId, - TemplateId, - TransactionId, - UpdateId, - User, - ) - - def fromString(str: String): Option[ErrorResource] = str.split("NULLABLE_") match { - case Array("", resource) => - all.find(_.asString == resource).map(_.nullable) - case Array(resource) => - all.find(_.asString == resource) - case _ => None - } -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/GrpcStatuses.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/GrpcStatuses.scala deleted file mode 100644 index 66ec1c88b7..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/GrpcStatuses.scala +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.google.rpc.error_details.ErrorInfo -import com.google.rpc.status.Status as StatusProto - -import scala.util.Try - -object GrpcStatuses { - val DefiniteAnswerKey = "definite_answer" - val CompletionOffsetKey = "completion_offset" - - def isDefiniteAnswer(status: StatusProto): Boolean = - status.details.exists { any => - if (any.is(ErrorInfo.messageCompanion)) { - Try(any.unpack(ErrorInfo.messageCompanion)).toOption - .exists(isDefiniteAnswer) - } else { - false - } - } - - private def isDefiniteAnswer(errorInfo: ErrorInfo): Boolean = - errorInfo.metadata.get(DefiniteAnswerKey).exists(value => java.lang.Boolean.valueOf(value)) -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/LogOnCreation.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/LogOnCreation.scala deleted file mode 100644 index 656a2f97ab..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/LogOnCreation.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -/** Trait to log on creation */ -trait LogOnCreation { - def logOnCreation: Boolean = true - def logError(): Unit - if (logOnCreation) { - logError() - } -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/RpcError.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/RpcError.scala deleted file mode 100644 index a3deb52cd2..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/RpcError.scala +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.google.rpc.Status -import io.grpc.StatusRuntimeException - -trait RpcError { - - /** The error code, usually passed in as implicit where the error class is defined */ - def code: ErrorCode - - /** The context (declared fields) of this error */ - def context: Map[String, String] - - /** A human readable string indicating the error */ - def cause: String - - /** The resources related to this error */ - def resources: Seq[(ErrorResource, String)] - - /** The correlationId (e.g. submissionId) associated with the request that caused the error */ - def correlationId: Option[String] - - /** The traceId associated with the TraceContext at error creation */ - def traceId: Option[String] - - /** The gRPC status */ - def asGrpcStatus: Status - - /** The gRPC status encoded as a StatusRuntimeException */ - def asGrpcError: StatusRuntimeException - -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/SerializableErrorComponents.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/SerializableErrorComponents.scala deleted file mode 100644 index 27e129fd19..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/SerializableErrorComponents.scala +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.digitalasset.base.error.utils.ErrorDetails -import io.grpc.Status.Code - -import scala.concurrent.duration.FiniteDuration -import scala.jdk.CollectionConverters.IterableHasAsJava -import scala.util.matching.Regex - -import NonSecuritySensitiveErrorCodeComponents.{ - MaxCauseLogLength, - stringsPackedSize, - truncateDetails, -} -import SerializableErrorCodeComponents.* - -object SerializableErrorCodeComponents { - private[error] val ValidMetadataKeyRegex: Regex = "[^(a-zA-Z0-9-_)]".r - private[error] val GrpcCodeBytes = 1 - // Each string is encoded additionally with a tag and a size value - // Hence, we add some overhead to account for them - private[error] val MaximumPerTagOverheadBytes = 5 - private[error] val ErrorInfoDetailPackingOverheadBytes = - ErrorDetails.ErrorInfoDetail("", Map.empty).toRpcAny.getSerializedSize - private[error] val ResourceInfoDetailPackingOverheadBytes = - ErrorDetails.ResourceInfoDetail("", "").toRpcAny.getSerializedSize - // By default, hex encoded correlation-ids and trace-ids have 32 characters - // Since we don't have any explicit enforcement, truncate everything above 256 to disallow error serialization issues - private[error] val MaxTraceIdCorrelationIdSize = 256 - - def apply( - errorCode: ErrorCode, - loggingContext: BaseErrorLogger, - rawTraceId: Option[String], - rawCorrelationId: Option[String], - // Next parameters are by-name to avoid unnecessary computation if the error is security sensitive - cause: => String, - definiteAnswer: => Option[Boolean], - errorResources: => Seq[(ErrorResource, String)], - contextMap: => Map[String, String], - retryableInfo: => Option[FiniteDuration], - ): SerializableErrorCodeComponents = { - val (traceId, correlationId) = - validateTraceIdAndCorrelationId(rawTraceId, rawCorrelationId)(loggingContext) - - if (errorCode.category.redactDetails) - SecuritySensitiveErrorCodeComponents( - grpcStatusCode = errorCode.category.grpcCode, - traceId = traceId, - correlationId = correlationId, - )(logger = loggingContext) - else - NonSecuritySensitiveErrorCodeComponents( - traceId = traceId, - correlationId = correlationId, - cause = cause, - errorCode = errorCode, - definiteAnswer = definiteAnswer, - errorResources = errorResources, - contextMap = contextMap ++ loggingContext.properties, - retryableInfo = retryableInfo, - )(loggingContext = loggingContext) - } - - private[error] def validateTraceIdAndCorrelationId( - rawTraceId: Option[String], - rawCorrelationId: Option[String], - )(implicit loggingContext: BaseErrorLogger) = { - val traceId = rawTraceId.map(tId => - truncateString( - tId, - MaxTraceIdCorrelationIdSize, - loggingContext.warn( - s"Trace-id $tId exceeded maximum allowed size of $MaxTraceIdCorrelationIdSize and has been truncated for gRPC error serialization" - ), - ) - ) - val correlationId = rawCorrelationId.map(cId => - truncateString( - cId, - MaxTraceIdCorrelationIdSize, - loggingContext.warn( - s"Correlation-id $cId exceeded maximum allowed size of $MaxTraceIdCorrelationIdSize and has been truncated for gRPC error serialization" - ), - ) - ) - (traceId, correlationId) - } - - private[error] def truncateString(v: String, maxSize: Int, onTruncate: => Unit = ()): String = - if (v.length > maxSize) { - onTruncate - s"${v.take(maxSize - 3)}..." - } else v -} - -sealed trait SerializableErrorCodeComponents { - def toStatusProto(maxSizeBytes: Int): com.google.rpc.Status -} - -private[error] final case class SecuritySensitiveErrorCodeComponents( - grpcStatusCode: Option[Code], - traceId: Option[String], - correlationId: Option[String], -)(logger: BaseErrorLogger) - extends SerializableErrorCodeComponents { - - override def toStatusProto(maxSizeBytes: Int): com.google.rpc.Status = - com.google.rpc.Status - .newBuilder() - .setCode( - grpcStatusCode - .getOrElse { - logger.warn("Missing grpc status code for security sensitive error") - Code.INTERNAL - } - .value() - ) - .setMessage(BaseError.RedactedMessage(correlationId, traceId)) - .addAllDetails( - correlationId - .orElse(traceId) - .map(ErrorDetails.RequestInfoDetail.apply) - .toList - .map(_.toRpcAny) - .asJava - ) - .build() -} - -private[error] final case class NonSecuritySensitiveErrorCodeComponents( - traceId: Option[String], - correlationId: Option[String], - cause: String, - errorCode: ErrorCode, - definiteAnswer: Option[Boolean], - errorResources: Seq[(ErrorResource, String)], - contextMap: Map[String, String], - retryableInfo: Option[FiniteDuration], -)(loggingContext: BaseErrorLogger) - extends SerializableErrorCodeComponents { - - /** Truncates and serializes the self-service error components into a [[com.google.rpc.Status]]. - * - * Truncation happens for both the error message and error details aiming to ensure that the - * maximum message size ([[NonSecuritySensitiveErrorCodeComponents.MaxCauseLogLength]]) and - * maximum total Status serialization size ([[ErrorCode.MaxErrorContentBytes]]) are respected. - */ - def toStatusProto(maxSizeBytes: Int): com.google.rpc.Status = { - val grpcStatusCode = validatedGrpcErrorCode(errorCode.category.grpcCode) - val errorCategoryContext = "category" -> errorCode.category.asInt.toString - val traceIdContext = traceId.map("tid" -> _) - val definiteAnswerContext = - definiteAnswer.map(value => GrpcStatuses.DefiniteAnswerKey -> value.toString) - - val retryInfoRpc = retryableInfo.map(ErrorDetails.RetryInfoDetail(_).toRpcAny) - val requestInfoRpc = - correlationId.orElse(traceId).map(ErrorDetails.RequestInfoDetail(_).toRpcAny) - val errorCodeId = errorCode.id - - val validatedMessage = errorCode.toMsg(cause, correlationId, limit = Some(MaxCauseLogLength)) - val mandatoryDetailsEncodedSize = - GrpcCodeBytes + MaximumPerTagOverheadBytes + // Grpc code size + its byte tag overhead - stringsPackedSize( - Seq(validatedMessage, errorCodeId, errorCategoryContext._1, errorCategoryContext._2) ++ - traceIdContext.map(v => Seq(v._1, v._2)).getOrElse(Seq.empty) ++ - definiteAnswerContext.map(v => Seq(v._1, v._2)).getOrElse(Seq.empty) - ) + - retryInfoRpc.fold(0)(_.getSerializedSize + MaximumPerTagOverheadBytes) + - requestInfoRpc.fold(0)(_.getSerializedSize + MaximumPerTagOverheadBytes) + - // overhead bytes for packing ErrorInfo - ErrorInfoDetailPackingOverheadBytes - - val bytesLeftForTruncateableDetails = maxSizeBytes - mandatoryDetailsEncodedSize - - // Truncate-able error details - val (truncatedContext, truncatedErrorResources) = - truncateDetails(contextMap, errorResources, bytesLeftForTruncateableDetails) - - val errorInfoDetail = ErrorDetails.ErrorInfoDetail( - errorCodeId = errorCodeId, - // For simplicity, all key-values added here have been accounted with tag overheads - // even though they are not tagged when serialized. - metadata = - (truncatedContext ++ definiteAnswerContext.toList ++ traceIdContext.toList :+ errorCategoryContext).toMap, - ) - - val resourceInfos = - truncatedErrorResources.view - .map(_.swap) - .map((ErrorDetails.ResourceInfoDetail.apply _).tupled) - .toList - - val allDetails = - Seq(errorInfoDetail.toRpcAny) ++ retryInfoRpc.toList ++ requestInfoRpc.toList ++ resourceInfos - .map(_.toRpcAny) - - // Build status - com.google.rpc.Status - .newBuilder() - .setCode(grpcStatusCode.value()) - .setMessage(validatedMessage) - .addAllDetails(allDetails.asJava) - .build() - } - - private def validatedGrpcErrorCode(grpcCode: Option[Code]): Code = - grpcCode.getOrElse { - loggingContext.warn(s"Passing non-grpc error via grpc ${errorCode.id} ") - Code.INTERNAL - } -} - -private[error] object NonSecuritySensitiveErrorCodeComponents { - - /** The maximum size (in characters) of the self-service error description, truncated for - * transport as part of a Status - */ - val MaxCauseLogLength = 512 - - private[error] def truncateDetails( - context: Map[String, String], - errResources: Seq[(ErrorResource, String)], - remainingBudgetBytes: Int, - ): (Seq[(String, String)], Seq[(String, String)]) = { - - val numberOfEntries = context.size + errResources.size - if (numberOfEntries == 0) (Seq.empty, Seq.empty) - else { - val remainingBudgetForEntries = remainingBudgetBytes - - // account for two tags per key-value pair - 2 * MaximumPerTagOverheadBytes * numberOfEntries - - val budgetForTruncatedResources = - (errResources.size.toLong * remainingBudgetForEntries.toLong) / numberOfEntries.toLong - - val budgetForTruncatedContext = remainingBudgetForEntries - budgetForTruncatedResources - - val truncatedErrorResources = truncateResources( - errResources.map { case (res, v) => res.asString -> v }, - budgetForTruncatedResources.toInt, - ) - - val truncatedContext = truncateContext(context.toSeq, budgetForTruncatedContext.toInt) - (truncatedContext, truncatedErrorResources) - } - } - - private[error] def truncateContext( - rawContextEntries: Seq[(String, String)], - maxBudgetBytes: Int, - ): Seq[(String, String)] = { - val raw: Seq[(String, String)] = rawContextEntries.view - // Make key gRPC compliant - .map { case (k, v) => ValidMetadataKeyRegex.replaceAllIn(k, "").take(63) -> v } - // Discard empty values - .filter(_._1.nonEmpty) - // Discard empty keys - .filter(_._2.nonEmpty) - .toSeq - .sortBy(v => v._1.length + v._2.length) - - val rawSize = raw.size - - raw.view.zipWithIndex - .foldLeft((Vector.empty[(String, String)], maxBudgetBytes)) { - case ((acc, free), ((k, v), idx)) => - // This value can be regarded as an "inverse" moving average, - // (i.e. computed on the remaining entries instead of the past ones). - // Since the series is sorted by the entries size, this value progresses strictly ascending - // which allows more aggressive truncation of the bigger/later entries of this series. - val maxSize = free / (rawSize - idx) - - val maybeNewEntry = if (k.length + v.length > maxSize) { - // We need at least 8 chars per (k,v) entry to get something meaningful after truncation - // since we might suffix each of the k,v strings with `...` (3 chars) - see truncateString - Option.when(maxSize > 7) { - val truncatedKey = truncateString(k, maxSize / 2) - truncatedKey -> truncateString(v, maxSize - truncatedKey.length) - } - } else Some(k -> v) - - maybeNewEntry - .map { case (k, v) => (k, v, encSize(k) + encSize(v)) } - // Check again that unexpected encoding did not lead to exceeding the budget - // (For simplicity, in truncateString, we assume 1-byte per string encoding) - .filter { case (_, _, encodedSize) => encodedSize <= maxSize } - .map { case (newK, newV, encodedSize) => (acc :+ (newK -> newV), free - encodedSize) } - .getOrElse((acc, free)) - } - ._1 - } - - private def truncateResources( - details: Seq[(String, String)], - remaining: Int, - ): Seq[(String, String)] = - details - .foldLeft(Vector.empty[(String, String)] -> remaining) { case ((acc, free), (k, v)) => - // Account for the resource being packed as a ResourceInfo - val newFree = - free - (encSize(k) + encSize(v) + ResourceInfoDetailPackingOverheadBytes) - if (newFree < 0) (acc, free) - else (acc :+ (k -> v), newFree) - } - ._1 - - private def stringsPackedSize(strings: Seq[String]): Int = - strings.map(encSize(_) + MaximumPerTagOverheadBytes).sum - - private def encSize(s: String): Int = s.getBytes("UTF-8").length -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/samples/Example.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/samples/Example.scala deleted file mode 100644 index 3d9d29d349..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/samples/Example.scala +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error.samples - -import com.digitalasset.base.error.{BaseErrorLogger, ContextualizedDamlError, NoBaseLogging} - -import scala.concurrent.duration.* - -object DummmyServer { - - import com.digitalasset.base.error.{ - ErrorCategory, - ErrorCategoryRetry, - ErrorClass, - ErrorCode, - ErrorResource, - } - - object ErrorCodeFoo - extends ErrorCode(id = "MY_ERROR_CODE_ID", ErrorCategory.ContentionOnSharedResources)( - ErrorClass.root() - ) { - - implicit val errorLogger: BaseErrorLogger = new NoBaseLogging( - correlationId = Some("full-correlation-id-123456790"), - properties = Map.empty, - ) - - final case class Error(message: String) extends ContextualizedDamlError(cause = message) { - - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.ContractId -> "someContractId" - ) - - override def retryable: Option[ErrorCategoryRetry] = Some( - ErrorCategoryRetry(123.second + 456.milliseconds) - ) - - override def context: Map[String, String] = Map("foo" -> "bar") - } - - } - - def serviceEndpointDummy(): Unit = - throw ErrorCodeFoo.Error("A user oriented message").asGrpcError - -} - -/** This shows how a user can handle error codes. In particular it shows how to extract useful - * information from the signalled exception with minimal library dependencies. - * - * NOTE: This class is given as an example in the official Daml documentation. If you change it - * here, change it also in the docs. - */ -object SampleClientSide { - - import com.google.rpc.ResourceInfo - import com.google.rpc.{ErrorInfo, RequestInfo, RetryInfo} - import io.grpc.StatusRuntimeException - import scala.jdk.CollectionConverters.* - - def example(): Unit = - try { - DummmyServer.serviceEndpointDummy() - } catch { - case e: StatusRuntimeException => - // Converting to a status object. - val status = io.grpc.protobuf.StatusProto.fromThrowable(e) - - // Extracting gRPC status code. - assert(status.getCode == io.grpc.Status.Code.ABORTED.value()) - assert(status.getCode == 10) - - // Extracting error message, both - // machine oriented part: "MY_ERROR_CODE_ID(2,full-cor):", - // and human oriented part: "A user oriented message". - assert(status.getMessage == "MY_ERROR_CODE_ID(2,full-cor): A user oriented message") - - // Getting all the details - val rawDetails: Seq[com.google.protobuf.Any] = status.getDetailsList.asScala.toSeq - - // Extracting error code id, error category id and optionally additional metadata. - assert { - rawDetails.collectFirst { - case any if any.is(classOf[ErrorInfo]) => - val v = any.unpack(classOf[ErrorInfo]) - assert(v.getReason == "MY_ERROR_CODE_ID") - assert(v.getMetadataMap.asScala.toMap == Map("category" -> "2", "foo" -> "bar")) - }.isDefined - } - - // Extracting full correlation id, if present. - assert { - rawDetails.collectFirst { - case any if any.is(classOf[RequestInfo]) => - val v = any.unpack(classOf[RequestInfo]) - assert(v.getRequestId == "full-correlation-id-123456790") - }.isDefined - } - - // Extracting retry information if the error is retryable. - assert { - rawDetails.collectFirst { - case any if any.is(classOf[RetryInfo]) => - val v = any.unpack(classOf[RetryInfo]) - assert(v.getRetryDelay.getSeconds == 123, v.getRetryDelay.getSeconds) - assert(v.getRetryDelay.getNanos == 456 * 1000 * 1000, v.getRetryDelay.getNanos) - }.isDefined - } - - // Extracting resource if the error pertains to some well defined resource. - assert { - rawDetails.collectFirst { - case any if any.is(classOf[ResourceInfo]) => - val v = any.unpack(classOf[ResourceInfo]) - assert(v.getResourceType == "CONTRACT_ID") - assert(v.getResourceName == "someContractId") - }.isDefined - } - } -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/utils/DecodedCantonError.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/utils/DecodedCantonError.scala deleted file mode 100644 index c1b384e690..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/utils/DecodedCantonError.scala +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error.utils - -import cats.implicits.toTraverseOps -import cats.syntax.either.* -import com.digitalasset.base.error.BaseError.RedactedMessage -import com.digitalasset.base.error.ErrorCategory.GenericErrorCategory -import com.digitalasset.base.error.{ - BaseError, - ErrorCategory, - ErrorCategoryRetry, - ErrorClass, - ErrorCode, - ErrorCodeMsg, - ErrorResource, - Grouping, - NoBaseLogging, -} -import com.google.common.annotations.VisibleForTesting -import com.google.protobuf.any -import com.google.rpc.error_details.{ErrorInfo, RequestInfo, ResourceInfo, RetryInfo} -import com.google.rpc.status.Status as RpcStatus -import io.grpc.{Status, StatusRuntimeException} -import org.slf4j.event.Level -import scalapb.{GeneratedMessage, GeneratedMessageCompanion} - -import scala.concurrent.duration.FiniteDuration -import scala.jdk.DurationConverters.* -import scala.util.Try - -/** Generic error class used for creating error instances from deserialized gRPC statuses that - * resulted from serialization of self-service error codes (children of [[BaseError]]). Its aim is - * to be used in client applications and components for simplifying programmatic inspection and/or - * enrichment of Canton errors received over-the-wire or from persistence. - * - * Note: Do NOT use this class for explicitly instantiating errors. Instead, use or create a - * fully-typed error instance. - */ -final case class DecodedCantonError( - code: ErrorCode, - cause: String, - correlationId: Option[String], - traceId: Option[String], - override val context: Map[String, String], - override val resources: Seq[(ErrorResource, String)], - override val definiteAnswerO: Option[Boolean] = None, -) extends BaseError { - def toRpcStatusWithForwardedRequestId: RpcStatus = super.rpcStatus()( - new NoBaseLogging(properties = Map.empty, correlationId = correlationId, traceId = traceId) - ) - - def retryIn: Option[FiniteDuration] = code.category.retryable.map(_.duration) - - def isRetryable: Boolean = retryIn.nonEmpty -} - -object DecodedCantonError { - - /** Deserializes a [[com.google.rpc.status.Status]] to [[DecodedCantonError]]. With the exception - * of throwables, all serialized error information is extracted, making this method an inverse of - * [[BaseError.rpcStatus]]. - */ - def fromGrpcStatus(status: RpcStatus): Either[String, DecodedCantonError] = { - val rawDetails = status.details - - val statusCode = Status.fromCodeValue(status.code).getCode - status.message match { - case RedactedMessage(correlationId, traceId) => - Right( - redactedError( - grpcCode = statusCode, - correlationId = correlationId, - traceId = traceId, - ) - ) - case _ => tryDeserializeStatus(status, rawDetails) - } - } - - def fromStatusRuntimeException( - statusRuntimeException: StatusRuntimeException - ): Either[String, DecodedCantonError] = - Either - .catchOnly[IllegalArgumentException]( - io.grpc.protobuf.StatusProto.fromThrowable(statusRuntimeException) - ) - .leftMap(ex => s"Failed to decode error from exception: ${ex.getMessage}") - .map(RpcStatus.fromJavaProto) - .flatMap(fromGrpcStatus) - - private def tryDeserializeStatus( - status: RpcStatus, - rawDetails: Seq[any.Any], - ): Either[String, DecodedCantonError] = - for { - errorInfoSeq <- extractErrorDetail[ErrorInfo](rawDetails) - errorInfo <- errorInfoSeq.exactlyOne - requestInfoSeq <- extractErrorDetail[RequestInfo](rawDetails) - requestInfoO <- requestInfoSeq.atMostOne - retryInfoSeq <- extractErrorDetail[RetryInfo](rawDetails) - retryInfo <- retryInfoSeq.atMostOne - resourceInfo <- extractErrorDetail[ResourceInfo](rawDetails) - resources = resourceInfo.map { resourceInfo => - ErrorResource(resourceInfo.resourceType) -> resourceInfo.resourceName - } - errorCategory <- extractErrorCategory( - errorInfo = errorInfo, - statusCode = status.code, - retryableDuration = retryInfo.flatMap(_.retryDelay).map(_.asJavaDuration.toScala), - ) - traceId = errorInfo.metadata.get("tid") - cause = extractCause(status) - correlationId = requestInfoO.collect { - case requestInfo if !traceId.contains(requestInfo.requestId) => requestInfo.requestId - } - } yield DecodedCantonError( - code = GenericErrorCode(id = errorInfo.reason, category = errorCategory), - cause = cause, - context = errorInfo.metadata, - resources = resources, - correlationId = correlationId, - traceId = traceId, - ) - - @VisibleForTesting - def unapply(throwable: Throwable): Option[DecodedCantonError] = throwable match { - case statusRuntimeException: StatusRuntimeException => - fromStatusRuntimeException(statusRuntimeException).toOption - case _other => None - } - - private def extractCause(status: RpcStatus) = - ErrorCodeMsg - .extract(status.message) - .map { case (_, _, _, cause) => cause } - // We don't guarantee backwards-compatibility for error message formats - // Hence fallback to the original cause on failure to parse - .getOrElse(status.message) - - private def redactedError( - grpcCode: Status.Code, - correlationId: Option[String], - traceId: Option[String], - ): DecodedCantonError = - DecodedCantonError( - code = GenericErrorCode( - id = "NA", - category = GenericErrorCategory( - grpcCode = Some(grpcCode), - logLevel = Level.ERROR, - retryable = None, - redactDetails = true, - // Security sensitive errors do not carry the category id - asInt = -1, - rank = 1, - ), - ), - cause = "A security-sensitive error has been received", - correlationId = correlationId, - traceId = traceId, - context = Map.empty, - resources = Seq.empty, - ) - - private def extractErrorCategory( - errorInfo: ErrorInfo, - statusCode: Int, - retryableDuration: Option[FiniteDuration], - ): Either[String, ErrorCategory] = { - def unknownCategory(categoryId: Int) = - GenericErrorCategory( - grpcCode = Some(Status.fromCodeValue(statusCode).getCode), - // If we log it, we use INFO since it's received from an - // external component - logLevel = Level.INFO, - retryable = retryableDuration.map(ErrorCategoryRetry.apply), - redactDetails = false, - asInt = categoryId, - rank = -1, - ) - - for { - categoryValue <- errorInfo.metadata - .get("category") - .toRight(s"category key not found in error metadata: ${errorInfo.metadata}") - categoryId <- Try(categoryValue.toInt).toEither.left.map(e => - s"Failed parsing category value: ${e.getMessage}" - ) - } yield ErrorCategory.all.find(_.asInt == categoryId).getOrElse(unknownCategory(categoryId)) - } - - private def extractErrorDetail[T <: GeneratedMessage]( - errorDetails: Seq[com.google.protobuf.any.Any] - )(implicit - expectedTypeCompanion: GeneratedMessageCompanion[T] - ): Either[String, List[T]] = - errorDetails.toList - .filter(_ is expectedTypeCompanion) - .traverse { errDetail => - Try(errDetail.unpack[T]).toEither.left.map(throwable => - s"Could not extract ${expectedTypeCompanion.scalaDescriptor.fullName} from error details: ${throwable.getMessage}" - ) - } - - private implicit class AritySelectors[T <: GeneratedMessage](seq: Seq[T])(implicit - expectedTypeCompanion: GeneratedMessageCompanion[T] - ) { - def atMostOne: Either[String, Option[T]] = - Either.cond(seq.sizeIs <= 1, seq.headOption, invalid("at most one")) - - def exactlyOne: Either[String, T] = seq match { - case Seq(errInfo) => Right(errInfo) - case _ => Left(invalid("exactly one")) - } - - private def invalid(times: String) = - s"Could not extract error detail. Expected $times ${expectedTypeCompanion.scalaDescriptor.fullName} in status details, but got ${seq.size}" - } - - /** Dummy error class for the purpose of creating the [[GenericErrorCode]]. It has no effect on - * documentation as its intended user ([[DecodedCantonError]]) does not appear in documentation. - */ - private implicit val genericErrorClass: ErrorClass = ErrorClass( - List(Grouping("generic", "ErrorClass")) - ) - - /** Generic wrapper for error codes received from deserialized gRPC-statuses */ - private final case class GenericErrorCode( - override val id: String, - override val category: ErrorCategory, - ) extends ErrorCode(id, category) -} diff --git a/canton/base/errors/src/main/scala/com/digitalasset/base/error/utils/ErrorDetails.scala b/canton/base/errors/src/main/scala/com/digitalasset/base/error/utils/ErrorDetails.scala deleted file mode 100644 index 8858ad69a2..0000000000 --- a/canton/base/errors/src/main/scala/com/digitalasset/base/error/utils/ErrorDetails.scala +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error.utils - -import com.digitalasset.base.error.ErrorCode -import com.google.protobuf -import com.google.rpc.{ErrorInfo, RequestInfo, ResourceInfo, RetryInfo} -import io.grpc.StatusRuntimeException -import io.grpc.protobuf.StatusProto - -import scala.concurrent.duration.* -import scala.jdk.CollectionConverters.* - -object ErrorDetails { - - sealed trait ErrorDetail extends Product with Serializable { - type T <: com.google.protobuf.Message - def toRpc: T - def toRpcAny: com.google.protobuf.Any = com.google.protobuf.Any.pack(toRpc) - } - - final case class ResourceInfoDetail(name: String, typ: String) extends ErrorDetail { - type T = ResourceInfo - def toRpc: ResourceInfo = - ResourceInfo.newBuilder().setResourceType(typ).setResourceName(name).build() - } - final case class ErrorInfoDetail(errorCodeId: String, metadata: Map[String, String]) - extends ErrorDetail { - type T = ErrorInfo - def toRpc: ErrorInfo = - ErrorInfo - .newBuilder() - .setReason(errorCodeId) - .putAllMetadata(metadata.asJava) - .build() - } - final case class RetryInfoDetail(duration: Duration) extends ErrorDetail { - type T = RetryInfo - def toRpc: RetryInfo = { - val millis = duration.toMillis - val fullSeconds = millis / 1000 - val remainderMillis = millis % 1000 - // Ensuring that we do not exceed max allowed value of nanos as documented in [[com.google.protobuf.Duration.Builder.setNanos]] - val remainderNanos = Math.min(remainderMillis * 1000 * 1000, 999999999).toInt - val protoDuration = com.google.protobuf.Duration - .newBuilder() - .setNanos(remainderNanos) - .setSeconds(fullSeconds) - .build() - RetryInfo - .newBuilder() - .setRetryDelay(protoDuration) - .build() - } - } - final case class RequestInfoDetail(correlationId: String) extends ErrorDetail { - type T = RequestInfo - def toRpc: RequestInfo = - RequestInfo - .newBuilder() - .setRequestId(correlationId) - .setServingData("") - .build() - } - - def from(status: com.google.rpc.Status): Seq[ErrorDetail] = - from(status.getDetailsList.asScala.toSeq) - def from(e: StatusRuntimeException): Seq[ErrorDetail] = - from(StatusProto.fromThrowable(e)) - - def from(anys: Seq[protobuf.Any]): Seq[ErrorDetail] = anys.toList.map { - case any if any.is(classOf[ResourceInfo]) => - val v = any.unpack(classOf[ResourceInfo]) - ResourceInfoDetail(typ = v.getResourceType, name = v.getResourceName) - - case any if any.is(classOf[ErrorInfo]) => - val v = any.unpack(classOf[ErrorInfo]) - ErrorInfoDetail(errorCodeId = v.getReason, metadata = v.getMetadataMap.asScala.toMap) - - case any if any.is(classOf[RetryInfo]) => - val v = any.unpack(classOf[RetryInfo]) - val delay = v.getRetryDelay - val duration = (delay.getSeconds.seconds + delay.getNanos.nanos).toCoarsest - RetryInfoDetail(duration = duration) - - case any if any.is(classOf[RequestInfo]) => - val v = any.unpack(classOf[RequestInfo]) - RequestInfoDetail(correlationId = v.getRequestId) - - case any => throw new IllegalStateException(s"Could not unpack value of: |$any|") - } - - /** @return - * whether a status runtime exception matches the error code. - * - * NOTE: This method is not suitable for: - * 1. security sensitive error codes (e.g. internal or authentication related) as they are - * stripped from all the details when being converted to instances of - * [[io.grpc.StatusRuntimeException]], - * 1. error codes that do not translate to gRPC level errors (i.e. error codes that don't have - * a corresponding gRPC status) - */ - def matches(e: StatusRuntimeException, errorCode: ErrorCode): Boolean = { - val matchesErrorCodeId = from(e).exists { - case ErrorInfoDetail(errorCodeId, _) => errorCodeId == errorCode.id - case _ => false - } - val matchesMessagePrefix = Option(e.getStatus.getDescription).exists(_.startsWith(errorCode.id)) - val matchesStatusCode = errorCode.category.grpcCode.contains(e.getStatus.getCode) - matchesErrorCodeId && matchesMessagePrefix && matchesStatusCode - } - - def matches(t: Throwable, errorCode: ErrorCode): Boolean = t match { - case e: StatusRuntimeException => matches(e, errorCode) - case _ => false - } -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorCodeSpec.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorCodeSpec.scala deleted file mode 100644 index b385f8ef10..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorCodeSpec.scala +++ /dev/null @@ -1,387 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.digitalasset.base.error.utils.{DecodedCantonError, ErrorDetails} -import com.google.rpc.Status -import io.grpc.Status.Code -import org.scalatest.EitherValues -import org.scalatest.concurrent.{Eventually, IntegrationPatience} -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import scala.concurrent.duration.* -import scala.jdk.CollectionConverters.* - -class ErrorCodeSpec - extends AnyFreeSpec - with Matchers - with Eventually - with IntegrationPatience - with ErrorsAssertions - with ScalaCheckDrivenPropertyChecks - with EitherValues { - - object FooErrorCodeRedacted - extends ErrorCode( - "FOO_ERROR_CODE_SECURITY_SENSITIVE", - ErrorCategory.SystemInternalAssumptionViolated, - )(ErrorClass.root()) - - object FooErrorCode - extends ErrorCode("FOO_ERROR_CODE", ErrorCategory.InvalidIndependentOfSystemState)( - ErrorClass.root() - ) - - classOf[ErrorCode].getSimpleName - { - - "meet test preconditions" in { - FooErrorCodeRedacted.category.redactDetails shouldBe true - FooErrorCode.category.redactDetails shouldBe false - FooErrorCodeRedacted.category.grpcCode shouldBe Some(Code.INTERNAL) - FooErrorCode.category.grpcCode shouldBe Some(Code.INVALID_ARGUMENT) - } - - val maxLength = 512 - - "create correct message" in { - FooErrorCode.toMsg( - cause = "cause123", - correlationId = Some("123correlationId"), - limit = Some(maxLength), - ) shouldBe "FOO_ERROR_CODE(8,123corre): cause123" - FooErrorCode.toMsg( - cause = "cause123", - correlationId = None, - limit = Some(maxLength), - ) shouldBe "FOO_ERROR_CODE(8,0): cause123" - FooErrorCode.toMsg( - cause = "x" * maxLength * 2, - correlationId = Some("123correlationId"), - limit = Some(maxLength), - ) shouldBe s"FOO_ERROR_CODE(8,123corre): ${"x" * maxLength}..." - FooErrorCodeRedacted.toMsg( - cause = "cause123", - correlationId = Some("123correlationId"), - limit = Some(maxLength), - ) shouldBe "FOO_ERROR_CODE_SECURITY_SENSITIVE(4,123corre): cause123" - } - - "create a minimal grpc status and exception" - { - - "when correlation-id and trace-id are not set" in { - testMinimalGrpcStatus(NoBaseLogging) - } - - "when only correlation-id is set" in { - testMinimalGrpcStatus( - new NoBaseLogging( - properties = Map.empty, - correlationId = Some("123correlationId"), - ) - ) - } - - "when only trace-id is set" in { - testMinimalGrpcStatus( - new NoBaseLogging( - properties = Map.empty, - // correlationId should be the traceId when not set - correlationId = Some("123traceId"), - traceId = Some("123traceId"), - ) - ) - } - - "when correlation-id and trace-id are set" in { - testMinimalGrpcStatus( - new NoBaseLogging( - properties = Map.empty, - correlationId = Some("123correlationId"), - traceId = Some("123traceId"), - ) - ) - } - - } - - "create a big grpc status and exception" - { - class FooErrorBig(override val code: ErrorCode) extends BaseError { - override val cause: String = "cause123" - - override def retryable: Option[ErrorCategoryRetry] = Some( - ErrorCategoryRetry(duration = 123.seconds + 456.milliseconds) - ) - - override def resources: Seq[(ErrorResource, String)] = - super.resources ++ - Seq[(ErrorResource, String)]( - ErrorResource.CommandId -> "commandId1", - ErrorResource.CommandId -> "commandId2", - ErrorResource.Party -> "party1", - ) - - override def context: Map[String, String] = - super.context ++ Map( - "contextKey1" -> "contextValue1", - "key????" -> "keyWithInvalidCharacters", - ) - - override def definiteAnswerO: Option[Boolean] = Some(false) - - override def throwableO: Option[Throwable] = - Some(new RuntimeException("runtimeException123")) - } - - val errorLoggerBig: BaseErrorLogger = new NoBaseLogging( - correlationId = Some("123correlationId"), - properties = Map( - "loggingEntryKey" -> "loggingEntryValue" - ), - ) - val requestInfo = ErrorDetails.RequestInfoDetail("123correlationId") - val retryInfo = ErrorDetails.RetryInfoDetail(123.seconds + 456.milliseconds) - - def getDetails(tested: ErrorCode) = Seq( - ErrorDetails - .ErrorInfoDetail( - tested.id, - Map( - "category" -> tested.category.asInt.toString, - "definite_answer" -> "false", - "loggingEntryKey" -> "loggingEntryValue", - "contextKey1" -> "contextValue1", - "key" -> "keyWithInvalidCharacters", - ), - ), - requestInfo, - retryInfo, - ErrorDetails.ResourceInfoDetail(name = "commandId1", typ = "COMMAND_ID"), - ErrorDetails.ResourceInfoDetail(name = "commandId2", typ = "COMMAND_ID"), - ErrorDetails.ResourceInfoDetail(name = "party1", typ = "PARTY"), - ) - - "not security sensitive" in { - val testedErrorCode = FooErrorCode - val details = getDetails(testedErrorCode) - final case class TestedError() extends FooErrorBig(testedErrorCode) - - val expectedStatus = Status - .newBuilder() - .setMessage("FOO_ERROR_CODE(8,123corre): cause123") - .setCode(testedErrorCode.category.grpcCode.value.value()) - .addAllDetails(details.map(_.toRpcAny).asJava) - .build() - val testedError = TestedError() - - assertStatus( - actual = ErrorCode.asGrpcStatus(testedError)(errorLoggerBig), - expected = expectedStatus, - ) - assertError( - actual = ErrorCode.asGrpcError(testedError)(errorLoggerBig), - expectedStatusCode = testedErrorCode.category.grpcCode.value, - expectedMessage = "FOO_ERROR_CODE(8,123corre): cause123", - expectedDetails = details, - ) - } - - "security sensitive" in { - val testedErrorCode = FooErrorCodeRedacted - final case class FooError() extends FooErrorBig(testedErrorCode) - val expectedStatus = Status - .newBuilder() - .setMessage( - BaseError.RedactedMessage(Some("123correlationId")) - ) - .setCode(testedErrorCode.category.grpcCode.value.value()) - .addDetails(requestInfo.toRpcAny) - .build() - val testedError = FooError() - testedError.logWithContext(Map.empty)(errorLoggerBig) - - assertStatus( - actual = ErrorCode.asGrpcStatus(testedError)(errorLoggerBig), - expected = expectedStatus, - ) - assertError( - actual = ErrorCode.asGrpcError(testedError)(errorLoggerBig), - expectedStatusCode = testedErrorCode.category.grpcCode.value, - expectedMessage = BaseError.RedactedMessage(Some("123correlationId")), - expectedDetails = Seq(requestInfo), - ) - } - - } - - "create a grpc status and exception for input exceeding details size limits" in { - class FooErrorBig(override val code: ErrorCode) extends BaseError { - override val cause: String = "cause123" - - override def context: Map[String, String] = - super.context ++ Map( - ("y" * ErrorCode.MaxErrorContentBytes) -> ("y" * ErrorCode.MaxErrorContentBytes) - ) - - override def retryable: Option[ErrorCategoryRetry] = Some( - ErrorCategoryRetry(duration = 123.seconds + 456.milliseconds) - ) - - override def resources: Seq[(ErrorResource, String)] = - super.resources ++ - Seq[(ErrorResource, String)]( - ErrorResource.CommandId -> "commandId1", - ErrorResource.CommandId -> "commandId2", - ErrorResource.Party -> "party1", - ErrorResource.Party -> ("x" * ErrorCode.MaxErrorContentBytes), - ) - - override def definiteAnswerO: Option[Boolean] = Some(false) - } - val errorLoggerOversized: BaseErrorLogger = new NoBaseLogging( - correlationId = Some("123correlationId"), - properties = Map( - "loggingEntryKey" -> "loggingEntryValue", - "loggingEntryValueTooBig" -> ("x" * ErrorCode.MaxErrorContentBytes), - ("x" * ErrorCode.MaxErrorContentBytes) -> "loggingEntryKeyTooBig", - ), - ) - val requestInfo = ErrorDetails.RequestInfoDetail("123correlationId") - val retryInfo = ErrorDetails.RetryInfoDetail(123.seconds + 456.milliseconds) - - val testedErrorCode = FooErrorCode - final case class TestedError() extends FooErrorBig(FooErrorCode) - val testedError = TestedError() - - val expectedDetails = Seq( - ErrorDetails - .ErrorInfoDetail( - testedErrorCode.id, - Map( - "category" -> testedErrorCode.category.asInt.toString, - "definite_answer" -> "false", - "loggingEntryKey" -> "loggingEntryValue", - "loggingEntryValueTooBig" -> ("x" * 849 + "..."), - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -> "loggingEntryKeyTooBig", - "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" -> ("y" * 809 + "..."), - ), - ), - requestInfo, - retryInfo, - ErrorDetails.ResourceInfoDetail(name = "commandId1", typ = "COMMAND_ID"), - ErrorDetails.ResourceInfoDetail(name = "commandId2", typ = "COMMAND_ID"), - ErrorDetails.ResourceInfoDetail(name = "party1", typ = "PARTY"), - ) - val expectedMessage = "FOO_ERROR_CODE(8,123corre): cause123" - val expectedStatus = Status - .newBuilder() - .setMessage(expectedMessage) - .setCode(testedErrorCode.category.grpcCode.value.value()) - .addAllDetails(expectedDetails.map(_.toRpcAny).asJava) - .build() - - assertStatus( - actual = ErrorCode.asGrpcStatus(testedError)(errorLoggerOversized), - expected = expectedStatus, - ) - assertError( - actual = ErrorCode.asGrpcError(testedError)(errorLoggerOversized), - expectedStatusCode = testedErrorCode.category.grpcCode.value, - expectedMessage = expectedMessage, - expectedDetails = expectedDetails, - ) - } - - "do not exceed the safe-to-serialize limit" in { - implicit val generatorDrivenConfig: PropertyCheckConfiguration = - PropertyCheckConfiguration(minSuccessful = 100) - - // The description gets added to the io.grpc.Status and to the StatusProto as well - // so we must leave some space for it - forAll(ErrorGenerator.defaultErrorGen) { err => - whenever( - ErrorCode - // Ensure we evaluate the test only for errors that must be truncated - .asGrpcStatus(err, ErrorCode.MaxErrorContentBytes * 10)(err.logger) - .getSerializedSize > ErrorCode.MaxErrorContentBytes - ) { - val protoResult = err.asGrpcStatus - val serializedSize = protoResult.getSerializedSize - - serializedSize should be <= ErrorCode.MaxErrorContentBytes withClue s"for $err" - } - } - } - - "truncate the trace-id if abnormaly large" in { - val errWithLargeTraceId = - ErrorGenerator.defaultErrorGen.sample.value.copy(traceId = Some("x" * 1000)).asGrpcError - - DecodedCantonError - .fromStatusRuntimeException(errWithLargeTraceId) - .value - .traceId - .value shouldBe ("x" * 253 + "...") - } - - "truncate the correlation-id if abnormaly large" in { - val errWithLargeTraceId = - ErrorGenerator.defaultErrorGen.sample.value - .copy(correlationId = Some("x" * 1000)) - .asGrpcError - - DecodedCantonError - .fromStatusRuntimeException(errWithLargeTraceId) - .value - .correlationId - .value shouldBe ("x" * 253 + "...") - } - } - - def testMinimalGrpcStatus(errorLoggerSmall: BaseErrorLogger): Unit = { - class FooErrorMinimal(override val code: ErrorCode) extends BaseError { - override val cause: String = "cause123" - } - val testedErrorCode = FooErrorCode - val id = errorLoggerSmall.correlationId.orElse(errorLoggerSmall.traceId) - val idTruncated = id.getOrElse("0").take(8) - final case class TestedError() extends FooErrorMinimal(testedErrorCode) - val details = Seq( - ErrorDetails - .ErrorInfoDetail( - testedErrorCode.id, - Map( - "category" -> testedErrorCode.category.asInt.toString - ) ++ errorLoggerSmall.traceId.fold(Map.empty[String, String])(tid => Map("tid" -> tid)), - ) - ) ++ id - .map(correlationId => - ErrorDetails.RequestInfoDetail( - correlationId = correlationId - ) - ) - .toList - - val expected = Status - .newBuilder() - .setMessage(s"FOO_ERROR_CODE(8,$idTruncated): cause123") - .setCode(Code.INVALID_ARGUMENT.value()) - .addAllDetails(details.map(_.toRpcAny).asJava) - .build() - val testedError = TestedError() - - assertStatus( - actual = ErrorCode.asGrpcStatus(testedError)(errorLoggerSmall), - expected = expected, - ) - assertError( - actual = ErrorCode.asGrpcError(testedError)(errorLoggerSmall), - expectedStatusCode = testedErrorCode.category.grpcCode.value, - expectedMessage = s"FOO_ERROR_CODE(8,$idTruncated): cause123", - expectedDetails = details, - ) - } - -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorGenerator.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorGenerator.scala deleted file mode 100644 index 63b4068341..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorGenerator.scala +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import org.scalacheck.{Arbitrary, Gen} - -object ErrorGenerator { - final case class RichTestError( - errorCode: ErrorCode, - override val correlationId: Option[String] = None, - override val traceId: Option[String] = None, - contextMap: Map[String, Any] = Map(), - loggingProperties: Map[String, String] = Map(), - override val cause: String, - override val throwableO: Option[Throwable] = None, - override val definiteAnswerO: Option[Boolean] = None, - override val resources: Seq[(ErrorResource, String)] = Seq(), - ) extends ContextualizedDamlError( - cause, - throwableO, - contextMap, - )(errorCode, new NoBaseLogging(loggingProperties, correlationId, traceId)) - - private final case class TestErrorCode(override val id: String, errorCategory: ErrorCategory) - extends ErrorCode(id, errorCategory)(ErrorClass.root()) {} - - private[error] def asciiPrintableStrOfN(maxSize: Int) = for { - chars <- Gen.listOfN(maxSize, Gen.alphaNumChar) - } yield chars.mkString - - private val errorResourceGen = for { - typ <- asciiPrintableStrOfN(256).map(ErrorResource(_)) - msg <- asciiPrintableStrOfN(1024) - } yield (typ, msg) - - private val contextMapGen = for { - contextMap <- Gen.mapOfN( - 50, - for { - k <- asciiPrintableStrOfN(256) - v <- asciiPrintableStrOfN(256) - } yield (k, v), - ) - } yield contextMap - - val defaultErrorGen: Gen[RichTestError] = errorGenerator(None) - - def errorGenerator( - redactDetails: Option[Boolean], - additionalErrorCategoryFilter: ErrorCategory => Boolean = _ => true, - ): Gen[RichTestError] = - for { - errorCodeId <- Gen.listOfN(63, Gen.alphaUpperChar).map(_.mkString) - category <- Gen.oneOf( - redactDetails - .fold(ErrorCategory.all)(redact => ErrorCategory.all.filter(_.redactDetails == redact)) - .filter(additionalErrorCategoryFilter) - ) - errorCode = TestErrorCode(errorCodeId, category) - correlationId <- asciiPrintableStrOfN( - SerializableErrorCodeComponents.MaxTraceIdCorrelationIdSize - ).map(Option(_).filter(_.nonEmpty)) - traceId <- asciiPrintableStrOfN(SerializableErrorCodeComponents.MaxTraceIdCorrelationIdSize) - .map(Option(_).filter(_.nonEmpty)) - message <- asciiPrintableStrOfN(2000) - definiteAnswerO <- Arbitrary.arbitrary[Option[Boolean]] - errorResources <- Gen.listOfN(50, errorResourceGen) - extraContextMap <- contextMapGen - loggingProperties <- contextMapGen - throwableO <- Gen.option(Gen.asciiPrintableStr.map(new RuntimeException(_))) - } yield RichTestError( - errorCode = errorCode, - correlationId = correlationId, - traceId = traceId, - contextMap = extraContextMap, - loggingProperties = loggingProperties, - cause = message, - definiteAnswerO = definiteAnswerO, - throwableO = throwableO, - resources = errorResources, - ) -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorGroupSpec.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorGroupSpec.scala deleted file mode 100644 index 813215d124..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorGroupSpec.scala +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import org.scalatest.BeforeAndAfter -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class ErrorGroupSpec extends AnyFlatSpec with Matchers with BeforeAndAfter { - - object ErrorGroupBar extends ErrorGroup()(ErrorClass.root()) - - object ErrorGroupFoo1 extends ErrorGroup()(ErrorClass.root()) { - object ErrorGroupFoo2 extends ErrorGroup() { - object ErrorGroupFoo3 extends ErrorGroup() - } - } - - it should "resolve correct error group names" in { - ErrorGroupFoo1.ErrorGroupFoo2.ErrorGroupFoo3.errorClass shouldBe ErrorClass( - List( - Grouping("ErrorGroupFoo1", ErrorGroupFoo1.fullClassName), - Grouping("ErrorGroupFoo2", ErrorGroupFoo1.ErrorGroupFoo2.fullClassName), - Grouping( - "ErrorGroupFoo3", - ErrorGroupFoo1.ErrorGroupFoo2.ErrorGroupFoo3.fullClassName, - ), - ) - ) - ErrorGroupBar.errorClass shouldBe ErrorClass( - List(Grouping("ErrorGroupBar", ErrorGroupBar.fullClassName)) - ) - } - -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorsAssertions.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorsAssertions.scala deleted file mode 100644 index a476ea71ef..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/ErrorsAssertions.scala +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.digitalasset.base.error.utils.ErrorDetails -import com.digitalasset.base.error.utils.ErrorDetails.{ErrorInfoDetail, RequestInfoDetail} -import io.grpc.Status.Code -import io.grpc.StatusRuntimeException -import io.grpc.protobuf.StatusProto -import org.scalatest.Checkpoints.Checkpoint -import org.scalatest.matchers.should.Matchers -import org.scalatest.{AppendedClues, Assertion, OptionValues} - -import scala.jdk.CollectionConverters.* - -trait ErrorsAssertions extends Matchers with OptionValues with AppendedClues { - - /** NOTE: This method is not suitable for: - * 1. security sensitive error codes (e.g. internal or authentication related) as they are - * stripped from all the details when being converted to instances of - * [[StatusRuntimeException]], - * 1. error codes that do not translate to gRPC level errors (i.e. error codes that don't have - * a corresponding gRPC status) - */ - def assertMatchesErrorCode( - actual: StatusRuntimeException, - expectedErrorCode: ErrorCode, - ): Assertion = { - val actualErrorCodeId = ErrorDetails.from(actual).collectFirst { - case ErrorInfoDetail(errorCodeId, _) => errorCodeId - } - val actualDescription = Option(actual.getStatus.getDescription) - val actualStatusCode = actual.getStatus.getCode - val cp = new Checkpoint - cp(actualErrorCodeId.value shouldBe expectedErrorCode.id) - cp(Some(actualStatusCode) shouldBe expectedErrorCode.category.grpcCode) - cp(actualDescription.value should startWith(expectedErrorCode.id)) - cp.reportAll() - succeed - } - - def assertStatus( - actual: com.google.rpc.Status, - expected: com.google.rpc.Status, - ): Assertion = { - val actualDetails = ErrorDetails.from(actual) - val expectedDetails = ErrorDetails.from(expected) - val actualDescription = Option(actual.getMessage) - val expectedDescription = Option(expected.getMessage) - val actualStatusCode = actual.getCode - val expectedStatusCode = expected.getCode - val cp = new Checkpoint - cp(actualDescription shouldBe expectedDescription) - cp { - actualStatusCode shouldBe expectedStatusCode withClue (s", expecting status code: '$expectedStatusCode''") - } - cp(actualDetails should contain theSameElementsAs expectedDetails) - cp.reportAll() - succeed - } - - /** Asserts that the two errors have the same code, message and details. - */ - def assertError( - actual: StatusRuntimeException, - expected: StatusRuntimeException, - ): Unit = { - val expectedStatus = StatusProto.fromThrowable(expected) - val expectedDetails = expectedStatus.getDetailsList.asScala.toSeq - assertError( - actual = actual, - expectedStatusCode = expected.getStatus.getCode, - expectedMessage = expectedStatus.getMessage, - expectedDetails = ErrorDetails.from(expectedDetails), - ) - } - - /** @param verifyEmptyStackTrace - * should be enabled for the server-side testing and disabled for the client side testing - */ - def assertError( - actual: StatusRuntimeException, - expectedStatusCode: Code, - expectedMessage: String, - expectedDetails: Seq[ErrorDetails.ErrorDetail], - verifyEmptyStackTrace: Boolean = true, - ): Unit = { - val actualStatus = StatusProto.fromThrowable(actual) - val actualDetails = actualStatus.getDetailsList.asScala.toSeq - val cp = new Checkpoint - cp(actual.getStatus.getCode shouldBe expectedStatusCode) - cp(actualStatus.getMessage shouldBe expectedMessage) - cp { - ErrorDetails.from(actualDetails) should contain theSameElementsAs expectedDetails - } - if (verifyEmptyStackTrace) { - cp { - actual.getStackTrace.length shouldBe 0 withClue ("it should contain no stacktrace") - } - } - cp(actual.getCause shouldBe null) - cp.reportAll() - } - - /** Asserts that the error has the expected code and matches the form of the message and details. - */ - def assertError( - actual: StatusRuntimeException, - expectedStatusCode: Code, - expectedMessage: String => String, - expectedDetails: String => String => Seq[ErrorDetails.ErrorDetail], - ): Unit = { - val actualStatus = StatusProto.fromThrowable(actual) - val actualDetails = actualStatus.getDetailsList.asScala.toSeq - val errorDetails = ErrorDetails.from(actualDetails) - val tid = errorDetails.collectFirst { case RequestInfoDetail(tid) => tid }.value - val errorInfoDetail = - errorDetails.collectFirst { case detail: ErrorInfoDetail => detail }.value - val submissionId = errorInfoDetail.metadata.get("submissionId").value - assertError( - actual, - expectedStatusCode, - expectedMessage(tid), - expectedDetails(tid)(submissionId), - verifyEmptyStackTrace = false, - ) - - } - -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/GrpcStatusesSpec.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/GrpcStatusesSpec.scala deleted file mode 100644 index 6783693c0b..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/GrpcStatusesSpec.scala +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import com.google.protobuf.any -import com.google.rpc.error_details.ErrorInfo -import com.google.rpc.status.Status -import org.scalatest.matchers.should.Matchers -import org.scalatest.prop.TableDrivenPropertyChecks.* -import org.scalatest.wordspec.AnyWordSpec - -import GrpcStatuses.DefiniteAnswerKey - -class GrpcStatusesSpec extends AnyWordSpec with Matchers { - "isDefiniteAnswer" should { - "return correct value" in { - val anErrorInfo = ErrorInfo.of("reason", "synchronizer", Map.empty) - val testCases = Table( - ("Description", "Error Info", "Expected"), - ( - "ErrorInfo contains definite answer key and its value is true", - Some(anErrorInfo.copy(metadata = Map(DefiniteAnswerKey -> "true"))), - true, - ), - ( - "ErrorInfo contains definite answer key and its value is false", - Some(anErrorInfo.copy(metadata = Map(DefiniteAnswerKey -> "false"))), - false, - ), - ( - "ignore casing of value associated to definite answer key (#1)", - Some(anErrorInfo.copy(metadata = Map(DefiniteAnswerKey -> "TRUE"))), - true, - ), - ( - "ignore casing of value associated to definite answer key (#2)", - Some(anErrorInfo.copy(metadata = Map(DefiniteAnswerKey -> "True"))), - true, - ), - ( - "ErrorInfo does not contain definite answer key", - Some(anErrorInfo.copy(metadata = Map("some" -> "key"))), - false, - ), - ("no ErrorInfo is available", None, false), - ) - - forAll(testCases) { case (_, errorInfoMaybe, expected) => - val details = - errorInfoMaybe.map(errorInfo => any.Any.pack(errorInfo)).map(Seq(_)).getOrElse(Seq.empty) - val inputStatus = Status.of(123, "an error", details) - GrpcStatuses.isDefiniteAnswer(inputStatus) should be(expected) - } - } - } - -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/RedactedMessageSpec.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/RedactedMessageSpec.scala deleted file mode 100644 index 68cd315d0c..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/RedactedMessageSpec.scala +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import org.scalacheck.Gen -import org.scalatest.Inside -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks - -import BaseError.RedactedMessage - -class RedactedMessageSpec extends AnyFlatSpec with ScalaCheckPropertyChecks with Inside { - private val traceIdGen = - Gen.option(Gen.asciiPrintableStr).filterNot(_.exists(v => v.isEmpty || v == "")) - private val correlationIdGen = Gen - .option(Gen.asciiPrintableStr) - .filterNot(_.exists(v => v.isEmpty || v == "")) - - RedactedMessage.getClass.getSimpleName should "correctly construct and extract the security message fields" in { - forAll(correlationIdGen, traceIdGen) { (corrIdO, tIdO) => - inside(RedactedMessage(corrIdO, tIdO)) { case RedactedMessage(`corrIdO`, `tIdO`) => - succeed - } - } - } -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/SerializableErrorComponentsSpec.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/SerializableErrorComponentsSpec.scala deleted file mode 100644 index 67575161ad..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/SerializableErrorComponentsSpec.scala +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error - -import org.scalacheck.Gen -import org.scalatest.Assertions.fail -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{EitherValues, Inside, OptionValues} -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import ErrorGenerator.asciiPrintableStrOfN -import SerializableErrorComponentsSpec.* - -class SerializableErrorComponentsSpec - extends AnyFlatSpec - with Matchers - with EitherValues - with Inside - with OptionValues - with ScalaCheckDrivenPropertyChecks { - - behavior of "truncateDetails" - - it should "not change details if reasonably-sized and valid" in { - val (truncatedContext, truncatedResources) = - NonSecuritySensitiveErrorCodeComponents.truncateDetails( - context = rawContextMap, - errResources = rawErrorResources, - remainingBudgetBytes = 1000, - ) - - truncatedContext should contain theSameElementsAs rawContextMap - truncatedResources should contain theSameElementsAs comparable(rawErrorResources) - } - - it should "truncate context map and resources when oversize" in { - val cMap = - Map( - "k1".padTo(20, 'a') -> cValue(20), - "k2".padTo(30, 'a') -> cValue(40), - "k3".padTo(20, 'a') -> cValue(100), - "k4".padTo(50, 'a') -> cValue(160), - "k5".padTo(60, 'a') -> cValue(320), - ) - - val errRes: Seq[(ErrorResource, String)] = - (1 to 10).map(i => ErrorResource.CommandId -> s"some-command-id-$i".padTo(50, 'a')) - - val (truncatedContext, truncatedResources) = - NonSecuritySensitiveErrorCodeComponents.truncateDetails( - context = cMap, - errResources = errRes, - remainingBudgetBytes = 1000, - ) - - val expected = Map( - "k1".padTo(20, 'a') -> cValue(20), - "k2".padTo(30, 'a') -> truncatedValue("", 31), - "k3".padTo(20, 'a') -> truncatedValue("", 41), - truncatedValue("k4", 30) -> truncatedValue("", 31), - truncatedValue("k5", 30) -> truncatedValue("", 31), - ) - truncatedContext should contain theSameElementsAs expected - truncatedResources should contain theSameElementsAs comparable(errRes.take(5)) - } - - behavior of "truncateContext" - - it should "validate context map keys" in { - val invalidKeyEntry = "%k4!" -> cValue(50) - val invalidKeyEntryCorrectSize = "%k5!".padTo(65, 'a') -> cValue(50) - val invalidKeyEntryOversize = "%k6!".padTo(100, 'a') -> cValue(50) - val correctKeyOversize = "k7-size".padTo(64, 'a') -> cValue(50) - - val truncatedContext = - NonSecuritySensitiveErrorCodeComponents.truncateContext( - rawContextEntries = rawContextMap.toVector ++ Vector( - invalidKeyEntry, - invalidKeyEntryCorrectSize, - invalidKeyEntryOversize, - correctKeyOversize, - ), - maxBudgetBytes = 1000, - ) - - truncatedContext should contain theSameElementsAs rawContextMap.toVector ++ Vector( - "k4" -> cValue(50), - "k5".padTo(63, 'a') -> cValue(50), - "k6".padTo(63, 'a') -> cValue(50), - "k7-size".padTo(63, 'a') -> cValue(50), - ) - } - - it should "return an empty list on empty input" in { - NonSecuritySensitiveErrorCodeComponents.truncateContext( - rawContextEntries = Vector.empty, - maxBudgetBytes = 4096, - ) shouldBe empty - } - - it should "return an empty list on too small budget" in { - NonSecuritySensitiveErrorCodeComponents.truncateContext( - rawContextEntries = Vector("1" -> "23", "123" -> "45", "12345" -> "678"), - maxBudgetBytes = 3, - ) shouldBe empty - } - - it should "not add context map entries if one from the pair is empty" in { - val truncatedContext = - NonSecuritySensitiveErrorCodeComponents.truncateContext( - rawContextEntries = rawContextMap.toVector ++ Vector( - "" -> cValue(50), - "k" -> "", - // After key is cleaned, it remains empty - "$!@" -> cValue(50), - "" -> "", - ), - maxBudgetBytes = 1000, - ) - - truncatedContext should contain theSameElementsAs rawContextMap - } - - it should "pack everything on exact size" in { - val validContextMapGen = for { - contextMap <- Gen.mapOfN( - 50, - for { - k <- asciiPrintableStrOfN(63) - v <- asciiPrintableStrOfN(63) - } yield (k, v), - ) - } yield contextMap - - forAll(validContextMapGen) { map => - whenever(!map.exists { case (k, v) => k.isEmpty || v.isEmpty }) { - val input = map.toVector - NonSecuritySensitiveErrorCodeComponents.truncateContext( - rawContextEntries = input, - maxBudgetBytes = input.map(v => v._1.length + v._2.length).sum, - ) should contain theSameElementsAs input - } - } - } - - it should "too small entries that need to be truncated are skipped instead" in { - NonSecuritySensitiveErrorCodeComponents.truncateContext( - rawContextEntries = Vector("123" -> "456"), - maxBudgetBytes = 5, - ) shouldBe empty - - NonSecuritySensitiveErrorCodeComponents.truncateContext( - rawContextEntries = Vector("1" -> "23", "123" -> "45", "12345" -> "678"), - maxBudgetBytes = 8, - ) should contain theSameElementsAs Vector("12345" -> "678") - } - - it should "truncate bigger entries more" in { - val inputSize = 20 - // Input with entries sorted by increasing size - val input = (1 to inputSize).map(idx => s"k$idx".padTo(5 + idx * 2, 'a') -> cValue(idx * 2)) - val requestedEntriesSize = input.map(v => v._1.length + v._2.length).sum - val output = - NonSecuritySensitiveErrorCodeComponents.truncateContext( - rawContextEntries = input, - maxBudgetBytes = requestedEntriesSize / 2, - ) - - def sum(s: Seq[(String, String)]) = s.map(v => v._1.length + v._2.length).sum - def truncationPercentage(in: Seq[(String, String)], out: Seq[(String, String)]): Double = - sum(out).toDouble / sum(in).toDouble - def avgSize(s: Seq[(String, String)]) = sum(s).toDouble / s.size - - val (inputFirstHalf, inputSecondHalf) = input.splitAt(inputSize / 2) - val (outputFirstHalf, outputSecondHalf) = output.splitAt(inputSize / 2) - - val truncPCSmaller = truncationPercentage(inputFirstHalf, outputFirstHalf) - val truncPCBigger = truncationPercentage(inputSecondHalf, outputSecondHalf) - - // Check that truncation percentage is lower for the for smaller entries - truncPCSmaller should be > truncPCBigger - - // Check that the average size still remains higher for bigger entries - avgSize(outputFirstHalf) < avgSize(outputSecondHalf) - } -} - -private object SerializableErrorComponentsSpec { - val rawContextMap: Map[String, String] = - Map("key" -> cValue(10), "key-max-size".padTo(63, 'a') -> cValue(50)) - val rawErrorResources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.CommandId -> "some-command-id", - ErrorResource.ContractKey -> "some-contract-key", - ) - - private def cValue(size: Int): String = "a" * size - private def truncatedValue(prefix: String, size: Int): String = - if (size < 3 + prefix.length) fail(s"size: $size") - else s"$prefix${cValue(size - 3 - prefix.length)}..." - - private def comparable(res: Seq[(ErrorResource, String)]): Seq[(String, String)] = - res.map { case (k, v) => k.asString -> v } -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/samples/SampleClientSideSpec.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/samples/SampleClientSideSpec.scala deleted file mode 100644 index 08c1200141..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/samples/SampleClientSideSpec.scala +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error.samples - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class SampleClientSideSpec extends AnyFlatSpec with Matchers { - - it should "run successfully" in { - SampleClientSide.example() - } - -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/BenignError.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/BenignError.scala deleted file mode 100644 index 9dd9aa268d..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/BenignError.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error.utils - -import com.digitalasset.base.error.{ - BaseErrorLogger, - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorClass, - ErrorCode, - Explanation, - Resolution, -} - -@Explanation("Not the end of the world.") -@Resolution( - "Retry re-submitting the request. If the error persists, contact the participant operator." -) -case object BenignError - extends ErrorCode("BENIGN_ERROR", ErrorCategory.TransientServerFailure)( - ErrorClass.root() - ) { - - final case class Reject(serviceName: String)(implicit - loggingContext: BaseErrorLogger - ) extends DamlErrorWithDefiniteAnswer( - cause = s"Benign problem in $serviceName.", - extraContext = Map("service_name" -> serviceName), - ) -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/DecodedCantonErrorSpec.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/DecodedCantonErrorSpec.scala deleted file mode 100644 index 3b733b38bb..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/DecodedCantonErrorSpec.scala +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error.utils - -import com.digitalasset.base.error.{ - BaseError, - BaseErrorLogger, - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorClass, - ErrorCode, - ErrorResource, - NoBaseLogging, -} -import com.google.protobuf.any.Any -import com.google.protobuf.any.Any.toJavaProto -import com.google.rpc.error_details.{ErrorInfo, RequestInfo, ResourceInfo, RetryInfo} -import com.google.rpc.status.Status as RpcStatus -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, EitherValues, OptionValues} -import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks -import scalapb.{GeneratedMessage, GeneratedMessageCompanion} - -import ErrorDetails.ErrorDetail - -class DecodedCantonErrorSpec - extends AnyFlatSpec - with Matchers - with EitherValues - with OptionValues - with ScalaCheckPropertyChecks { - behavior of DecodedCantonError.getClass.getSimpleName - - it should s"correctly deserialize a gRPC status code to a ${BaseError.getClass.getSimpleName}" in - forAll(errorCategoriesTable) { errorCategory => - forAll(propertyMapTable) { propertyMap => - forAll(correlationIdTable) { correlationId => - forAll(traceIdTable) { traceId => - val fromGrpc: RpcStatus => DecodedCantonError = - DecodedCantonError.fromGrpcStatus(_).value - val toGrpc: DecodedCantonError => RpcStatus = _.toRpcStatusWithForwardedRequestId - - val someGrpcStatus = - createGrpcStatus(errorCategory, propertyMap, correlationId, traceId) - - checkEquivalence(someGrpcStatus, toGrpc(fromGrpc(someGrpcStatus))) - } - } - } - } - - it should "fallback to the original cause if error message format is not recognized" in { - implicit val contextErrorLogger: BaseErrorLogger = NoBaseLogging - - val nonStandardMessage = "!!!Some non-standard message" - val error = BenignError.Reject("nvm").rpcStatus().copy(message = nonStandardMessage) - val deserializedError = DecodedCantonError.fromGrpcStatus(error).value - - deserializedError.cause shouldBe nonStandardMessage - } - - private val errorGrpcStatus = - createGrpcStatus(ErrorCategory.TransientServerFailure, Map("1" -> "2"), Some("c"), Some("t")) - - it should "return a Left on missing category information in error metadata" in { - val errInfo = errorGrpcStatus.details.find(_ is ErrorInfo).value.unpack[ErrorInfo] - val modifiedErrInfo = errInfo.copy(metadata = errInfo.metadata.removed("category")) - val modifiedErrorGrpcStatus = errorGrpcStatus.copy(details = - errorGrpcStatus.details.filterNot(_ is ErrorInfo) :+ Any.pack(modifiedErrInfo) - ) - - DecodedCantonError.fromGrpcStatus(modifiedErrorGrpcStatus).left.value should include( - "category key not found in error metadata" - ) - } - - it should s"return a Left on invalid number of ${classOf[ErrorInfo].getSimpleName} in the gRPC status" in { - testErrorDetails[ErrorInfo]( - "exactly one", - Map(0 -> false, 1 -> true, 2 -> false), - ) - } - - it should s"return a Left on invalid number of ${classOf[RequestInfo].getSimpleName} in the gRPC status" in { - testErrorDetails[RequestInfo]("at most one", Map(0 -> true, 1 -> true, 2 -> false)) - } - - it should s"return a Left on invalid number of ${classOf[RetryInfo].getSimpleName} in the gRPC status" in { - testErrorDetails[RetryInfo]("at most one", Map(0 -> true, 1 -> true, 2 -> false)) - } - - it should s"allow any number of ${classOf[ResourceInfo].getSimpleName} in the gRPC status" in { - testErrorDetails[ResourceInfo]("doesn't matter", Map(0 -> true, 1 -> true, 2 -> true)) - } - - it should s"use ${ErrorCategory.UnredactedSecurityAlert} for an unredacted security alert" in { - val status = - createGrpcStatus(ErrorCategory.UnredactedSecurityAlert, Map.empty, Some("c"), Some("t")) - val decoded = DecodedCantonError.fromGrpcStatus(status).value - decoded.code.code.category shouldBe ErrorCategory.UnredactedSecurityAlert - } - - it should s"handle redacted security alerts" in { - val status = createGrpcStatus(ErrorCategory.SecurityAlert, Map.empty, Some("c"), Some("t")) - val decoded = DecodedCantonError.fromGrpcStatus(status).value - decoded.cause shouldBe "A security-sensitive error has been received" - decoded.correlationId shouldBe Some("c") - decoded.traceId shouldBe Some("t") - } - - private def testErrorDetails[T <: GeneratedMessage]( - arityMsg: String, - allowedNumbers: Map[Int, Boolean], - )(implicit - expectedTypeCompanion: GeneratedMessageCompanion[T] - ): Assertion = { - allowedNumbers.foreach { case (times, expectPass) => - val errorDetails = errorGrpcStatus.details - val errDetail = errorDetails.find(_ is expectedTypeCompanion).value - val modifiedStatus = errorGrpcStatus.copy(details = - errorDetails.filterNot(_ is expectedTypeCompanion) ++ (1 to times).map(_ => errDetail) - ) - - val actual = DecodedCantonError.fromGrpcStatus(modifiedStatus) - - if (expectPass) - actual.isRight shouldBe true - else - actual shouldBe Left( - s"Could not extract error detail. Expected $arityMsg ${expectedTypeCompanion.scalaDescriptor.fullName} in status details, but got $times" - ) - } - succeed - } - - private def createGrpcStatus( - errorCategory: ErrorCategory, - propertyMap: Map[String, String], - correlationId: Option[String], - traceId: Option[String], - ) = { - implicit val contextErrorLogger: NoBaseLogging = - new NoBaseLogging(propertyMap, correlationId, traceId) - implicit val errorCode: ErrorCode = - new ErrorCode("SOME_ERROR_CODE_ID", errorCategory)(ErrorClass(List.empty)) {} - { - new DamlErrorWithDefiniteAnswer( - cause = "Some cause", - throwableO = Some(new RuntimeException("oups")), - definiteAnswer = true, - extraContext = Map("key" -> "val"), - ) { - override def resources: Seq[(ErrorResource, String)] = - super.resources :+ (ErrorResource.CommandId -> "some-cmd-id") - } - }.rpcStatus() - } - - private def propertyMapTable = Table( - "property map", - Map("p1" -> "k1", "p2" -> "k2"), - Map("p1" -> "k1"), - Map.empty[String, String], - ) - - private def correlationIdTable = Table( - "correlation id", - Some("corr-id"), - None, - ) - - private def traceIdTable = Table("trace id", Some("trace-id"), None) - - private def errorCategoriesTable = { - val grpcAwareErrorCats = ErrorCategory.all.filter(_.grpcCode.nonEmpty) - Table( - "error category", - grpcAwareErrorCats* - ) - } - - // Manual equality check for rpc statuses since encoding of maps (error details) - // in ByteString is non-deterministic - private def checkEquivalence( - rpcStatusOriginal: RpcStatus, - rpcStatusFromReconstructedError: RpcStatus, - ): Assertion = { - def refine(details: Seq[com.google.protobuf.any.Any]): Seq[ErrorDetail] = - ErrorDetails.from(details.map(toJavaProto)) - - rpcStatusFromReconstructedError.code shouldBe rpcStatusOriginal.code - rpcStatusFromReconstructedError.message shouldBe rpcStatusOriginal.message - val recontructedErrorDetails = refine(rpcStatusFromReconstructedError.details) - val originalErrorDetails = refine(rpcStatusOriginal.details) - recontructedErrorDetails shouldBe originalErrorDetails - } -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/ErrorDetailsSpec.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/ErrorDetailsSpec.scala deleted file mode 100644 index 0bc5c267c1..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/ErrorDetailsSpec.scala +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error.utils - -import com.digitalasset.base.error.ErrorCategory.BackgroundProcessDegradationWarning -import com.digitalasset.base.error.{ErrorClass, ErrorCode, NoBaseLogging} -import com.google.protobuf -import io.grpc.{Status, StatusRuntimeException} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.concurrent.duration.* - -class ErrorDetailsSpec extends AnyFlatSpec with Matchers { - - private val errorLogger = NoBaseLogging - - behavior of classOf[ErrorDetails.type].getName - - it should "correctly match exception to error codes " in { - val securitySensitive = - SevereError.Reject("some internal failure")(errorLogger).asGrpcError - val notSecuritySensitive = - BenignError.Reject("some service")(errorLogger).asGrpcError - - ErrorDetails.matches( - securitySensitive, - SevereError, - ) shouldBe false - - ErrorDetails.matches( - notSecuritySensitive, - BenignError, - ) shouldBe true - - ErrorDetails.matches( - new StatusRuntimeException(Status.ABORTED), - BenignError, - ) shouldBe false - - ErrorDetails.matches( - new Exception, - BenignError, - ) shouldBe false - - object NonGrpcErrorCode - extends ErrorCode( - id = "NON_GRPC_ERROR_CODE_123", - BackgroundProcessDegradationWarning, - )(ErrorClass.root()) - NonGrpcErrorCode.category.grpcCode shouldBe empty - ErrorDetails.matches( - new StatusRuntimeException(Status.ABORTED), - NonGrpcErrorCode, - ) shouldBe false - } - - it should "should preserve details when going through grpc Any" in { - val details = Seq( - ErrorDetails - .ErrorInfoDetail(errorCodeId = "errorCodeId1", metadata = Map("a" -> "b", "c" -> "d")), - ErrorDetails.ResourceInfoDetail(name = "name1", typ = "type1"), - ErrorDetails.RequestInfoDetail(correlationId = "correlationId1"), - ErrorDetails.RetryInfoDetail(1.seconds + 2.milliseconds), - ) - val anys: Seq[protobuf.Any] = details.map(_.toRpcAny) - ErrorDetails.from(anys) shouldBe details - } -} diff --git a/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/SevereError.scala b/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/SevereError.scala deleted file mode 100644 index 8f81b700f8..0000000000 --- a/canton/base/errors/src/test/scala/com/digitalasset/base/error/utils/SevereError.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.base.error.utils - -import com.digitalasset.base.error.{ - BaseErrorLogger, - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorClass, - ErrorCode, - Explanation, - Resolution, -} - -@Explanation("Things happen.") -@Resolution("Turn it off and on again.") -case object SevereError - extends ErrorCode("BLUE_SCREEN", ErrorCategory.SystemInternalAssumptionViolated)( - ErrorClass.root() - ) { - final case class Reject( - message: String, - override val throwableO: Option[Throwable] = None, - )(implicit - loggingContext: BaseErrorLogger - ) extends DamlErrorWithDefiniteAnswer( - cause = message, - extraContext = Map("throwableO" -> throwableO.toString), - ) -} diff --git a/canton/base/testing-utils/src/main/resources/logback-test.xml b/canton/base/testing-utils/src/main/resources/logback-test.xml new file mode 100644 index 0000000000..bab07ca00c --- /dev/null +++ b/canton/base/testing-utils/src/main/resources/logback-test.xml @@ -0,0 +1,61 @@ + + + + + true + + + + + + %highlight(%-5level %logger{10} %replace(tid:%mdc{trace-id} ){'tid: ', ''}- %msg%replace(, context: %marker){', context: $', ''}%replace( err-context:%mdc{err-context} ){' err-context: ', ''}%n) + + + WARN + + + + + ${LOG_FILE_NAME:-log/canton_test.log} + ${LOG_APPEND:-true} + + + %date [%thread] %-5level %logger{10} %replace(tid:%mdc{trace-id} ){'tid: ', ''}- %msg%replace(, context: %marker){', context: $', ''}%replace( err-context:%mdc{err-context} ){' err-context: ', ''}%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/canton/base/testing-utils/src/main/scala/com/daml/testing/utils/GrpcServerResource.scala b/canton/base/testing-utils/src/main/scala/com/daml/testing/utils/GrpcServerResource.scala index 6ae3d9fc47..81820186d6 100644 --- a/canton/base/testing-utils/src/main/scala/com/daml/testing/utils/GrpcServerResource.scala +++ b/canton/base/testing-utils/src/main/scala/com/daml/testing/utils/GrpcServerResource.scala @@ -6,6 +6,7 @@ package com.daml.testing.utils import io.grpc.* import java.net.SocketAddress +import java.util.UUID import java.util.concurrent.TimeUnit final class GrpcServerResource( @@ -17,7 +18,11 @@ final class GrpcServerResource( override protected def construct(): ServerWithChannelProvider = { boundServices = services() - ServerWithChannelProvider.fromServices(boundServices, port, "server") + ServerWithChannelProvider.fromServices( + boundServices, + port, + s"server_${UUID.randomUUID().toString}", + ) } override protected def destruct(resource: ServerWithChannelProvider): Unit = { diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/ConfidentialConfigWriter.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/ConfidentialConfigWriter.scala deleted file mode 100644 index d6d9216043..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/ConfidentialConfigWriter.scala +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.config - -import pureconfig.ConfigWriter -import pureconfig.generic.DerivedConfigWriter -import pureconfig.generic.semiauto.deriveWriter -import shapeless.Lazy - -class ConfidentialConfigWriter(confidential: Boolean) { - def apply[Config]( - map: Config => Config - )(implicit writer: Lazy[DerivedConfigWriter[Config]]): ConfigWriter[Config] = { - val parent = deriveWriter[Config] - if (confidential) - (a: Config) => parent.to(map(a)) - else - parent - } -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/KeyStoreConfig.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/KeyStoreConfig.scala deleted file mode 100644 index a288005939..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/KeyStoreConfig.scala +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.config - -import pureconfig.ConfigReader -import pureconfig.generic.semiauto.deriveReader - -/** Password wrapper for keystores to prevent the values being printed in logs. - * @param pw - * password value - public for supporting PureConfig parsing but callers should prefer accessing - * through unwrap - */ -final case class Password(pw: String) extends AnyVal { - def unwrap: String = pw - - def toCharArray: Array[Char] = pw.toCharArray - - // We do not want to print out the password in log files - override def toString: String = s"Password(****)" -} - -object Password { - implicit val passwordReader: ConfigReader[Password] = deriveReader[Password] - - def empty: Password = Password("") -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/PemFileOrString.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/PemFileOrString.scala deleted file mode 100644 index 9ae2c05b92..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/PemFileOrString.scala +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.config - -import better.files.* -import com.digitalasset.canton.config.RequireTypes.ExistingFile -import com.google.protobuf.ByteString - -/** A class representing either an existing PEM file path or an inlined PEM string for configuration - * file fields - */ -sealed trait PemFileOrString { - def pemBytes: ByteString - def pemStream: java.io.InputStream = pemBytes.newInput() -} - -/** A class representing an existing PEM file path for configuration file fields - */ -final case class PemFile(pemFile: ExistingFile) extends PemFileOrString { - override lazy val pemBytes: ByteString = - ByteString.copyFrom(File(pemFile.unwrap.getAbsolutePath).loadBytes) -} - -/** A class representing an inlined PEM string for configuration file fields - */ -final case class PemString(override val pemBytes: ByteString) extends PemFileOrString { - lazy val pemString: String = pemBytes.toStringUtf8 -} - -object PemString { - def apply(pemString: String): PemString = new PemString(ByteString.copyFromUtf8(pemString)) -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/RequireTypes.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/RequireTypes.scala deleted file mode 100644 index 4b50bcba6f..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/config/RequireTypes.scala +++ /dev/null @@ -1,418 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.config - -import cats.Monoid -import cats.syntax.either.* -import com.digitalasset.canton.config.RequireTypes.NonNegativeNumeric.SubtractionResult -import pureconfig.error.{CannotConvert, FailureReason} -import pureconfig.{ConfigReader, ConfigWriter} -import slick.jdbc.{GetResult, SetParameter} - -import java.io.File - -/** Encapsulates those classes and their utility methods which enforce a given invariant via the use - * of require. - */ -object RequireTypes { - final case class InvariantViolation(message: String) - - final case class Port private (private val n: Int) extends Ordered[Port] { - def unwrap: Int = n - - require( - n >= Port.minValidPort && n <= Port.maxValidPort, - s"Unable to create Port as value $n was given, but only values between ${Port.minValidPort} and ${Port.maxValidPort} (inclusive) are allowed.", - ) - - def +(n: Int): Port = Port.tryCreate(this.unwrap + n) - override def compare(that: Port): Int = this.unwrap compare that.unwrap - - override def toString: String = n.toString - } - - object Port { - val (minValidPort, maxValidPort) = (0, scala.math.pow(2, 16).toInt - 1) - - def create(n: Int): Either[InvariantViolation, Port] = - Either.cond( - n >= Port.minValidPort && n <= Port.maxValidPort, - new Port(n), - InvariantViolation( - s"Unable to create Port as value $n was given, but only values between ${Port.minValidPort} and ${Port.maxValidPort} are allowed." - ), - ) - - def tryCreate(n: Int): Port = - new Port(n) - - lazy implicit val portReader: ConfigReader[Port] = - ConfigReader.fromString[Port] { str => - def err(message: String) = - CannotConvert(str, Port.getClass.getName, message) - - Either - .catchOnly[NumberFormatException](str.toInt) - .leftMap[FailureReason](error => err(error.getMessage)) - .flatMap(n => create(n).leftMap(_ => InvalidPort(n))) - } - - implicit val portWriter: ConfigWriter[Port] = ConfigWriter.toString(x => x.unwrap.toString) - - final case class InvalidPort(n: Int) extends FailureReason { - override def description: String = - s"Unable to create Port as value $n was given, but only values between ${Port.minValidPort} and ${Port.maxValidPort} are allowed" - } - - /** This instructs the server to automatically choose a free port. - */ - lazy val Dynamic: Port = Port.tryCreate(0) - } - - sealed trait RefinedNumeric[T] extends Ordered[RefinedNumeric[T]] { - protected def value: T - implicit def num: Numeric[T] - - def unwrap: T = value - - override def compare(that: RefinedNumeric[T]): Int = num.compare(value, that.value) - - override def toString: String = value.toString - } - - final case class NonNegativeNumeric[T] private (value: T)(implicit val num: Numeric[T]) - extends RefinedNumeric[T] { - import num.* - - def map[U](f: T => U)(implicit num: Numeric[U]) = NonNegativeNumeric.tryCreate(f(value)) - def increment: PositiveNumeric[T] = PositiveNumeric.tryCreate(value + num.one) - def toPositiveNumeric: Option[PositiveNumeric[T]] = PositiveNumeric.create(value).toOption - - def +(other: NonNegativeNumeric[T]): NonNegativeNumeric[T] = - NonNegativeNumeric.tryCreate(value + other.value) - def *(other: NonNegativeNumeric[T]): NonNegativeNumeric[T] = - NonNegativeNumeric.tryCreate(value * other.value) - def /(other: NonNegativeNumeric[T])(implicit fractional: Fractional[T]): NonNegativeNumeric[T] = - NonNegativeNumeric.tryCreate(fractional.div(value, other.value)) - def tryAdd(other: T): NonNegativeNumeric[T] = NonNegativeNumeric.tryCreate(value + other) - - /** Subtract other from this. Subtracts as much as possible of "other" from "this" such that - * "this" stays >= 0. Any remaining amount will be the remainder. e.g: - * {{{ - * NonNegativeNumeric(5).subtract(NonNegativeNumeric(3)) == SubtractionResult(NonNegativeNumeric(2), NonNegativeNumeric(0)) - * NonNegativeNumeric(2).subtract(NonNegativeNumeric(3)) == SubtractionResult(NonNegativeNumeric(0), NonNegativeNumeric(1)) - * }}} - * @param other - * value to subtract to this - */ - def subtract(other: NonNegativeNumeric[T]): SubtractionResult[T] = { - val difference = value - other.value - if (difference < num.zero) { - SubtractionResult(NonNegativeNumeric(num.zero), NonNegativeNumeric(-difference)) - } else { - SubtractionResult(NonNegativeNumeric.tryCreate(difference), NonNegativeNumeric(num.zero)) - } - } - } - - object NonNegativeNumeric { - - final case class SubtractionResult[T]( - result: NonNegativeNumeric[T], - remainder: NonNegativeNumeric[T], - ) - def tryCreate[T](t: T)(implicit num: Numeric[T]): NonNegativeNumeric[T] = - create(t).valueOr(err => throw new IllegalArgumentException(err.message)) - - def create[T]( - t: T - )(implicit num: Numeric[T]): Either[InvariantViolation, NonNegativeNumeric[T]] = - Either.cond( - num.compare(t, num.zero) >= 0, - NonNegativeNumeric(t), - InvariantViolation( - s"Received the negative $t as argument, but we require a non-negative value here." - ), - ) - - implicit val readNonNegativeLong: GetResult[NonNegativeLong] = GetResult { r => - NonNegativeLong.tryCreate(r.nextLong()) - } - - implicit val readNonNegativeInt: GetResult[NonNegativeInt] = GetResult { r => - NonNegativeInt.tryCreate(r.nextInt()) - } - - implicit val readNonNegativeIntOption: GetResult[Option[NonNegativeInt]] = GetResult { r => - r.nextIntOption().map(NonNegativeInt.tryCreate) - } - - implicit val readNonNegativeLongOption: GetResult[Option[NonNegativeLong]] = GetResult { r => - r.nextLongOption().map(NonNegativeLong.tryCreate) - } - - implicit def writeNonNegativeNumeric[T](implicit - f: SetParameter[T] - ): SetParameter[NonNegativeNumeric[T]] = - (s, pp) => { - pp >> s.unwrap - } - - implicit def writeNonNegativeNumericOption[T](implicit - f: SetParameter[Option[T]] - ): SetParameter[Option[NonNegativeNumeric[T]]] = (s, pp) => { - pp >> s.map(_.unwrap) - } - - implicit def nonNegativeNumericReader[T](implicit - num: Numeric[T] - ): ConfigReader[NonNegativeNumeric[T]] = - ConfigReader.fromString[NonNegativeNumeric[T]] { str => - def err(message: String) = - CannotConvert(str, NonNegativeNumeric.getClass.getName, message) - - num - .parseString(str) - .toRight[FailureReason](err("Cannot convert `str` to numeric")) - .flatMap(n => Either.cond(num.compare(n, num.zero) >= 0, tryCreate(n), NegativeValue(n))) - } - - implicit def nonNegativeNumericWriter[T]: ConfigWriter[NonNegativeNumeric[T]] = - ConfigWriter.toString(x => x.unwrap.toString) - - final case class NegativeValue[T](t: T) extends FailureReason { - override def description: String = - s"The value you gave for this configuration setting ($t) was negative, but we require a non-negative value for this configuration setting" - } - } - - type NonNegativeInt = NonNegativeNumeric[Int] - - object NonNegativeInt { - lazy val zero: NonNegativeInt = NonNegativeInt.tryCreate(0) - lazy val one: NonNegativeInt = NonNegativeInt.tryCreate(1) - lazy val two: NonNegativeInt = NonNegativeInt.tryCreate(2) - lazy val three: NonNegativeInt = NonNegativeInt.tryCreate(3) - lazy val maxValue: NonNegativeInt = NonNegativeInt.tryCreate(Int.MaxValue) - - def create(n: Int): Either[InvariantViolation, NonNegativeInt] = NonNegativeNumeric.create(n) - def tryCreate(n: Int): NonNegativeInt = NonNegativeNumeric.tryCreate(n) - def size[T](collection: Iterable[T]): NonNegativeInt = tryCreate(collection.size) - } - - type NonNegativeDouble = NonNegativeNumeric[Double] - - object NonNegativeDouble { - lazy val zero: NonNegativeDouble = NonNegativeDouble.tryCreate(0.0) - lazy val one: NonNegativeDouble = NonNegativeDouble.tryCreate(1.0) - lazy val maxValue: NonNegativeDouble = NonNegativeDouble.tryCreate(Double.MaxValue) - - def create(n: Double): Either[InvariantViolation, NonNegativeDouble] = - NonNegativeNumeric.create(n) - def tryCreate(n: Double): NonNegativeDouble = NonNegativeNumeric.tryCreate(n) - } - - final case class NonNegativeProportion(n: NonNegativeDouble) { - require(n <= NonNegativeDouble.one, "proportion may not be larger than 1") - } - - object NonNegativeProportion { - lazy val zero: NonNegativeProportion = NonNegativeProportion(NonNegativeDouble.zero) - } - - type NonNegativeLong = NonNegativeNumeric[Long] - - object NonNegativeLong { - implicit val nonNegativeLongMonoid: Monoid[NonNegativeLong] = new Monoid[NonNegativeLong] { - override def empty: NonNegativeLong = NonNegativeLong.zero - override def combine(x: NonNegativeLong, y: NonNegativeLong): NonNegativeLong = x + y - } - lazy val zero: NonNegativeLong = NonNegativeLong.tryCreate(0) - lazy val one: NonNegativeLong = NonNegativeLong.tryCreate(1) - lazy val maxValue: NonNegativeLong = NonNegativeLong.tryCreate(Long.MaxValue) - - def create(n: Long): Either[InvariantViolation, NonNegativeLong] = NonNegativeNumeric.create(n) - def tryCreate(n: Long): NonNegativeLong = NonNegativeNumeric.tryCreate(n) - def size[T](collection: Iterable[T]): NonNegativeLong = tryCreate(collection.size.toLong) - } - - final case class PositiveNumeric[T] private (value: T)(implicit val num: Numeric[T]) - extends RefinedNumeric[T] { - import num.* - - def +(other: PositiveNumeric[T]): PositiveNumeric[T] = - PositiveNumeric.tryCreate(value + other.value) - - def +(other: NonNegativeNumeric[T]): PositiveNumeric[T] = - PositiveNumeric.tryCreate(value + other.value) - - def *(other: PositiveNumeric[T]): PositiveNumeric[T] = - PositiveNumeric.tryCreate(value * other.value) - - def max(other: PositiveNumeric[T]): PositiveNumeric[T] = - PositiveNumeric.tryCreate(num.max(value, other.value)) - - def increment: PositiveNumeric[T] = PositiveNumeric.tryCreate(value + num.one) - def decrement: NonNegativeNumeric[T] = NonNegativeNumeric.tryCreate(value - num.one) - - def tryAdd(other: T): PositiveNumeric[T] = PositiveNumeric.tryCreate(value + other) - - def toNonNegative: NonNegativeNumeric[T] = - NonNegativeNumeric.tryCreate(value) // always possible to convert positive to non negative num - } - - type PositiveInt = PositiveNumeric[Int] - - object PositiveInt { - def create(n: Int): Either[InvariantViolation, PositiveInt] = PositiveNumeric.create(n) - def tryCreate(n: Int): PositiveInt = PositiveNumeric.tryCreate(n) - - lazy val one: PositiveInt = PositiveInt.tryCreate(1) - lazy val two: PositiveInt = PositiveInt.tryCreate(2) - lazy val three: PositiveInt = PositiveInt.tryCreate(3) - lazy val four: PositiveInt = PositiveInt.tryCreate(4) - lazy val MaxValue: PositiveInt = PositiveInt.tryCreate(Int.MaxValue) - } - - type PositiveLong = PositiveNumeric[Long] - - object PositiveLong { - def create(n: Long): Either[InvariantViolation, PositiveLong] = PositiveNumeric.create(n) - def tryCreate(n: Long): PositiveLong = PositiveNumeric.tryCreate(n) - - lazy val one: PositiveLong = PositiveLong.tryCreate(1) - lazy val MaxValue: PositiveLong = PositiveLong.tryCreate(Long.MaxValue) - } - - type PositiveDouble = PositiveNumeric[Double] - object PositiveDouble { - def create(n: Double): Either[InvariantViolation, PositiveDouble] = PositiveNumeric.create(n) - def tryCreate(n: Double): PositiveDouble = PositiveNumeric.tryCreate(n) - } - - object PositiveNumeric { - def tryCreate[T](t: T)(implicit num: Numeric[T]): PositiveNumeric[T] = - create(t).valueOr(err => throw new IllegalArgumentException(err.message)) - - def create[T]( - t: T - )(implicit num: Numeric[T]): Either[InvariantViolation, PositiveNumeric[T]] = - Either.cond( - num.compare(t, num.zero) > 0, - PositiveNumeric(t), - InvariantViolation( - s"Received the non-positive $t as argument, but we require a positive value here." - ), - ) - - implicit def positiveNumericReader[T](implicit - num: Numeric[T], - tReader: ConfigReader[T], - ): ConfigReader[PositiveNumeric[T]] = - tReader.emap(n => - Either.cond(num.compare(n, num.zero) >= 0, tryCreate(n), NonPositiveValue(n)) - ) - - implicit def readPositiveDouble: GetResult[PositiveDouble] = GetResult { r => - PositiveNumeric.tryCreate(r.nextDouble()) - } - - implicit def writePositiveNumeric[T](implicit - f: SetParameter[T] - ): SetParameter[PositiveNumeric[T]] = - (s, pp) => { - pp >> s.unwrap - } - - implicit def positiveNumericWriter[T](implicit - tWriter: ConfigWriter[T] - ): ConfigWriter[PositiveNumeric[T]] = - tWriter.contramap[PositiveNumeric[T]](pn => pn.value) - - final case class NonPositiveValue[T](t: T) extends FailureReason { - override def description: String = - s"The value you gave for this configuration setting ($t) was non-positive, but we require a positive value for this configuration setting" - } - } - - final case class DoubleGreaterEqual1 private (override val value: Double) - extends RefinedNumeric[Double] { - override implicit def num: Numeric[Double] = Numeric.DoubleIsFractional - } - - object DoubleGreaterEqual1 { - def tryCreate(t: Double): DoubleGreaterEqual1 = - create(t).valueOr(err => throw new IllegalArgumentException(err.message)) - - def create( - value: Double - ): Either[InvariantViolation, DoubleGreaterEqual1] = - Either.cond( - value >= 1.0, - DoubleGreaterEqual1(value), - InvariantViolation( - s"Received $value < 1 as argument, but we require a value greter or equal to 1." - ), - ) - - implicit def doubleGreaterThanEqual1Reader[T]: ConfigReader[DoubleGreaterEqual1] = - ConfigReader.fromString[DoubleGreaterEqual1] { str => - def err(message: String) = - CannotConvert(str, NonNegativeNumeric.getClass.getName, message) - - Numeric[Double] - .parseString(str) - .toRight[FailureReason](err("Cannot convert `str` to numeric")) - .flatMap(n => Either.cond(n >= 1, tryCreate(n), LessThan1Value(n))) - } - - implicit def doubleGreaterThanEqual1Writer[T]: ConfigWriter[DoubleGreaterEqual1] = - ConfigWriter.toString(x => x.unwrap.toString) - - final case class LessThan1Value[T](t: T) extends FailureReason { - override def description: String = - s"The value you gave for this configuration setting ($t) was less than one, but we require a value >= 1 for this configuration setting" - } - } - - final case class ExistingFile private (private val file: File) { - def unwrap: File = file - require(file.exists(), s"Unable to create ExistingFile as non-existing file $file was given.") - } - - object ExistingFile { - - def create(file: File): Either[InvariantViolation, ExistingFile] = - Either.cond( - file.exists, - new ExistingFile(file), - InvariantViolation( - s"The specified file $file does not exist/was not found. Please specify an existing file" - ), - ) - - def tryCreate(file: File): ExistingFile = - new ExistingFile(file) - - def tryCreate(path: String): ExistingFile = - new ExistingFile(new File(path)) - - lazy implicit val existingFileReader: ConfigReader[ExistingFile] = - ConfigReader.fromString[ExistingFile] { str => - def err(message: String) = - CannotConvert(str, ExistingFile.getClass.getName, message) - - Either - .catchOnly[NullPointerException](new File(str)) - .leftMap[FailureReason](error => err(error.getMessage)) - .flatMap(f => create(f).leftMap(_ => NonExistingFile(f))) - } - - final case class NonExistingFile(file: File) extends FailureReason { - override def description: String = - s"The specified file $file does not exist/was not found. Please specify an existing file" - } - } - -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/discard/Implicits.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/discard/Implicits.scala deleted file mode 100644 index 03d2794bf0..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/discard/Implicits.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.discard - -object Implicits { - - /** Evaluate the expression and discard the result. */ - implicit final class DiscardOps[A](private val a: A) extends AnyVal { - @inline - def discard[B](implicit ev: A =:= B): Unit = () - } - -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/time/TimeProvider.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/time/TimeProvider.scala deleted file mode 100644 index dd02c40567..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/time/TimeProvider.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.time - -trait TimeProvider { - - /** Potentially non-monotonic time provider - */ - def nowInMicrosecondsSinceEpoch: Long - -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/BytesUnit.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/BytesUnit.scala deleted file mode 100644 index 4bd17b01b7..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/BytesUnit.scala +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.util - -import scala.math.Numeric.LongIsIntegral - -final case class BytesUnit(bytes: Long) { - - def *(that: Long): BytesUnit = BytesUnit(this.bytes * that) - - override def toString: String = { - val factorL = BytesUnit.factor - val factorD = BytesUnit.factor.toDouble - val (convertedValue: Double, unit) = bytes match { - case v if v < factorL => (v.toDouble, "B") - case v if v >= factorL && v < factorL * factorL => (v / factorD, "KB") - case v if v >= factorL * factorL && v < factorL * factorL * factorL => - (v / (factorD * factorD), "MB") - case v => - (v / (factorD * factorD * factorD), "GB") - } - f"$convertedValue%.2f $unit" - } -} - -object BytesUnit { - private[BytesUnit] val factor = 1024L - - val zero: BytesUnit = BytesUnit(0L) - - def KB(value: Long): BytesUnit = BytesUnit(value) * factor - def MB(value: Long): BytesUnit = KB(value) * factor - - implicit val bytesUnitIsNumeric: Numeric[BytesUnit] = new Numeric[BytesUnit] { - override def plus(x: BytesUnit, y: BytesUnit): BytesUnit = BytesUnit(x.bytes + y.bytes) - - override def minus(x: BytesUnit, y: BytesUnit): BytesUnit = BytesUnit(x.bytes - y.bytes) - - override def times(x: BytesUnit, y: BytesUnit): BytesUnit = BytesUnit(x.bytes * y.bytes) - - override def negate(x: BytesUnit): BytesUnit = BytesUnit(-x.bytes) - - override def fromInt(x: Int): BytesUnit = BytesUnit(x.toLong) - - override def parseString(str: String): Option[BytesUnit] = - LongIsIntegral.parseString(str).map(BytesUnit(_)) - - override def toInt(x: BytesUnit): Int = x.bytes.toInt - - override def toLong(x: BytesUnit): Long = x.bytes - - override def toFloat(x: BytesUnit): Float = x.bytes.toFloat - - override def toDouble(x: BytesUnit): Double = x.bytes.toDouble - - override def compare(x: BytesUnit, y: BytesUnit): Int = java.lang.Long.compare(x.bytes, y.bytes) - } - -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/JarResourceUtils.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/JarResourceUtils.scala deleted file mode 100644 index bd103fca6f..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/JarResourceUtils.scala +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.util - -import java.io.File -import java.nio.file.{Files, Paths, StandardCopyOption} -import scala.util.Using - -/** Utility methods for loading resource test files. - */ -object JarResourceUtils { - // Extract a resource and store it in a temporary file. - // This method works for resources embedded in a JAR. - def extractFileFromJar(path: String): File = - Using(getClass.getClassLoader.getResourceAsStream(path)) { inputStream => - if (inputStream == null) throw new RuntimeException(s"Resource for $path not found") - - // In case of absolute path, get only the file name - // (to be used as temp file name) - val tmpFileName = new File(path).getName - val tmpFilePath = Files.createTempFile(tmpFileName, "tmp") - Files.copy(inputStream, tmpFilePath, StandardCopyOption.REPLACE_EXISTING) - tmpFilePath.toFile - }.getOrElse(throw new RuntimeException(s"Resource for $path not found")) - - // Use resource file directly wrapping it in a File. This method works in unit tests. - // It doesn't work for resources from a JAR - def resourceFile(path: String): File = - Option(getClass.getClassLoader.getResource(path)) - .map(_.toURI) - .map(Paths.get(_).toFile) - .getOrElse(throw new RuntimeException(s"Resource for $path not found")) -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/Mutex.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/Mutex.scala deleted file mode 100644 index 6e47d8f5a5..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/Mutex.scala +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.util - -import com.digitalasset.canton.util.Mutex.MutexBlocker - -import java.lang.Thread.onSpinWait -import java.util.concurrent.ForkJoinPool -import java.util.concurrent.ForkJoinPool.ManagedBlocker -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.locks.ReentrantLock -import scala.annotation.tailrec - -/** Lock to be used instead of blocking synchronized - * - * The fork join pool is leaking threads potentially with every invocation of blocking. Therefore, - * only invoke it if really necessary. - */ -class Mutex { - - private val lock = new ReentrantLock() - - @deprecated("use exclusive, not synchronize", since = "3.4") - def synchronized[T](body: => T): T = sys.error("use exclusive to distinguish") - - def exclusive[T](f: => T): T = - // We perform lock inflation to reduce the chance of having to use expensive locks - // First level: Immediate acquisition - if (lock.tryLock()) { - try { - f - } finally { - lock.unlock() - } - } else { - // Second level: Spinning - Trying to avoid parking the thread - val maxSpins = Mutex.MaxSpins.get() - @tailrec - def retry(spins: Int): T = - if (spins < maxSpins) { - if (lock.tryLock()) { - try { - f - } finally { - lock.unlock() - } - } else { - onSpinWait() - retry(spins + 1) - } - } else { - // 3. "Heavy Lock" (Inflation) - Trigger FJP compensation - // Only at this point do we notify the ForkJoinPool. - val blocker = new MutexBlocker(lock) - ForkJoinPool.managedBlock(blocker) - try { - f - } finally { - lock.unlock() - } - } - - retry(0) - } -} - -object Mutex { - def apply(): Mutex = new Mutex - val MaxSpins = new AtomicInteger(3000) - // Inner class to handle the official FJP integration - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private class MutexBlocker(lock: ReentrantLock) extends ManagedBlocker { - private var acquired = false - - // FJP calls this to see if it even needs to block/compensate - override def isReleasable: Boolean = { - acquired = lock.tryLock() - acquired - } - - // FJP calls this if isReleasable returned false. - // This is where thread compensation (spawning a new thread) happens. - override def block(): Boolean = { - if (!acquired) { - lock.lock() // This is the heavy-weight park - acquired = true - } - true - } - } -} diff --git a/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/VersionUtil.scala b/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/VersionUtil.scala deleted file mode 100644 index d52ce0b64d..0000000000 --- a/canton/base/util-external/src/main/scala/com/digitalasset/canton/util/VersionUtil.scala +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.util - -import cats.syntax.traverse.* - -object VersionUtil { - def create( - rawVersion: String, - baseName: String, - ): Either[String, (Int, Int, Int, Option[String])] = { - // `?:` removes the capturing group, so we get a cleaner pattern-match statement - val regex = raw"([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,4})(?:-(.*))?".r - - rawVersion match { - case regex(rawMajor, rawMinor, rawPatch, suffix) => - val parsedDigits = List(rawMajor, rawMinor, rawPatch).traverse(raw => - raw.toIntOption.toRight(s"Couldn't parse number `$raw`") - ) - parsedDigits.flatMap { - case List(major, minor, patch) => - // `suffix` is `null` if no suffix is given - Right((major, minor, patch, Option(suffix))) - case _ => Left(s"Unexpected error while parsing version `$rawVersion`") - } - - case _ => - Left( - s"Unable to convert string `$rawVersion` to a valid $baseName. A $baseName is similar to a semantic version. For example, '1.2.3' or '1.2.3-SNAPSHOT' are valid ${baseName}s." - ) - } - } -} diff --git a/canton/base/util-external/src/test/scala/com/digitalasset/canton/config/RequireTypesTest.scala b/canton/base/util-external/src/test/scala/com/digitalasset/canton/config/RequireTypesTest.scala deleted file mode 100644 index 44f964db58..0000000000 --- a/canton/base/util-external/src/test/scala/com/digitalasset/canton/config/RequireTypesTest.scala +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.config - -import com.digitalasset.canton.config.RequireTypes.NonNegativeNumeric -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class RequireTypesTest extends AnyWordSpec with Matchers { - "NonNegativeNumeric" should { - "subtract" in { - val ten = NonNegativeNumeric.tryCreate(10) - val three = NonNegativeNumeric.tryCreate(3) - val tenMinusThree = ten.subtract(three) - tenMinusThree.result.value shouldBe 7 - tenMinusThree.remainder.value shouldBe 0 - - val threeMinusTen = three.subtract(ten) - threeMinusTen.result.value shouldBe 0 - threeMinusTen.remainder.value shouldBe 7 - } - } -} diff --git a/canton/build.sbt b/canton/build.sbt index 2db3b9fdad..8bc1022332 100644 --- a/canton/build.sbt +++ b/canton/build.sbt @@ -464,6 +464,10 @@ lazy val `model-based-testing-drivers` = CommunityProjects.`model-based-testing-drivers` lazy val `model-based-testing-integration-tests` = CommunityProjects.`model-based-testing-integration-tests` +lazy val `traffic-enforcement-api` = + CommunityProjects.`traffic-enforcement-api` +lazy val `traffic-enforcement-component` = + CommunityProjects.`traffic-enforcement-component` lazy val `scalatest-utils` = DamlProjects.`scalatest-utils` lazy val `scala-utils` = DamlProjects.`scala-utils` diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala index 4ec8a5b3f6..9d02427fa6 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala @@ -190,10 +190,18 @@ import com.digitalasset.canton.networking.grpc.ForwardingStreamObserver import com.digitalasset.canton.platform.apiserver.execution.CommandStatus import com.digitalasset.canton.protocol.LfContractId import com.digitalasset.canton.serialization.ProtoConverter +import com.digitalasset.canton.tea.v1.TrafficServiceGrpc.TrafficServiceStub +import com.digitalasset.canton.tea.v1.{ + GetAccountRequest, + GetAccountResponse, + TrafficServiceGrpc, + UpdateAccountRequest, + UpdateAccountResponse, +} import com.digitalasset.canton.topology.transaction.TopologyTransaction.GenericTopologyTransaction import com.digitalasset.canton.topology.{ParticipantId, Party, PartyId, SynchronizerId} import com.digitalasset.canton.util.BinaryFileUtil -import com.digitalasset.canton.{LfPackageId, LfPackageName, LfPartyId} +import com.digitalasset.canton.{LfPackageId, LfPackageName, LfPartyId, config} import com.google.protobuf.empty.Empty import com.google.protobuf.field_mask.FieldMask import io.grpc.* @@ -1747,6 +1755,7 @@ object LedgerApiCommands { minLedgerTimeAbs: Option[Instant], deduplicationPeriod: Option[DeduplicationPeriod], hashingSchemeVersion: HashingSchemeVersion, + optTimeout: Option[config.NonNegativeDuration], ) extends BaseCommand[ ExecuteSubmissionAndWaitRequest, ExecuteSubmissionAndWaitResponse, @@ -1776,7 +1785,8 @@ object LedgerApiCommands { ): Either[String, ExecuteSubmissionAndWaitResponse] = Right(response) - override def timeoutType: TimeoutType = DefaultUnboundedTimeout + override def timeoutType: TimeoutType = + optTimeout.map(CustomClientTimeout(_)).getOrElse(DefaultUnboundedTimeout) } final case class ExecuteAndWaitForTransactionCommand( @@ -1790,6 +1800,7 @@ object LedgerApiCommands { transactionShape: Option[TransactionShape], includeCreatedEventBlob: Boolean, customEventFormat: Option[EventFormat], + optTimeout: Option[config.NonNegativeDuration], ) extends BaseCommand[ ExecuteSubmissionAndWaitForTransactionRequest, ExecuteSubmissionAndWaitForTransactionResponse, @@ -1851,7 +1862,8 @@ object LedgerApiCommands { ): Either[String, ExecuteSubmissionAndWaitForTransactionResponse] = Right(response) - override def timeoutType: TimeoutType = DefaultUnboundedTimeout + override def timeoutType: TimeoutType = + optTimeout.map(CustomClientTimeout(_)).getOrElse(DefaultUnboundedTimeout) } final case class PreferredPackageVersion( @@ -2450,4 +2462,48 @@ object LedgerApiCommands { } } + object Traffic { + + abstract class BaseCommand[Req, Res] extends GrpcAdminCommand[Req, Res, Res] { + override type Svc = TrafficServiceStub + + override def createService(channel: ManagedChannel): TrafficServiceStub = + TrafficServiceGrpc.stub(channel) + + override protected def handleResponse(response: Res): Either[String, Res] = Right(response) + } + + final case class GetAccount(accountId: String) + extends BaseCommand[ + GetAccountRequest, + GetAccountResponse, + ] { + override protected def createRequest(): Either[String, GetAccountRequest] = + Right(GetAccountRequest(accountId)) + + override protected def submitRequest( + service: TrafficServiceStub, + request: GetAccountRequest, + ): Future[GetAccountResponse] = + service.getAccount(request) + } + + final case class UpdateAccount( + accountId: String, + balance: Option[Long], + deduplicationId: String, + ) extends BaseCommand[ + UpdateAccountRequest, + UpdateAccountResponse, + ] { + override protected def createRequest(): Either[String, UpdateAccountRequest] = + Right(UpdateAccountRequest(accountId, balance, deduplicationId)) + + override protected def submitRequest( + service: TrafficServiceStub, + request: UpdateAccountRequest, + ): Future[UpdateAccountResponse] = + service.updateAccount(request) + } + } } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerBftAdminCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerBftAdminCommands.scala index db253637a1..fd997ead63 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerBftAdminCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerBftAdminCommands.scala @@ -32,6 +32,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.admin.Se endpointToProto, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.p2p.grpc.P2PGrpcNetworking.P2PEndpoint +import com.digitalasset.canton.topology.SequencerId import io.grpc.ManagedChannel import scala.concurrent.Future @@ -100,7 +101,7 @@ object SequencerBftAdminCommands { extends BaseSequencerBftAdministrationCommand[ ListConfiguredEndpointsRequest, ListConfiguredEndpointsResponse, - Seq[P2PEndpoint], + Seq[(P2PEndpoint, Option[SequencerId])], ] { override protected def createRequest(): Either[String, ListConfiguredEndpointsRequest] = Right( @@ -115,10 +116,22 @@ object SequencerBftAdminCommands { override protected def handleResponse( response: ListConfiguredEndpointsResponse - ): Either[String, Seq[P2PEndpoint]] = + ): Either[String, Seq[(P2PEndpoint, Option[SequencerId])]] = response.endpoints - .map(endpointFromProto) + .map { peerEndpointAndSequencerId => + for { + endpoint <- endpointFromProto(peerEndpointAndSequencerId) + sequencerIdO <- peerEndpointAndSequencerId.sequencerId + .map( + SequencerId + .fromProtoPrimitive(_, "sequencerId") + .leftMap(err => s"Failed to parse sequencerId: $err") + ) + .sequence + } yield (endpoint, sequencerIdO) + } .sequence + .leftMap(err => s"Failed to parse response: $err") } final case class GetPeerNetworkStatus(endpoints: Option[Iterable[P2PEndpoint.Id]]) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala index 6bdf09702e..5debe1cf55 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala @@ -243,7 +243,10 @@ final case class SynchronizerConnectionConfig( ) def toInternal: SynchronizerConnectionConfigInternal = - this.transformInto[SynchronizerConnectionConfigInternal] + this + .into[SynchronizerConnectionConfigInternal] + .withFieldRenamed(_.synchronizerId, _.psid) + .transform } object SynchronizerConnectionConfig { @@ -320,5 +323,5 @@ object SynchronizerConnectionConfig { private[canton] def fromInternal( internal: SynchronizerConnectionConfigInternal ): SynchronizerConnectionConfig = - internal.transformInto[SynchronizerConnectionConfig] + internal.into[SynchronizerConnectionConfig].withFieldRenamed(_.psid, _.synchronizerId).transform } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala index 1f9490006b..098d1a1f53 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala @@ -70,6 +70,8 @@ import com.digitalasset.canton.platform.config.{ InteractiveSubmissionServiceConfig, StateServiceConfig, TopologyAwarePackageSelectionConfig, + TrafficEnforcementConfig, + TrafficEnforcementServerConfig, UpdateServiceConfig, } import com.digitalasset.canton.pureconfigutils.SharedConfigReaders.catchConvertError @@ -84,6 +86,7 @@ import com.digitalasset.canton.synchronizer.block.{SequencerDriver, SequencerDri import com.digitalasset.canton.synchronizer.config.{DeclarativeSequencerConfig, PublicServerConfig} import com.digitalasset.canton.synchronizer.mediator.{ DeduplicationStoreConfig, + DelayedVerdictSenderConfig, MediatorConfig, MediatorNodeConfig, MediatorNodeParameterConfig, @@ -99,6 +102,7 @@ import com.digitalasset.canton.synchronizer.sequencer.SequencerConfig.{ import com.digitalasset.canton.synchronizer.sequencer.block.DriverBlockSequencerFactory import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.canton.sequencing.BftSequencerFactory import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig.SequencerCoreSubscriptionConfig import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.BlacklistLeaderSelectionPolicyConfig import com.digitalasset.canton.synchronizer.sequencer.config.{ AsyncWriterConfig, @@ -381,13 +385,10 @@ final case class CantonFeatures( enableRepairCommands: Boolean = false, ) { def featureFlags: Set[FeatureFlag] = - (Seq(FeatureFlag.Stable) ++ (if (enableTestingCommands) Seq(FeatureFlag.Testing) - else Seq()) ++ (if (enablePreviewCommands) Seq(FeatureFlag.Preview) - else Seq()) ++ (if (enableRepairCommands) - Seq( - FeatureFlag.Repair - ) - else Seq())).toSet + (Seq(FeatureFlag.Stable) + ++ (if (enableTestingCommands) Seq(FeatureFlag.Testing) else Seq()) + ++ (if (enablePreviewCommands) Seq(FeatureFlag.Preview) else Seq()) + ++ (if (enableRepairCommands) Seq(FeatureFlag.Repair) else Seq())).toSet } /** The commonality between [[CantonConfig]] and `SpliceConfig` in the Splice repo, so that either @@ -498,7 +499,7 @@ trait SharedCantonConfig[Self] extends ConfigDefaults[Option[DefaultPorts], Self commitmentUseDbSnapshotForParticipantLookup = participantParameters.commitmentUseDbSnapshotForParticipantLookup, autoSyncProtocolFeatureFlags = participantParameters.autoSyncProtocolFeatureFlags, - alphaMultiSynchronizerSupport = participantParameters.alphaMultiSynchronizerSupport, + enableAllLedgerApiReassignments = participantParameters.enableAllLedgerApiReassignments, commitAfterFailedActivenessCheck = participantParameters.commitAfterFailedActivenessCheck, validateLegacyContractsV11 = participantParameters.validateLegacyContractsV11, ) @@ -533,11 +534,14 @@ trait SharedCantonConfig[Self] extends ConfigDefaults[Option[DefaultPorts], Self .map(DisasterRecoverySequencingTimeUpperBound(_)), delayRequestsBeforeLsuTrafficInit = sequencerNodeConfig.parameters.delayRequestsBeforeLsuTrafficInit, + enableRejectDeliveredAggregationsOnPv35 = + sequencerNodeConfig.parameters.enableRejectDeliveredAggregationsOnPv35, disableSubmissionChecksForTesting = sequencerNodeConfig.parameters.disableSubmissionChecksForTesting, disableReleaseVersionHandshakeCheck = sequencerNodeConfig.parameters.disableReleaseVersionHandshakeCheck, lsuConfig = sequencerNodeConfig.parameters.lsu, + enablePrevalidation = sequencerNodeConfig.parameters.enablePrevalidation, ) } @@ -553,6 +557,7 @@ trait SharedCantonConfig[Self] extends ConfigDefaults[Option[DefaultPorts], Self MediatorNodeParameters( general = CantonNodeParameterConverter.general(this, mediatorNodeConfig), protocol = CantonNodeParameterConverter.protocol(this, mediatorNodeConfig.parameters), + delayedVerdictSender = mediatorNodeConfig.parameters.delayedVerdictSender, ) } @@ -640,9 +645,8 @@ final case class CantonConfig( /** run a validation on the current config and return possible warning messages */ private def validate( ensurePortsSet: Boolean - ): Validated[NonEmpty[Seq[String]], Unit] = { + ): Validated[NonEmpty[Seq[String]], Unit] = ConfigValidations.validate(this, ensurePortsSet = ensurePortsSet) - } /** Produces a message in the structure * "da:admin-api=1,public-api=2;participant1:admin-api=3,ledger-api=4". Helpful for diagnosing @@ -699,7 +703,7 @@ final case class CantonConfig( .modify(mapWithDefaults) } - def mergeDynamicChanges(newConfig: CantonConfig): CantonConfig = { + override def mergeDynamicChanges(newConfig: CantonConfig): CantonConfig = { def merge[T](cur: Map[InstanceName, T], newConfig: Map[InstanceName, T], merger: (T, T) => T) = cur.map { case (name, config) => (name, newConfig.get(name).map(merger(config, _)).getOrElse(config)) @@ -1226,6 +1230,16 @@ object CantonConfig { lazy implicit val bftBlockOrdererLeaderSelectionPolicyHowLongToBlacklistLinearConfigReader : ConfigReader[BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear] = deriveReader[BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear] + lazy implicit val bftBlockOrdererLeaderSelectionPolicyHowLongToBlacklistLinearWithParametersConfigReader + : ConfigReader[ + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.LinearWithParameters + ] = + deriveReader[BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.LinearWithParameters] + lazy implicit val bftBlockOrdererLeaderSelectionPolicyHowLongToBlacklistExponentialConfigReader + : ConfigReader[ + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Exponential + ] = + deriveReader[BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Exponential] lazy implicit val bftBlockOrdererLeaderSelectionPolicyHowLongToBlacklistNoBlacklistingConfigReader : ConfigReader[ BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.NoBlacklisting.type @@ -1237,6 +1251,9 @@ object CantonConfig { lazy implicit val bftBlockOrdererLeaderSelectionPolicyConfigReader : ConfigReader[BlacklistLeaderSelectionPolicyConfig] = deriveReader[BlacklistLeaderSelectionPolicyConfig] + lazy implicit val bftBlockOrdererSequencerCoreSubscriptionConfigReader + : ConfigReader[SequencerCoreSubscriptionConfig] = + deriveReader[SequencerCoreSubscriptionConfig] lazy implicit val bftBlockOrdererConfigReader: ConfigReader[BftBlockOrdererConfig] = deriveReader[BftBlockOrdererConfig] lazy implicit val sequencerConfigBftSequencerReader @@ -1326,16 +1343,35 @@ object CantonConfig { lazy implicit final val remoteSequencerConfigReader: ConfigReader[RemoteSequencerConfig] = deriveReader[RemoteSequencerConfig] + lazy implicit final val mediatorNodeParameterConfigReader - : ConfigReader[MediatorNodeParameterConfig] = + : ConfigReader[MediatorNodeParameterConfig] = { + implicit val verdictSenderReaderConfig: ConfigReader[DelayedVerdictSenderConfig] = { + import NonNegativeNumeric.* + deriveReader[DelayedVerdictSenderConfig] + } deriveReader[MediatorNodeParameterConfig] + } lazy implicit final val mediatorConfigReader: ConfigReader[MediatorConfig] = { implicit val mediatorPruningConfigReader: ConfigReader[MediatorPruningConfig] = deriveReader[MediatorPruningConfig] implicit val deduplicationStoreConfigReader: ConfigReader[DeduplicationStoreConfig] = deriveReader[DeduplicationStoreConfig] - deriveReader[MediatorConfig] + + implicit val deprecatedFields: DeprecatedFieldsFor[MediatorConfig] = + new DeprecatedFieldsFor[MediatorConfig] { + + override def deprecatePath: List[DeprecatedConfigPath[?]] = + List( + DeprecatedConfigPath[Boolean]( + "asynchronous-processing", + since = "3.5.1", + ) + ) + } + + deriveReader[MediatorConfig].applyDeprecations } lazy implicit final val remoteMediatorConfigReader: ConfigReader[RemoteMediatorConfig] = deriveReader[RemoteMediatorConfig] @@ -1503,6 +1539,11 @@ object CantonConfig { since = "3.5.0", to = Seq("alpha-online-party-replication-support"), ), + DeprecatedConfigUtils.MovedConfigPath( + "alpha-multi-synchronizer-support", + since = "3.5.4", + to = Seq("enable-all-ledger-api-reassignments"), + ), ) override def deprecatePath: List[DeprecatedConfigPath[?]] = List( @@ -1559,7 +1600,20 @@ object CantonConfig { implicit val reassignmentsReader: ConfigReader[ReassignmentsConfig] = deriveReader[ReassignmentsConfig] implicit val purgeReader: ConfigReader[PurgeConfig] = deriveReader[PurgeConfig] - implicit val lsuReader: ConfigReader[LsuConfig] = deriveReader[LsuConfig] + implicit val lsuHandshakeReader: ConfigReader[LsuHandshake] = deriveReader[LsuHandshake] + + implicit val deprecatedFieldsLsuConfig: DeprecatedFieldsFor[LsuConfig] = + new DeprecatedFieldsFor[LsuConfig] { + override def movedFields: List[DeprecatedConfigUtils.MovedConfigPath] = List( + DeprecatedConfigUtils.MovedConfigPath( + "handshake-retry", + since = "3.5.1", + to = Seq("handshake.retry"), + ) + ) + } + + implicit val lsuReader: ConfigReader[LsuConfig] = deriveReader[LsuConfig].applyDeprecations deriveReader[ParticipantNodeParameterConfig].applyDeprecations } lazy implicit final val timeTrackerConfigReader: ConfigReader[SynchronizerTimeTrackerConfig] = { @@ -1649,6 +1703,21 @@ object CantonConfig { import DeclarativeSequencerConfig.Readers.* deriveReader[SequencerNodeConfig] } + + lazy implicit val trafficEnforcementProjectionConfigReader + : ConfigReader[TrafficEnforcementServerConfig.ProjectionConfig] = + deriveReader[TrafficEnforcementServerConfig.ProjectionConfig] + + lazy implicit val trafficEnforcementConfigInternalReader + : ConfigReader[TrafficEnforcementServerConfig.Internal] = + deriveReader[TrafficEnforcementServerConfig.Internal] + + lazy implicit val TrafficEnforcementServerConfigReader + : ConfigReader[TrafficEnforcementServerConfig] = + deriveReader[TrafficEnforcementServerConfig] + + lazy implicit val trafficEnforcementConfigReader: ConfigReader[TrafficEnforcementConfig] = + deriveReader[TrafficEnforcementConfig] } private implicit def cantonConfigReader(implicit @@ -2046,6 +2115,16 @@ object CantonConfig { lazy implicit val bftBlockOrdererLeaderSelectionPolicyHowLongToBlacklistLinearConfigWriter : ConfigWriter[BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear] = deriveWriter[BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear] + lazy implicit val bftBlockOrdererLeaderSelectionPolicyHowLongToBlacklistLinearWithParametersConfigWriter + : ConfigWriter[ + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.LinearWithParameters + ] = + deriveWriter[BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.LinearWithParameters] + lazy implicit val bftBlockOrdererLeaderSelectionPolicyHowLongToBlacklistExponentialConfigWriter + : ConfigWriter[ + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Exponential + ] = + deriveWriter[BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Exponential] lazy implicit val bftBlockOrdererLeaderSelectionPolicyHowLongToBlacklistNoBlacklistingConfigWriter : ConfigWriter[ BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.NoBlacklisting.type @@ -2057,6 +2136,9 @@ object CantonConfig { lazy implicit val bftBlockOrdererLeaderSelectionPolicyConfigWriter : ConfigWriter[BlacklistLeaderSelectionPolicyConfig] = deriveWriter[BlacklistLeaderSelectionPolicyConfig] + lazy implicit val bftBlockOrdererSequencerCoreSubscriptionConfigWriter + : ConfigWriter[SequencerCoreSubscriptionConfig] = + deriveWriter[SequencerCoreSubscriptionConfig] lazy implicit val bftBlockOrdererConfigWriter: ConfigWriter[BftBlockOrdererConfig] = deriveWriter[BftBlockOrdererConfig] @@ -2137,8 +2219,11 @@ object CantonConfig { deriveWriter[MediatorConfig] } lazy implicit final val mediatorNodeParameterConfigWriter - : ConfigWriter[MediatorNodeParameterConfig] = + : ConfigWriter[MediatorNodeParameterConfig] = { + implicit val verdictSenderConfigWriter: ConfigWriter[DelayedVerdictSenderConfig] = + deriveWriter[DelayedVerdictSenderConfig] deriveWriter[MediatorNodeParameterConfig] + } lazy implicit final val remoteMediatorConfigWriter: ConfigWriter[RemoteMediatorConfig] = deriveWriter[RemoteMediatorConfig] @@ -2275,6 +2360,7 @@ object CantonConfig { implicit val reassignmentsConfigWriter: ConfigWriter[ReassignmentsConfig] = deriveWriter[ReassignmentsConfig] implicit val purgeWriter: ConfigWriter[PurgeConfig] = deriveWriter[PurgeConfig] + implicit val lsuHandshakeWriter: ConfigWriter[LsuHandshake] = deriveWriter[LsuHandshake] implicit val lsuWriter: ConfigWriter[LsuConfig] = deriveWriter[LsuConfig] deriveWriter[ParticipantNodeParameterConfig] } @@ -2349,6 +2435,21 @@ object CantonConfig { import DeclarativeSequencerConfig.Writers.* deriveWriter[SequencerNodeConfig] } + + lazy implicit val trafficEnforcementProjectionConfigWriter + : ConfigWriter[TrafficEnforcementServerConfig.ProjectionConfig] = + deriveWriter[TrafficEnforcementServerConfig.ProjectionConfig] + + lazy implicit val trafficEnforcementConfigInternalWriter + : ConfigWriter[TrafficEnforcementServerConfig.Internal] = + deriveWriter[TrafficEnforcementServerConfig.Internal] + + lazy implicit val trafficEnforcementServerConfigWriter + : ConfigWriter[TrafficEnforcementServerConfig] = + deriveWriter[TrafficEnforcementServerConfig] + + lazy implicit val trafficEnforcementConfigWriter: ConfigWriter[TrafficEnforcementConfig] = + deriveWriter[TrafficEnforcementConfig] } private def makeWriter(confidential: Boolean): ConfigWriter[CantonConfig] = { diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala index a1e338dc90..c802377877 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala @@ -1352,7 +1352,7 @@ abstract class SequencerReference( } @Help.Summary("List peer endpoints configured and stored in DB") - def list_configured_peer_endpoints(): Seq[P2PEndpoint] = + def list_configured_peer_endpoints(): Seq[(P2PEndpoint, Option[SequencerId])] = consoleEnvironment.run { runner.adminCommand(SequencerBftAdminCommands.ListConfiguredEndpoints) } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala index 73fa7e5623..3682bfd782 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala @@ -102,6 +102,7 @@ import com.digitalasset.canton.networking.grpc.{ import com.digitalasset.canton.participant.ledger.api.client.JavaDecodeUtil import com.digitalasset.canton.platform.apiserver.execution.CommandStatus import com.digitalasset.canton.protocol.LfContractId +import com.digitalasset.canton.tea.v1.{GetAccountResponse, UpdateAccountResponse} import com.digitalasset.canton.topology.transaction.TopologyTransaction.GenericTopologyTransaction import com.digitalasset.canton.topology.{ ExternalParty, @@ -836,6 +837,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper userId: String = userId, deduplicationPeriod: Option[DeduplicationPeriod] = None, minLedgerTimeAbs: Option[Instant] = None, + optTimeout: Option[config.NonNegativeDuration] = Some(timeouts.ledgerCommand), ): ExecuteAndWaitResponseProto = consoleEnvironment.run { ledgerApiCommand( @@ -847,6 +849,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper deduplicationPeriod = deduplicationPeriod, minLedgerTimeAbs = minLedgerTimeAbs, hashingSchemeVersion = hashingSchemeVersion, + optTimeout = optTimeout, ) ) } @@ -873,6 +876,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper minLedgerTimeAbs: Option[Instant] = None, includeCreatedEventBlob: Boolean = false, customEventFormat: Option[EventFormat] = None, + optTimeout: Option[config.NonNegativeDuration] = Some(timeouts.ledgerCommand), ): ApiTransaction = consoleEnvironment.run { ledgerApiCommand( @@ -887,6 +891,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper transactionShape = transactionShape, includeCreatedEventBlob = includeCreatedEventBlob, customEventFormat = customEventFormat, + optTimeout = optTimeout, ) ) }.getTransaction @@ -3573,6 +3578,33 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper .pipe(GetEventsByContractIdResponse.toJavaProto) } } + + @Help.Summary("Participant user traffic service") + @Help.Group("Traffic") + object traffic extends Helpful { + @Help.Summary("Get account details", FeatureFlag.Testing) + @Help.Description("Get the details for the specified account-id") + def get_account(accountId: String): GetAccountResponse = + consoleEnvironment.run { + ledgerApiCommand(LedgerApiCommands.Traffic.GetAccount(accountId)) + } + + @Help.Summary("Update details for the account-id", FeatureFlag.Testing) + @Help.Description( + """Update the account details (the balance) for the specified account-id. + |If unset, the balance will not be updated + |subsequent balance updates with the same deduplicationId will be ignored""" + ) + def update_account( + accountId: String, + balance: Option[Long], + deduplicationId: String = UUID.randomUUID().toString, + ): UpdateAccountResponse = consoleEnvironment.run { + ledgerApiCommand( + LedgerApiCommands.Traffic.UpdateAccount(accountId, balance, deduplicationId) + ) + } + } } /** @return diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala index e88d2946b1..33c081bf88 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala @@ -190,8 +190,7 @@ class DeclarativeParticipantApi( val mapping = SynchronizerTrustCertificate( participantId, synchronizerId, - featureFlags = - (ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer +: oldFeatureFlags), + featureFlags = (ParticipantTopologyFeatureFlag.EnableMultiSynchronizer +: oldFeatureFlags), ) queryAdminApi( TopologyAdminCommands.Write.Propose( @@ -212,7 +211,7 @@ class DeclarativeParticipantApi( current <- fetchSynchronizerTrustCertificate(sid.synchronizerId) currentFeatureFlags = current.headOption.map(_.item.featureFlags).getOrElse(Seq.empty) shouldUpdate = !currentFeatureFlags.contains( - ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer + ParticipantTopologyFeatureFlag.EnableMultiSynchronizer ) done <- if (shouldUpdate) { diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala index 182d0c25b9..10f346924b 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala @@ -45,7 +45,7 @@ import com.digitalasset.canton.time.* import com.digitalasset.canton.tracing.TraceContext.withNewTraceContext import com.digitalasset.canton.tracing.{NoTracing, TraceContext, TracerProvider} import com.digitalasset.canton.util.FutureInstances.parallelFuture -import com.digitalasset.canton.util.{MonadUtil, Mutex, PekkoUtil, SingleUseCell} +import com.digitalasset.canton.util.{EitherTUtil, MonadUtil, Mutex, PekkoUtil, SingleUseCell} import com.google.common.annotations.VisibleForTesting import io.circe.Encoder import io.circe.generic.semiauto.deriveEncoder @@ -84,7 +84,7 @@ abstract class Environment[Config <: SharedCantonConfig[Config]]( noTracingLogger, ) - final def config: Config = currentConfig.get() + def config: Config = currentConfig.get() def pokeOrUpdateConfig( newConfig: Option[Either[String, Config]] )(implicit traceContext: TraceContext): Unit = { @@ -375,6 +375,7 @@ abstract class Environment[Config <: SharedCantonConfig[Config]]( runner writePortsFile() } // write ports after the runner has completed + // log results startup .leftMap(error => logger.error(s"Failed to start ${error.name}: ${error.message}")) @@ -427,12 +428,19 @@ abstract class Environment[Config <: SharedCantonConfig[Config]]( } EitherT.rightT(()) case Some(node) => - node - .reconnectSynchronizersIgnoreFailures(isTriggeredManually = false) - .leftMap(err => StartFailed(instance.name.unwrap, err.toString)) - .onShutdown(Left(StartFailed(instance.name.unwrap, "aborted due to shutdown"))) - + if (node.config.parameters.connectToSynchronizersOnStartup) + node + .reconnectSynchronizersIgnoreFailures(isTriggeredManually = false) + .leftMap(err => StartFailed(instance.name.unwrap, err.toString)) + .onShutdown(Left(StartFailed(instance.name.unwrap, "aborted due to shutdown"))) + else { + logger.info( + s"Not reconnecting $node to synchronizers because reconnect on startup is disabled" + ) + EitherTUtil.unit + } } + config.parameters.timeouts.processing.unbounded.await("reconnect-participants")( MonadUtil .parTraverseWithLimit_(config.parameters.getStartupParallelism(numThreads))( diff --git a/canton/community/app/src/main/resources/sandbox/bootstrap.canton b/canton/community/app/src/main/resources/sandbox/bootstrap.canton index d3877aa525..cd89f751f8 100644 --- a/canton/community/app/src/main/resources/sandbox/bootstrap.canton +++ b/canton/community/app/src/main/resources/sandbox/bootstrap.canton @@ -15,7 +15,7 @@ val syncDefs = mediators.local .zipWithIndex .map { case ((m, s), i) => SyncDef(m, s, s"synchronizer-${i + 1}") } -val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) +val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) syncDefs.foreach{ syncDef => val staticSynchronizerParameters = StaticSynchronizerParameters.defaults( diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala index c73b474917..538cd19802 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala @@ -63,7 +63,8 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { Console.out.println(s"$name: $version") } - protected def logAppVersion(): Unit = logger.info(s"Starting Canton version ${BuildInfo.version}") + protected def logAppVersion(): Unit = + logger.info(s"Starting Canton version ${BuildInfo.version}") // BE CAREFUL: Set the environment variables before you touch anything related to // logback as otherwise, the logback configuration will be read without these diff --git a/canton/community/app/src/pack/examples/05-composability/composability-auto-reassignment.canton b/canton/community/app/src/pack/examples/05-composability/composability-auto-reassignment.canton index 0ca0a797fd..fa3fd4d790 100644 --- a/canton/community/app/src/pack/examples/05-composability/composability-auto-reassignment.canton +++ b/canton/community/app/src/pack/examples/05-composability/composability-auto-reassignment.canton @@ -32,7 +32,7 @@ paintSynchronizerOwner.topology.synchronizer_parameters .propose_update(paintId, _.update(assignmentExclusivityTimeout = 2 seconds)) // enable multi-synchronizer topology feature flag for all participants for both synchronizers -val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) +val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) participant1.topology.synchronizer_trust_certificates.propose(participant1, iouId, featureFlags = featureFlag) participant2.topology.synchronizer_trust_certificates.propose(participant2, iouId, featureFlags = featureFlag) participant3.topology.synchronizer_trust_certificates.propose(participant3, iouId, featureFlags = featureFlag) diff --git a/canton/community/app/src/pack/examples/05-composability/composability1.canton b/canton/community/app/src/pack/examples/05-composability/composability1.canton index e2b98b63f5..f6b1ebe15c 100644 --- a/canton/community/app/src/pack/examples/05-composability/composability1.canton +++ b/canton/community/app/src/pack/examples/05-composability/composability1.canton @@ -41,7 +41,7 @@ participant2.synchronizers.connect_local(paint_sequencer, alias = paintAlias) participant3.synchronizers.connect_local(paint_sequencer, alias = paintAlias) // enable multi-synchronizer topology feature flag for all participants for both synchronizers -val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) +val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) participant1.topology.synchronizer_trust_certificates.propose(participant1, iouId, featureFlags = featureFlag) participant2.topology.synchronizer_trust_certificates.propose(participant2, iouId, featureFlags = featureFlag) participant3.topology.synchronizer_trust_certificates.propose(participant3, iouId, featureFlags = featureFlag) diff --git a/canton/community/app/src/pack/examples/05-composability/composability2.canton b/canton/community/app/src/pack/examples/05-composability/composability2.canton index b9e7bde715..8359962b68 100644 --- a/canton/community/app/src/pack/examples/05-composability/composability2.canton +++ b/canton/community/app/src/pack/examples/05-composability/composability2.canton @@ -33,7 +33,7 @@ paintSynchronizerOwner.topology.synchronizer_parameters .propose_update(paintId, _.update(assignmentExclusivityTimeout = 2 seconds)) // enable multi-synchronizer topology feature flag for all participants for both synchronizers -val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) +val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) participant1.topology.synchronizer_trust_certificates.propose(participant1, iouId, featureFlags = featureFlag) participant2.topology.synchronizer_trust_certificates.propose(participant2, iouId, featureFlags = featureFlag) participant3.topology.synchronizer_trust_certificates.propose(participant3, iouId, featureFlags = featureFlag) diff --git a/canton/community/app/src/pack/examples/08-interactive-submission/requirements.txt b/canton/community/app/src/pack/examples/08-interactive-submission/requirements.txt index a4a5bbbc4a..2b959f353e 100644 --- a/canton/community/app/src/pack/examples/08-interactive-submission/requirements.txt +++ b/canton/community/app/src/pack/examples/08-interactive-submission/requirements.txt @@ -1,3 +1,3 @@ -cryptography==44.0.1 +cryptography>=46.0.7 grpcio-tools==1.70.0 protobuf==5.29.3 diff --git a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/canton-network/sequencer.json b/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/canton-network/sequencer.json index e8cbf3b4eb..0839ea2ead 100644 --- a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/canton-network/sequencer.json +++ b/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/canton-network/sequencer.json @@ -1321,7 +1321,7 @@ "timepicker": {}, "timezone": "", "title": "Sequencer Traffic", - "uid": "fdjrxql2alblsd", + "uid": "fdjrxql3alclsd", "version": 9, "weekStart": "" } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/config/CantonConfigTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/config/CantonConfigTest.scala index c60f39e833..138f823390 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/config/CantonConfigTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/config/CantonConfigTest.scala @@ -605,7 +605,7 @@ class CantonConfigTest extends AnyWordSpec with BaseTest { participant.httpLedgerApi.requestTimeout.toMinutes shouldBe 105 // verify that `crypto.sessionSigningKeys` is configured with the expected values - participant.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.default + participant.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.enabled } // In this test case, both deprecated and new fields are set with opposite values, we make sure the new fields @@ -696,5 +696,71 @@ class CantonConfigTest extends AnyWordSpec with BaseTest { ) } } - + "AuthServiceConfig parsing" should { + "correctly parse max-token-life for all provider types" in { + import com.digitalasset.canton.config.AuthServiceConfig.* + import scala.concurrent.duration.* + + val authConfigStr = + """ + |canton.participants.participant1.ledger-api.auth-services = [ + | { + | type = "unsafe-jwt-hmac-256" + | secret = "super-secret-test-key-here" + | max-token-life = "10m" + | }, + | { + | type = "jwt-rs-256-crt" + | certificate = "path/to/rsa-cert.crt" + | max-token-life = "20m" + | }, + | { + | type = "jwt-jwks" + | url = "https://example.com/.well-known/jwks.json" + | max-token-life = "30m" + | }, + | { + | type = "jwt-es-256-crt" + | certificate = "path/to/cert256.crt" + | max-token-life = "40m" + | }, + | { + | type = "jwt-es-512-crt" + | certificate = "path/to/cert512.crt" + | max-token-life = "50m" + | } + |] + |""".stripMargin + + File.usingTemporaryFile("auth-services-test", ".conf") { tempFile => + tempFile.writeText(authConfigStr) + + val parsedConfig = CantonConfig + .parseAndLoad( + Seq(simpleConf.toJava, tempFile.toJava), + Some(DefaultPorts.create()), + ) + .valueOrFail("Failed to parse config with auth services") + + val authServices = parsedConfig.participantsByString("participant1").ledgerApi.authServices + + authServices should have size 5 + + inside(authServices) { + case Seq( + unsafe: UnsafeJwtHmac256, + rs256: JwtRs256Crt, + jwks: JwtJwks, + es256: JwtEs256Crt, + es512: JwtEs512Crt, + ) => + unsafe.maxTokenLife.duration shouldBe 10.minutes + rs256.maxTokenLife.duration shouldBe 20.minutes + jwks.maxTokenLife.duration shouldBe 30.minutes + es256.maxTokenLife.duration shouldBe 40.minutes + es512.maxTokenLife.duration shouldBe 50.minutes + } + } + } + } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/config/ConfigValidationsTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/config/ConfigValidationsTest.scala index de61db16d2..4716b25e1a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/config/ConfigValidationsTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/config/ConfigValidationsTest.scala @@ -689,7 +689,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { ), kms = Some(KmsConfig.Aws.defaultTestConfig), sessionSigningKeys = - SessionSigningKeysConfig.default.copy(signingAlgorithmSpec = signingAlgorithmSpec), + SessionSigningKeysConfig.enabled.copy(signingAlgorithmSpec = signingAlgorithmSpec), ) ) ), @@ -750,7 +750,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { ), kms = Some(KmsConfig.Aws.defaultTestConfig), sessionSigningKeys = - SessionSigningKeysConfig.default.copy(signingKeySpec = sessionSigningKeySpec), + SessionSigningKeysConfig.enabled.copy(signingKeySpec = sessionSigningKeySpec), ) ) ) @@ -767,16 +767,16 @@ class ConfigValidationsTest extends BaseTestWordSpec { crypto = CryptoConfig( provider = Kms, kms = Some(KmsConfig.Aws.defaultTestConfig), - sessionSigningKeys = SessionSigningKeysConfig.default, + sessionSigningKeys = SessionSigningKeysConfig.enabled, ) ) val config = CantonConfig(participants = Map(InstanceName.tryCreate("p1") -> participantNodeConfig)) - val defaultKeyValidityDuration = SessionSigningKeysConfig.default.keyValidityDuration - val defaultToleranceShiftDuration = SessionSigningKeysConfig.default.toleranceShiftDuration - val defaultCutOffDuration = SessionSigningKeysConfig.default.cutOffDuration - val defaultKeyEvictionPeriod = SessionSigningKeysConfig.default.keyEvictionPeriod + val defaultKeyValidityDuration = SessionSigningKeysConfig.enabled.keyValidityDuration + val defaultToleranceShiftDuration = SessionSigningKeysConfig.enabled.toleranceShiftDuration + val defaultCutOffDuration = SessionSigningKeysConfig.enabled.cutOffDuration + val defaultKeyEvictionPeriod = SessionSigningKeysConfig.enabled.keyEvictionPeriod def changeSessionSigningKeyParams( keyValidityDuration: Option[PositiveFiniteDuration], @@ -784,7 +784,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { cutOffDuration: Option[NonNegativeFiniteDuration] = None, keyEvictionPeriod: Option[PositiveFiniteDuration] = None, ) = { - val params = SessionSigningKeysConfig.default.copy( + val params = SessionSigningKeysConfig.enabled.copy( keyValidityDuration = keyValidityDuration.getOrElse(defaultKeyValidityDuration), toleranceShiftDuration = toleranceShiftDuration.getOrElse(defaultToleranceShiftDuration), cutOffDuration = cutOffDuration.getOrElse(defaultCutOffDuration), @@ -832,7 +832,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { )( s"participant p1: The selected session signing key tolerance shift duration " + s"of $invalidToleranceShiftDuration must be longer than the cut-off " + - s"(${SessionSigningKeysConfig.default.cutOffDuration})." + s"(${SessionSigningKeysConfig.enabled.cutOffDuration})." ) assertErrors( @@ -845,14 +845,14 @@ class ConfigValidationsTest extends BaseTestWordSpec { .focus(_.sequencerClient.defaultMaxSequencingTimeOffset) .replace( NonNegativeFiniteDuration.ofMinutes( - SessionSigningKeysConfig.default.keyValidityDuration.duration.toMinutes + SessionSigningKeysConfig.enabled.keyValidityDuration.duration.toMinutes ) ) ) ) )( s"participant p1: The selected session signing key validity parameters do not align with " + - s"the current default max sequencing time offset (${SessionSigningKeysConfig.default.keyValidityDuration}). " + + s"the current default max sequencing time offset (${SessionSigningKeysConfig.enabled.keyValidityDuration}). " + s"Parameters must be chosen so that " + s"`keyValidityDuration` - `cutOffDuration` > `defaultMaxSequencingTimeOffset`." ) @@ -866,13 +866,13 @@ class ConfigValidationsTest extends BaseTestWordSpec { )( s"participant p1: The selected session signing key eviction period " + s"of $invalidKeyEvictionPeriod must be longer than the key validity duration " + - s"(${SessionSigningKeysConfig.default.keyValidityDuration})." + s"(${SessionSigningKeysConfig.enabled.keyValidityDuration})." ) } "pass config validation with parameters out of bounds and checks disabled" in { - val invalidKeyValidityDuration = SessionSigningKeysConfig.default.keyEvictionPeriod + val invalidKeyValidityDuration = SessionSigningKeysConfig.enabled.keyEvictionPeriod val config = CantonConfig( parameters = CantonParameters(nonStandardConfig = true), participants = Map( @@ -881,7 +881,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { crypto = CryptoConfig( provider = Kms, kms = Some(KmsConfig.Aws.defaultTestConfig), - sessionSigningKeys = SessionSigningKeysConfig.default.copy( + sessionSigningKeys = SessionSigningKeysConfig.enabled.copy( keyValidityDuration = invalidKeyValidityDuration, disableBoundChecks = true, ), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleEnvironmentTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleEnvironmentTest.scala index 8368305f4d..d7ccbf8b75 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleEnvironmentTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleEnvironmentTest.scala @@ -7,7 +7,7 @@ import com.daml.metrics.OnDemandMetricsReader.NoOpOnDemandMetricsReader$ import com.digitalasset.canton.BaseTest import com.digitalasset.canton.config.CantonConfig import com.digitalasset.canton.console.CommandErrors.GenericCommandError -import com.digitalasset.canton.environment.Environment +import com.digitalasset.canton.environment.CantonEnvironment import com.digitalasset.canton.logging.{NamedEventCapturingLogger, SuppressingLogger} import com.digitalasset.canton.telemetry.ConfiguredOpenTelemetry import com.digitalasset.canton.tracing.TracerProvider @@ -36,9 +36,9 @@ class ConsoleEnvironmentTest extends AnyWordSpec with BaseTest { val testConsoleOutput: TestConsoleOutput = new TestConsoleOutput(capturingLoggerFactory) // Setup environment to inject capturing loggerFactory - val environment = mock[Environment] + val environment = mock[CantonEnvironment] when(environment.loggerFactory).thenReturn(capturingLoggerFactory) - when(environment.config).thenReturn(CantonConfig()) + doReturn(CantonConfig()).when(environment).config when(environment.tracerProvider).thenReturn(mock[TracerProvider]) when(environment.configuredOpenTelemetry).thenReturn( ConfiguredOpenTelemetry( @@ -49,7 +49,7 @@ class ConsoleEnvironmentTest extends AnyWordSpec with BaseTest { ) // The ConsoleEnvironment to be tested - val consoleEnvironment: ConsoleEnvironment = new ConsoleEnvironment( + val consoleEnvironment: CantonConsoleEnvironment = new CantonConsoleEnvironment( environment, consoleOutput = testConsoleOutput, ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala index ce6b4cf486..fde97a17a4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala @@ -82,7 +82,7 @@ final class ConsoleTest extends AnyWordSpec with BaseTest { val adminToken: String = "0" * 64 when(environment.tracerProvider).thenReturn(mock[TracerProvider]) - when(environment.config).thenReturn(config) + doReturn(config).when(environment).config when(environment.testingConfig).thenReturn( TestingConfigInternal(initializeGlobalOpenTelemetry = false) ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/environment/CommunityEnvironmentFixture.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/environment/CommunityEnvironmentFixture.scala index 176b380f02..f0dcbf94ca 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/environment/CommunityEnvironmentFixture.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/environment/CommunityEnvironmentFixture.scala @@ -123,7 +123,7 @@ trait CommunityEnvironmentFixture extends BaseTest with HasExecutionContext { th def mockParticipant: ParticipantNodeBootstrap = mockParticipantAndNode._1 - val environment = new Environment( + val environment = new CantonEnvironment( config, TestingConfigInternal(initializeGlobalOpenTelemetry = false, warnOnJwtScopeUsage = false), new ParticipantNodeBootstrapFactory { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/environment/NodesTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/environment/NodesTest.scala index db0093367d..8aeb13c3ef 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/environment/NodesTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/environment/NodesTest.scala @@ -31,6 +31,7 @@ import com.digitalasset.canton.lifecycle.{LifeCycle, ShutdownFailedException} import com.digitalasset.canton.metrics.ActiveRequestsMetrics.GrpcServerMetricsX import com.digitalasset.canton.metrics.{ CommonMockMetrics, + CryptoMetrics, DbStorageMetrics, DeclarativeApiMetrics, LedgerApiServerMetrics, @@ -126,6 +127,7 @@ class NodesTest extends FixtureAnyWordSpec with BaseTest with HasExecutionContex (LedgerApiServerMetrics.ForTesting.grpc, LedgerApiServerMetrics.ForTesting.requests), healthMetrics: HealthMetrics = LedgerApiServerMetrics.ForTesting.health, storageMetrics: DbStorageMetrics = CommonMockMetrics.dbStorage, + cryptoMetrics: CryptoMetrics = CommonMockMetrics.cryptoMetrics, ) extends BaseMetrics { override val declarativeApiMetrics: DeclarativeApiMetrics = diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DownloadTopologyForInitIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DownloadTopologyForInitIntegrationTest.scala index 66ae4011c5..3b2f4f87f9 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DownloadTopologyForInitIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DownloadTopologyForInitIntegrationTest.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.integration.tests import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.config.{DbConfig, DefaultProcessingTimeouts} import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -46,9 +47,17 @@ abstract class DownloadTopologyForInitIntegrationTest Map( // A threshold of 2 ensures that the mediators connect to all sequencers. // TODO(#19911) Make this properly configurable - mediator1 -> (Seq(sequencer1, sequencer2), PositiveInt.two, NonNegativeInt.zero), + mediator1 -> MediatorSequencersConfiguration( + Seq(sequencer1, sequencer2), + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ), // Have this so that mediator2 gets registered in the topology state. - mediator2 -> (Seq(sequencer2), PositiveInt.one, NonNegativeInt.zero), + mediator2 -> MediatorSequencersConfiguration( + Seq(sequencer2), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ), ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DumpIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DumpIntegrationTest.scala index 3f656853a4..63d0ac37ce 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DumpIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DumpIntegrationTest.scala @@ -18,6 +18,7 @@ import com.digitalasset.canton.integration.{ EnvironmentDefinition, SharedEnvironment, } +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.protocol.messages.AcsCommitment import com.digitalasset.canton.protocol.v30 import com.digitalasset.canton.sequencing.PossiblyIgnoredProtocolEvent @@ -64,6 +65,7 @@ sealed trait DumpIntegrationTest extends CommunityIntegrationTest with SharedEnv CryptoSchemes .fromConfig(config.crypto) .valueOrFail("fail to validate crypto schemes from the configuration file"), + CommonMockMetrics.cryptoMetrics, loggerFactory, ) .valueOr(err => throw new RuntimeException(s"Failed to create pure crypto api: $err")) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/MultipleMediatorsMultipleSynchronizersIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/MultipleMediatorsMultipleSynchronizersIntegrationTest.scala index 219c069419..a7f59d023f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/MultipleMediatorsMultipleSynchronizersIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/MultipleMediatorsMultipleSynchronizersIntegrationTest.scala @@ -46,7 +46,7 @@ final class MultipleMediatorsMultipleSynchronizersIntegrationTest ).withManualStart .addConfigTransforms( ProgrammableSequencer.configOverride(this.getClass.toString, loggerFactory), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ProtobufCompatibilityTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ProtobufCompatibilityTest.scala index a9c399f1d5..6f634df783 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ProtobufCompatibilityTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ProtobufCompatibilityTest.scala @@ -167,10 +167,41 @@ final class ProtobufCompatibilityReaderTest """com/digitalasset/canton/topology/admin/v30/topology_manager_read_service.proto:Previously present RPC "ListSequencerConnectionSuccessor" on service "TopologyManagerReadService" was deleted.""", """com/digitalasset/canton/topology/admin/v30/topology_manager_read_service.proto:Previously present RPC "ListSynchronizerUpgradeAnnouncement" on service "TopologyManagerReadService" was deleted.""", """com/digitalasset/canton/topology/admin/v30/topology_manager_read_service.proto:Previously present RPC "LogicalUpgradeState" on service "TopologyManagerReadService" was deleted.""", + + /// Backward compatibility + """com/digitalasset/canton/admin/sequencer/v30/sequencer_connection.proto:Previously present field "3" with name "confirmation_response_factor" on message "SubmissionRequestAmplification" was deleted.""", + """com/digitalasset/canton/admin/sequencer/v30/sequencer_connection.proto:Previously present field "4" with name "confirmation_response_patience" on message "SubmissionRequestAmplification" was deleted.""", + """com/digitalasset/canton/mediator/admin/v30/mediator_inspection_service.proto:Previously present field "2" with name "complete" on message "VerdictsResponse" was deleted.""", + """com/digitalasset/canton/mediator/admin/v30/mediator_inspection_service.proto:Previously present oneof "payload" on message "VerdictsResponse" was deleted.""", + """com/digitalasset/canton/mediator/admin/v30/mediator_inspection_service.proto:Field "1" with name "verdict" on message "VerdictsResponse" moved from inside to outside a oneof.""", + """com/digitalasset/canton/mediator/admin/v30/mediator_inspection_service.proto:Previously present field "4" with name "view_hash" on message "TransactionView" was deleted.""", + """com/digitalasset/canton/participant/protocol/v30/submission_tracking.proto:Previously present field "6" with name "paid_traffic_cost" on message "CompletionInfo" was deleted.""", + """com/digitalasset/canton/protocol/v30/synchronization.proto:Previously present field "7" with name "lsu_sequencing_test_message" on message "EnvelopeContent" was deleted.""", + """com/digitalasset/canton/protocol/v30/topology.proto:Previously present enum value "2" on enum "ParticipantFeatureFlag" was deleted.""", + """com/digitalasset/canton/protocol/v30/topology.proto:Field "17" with name "synchronizer_upgrade_announcement" on message "TopologyMapping" changed type from "com.digitalasset.canton.protocol.v30.LsuAnnouncement" to "com.digitalasset.canton.protocol.v30.SynchronizerUpgradeAnnouncement".""", + """com/digitalasset/canton/protocol/v30/topology.proto:Field "18" with name "sequencer_connection_successor" on message "TopologyMapping" changed type from "com.digitalasset.canton.protocol.v30.LsuSequencerConnectionSuccessor" to "com.digitalasset.canton.protocol.v30.SequencerConnectionSuccessor".""", + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present field "3" with name "dynamic_sequencing_parameters_payload" on message "GetOrderingTopologyResponse" was deleted.""", + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present field "4" with name "dynamic_sequencing_parameters_payload31" on message "GetOrderingTopologyResponse" was deleted.""", + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present oneof "dynamic_sequencing_parameters" on message "GetOrderingTopologyResponse" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_authentication_service.proto:Previously present field "3" with name "client_version" on message "ChallengeRequest" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present field "3" with name "client_version" on message "HandshakeRequest" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present reserved name "failure" on message "HandshakeResponse" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present reserved range "[3]" on message "HandshakeResponse" is missing values: [3] were removed.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present reserved name "failure" on message "VerifyActiveResponse" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present reserved range "[2]" on message "VerifyActiveResponse" is missing values: [2] were removed.""", + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Previously present field "5" with name "ordering_start_instant" on message "OrderingRequest" was deleted.""", + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Field "3" with name "payload" on message "OrderingRequest" changed type from "string" to "bytes".""", + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Field "4" with name "ordering_start_instant" on message "OrderingRequest" changed cardinality from "optional with implicit presence" to "optional with explicit presence".""", + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Field "4" with name "ordering_start_instant" on message "OrderingRequest" changed type from "bytes" to "message".""", + """com/digitalasset/canton/synchronizer/v30/synchronizer.proto:Previously present field "3" with name "is_late_upgrade" on message "SynchronizerPredecessor" was deleted.""", + """com/digitalasset/canton/admin/health/v30/status_service.proto:Previously present field "3" with name "version" on message "NotInitialized" was deleted.""", + // Added DABFT leaders and blacklisted nodes to `get_ordering_topology` console admin function's output + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present field "5" with name "leader_sequencer_ids" on message "GetOrderingTopologyResponse" was deleted.""", + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present field "6" with name "blacklisted_sequencer_ids" on message "GetOrderingTopologyResponse" was deleted.""", + // undefined epoch_number case is handled + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Previously present field "2" with name "epoch_number" on message "BatchRequest" was deleted.""", ), (3, 5) -> Seq( - // Expose the release version in NotInitialized, is going to be added to 3.5.1-rc4+ - """com/digitalasset/canton/admin/health/v30/status_service.proto:Previously present field "3" with name "version" on message "NotInitialized" was deleted.""" ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedMediatorIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedMediatorIntegrationTest.scala index d60de8e3ca..14f3f55a3d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedMediatorIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedMediatorIntegrationTest.scala @@ -14,11 +14,11 @@ import com.digitalasset.canton.integration.bootstrap.{ } import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres, UseSharedStorage} import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransform, ConfigTransforms, EnvironmentDefinition, - EnvironmentSetup, EnvironmentSetupPlugin, SharedEnvironment, TestConsoleEnvironment, @@ -31,7 +31,7 @@ import monocle.macros.syntax.lens.* import org.slf4j.event.Level trait ReplicatedMediatorTestSetup extends ReplicatedNodeHelper { - self: CommunityIntegrationTest & EnvironmentSetup => + self: CommunityIntegrationTest & CantonEnvironmentSetup => protected lazy val mediator1Name = "mediatorReplicated1" protected lazy val mediator2Name = "mediatorReplicated2" diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedParticipantTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedParticipantTest.scala index 2b165dd538..0ce14dfb6b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedParticipantTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedParticipantTest.scala @@ -29,10 +29,10 @@ import com.digitalasset.canton.integration.bootstrap.{ } import com.digitalasset.canton.integration.plugins.* import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransforms, EnvironmentDefinition, - EnvironmentSetup, EnvironmentSetupPlugin, SharedEnvironment, TestConsoleEnvironment, @@ -113,7 +113,7 @@ trait ReplicatedNodeHelper { self: CommunityIntegrationTest => } trait ReplicatedParticipantTestSetup extends ReplicatedNodeHelper { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def setupPlugins( storagePlugin: EnvironmentSetupPlugin, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SequencerIdsRetrieverIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SequencerIdsRetrieverIntegrationTest.scala index d8580dc776..859d6c972a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SequencerIdsRetrieverIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SequencerIdsRetrieverIntegrationTest.scala @@ -13,7 +13,8 @@ import com.digitalasset.canton.admin.api.client.data.{ } import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.config.{ExponentialBackoffConfig, NonNegativeDuration} -import com.digitalasset.canton.console.{MediatorReference, SequencerReference} +import com.digitalasset.canton.console.MediatorReference +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -50,12 +51,18 @@ final class SequencerIdsRetrieverIntegrationTest Ensure each mediator is connected to a single sequencer. The goal is that if a sequencer is stopped (sequencer4 in this test) it does not impact */ - val mediatorToSequencers - : Map[MediatorReference, (Seq[SequencerReference], PositiveInt, NonNegativeInt)] = + val mediatorToSequencers: Map[MediatorReference, MediatorSequencersConfiguration] = sequencers .zip(mediators) .map { case (sequencer, mediator) => - (mediator, (Seq(sequencer), PositiveInt.one, NonNegativeInt.zero)) + ( + mediator, + MediatorSequencersConfiguration( + Seq(sequencer), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ), + ) } .toMap diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SubmissionRequestAmplificationIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SubmissionRequestAmplificationIntegrationTest.scala index d26918eb3c..1fd18fe855 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SubmissionRequestAmplificationIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SubmissionRequestAmplificationIntegrationTest.scala @@ -24,6 +24,7 @@ import com.digitalasset.canton.console.{ } import com.digitalasset.canton.integration.EnvironmentDefinition.S2M2 import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.plugins.{ UseBftSequencer, UsePostgres, @@ -79,8 +80,16 @@ abstract class SubmissionRequestAmplificationIntegrationTest Map( // A threshold of two ensures that the mediators connect to both sequencers. // TODO(#19911) Make this properly configurable - mediator1 -> (Seq(sequencer1, sequencer2), PositiveInt.two, NonNegativeInt.zero), - mediator2 -> (Seq(sequencer1, sequencer2), PositiveInt.two, NonNegativeInt.zero), + mediator1 -> MediatorSequencersConfiguration( + Seq(sequencer1, sequencer2), + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ), + mediator2 -> MediatorSequencersConfiguration( + Seq(sequencer1, sequencer2), + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ), ) ) ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerChangeIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerChangeIntegrationTest.scala index 89064323f8..add3754a4b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerChangeIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerChangeIntegrationTest.scala @@ -110,7 +110,7 @@ abstract class SynchronizerChangeIntegrationTest(config: SynchronizerChangeInteg .updateTargetTimestampForwardTolerance( config.targetTimestampForwardTolerance.duration ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .addConfigTransforms(additionalConfigTransforms*) .withSetup(setUp) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerConnectivityIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerConnectivityIntegrationTest.scala index 1e816b0938..a5f4b4f6cc 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerConnectivityIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerConnectivityIntegrationTest.scala @@ -144,7 +144,21 @@ sealed trait SynchronizerConnectivityIntegrationTest participant2.synchronizers.list_connected().map(_.synchronizerAlias) shouldBe Seq(daName) participant2.synchronizers.disconnect_all() participant2.synchronizers.list_connected() shouldBe empty + } + } + + "A participant" must { + "Be able to change the config" in { implicit env => + import env.* + + participant1.synchronizers.modify(daName, _.focus(_.priority).modify(_ + 1)) + participant1.synchronizers.modify( + daName, + _.focus(_.priority).modify(_ + 1), + physicalSynchronizerId = Some(daId), + ) + succeed } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerRouterIntegrationTestSetup.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerRouterIntegrationTestSetup.scala index aea5e11b14..a0dbf4a21a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerRouterIntegrationTestSetup.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerRouterIntegrationTestSetup.scala @@ -49,7 +49,7 @@ trait SynchronizerRouterIntegrationTestSetup EnvironmentDefinition .P4_S1M1_S1M1_S1M1() .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/TickRequestIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/TickRequestIntegrationTest.scala index a2cec73877..a9c0076260 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/TickRequestIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/TickRequestIntegrationTest.scala @@ -89,7 +89,7 @@ sealed trait TickRequestIntegrationTest ConfigTransforms.useStaticTime, ConfigTransforms.updateSynchronizerTimeTrackerConfigs_(_ => synchronizerTimeTrackerConfig), ConfigTransforms.updateTargetTimestampForwardTolerance(Duration.ofHours(1)), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .addConfigTransforms( ConfigTransforms.setTopologyTransactionRegistrationTimeout( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/ActiveContractsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/ActiveContractsIntegrationTest.scala index 1704370b90..bd61cd163e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/ActiveContractsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/ActiveContractsIntegrationTest.scala @@ -68,7 +68,7 @@ import scala.jdk.CollectionConverters.* import ActiveContractsIntegrationTestBase.* -abstract class ActiveContractsIntegrationTestBase(alphaMultiSynchronizerSupport: Boolean = false) +abstract class ActiveContractsIntegrationTestBase(enableAllLedgerApiReassignments: Boolean = false) extends CommunityIntegrationTest with SharedEnvironment with AcsInspection @@ -96,9 +96,10 @@ abstract class ActiveContractsIntegrationTestBase(alphaMultiSynchronizerSupport: // Ensure reassignments are not tripped up by some participants being a little behind. ConfigTransforms.updateTargetTimestampForwardTolerance(30.seconds), ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport).replace(alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* @@ -263,7 +264,7 @@ abstract class ActiveContractsIntegrationTestBase(alphaMultiSynchronizerSupport: val createdEvent = eventually() { val endOffset = participant1.ledger_api.state.end() - if (participant1.config.parameters.alphaMultiSynchronizerSupport) { + if (participant1.config.parameters.enableAllLedgerApiReassignments) { participant1.ledger_api.updates .reassignments( partyIds = Set(signatory), @@ -900,4 +901,4 @@ private object ActiveContractsIntegrationTestBase { final class ActiveContractsIntegrationTest extends ActiveContractsIntegrationTestBase final class ActiveContractsReassignmentIntegrationTest - extends ActiveContractsIntegrationTestBase(alphaMultiSynchronizerSupport = true) + extends ActiveContractsIntegrationTestBase(enableAllLedgerApiReassignments = true) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentProcessorIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentProcessorIntegrationTest.scala index bf17f6cba9..0224256376 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentProcessorIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentProcessorIntegrationTest.scala @@ -90,7 +90,7 @@ sealed trait AcsCommitmentProcessorIntegrationTest ConfigTransforms.useStaticTime, // this only sets/enables session signing keys when running with PV35 or higher ConfigTransforms.setSigningKeysIfPV35OrHigher( - SessionSigningKeysConfig.default.copy( + SessionSigningKeysConfig.enabled.copy( // we evict the session key cache right away to make sure we use a fresh session signing key for each request keyEvictionPeriod = config.PositiveFiniteDuration.ofMillis(1), // we must disable bound checks because `keyEvictionPeriod` is shorter than `keyValidityDuration` diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentToolingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentToolingIntegrationTest.scala index 9d08efae33..179c4250f1 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentToolingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentToolingIntegrationTest.scala @@ -101,7 +101,7 @@ trait AcsCommitmentToolingIntegrationTest ConfigTransforms.useStaticTime, ConfigTransforms.updateMaxDeduplicationDurations(maxCommandDeduplicationDuration), ConfigTransforms.updateTargetTimestampForwardTolerance(24.hours), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .updateTestingConfig( _.focus(_.commitmentSendDelay).replace( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/util/CommitmentTestUtil.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/util/CommitmentTestUtil.scala index da391bef40..3817ac298f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/util/CommitmentTestUtil.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/util/CommitmentTestUtil.scala @@ -379,32 +379,74 @@ trait CommitmentTestUtil protected def awaitNextTick( participant: LocalParticipantReference, - counterparticipant: ParticipantReference, - )(implicit env: TestConsoleEnvironment, intervalDuration: IntervalDuration): CommitmentPeriod = { + counterParticipant: LocalParticipantReference, + )(implicit + env: TestConsoleEnvironment, + intervalDuration: IntervalDuration, + ): CommitmentPeriod = { import env.* val simClock = environment.simClock.value val tick1 = tickAfter(simClock.uniqueTime()) simClock.advanceTo(tick1.forgetRefinement.immediateSuccessor) + // Await the synchronizer time. Internally this will trigger a fetch of the synchronizer time. participant.testing.await_synchronizer_time(daId, tick1.forgetRefinement.immediateSuccessor) + counterParticipant.testing.await_synchronizer_time( + daId, + tick1.forgetRefinement.immediateSuccessor, + ) - val p1Computed = eventually() { - val p1Computed = participant.commitments.computed( + val participantComputed = eventually() { + val participantComputed = participant.commitments.computed( daName, tick1.toInstant.minusMillis(1), tick1.toInstant, - Some(counterparticipant.id), + Some(counterParticipant.id), ) - p1Computed should have size 1L - p1Computed + participantComputed should have size 1L + + val counterParticipantComputed = counterParticipant.commitments.computed( + daName, + tick1.toInstant.minusMillis(1), + tick1.toInstant, + Some(participant.id), + ) + counterParticipantComputed should have size 1L + + participantComputed } - val (period, _participant, commitment) = p1Computed.loneElement + // the values are the same for the participant and counter participant, but it is better to wait for both in wallClock time + val (period, _participantId, commitment) = participantComputed.loneElement period } - protected def checkReceivedCommitment( + protected def checkSentCommitmentTo( + recipients: Seq[ParticipantReference] + )( + period: CommitmentPeriod, + participant: ParticipantReference, + synchronizer: SynchronizerId, + expected: Int = 1, + ): Unit = eventually() { + val timeRange = + TimeRange(period.fromExclusive.forgetRefinement, period.toInclusive.forgetRefinement) + val sentCommitments = participant.commitments.lookup_sent_acs_commitments( + synchronizerTimeRanges = Seq(SynchronizerTimeRange(synchronizer, Some(timeRange))), + counterParticipants = Seq.empty, + commitmentState = Seq.empty, + verboseMode = false, + ) + + val sentCommitmentsOnSynchronizer = sentCommitments.get(synchronizer).value + sentCommitmentsOnSynchronizer.size should be >= expected + sentCommitmentsOnSynchronizer.map( + _.destCounterParticipant.uid + ) should contain theSameElementsAs (recipients.map(_.uid)) + } + + protected def checkReceivedCommitments( period: CommitmentPeriod, participant: ParticipantReference, synchronizer: SynchronizerId, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/benchmarks/ReplayingParticipant.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/benchmarks/ReplayingParticipant.scala index f851f09fde..7f98b54fe8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/benchmarks/ReplayingParticipant.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/benchmarks/ReplayingParticipant.scala @@ -267,6 +267,7 @@ object ReplayingParticipant extends FutureHelpers with EitherValues with OptionV testedReleaseProtocolVersion, futureSupervisor, clock, + CommonMockMetrics.cryptoMetrics, executionContext, timeouts, BatchingConfig(), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/bftsynchronizer/ReassignmentTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/bftsynchronizer/ReassignmentTest.scala index 24f071cfb0..973ae97d4b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/bftsynchronizer/ReassignmentTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/bftsynchronizer/ReassignmentTest.scala @@ -28,7 +28,7 @@ trait ReassignmentTest extends CommunityIntegrationTest with SharedEnvironment { override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P5S4M4_Manual - .addConfigTransform(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransform(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) protected val sequencerGroups: MultiSynchronizer = MultiSynchronizer( Seq( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/BftSequencerConnectionsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/BftSequencerConnectionsIntegrationTest.scala index 5d53112e61..cac15cd3a8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/BftSequencerConnectionsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/BftSequencerConnectionsIntegrationTest.scala @@ -14,6 +14,7 @@ import com.digitalasset.canton.annotations.UnstableTest import com.digitalasset.canton.config.DbConfig import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.InstanceReference +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -106,8 +107,11 @@ sealed trait BftSequencerConnectionsIntegrationTest mediators = Seq(mediator1), overrideMediatorToSequencers = Some( Map( - mediator1 -> (sequencers.remote, - /* trust threshold */ PositiveInt.two, /* liveness margin */ NonNegativeInt.zero) + mediator1 -> MediatorSequencersConfiguration( + sequencers.remote, + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ) ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/ConnectionPoolHealthIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/ConnectionPoolHealthIntegrationTest.scala index 6c4e691301..0bdcc4ef80 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/ConnectionPoolHealthIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/ConnectionPoolHealthIntegrationTest.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.admin.api.client.data.{ } import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.{InstanceReference, LocalInstanceReference} +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -46,8 +47,11 @@ sealed trait ConnectionPoolHealthIntegrationTest mediators = Seq(mediator1), overrideMediatorToSequencers = Some( Map( - mediator1 -> (sequencers.local, - /* trust threshold */ PositiveInt.two, /* liveness margin */ NonNegativeInt.one) + mediator1 -> MediatorSequencersConfiguration( + sequencers.local, + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.one, + ) ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/SequencerConnectionServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/SequencerConnectionServiceIntegrationTest.scala index da67689410..8f01ab9b4f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/SequencerConnectionServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/SequencerConnectionServiceIntegrationTest.scala @@ -11,6 +11,7 @@ import com.digitalasset.canton.admin.api.client.data.{ } import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.InstanceReference +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -61,8 +62,11 @@ sealed trait SequencerConnectionServiceIntegrationTest mediators = Seq(mediator1), overrideMediatorToSequencers = Some( Map( - mediator1 -> (Seq(sequencer1, sequencer2), - /* trust threshold */ PositiveInt.one, /* liveness margin */ NonNegativeInt.zero) + mediator1 -> MediatorSequencersConfiguration( + Seq(sequencer1, sequencer2), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ) ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/AcsCommitmentCrashIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/AcsCommitmentCrashIntegrationTest.scala index aa9c237624..75f26753ea 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/AcsCommitmentCrashIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/AcsCommitmentCrashIntegrationTest.scala @@ -152,6 +152,11 @@ class AcsCommitmentCrashIntegrationTest IouSyntax.createIou(participant1)(participant1.adminParty, participant2.adminParty) val period = awaitNextTick(participant1, participant2) - checkReceivedCommitment(period, participant2, daId, Match) + + // Check that we sent out the commitments (even if the send delay is zero) + checkSentCommitmentTo(Seq(participant2))(period, participant1, daId) + checkSentCommitmentTo(Seq(participant1))(period, participant2, daId) + + checkReceivedCommitments(period, participant2, daId, Match) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/MediatorFailoverIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/MediatorFailoverIntegrationTest.scala index b68df2a2da..be215539fc 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/MediatorFailoverIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/MediatorFailoverIntegrationTest.scala @@ -21,9 +21,9 @@ import com.digitalasset.canton.integration.plugins.{ } import com.digitalasset.canton.integration.tests.* import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, EnvironmentDefinition, - EnvironmentSetup, EnvironmentSetupPlugin, SharedEnvironment, TestConsoleEnvironment, @@ -34,7 +34,7 @@ import com.digitalasset.canton.sequencing.client.SequencerClient trait MediatorFailoverIntegrationTest extends ReliabilityTestSuite with ReplicatedMediatorTestSetup { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def startAndGet(external: UseExternalProcess)( mediatorName: String diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/ParticipantRestartTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/ParticipantRestartTest.scala index 158cbafa07..71b1ea8ef1 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/ParticipantRestartTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/ParticipantRestartTest.scala @@ -84,6 +84,7 @@ import com.digitalasset.canton.integration.util.{EntitySyntax, PartiesAllocator} import com.digitalasset.canton.ledger.error.groups.ConsistencyErrors.SubmissionAlreadyInFlight import com.digitalasset.canton.logging.ErrorLoggingContext import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.networking.Endpoint import com.digitalasset.canton.participant.ParticipantNodeParameters import com.digitalasset.canton.participant.admin.inspection.SyncStateInspection @@ -391,6 +392,7 @@ abstract class ParticipantRestartTest testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executionContext, timeouts, BatchingConfig(), @@ -610,7 +612,7 @@ class ParticipantRestartCausalityIntegrationTest extends ParticipantRestartTest EnvironmentDefinition.P4S2M2_Manual .addConfigTransforms( ConfigTransforms.updateTargetTimestampForwardTolerance(30.seconds), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => NetworkBootstrapper(EnvironmentDefinition.S1M1_S1M1) @@ -975,7 +977,7 @@ class ParticipantRestartRealClockIntegrationTest extends ParticipantRestartTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3S2M2_Manual .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ProgrammableSequencer.configOverride(getClass.toString, loggerFactory), ) @@ -1589,7 +1591,7 @@ class ParticipantRestartRealClockIntegrationTest extends ParticipantRestartTest } abstract class ParticipantRestartStaticTimeIntegrationTestBase( - alphaMultiSynchronizerSupport: Boolean = false + enableAllLedgerApiReassignments: Boolean = false ) extends ParticipantRestartTest { private val overrideMaxRequestSize = NonNegativeInt.tryCreate(100 * 1024) @@ -1608,7 +1610,8 @@ abstract class ParticipantRestartStaticTimeIntegrationTestBase( _.focus(_.sequencerClient.overrideMaxRequestSize).replace(Some(overrideMaxRequestSize)) ), ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport).replace(alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ), ) .withSetup { implicit env => @@ -1746,7 +1749,7 @@ abstract class ParticipantRestartStaticTimeIntegrationTestBase( participant1.repair.purge(daName, Seq(baselineContractId), ignoreAlreadyPurged = false) - val (repairOffset, repairRecordTime) = if (alphaMultiSynchronizerSupport) { + val (repairOffset, repairRecordTime) = if (enableAllLedgerApiReassignments) { participant1.ledger_api.updates .reassignments( Set(party), @@ -2199,7 +2202,7 @@ class ParticipantRestartStaticTimeIntegrationTest @UnstableTest // TODO(#30408) class ParticipantRestartStaticTimeReassignmentIntegrationTest - extends ParticipantRestartStaticTimeIntegrationTestBase(alphaMultiSynchronizerSupport = true) + extends ParticipantRestartStaticTimeIntegrationTestBase(enableAllLedgerApiReassignments = true) @nowarn("msg=match may not be exhaustive") class ParticipantRestartContractKeyIntegrationTest extends ParticipantRestartTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/SequencerRestartTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/SequencerRestartTest.scala index 2bb28e2e51..c007974e86 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/SequencerRestartTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/SequencerRestartTest.scala @@ -350,7 +350,7 @@ abstract class BaseSynchronizerRestartTest } -class SequencerRestartTest +final class SequencerRestartTest extends BaseSynchronizerRestartTest with FlagCloseable with HasCloseContext { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala index 55051ae349..1702cc7577 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala @@ -8,7 +8,7 @@ import com.digitalasset.canton.ConsoleScriptRunner import com.digitalasset.canton.config.CantonConfig import com.digitalasset.canton.console.BufferedProcessLogger import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.environment.Environment +import com.digitalasset.canton.environment.CantonEnvironment import com.digitalasset.canton.integration.{ CantonBaseIntegrationTest, ConfigTransform, @@ -20,7 +20,7 @@ import com.digitalasset.canton.util.Mutex import com.digitalasset.canton.util.ShowUtil.* abstract class ExampleIntegrationTest(configPaths: File*) - extends BaseIntegrationTest + extends CantonBaseIntegrationTest with IsolatedEnvironments with HasConsoleScriptRunner { @@ -73,7 +73,7 @@ abstract class ExampleIntegrationTest(configPaths: File*) trait HasConsoleScriptRunner { this: NamedLogging => import org.scalatest.EitherValues.* - def runScript(scriptPath: File)(implicit env: Environment): Unit = + def runScript(scriptPath: File)(implicit env: CantonEnvironment): Unit = ConsoleScriptRunner.run(env, scriptPath.toJava, logger = logger).value.discard } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala index 84c83ea9b7..9ad4c9cdb3 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala @@ -26,7 +26,7 @@ import org.slf4j.event.Level import scala.sys.process.Process class MultisyncExampleIntegrationTest - extends BaseIntegrationTest + extends CantonBaseIntegrationTest with IsolatedEnvironments with CommunityIntegrationTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/health/RemoteDumpIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/health/RemoteDumpIntegrationTest.scala index 344eb6402c..3bd8c748bb 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/health/RemoteDumpIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/health/RemoteDumpIntegrationTest.scala @@ -14,7 +14,7 @@ import com.digitalasset.canton.console.{ HealthDumpGenerator, InstanceReference, } -import com.digitalasset.canton.environment.{Environment, EnvironmentFactory} +import com.digitalasset.canton.environment.{CantonEnvironment, CantonEnvironmentFactory} import com.digitalasset.canton.integration.plugins.{ UseBftSequencer, UseExternalProcess, @@ -334,13 +334,13 @@ class NegativeRemoteDumpIntegrationTest registerPlugin(new UseBftSequencer(loggerFactory)) // Customize the environment factory to tweak the health dump generation - override protected val environmentFactory: EnvironmentFactory = + override protected val environmentFactory: CantonEnvironmentFactory = ( config: CantonConfig, loggerFactory: NamedLoggerFactory, testingConfigInternal: TestingConfigInternal, ) => - new Environment( + new CantonEnvironment( config, testingConfigInternal, ParticipantNodeBootstrapFactoryImpl, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsInitIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsInitIntegrationTest.scala new file mode 100644 index 0000000000..161eaac2c9 --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsInitIntegrationTest.scala @@ -0,0 +1,162 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.ledgerapi + +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, NonNegativeLong} +import com.digitalasset.canton.examples.java.iou.Dummy +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} +import com.digitalasset.canton.integration.{ + CommunityIntegrationTest, + ConfigTransform, + ConfigTransforms, + EnvironmentDefinition, + IsolatedEnvironments, + TestConsoleEnvironment, +} +import com.digitalasset.canton.logging.SuppressionRule +import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig +import com.digitalasset.canton.util.ResourceUtil.withResource +import monocle.macros.syntax.lens.* +import org.slf4j.event.Level + +import scala.concurrent.duration.* +import scala.concurrent.{Await, Future} +import scala.jdk.CollectionConverters.* + +trait AchsInitIntegrationTest extends CommunityIntegrationTest with IsolatedEnvironments { + + override lazy val environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition.P1_S1M1 + .addConfigTransforms( + ConfigTransforms.disableAchs, + ConfigTransforms.disableAdditionalConsistencyChecks, + ) + + private val achsLogRule: SuppressionRule = + SuppressionRule.LoggerNameContains("InitializeParallelIngestion") && SuppressionRule + .LevelAndAbove(Level.INFO) + + private def stopAllNodes(implicit env: TestConsoleEnvironment): Unit = { + import env.* + participants.all.synchronizers.disconnect(daName) + nodes.local.stop() + } + + "ACHS initialization is interrupted when participant is shut down during startup" when { + "shutdown is triggered via participant.stop()" in { implicit env => + runAchsInitInterruptionTest { participant => + participant.stop() + } + } + + "shutdown is triggered via participant.close()" in { implicit env => + runAchsInitInterruptionTest { participant => + // Bypass Nodes.stopAndWait and close the bootstrap directly. This + // exercises the isClosing-driven cancellation path (no + // cancelInitializationHint() call): OnShutdownRunner.close() flips + // isClosing as its first action, which the ACHS init pipe observes + // via externalShutdownSignal. + val bootstrap = participant.consoleEnvironment.environment.participants + .getStarting(participant.name) + .getOrElse(fail(s"participant ${participant.name} is not in StartingUp state")) + bootstrap.close() + } + } + } + + private def runAchsInitInterruptionTest( + triggerShutdown: com.digitalasset.canton.console.LocalParticipantReference => Unit + )(implicit env: TestConsoleEnvironment): Unit = { + import env.* + + participant1.synchronizers.connect_local(sequencer1, daName) + + participant1.dars.upload(CantonTestsPath) + + val alice = participant1.parties.enable("Alice") + + // Generate a lot of ACHS work (while ACHS is disabled) so that the + // eventual ACHS initialization, with `initParallelism = 1` and + // `initAggregationThreshold = 1`, takes a meaningful amount of time and + // can be reliably interrupted by a shutdown mid-init. + val createAndArchiveDummy = + new Dummy(alice.toProtoPrimitive) + .createAnd() + .exerciseArchive() + .commands + .asScala + .toSeq + val commandsPerTx = 100 + val txCount = 20 + for (i <- 1 to txCount) { + participant1.ledger_api.javaapi.commands.submit( + Seq(alice), + (1 to commandsPerTx).flatMap(_ => createAndArchiveDummy), + commandId = s"setup-dummy-$i", + ) + } + + val slowInitAchsConfig = AchsConfig( + validAtDistanceTarget = NonNegativeLong.tryCreate(10L), + lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(5L), + aggregationThreshold = 5L, + initParallelism = NonNegativeInt.tryCreate(1), + initAggregationThreshold = 1L, + ) + val slowInitAchsTransform: ConfigTransform = + ConfigTransforms.updateAllParticipantConfigs_( + _.focus(_.parameters.ledgerApiServer.indexer.achsConfig) + .replace(Some(slowInitAchsConfig)) + ) + // Force manual start so we drive participant startup ourselves and can race + // a shutdown against ACHS initialization. + val manualStartTransform: ConfigTransform = + c => c.copy(parameters = c.parameters.copy(manualStart = true)) + + stopAllNodes(env) + val newEnv = manualCreateEnvironmentWithPreviousState( + env.actualConfig, + _ => manualStartTransform(slowInitAchsTransform(env.actualConfig)), + ) + withResource(newEnv) { achsEnv => + import achsEnv.* + + sequencer1.start() + mediator1.start() + + val interruptedMessage = "ACHS snapshot initialization interrupted by shutdown request" + loggerFactory.assertLogsSeq(achsLogRule)( + { + // Start the participant in the background, start() blocks on full init, + // which in turn blocks on ACHS initialization completing. + val startF = Future(participant1.start()) + + eventually(60.seconds, maxPollInterval = 100.millis) { + loggerFactory.fetchRecordedLogEntries.exists( + _.message.contains("Initializing ACHS snapshot") + ) shouldBe true + } + + triggerShutdown(participant1) + scala.util.Try(Await.result(startF, 5.minute)) + }, + logEntries => { + val interruptedLogs = + logEntries.filter(_.message.contains(interruptedMessage)) + withClue( + s"Expected '$interruptedMessage' log message during shutdown. " + + s"All ACHS-related log entries:\n${logEntries.map(_.message).mkString("\n")}" + ) { + interruptedLogs should not be empty + } + }, + ) + } + } +} + +class AchsInitIntegrationTestPostgres extends AchsInitIntegrationTest { + registerPlugin(new UsePostgres(loggerFactory)) + registerPlugin(new UseBftSequencer(loggerFactory)) +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsIntegrationTest.scala index d27ecc8683..254fa23ac2 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsIntegrationTest.scala @@ -52,8 +52,8 @@ trait AchsIntegrationTest extends CommunityIntegrationTest with SharedEnvironmen storageBackendFactory.createParameterStorageBackend(ledgerApiStore.stringInterningView) ledgerApiStore.ledgerApiDbSupport.dbDispatcher .executeSql( - DatabaseMetrics.ForTesting("fetchACHSState") - )(parameterStorageBackend.fetchACHSState)( + DatabaseMetrics.ForTesting("fetchAchsState") + )(parameterStorageBackend.fetchAchsState)( LoggingContextWithTrace.empty ) .futureValue diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsRepairIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsRepairIntegrationTest.scala index d5a78be1f3..544717a02a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsRepairIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsRepairIntegrationTest.scala @@ -50,8 +50,8 @@ sealed trait AchsRepairIntegrationTest extends RepairServiceIntegrationTest { storageBackendFactory.createParameterStorageBackend(ledgerApiStore.stringInterningView) ledgerApiStore.ledgerApiDbSupport.dbDispatcher .executeSql( - DatabaseMetrics.ForTesting("fetchACHSState") - )(parameterStorageBackend.fetchACHSState)( + DatabaseMetrics.ForTesting("fetchAchsState") + )(parameterStorageBackend.fetchAchsState)( LoggingContextWithTrace.empty ) .futureValue diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiParticipantPruningTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiParticipantPruningTest.scala index 3590dbc093..60a5db52e9 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiParticipantPruningTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiParticipantPruningTest.scala @@ -38,11 +38,17 @@ import com.digitalasset.canton.ledger.error.LedgerApiErrors.ParticipantContractP import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.ParticipantPruningInProgress import com.digitalasset.canton.logging.SuppressionRule import com.digitalasset.canton.participant.ledger.api.client.JavaDecodeUtil +import com.digitalasset.canton.platform.store.backend.DataSourceStorageBackend.DataSourceConfig +import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation +import com.digitalasset.canton.platform.store.backend.common.QueryStrategy +import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache +import com.digitalasset.canton.platform.store.interning.MockStringInterning import com.digitalasset.canton.topology.SynchronizerId import com.digitalasset.daml.lf.value.Value.ContractId import monocle.macros.syntax.lens.* import org.slf4j.event +import java.sql.Connection import java.time.Duration as JDuration import java.util.UUID import java.util.concurrent.atomic.AtomicReference @@ -94,7 +100,7 @@ trait LedgerApiParticipantPruningTest ConfigTransforms.useStaticTime, ConfigTransforms.updateMaxDeduplicationDurations(transactionTolerance.asJava), lowerLedgerApiServerBatchSize, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* @@ -174,8 +180,7 @@ trait LedgerApiParticipantPruningTest participant1.pruning.get_offset_by_time(tsOfLastPrunedEvent.toInstant) // Simulate concurrent pruning from another replica by issuing the pruning lock - val lockedPruning = - participant1.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore.lockPruning + val lockedPruning = withConnectionForTest(participant1)(lockPruning(participant1)) // Prune and remember offsets. val pruneF = Future(participant1.pruning.prune(offsetAtTheBeginning)) @@ -194,16 +199,14 @@ trait LedgerApiParticipantPruningTest lockedPruning.commitAndClose() pruneF.futureValue participant1.testing.state_inspection.internalContractIdOf(cidBeginningContractId) shouldBe None - participant1.testing.state_inspection.internalContractIdOf( - cidMiddleContractId - ) should not be empty + val cidMiddleInternalContractIdOpt = + participant1.testing.state_inspection.internalContractIdOf(cidMiddleContractId) + cidMiddleInternalContractIdOpt should not be empty // Simulate blocking pruning by read locking one of the to-be-pruned contracts - val contractLock = - participant1.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore - .readLockContract( - participant1.testing.state_inspection.internalContractIdOf(cidMiddleContractId).value - ) + val contractLock = withConnectionForTest(participant1)( + readLockContract(participant1, cidMiddleInternalContractIdOpt.value) + ) // Pruning fails if contract pruning cannot resolve the optimistic lock after retries loggerFactory.assertThrowsAndLogs[CommandFailure]( @@ -465,31 +468,28 @@ trait LedgerApiParticipantPruningTest val c1InternalContractId = participant1.testing.state_inspection.internalContractIdOf(c1ContractId).value - val (_, c2cid) = createContract(participant1, daId) - val c2ContractId = ContractId.assertFromString(c2cid) - val c2InternalContractId = - participant1.testing.state_inspection.internalContractIdOf(c2ContractId).value - - // this is needed for the locking approach to work later - c1InternalContractId should be < (c2InternalContractId) - // unassign val unassign = participant1.ledger_api.commands.submit_unassign( submitter = participant1.adminParty, - contractIds = Seq(c1ContractId, c2ContractId), + contractIds = Seq(c1ContractId), source = daId, target = acmeId, ) val pruningOffset = participant1.ledger_api.state.end() - // issue write lock on participant1 for C1, this will block the Indexer at ingestion of the following assignation on the first contract - val c1Lock = - participant1.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore - .writeLockContract(c1InternalContractId) + // issue write lock on participant1 for C1, this will block the Indexer at ingestion of the following assignation on the contract + val c1Lock = withConnectionForTest(participant1)( + testFunction = writeLockContract(participant1, c1InternalContractId), + onCommit = conn => { + // simulate pruning by manually remove C1 from the contract store + deleteContract(participant1, c1InternalContractId)(conn) + logger.info("C1 contract removed") + }, + ) logger.info("C1 locked") - // reassign C1 and C2 to acme, this should be blocked on Indexing the assignment because of the lock above + // reassign C1 to acme, this should be blocked on Indexing the assignment because of the lock above val reassignmentF = Future( participant1.ledger_api.commands.submit_assign( submitter = participant1.adminParty, @@ -498,16 +498,11 @@ trait LedgerApiParticipantPruningTest target = acmeId, ) ) - logger.info("Reassignment of C1,C2 started") + logger.info("Reassignment of C1 started") // wait a little to make sure the assignment is already blocked Threading.sleep(5000) - logger.info("Waited 5 second") - - // simulate pruning by manually remove C2 from the contract store - participant1.testing.state_inspection.deleteContract(c2InternalContractId) - participant1.testing.state_inspection.internalContractIdOf(c2ContractId) shouldBe None - logger.info("C2 contract removed") + logger.info("Waited 5 seconds") loggerFactory.assertLogsSeq( SuppressionRule.Level(event.Level.INFO) && @@ -530,10 +525,10 @@ trait LedgerApiParticipantPruningTest }, ) - // C2 is reinserted - val c2NewInternalContractId = - participant1.testing.state_inspection.internalContractIdOf(c2ContractId).value - c2InternalContractId should be < (c2NewInternalContractId) + // C1 is reinserted + val c1NewInternalContractId = + participant1.testing.state_inspection.internalContractIdOf(c1ContractId).value + c1InternalContractId should be < (c1NewInternalContractId) // pruning before assign so that referential integrity is restored waitUntilSafeToPrune(participant1, Some(pruningOffset)) @@ -689,6 +684,60 @@ trait LedgerApiParticipantPruningTest .value offer.id.exerciseAcceptByPainter().commands.loneElement } + + private def withConnectionForTest( + participant: LocalParticipantReference + )(testFunction: Connection => Unit, onCommit: Connection => Unit = _ => ()) = { + val ledgerApiStore = + participant.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore + val conn = + ledgerApiStore.ledgerApiDbSupport.storageBackendFactory.createDataSourceStorageBackend + .createDataSource( + dataSourceConfig = DataSourceConfig(ledgerApiStore.ledgerApiStorage.jdbcUrl), + loggerFactory = loggerFactory, + ) + .getConnection + conn.setAutoCommit(false) + QueryStrategy.withoutNetworkTimeout(testFunction(_))(conn, noTracingLogger) + new Object { + def commitAndClose(): Unit = { + onCommit(conn) + conn.commit() + conn.close() + } + } + } + + private def eventStorageBackend(participant: LocalParticipantReference) = + participant.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore.ledgerApiDbSupport.storageBackendFactory + .createEventStorageBackend( + ledgerEndCache = MutableLedgerEndCache(), + stringInterning = new MockStringInterning, + loggerFactory = loggerFactory, + ) + + private def lockPruning(participant: LocalParticipantReference)(conn: Connection) = + eventStorageBackend(participant).lockExclusivelyPruningProcessingTable(conn) + + private def readLockContract(participant: LocalParticipantReference, internalContractId: Long)( + conn: Connection + ) = eventStorageBackend(participant).readLockInternalContractIds(Set(internalContractId))(conn) + + private def writeLockContract(participant: LocalParticipantReference, internalContractId: Long)( + conn: Connection + ) = + eventStorageBackend(participant).writeLockInternalContractIds(cSQL"= $internalContractId")(conn) + + def deleteContract(participant: LocalParticipantReference, internalContractId: Long)(implicit + conn: Connection + ): Int = { + val removed = SQL"DELETE FROM par_contracts WHERE internal_contract_id=$internalContractId" + .executeUpdate()(conn) + participant.underlying.value.sync.participantNodePersistentState.value.contractStore + .contractsPruned(List(internalContractId)) + removed + } + } class LedgerApiParticipantPruningTestPostgres extends LedgerApiParticipantPruningTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/GetCompletionsAuthIT.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/GetCompletionsAuthIT.scala new file mode 100644 index 0000000000..afdaf642d2 --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/GetCompletionsAuthIT.scala @@ -0,0 +1,118 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.ledgerapi.auth + +import com.daml.grpc.test.StreamConsumer +import com.daml.ledger.api.v2.command_completion_service.{ + CommandCompletionServiceGrpc, + CompletionStreamResponse, + GetCompletionsRequest, +} +import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits.* +import com.digitalasset.canton.integration.TestConsoleEnvironment +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UseH2} +import com.digitalasset.canton.integration.tests.ledgerapi.services.SubmitAndWaitDummyCommand +import io.grpc.stub.StreamObserver + +import scala.concurrent.Future + +final class GetCompletionsAuthIT + extends ExpiringStreamServiceCallAuthTests[CompletionStreamResponse] + with SubmitAndWaitDummyCommand { + registerPlugin(new UseH2(loggerFactory)) + registerPlugin(new UseBftSequencer(loggerFactory)) + + override def serviceCallName: String = "CommandCompletionService#GetCompletions" + + override protected def stream( + context: ServiceCallContext, + env: TestConsoleEnvironment, + ): StreamObserver[CompletionStreamResponse] => Unit = + streamFor(context) + + private def mkRequest(parties: List[String]) = + GetCompletionsRequest(parties, 0) + + private def streamFor( + context: ServiceCallContext + ): StreamObserver[CompletionStreamResponse] => Unit = + observer => + stub(CommandCompletionServiceGrpc.stub(channel), context.token) + .getCompletions(mkRequest(List(context.mainActorId)), observer) + + override def serviceCall(context: ServiceCallContext)(implicit + env: TestConsoleEnvironment + ): Future[Any] = { + import env.* + val mainActorId = getMainActorId + submitAndWaitAsMainActor(mainActorId).flatMap(_ => + new StreamConsumer[CompletionStreamResponse]( + streamFor(context.copy(mainActorId = mainActorId)) + ).first() + ) + } + + serviceCallName should { + "allow calls with valid parties" taggedAs securityAsset + .setHappyCase( + "Ledger API client can make a GetCompletions call for its own parties" + ) in { implicit env => + import env.* + expectSuccess(serviceCall(canActAsMainActor)) + } + + "deny calls with empty parties for user without CanReadAsAnyParty" taggedAs securityAsset + .setAttack( + attackPermissionDenied(threat = + "Present a JWT without CanReadAsAnyParty and request completions with empty parties" + ) + ) in { implicit env => + import env.* + val mainActorId = getMainActorId + expectPermissionDenied( + submitAndWaitAsMainActor(mainActorId).flatMap { _ => + new StreamConsumer[CompletionStreamResponse](observer => + stub(CommandCompletionServiceGrpc.stub(channel), canActAsMainActor.token) + .getCompletions(mkRequest(List.empty), observer) + ).first() + } + ) + } + + "allow calls with empty parties for CanReadAsAnyParty user" taggedAs securityAsset + .setHappyCase( + "Ledger API client with CanReadAsAnyParty can call GetCompletions with empty parties" + ) in { implicit env => + import env.* + val mainActorId = getMainActorId + expectSuccess( + submitAndWaitAsMainActor(mainActorId).flatMap { _ => + new StreamConsumer[CompletionStreamResponse](observer => + stub(CommandCompletionServiceGrpc.stub(channel), canReadAsAnyParty.token) + .getCompletions(mkRequest(List.empty), observer) + ).first() + } + ) + } + + "deny calls requesting a party the user is not authorized for" taggedAs securityAsset + .setAttack( + attackPermissionDenied(threat = + "Present a JWT authorized for the main actor and request completions for a foreign party" + ) + ) in { implicit env => + import env.* + val mainActorId = getMainActorId + val foreignParty = getRandomPartyId + expectPermissionDenied( + submitAndWaitAsMainActor(mainActorId).flatMap { _ => + new StreamConsumer[CompletionStreamResponse](observer => + stub(CommandCompletionServiceGrpc.stub(channel), canActAsMainActor.token) + .getCompletions(mkRequest(List(foreignParty)), observer) + ).first() + } + ) + } + } +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/IDPBoxingServiceCallOutTests.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/IDPBoxingServiceCallOutTests.scala index 94be66f9d3..667f5d22b0 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/IDPBoxingServiceCallOutTests.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/IDPBoxingServiceCallOutTests.scala @@ -141,6 +141,46 @@ trait IDPBoxingServiceCallOutTests } } + "IDP admin granting rights that transcend IDP boundaries" should { + for ( + (description, kind) <- List[(String, uproto.Right.Kind)]( + ( + "read as any party", + uproto.Right.Kind.CanReadAsAnyParty(uproto.Right.CanReadAsAnyParty()), + ), + ( + "execute as any party", + uproto.Right.Kind.CanExecuteAsAnyParty(uproto.Right.CanExecuteAsAnyParty()), + ), + ( + "participant admin", + uproto.Right.Kind.ParticipantAdmin(uproto.Right.ParticipantAdmin()), + ), + ) + ) { + s"deny granting $description rights" taggedAs adminSecurityAsset + .setAttack( + attackUnknownResource(threat = s"Grant $description rights") + ) in { implicit env => + import env.* + loggerFactory.suppress(AuthServiceJWTSuppressionRule) { + expectPermissionDenied { + val suffix = UUID.randomUUID().toString + for { + (_, idpAdminContext, _) <- createIDPBundle(canBeAnAdmin, suffix) + + _ <- boxedCall( + "user-" + suffix, + idpAdminContext, + Vector(uproto.Right(kind)), + ) + } yield () + } + } + } + } + } + "allow Admin granting permissions to parties which do not exist" taggedAs adminSecurityAsset .setHappyCase( "Grant rights to non existing parties" diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/fixture/CantonFixture.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/fixture/CantonFixture.scala index d31bc18f2a..01a57cacc7 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/fixture/CantonFixture.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/fixture/CantonFixture.scala @@ -8,10 +8,10 @@ import com.digitalasset.canton.config.{AuthServiceConfig, PositiveDurationSecond import com.digitalasset.canton.console.LocalParticipantReference import com.digitalasset.canton.integration.tests.ledgerapi.auth.SandboxRequiringAuthorizationFuns import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransforms, EnvironmentDefinition, - EnvironmentSetup, IsolatedEnvironments, SharedEnvironment, TestConsoleEnvironment, @@ -47,7 +47,7 @@ trait CantonFixtureIsolated trait CantonFixtureAbstract extends CommunityIntegrationTest with SandboxRequiringAuthorizationFuns { - this: EnvironmentSetup => + this: CantonEnvironmentSetup => protected def darFile = new File(CantonTestsPath) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/ExternalPartyOnboardingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/ExternalPartyOnboardingIntegrationTest.scala index 00ac40d346..6344e3fb5a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/ExternalPartyOnboardingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/ExternalPartyOnboardingIntegrationTest.scala @@ -9,7 +9,9 @@ import com.digitalasset.canton.config import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.console.CommandFailure import com.digitalasset.canton.console.commands.PartiesAdministration +import com.digitalasset.canton.crypto.KeyPurpose.Signing import com.digitalasset.canton.crypto.{SigningKeyUsage, SigningKeysWithThreshold} +import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.MediatorError import com.digitalasset.canton.integration.{ CommunityIntegrationTest, @@ -17,16 +19,21 @@ import com.digitalasset.canton.integration.{ EnvironmentDefinition, HasCycleUtils, SharedEnvironment, + TestConsoleEnvironment, } import com.digitalasset.canton.logging.LogEntry import com.digitalasset.canton.participant.topology.ParticipantTopologyManagerError.ExternalPartyAlreadyExists import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId -import com.digitalasset.canton.topology.transaction.DelegationRestriction.CanSignAllMappings +import com.digitalasset.canton.topology.transaction.DelegationRestriction.{ + CanSignAllButNamespaceDelegations, + CanSignAllMappings, +} import com.digitalasset.canton.topology.transaction.ParticipantPermission.{ Confirmation, Observation, } import com.digitalasset.canton.topology.transaction.{ + DelegationRestriction, HostingParticipant, MultiTransactionSignature, NamespaceDelegation, @@ -85,6 +92,179 @@ class ExternalPartyOnboardingIntegrationTest extends ExternalPartyOnboardingInte .party shouldBe patrick.partyId } + "sign the external party transaction for the participant" when { + // This test case checks that a participant signs external party allocations only for itself, even if + // the participant owns a signing key with a delegation from another participant. This restriction + // is in place to avoid a loophole where a participant might not want to host an external party, + // but through a namespace delegation, the external party might try to acquire the participant's signature + // through other means (e.g. by submitting the allocation via a participant with a namespace delegation in place). + "the participant also has a delegated key for another participant" in { implicit env => + import env.* + + // set up the key delegation from participant2 to a key owned by participant1 + val delegatedKey = + participant1.keys.secret.generate_signing_key(usage = Set(SigningKeyUsage.Namespace)) + participant2.topology.namespace_delegations.propose_delegation( + participant2.namespace, + delegatedKey, + DelegationRestriction.CanSignSpecificMappings(PartyToParticipant.code), + store = daId, + ) + + eventually() { + participant1.topology.namespace_delegations + .list( + daId, + filterNamespace = participant2.namespace.filterString, + filterTargetKey = Some(delegatedKey.fingerprint), + ) + .loneElement + .discard + } + // validate that the delegation from participant2 to participant1's key actually works for PTPs + participant1.parties.enable( + "party-on-p2", + namespace = participant2.namespace, + synchronizer = daName, + ) + + val partyId = allocateExternalParty().partyId + + val ptp = eventually() { + participant2.topology.party_to_participant_mappings + .list(daId, proposals = true, filterParty = partyId) + .loneElement + } + // future-proofing the test: ensure that the PTP has both participants as hosting participants (and therefore eligigle for signing it) + ptp.item.participantIds should contain theSameElementsAs Seq( + participant1.id, + participant2.id, + ) + // check that participant1 only signed with the key for participant1, even though it owns a delegated key for participant2 + ptp.context.signedBy.forgetNE.loneElement shouldBe participant1.fingerprint + + // clean up: revoke the delegation again + participant2.topology.namespace_delegations.propose_revocation( + participant2.namespace, + targetKey = delegatedKey, + store = daId, + ) + + eventually() { + participant1.topology.namespace_delegations.list( + daId, + filterNamespace = participant2.namespace.filterString, + filterTargetKey = Some(delegatedKey.fingerprint), + ) shouldBe empty + } + + } + "the participant has an offline root key" in { implicit env => + import env.* + + // download the root key so that we can restore it at the end of the test + val rootNamespaceKey = participant1.keys.secret.download(participant1.fingerprint) + + // delete the root namespace key to make it "offline" + participant1.keys.secret.delete(participant1.fingerprint, force = true) + + // try to allocate the external party without any valid topology signing key + loggerFactory.assertThrowsAndLogs[CommandFailure]( + allocateExternalParty(), + _.errorMessage should include( + "Could not find an appropriate signing key to issue the topology transaction" + ), + ) + + // temporarily restore the root namespace key and issue a namespace delegation + // for an intermediate key + participant1.keys.secret.upload(rootNamespaceKey, name = None) + + val intermediateKey = + participant1.keys.secret.generate_signing_key(usage = Set(SigningKeyUsage.Namespace)) + participant1.topology.namespace_delegations.propose_delegation( + participant1.namespace, + intermediateKey, + CanSignAllButNamespaceDelegations, + store = daId, + ) + + eventually() { + participant1.topology.namespace_delegations + .list(daId, filterTargetKey = Some(intermediateKey.fingerprint)) + .loneElement + } + + // delete the root namespace key again + participant1.keys.secret.delete(participant1.fingerprint, force = true) + + // allocating the external party should work now + val externalPartyId = allocateExternalParty().partyId + + // validate that the mapping for the external party exists, + // and that it was only signed by intermediate key + eventually() { + participant1.topology.party_to_participant_mappings + .list( + daId, + proposals = true, + filterParty = externalPartyId, + ) + .loneElement + .context + .signedBy + .forgetNE should contain theSameElementsAs Seq(intermediateKey.fingerprint) + } + + participant1.keys.secret.upload(rootNamespaceKey, name = None) + } + } + + /** Allocates an external party with a random name in sequencer1's namespace with participant1 + * and participant2 as the hosting participants. + */ + def allocateExternalParty()(implicit env: TestConsoleEnvironment) = { + import env.* + participant1.ledger_api.parties + .allocate_external( + daId, + Seq( + TopologyTransaction( + TopologyChangeOp.Replace, + PositiveInt.one, + PartyToParticipant.tryCreate( + PartyId.tryCreate(UUID.randomUUID().toString, sequencer1.namespace), + PositiveInt.one, + Seq( + HostingParticipant(participant1.id, ParticipantPermission.Confirmation), + HostingParticipant(participant2.id, ParticipantPermission.Confirmation), + ), + partySigningKeysWithThreshold = Some( + SigningKeysWithThreshold( + NonEmpty( + Set, + // pick some key as the party's signing key + sequencer1.keys.public + .list( + filterPurpose = Set(Signing), + filterUsage = Set(SigningKeyUsage.Protocol), + ) + .head + .publicKey + .asSigningKey + .value, + ), + PositiveInt.one, + ) + ), + ), + testedProtocolVersion, + ) -> Seq.empty + ), + Seq.empty, + ) + } + "allocate a party with a PartyToKeyMapping" in { implicit env => import env.* val namespaceKey = global_secret.keys.secret diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftOrderingBenchmark.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftOrderingBenchmark.scala index c58331d5fb..13918eb5a4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftOrderingBenchmark.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftOrderingBenchmark.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.integration.tests.manual +import com.digitalasset.canton.config.ReplicationConfig import com.digitalasset.canton.config.RequireTypes.{ NonNegativeInt, Port, @@ -66,6 +67,7 @@ import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} * -Dscala.concurrent.context.numThreads=30 \ * -Dbft-ordering-benchmark.num-db-connections-per-node=5 \ * -Dbft-ordering-benchmark.transaction-sizes-and-weights={payloads=[{size-bytes=2000,weight=1}]} \ + * -Dbft-ordering-benchmark.test-catchup={nodes-to-stop=[2],duration-nodes-are-down=1minutes,duration-node-need-to-startup=10seconds}\ * -Dbft-ordering-benchmark.benchmark-duration=1minute" * * export CI=1 # When this defined, it ensures no dockerized Postgres is being used @@ -158,7 +160,7 @@ class BftOrderingBenchmark PositiveInt.tryCreate( Option(System.getProperty(s"$BFTOrderingBenchmarkPrefix.num-db-connections-per-node")) .map(_.toInt) - .getOrElse(5) + .getOrElse(12) ) /** Tracing options. Disabled by default. */ @@ -228,6 +230,27 @@ class BftOrderingBenchmark Option(System.getProperty(s"$BFTOrderingBenchmarkPrefix.sequencer-db-latency-millis")) .map(_.toLong) + /** Whether DB replication (`DbMultiStorage`) is enabled. Default is [[Some(true)]]. To disable, + * set to [[Some(false)]]. Note that `DbMultiStorage` uses a separate connection pool, reserving + * roughly half of the total DB connections available in the [[num-db-connections-per-node]]. + */ + private val dbReplicationEnabled: Option[Boolean] = + Option(System.getProperty(s"$BFTOrderingBenchmarkPrefix.db-replication-enabled")) + .map(_.toBoolean) + .orElse(Some(true)) + + private val testCatchupConfig: BftBenchmarkConfig.TestCatchup = + Option(System.getProperty(s"$BFTOrderingBenchmarkPrefix.test-catchup")) + .map { s => + val result = + ConfigSource + .string(s) + .load[BftBenchmarkConfig.TestCatchup] + result.left.foreach(errors => logger.error(s"Failed to parse testCatchup config: $errors")) + result.getOrElse(throw new RuntimeException("Invalid test catchup configuration")) + } + .getOrElse(BftBenchmarkConfig.TestCatchup.NoTestCatchup) + registerPlugin( new UsePostgres( loggerFactory, @@ -279,6 +302,15 @@ class BftOrderingBenchmark ) } }) + .addConfigTransforms( + ConfigTransforms.updateAllSequencerConfigs { case (_, config) => + dbReplicationEnabled.fold(config) { replicationEnabled => + config + .focus(_.replication) + .replace(Some(ReplicationConfig(enabled = Some(replicationEnabled)))) + } + } + ) .addConfigTransforms( _.focus(_.monitoring.tracing.tracer).replace( TracingConfig.Tracer( @@ -377,36 +409,49 @@ class BftOrderingBenchmark waitUntilAllBftSequencersAuthenticateDisseminationQuorum(5.minutes) + val nodesToStop = env.sequencers.local.zipWithIndex + .filter(x => testCatchupConfig.nodesToStop.contains(x._2)) + .map(_._1) + + if (nodesToStop.nonEmpty) { + + nodesToStop.foreach(_.stop()) + + env.actorSystem.scheduler.scheduleOnce(testCatchupConfig.durationNodesAreDown) { + nodesToStop.foreach(_.start()) + } + } + val benchmarkTool = new BftBenchmarkTool(new DaBftBindingFactory(loggerFactory), loggerFactory) + val p2pEndpoints = bftSequencerPlugin.p2pEndpoints.getOrElse(fail("No P2P endpoints found")) val benchmarkToolConfig = BftBenchmarkConfig( transactionSizesAndWeights = transactionSizesAndWeights.payloads, + testCatchup = testCatchupConfig, runDuration = runDuration, perNodeWritePeriod = perNodeWritePeriod, reportingInterval = reportingIntervalOpt, - nodes = bftSequencerPlugin.p2pEndpoints - .getOrElse(fail("No P2P endpoints found")) - .values - .zipWithIndex - .map { case (p2pConfig, idx) => - val host = p2pConfig.address - val port = p2pConfig.port.unwrap - val node: BftBenchmarkConfig.Node = - if (idx == 0) { - BftBenchmarkConfig.NetworkedReadWriteNode( - host = host, - writePort = port, - readPort = port, - ) - } else { - BftBenchmarkConfig.NetworkedWriteOnlyNode( - host = host, - writePort = port, - ) - } - node - } - .toSeq, + nodes = env.sequencers.local.zipWithIndex.map { case (sequencer, idx) => + val name = sequencer.name + val p2pConfig = p2pEndpoints(name) + + val host = p2pConfig.address + val port = p2pConfig.port.unwrap + val node: BftBenchmarkConfig.Node = + if (idx == 0) { + BftBenchmarkConfig.NetworkedReadWriteNode( + host = host, + writePort = port, + readPort = port, + ) + } else { + BftBenchmarkConfig.NetworkedWriteOnlyNode( + host = host, + writePort = port, + ) + } + node + }, ) benchmarkTool.run(benchmarkToolConfig).discard } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftSingleNodePerformanceTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftSingleNodePerformanceTest.scala index d20bb417e9..d27c6a1d96 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftSingleNodePerformanceTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftSingleNodePerformanceTest.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.console.{ LocalSequencerReference, } import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -128,7 +129,11 @@ class BftSingleNodePerformanceTest mediators = allMediators_, overrideMediatorToSequencers = Some( allMediators_.map { mediator => - mediator -> (Seq(sequencer1), PositiveInt.one, NonNegativeInt.zero) + mediator -> MediatorSequencersConfiguration( + Seq(sequencer1), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ) }.toMap ), mediatorThreshold = PositiveInt.one, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/DataContinuityTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/DataContinuityTest.scala index 758e54fab8..1bfe5463dd 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/DataContinuityTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/DataContinuityTest.scala @@ -39,9 +39,7 @@ import com.digitalasset.canton.integration.{ SharedEnvironment, TestConsoleEnvironment, } -import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality import com.digitalasset.canton.logging.{LogEntry, TracedLogger} -import com.digitalasset.canton.resource.DatabaseStorageError import com.digitalasset.canton.synchronizer.sequencer.ProgrammableSequencer import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext @@ -103,34 +101,6 @@ trait DataContinuityTest // Set to true if you want to persist the dumps locally even if a test container is found lazy val forceLocalDumps = false - // Suppress flaky DB task rejection warnings during outer environment transitions and final teardown. - // When nodes are stopped, Slick connection pools terminate immediately ("Terminated"). If background - // processes (like the ACS Commitment Processor running heavy SQL queries) are still executing, - // subsequent pool submissions trigger benign DB_STORAGE_DEGRADATION warnings caused by underlying - // RejectedExecutionException failures. - // - // TODO(#16601): This is a workaround for an uncoordinated shutdown sequence - // A coordinated graceful shutdown that actively cancels ongoing queries would make this suppression obsolete. - protected def assertBenignDbStorageDegradationShutdown(e: LogEntry): Assertion = - e.shouldBeCantonError( - DatabaseStorageError.DatabaseStorageDegradation, - messageAssertion = msg => { - // Check the top-level message text - msg should include("A database task was rejected from the database task queue") - }, - contextAssertion = mdc => { - // Inspect the raw error context dumped by Slick into the MDC map - val slickMsg = mdc.getOrElse("messageFromSlick", "") - slickMsg should include("RejectedExecutionException") - // These strings originate from java.util.concurrent.ThreadPoolExecutor#toString(). - // We match only shutdown phases to suppress benign teardown races, while ensuring - // actual load-based queue rejections ("Running") remain visible. - slickMsg should include regex "Shutting down|Terminated" - }, - loggerAssertion = - loggerName => loggerName should startWith("com.digitalasset.canton.resource.DbStorageMulti"), - ) - override val logsToBeHandledAtStartup: Option[Seq[LogEntry] => Assertion] = Some( LogEntry.assertLogSeq( Seq.empty, @@ -143,8 +113,6 @@ trait DataContinuityTest s"Using a session signing key is not possible with protocol version 34." ) ), - // Flake prevention: Suppress trailing DB task rejections during mid-test dump restorations via loadState - assertBenignDbStorageDegradationShutdown, ), ) ) @@ -154,12 +122,7 @@ trait DataContinuityTest protocolVersion: ProtocolVersion, )(f: TestConsoleEnvironment => Unit): Unit = { - // Flake prevention: Catch benign DB rejections when stopping the old environment. - // Wrapped separately to avoid nesting suppression scopes. - loggerFactory.assertLogsUnorderedOptional( - oldEnv.nodes.local.foreach(_.stop()), - LogEntryOptionality.OptionalMany -> assertBenignDbStorageDegradationShutdown, - ) + oldEnv.nodes.local.foreach(_.stop()) val newEnv = manualCreateEnvironment( initialConfig = oldEnv.environment.config, configTransform = config => @@ -173,12 +136,7 @@ trait DataContinuityTest logger.info(s"About to run with protocol version $protocolVersion") f(newEnv) } finally { - // Flake prevention: Catch benign DB rejections during final environment teardown. - // Wrapped separately so mid-test calls to `handleStartupLogs` do not trigger nested suppression errors. - loggerFactory.assertLogsUnorderedOptional( - destroyEnvironment(newEnv), - LogEntryOptionality.OptionalMany -> assertBenignDbStorageDegradationShutdown, - ) + destroyEnvironment(newEnv) } } @@ -727,16 +685,11 @@ trait SynchronizerChangeDataContinuityTest extends SynchronizerChangeDataContinu val unassignedEvent = incompleteUnassignedEvents.loneElement // act on state clue("starting assignment and paint offer acceptance") { - // Flake prevention: Catch trailing DB task rejections if background processors - // flake concurrently with active test execution. - loggerFactory.assertLogsUnorderedOptional( - assignmentAndPaintOfferAcceptance( - Alice.toPartyId(), - Bank.toPartyId(), - Painter.toPartyId(), - unassignedEvent.entry.getUnassignedEvent, - ), - LogEntryOptionality.OptionalMany -> assertBenignDbStorageDegradationShutdown, + assignmentAndPaintOfferAcceptance( + Alice.toPartyId(), + Bank.toPartyId(), + Painter.toPartyId(), + unassignedEvent.entry.getUnassignedEvent, ) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/S3Synchronization.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/S3Synchronization.scala index eba41b394f..6048be328e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/S3Synchronization.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/S3Synchronization.scala @@ -203,7 +203,7 @@ object S3Synchronization { final case class ContinuityDumpS3Ref(override val path: String) extends ContinuityDumpRef { lazy val localDownloadPath: File = { val syncCommand = - s"aws s3 sync s3://canton-public-releases/data-continuity-dumps/$path ${baseDbDumpPath.path}/$path --no-sign-request" + s"aws s3 sync s3://canton-public-releases/data-continuity-dumps/$path ${baseDbDumpPath.path}/$path --no-sign-request --quiet" val syncResult = runSynchronized(syncCommand) if (syncResult != 0) { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/topology/TopologyStateVerification.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/topology/TopologyStateVerification.scala index ae183ad196..bf8a0adfe2 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/topology/TopologyStateVerification.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/topology/TopologyStateVerification.scala @@ -281,6 +281,7 @@ class TopologyStateVerification( ReleaseProtocolVersion(BaseTest.testedProtocolVersion), futureSupervisor, clock, + CommonMockMetrics.cryptoMetrics, executionContext, timeouts, BatchingConfig(), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/metrics/SequencerConnectionPoolMetricsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/metrics/SequencerConnectionPoolMetricsIntegrationTest.scala new file mode 100644 index 0000000000..3a75ef82bd --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/metrics/SequencerConnectionPoolMetricsIntegrationTest.scala @@ -0,0 +1,232 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.metrics + +import com.daml.metrics.api.MetricQualification +import com.digitalasset.canton.admin.api.client.data.{ + GrpcSequencerConnection, + SubmissionRequestAmplification, +} +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} +import com.digitalasset.canton.console.{ + InstanceReference, + LocalInstanceReference, + LocalMediatorReference, + LocalParticipantReference, +} +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration +import com.digitalasset.canton.integration.bootstrap.{ + NetworkBootstrapper, + NetworkTopologyDescription, +} +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} +import com.digitalasset.canton.integration.{ + CommunityIntegrationTest, + EnvironmentDefinition, + SharedEnvironment, +} +import com.digitalasset.canton.metrics.{MetricsConfig, MetricsReporterConfig} +import com.digitalasset.canton.{SequencerAlias, UniquePortGenerator, config} +import monocle.macros.syntax.lens.* + +import scala.concurrent.duration.* + +/** This test checks that the sequencer connection pool metrics properly obtain a `psid` label in + * their context. + * + * The environment is as follows: + * - 2 participants + * - 4 sequencers + * - 1 mediator + * + * The participants and the mediator connect to all sequencers with a trust threshold of 4. + * + * The test first restarts all the participants because the `psid` is known only starting at the + * second connect. + * + * The test then pings participant2 from participant1, and proceeds to examine all the connection + * pool metrics for all participant and mediator nodes, validating that they have the `psid` label + * where expected. + */ +final class SequencerConnectionPoolMetricsIntegrationTest + extends CommunityIntegrationTest + with SharedEnvironment { + registerPlugin(new UsePostgres(loggerFactory)) + registerPlugin(new UseBftSequencer(loggerFactory)) + + override def environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition.P2S4M1_Config + .addConfigTransform( + _.focus(_.monitoring.metrics) + .replace( + MetricsConfig( + qualifiers = MetricQualification.All, + reporters = Seq( + MetricsReporterConfig.Prometheus( + port = UniquePortGenerator.next + ) + ), + ) + ) + ) + .withNetworkBootstrap { implicit env => + import env.* + + new NetworkBootstrapper( + NetworkTopologyDescription( + daName, + synchronizerOwners = Seq[InstanceReference](sequencer1, mediator1), + synchronizerThreshold = PositiveInt.one, + sequencers = sequencers.local, + mediators = Seq(mediator1), + overrideMediatorToSequencers = Some( + Map( + mediator1 -> MediatorSequencersConfiguration( + sequencers.local, + trustThreshold = PositiveInt.four, + livenessMargin = NonNegativeInt.zero, + ) + ) + ), + ) + ) + } + .withSetup { implicit env => + import env.* + + val amplification = SubmissionRequestAmplification( + factor = 20, + patience = config.NonNegativeFiniteDuration.tryFromDuration(1.seconds), + ) + + Seq(participant1, participant2).foreach( + _.synchronizers.connect_bft( + sequencers.local.map(s => + GrpcSequencerConnection.fromInternal( + s.config.publicApi.clientConfig + .asSequencerConnection(SequencerAlias.tryCreate(s.name), sequencerId = None) + ) + ), + synchronizerAlias = daName, + sequencerTrustThreshold = PositiveInt.four, + sequencerLivenessMargin = NonNegativeInt.zero, + submissionRequestAmplification = amplification, + ) + ) + } + + "SequencerConnectionPoolMetrics" should { + "have the `psid` label after the first connect" in { implicit env => + import env.* + + val nodes = Seq[LocalInstanceReference](participant1, participant2, mediator1) + + // For participants, the `psid` will be populated starting at the second connect + clue("disconnect and reconnect participants") { + participants.all.foreach(_.synchronizers.disconnect_all()) + participants.all.foreach(_.synchronizers.reconnect_all()) + } + + participant1.health.ping(participant2) + + clue("check metrics") { + val psidKeyName = "psid" + val prefix = "daml.sequencer-client.sequencer-connection-pool" + + forAll(nodes) { node => + val metrics = node.metrics.list(prefix) + // Ensure the test fails if we change the prefix, which would result in an empty `metrics` + metrics.size should be >= 8 + + node match { + case _: LocalMediatorReference => + // Mediators have their `psid` always defined, even at their first connection + forAll(metrics) { case (_name, values) => + values.size should be >= 1 + forAll(values)(_.attributes should contain key psidKeyName) + } + + case _: LocalParticipantReference => + val uniqueMetrics = Seq( + "trust-threshold", + "tracked-connections", + "validated-connections", + "subscription-threshold", + "active-subscriptions", + ).map(m => s"$prefix.$m") + + forAll(metrics) { + case (name, values) if uniqueMetrics.contains(name) => + // These metrics don't have separate instances for different sets of labels, so they will all have the `psid` + values.loneElement.attributes should contain key psidKeyName + + case (name, values) + if name == s"$prefix.connection-health" || name == s"$prefix.subscription-health" => + // The metrics on the first connect will be without `psid`, but they are closed when disconnecting. + // Metrics for the second connect will have the `psid`. + // With 4 sequencers and trust threshold = 4, we will have 4 connections and 4 subscriptions. + values should have size 4 + forAll(values)(_.attributes should contain key psidKeyName) + + case (name, values) if name == s"$prefix.grpc-requests" => + // These metrics are counters and are not closed when the pool closes, so the metrics without `psid` will be around + + val metricsPerEndPoint = values.groupBy(_.attributes("endpoint")) + val endpointsWithPsidOnAllConnections = Seq( + "GetApiInfo", + "GetSynchronizerId", + "GetSynchronizerParameters", + "Handshake", + "Authenticate", + "Challenge", + "Subscribe", + ) + val endpointsWithPsidOnSomeConnections = Seq( + "SendAsync", + "AcknowledgeSigned", + ) + + val endpointsWithoutPsid = Seq( + "DownloadTopologyStateForInit", + "DownloadTopologyStateForInitHash", + ) + + val endpointsNotCalled = Seq( + "Logout", + "GetTime", + "GetTrafficStateForMember", + ) + + forAll(metricsPerEndPoint) { + case (endpoint, metrics) + if endpointsWithPsidOnAllConnections.contains(endpoint) => + // All the connections will have a `psid` because all the connections use these endpoints + forExactly(4, metrics)(_.attributes should contain key psidKeyName) + + case (endpoint, metrics) + if endpointsWithPsidOnSomeConnections.contains(endpoint) => + // Not all connections will necessarily have a `psid` because not all connections may use these endpoints + forAtLeast(1, metrics)(_.attributes should contain key psidKeyName) + + case (endpoint, metrics) if endpointsWithoutPsid.contains(endpoint) => + // There are no metric with `psid` because these endpoints are only used during the first connection + forAll(metrics)(_.attributes should not(contain key psidKeyName)) + + case (endpoint, _) if endpointsNotCalled.contains(endpoint) => + // These endpoints should not be called during this test + fail(s"endpoint should not have been called: $endpoint") + + case (endpoint, _) => fail(s"unknown endpoint: $endpoint") + } + + case (other, _) => fail(s"unexpected metric: $other") + } + + case _ => fail(s"unexpected node type: $node") + } + } + } + } + } +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/AutomaticReassignmentDecentralizedPartyIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/AutomaticReassignmentDecentralizedPartyIntegrationTest.scala index ae61cb77ed..5e67e4d947 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/AutomaticReassignmentDecentralizedPartyIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/AutomaticReassignmentDecentralizedPartyIntegrationTest.scala @@ -48,7 +48,7 @@ class AutomaticReassignmentDecentralizedPartyIntegrationTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/DivulgenceIntegrationTestHelpers.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/DivulgenceIntegrationTestHelpers.scala index acc499ffb3..ae46ba543f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/DivulgenceIntegrationTestHelpers.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/DivulgenceIntegrationTestHelpers.scala @@ -27,7 +27,7 @@ object DivulgenceIntegrationTestHelpers { case object NonConsumed extends EventType implicit class ParticipantSimpleStreamHelper(val participant: LocalParticipantReference)(implicit - val alphaMultiSynchronizerSupport: Boolean = false + val enableAllLedgerApiReassignments: Boolean = false ) { def acs(party: Party): Seq[OffsetCid] = @@ -111,7 +111,7 @@ object DivulgenceIntegrationTestHelpers { parties: Seq[PartyId], beginOffsetExclusive: Long, ): Seq[UpdateService.UpdateWrapper] = { - val reassignmentsFilter = if (alphaMultiSynchronizerSupport) { + val reassignmentsFilter = if (enableAllLedgerApiReassignments) { Some( EventFormat( filtersByParty = parties.map(party => party.toLf -> Filters(Nil)).toMap, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/OnlinePartyReplicationParticipantProtocolTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/OnlinePartyReplicationParticipantProtocolTest.scala index e2b761a7dc..08c1bbf38d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/OnlinePartyReplicationParticipantProtocolTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/OnlinePartyReplicationParticipantProtocolTest.scala @@ -89,7 +89,7 @@ sealed trait OnlinePartyReplicationParticipantProtocolTest ConfigTransforms .enableAlphaOnlinePartyReplicationSupport(enableUnsafeSequencerChannelSupport = true)* ) - .addConfigTransform(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransform(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => import env.* // More frequent ACS commitments by configuring a smaller reconciliation interval. diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/DivulgenceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/DivulgenceIntegrationTest.scala index 56d5a760c4..c40b4795d2 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/DivulgenceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/DivulgenceIntegrationTest.scala @@ -16,10 +16,10 @@ trait DivulgenceIntegrationTest extends OfflinePartyReplicationIntegrationTestBa import com.digitalasset.canton.integration.tests.multihostedparties.DivulgenceIntegrationTestHelpers.* // Whether to use Assign/Unassign (multi-synchronizer) or Create/Archive for the ACS import - def alphaMultiSynchronizerSupport: Boolean + def enableAllLedgerApiReassignments: Boolean // Inject this setting into the implicit scope for the helper class - implicit def alphaSupportImplicit: Boolean = alphaMultiSynchronizerSupport + implicit def alphaSupportImplicit: Boolean = enableAllLedgerApiReassignments // Make sure deduplication duration does not block pruning private val maxDedupDuration = java.time.Duration.ofSeconds(2) @@ -29,7 +29,8 @@ trait DivulgenceIntegrationTest extends OfflinePartyReplicationIntegrationTestBa super.environmentDefinition .addConfigTransforms( ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport).replace(alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ), ConfigTransforms.updateMaxDeduplicationDurations(maxDedupDuration), ) @@ -100,7 +101,7 @@ trait DivulgenceIntegrationTest extends OfflinePartyReplicationIntegrationTestBa participant: LocalParticipantReference, contractId: String, party: Party, - ) = if (alphaMultiSynchronizerSupport) { + ) = if (enableAllLedgerApiReassignments) { assertEventNotFound(participant, contractId, party) } else { checkCreatedEventFor(participant, contractId, party) @@ -525,13 +526,13 @@ trait DivulgenceIntegrationTest extends OfflinePartyReplicationIntegrationTestBa immediateDivulged2P2Import, ) // event query - if (alphaMultiSynchronizerSupport) + if (enableAllLedgerApiReassignments) assertEventNotFound(participant2, aliceStakeholderCreatedP1.contractId, alice) else checkCreatedEventFor(participant2, aliceStakeholderCreatedP1.contractId, alice) checkCreatedEventFor(participant2, aliceBobStakeholderCreatedP1.contractId, alice) checkCreatedEventFor(participant2, divulgeIouByExerciseP1.contractId, alice) assertEventNotFound(participant2, immediateDivulged1P1.contractId, alice) - if (alphaMultiSynchronizerSupport) + if (enableAllLedgerApiReassignments) assertEventNotFound(participant2, immediateDivulged2P1.contractId, alice) else checkCreatedEventFor(participant2, immediateDivulged2P1.contractId, alice) assertEventNotFound(participant2, immediateDivulged1ArchiveP1.contractId, alice) @@ -596,18 +597,18 @@ trait DivulgenceIntegrationTestWithoutCache extends DivulgenceIntegrationTest { } class DivulgenceIntegrationTestReassignmentWithCache extends DivulgenceIntegrationTest { - override def alphaMultiSynchronizerSupport: Boolean = true + override def enableAllLedgerApiReassignments: Boolean = true } class DivulgenceIntegrationTestLegacyWithCache extends DivulgenceIntegrationTest { - override def alphaMultiSynchronizerSupport: Boolean = false + override def enableAllLedgerApiReassignments: Boolean = false } class DivulgenceIntegrationTestReassignmentWithoutCache extends DivulgenceIntegrationTestWithoutCache { - override def alphaMultiSynchronizerSupport: Boolean = true + override def enableAllLedgerApiReassignments: Boolean = true } class DivulgenceIntegrationTestLegacyWithoutCache extends DivulgenceIntegrationTestWithoutCache { - override def alphaMultiSynchronizerSupport: Boolean = false + override def enableAllLedgerApiReassignments: Boolean = false } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/WorkflowIdsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/WorkflowIdsIntegrationTest.scala index b5a16bff8e..9b35bfdc89 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/WorkflowIdsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/WorkflowIdsIntegrationTest.scala @@ -24,15 +24,15 @@ import java.util.Collections * custom-configured. */ abstract class WorkflowIdsIntegrationTestBase( - alphaMultiSynchronizerSupport: Boolean = false + enableAllLedgerApiReassignments: Boolean = false ) extends OfflinePartyReplicationIntegrationTestBase { override lazy val environmentDefinition: EnvironmentDefinition = super.environmentDefinition .addConfigTransforms( ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport) - .replace(alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ) ) @@ -113,7 +113,7 @@ abstract class WorkflowIdsIntegrationTestBase( party: Party, expectedCount: Int, ): Seq[NormalizedEvent] = - if (alphaMultiSynchronizerSupport) { + if (enableAllLedgerApiReassignments) { val reassignments = target.ledger_api.updates .reassignments(Set(party), completeAfter = PositiveInt.tryCreate(expectedCount)) reassignments.map { r => @@ -158,4 +158,4 @@ abstract class WorkflowIdsIntegrationTestBase( final class WorkflowIdsIntegrationTest extends WorkflowIdsIntegrationTestBase final class WorkflowIdsReassignmentIntegrationTest - extends WorkflowIdsIntegrationTestBase(alphaMultiSynchronizerSupport = true) + extends WorkflowIdsIntegrationTestBase(enableAllLedgerApiReassignments = true) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AssignmentBeforeUnassignmentIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AssignmentBeforeUnassignmentIntegrationTest.scala index 4c5c10b24f..472cba7f35 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AssignmentBeforeUnassignmentIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AssignmentBeforeUnassignmentIntegrationTest.scala @@ -47,7 +47,7 @@ sealed trait AssignmentBeforeUnassignmentIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AsynchronousReassignmentProtocolIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AsynchronousReassignmentProtocolIntegrationTest.scala index e9947701a9..ae20129a5b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AsynchronousReassignmentProtocolIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AsynchronousReassignmentProtocolIntegrationTest.scala @@ -70,7 +70,7 @@ final class AsynchronousReassignmentProtocolIntegrationTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AutomaticReassignmentBatchingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AutomaticReassignmentBatchingIntegrationTest.scala index e547fa6096..15ca2767f9 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AutomaticReassignmentBatchingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AutomaticReassignmentBatchingIntegrationTest.scala @@ -36,7 +36,7 @@ class AutomaticReassignmentBatchingIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1_S1M1_S1M1 .addConfigTransforms( ProgrammableSequencer.configOverride(this.getClass.toString, loggerFactory), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationAdminPartyIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationAdminPartyIntegrationTest.scala index df6de065ee..5c2f4cc189 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationAdminPartyIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationAdminPartyIntegrationTest.scala @@ -69,7 +69,7 @@ sealed trait ReassignmentConfirmationAdminPartyIntegrationTest // Because we play with the simClock, ensure we have enough forward tolerance // on the target timestamp to not impact up unassigments. ConfigTransforms.updateTargetTimestampForwardTolerance(1.hours), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationPoliciesIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationPoliciesIntegrationTest.scala index 20ac9e3605..dee97c89a8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationPoliciesIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationPoliciesIntegrationTest.scala @@ -44,7 +44,7 @@ sealed trait ReassignmentConfirmationPoliciesPartyIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentNoReassignmentDataIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentNoReassignmentDataIntegrationTest.scala index 22bc701f7c..bbf047740f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentNoReassignmentDataIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentNoReassignmentDataIntegrationTest.scala @@ -63,7 +63,7 @@ sealed trait ReassignmentNoReassignmentDataIntegrationTest EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.useStaticTime, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceConcurrentReassignmentsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceConcurrentReassignmentsIntegrationTest.scala index 29ef10f11b..eb98e0ec9d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceConcurrentReassignmentsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceConcurrentReassignmentsIntegrationTest.scala @@ -56,7 +56,7 @@ trait ReassignmentServiceConcurrentReassignmentsIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceIntegrationTest.scala index e1bc0b2f47..d0e8f1ad50 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceIntegrationTest.scala @@ -92,7 +92,7 @@ abstract class ReassignmentServiceIntegrationTest .addConfigTransforms( // Ensure reassignments are not tripped up by some participants being a little behind. ConfigTransforms.updateTargetTimestampForwardTolerance(30.seconds), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceTimeoutCommandRejectedIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceTimeoutCommandRejectedIntegrationTest.scala index 22880f7a07..5b0a766f16 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceTimeoutCommandRejectedIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceTimeoutCommandRejectedIntegrationTest.scala @@ -77,7 +77,7 @@ sealed trait ReassignmentServiceTimeoutCommandRejectedIntegrationTest .addConfigTransforms( ConfigTransforms.useStaticTime, ConfigTransforms.updateTargetTimestampForwardTolerance(60.seconds), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentSubmissionIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentSubmissionIntegrationTest.scala index 1743a5e421..c408e6b71d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentSubmissionIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentSubmissionIntegrationTest.scala @@ -52,7 +52,7 @@ sealed trait ReassignmentSubmissionIntegrationTest // We want to trigger time out .addConfigTransforms( ConfigTransforms.useStaticTime, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentTargetTimestampIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentTargetTimestampIntegrationTest.scala index f9b74820ca..2086552209 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentTargetTimestampIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentTargetTimestampIntegrationTest.scala @@ -45,7 +45,7 @@ class ReassignmentTargetTimestampIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationObserversIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationObserversIntegrationTest.scala index e0de9be45f..f48da31091 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationObserversIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationObserversIntegrationTest.scala @@ -85,7 +85,7 @@ sealed trait ReassignmentsConfirmationObserversIntegrationTest // We want to trigger time out .addConfigTransforms( ConfigTransforms.useStaticTime, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationThresholdIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationThresholdIntegrationTest.scala index ab4d6ab16b..f56a3509ac 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationThresholdIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationThresholdIntegrationTest.scala @@ -91,7 +91,7 @@ sealed trait ReassignmentsConfirmationThresholdIntegrationTest .addConfigTransforms( ConfigTransforms.useStaticTime, ConfigTransforms.updateTargetTimestampForwardTolerance(10.minutes), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/RepairServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/RepairServiceIntegrationTest.scala index 42b589d7fb..4960f5679d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/RepairServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/RepairServiceIntegrationTest.scala @@ -50,7 +50,7 @@ abstract class RepairServiceIntegrationTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1_S2M1_S2M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/UpdateServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/UpdateServiceIntegrationTest.scala index 7a68b905c3..d6c5f53838 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/UpdateServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/UpdateServiceIntegrationTest.scala @@ -47,7 +47,7 @@ abstract class UpdateServiceIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1_S2M1_S2M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/kms/KmsCryptoNoPreDefinedKeysIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/kms/KmsCryptoNoPreDefinedKeysIntegrationTest.scala index 84742c2630..3995e2ac87 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/kms/KmsCryptoNoPreDefinedKeysIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/kms/KmsCryptoNoPreDefinedKeysIntegrationTest.scala @@ -15,8 +15,8 @@ import com.digitalasset.canton.crypto.store.{CryptoPrivateStore, KmsCryptoPrivat import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UseKms} import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoIntegrationTestBase import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } import com.digitalasset.canton.lifecycle.FutureUnlessShutdown @@ -28,7 +28,7 @@ import scala.concurrent.Future * keys (i.e. keys are generated on-the-fly using a KMS and nodes are automatically initialized). */ trait KmsCryptoNoPreDefinedKeysIntegrationTest extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def kmsConfig: KmsConfig diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/sequencer/SequencerCatchUpPerformanceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/sequencer/SequencerCatchUpPerformanceIntegrationTest.scala index 22b926d6ce..e0aaaebce8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/sequencer/SequencerCatchUpPerformanceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/sequencer/SequencerCatchUpPerformanceIntegrationTest.scala @@ -24,6 +24,7 @@ import com.digitalasset.canton.console.{ LocalSequencerReference, } import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -72,7 +73,7 @@ import scala.concurrent.duration.* * Check the events/s in the `log` folder after the run: * * {{{ - * cat canton_test.log | grep 'sequencer2 events/s' + * cat canton_test.log | grep 'sequencer4 events/s' * }}} * * More options: @@ -137,7 +138,7 @@ class SequencerCatchUpPerformanceIntegrationTest // Enable to see metrics in Prometheus or Grafana. For some reason, doesn't work with the 'countBlockEvents' flag private val exposeHttpMetrics = false - // Enable to automatically count events/s, grep 'sequencer2 events/s' after the test run + // Enable to automatically count events/s, grep 'sequencer4 events/s' after the test run // Set to true by default, so we can print the sequencer speed on CircleCI private val countBlockEvents = true @@ -150,13 +151,13 @@ class SequencerCatchUpPerformanceIntegrationTest // The duration of load producing before the restart. The bigger the number, the more events will accumulate to catch up with private val beforeRestartDurationMillis = 5 * 60000L - // The max time for the sequencer2 to catch up + // The max time for the sequencer4 to catch up private val afterRestartDurationMillis = beforeRestartDurationMillis / 2 - // Useful to isolate the sequencer2 performance + // Useful to isolate the sequencer4 performance private val stopOtherNodesDuringCatchUp = true - // Keep producing load after catch-up. May result in sequencer2 never catching up, depending on the other settings. + // Keep producing load after catch-up. May result in sequencer4 never catching up, depending on the other settings. private val produceLoadAfterRestart = false private val enableTrafficManagement = true @@ -215,7 +216,7 @@ class SequencerCatchUpPerformanceIntegrationTest Option.when(useExternalSequencerProcess)( new UseExternalProcess( loggerFactory, - externalSequencers = Set("sequencer2"), + externalSequencers = Set("sequencer4"), fileNameHint = this.getClass.getSimpleName, configTransforms = if (exposeHttpMetrics) Seq(metricsConfigTransform, streamInstrumentationConfigTransform) @@ -223,11 +224,11 @@ class SequencerCatchUpPerformanceIntegrationTest ) ) - private val sequencer2Proxy = "sequencer2-to-postgres" + private val sequencer4Proxy = "sequencer4-to-postgres" private val toxiproxyPluginOpt: Option[UseToxiproxy] = Option.when(useToxiProxy)( new UseToxiproxy( - ToxiproxyConfig(proxies = Seq(SequencerToPostgres(sequencer2Proxy, "sequencer2"))) + ToxiproxyConfig(proxies = Seq(SequencerToPostgres(sequencer4Proxy, "sequencer4"))) ) ) @@ -237,7 +238,7 @@ class SequencerCatchUpPerformanceIntegrationTest new UsePostgres( loggerFactory, customMaxConnectionsByNode = Some { - case "sequencer2" => PositiveInt.tryCreate(10).some + case "sequencer4" => PositiveInt.tryCreate(10).some case _ => PositiveInt.tryCreate(5).some }, ) @@ -256,7 +257,7 @@ class SequencerCatchUpPerformanceIntegrationTest EnvironmentDefinition .buildBaseEnvironmentDefinition( numParticipants = 2, - numSequencers = 2, + numSequencers = 4, numMediators = 5, ) .withManualStart @@ -273,6 +274,8 @@ class SequencerCatchUpPerformanceIntegrationTest allMediators().foreach(_.start()) sequencer1.start() + sequencer2.start() + sequencer3.start() startSequencer() participant1.start() @@ -285,27 +288,33 @@ class SequencerCatchUpPerformanceIntegrationTest ) sequencer1.health.wait_for_ready_for_initialization() + sequencer2.health.wait_for_ready_for_initialization() + sequencer3.health.wait_for_ready_for_initialization() - val seq2 = if (useExternalSequencerProcess) remoteSequencer2 else sequencer2 - seq2.health.wait_for_ready_for_initialization() + val seq4 = if (useExternalSequencerProcess) remoteSequencer4 else sequencer4 + seq4.health.wait_for_ready_for_initialization() } .withNetworkBootstrap { implicit env => import env.* - val seq2 = if (useExternalSequencerProcess) remoteSequencer2 else sequencer2 + val seq4 = if (useExternalSequencerProcess) remoteSequencer4 else sequencer4 val allMediators_ = allMediators() new NetworkBootstrapper( NetworkTopologyDescription( daName, - synchronizerOwners = Seq(sequencer1, seq2), + synchronizerOwners = Seq(sequencer1, sequencer2, sequencer3, seq4), synchronizerThreshold = PositiveInt.one, - sequencers = Seq(sequencer1, seq2), + sequencers = Seq(sequencer1, sequencer2, sequencer3, seq4), mediators = allMediators_, overrideMediatorToSequencers = Some( allMediators_.map { mediator => // Make sure both mediators are connected the first sequencer to avoid the SEQUENCER_SUBSCRIPTION_LOST warning // And flaky results (load produced super slowly from time to time) - mediator -> (Seq(sequencer1), PositiveInt.one, NonNegativeInt.zero) + mediator -> MediatorSequencersConfiguration( + Seq(sequencer1), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ) }.toMap ), mediatorThreshold = allMediators_.length - 1, @@ -413,7 +422,7 @@ class SequencerCatchUpPerformanceIntegrationTest participant2.health.ping(participant1.id) participant1.health.ping(participant2.id) - testLogger.info("sequencer2 STOPPING") + testLogger.info("sequencer4 STOPPING") stopSequencer() runners.foreach(env.environment.addUserCloseable(_)) @@ -435,11 +444,11 @@ class SequencerCatchUpPerformanceIntegrationTest loggerFactory.assertLoggedWarningsAndErrorsSeq( { - testLogger.info("sequencer2 RESTARTING") + testLogger.info("sequencer4 RESTARTING") // enable db toxiproxy toxiproxyPluginOpt.foreach { toxiproxyPlugin => - val proxy = toxiproxyPlugin.runningToxiproxy.getProxy(sequencer2Proxy) + val proxy = toxiproxyPlugin.runningToxiproxy.getProxy(sequencer4Proxy) val client = proxy .valueOrFail("must be here") .underlying @@ -452,7 +461,7 @@ class SequencerCatchUpPerformanceIntegrationTest val before = Instant.now() val seq2EventCountBeforeOpt: Option[Long] = - if (exposeHttpMetrics || !countBlockEvents) None else Some(blockEventCount(sequencer2)) + if (exposeHttpMetrics || !countBlockEvents) None else Some(blockEventCount(sequencer4)) def instantToSeconds(instant: Instant): Double = instant.getEpochSecond.toDouble + (instant.getNano.toDouble / 1000_000_000) @@ -464,26 +473,26 @@ class SequencerCatchUpPerformanceIntegrationTest maxPollInterval = 50 milliseconds, ) { val seq1EventCount = blockEventCount(sequencer1) - val seq2EventCount = blockEventCount(sequencer2) + val seq4EventCount = blockEventCount(sequencer4) testLogger.info( - s"Checking if sequencer2 has caught up. sequencer1 events: $seq1EventCount, sequencer2 events: $seq2EventCount" + s"Checking if sequencer4 has caught up. sequencer1 events: $seq1EventCount, sequencer4 events: $seq4EventCount" ) // seq2 has caught up if it has seen all events. // it may see more in case of shutdown during async processing - val seq2CaughtUp = seq2EventCount >= seq1EventCount + val seq2CaughtUp = seq4EventCount >= seq1EventCount assert(seq2CaughtUp) if (seq2CaughtUp) { val now = Instant.now() - val processedEventCount = seq2EventCount - seq2EventCountBefore + val processedEventCount = seq4EventCount - seq2EventCountBefore val eventsPerSec = processedEventCount.toDouble / (instantToSeconds(now) - instantToSeconds(before)) val logMessage = - f"sequencer2 events/s: $eventsPerSec%.2f, processed events: $processedEventCount" + f"sequencer4 events/s: $eventsPerSec%.2f, processed events: $processedEventCount" testLogger.info(logMessage) // Ugly solution to print the sequencer speed on CircleCI @@ -494,7 +503,7 @@ class SequencerCatchUpPerformanceIntegrationTest case None => Threading.sleep(afterRestartDurationMillis) } - testLogger.info("sequencer2 COMPLETED") + testLogger.info("sequencer4 COMPLETED") if (turnOffDebugLogging) NodeLoggingUtil.setLevel(level = "DEBUG") }, @@ -533,11 +542,11 @@ class SequencerCatchUpPerformanceIntegrationTest externalProcessOpt match { case Some(externalProcess) => - externalProcess.start("sequencer2") - assert(externalProcess.isRunning("sequencer2")) + externalProcess.start("sequencer4") + assert(externalProcess.isRunning("sequencer4")) case None => - sequencer2.start() - assert(sequencer2.is_running) + sequencer4.start() + assert(sequencer4.is_running) } } @@ -546,11 +555,11 @@ class SequencerCatchUpPerformanceIntegrationTest externalProcessOpt match { case Some(externalProcess) => - externalProcess.kill("sequencer2") - assert(!externalProcess.isRunning("sequencer2")) + externalProcess.kill("sequencer4") + assert(!externalProcess.isRunning("sequencer4")) case None => - sequencer2.stop() - assert(!sequencer2.is_running) + sequencer4.stop() + assert(!sequencer4.is_running) } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/operations/ViewConsistencyTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/operations/ViewConsistencyTest.scala index 0cd3009160..5e6109e379 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/operations/ViewConsistencyTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/operations/ViewConsistencyTest.scala @@ -71,7 +71,9 @@ sealed trait ViewConsistencyTest -- no views for flyway or blocks tables table_name not in ('flyway_schema_history', 'blocks') and -- DEV version adds a column, but it doesn't have any significance, so let's ignore it - not (table_name = 'common_node_id' and column_name = 'test_column') + not (table_name = 'common_node_id' and column_name = 'test_column') and + -- exclude partition tables + table_name !~ '_p[0-9]+$$' group by table_name, column_name having count(column_name) != 2""".as[(String, String, String)], "select", diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pkgdars/PackageUsableMixin.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pkgdars/PackageUsableMixin.scala index 49e73cd7f1..b60bb33482 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pkgdars/PackageUsableMixin.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pkgdars/PackageUsableMixin.scala @@ -7,14 +7,14 @@ import com.daml.ledger.javaapi.data.Command import com.digitalasset.canton.config import com.digitalasset.canton.console.ParticipantReference import com.digitalasset.canton.damltests.java.conflicttest.Many -import com.digitalasset.canton.integration.BaseIntegrationTest +import com.digitalasset.canton.integration.CantonBaseIntegrationTest import com.digitalasset.canton.topology.{PartyId, SynchronizerId} import org.scalatest.Assertion import scala.jdk.CollectionConverters.* trait PackageUsableMixin { - this: BaseIntegrationTest => + this: CantonBaseIntegrationTest => protected def submitCommand( submittingParticipant: ParticipantReference, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/LedgerPruningIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/LedgerPruningIntegrationTest.scala index 09fc47aa71..8fd8c69b87 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/LedgerPruningIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/LedgerPruningIntegrationTest.scala @@ -3,9 +3,12 @@ package com.digitalasset.canton.integration.tests.pruning +import anorm.SqlParser.int +import anorm.SqlStringInterpolation import com.daml.ledger.api.v2.event_query_service.GetEventsByContractIdResponse import com.daml.ledger.api.v2.transaction.Transaction import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_LEDGER_EFFECTS +import com.daml.metrics.DatabaseMetrics import com.digitalasset.canton.BigDecimalImplicits.* import com.digitalasset.canton.config.DbConfig import com.digitalasset.canton.config.RequireTypes.PositiveInt @@ -22,6 +25,7 @@ import com.digitalasset.canton.integration.plugins.{ import com.digitalasset.canton.integration.tests.examples.IouSyntax import com.digitalasset.canton.integration.tests.multihostedparties.DivulgenceIntegrationTestHelpers.ParticipantSimpleStreamHelper import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.OffsetOutOfRange +import com.digitalasset.canton.logging.LoggingContextWithTrace import com.digitalasset.canton.participant.admin.data.{ ContractImportMode, RepairContract, @@ -776,11 +780,19 @@ abstract class LedgerPruningIntegrationTest contract.contractId } val cid1 = pushContract("0.01").coid - val _ = pushContract("0.02") + val cid2 = pushContract("0.02").coid - // eventually the first should be available in the contract store + // eventually the two contracts should be available in the contract store and in the event store too eventually() { contractFor(participant1, daId, cid1).isDefined shouldBe true + contractFor(participant1, daId, cid2).isDefined shouldBe true + participant1.underlying.value.sync.ledgerApiIndexer.asEval.value.ledgerApiStore.value.ledgerApiDbSupport.dbDispatcher + .executeSql(DatabaseMetrics.ForTesting("getting-all-activations-with-workflow-id"))( + SQL"""select count(*) c + from lapi_events_activate_contract + where workflow_id like 'failedAddContractOperation%'""".as(int("c").single)(_) + )(LoggingContextWithTrace.ForTesting) + .futureValue shouldBe 2 } loggerFactory.assertLoggedWarningsAndErrorsSeq( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/ReassignmentPruningIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/ReassignmentPruningIntegrationTest.scala index 79f73cdd44..5967c97d96 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/ReassignmentPruningIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/ReassignmentPruningIntegrationTest.scala @@ -62,7 +62,7 @@ sealed trait ReassignmentPruningIntegrationTest .addConfigTransforms( ConfigTransforms.useStaticTime, ConfigTransforms.updateMaxDeduplicationDurations(maxDedupDuration), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/AcsImportReassignmentCounterIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/AcsImportReassignmentCounterIntegrationTest.scala index 8af1bde8ca..346d844216 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/AcsImportReassignmentCounterIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/AcsImportReassignmentCounterIntegrationTest.scala @@ -31,10 +31,10 @@ sealed trait AcsImportReassignmentCounterIntegrationTest EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) .replace(true) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/ImportContractsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/ImportContractsIntegrationTest.scala index 8cda7b22fb..a1a19458ac 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/ImportContractsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/ImportContractsIntegrationTest.scala @@ -29,7 +29,7 @@ trait ImportContractsIntegrationTestBase with SharedEnvironment with EntitySyntax { - protected def enableAlphaMultiSynchronizerSupport: Boolean + protected def enableAllLedgerApiReassignments: Boolean private var alice: PartyId = _ private var bob: PartyId = _ @@ -39,10 +39,10 @@ trait ImportContractsIntegrationTestBase EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport) - .replace(enableAlphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* @@ -91,7 +91,7 @@ trait ImportContractsIntegrationTestBase "Importing an ACS" should { - s"handle contracts with non-zero reassignment counter (multi-synchronizer-support=$enableAlphaMultiSynchronizerSupport)" in { + s"handle contracts with non-zero reassignment counter (multi-synchronizer-support=$enableAllLedgerApiReassignments)" in { implicit env => import env.* @@ -129,7 +129,7 @@ trait ImportContractsIntegrationTestBase participant3.synchronizers.disconnect_all() - if (enableAlphaMultiSynchronizerSupport) { + if (enableAllLedgerApiReassignments) { participant3.repair.import_acs(daId, file.canonicalPath) val reassignedContract = participant3.ledger_api.state.acs @@ -157,11 +157,11 @@ trait ImportContractsIntegrationTestBase * The ACS import will simply drop contracts that are associated to a different synchronizer; * and logs that fact once. */ - s"import a multi-synchronizer ACS snapshot (multi-synchronizer-support=$enableAlphaMultiSynchronizerSupport)" in { + s"import a multi-synchronizer ACS snapshot (multi-synchronizer-support=$enableAllLedgerApiReassignments)" in { implicit env => import env.* - // Create several contracts for both synchronizers having reassignment counter zero (thus independent of enableAlphaMultiSynchronizerSupport) + // Create several contracts for both synchronizers having reassignment counter zero (thus independent of enableAllLedgerApiReassignments) val contractsDa = (1 to 2).map { _ => IouSyntax.createIou(participant1, synchronizerId = Some(daId))(charlie, bob) } @@ -208,7 +208,7 @@ trait ImportContractsIntegrationTestBase // 1. Non-zero reassignment counter in recovered contract // 2. Imports contract using Assign/Unassign events // 3. Recovery retains pre-existing active contracts - s"recover successfully ACS import mid-crash for a reassigned contract preserving pre-existing state (multi-synchronizer-support=$enableAlphaMultiSynchronizerSupport)" in { + s"recover successfully ACS import mid-crash for a reassigned contract preserving pre-existing state (multi-synchronizer-support=$enableAllLedgerApiReassignments)" in { implicit env => import env.* @@ -244,7 +244,7 @@ trait ImportContractsIntegrationTestBase participant3.synchronizers.disconnect_all() - if (enableAlphaMultiSynchronizerSupport) { + if (enableAllLedgerApiReassignments) { val contractInstance = participant1.underlying.value.sync.participantNodePersistentState.value.contractStore .lookup(cid) @@ -339,10 +339,10 @@ trait ImportContractsIntegrationTestBase } final class ImportContractsIntegrationTest extends ImportContractsIntegrationTestBase { - override protected def enableAlphaMultiSynchronizerSupport: Boolean = false + override protected def enableAllLedgerApiReassignments: Boolean = false } final class ImportContractsWithReassignmentIntegrationTest extends ImportContractsIntegrationTestBase { - override protected def enableAlphaMultiSynchronizerSupport: Boolean = true + override protected def enableAllLedgerApiReassignments: Boolean = true } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RepairServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RepairServiceIntegrationTest.scala index bc8bb9e5b1..7bc838391e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RepairServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RepairServiceIntegrationTest.scala @@ -83,7 +83,7 @@ trait RepairServiceIntegrationTest EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.enableAdvancedCommands(FeatureFlag.Repair), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) override val defaultParticipant: String = "participant1" @@ -858,7 +858,7 @@ sealed trait RepairServiceIntegrationTestStableLf extends RepairServiceIntegrati import env.* // If multi-synchronizer support is enabled, the purge will be represented as an Unassigned event, otherwise as a synthetic Archive event - val multiSynchronizerSupport = participant1.config.parameters.alphaMultiSynchronizerSupport + val multiSynchronizerSupport = participant1.config.parameters.enableAllLedgerApiReassignments val eventFormat = EventFormat( filtersByParty = @@ -1098,24 +1098,24 @@ sealed trait WithMultiSynchronizerSupport extends RepairServiceIntegrationTest { .addConfigTransforms( ConfigTransforms.enableAdvancedCommands(FeatureFlag.Repair), ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport).replace(true) + _.focus(_.parameters.enableAllLedgerApiReassignments).replace(true) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) } -/** Contains tests that ONLY work when alphaMultiSynchronizerSupport = true */ +/** Contains tests that ONLY work when enableAllLedgerApiReassignments = true */ sealed trait RepairServiceMultiSynchronizerTests extends RepairServiceIntegrationTest { "RepairServiceMultiSynchronizerTests" must { - "run test only with enabled `alphaMultiSynchronizerSupport`" in { implicit env => + "run test only with enabled `enableAllLedgerApiReassignments`" in { implicit env => import env.* participants.local.foreach { participant => assert( - participant.config.parameters.alphaMultiSynchronizerSupport, - s"alphaMultiSynchronizerSupport must be true for ${participant.name} in this test suite", + participant.config.parameters.enableAllLedgerApiReassignments, + s"enableAllLedgerApiReassignments must be true for ${participant.name} in this test suite", ) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RollbackUnassignmentIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RollbackUnassignmentIntegrationTest.scala index 110a4d0787..70eb37d37a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RollbackUnassignmentIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RollbackUnassignmentIntegrationTest.scala @@ -32,7 +32,7 @@ sealed trait RollbackUnassignmentIntegrationTest EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.enableAdvancedCommands(FeatureFlag.Repair), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/CryptoMetricsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/CryptoMetricsIntegrationTest.scala new file mode 100644 index 0000000000..373b761946 --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/CryptoMetricsIntegrationTest.scala @@ -0,0 +1,58 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.security + +import com.daml.metrics.api.testing.MetricValues.* +import com.digitalasset.canton.crypto.provider.jce.JcePrivateCrypto +import com.digitalasset.canton.crypto.provider.kms.KmsPrivateCrypto +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} +import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoWithPreDefinedKeysIntegrationTest +import com.digitalasset.canton.integration.tests.security.kms.gcp.GcpKmsCryptoIntegrationTestBase +import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, + CommunityIntegrationTest, + SharedEnvironment, +} + +/** Integration tests verifying that cryptographic operations using KMS providers are correctly + * instrumented to correctly record crypto-related metrics. + * + * In particular, this suite ensures that signing and decryption latencies are captured, as well as + * KMS-related metrics. + */ +trait CryptoMetricsIntegrationTest extends KmsCryptoWithPreDefinedKeysIntegrationTest { + self: CommunityIntegrationTest & CantonEnvironmentSetup => + + override lazy val protectedNodes: Set[String] = Set("participant1") + + "signing, decryption latencies, and KMS metrics are recorded" in { implicit env => + import env.* + + participant1.crypto.privateCrypto.isInstanceOf[KmsPrivateCrypto] shouldBe true + participant2.crypto.privateCrypto.isInstanceOf[JcePrivateCrypto] shouldBe true + + assertPingSucceeds(participant1, participant1) + + participant1.underlying.value.metrics.cryptoMetrics.kmsMetricsO shouldBe defined + + // Even when KMS is not used, signing and decryption operations always record latency metrics. + forAll(Seq(participant1, participant2)) { p => + p.underlying.value.metrics.cryptoMetrics.signingMetrics.signingLatency.valuesWithContext should not be empty + p.underlying.value.metrics.cryptoMetrics.decryptionMetrics.decryptLatency.valuesWithContext should not be empty + } + } + +} + +class GcpKmsCryptoMetricsIntegrationTestPostgres + extends CommunityIntegrationTest + with SharedEnvironment + with GcpKmsCryptoIntegrationTestBase + with KmsCryptoWithPreDefinedKeysIntegrationTest { + setupPlugins( + withAutoInit = false, + storagePlugin = Some(new UsePostgres(loggerFactory)), + sequencerPlugin = new UseBftSequencer(loggerFactory), + ) +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidAssignmentRequestIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidAssignmentRequestIntegrationTest.scala index 65255c156b..3fa1ad0fb4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidAssignmentRequestIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidAssignmentRequestIntegrationTest.scala @@ -64,7 +64,7 @@ final class InvalidAssignmentRequestIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidReassignmentIdIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidReassignmentIdIntegrationTest.scala index 4ce90ff29d..5613504f67 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidReassignmentIdIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidReassignmentIdIntegrationTest.scala @@ -78,7 +78,7 @@ sealed trait InvalidReassignmentIdIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidUnassignmentRequestIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidUnassignmentRequestIntegrationTest.scala index e73bdec3fb..2bd66734d3 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidUnassignmentRequestIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidUnassignmentRequestIntegrationTest.scala @@ -58,7 +58,7 @@ class InvalidUnassignmentRequestIntegrationTest EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( ProgrammableSequencer.configOverride(getClass.toString, loggerFactory), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/KeyManagementIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/KeyManagementIntegrationTest.scala index a8f1bd7c75..c5b0bfe60e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/KeyManagementIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/KeyManagementIntegrationTest.scala @@ -22,7 +22,7 @@ import com.digitalasset.canton.util.OptionUtil import org.scalatest.Assertion trait KeyManagementIntegrationTestHelper extends KeyManagementTestHelper { - self: BaseIntegrationTest => + self: CantonBaseIntegrationTest => protected def waitForKeyTopologyUpdate( nodes: Seq[InstanceReference], diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoIntegrationTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoIntegrationTestBase.scala index 0cdb6691f0..6ee91993a5 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoIntegrationTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoIntegrationTestBase.scala @@ -19,7 +19,7 @@ import com.digitalasset.canton.time.{RemoteClock, SimClock} * Check contributing/kms.md on how to run the tests */ trait KmsCryptoIntegrationTestBase extends TopologyManagementHelper { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => // Defines which nodes will run an external KMS. protected lazy val protectedNodes: Set[String] = Set("participant1") diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoWithPreDefinedKeysIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoWithPreDefinedKeysIntegrationTest.scala index bce32988f0..9888b01cb4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoWithPreDefinedKeysIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoWithPreDefinedKeysIntegrationTest.scala @@ -5,14 +5,14 @@ package com.digitalasset.canton.integration.tests.security.kms import com.digitalasset.canton.config.CryptoProvider import com.digitalasset.canton.crypto.store.KmsCryptoPrivateStore -import com.digitalasset.canton.integration.{CommunityIntegrationTest, EnvironmentSetup} +import com.digitalasset.canton.integration.{CantonEnvironmentSetup, CommunityIntegrationTest} /** Runs a crypto integration tests with one participant using a KMS provider with pre-generated * keys. Runs with persistence so we also check that it is able to recover from an unexpected * shutdown. */ trait KmsCryptoWithPreDefinedKeysIntegrationTest extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest & EnvironmentSetup => + self: CommunityIntegrationTest & CantonEnvironmentSetup => "be able to restart from a persisted state" in { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysIntegrationTest.scala index 3c4943d665..cbaf4b7052 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysIntegrationTest.scala @@ -38,7 +38,7 @@ trait SessionSigningKeysIntegrationTest override protected def otherConfigTransforms: Seq[ConfigTransform] = Seq( ConfigTransforms.setSigningKeysIfPV35OrHigher( - SessionSigningKeysConfig.default, + SessionSigningKeysConfig.enabled, nodeFilter = name => !nodesWithSessionSigningKeysDisabled.contains(name), ) ) @@ -48,13 +48,14 @@ trait SessionSigningKeysIntegrationTest ): Map[MetricsContext, Long] = { val kmsMetrics = node match { case p: LocalParticipantReference => - p.underlying.value.metrics.kmsMetrics + p.underlying.value.metrics.cryptoMetrics.kmsMetricsO.valueOrFail("no KMS metrics") case m: LocalMediatorReference => - m.underlying.value.replicaManager.mediatorRuntime.value.mediator.metrics.kmsMetrics + m.underlying.value.replicaManager.mediatorRuntime.value.mediator.metrics.cryptoMetrics.kmsMetricsO + .valueOrFail("no KMS metrics") case s: LocalSequencerReference => - s.underlying.value.sequencer.metrics.kmsMetrics + s.underlying.value.sequencer.metrics.cryptoMetrics.kmsMetricsO.valueOrFail("no KMS metrics") case _ => fail("unexpected node") } @@ -69,7 +70,7 @@ trait SessionSigningKeysIntegrationTest if (nodesWithSessionSigningKeysDisabled.contains(node.name)) node.config.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.disabled else - node.config.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.default + node.config.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.enabled } assertPingSucceeds(participant1, participant2) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysLifecycleIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysLifecycleIntegrationTest.scala index 65f9d3a139..cb652bc9b9 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysLifecycleIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysLifecycleIntegrationTest.scala @@ -78,7 +78,10 @@ trait SessionSigningKeysLifecycleIntegrationTest // record initial metrics to establish a baseline, as a fallback to the long-term key // may have occurred during bootstrap. val participantKmsMetrics = Seq(participant1, participant2).map( - _.underlying.value.metrics.kmsMetrics.sessionSigningKeysFallback.valuesWithContext + _.underlying.value.metrics.cryptoMetrics.kmsMetricsO + .valueOrFail("no KMS metrics") + .sessionSigningKeysFallback + .valuesWithContext ) env.nodes.local.foreach { node => @@ -100,7 +103,10 @@ trait SessionSigningKeysLifecycleIntegrationTest // we expect that no fallback has been triggered for the ping requests and that // session signing keys have been used and rotated successfully. Seq(participant1, participant2).map( - _.underlying.value.metrics.kmsMetrics.sessionSigningKeysFallback.valuesWithContext + _.underlying.value.metrics.cryptoMetrics.kmsMetricsO + .valueOrFail("no KMS metrics") + .sessionSigningKeysFallback + .valuesWithContext ) shouldBe participantKmsMetrics } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsEncryptedCryptoPrivateStoreTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsEncryptedCryptoPrivateStoreTestBase.scala index da4569679d..64ec2190f4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsEncryptedCryptoPrivateStoreTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsEncryptedCryptoPrivateStoreTestBase.scala @@ -5,13 +5,13 @@ package com.digitalasset.canton.integration.tests.security.kms.aws import com.digitalasset.canton.integration.plugins.UseAwsKms import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } trait AwsEncryptedCryptoPrivateStoreTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def setupPlugins( protectedNodes: Set[String], diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsKmsCryptoIntegrationTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsKmsCryptoIntegrationTestBase.scala index 12c653a22c..1e154dc6da 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsKmsCryptoIntegrationTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsKmsCryptoIntegrationTestBase.scala @@ -16,14 +16,14 @@ import com.digitalasset.canton.integration.plugins.{ } import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoIntegrationTestBase import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransforms, - EnvironmentSetup, EnvironmentSetupPlugin, } trait AwsKmsCryptoIntegrationTestBase extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected val kmsConfig: KmsConfig = KmsConfig.Aws.defaultTestConfig diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpEncryptedCryptoPrivateStoreTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpEncryptedCryptoPrivateStoreTestBase.scala index d959f24c6a..a88b23e07d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpEncryptedCryptoPrivateStoreTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpEncryptedCryptoPrivateStoreTestBase.scala @@ -6,13 +6,13 @@ package com.digitalasset.canton.integration.tests.security.kms.gcp import com.digitalasset.canton.config.KmsConfig import com.digitalasset.canton.integration.plugins.UseGcpKms import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } trait GcpEncryptedCryptoPrivateStoreTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def setupPlugins( protectedNodes: Set[String], diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpKmsCryptoIntegrationTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpKmsCryptoIntegrationTestBase.scala index fe03234d72..1f1d77e1e4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpKmsCryptoIntegrationTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpKmsCryptoIntegrationTestBase.scala @@ -16,14 +16,14 @@ import com.digitalasset.canton.integration.plugins.{ } import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoIntegrationTestBase import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransforms, - EnvironmentSetup, EnvironmentSetupPlugin, } trait GcpKmsCryptoIntegrationTestBase extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected val kmsConfig: KmsConfig = KmsConfig.Gcp.defaultTestConfig diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockEncryptedCryptoPrivateStoreTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockEncryptedCryptoPrivateStoreTestBase.scala index 6ce7858a09..495a93cf6d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockEncryptedCryptoPrivateStoreTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockEncryptedCryptoPrivateStoreTestBase.scala @@ -6,13 +6,13 @@ package com.digitalasset.canton.integration.tests.security.kms.mock import com.digitalasset.canton.crypto.kms.mock.v1.MockKmsDriverFactory.mockKmsDriverName import com.digitalasset.canton.integration.plugins.UseKmsDriver import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } trait MockEncryptedCryptoPrivateStoreTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def setupPlugins( protectedNodes: Set[String], diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockKmsDriverCryptoIntegrationTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockKmsDriverCryptoIntegrationTestBase.scala index 122cb1a701..fcdc56bfca 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockKmsDriverCryptoIntegrationTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockKmsDriverCryptoIntegrationTestBase.scala @@ -14,8 +14,8 @@ import com.digitalasset.canton.integration.plugins.{EncryptedPrivateStoreStatus, import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoIntegrationTestBase import com.digitalasset.canton.integration.tests.security.kms.mock.MockKmsDriverCryptoIntegrationTestBase.mockKmsDriverConfig import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } import com.typesafe.config.ConfigValueFactory @@ -23,7 +23,7 @@ import com.typesafe.config.ConfigValueFactory import scala.jdk.CollectionConverters.* trait MockKmsDriverCryptoIntegrationTestBase extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected val kmsConfig: KmsConfig = mockKmsDriverConfig diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageRemovalIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageRemovalIntegrationTest.scala index 66f0a6b6d3..873d29794e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageRemovalIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageRemovalIntegrationTest.scala @@ -73,7 +73,7 @@ sealed trait PackageRemovalIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) // Note that CantonTests depends on CantonExamples private val cantonTestsPkg = PackageId.assertFromString(Many.PACKAGE_ID) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageVettingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageVettingIntegrationTest.scala index af2b8cccc9..6096935e97 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageVettingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageVettingIntegrationTest.scala @@ -47,6 +47,7 @@ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.MaliciousParticipantNode import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} import com.digitalasset.canton.util.ShowUtil.* +import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.daml.lf.archive.{DamlLf, DarParser, DarReader} import com.digitalasset.daml.lf.data.Ref.PackageId import monocle.macros.syntax.lens.* @@ -69,6 +70,8 @@ sealed trait PackageVettingIntegrationTest val ledgerIntegrity: SecurityTest = SecurityTest(property = Integrity, asset = "virtual shared ledger") + private val pvSupportsUnvettedDependencies: Boolean = testedProtocolVersion > ProtocolVersion.v34 + private lazy val pureCryptoRef: AtomicReference[CryptoPureApi] = new AtomicReference() def pureCrypto: CryptoPureApi = pureCryptoRef.get() @@ -84,7 +87,7 @@ sealed trait PackageVettingIntegrationTest _.focus(_.parameters.reassignmentsConfig.targetTimestampForwardTolerance) .replace(config.NonNegativeFiniteDuration.ofMinutes(10)) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* @@ -591,7 +594,7 @@ sealed trait PackageVettingIntegrationTest } "all packages are stored, but a dependent package has not been vetted" must { - "refuse to vet the package" taggedAs_ { mit => + s"refuse to vet the package on PV=${ProtocolVersion.v34}" taggedAs_ { mit => ledgerIntegrity.setAttack( Attack( actor = "participant operator", @@ -599,7 +602,7 @@ sealed trait PackageVettingIntegrationTest mitigation = mit, ) ) - } in { implicit env => + } onlyRunWhen (!pvSupportsUnvettedDependencies) in { implicit env => import env.* participant3.packages.list().filter(_.packageId == packId) should not be empty @@ -620,9 +623,16 @@ sealed trait PackageVettingIntegrationTest ) in { implicit env => import env.* - // can vet dependencies one by one using force archive.dependencies.foreach { dep => - vettingCmd(adds = List(dep), force = ForceFlags(ForceFlag.AllowUnvettedDependencies)) + vettingCmd( + adds = List(dep), + force = + if (pvSupportsUnvettedDependencies) ForceFlags.none + else { + // can vet dependencies one by one using force + ForceFlags(ForceFlag.AllowUnvettedDependencies) + }, + ) eventually() { participant3.topology.vetted_packages .list(daId, filterParticipant = participant3.filterString) @@ -861,17 +871,27 @@ sealed trait PackageVettingIntegrationTest val vettingDepDar = tryReadDar(VettingDepPath) val vettingMainDar = tryReadDar(VettingMainPath) - "refuse to unvet if the package is used as a dependency" in { implicit env => - import env.* + s"refuse to unvet if the package is used as a dependency on PV=${ProtocolVersion.v34} or before" onlyRunWhen (!pvSupportsUnvettedDependencies) in { + implicit env => + import env.* - // upload and vet the main dar and its dependencies - participant3.dars.upload(VettingMainPath, vetAllPackages = true) + // upload and vet the main dar and its dependencies + participant3.dars.upload(VettingMainPath, vetAllPackages = true) - // unvetting the dep package should fail - assertThrowsAndLogsCommandFailures( - vettingCmd(removes = Seq(vettingDepDar.main)), - _.shouldBeCantonErrorCode(ParticipantTopologyManagerError.DependenciesNotVetted), - ) + assertThrowsAndLogsCommandFailures( + vettingCmd(removes = Seq(vettingDepDar.main)), + _.shouldBeCantonErrorCode(ParticipantTopologyManagerError.DependenciesNotVetted), + ) + } + + s"allow to unvet if the package is used as a dependency on PV=${ProtocolVersion.v35} or after" onlyRunWhen (pvSupportsUnvettedDependencies) in { + implicit env => + import env.* + + // upload and vet the main dar and its dependencies + participant3.dars.upload(VettingMainPath, vetAllPackages = true) + + vettingCmd(removes = Seq(vettingDepDar.main)) } "allow to unvet if the package is used as a dependency and AllowUnvettedDependencies is used" in { @@ -882,20 +902,35 @@ sealed trait PackageVettingIntegrationTest ) } - "refuse to unvet while vetting a dependent package" in { implicit env => - // vet the dep package and unvet the main package - vettingCmd( - adds = Seq(vettingDepDar.main), - removes = Seq(vettingMainDar.main), - ) + s"refuse to unvet while vetting a dependent package (PV=${ProtocolVersion.v34})" onlyRunWhen (!pvSupportsUnvettedDependencies) in { + implicit env => + // vet the dep package and unvet the main package + vettingCmd( + adds = Seq(vettingDepDar.main), + removes = Seq(vettingMainDar.main), + ) + + assertThrowsAndLogsCommandFailures( + vettingCmd( + adds = Seq(vettingMainDar.main), + removes = Seq(vettingDepDar.main), + ), + _.shouldBeCantonErrorCode(ParticipantTopologyManagerError.DependenciesNotVetted), + ) + } + + s"allow to unvet while vetting a dependent package (PV=${ProtocolVersion.v35}+)" onlyRunWhen pvSupportsUnvettedDependencies in { + implicit env => + // vet the dep package and unvet the main package + vettingCmd( + adds = Seq(vettingDepDar.main), + removes = Seq(vettingMainDar.main), + ) - assertThrowsAndLogsCommandFailures( vettingCmd( adds = Seq(vettingMainDar.main), removes = Seq(vettingDepDar.main), - ), - _.shouldBeCantonErrorCode(ParticipantTopologyManagerError.DependenciesNotVetted), - ) + ) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/DynamicOnboardingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/DynamicOnboardingIntegrationTest.scala index a51722d849..bd393b5e28 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/DynamicOnboardingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/DynamicOnboardingIntegrationTest.scala @@ -348,6 +348,17 @@ abstract class DynamicOnboardingIntegrationTest(val name: String) ) ) => message should include(s"was previously delivered at $aggregationSequenced2") + case SendResult.Error( + DeliverError( + _, + _, + _, + _, + SequencerErrors.AggregateSubmissionAlreadySentV2(message), + _, + ) + ) => + message should include(s"was previously delivered at $aggregationSequenced2") } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/ToxiproxyIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/ToxiproxyIntegrationTest.scala index 27da1ddbe1..4ba7fb8e8a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/ToxiproxyIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/ToxiproxyIntegrationTest.scala @@ -8,6 +8,7 @@ import com.digitalasset.canton.concurrent.Threading import com.digitalasset.canton.config import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.SequencerReference +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -83,7 +84,15 @@ abstract class ToxiproxyIntegrationTest // Use a threshold of two to ensure that the mediator connects to all sequencers. // TODO(#19911) Reduce to one again once this can be configured independently. Some( - testMediators.map(_ -> (testSequencers, PositiveInt.two, NonNegativeInt.zero)).toMap + testMediators + .map( + _ -> MediatorSequencersConfiguration( + testSequencers, + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ) + ) + .toMap ), ) NetworkBootstrapper(Seq(description)) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftOrderingSequencerWithTrafficControlApiTestPostgres.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftOrderingSequencerWithTrafficControlApiTestPostgres.scala index 7b231f8657..25ba787e20 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftOrderingSequencerWithTrafficControlApiTestPostgres.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftOrderingSequencerWithTrafficControlApiTestPostgres.scala @@ -17,6 +17,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.BlockSequencerFactor import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.canton.sequencing.BftSequencerFactory import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig import com.digitalasset.canton.synchronizer.sequencer.config.SequencerNodeParameters +import com.digitalasset.canton.synchronizer.sequencer.time.LsuSequencingBounds import com.digitalasset.canton.synchronizer.sequencer.traffic.{ SequencerRateLimitManager, SequencerTrafficConfig, @@ -81,6 +82,7 @@ class BftOrderingSequencerWithTrafficControlApiTestPostgres synchronizerSyncCryptoApi: SynchronizerCryptoClient, protocolVersion: ProtocolVersion, trafficConfig: SequencerTrafficConfig, + lsuSequencingBounds: Option[LsuSequencingBounds], ): SequencerRateLimitManager = rateLimitManager } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftSequencerApiTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftSequencerApiTest.scala index 7792bde4ca..68d6f51cae 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftSequencerApiTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftSequencerApiTest.scala @@ -56,7 +56,9 @@ class BftSequencerApiTest extends SequencerApiTest with RateLimitManagerTesting asyncWriter = AsyncWriterParameters(), timeAdvancingTopology = TimeAdvancingTopologyConfig(), delayRequestsBeforeLsuTrafficInit = false, + enableRejectDeliveredAggregationsOnPv35 = Seq("MED", "PAR"), lsuConfig = SequencerLsuConfig(), + enablePrevalidation = true, ) override final def createSequencer(crypto: SynchronizerCryptoClient)(implicit diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerApiTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerApiTest.scala index df6a325c54..c6cf3453a6 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerApiTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerApiTest.scala @@ -91,7 +91,9 @@ class ReferenceSequencerApiTest extends SequencerApiTest with RateLimitManagerTe asyncWriter = AsyncWriterParameters(), timeAdvancingTopology = TimeAdvancingTopologyConfig(), delayRequestsBeforeLsuTrafficInit = false, + enableRejectDeliveredAggregationsOnPv35 = Seq("MED", "PAR"), lsuConfig = SequencerLsuConfig(), + enablePrevalidation = true, ) "Reference sequencer" when runSequencerApiTests() diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestBase.scala index 20c26d4f34..f3327bd1dc 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestBase.scala @@ -313,8 +313,10 @@ abstract class ReferenceSequencerWithTrafficControlApiTestBase asyncWriter = AsyncWriterParameters(), timeAdvancingTopology = TimeAdvancingTopologyConfig(), delayRequestsBeforeLsuTrafficInit = false, + enableRejectDeliveredAggregationsOnPv35 = Seq("MED"), disableSubmissionChecksForTesting = disableSubmissionChecksForTesting, lsuConfig = SequencerLsuConfig(), + enablePrevalidation = true, ) // Important to create the histograms before the factory, because creating the factory will // register them once and for all and we can't add more afterwards @@ -1320,6 +1322,7 @@ object ReferenceSequencerWithTrafficControlApiTestBase { trafficConfig, sequencerMemberRateLimiterFactory, eventCostCalculator, + lsuSequencingBounds = None, ) { private val isWriteSideEnforcementDisabled = new AtomicBoolean(false) private val readValidationResponse = new AtomicReference[ diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestPostgres.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestPostgres.scala index 7496aef193..fd36c5f241 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestPostgres.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestPostgres.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.BlockSequencerConfig import com.digitalasset.canton.synchronizer.sequencer.block.DriverBlockSequencerFactory import com.digitalasset.canton.synchronizer.sequencer.config.SequencerNodeParameters +import com.digitalasset.canton.synchronizer.sequencer.time.LsuSequencingBounds import com.digitalasset.canton.synchronizer.sequencer.traffic.{ SequencerRateLimitManager, SequencerTrafficConfig, @@ -58,6 +59,7 @@ class ReferenceSequencerWithTrafficControlApiTestPostgres synchronizerSyncCryptoApi: SynchronizerCryptoClient, protocolVersion: ProtocolVersion, trafficConfig: SequencerTrafficConfig, + lsuSequencingBounds: Option[LsuSequencingBounds], ): SequencerRateLimitManager = rateLimitManager } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/PartyToParticipantAuthIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/PartyToParticipantAuthIntegrationTest.scala index 6e48579537..4ac0db9363 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/PartyToParticipantAuthIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/PartyToParticipantAuthIntegrationTest.scala @@ -35,6 +35,7 @@ import org.scalatest.Assertion import org.slf4j.event.Level import java.util.UUID +import scala.concurrent.ExecutionContext trait PartyToParticipantAuthIntegrationTest extends CommunityIntegrationTest @@ -1096,7 +1097,10 @@ trait PartyToParticipantAuthIntegrationTest fingerprints: Seq[Fingerprint], serial: PositiveInt = PositiveInt.one, topologyChangeOp: TopologyChangeOp = TopologyChangeOp.Replace, - )(implicit env: FixtureParam): SignedTopologyTransaction[TopologyChangeOp, TopologyMapping] = { + )(implicit + env: FixtureParam, + ec: ExecutionContext, + ): SignedTopologyTransaction[TopologyChangeOp, TopologyMapping] = { val tx = topologyTransaction(topologyMapping, serial = serial, topologyChangeOp = topologyChangeOp) @@ -1128,13 +1132,13 @@ trait PartyToParticipantAuthIntegrationTest private def sign[Op <: TopologyChangeOp, M <: TopologyMapping]( topologyTransaction: TopologyTransaction[Op, M], fingerprint: Fingerprint, - )(implicit env: FixtureParam): Signature = + )(implicit env: FixtureParam, ec: ExecutionContext): Signature = signBytes(topologyTransaction.hash.hash.getCryptographicEvidence, fingerprint) private def signBytes( bytes: ByteString, fingerprint: Fingerprint, - )(implicit env: FixtureParam): Signature = + )(implicit env: FixtureParam, ec: ExecutionContext): Signature = env.tryGlobalCrypto.privateCrypto .signBytes( bytes, @@ -1148,7 +1152,7 @@ trait PartyToParticipantAuthIntegrationTest participant: ParticipantReference, partyId: PartyId, signingKey: SigningPublicKey, - )(implicit env: FixtureParam): ExecuteSubmissionAndWaitResponse = { + )(implicit env: FixtureParam, ec: ExecutionContext): ExecuteSubmissionAndWaitResponse = { val prepared: PrepareSubmissionResponse = participant.ledger_api.javaapi.interactive_submission.prepare( Seq(partyId), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/TopologyValidationMultiSynchronizerIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/TopologyValidationMultiSynchronizerIntegrationTest.scala index d90f37df5a..fe80f4bd7a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/TopologyValidationMultiSynchronizerIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/TopologyValidationMultiSynchronizerIntegrationTest.scala @@ -36,7 +36,7 @@ class TopologyValidationMultiSynchronizerIntegrationTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/toxiproxy/slow/ToxiproxyBftSequencerConnectionsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/toxiproxy/slow/ToxiproxyBftSequencerConnectionsIntegrationTest.scala index 10ceb0a2a3..d73949b73e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/toxiproxy/slow/ToxiproxyBftSequencerConnectionsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/toxiproxy/slow/ToxiproxyBftSequencerConnectionsIntegrationTest.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.config import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.{CommandFailure, InstanceReference} import com.digitalasset.canton.error.TransactionRoutingError.TopologyErrors.UnknownContractSynchronizers +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -102,8 +103,11 @@ sealed trait ToxiproxyBftSequencerConnectionsIntegrationTest mediators = Seq(mediator1), overrideMediatorToSequencers = Some( Map( - mediator1 -> (sequencers.local, - /* trust threshold */ PositiveInt.two, /* liveness margin */ NonNegativeInt.zero) + mediator1 -> MediatorSequencersConfiguration( + sequencers.local, + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ) ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/ParticipantTrafficEnforcementTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/ParticipantTrafficEnforcementTest.scala new file mode 100644 index 0000000000..99e19a1a6b --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/ParticipantTrafficEnforcementTest.scala @@ -0,0 +1,213 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.traffic + +import com.digitalasset.canton.config +import com.digitalasset.canton.config.RequireTypes.PositiveInt +import com.digitalasset.canton.console.CommandFailure +import com.digitalasset.canton.integration.* +import com.digitalasset.canton.integration.util.{TestUtils, TrafficControlUtils} +import com.digitalasset.canton.ledger.error.CommonErrors.ServiceNotRunning +import com.digitalasset.canton.logging.LogEntry +import com.digitalasset.canton.platform.config.{ + TrafficEnforcementConfig, + TrafficEnforcementServerConfig, +} +import com.digitalasset.canton.topology.ExternalParty +import io.grpc.Status +import monocle.macros.syntax.lens.* +import org.scalatest.Assertion + +import java.time.Duration +import java.util.UUID +import scala.concurrent.duration.DurationInt + +sealed trait ParticipantTrafficEnforcementTest + extends CommunityIntegrationTest + with SharedEnvironment + with HasCycleUtils { + + protected var aliceE: ExternalParty = _ + + /** Participant config transforms to enable or disable participant-local traffic enforcement. + */ + protected def participantConfigTransforms: Seq[ConfigTransform] + + override def environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition.P2_S1M1 + .addConfigTransforms(participantConfigTransforms*) + .addConfigTransform(ConfigTransforms.enableInteractiveSubmissionTransforms) + .addConfigTransforms(ConfigTransforms.useStaticTime) + .addConfigTransform( + ConfigTransforms.updateAllSequencerConfigs_( + _.focus(_.trafficConfig.pruningRetentionWindow) + .replace(config.NonNegativeFiniteDuration.ofSeconds(5)) + .focus(_.trafficConfig.trafficPurchasedCacheSizePerMember) + .replace(PositiveInt.one) + ) + ) + .withSetup { implicit env => + import env.* + participants.local.foreach { participant => + participant.synchronizers.connect_local(sequencer1, alias = daName) + participant.dars.upload(CantonExamplesPath, synchronizerId = daId) + } + + aliceE = participant1.parties.testing.external.enable("Alice") + } + .withTrafficControl( + TestUtils.waitForTargetTimeOnSynchronizerNode(wallClock.now, logger), + trafficControlParameters = TrafficControlUtils.predictableTraffic, + topUpAllMembers = true, + disableCommitments = true, + ) + + protected def assertUnimplemented(entry: LogEntry): Assertion = + entry.message should include(Status.Code.UNIMPLEMENTED.toString) +} + +final class ParticipantTrafficEnforcementDisabledTest extends ParticipantTrafficEnforcementTest { + override protected def participantConfigTransforms: Seq[ConfigTransform] = Seq( + ConfigTransforms.updateParticipantConfig("participant1")( + _.focus(_.trafficEnforcement).replace(TrafficEnforcementConfig(enabled = false)) + ) + ) + + "Participant" when { + "traffic enforcement is disabled" should { + "not expose the traffic service endpoints on the Ledger API" in { implicit env => + import env.* + + assertThrowsAndLogsCommandFailures( + participant1.ledger_api.traffic.get_account(aliceE.partyId.toProtoPrimitive), + assertUnimplemented, + ) + + assertThrowsAndLogsCommandFailures( + participant1.ledger_api.traffic.update_account( + aliceE.partyId.toProtoPrimitive, + None, + ), + assertUnimplemented, + ) + } + } + + "support interactive submissions" in { implicit env => + import env.* + + // Pass some time to allow traffic re-fill the submission below + environment.simClock.value.advance(Duration.ofSeconds(5L)) + + // Prepare and execute should work seamlessly + val prepared = participant1.ledger_api.interactive_submission.prepare( + actAs = Seq(aliceE), + commands = Seq(createCycleCommand(aliceE.partyId, "traffic")), + hashingSchemeVersion = testedApiHashingSchemeVersion, + ) + + participant1.ledger_api.interactive_submission.execute_and_wait( + prepared.getPreparedTransaction, + Map(aliceE.partyId -> global_secret.sign(prepared.preparedTransactionHash, aliceE)), + UUID.randomUUID().toString, + prepared.hashingSchemeVersion, + ) + } + } +} + +final class ParticipantTrafficEnforcementEnabledTest extends ParticipantTrafficEnforcementTest { + private val nonExistentTeaServerName = s"non-existent-tea-server" + + override protected def participantConfigTransforms: Seq[ConfigTransform] = Seq( + ConfigTransforms.updateParticipantConfig("participant1")( + _.focus(_.trafficEnforcement) + .replace( + TrafficEnforcementConfig( + enabled = true, + trafficEnforcementServer = + TrafficEnforcementServerConfig.Internal(nonExistentTeaServerName), + ) + ) + ), + // Shorten network timeout so retries to the non-existent traffic service give up quickly + _.focus(_.parameters.timeouts.processing.network) + .replace(config.NonNegativeDuration.tryFromDuration(5.seconds)), + ) + + "Participant" when { + "traffic enforcement is enabled but traffic enforcement server is not available" should { + "return graceful errors on traffic and interactive submission service endpoints" in { + implicit env => + import env.* + + def assertEntriesTeaUnavailable(entries: Seq[LogEntry]): Assertion = + entries.foldLeft(succeed) { case (_, entry) => + entry.message should ((include(ServiceNotRunning.id) and include( + "User traffic service is not running" + )) or + (include(Status.Code.UNAVAILABLE.toString) and include( + s"Could not find server: $nonExistentTeaServerName" + ) or + include("Retry timeout has elapsed, giving up."))) + } + + // GetAccount on P1 fails due to TEA not enabled + loggerFactory.assertThrowsAndLogsSeq[CommandFailure]( + participant1.ledger_api.traffic.get_account(aliceE.partyId.toProtoPrimitive), + assertEntriesTeaUnavailable, + ) + + // UpdateAccount on P1 fails due to TEA not enabled + loggerFactory.assertThrowsAndLogsSeq[CommandFailure]( + participant1.ledger_api.traffic.update_account( + aliceE.partyId.toProtoPrimitive, + None, + ), + assertEntriesTeaUnavailable, + ) + + // Preparing on P1 fails due to TEA not enabled + loggerFactory.assertThrowsAndLogsSeq[CommandFailure]( + participant1.ledger_api.interactive_submission.prepare( + actAs = Seq(aliceE), + commands = Seq(createCycleCommand(aliceE.partyId, "traffic")), + hashingSchemeVersion = testedApiHashingSchemeVersion, + ), + assertEntriesTeaUnavailable, + ) + + // Prepare a transaction on P2 for Alice (P2 does not have traffic enabled so we can prepare) + val prepared = participant2.ledger_api.interactive_submission.prepare( + actAs = Seq(aliceE), + commands = Seq(createCycleCommand(aliceE.partyId, "traffic")), + hashingSchemeVersion = testedApiHashingSchemeVersion, + ) + + // Executing on P1 fails due to TEA not enabled + loggerFactory.assertThrowsAndLogsSeq[CommandFailure]( + participant1.ledger_api.interactive_submission.execute_and_wait( + prepared.getPreparedTransaction, + Map(aliceE.partyId -> global_secret.sign(prepared.preparedTransactionHash, aliceE)), + UUID.randomUUID().toString, + prepared.hashingSchemeVersion, + // Short timeout to reduce test time + optTimeout = Some(5.seconds), + ), + _.foldLeft(succeed) { case (_, entry) => + entry.message should ((include(ServiceNotRunning.id) and include( + "User traffic service is not running" + )) or + (include(Status.Code.UNAVAILABLE.toString) and include( + s"Could not find server: $nonExistentTeaServerName" + )) or + include("Retry timeout has elapsed, giving up.") or + include("Failed to submit submission") or + include("DEADLINE_EXCEEDED")) + }, + ) + } + } + } +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/TrafficControlTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/TrafficControlTest.scala index e78ba7250c..113b1abb73 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/TrafficControlTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/TrafficControlTest.scala @@ -11,6 +11,7 @@ import com.digitalasset.canton.admin.api.client.data.{ ComponentHealthState, TrafficControlParameters, } +import com.digitalasset.canton.annotations.UnstableTest import com.digitalasset.canton.config.CantonRequireTypes.InstanceName import com.digitalasset.canton.config.RequireTypes.{ NonNegativeLong, @@ -1006,6 +1007,7 @@ trait TrafficControlTest ) } +@UnstableTest // TOOD(#31976) class TrafficControlTestBftOrderingPostgres extends TrafficControlTest { private val useBftSequencer = new UseBftSequencer( loggerFactory, @@ -1018,6 +1020,7 @@ class TrafficControlTestBftOrderingPostgres extends TrafficControlTest { registerPlugin(new UseProgrammableSequencer(this.getClass.toString, loggerFactory)) } +@UnstableTest // TOOD(#32073) class TrafficControlTestBftOrderingH2 extends TrafficControlTest { private val useBftSequencer = new UseBftSequencer( loggerFactory, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/CantonNetworkTopologyStateIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/CantonNetworkTopologyStateIntegrationTest.scala index 49190e2964..9de116b926 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/CantonNetworkTopologyStateIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/CantonNetworkTopologyStateIntegrationTest.scala @@ -12,9 +12,9 @@ import com.digitalasset.canton.integration.plugins.toxiproxy.UseToxiproxy.Toxipr import com.digitalasset.canton.integration.plugins.toxiproxy.{ParticipantToPostgres, UseToxiproxy} import com.digitalasset.canton.integration.tests.toxiproxy.ToxiproxyHelpers import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, EnvironmentDefinition, - EnvironmentSetup, SharedEnvironment, TestConsoleEnvironment, } @@ -43,7 +43,7 @@ import scala.annotation.nowarn import scala.concurrent.duration.* trait CantonNetworkTopologyIntegrationTestBase extends CommunityIntegrationTest { - this: EnvironmentSetup => + this: CantonEnvironmentSetup => protected def runValidation( topoStoreIdx: Int, txs: GenericStoredTopologyTransactions, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuCommandIdIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuCommandIdIntegrationTest.scala index ed12c111e3..fdf2b997c6 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuCommandIdIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuCommandIdIntegrationTest.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.integration.tests.upgrade.lsu import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.MediatorError +import com.digitalasset.canton.error.TransactionRoutingError.ConfigurationErrors import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UseProgrammableSequencer} @@ -13,15 +14,11 @@ import com.digitalasset.canton.integration.tests.examples.IouSyntax import com.digitalasset.canton.integration.util.TestUtils.waitForTargetTimeOnSequencer import com.digitalasset.canton.integration.{EnvironmentDefinition, TestEnvironment} import com.digitalasset.canton.ledger.error.groups.ConsistencyErrors.DuplicateCommand -import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality.{ - Optional, - OptionalMany, -} import com.digitalasset.canton.participant.protocol.TransactionProcessor.SubmissionErrors import com.digitalasset.canton.participant.protocol.TransactionProcessor.SubmissionErrors.SequencerRequest +import com.digitalasset.canton.participant.sync.SyncServiceInjectionError import com.digitalasset.canton.protocol.LocalRejectError.TimeRejects -import com.digitalasset.canton.sequencing.protocol.SubmissionRequest -import com.digitalasset.canton.synchronizer.sequencer.errors.SequencerError +import com.digitalasset.canton.sequencing.protocol.{SequencerErrors, SubmissionRequest} import com.digitalasset.canton.synchronizer.sequencer.{HasProgrammableSequencer, SendDecision} import com.digitalasset.canton.topology.PartyId import com.google.rpc.Code @@ -145,48 +142,35 @@ final class LsuCommandIdIntegrationTest extends LsuBase with HasProgrammableSequ val cmdIdAtUpgradeTime = "cmd-id-at-upgrade-time" withClue("test command at upgrade time") { - loggerFactory.assertLogsUnorderedOptional( - { - // Move to upgrade time so that command submissions will fail due to overlap with LSU - environment.simClock.value.advanceTo(upgradeTime) - - val offsetBeforeSubmit = participant1.ledger_api.state.end() - participant1.ledger_api.javaapi.commands - .submit_async(Seq(bank), createIouCmd, commandId = cmdIdAtUpgradeTime) - - // Move forward until the new psid is up - environment.simClock.value.advanceTo(upgradeTime.immediateSuccessor) - transferTraffic(suppressLogs = false) - eventually() { - environment.simClock.value.advance(Duration.ofSeconds(1)) - participants.all.forall(_.synchronizers.is_connected(fixture.newPsid)) shouldBe true - } - waitForTargetTimeOnSequencer(sequencer2, environment.clock.now, logger) + // Move to upgrade time so that command submissions will fail due to overlap with LSU + environment.simClock.value.advanceTo(upgradeTime) + // wait for the mediator to observe the upgrade time on the old synchronizer. + mediator1.testing.await_synchronizer_time(upgradeTime, commandTimeouts.ledgerCommand) - oldSynchronizerNodes.all.stop() + assertThrowsAndLogsCommandFailures( + participant1.ledger_api.javaapi.commands + .submit(Seq(bank), createIouCmd, commandId = cmdIdAtUpgradeTime), + _.message should ( + include(SyncServiceInjectionError.NotConnectedToAnySynchronizer.id) or + include(ConfigurationErrors.SubmissionSynchronizerNotReady.id) or + include(SubmissionErrors.TimeoutError.id) or + include(SequencerErrors.PassedUpgradeTime.id) or + // the command gets rejected with this error, when the synthetic LSU tombstone gets sequenced and processed + // before the command gets submitted, and therefore it gets rejected directly during the synchronous processing of sendAsync + include(SequencerRequest.id) + ), + ) - // Move further forward until this decision timeout has expired, and expect to see our submission timeout on the completion stream - environment.simClock.value.advance(decisionTimeout.plusSeconds(1).asJava) - participant1.health.ping(participant1) // To notify the sequencer that time has passed + // Move forward until the new psid is up + environment.simClock.value.advanceTo(upgradeTime.immediateSuccessor) + transferTraffic(suppressLogs = false) + eventually() { + environment.simClock.value.advance(Duration.ofSeconds(1)) + participants.all.forall(_.synchronizers.is_connected(fixture.newPsid)) shouldBe true + } + waitForTargetTimeOnSequencer(sequencer2, environment.clock.now, logger) - assertCommandEventuallyFailed( - cmdIdAtUpgradeTime, - offsetBeforeSubmit, - bank, - _ should ( - include(TimeRejects.LocalTimeout.id) or - include(SubmissionErrors.TimeoutError.id) or - // the command gets rejected with this error, when the synthetic LSU tombstone gets sequenced and processed - // before the command gets submitted, and therefore it gets rejected right at the start - include(SequencerRequest.id) - ), - ) - }, - Optional -> (_.warningMessage should (include regex "Response message for request .* timed out at")), - Optional -> (_.warningMessage should include("Submission timed out at")), - Optional -> (_.warningMessage should include("Time validation has failed")), - OptionalMany -> (_.shouldBeCantonErrorCode(SequencerError.NotAtUpgradeTimeOrBeyond)), - ) + oldSynchronizerNodes.all.stop() } withClue( @@ -211,7 +195,7 @@ final class LsuCommandIdIntegrationTest extends LsuBase with HasProgrammableSequ sinceOffset: Long, party: PartyId, assertErrorMessage: String => Unit, - )(implicit env: TestEnvironment): Unit = { + )(implicit env: TestEnvironment[?]): Unit = { val status = env.participant1.ledger_api.completions .list( party, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEarlyHandshakeIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEarlyHandshakeIntegrationTest.scala index ab0cc39b2c..036920bb83 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEarlyHandshakeIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEarlyHandshakeIntegrationTest.scala @@ -186,7 +186,7 @@ final class LsuEarlyHandshakeIntegrationTest extends LsuBase { forAll(participants.local)( _.topology.lsu.sequencer_successors .list() - .filter(_.item.sequencerId == sequencer1.id) + .filter(_.item.sequencerId == sequencer2.id) .loneElement ) } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala index 309ef2387a..bb9bd05869 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala @@ -210,6 +210,52 @@ private object LsuEndToEndIntegrationTest { } } +private object LsuEndToEndIntegrationTest { + import org.scalatest.OptionValues.* + + final case class ExpectedRegisteredSynchronizers( + initialSynchronizerConnectionConfig: SynchronizerConnectionConfig, + fixture: Fixture, + )(implicit env: TestConsoleEnvironment) { + import env.* + + val currentRegisteredSynchronizerBeforeLsu = RegisteredSynchronizer( + config = initialSynchronizerConnectionConfig, + status = Status.Active, + psid = KnownPhysicalSynchronizerId(fixture.currentPsid), + predecessor = None, + isConnected = true, + ) + + val newSequencerConnections: NonEmpty[Map[SequencerAlias, SequencerConnection]] = NonEmpty + .from( + Map( + sequencer1.sequencerAlias -> sequencer2.sequencerConnection + .copy(sequencerAlias = sequencer1.sequencerAlias, sequencerId = Some(sequencer1.id)) + ) + ) + .value + + val newRegisteredSynchronizerBeforeLsu = RegisteredSynchronizer( + config = initialSynchronizerConnectionConfig + .focus(_.sequencerConnections.aliasToConnection) + .replace(newSequencerConnections) + .focus(_.synchronizerId) + .replace(Some(fixture.newPsid)), + status = Status.LsuTarget, + psid = KnownPhysicalSynchronizerId(fixture.newPsid), + predecessor = Some( + SynchronizerPredecessor( + psid = fixture.currentPsid, + upgradeTime = fixture.upgradeTime, + isLateUpgrade = false, + ) + ), + isConnected = false, + ) + } +} + final class LsuEndToEndSimClockIntegrationTest extends LsuEndToEndIntegrationTest final class LsuEndToEndWallClockIntegrationTest extends LsuEndToEndIntegrationTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuExhaustiveHandshakeIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuExhaustiveHandshakeIntegrationTest.scala new file mode 100644 index 0000000000..4b611224d4 --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuExhaustiveHandshakeIntegrationTest.scala @@ -0,0 +1,206 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.upgrade.lsu + +import com.daml.metrics.api.MetricQualification +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.integration.* +import com.digitalasset.canton.integration.EnvironmentDefinition.S2M2 +import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper +import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} +import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuBase.{ + getLsuStatusMetricValues, + getParticipantHandshakesMetricValues, +} +import com.digitalasset.canton.logging.SuppressionRule +import com.digitalasset.canton.metrics.{MetricsConfig, MetricsReporterConfig} +import com.digitalasset.canton.participant.metrics.ParticipantMetrics +import com.digitalasset.canton.participant.synchronizer.grpc.GrpcSynchronizerRegistry +import com.digitalasset.canton.{UniquePortGenerator, config} +import monocle.macros.syntax.lens.* +import org.slf4j.event.Level + +/** The goal of this test is to ensure that participants do the handshake with as many sequencers as + * possible and that this "waiting" is interrupted in case of shutdown or when minimumDuration has + * elapsed. + * + * Topology: + * - P1, P2, P3 connected to S1 and S2 with threshold=1 + * - The handshake is done as soon as S1 successor is known + * - Announcement of S2 successor triggers another handshake on which we do checks/assertions + * + * - Two sequencers: + * - S1 has S3 as successor + * - S2 has S4 as successor + * + * Tests: + * - P1 waits until all sequencers are up + * - P2 stops waiting because of shutdown + * - P3 stops waiting because of minimumDuration has elapsed + */ +final class LsuExhaustiveHandshakeIntegrationTest extends LsuBase { + + override protected def testName: String = "lsu-exhaustive-handshake" + + registerPlugin( + new UseBftSequencer( + loggerFactory, + MultiSynchronizer.tryCreate(Set("sequencer1", "sequencer2"), Set("sequencer3", "sequencer4")), + ) + ) + registerPlugin(new UsePostgres(loggerFactory)) + + override protected lazy val newOldSequencers: Map[String, String] = + Map("sequencer3" -> "sequencer1", "sequencer4" -> "sequencer2") + override protected lazy val newOldMediators: Map[String, String] = + Map("mediator3" -> "mediator1", "mediator4" -> "mediator2") + override protected lazy val upgradeTime: CantonTimestamp = CantonTimestamp.Epoch.plusSeconds(30) + + override lazy val environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition.P3S4M4_Config + .withNetworkBootstrap { implicit env => + new NetworkBootstrapper(S2M2) + } + .addConfigTransforms( + _.focus(_.monitoring.metrics) + .replace( + MetricsConfig( + qualifiers = Seq[MetricQualification](MetricQualification.Debug), + reporters = Seq( + MetricsReporterConfig.Prometheus( + port = UniquePortGenerator.next + ) + ), + ) + ) + ) + .addConfigTransform( + ConfigTransforms.updateAllParticipantConfigs { + + case ("participant1" | "participant2", pConfig) => + pConfig + .focus(_.parameters.lsu.handshake.minimumDuration) + // big value to ensure that the minimumDuration does not interrupt the wait + .replace(Some(config.NonNegativeFiniteDuration.ofDays(1))) + + case ("participant3", pConfig) => + pConfig + .focus(_.parameters.lsu.handshake.minimumDuration) + // small value to ensure that the minimumDuration interrupts the wait + .replace(Some(config.NonNegativeFiniteDuration.ofSeconds(3))) + + case (_other, pConfig) => pConfig + } + ) + .addConfigTransforms(configTransforms*) + .withSetup { implicit env => + import env.* + + defaultEnvironmentSetup(connectParticipants = false) + + participants.local.foreach( + _.synchronizers.connect_by_config( + synchronizerConnectionConfig(Seq(sequencer1, sequencer2), threshold = 1) + ) + ) + } + + "Participants" should { + "perform the handshake with as many sequencers as possible" in { implicit env => + import env.* + + val fixture = fixtureWithDefaults() + val p2Id = participant2.id + + fixture.oldSynchronizerOwners.foreach( + _.topology.lsu.announcement.propose(fixture.newPsid, fixture.upgradeTime) + ) + + // Ensure all nodes see the announcement + eventually() { + forAll(fixture.oldSynchronizerNodes.all ++ participants.local)( + _.topology.lsu.announcement + .list(store = Some(fixture.currentPsid)) + .filter(_.item.successorSynchronizerId == fixture.newPsid) + .loneElement + ) + } + + migrateSynchronizerNodes(fixture) + + /* + Stopping S4 so that handshake with S4 fails. + Stopping the mediators first to avoid connectivity warnings in the logs. + */ + + // Ensure that handshake with S4 fails + fixture.newSynchronizerNodes.mediators.stop() + sequencer4.stop() + + sequencer1.topology.lsu.sequencer_successors.propose_successor( + sequencerId = sequencer1.id, + endpoints = sequencer3.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + successorSynchronizerId = fixture.newPsid, + ) + + // Initial handshake succeed because threshold=1 + eventually() { + forAll(participants.local) { p => + getLsuStatusMetricValues(p) + .get(fixture.newPsid) + .value should be >= ParticipantMetrics.LsuStatus.LocalCopyDone + } + } + + loggerFactory.assertEventuallyLogsSeq( + SuppressionRule.Level(Level.DEBUG) && SuppressionRule.forLogger[GrpcSynchronizerRegistry] + )( + sequencer2.topology.lsu.sequencer_successors.propose_successor( + sequencerId = sequencer2.id, + endpoints = sequencer4.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + successorSynchronizerId = fixture.newPsid, + ), + entries => { + // All participants start the waiting + forExactly(3, entries)( + _.debugMessage should include("Handshake was successful. Starting to wait until") + ) + // P3 eventually stops waiting + forExactly(1, entries) { entry => + entry.debugMessage should include( + "Stopping the wait because max waiting time is reached." + ) + entry.loggerName should include("participant3") + } + }, + ) + + loggerFactory.assertLogsSeq( + SuppressionRule.Level(Level.DEBUG) && SuppressionRule.forLogger[GrpcSynchronizerRegistry] + )( + participant2.stop(), + forExactly(1, _) { entry => + entry.debugMessage should include("Stopping the wait because of shutdown.") + entry.loggerName should include("participant2") + }, + ) + + // Starting S4 should eventually allow P1 to handshake with S4 + sequencer4.start() + + eventually() { + getParticipantHandshakesMetricValues(sequencer4) + .get((participant1.id, "success")) + .value should be >= 1L + } + + getParticipantHandshakesMetricValues(sequencer4).get((p2Id, "success")) shouldBe empty + + getParticipantHandshakesMetricValues(sequencer4).get( + (participant3.id, "success") + ) shouldBe empty + } + } +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuIncorrectSequencerIdentityIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuIncorrectSequencerIdentityIntegrationTest.scala index 7b9f6d89ef..86038a4033 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuIncorrectSequencerIdentityIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuIncorrectSequencerIdentityIntegrationTest.scala @@ -135,6 +135,20 @@ final class LsuIncorrectSequencerIdentityIntegrationTest extends LsuBase { LogEntryOptionality.OptionalMany, _.shouldBeCantonErrorCode(SequencerError.NotAtUpgradeTimeOrBeyond), ), + // sequencer1 fails to contact/handshake its successor + ( + LogEntryOptionality.Required, + _.warningMessage should include( + s"Error when contacting successor: expecting sequencer id to be ${sequencer1.id} but found ${sequencer2.id}" + ), + ), + // sequencer2 fails to contact/handshake its successor + ( + LogEntryOptionality.Required, + _.warningMessage should include( + s"Error when contacting successor: expecting sequencer id to be ${sequencer2.id} but found ${sequencer1.id}" + ), + ), ) } } @@ -288,6 +302,13 @@ final class LsuSuccessorSequencerIsPredecessorIntegrationTest extends LsuBase { LogEntryOptionality.OptionalMany, _.shouldBeCantonErrorCode(SequencerError.NotAtUpgradeTimeOrBeyond), ), + // failed contact between sequencers and their successor + ( + LogEntryOptionality.OptionalMany, + _.warningMessage should include( + s"Error when contacting successor: expecting psid to be ${fixture.newPsid} but found ${fixture.currentPsid}" + ), + ), ) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuMetricsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuMetricsIntegrationTest.scala index 0f1a42ada0..6ebaff3b89 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuMetricsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuMetricsIntegrationTest.scala @@ -85,7 +85,7 @@ final class LsuMetricsIntegrationTest extends LsuBase { .addConfigTransform( // We want to retry more aggressively in the test ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.lsu.handshakeRetry) + _.focus(_.parameters.lsu.handshake.retry) .replace( ExponentialBackoffConfig( initialDelay = config.NonNegativeFiniteDuration.ofMillis(100), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuOfflinePartyReplicationIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuOfflinePartyReplicationIntegrationTest.scala index 46a58c9120..dc6a7f8b4b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuOfflinePartyReplicationIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuOfflinePartyReplicationIntegrationTest.scala @@ -100,7 +100,7 @@ abstract class LsuOfflinePartyReplicationIntegrationTest extends LsuBase with Ha protected val acsSnapshotFile: TempFile = tempDirectory.toTempFile("offpr_test_acs_snapshot.gz") - protected def makeFixture1(implicit env: TestEnvironment): Fixture = Fixture( + protected def makeFixture1(implicit env: TestEnvironment[?]): Fixture = Fixture( currentPsid = env.daId, upgradeTime = upgradeTime1, oldSynchronizerNodes = SynchronizerNodes(Seq(env.sequencer1), Seq(env.mediator1)), @@ -111,16 +111,17 @@ abstract class LsuOfflinePartyReplicationIntegrationTest extends LsuBase with Ha newSerial = env.daId.serial.increment.toNonNegative, ) - protected def makeFixture2(fixture1: Fixture)(implicit env: TestEnvironment): Fixture = Fixture( - currentPsid = fixture1.newPsid, - upgradeTime = upgradeTime2, - oldSynchronizerNodes = fixture1.newSynchronizerNodes, - newSynchronizerNodes = SynchronizerNodes(Seq(env.sequencer3), Seq(env.mediator3)), - newOldNodesResolution = Map("sequencer3" -> "sequencer2", "mediator3" -> "mediator2"), - oldSynchronizerOwners = Set[InstanceReference](env.sequencer2, env.mediator2), - newPV = testedProtocolVersion, - newSerial = fixture1.newSerial.increment.toNonNegative, - ) + protected def makeFixture2(fixture1: Fixture)(implicit env: TestEnvironment[?]): Fixture = + Fixture( + currentPsid = fixture1.newPsid, + upgradeTime = upgradeTime2, + oldSynchronizerNodes = fixture1.newSynchronizerNodes, + newSynchronizerNodes = SynchronizerNodes(Seq(env.sequencer3), Seq(env.mediator3)), + newOldNodesResolution = Map("sequencer3" -> "sequencer2", "mediator3" -> "mediator2"), + oldSynchronizerOwners = Set[InstanceReference](env.sequencer2, env.mediator2), + newPV = testedProtocolVersion, + newSerial = fixture1.newSerial.increment.toNonNegative, + ) protected def assertParticipantHostsParty( participant: ParticipantReference, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuPurgeStoresAfterLsuIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuPurgeStoresAfterLsuIntegrationTest.scala index 8310dece36..208cbbe94c 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuPurgeStoresAfterLsuIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuPurgeStoresAfterLsuIntegrationTest.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.integration.tests.examples.IouSyntax import com.digitalasset.canton.integration.util.TestUtils.waitForTargetTimeOnSequencer import com.digitalasset.canton.integration.{ConfigTransforms, EnvironmentDefinition} import com.digitalasset.canton.participant.config.PurgeConfig +import com.digitalasset.canton.topology.transaction.TopologyMapping import monocle.macros.syntax.lens.* import java.time.Duration @@ -62,6 +63,7 @@ final class LsuPurgeStoresAfterLsuIntegrationTest extends LsuBase { PurgeConfig().copy( chunkSize = PositiveInt.tryCreate(2), cron = "/5 * * * * ?", + purgeableStoresListValidity = config.NonNegativeFiniteDuration.ofSeconds(1), ) ) ) @@ -85,8 +87,9 @@ final class LsuPurgeStoresAfterLsuIntegrationTest extends LsuBase { performSynchronizerNodesLsu(fixture) - clue("old stores are not empty") { - oldTopologyStore.dumpStoreContent().futureValueUS.result should not be empty + val oldTopology = clue("old stores are not empty") { + val oldTopology = oldTopologyStore.dumpStoreContent().futureValueUS.result.toSet + oldTopology should not be empty participant1.underlying.value.sync.syncPersistentStateManager .get(fixture.currentPsid) @@ -94,6 +97,8 @@ final class LsuPurgeStoresAfterLsuIntegrationTest extends LsuBase { .submissionTrackerStore .size .futureValueUS shouldBe 1 + + oldTopology } environment.simClock.value.advanceTo(upgradeTime.immediateSuccessor) @@ -126,8 +131,16 @@ final class LsuPurgeStoresAfterLsuIntegrationTest extends LsuBase { .value .topologyStore - clue("new stores are not empty") { - newTopologyStore.dumpStoreContent().futureValueUS.result should not be empty + clue("new stores are not purged") { + newTopologyStore + .dumpStoreContent() + .futureValueUS + .result + .toSet shouldBe oldTopology + // LsuSequencerConnectionSuccessor is filtered out when doing the local copy + .filterNot( + _.mapping.code == TopologyMapping.Code.LsuSequencerConnectionSuccessor + ) participant1.underlying.value.sync.syncPersistentStateManager .get(fixture.newPsid) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuReassignmentsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuReassignmentsIntegrationTest.scala index a5e34e150a..7b970120e8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuReassignmentsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuReassignmentsIntegrationTest.scala @@ -52,7 +52,7 @@ final class LsuReassignmentsIntegrationTest extends LsuBase { NetworkBootstrapper(S1M1_S1M1) } .addConfigTransforms(configTransforms*) - .addConfigTransform(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransform(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => defaultEnvironmentSetup() } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSanityCheckSuccessorSynchronizerIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSanityCheckSuccessorSynchronizerIntegrationTest.scala index 6ee7f6a414..751f21c4a7 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSanityCheckSuccessorSynchronizerIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSanityCheckSuccessorSynchronizerIntegrationTest.scala @@ -19,6 +19,7 @@ import com.digitalasset.canton.integration.plugins.{ UseReferenceBlockSequencer, } import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuBase.getLsuSequencingTestMetricValues +import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality import com.digitalasset.canton.logging.SuppressionRule import com.digitalasset.canton.metrics.{MetricsConfig, MetricsReporterConfig} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.OutputModule @@ -101,21 +102,45 @@ sealed abstract class LsuSanityCheckSuccessorSynchronizerIntegrationTest extends m.get(sequencer3.id).value should be > 0L } - sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) - sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) - eventually() { - getLsuSequencingTestMetricValues(mediator3).get(sequencer4.id).value shouldBe 2 - getLsuSequencingTestMetricValues(mediator4).get(sequencer4.id).value shouldBe 2 - } - - // Check whether the command behaves well with a restart - sequencer4.stop() - sequencer4.start() - sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) - eventually() { - getLsuSequencingTestMetricValues(mediator3).get(sequencer4.id).value shouldBe 3 - getLsuSequencingTestMetricValues(mediator4).get(sequencer4.id).value shouldBe 3 - } + mediator3.stop() + mediator3.start() + + loggerFactory.assertLogsUnorderedOptional( + { + sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) + sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) + eventually() { + getLsuSequencingTestMetricValues(mediator3).get(sequencer4.id).value shouldBe 2 + getLsuSequencingTestMetricValues(mediator4).get(sequencer4.id).value shouldBe 2 + } + + // Check whether the command behaves well with a restart + sequencer4.stop() + sequencer4.start() + sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) + eventually() { + getLsuSequencingTestMetricValues(mediator3).get(sequencer4.id).value shouldBe 3 + getLsuSequencingTestMetricValues(mediator4).get(sequencer4.id).value shouldBe 3 + } + }, + // can happen if one mediator tries to ack when sequencer4 is stopped + ( + LogEntryOptionality.OptionalMany, + _.warningMessage should include("Failed to acknowledge clean timestamp"), + ), + ( + LogEntryOptionality.OptionalMany, + _.warningMessage should include( + "Is the server running? Did you configure the server address" + ), + ), + ( + LogEntryOptionality.Optional, + _.warningMessage should include( + "shutdown did not complete gracefully in allotted 3 seconds" + ), + ), + ) environment.simClock.value.advanceTo(upgradeTime.immediateSuccessor) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSequencerContactSuccessorIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSequencerContactSuccessorIntegrationTest.scala index 8426e7fbe5..04a5e2805d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSequencerContactSuccessorIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSequencerContactSuccessorIntegrationTest.scala @@ -7,7 +7,7 @@ import com.daml.metrics.api.MetricQualification import com.digitalasset.canton.UniquePortGenerator import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.integration.* -import com.digitalasset.canton.integration.EnvironmentDefinition.S1M1 +import com.digitalasset.canton.integration.EnvironmentDefinition.S2M2 import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} @@ -16,11 +16,16 @@ import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuBase.{ getLsuSuccessorContactStatusMetricValues, } import com.digitalasset.canton.integration.util.TestUtils.waitForTargetTimeOnSequencer +import com.digitalasset.canton.logging.{LogEntry, SuppressionRule} import com.digitalasset.canton.metrics.{MetricsConfig, MetricsReporterConfig} import monocle.macros.syntax.lens.* +import org.slf4j.event.Level import java.time.Duration +/** Each sequencer attempts to contact its successor when it processes its own successor + * announcement. Upon successful contact, a metric should be changed. + */ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { override protected def testName: String = "lsu_sequencer_contact_successor" @@ -28,23 +33,27 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { registerPlugin( new UseBftSequencer( loggerFactory, - MultiSynchronizer.tryCreate(Set("sequencer1"), Set("sequencer2")), + MultiSynchronizer.tryCreate(Set("sequencer1", "sequencer2"), Set("sequencer3", "sequencer4")), ) ) registerPlugin(new UsePostgres(loggerFactory)) override protected lazy val newOldSequencers: Map[String, String] = Map( - "sequencer2" -> "sequencer1" + "sequencer3" -> "sequencer1", + "sequencer4" -> "sequencer2", + ) + override protected lazy val newOldMediators: Map[String, String] = Map( + "mediator3" -> "mediator1", + "mediator4" -> "mediator2", ) - override protected lazy val newOldMediators: Map[String, String] = Map("mediator2" -> "mediator1") override protected lazy val upgradeTime: CantonTimestamp = CantonTimestamp.Epoch.plusSeconds(30) override lazy val environmentDefinition: EnvironmentDefinition = - EnvironmentDefinition.P2S2M2_Config + EnvironmentDefinition.P2S4M4_Config .withNetworkBootstrap { implicit env => - new NetworkBootstrapper(S1M1) + new NetworkBootstrapper(S2M2) } .addConfigTransforms(configTransforms*) .addConfigTransforms( @@ -66,8 +75,11 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { private var fixture: Fixture = _ + private lazy val handshakeFailureWarn = + s"Unable to perform handshake with ${fixture.newPsid}" + "Sequencers" should { - "contact their successor and update metrics to reflect status" in { implicit env => + "have contact metrics set to 0 initially" in { implicit env => import env.* fixture = fixtureWithDefaults() @@ -84,11 +96,108 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { eventually() { getLsuSuccessorContactStatusMetricValues(sequencer1) shouldBe Map(fixture.newPsid -> 0) } + } + + "not update the metric if psid is incorrect" in { implicit env => + import env.* + + // Incorrect announcement of the successor: wrong psid + loggerFactory.assertEventuallyLogsSeq(SuppressionRule.Level(Level.WARN))( + sequencer1.topology.lsu.sequencer_successors.propose_successor( + sequencerId = sequencer1.id, + // announced successor is itself -> wrong psid + endpoints = sequencer1.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + successorSynchronizerId = fixture.newPsid, + ), + LogEntry.assertLogSeq( + Seq( + ( + _.warningMessage should include( + s"Error when contacting successor: expecting psid to be ${fixture.newPsid} but found ${fixture.currentPsid}" + ), + "warning on sequencer", + ), + ( + _.warningMessage should include( + "Connection internal-sequencer-connection-sequencer1-0: Invalid synchronizer" + ), + "connection pool warn on p1", + ), + ( + _.warningMessage should include( + "Connection internal-sequencer-connection-sequencer1-0: Invalid synchronizer" + ), + "connection pool warn on p2", + ), + ( + _.warningMessage should include(handshakeFailureWarn), + "handshake failure on p1", + ), + ( + _.warningMessage should include(handshakeFailureWarn), + "handshake failure on p2", + ), + ) + ), + ) + + // metric is not updated + getLsuSuccessorContactStatusMetricValues(sequencer1) shouldBe Map(fixture.newPsid -> 0) + } + + "not update the metric if sequencer id is incorrect" in { implicit env => + import env.* + + // Incorrect announcement of the successor: wrong sequencer id + loggerFactory.assertEventuallyLogsSeq(SuppressionRule.Level(Level.WARN))( + sequencer1.topology.lsu.sequencer_successors.propose_successor( + sequencerId = sequencer1.id, + // announced successor is another sequencer -> wrong sequencer id + endpoints = sequencer4.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + successorSynchronizerId = fixture.newPsid, + ), + LogEntry.assertLogSeq( + Seq( + ( + _.warningMessage should include( + s"Error when contacting successor: expecting sequencer id to be ${sequencer1.id} but found ${sequencer2.id}" + ), + "warning on sequencer", + ), + ( + _.warningMessage should include( + "Validation failure: Connection is not on expected sequencer" + ), + "connection pool warn on p1", + ), + ( + _.warningMessage should include( + "Validation failure: Connection is not on expected sequencer" + ), + "connection pool warn on p2", + ), + ( + _.warningMessage should include(handshakeFailureWarn), + "handshake failure on p1", + ), + ( + _.warningMessage should include(handshakeFailureWarn), + "handshake failure on p2", + ), + ) + ), + ) + + // metric is not updated + getLsuSuccessorContactStatusMetricValues(sequencer1) shouldBe Map(fixture.newPsid -> 0) + } + + "update the metric when the successor is correct" in { implicit env => + import env.* - // Announcement of the successor sequencer1.topology.lsu.sequencer_successors.propose_successor( sequencerId = sequencer1.id, - endpoints = sequencer2.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + endpoints = sequencer3.sequencerConnection.endpoints.map(_.toURI(useTls = false)), successorSynchronizerId = fixture.newPsid, ) @@ -103,7 +212,7 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { participants.all.forall(_.synchronizers.is_connected(fixture.newPsid)) shouldBe true } - waitForTargetTimeOnSequencer(sequencer2, environment.clock.now, logger) + waitForTargetTimeOnSequencer(sequencer3, environment.clock.now, logger) oldSynchronizerNodes.all.stop() participant1.health.ping(participant2) @@ -116,22 +225,22 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { val upgradeTime2 = fixture.upgradeTime.plusSeconds(30) val upgradeTime3 = upgradeTime2.plusSeconds(30) - sequencer2.topology.lsu.announcement.propose(psid2, upgradeTime2) + sequencer3.topology.lsu.announcement.propose(psid2, upgradeTime2) eventually() { - getLsuSuccessorContactStatusMetricValues(sequencer2) shouldBe Map(psid2 -> 0) + getLsuSuccessorContactStatusMetricValues(sequencer3) shouldBe Map(psid2 -> 0) } - sequencer2.topology.lsu.announcement.propose(psid3, upgradeTime3) + sequencer3.topology.lsu.announcement.propose(psid3, upgradeTime3) eventually() { - getLsuSuccessorContactStatusMetricValues(sequencer2) shouldBe Map(psid2 -> 0, psid3 -> 0) + getLsuSuccessorContactStatusMetricValues(sequencer3) shouldBe Map(psid2 -> 0, psid3 -> 0) } - sequencer2.topology.lsu.announcement.revoke(psid3, upgradeTime3) + sequencer3.topology.lsu.announcement.revoke(psid3, upgradeTime3) eventually() { - getLsuSuccessorContactStatusMetricValues(sequencer2) shouldBe Map(psid2 -> 0, psid3 -> -1) + getLsuSuccessorContactStatusMetricValues(sequencer3) shouldBe Map(psid2 -> 0, psid3 -> -1) } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSessionSigningKeysIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSessionSigningKeysIntegrationTest.scala index e1f4cb1bf5..f2aa2ef39c 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSessionSigningKeysIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSessionSigningKeysIntegrationTest.scala @@ -53,7 +53,7 @@ final class LsuSessionSigningKeysIntegrationTest override protected def configTransforms: Seq[ConfigTransform] = super.configTransforms :+ ConfigTransforms.setSigningKeysIfPV35OrHigher( - SessionSigningKeysConfig.default + SessionSigningKeysConfig.enabled ) override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1S2M2_Config diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuTrafficTransferRestartIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuTrafficTransferRestartIntegrationTest.scala index 023278f24f..bc76f6a23d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuTrafficTransferRestartIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuTrafficTransferRestartIntegrationTest.scala @@ -25,7 +25,8 @@ import java.time.Duration * * Topology: * - P1 connected to S1 - * - P2 connected to S2 + * - P2 connected to S1 and S2 with threshold=2 + * - P3 connected to S2, offboarded before the LSU * * This test: * - Generates purchased/consumed traffic by performing some activity on the predecessor @@ -74,7 +75,9 @@ final class LsuTrafficTransferRestartIntegrationTest extends LsuBase with Traffi changeDynamicSynchronizerParameters = false, ) participant1.synchronizers.connect_by_config(synchronizerConnectionConfig(sequencer1)) - participant2.synchronizers.connect_by_config(synchronizerConnectionConfig(sequencer2)) + participant2.synchronizers.connect_by_config( + synchronizerConnectionConfig(Seq(sequencer1, sequencer2), 2) + ) participant3.synchronizers.connect_by_config(synchronizerConnectionConfig(sequencer2)) participants.all.dars.upload(CantonExamplesPath) @@ -143,6 +146,11 @@ final class LsuTrafficTransferRestartIntegrationTest extends LsuBase with Traffi } } + /* + Regression test. + This ensures that checks about the number of traffic entries are consistent + between export and import. + */ "offboard participant3" in { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/version/MultipleProtocolVersionReassignmentIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/version/MultipleProtocolVersionReassignmentIntegrationTest.scala index ac03f18445..d3c27a9299 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/version/MultipleProtocolVersionReassignmentIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/version/MultipleProtocolVersionReassignmentIntegrationTest.scala @@ -51,7 +51,7 @@ sealed trait MultipleProtocolVersionReassignmentIntegrationTest _.focus(_.parameters.minimumProtocolVersion) .replace(Some(ParticipantProtocolVersion(beforeLastStable))) }, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .addConfigTransforms(ConfigTransforms.dontWarnOnDeprecatedPV*) .withSetup { implicit env => diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala index 43ab712b3f..a54dd65974 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala @@ -171,7 +171,7 @@ class PartyToParticipantDeclarative( Map[PhysicalSynchronizerId, PartyHostingState], ], onboarding: Boolean, -)(implicit executionContext: ExecutionContext, env: TestEnvironment) +)(implicit executionContext: ExecutionContext, env: TestEnvironment[?]) extends PartyToParticipantDeclarativeCommon[Party] { override protected def partyReference: (PartyToParticipant, HashingSchemeVersion) => Party = @@ -417,7 +417,7 @@ object PartyToParticipantDeclarative { ], forceFlags: ForceFlags = ForceFlags.none, onboarding: Boolean = false, // participants added in target topology are marked as onboarding - )(implicit executionContext: ExecutionContext, env: TestEnvironment): Unit = { + )(implicit executionContext: ExecutionContext, env: CantonTestEnvironment): Unit = { val participantReference = participants.headOption.getOrElse( fail("No participant set in PartyToParticipantDeclarative") ) @@ -466,7 +466,7 @@ object PartyToParticipantDeclarative { threshold: PositiveInt, hosting: Set[(ParticipantId, ParticipantPermission)], forceFlags: ForceFlags = ForceFlags.none, - )(implicit executionContext: ExecutionContext, env: TestEnvironment): Unit = + )(implicit executionContext: ExecutionContext, env: CantonTestEnvironment): Unit = apply(participants, Set(synchronizerId))( Map(party.partyId -> owningParticipant), Map(party -> Map(synchronizerId -> (threshold, hosting))), @@ -496,7 +496,7 @@ class PartiesAllocator( )( newParties: Seq[(String, ParticipantId)], val targetTopology: Map[String, Map[PhysicalSynchronizerId, PartyHostingState]], -)(implicit executionContext: ExecutionContext, env: TestEnvironment) +)(implicit executionContext: ExecutionContext, env: TestEnvironment[?]) extends PartyToParticipantDeclarativeCommon[String] { override def externalParties: Set[ExternalParty] = Set.empty @@ -631,7 +631,7 @@ object PartiesAllocator { ], )(implicit executionContext: ExecutionContext, - env: TestEnvironment, + env: TestEnvironment[?], partyKind: PartyKind, ): Seq[Party] = new PartiesAllocator(participants)( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/TestSubmissionService.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/TestSubmissionService.scala index 9bed31e4e3..015dd54c3c 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/TestSubmissionService.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/TestSubmissionService.scala @@ -355,7 +355,7 @@ class TestSubmissionService( response: Response = contractOpt match { case Some(contract) => - Response.ContractFound(contract, Hash.HashingMethod.UpgradeFriendly, _ => true) + Response.ContractFound(contract, Hash.HashingMethod.TypedNormalForm, _ => true) case None => Response.ContractNotFound } @@ -378,7 +378,22 @@ class TestSubmissionService( for { cidO <- keyResolver.resolveKey(key)(traceContext) contracts <- cidO.toList.parTraverse(contractResolver(_)(traceContext)) - r <- resolve(resume(contracts.flatten.toVector, NeedKeyProgression.Finished)) + r <- resolve( + resume( + ResultNeedKey.Response( + contracts.flatten + .map(fci => + ResultNeedKey.Response.AuthenticableFatContractInstance( + fci, + Hash.HashingMethod.TypedNormalForm, + _ => true, + ) + ) + .toVector, + NeedKeyProgression.Finished, + ) + ) + ) } yield r case ResultInterruption(continue, _) => diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/util/ReleaseUtils.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/util/ReleaseUtils.scala index 2eda2caf52..3bfce40700 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/util/ReleaseUtils.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/util/ReleaseUtils.scala @@ -15,9 +15,11 @@ import com.digitalasset.canton.version.{ ProtocolVersionCompatibility, ReleaseVersion, } +import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import java.nio.file.{Files, Paths} import scala.collection.concurrent.TrieMap +import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ExecutionContext, Future} /** A collection of small utilities for tests that have no obvious home */ @@ -119,6 +121,8 @@ object ReleaseUtils { private val releasesRetrieval: TrieMap[ReleaseVersion, Future[String]] = TrieMap.empty private val lock = new Mutex() + val DefaultReleaseDownloadTimeout: FiniteDuration = 5.minutes + /** If the .tar.gz corresponding to release is not found locally, attempts to download it from * artifactory. Then, extract the .tar.gz file. * @param release diff --git a/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto b/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto index ffe2febce4..3a9cfeda96 100644 --- a/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto +++ b/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto @@ -67,7 +67,7 @@ message Enums { // in model conformance in PV 33 PARTICIPANT_FEATURE_FLAG_PV33_EXTERNAL_SIGNING_LOCAL_CONTRACT_IN_SUBVIEW = 1; // This flag indicates that the participant supports reassignments between synchronizers. - PARTICIPANT_FEATURE_FLAG_ENABLE_ALPHA_MULTI_SYNCHRONIZER = 2; + PARTICIPANT_FEATURE_FLAG_ENABLE_MULTI_SYNCHRONIZER = 2; } } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/AuthServiceConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/AuthServiceConfig.scala index eeaedaf311..eeed16b3af 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/AuthServiceConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/AuthServiceConfig.scala @@ -153,6 +153,7 @@ object AuthServiceConfig { override val privileged: Boolean = false, accessLevel: AccessLevel = AccessLevel.Wildcard, override val users: Seq[AuthorizedUser] = Seq.empty, + override val maxTokenLife: config.NonNegativeDuration = NonNegativeDuration(Duration.Inf), ) extends AuthServiceConfig { @SuppressWarnings(Array("org.wartremover.warts.Null")) private def verifier( @@ -198,6 +199,7 @@ object AuthServiceConfig { override val privileged: Boolean = false, accessLevel: AccessLevel = AccessLevel.Wildcard, override val users: Seq[AuthorizedUser] = Seq.empty, + override val maxTokenLife: config.NonNegativeDuration = NonNegativeDuration(Duration.Inf), ) extends AuthServiceConfig { @SuppressWarnings(Array("org.wartremover.warts.Null")) private def verifier( @@ -243,6 +245,7 @@ object AuthServiceConfig { override val privileged: Boolean = false, accessLevel: AccessLevel = AccessLevel.Wildcard, override val users: Seq[AuthorizedUser] = Seq.empty, + override val maxTokenLife: config.NonNegativeDuration = NonNegativeDuration(Duration.Inf), ) extends AuthServiceConfig { private def verifier( jwksCacheConfig: JwksCacheConfig, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/BaseCantonConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/BaseCantonConfig.scala index 1f9cfbfb0d..f57273748a 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/BaseCantonConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/BaseCantonConfig.scala @@ -31,6 +31,9 @@ object BaseCantonConfig { lazy implicit final val connectionAllocationReader: ConfigReader[ConnectionAllocation] = deriveReader[ConnectionAllocation] + lazy implicit final val partitionConfigReader: ConfigReader[PartitionConfig] = + deriveReader[PartitionConfig] + lazy implicit final val dbParamsReader: ConfigReader[DbParametersConfig] = deriveReader[DbParametersConfig] @@ -57,6 +60,9 @@ object BaseCantonConfig { lazy implicit final val connectionAllocationWriter: ConfigWriter[ConnectionAllocation] = deriveWriter[ConnectionAllocation] + lazy implicit final val partitionConfigWriter: ConfigWriter[PartitionConfig] = + deriveWriter[PartitionConfig] + lazy implicit final val dbParamsWriter: ConfigWriter[DbParametersConfig] = deriveWriter[DbParametersConfig] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/SessionSigningKeysConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/SessionSigningKeysConfig.scala index db71c66ba7..68fd51ac1f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/SessionSigningKeysConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/SessionSigningKeysConfig.scala @@ -97,7 +97,7 @@ final case class SessionSigningKeysConfig( object SessionSigningKeysConfig { val disabled: SessionSigningKeysConfig = SessionSigningKeysConfig(enabled = false) - val default: SessionSigningKeysConfig = SessionSigningKeysConfig(enabled = true) + val enabled: SessionSigningKeysConfig = SessionSigningKeysConfig(enabled = true) /** Short test-only configuration: durations are small enough to trigger key rotation and validity * edge cases within a test. diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala index e263efee74..8b602ff0e7 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala @@ -73,6 +73,7 @@ final case class DbParametersConfig( // Make the default settings a part of repeatable migrations repeatableMigrationsPaths: Seq[String] = Seq.empty, + partitions: PartitionConfig = PartitionConfig(), ) extends PrettyPrinting { override protected def pretty: Pretty[DbParametersConfig] = prettyOfClass( @@ -203,6 +204,13 @@ final case class ConnectionAllocation( ) } +/** @param initialBftOrdererTablesPartitionSize + * Initial partition size for bft-orderer tables. Note that this config is only read once, during + * the initial database setup and later changes to this value won't have any effect. This is also + * only used in Postgres setups. + */ +final case class PartitionConfig(initialBftOrdererTablesPartitionSize: Int = 1500) + object DbParametersConfig { private val defaultWarnOnSlowQueryInterval: PositiveFiniteDuration = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/TestingConfigInternal.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/TestingConfigInternal.scala index 84a27d9162..f7df89d3c9 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/TestingConfigInternal.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/TestingConfigInternal.scala @@ -58,9 +58,10 @@ import com.digitalasset.canton.metrics.MetricsFactoryType.External * @param useLegacyContractIdVersionV11 * Uses contract id version V11 for testing purposes. * @param warnOnJwtScopeUsage - * When true, we log a warning on the first time a JWT token with scope is sent to the Ledger API - * This pattern is currently discouraged for security reasons but will be re-enabled in future - * versions. + * When true, we log a warning on the first time a JWT with a scope but no audience is sent to + * the Ledger API, provided that no explicit targetAudience or targetScope is configured. (In + * other cases, other appropriate warnings are raised on startup if needed). Such tokens are + * currently discouraged for security reasons and will be removed from use in future versions. */ final case class TestingConfigInternal( testSequencerClientFor: Set[TestSequencerClientFor] = Set.empty, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/CryptoApi.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/CryptoApi.scala index 5cce8ef7ac..3b1d6377eb 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/CryptoApi.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/CryptoApi.scala @@ -44,6 +44,7 @@ import com.digitalasset.canton.health.{ import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, LifeCycle} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.CryptoMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.Storage @@ -76,6 +77,7 @@ sealed trait BaseCrypto extends NamedLogging { def privateCrypto: CryptoPrivateApi def cryptoPrivateStore: CryptoPrivateStore def cryptoPublicStore: CryptoPublicStore + def cryptoMetrics: CryptoMetrics /** Helper method to generate a new signing key pair and store the public key in the public store * as well. @@ -116,6 +118,7 @@ class Crypto private[crypto] ( override val privateCrypto: CryptoPrivateApi, override val cryptoPrivateStore: CryptoPrivateStore, override val cryptoPublicStore: CryptoPublicStore, + override val cryptoMetrics: CryptoMetrics, override val timeouts: ProcessingTimeout, override val loggerFactory: NamedLoggerFactory, )(override implicit val ec: ExecutionContext) @@ -155,12 +158,15 @@ final case class SynchronizerCrypto( new SynchronizerCryptoPrivateApi( staticSynchronizerParameters, crypto.privateCrypto, + crypto.cryptoMetrics.signingMetrics, + crypto.cryptoMetrics.decryptionMetrics, crypto.timeouts, crypto.loggerFactory, ) override val cryptoPrivateStore: CryptoPrivateStore = crypto.cryptoPrivateStore override val cryptoPublicStore: CryptoPublicStore = crypto.cryptoPublicStore + override val cryptoMetrics: CryptoMetrics = crypto.cryptoMetrics override protected val loggerFactory: NamedLoggerFactory = crypto.loggerFactory } @@ -385,6 +391,7 @@ object Crypto { releaseProtocolVersion: ReleaseProtocolVersion, futureSupervisor: FutureSupervisor, clock: Clock, + cryptoMetrics: CryptoMetrics, executionContext: ExecutionContext, timeouts: ProcessingTimeout, batchingConfig: BatchingConfig, @@ -431,6 +438,7 @@ object Crypto { publicKeyConversionCacheConfig, cryptoPrivateStore, cryptoPublicStore, + cryptoMetrics, timeouts, loggerFactory, ) @@ -478,6 +486,7 @@ object Crypto { kmsSchemes.encryptionSchemes, cryptoPublicStore, kmsCryptoPrivateStore, + cryptoMetrics, timeouts, loggerFactory, ) @@ -488,6 +497,7 @@ object Crypto { sessionEncryptionKeyCacheConfig, publicKeyConversionCacheConfig, cryptoSchemes, + cryptoMetrics, loggerFactory, ) .toEitherT[FutureUnlessShutdown] @@ -496,6 +506,7 @@ object Crypto { kmsPrivateCrypto, kmsCryptoPrivateStore, cryptoPublicStore, + cryptoMetrics, timeouts, loggerFactory, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Encryption.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Encryption.scala index c496c6e69e..b0ffe0ec01 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Encryption.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Encryption.scala @@ -16,6 +16,7 @@ import com.digitalasset.canton.crypto.store.{CryptoPrivateStoreError, CryptoPriv import com.digitalasset.canton.error.{CantonBaseError, CantonErrorGroups} import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} +import com.digitalasset.canton.metrics.DecryptionMetrics import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.{ CryptoParseAndValidationError, @@ -36,14 +37,7 @@ import scala.concurrent.ExecutionContext /** Encryption operations that do not require access to a private key store but operates with * provided keys. */ -trait EncryptionOps { - - private[crypto] def decryptWithInternal[M]( - encrypted: AsymmetricEncrypted[M], - privateKey: EncryptionPrivateKey, - )( - deserialize: ByteString => Either[DeserializationError, M] - ): Either[DecryptionError, M] +trait EncryptionOps extends DecryptionMetricsSupport { def defaultSymmetricKeyScheme: SymmetricKeyScheme @@ -76,11 +70,6 @@ trait EncryptionOps { encryptionAlgorithmSpec: EncryptionAlgorithmSpec = encryptionAlgorithmSpecs.default, )(implicit traceContext: TraceContext): Either[EncryptionError, AsymmetricEncrypted[M]] - /** Decrypts a message encrypted using `encryptWith` */ - def decryptWith[M](encrypted: AsymmetricEncrypted[M], privateKey: EncryptionPrivateKey)( - deserialize: ByteString => Either[DeserializationError, M] - ): Either[DecryptionError, M] = decryptWithInternal(encrypted, privateKey)(deserialize) - /** Encrypts the bytes of the serialized message using the given symmetric key. Where the message * embedded protocol version determines the message serialization. */ @@ -97,6 +86,24 @@ trait EncryptionOps { symmetricKey: SymmetricKey, ): Either[EncryptionError, ByteString] + /** Decrypts a message encrypted using `encryptWith`. Records latency for the decryption + * operation. + */ + def decryptWith[M](encrypted: AsymmetricEncrypted[M], privateKey: EncryptionPrivateKey)( + deserialize: ByteString => Either[DeserializationError, M] + ): Either[DecryptionError, M] = + decryptionMetrics.decryptLatency.time(decryptWithInternal(encrypted, privateKey)(deserialize)) + + /** Internal decryption primitive implemented by concrete backends. This bypasses higher-level + * wrappers (e.g. metrics and validation) and should only be used by internal decryption logic. + */ + private[crypto] def decryptWithInternal[M]( + encrypted: AsymmetricEncrypted[M], + privateKey: EncryptionPrivateKey, + )( + deserialize: ByteString => Either[DeserializationError, M] + ): Either[DecryptionError, M] + /** Decrypts a message encrypted using `encryptWith` */ def decryptWith[M](encrypted: Encrypted[M], symmetricKey: SymmetricKey)( deserialize: ByteString => Either[DeserializationError, M] @@ -105,17 +112,10 @@ trait EncryptionOps { } /** Encryption operations that require access to stored private keys. */ -trait EncryptionPrivateOps { +trait EncryptionPrivateOps extends DecryptionMetricsSupport { def encryptionSchemes: EncryptionCryptoSchemes - /** Decrypts an encrypted message using the referenced private encryption key */ - def decrypt[M](encrypted: AsymmetricEncrypted[M])( - deserialize: ByteString => Either[DeserializationError, M] - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, DecryptionError, M] - /** Generates a new encryption key pair with the given scheme and optional name, stores the * private key and returns the public key. */ @@ -125,6 +125,29 @@ trait EncryptionPrivateOps { )(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, EncryptionKeyGenerationError, EncryptionPublicKey] + + /** Decrypts an encrypted message using the referenced private encryption key. Records latency for + * the decryption operation. + */ + def decrypt[M](encrypted: AsymmetricEncrypted[M])( + deserialize: ByteString => Either[DeserializationError, M] + )(implicit + executionContext: ExecutionContext, + traceContext: TraceContext, + ): EitherT[FutureUnlessShutdown, DecryptionError, M] = + EitherTUtil.timed(decryptionMetrics.decryptLatency)( + decryptInternal(encrypted)(deserialize) + ) + + /** Internal decryption primitive implemented by concrete backends. This bypasses higher-level + * wrappers (e.g. metrics and validation) and should only be used by internal decryption logic. + */ + private[crypto] def decryptInternal[M](encrypted: AsymmetricEncrypted[M])( + deserialize: ByteString => Either[DeserializationError, M] + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, DecryptionError, M] + } /** A default implementation with a private key store */ @@ -137,7 +160,7 @@ trait EncryptionPrivateStoreOps extends EncryptionPrivateOps { protected val encryptionOps: EncryptionOps /** Decrypts an encrypted message using the referenced private encryption key */ - override def decrypt[M](encryptedMessage: AsymmetricEncrypted[M])( + override private[crypto] def decryptInternal[M](encryptedMessage: AsymmetricEncrypted[M])( deserialize: ByteString => Either[DeserializationError, M] )(implicit tc: TraceContext): EitherT[FutureUnlessShutdown, DecryptionError, M] = store @@ -170,6 +193,11 @@ trait EncryptionPrivateStoreOps extends EncryptionPrivateOps { } +/** Provides decryption-related metrics. */ +trait DecryptionMetricsSupport { + def decryptionMetrics: DecryptionMetrics +} + /** A tag to denote encrypted data. */ final case class Encrypted[+M] private[crypto] (ciphertext: ByteString) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala index bb612bd011..4ddacd2401 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala @@ -23,6 +23,7 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.error.{CantonBaseError, CantonErrorGroups} import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} +import com.digitalasset.canton.metrics.SigningMetrics import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.{ CryptoParseAndValidationError, @@ -33,7 +34,7 @@ import com.digitalasset.canton.serialization.{ import com.digitalasset.canton.store.db.DbDeserializationException import com.digitalasset.canton.topology.{Member, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.EitherUtil +import com.digitalasset.canton.util.{EitherTUtil, EitherUtil} import com.digitalasset.canton.version.* import com.google.common.annotations.VisibleForTesting import com.google.protobuf.ByteString @@ -50,7 +51,7 @@ import scala.concurrent.ExecutionContext /** Signing operations that do not require access to a private key store but operates with provided * keys. */ -trait SigningOps { +trait SigningOps extends SigningMetricsSupport { def signatureVerificationParallelism: PositiveInt @@ -69,14 +70,32 @@ trait SigningOps { usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, )(implicit traceContext: TraceContext): Either[SigningError, Signature] = - signBytes(hash.getCryptographicEvidence, signingKey, usage, signingAlgorithmSpec) + signingMetrics.signingLatency.time( + signBytesInternal(hash.getCryptographicEvidence, signingKey, usage, signingAlgorithmSpec) + ) - /** Preferably, we sign a hash; however, we also allow signing arbitrary bytes when necessary. */ + /** Signs raw bytes using the private signing key. Convenience wrapper used when signing + * non-hashed data. + */ protected[crypto] def signBytes( bytes: ByteString, signingKey: SigningPrivateKey, usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, + )(implicit traceContext: TraceContext): Either[SigningError, Signature] = + signingMetrics.signingLatency.time( + signBytesInternal(bytes, signingKey, usage, signingAlgorithmSpec) + ) + + /** Internal signing primitive implemented by concrete backends. Performs the actual cryptographic + * signing of raw bytes. This bypasses higher-level wrappers (e.g. metrics and validation) and + * should only be used by internal signing logic. + */ + private[crypto] def signBytesInternal( + bytes: ByteString, + signingKey: SigningPrivateKey, + usage: NonEmpty[Set[SigningKeyUsage]], + signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, )(implicit traceContext: TraceContext): Either[SigningError, Signature] /** Confirms if the provided signature is a valid signature of the payload using the public key */ @@ -97,27 +116,51 @@ trait SigningOps { } /** Signing operations that require access to stored private keys. */ -trait SigningPrivateOps { +trait SigningPrivateOps extends SigningMetricsSupport { def signingSchemes: SigningCryptoSchemes - /** Signs the given hash using the referenced private signing key. */ + /** Signs the given hash using the referenced private signing key. Latency of the signing + * operation is recorded for all outcomes (successful signatures and signing failures). + */ def sign( hash: Hash, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingSchemes.algorithmSpecs.default, )(implicit - tc: TraceContext + ec: ExecutionContext, + tc: TraceContext, ): EitherT[FutureUnlessShutdown, SigningError, Signature] = - signBytes(hash.getCryptographicEvidence, signingKeyId, usage, signingAlgorithmSpec) + EitherTUtil.timed(signingMetrics.signingLatency)( + signBytesInternal(hash.getCryptographicEvidence, signingKeyId, usage, signingAlgorithmSpec) + ) - /** Signs the byte string directly, however it is encouraged to sign a hash. */ + /** Signs the byte string directly, however it is encouraged to sign a hash. Latency of the + * signing operation is recorded for all outcomes (successful signatures and signing failures). + */ def signBytes( bytes: ByteString, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingSchemes.algorithmSpecs.default, + )(implicit + ec: ExecutionContext, + tc: TraceContext, + ): EitherT[FutureUnlessShutdown, SigningError, Signature] = + EitherTUtil.timed(signingMetrics.signingLatency)( + signBytesInternal(bytes, signingKeyId, usage, signingAlgorithmSpec) + ) + + /** Internal signing primitive that produces a signature for the given bytes. This bypasses + * higher-level wrappers (e.g. metrics and validation) and should only be used by internal + * signing logic. + */ + private[crypto] def signBytesInternal( + bytes: ByteString, + signingKeyId: Fingerprint, + usage: NonEmpty[Set[SigningKeyUsage]], + signingAlgorithmSpec: SigningAlgorithmSpec = signingSchemes.algorithmSpecs.default, )(implicit tc: TraceContext): EitherT[FutureUnlessShutdown, SigningError, Signature] /** Generates a new signing key pair with the given scheme and optional name, stores the private @@ -133,6 +176,11 @@ trait SigningPrivateOps { } +/** Provides signing-related metrics. */ +trait SigningMetricsSupport { + def signingMetrics: SigningMetrics +} + /** A default implementation with a private key store */ trait SigningPrivateStoreOps extends SigningPrivateOps { @@ -142,7 +190,7 @@ trait SigningPrivateStoreOps extends SigningPrivateOps { protected val signingOps: SigningOps - override def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SyncCryptoApiParticipantProvider.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SyncCryptoApiParticipantProvider.scala index 73dc377cf8..a661111b2e 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SyncCryptoApiParticipantProvider.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SyncCryptoApiParticipantProvider.scala @@ -19,7 +19,7 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, LifeCycle} import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.KmsMetrics +import com.digitalasset.canton.metrics.CryptoMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.serialization.DeserializationError import com.digitalasset.canton.topology.* @@ -54,7 +54,7 @@ class SyncCryptoApiParticipantProvider( val ips: IdentityProvidingServiceClient, val crypto: Crypto, cryptoConfig: CryptoConfig, - kmsMetrics: Option[KmsMetrics], + cryptoMetrics: CryptoMetrics, publicKeyConversionCacheConfig: CacheConfig, timeouts: ProcessingTimeout, futureSupervisor: FutureSupervisor, @@ -93,7 +93,7 @@ class SyncCryptoApiParticipantProvider( staticSynchronizerParameters, SynchronizerCrypto(crypto, staticSynchronizerParameters), cryptoConfig, - kmsMetrics, + cryptoMetrics, publicKeyConversionCacheConfig, timeouts, futureSupervisor, @@ -426,7 +426,7 @@ object SynchronizerCryptoClient { staticSynchronizerParameters: StaticSynchronizerParameters, synchronizerCrypto: SynchronizerCrypto, cryptoConfig: CryptoConfig, - kmsMetrics: Option[KmsMetrics], + cryptoMetrics: CryptoMetrics, publicKeyConversionCacheConfig: CacheConfig, timeouts: ProcessingTimeout, futureSupervisor: FutureSupervisor, @@ -440,7 +440,7 @@ object SynchronizerCryptoClient { member, synchronizerCrypto, cryptoConfig, - kmsMetrics, + cryptoMetrics, publicKeyConversionCacheConfig, futureSupervisor, timeouts, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPrivateApi.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPrivateApi.scala index 005b8873e6..71abde8dd8 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPrivateApi.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPrivateApi.scala @@ -10,6 +10,7 @@ import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.health.ComponentHealthState import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.serialization.DeserializationError import com.digitalasset.canton.tracing.TraceContext @@ -26,6 +27,8 @@ import scala.concurrent.ExecutionContext final class SynchronizerCryptoPrivateApi( override val staticSynchronizerParameters: StaticSynchronizerParameters, privateCrypto: CryptoPrivateApi, + override val signingMetrics: SigningMetrics, + override val decryptionMetrics: DecryptionMetrics, override protected val timeouts: ProcessingTimeout, override protected val loggerFactory: NamedLoggerFactory, )(implicit executionContext: ExecutionContext) @@ -36,7 +39,7 @@ final class SynchronizerCryptoPrivateApi( override private[crypto] def getInitialHealthState: ComponentHealthState = privateCrypto.getInitialHealthState - override def decrypt[M]( + override private[crypto] def decryptInternal[M]( encrypted: AsymmetricEncrypted[M] )( deserialize: ByteString => Either[DeserializationError, M] @@ -67,7 +70,7 @@ final class SynchronizerCryptoPrivateApi( override def signingSchemes: SigningCryptoSchemes = privateCrypto.signingSchemes - override def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPureApi.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPureApi.scala index ad04c3ac76..fbf8d6ba37 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPureApi.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPureApi.scala @@ -5,6 +5,7 @@ package com.digitalasset.canton.crypto import com.daml.nonempty.NonEmpty import com.digitalasset.canton.config.RequireTypes.PositiveInt +import com.digitalasset.canton.metrics.{DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.serialization.DeserializationError import com.digitalasset.canton.tracing.TraceContext @@ -111,6 +112,8 @@ final class SynchronizerCryptoPureApi( symmetricKey: SymmetricKey, ): Either[EncryptionError, ByteString] = pureCrypto.encryptSymmetricWith(data, symmetricKey) + override def decryptionMetrics: DecryptionMetrics = pureCrypto.decryptionMetrics + override def decryptWith[M]( encrypted: Encrypted[M], symmetricKey: SymmetricKey, @@ -145,11 +148,14 @@ final class SynchronizerCryptoPureApi( override def signingAlgorithmSpecs: CryptoScheme[SigningAlgorithmSpec] = pureCrypto.signingAlgorithmSpecs - override protected[crypto] def signBytes( + override def signingMetrics: SigningMetrics = pureCrypto.signingMetrics + + override private[crypto] def signBytesInternal( bytes: ByteString, signingKey: SigningPrivateKey, usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, )(implicit traceContext: TraceContext): Either[SigningError, Signature] = pureCrypto.signBytes(bytes, signingKey, usage, signingAlgorithmSpec) + } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JceCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JceCrypto.scala index c71fd873d6..979ccdb7f6 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JceCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JceCrypto.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.config.{ import com.digitalasset.canton.crypto.store.{CryptoPrivateStore, CryptoPublicStore} import com.digitalasset.canton.crypto.{Crypto, CryptoSchemes} import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.metrics.CryptoMetrics import com.digitalasset.canton.util.EitherUtil import scala.concurrent.ExecutionContext @@ -26,6 +27,7 @@ object JceCrypto { publicKeyConversionCacheConfig: CacheConfig, cryptoPrivateStore: CryptoPrivateStore, cryptoPublicStore: CryptoPublicStore, + cryptoMetrics: CryptoMetrics, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, )(implicit @@ -46,6 +48,7 @@ object JceCrypto { sessionEncryptionKeyCacheConfig, publicKeyConversionCacheConfig, cryptoSchemes, + cryptoMetrics, loggerFactory, ) privateCrypto = @@ -54,6 +57,8 @@ object JceCrypto { signingSchemes = cryptoSchemes.signingSchemes, encryptionSchemes = cryptoSchemes.encryptionSchemes, store = cryptoPrivateStoreExtended, + signingMetrics = cryptoMetrics.signingMetrics, + decryptionMetrics = cryptoMetrics.decryptionMetrics, timeouts = timeouts, loggerFactory = loggerFactory, ) @@ -62,6 +67,7 @@ object JceCrypto { privateCrypto, cryptoPrivateStore, cryptoPublicStore, + cryptoMetrics, timeouts, loggerFactory, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePrivateCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePrivateCrypto.scala index 16c16581f3..3492ed9b0d 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePrivateCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePrivateCrypto.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.crypto.store.CryptoPrivateStoreExtended import com.digitalasset.canton.health.ComponentHealthState import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.tracing.TraceContext import com.google.protobuf.ByteString import org.bouncycastle.asn1.DEROctetString @@ -45,6 +46,8 @@ class JcePrivateCrypto( override val signingSchemes: SigningCryptoSchemes, override val encryptionSchemes: EncryptionCryptoSchemes, override protected val store: CryptoPrivateStoreExtended, + override val signingMetrics: SigningMetrics, + override val decryptionMetrics: DecryptionMetrics, override protected val timeouts: ProcessingTimeout, override protected val loggerFactory: NamedLoggerFactory, )(override implicit val ec: ExecutionContext) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala index 5e4a97bf9d..d816feda33 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala @@ -22,6 +22,7 @@ import com.digitalasset.canton.crypto.HmacError.{ import com.digitalasset.canton.crypto.deterministic.encryption.DeterministicRandom import com.digitalasset.canton.crypto.{SignatureCheckError, *} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{CryptoMetrics, DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.serialization.{ DefaultDeserializationError, DeserializationError, @@ -90,6 +91,8 @@ class JcePureCrypto( publicKeyConversionCacheConfig: CacheConfig, privateKeyConversionCacheTtl: Option[FiniteDuration], override val signatureVerificationParallelism: PositiveInt, + override val signingMetrics: SigningMetrics, + override val decryptionMetrics: DecryptionMetrics, override val loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext) extends CryptoPureApi @@ -318,7 +321,7 @@ class JcePureCrypto( SymmetricKey.create(CryptoKeyFormat.Raw, bytes.unwrap, scheme) } - override def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKey: SigningPrivateKey, usage: NonEmpty[Set[SigningKeyUsage]], @@ -841,6 +844,13 @@ class JcePureCrypto( } } + override def signBytes( + bytes: ByteString, + signingKey: SigningPrivateKey, + usage: NonEmpty[Set[SigningKeyUsage]], + signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, + )(implicit traceContext: TraceContext): Either[SigningError, Signature] = + super.signBytes(bytes, signingKey, usage, signingAlgorithmSpec) } object JcePureCrypto { @@ -850,6 +860,7 @@ object JcePureCrypto { sessionEncryptionKeyCacheConfig: SessionEncryptionKeyCacheConfig, publicKeyConversionCacheConfig: CacheConfig, cryptoSchemes: CryptoSchemes, + cryptoMetrics: CryptoMetrics, loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext): Either[String, JcePureCrypto] = { @@ -889,6 +900,8 @@ object JcePureCrypto { publicKeyConversionCacheConfig = publicKeyConversionCacheConfig, privateKeyConversionCacheTtl = minimumPrivateKeyCacheDuration, signatureVerificationParallelism = config.parallelism.signatureVerificationParallelism, + signingMetrics = cryptoMetrics.signingMetrics, + decryptionMetrics = cryptoMetrics.decryptionMetrics, loggerFactory = loggerFactory, ) } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/kms/KmsPrivateCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/kms/KmsPrivateCrypto.scala index cfc2619b73..6675366855 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/kms/KmsPrivateCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/kms/KmsPrivateCrypto.scala @@ -24,6 +24,7 @@ import com.digitalasset.canton.health.{ } import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{CryptoMetrics, DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.serialization.DeserializationError import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.{ByteString256, ByteString4096} @@ -37,6 +38,8 @@ class KmsPrivateCrypto( private[kms] val publicStore: CryptoPublicStore, override val signingSchemes: SigningCryptoSchemes, override val encryptionSchemes: EncryptionCryptoSchemes, + override val signingMetrics: SigningMetrics, + override val decryptionMetrics: DecryptionMetrics, override protected val timeouts: ProcessingTimeout, override protected val loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext) @@ -137,7 +140,7 @@ class KmsPrivateCrypto( ) } yield publicKey - def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], @@ -268,7 +271,7 @@ class KmsPrivateCrypto( _ = privateStore.storeKeyMetadata(KmsMetadata(publicKey.id, keyId, KeyPurpose.Encryption)) } yield publicKey - override def decrypt[M](encrypted: AsymmetricEncrypted[M])( + override private[crypto] def decryptInternal[M](encrypted: AsymmetricEncrypted[M])( deserialize: ByteString => Either[DeserializationError, M] )(implicit tc: TraceContext): EitherT[FutureUnlessShutdown, DecryptionError, M] = for { @@ -324,6 +327,7 @@ object KmsPrivateCrypto { encryptionSchemes: EncryptionCryptoSchemes, cryptoPublicStore: CryptoPublicStore, kmsCryptoPrivateStore: KmsCryptoPrivateStore, + cryptoMetrics: CryptoMetrics, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, )(implicit executionContext: ExecutionContext): KmsPrivateCrypto = @@ -333,6 +337,8 @@ object KmsPrivateCrypto { cryptoPublicStore, signingSchemes, encryptionSchemes, + cryptoMetrics.signingMetrics, + cryptoMetrics.decryptionMetrics, timeouts, loggerFactory, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSigner.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSigner.scala index d0cf61e031..d04f9b4806 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSigner.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSigner.scala @@ -24,7 +24,7 @@ import com.digitalasset.canton.crypto.{ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.KmsMetrics +import com.digitalasset.canton.metrics.CryptoMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.sequencing.client.SequencerClientConfig import com.digitalasset.canton.time.Clock @@ -108,7 +108,7 @@ object SyncCryptoSigner { member: Member, crypto: SynchronizerCrypto, cryptoConfig: CryptoConfig, - kmsMetrics: Option[KmsMetrics], + cryptoMetrics: CryptoMetrics, publicKeyConversionCacheConfig: CacheConfig, futureSupervisor: FutureSupervisor, timeouts: ProcessingTimeout, @@ -130,7 +130,7 @@ object SyncCryptoSigner { staticSynchronizerParameters, member, crypto.privateCrypto, - kmsMetrics, + cryptoMetrics, crypto.cryptoPrivateStore, cryptoConfig.sessionSigningKeys, publicKeyConversionCacheConfig, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSignerWithSessionKeys.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSignerWithSessionKeys.scala index 6e94d06a5b..c91a22adca 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSignerWithSessionKeys.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSignerWithSessionKeys.scala @@ -5,6 +5,8 @@ package com.digitalasset.canton.crypto.signer import cats.data.EitherT import cats.syntax.either.* +import com.daml.metrics.api.noop.NoOpMetricsFactory +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} import com.daml.nonempty.NonEmpty import com.digitalasset.canton.concurrent.{ExecutorServiceExtensions, FutureSupervisor, Threading} import com.digitalasset.canton.config.RequireTypes.PositiveInt @@ -31,7 +33,7 @@ import com.digitalasset.canton.lifecycle.{ UnlessShutdown, } import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.KmsMetrics +import com.digitalasset.canton.metrics.{CryptoMetrics, SigningHistograms, SigningMetrics} import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.topology.{Member, SynchronizerId} @@ -66,7 +68,7 @@ class SyncCryptoSignerWithSessionKeys( staticSynchronizerParameters: StaticSynchronizerParameters, member: Member, signPrivateApiWithLongTermKeys: SigningPrivateOps, - kmsMetrics: Option[KmsMetrics], + cryptoMetrics: CryptoMetrics, override protected val cryptoPrivateStore: CryptoPrivateStore, sessionSigningKeysConfig: SessionSigningKeysConfig, publicKeyConversionCacheConfig: CacheConfig, @@ -106,6 +108,13 @@ class SyncCryptoSignerWithSessionKeys( // this `JcePureCrypto` object only holds private key conversions spawned from sign calls privateKeyConversionCacheTtl = Some(sessionSigningKeysConfig.keyEvictionPeriod.underlying), signatureVerificationParallelism = PositiveInt.one, // not used + signingMetrics = new SigningMetrics( + new SigningHistograms(MetricName("signing"))(new HistogramInventory()), + NoOpMetricsFactory, + )( + MetricsContext.Empty + ), // not used since we only want to record latency for KMS signing requests + decryptionMetrics = cryptoMetrics.decryptionMetrics, // not used loggerFactory = loggerFactory, ) @@ -548,7 +557,9 @@ class SyncCryptoSignerWithSessionKeys( _.validityPeriodEnd.contains(CantonTimestamp.MaxValue) ) ) - kmsMetrics.foreach(_.sessionSigningKeysFallback.inc()) + cryptoMetrics.kmsMetricsO.foreach(kmsMetrics => + kmsMetrics.sessionSigningKeysFallback.inc() + ) signPrivateApiWithLongTermKeys .sign(hash, activeLongTermKey.id, usage) .leftMap[SyncCryptoError](SyncCryptoError.SyncCryptoSigningError.apply) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/verifier/SyncCryptoVerifier.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/verifier/SyncCryptoVerifier.scala index 6308039807..540e39fcb5 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/verifier/SyncCryptoVerifier.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/verifier/SyncCryptoVerifier.scala @@ -6,6 +6,8 @@ package com.digitalasset.canton.crypto.verifier import cats.data.EitherT import cats.implicits.{catsSyntaxAlternativeSeparate, catsSyntaxValidatedId} import cats.syntax.either.* +import com.daml.metrics.api.noop.NoOpMetricsFactory +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} import com.daml.nonempty.NonEmpty import com.digitalasset.canton.config.CacheConfig import com.digitalasset.canton.config.RequireTypes.PositiveInt @@ -33,6 +35,12 @@ import com.digitalasset.canton.crypto.{ } import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{ + DecryptionHistograms, + DecryptionMetrics, + SigningHistograms, + SigningMetrics, +} import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.topology.{Member, SynchronizerId} @@ -88,6 +96,14 @@ class SyncCryptoVerifier( // with a public signing key, and the private key conversion cache is never used. privateKeyConversionCacheTtl = None, signatureVerificationParallelism = signatureVerificationParallelism, + signingMetrics = new SigningMetrics( + new SigningHistograms(MetricName("signing"))(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty), // not used + decryptionMetrics = new DecryptionMetrics( + new DecryptionHistograms(MetricName("decryption"))(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty), // not used loggerFactory = loggerFactory, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/CryptoMetrics.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/CryptoMetrics.scala new file mode 100644 index 0000000000..31db14708f --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/CryptoMetrics.scala @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.metrics + +/** Aggregates all cryptographic-related metrics. + * + * @param signingMetrics + * Metrics for signing operations + * @param decryptionMetrics + * Metrics for decryption operations + * @param kmsMetricsO + * Optional metrics for KMS-backed operations; defined only when KMS is in use + */ +class CryptoMetrics( + val signingMetrics: SigningMetrics, + val decryptionMetrics: DecryptionMetrics, + val kmsMetricsO: Option[KmsMetrics], +) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/DecryptionMetrics.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/DecryptionMetrics.scala new file mode 100644 index 0000000000..8fe279b71b --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/DecryptionMetrics.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.metrics + +import com.daml.metrics.api.HistogramInventory.Item +import com.daml.metrics.api.MetricHandle.{LabeledMetricsFactory, Timer} +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricQualification, MetricsContext} + +class DecryptionHistograms(val parent: MetricName)(implicit + inventory: HistogramInventory +) { + + private[metrics] val prefix: MetricName = parent :+ "decryption" + + private[metrics] val decryptLatency: Item = Item( + prefix :+ "latency", + summary = "Latency of decryption requests.", + description = "Measures the latency of decryption operations.", + qualification = MetricQualification.Latency, + ) +} + +class DecryptionMetrics( + histograms: DecryptionHistograms, + labeledMetricsFactory: LabeledMetricsFactory, +)(implicit context: MetricsContext) { + + val prefix: MetricName = histograms.prefix + val decryptLatency: Timer = labeledMetricsFactory.timer(histograms.decryptLatency.info) +} diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SequencerConnectionPoolMetrics.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SequencerConnectionPoolMetrics.scala index c275e9c61e..3cdbd92113 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SequencerConnectionPoolMetrics.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SequencerConnectionPoolMetrics.scala @@ -105,25 +105,19 @@ class SequencerConnectionPoolMetrics( connectionHealthMetrics.getOrElseUpdate(mc, Eval.later(createConnectionHealthGauge)).value } - def removeMetricsForAllConnections(): Unit = { - connectionHealthMetrics.values.foreach(_.value.close) - connectionHealthMetrics.clear() + def removeMetricsForAllConnections(psidO: Option[String]): Unit = + removeInternal(_.labels.get("psid") == psidO) - subscriptionHealthMetrics.values.foreach(_.value.close) - subscriptionHealthMetrics.clear() - } - - def removeMetricsForConnection(toRemove: Set[String]): Unit = { - // remove from map - val connsToRemove = connectionHealthMetrics.keys - .filter(_.labels.get("connection").exists(toRemove(_))) - connsToRemove.foreach(mc => connectionHealthMetrics.remove(mc).foreach(_.value.close())) + def removeMetricsForConnection(toRemove: Set[String], psidO: Option[String]): Unit = + removeInternal(mc => + mc.labels.get("psid") == psidO && mc.labels.get("connection").exists(toRemove(_)) + ) - val subsToRemove = subscriptionHealthMetrics.keys - .filter(_.labels.get("connection").exists(toRemove(_))) - subsToRemove - .foreach(mc => subscriptionHealthMetrics.remove(mc).foreach(_.value.close)) - } + private def removeInternal[T](filter: MetricsContext => Boolean): Unit = + Seq(connectionHealthMetrics, subscriptionHealthMetrics).foreach { metricsMap => + val toRemove = metricsMap.keys.filter(filter) + toRemove.foreach(metricsMap.remove(_).foreach(_.value.close)) + } // Gauges don't support metrics context per update. So instead create a map with a gauge per context. private val subscriptionHealthMetrics: TrieMap[MetricsContext, Eval[Gauge[Int]]] = TrieMap.empty diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SigningMetrics.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SigningMetrics.scala new file mode 100644 index 0000000000..476b145934 --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SigningMetrics.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.metrics + +import com.daml.metrics.api.HistogramInventory.Item +import com.daml.metrics.api.MetricHandle.{LabeledMetricsFactory, Timer} +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricQualification, MetricsContext} + +class SigningHistograms(val parent: MetricName)(implicit + inventory: HistogramInventory +) { + + private[metrics] val prefix: MetricName = parent :+ "signing" + + private[metrics] val signingLatency: Item = Item( + prefix :+ "latency", + summary = "Latency of signing requests.", + description = "Measures the latency of signing operations.", + qualification = MetricQualification.Latency, + ) +} + +class SigningMetrics( + histograms: SigningHistograms, + labeledMetricsFactory: LabeledMetricsFactory, +)(implicit context: MetricsContext) { + + val prefix: MetricName = histograms.prefix + val signingLatency: Timer = labeledMetricsFactory.timer(histograms.signingLatency.info) +} diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcError.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcError.scala index 8378d5b32c..53d3b509f6 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcError.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcError.scala @@ -10,6 +10,7 @@ import com.digitalasset.canton.sequencing.authentication.MemberAuthentication.{ MissingToken, } import com.digitalasset.canton.sequencing.authentication.grpc.Constant +import com.digitalasset.canton.sequencing.protocol.SequencerErrors.AggregateSubmissionAlreadySent import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* import io.grpc.Status.Code.* @@ -122,6 +123,28 @@ object GrpcError { } } + /** The server rejected this request because it duplicates an existing resource. */ + final case class GrpcRequestRefusedAlreadyExists( + request: String, + serverName: String, + status: Status, + optTrailers: Option[Metadata], + decodedCantonError: Option[DecodedCantonError], + ) extends GrpcError { + + override protected def logFullCause: Boolean = true + + def isAuthenticationTokenMissing: Boolean = false + + override def log(logger: TracedLogger)(implicit traceContext: TraceContext): Unit = + // no need to log this dramatically as it is expected to happen + if (decodedCantonError.exists(_.code.id == AggregateSubmissionAlreadySent.id)) { + logger.info(s"""Request failed for $serverName.$hint + | ${getClass.getSimpleName}: ${status.getCode} / ${status.getDescription}""".mkString) + } else logger.info(toString) + + } + /** The client gave up waiting for a response. The server may or may not process the request. It * may or may not make sense to retry, depending on the specific situation. */ @@ -199,9 +222,12 @@ object GrpcError { else GrpcClientError(request, serverName, status, optTrailers, decodedError) case FAILED_PRECONDITION | NOT_FOUND | OUT_OF_RANGE | RESOURCE_EXHAUSTED | ABORTED | - PERMISSION_DENIED | ALREADY_EXISTS => + PERMISSION_DENIED => GrpcRequestRefusedByServer(request, serverName, status, optTrailers, decodedError) + case ALREADY_EXISTS => + GrpcRequestRefusedAlreadyExists(request, serverName, status, optTrailers, decodedError) + case DEADLINE_EXCEEDED | CANCELLED => GrpcClientGaveUp(request, serverName, status, optTrailers, decodedError) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala index b7e6fe8dac..9568d0938f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala @@ -26,6 +26,7 @@ import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.retry.RetryEither import com.digitalasset.canton.util.{LoggerUtil, MonadUtil, ResourceUtil} import org.flywaydb.core.Flyway +import org.flywaydb.core.api.configuration.FluentConfiguration import org.flywaydb.core.api.{FlywayException, MigrationInfo} import slick.jdbc.JdbcBackend.Database import slick.jdbc.hikaricp.HikariCPJdbcDataSource @@ -35,6 +36,7 @@ import java.sql.SQLException import javax.sql.DataSource import scala.concurrent.duration.Duration import scala.concurrent.{ExecutionContext, blocking} +import scala.jdk.CollectionConverters.* /** Performs DB migrations using Flyway. * @@ -59,13 +61,20 @@ class DbMigrations( * https://flywaydb.org/documentation/getstarted/firststeps/api */ protected def createFlyway(dataSource: DataSource): Flyway = + createFlywayConfig(dataSource: DataSource).load() + + protected def createFlywayConfig(dataSource: DataSource): FluentConfiguration = Flyway.configure .locations(dbConfig.buildMigrationsPaths(alphaVersionSupport)*) .dataSource(dataSource) .cleanDisabled(!dbConfig.parameters.unsafeCleanOnValidationError) .baselineOnMigrate(dbConfig.parameters.unsafeBaselineOnMigrate) .lockRetryCount(60) - .load() + .placeholders( + Map( + "initialBftOrdererTablesPartitionSize" -> dbConfig.parameters.partitions.initialBftOrdererTablesPartitionSize.toString + ).asJava + ) protected def withCreatedDb[A](retryConfig: DbStorage.RetryConfig)( fn: Database => EitherT[UnlessShutdown, DbMigrations.Error, A] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageMulti.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageMulti.scala index 3f63f46b7e..5950b788e1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageMulti.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageMulti.scala @@ -193,7 +193,7 @@ final class DbStorageMulti private ( // Run the initial health check checkHealth(clock.now) - private val writeDb: Database = DbLockedConnectionPool.createDatabaseFromPool( + private[canton] val writeDb: Database = DbLockedConnectionPool.createDatabaseFromPool( writeConnectionPool, writeDbExecutor, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageSingle.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageSingle.scala index 9c98041df8..b296c83d8f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageSingle.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageSingle.scala @@ -38,7 +38,7 @@ import scala.concurrent.{ExecutionContext, Future, blocking} final class DbStorageSingle private ( override val profile: DbStorage.Profile, override val dbConfig: DbConfig, - db: Database, + private[canton] val db: Database, clock: Clock, override protected val logOperations: Boolean, override val metrics: DbStorageMetrics, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/PeriodicAcknowledgements.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/PeriodicAcknowledgements.scala index fa1d37af6b..8f5c78708c 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/PeriodicAcknowledgements.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/PeriodicAcknowledgements.scala @@ -8,7 +8,12 @@ import com.daml.nameof.NameOf.functionFullName import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, UnlessShutdown} +import com.digitalasset.canton.lifecycle.{ + FlagCloseable, + FutureUnlessShutdown, + HasCloseContext, + UnlessShutdown, +} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.store.SequencerCounterTrackerStore import com.digitalasset.canton.time.Clock @@ -45,7 +50,8 @@ class PeriodicAcknowledgements( )(implicit executionContext: ExecutionContext) extends NamedLogging with FlagCloseable - with HasFlushFuture { + with HasFlushFuture + with HasCloseContext { private val priorAckRef = new AtomicReference[Option[CantonTimestamp]](None) @@ -94,7 +100,7 @@ class PeriodicAcknowledgements( withNewTraceContext("schedule_next_periodic_ack") { implicit traceContext => synchronizeWithClosingSync(functionFullName)( clock - .scheduleAfter( + .scheduleAfterCancelledOnShutdown( { _ => // Schedule the next update as soon as possible after the interval has passed; // for static time tests, this runs synchronously when the time is advanced @@ -102,6 +108,7 @@ class PeriodicAcknowledgements( scheduleNextUpdate() // Async-trampolined update() }, + "periodick-ack", interval.toJava, ) .discard[FutureUnlessShutdown[Unit]] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala index d5d353dfd2..5dd311e907 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala @@ -60,6 +60,7 @@ import com.digitalasset.canton.sequencing.client.SendTracker.{LatestAttempt, Lat import com.digitalasset.canton.sequencing.client.SequencerClient.{ ConnectionContainer, SequencerTransports, + TrafficCostValidator, } import com.digitalasset.canton.sequencing.client.SequencerClientImpl.SequencerClientTimeSourcesPool import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps @@ -121,7 +122,7 @@ import org.apache.pekko.{Done, NotUsed} import org.slf4j.event.Level import java.util.concurrent.atomic.AtomicReference -import scala.annotation.nowarn +import scala.annotation.{nowarn, unused} import scala.compat.java8.DurationConverters.FiniteDurationops import scala.concurrent.* import scala.concurrent.duration.* @@ -300,6 +301,7 @@ abstract class SequencerClientImpl( messageId: MessageId, aggregationRule: Option[AggregationRule], callback: SendCallback, + trafficCostValidator: TrafficCostValidator, amplify: Boolean, useConfirmationResponseAmplificationParameters: Boolean, )(implicit @@ -312,6 +314,7 @@ abstract class SequencerClientImpl( messageId, aggregationRule, callback, + trafficCostValidator, amplify, useConfirmationResponseAmplificationParameters, metricsContext, @@ -339,6 +342,7 @@ abstract class SequencerClientImpl( messageId: MessageId, aggregationRule: Option[AggregationRule], callback: SendCallback, + trafficCostValidator: TrafficCostValidator, amplify: Boolean, useConfirmationResponseAmplificationParameters: Boolean, metricsContext: MetricsContext, @@ -477,6 +481,9 @@ abstract class SequencerClientImpl( .checkSenderAndRecipientsAreRegistered(request, snapshot) .leftMap(_.toSendAsyncClientError) acceptableSequencersO <- EitherT.right(getAcceptableSequencers(snapshot)) + _ <- EitherT.liftF( + cost.parTraverse_(c => trafficCostValidator.validate(c.cost.unwrap, traceContext)) + ) latestAttemptRef <- EitherT.fromEither[FutureUnlessShutdown](trackSend) _ = recorderO.foreach(_.recordSubmission(request)) res <- performSend( @@ -1522,7 +1529,7 @@ class RichSequencerClientImpl( sequencerSubscriptionFactory, subscriptionHandlerFactory, metrics.connectionPool, - metricsContext = MetricsContext.Empty, + connectionPool.metricsContext, timeouts, loggerFactory, ) @@ -2340,4 +2347,25 @@ object SequencerClient { sequencerId: SequencerId, ): NamedLoggerFactory = loggerFactory.append("sequencerId", sequencerId.uid.toString) + + trait TrafficCostValidator { + + /** Validates that the traffic cost is valid in the context of the submitting member and the + * current submission request. + * + * Practically, this is relevant for requests from submitting participants that perform traffic + * enforcement against local user traffic accounts. + */ + def validate(trafficCost: Long, traceContext: TraceContext): FutureUnlessShutdown[Unit] + } + + object TrafficCostValidator { + val NoTrafficCostValidation: TrafficCostValidator = new TrafficCostValidator { + override def validate( + @unused trafficCost: Long, + @unused traceContext: TraceContext, + ): FutureUnlessShutdown[Unit] = + FutureUnlessShutdown.unit + } + } } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala index d2dd73d0cc..535178e739 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala @@ -243,11 +243,13 @@ object SequencerClientFactory { // Make a BFT call to all the transports to retrieve the current traffic state from the synchronizer // and initialize the trafficStateController with it trafficInitTimestampO = latestSequencedTimestampO - .orElse( - synchronizerPredecessor.map( - _.upgradeTime - ) - ) + .orElse(synchronizerPredecessor.map(_.upgradeTime)) + /* + Mediator nodes don't expose traffic. + This also prevent them from connecting to the sequencer during LSU before upgrade time, which + is needed for the test sequencing messages. + */ + .filter(_ => member.code != MediatorId.Code) _ = logger.info( s"Initializing traffic state at timestamp: $trafficInitTimestampO" diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientSend.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientSend.scala index 9a0f9b95a6..dab9680511 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientSend.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientSend.scala @@ -8,6 +8,8 @@ import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.protocol.messages.DefaultOpenEnvelope +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator.NoTrafficCostValidation import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.protocol.{AggregationRule, Batch, MessageId} import com.digitalasset.canton.time.Clock @@ -71,6 +73,10 @@ trait SequencerClientSend { * independent of the configuration. * @param timestamps * Aggregated timestamps needed for sending a request. + * @param trafficCostValidator + * Validate the traffic cost of the submission batch against the local traffic state of the + * member, only if traffic control is enabled for the sequencer. If validation fails, the + * submission request is not sent to the sequencer and an error is returned. */ def sendAsync( batch: Batch[DefaultOpenEnvelope], @@ -78,6 +84,7 @@ trait SequencerClientSend { messageId: MessageId = generateMessageId, aggregationRule: Option[AggregationRule] = None, callback: SendCallback = SendCallback.empty, + trafficCostValidator: TrafficCostValidator, amplify: Boolean = false, useConfirmationResponseAmplificationParameters: Boolean = false, )(implicit @@ -95,6 +102,7 @@ trait SequencerClientSend { messageId: MessageId = generateMessageId, aggregationRule: Option[AggregationRule] = None, callback: SendCallback = SendCallback.empty, + trafficCostValidator: TrafficCostValidator = NoTrafficCostValidation, amplify: Boolean = false, useConfirmationResponseAmplificationParameters: Boolean = false, )(implicit @@ -106,6 +114,7 @@ trait SequencerClientSend { messageId = messageId, aggregationRule = aggregationRule, callback = callback, + trafficCostValidator = trafficCostValidator, amplify = amplify, useConfirmationResponseAmplificationParameters = useConfirmationResponseAmplificationParameters, ).value.flatMap(identity) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcInternalSequencerConnection.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcInternalSequencerConnection.scala index 4723d6c8eb..8b21366659 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcInternalSequencerConnection.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcInternalSequencerConnection.scala @@ -66,7 +66,7 @@ class GrpcInternalSequencerConnection private[sequencing] ( private val connection: GrpcConnection = GrpcConnection(config, params, metrics, timeouts, loggerFactory) - private val connectionMetricsContext: MetricsContext = metricsContext.withExtraLabels( + private implicit val connectionMetricsContext: MetricsContext = metricsContext.withExtraLabels( "connection" -> connection.config.name ) private val stub: SequencerConnectionStub = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcSequencerConnection.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcSequencerConnection.scala index 4f097360bb..cfe4e9138b 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcSequencerConnection.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcSequencerConnection.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.networking.grpc.GrpcError.{ GrpcClientError, + GrpcRequestRefusedAlreadyExists, GrpcRequestRefusedByServer, GrpcServiceUnavailable, } @@ -134,10 +135,17 @@ class GrpcSequencerConnection( // Adapted from GrpcSequencerClientTransportCommon Either.cond( !bubbleSendErrorPolicy(error), { - // log that we're swallowing the error - logger.info( - s"Send [$messageId] returned an error however may still be possibly sequenced so we are ignoring the error: $error" - ) + error match { + // TODO(#12377) Do not trust the sequencer but monitor and stop using the given sequencer if it is denying service + case ConnectionError.TransportError(error: GrpcRequestRefusedAlreadyExists) => + // already logged as an info in GrpcConnection + case _ => + // log that we're swallowing the error + logger.info( + s"Send [$messageId] returned an error however may still be possibly sequenced so we are ignoring the error: $error" + ) + } + () }, error match { @@ -172,6 +180,8 @@ class GrpcSequencerConnection( case _: GrpcError.GrpcClientError => true // the request was rejected by the server as it wasn't in a state to accept it case _: GrpcError.GrpcRequestRefusedByServer => true + // the request was rejected by the server because it already exists, so we don't need to bubble up + case _: GrpcError.GrpcRequestRefusedAlreadyExists => false // an internal error happened at the server, this could have been when constructing or sending the response // after accepting the request so we cannot safely bubble the error case _: GrpcError.GrpcServerError => false diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcUserSequencerConnectionStub.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcUserSequencerConnectionStub.scala index 317330bbf4..0ea28583ea 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcUserSequencerConnectionStub.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcUserSequencerConnectionStub.scala @@ -91,9 +91,8 @@ class GrpcUserSequencerConnectionStub( SendAsyncRequest(signedSubmissionRequest = request.toByteString) ) ) - .leftMap( - SequencerConnectionStubError.ConnectionError.apply - ) + .leftMap(SequencerConnectionStubError.ConnectionError.apply) + } yield () } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPool.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPool.scala index 88ad976667..33771788b8 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPool.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPool.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.sequencing.client.pool import cats.data.EitherT import cats.syntax.either.* import com.daml.grpc.adapter.ExecutionSequencerFactory +import com.daml.metrics.api.MetricsContext import com.daml.nonempty.NonEmpty import com.digitalasset.canton.SequencerAlias import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} @@ -145,6 +146,9 @@ trait SequencerConnectionPool extends FlagCloseable with NamedLogging { /** Obtain all the sequencer IDs present in the pool, associated to their sequencer alias */ def getAllSequencerIds(implicit traceContext: TraceContext): Map[SequencerAlias, SequencerId] + /** The context for the pool metrics */ + def metricsContext: MetricsContext + /** Determine whether the connection pool can still reach the given threshold, ignoring the * `ignored` connections and considering an additional `extraUndecided` number of undecided * connections. diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImpl.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImpl.scala index 4d19be7be2..1e4dfa60b9 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImpl.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImpl.scala @@ -64,7 +64,7 @@ class SequencerConnectionPoolImpl private[sequencing] ( crypto: Crypto, seedForRandomnessO: Option[Long], metrics: SequencerConnectionPoolMetrics, - metricsContext: MetricsContext, + override val metricsContext: MetricsContext, futureSupervisor: FutureSupervisor, override protected val timeouts: ProcessingTimeout, override protected val loggerFactory: NamedLoggerFactory, @@ -138,7 +138,8 @@ class SequencerConnectionPoolImpl private[sequencing] ( override def staticSynchronizerParametersO: Option[StaticSynchronizerParameters] = bootstrapCell.get.map(_.staticParameters) - private implicit def mc: MetricsContext = metricsContext + private implicit val mc: MetricsContext = metricsContext + metrics.trustThreshold.updateValue(config.trustThreshold.value) override def start()(implicit @@ -153,7 +154,6 @@ class SequencerConnectionPoolImpl private[sequencing] ( updateTrackedConnections( toBeAdded = config.connections, toBeRemoved = Set.empty, - isInitialUpdate = true, ) } @@ -236,7 +236,6 @@ class SequencerConnectionPoolImpl private[sequencing] ( private def updateTrackedConnections( toBeAdded: immutable.Iterable[ConnectionConfig], toBeRemoved: Set[ConnectionConfig], - isInitialUpdate: Boolean, )(implicit traceContext: TraceContext): Unit = lock.exclusive { val removedConnections = @@ -260,11 +259,8 @@ class SequencerConnectionPoolImpl private[sequencing] ( removedConnections.foreach { connection => connection.fatal("Removed from configuration") } - if (isInitialUpdate) { - metrics.removeMetricsForAllConnections() - } else { - metrics.removeMetricsForConnection(toBeRemoved.map(_.name)) - } + + metrics.removeMetricsForConnection(toBeRemoved.map(_.name), mc.labels.get("psid")) // If start() or updateConfig() is called after the pool has been closed, we don't want to start new connections if (!isClosing) { @@ -464,7 +460,6 @@ class SequencerConnectionPoolImpl private[sequencing] ( updateTrackedConnections( toBeAdded = changedConnections.added, toBeRemoved = changedConnections.removed, - isInitialUpdate = false, ) } } @@ -573,6 +568,7 @@ class SequencerConnectionPoolImpl private[sequencing] ( // We close the connections outside the critical section to avoid shutdown problems in case // it triggers health callbacks LifeCycle.close(instances*)(logger) + metrics.removeMetricsForAllConnections(mc.labels.get("psid")) super.onClosed() } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerSubscriptionPoolImpl.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerSubscriptionPoolImpl.scala index 9be2ed91f7..d003bd97d1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerSubscriptionPoolImpl.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerSubscriptionPoolImpl.scala @@ -86,7 +86,8 @@ final class SequencerSubscriptionPoolImpl private[sequencing] ( private def currentConfigWithThreshold: ConfigWithThreshold = ConfigWithThreshold(config, pool.config.trustThreshold) - private implicit def mc: MetricsContext = metricsContext + private implicit val mc: MetricsContext = metricsContext + metrics.subscriptionThreshold.updateValue( currentConfigWithThreshold.activeThreshold.value ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSubscriptionErrorRetryPolicy.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSubscriptionErrorRetryPolicy.scala index 09378fccfe..53541d302d 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSubscriptionErrorRetryPolicy.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSubscriptionErrorRetryPolicy.scala @@ -50,6 +50,9 @@ object GrpcSubscriptionErrorRetryPolicy { loggingContext.debug("Not trying to reconnect.") retry + case error: GrpcError.GrpcRequestRefusedAlreadyExists => + false // not retrying + case _: GrpcError.GrpcServerError => // We believe these errors (INTERNAL, UNKNOWN, DATA_LOSS) can in some circumstances by transient, and // therefore we err on the side of caution and retry. diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedEnvelope.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedEnvelope.scala index 5c0a307cdf..9286bbd00f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedEnvelope.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedEnvelope.scala @@ -60,6 +60,20 @@ object ClosedEnvelope { snapshot.verifySignatures(hash, sender, signatures, SigningKeyUsage.ProtocolOnly) } + def verifyKeyUsage( + snapshot: SyncCryptoApi, + sender: Member, + signature: Signature, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, SignatureCheckError, Unit] = + snapshot.verifyKeyUsage( + sender, + signature.authorizingLongTermKey, + signature.signatureDelegation, + SigningKeyUsage.ProtocolOnly, + ) + def verifyMediatorSignatures( snapshot: SyncCryptoApi, mediatorGroupIndex: MediatorGroupIndex, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedUncompressedEnvelope.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedUncompressedEnvelope.scala index 60a0b7cb85..817b1c8c82 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedUncompressedEnvelope.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedUncompressedEnvelope.scala @@ -26,7 +26,7 @@ import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.topology.Member import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.{ByteStringUtil, MaxBytesToDecompress} +import com.digitalasset.canton.util.{ByteStringUtil, MaxBytesToDecompress, MonadUtil} import com.digitalasset.canton.version.{ HasProtocolVersionedWrapper, ProtoVersion, @@ -171,6 +171,15 @@ final case class ClosedUncompressedEnvelope private[protocol] ( .from(signatures) .traverse_(ClosedEnvelope.verifySignatures(snapshot, sender, bytes, _)) + def verifyKeyUsage( + snapshot: SyncCryptoApi, + sender: Member, + )(implicit + ec: ExecutionContext, + traceContext: TraceContext, + ): EitherT[FutureUnlessShutdown, SignatureCheckError, Unit] = + MonadUtil.sequentialTraverse_(signatures)(ClosedEnvelope.verifyKeyUsage(snapshot, sender, _)) + @VisibleForTesting override def withRecipients(newRecipients: Recipients): ClosedUncompressedEnvelope = copy(recipients = newRecipients) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SendAsyncError.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SendAsyncError.scala index 196aa7052b..c2d4473de3 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SendAsyncError.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SendAsyncError.scala @@ -24,6 +24,7 @@ sealed trait SendAsyncError extends PrettyPrinting { /** The max sequencing time has elapsed and the request was refused */ def hasMaxSequencingTimeElapsed: Boolean + } object SendAsyncError { @@ -56,6 +57,7 @@ object SendAsyncError { } case _ => false } + } /** Implementation of [[SendAsyncError]]s for direct transports */ @@ -67,5 +69,6 @@ object SendAsyncError { // Only used for amplification, but direct sequencer transport doesn't use amplification override def isMaxSequencingTimeTooFar: Boolean = false + } } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SequencerDeliverError.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SequencerDeliverError.scala index 17c30276a0..0c624940c1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SequencerDeliverError.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SequencerDeliverError.scala @@ -17,6 +17,7 @@ import com.digitalasset.canton.error.CantonErrorGroups.SequencerErrorGroup import com.digitalasset.canton.error.{CantonBaseError, TransactionError, TransactionErrorImpl} import com.digitalasset.canton.networking.grpc.GrpcError import com.digitalasset.canton.topology.Member +import com.digitalasset.canton.version.ProtocolVersion import com.google.rpc.status.Status import java.time.Instant @@ -202,6 +203,28 @@ object SequencerErrors extends SequencerErrorGroup { extends SequencerDeliverErrorCode( id = "SEQUENCER_AGGREGATE_SUBMISSION_ALREADY_SENT", ErrorCategory.InvalidGivenCurrentSystemStateOther, + ) { + + def apply(message: String, protocolVersion: ProtocolVersion): SequencerDeliverError = + // for backward compatibility reasons, we need to keep the message format unchanged + if (protocolVersion <= ProtocolVersion.v35) + super.apply(message) + else + AggregateSubmissionAlreadySentV2.apply(message) + } + + @Explanation( + """This error occurs when the sequencer has already sent out the aggregate submission for the request.""" + ) + @Resolution( + """This is expected to happen during operation of a system with aggregate submissions enabled. No action required. + |This error code has been modified to report Grpc RESOURCE_EXISTS and is used for synchronous rejects and + |async rejects starting with PV36.""" + ) + case object AggregateSubmissionAlreadySentV2 + extends SequencerDeliverErrorCode( + id = "SEQUENCER_AGGREGATE_SUBMISSION_ALREADY_SENT", + ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, ) @Explanation( diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SignedContent.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SignedContent.scala index 1a755c6baa..dac698fc19 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SignedContent.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SignedContent.scala @@ -84,6 +84,19 @@ final case class SignedContent[+A <: HasCryptographicEvidence] private ( snapshot.verifySignature(hash, member, signature, SigningKeyUsage.ProtocolOnly) } + def verifyKeyUsage( + snapshot: SyncCryptoApi, + member: Member, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, SignatureCheckError, Unit] = + snapshot.verifyKeyUsage( + member, + signature.authorizingLongTermKey, + signature.signatureDelegation, + SigningKeyUsage.ProtocolOnly, + ) + def deserializeContent[B <: HasCryptographicEvidence]( contentDeserializer: ByteString => ParsingResult[B] ): ParsingResult[SignedContent[B]] = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/store/Purgeable.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/store/Purgeable.scala index ee92f296b6..abfcaae62f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/store/Purgeable.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/store/Purgeable.scala @@ -23,6 +23,8 @@ trait Purgeable { trait ChunkPurgeable { self: FlagCloseable => + def name: String + /** Deletes a chunk of items from this store. No guarantees are made around transactionality, nor * about which specific items are deleted. * diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala index 3c1e027b6d..7a6fecc6b0 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala @@ -12,11 +12,15 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.CantonError import com.digitalasset.canton.error.CantonErrorGroups.ClockErrorGroup +import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.* import com.digitalasset.canton.lifecycle.UnlessShutdown.AbortedDueToShutdown import com.digitalasset.canton.lifecycle.{ + CloseContext, FlagCloseable, FutureUnlessShutdown, LifeCycle, + LifeCycleRegistrationHandle, + RunOnClosing, SyncCloseable, UnlessShutdown, } @@ -42,6 +46,7 @@ import com.digitalasset.canton.topology.admin.v30.{ } import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* +import com.digitalasset.canton.util.Thereafter.syntax.* import com.digitalasset.canton.util.retry.{AllExceptionRetryPolicy, Pause} import com.digitalasset.canton.util.{ErrorUtil, PriorityBlockingQueueUtil} import com.google.common.annotations.VisibleForTesting @@ -52,7 +57,7 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} import java.util.concurrent.{Callable, PriorityBlockingQueue, TimeUnit} import scala.annotation.tailrec import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContextExecutor, Promise} +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Promise} import scala.util.Try /** A clock returning the current time, but with a twist: it always returns unique timestamps. If @@ -271,6 +276,70 @@ abstract class Clock() extends TimeProvider with AutoCloseable with NamedLogging } } + /** Schedule an action to be performed at the given timestamp. If the closeContext is closed, then + * the task is cancelled. + * + * If the provided timestamp is before `now`, the action skips queueing and is executed + * immediately. + * + * @param action + * action to run at the given timestamp (passing in the timestamp for when the task was + * scheduled) + * @param taskName + * name of the task + * @param timestamp + * timestamp when to run the task + * @return + * a future for the given task + */ + def scheduleAtCancelledOnShutdown[A]( + action: CantonTimestamp => A, + taskName: String, + timestamp: CantonTimestamp, + )(implicit ec: ExecutionContext, closeContext: CloseContext): FutureUnlessShutdown[A] = { + + val (f, handle) = scheduleAtCancellable(action, timestamp) + + val cancelTask = new RunOnClosing { + override def name: String = s"cancel-$taskName" + override def done: Boolean = f.isCompleted + override def run()(implicit traceContext: TraceContext): Unit = + handle.cancel(AbortedDueToShutdown) + } + + // Cancel the task upon shutdown + val lifeCycleRegistrationHandleUS: UnlessShutdown[LifeCycleRegistrationHandle] = + closeContext.context.runOnClose(cancelTask) + + f.thereafter { _ => + // Remove the lifeCycleRegistrationHandle when the task is finished + lifeCycleRegistrationHandleUS.foreach(_.cancel().discard) + } + } + + /** Schedule an action to be performed at the given timestamp. If the closeContext is closed, then + * the task is cancelled. + * + * If the provided timestamp is before `now`, the action skips queueing and is executed + * immediately. + * + * @param action + * action to run at the given timestamp (passing in the timestamp for when the task was + * scheduled) + * @param taskName + * name of the task + * @param delta + * duration to wait before running the task + * @return + * a future for the given task + */ + def scheduleAfterCancelledOnShutdown[A]( + action: CantonTimestamp => A, + taskName: String, + delta: Duration, + )(implicit ec: ExecutionContext, closeContext: CloseContext): FutureUnlessShutdown[A] = + scheduleAtCancelledOnShutdown(action, taskName, now.add(delta)) + // flush the task queue, stopping once we hit a task in the future @tailrec private def doFlush(): Option[CantonTimestamp] = { @@ -487,9 +556,8 @@ class SimClock( override def close(): Unit = {} - override protected def addToQueue(queue: Queued[?]): Unit = { - val _ = tasks.add(queue) - } + override protected def addToQueue(queue: Queued[?]): Unit = + tasks.add(queue).discard def reset(): Unit = { failTasks() diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/time/SynchronizerTimeTracker.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/time/SynchronizerTimeTracker.scala index ff93efb981..ec2951b9b1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/time/SynchronizerTimeTracker.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/time/SynchronizerTimeTracker.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.discard.Implicits.* import com.digitalasset.canton.lifecycle.{ FlagCloseable, FutureUnlessShutdown, + HasCloseContext, LifeCycle, UnlessShutdown, } @@ -71,7 +72,8 @@ class SynchronizerTimeTracker( )(implicit executionContext: ExecutionContext) extends NamedLogging with FlagCloseable - with HasFlushFuture { + with HasFlushFuture + with HasCloseContext { /** Timestamps that we are waiting to observe held in ascending order. Queue access must be made * while holding the [[lock]]. @@ -517,7 +519,14 @@ class SynchronizerTimeTracker( val latestTimestamp = timestampRef.get().latest.fold(clock.now)(_.receivedAt) val expectUpdateBy = latestTimestamp.add(minObservationDuration).immediateSuccessor - val _ = clock.scheduleAt(performUpdate, expectUpdateBy) + clock + .scheduleAtCancelledOnShutdown( + action = performUpdate, + taskName = "min-observation", + expectUpdateBy, + ) + .discard + }.onShutdown(()) scheduleNextUpdate() diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ConfiguredPhysicalSynchronizerId.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ConfiguredPhysicalSynchronizerId.scala index 0ed122456c..440e66ec1a 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ConfiguredPhysicalSynchronizerId.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ConfiguredPhysicalSynchronizerId.scala @@ -22,6 +22,7 @@ final case class KnownPhysicalSynchronizerId(psid: PhysicalSynchronizerId) override def toOption: Option[PhysicalSynchronizerId] = Some(psid) } + case object UnknownPhysicalSynchronizerId extends ConfiguredPhysicalSynchronizerId { override protected def pretty: Pretty[UnknownPhysicalSynchronizerId.type] = prettyOfString(_ => "UnknownPhysicalSynchronizerId") diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala index 4499f4332f..4541401f27 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala @@ -451,6 +451,8 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC * the mapping that should be added * @param signingKeys * the keys which should be used to sign + * @param namespacesToSignFor + * the namespaces for which to sign * @param protocolVersion * the protocol version corresponding to the transaction * @param expectFullAuthorization @@ -466,6 +468,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC mapping: TopologyMapping, serial: Option[PositiveInt], signingKeys: Seq[Fingerprint], + namespacesToSignFor: Seq[Namespace], protocolVersion: ProtocolVersion, expectFullAuthorization: Boolean, forceChanges: ForceFlags = ForceFlags.none, @@ -490,6 +493,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC signedTx <- signTransaction( tx, signingKeys, + namespacesToSignFor, isProposal = !expectFullAuthorization, protocolVersion, existingTransaction, @@ -553,7 +557,12 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC .Failure(transactionHash, effective, tooManyActiveTransactionsWithSameHash) ) }) - extendedTransaction <- extendSignature(existingTransaction, signingKeys, forceChanges) + extendedTransaction <- extendSignature( + existingTransaction, + signingKeys, + namespacesToSignFor = Seq.empty, + forceChanges, + ) _ <- add( Seq(extendedTransaction), expectFullAuthorization = expectFullAuthorization, @@ -631,6 +640,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC private def signTransaction[Op <: TopologyChangeOp, M <: TopologyMapping]( transaction: TopologyTransaction[Op, M], signingKeys: Seq[Fingerprint], + namespacesToSignFor: Seq[Namespace], isProposal: Boolean, protocolVersion: ProtocolVersion, existingTransaction: Option[GenericSignedTopologyTransaction], @@ -644,7 +654,12 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC val transactionMapping = transaction.mapping for { // find signing keys. - keysToUseForSigning <- determineKeysToUse(transaction, signingKeys, forceChanges) + keysToUseForSigning <- determineKeysToUse( + transaction, + signingKeys, + namespacesToSignFor, + forceChanges, + ) // If the same operation and mapping is proposed repeatedly, insist that // new keys are being added. Otherwise, reject consistently with daml 2.x-based topology management. _ <- existingTransactionTuple match { @@ -678,13 +693,19 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC def extendSignature[Op <: TopologyChangeOp, M <: TopologyMapping]( transaction: SignedTopologyTransaction[Op, M], signingKeys: Seq[Fingerprint], + namespacesToSignFor: Seq[Namespace], forceFlags: ForceFlags, )(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, TopologyManagerError, SignedTopologyTransaction[Op, M]] = for { // find signing keys - keys <- determineKeysToUse(transaction.transaction, signingKeys, forceFlags) + keys <- determineKeysToUse( + transaction.transaction, + signingKeys, + namespacesToSignFor, + forceFlags, + ) keysWithNoExistingSignature = keys.diff(transaction.signatures.map(_.authorizingLongTermKey)) updatedSignedTransaction <- NonEmpty.from(keysWithNoExistingSignature) match { case Some(keysWithNoExistingSignatureNE) => @@ -712,6 +733,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC private def determineKeysToUse( transaction: GenericTopologyTransaction, signingKeysToUse: Seq[Fingerprint], + namespacesToSignFor: Seq[Namespace], forceFlags: ForceFlags, )(implicit traceContext: TraceContext @@ -723,6 +745,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC for { requiredAuthAndUsableKeys <- loadValidSigningKeys( transaction, + namespacesToSignFor, returnAllValidKeys = true, ) (requiredAuth, usableKeys) = requiredAuthAndUsableKeys @@ -750,6 +773,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC for { requiredAuthAndDetectedKeysToUse <- loadValidSigningKeys( transaction, + namespacesToSignFor, returnAllValidKeys = false, ) (requiredAuth, detectedKeysToUse) = requiredAuthAndDetectedKeysToUse @@ -764,6 +788,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC private def loadValidSigningKeys( transaction: GenericTopologyTransaction, + namespacesToSignFor: Seq[Namespace], returnAllValidKeys: Boolean, )(implicit traceContext: TraceContext) = for { @@ -785,6 +810,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC ts, transaction, existing.headOption.map(_.transaction), // there should be at most one entry + namespacesToSignFor, returnAllValidKeys, ) } yield result diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetection.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetection.scala index 927a6f1cb7..51117d8c99 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetection.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetection.scala @@ -114,6 +114,8 @@ class TopologyManagerSigningKeyDetection[+PureCrypto <: CryptoPureApi]( * the topology transaction to sign * @param inStore * the latest fully authorized topology transaction with the same unique key as `toSign` + * @param namespacesToSignFor + * if non empty, only keys for the specified namespaces are returned * @param returnAllValidKeys * if true, returns all keys that can be used to sign. if false, only returns the most specific * keys per namespace/uid. @@ -124,6 +126,7 @@ class TopologyManagerSigningKeyDetection[+PureCrypto <: CryptoPureApi]( asOfExclusive: CantonTimestamp, toSign: GenericTopologyTransaction, inStore: Option[GenericTopologyTransaction], + namespacesToSignFor: Seq[Namespace], returnAllValidKeys: Boolean, )(implicit traceContext: TraceContext @@ -142,12 +145,17 @@ class TopologyManagerSigningKeyDetection[+PureCrypto <: CryptoPureApi]( relaxChecksForBackwardsCompatibility = false, ) ) + requestedAuthScope = Option.when(namespacesToSignFor.nonEmpty)( + ReferencedAuthorizations(namespaces = namespacesToSignFor.toSet) + ) - referencedAuth = requiredAuthFor( - toSign, - inStore, - relaxChecksForBackwardsCompatibility = false, - ).referenced + referencedAuth = requestedAuthScope.getOrElse( + requiredAuthFor( + toSign, + inStore, + relaxChecksForBackwardsCompatibility = false, + ).referenced + ) knownNsKeys = referencedAuth.namespaces.toSeq .parFlatTraverse(namespace => @@ -169,7 +177,8 @@ class TopologyManagerSigningKeyDetection[+PureCrypto <: CryptoPureApi]( selfSigned = EitherT.rightT[FutureUnlessShutdown, CryptoPrivateStoreError]( toSign.mapping match { case nsd @ NamespaceDelegation(ns, target, _) - if ns.fingerprint == target.fingerprint && nsd.canSign(Code.NamespaceDelegation) => + if ns.fingerprint == target.fingerprint && nsd + .canSign(Code.NamespaceDelegation) && referencedAuth.namespaces.contains(ns) => Seq(target.fingerprint) case _ => Seq.empty } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala index 2c8389f429..fa254d2012 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala @@ -260,6 +260,8 @@ abstract class TopologyStore[+StoreID <: TopologyStoreId](implicit def predecessor: Option[SynchronizerPredecessor] + def name: String = s"topology-store ($storeId)" + /** fetch the effective time updates greater than or equal to a certain timestamp * * this function is used to recover the future effective timestamp such that we can reschedule @@ -335,6 +337,33 @@ abstract class TopologyStore[+StoreID <: TopologyStoreId](implicit traceContext: TraceContext ): FutureUnlessShutdown[PositiveStoredTopologyTransactions] + /** Returns the upgrade time of the LSU that lead to current synchronizer. + */ + def findUpgradeTimeFromPredecessor()(implicit + ev: StoreID <:< SynchronizerStore, + traceContext: TraceContext, + ): FutureUnlessShutdown[ + Option[CantonTimestamp] + ] = { + val currentPsid = ev(storeId).psid + + inspect( + proposals = false, + timeQuery = TimeQuery.Range(None, None), + asOfExclusiveO = None, + op = Some(TopologyChangeOp.Replace), + types = Seq(TopologyMapping.Code.LsuAnnouncement), + idFilter = Some(currentPsid.identifier.toProtoPrimitive), + namespaceFilter = Some(currentPsid.namespace.toProtoPrimitive), + ).map( + _.collectOfMapping[LsuAnnouncement] + .filter(_.mapping.successor.psid == currentPsid) + .result + .maxByOption(_.serial) + .map(_.mapping.upgradeTime) + ) + } + /** Same as [[findPositiveTransactions]] but returns negative transactions (with a remove * operation) */ @@ -608,7 +637,14 @@ abstract class TopologyStore[+StoreID <: TopologyStoreId](implicit .map(_.psid)}]" ) } else if (ongoingCopyFromPredecessor.compareAndSet(None, newRef)) { - val work = doCopyFromPredecessorSynchronizerStore(sourceStore) + errorLoggingContext.info( + s"LSU: About to copy topology from ${sourceStore.storeId.psid.suffix}" + ) + val work = doCopyFromPredecessorSynchronizerStore(sourceStore).map { _ => + errorLoggingContext.info( + s"LSU: Done copying topology from ${sourceStore.storeId.psid.suffix}" + ) + } newPromise .completeWithUS( work.thereafter(_ => ongoingCopyFromPredecessor.compareAndSet(newRef, None).discard) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala index bf1f90cf4c..435432a18d 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala @@ -1454,8 +1454,8 @@ class DbTopologyStore[+StoreId <: TopologyStoreId]( } } yield { logger.info( - if (deleted > 0) s"Deleted chunk of $deleted from topology store $storeId." - else s"No chunk to delete from topology store $storeId." + if (deleted > 0) s"Deleted chunk of $deleted from topology store" + else s"No chunk to delete from topology store." ) deleted > 0 } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala index 0dfbd2b545..73510a8468 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala @@ -236,7 +236,7 @@ object TopologyMapping { } - // Small wrapper to not have to work with a tuple3 (Set[Namespace], Set[Uid], Set[Fingerprint]) + // Small wrapper to not have to work with a 2-tuple (Set[Namespace], Set[Fingerprint]) final case class ReferencedAuthorizations( namespaces: Set[Namespace] = Set.empty, extraKeys: Set[Fingerprint] = Set.empty, @@ -1169,14 +1169,14 @@ object SynchronizerTrustCertificate extends TopologyMappingCompanion { /** When this feature flag is enabled, the participant will allow to reassign contracts between * synchronizers. This feature is in alpha and should not be used in production. */ - val EnableAlphaMultiSynchronizer: ParticipantTopologyFeatureFlag = + val EnableMultiSynchronizer: ParticipantTopologyFeatureFlag = ParticipantTopologyFeatureFlag( - v30.Enums.ParticipantFeatureFlag.PARTICIPANT_FEATURE_FLAG_ENABLE_ALPHA_MULTI_SYNCHRONIZER.value - )(Some("EnableAlphaMultiSynchronizer")) + v30.Enums.ParticipantFeatureFlag.PARTICIPANT_FEATURE_FLAG_ENABLE_MULTI_SYNCHRONIZER.value + )(Some("EnableMultiSynchronizer")) val knownTopologyFeatureFlags: Seq[ParticipantTopologyFeatureFlag] = Seq( ExternalSigningLocalContractsInSubview, - EnableAlphaMultiSynchronizer, + EnableMultiSynchronizer, ) def fromProtoV30( diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala index 9e39cd78d1..c89aa88bc7 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala @@ -1149,6 +1149,8 @@ object PekkoUtil extends HasLoggerName { def apply(index: Long): Unit } + type ShutdownInProgress = () => Boolean + final case class FutureQueueConsumer[T]( futureQueue: FutureQueue[(Long, T)], fromExclusive: Long, @@ -1198,8 +1200,7 @@ object PekkoUtil extends HasLoggerName { retryAttemptErrorThreshold: Int, uncommittedWarnTreshold: Int, recoveringQueueMetrics: RecoveringQueueMetrics, - consumerFactory: Commit => Future[FutureQueueConsumer[T]], - initializationKillSwitch: Option[() => Unit], + consumerFactory: Commit => ShutdownInProgress => Future[Future[FutureQueueConsumer[T]]], ) extends RecoveringFutureQueue[T] { assert(maxBlockedOffer > 0) assert(retryAttemptWarnThreshold > 0) @@ -1209,7 +1210,7 @@ object PekkoUtil extends HasLoggerName { private val logger = loggerFactory.getLogger(this.getClass) private implicit val directEC: ExecutionContext = DirectExecutionContext(logger) - private var consumer: Consumer[T] = Consumer.InitializationInProgress(initializationKillSwitch) + private var consumer: Consumer[T] = Consumer.InitializationInProgress private val recoveringQueue: RecoveringQueue[T] = new RecoveringQueue( maxBlocked = maxBlockedOffer, @@ -1220,7 +1221,7 @@ object PekkoUtil extends HasLoggerName { ) private val lock = new Mutex() private val timer: Timer = new Timer() - private var shuttingDown: Boolean = false + private val shuttingDown: AtomicBoolean = new AtomicBoolean(false) private var shuttingDownTimerCancelled: Boolean = false private val donePromise: Promise[Done] = Promise() private val firstSuccessfulConsumerInitializationPromise: Promise[Unit] = Promise() @@ -1234,7 +1235,7 @@ object PekkoUtil extends HasLoggerName { firstSuccessfulConsumerInitializationPromise.future override def offer(elem: T): Future[Done] = blockingSynchronized { - if (shuttingDown) { + if (shuttingDown.get()) { Future.failed( new IllegalStateException( "Cannot offer new elements to the queue, after shutdown is initiated" @@ -1248,7 +1249,7 @@ object PekkoUtil extends HasLoggerName { } override def shutdown(): Unit = blockingSynchronized { - if (shuttingDown || shuttingDownTimerCancelled) { + if (shuttingDown.get() || shuttingDownTimerCancelled) { logger.debug("Already shutting down, nothing to do") } else { shuttingDownTimerCancelled = true @@ -1272,23 +1273,17 @@ object PekkoUtil extends HasLoggerName { private def shutdownStepTwo(): Unit = blockingSynchronized { logger.info("Shutdown initiated") - shuttingDown = true + shuttingDown.set(true) recoveringQueue.shutdown() consumer match { case Consumer.Initialized(c) => logger.info("Consumer shutdown initiated") c.shutdown() - case Consumer.InitializationInProgress(killSwitch) => - killSwitch match { - case Some(kill) => - logger.info( - "Consumer initialization is in progress, invoking initialization kill switch..." - ) - kill() - case None => - logger.debug("Consumer initialization is in progress, delaying shutdown...") - } + case Consumer.InitializationInProgress => + logger.debug( + "Consumer initialization is in progress, shutdown signal will be propagated to consumer..." + ) case Consumer.WaitingForRetry => logger.info("Interrupting wait for initialization retry, shutdown complete") @@ -1298,8 +1293,12 @@ object PekkoUtil extends HasLoggerName { private def initializeConsumer(attempt: Int = 1): Unit = blockingSynchronized { logger.info("Initializing consumer...") - consumer = Consumer.InitializationInProgress(initializationKillSwitch) - consumerFactory(recoveringQueue.commit) + consumer = Consumer.InitializationInProgress + consumerFactory(recoveringQueue.commit)(() => shuttingDown.get()) + .flatMap { innerFuture => + firstSuccessfulConsumerInitializationPromise.trySuccess(()).discard + innerFuture + }(directEC) .onComplete(consumerInitialized(_, attempt))(directEC) } @@ -1316,14 +1315,13 @@ object PekkoUtil extends HasLoggerName { logger.error(s"Exception caught while recovering: ${t.getMessage}. Shutting down.", t) shutdown() } - if (shuttingDown) { + if (shuttingDown.get()) { logger.info( "Consumer initialized, but since shutdown already in progress, consumer shutdown initiated" ) queueConsumer.futureQueue.shutdown() queueConsumer.futureQueue.done.onComplete(consumerTerminated)(directEC) } else { - firstSuccessfulConsumerInitializationPromise.trySuccess(()).discard logger.info("Consumer initialized") consumer = Consumer.Initialized( new FutureQueuePullProxy( @@ -1339,7 +1337,7 @@ object PekkoUtil extends HasLoggerName { } case Failure(failure) => - if (shuttingDown) { + if (shuttingDown.get()) { logger.info( "Consumer initialization failed, but not retrying anymore since already shutting down", failure, @@ -1373,7 +1371,7 @@ object PekkoUtil extends HasLoggerName { case Failure(failure) => logger.info("Consumer terminated with a failure", failure) } - if (shuttingDown) { + if (shuttingDown.get()) { logger.info("Terminated (consumer terminated), shutdown complete") discard(donePromise.trySuccess(Done)) } else { @@ -1400,8 +1398,7 @@ object PekkoUtil extends HasLoggerName { } private object Consumer { - final case class InitializationInProgress(killSwitch: Option[() => Unit]) - extends Consumer[Nothing] + case object InitializationInProgress extends Consumer[Nothing] case object WaitingForRetry extends Consumer[Nothing] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/ShardedSequentialProcessingQueue.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/ShardedSequentialProcessingQueue.scala new file mode 100644 index 0000000000..0ee6a0930c --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/ShardedSequentialProcessingQueue.scala @@ -0,0 +1,174 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.util + +import cats.data.{EitherT, Nested} +import cats.syntax.functor.* +import com.digitalasset.canton.concurrent.FutureSupervisor +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.discard.Implicits.* +import com.digitalasset.canton.lifecycle.{ + FlagCloseable, + FutureUnlessShutdown, + PromiseUnlessShutdown, +} +import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.Thereafter.syntax.ThereafterOps +import com.google.common.annotations.VisibleForTesting + +import scala.collection.concurrent.TrieMap +import scala.concurrent.ExecutionContext + +/** A processing queue that allows scheduling work associated with a particular identifier. + * + * It comes in two flavours: + * - [[GarbageCollectedShardedSequentialProcessingQueue]] when the number of identifiers is big + * and each identifier is usually short-lived. + * - [[NonGarbageCollectedShardedSequentialProcessingQueue]] when the number of identifiers is + * small and each identifier is potentially long-lived. + * + * @tparam Ident + * The type of the identifiers + */ +sealed trait ShardedSequentialProcessingQueue[Ident] { + def executeUS[A](id: Ident)(action: => FutureUnlessShutdown[A], taskName: String)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[A] + + def executeEUS[A, B](id: Ident)(action: => EitherT[FutureUnlessShutdown, A, B], taskName: String)( + implicit traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, A, B] = EitherT(executeUS(id)(action.value, taskName)) + + @VisibleForTesting + def isQueueEmpty(id: Ident): Boolean + + @VisibleForTesting + def areQueuesEmpty: Boolean +} + +/** A processing queue that runs units of work for a particular identifier in a sequential manner, + * but allows parallel processing of work for different identifiers. If a unit of work fails or + * throws an exception, subsequent units of work for the same identifier are not executed. + * + * Use this queue when the number of identifiers is big and each identifier is usually short-lived. + * + * @tparam Ident + * The type of the identifiers + */ +class GarbageCollectedShardedSequentialProcessingQueue[Ident](implicit ec: ExecutionContext) + extends ShardedSequentialProcessingQueue[Ident] { + + @VisibleForTesting + val processingQueuePerId = new TrieMap[Ident, FutureUnlessShutdown[Unit]]() + + override def isQueueEmpty(id: Ident): Boolean = !processingQueuePerId.isDefinedAt(id) + override def areQueuesEmpty: Boolean = processingQueuePerId.isEmpty + + override def executeUS[A]( + id: Ident + )(action: => FutureUnlessShutdown[A], taskName: String)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[A] = { + val processingPromise: PromiseUnlessShutdown[Unit] = PromiseUnlessShutdown.unsupervised() + val processingFuture = processingPromise.futureUS + + val previousProcessingFuture: FutureUnlessShutdown[Unit] = processingQueuePerId + .put(id, processingFuture) + .getOrElse(FutureUnlessShutdown.unit) + + previousProcessingFuture.flatMap(_ => action).thereafter { result => + processingPromise.complete(Nested(result).void.value) + // cleanup the processing queue + processingQueuePerId + .updateWith(id) { + case Some(`processingFuture`) => + // if the "processing queue" still contains the same future that we put in, we can remove the entry from the map + None + case Some(other) => + // some other future was put into the map, retain it + Some(other) + case None => + // the entry was already removed, nothing to do + None + } + .discard + + } + } +} + +/** A processing queue that runs units of work for a particular identifier in a sequential manner, + * but allows parallel processing of work for different identifiers. For an identifier, the + * behavior of the queue when one task fails or throws an exception depends on the failureMode. + * + * If a unit of work fails or throws an exception, subsequent units of work for the same identifier + * are not executed. + * + * IMPORTANT NOTE: The underlying execution queues for the identifiers are not cleaned up. If you + * have many short-lived identifier, then [[GarbageCollectedShardedSequentialProcessingQueue]] + * should be preferred. + * + * Use this queue when the number of identifiers is small and each identifier is potentially + * long-lived. + * + * @param name + * For logging purposes + * @param logTaskTiming + * If true logs wait and run time for each of the tasks + * @param failureMode + * How the queue handles the execution of tasks after a previous task had failed + * @tparam Ident + * The type of the identifiers + */ +class NonGarbageCollectedShardedSequentialProcessingQueue[Ident: Pretty]( + private val name: String, + futureSupervisor: FutureSupervisor, + override val timeouts: ProcessingTimeout, + override val loggerFactory: NamedLoggerFactory, + private val logTaskTiming: Boolean, + failureMode: FailureMode, +) extends ShardedSequentialProcessingQueue[Ident] + with PrettyPrinting + with NamedLogging + with FlagCloseable { + + private val processingQueues = new TrieMap[Ident, SimpleExecutionQueue]() + + override def isQueueEmpty(id: Ident): Boolean = processingQueues.get(id).fold(true)(_.isEmpty) + override def areQueuesEmpty: Boolean = processingQueues.forall { case (_, queue) => + queue.isEmpty + } + + override def executeUS[A]( + id: Ident + )(action: => FutureUnlessShutdown[A], taskName: String)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[A] = { + + val queue = processingQueues.getOrElseUpdate( + id, + new SimpleExecutionQueue( + name = s"$name-$id", + futureSupervisor = futureSupervisor, + timeouts = timeouts, + loggerFactory = loggerFactory, + logTaskTiming = logTaskTiming, + failureMode = failureMode, + ), + ) + + queue.executeUS(action, taskName) + } + + override protected def pretty + : Pretty[NonGarbageCollectedShardedSequentialProcessingQueue[Ident]] = + prettyOfClass( + param("tasks", _.processingQueues) + ) + + override protected def onClosed(): Unit = + processingQueues.readOnlySnapshot().values.foreach(_.onClosed()) +} diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/SimpleExecutionQueue.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/SimpleExecutionQueue.scala index 6c4d60d427..43d8a637a1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/util/SimpleExecutionQueue.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/SimpleExecutionQueue.scala @@ -13,6 +13,11 @@ import com.digitalasset.canton.lifecycle.UnlessShutdown.AbortedDueToShutdown import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.FailureMode.{ + ContinueAfterFailure, + CrashAfterFailure, + StopAfterFailure, +} import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.SimpleExecutionQueue.TaskCell import com.digitalasset.canton.util.Thereafter.syntax.* @@ -27,18 +32,21 @@ import scala.util.{Failure, Success, Try} */ sealed trait FailureMode -/** Causes the queue to crash the entire process if a task is scheduled after a previously failed - * task. - */ -object CrashAfterFailure extends FailureMode +object FailureMode { -/** Causes the queue to not process any further tasks after a previously failed task. - */ -object StopAfterFailure extends FailureMode + /** Causes the queue to crash the entire process if a task is scheduled after a previously failed + * task. + */ + object CrashAfterFailure extends FailureMode -/** The queue will continue the execution of tasks even if previous tasks had failed. - */ -object ContinueAfterFailure extends FailureMode + /** Causes the queue to not process any further tasks after a previously failed task. + */ + object StopAfterFailure extends FailureMode + + /** The queue will continue the execution of tasks even if previous tasks had failed. + */ + object ContinueAfterFailure extends FailureMode +} /** Functions executed with this class will only run when all previous calls have completed * executing. This can be used when async code should not be run concurrently. @@ -86,7 +94,7 @@ class SimpleExecutionQueue( timeouts, loggerFactory, logTaskTiming, - if (crashOnFailure) CrashAfterFailure else StopAfterFailure, + failureMode = if (crashOnFailure) CrashAfterFailure else StopAfterFailure, ) protected val directExecutionContext: DirectExecutionContext = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/TracedPossiblyPrevalidated.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/TracedPossiblyPrevalidated.scala new file mode 100644 index 0000000000..beb681b233 --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/TracedPossiblyPrevalidated.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.util + +import com.digitalasset.canton.tracing.{TraceContext, Traced} + +/** Tag an object to have already passed some validation steps + * + * We use this specifically to optimistically validate signatures against older topology snapshots. + * If they pass validation (signature is valid and key is valid), we can then skip the more + * expensive validation steps and only validate the correct key usage. + */ +final case class TracedPossiblyPrevalidated[E](value: E, prevalidated: Boolean)(implicit + val traceContext: TraceContext +) { + def map[F](fn: E => F): TracedPossiblyPrevalidated[F] = + TracedPossiblyPrevalidated(fn(value), prevalidated) + + def tracedValue: Traced[E] = Traced(value)(traceContext) + + def withTraceContext[F](fn: TraceContext => E => F): F = fn(traceContext)(value) + +} + +object TracedPossiblyPrevalidated { + def notValidated[T](value: T)(implicit + traceContext: TraceContext + ): TracedPossiblyPrevalidated[T] = + TracedPossiblyPrevalidated(value, prevalidated = false) +} diff --git a/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sha256 b/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sha256 new file mode 100644 index 0000000000..8ae5e4d649 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sha256 @@ -0,0 +1 @@ +4d9b1a8be5e6e28261f6306c7b4a356e4172cfd98512185b72c401901cbbbfe9 diff --git a/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sql b/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sql new file mode 100644 index 0000000000..dd6b236801 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sql @@ -0,0 +1,75 @@ +-- Traffic enforcement tables +create table par_traffic_enforcement_event +( + -- Increasing sequence number for each event + -- This is used to provide a total ordering of events + sequence_nb bigint generated always as identity, + -- Event source is a small integer that identifies the source of the event (e.g: 0 == LEDGER_API, 1 = TEA_API) + event_source smallint not null, + -- Event type is a small integer that identifies the type of event (e.g 0 == USAGE for standard traffic usage) + event_type smallint not null, + -- Event ID is a unique identifier per event_source, it is used for event deduplication + event_id varchar not null, + -- Account ID is the identifier of the account that the event applies to + account_id varchar not null, + -- Amount is the amount of traffic tied to the event, it can be positive or negative. + -- The actual semantics depend on the event type. + amount bigint not null, + -- Timestamp when the event was recorded + -- Critically this is NOT the record time of a transaction. It is generated by the database. + -- Note that it is possible for events to have timestamps reversed compared to their sequence number + timestamp bigint not null, + -- Events are unique per (event_source, event_id). + primary key (event_source, event_id) +); + +-- For fast querying by sequence number +create index idx_event_sequence on par_traffic_enforcement_event (sequence_nb); +-- For fast querying by account ID, event type and timestamp +create index account_id_timestamp_index on par_traffic_enforcement_event (account_id, event_type, timestamp); + +-- Balance table for each account +-- Contains one row per account, continuously updated with the total debits and credits for that account +-- as events are processed. +create table par_traffic_enforcement_balance +( + -- Account ID is the identifier of the account that the balance applies to + account_id varchar not null, + -- Event sequence number is the sequence number of the last event that was applied to the balance + event_sequence_nb bigint not null, + -- Matches the event_type of the event table. There's one row per account and event type. + event_type smallint not null, + -- Total debits is the total amount of traffic that has been debited from the account + total_debits bigint not null, + -- Total credits is the total amount of traffic that has been credited to the account + total_credits bigint not null, + -- Updated at is the timestamp of the last update to the balance. + -- It is at least as recent as the timestamp of the last event that was applied to the balance. + updated_at bigint not null, + -- One event type per account + primary key (account_id, event_type) +); + +-- Offset tables used by pekko projection +-- See https://pekko.apache.org/docs/pekko-projection/current/jdbc.html#schema +create table pekko_projection_offset_store +( + projection_name varchar not null, + projection_key varchar not null, + current_offset varchar not null, + manifest varchar not null, + mergeable boolean not null, + last_updated bigint not null, + primary key (projection_name, projection_key) +); + +create index projection_name_index on pekko_projection_offset_store (projection_name); + +create table pekko_projection_management +( + projection_name varchar not null, + projection_key varchar not null, + paused boolean not null, + last_updated bigint not null, + primary key (projection_name, projection_key) +); \ No newline at end of file diff --git a/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sha256 b/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sha256 new file mode 100644 index 0000000000..82524ad01b --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sha256 @@ -0,0 +1 @@ +327fa628515fb0a00c639d2718cefaa37ad1566203ae157e6236a5914afbbc9e diff --git a/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sql b/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sql new file mode 100644 index 0000000000..7801bf7980 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sql @@ -0,0 +1,8 @@ +drop table ord_availability_batch; +create table ord_availability_batch( + id varchar not null, + batch binary large object not null, + -- assigned at batch creation and used to calculate epoch expiration + epoch_number bigint not null, + primary key (epoch_number, id) +); diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sha256 b/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sha256 new file mode 100644 index 0000000000..18b4877df7 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sha256 @@ -0,0 +1 @@ +65699df26b7762f024355555db78f6d6e6f363fb2f2d9684fa5bcbbd09d589c4 diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sql b/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sql new file mode 100644 index 0000000000..d4c3323032 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sql @@ -0,0 +1,75 @@ +-- Traffic enforcement tables +create table par_traffic_enforcement_event +( + -- Increasing sequence number for each event + -- This is used to provide a total ordering of events + sequence_nb bigint generated always as identity, + -- Event source is a small integer that identifies the source of the event (e.g: 0 == LEDGER_API, 1 = TEA_API) + event_source smallint not null, + -- Event type is a small integer that identifies the type of event (e.g 0 == USAGE for standard traffic usage) + event_type smallint not null, + -- Event ID is a unique identifier per event_source, it is used for event deduplication + event_id varchar collate "C" not null, + -- Account ID is the identifier of the account that the event applies to + account_id varchar collate "C" not null, + -- Amount is the amount of traffic tied to the event, it can be positive or negative. + -- The actual semantics depend on the event type. + amount bigint not null, + -- Timestamp when the event was recorded + -- Critically this is NOT the record time of a transaction. It is generated by the database. + -- Note that it is possible for events to have timestamps reversed compared to their sequence number + timestamp bigint not null, + -- Events are unique per (event_source, event_id). + primary key (event_source, event_id) +); + +-- For fast querying by sequence number +create index idx_event_sequence on par_traffic_enforcement_event (sequence_nb); +-- For fast querying by account ID, event type and timestamp +create index account_id_timestamp_index on par_traffic_enforcement_event (account_id, event_type, timestamp); + +-- Balance table for each account +-- Contains one row per account, continuously updated with the total debits and credits for that account +-- as events are processed. +create table par_traffic_enforcement_balance +( + -- Account ID is the identifier of the account that the balance applies to + account_id varchar collate "C" not null, + -- Event sequence number is the sequence number of the last event that was applied to the balance + event_sequence_nb bigint not null, + -- Matches the event_type of the event table. There's one row per account and event type. + event_type smallint not null, + -- Total debits is the total amount of traffic that has been debited from the account + total_debits bigint not null, + -- Total credits is the total amount of traffic that has been credited to the account + total_credits bigint not null, + -- Updated at is the timestamp of the last update to the balance. + -- It is at least as recent as the timestamp of the last event that was applied to the balance. + updated_at bigint not null, + -- One event type per account + primary key (account_id, event_type) +); + +-- Offset tables used by pekko projection +-- See https://pekko.apache.org/docs/pekko-projection/current/jdbc.html#schema +create table pekko_projection_offset_store +( + projection_name varchar collate "C" not null, + projection_key varchar collate "C" not null, + current_offset varchar collate "C" not null, + manifest varchar collate "C" not null, + mergeable boolean not null, + last_updated bigint not null, + primary key (projection_name, projection_key) +); + +create index projection_name_index on pekko_projection_offset_store (projection_name); + +create table pekko_projection_management +( + projection_name varchar collate "C" not null, + projection_key varchar collate "C" not null, + paused boolean not null, + last_updated bigint not null, + primary key (projection_name, projection_key) +); \ No newline at end of file diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sha256 b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sha256 new file mode 100644 index 0000000000..a4578f2695 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sha256 @@ -0,0 +1 @@ +12fb43a202d6a5927ba49d647dcfc73aff99a5807f3e5d0ed506f8be09f916be diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sql b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sql new file mode 100644 index 0000000000..8b0b70b1f7 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sql @@ -0,0 +1,6 @@ +-- Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- Recreate the activate contract key index with internal_contract_id as an included column. +drop index lapi_events_activate_contract_key_idx; +create index lapi_events_activate_contract_key_idx on lapi_events_activate_contract using btree (create_key_hash, event_sequential_id) include (internal_contract_id) where create_key_hash is not null; diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sha256 b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sha256 new file mode 100644 index 0000000000..d03ff16a35 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sha256 @@ -0,0 +1 @@ +d4263917805c62bd820e184206f217fa074e727ffa7414a843a71adec87b1baa diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sql b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sql new file mode 100644 index 0000000000..07835ba25b --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sql @@ -0,0 +1,293 @@ +drop view debug.ord_epochs; +drop view debug.ord_availability_batch; +drop view debug.ord_pbft_messages_in_progress; +drop view debug.ord_pbft_messages_completed; +drop view debug.ord_metadata_output_blocks; +drop view debug.ord_metadata_output_epochs; +drop view debug.ord_leader_selection_state; + +alter table ord_epochs rename to ord_epochs_old; +alter table ord_availability_batch rename to ord_availability_batch_old; +alter table ord_pbft_messages_in_progress rename to ord_pbft_messages_in_progress_old; +alter table ord_pbft_messages_completed rename to ord_pbft_messages_completed_old; +alter table ord_metadata_output_blocks rename to ord_metadata_output_blocks_old; +alter table ord_metadata_output_epochs rename to ord_metadata_output_epochs_old; +alter table ord_leader_selection_state rename to ord_leader_selection_state_old; + +-- Stores metadata for epochs +-- Individual blocks/transactions exist in separate table +create table ord_epochs ( + -- strictly-increasing, contiguous epoch number + epoch_number bigint not null primary key, + -- first block sequence number (globally) of the epoch + start_block_number bigint not null, + -- number of total blocks in the epoch + epoch_length bigint not null, + -- Sequencing instant of the topology snapshot in force for the epoch + topology_ts bigint not null, + -- whether the epoch is in progress + in_progress bool not null +) partition by range (epoch_number); + +create table ord_availability_batch ( + id varchar collate "C" not null, + batch bytea not null, + -- assigned at batch creation and used to calculate epoch expiration + epoch_number bigint not null, + primary key (epoch_number, id) +) partition by range (epoch_number); + +-- messages stored during the progress of a block possibly across different pbft views +create table ord_pbft_messages_in_progress( + -- global sequence number of the ordered block + block_number bigint not null, + + -- epoch number of the block + epoch_number bigint not null, + + -- view number + view_number bigint not null, + + -- pbft message for the block + message bytea not null, + + -- pbft message discriminator (0 = pre-prepare, 1 = prepare, 2 = commit) + discriminator smallint not null, + + -- sender of the message + from_sequencer_id varchar collate "C" not null, + + -- for each block number, we only expect one message of each kind for the same sender and view number. + primary key (epoch_number, block_number, view_number, from_sequencer_id, discriminator) +) partition by range (epoch_number); + +create table ord_pbft_messages_completed( + -- global sequence number of the ordered block + block_number bigint not null, + + -- epoch number of the block + epoch_number bigint not null, + + -- pbft message for the block + message bytea not null, + + -- pbft message discriminator (0 = pre-prepare, 2 = commit) + discriminator smallint not null, + + -- sender of the message + from_sequencer_id varchar collate "C" not null, + + -- for each completed block number, we only expect one message of each kind for the same sender. + -- in the case of pre-prepare, we only expect one message for the whole block, but for simplicity + -- we won't differentiate that at the database level. + primary key (epoch_number, block_number, from_sequencer_id, discriminator) +) partition by range (epoch_number); + +-- Stores metadata for blocks that have been assigned timestamps in the output module +create table ord_metadata_output_blocks ( + epoch_number bigint not null, + block_number bigint not null, + bft_ts bigint not null, + primary key (epoch_number, block_number) +) partition by range (epoch_number); + +create index idx_ord_metadata_output_blocks_bft_ts on ord_metadata_output_blocks(bft_ts); +create index idx_ord_metadata_output_blocks_block_number on ord_metadata_output_blocks(block_number); + +-- Stores output metadata for epochs +create table ord_metadata_output_epochs ( + epoch_number bigint not null primary key, + could_alter_ordering_topology bool not null +) partition by range (epoch_number); + +create table ord_leader_selection_state ( + epoch_number bigint not null primary key, + state bytea not null +) partition by range (epoch_number); + +create function create_partitions_for_epoch_range( + parent_table regclass, + min_val bigint, + max_val bigint +) returns void as $$ +declare + partition_size bigint := ${initialBftOrdererTablesPartitionSize}; + current_val bigint; + from_val bigint; + next_val bigint; + final_val bigint; + partition_index bigint; + partition_name text; +begin + if partition_size <= 0 then + raise exception 'partition_size must be positive'; + end if; + + current_val := floor(min_val::numeric / partition_size)::bigint * partition_size; + + -- creates one extra partition after the partition containing max_val + final_val := max_val + partition_size; + + while current_val <= final_val loop + next_val := current_val + partition_size; + partition_index := current_val / partition_size; + partition_name := format( + '%s_p%s', + replace(parent_table::text, '.', '_'), + partition_index + ); + + -- the first partition will start at -1 + if current_val = 0 then + from_val := -1; + else + from_val := current_val; + end if; + + execute format( + 'create table if not exists %I partition of %s for values from (%L) to (%L)', + partition_name, + parent_table::text, + from_val, + next_val + ); + + current_val := next_val; + end loop; +end; +$$ language plpgsql; + +do $$ +declare + min_epoch bigint; + max_epoch bigint; +begin + -- Use a shared epoch range across ord_epochs_old and ord_availability_batch_old. + -- ord_availability_batch_old can contain epochs ahead of ord_epochs_old, + -- while other transient tables (ord_pbft_messages_in_progress) may be empty during the migration. + select min(epoch_number), max(epoch_number) + into min_epoch, max_epoch + from ( + select epoch_number from ord_epochs_old + union all + select epoch_number from ord_availability_batch_old + ) all_epochs; + + if min_epoch is null or max_epoch is null then + min_epoch := 0; + max_epoch := 0; + end if; + + perform create_partitions_for_epoch_range('ord_epochs'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_availability_batch'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_pbft_messages_in_progress'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_pbft_messages_completed'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_metadata_output_blocks'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_metadata_output_epochs'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_leader_selection_state'::regclass, min_epoch, max_epoch); +end $$; + +insert into ord_epochs (epoch_number, start_block_number, epoch_length, topology_ts, in_progress) +select epoch_number, start_block_number, epoch_length, topology_ts, in_progress +from ord_epochs_old; + +insert into ord_availability_batch (id, batch, epoch_number) +select id, batch, epoch_number +from ord_availability_batch_old; + +insert into ord_pbft_messages_in_progress (block_number, epoch_number, view_number, message, discriminator, from_sequencer_id) +select block_number, epoch_number, view_number, message, discriminator, from_sequencer_id +from ord_pbft_messages_in_progress_old; + +insert into ord_pbft_messages_completed (block_number, epoch_number, message, discriminator, from_sequencer_id) +select block_number, epoch_number, message, discriminator, from_sequencer_id +from ord_pbft_messages_completed_old; + +insert into ord_metadata_output_blocks (epoch_number, block_number, bft_ts) +select epoch_number, block_number, bft_ts +from ord_metadata_output_blocks_old; + +insert into ord_metadata_output_epochs (epoch_number, could_alter_ordering_topology) +select epoch_number, could_alter_ordering_topology +from ord_metadata_output_epochs_old; + +insert into ord_leader_selection_state (epoch_number, state) +select epoch_number, state +from ord_leader_selection_state_old; + +drop table ord_epochs_old; +drop table ord_availability_batch_old; +drop table ord_pbft_messages_in_progress_old; +drop table ord_pbft_messages_completed_old; +drop table ord_metadata_output_blocks_old; +drop table ord_metadata_output_epochs_old; +drop table ord_leader_selection_state_old; + +create table ord_partition_size_history ( + epoch_number bigint not null primary key, + partition_number bigint not null, + partition_size integer not null +); +insert into ord_partition_size_history(epoch_number, partition_number, partition_size) + values (0, 0, ${initialBftOrdererTablesPartitionSize}); + +create or replace view debug.ord_epochs as +select + epoch_number, + start_block_number, + epoch_length, + debug.canton_timestamp(topology_ts) as topology_ts, + in_progress +from ord_epochs; + +create or replace view debug.ord_availability_batch as +select + id, + batch, + epoch_number +from ord_availability_batch; + +create or replace view debug.ord_pbft_messages_in_progress as +select + block_number, + epoch_number, + view_number, + message, + discriminator, + from_sequencer_id +from ord_pbft_messages_in_progress; + +create or replace view debug.ord_pbft_messages_completed as +select + block_number, + epoch_number, + message, + discriminator, + from_sequencer_id +from ord_pbft_messages_completed; + +create or replace view debug.ord_metadata_output_blocks as +select + epoch_number, + block_number, + debug.canton_timestamp(bft_ts) as bft_ts +from ord_metadata_output_blocks; + +create or replace view debug.ord_metadata_output_epochs as +select + epoch_number, + could_alter_ordering_topology +from ord_metadata_output_epochs; + +create or replace view debug.ord_leader_selection_state as +select + epoch_number, + state +from ord_leader_selection_state; + +create or replace view debug.ord_partition_size_history as +select + epoch_number, + partition_number, + partition_size +from ord_partition_size_history; diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sha256 b/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sha256 index 005d982cdb..9b76b97111 100644 --- a/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sha256 +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sha256 @@ -1 +1 @@ -ba9abfee60cf3b9023278798a98247376a623edc14fc6873c01e0c9cdfaa3491 +33e30818cb98745e054432c6e6cd015c7160cd1918bcf136cd23a338f32c5d7b diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sql b/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sql index 6b371faf21..2ac380c719 100644 --- a/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sql +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sql @@ -55,65 +55,3 @@ alter table seq_in_flight_aggregation autovacuum_freeze_max_age = 600000000, autovacuum_freeze_table_age = 600000000 ); - --- ====== BFT ordering autovacuum settings ===== -alter table ord_epochs - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_availability_batch - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_pbft_messages_in_progress - set ( - autovacuum_vacuum_scale_factor = 0.0, - autovacuum_vacuum_threshold = 10000, - autovacuum_vacuum_cost_limit = 2000, - autovacuum_vacuum_cost_delay = 5, - autovacuum_vacuum_insert_scale_factor = 0.0, - autovacuum_vacuum_insert_threshold = 100000, - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_pbft_messages_completed - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_metadata_output_blocks - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_metadata_output_epochs - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_leader_selection_state - set ( - autovacuum_vacuum_scale_factor = 0.0, - autovacuum_vacuum_threshold = 10000, - autovacuum_vacuum_cost_limit = 2000, - autovacuum_vacuum_cost_delay = 5, - autovacuum_vacuum_insert_scale_factor = 0.0, - autovacuum_vacuum_insert_threshold = 100000, - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala index 3cf9311836..5742afb50a 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala @@ -63,7 +63,7 @@ import com.digitalasset.canton.lifecycle.{ } import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.metrics.ActiveRequestsMetrics.GrpcServerMetricsX -import com.digitalasset.canton.metrics.{DbStorageMetrics, DeclarativeApiMetrics} +import com.digitalasset.canton.metrics.{CryptoMetrics, DbStorageMetrics, DeclarativeApiMetrics} import com.digitalasset.canton.networking.grpc.{ CantonGrpcUtil, CantonMutableHandlerRegistry, @@ -181,6 +181,7 @@ trait BaseMetrics { new CacheMetrics("topology", openTelemetryMetricsFactory) def healthMetrics: HealthMetrics def storageMetrics: DbStorageMetrics + def cryptoMetrics: CryptoMetrics val declarativeApiMetrics: DeclarativeApiMetrics } @@ -492,6 +493,7 @@ abstract class CantonNodeBootstrapImpl[ ReleaseProtocolVersion.latest, arguments.futureSupervisor, arguments.clock, + arguments.metrics.cryptoMetrics, executionContext, bootstrapStageCallback.timeouts, arguments.config.parameters.batching, @@ -1268,6 +1270,7 @@ abstract class CantonNodeBootstrapImpl[ mapping, serial = None, keys, + namespacesToSignFor = Seq.empty, protocolVersion, expectFullAuthorization = true, waitToBecomeEffective = None, diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerReadService.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerReadService.scala index 644de6ea87..efa8d4a987 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerReadService.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerReadService.scala @@ -724,13 +724,15 @@ class GrpcTopologyManagerReadService( override def exportTopologySnapshot( request: ExportTopologySnapshotRequest, responseObserver: StreamObserver[ExportTopologySnapshotResponse], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient[ExportTopologySnapshotResponse]( (out: OutputStream) => getTopologySnapshot(request, out), responseObserver, byteString => ExportTopologySnapshotResponse(byteString), processingTimeout.unbounded.duration, ) + } private def getTopologySnapshot( request: ExportTopologySnapshotRequest, @@ -759,13 +761,15 @@ class GrpcTopologyManagerReadService( override def exportTopologySnapshotV2( request: ExportTopologySnapshotV2Request, responseObserver: StreamObserver[ExportTopologySnapshotV2Response], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient[ExportTopologySnapshotV2Response]( (out: OutputStream) => getTopologySnapshotV2(request, out), responseObserver, byteString => ExportTopologySnapshotV2Response(byteString), processingTimeout.unbounded.duration, ) + } private def getTopologySnapshotV2( request: ExportTopologySnapshotV2Request, @@ -844,13 +848,15 @@ class GrpcTopologyManagerReadService( override def genesisState( request: GenesisStateRequest, responseObserver: StreamObserver[GenesisStateResponse], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient( (out: OutputStream) => getGenesisState(request.synchronizerStore, request.timestamp, out), responseObserver, byteString => GenesisStateResponse(byteString), processingTimeout.unbounded.duration, ) + } private def getGenesisState( filterSynchronizerStore: Option[StoreId], @@ -867,12 +873,15 @@ class GrpcTopologyManagerReadService( override def genesisStateV2( request: GenesisStateV2Request, responseObserver: StreamObserver[GenesisStateV2Response], - ): Unit = GrpcStreamingUtils.streamToClient( - (out: OutputStream) => getGenesisStateV2(request.synchronizerStore, request.timestamp, out), - responseObserver, - byteString => GenesisStateV2Response(byteString), - processingTimeout.unbounded.duration, - ) + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + GrpcStreamingUtils.streamToClient( + (out: OutputStream) => getGenesisStateV2(request.synchronizerStore, request.timestamp, out), + responseObserver, + byteString => GenesisStateV2Response(byteString), + processingTimeout.unbounded.duration, + ) + } private def getGenesisStateV2( filterSynchronizerStore: Option[StoreId], @@ -949,13 +958,16 @@ class GrpcTopologyManagerReadService( override def sequencerLsuState( request: SequencerLsuStateRequest, responseObserver: StreamObserver[SequencerLsuStateResponse], - ): Unit = GrpcStreamingUtils.streamToClient( - (out: OutputStream) => - getLogicalUpgradeState(request.synchronizerStore, request.timestamp, out), - responseObserver, - byteString => SequencerLsuStateResponse(byteString), - processingTimeout.unbounded.duration, - ) + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + GrpcStreamingUtils.streamToClient( + (out: OutputStream) => + getLogicalUpgradeState(request.synchronizerStore, request.timestamp, out), + responseObserver, + byteString => SequencerLsuStateResponse(byteString), + processingTimeout.unbounded.duration, + ) + } private def getLogicalUpgradeState( synchronizerStore: Option[StoreId], diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala index e76da95d8b..d1e94e1007 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala @@ -148,6 +148,7 @@ class GrpcTopologyManagerWriteService( validatedMapping, serial, signingKeys, + namespacesToSignFor = Seq.empty, manager.managerVersion.serialization, expectFullAuthorization = mustFullyAuthorize, forceChanges = forceChanges, @@ -184,7 +185,7 @@ class GrpcTopologyManagerWriteService( extendedTransactions <- signedTxs.parTraverse(tx => targetManager - .extendSignature(tx, signingKeys, forceFlags) + .extendSignature(tx, signingKeys, namespacesToSignFor = Seq.empty, forceFlags) .leftWiden[RpcError] ) } yield extendedTransactions diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala index e6b78de10d..c5cfbd1aa5 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala @@ -7,8 +7,10 @@ import better.files.* import better.files.File.newTemporaryFile import com.digitalasset.canton.ProtoDeserializationError import com.digitalasset.canton.config.DefaultProcessingTimeouts +import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.grpc.ByteStringStreamObserverWithContext import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.thereafterFutureUnlessShutdown import com.digitalasset.canton.lifecycle.UnlessShutdown.{AbortedDueToShutdown, Outcome} import com.digitalasset.canton.logging.ErrorLoggingContext import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors @@ -21,28 +23,22 @@ import com.digitalasset.canton.version.{ VersioningCompanion, } import com.google.protobuf.ByteString -import io.grpc.Context -import io.grpc.stub.StreamObserver +import io.grpc.stub.{ServerCallStreamObserver, StreamObserver} +import io.grpc.{Context, Status} import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream import org.apache.pekko.NotUsed import org.apache.pekko.stream.scaladsl.{Source, Source as PekkoSource} -import java.io.{ - BufferedInputStream, - ByteArrayInputStream, - ByteArrayOutputStream, - InputStream, - OutputStream, - PipedInputStream, - PipedOutputStream, -} +import java.io.{InputStream, OutputStream, PipedInputStream, PipedOutputStream} import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} import scala.annotation.tailrec -import scala.concurrent.duration.Duration +import scala.concurrent.duration.{Duration, DurationInt} import scala.concurrent.{Await, ExecutionContext, Future, Promise, blocking} +import scala.util.control.NonFatal import scala.util.{Failure, Success, Try} object GrpcStreamingUtils { + private[util] final val defaultChunkSize: Int = 1024 * 1024 * 2 // 2MB - This is half of the default max message size of gRPC @@ -313,22 +309,54 @@ object GrpcStreamingUtils { fromByteString: FromByteString[T], processingTimeout: Duration = DefaultProcessingTimeouts.unbounded.duration, chunkSizeO: Option[Int] = None, - )(implicit ec: ExecutionContext): Unit = { - val context = io.grpc.Context - .current() - .withCancellation() + )(implicit ec: ExecutionContext, loggingContext: ErrorLoggingContext): Unit = { + val context = io.grpc.Context.current().withCancellation() + val chunkSize = chunkSizeO.getOrElse(defaultChunkSize) + val pipedInput = new PipedInputStream(chunkSize) + // Introduced so that clients do not have to enclose calls in blocking + val blockingPipedOutput = new PipedOutputStream(pipedInput) { + override def write(b: Int): Unit = blocking(super.write(b)) + + override def write(b: Array[Byte], off: Int, len: Int): Unit = blocking { + super.write(b, off, len) + } + + override def write(b: Array[Byte]): Unit = blocking(super.write(b)) + } + + def closeQuietly(c: java.io.Closeable): Unit = Try(c.close()).discard - val outputStream = new ByteArrayOutputStream() context.run { () => - val processingResult = responseF(outputStream).map { _ => - val chunkSize = chunkSizeO.getOrElse(defaultChunkSize) - val inputStream = new ByteArrayInputStream(outputStream.toByteArray) + // Close both piped streams on context cancellation so the producer unblocks promptly + context.addListener( + (_: Context) => { + closeQuietly(pipedInput) + closeQuietly(blockingPipedOutput) + }, + (command: Runnable) => command.run(), + ) + + val consumerF = streamResponseChunks(context, responseObserver)( - new BufferedInputStream(inputStream), + pipedInput, chunkSize, fromByteString, ) + + val producerF = responseF(blockingPipedOutput).transform { result => + val closeResult = Try(blockingPipedOutput.close()) + result match { + case Success(_) => closeResult + case Failure(ex) => + closeResult.forFailed(ex.addSuppressed(_)) + result + } } + + val processingResult = producerF + .flatMap(_ => consumerF) + .thereafter(_ => closeQuietly(pipedInput)) + finishStream(context, responseObserver)(processingResult, processingTimeout) } } @@ -339,7 +367,7 @@ object GrpcStreamingUtils { fromByteString: FromByteString[T], processingTimeout: Duration = DefaultProcessingTimeouts.unbounded.duration, chunkSizeO: Option[Int] = None, - )(implicit ec: ExecutionContext): Unit = { + )(implicit ec: ExecutionContext, loggingContext: ErrorLoggingContext): Unit = { val file = newTemporaryFile() val context = io.grpc.Context @@ -347,7 +375,7 @@ object GrpcStreamingUtils { .withCancellation() context.run { () => - val processingResult = responseF(file).map { _ => + val processingResult = responseF(file).flatMap { _ => val chunkSize = chunkSizeO.getOrElse(defaultChunkSize) streamResponseChunks(context, responseObserver)( file.newInputStream.buffered(chunkSize), @@ -415,42 +443,123 @@ object GrpcStreamingUtils { read(Nil) } - private def streamResponseChunks[T]( + private[util] def streamResponseChunks[T]( context: Context.CancellableContext, responseObserver: StreamObserver[T], )( inputStream: InputStream, chunkSize: Int, fromByteString: FromByteString[T], - ): Unit = - inputStream.autoClosed { s => - Iterator - .continually(s.readNBytes(chunkSize)) + )(implicit + executionContext: ExecutionContext, + loggingContext: ErrorLoggingContext, + ): Future[Unit] = + withServerCallStreamObserverF(responseObserver) { scso => + val iter = Iterator + .continually(inputStream.readNBytes(chunkSize)) // Before pushing new chunks to the stream, keep checking that the context has not been cancelled // This avoids the server reading the entire dump file for nothing if the client has already cancelled .takeWhile(_.nonEmpty && !context.isCancelled) - .foreach { byteArray => - val chunk: ByteString = ByteString.copyFrom(byteArray) - responseObserver.onNext(fromByteString.toT(chunk)) - } + + val isWorkerRunning = new AtomicBoolean(false) + val allBytesWrittenPromise = Promise[Unit]() + + def runWorkerInBackground(): Unit = FutureUtil.doNotAwait( + Future { + // Returns true if the stream was exhausted (EOF reached), false if the loop stopped + // because the observer is no longer ready + Try { + @tailrec + def sendChunks(): Boolean = + if (!scso.isReady) { + false + } else if (iter.hasNext) { + val byteArray = iter.next() + val chunk: ByteString = ByteString.copyFrom(byteArray) + responseObserver.onNext(fromByteString.toT(chunk)) + sendChunks() + } else { + true + } + sendChunks() + } match { + case Failure(ex) => + isWorkerRunning.set(false) + allBytesWrittenPromise.tryFailure(ex).discard + case Success(exhausted) => + if (exhausted) { + allBytesWrittenPromise.trySuccess(()).discard + } + isWorkerRunning.set(false) + if (!exhausted) { + if (context.isCancelled) { + allBytesWrittenPromise.trySuccess(()).discard + } else if (scso.isReady) { + // this check is important in case the `onReadyHandler` gets triggered before setting `isWorkerRunning` to `false` above. Not triggering here would mean that the stream will stall. + triggerWorker() + } else { + // Fix for misbehaving observer - that never triggers onReady/onCancel + DelayUtil + .delay(100.millis) + .foreach(_ => triggerWorker().discard) + } + } + } + }, + "Cannot start streaming response to client", + ) + + def triggerWorker(): Future[Unit] = Future { + // Do not start a new worker once streaming is finished or the client has gone away, + // to avoid spinning up workers (and rescheduling them) for an already-completed stream. + if (context.isCancelled) { + allBytesWrittenPromise.trySuccess(()).discard + } else if ( + !allBytesWrittenPromise.isCompleted && isWorkerRunning + .compareAndSet(false, true) + ) + runWorkerInBackground() + } + + scso.setOnReadyHandler(() => triggerWorker().discard) + scso.setOnCancelHandler { () => + Try(inputStream.close()).discard + allBytesWrittenPromise.trySuccess(()).discard + } + + triggerWorker().discard + + allBytesWrittenPromise.future.thereafter(_ => Try(inputStream.close()).discard) } private def finishStream[T]( context: Context.CancellableContext, responseObserver: StreamObserver[T], - )(f: Future[Unit], timeout: Duration): Unit = - Try(Await.result(f, timeout)) match { - case Failure(exception) => + )(f: Future[Unit], timeout: Duration): Unit = { + def cancelContext(): Unit = { + context.cancel(new io.grpc.StatusRuntimeException(io.grpc.Status.CANCELLED)) + () + } + + try { + Await.result(f, timeout) + if (!context.isCancelled) responseObserver.onCompleted() + else cancelContext() + } catch { + case _: InterruptedException => + // The serving thread was interrupted (e.g. node shutdown or call cancellation) while blocked + // in `Await.result`. Cancel the context to terminate the gRPC call exactly once (CANCELLED), + // and only restore the thread's interrupt status. We must NOT rethrow: letting the + // InterruptedException escape into the gRPC request handler makes it close the call a second + // time, writing to the already-terminated HTTP/2 stream, which Netty logs as + // "Stream closed before write could take place". + cancelContext() + Thread.currentThread().interrupt() + case NonFatal(exception) => responseObserver.onError(exception) - context.cancel(new io.grpc.StatusRuntimeException(io.grpc.Status.CANCELLED)) - () - case Success(_) => - if (!context.isCancelled) responseObserver.onCompleted() - else { - context.cancel(new io.grpc.StatusRuntimeException(io.grpc.Status.CANCELLED)) - () - } + cancelContext() } + } def futureUnlessShutdownToStreamObserver[A]( fus: FutureUnlessShutdown[A], @@ -463,6 +572,58 @@ object GrpcStreamingUtils { case Success(AbortedDueToShutdown) => observer.onError(GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) } + + private def withServerCallStreamObserverG[R, A]( + observer: StreamObserver[R] + )(ifNotSupported: => A)( + handler: ServerCallStreamObserver[R] => A + )(implicit errorLoggingContext: ErrorLoggingContext): A = + observer match { + case serverCallStreamObserver: ServerCallStreamObserver[R] => + handler(serverCallStreamObserver) + case other => + val statusException = + Status.INTERNAL + .withDescription(s"Unknown stream observer request") + .asException() + errorLoggingContext.warn( + s"${statusException.getMessage} StreamObserver:(${other.getClass})", + statusException, + ) + observer.onError(statusException) + ifNotSupported + } + + /** Ensure the observer is a ServerCallStreamObserver, running `handler` if so. Otherwise reports + * an INTERNAL error to the observer. See `withServerCallStreamObserverG`. + * + * @param observer + * underlying observer + * @param handler + * handler requiring a ServerCallStreamObserver + */ + def withServerCallStreamObserver[R]( + observer: StreamObserver[R] + )(handler: ServerCallStreamObserver[R] => Unit)(implicit + errorLoggingContext: ErrorLoggingContext + ): Unit = + withServerCallStreamObserverG(observer)(())(handler) + + /** Ensure the observer is a ServerCallStreamObserver, running `handler` if so. Otherwise reports + * an INTERNAL error to the observer and returns a completed future. See + * `withServerCallStreamObserverG`. + * + * @param observer + * underlying observer + * @param handler + * handler requiring a ServerCallStreamObserver + */ + def withServerCallStreamObserverF[R]( + observer: StreamObserver[R] + )(handler: ServerCallStreamObserver[R] => Future[Unit])(implicit + errorLoggingContext: ErrorLoggingContext + ): Future[Unit] = + withServerCallStreamObserverG(observer)(Future.unit)(handler) } // Define a type class for converting ByteString to the generic type T diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/kms/KmsTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/kms/KmsTest.scala index fc7687c806..9b07fb6e33 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/kms/KmsTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/kms/KmsTest.scala @@ -38,6 +38,7 @@ import com.digitalasset.canton.crypto.{ SigningPublicKey, } import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.Thereafter.syntax.* import com.digitalasset.canton.util.{ByteString190, ByteString256, ByteString4096} @@ -94,6 +95,7 @@ trait KmsTest extends BaseTest with BeforeAndAfterAll { SessionEncryptionKeyCacheConfig(), CachingConfigs.defaultPublicKeyConversionCache, CryptoSchemes.tryFromConfig(config), + CommonMockMetrics.cryptoMetrics, loggerFactory, ) .valueOrFail("create crypto with JCE provider") diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/jce/JceCryptoTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/jce/JceCryptoTest.scala index ec5311cce1..b0ea5359ff 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/jce/JceCryptoTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/jce/JceCryptoTest.scala @@ -19,6 +19,7 @@ import com.digitalasset.canton.crypto.CryptoKeyFormat.Raw import com.digitalasset.canton.crypto.CryptoTestHelper.TestMessage import com.digitalasset.canton.crypto.SigningKeySpec.EcSecp256k1 import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage import com.digitalasset.canton.tracing.NoReportingTracerProvider @@ -61,6 +62,7 @@ class JceCryptoTest testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executionContext, timeouts, BatchingConfig(), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/kms/KmsCryptoTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/kms/KmsCryptoTest.scala index a391d86633..afa455d0b4 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/kms/KmsCryptoTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/kms/KmsCryptoTest.scala @@ -21,6 +21,7 @@ import com.digitalasset.canton.config.{ import com.digitalasset.canton.crypto.* import com.digitalasset.canton.crypto.kms.Kms import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage import com.digitalasset.canton.tracing.NoReportingTracerProvider @@ -87,6 +88,7 @@ trait KmsCryptoTest testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executorService, timeouts, BatchingConfig(), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoTest.scala index ee1335ca74..dc56ab8381 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoTest.scala @@ -28,6 +28,7 @@ import com.digitalasset.canton.crypto.{ TestHash, } import com.digitalasset.canton.lifecycle.* +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage @@ -139,6 +140,7 @@ trait SyncCryptoTest testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executorService, timeouts, BatchingConfig(), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithLongTermKeysTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithLongTermKeysTest.scala index 47850f6612..aaf6c10867 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithLongTermKeysTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithLongTermKeysTest.scala @@ -39,7 +39,7 @@ class SyncCryptoWithLongTermKeysTest TestingTopology() .withSimpleParticipants(participant1) .withCryptoConfig( - cryptoConfigWithSessionSigningKeysConfig(SessionSigningKeysConfig.default) + cryptoConfigWithSessionSigningKeysConfig(SessionSigningKeysConfig.enabled) ) .build(crypto, loggerFactory) diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithSessionKeysTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithSessionKeysTest.scala index 9f90314c18..a78b14fa16 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithSessionKeysTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithSessionKeysTest.scala @@ -36,7 +36,7 @@ class SyncCryptoWithSessionKeysTest with ProtocolVersionChecksAnyWordSpec { override protected lazy val sessionSigningKeysConfig: SessionSigningKeysConfig = - if (testedProtocolVersion >= ProtocolVersion.v35) SessionSigningKeysConfig.default + if (testedProtocolVersion >= ProtocolVersion.v35) SessionSigningKeysConfig.enabled else SessionSigningKeysConfig.disabled private var p1PV34: SynchronizerCryptoClient = _ @@ -601,7 +601,7 @@ class SyncCryptoWithSessionKeysTest val signature = loggerFactory.assertLoggedWarningsAndErrorsSeq( { - p1PV34 = createTestingTopologyWith(SessionSigningKeysConfig.default) + p1PV34 = createTestingTopologyWith(SessionSigningKeysConfig.enabled) .forOwnerAndSynchronizer(participant1) p1PV34.syncCryptoSigner.isInstanceOf[SyncCryptoSignerWithLongTermKeys] shouldBe true p1PV34.syncCryptoSigner diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/validations/SyncSchemeValidationsTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/validations/SyncSchemeValidationsTest.scala index ed5ad45a4b..0a1b1df707 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/validations/SyncSchemeValidationsTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/validations/SyncSchemeValidationsTest.scala @@ -16,6 +16,7 @@ import com.digitalasset.canton.config.{ } import com.digitalasset.canton.crypto.* import com.digitalasset.canton.crypto.store.CryptoPrivateStoreExtended +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage @@ -48,6 +49,7 @@ class SyncSchemeValidationsTest extends AnyWordSpec with BaseTest with HasExecut testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executorService, timeouts, BatchingConfig(), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala index 5def0ffae6..462e15a3ac 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala @@ -1822,7 +1822,7 @@ final class SequencerClientTest override protected def loggerFactory: NamedLoggerFactory = SequencerClientTest.this.loggerFactory - override def physicalSynchronizerIdO: Option[PhysicalSynchronizerId] = ??? + override def physicalSynchronizerIdO: Option[PhysicalSynchronizerId] = None override def staticSynchronizerParametersO: Option[StaticSynchronizerParameters] = ??? @@ -1884,6 +1884,8 @@ final class SequencerClientTest )(implicit traceContext: TraceContext ): Either[SequencerConnectionPoolError.ThresholdUnreachableError, Unit] = Either.unit + + override val metricsContext: MetricsContext = MetricsContext.Empty } private object MockPool { diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/TestSequencerClientSend.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/TestSequencerClientSend.scala index 7e136a22ea..0f1ac98fee 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/TestSequencerClientSend.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/TestSequencerClientSend.scala @@ -7,6 +7,7 @@ import cats.data.{EitherT, Nested} import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.protocol.messages.DefaultOpenEnvelope +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.client.TestSequencerClientSend.Request import com.digitalasset.canton.sequencing.protocol.{ @@ -43,6 +44,7 @@ class TestSequencerClientSend(override protected[canton] val clock: Clock)(impli messageId: MessageId, aggregationRule: Option[AggregationRule], callback: SendCallback, + trafficCostValidator: TrafficCostValidator, amplify: Boolean, useConfirmationResponseAmplificationParameters: Boolean, )(implicit diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/ConnectionPoolTestHelpers.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/ConnectionPoolTestHelpers.scala index f0f29ae5a7..3e90226379 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/ConnectionPoolTestHelpers.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/ConnectionPoolTestHelpers.scala @@ -222,6 +222,7 @@ trait ConnectionPoolTestHelpers { poolDelays: SequencerConnectionPoolDelays = SequencerConnectionPoolDelays.default, blockValidation: Int => Boolean = _ => false, metrics: SequencerConnectionPoolMetrics = CommonMockMetrics.sequencerClient.connectionPool, + metricsContext: MetricsContext = MetricsContext.Empty, namePrefix: String = "test", )( f: ( @@ -251,6 +252,7 @@ trait ConnectionPoolTestHelpers { testCrypto.crypto, Some(seedForRandomness), metrics = metrics, + metricsContext = metricsContext, futureSupervisor, testTimeouts, loggerFactory, @@ -286,7 +288,7 @@ trait ConnectionPoolTestHelpers { sequencerSubscriptionFactory = new TestSequencerSubscriptionFactory(timeouts, loggerFactory), subscriptionHandlerFactory = TestSubscriptionHandlerFactory, metrics = CommonMockMetrics.sequencerClient.connectionPool, - metricsContext = MetricsContext.Empty, + metricsContext = connectionPool.metricsContext, timeouts = timeouts, loggerFactory = loggerFactory, ) @@ -450,6 +452,7 @@ protected object ConnectionPoolTestHelpers { crypto: Crypto, seedForRandomnessO: Option[Long], metrics: SequencerConnectionPoolMetrics, + metricsContext: MetricsContext, futureSupervisor: FutureSupervisor, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, @@ -462,6 +465,7 @@ protected object ConnectionPoolTestHelpers { responsesForConnection, validationBlocker, metrics, + metricsContext, futureSupervisor, timeouts, loggerFactory, @@ -489,7 +493,7 @@ protected object ConnectionPoolTestHelpers { crypto, seedForRandomnessO, metrics, - MetricsContext.Empty, + metricsContext, futureSupervisor, timeouts, loggerFactory, @@ -514,6 +518,7 @@ protected object ConnectionPoolTestHelpers { responsesForConnection: PartialFunction[Int, TestResponses], validationBlocker: TestValidationBlocker, metrics: SequencerConnectionPoolMetrics, + metricsContext: MetricsContext, futureSupervisor: FutureSupervisor, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, @@ -559,7 +564,7 @@ protected object ConnectionPoolTestHelpers { ClientChannelParams.ForTesting, stubFactory = stubFactory, metrics = metrics, - metricsContext = MetricsContext.Empty, + metricsContext = metricsContext, futureSupervisor = futureSupervisor, timeouts = timeouts, loggerFactory = loggerFactory.append("connection", config.name), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImplTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImplTest.scala index 257bda3ecf..999e4e85be 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImplTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImplTest.scala @@ -749,6 +749,9 @@ class SequencerConnectionPoolImplTest } } + // Closing the pool closes all the metrics + poolMetrics.connectionHealthMetrics shouldBe empty + withConnectionPool( nbConnections = PositiveInt.three, trustThreshold = PositiveInt.two, @@ -756,15 +759,6 @@ class SequencerConnectionPoolImplTest metrics = poolMetrics, // reuse the same metrics namePrefix = "second-config", ) { (pool, _, _, _) => - // when creating a new pool for the same synchronizer, the same metrics are reused and - // the connections from the first connect are still in there. - // this means, disconnecting from a synchronizer keeps the metric in fatal state - poolMetrics.connectionHealthMetrics should not be empty - forAll(currentMetrics) { case (name, value) => - name should startWith("first-config") - value shouldBe 0 // fatal - } - pool.start().futureValueUS.value // the pool startup removes all previous (lingering) metrics and creates new ones @@ -777,6 +771,51 @@ class SequencerConnectionPoolImplTest } } } + + "clean up only the metrics associated to a pool" in { + val poolMetrics = new SequencerClientMetrics( + new SequencerClientHistograms(MetricName("test"))(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty).connectionPool + + def currentMetricsPsids = + poolMetrics.connectionHealthMetrics.map { case (mc, _gauge) => mc.labels.get("psid") }.toSet + def currentMetricsSize = poolMetrics.connectionHealthMetrics.size + + withConnectionPool( + nbConnections = PositiveInt.three, + trustThreshold = PositiveInt.three, + i => mkConnectionAttributes(synchronizerIndex = 1, sequencerIndex = i + 1), + metrics = poolMetrics, // reuse the same metrics + metricsContext = MetricsContext("psid" -> "psid1"), + namePrefix = "first-config", + ) { (pool1, _, _, _) => + pool1.start().futureValueUS.value + + currentMetricsSize shouldBe 3 + currentMetricsPsids shouldBe Set(Some("psid1")) + + withConnectionPool( + nbConnections = PositiveInt.two, + trustThreshold = PositiveInt.two, + i => mkConnectionAttributes(synchronizerIndex = 1, sequencerIndex = i + 1), + metrics = poolMetrics, // reuse the same metrics + metricsContext = MetricsContext("psid" -> "psid2"), + namePrefix = "second-config", + ) { (pool2, _, _, _) => + pool2.start().futureValueUS.value + + currentMetricsSize shouldBe 5 + currentMetricsPsids shouldBe Set(Some("psid1"), Some("psid2")) + } + + currentMetricsSize shouldBe 3 + currentMetricsPsids shouldBe Set(Some("psid1")) + } + + currentMetricsSize shouldBe 0 + currentMetricsPsids shouldBe Set() + } } private def badBootstrapAssertion( diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala index 3a3a222a08..d6e26ba598 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala @@ -44,7 +44,7 @@ trait DbTest protected def cleanDb(storage: DbStorage)(implicit tc: TraceContext): FutureUnlessShutdown[?] @SuppressWarnings(Array("org.wartremover.warts.Var", "org.wartremover.warts.Null")) - private var setup: DbStorageSetup = _ + protected var setup: DbStorageSetup = _ /** Stores the db storage implementation. Will throw if accessed before the test has started */ protected lazy val storage: DbStorageIdempotency = { diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetectionTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetectionTest.scala index ef2a31b175..0c297585cb 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetectionTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetectionTest.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.topology.store.* import com.digitalasset.canton.topology.store.TopologyStoreId.SynchronizerStore import com.digitalasset.canton.topology.store.memory.InMemoryTopologyStore import com.digitalasset.canton.topology.transaction.* +import com.digitalasset.canton.topology.transaction.ParticipantPermission.Submission import com.digitalasset.canton.topology.transaction.TopologyChangeOp.Replace import com.digitalasset.canton.{BaseTest, HasExecutionContext} import org.scalatest.wordspec.AnyWordSpec @@ -62,13 +63,25 @@ class TopologyManagerSigningKeyDetectionTest .futureValueUS detector - .getValidSigningKeysForTransaction(ts(1), dtc_uid1a, None, returnAllValidKeys = false) + .getValidSigningKeysForTransaction( + ts(1), + dtc_uid1a, + None, + returnAllValidKeys = false, + namespacesToSignFor = Seq.empty, + ) .map(_._2) .futureValueUS shouldBe Right(Seq(SigningKeys.key3.fingerprint)) // test getting all valid keys detector - .getValidSigningKeysForTransaction(ts(1), dtc_uid1a, None, returnAllValidKeys = true) + .getValidSigningKeysForTransaction( + ts(1), + dtc_uid1a, + None, + returnAllValidKeys = true, + namespacesToSignFor = Seq.empty, + ) .futureValueUS .value ._2 should contain theSameElementsAs Seq( @@ -94,7 +107,13 @@ class TopologyManagerSigningKeyDetectionTest loggerFactory.assertLoggedWarningsAndErrorsSeq( detector - .getValidSigningKeysForTransaction(ts(2), dtc_uid1a, None, returnAllValidKeys = false) + .getValidSigningKeysForTransaction( + ts(2), + dtc_uid1a, + None, + returnAllValidKeys = false, + namespacesToSignFor = Seq.empty, + ) .map(_._2) .futureValueUS shouldBe Right( Seq(SigningKeys.key1.fingerprint) @@ -112,6 +131,66 @@ class TopologyManagerSigningKeyDetectionTest ) } + "respsect the requested authorization scope" in { + val detector = mk() + + detector.store + .update( + SequencedTime(ts(0)), + EffectiveTime(ts(0)), + removals = Map.empty, + additions = Seq(ns1k1_k1, ns2k2_k2, ns3k3_k3).map(ValidatedTopologyTransaction(_)), + ) + .futureValueUS + + val ptp = PartyToParticipant.tryCreate( + PartyId.tryCreate("alice", ns1), + PositiveInt.one, + Seq( + HostingParticipant(ParticipantId(UniqueIdentifier.tryCreate("p2", ns2)), Submission), + HostingParticipant(ParticipantId(UniqueIdentifier.tryCreate("p3", ns3)), Submission), + ), + ) + + detector + .getValidSigningKeysForTransaction( + ts(1), + TopologyTransaction( + TopologyChangeOp.Replace, + PositiveInt.one, + ptp, + testedProtocolVersion, + ), + None, + namespacesToSignFor = Seq(ns2), + returnAllValidKeys = false, + ) + .map(_._2) + .futureValueUS shouldBe Right(Seq(SigningKeys.key2.fingerprint)) + + // test getting all valid keys + detector + .getValidSigningKeysForTransaction( + ts(1), + TopologyTransaction( + TopologyChangeOp.Replace, + PositiveInt.one, + ptp, + testedProtocolVersion, + ), + None, + namespacesToSignFor = Seq.empty, + returnAllValidKeys = true, + ) + .futureValueUS + .value + ._2 should contain theSameElementsAs Seq( + SigningKeys.key1, + SigningKeys.key2, + SigningKeys.key3, + ).map(_.fingerprint) + } + "resolves decentralized namespace definitions for finding appropriate signing keys" in { val detector = mk() @@ -140,7 +219,13 @@ class TopologyManagerSigningKeyDetectionTest .futureValueUS detector - .getValidSigningKeysForTransaction(ts(1), otk, None, returnAllValidKeys = false) + .getValidSigningKeysForTransaction( + ts(1), + otk, + None, + returnAllValidKeys = false, + namespacesToSignFor = Seq.empty, + ) .futureValueUS .value ._2 should contain theSameElementsAs Seq( @@ -151,7 +236,13 @@ class TopologyManagerSigningKeyDetectionTest ).map(_.fingerprint) detector - .getValidSigningKeysForTransaction(ts(1), otk, None, returnAllValidKeys = true) + .getValidSigningKeysForTransaction( + ts(1), + otk, + None, + returnAllValidKeys = true, + namespacesToSignFor = Seq.empty, + ) .futureValueUS .value ._2 should contain theSameElementsAs Seq( diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/DownloadTopologyStateForInitializationServiceTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/DownloadTopologyStateForInitializationServiceTest.scala index 41728c80bc..1187a4d9d3 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/DownloadTopologyStateForInitializationServiceTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/DownloadTopologyStateForInitializationServiceTest.scala @@ -12,7 +12,7 @@ import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime import com.digitalasset.canton.topology.store.StoredTopologyTransactions.GenericStoredTopologyTransactions import com.digitalasset.canton.topology.store.TopologyStoreId.SynchronizerStore import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.GenericSignedTopologyTransaction -import com.digitalasset.canton.{FailOnShutdown, HasActorSystem} +import com.digitalasset.canton.{FailOnShutdown, HasActorSystem, HasExecutionContext} import org.apache.pekko.NotUsed import org.apache.pekko.stream.scaladsl.{Sink, Source} import org.scalatest.wordspec.AsyncWordSpec @@ -21,7 +21,8 @@ trait DownloadTopologyStateForInitializationServiceTest extends AsyncWordSpec with TopologyStoreTestBase with FailOnShutdown - with HasActorSystem { + with HasActorSystem + with HasExecutionContext { protected def mkStore( synchronizerId: PhysicalSynchronizerId, @@ -29,7 +30,7 @@ trait DownloadTopologyStateForInitializationServiceTest ): TopologyStore[SynchronizerStore] private val testData = - new TopologyStoreTestData(testedProtocolVersion, loggerFactory, executionContext) + new TopologyStoreTestData(testedProtocolVersion, loggerFactory) import testData.* val bootstrapTransactions = StoredTopologyTransactions( diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTest.scala index cd9abc9f72..746988f9a0 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTest.scala @@ -30,7 +30,7 @@ import com.digitalasset.canton.topology.transaction.TopologyMapping.Code import com.digitalasset.canton.topology.transaction.{TopologyMapping, *} import com.digitalasset.canton.util.MonadUtil import com.digitalasset.canton.version.ProtocolVersion -import com.digitalasset.canton.{FailOnShutdown, HasActorSystem} +import com.digitalasset.canton.{FailOnShutdown, HasActorSystem, HasExecutionContext} import org.apache.pekko.stream.scaladsl.Sink import org.scalatest.Assertion import org.scalatest.wordspec.AsyncWordSpec @@ -40,12 +40,13 @@ trait TopologyStoreTest extends AsyncWordSpec with TopologyStoreTestBase with FailOnShutdown - with HasActorSystem { + with HasActorSystem + with HasExecutionContext { implicit def closeContext: CloseContext private[store] val testData = - new TopologyStoreTestData(testedProtocolVersion, loggerFactory, executionContext) + new TopologyStoreTestData(testedProtocolVersion, loggerFactory) import testData.* private[store] lazy val largeTestSnapshot = { diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTestData.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTestData.scala index e0a5e5c7c2..e760154ade 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTestData.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTestData.scala @@ -20,6 +20,7 @@ import com.digitalasset.canton.topology.transaction.* import com.digitalasset.canton.topology.transaction.DelegationRestriction.CanSignAllMappings import com.digitalasset.canton.topology.transaction.ParticipantPermission.Submission import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.GenericSignedTopologyTransaction +import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.version.ProtocolVersion import org.scalatest.Assertions.fail import org.scalatest.concurrent.ScalaFutures.convertScalaFuture @@ -31,15 +32,13 @@ import scala.concurrent.ExecutionContext class TopologyStoreTestData( testedProtocolVersion: ProtocolVersion, loggerFactory: NamedLoggerFactory, - executionContext: ExecutionContext, -) { +)(implicit executionContext: ExecutionContext) { def makeSignedTx[Op <: TopologyChangeOp, M <: TopologyMapping]( mapping: M, op: Op = TopologyChangeOp.Replace, isProposal: Boolean = false, serial: PositiveInt = PositiveInt.one, )(signingKeys: SigningPublicKey*): SignedTopologyTransaction[Op, M] = { - import com.digitalasset.canton.tracing.TraceContext.Implicits.Empty.* val tx = TopologyTransaction( op, serial, @@ -57,7 +56,7 @@ class TopologyStoreTestData( .from( keysWithUsage.toSeq.map { case (keyId, usage) => factory.syncCryptoClient.crypto.privateCrypto - .sign(tx.hash.hash, keyId, usage) + .sign(tx.hash.hash, keyId, usage)(executionContext, TraceContext.empty) .value .onShutdown(fail("shutdown"))( DirectExecutionContext(loggerFactory.getLogger(this.getClass)) @@ -67,7 +66,6 @@ class TopologyStoreTestData( } ) .getOrElse(fail("no keys provided")) - SignedTopologyTransaction.withSignatures[Op, M]( tx, signatures = signatures, diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficPurchasedSubmissionHandlerTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficPurchasedSubmissionHandlerTest.scala index 575307e710..bcf81d5c4d 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficPurchasedSubmissionHandlerTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficPurchasedSubmissionHandlerTest.scala @@ -18,6 +18,7 @@ import com.digitalasset.canton.protocol.messages.{ } import com.digitalasset.canton.protocol.{DynamicSynchronizerParameters, SynchronizerParameters} import com.digitalasset.canton.sequencing.TrafficControlParameters +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.client.{ SendAsyncClientError, @@ -96,6 +97,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], aggregationRuleCapture.capture(), callbackCapture.capture(), + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -175,6 +177,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], any[Option[AggregationRule]], callbackCapture.capture(), + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -231,6 +234,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -265,6 +269,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], any[Option[AggregationRule]], callbackCapture.capture(), + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -325,6 +330,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], any[Option[AggregationRule]], callbackCapture.capture(), + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/util/GrpcStreamingUtilsTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/util/GrpcStreamingUtilsTest.scala index f7f26c70e6..cfc0571763 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/util/GrpcStreamingUtilsTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/util/GrpcStreamingUtilsTest.scala @@ -3,15 +3,18 @@ package com.digitalasset.canton.util -import com.digitalasset.canton.BaseTest -import io.grpc.stub.StreamObserver +import com.digitalasset.canton.{BaseTest, HasExecutionContext} +import com.google.protobuf.ByteString +import io.grpc.stub.{ServerCallStreamObserver, StreamObserver} import org.scalatest.wordspec.AnyWordSpec -import java.io.ByteArrayInputStream -import java.util.concurrent.atomic.AtomicInteger +import java.io.{ByteArrayInputStream, OutputStream} +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicReference} +import java.util.concurrent.{CountDownLatch, TimeUnit} import scala.collection.mutable +import scala.concurrent.{Future, Promise, blocking} -final class GrpcStreamingUtilsTest extends AnyWordSpec with BaseTest { +final class GrpcStreamingUtilsTest extends AnyWordSpec with BaseTest with HasExecutionContext { // we need to use the same value as in GrpcStreamingUtils since it's not configurable val defaultChunkSize = GrpcStreamingUtils.defaultChunkSize private def load( @@ -27,6 +30,54 @@ final class GrpcStreamingUtilsTest extends AnyWordSpec with BaseTest { } def makeRequest(bytes: Array[Byte]): String = new String(bytes) + /** Base test double for [[ServerCallStreamObserver]] with no-op implementations of the + * flow-control and lifecycle callbacks. Tests override only what they care about (typically + * `onNext` and the readiness behavior). + */ + private class TestServerCallStreamObserver[T] extends ServerCallStreamObserver[T] { + override def isReady: Boolean = true + override def setOnReadyHandler(onReadyHandler: Runnable): Unit = () + override def disableAutoInboundFlowControl(): Unit = () + override def request(count: Int): Unit = () + override def setMessageCompression(enable: Boolean): Unit = () + override def setCompression(compression: String): Unit = () + override def isCancelled: Boolean = false + override def setOnCancelHandler(onCancelHandler: Runnable): Unit = () + + override def onNext(value: T): Unit = () + override def onError(t: Throwable): Unit = () + override def onCompleted(): Unit = () + } + + /** Test double whose readiness can be toggled to mimic gRPC flow control. It starts ready by + * default. `setReady` flips readiness synchronously and runs the registered onReady handler, + * while `goNotReadyThenReadyAfter` simulates the transport buffer filling up and then draining + * asynchronously (firing the onReady handler from another thread). + */ + private class ControllableReadyObserver[T](initiallyReady: Boolean = true) + extends TestServerCallStreamObserver[T] { + private val readyRef = new AtomicBoolean(initiallyReady) + @volatile private var onReadyHandler: Runnable = () => () + + override def isReady: Boolean = readyRef.get() + override def setOnReadyHandler(handler: Runnable): Unit = onReadyHandler = handler + + def setReady(value: Boolean): Unit = { + readyRef.set(value) + onReadyHandler.run() + } + + def goNotReadyThenReadyAfter(delayMillis: Long): Unit = { + readyRef.set(false) + val handler = onReadyHandler + val _ = Future { + blocking(Thread.sleep(delayMillis)) + readyRef.set(true) + handler.run() + } + } + } + "streamToServer with InputStream" should { "stream all chunks to the server" in { val data = Array.tabulate[Byte](defaultChunkSize * 2 + 100)(_.toByte) @@ -114,4 +165,314 @@ final class GrpcStreamingUtilsTest extends AnyWordSpec with BaseTest { } } } + + "streamToClient" should { + + val fromByteString: FromByteString[ByteString] = (chunk: ByteString) => chunk + + "deliver chunks incrementally (not all at once after producer finishes)" in { + val chunkSize = 1024 * 64 + val totalChunks = 5 + + val chunkDelivered = Array.fill(totalChunks)(new CountDownLatch(1)) + val proceedWithNext = Array.fill(totalChunks)(new CountDownLatch(1)) + val completed = new CountDownLatch(1) + val chunksReceived = new AtomicInteger(0) + val producerCompleted = new CountDownLatch(1) + + def awaitLatch(latch: CountDownLatch, clue: String): Unit = + withClue(clue)(latch.await(10, TimeUnit.SECONDS) shouldBe true) + + def assertProducerStillRunning(chunkIdx: Int): Unit = + withClue(s"Producer must not have completed when chunk $chunkIdx is delivered") { + producerCompleted.getCount shouldBe 1L + } + + val responseObserver = new TestServerCallStreamObserver[ByteString] { + override def onNext(value: ByteString): Unit = { + val idx = chunksReceived.getAndIncrement() + if (idx < totalChunks) { + chunkDelivered(idx).countDown() + awaitLatch(proceedWithNext(idx), s"Chunk $idx should be allowed to proceed") + } + } + override def onError(t: Throwable): Unit = completed.countDown() + override def onCompleted(): Unit = completed.countDown() + } + + def producer(os: OutputStream): Future[Unit] = Future { + blocking { + val data = new Array[Byte](chunkSize) + for (i <- 0 until totalChunks) { + java.util.Arrays.fill(data, i.toByte) + os.write(data) + os.flush() + awaitLatch(chunkDelivered(i), s"Chunk $i should be delivered before producer continues") + } + producerCompleted.countDown() + } + } + + // Run in background because streamToClient blocks via Await.result + val streamingF = Future { + blocking { + GrpcStreamingUtils.streamToClient( + responseF = producer, + responseObserver = responseObserver, + fromByteString = fromByteString, + chunkSizeO = Some(chunkSize), + ) + } + } + + // Verify first chunk is delivered while producer is still writing + awaitLatch(chunkDelivered(0), "First chunk should be delivered") + assertProducerStillRunning(0) + proceedWithNext(0).countDown() + + // Verify remaining chunks are delivered incrementally + for (i <- 1 until totalChunks) { + awaitLatch(chunkDelivered(i), s"Chunk $i should be delivered") + if (i < totalChunks - 1) assertProducerStillRunning(i) + proceedWithNext(i).countDown() + } + + completed.await(30, TimeUnit.SECONDS) shouldBe true + chunksReceived.get() shouldBe totalChunks + streamingF.futureValue + } + + "wait until observer is ready before calling onNext" in { + val chunkSize = 8 + val inputData = Array.tabulate[Byte](chunkSize)((i: Int) => i.toByte) + val context = io.grpc.Context.current().withCancellation() + + val chunksReceived = new AtomicInteger(0) + val calledWhileNotReady = new AtomicInteger(0) + val onNextCalled = new CountDownLatch(1) + + val responseObserver = new ControllableReadyObserver[ByteString](initiallyReady = false) { + override def onNext(value: ByteString): Unit = { + if (!isReady) calledWhileNotReady.incrementAndGet() + chunksReceived.incrementAndGet() + onNextCalled.countDown() + } + } + + val streamingF = + GrpcStreamingUtils.streamResponseChunks(context, responseObserver)( + new ByteArrayInputStream(inputData), + chunkSize, + (chunk: ByteString) => chunk, + ) + + onNextCalled.await(5, TimeUnit.SECONDS) shouldBe false + calledWhileNotReady.get() shouldBe 0 + chunksReceived.get() shouldBe 0 + + responseObserver.setReady(true) + + onNextCalled.await(10, TimeUnit.SECONDS) shouldBe true + chunksReceived.get() shouldBe 1 + calledWhileNotReady.get() shouldBe 0 + + streamingF.futureValue + } + + "complete the future when responseObserver.onNext throws an exception" in { + val chunkSize = 8 + val inputData = Array.tabulate[Byte](chunkSize)((i: Int) => i.toByte) + val context = io.grpc.Context.current().withCancellation() + + val responseObserver = new TestServerCallStreamObserver[ByteString] { + override def onNext(value: ByteString): Unit = + throw new RuntimeException("onNext exploded") + } + + val resultF = GrpcStreamingUtils.streamResponseChunks(context, responseObserver)( + new ByteArrayInputStream(inputData), + chunkSize, + (chunk: ByteString) => chunk, + ) + + // If the bug is present, this future never completes and the test times out + resultF.failed.futureValue shouldBe a[RuntimeException] + } + + "handle producer failure gracefully" in { + val completed = new CountDownLatch(1) + val error = new AtomicInteger(0) + + val responseObserver = new TestServerCallStreamObserver[ByteString] { + override def onError(t: Throwable): Unit = { + error.incrementAndGet() + completed.countDown() + } + override def onCompleted(): Unit = completed.countDown() + } + + GrpcStreamingUtils.streamToClient( + responseF = { (_: OutputStream) => + Future.failed(new RuntimeException("producer exploded")) + }, + responseObserver = responseObserver, + fromByteString = fromByteString, + chunkSizeO = Some(defaultChunkSize), + ) + + completed.await(10, TimeUnit.SECONDS) shouldBe true + error.get() shouldBe 1 + } + + "complete the future when observer becomes not-ready right at EOF" in { + // Reproduces a race where scso.isReady transitions to false right after sending a chunk + // and the worker exits the loop without observing EOF. The completion must still be + // signaled once the observer becomes ready again, even if iter.hasNext was never + // queried at the point where the observer was ready. + val chunkSize = 8 + val inputData = Array.tabulate[Byte](chunkSize)(_.toByte) + val context = io.grpc.Context.current().withCancellation() + + val responseObserver = new ControllableReadyObserver[ByteString] { + override def onNext(value: ByteString): Unit = + // Simulate flow control: observer goes not-ready right after buffering a chunk, + // then becomes ready again shortly after (as the network drains the buffer). + goNotReadyThenReadyAfter(20) + } + + val resultF = GrpcStreamingUtils.streamResponseChunks(context, responseObserver)( + new ByteArrayInputStream(inputData), + chunkSize, + (chunk: ByteString) => chunk, + ) + + // If the bug is present, this future never completes and the test times out + resultF.futureValue + } + + "complete streamToClient end-to-end with a ServerCallStreamObserver under flow control" in { + // Mirrors RemoteDumpIntegrationTest: a producer writes multiple chunks to a piped + // output stream and a ServerCallStreamObserver simulates flow control by going + // not-ready after each onNext and firing onReadyHandler shortly afterwards. + // Regression test for a hang where the worker exited the send loop with the observer + // not ready and EOF was never signaled. + val chunkSize = 1024 + val totalChunks = 50 + + val onCompletedLatch = new CountDownLatch(1) + val received = new AtomicInteger(0) + + val responseObserver = new ControllableReadyObserver[ByteString] { + override def onNext(value: ByteString): Unit = { + received.incrementAndGet() + // Half the time, simulate the buffer filling up: go not-ready and let it drain + // asynchronously, firing onReadyHandler from another thread. + if (received.get() % 2 == 0) goNotReadyThenReadyAfter(5) + } + + override def onError(t: Throwable): Unit = onCompletedLatch.countDown() + override def onCompleted(): Unit = onCompletedLatch.countDown() + } + + def producer(os: OutputStream): Future[Unit] = Future { + blocking { + val data = new Array[Byte](chunkSize) + for (i <- 0 until totalChunks) { + java.util.Arrays.fill(data, i.toByte) + os.write(data) + } + } + } + + val streamingF = Future { + blocking { + GrpcStreamingUtils.streamToClient( + responseF = producer, + responseObserver = responseObserver, + fromByteString = (chunk: ByteString) => chunk, + chunkSizeO = Some(chunkSize), + ) + } + } + + onCompletedLatch.await(30, TimeUnit.SECONDS) shouldBe true + received.get() shouldBe totalChunks + streamingF.futureValue + } + + "complete the future when the context is cancelled while the observer is not ready" in { + val chunkSize = 8 + val inputData = Array.tabulate[Byte](chunkSize * 4)(_.toByte) + val context = io.grpc.Context.current().withCancellation() + + val onNextCalls = new AtomicInteger(0) + + val workerParkedWhileNotReady = new CountDownLatch(1) + + val responseObserver = new TestServerCallStreamObserver[ByteString] { + // The observer never becomes ready and never invokes the onReady/onCancel handlers + override def isReady: Boolean = { + workerParkedWhileNotReady.countDown() + false + } + + override def onNext(value: ByteString): Unit = { + onNextCalls.incrementAndGet() + () + } + } + + val resultF = GrpcStreamingUtils.streamResponseChunks(context, responseObserver)( + new ByteArrayInputStream(inputData), + chunkSize, + (chunk: ByteString) => chunk, + ) + + workerParkedWhileNotReady.await(10, TimeUnit.SECONDS) shouldBe true + context.cancel(new io.grpc.StatusRuntimeException(io.grpc.Status.CANCELLED)) + + resultF.futureValue + onNextCalls.get() shouldBe 0 + } + + "not propagate InterruptedException when the serving thread is interrupted while awaiting" in { + // Regression test: when the gRPC serving thread is interrupted (e.g. the client cancelled or + // timed out) while finishStream is blocked in Await.result, the InterruptedException must be + // handled internally and not escape. Otherwise it bubbles up to the gRPC request handler and + // is logged as an "unexpected throwable" + val responseObserver = new TestServerCallStreamObserver[ByteString] + + val producerStarted = new CountDownLatch(1) + val producer: OutputStream => Future[Unit] = { _ => + producerStarted.countDown() + Promise[Unit]().future + } + + val thrown = new AtomicReference[Throwable](null) + val finished = new CountDownLatch(1) + val thread = new Thread(() => { + try + GrpcStreamingUtils.streamToClient( + responseF = producer, + responseObserver = responseObserver, + fromByteString = fromByteString, + chunkSizeO = Some(defaultChunkSize), + ) + catch { + case t: Throwable => thrown.set(t) + } finally finished.countDown() + }) + thread.start() + + producerStarted.await(10, TimeUnit.SECONDS) shouldBe true + blocking(Thread.sleep(200)) + thread.interrupt() + + withClue("streamToClient must return after the serving thread is interrupted") { + finished.await(10, TimeUnit.SECONDS) shouldBe true + } + // With the fix, the InterruptedException is handled internally and never propagates. + Option(thrown.get()) shouldBe None + } + } } diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/util/PekkoUtilTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/util/PekkoUtilTest.scala index fb42c470bb..481a6f44b5 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/util/PekkoUtilTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/util/PekkoUtilTest.scala @@ -970,43 +970,45 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - if (finalSucceeded.isCompleted) - fail("Should not get so far") - else if (thirdFailed.isCompleted) - finalSucceed.future.map { _ => - finalSucceeded.trySuccess(()) - val (sourceQueue, sourceDone) = Source - .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) - .map { elem => - commit(elem._1) - } - .toMat(Sink.ignore)(Keep.both) - .run() - FutureQueueConsumer( - futureQueue = new PekkoSourceQueueToFutureQueue( - sourceQueue = sourceQueue, - sourceDone = sourceDone, - loggerFactory = loggerFactory, - ), - fromExclusive = 0, - ) - } - else if (secondFailed.isCompleted) - thirdFail.future.map { _ => - thirdFailed.trySuccess(()) - throw new Exception("boom") - } - else if (firstFailed.isCompleted) - secondFail.future.map { _ => - secondFailed.trySuccess(()) - throw new Exception("boom") - } - else - firstFail.future.map { _ => - firstFailed.trySuccess(()) - throw new Exception("boom") - }, - initializationKillSwitch = None, + _ => + if (finalSucceeded.isCompleted) + fail("Should not get so far") + else if (thirdFailed.isCompleted) + finalSucceed.future.map { _ => + finalSucceeded.trySuccess(()) + val (sourceQueue, sourceDone) = Source + .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) + .map { elem => + commit(elem._1) + } + .toMat(Sink.ignore)(Keep.both) + .run() + Future.successful( + FutureQueueConsumer( + futureQueue = new PekkoSourceQueueToFutureQueue( + sourceQueue = sourceQueue, + sourceDone = sourceDone, + loggerFactory = loggerFactory, + ), + fromExclusive = 0, + ) + ) + } + else if (secondFailed.isCompleted) + thirdFail.future.map { _ => + thirdFailed.trySuccess(()) + throw new Exception("boom") + } + else if (firstFailed.isCompleted) + secondFail.future.map { _ => + secondFailed.trySuccess(()) + throw new Exception("boom") + } + else + firstFail.future.map { _ => + firstFailed.trySuccess(()) + throw new Exception("boom") + }, ) recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false firstFail.trySuccess(()) @@ -1040,8 +1042,7 @@ class PekkoUtilTest retryAttemptErrorThreshold = 200, uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = _ => consumerPromise.future, - initializationKillSwitch = None, + consumerFactory = _ => _ => consumerPromise.future.map(Future.successful(_)), ) Threading.sleep(10) recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false @@ -1051,6 +1052,52 @@ class PekkoUtilTest recoveringQueue.firstSuccessfulConsumerInitialization.failed.futureValue } + "two-step initialization: firstSuccessfulConsumerInitialization is set after outer but before inner completes cleanly" in assertAllStagesStopped { + val outerPromise = Promise[Future[FutureQueueConsumer[Int]]]() + val innerPromise = Promise[FutureQueueConsumer[Int]]() + val recoveringQueue = new RecoveringFutureQueueImpl[Int]( + maxBlockedOffer = 1, + bufferSize = 20, + loggerFactory = loggerFactory, + retryStategy = PekkoUtil.exponentialRetryWithCap( + minWait = 2, + multiplier = 2, + cap = 10, + ), + retryAttemptWarnThreshold = 100, + retryAttemptErrorThreshold = 200, + uncommittedWarnTreshold = 100, + recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, + consumerFactory = _ => _ => outerPromise.future, + ) + // Initiate shutdown while the outer Future is still pending. Because + // initialization is in progress, the queue is not yet considered done. + recoveringQueue.shutdown() + Threading.sleep(500) + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false + recoveringQueue.done.isCompleted shouldBe false + + // Resolve outer so firstSuccessfulConsumerInitialization fires. + outerPromise.success(innerPromise.future) + // give some time for the firstSuccessfulConsumerInitialization to react to the outer promise completion + Threading.sleep(500) + recoveringQueue.firstSuccessfulConsumerInitialization.futureValue + // The inner Future is still pending, so the queue is still not done. + recoveringQueue.done.isCompleted shouldBe false + + innerPromise.success( + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + override def offer(elem: (Long, Int)): Future[Done] = Future.successful(Done) + override def shutdown(): Unit = () + override def done: Future[Done] = Future.successful(Done) + }, + fromExclusive = 0, + ) + ) + recoveringQueue.done.futureValue + } + "block offer if buffer is full" in assertAllStagesStopped { val received = new AtomicReference[Vector[(Long, Int)]](Vector.empty) val offerGated = Promise[Unit]() @@ -1068,25 +1115,25 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = _ => - Future { - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.map { _ => - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Done - } + _ => + Future.successful(Future { + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.map { _ => + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Done + } - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = 0, - ) - }, - initializationKillSwitch = None, + override def shutdown(): Unit = shutdownPromise.trySuccess(()) + + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = 0, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1129,8 +1176,7 @@ class PekkoUtilTest retryAttemptErrorThreshold = 200, uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = _ => consumerPromise.future, - initializationKillSwitch = None, + consumerFactory = _ => _ => consumerPromise.future.map(Future.successful(_)), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1179,20 +1225,20 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = _ => - Future { - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - override def offer(elem: (Long, Int)): Future[Done] = - Future.successful(Done) + _ => + Future.successful(Future { + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + override def offer(elem: (Long, Int)): Future[Done] = + Future.successful(Done) - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = donePromise.future.map(_ => Done) - }, - fromExclusive = 0, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = donePromise.future.map(_ => Done) + }, + fromExclusive = 0, + ) + }), ) recoveringQueue.firstSuccessfulConsumerInitialization.futureValue shutdownPromise.isCompleted shouldBe false @@ -1225,20 +1271,22 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = _ => - initializedPromise.future.map { _ => - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - override def offer(elem: (Long, Int)): Future[Done] = - Future.successful(Done) + _ => + initializedPromise.future.map { _ => + Future.successful( + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + override def offer(elem: (Long, Int)): Future[Done] = + Future.successful(Done) - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = donePromise.future.map(_ => Done) - }, - fromExclusive = 0, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = donePromise.future.map(_ => Done) + }, + fromExclusive = 0, + ) + ) + }, ) recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false shutdownPromise.isCompleted shouldBe false @@ -1259,7 +1307,7 @@ class PekkoUtilTest "Shutdown initiated" ) logEntries(2).debugMessage should include( - "Consumer initialization is in progress, delaying shutdown" + "Consumer initialization is in progress, shutdown signal will be propagated to consumer" ) }, ) @@ -1280,13 +1328,79 @@ class PekkoUtilTest shutdownPromise.isCompleted shouldBe false initializedPromise.trySuccess(()) shutdownPromise.future.futureValue - recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe true recoveringQueue.done.isCompleted shouldBe false Threading.sleep(10) - recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe true recoveringQueue.done.isCompleted shouldBe false donePromise.trySuccess(()) - recoveringQueue.firstSuccessfulConsumerInitialization.failed.futureValue + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe true + recoveringQueue.done.futureValue + } + + "propagate shutdown signal while initialization is in progress" in assertAllStagesStopped { + val initializedPromise = Promise[Unit]() + val shutdownPromise = Promise[Unit]() + val donePromise = Promise[Unit]() + val isShuttingDownObserved = Promise[Unit]() + val recoveringQueue = new RecoveringFutureQueueImpl[Int]( + maxBlockedOffer = 2, + bufferSize = 2, + loggerFactory = loggerFactory, + retryStategy = PekkoUtil.exponentialRetryWithCap( + minWait = 2, + multiplier = 2, + cap = 10, + ), + retryAttemptWarnThreshold = 100, + retryAttemptErrorThreshold = 200, + uncommittedWarnTreshold = 100, + recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, + consumerFactory = _ => + isShuttingDown => + initializedPromise.future.map { _ => + if (isShuttingDown()) isShuttingDownObserved.trySuccess(()) + Future.successful( + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + override def offer(elem: (Long, Int)): Future[Done] = + Future.successful(Done) + + override def shutdown(): Unit = shutdownPromise.trySuccess(()) + + override def done: Future[Done] = donePromise.future.map(_ => Done) + }, + fromExclusive = 0, + ) + ) + }, + ) + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false + isShuttingDownObserved.isCompleted shouldBe false + // shutdown while consumer initialization is still in progress + loggerFactory.assertEventuallyLogsSeq( + SuppressionRule.LoggerNameContains("RecoveringFutureQueueImpl") && + SuppressionRule.LevelAndAbove(org.slf4j.event.Level.DEBUG) + )( + recoveringQueue.shutdown(), + logEntries => { + logEntries should have size (3) + logEntries.head.infoMessage should include( + "Before shutting down, preventing further initialization retries" + ) + logEntries(1).infoMessage should include( + "Shutdown initiated" + ) + logEntries(2).debugMessage should include( + "Consumer initialization is in progress, shutdown signal will be propagated to consumer" + ) + }, + ) + initializedPromise.trySuccess(()) + isShuttingDownObserved.future.futureValue + shutdownPromise.future.futureValue + donePromise.trySuccess(()) + recoveringQueue.firstSuccessfulConsumerInitialization.futureValue recoveringQueue.done.futureValue } @@ -1305,11 +1419,11 @@ class PekkoUtilTest retryAttemptErrorThreshold = 200, uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = _ => { - firstConsumerInitializationFailedPromise.trySuccess(()) - Future.failed(new Exception("boom")) - }, - initializationKillSwitch = None, + consumerFactory = _ => + _ => { + firstConsumerInitializationFailedPromise.trySuccess(()) + Future.failed(new Exception("boom")) + }, ) firstConsumerInitializationFailedPromise.future.futureValue Threading.sleep(10) @@ -1367,14 +1481,13 @@ class PekkoUtilTest retryAttemptErrorThreshold = 6, uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = { _ => + consumerFactory = { _ => _ => val f = initializationContinuePromise.get().future.map { _ => throw new Exception("initialization fails") } initializationStartedPromise.get().trySuccess(()) f }, - initializationKillSwitch = None, ) // info 1 initializationStartedPromise.get().future.futureValue @@ -1454,7 +1567,7 @@ class PekkoUtilTest "Shutdown initiated" ) logEntries(2).debugMessage should include( - "Consumer initialization is in progress, delaying shutdown" + "Consumer initialization is in progress, shutdown signal will be propagated to consumer" ) }, ) @@ -1483,36 +1596,36 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.map { _ => - if (elem._2 == 4 && firstConsumer.get()) throw new Exception("boom") - else { - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - commit(elem._1) - Done + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.map { _ => + if (elem._2 == 4 && firstConsumer.get()) throw new Exception("boom") + else { + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + commit(elem._1) + Done + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1559,38 +1672,38 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.flatMap { _ => - if (elem._2 == 4 && firstConsumer.get()) { - shutdownPromise.tryFailure(new Exception("delegate boom")) - Future.never - } else { - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - commit(elem._1) - Future.successful(Done) + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.flatMap { _ => + if (elem._2 == 4 && firstConsumer.get()) { + shutdownPromise.tryFailure(new Exception("delegate boom")) + Future.never + } else { + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + commit(elem._1) + Future.successful(Done) + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1632,45 +1745,45 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.flatMap { _ => - if (elem._2 == 4 && firstConsumer.get()) { - shutdownPromise.tryFailure(new Exception("delegate boom")) - Future.never - } else if (elem._2 >= 3 && firstConsumer.get()) { - // forget, not commit - Future.successful(Done) - } else if (elem._2 >= 2 && firstConsumer.get()) { - // not commit - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Future.successful(Done) - } else { - commit(elem._1) - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Future.successful(Done) + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.flatMap { _ => + if (elem._2 == 4 && firstConsumer.get()) { + shutdownPromise.tryFailure(new Exception("delegate boom")) + Future.never + } else if (elem._2 >= 3 && firstConsumer.get()) { + // forget, not commit + Future.successful(Done) + } else if (elem._2 >= 2 && firstConsumer.get()) { + // not commit + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Future.successful(Done) + } else { + commit(elem._1) + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Future.successful(Done) + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1712,41 +1825,41 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.flatMap { _ => - if (elem._2 == 4 && firstConsumer.get()) { - shutdownPromise.tryFailure(new Exception("delegate boom")) - Future.never - } else if (elem._2 >= 2 && firstConsumer.get()) { - // forget, not commit - Future.successful(Done) - } else { - commit(elem._1) - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Future.successful(Done) + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.flatMap { _ => + if (elem._2 == 4 && firstConsumer.get()) { + shutdownPromise.tryFailure(new Exception("delegate boom")) + Future.never + } else if (elem._2 >= 2 && firstConsumer.get()) { + // forget, not commit + Future.successful(Done) + } else { + commit(elem._1) + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Future.successful(Done) + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1788,42 +1901,42 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.flatMap { _ => - if (elem._2 == 4 && firstConsumer.get()) { - shutdownPromise.tryFailure(new Exception("delegate boom")) - Future.never - } else if (elem._2 >= 2 && firstConsumer.get()) { - // forget, but commit: very wrong - commit(elem._1) - Future.successful(Done) - } else { - commit(elem._1) - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Future.successful(Done) + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.flatMap { _ => + if (elem._2 == 4 && firstConsumer.get()) { + shutdownPromise.tryFailure(new Exception("delegate boom")) + Future.never + } else if (elem._2 >= 2 && firstConsumer.get()) { + // forget, but commit: very wrong + commit(elem._1) + Future.successful(Done) + } else { + commit(elem._1) + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Future.successful(Done) + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1862,38 +1975,38 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - if (sleepy) Threading.sleep(Random.nextLong(2)) - if (Random.nextLong(6) > 0) throw new Exception("initialization boom") - val (sourceQueue, sourceDone) = Source - .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) - .via(BatchN(5, 3)) - .mapAsync(3) { batch => - Future { - if (sleepy) Threading.sleep(Random.nextLong(4) / 4) - if (Random.nextLong(10) == 0) { - boomCount.incrementAndGet() - throw new Exception("boom") + _ => + Future.successful(Future[FutureQueueConsumer[Int]] { + if (sleepy) Threading.sleep(Random.nextLong(2)) + if (Random.nextLong(6) > 0) throw new Exception("initialization boom") + val (sourceQueue, sourceDone) = Source + .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) + .via(BatchN(5, 3)) + .mapAsync(3) { batch => + Future { + if (sleepy) Threading.sleep(Random.nextLong(4) / 4) + if (Random.nextLong(10) == 0) { + boomCount.incrementAndGet() + throw new Exception("boom") + } + batch } - batch } - } - .map { batch => - batch.foreach(elem => sink.getAndUpdate(elem :: _)) - commit(batch.last._1) - } - .toMat(Sink.ignore)(Keep.both) - .run() - FutureQueueConsumer( - futureQueue = new PekkoSourceQueueToFutureQueue( - sourceQueue = sourceQueue, - sourceDone = sourceDone, - loggerFactory = loggerFactory, - ), - fromExclusive = sink.get().headOption.map(_._1).getOrElse(0), - ) - }, - initializationKillSwitch = None, + .map { batch => + batch.foreach(elem => sink.getAndUpdate(elem :: _)) + commit(batch.last._1) + } + .toMat(Sink.ignore)(Keep.both) + .run() + FutureQueueConsumer( + futureQueue = new PekkoSourceQueueToFutureQueue( + sourceQueue = sourceQueue, + sourceDone = sourceDone, + loggerFactory = loggerFactory, + ), + fromExclusive = sink.get().headOption.map(_._1).getOrElse(0), + ) + }), ) val testF = Future { val inputFixture = Iterator.iterate(1)(_ + 1).take(inputSize).toList @@ -1970,30 +2083,30 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val (sourceQueue, sourceDone) = Source - .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) - .via(BatchN(5, 3)) - .mapAsync(3) { batch => - Future { - batch + _ => + Future.successful(Future { + val (sourceQueue, sourceDone) = Source + .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) + .via(BatchN(5, 3)) + .mapAsync(3) { batch => + Future { + batch + } } - } - .map { batch => - commit(batch.last._1) - } - .toMat(Sink.ignore)(Keep.both) - .run() - FutureQueueConsumer( - futureQueue = new PekkoSourceQueueToFutureQueue( - sourceQueue = sourceQueue, - sourceDone = sourceDone, - loggerFactory = loggerFactory, - ), - fromExclusive = 0, - ) - }, - initializationKillSwitch = None, + .map { batch => + commit(batch.last._1) + } + .toMat(Sink.ignore)(Keep.both) + .run() + FutureQueueConsumer( + futureQueue = new PekkoSourceQueueToFutureQueue( + sourceQueue = sourceQueue, + sourceDone = sourceDone, + loggerFactory = loggerFactory, + ), + fromExclusive = 0, + ) + }), ) val start = System.nanoTime() Iterator diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/util/SimpleExecutionQueueTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/util/SimpleExecutionQueueTest.scala index f25848a7ef..e5f5d165f4 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/util/SimpleExecutionQueueTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/util/SimpleExecutionQueueTest.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.util import com.digitalasset.canton.lifecycle.UnlessShutdown.{AbortedDueToShutdown, Outcome} import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, UnlessShutdown} import com.digitalasset.canton.logging.{LogEntry, SuppressionRule} +import com.digitalasset.canton.util.FailureMode.{ContinueAfterFailure, StopAfterFailure} import com.digitalasset.canton.{BaseTest, HasExecutionContext, config} import org.scalatest.BeforeAndAfterEach import org.scalatest.wordspec.AsyncWordSpec diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/continuity/ProtocolContinuityConformanceTest.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/continuity/ProtocolContinuityConformanceTest.scala index 066cc92a03..792dff6f90 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/continuity/ProtocolContinuityConformanceTest.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/continuity/ProtocolContinuityConformanceTest.scala @@ -36,8 +36,6 @@ import com.digitalasset.canton.version.{ProtocolVersion, ReleaseVersion} import monocle.macros.syntax.lens.* import org.scalatest.concurrent.PatienceConfiguration -import scala.concurrent.duration.DurationInt - trait MultiVersionLedgerApiConformanceBase extends LedgerApiConformanceBase { protected def testedReleases: List[TestedRelease] @@ -96,7 +94,7 @@ trait MultiVersionLedgerApiConformanceBase extends LedgerApiConformanceBase { "ExplicitDisclosureIT:EDDuplicates", ) else Seq.empty - LedgerApiConformanceBase.excludedTests ++ perReleaseExclusions + perReleaseExclusions ++ LedgerApiConformanceBase.excludedTests(testedProtocolVersion) } } @@ -169,7 +167,7 @@ trait ProtocolContinuityConformanceTestSynchronizer extends ProtocolContinuityCo testedReleases.foreach { case TestedRelease(release, protocolVersions) => lazy val binDir = ReleaseUtils .retrieve(release) - .futureValue(timeout = PatienceConfiguration.Timeout(2.minutes)) + .futureValue(PatienceConfiguration.Timeout(ReleaseUtils.DefaultReleaseDownloadTimeout)) lazy val pv = protocolVersions.max1 s"run conformance tests of shard $shard with release $release and protocol $pv" in { @@ -245,7 +243,7 @@ trait ProtocolContinuityConformanceTestParticipant extends ProtocolContinuityCon testedReleases.foreach { case TestedRelease(release, protocolVersions) => lazy val binDir = ReleaseUtils .retrieve(release) - .futureValue(timeout = PatienceConfiguration.Timeout(2.minutes)) + .futureValue(PatienceConfiguration.Timeout(ReleaseUtils.DefaultReleaseDownloadTimeout)) lazy val pv = protocolVersions.max1 s"run conformance tests of shard $shard with release $release and protocol $pv" in { @@ -316,7 +314,7 @@ trait ProtocolContinuityConformanceTestPing extends ProtocolContinuityConformanc testedReleases.foreach { case TestedRelease(release, protocolVersions) => lazy val binDir = ReleaseUtils .retrieve(release) - .futureValue(timeout = PatienceConfiguration.Timeout(2.minutes)) + .futureValue(PatienceConfiguration.Timeout(ReleaseUtils.DefaultReleaseDownloadTimeout)) lazy val pv = protocolVersions.max1 s"ping between current-branch participant and release $release participant (pv=$pv)" in { @@ -436,11 +434,11 @@ private[continuity] object ProtocolContinuityConformanceTest { s"$base.ledger-api.topology-aware-package-selection.max-passes-default", s"$base.ledger-api.topology-aware-package-selection.max-passes-limit", s"$base.ledger-api.update-service", - s"$base.parameters.alpha-multi-synchronizer-support", s"$base.parameters.caching.bft-ordering-batch-cache", s"$base.parameters.caching.sequencer-catchup-payload-cache", s"$base.parameters.commit-after-failed-activeness-check", s"$base.parameters.commitment-use-db-snapshot-for-participant-lookup", + s"$base.parameters.enable-all-ledger-api-reassignments", s"$base.parameters.validate-legacy-contracts-v-11", s"$base.parameters.ledger-api-server.indexer.achs-config", s"$base.parameters.ledger-api-server.indexer.postgres-data-source", @@ -452,6 +450,8 @@ private[continuity] object ProtocolContinuityConformanceTest { s"$base.sequencer-client.channel-max-inbound-message-size", s"$base.sequencer-client.keep-alive-client.idle-timeout", s"$base.sequencer-client.keep-alive-client.keep-alive-without-calls", + s"$base.parameters.connect-to-synchronizers-on-startup", + s"$base.traffic-enforcement", ) } val perMediator = { @@ -464,6 +464,7 @@ private[continuity] object ProtocolContinuityConformanceTest { s"$base.crypto.session-signing-keys", s"$base.parameters.caching.bft-ordering-batch-cache", s"$base.parameters.caching.sequencer-catchup-payload-cache", + s"$base.parameters.delayed-verdict-sender", s"$base.sequencer-client.amplify-on-max-sequencing-time-too-far", s"$base.sequencer-client.channel-flow-control-window", s"$base.sequencer-client.channel-max-inbound-message-size", @@ -488,6 +489,8 @@ private[continuity] object ProtocolContinuityConformanceTest { s"$base.parameters.unsafe-sequencer-channel-support", // Once we remove PV34, we can remove this exception s"$base.parameters.disable-release-version-handshake-check", + s"$base.parameters.enable-prevalidation", + s"$base.parameters.enable-reject-delivered-aggregations-on-pv-35", s"$base.public-api.max-concurrent-calls-per-connection", s"$base.sequencer-client.amplify-on-max-sequencing-time-too-far", s"$base.sequencer-client.channel-flow-control-window", diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/ExcludedTests.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/ExcludedTests.scala index 1c2e50a53c..7c4859c3b5 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/ExcludedTests.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/ExcludedTests.scala @@ -12,16 +12,21 @@ import com.daml.ledger.api.testtool.runner.AvailableTests object ExcludedTests { /** Suites excluded when running via JSON API (service-level unsupported operations) */ - val jsonApiExcludedSuites: Seq[String] = Seq( + val jsonApiExcludedTests: Seq[String] = Seq( // health service not available in JSON API "HealthServiceIT", // updatePartyIdentityProviderId not available; getParties fails with empty party list "PartyManagementServiceIT", // PERMISSION_DENIED due to user-management auth mismatch in JSON API "UserManagementServiceIT", + // TODO(#33111): Testing wrongly typed contract keys is not supported for JSON API + "PrefetchContractKeysIT:CSprefetchContractKeysPrepareWronglyTyped", + "PrefetchContractKeysIT:CSprefetchContractKeysWronglyTyped", + // TODO(#27501): Remove the exclusion once JSON API supports Commands.prefetchContractKeys + "PrefetchContractKeysIT", ) - lazy val grpcOnlyTestNames: Seq[String] = AvailableTests.v2_2 + lazy val grpcOnlyTestNames: Seq[String] = AvailableTests.latestStableLf .defaultTests(timeoutScaleFactor = 1.0) .flatMap(_.tests) .collect { @@ -35,5 +40,5 @@ object ExcludedTests { // On the other hand we might occasionally have tests only in previous release lines, that current main does not know about // So ideally we need to have exclude using current code plus test tools def findExcludedTests(useJson: Boolean): Seq[String] = - if (useJson) grpcOnlyTestNames ++ jsonApiExcludedSuites else Seq.empty + if (useJson) grpcOnlyTestNames ++ jsonApiExcludedTests else Seq.empty } diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/JsonApiConformanceIntegrationTest.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/JsonApiConformanceIntegrationTest.scala index a5e77c5e97..3be6bcc066 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/JsonApiConformanceIntegrationTest.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/JsonApiConformanceIntegrationTest.scala @@ -65,11 +65,12 @@ sealed trait JsonApiConformanceBase concurrentTestRuns = 4, // these tests run together with all other tests connectedSynchronizers = env.environment.config.sequencers.size, ) - val availableTests = AvailableTests.v2_2 + + val availableTests = AvailableTests.testsForProtocol(testedProtocolVersion) val envArgInclusion = envArgTestsInclusion.getOrElse(TestInclusions.AllIncluded) val testsToRun = - new ConfiguredTests(availableTests, config).defaultTests.view + ConfiguredTests(availableTests, config).defaultTests.view .flatMap(_.tests) .filter { testCase => testCase.limitation match { @@ -182,7 +183,7 @@ sealed abstract class JsonApiConformanceIntegrationShardedTest( override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 .prependConfigTransform(ConfigTransforms.enableHttpLedgerApi) - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => import env.* participants.all.synchronizers.connect_local(sequencer1, alias = daName) @@ -191,8 +192,10 @@ sealed abstract class JsonApiConformanceIntegrationShardedTest( .withTrafficControl(TestUtils.waitForTargetTimeOnSynchronizerNode(wallClock.now, logger)) protected def inclusions: TestInclusions = TestInclusions.AllIncluded - override protected def exclusions: Set[String] = LedgerApiConformanceBase.excludedTests.toSet ++ - ExcludedTests.jsonApiExcludedSuites.toSet + override protected def exclusions: Set[String] = + ExcludedTests.jsonApiExcludedTests.toSet ++ LedgerApiConformanceBase.excludedTests( + testedProtocolVersion + ) protected def testCaseName = "pass the Ledger API conformance tests" } diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiConformanceTest.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiConformanceTest.scala index 914e710e22..1e59ba1996 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiConformanceTest.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiConformanceTest.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.integration.tests.ledgerapi +import com.daml.ledger.api.testtool.runner.AvailableTests import com.digitalasset.canton.config import com.digitalasset.canton.config.* import com.digitalasset.canton.config.CantonRequireTypes.InstanceName @@ -11,7 +12,6 @@ import com.digitalasset.canton.integration.ConfigTransforms.updateAllParticipant import com.digitalasset.canton.integration.plugins.* import com.digitalasset.canton.integration.plugins.UseLedgerApiTestTool.LAPITTVersion import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer -import com.digitalasset.canton.integration.tests.ledgerapi.LedgerApiConformanceBase.excludedTests import com.digitalasset.canton.integration.tests.ledgerapi.SuppressionRules.ApiUserManagementServiceSuppressionRule import com.digitalasset.canton.integration.util.TestUtils import com.digitalasset.canton.integration.{ @@ -29,7 +29,8 @@ import monocle.macros.syntax.lens.* import org.slf4j.event trait SingleVersionLedgerApiConformanceBase extends LedgerApiConformanceBase { - protected def lfVersion: LanguageVersion = LanguageVersion.v2_2 + protected def lfVersion: LanguageVersion = + AvailableTests.testsForProtocol(testedProtocolVersion).lfVersion protected def lapittVersion: LAPITTVersion = LAPITTVersion.Local @@ -48,7 +49,7 @@ trait SingleVersionLedgerApiConformanceBase extends LedgerApiConformanceBase { ledgerApiTestToolPlugin.runShardedSuites( shard, numShards, - exclude = excludedTests, + exclude = LedgerApiConformanceBase.excludedTests(testedProtocolVersion), useJson = false, )(env) } @@ -109,7 +110,7 @@ class LedgerApiConformanceMultiSynchronizerTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup(setupLedgerApiConformanceEnvironment) // ensure ledger api conformance tests have less noisy neighbours @@ -129,7 +130,7 @@ class LedgerApiConformanceMultiSynchronizerTest new UseLedgerApiTestTool( loggerFactory, connectedSynchronizersCount = connectedSynchronizersCount, - lfVersion = LanguageVersion.v2_2, + lfVersion = AvailableTests.testsForProtocol(testedProtocolVersion).lfVersion, version = LAPITTVersion.Local, ) registerPlugin(new UsePostgres(loggerFactory)) @@ -167,7 +168,7 @@ object LedgerApiConformanceBase { "VettingIT:PVListVettedPackagesMultiSynchronizer", "VettingIT:PVListVettedPackagesPagination", ) - val excludedTests = Seq( + private val disabledTests = Seq( // Exclude tests which are run separately below "ParticipantPruningIT", "TLSOnePointThreeIT", @@ -193,7 +194,36 @@ object LedgerApiConformanceBase { "CommandServiceIT:CSRefuseBadParameter", // TODO(i31186): enable this once the issue is fixed "TransactionServiceVisibilityIT:TXLedgerEffectsHideCommandIdToNonSubmittingStakeholders", + // TODO(#33111): Test is disabled because it generates multiple command root nodes for the prepare endpoint, which is currently not supported + "PrefetchContractKeysIT:CSprefetchContractPrepareKeysMany", + // TODO(#33111): The tests below were written with UCK semantics in mind and are generally considered broken now. + // They should be checked up one by one and either fixed or removed. For now, they are excluded to unblock general testing of LF 2.3 + // tests with divulged/disclosed contracts fail on Canton as does scoping by maintainer unless we're on a UCK synchronizer (see below) + "ContractKeysIT:CKFetchOrLookup", + "ContractKeysIT:CKMaintainerScoped", + "ContractKeysIT:CKNoFetchUndisclosed", + // tests with unique contract key assumption fail as does RWArchiveVsFailedLookupByKey (finding a lookup failure after contract creation) + "RaceConditionIT:RWArchiveVsFailedLookupByKey", + "RaceConditionIT:WWArchiveVsNonTransientCreate", + "RaceConditionIT:WWDoubleNonTransientCreate", + "RaceConditionIT:RWTransientCreateVsNonTransientCreate", ) + + private val excludedTestsForPV34 = Seq( + // Package dependency unvetting becomes supported starting with PV35 + "VettingIT:PVUnvettedDependenciesSupported" + ) + + private val excludedTestsForPV35AndAbove = Seq( + // Test disabled due to package dependency unvetting becoming supported starting with PV35 + "VettingIT:PVCheckUnvettedPackagesExceptWithForceFlag" + ) + + def excludedTests(testedProtocolVersion: ProtocolVersion): Seq[String] = + disabledTests ++ { + if (testedProtocolVersion <= ProtocolVersion.v34) excludedTestsForPV34 + else excludedTestsForPV35AndAbove + } } abstract class LedgerApiShardedConformanceBase(shard: Int) diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/variations/LedgerApiVariationsConformanceTest.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/variations/LedgerApiVariationsConformanceTest.scala index e787769d05..69a73f8c28 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/variations/LedgerApiVariationsConformanceTest.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/variations/LedgerApiVariationsConformanceTest.scala @@ -7,9 +7,11 @@ import com.daml.tls.{TlsServerConfig, TlsVersion} import com.digitalasset.canton.config.* import com.digitalasset.canton.config.RequireTypes.ExistingFile import com.digitalasset.canton.integration.plugins.* -import com.digitalasset.canton.integration.tests.ledgerapi.LedgerApiConformanceBase.excludedTests -import com.digitalasset.canton.integration.tests.ledgerapi.SingleVersionLedgerApiConformanceBase import com.digitalasset.canton.integration.tests.ledgerapi.SuppressionRules.ApiUserManagementServiceSuppressionRule +import com.digitalasset.canton.integration.tests.ledgerapi.{ + LedgerApiConformanceBase, + SingleVersionLedgerApiConformanceBase, +} import com.digitalasset.canton.integration.util.TestUtils import com.digitalasset.canton.integration.{ConfigTransforms, EnvironmentDefinition} import com.digitalasset.canton.logging.SuppressionRule @@ -52,7 +54,7 @@ sealed abstract class LedgerApiInMemoryFanOutConformanceTestShardedPostgres(shar ledgerApiTestToolPlugin.runShardedSuites( shard = shard, numShards = numShards, - exclude = excludedTests, + exclude = LedgerApiConformanceBase.excludedTests(testedProtocolVersion), concurrentTestRuns = VariationsConformanceTestUtils.ConcurrentTestRuns, useJson = false, ) @@ -120,7 +122,7 @@ sealed abstract class LedgerApiTinyBuffersConformanceShardedTestPostgres(shard: ledgerApiTestToolPlugin.runShardedSuites( shard = shard, numShards = numShards, - exclude = excludedTests, + exclude = LedgerApiConformanceBase.excludedTests(testedProtocolVersion), concurrentTestRuns = VariationsConformanceTestUtils.ConcurrentTestRuns, useJson = false, ) @@ -256,7 +258,7 @@ trait LedgerApiStaticTimeConformanceTest extends SingleVersionLedgerApiConforman ledgerApiTestToolPlugin.runShardedSuites( shard = 0, numShards = 1, - exclude = excludedTests ++ exclusions, + exclude = LedgerApiConformanceBase.excludedTests(testedProtocolVersion) ++ exclusions, concurrentTestRuns = VariationsConformanceTestUtils.ConcurrentTestRuns, useJson = false, ) diff --git a/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala b/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala index 20fce5166d..f5199ee091 100644 --- a/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala +++ b/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala @@ -706,7 +706,7 @@ class Engine( // token's buffer before going back to the caller. def wrapHasStarted( - overflow: Vector[FatContractInstance], + overflow: Vector[ResultNeedKey.Response.ContractEntry], callerProgression: NeedKeyProgression.HasStarted, ): NeedKeyProgression.HasStarted = if (overflow.nonEmpty) @@ -723,19 +723,34 @@ class Engine( NeedKeyProgression.Finished } + def resumeWithNeededEntries( + entries: Vector[ResultNeedKey.Response.ContractEntry], + callerHasStarted: NeedKeyProgression.HasStarted, + ) = { + val (enginePage, engineRest) = entries.splitAt(n) + val callerFcis = enginePage.map { + case ResultNeedKey.Response.AuthenticableFatContractInstance( + contractInstance, + _, + _, + ) => + contractInstance + case ResultNeedKey.Response.UnsupportedContractIdVersion(_) => + throw new NotImplementedError( + "UnsupportedContractIdVersion is not yet supported." + ) + } + callback(callerFcis, wrapHasStarted(engineRest, callerHasStarted)) + interpretLoop(machine, time, submissionInfo) + } + def askCaller(callerToken: NeedKeyProgression.CanContinue) = ResultNeedKey( gk, n, callerToken, - { - ( - callerContracts: Vector[FatContractInstance], - callerHasStarted: NeedKeyProgression.HasStarted, - ) => - val (enginePage, engineRest) = callerContracts.splitAt(n) - callback(enginePage, wrapHasStarted(engineRest, callerHasStarted)) - interpretLoop(machine, time, submissionInfo) + { case ResultNeedKey.Response(callerContracts, callerHasStarted) => + resumeWithNeededEntries(callerContracts, callerHasStarted) }, ) @@ -746,9 +761,7 @@ class Engine( if (overflow.nonEmpty) { // We have buffered contracts from a previous caller response. // Serve from the buffer without asking the caller. - val (page, rest) = overflow.splitAt(n) - callback(page, wrapHasStarted(rest, callerProgression)) - interpretLoop(machine, time, submissionInfo) + resumeWithNeededEntries(overflow, callerProgression) } else { // Empty buffer — unwrap the caller progression. callerProgression match { @@ -1110,7 +1123,7 @@ object Engine { Error.Interpretation(Error.Interpretation.DamlException(error), None) private final case class BufferedKeyContracts( - overflow: Vector[FatContractInstance], + overflow: Vector[ResultNeedKey.Response.ContractEntry], callerProgression: NeedKeyProgression.HasStarted, ) extends NeedKeyProgression.Token diff --git a/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala b/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala index 641a3e1cff..ce8dbe13e7 100644 --- a/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala +++ b/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala @@ -6,12 +6,13 @@ package engine import cats.Applicative import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref._ +import com.digitalasset.daml.lf.data.Ref.* import com.digitalasset.daml.lf.data.{BackStack, FrontStack, ImmArray} import com.digitalasset.daml.lf.engine.ResultNeedContract.Response -import com.digitalasset.daml.lf.language.Ast._ +import com.digitalasset.daml.lf.engine.ResultNeedKey.Response.AuthenticableFatContractInstance +import com.digitalasset.daml.lf.language.Ast.* import com.digitalasset.daml.lf.transaction.{FatContractInstance, GlobalKey, NeedKeyProgression} -import com.digitalasset.daml.lf.value.Value._ +import com.digitalasset.daml.lf.value.Value.* import scalaz.Monad import scala.annotation.tailrec @@ -31,7 +32,7 @@ sealed trait Result[+A] extends Product with Serializable { case ResultNeedPackage(pkgId, resume) => ResultNeedPackage(pkgId, mbPkg => resume(mbPkg).map(f)) case ResultNeedKey(gk, limit, token, resume) => - ResultNeedKey(gk, limit, token, (cids, token) => resume(cids, token).map(f)) + ResultNeedKey(gk, limit, token, response => resume(response).map(f)) case ResultPrefetch(contractIds, keys, resume) => ResultPrefetch(contractIds, keys, () => resume().map(f)) } @@ -50,7 +51,7 @@ sealed trait Result[+A] extends Product with Serializable { gk, limit, token, - (mbAcoid, nextToken) => resume(mbAcoid, nextToken).flatMap(f), + response => resume(response).flatMap(f), ) case ResultPrefetch(contractIds, keys, resume) => ResultPrefetch(contractIds, keys, () => resume().flatMap(f)) @@ -78,7 +79,24 @@ sealed trait Result[+A] extends Product with Serializable { })) case ResultNeedPackage(pkgId, resume) => go(resume(pkgs.lift(pkgId))) case ResultNeedKey(key, _, _, resume) => - go(resume(keys.lift(key).getOrElse(Vector.empty), NeedKeyProgression.Finished)) + go( + resume( + ResultNeedKey.Response( + keys + .lift(key) + .getOrElse(Vector.empty) + .map(fci => + AuthenticableFatContractInstance( + fci, + hashingMethod(fci.contractId), + hash => idValidator(fci.contractId, hash), + ) + ), + NeedKeyProgression.Finished, + ) + ) + ) + case ResultPrefetch(_, _, result) => go(result()) } go(this) @@ -165,9 +183,9 @@ final case class ResultNeedPackage[A](packageId: PackageId, resume: Option[Packa extends Result[A] /** Intermediate result indicating that contracts matching a key are required to complete the computation. - * To resume the computation, the caller must invoke `resume` with the following arguments: + * To resume the computation, the caller must invoke `resume` with a page containting the following information: *
    - *
  • `contracts`: a vector of fat contract instances whose key matches `key`. + *
  • `contracts`: a vector of authenticable fat contract instances whose key matches `key`. * `limit` is a hint for the preferred page size; the caller may return more than `limit` entries * and the engine will buffer the overflow internally. * If no contracts match, an empty vector should be provided.
  • @@ -185,9 +203,50 @@ final case class ResultNeedKey[A]( key: GlobalKey, limit: Int, continuationToken: NeedKeyProgression.CanContinue, - resume: (Vector[FatContractInstance], NeedKeyProgression.HasStarted) => Result[A], + resume: ResultNeedKey.Response => Result[A], ) extends Result[A] +object ResultNeedKey { + + object Response { + + /** An entry in a [[Response]] result: either an authenticable contract or an error. */ + sealed trait ContractEntry extends Product with Serializable + + /** + * A fat contract instance and the necessary information to authenticate it. + * + * @param contractInstance a fat contract instance whose key matches the requested key. + * @param expectedHashingMethod the hashing method that the engine expects the engine to use for authenticating the + * contract instance. + * @param idValidator a function that authenticates the contract given a hash of the contract instance + * computed by the engine using the `expectedHashingMethod`. + */ + final case class AuthenticableFatContractInstance( + contractInstance: FatContractInstance, + expectedHashingMethod: Hash.HashingMethod, + idValidator: Hash => Boolean, + ) extends ContractEntry + + /** Indicates that the contract ID uses an unsupported version. */ + final case class UnsupportedContractIdVersion(contractId: ContractId) extends ContractEntry + } + + /** + * The response to the [[ResultNeedKey]] question. + * + * @param contracts a vector of contract entries whose key matches the requested key. Each entry is either an + * [[Response.AuthenticableFatContractInstance]] or an [[Response.UnsupportedContractIdVersion]]. + * @param hasStarted a token indicating the progression state: [[transaction.NeedKeyProgression.Finished]] if all + * matching contracts have been returned, [[transaction.NeedKeyProgression.InProgress]] if there + * may be more results. + */ + final case class Response( + contracts: Vector[Response.ContractEntry], + hasStarted: NeedKeyProgression.HasStarted, + ) +} + /** Indicates that the interpretation will likely need to resolve the given contract keys. * The caller may resolve the keys in parallel to the interpretation, but does not have to. * The keys map associates each key with the maximum number of contracts to prefetch for it. @@ -272,8 +331,8 @@ object Result { gk, limit, token, - (mbAcoid, token) => - resume(mbAcoid, token).flatMap(x => + response => + resume(response).flatMap(x => Result .sequence(results_) .map(otherResults => (okResults :+ x) :++ otherResults) diff --git a/canton/community/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/UnsupportedContractIdEngineSpec.scala b/canton/community/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/UnsupportedContractIdEngineSpec.scala new file mode 100644 index 0000000000..da04d79028 --- /dev/null +++ b/canton/community/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/UnsupportedContractIdEngineSpec.scala @@ -0,0 +1,128 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.daml.lf +package engine + +import com.daml.logging.LoggingContext +import com.digitalasset.canton.logging.SuppressingLogging +import com.digitalasset.daml.lf.crypto.Hash +import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time} +import com.digitalasset.daml.lf.speedy.{InitialSeeding, SValue} +import com.digitalasset.daml.lf.transaction.NeedKeyProgression +import com.digitalasset.daml.lf.transaction.{NextGenContractStateMachine => ContractStateMachine} +import com.digitalasset.daml.lf.value.ContractIdVersion +import org.scalatest.Inside +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class UnsupportedContractIdEngineSpec + extends AnyWordSpec + with Matchers + with Inside + with SuppressingLogging { + + implicit val logContext: LoggingContext = LoggingContext.ForTesting + + private val helpers = + new EngineTestHelpers(ContractIdVersion.V1, "BasicTests-keys.dar", loggerFactory) + import helpers._ + + private val seed = hash("UnsupportedContractIdEngineSpec") + private val now = Time.Timestamp.now() + private val withKeyTemplateId = Ref.Identifier(basicTestsPkgId, "BasicTests:WithKey") + private val withKeySKey = mkSValuePair(SValue.SParty(alice), SValue.SInt64(42)) + + private def driveAuxiliary[A](result: Result[A]): Result[A] = result match { + case ResultNeedPackage(pkgId, resume) => driveAuxiliary(resume(lookupPackage.lift(pkgId))) + case ResultPrefetch(_, _, r) => driveAuxiliary(r()) + case ResultInterruption(continue, _) => driveAuxiliary(continue()) + case other => other + } + + private def runFetchTemplate(coid: com.digitalasset.daml.lf.value.Value.ContractId) = { + val templateId = Ref.Identifier(basicTestsPkgId, "BasicTests:Simple") + val cmds = ImmArray(speedy.Command.FetchTemplate(templateId, SValue.SContractId(coid))) + suffixLenientEngine.interpretCommands( + validating = false, + submitters = Set(party), + readAs = Set.empty, + commands = cmds, + ledgerTime = now, + preparationTime = now, + seeding = InitialSeeding.TransactionSeed(seed), + contractIdVersion = ContractIdVersion.V1, + contractStateMode = ContractStateMachine.Mode.NoKey, + ) + } + + private def runFetchByKey() = { + val cmds = ImmArray(speedy.Command.FetchByKey(withKeyTemplateId, withKeySKey)) + suffixLenientEngine.interpretCommands( + validating = false, + submitters = Set(alice), + readAs = Set.empty, + commands = cmds, + ledgerTime = now, + preparationTime = now, + seeding = InitialSeeding.TransactionSeed(seed), + contractIdVersion = ContractIdVersion.V1, + contractStateMode = ContractStateMachine.Mode.NUCK, + ) + } + + "Engine" should { + + "crash when ResultNeedContract receives UnsupportedContractIdVersion" in { + // Use a CID that is not in defaultContracts so the engine asks for it. + val coid = toContractId("BasicTests:Simple:99") + val result = runFetchTemplate(coid) + + inside(driveAuxiliary(result)) { case ResultNeedContract(_, resume) => + an[NotImplementedError] should be thrownBy + resume(ResultNeedContract.Response.UnsupportedContractIdVersion) + } + } + + "crash when a ResultNeedKey response contains only UnsupportedContractIdVersion" in { + val coid = toContractId("BasicTests:WithKey:unsupported") + val result = runFetchByKey() + + inside(driveAuxiliary(result)) { case ResultNeedKey(_, _, _, resume) => + an[NotImplementedError] should be thrownBy + resume( + ResultNeedKey.Response( + Vector(ResultNeedKey.Response.UnsupportedContractIdVersion(coid)), + NeedKeyProgression.Finished, + ) + ) + } + } + + "defer crashing when first NeedKey entry is supported" in { + val coid = toContractId("BasicTests:WithKey:unsupported") + val result = runFetchByKey() + + inside(driveAuxiliary(result)) { case ResultNeedKey(_, _, _, resume) => + val continued = driveAuxiliary( + resume( + ResultNeedKey.Response( + Vector( + ResultNeedKey.Response.AuthenticableFatContractInstance( + withKeyContractInst, + Hash.HashingMethod.TypedNormalForm, + _ => true, + ), + ResultNeedKey.Response.UnsupportedContractIdVersion(coid), + ), + NeedKeyProgression.Finished, + ) + ) + ) + inside(continued) { case ResultNeedContract(fetchedCoid, _) => + fetchedCoid shouldBe withKeyContractInst.contractId + } + } + } + } +} diff --git a/canton/community/daml-lf/upgrades-matrix-integration/src/test/scala/com/digitalasset/canton/integration/tests/UpgradesMatrixIT.scala b/canton/community/daml-lf/upgrades-matrix-integration/src/test/scala/com/digitalasset/canton/integration/tests/UpgradesMatrixIT.scala index 1e2f8eafda..af0e5bfd6e 100644 --- a/canton/community/daml-lf/upgrades-matrix-integration/src/test/scala/com/digitalasset/canton/integration/tests/UpgradesMatrixIT.scala +++ b/canton/community/daml-lf/upgrades-matrix-integration/src/test/scala/com/digitalasset/canton/integration/tests/UpgradesMatrixIT.scala @@ -25,7 +25,9 @@ import com.daml.ledger.api.v2.commands.{ ExerciseByKeyCommand, ExerciseCommand, } +import com.daml.ledger.api.v2.transaction.Transaction import com.daml.ledger.api.v2.transaction_filter.CumulativeFilter.IdentifierFilter +import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_LEDGER_EFFECTS import com.daml.ledger.api.v2.transaction_filter.{ CumulativeFilter, EventFormat, @@ -33,6 +35,11 @@ import com.daml.ledger.api.v2.transaction_filter.{ TemplateFilter, } import com.daml.ledger.api.v2.value as api +import com.daml.ledger.javaapi.data.{ + Event as JavaEvent, + ExercisedEvent as JavaExercisedEvent, + Transaction as JavaTransaction, +} import com.digitalasset.base.error.utils.ErrorDetails import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.console.InstanceReference @@ -76,6 +83,8 @@ import java.time.Duration import java.util.UUID import scala.concurrent.duration.* import scala.concurrent.{Await, ExecutionContext, Future} +import scala.jdk.CollectionConverters.* +import scala.jdk.OptionConverters.* // Split the tests across eight suites with eight Canton runners, which brings // down the runtime from ~4000s on a single suite to ~1400s @@ -363,11 +372,17 @@ abstract class UpgradesMatrixIntegration( testHelper.clientGlobalTplId, testHelper.clientContractArg(alice, bob), ) + extraGlobalContractId <- createContract( + ledgerClient, + alice, + testHelper.v1TplId, + testHelper.globalContractArgV1(alice, bob), + ) globalContractId <- createContract( ledgerClient, alice, testHelper.v1TplId, - testHelper.globalContractArg(alice, bob), + testHelper.globalContractArgV1(alice, bob), ) } yield UpgradesMatrixCases.SetupData( alice = alice, @@ -375,6 +390,7 @@ abstract class UpgradesMatrixIntegration( clientLocalContractId = clientLocalContractId, clientGlobalContractId = clientGlobalContractId, globalContractId = globalContractId, + extraGlobalContractId = extraGlobalContractId, additionalSetup = ledgerClient, ) } @@ -597,7 +613,8 @@ abstract class UpgradesMatrixIntegration( packageIdSelectionPreference = List(cases.commonDefsPkgId, cases.templateDefsV2PkgId, cases.clientLocalPkgId), commands = commands, - ) + ), + transactionShape = TRANSACTION_SHAPE_LEDGER_EFFECTS, ) } } yield result @@ -619,7 +636,33 @@ abstract class UpgradesMatrixIntegration( expectedOutcome match { case UpgradesMatrixCases.ExpectSuccess => - result shouldBe a[Right[?, ?]] + inside(result) { case Right(response) => + val tx = JavaTransaction.fromProto(Transaction.toJavaProto(response.getTransaction)) + val eventsById: Map[Integer, JavaEvent] = tx.getEventsById().asScala.toMap + val exercisedEvents: Seq[JavaExercisedEvent] = + tx.getRootNodeIds.asScala.toSeq.map(eventsById(_)).collect { + case e: JavaExercisedEvent => e + } + exercisedEvents match { + case Seq(e) => + Option(e.getExerciseResult).map(_.asText.toScala) match { + case Some(Some(t)) => + // TODO(#32310) We have choices that are expected to succeed and + // need to have an exception-free failure when they fail. + // Currently, they return text, and we match for the phrase in + // UpgradesMatrixCases.unexpectedErrorMessage. Later, we should + // return Either and match on the variant instead. + t.getValue should not include UpgradesMatrixCases.unexpectedErrorMessage + case Some(None) => succeed // non-text type in result + case None => + fail("Expected success, got an exercise with no result value") + } + case Seq() => + fail("Expected success, got no exercise node") + case _ => + fail("Expected success, got more than one exercise node") + } + } case UpgradesMatrixCases.ExpectUpgradeError => inside(result) { case Left(statusError) => expectStatusHasErrorCode(statusError, "INTERPRETATION_UPGRADE_ERROR_VALIDATION_FAILED") @@ -645,9 +688,10 @@ abstract class UpgradesMatrixIntegration( inside(result) { case Left(statusError) => expectStatusHasErrorCode(statusError, "TEMPLATE_PRECONDITION_VIOLATED") } - case UpgradesMatrixCases.ExpectUnhandledException => + case UpgradesMatrixCases.ExpectUnhandledException(expectedMsg) => inside(result) { case Left(statusError) => expectStatusHasErrorCode(statusError, "DAML_FAILURE") + statusError.message should include(expectedMsg) } case UpgradesMatrixCases.ExpectInternalInterpretationError => inside(result) { case Left(statusError) => diff --git a/canton/community/daml-lf/upgrades-matrix/src/main/scala/com/digitalasset/daml/lf/engine/UpgradesMatrix.scala b/canton/community/daml-lf/upgrades-matrix/src/main/scala/com/digitalasset/daml/lf/engine/UpgradesMatrix.scala index 3d115cffe7..78d4894c0e 100644 --- a/canton/community/daml-lf/upgrades-matrix/src/main/scala/com/digitalasset/daml/lf/engine/UpgradesMatrix.scala +++ b/canton/community/daml-lf/upgrades-matrix/src/main/scala/com/digitalasset/daml/lf/engine/UpgradesMatrix.scala @@ -237,7 +237,7 @@ class UpgradesMatrixCases( val (commonDefsDalfName, commonDefsDalf, commonDefsPkg, commonDefsPkgId) = encodeDalfArchive( PackageId.assertFromString("-common-defs-id-"), - """metadata ( '-common-defs-' : '1.0.0' ) + s"""metadata ( '-common-defs-' : '1.0.0' ) module Mod { record @serializable MyView = { value : Int64 }; interface (this : Iface) = { @@ -252,10 +252,24 @@ class UpgradesMatrixCases( to upure @Text "InterfaceChoice was called"; }; + record @serializable LookupNContracts (key: *) (contract: *) = { + key: key, + globalContractId: ContractId contract, + extraGlobalContractId: ContractId contract + }; record @serializable Ex = { message: Text } ; exception Ex = { - message \(e: Mod:Ex) -> Mod:Ex {message} e + message \\(e: Mod:Ex) -> Mod:Ex {message} e }; + + val concatCids: forall (tpl: *). List (ContractId tpl) -> Text = /\\ (tpl: *). FOLDR @(ContractId tpl) @Text (\\ (a: ContractId tpl) -> \\ (t: Text) -> APPEND_TEXT (case CONTRACT_ID_TO_TEXT @tpl a of Some t -> t | None -> "unprintable") (APPEND_TEXT ", " t)) ""; + val listMap: forall (a: *). forall (b: *). (a -> b) -> Option (List a) -> List b = + /\\(a: *). /\\(b: *). + \\ (f: (a -> b)) -> \\ (mas: Option (List a)) -> + case mas of + None -> Nil @b + | Some as -> FOLDR @a @(List b) (\\ (a: a) -> \\ (bs: List b) -> Cons @b [f a] bs) (Nil @b) as; + val unexpectedError: Text -> Update Text = \\ (t: Text) -> upure @Text (APPEND_TEXT "${UpgradesMatrixCases.unexpectedErrorMessage}: " t); } """, ) @@ -283,7 +297,7 @@ class UpgradesMatrixCases( def v1Key: String = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = Mod:$templateName {label} this, | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin def v1Maintainers: String = @@ -322,9 +336,9 @@ class UpgradesMatrixCases( | };""" // Used for creating contracts in choice bodies - def additionalCreateArgsLf(v1PkgId: PackageId): String = "" + def additionalCreateArgsLf(pkgId: PackageId): String = "" // Used for creating contracts in commands - def additionalCreateArgsValue(@nowarn v1PkgId: PackageId): ImmArray[(Option[Name], Value)] = + def additionalCreateArgsValue(@nowarn pkgId: PackageId): ImmArray[(Option[Name], Value)] = ImmArray.empty // Used for creating the "lookup contract by key" map passed to the engine. Specified as SValues instead of Values @@ -374,6 +388,7 @@ class UpgradesMatrixCases( | record @serializable $templateName = | { p1: Party | , p2: Party + | , label: Text | $additionalFields | }; | @@ -453,7 +468,7 @@ class UpgradesMatrixCases( | ubind __:Text <- exercise @$clientTplQualifiedName ExerciseNoCatchGlobal$templateName self cid | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming ExerciseInterfaceNoCatchGlobal$templateName | (self) @@ -477,7 +492,7 @@ class UpgradesMatrixCases( | ubind __:Text <- exercise @$clientTplQualifiedName ExerciseInterfaceNoCatchGlobal$templateName self cid | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming FetchNoCatchGlobal$templateName (self) (cid: ContractId $v2TplQualifiedName) | : $v2TplQualifiedName @@ -495,7 +510,7 @@ class UpgradesMatrixCases( | exercise @$clientTplQualifiedName FetchNoCatchGlobal$templateName self cid | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming FetchInterfaceNoCatchGlobal$templateName (self) (cid: ContractId $ifaceQualifiedName) | : Text @@ -514,7 +529,7 @@ class UpgradesMatrixCases( | exercise @$clientTplQualifiedName FetchInterfaceNoCatchGlobal$templateName self cid | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming ExerciseByKeyNoCatchGlobal$templateName (self) (key: $v2KeyTypeQualifiedName): Text | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) @@ -533,7 +548,7 @@ class UpgradesMatrixCases( | ubind __:Text <- exercise @$clientTplQualifiedName ExerciseByKeyNoCatchGlobal$templateName self key | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming FetchByKeyNoCatchGlobal$templateName (self) (key: $v2KeyTypeQualifiedName) | : $v2TplQualifiedName @@ -554,7 +569,75 @@ class UpgradesMatrixCases( | exercise @$clientTplQualifiedName FetchByKeyNoCatchGlobal$templateName self key | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); + | + | choice @nonConsuming LookupNByKeyNoCatchGlobal$templateName (self) (lookupNContracts: '$commonDefsPkgId':Mod:LookupNContracts $v2KeyTypeQualifiedName $v2TplQualifiedName) + | : Text + | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) + | , observers (Nil @Party) + | to + | let globalContractId: ContractId $v2TplQualifiedName = + | '$commonDefsPkgId':Mod:LookupNContracts @$v2KeyTypeQualifiedName @$v2TplQualifiedName + | {globalContractId} + | lookupNContracts in + | let extraGlobalContractId: ContractId $v2TplQualifiedName = + | '$commonDefsPkgId':Mod:LookupNContracts @$v2KeyTypeQualifiedName @$v2TplQualifiedName + | {extraGlobalContractId} + | lookupNContracts in + | ubind + | pairs:Option (List ($tuple2TyCon (ContractId $v2TplQualifiedName) $v2TplQualifiedName)) <- + | query_n_by_key + | @$v2TplQualifiedName + | 4 + | ('$commonDefsPkgId':Mod:LookupNContracts @$v2KeyTypeQualifiedName @$v2TplQualifiedName {key} lookupNContracts) in + | let cids:List (ContractId $v2TplQualifiedName) = + | '$commonDefsPkgId':Mod:listMap + | @($tuple2TyCon (ContractId $v2TplQualifiedName) $v2TplQualifiedName) + | @(ContractId $v2TplQualifiedName) + | (\\ (pair:$tuple2TyCon (ContractId $v2TplQualifiedName) $v2TplQualifiedName) -> + | $tuple2TyCon @(ContractId $v2TplQualifiedName) @$v2TplQualifiedName {_1} pair) + | pairs in + | let matches: List (ContractId $v2TplQualifiedName) -> List (ContractId $v2TplQualifiedName) -> Bool = + | EQUAL_LIST + | @(ContractId $v2TplQualifiedName) + | (EQUAL @(ContractId $v2TplQualifiedName)) in + | let expectedCidOrder1: List (ContractId $v2TplQualifiedName) = + | Cons @(ContractId $v2TplQualifiedName) + | [ extraGlobalContractId + | , globalContractId + | ] (Nil @(ContractId $v2TplQualifiedName)) in + | let expectedCidOrder2: List (ContractId $v2TplQualifiedName) = + | Cons @(ContractId $v2TplQualifiedName) + | [ globalContractId + | , extraGlobalContractId + | ] (Nil @(ContractId $v2TplQualifiedName)) in + | let failureMessage: Text = + | APPEND_TEXT \"could not match \" + | (APPEND_TEXT ('$commonDefsPkgId':Mod:concatCids @$v2TplQualifiedName cids) + | (APPEND_TEXT \" with \" + | (APPEND_TEXT ('$commonDefsPkgId':Mod:concatCids @$v2TplQualifiedName expectedCidOrder1) + | (APPEND_TEXT \" nor with \" + | ('$commonDefsPkgId':Mod:concatCids @$v2TplQualifiedName expectedCidOrder2))))) in + | ubind + | msg:Text <- + | case matches cids expectedCidOrder1 of + | True -> upure @Text \"success on branch 1\" + | | False -> + | (case matches cids expectedCidOrder2 of + | True -> upure @Text \"success on branch 2\" + | | False -> '$commonDefsPkgId':Mod:unexpectedError failureMessage) in + | upure @Text msg; + | + | choice @nonConsuming LookupNByKeyAttemptCatchGlobal$templateName (self) (lookupNContracts: '$commonDefsPkgId':Mod:LookupNContracts $v2KeyTypeQualifiedName $v2TplQualifiedName) + | : Text + | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) + | , observers (Nil @Party) + | to try @Text + | ubind t:Text <- + | exercise @$clientTplQualifiedName LookupNByKeyNoCatchGlobal$templateName self lookupNContracts + | in upure @Text t + | catch + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | |""".stripMargin @@ -576,7 +659,7 @@ class UpgradesMatrixCases( | exercise @$clientTplQualifiedName LookupByKeyNoCatchGlobal$templateName self key | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); |""".stripMargin nonByKeyChoices + whenLookupByKeysOtherwiseEmpty(byKeyChoices) @@ -599,13 +682,14 @@ class UpgradesMatrixCases( | ($v1TplQualifiedName | { p1 = Mod:Client {alice} this | , p2 = Mod:Client {bob} this + | , label = Mod:Client {label} this | ${additionalCreateArgsLf(v1PkgId)} | }) |""".stripMargin val v2KeyExpr = s""" ($v2KeyTypeQualifiedName - | { label = "test-key" + | { label = (Mod:Client {label} this) | , maintainers = (Cons @Party [Mod:Client {alice} this] (Nil @Party)) | ${additionalv2KeyArgsLf(v2PkgId)} | })""".stripMargin @@ -630,7 +714,7 @@ class UpgradesMatrixCases( | ubind __:Text <- exercise @$clientTplQualifiedName ExerciseNoCatchLocal$templateName self () | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming ExerciseInterfaceNoCatchLocal$templateName (self) (u: Unit): Text | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) @@ -649,7 +733,7 @@ class UpgradesMatrixCases( | ubind __:Text <- exercise @$clientTplQualifiedName ExerciseInterfaceNoCatchLocal$templateName self () | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming FetchNoCatchLocal$templateName (self) (u: Unit): $v2TplQualifiedName | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) @@ -667,7 +751,7 @@ class UpgradesMatrixCases( | exercise @$clientTplQualifiedName FetchNoCatchLocal$templateName self () | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming FetchInterfaceNoCatchLocal$templateName (self) (u: Unit): Text | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) @@ -687,7 +771,7 @@ class UpgradesMatrixCases( | exercise @$clientTplQualifiedName FetchInterfaceNoCatchLocal$templateName self () | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming ExerciseByKeyNoCatchLocal$templateName (self) (u: Unit): Text | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) @@ -706,7 +790,7 @@ class UpgradesMatrixCases( | ubind __:Text <- exercise @$clientTplQualifiedName ExerciseByKeyNoCatchLocal$templateName self () | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | | choice @nonConsuming FetchByKeyNoCatchLocal$templateName (self) (u: Unit): $v2TplQualifiedName | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) @@ -726,7 +810,83 @@ class UpgradesMatrixCases( | exercise @$clientTplQualifiedName FetchByKeyNoCatchLocal$templateName self () | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); + | + | choice @nonConsuming LookupNByKeyNoCatchLocal$templateName (self) (lookupNContracts: '$commonDefsPkgId':Mod:LookupNContracts $v2KeyTypeQualifiedName $v2TplQualifiedName) + | : Text + | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) + | , observers (Nil @Party) + | to + | let globalContractId: ContractId $v2TplQualifiedName = + | '$commonDefsPkgId':Mod:LookupNContracts @$v2KeyTypeQualifiedName @$v2TplQualifiedName + | {globalContractId} + | lookupNContracts in + | let extraGlobalContractId: ContractId $v2TplQualifiedName = + | '$commonDefsPkgId':Mod:LookupNContracts @$v2KeyTypeQualifiedName @$v2TplQualifiedName + | {extraGlobalContractId} + | lookupNContracts in + | ubind + | localCid: ContractId $v1TplQualifiedName <- $createV1ContractExpr; + | extraLocalCid: ContractId $v1TplQualifiedName <- $createV1ContractExpr; + | __: Unit <- upure @Unit () in + | ubind + | pairs:Option (List ($tuple2TyCon (ContractId $v2TplQualifiedName) $v2TplQualifiedName)) <- + | query_n_by_key + | @$v2TplQualifiedName + | 4 + | ('$commonDefsPkgId':Mod:LookupNContracts @$v2KeyTypeQualifiedName @$v2TplQualifiedName {key} lookupNContracts) in + | let cids:List (ContractId $v2TplQualifiedName) = + | '$commonDefsPkgId':Mod:listMap + | @($tuple2TyCon (ContractId $v2TplQualifiedName) $v2TplQualifiedName) + | @(ContractId $v2TplQualifiedName) + | (\\ (pair:$tuple2TyCon (ContractId $v2TplQualifiedName) $v2TplQualifiedName) -> + | $tuple2TyCon @(ContractId $v2TplQualifiedName) @$v2TplQualifiedName {_1} pair) + | pairs in + | let matches: List (ContractId $v2TplQualifiedName) -> List (ContractId $v2TplQualifiedName) -> Bool = + | EQUAL_LIST + | @(ContractId $v2TplQualifiedName) + | (EQUAL @(ContractId $v2TplQualifiedName)) in + | let expectedCidOrder1: List (ContractId $v2TplQualifiedName) = + | Cons @(ContractId $v2TplQualifiedName) + | [ COERCE_CONTRACT_ID @$v1TplQualifiedName @$v2TplQualifiedName extraLocalCid + | , COERCE_CONTRACT_ID @$v1TplQualifiedName @$v2TplQualifiedName localCid + | , extraGlobalContractId + | , globalContractId + | ] (Nil @(ContractId $v2TplQualifiedName)) in + | let expectedCidOrder2: List (ContractId $v2TplQualifiedName) = + | Cons @(ContractId $v2TplQualifiedName) + | [ COERCE_CONTRACT_ID @$v1TplQualifiedName @$v2TplQualifiedName extraLocalCid + | , COERCE_CONTRACT_ID @$v1TplQualifiedName @$v2TplQualifiedName localCid + | , globalContractId + | , extraGlobalContractId + | ] (Nil @(ContractId $v2TplQualifiedName)) in + | let failureMessage: Text = + | APPEND_TEXT \"could not match \" + | (APPEND_TEXT ('$commonDefsPkgId':Mod:concatCids @$v2TplQualifiedName cids) + | (APPEND_TEXT \" with \" + | (APPEND_TEXT ('$commonDefsPkgId':Mod:concatCids @$v2TplQualifiedName expectedCidOrder1) + | (APPEND_TEXT \" nor with \" + | ('$commonDefsPkgId':Mod:concatCids @$v2TplQualifiedName expectedCidOrder2))))) in + | ubind + | msg:Text <- + | case matches cids expectedCidOrder1 of + | True -> upure @Text \"success on branch 1\" + | | False -> + | (case matches cids expectedCidOrder2 of + | True -> upure @Text \"success on branch 2\" + | | False -> '$commonDefsPkgId':Mod:unexpectedError failureMessage) in + | upure @Text msg; + | + | choice @nonConsuming LookupNByKeyAttemptCatchLocal$templateName (self) (lookupNContracts: '$commonDefsPkgId':Mod:LookupNContracts $v2KeyTypeQualifiedName $v2TplQualifiedName) + | : Text + | , controllers (Cons @Party [Mod:Client {alice} this] (Nil @Party)) + | , observers (Nil @Party) + | to try @Text + | ubind t:Text <- + | exercise @$clientTplQualifiedName LookupNByKeyNoCatchLocal$templateName self lookupNContracts + | in upure @Text t + | catch + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); | |""".stripMargin @@ -748,7 +908,7 @@ class UpgradesMatrixCases( | exercise @$clientTplQualifiedName LookupByKeyNoCatchLocal$templateName self () | in upure @Text "no exception was caught" | catch - | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | e -> Some @(Update Text) ('$commonDefsPkgId':Mod:unexpectedError "some exception was caught"); |""".stripMargin nonByKeyChoices + whenLookupByKeysOtherwiseEmpty(byKeyChoices) @@ -770,10 +930,10 @@ class UpgradesMatrixCases( // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: errors thrown by the precondition expressions cannot be caught case object ThrowingPrecondition - extends TestCase("ThrowingPrecondition", ExpectUnhandledException) { + extends TestCase("ThrowingPrecondition", ExpectUnhandledException("ThrowingPrecondition")) { override def v1Precondition = "True" override def v2Precondition = - s"""throw @Bool @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "Precondition"})""" + s"""throw @Bool @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "ThrowingPrecondition"})""" } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: changed signatories expression, evaluates to the same value, upgrade succeeds @@ -792,10 +952,10 @@ class UpgradesMatrixCases( // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: errors thrown by the signatories expression cannot be caught case object ThrowingSignatories - extends TestCase("ThrowingSignatories", ExpectUnhandledException) { + extends TestCase("ThrowingSignatories", ExpectUnhandledException("ThrowingSignatories")) { override def v1Signatories = s"Cons @Party [Mod:$templateName {p1} this] (Nil @Party)" override def v2Signatories = - s"""throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "Signatories"})""" + s"""throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "ThrowingSignatories"})""" } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: changed observers expression, evaluates to the same value, upgrade succeeds @@ -811,10 +971,11 @@ class UpgradesMatrixCases( } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: errors thrown by the observers expression cannot be caught - case object ThrowingObservers extends TestCase("ThrowingObservers", ExpectUnhandledException) { + case object ThrowingObservers + extends TestCase("ThrowingObservers", ExpectUnhandledException("ThrowingObservers")) { override def v1Observers = "Nil @Party" override def v2Observers = - s"""throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "Observers"})""" + s"""throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "ThrowingObservers"})""" } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: added an interface instance in v2 when it doesn't yet exist in v1, make sure that it gets picked up @@ -852,12 +1013,12 @@ class UpgradesMatrixCases( case object UnchangedKey extends TestCase("UnchangedKey", ExpectSuccess) { override def v1Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin override def v2Key = s""" case () of () -> | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin } @@ -866,25 +1027,25 @@ class UpgradesMatrixCases( case object ChangedKey extends TestCase("ChangedKey", ExpectUpgradeError) { override def v1Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin override def v2Key = s""" | Mod:${templateName}Key { - | label = "test-key-2", + | label = (APPEND_TEXT (Mod:$templateName {label} this) "-2"), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: errors thrown by the key expression cannot be caught - case object ThrowingKey extends TestCase("ThrowingKey", ExpectUnhandledException) { + case object ThrowingKey extends TestCase("ThrowingKey", ExpectUnhandledException("ThrowingKey")) { override def v1Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin override def v2Key = - s"""throw @Mod:${templateName}Key @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "Key"})""" + s"""throw @Mod:${templateName}Key @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "ThrowingKey"})""" } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: changed maintainers expression, evaluates to the same value, upgrade succeeds @@ -902,7 +1063,7 @@ class UpgradesMatrixCases( override def v1Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)), | maintainers2 = (Cons @Party [Mod:$templateName {p2} this] (Nil @Party)) | }""".stripMargin @@ -933,20 +1094,23 @@ class UpgradesMatrixCases( // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: errors thrown by the maintainers expression cannot be caught case object ThrowingMaintainers - extends TestCase("ThrowingMaintainers", ExpectUnhandledException) { + extends TestCase("ThrowingMaintainers", ExpectUnhandledException("ThrowingMaintainers")) { override def v1Maintainers = s"\\(key: Mod:${templateName}Key) -> (Mod:${templateName}Key {maintainers} key)" override def v2Maintainers = - s"""throw @(Mod:${templateName}Key -> List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "Maintainers"})""" + s"""throw @(Mod:${templateName}Key -> List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "ThrowingMaintainers"})""" } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: errors thrown by the maintainers function body cannot be caught case object ThrowingMaintainersBody - extends TestCase("ThrowingMaintainersBody", ExpectUnhandledException) { + extends TestCase( + "ThrowingMaintainersBody", + ExpectUnhandledException("ThrowingMaintainersBody"), + ) { override def v1Maintainers = s"\\(key: Mod:${templateName}Key) -> (Mod:${templateName}Key {maintainers} key)" override def v2Maintainers = - s"""\\(key: Mod:${templateName}Key) -> throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "MaintainersBody"})""" + s"""\\(key: Mod:${templateName}Key) -> throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "ThrowingMaintainersBody"})""" } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: additional optional field in record used as choice parameter, upgrade succeeds @@ -1388,26 +1552,32 @@ class UpgradesMatrixCases( override def additionalCreateArgsLf(v1PkgId: PackageId): String = s", extra = ()" - override def additionalCreateArgsValue(v1PkgId: PackageId): ImmArray[(Option[Name], Value)] = + override def additionalCreateArgsValue(v1PkgId: PackageId) = ImmArray(None /* extra */ -> ValueUnit) } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: errors thrown by the choice controllers expression cannot be caught case object ThrowingInterfaceChoiceControllers - extends TestCase("ThrowingInterfaceChoiceControllers", ExpectUnhandledException) { + extends TestCase( + "ThrowingInterfaceChoiceControllers", + ExpectUnhandledException("ThrowingInterfaceChoiceControllers"), + ) { override def v1InterfaceChoiceControllers = s"Cons @Party [Mod:$templateName {p1} this] (Nil @Party)" override def v2InterfaceChoiceControllers = - s"""throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "InterfaceChoiceControllers"})""" + s"""throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "ThrowingInterfaceChoiceControllers"})""" } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: errors thrown by the choice observers expression cannot be caught case object ThrowingInterfaceChoiceObservers - extends TestCase("ThrowingInterfaceChoiceObservers", ExpectUnhandledException) { + extends TestCase( + "ThrowingInterfaceChoiceObservers", + ExpectUnhandledException("ThrowingInterfaceChoiceObservers"), + ) { override def v1InterfaceChoiceObservers = s"Cons @Party [Mod:$templateName {p1} this] (Nil @Party)" override def v2InterfaceChoiceObservers = - s"""throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "InterfaceChoiceObservers"})""" + s"""throw @(List Party) @'$commonDefsPkgId':Mod:Ex ('$commonDefsPkgId':Mod:Ex {message = "ThrowingInterfaceChoiceObservers"})""" } // TEST_EVIDENCE: Integrity: Smart Contract Upgrade: interface views are not calculated during fetches and exercises @@ -1647,12 +1817,12 @@ class UpgradesMatrixCases( override def v1Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin override def v2Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)), | extra = None @Unit | }""".stripMargin @@ -1674,12 +1844,12 @@ class UpgradesMatrixCases( override def v1Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin override def v2Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)), | extra = Some @Unit () | }""".stripMargin @@ -1693,13 +1863,13 @@ class UpgradesMatrixCases( override def v1Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)), | extra = None @Unit | }""".stripMargin override def v2Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin } @@ -1719,13 +1889,13 @@ class UpgradesMatrixCases( override def v1Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)), | extra = Some @Unit () | }""".stripMargin override def v2Key = s""" | Mod:${templateName}Key { - | label = "test-key", + | label = (Mod:$templateName {label} this), | maintainers = (Cons @Party [Mod:$templateName {p1} this] (Nil @Party)) | }""".stripMargin } @@ -1842,7 +2012,7 @@ class UpgradesMatrixCases( .map(_.clientChoicesLocal(templateDefsV1PkgId, templateDefsV2PkgId)) s"""metadata ( '-client-local-' : '1.0.0' ) module Mod { - record @serializable Client = { alice: Party, bob: Party }; + record @serializable Client = { alice: Party, bob: Party, label: Text }; template (this: Client) = { precondition True; signatories Cons @Party [Mod:Client {alice} this] (Nil @Party); @@ -1862,7 +2032,7 @@ class UpgradesMatrixCases( .map(_.clientChoicesGlobal(templateDefsV2PkgId)) s"""metadata ( '-client-global-' : '1.0.0' ) module Mod { - record @serializable Client = { alice: Party, bob: Party }; + record @serializable Client = { alice: Party, bob: Party, label: Text }; template (this: Client) = { precondition True; signatories Cons @Party [Mod:Client {alice} this] (Nil @Party); @@ -1912,6 +2082,7 @@ class UpgradesMatrixCases( FetchInterface, ExerciseByKey, FetchByKey, + LookupNByKey, ) ++ whenLookupByKeysOtherwiseEmpty( List( LookupByKey @@ -1964,22 +2135,27 @@ class UpgradesMatrixCases( val v1TplId: Identifier = Identifier(templateDefsV1PkgId, tplQualifiedName) val v1KeyId: Identifier = Identifier(templateDefsV1PkgId, keyQualifiedName) val v2TplId: Identifier = Identifier(templateDefsV2PkgId, tplQualifiedName) + val v2KeyId: Identifier = Identifier(templateDefsV2PkgId, keyQualifiedName) + + def clientContractArg(alice: Party, bob: Party, label: String = "test-key"): ValueRecord = + ValueRecord( + None /* clientTplId */, + ImmArray( + None /* alice */ -> ValueParty(alice), + None /* bob */ -> ValueParty(bob), + None /* label */ -> ValueText(label), + ), + ) - def clientContractArg(alice: Party, bob: Party): ValueRecord = ValueRecord( - None /* clientTplId */, - ImmArray( - None /* alice */ -> ValueParty(alice), - None /* bob */ -> ValueParty(bob), - ), - ) - - def globalContractArg(alice: Party, bob: Party): ValueRecord = ValueRecord( - None /* v1TplId */, - ImmArray( - None /* p1 */ -> ValueParty(alice), - None /* p2 */ -> ValueParty(bob), - ).slowAppend(testCase.additionalCreateArgsValue(templateDefsV1PkgId)), - ) + def globalContractArgV1(alice: Party, bob: Party, label: String = "test-key"): ValueRecord = + ValueRecord( + None /* v1TplId */, + ImmArray( + None /* p1 */ -> ValueParty(alice), + None /* p2 */ -> ValueParty(bob), + None /* label */ -> ValueText(label), + ).slowAppend(testCase.additionalCreateArgsValue(templateDefsV1PkgId)), + ) def globalContractv1KeySValue[AdditionalSetup]( setupData: SetupData[AdditionalSetup] @@ -2002,7 +2178,7 @@ class UpgradesMatrixCases( val (additionalFields, additionalValues) = testCase.additionalv2KeyArgsSValue(templateDefsV2PkgId, setupData).unzip SValue.SRecord( - v2TplId, + v2KeyId, ImmArray.from(List[Name]("label", "maintainers") ++ additionalFields), ArraySeq( SValue.SText("test-key"), @@ -2011,7 +2187,7 @@ class UpgradesMatrixCases( ) } - def globalContractKeyWithMaintainers[AdditionalSetup]( + def globalContractV1KeyWithMaintainers[AdditionalSetup]( setupData: SetupData[AdditionalSetup] ): Option[GlobalKeyWithMaintainers] = Some { @@ -2029,6 +2205,24 @@ class UpgradesMatrixCases( ) } + def globalContractV2KeyWithMaintainers[AdditionalSetup]( + setupData: SetupData[AdditionalSetup] + ): Option[GlobalKeyWithMaintainers] = + Some { + val keySValue = globalContractv2KeySValue(setupData) + GlobalKeyWithMaintainers.assertBuild( + v2TplId, + keySValue.toNormalizedValue, + SValueHash.assertHashContractKey( + templateDefsPkgName, + v2TplId.qualifiedName, + keySValue, + ), + Set(setupData.alice), + templateDefsPkgName, + ) + } + def makeApiCommands: Option[SetupData[Any] => ImmArray[ApiCommand]] = { val choiceArg = ValueRecord( @@ -2051,7 +2245,14 @@ class UpgradesMatrixCases( ) match { case (_, _, _, _, Local, CreationPackageUnvetted) => None // local contracts cannot be created from unvetted packages - case (_, Fetch | FetchInterface | FetchByKey | LookupByKey, _, Command, _, _) => + case ( + _, + Fetch | FetchInterface | FetchByKey | LookupNByKey | LookupByKey, + _, + Command, + _, + _, + ) => None // There are no fetch* or lookupByKey commands case (_, Exercise | ExerciseInterface, _, Command, Local, _) => None // Local contracts cannot be exercised by commands, except by key @@ -2075,7 +2276,8 @@ class UpgradesMatrixCases( None // *ChoiceArg* test cases only make sense for non-interface exercise commands case ( ThrowingInterfaceChoiceControllers | ThrowingInterfaceChoiceObservers, - Fetch | FetchInterface | FetchByKey | LookupByKey | Exercise | ExerciseByKey, + Fetch | FetchInterface | FetchByKey | LookupNByKey | LookupByKey | Exercise | + ExerciseByKey, _, _, _, @@ -2084,7 +2286,7 @@ class UpgradesMatrixCases( None // ThrowingInterfaceChoice* test cases only makes sense for ExerciseInterface case ( InvalidKeyDowngradeAdditionalField, - ExerciseByKey | FetchByKey | LookupByKey, + ExerciseByKey | FetchByKey | LookupNByKey | LookupByKey, _, _, _, @@ -2093,18 +2295,18 @@ class UpgradesMatrixCases( None // InvalidKeyDowngradeAdditionalField does not make sense for *ByKey operations case ( ThrowingView, - Fetch | FetchByKey | LookupByKey | Exercise | ExerciseByKey, + Fetch | FetchByKey | LookupNByKey | LookupByKey | Exercise | ExerciseByKey, _, _, _, _, ) => None // ThrowingView only makes sense for *Interface operations - case (ChangedMaintainers, LookupByKey, _, _, _, _) => + case (ChangedMaintainers, LookupByKey | LookupNByKey, _, _, _, _) => // TODO(#31844): change the ChangedMaintainers test case to shrink the set of maintainers in V2 in order // for the authorization check to pass and the test to reach the upgrade check. This is not trivial // as the test framework currently assumes only one signatory. - None // LookupByKey will enforce authorization rules before it even gets to the upgrade check + None // LookupByKey and LookupNByKey will enforce authorization rules before it even gets to the upgrade check case (_, Exercise, _, Command, Global | Disclosed, _) => Some(setupData => ImmArray( @@ -2143,7 +2345,7 @@ class UpgradesMatrixCases( ImmArray( ApiCommand.Create( v1TplId.toRef, - globalContractArg(setupData.alice, setupData.bob), + globalContractArgV1(setupData.alice, setupData.bob), ), ApiCommand.ExerciseByKey( tplRef, // we let package preference select v2 @@ -2167,6 +2369,20 @@ class UpgradesMatrixCases( ValueContractId(setupData.globalContractId) case FetchByKey | LookupByKey | ExerciseByKey => globalContractv2KeySValue(setupData).toNormalizedValue + case LookupNByKey => + SValue + .SRecord( + Identifier(commonDefsPkgId, "Mod:LookupNContracts"), + ImmArray.from( + List[Name]("target", "globalContractId", "extraGlobalContractId") + ), + ArraySeq( + globalContractv2KeySValue(setupData), + SValue.SContractId(setupData.globalContractId), + SValue.SContractId(setupData.extraGlobalContractId), + ), + ) + .toNormalizedValue }, ) ) @@ -2180,7 +2396,24 @@ class UpgradesMatrixCases( ChoiceName.assertFromString( s"${operation.name}${catchBehavior.name}Local$templateName" ), - ValueUnit, + operation match { + case LookupNByKey => + SValue + .SRecord( + Identifier(commonDefsPkgId, "Mod:LookupNContracts"), + ImmArray.from( + List[Name]("target", "globalContractId", "extraGlobalContractId") + ), + ArraySeq( + globalContractv2KeySValue(setupData), + SValue.SContractId(setupData.globalContractId), + SValue.SContractId(setupData.extraGlobalContractId), + ), + ) + .toNormalizedValue + case _ => + ValueUnit + }, ) ) ) @@ -2201,8 +2434,8 @@ object UpgradesMatrixCases { extends ExpectedOutcome("should fail with an authentication err") case object ExpectPreprocessingError extends ExpectedOutcome("should fail with a preprocessing err") - case object ExpectUnhandledException - extends ExpectedOutcome("should fail with an unhandled excption") + case class ExpectUnhandledException(expectedMsg: String) + extends ExpectedOutcome(s"should fail with an unhandled excption with message: $expectedMsg") case object ExpectInternalInterpretationError extends ExpectedOutcome("should fail with an internal interpretation err") @@ -2212,6 +2445,7 @@ object UpgradesMatrixCases { clientLocalContractId: ContractId, clientGlobalContractId: ContractId, globalContractId: ContractId, + extraGlobalContractId: ContractId, additionalSetup: AdditionalSetup, ) @@ -2221,6 +2455,7 @@ object UpgradesMatrixCases { case object ExerciseInterface extends Operation("ExerciseInterface") case object Fetch extends Operation("Fetch") case object FetchByKey extends Operation("FetchByKey") + case object LookupNByKey extends Operation("LookupNByKey") case object FetchInterface extends Operation("FetchInterface") case object LookupByKey extends Operation("LookupByKey") @@ -2240,4 +2475,6 @@ object UpgradesMatrixCases { sealed abstract class CreationPackageStatus case object CreationPackageVetted extends CreationPackageStatus case object CreationPackageUnvetted extends CreationPackageStatus + + val unexpectedErrorMessage: String = "Unexpected UpgradeMatrix result" } diff --git a/canton/community/daml-lf/upgrades-matrix/src/test/scala/com/digitalasset/daml/lf/engine/UpgradesMatrixUnit.scala b/canton/community/daml-lf/upgrades-matrix/src/test/scala/com/digitalasset/daml/lf/engine/UpgradesMatrixUnit.scala index 99270229db..f31222b5e0 100644 --- a/canton/community/daml-lf/upgrades-matrix/src/test/scala/com/digitalasset/daml/lf/engine/UpgradesMatrixUnit.scala +++ b/canton/community/daml-lf/upgrades-matrix/src/test/scala/com/digitalasset/daml/lf/engine/UpgradesMatrixUnit.scala @@ -26,10 +26,10 @@ import scala.concurrent.{ExecutionContext, Future} // Split the Upgrade unit tests over four suites, which seems to be the sweet // spot (~95s instead of ~185s runtime) -class UpgradesMatrixUnit0 extends UpgradesMatrixUnit(UpgradesMatrixCasesV2MaxStable, 3, 0) -class UpgradesMatrixUnit1 extends UpgradesMatrixUnit(UpgradesMatrixCasesV2MaxStable, 3, 1) -class UpgradesMatrixUnit2 extends UpgradesMatrixUnit(UpgradesMatrixCasesV2MaxStable, 3, 2) -class UpgradesMatrixUnit3 extends UpgradesMatrixUnit(UpgradesMatrixCasesV2Dev, 1, 0) +class UpgradesMatrixUnit0 extends UpgradesMatrixUnit(UpgradesMatrixCasesV2MaxStable, 4, 0) +class UpgradesMatrixUnit1 extends UpgradesMatrixUnit(UpgradesMatrixCasesV2MaxStable, 4, 1) +class UpgradesMatrixUnit2 extends UpgradesMatrixUnit(UpgradesMatrixCasesV2MaxStable, 4, 2) +class UpgradesMatrixUnit3 extends UpgradesMatrixUnit(UpgradesMatrixCasesV2MaxStable, 4, 3) /** A test suite to run the UpgradesMatrix matrix directly in the engine * @@ -66,6 +66,7 @@ class UpgradesMatrixUnit(upgradesMatrixCases: UpgradesMatrixCases, n: Int, k: In clientLocalContractId = toContractId("client-local"), clientGlobalContractId = toContractId("client-global"), globalContractId = toContractId("1"), + extraGlobalContractId = toContractId("2"), additionalSetup = (), ) ) @@ -127,12 +128,28 @@ class UpgradesMatrixUnit(upgradesMatrixCases: UpgradesMatrixCases, n: Int, k: In packageName = cases.templateDefsPkgName, templateId = testHelper.v1TplId, createArg = normalize( - testHelper.globalContractArg(setupData.alice, setupData.bob), + testHelper.globalContractArgV1(setupData.alice, setupData.bob), Ast.TTyCon(testHelper.v1TplId), ), signatories = immutable.TreeSet(setupData.alice), stakeholders = immutable.TreeSet(setupData.alice), - contractKeyWithMaintainers = testHelper.globalContractKeyWithMaintainers(setupData), + contractKeyWithMaintainers = testHelper.globalContractV1KeyWithMaintainers(setupData), + createdAt = CreationTime.CreatedAt(Time.Timestamp.Epoch), + authenticationData = Bytes.assertFromString("00"), + ) + + val extraGlobalContract: FatContractInstance = FatContractInstanceImpl( + version = cases.serializationVersion, + contractId = setupData.extraGlobalContractId, + packageName = cases.templateDefsPkgName, + templateId = testHelper.v1TplId, + createArg = normalize( + testHelper.globalContractArgV1(setupData.alice, setupData.bob), + Ast.TTyCon(testHelper.v1TplId), + ), + signatories = immutable.TreeSet(setupData.alice), + stakeholders = immutable.TreeSet(setupData.alice), + contractKeyWithMaintainers = testHelper.globalContractV1KeyWithMaintainers(setupData), createdAt = CreationTime.CreatedAt(Time.Timestamp.Epoch), authenticationData = Bytes.assertFromString("00"), ) @@ -142,8 +159,15 @@ class UpgradesMatrixUnit(upgradesMatrixCases: UpgradesMatrixCases, n: Int, k: In val submitters = Set(setupData.alice) val readAs = Set.empty[Party] - val lookupContractById = contractOrigin match { - case UpgradesMatrixCases.Global | UpgradesMatrixCases.Disclosed => + val lookupContractById = (contractOrigin, testHelper.operation) match { + case (_, UpgradesMatrixCases.LookupNByKey) => + Map( + setupData.clientLocalContractId -> clientLocalContract, + setupData.clientGlobalContractId -> clientGlobalContract, + setupData.globalContractId -> globalContract, + setupData.extraGlobalContractId -> extraGlobalContract, + ) + case (UpgradesMatrixCases.Global | UpgradesMatrixCases.Disclosed, _) => Map( setupData.clientLocalContractId -> clientLocalContract, setupData.clientGlobalContractId -> clientGlobalContract, @@ -155,12 +179,23 @@ class UpgradesMatrixUnit(upgradesMatrixCases: UpgradesMatrixCases, n: Int, k: In setupData.clientGlobalContractId -> clientGlobalContract, ) } - val lookupContractByKey = contractOrigin match { - case UpgradesMatrixCases.Global | UpgradesMatrixCases.Disclosed => + val lookupContractByKey = (contractOrigin, testHelper.operation) match { + case (_, UpgradesMatrixCases.LookupNByKey) => + ( + (gkey: GlobalKey) => + testHelper + .globalContractV1KeyWithMaintainers(setupData) + .flatMap(helperKey => + Option.when(helperKey.globalKey == gkey)( + Vector(extraGlobalContract, globalContract) + ) + ) + ).unlift + case (UpgradesMatrixCases.Global | UpgradesMatrixCases.Disclosed, _) => ( (gkey: GlobalKey) => testHelper - .globalContractKeyWithMaintainers(setupData) + .globalContractV1KeyWithMaintainers(setupData) .flatMap(helperKey => Option.when(helperKey.globalKey == gkey)(Vector(globalContract)) ) @@ -178,6 +213,7 @@ class UpgradesMatrixUnit(upgradesMatrixCases: UpgradesMatrixCases, n: Int, k: In setupData.clientLocalContractId -> hash(clientLocalContract), setupData.clientGlobalContractId -> hash(clientGlobalContract), setupData.globalContractId -> hash(globalContract), + setupData.extraGlobalContractId -> hash(extraGlobalContract), ) newEngine() @@ -219,7 +255,28 @@ class UpgradesMatrixUnit(upgradesMatrixCases: UpgradesMatrixCases, n: Int, k: In )(implicit ec: ExecutionContext): Assertion = expectedOutcome match { case UpgradesMatrixCases.ExpectSuccess => - result shouldBe a[Right[?, ?]] + inside(result) { case Right((VersionedTransaction(_, nodes, rootNodes), _)) => + rootNodes.toSeq.map(nodes(_)).collect { case e: Node.Exercise => e } match { + case Seq(e) => + e.exerciseResult match { + case Some(ValueText(t)) => + // TODO(#32310) We have choices that are expected to succeed and + // need to have an exception-free failure when they fail. + // Currently, they return text, and we match for the phrase in + // UpgradesMatrixCases.unexpectedErrorMessage. Later, we should + // return Either and match on the variant instead. + t should not include UpgradesMatrixCases.unexpectedErrorMessage + case Some(_) => + succeed // non-text type in result + case None => + fail("Expected success, got an exercise with no result value") + } + case Seq() => + fail("Expected success, got no exercise node") + case _ => + fail("Expected success, got more than one exercise node") + } + } case UpgradesMatrixCases.ExpectUpgradeError => inside(result) { case Left(EE.Interpretation(EE.Interpretation.DamlException(error), _)) => error shouldBe a[IE.Upgrade] @@ -242,9 +299,11 @@ class UpgradesMatrixUnit(upgradesMatrixCases: UpgradesMatrixCases, n: Int, k: In inside(result) { case Left(EE.Interpretation(EE.Interpretation.DamlException(error), _)) => error shouldBe a[IE.TemplatePreconditionViolated] } - case UpgradesMatrixCases.ExpectUnhandledException => + case UpgradesMatrixCases.ExpectUnhandledException(expectedMsg) => inside(result) { case Left(EE.Interpretation(EE.Interpretation.DamlException(error), _)) => - error shouldBe a[IE.FailureStatus] + inside(error) { case e: IE.FailureStatus => + e.errorMessage should include(expectedMsg) + } } case UpgradesMatrixCases.ExpectInternalInterpretationError => inside(result) { case Left(EE.Interpretation(error, _)) => diff --git a/canton/community/daml-lf/validation/src/main/scala/com/digitalasset/daml/lf/validation/Typing.scala b/canton/community/daml-lf/validation/src/main/scala/com/digitalasset/daml/lf/validation/Typing.scala index 32d9691e3c..fbad929c61 100644 --- a/canton/community/daml-lf/validation/src/main/scala/com/digitalasset/daml/lf/validation/Typing.scala +++ b/canton/community/daml-lf/validation/src/main/scala/com/digitalasset/daml/lf/validation/Typing.scala @@ -519,7 +519,7 @@ private[validation] object Typing { private def checkChoice(tplId: TypeConId, choice: TemplateChoice): Unit = choice match { case TemplateChoice( - name @ _, + name, consuming @ _, controllers, choiceObservers, @@ -529,16 +529,18 @@ private[validation] object Typing { returnType, update, ) => - checkType(paramType, KStar) - checkType(returnType, KStar) - introExprVar(param, paramType).checkTopExpr(controllers, TParties) + val env = this.copy(ctx = Context.Reference(Reference.TemplateChoice(tplId, name))) + env.checkType(paramType, KStar) + env.checkType(returnType, KStar) + env.introExprVar(param, paramType).checkTopExpr(controllers, TParties) choiceObservers.foreach( - introExprVar(param, paramType).checkTopExpr(_, TParties) + env.introExprVar(param, paramType).checkTopExpr(_, TParties) ) choiceAuthorizers.foreach( - introExprVar(param, paramType).checkTopExpr(_, TParties) + env.introExprVar(param, paramType).checkTopExpr(_, TParties) ) - introExprVar(selfBinder, TContractId(TTyCon(tplId))) + env + .introExprVar(selfBinder, TContractId(TTyCon(tplId))) .introExprVar(param, paramType) .checkTopExpr(update, TUpdate(returnType)) () diff --git a/canton/community/daml-script-tests/src/test/scala/com/digitalasset/canton/integration/tests/DamlScriptIT.scala b/canton/community/daml-script-tests/src/test/scala/com/digitalasset/canton/integration/tests/DamlScriptIT.scala index 88db573739..77cef223df 100644 --- a/canton/community/daml-script-tests/src/test/scala/com/digitalasset/canton/integration/tests/DamlScriptIT.scala +++ b/canton/community/daml-script-tests/src/test/scala/com/digitalasset/canton/integration/tests/DamlScriptIT.scala @@ -9,11 +9,11 @@ import com.digitalasset.canton.config.DbConfig import com.digitalasset.canton.config.RequireTypes.Port import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransform, ConfigTransforms, EnvironmentDefinition, - EnvironmentSetup, SharedEnvironment, } import com.digitalasset.canton.logging.LogEntry @@ -32,7 +32,7 @@ abstract class DamlScriptIT(langVersion: LanguageVersion) extends CommunityIntegrationTest with SharedEnvironment with BeforeAndAfterAll { - self: EnvironmentSetup => + self: CantonEnvironmentSetup => registerPlugin(new UseReferenceBlockSequencer[DbConfig.H2](loggerFactory)) diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala index 5cf4821f09..f23ddece72 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package com.digitalasset.canton.integration @@ -20,9 +20,12 @@ import com.digitalasset.canton.logging.NamedLoggerFactory * transforms to perform on the base configuration before starting the environment (typically * making ports unique or some other specialization for the particular tests you're running) */ -abstract class BaseEnvironmentDefinition[C <: SharedCantonConfig[ - C -], E <: Environment[C]]( +abstract class BaseEnvironmentDefinition[ + C <: SharedCantonConfig[ + C + ], + E <: Environment[C], +]( val baseConfig: C, val testingConfig: TestingConfigInternal, val setups: List[BaseTestConsoleEnvironment[C, E] => Unit] = Nil, diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseIntegrationTest.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseIntegrationTest.scala index 71191552b5..9113f0b901 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseIntegrationTest.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseIntegrationTest.scala @@ -4,7 +4,9 @@ package com.digitalasset.canton.integration import com.daml.ledger.javaapi.data.Event +import com.digitalasset.canton.config.SharedCantonConfig import com.digitalasset.canton.console.{BufferedProcessLogger, CommandFailure, ParticipantReference} +import com.digitalasset.canton.environment.Environment import com.digitalasset.canton.logging.LogEntry import com.digitalasset.canton.topology.SynchronizerId import com.digitalasset.canton.{ @@ -13,8 +15,6 @@ import com.digitalasset.canton.{ TestPredicateFiltersFixtureAnyWordSpec, config, } -import com.digitalasset.canton.config.SharedCantonConfig -import com.digitalasset.canton.environment.Environment import org.scalactic.source import org.scalactic.source.Position import org.scalatest.wordspec.FixtureAnyWordSpec @@ -61,7 +61,7 @@ trait BaseIntegrationTest[C <: SharedCantonConfig[C], E <: Environment[C]] with RepeatableTestSuiteTest with PartyTopologyUtils with TestPredicateFiltersFixtureAnyWordSpec { - this: EnvironmentSetup[C, E] => + self: EnvironmentSetup[C, E] => type FixtureParam = BaseTestConsoleEnvironment[C, E] diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/CommunityIntegrationTest.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/CommunityIntegrationTest.scala index c1f989c790..cb190f028b 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/CommunityIntegrationTest.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/CommunityIntegrationTest.scala @@ -3,7 +3,7 @@ package com.digitalasset.canton.integration -import com.digitalasset.canton.environment.{CommunityEnvironmentFactory, CantonEnvironmentFactory} +import com.digitalasset.canton.environment.{CantonEnvironmentFactory, CommunityEnvironmentFactory} trait CommunityIntegrationTest extends CantonBaseIntegrationTest { this: CantonEnvironmentSetup => diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConfigTransforms.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConfigTransforms.scala index 2591ec2acd..1b1987b7a8 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConfigTransforms.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConfigTransforms.scala @@ -937,7 +937,7 @@ object ConfigTransforms { ) ) - def enableAlphaMultiSynchronizerTopologyFeatureFlag: ConfigTransform = { + def enableMultiSynchronizerTopologyFeatureFlag: ConfigTransform = { (cantonConfig: CantonConfig) => cantonConfig.focus(_.parameters.enableAlphaStateViaConfig).replace(true) }.compose( diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentDefinition.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentDefinition.scala index ccaa4ef008..a75c2da096 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentDefinition.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentDefinition.scala @@ -26,7 +26,7 @@ import com.digitalasset.canton.console.{ InstanceReference, TestConsoleOutput, } -import com.digitalasset.canton.environment.CantonNode +import com.digitalasset.canton.environment.{CantonEnvironment, CantonNode} import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -42,7 +42,6 @@ import com.digitalasset.canton.{BaseTest, SynchronizerAlias} import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.LazyLogging import monocle.macros.syntax.lens.* -import com.digitalasset.canton.environment.CantonEnvironment /** Definition of how a environment should be configured and setup. * @param baseConfig diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetup.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetup.scala index 33fe1080c1..c9652c2e56 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetup.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetup.scala @@ -156,7 +156,7 @@ sealed trait EnvironmentSetup[C <: SharedCantonConfig[C], E <: Environment[C]] } try { - val testEnvironment: BaseTestConsoleEnvironment[C, E] = step("Creating test console") { + val testEnvironment = step("Creating test console") { envDef.createTestConsole(environmentFixture, loggerFactory) } @@ -272,11 +272,10 @@ sealed trait EnvironmentSetup[C <: SharedCantonConfig[C], E <: Environment[C]] testName = testName, ) - protected def createEnvironment(testName: Option[String]): BaseTestConsoleEnvironment[C, E] = { - ConcurrentEnvironmentLimiter.create(getClass.getName, numPermits) { + protected def createEnvironment(testName: Option[String]): BaseTestConsoleEnvironment[C, E] = + ConcurrentEnvironmentLimiter.create(getClass.getName, numPermits)( manualCreateEnvironment(testName = testName) - } - } + ) protected def manualDestroyEnvironment(environment: BaseTestConsoleEnvironment[C, E]): Unit = { val config = environment.actualConfig diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/HasCycleUtils.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/HasCycleUtils.scala new file mode 100644 index 0000000000..cab6889514 --- /dev/null +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/HasCycleUtils.scala @@ -0,0 +1,202 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration + +import com.daml.ledger.api.v2.commands.Command +import com.daml.ledger.javaapi.data +import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers +import com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.WrappedCreatedEvent +import com.digitalasset.canton.config +import com.digitalasset.canton.config.ConsoleCommandTimeout +import com.digitalasset.canton.console.ParticipantReference +import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.examples.java.cycle as M +import com.digitalasset.canton.examples.java.cycle.Cycle +import com.digitalasset.canton.participant.ledger.api.client.JavaDecodeUtil +import com.digitalasset.canton.topology.{Party, PartyId} + +/** Adds the ability to run cycles to integration tests + */ +trait HasCycleUtils { + this: CantonBaseIntegrationTest => + + /** @param partyId + * assumes that the party is hosted on participant1 AND participant2 (in the simplest case this + * could simply mean that participant1 == participant2) + */ + def runCycle( + partyId: PartyId, + participant1: ParticipantReference, + participant2: ParticipantReference, + commandId: String = "", + ): Unit = { + + Seq(participant2, participant1).map { participant => + if (participant.packages.find_by_module("Cycle").isEmpty) { + participant.dars.upload(CantonExamplesPath) + } + } + + participantAcs(participant2, partyId) shouldBe empty + + clue("creating cycle " + commandId) { + createCycleContract( + participant1, + partyId, + "I SHALL CREATE", + commandId, + ) + } + + awaitAndArchiveCycleContract(participant2, partyId, commandId).discard + + eventually() { + participantAcs(participant2, partyId) shouldBe empty + } + } + + private def participantAcs( + participant: ParticipantReference, + partyId: PartyId, + ): Seq[LedgerApiTypeWrappers.WrappedCreatedEvent] = + participant.ledger_api.state.acs + .of_party(partyId) + .filter(_.templateId.isModuleEntity("Cycle", "Cycle")) + .map(entry => WrappedCreatedEvent(entry.event)) + + def createCycleCommandJava(party: Party, id: String): data.Command = + new Cycle(id, party.toProtoPrimitive) + .create() + .commands + .loneElement + + def createCycleCommand(party: Party, id: String): Command = + Command.fromJavaProto(createCycleCommandJava(party, id).toProtoCommand) + + def cleanupCycles( + partyId: PartyId, + participant: ParticipantReference, + commandId: String = "", + ): Unit = { + NonEmpty + .from(participant.ledger_api.javaapi.state.acs.filter(M.Cycle.COMPANION)(partyId)) + .foreach(coidsNE => + clue(s"submitting ${coidsNE.size} response(s) for cleanup") { + archiveCycleContracts(participant, partyId, coidsNE, commandId) + } + ) + + eventually() { + participantAcs(participant, partyId) shouldBe empty + } + } + + def createCycleContract( + participant: ParticipantReference, + party: Party, + id: String, + commandId: String = "", + optTimeout: Option[config.NonNegativeDuration] = Some( + ConsoleCommandTimeout.defaultLedgerCommandsTimeout + ), + ): Cycle.Contract = { + val cycle = new M.Cycle(id, party.toProtoPrimitive).create.commands.loneElement + + val tx = participant.ledger_api.javaapi.commands + .submit( + Seq(party), + Seq(cycle), + commandId = commandId, + optTimeout = optTimeout, + ) + + JavaDecodeUtil.decodeAllCreated(Cycle.COMPANION)(tx).loneElement + } + + def awaitAndArchiveCycleContract( + participant: ParticipantReference, + partyId: PartyId, + commandId: String = "", + ): Unit = { + val coid = participant.ledger_api.javaapi.state.acs.await(M.Cycle.COMPANION)(partyId) + archiveCycleContract( + participant, + partyId, + coid, + commandId, + ) + } + def awaitAndTouchCycleContract( + participant: ParticipantReference, + partyId: PartyId, + commandId: String = "", + ): Unit = { + val coid = participant.ledger_api.javaapi.state.acs.await(M.Cycle.COMPANION)(partyId) + touchCycleContract( + participant, + partyId, + coid, + commandId, + ) + } + + def createCycleContracts( + participant: ParticipantReference, + partyId: PartyId, + ids: Seq[String], + commandId: String = "", + optTimeout: Option[config.NonNegativeDuration] = Some( + ConsoleCommandTimeout.defaultLedgerCommandsTimeout + ), + ): Unit = { + if (participant.packages.find_by_module("Cycle").isEmpty) { + participant.dars.upload(CantonExamplesPath) + } + val cycles = ids.map(new M.Cycle(_, partyId.toProtoPrimitive).create.commands.loneElement) + participant.ledger_api.javaapi.commands + .submit(Seq(partyId), cycles, commandId = commandId, optTimeout = optTimeout) + } + + def archiveCycleContract( + participant: ParticipantReference, + partyId: PartyId, + coid: Cycle.Contract, + commandId: String = "", + ): Unit = { + val cycleEx = coid.id.exerciseArchive().commands.loneElement + participant.ledger_api.javaapi.commands.submit( + Seq(partyId), + Seq(cycleEx), + commandId = (if (commandId.isEmpty) "" else s"$commandId-response"), + ) + } + def touchCycleContract( + participant: ParticipantReference, + partyId: PartyId, + coid: Cycle.Contract, + commandId: String = "", + ): Unit = { + val cycleEx = coid.id.exerciseRepeat().commands.loneElement + participant.ledger_api.javaapi.commands.submit( + Seq(partyId), + Seq(cycleEx), + commandId = (if (commandId.isEmpty) "" else s"$commandId-touch"), + ) + } + + def archiveCycleContracts( + participant: ParticipantReference, + partyId: PartyId, + coids: NonEmpty[Seq[Cycle.Contract]], + commandId: String = "", + ): Unit = { + val cycleExs = coids.map(_.id.exerciseArchive().commands.loneElement) + participant.ledger_api.javaapi.commands.submit( + Seq(partyId), + cycleExs, + commandId = (if (commandId.isEmpty) "" else s"$commandId-responses"), + ) + } +} diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/HasTrailingNoneUtils.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/HasTrailingNoneUtils.scala new file mode 100644 index 0000000000..b6fc7fe7c5 --- /dev/null +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/HasTrailingNoneUtils.scala @@ -0,0 +1,47 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration + +import com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers +import com.digitalasset.canton.admin.api.client.data.TemplateId +import com.digitalasset.canton.config +import com.digitalasset.canton.config.ConsoleCommandTimeout +import com.digitalasset.canton.console.ParticipantReference +import com.digitalasset.canton.examples.java.trailingnone.TrailingNone +import com.digitalasset.canton.topology.PartyId + +/** Adds the ability to create TrailingNone instances to integration tests. + */ +trait HasTrailingNoneUtils { + this: CantonBaseIntegrationTest => + + def createTrailingNoneContract( + participant: ParticipantReference, + partyId: PartyId, + commandId: String = "", + optTimeout: Option[config.NonNegativeDuration] = Some( + ConsoleCommandTimeout.defaultLedgerCommandsTimeout + ), + ): Unit = { + if (participant.packages.find_by_module("TrailingNone").isEmpty) { + participant.dars.upload(CantonExamplesPath) + } + val cmd = + TrailingNone.create(partyId.toProtoPrimitive, java.util.Optional.empty()).commands.loneElement + participant.ledger_api.javaapi.commands + .submit(Seq(partyId), Seq(cmd), commandId = commandId, optTimeout = optTimeout) + } + + def trailingNoneAcsWithBlobs( + participant: ParticipantReference, + party: PartyId, + ): Seq[LedgerApiTypeWrappers.WrappedContractEntry] = + participant.ledger_api.state.acs + .of_party( + party, + filterTemplates = Seq(TemplateId.fromJavaIdentifier(TrailingNone.TEMPLATE_ID)), + includeCreatedEventBlob = true, + verbose = false, + ) +} diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/TestEnvironment.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/TestEnvironment.scala index 72fc407815..f53698853a 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/TestEnvironment.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/TestEnvironment.scala @@ -18,6 +18,7 @@ import com.digitalasset.canton.config.{ CryptoConfig, SessionEncryptionKeyCacheConfig, } +import com.digitalasset.canton.console.commands.GlobalSecretKeyAdministration import com.digitalasset.canton.console.{ ConsoleEnvironment, ConsoleEnvironmentTestHelpers, @@ -25,10 +26,10 @@ import com.digitalasset.canton.console.{ InstanceReference, LocalInstanceReference, } -import com.digitalasset.canton.console.commands.GlobalSecretKeyAdministration import com.digitalasset.canton.crypto.Crypto import com.digitalasset.canton.integration.bootstrap.InitializedSynchronizer import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage import com.digitalasset.canton.tracing.{NoReportingTracerProvider, TraceContext} @@ -69,6 +70,7 @@ trait TestEnvironment[+C] testedReleaseProtocolVersion, FutureSupervisor.Noop, environment.clock, + CommonMockMetrics.cryptoMetrics, executionContext, environmentTimeouts, BatchingConfig(), diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/bootstrap/NetworkBootstrapper.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/bootstrap/NetworkBootstrapper.scala index 61f560198e..3853fda045 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/bootstrap/NetworkBootstrapper.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/bootstrap/NetworkBootstrapper.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.console.{ MediatorReference, SequencerReference, } +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.{EnvironmentDefinition, TestConsoleEnvironment} import com.digitalasset.canton.topology.{ MediatorId, @@ -48,13 +49,21 @@ class NetworkBootstrapper(networks: NetworkTopologyDescription*)(implicit private def bootstrapSynchronizer(desc: NetworkTopologyDescription): Unit = { val mediatorsToSequencers = desc.overrideMediatorToSequencers.getOrElse( - desc.mediators.map(_ -> (desc.sequencers, PositiveInt.one, NonNegativeInt.zero)).toMap + desc.mediators + .map( + _ -> MediatorSequencersConfiguration( + desc.sequencers, + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ) + ) + .toMap ) val synchronizerId = env.bootstrap.synchronizer( synchronizerName = desc.synchronizerName, sequencers = desc.sequencers, - mediatorsToSequencers = mediatorsToSequencers, + mediatorsToSequencers = mediatorsToSequencers.view.mapValues(_.toTuple).toMap, synchronizerOwners = desc.synchronizerOwners, synchronizerThreshold = desc.synchronizerThreshold, staticSynchronizerParameters = desc.staticSynchronizerParameters, @@ -82,7 +91,7 @@ object NetworkBootstrapper { /** @param overrideMediatorToSequencers * By default, mediators connect to all sequencers. If set, the provided map will override the - * default behavior. The positive int defines the mediator's sequencer trust threshold. + * default behavior. */ final case class NetworkTopologyDescription( synchronizerName: String, @@ -92,9 +101,7 @@ final case class NetworkTopologyDescription( mediators: Seq[MediatorReference], staticSynchronizerParameters: StaticSynchronizerParameters, mediatorRequestAmplification: SubmissionRequestAmplification, - overrideMediatorToSequencers: Option[ - Map[MediatorReference, (Seq[SequencerReference], PositiveInt, NonNegativeInt)] - ], + overrideMediatorToSequencers: Option[Map[MediatorReference, MediatorSequencersConfiguration]], mediatorThreshold: PositiveInt, ) { def withTopologyChangeDelay( @@ -116,7 +123,7 @@ object NetworkTopologyDescription { mediatorRequestAmplification: SubmissionRequestAmplification = SubmissionRequestAmplification.NoAmplification, overrideMediatorToSequencers: Option[ - Map[MediatorReference, (Seq[SequencerReference], PositiveInt, NonNegativeInt)] + Map[MediatorReference, MediatorSequencersConfiguration] ] = None, overrideStaticSynchronizerParameters: Option[StaticSynchronizerParameters] = None, mediatorThreshold: PositiveInt = PositiveInt.one, @@ -157,6 +164,15 @@ object NetworkTopologyDescription { PositiveInt.one, ) + /** Defines how mediators connect to the sequencers */ + final case class MediatorSequencersConfiguration( + sequencers: Seq[SequencerReference], + trustThreshold: PositiveInt, + livenessMargin: NonNegativeInt, + ) { + def toTuple: (Seq[SequencerReference], PositiveInt, NonNegativeInt) = + (sequencers, trustThreshold, livenessMargin) + } } /** A data container to hold useful information for initialized synchronizers diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseLedgerApiTestTool.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseLedgerApiTestTool.scala new file mode 100644 index 0000000000..33b825f0e3 --- /dev/null +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseLedgerApiTestTool.scala @@ -0,0 +1,437 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.plugins + +import better.files.File +import com.daml.ledger.api.testtool.CliParser +import com.daml.ledger.api.testtool.runner.{AvailableTests, Config, ConfiguredTests, TestRunner} +import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.config.{ + CantonConfig, + ClientConfig, + NonNegativeFiniteDuration as NonNegativeFiniteDurationConfig, +} +import com.digitalasset.canton.console.{LocalParticipantReference, RemoteParticipantReference} +import com.digitalasset.canton.integration.plugins.UseLedgerApiTestTool.{ + EnvVarTestOverrides, + LAPITTVersion, + LedgerTestTool, +} +import com.digitalasset.canton.integration.util.ExternalCommandExecutor +import com.digitalasset.canton.integration.{ + ConfigTransforms, + EnvironmentSetupPlugin, + TestConsoleEnvironment, +} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} +import com.digitalasset.canton.platform.apiserver.SeedService.Seeding +import com.digitalasset.canton.tracing.{NoTracing, TraceContext} +import com.digitalasset.canton.version.ReleaseVersion +import com.digitalasset.daml.lf.language.LanguageVersion +import monocle.macros.syntax.lens.* +import org.scalatest.Assertions +import org.scalatest.concurrent.ScalaFutures.* +import org.scalatest.time.{Seconds, Span} + +import scala.concurrent.blocking +import scala.util.{Failure, Success, Try} + +/** Plugin to provide the LedgerApiTestTool to a + * [[com.digitalasset.canton.integration.BaseIntegrationTest]] instance for + * - invoking ledger api test tool in an external java process + * - configuring canton with config settings required for conformance tests + */ +class UseLedgerApiTestTool( + protected val loggerFactory: NamedLoggerFactory, + connectedSynchronizersCount: Int, + lfVersion: LanguageVersion = LanguageVersion.latestStableLfVersion, + // If set, unique benchmark name for uploading benchmark results to datadog. + benchmarkReportFileO: Option[String] = None, + version: LAPITTVersion = LAPITTVersion.Latest, + javaOpts: String = "-Xmx500m", + defaultExtraArguments: Map[String, String] = Map("--timeout-scale-factor" -> "4"), +) extends EnvironmentSetupPlugin + with NoTracing + with EnvVarTestOverrides { + + protected val ledgerApiTestToolPatience: PatienceConfig = + PatienceConfig(timeout = scaled(Span(500, Seconds))) + + private def defaultExtraArgumentsSeq: Seq[String] = defaultExtraArguments.flatMap { case (k, v) => + Seq(k, v) + }.toSeq + + require( + benchmarkReportFileO.forall(_.startsWith("benchmark_")), + s"Benchmark report file must start with 'benchmark_', otherwise it won't be reported to DataDog. Found: $benchmarkReportFileO", + ) + + private var testTool: LedgerTestTool = _ + + private val tempDir = File.newTemporaryDirectory() + + private val commandExecutor = new ExternalCommandExecutor(loggerFactory) + + override def beforeEnvironmentCreated(config: CantonConfig): CantonConfig = { + // First ensure we are able to find and invoke java as that is needed to invoke the test tool. + commandExecutor.exec(cmd = "java --version", errorHint = "Is 'java' not on the path?") + + def tryDownload(testToolRelease: ReleaseVersion): LedgerTestTool.Assembly = { + val otherLfVersions = + if (LanguageVersion.stableLfVersions.contains(lfVersion)) + LanguageVersion.stableLfVersions.takeWhile(_ < lfVersion) + else List.empty + + // find and download test tool with the higher stable LF version + otherLfVersions + .foldRight(getOrDownloadTestTool(testToolRelease, lfVersion)) { (otherLfVersion, res) => + res.recoverWith { + case error if isMissingArtifact(error) => + logger.info( + s"LAPITT for LF $lfVersion is not published for release $testToolRelease. " + + s"Falling back to LF $otherLfVersion. This is NOT a test failure." + ) + getOrDownloadTestTool(testToolRelease, otherLfVersion).orElse(Failure(error)) + } + } + .fold(throw _, identity) + } + + testTool = version match { + case LAPITTVersion.Local => LedgerTestTool.Local(AvailableTests(lfVersion)) + case LAPITTVersion.Latest => tryDownload(UseLedgerApiTestTool.latestRelease(logger)) + case LAPITTVersion.Explicit(release) => tryDownload(release) + } + + // ensure we use production seeding setting in ledger api conformance and performance tests + (ConfigTransforms.updateContractIdSeeding(Seeding.Weak) andThen + // static time tests require this + (_.focus(_.monitoring.logging.delayLoggingThreshold) + .replace(NonNegativeFiniteDurationConfig.ofSeconds(1000))))(config) + } + + override def afterEnvironmentDestroyed(config: CantonConfig): Unit = + // clear dars in temp dir + tempDir.clear() + + def runSuites( + suites: String, // comma-separated list of suites + exclude: Seq[String], + concurrency: Int, + kv: (String, String)* + )(implicit env: TestConsoleEnvironment): Unit = { + val excludeParameter = NonEmpty.from(exclude) match { + case Some(suitesNE) => Seq("--exclude", suitesNE.mkString(",")) + case None => Nil + } + + val additionalParameters = (defaultExtraArguments ++ kv).flatMap { case (k, v) => Seq(k, v) } + + runTestsInternal( + concurrentTestRuns = concurrency, + connectedSynchronizersCount = connectedSynchronizersCount, + testInclusions = suites.split(",").toSeq, + extraArgs = additionalParameters.toSeq ++ excludeParameter, + testParticipants = testParticipants(useJson = false), + useJson = false, + ) + } + + def runSuitesSerially( + suites: String, // comma-separated list of suites + exclude: Seq[String], + kv: (String, String)* + )(implicit env: TestConsoleEnvironment): Unit = + runSuites(suites = suites, exclude = exclude, concurrency = 1, kv*) + + def runShardedSuites( + shard: Int, + numShards: Int, + exclude: Seq[String], + concurrentTestRuns: Int = 4, + useJson: Boolean, + )(implicit + env: TestConsoleEnvironment + ): Unit = { + val allTests: Seq[String] = testTool match { + case LedgerTestTool.Assembly(assemblyJar) => + execTestTool(assemblyJar, Array("--list-all")) + .split("\n") + .toSeq + .filter(_.contains(":")) + .map(_.trim) + case LedgerTestTool.Local(tests) => ConfiguredTests(tests, Config.default).allTestNames + } + val filteredTests = allTests + .filter(line => exclude.forall(not => !line.contains(not))) + .zipWithIndex + .collect { case (test, idx) if idx % numShards == shard => test } + + runTestsInternal( + concurrentTestRuns = concurrentTestRuns, + connectedSynchronizersCount = connectedSynchronizersCount, + testInclusions = filteredTests, + extraArgs = defaultExtraArgumentsSeq, + testParticipants = testParticipants(useJson), + useJson = useJson, + ) + } + + @SuppressWarnings(Array("com.digitalasset.canton.RequireBlocking")) + private def getOrDownloadTestTool( + release: ReleaseVersion, + lfVersion: LanguageVersion, + ): Try[LedgerTestTool.Assembly] = { + val testToolName: String = s"ledger-api-test-tool-$lfVersion" + val filename = s"$testToolName-${release.fullVersion}.jar" + val destination = + File(System.getProperty("user.home")) / ".cache" / testToolName / filename + // Check if test tool resides in destination. If not, download test tool. + blocking(this.synchronized { + if (!destination.exists) { + logger.info(s"Downloading $filename from S3.") + destination.parent.createDirectoryIfNotExists(createParents = true) + LAPITTResolver.download(release, lfVersion, destination, logger) + } else Success(()) + }).map(_ => LedgerTestTool.Assembly(destination)) + } + + private def isMissingArtifact(error: Throwable): Boolean = + Option(error.getMessage).exists { msg => + val lower = msg.toLowerCase + msg.contains("404") || lower.contains("not found") + } + + private def runTestsInternal( + concurrentTestRuns: Int, + connectedSynchronizersCount: Int, + testInclusions: Seq[String], + extraArgs: Seq[String], + testParticipants: Seq[String], + useJson: Boolean, + ): Unit = { + val testInclusionsAfterEnvArgConsideration = envArgTestsInclusion + .map { selectedTests => + val filtered = testInclusions.filter(selectedTests.testCaseEnabled) + if (filtered.isEmpty) { + // Fine to use the scalatest cancel here as this method is expected to be invoked from + // from a ScalaTest case. + org.scalatest.Assertions.cancel( + s"After applying the restriction from the env var $LapittRunOnlyEnvVarName no tests remain to be run. " + + s"Original test selection: $testInclusions. Restriction applied: $selectedTests." + ) + } + + filtered + } + .getOrElse(testInclusions) + val jsonOpt = if (useJson) Seq("--json-api-mode") else Seq.empty + val args = Array( + "--concurrent-test-runs", + concurrentTestRuns.toString, + "--connected-synchronizers", + connectedSynchronizersCount.toString, + "-v", + "--include", + testInclusionsAfterEnvArgConsideration.mkString(","), + ) ++ jsonOpt ++ extraArgs ++ testParticipants + + testTool match { + case LedgerTestTool.Assembly(assemblyJar) => execTestTool(assemblyJar, args) + case LedgerTestTool.Local(tests) => + val config = CliParser.parse(args).getOrElse(sys.error("Invalid config")) + val runner = new TestRunner(tests, config) + val failures = runner + .runInProcess(logger.underlying) + .futureValue(config = ledgerApiTestToolPatience, pos = implicitly) + .map(test => test.result.left.map(failure => s"${test.name} failed: $failure")) + .collect { case Left(failure) => failure } + if (failures.nonEmpty) + Assertions.fail( + s"Some Ledger API tests have failed: ${failures.mkString("\n\t", "\n\t", "")}" + ) + } + } + + private def execTestTool(assemblyJar: File, args: Array[String]): String = + commandExecutor.exec( + cmd = s"java $javaOpts -jar ${assemblyJar.toString} ${args.mkString(" ")}", + errorHint = s"Failures in aforementioned test suite.", + ) + + private def endpointAsString(config: ClientConfig) = s"${config.address}:${config.port.toString}" + + private def testParticipants( + useJson: Boolean + )(implicit env: TestConsoleEnvironment): Seq[String] = + env.participants.all + .map { p => + val ledgerApiEndpoint = p match { + case remote: RemoteParticipantReference if useJson => + remote.config.ledgerJsonApi + .map(_.endpointAsString) + .getOrElse(throw new IllegalArgumentException(s"invalid remote reference: $remote")) + case local: LocalParticipantReference if useJson => + local.config.httpLedgerApi.clientConfig + .map(_.endpointAsString) + .getOrElse(throw new IllegalArgumentException(s"invalid local reference: $local")) + case _ => endpointAsString(p.config.clientLedgerApi) + } + val adminApiEndpoint = endpointAsString(p.config.clientAdminApi) + s"$ledgerApiEndpoint;$adminApiEndpoint" + } + +} + +object UseLedgerApiTestTool { + sealed trait TestInclusions extends Product with Serializable { + def testCaseEnabled(testCaseName: String): Boolean + } + + object TestInclusions { + case object AllIncluded extends TestInclusions { + def testCaseEnabled(testCaseName: String): Boolean = true + } + + /** @param includedSuites + * Full suites to include + * @param includedTestCases + * Specific test cases to include. Adding individual test cases here is redundant if their + * suite is already included in [[includedSuites]]. + */ + final case class SelectedTests( + includedSuites: Set[String], + includedTestCases: Set[String] = Set.empty, + ) extends TestInclusions { + def testCaseEnabled(testCaseName: String): Boolean = + testCaseName.split(":").map(_.trim).toSeq match { + case Seq(suite, _) => + includedSuites.contains(suite) || includedTestCases.contains(testCaseName) + case Seq(suite) => includedSuites.contains(suite) + case _other => + throw new IllegalArgumentException( + s"Invalid test case name: $testCaseName. Expected format: SuiteName:TestCaseName" + ) + } + } + } + + trait EnvVarTestOverrides { + this: NamedLogging => + + protected val LapittRunOnlyEnvVarName = "LAPI_CONFORMANCE_TEST_RUN_ONLY" + + /** Set the environment variable `LAPI_CONFORMANCE_TEST_RUN_ONLY` to a comma-separated list of + * test suite names or test case names to restrict the tests being run as part of a specific + * conformance test suite target. + * + * e.g. LAPI_CONFORMANCE_TEST_RUN_ONLY=CommandServiceIT sbt "testOnly + * *JsonApiConformanceIntegrationShardedTest_Shard_0" + */ + protected lazy val envTestFilterO: Option[Seq[String]] = + sys.env.get(LapittRunOnlyEnvVarName).map(_.split(",").view.map(_.trim).toSeq) + + // Implementors of this trait should use this value to filter the tests being run + // with the restriction provided via the env var. + protected lazy val envArgTestsInclusion: Option[TestInclusions.SelectedTests] = envTestFilterO + .flatMap { envTestFilter => + val selectedTestsO = envTestFilter + .map(_.split(":").toSeq match { + case Seq(suite, test) => + TestInclusions + .SelectedTests(includedSuites = Set.empty, includedTestCases = Set(s"$suite:$test")) + case Seq(suite) => TestInclusions.SelectedTests(includedSuites = Set(suite)) + case _ => throw new IllegalArgumentException(s"Invalid test filter: $envTestFilter") + }) + .reduceOption((s1, s2) => + TestInclusions.SelectedTests( + includedSuites = s1.includedSuites ++ s2.includedSuites, + includedTestCases = s1.includedTestCases ++ s2.includedTestCases, + ) + ) + + selectedTestsO.foreach(selectedTests => + logger.debug( + s"$LapittRunOnlyEnvVarName set to $envTestFilterO. Filtering current test selection in ${getClass.getSimpleName} using restriction $selectedTests." + )(TraceContext.empty) + ) + + selectedTestsO + } + } + + sealed trait LAPITTVersion + + object LAPITTVersion { + // Run the latest released version of the LAPITT. + case object Latest extends LAPITTVersion + + // Run the specified version of the LAPITT. + final case class Explicit(version: ReleaseVersion) extends LAPITTVersion + + // Run the LAPITT from the classpath. + // Requires running `sbt ledger-test-tool/assembly` first + case object Local extends LAPITTVersion + } + + private sealed trait LedgerTestTool + + private object LedgerTestTool { + final case class Assembly(assemblyJar: File) extends LedgerTestTool + final case class Local(tests: AvailableTests) extends LedgerTestTool + } + + // finds all major.minor.patch releases + def findAllCoreVersions(toolReleases: Seq[ReleaseVersion]): Seq[(Int, Int, Int)] = + toolReleases.map(_.majorMinorPatch).distinct.sorted + + // finds versions of the release given and sorts them in ascending order + def findMatchingVersions( + toolReleases: Seq[ReleaseVersion], + majorMinorPatch: (Int, Int, Int), + ): Seq[ReleaseVersion] = + toolReleases + .filter(_.majorMinorPatch == majorMinorPatch) + .sorted + + def latestRelease( + logger: TracedLogger, + includeAdHoc: Boolean = true, + includeSnapshot: Boolean = true, + )(implicit tc: TraceContext): ReleaseVersion = { + val toolReleases: Seq[ReleaseVersion] = LAPITTResolver.listAllReleases().filter { r => + (includeAdHoc || !r.isAdHoc) && (includeSnapshot || !r.isSnapshot) + } + val latestCoreVersion = findAllCoreVersions(toolReleases).lastOption.getOrElse( + throw new RuntimeException( + s"No releases found among the following versions: ${toolReleases.map(_.fullVersion)}" + ) + ) + + val matchingVersions = findMatchingVersions(toolReleases, latestCoreVersion) + val matchingVersion = matchingVersions.lastOption.getOrElse( + throw new RuntimeException(s"No matching version found for release $latestCoreVersion") + ) + logger.debug(s"found ${matchingVersion.fullVersion} as latest version of $latestCoreVersion") + + matchingVersion + } + + def latestReleases( + logger: TracedLogger, + includeAdHoc: Boolean = false, + includeSnapshot: Boolean = true, + )(implicit tc: TraceContext): Seq[ReleaseVersion] = { + val toolVersions = LAPITTResolver.listAllReleases().filter { r => + (includeAdHoc || !r.isAdHoc) && (includeSnapshot || !r.isSnapshot) + } + + val coreVersions = findAllCoreVersions(toolVersions) + val latestReleases = coreVersions.flatMap(findMatchingVersions(toolVersions, _).lastOption) + logger.debug(s"found $latestReleases as latest versions for each release") + + latestReleases + } + +} diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/MultiSynchronizerFeatureFlag.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/MultiSynchronizerFeatureFlag.scala index a9238535cd..cc70507caa 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/MultiSynchronizerFeatureFlag.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/MultiSynchronizerFeatureFlag.scala @@ -26,14 +26,12 @@ object MultiSynchronizerFeatureFlag { .item .featureFlags - if ( - !currentFeatureFlags.contains(ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) - ) { + if (!currentFeatureFlags.contains(ParticipantTopologyFeatureFlag.EnableMultiSynchronizer)) { p.topology.synchronizer_trust_certificates.propose( p.id, synchronizerId, featureFlags = - currentFeatureFlags :+ ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer, + currentFeatureFlags :+ ParticipantTopologyFeatureFlag.EnableMultiSynchronizer, ) } else () @@ -45,7 +43,7 @@ object MultiSynchronizerFeatureFlag { ) .loneElement .item - .featureFlags should contain(ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) + .featureFlags should contain(ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) } } @@ -63,12 +61,12 @@ object MultiSynchronizerFeatureFlag { .item .featureFlags - if (currentFeatureFlags.contains(ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer)) + if (currentFeatureFlags.contains(ParticipantTopologyFeatureFlag.EnableMultiSynchronizer)) p.topology.synchronizer_trust_certificates.propose( p.id, synchronizerId, featureFlags = currentFeatureFlags.filterNot( - _ == ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer + _ == ParticipantTopologyFeatureFlag.EnableMultiSynchronizer ), ) else () @@ -80,7 +78,7 @@ object MultiSynchronizerFeatureFlag { ) .loneElement .item - .featureFlags should not contain (ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) + .featureFlags should not contain (ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) } } diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/KmsDriver.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/KmsDriver.scala deleted file mode 100644 index d1d8a54ad5..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/KmsDriver.scala +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.crypto.kms.driver.api - -trait KmsDriver diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/KmsDriverFactory.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/KmsDriverFactory.scala deleted file mode 100644 index 70292b2091..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/KmsDriverFactory.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.crypto.kms.driver.api - -import com.digitalasset.canton.driver.api.DriverFactory - -trait KmsDriverFactory extends DriverFactory { - override type Driver <: KmsDriver -} diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriver.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriver.scala deleted file mode 100644 index 00827300b8..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriver.scala +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.crypto.kms.driver.api.v1 - -import com.digitalasset.canton.crypto.kms.driver.api -import io.opentelemetry.context.Context - -import scala.concurrent.Future - -/** The interface for a pluggable KMS implementation, that is, a KMS Driver. - * - * Cryptographic operations are asynchronous, i.e., they return a Future. In case of failure, the - * Future must fail with a [[KmsDriverException]]. Transient failures should still fail the Future, - * but the exception’s `retryable` flag should be set to true. An exception should only be thrown - * for driver operations (e.g., sign), and not, for example, for health checks. - * - * Each KMS operation takes an OpenTelemetry [[io.opentelemetry.context.Context]] as a trace - * context that can optionally be propagated to the external KMS. - */ -trait KmsDriver extends api.KmsDriver with AutoCloseable { - - require( - supportedSigningKeySpecs.nonEmpty, - "Supported signing key specifications must not be empty.", - ) - require( - supportedSigningAlgoSpecs.nonEmpty, - "Supported signing algorithm specifications must not be empty.", - ) - require( - supportedEncryptionKeySpecs.nonEmpty, - "Supported encryption key specifications must not be empty.", - ) - require( - supportedEncryptionAlgoSpecs.nonEmpty, - "Supported encryption algorithm specifications must not be empty.", - ) - - /** Returns the current health of the driver. The driver should not throw an exception; instead, - * it should return a [[com.digitalasset.canton.crypto.kms.driver.api.v1.KmsDriverHealth]] value. - * - * @return - * A future that completes with the driver's health. - */ - def health: Future[KmsDriverHealth] - - /** The supported signing key specifications by the driver. This must not be empty. */ - def supportedSigningKeySpecs: Set[SigningKeySpec] - - /** The supported signing algorithm specifications by the driver. This must not be empty. */ - def supportedSigningAlgoSpecs: Set[SigningAlgoSpec] - - /** The supported encryption key specifications by the driver. This must not be empty. */ - def supportedEncryptionKeySpecs: Set[EncryptionKeySpec] - - /** The supported encryption algorithm specifications by the driver. This must not be empty. */ - def supportedEncryptionAlgoSpecs: Set[EncryptionAlgoSpec] - - /** Generate a new signing key pair. - * - * @param signingKeySpec - * The key specification for the new signing key pair. The caller ensures it is a - * [[supportedSigningKeySpecs]]. - * @param keyName - * An optional descriptive name for the key pair, max 300 characters long. - * - * @return - * A future that completes with the unique KMS key identifier, max 300 characters long. - */ - def generateSigningKeyPair( - signingKeySpec: SigningKeySpec, - keyName: Option[String], - )(traceContext: Context): Future[String] - - /** Generate a new asymmetric encryption key pair. - * - * @param encryptionKeySpec - * The key specification of the new encryption key pair. The caller ensures it is a - * [[supportedEncryptionKeySpecs]]. - * @param keyName - * An optional descriptive name for the key pair, max 300 characters long. - * - * @return - * A future that completes with the unique KMS key identifier, max 300 characters long. - */ - def generateEncryptionKeyPair( - encryptionKeySpec: EncryptionKeySpec, - keyName: Option[String], - )(traceContext: Context): Future[String] - - /** Generate a new symmetric encryption key. The default symmetric key specification of the KMS is - * used. - * - * @param keyName - * An optional descriptive name for the symmetric key, max 300 characters long. - * - * @return - * A future that completes with the unique KMS key identifier, max 300 characters long. - */ - def generateSymmetricKey(keyName: Option[String])(traceContext: Context): Future[String] - - /** Sign the given data using the private key identified by the keyId with the given signing - * algorithm specification. If the `algoSpec` is not compatible with the key spec of `keyId` then - * this method must fail with a non-retryable exception. - * - * @param data - * The data to be signed with the specified signature algorithm. The upper bound of the data - * size is 4kb. - * @param keyId - * The identifier of the private signing key. - * @param algoSpec - * The signature algorithm specification. The caller ensures it is a - * [[supportedSigningAlgoSpecs]]. - * - * @return - * A future that completes with the signature. - */ - def sign(data: Array[Byte], keyId: String, algoSpec: SigningAlgoSpec)( - traceContext: Context - ): Future[Array[Byte]] - - /** Asymmetrically decrypt the given ciphertext using the private key identified by the keyId with - * the given asymmetric encryption algorithm specification. If the `algoSpec` is not compatible - * with the key spec of `keyId` then this method must fail with a non-retryable exception. - * - * @param ciphertext - * The asymmetrically encrypted ciphertext that needs to be decrypted. The length of the - * ciphertext depends on the parameters of the asymmetric encryption algorithm. Implementations - * may assume that the length of the ciphertext is at most 6144 bytes in any case. - * @param keyId - * The identifier of the private encryption key to perform the asymmetric decryption with. - * @param algoSpec - * The asymmetric encryption algorithm specification. The caller ensures it is a - * [[supportedEncryptionAlgoSpecs]]. - * - * @return - * A future that completes with the plaintext. - */ - def decryptAsymmetric( - ciphertext: Array[Byte], - keyId: String, - algoSpec: EncryptionAlgoSpec, - )(traceContext: Context): Future[Array[Byte]] - - /** Symmetrically encrypt the given plaintext using the symmetric encryption key identified by the - * keyId. The same/default symmetric encryption algorithm of the KMS must be used for both - * symmetric encryption and decryption. - * - * @param data - * The plaintext to symmetrically encrypt. The upper bound of the data size is 4kb. - * @param keyId - * The identifier of the symmetric encryption key. - * - * @return - * A future that completes with the ciphertext. - */ - def encryptSymmetric(data: Array[Byte], keyId: String)(traceContext: Context): Future[Array[Byte]] - - /** Symmetrically decrypt the given ciphertext using the symmetric encryption key identified by - * the keyId. The same/default symmetric encryption algorithm of the KMS must be used for both - * symmetric encryption and decryption. - * - * @param ciphertext - * The ciphertext to symmetrically decrypt. The upper bound of the ciphertext size is 6144 - * bytes. - * @param keyId - * The identifier of the symmetric encryption key. - * - * @return - * A future that completes with the plaintext. - */ - def decryptSymmetric(ciphertext: Array[Byte], keyId: String)( - traceContext: Context - ): Future[Array[Byte]] - - /** Exports a public key from the KMS for the given key pair identified by keyId. - * - * @param keyId - * The identifier of the key pair. - * - * @return - * A future that completes with the exported [[PublicKey]] - */ - def getPublicKey(keyId: String)(traceContext: Context): Future[PublicKey] - - /** Asserts that the key given by its identifier exists and is active. - * - * @param keyId - * The identifier of the key to be checked. - * - * @return - * A future that completes successfully if the key exists and is active. Otherwise, the future - * must have been failed. - */ - def keyExistsAndIsActive(keyId: String)(traceContext: Context): Future[Unit] - - /** Deletes a key given by its identifier from the KMS. - * - * @param keyId - * The identifier of the key to be deleted. - * - * @return - * A future that completes when the key has been deleted or the deletion of the key has been - * scheduled. - */ - def deleteKey(keyId: String)(traceContext: Context): Future[Unit] - -} - -/** A public key exported from the KMS. - * - * @param key - * The DER-encoded X.509 public key (SubjectPublicKeyInfo). EC keys must be uncompressed. - * @param spec - * The key specification of the key pair - */ -final case class PublicKey(key: Array[Byte], spec: KeySpec) diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverException.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverException.scala deleted file mode 100644 index 5462ad9288..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverException.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.crypto.kms.driver.api.v1 - -/** An exception that should be thrown by the KMS driver in case of failure. - * - * @param exception - * The underlying exception of the KMS that lead to the failure. - * @param retryable - * If true the caller can retry the failing operation. - */ -final case class KmsDriverException(exception: Throwable, retryable: Boolean) - extends RuntimeException( - s"KMS Driver exception (retryable=$retryable): ${exception.getMessage}", - exception, - ) diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverFactory.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverFactory.scala deleted file mode 100644 index 34493d52fb..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverFactory.scala +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.crypto.kms.driver.api.v1 - -import com.digitalasset.canton.crypto.kms.driver.api -import com.digitalasset.canton.driver.api.v1 - -trait KmsDriverFactory extends api.KmsDriverFactory with v1.DriverFactory { - - override val version: Int = 1 - - override type Driver <: KmsDriver - -} diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverHealth.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverHealth.scala deleted file mode 100644 index e2fbd24177..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverHealth.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.crypto.kms.driver.api.v1 - -sealed trait KmsDriverHealth extends Product with Serializable - -object KmsDriverHealth { - - /** The driver is healthy. */ - case object Ok extends KmsDriverHealth - - /** The driver's state is degraded but still functional (e.g., increased latency). The node's - * crypto component will report as degraded, although the node's APIs will continue to serve - * requests. - */ - final case class Degraded(reason: String) extends KmsDriverHealth - - /** The driver has failed and is currently non-functional, but may recover without a restart. - * While in this failing state, the node's APIs will be marked as not serving, but the liveness - * probe will continue to succeed and will not trigger a restart. - */ - final case class Failed(reason: String) extends KmsDriverHealth - - /** The driver is in a fatal and irrecoverable state, requiring the entire node to be restarted. - * The [[Fatal]] health status will propagate to the node's liveness probe, causing it to fail - * and trigger a restart via Kubernetes. - */ - final case class Fatal(reason: String) extends KmsDriverHealth -} diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverSpecs.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverSpecs.scala deleted file mode 100644 index 1ec3a9785d..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/crypto/kms/driver/api/v1/KmsDriverSpecs.scala +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.crypto.kms.driver.api.v1 - -sealed trait KeySpec extends Product with Serializable - -sealed trait SigningKeySpec extends KeySpec -object SigningKeySpec { - - /** Elliptic Curve Key from the Curve25519 curve as defined in http://ed25519.cr.yp.to/ - */ - case object EcCurve25519 extends SigningKeySpec - - /** Elliptic Curve Signing Key based on NIST P-256 (aka secp256r1) as defined in - * https://doi.org/10.6028/NIST.FIPS.186-4 - */ - case object EcP256 extends SigningKeySpec - - /** Elliptic Curve Signing Key based on NIST P-384 as defined in - * https://doi.org/10.6028/NIST.FIPS.186-4 - */ - case object EcP384 extends SigningKeySpec - - /** Elliptic Curve Key from SECG P256k1 curve (aka secp256k1) commonly used in bitcoin and - * ethereum as defined in https://www.secg.org/sec2-v2.pdf - */ - case object EcSecp256k1 extends SigningKeySpec -} - -sealed trait SigningAlgoSpec -object SigningAlgoSpec { - - /** EdDSA signature scheme based on Curve25519 and SHA512 as defined in http://ed25519.cr.yp.to/ - */ - case object Ed25519 extends SigningAlgoSpec - - /** Elliptic Curve Digital Signature Algorithm with SHA256 as defined in - * https://doi.org/10.6028/NIST.FIPS.186-4 - */ - case object EcDsaSha256 extends SigningAlgoSpec - - /** Elliptic Curve Digital Signature Algorithm with SHA384 as defined in - * https://doi.org/10.6028/NIST.FIPS.186-4 - */ - case object EcDsaSha384 extends SigningAlgoSpec -} - -sealed trait EncryptionKeySpec extends KeySpec -object EncryptionKeySpec { - - /** Elliptic Curve Key from the P-256 curve (aka Secp256r1) as defined in - * https://doi.org/10.6028/NIST.FIPS.186-4 - */ - case object EcP256 extends EncryptionKeySpec - - /** RSA 2048 bit */ - case object Rsa2048 extends EncryptionKeySpec -} - -sealed trait EncryptionAlgoSpec -object EncryptionAlgoSpec { - - /** ECIES with ECDH, AES128 CBC, and HKDF and authentication (MAC) with HMAC-SHA256. This requires - * a P-256 key because we use SHA256, and we need to align the lengths of the curve and the hash - * function. - */ - case object EciesHkdfHmacSha256Aes128Cbc extends EncryptionAlgoSpec - - /** RSA Encryption Scheme with Optimal Asymmetric Encryption Padding (OAEP) using SHA256 as - * defined in https://datatracker.ietf.org/doc/html/rfc8017#section-7.1 - */ - case object RsaEsOaepSha256 extends EncryptionAlgoSpec -} diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/driver/api/DriverFactory.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/driver/api/DriverFactory.scala deleted file mode 100644 index 015cb29b2a..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/driver/api/DriverFactory.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.driver.api - -trait DriverFactory { - - /** The type of the driver that is instantiated by an implementation of the driver factory. */ - type Driver - - /** The name of the driver that is instantiated by an implementation of the driver factory. */ - def name: String - - /** The version of the driver API this factory is implemented against. */ - def version: Int - -} diff --git a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/driver/api/v1/DriverFactory.scala b/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/driver/api/v1/DriverFactory.scala deleted file mode 100644 index d004c5decc..0000000000 --- a/canton/community/kms-driver-api/src/main/scala/com/digitalasset/canton/driver/api/v1/DriverFactory.scala +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.driver.api.v1 - -import com.digitalasset.canton.driver.api -import org.slf4j.Logger -import pureconfig.{ConfigReader, ConfigWriter} - -import scala.concurrent.ExecutionContext - -/** The corresponding factory for an implementation of a [[Driver]] that can instantiate a new - * driver. - */ -trait DriverFactory extends api.DriverFactory { - - /** The name of the driver that is instantiated by an implementation of the driver factory. */ - def name: String - - /** The version of the driver API this factory is implemented against. */ - def version: Int - - /** Optional information for the build of the driver factory, e.g., git commit hash. */ - def buildInfo: Option[String] - - /** The driver-specific configuration type. */ - type ConfigType - - /** The parser to load the driver-specific configuration. */ - def configReader: ConfigReader[ConfigType] - - /** The configuration writer for the driver-specific configuration. - * - * @param confidential - * If the flag is true, the config writer should omit any sensitive configuration items, such - * as credentials. - */ - def configWriter(confidential: Boolean): ConfigWriter[ConfigType] - - /** The creation method of a driver by this factory. If the creation of the driver fails this - * method should throw an exception. - * - * @param config - * The driver-specific configuration. - * @param loggerFactory - * A logger factory that should be used by the driver to create a logger for a particular - * class. - * @param executionContext - * The execution context that should be used by the driver. - * - * @return - * A new instance of [[Driver]]. - */ - def create( - config: ConfigType, - loggerFactory: Class[?] => Logger, - executionContext: ExecutionContext, - ): Driver -} diff --git a/canton/community/kms-driver-testing/src/main/scala/com/digitalasset/canton/crypto/kms/driver/testing/v1/KmsDriverTestUtils.scala b/canton/community/kms-driver-testing/src/main/scala/com/digitalasset/canton/crypto/kms/driver/testing/v1/KmsDriverTestUtils.scala index 84c1b72a72..cf254282e6 100644 --- a/canton/community/kms-driver-testing/src/main/scala/com/digitalasset/canton/crypto/kms/driver/testing/v1/KmsDriverTestUtils.scala +++ b/canton/community/kms-driver-testing/src/main/scala/com/digitalasset/canton/crypto/kms/driver/testing/v1/KmsDriverTestUtils.scala @@ -24,6 +24,7 @@ import com.digitalasset.canton.crypto.{ SymmetricKeyScheme, } import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.{FutureHelpers, crypto} import com.google.protobuf.ByteString import io.scalaland.chimney.Transformer @@ -73,6 +74,8 @@ object KmsDriverTestUtils extends FutureHelpers { ) .valueOrFail("no supported encryption algo specs") + val cryptoMetrics = CommonMockMetrics.cryptoMetrics + new JcePureCrypto( defaultSymmetricKeyScheme = SymmetricKeyScheme.Aes128Gcm, signingAlgorithmSpecs = CryptoScheme @@ -85,6 +88,8 @@ object KmsDriverTestUtils extends FutureHelpers { privateKeyConversionCacheTtl = None, signatureVerificationParallelism = CryptoParallelismConfig.defaultSignatureVerificationParallelism, + signingMetrics = cryptoMetrics.signingMetrics, + decryptionMetrics = cryptoMetrics.decryptionMetrics, loggerFactory = NamedLoggerFactory.root, ) } diff --git a/canton/community/ledger-test-tool/src/main/resources/logback.xml b/canton/community/ledger-test-tool/src/main/resources/logback.xml new file mode 100644 index 0000000000..03a157a14b --- /dev/null +++ b/canton/community/ledger-test-tool/src/main/resources/logback.xml @@ -0,0 +1,23 @@ + + + + System.err + + trace + + + %date [%thread] %-5level %logger{10} - %msg%n + + + + + + + + + + + + + + diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/Main.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/Main.scala index e2aacd78dc..0b1efee443 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/Main.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/Main.scala @@ -9,6 +9,6 @@ object Main { def main(args: Array[String]): Unit = { val config = CliParser.parse(args).getOrElse(sys.exit(1)) // TODO(#32282) Make it configurable: Add lfVersion in CLI config - new TestRunner(AvailableTests.v2_2, config).runAndExit() + new TestRunner(AvailableTests.latestStableLf, config).runAndExit() } } diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerServices.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerServices.scala index c206c14e84..7373431a22 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerServices.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerServices.scala @@ -83,6 +83,7 @@ import com.daml.ledger.api.v2.command_completion_service.{ CommandCompletionServiceGrpc, CompletionStreamRequest, CompletionStreamResponse, + GetCompletionsRequest, } import com.daml.ledger.api.v2.command_service.CommandServiceGrpc.CommandService import com.daml.ledger.api.v2.command_service.{ @@ -521,16 +522,35 @@ private final class LedgerServicesJson( } yield response } - def commandCompletion: CommandCompletionService = ( - request: CompletionStreamRequest, - responseObserver: StreamObserver[CompletionStreamResponse], - ) => - wsCall( - JsCommandService.completionStreamEndpoint, - request, - responseObserver, - Future.successful(_: CompletionStreamResponse), - ) + def commandCompletion: CommandCompletionService = new CommandCompletionService { + + /** Deprecated: please use ``GetCompletions`` instead. Subscribe to command completion events. + */ + override def completionStream( + request: CompletionStreamRequest, + responseObserver: StreamObserver[CompletionStreamResponse], + ): Unit = + wsCall( + JsCommandService.completionStreamEndpoint, + request, + responseObserver, + Future.successful(_: CompletionStreamResponse), + ) + + /** Subscribe to command completion events. This streaming endpoint provides more flexibility in + * filtering than the predecessor ``CompletionStream``. + */ + override def getCompletions( + request: GetCompletionsRequest, + responseObserver: StreamObserver[CompletionStreamResponse], + ): Unit = + wsCall( + JsCommandService.commandCompletionsEndpoint, + request, + responseObserver, + Future.successful(_: CompletionStreamResponse), + ) + } def commandSubmission: CommandSubmissionService = new CommandSubmissionService { diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerTestSuite.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerTestSuite.scala index d4336bd89c..855634b443 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerTestSuite.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerTestSuite.scala @@ -103,6 +103,9 @@ abstract class LedgerTestSuite implicit class IdentifierConverter(id: JavaIdentifier) { def toV1: Identifier = Identifier.fromJavaProto(id.toProto) + + def withPackageId(packageId: Ref.PackageId) = + new JavaIdentifier(packageId, id.getModuleName(), id.getEntityName()) } implicit def partyToString(party: Party): String = party.getValue diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantTestContext.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantTestContext.scala index fd6db97b68..a8a971fc65 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantTestContext.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantTestContext.scala @@ -23,6 +23,7 @@ import com.daml.ledger.api.v2.admin.party_management_service.* import com.daml.ledger.api.v2.command_completion_service.{ CompletionStreamRequest, CompletionStreamResponse, + GetCompletionsRequest, } import com.daml.ledger.api.v2.command_service.{ SubmitAndWaitForTransactionRequest, @@ -612,6 +613,25 @@ trait ParticipantTestContext extends UserManagementTestContext { def findCompletion(parties: Party*)( p: Completion => Boolean ): Future[Option[Completion]] + + // GetCompletions replaces the deprecated completionStream. These helpers duplicate the + // completionStream ones above on purpose: those go away once everything moves to GetCompletions, + // so sharing code now isn't worth it. + def getCompletionsRequest(from: Long = referenceOffset)( + parties: Party* + ): GetCompletionsRequest + def completions( + within: NonNegativeFiniteDuration, + request: GetCompletionsRequest, + ): Future[Vector[CompletionStreamResponse.CompletionResponse]] + def completions( + take: Int, + request: GetCompletionsRequest, + ): Future[Vector[CompletionStreamResponse.CompletionResponse]] + def findCompletion( + request: GetCompletionsRequest + )(p: Completion => Boolean): Future[Option[Completion]] + def offsets(n: Int, request: CompletionStreamRequest): Future[Vector[Long]] def checkHealth(): Future[HealthCheckResponse] def watchHealth(): Future[Seq[HealthCheckResponse]] diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/SingleParticipantTestContext.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/SingleParticipantTestContext.scala index c8ea2cce2b..c1a78d5f73 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/SingleParticipantTestContext.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/SingleParticipantTestContext.scala @@ -37,6 +37,7 @@ import com.daml.ledger.api.v2.admin.party_management_service.* import com.daml.ledger.api.v2.command_completion_service.{ CompletionStreamRequest, CompletionStreamResponse, + GetCompletionsRequest, } import com.daml.ledger.api.v2.command_service.{ SubmitAndWaitForTransactionRequest, @@ -1681,6 +1682,43 @@ final class SingleParticipantTestContext private[participant] ( ): Future[Option[Completion]] = findCompletion(completionStreamRequest()(parties*))(p) + // GetCompletions helpers. Intentionally not sharing plumbing with the completionStream helpers + // above, which are deprecated and slated for removal. + override def getCompletionsRequest(from: Long = referenceOffset)( + parties: Party* + ): GetCompletionsRequest = + new GetCompletionsRequest( + parties = parties.map(_.getValue), + beginExclusive = from, + ) + + override def completions( + within: NonNegativeFiniteDuration, + request: GetCompletionsRequest, + ): Future[Vector[CompletionStreamResponse.CompletionResponse]] = + new StreamConsumer[CompletionStreamResponse]( + services.commandCompletion.getCompletions(request, _) + ) + .within(within.toScala) + .map(_.map(_.completionResponse)) + + override def completions( + take: Int, + request: GetCompletionsRequest, + ): Future[Vector[CompletionStreamResponse.CompletionResponse]] = + new StreamConsumer[CompletionStreamResponse]( + services.commandCompletion.getCompletions(request, _) + ).filterTake(_.completionResponse.isCompletion)(take) + .map(_.map(_.completionResponse)) + + override def findCompletion( + request: GetCompletionsRequest + )(p: Completion => Boolean): Future[Option[Completion]] = + new StreamConsumer[CompletionStreamResponse]( + services.commandCompletion.getCompletions(request, _) + ).find(_.completionResponse.completion.exists(p)) + .map(_.completionResponse.completion.filter(p)) + override def offsets(n: Int, request: CompletionStreamRequest): Future[Vector[Long]] = new StreamConsumer[CompletionStreamResponse]( services.commandCompletion.completionStream(request, _) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/TimeoutParticipantTestContext.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/TimeoutParticipantTestContext.scala index f7041b8081..29d6306955 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/TimeoutParticipantTestContext.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/TimeoutParticipantTestContext.scala @@ -20,6 +20,7 @@ import com.daml.ledger.api.v2.admin.party_management_service.* import com.daml.ledger.api.v2.command_completion_service.{ CompletionStreamRequest, CompletionStreamResponse, + GetCompletionsRequest, } import com.daml.ledger.api.v2.command_service.{ SubmitAndWaitForTransactionRequest, @@ -819,6 +820,32 @@ class TimeoutParticipantTestContext(timeoutScaleFactor: Double, delegate: Partic p: Completion => Boolean ): Future[Option[Completion]] = withTimeout(s"Find completion for parties $parties", delegate.findCompletion(parties*)(p)) + + // GetCompletions helpers (delegating). Kept separate from completionStream on purpose; the latter + // is deprecated and will be removed. + override def getCompletionsRequest(from: Long)( + parties: Party* + ): GetCompletionsRequest = delegate.getCompletionsRequest(from)(parties*) + + override def completions( + within: NonNegativeFiniteDuration, + request: GetCompletionsRequest, + ): Future[Vector[CompletionStreamResponse.CompletionResponse]] = + delegate.completions(within, request) + + override def completions( + take: Int, + request: GetCompletionsRequest, + ): Future[Vector[CompletionStreamResponse.CompletionResponse]] = + delegate.completions(take, request) + + override def findCompletion(request: GetCompletionsRequest)( + p: Completion => Boolean + ): Future[Option[Completion]] = withTimeout( + s"Find completion for request $request", + delegate.findCompletion(request)(p), + ) + override def offsets(n: Int, request: CompletionStreamRequest): Future[Vector[Long]] = withTimeout(s"$n checkpoints for request $request", delegate.offsets(n, request)) override def checkHealth(): Future[HealthCheckResponse] = diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/runner/AvailableTests.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/runner/AvailableTests.scala index ba234893a5..d6f8ceaf19 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/runner/AvailableTests.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/runner/AvailableTests.scala @@ -7,6 +7,7 @@ import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite import com.daml.ledger.api.testtool.suites.{V2_2, V2_3, V2_dev} import com.daml.ledger.api.testtool.{TestDar, TestDars} import com.daml.tls.TlsClientConfig +import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.daml.lf.language.LanguageVersion trait AvailableTests { @@ -25,6 +26,12 @@ object AvailableTests { val v2_3 = new V2_3(TestDars.v2_3) val v2_dev = new V2_dev(TestDars.v2_dev) + val latestStableLf = v2_3 + + def testsForProtocol(protocolVersion: ProtocolVersion): AvailableTests = + if (protocolVersion <= ProtocolVersion.v34) v2_2 + else latestStableLf + private def map = Map( LanguageVersion.v2_2 -> v2_2, LanguageVersion.v2_3 -> v2_3, diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_2.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_2.scala index e5f70e7563..bc1cc89d43 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_2.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_2.scala @@ -16,49 +16,50 @@ import com.daml.tls.TlsClientConfig class V2_2(override val testDars: TestDars) extends AvailableTests { override def defaultTests(timeoutScaleFactor: Double): Vector[LedgerTestSuite] = Vector( - new ActiveContractsServiceIT, - new CheckpointInTailingStreamsIT, + new ActiveContractsServiceIT(testDars), + new CheckpointInTailingStreamsIT(testDars), new CommandDeduplicationIT(timeoutScaleFactor), new CommandDeduplicationParallelIT, - new CommandDeduplicationPeriodValidationIT, - new CommandServiceIT, - new CommandSubmissionCompletionIT, + new CommandDeduplicationPeriodValidationIT(testDars), + new CommandServiceIT(testDars), + new GetCompletionsIT, + new CommandSubmissionCompletionIT(testDars), new CompletionDeduplicationInfoIT(CompletionDeduplicationInfoIT.CommandService), new CompletionDeduplicationInfoIT(CompletionDeduplicationInfoIT.CommandSubmissionService), new ContractIdIT, new DamlValuesIT, new DeeplyNestedValueIT, - new DivulgenceIT, - new EventQueryServiceIT, - new ExplicitDisclosureIT, + new DivulgenceIT(testDars), + new EventQueryServiceIT(testDars), + new ExplicitDisclosureIT(testDars), new HealthServiceIT, new IdentityProviderConfigServiceIT, - new InteractiveSubmissionServiceIT, - new InterfaceIT, + new InteractiveSubmissionServiceIT(testDars), + new InterfaceIT(testDars), new InterfaceSubscriptionsIT(testDars), new InterfaceSubscriptionsWithEventBlobsIT(testDars), new LimitsIT, new MultiPartySubmissionIT, new PackageManagementServiceIT(testDars), new PackageServiceIT, - new ParticipantPruningIT, - new PartyManagementServiceIT, + new ParticipantPruningIT(testDars), + new PartyManagementServiceIT(testDars), new ExternalPartyManagementServiceIT, new PartyManagementServiceObjectMetaIT, new PartyManagementServiceUpdateRpcIT, - new SemanticTests, + new SemanticTests(testDars), new StateServiceIT, new TimeServiceIT, - new TransactionServiceArgumentsIT, - new TransactionServiceAuthorizationIT, - new TransactionServiceCorrectnessIT, - new TransactionServiceExerciseIT, - new TransactionServiceFiltersIT, - new TransactionServiceOutputsIT, - new UpdateServiceQueryIT, - new TransactionServiceStakeholdersIT, - new TransactionServiceValidationIT, - new TransactionServiceVisibilityIT, + new TransactionServiceArgumentsIT(testDars), + new TransactionServiceAuthorizationIT(testDars), + new TransactionServiceCorrectnessIT(testDars), + new TransactionServiceExerciseIT(testDars), + new TransactionServiceFiltersIT(testDars), + new TransactionServiceOutputsIT(testDars), + new UpdateServiceQueryIT(testDars), + new TransactionServiceStakeholdersIT(testDars), + new TransactionServiceValidationIT(testDars), + new TransactionServiceVisibilityIT(testDars), new UpdateServiceStreamsIT(testDars), new UpdateServiceTopologyEventsIT, new UpgradingIT(testDars), @@ -67,9 +68,9 @@ class V2_2(override val testDars: TestDars) extends AvailableTests { new UserManagementServiceUpdateRpcIT, new ValueLimitsIT, new WitnessesIT, - new WronglyTypedContractIdIT, + new WronglyTypedContractIdIT(testDars), new VettingIT(testDars), - new ContractServiceIT, + new ContractServiceIT(testDars), ) override def optionalTests(tlsConfiguration: Option[TlsClientConfig]): Vector[LedgerTestSuite] = diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_3.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_3.scala index 2d162188a3..586eb79512 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_3.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_3.scala @@ -12,15 +12,15 @@ import com.daml.tls.TlsClientConfig class V2_3(override val testDars: TestDars) extends AvailableTests { override def defaultTests(timeoutScaleFactor: Double): Vector[LedgerTestSuite] = new V2_2(testDars).defaultTests(timeoutScaleFactor) ++ Vector( - new ContractKeysCommandDeduplicationIT, - new ContractKeysContractIdIT, + new ContractKeysCommandDeduplicationIT(testDars), + new ContractKeysContractIdIT(testDars), new ContractKeysDeeplyNestedValueIT, new ContractKeysDivulgenceIT, - new ContractKeysExplicitDisclosureIT, - new ContractKeysIT, + new ContractKeysExplicitDisclosureIT(testDars), + new ContractKeysIT(testDars), new ContractKeysMultiPartySubmissionIT, - new ContractKeysWronglyTypedContractIdIT, - new PrefetchContractKeysIT, + new ContractKeysWronglyTypedContractIdIT(testDars), + new PrefetchContractKeysIT(testDars), new RaceConditionIT, ) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_dev.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_dev.scala index 8148828562..92b61ec90b 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_dev.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/V2_dev.scala @@ -12,7 +12,7 @@ import com.daml.tls.TlsClientConfig class V2_dev(override val testDars: TestDars) extends AvailableTests { override def defaultTests(timeoutScaleFactor: Double): Vector[LedgerTestSuite] = new V2_3(testDars).defaultTests(timeoutScaleFactor) ++ Vector( - new EventsDescendantsIT, + new EventsDescendantsIT(testDars), new ExceptionRaceConditionIT, new ExceptionsIT, ) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ActiveContractsServiceIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ActiveContractsServiceIT.scala index 90735b0983..02fe0635fd 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ActiveContractsServiceIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ActiveContractsServiceIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.participant.ParticipantTestContext @@ -43,8 +44,8 @@ import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* import scala.util.Random -class ActiveContractsServiceIT extends LedgerTestSuite { - import CompanionImplicits.* +class ActiveContractsServiceIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "ACSemptyResponse", @@ -165,7 +166,7 @@ class ActiveContractsServiceIT extends LedgerTestSuite { assert( activeContracts.headOption.value.getTemplateId == Identifier.fromJavaProto( - Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.toProto + dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toProto ), s"Received contract is not of type Dummy, but ${activeContracts.headOption.value.templateId}.", ) @@ -357,22 +358,47 @@ class ActiveContractsServiceIT extends LedgerTestSuite { allContractsForAlice.sizeIs == 3, s"$alice expected 3 events, but received ${allContractsForAlice.size}.", ) - assertTemplates(Seq(alice), allContractsForAlice, Dummy.TEMPLATE_ID_WITH_PACKAGE_ID, 1) assertTemplates( Seq(alice), allContractsForAlice, - DummyWithParam.TEMPLATE_ID_WITH_PACKAGE_ID, + dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, + 1, + ) + assertTemplates( + Seq(alice), + allContractsForAlice, + dummyWithParamCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, + 1, + ) + assertTemplates( + Seq(alice), + allContractsForAlice, + dummyFactoryCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, 1, ) - assertTemplates(Seq(alice), allContractsForAlice, DummyFactory.TEMPLATE_ID_WITH_PACKAGE_ID, 1) assert( allContractsForBob.sizeIs == 3, s"$bob expected 3 events, but received ${allContractsForBob.size}.", ) - assertTemplates(Seq(bob), allContractsForBob, Dummy.TEMPLATE_ID_WITH_PACKAGE_ID, 1) - assertTemplates(Seq(bob), allContractsForBob, DummyWithParam.TEMPLATE_ID_WITH_PACKAGE_ID, 1) - assertTemplates(Seq(bob), allContractsForBob, DummyFactory.TEMPLATE_ID_WITH_PACKAGE_ID, 1) + assertTemplates( + Seq(bob), + allContractsForBob, + dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, + 1, + ) + assertTemplates( + Seq(bob), + allContractsForBob, + dummyWithParamCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, + 1, + ) + assertTemplates( + Seq(bob), + allContractsForBob, + dummyFactoryCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, + 1, + ) assert( allContractsForAliceAndBob.sizeIs == 6, @@ -381,19 +407,19 @@ class ActiveContractsServiceIT extends LedgerTestSuite { assertTemplates( Seq(alice, bob), allContractsForAliceAndBob, - Dummy.TEMPLATE_ID_WITH_PACKAGE_ID, + dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, 2, ) assertTemplates( Seq(alice, bob), allContractsForAliceAndBob, - DummyWithParam.TEMPLATE_ID_WITH_PACKAGE_ID, + dummyWithParamCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, 2, ) assertTemplates( Seq(alice, bob), allContractsForAliceAndBob, - DummyFactory.TEMPLATE_ID_WITH_PACKAGE_ID, + dummyFactoryCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, 2, ) @@ -408,7 +434,12 @@ class ActiveContractsServiceIT extends LedgerTestSuite { dummyContractsForAlice.sizeIs == 1, s"$alice expected 1 event, but received ${dummyContractsForAlice.size}.", ) - assertTemplates(Seq(alice), dummyContractsForAlice, Dummy.TEMPLATE_ID_WITH_PACKAGE_ID, 1) + assertTemplates( + Seq(alice), + dummyContractsForAlice, + dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, + 1, + ) assert( dummyContractsForAliceAndBob.sizeIs == 2, @@ -417,7 +448,7 @@ class ActiveContractsServiceIT extends LedgerTestSuite { assertTemplates( Seq(alice, bob), dummyContractsForAliceAndBob, - Dummy.TEMPLATE_ID_WITH_PACKAGE_ID, + dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, 2, ) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CheckpointInTailingStreamsIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CheckpointInTailingStreamsIT.scala index e1ee82d82c..a44a1e8293 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CheckpointInTailingStreamsIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CheckpointInTailingStreamsIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse.CompletionResponse @@ -18,8 +19,8 @@ import com.digitalasset.canton.time.NonNegativeFiniteDuration import scala.concurrent.Future -class CheckpointInTailingStreamsIT extends LedgerTestSuite { - import CompanionImplicits.* +class CheckpointInTailingStreamsIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* import com.daml.ledger.api.testtool.suites.v2_2.CheckpointInTailingStreamsIT.* test( diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandDeduplicationPeriodValidationIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandDeduplicationPeriodValidationIT.scala index 4213691b08..c3f60362dc 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandDeduplicationPeriodValidationIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandDeduplicationPeriodValidationIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.ProtobufConverters.* @@ -26,8 +27,8 @@ import java.util.regex.Pattern import scala.concurrent.ExecutionContext import scala.concurrent.duration.* -class CommandDeduplicationPeriodValidationIT extends LedgerTestSuite { - import CompanionImplicits.* +class CommandDeduplicationPeriodValidationIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* private implicit val loggingContext: LoggingContext = LoggingContext.ForTesting diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandServiceIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandServiceIT.scala index ec6a7341c8..8eef6c115d 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandServiceIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandServiceIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.TransactionHelpers.* @@ -51,9 +52,11 @@ import scala.collection.immutable.Seq import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* -import CompanionImplicits.* +final class CommandServiceIT(override protected val testDars: TestDars) + extends LedgerTestSuite + with CommandSubmissionTestUtils { + import testDars.companionImplicits.* -final class CommandServiceIT extends LedgerTestSuite with CommandSubmissionTestUtils { test( "CSsubmitAndWaitBasic", "CSsubmitAndWaitBasic returns a valid transaction identifier", @@ -165,8 +168,8 @@ final class CommandServiceIT extends LedgerTestSuite with CommandSubmissionTestU s"The returned transaction should contain an exercised event, but was ${event2.event}", ) assert( - event1.getCreated.getTemplateId == Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, - s"The template ID of the created event should be ${Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.toV1}, but was ${event1.getCreated.getTemplateId}", + event1.getCreated.getTemplateId == dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + s"The template ID of the created event should be ${dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1}, but was ${event1.getCreated.getTemplateId}", ) assert( transaction.paidTrafficCost.exists(_ > 0L), @@ -687,7 +690,7 @@ final class CommandServiceIT extends LedgerTestSuite with CommandSubmissionTestU assertEquals( "Unexpected template identifier in create event", transactionsLedgerEffects.flatMap(createdEvents).map(_.getTemplateId), - Vector(Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), + Vector(dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), ) val contractId = transactionsLedgerEffects.flatMap(createdEvents).headOption.value.contractId diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandSubmissionCompletionIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandSubmissionCompletionIT.scala index 05794f68f9..e9190b50bc 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandSubmissionCompletionIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandSubmissionCompletionIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.{ @@ -21,8 +22,8 @@ import java.util.regex.Pattern import scala.concurrent.Future import scala.concurrent.duration.DurationInt -final class CommandSubmissionCompletionIT extends LedgerTestSuite { - import CompanionImplicits.* +final class CommandSubmissionCompletionIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "CSCCompletions", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandSubmissionTestUtils.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandSubmissionTestUtils.scala index e23c843afc..ba92a5e8fc 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandSubmissionTestUtils.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CommandSubmissionTestUtils.scala @@ -3,14 +3,17 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite import com.daml.ledger.api.v2.transaction.Transaction -import com.daml.ledger.test.java.model.test.Dummy trait CommandSubmissionTestUtils { this: LedgerTestSuite => + protected val testDars: TestDars + protected def assertOnTransactionResponse( transaction: Transaction ): Unit = { + val dummyCompanion = testDars.companionImplicits.dummyCompanion assert( transaction.updateId.nonEmpty, "The transaction identifier was empty but shouldn't.", @@ -21,8 +24,8 @@ trait CommandSubmissionTestUtils { this: LedgerTestSuite => s"The returned transaction should contain a created-event, but was ${event.event}", ) assert( - event.getCreated.getTemplateId == Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, - s"The template ID of the created-event should by ${Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.toV1}, but was ${event.getCreated.getTemplateId}", + event.getCreated.getTemplateId == dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + s"The template ID of the created-event should be ${dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1}, but was ${event.getCreated.getTemplateId}", ) } } diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CompanionImplicits.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CompanionImplicits.scala deleted file mode 100644 index 31de65a436..0000000000 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CompanionImplicits.scala +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.ledger.api.testtool.suites.v2_2 - -import com.daml.ledger.javaapi.data.codegen.ContractCompanion -import com.daml.ledger.test.java.model.iou.Iou -import com.daml.ledger.test.java.model.test.{ - Agreement, - AgreementFactory, - CallablePayout, - Delegated, - Delegation, - DiscloseCreate, - Divulgence1, - Divulgence2, - Dummy, - DummyFactory, - DummyWithParam, - TriProposal, - WithObservers, - Witnesses as TestWitnesses, -} -import com.daml.ledger.test.java.model.trailingnones.TrailingNones -import com.daml.ledger.test.java.semantic.divulgencetests.DummyFlexibleController -import com.daml.ledger.test.java.semantic.semantictests - -object CompanionImplicits { - - implicit val dummyCompanion - : ContractCompanion.WithoutKey[Dummy.Contract, Dummy.ContractId, Dummy] = Dummy.COMPANION - implicit val dummyWithParamCompanion: ContractCompanion.WithoutKey[ - DummyWithParam.Contract, - DummyWithParam.ContractId, - DummyWithParam, - ] = DummyWithParam.COMPANION - implicit val dummyFactoryCompanion - : ContractCompanion.WithoutKey[DummyFactory.Contract, DummyFactory.ContractId, DummyFactory] = - DummyFactory.COMPANION - implicit val withObserversCompanion: ContractCompanion.WithoutKey[ - WithObservers.Contract, - WithObservers.ContractId, - WithObservers, - ] = WithObservers.COMPANION - implicit val callablePayoutCompanion: ContractCompanion.WithoutKey[ - CallablePayout.Contract, - CallablePayout.ContractId, - CallablePayout, - ] = CallablePayout.COMPANION - implicit val delegatedCompanion: ContractCompanion.WithoutKey[ - Delegated.Contract, - Delegated.ContractId, - Delegated, - ] = Delegated.COMPANION - implicit val delegationCompanion - : ContractCompanion.WithoutKey[Delegation.Contract, Delegation.ContractId, Delegation] = - Delegation.COMPANION - implicit val discloseCreatedCompanion: ContractCompanion.WithoutKey[ - DiscloseCreate.Contract, - DiscloseCreate.ContractId, - DiscloseCreate, - ] = DiscloseCreate.COMPANION - implicit val testWitnessesCompanion: ContractCompanion.WithoutKey[ - TestWitnesses.Contract, - TestWitnesses.ContractId, - TestWitnesses, - ] = TestWitnesses.COMPANION - implicit val divulgence1Companion - : ContractCompanion.WithoutKey[Divulgence1.Contract, Divulgence1.ContractId, Divulgence1] = - Divulgence1.COMPANION - implicit val divulgence2Companion - : ContractCompanion.WithoutKey[Divulgence2.Contract, Divulgence2.ContractId, Divulgence2] = - Divulgence2.COMPANION - implicit val semanticTestsIouCompanion: ContractCompanion.WithoutKey[ - semantictests.Iou.Contract, - semantictests.Iou.ContractId, - semantictests.Iou, - ] = semantictests.Iou.COMPANION - implicit val iouCompanion: ContractCompanion.WithoutKey[Iou.Contract, Iou.ContractId, Iou] = - Iou.COMPANION - implicit val agreementFactoryCompanion: ContractCompanion.WithoutKey[ - AgreementFactory.Contract, - AgreementFactory.ContractId, - AgreementFactory, - ] = AgreementFactory.COMPANION - implicit val agreementCompanion - : ContractCompanion.WithoutKey[Agreement.Contract, Agreement.ContractId, Agreement] = - Agreement.COMPANION - - implicit val divulgeIouByExerciseCompanion: ContractCompanion.WithoutKey[ - DummyFlexibleController.Contract, - DummyFlexibleController.ContractId, - DummyFlexibleController, - ] = DummyFlexibleController.COMPANION - - implicit val triProposalCompanion - : ContractCompanion.WithoutKey[TriProposal.Contract, TriProposal.ContractId, TriProposal] = - TriProposal.COMPANION - - implicit val trailingNonesCompanion: ContractCompanion.WithoutKey[ - TrailingNones.Contract, - TrailingNones.ContractId, - TrailingNones, - ] = - TrailingNones.COMPANION -} diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ContractServiceIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ContractServiceIT.scala index 3372590aad..730ee57bcc 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ContractServiceIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ContractServiceIT.scala @@ -3,14 +3,15 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite import com.daml.ledger.test.java.model.test.Dummy import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -class ContractServiceIT extends LedgerTestSuite { - import CompanionImplicits.* +class ContractServiceIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "CSNotFound", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/DivulgenceIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/DivulgenceIT.scala index f05e0b856a..1dfa2d7fd8 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/DivulgenceIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/DivulgenceIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite import com.daml.ledger.api.testtool.infrastructure.TransactionOps.* @@ -10,8 +11,8 @@ import com.daml.ledger.test.java.model.test.{Divulgence1, Divulgence2} import com.digitalasset.canton.ledger.api.TransactionShape.{AcsDelta, LedgerEffects} import com.digitalasset.canton.platform.store.utils.EventOps.EventOps -final class DivulgenceIT extends LedgerTestSuite { - import CompanionImplicits.* +final class DivulgenceIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "DivulgenceTx", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/EventQueryServiceIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/EventQueryServiceIT.scala index 913066103d..3c7b12dd82 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/EventQueryServiceIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/EventQueryServiceIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -13,8 +14,8 @@ import com.digitalasset.canton.ledger.api.TransactionShape.LedgerEffects import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors import com.digitalasset.daml.lf.value.Value.ContractId -class EventQueryServiceIT extends LedgerTestSuite { - import com.daml.ledger.api.testtool.suites.v2_2.CompanionImplicits.* +class EventQueryServiceIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* // TODO(i16065): Re-enable getEventsByContractKey tests // private def toOption(protoString: String): Option[String] = { diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ExplicitDisclosureIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ExplicitDisclosureIT.scala index 50506def61..bd637ecfa0 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ExplicitDisclosureIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ExplicitDisclosureIT.scala @@ -4,12 +4,12 @@ package com.daml.ledger.api.testtool.suites.v2_2 import cats.implicits.{catsStdInstancesForFuture, toFunctorOps} +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.TransactionHelpers.createdEvents import com.daml.ledger.api.testtool.infrastructure.participant.ParticipantTestContext import com.daml.ledger.api.testtool.infrastructure.{LedgerTestSuite, Party} -import com.daml.ledger.api.testtool.suites.v2_2.CompanionImplicits.* import com.daml.ledger.api.v2.commands.DisclosedContract import com.daml.ledger.api.v2.event.CreatedEvent import com.daml.ledger.api.v2.transaction_filter.CumulativeFilter.IdentifierFilter @@ -39,8 +39,9 @@ import scala.concurrent.{ExecutionContext, Future} import scala.util.chaining.scalaUtilChainingOps import scala.util.{Success, Try} -final class ExplicitDisclosureIT extends LedgerTestSuite { +final class ExplicitDisclosureIT(testDars: TestDars) extends LedgerTestSuite { import ExplicitDisclosureIT.* + import testDars.companionImplicits.* test( "EDCorrectCreatedEventBlobDisclosure", @@ -547,6 +548,47 @@ final class ExplicitDisclosureIT extends LedgerTestSuite { case (true, true) => fail("Exactly one request should have failed, but both failed") case (false, false) => fail("Exactly one request should have failed, but both succeeded") } + + private def initializeTest( + ownerParticipant: ParticipantTestContext, + delegateParticipant: ParticipantTestContext, + owner: Party, + delegate: Party, + transactionFormat: TransactionFormat, + )(implicit ec: ExecutionContext): Future[TestContext] = { + val contractKey = ownerParticipant.nextKeyId() + + for { + // Create a Delegation contract + // Contract is visible both to owner (as signatory) and delegate (as observer) + delegationCid <- ownerParticipant.create( + owner, + new Delegation(owner.getValue, delegate.getValue), + ) + + // Create Delegated contract + // This contract is only visible to the owner + delegatedCid <- ownerParticipant.create(owner, new Delegated(owner.getValue, contractKey)) + + // Get the contract payload from the transaction stream of the owner + txReq <- ownerParticipant.getTransactionsRequest(transactionFormat) + delegatedTx <- ownerParticipant.transactions(txReq) + createDelegatedEvent = createdEvents(delegatedTx.headOption.value).headOption.value + + // Copy the actual Delegated contract to a disclosed contract (which can be shared out of band). + disclosedContract = createEventToDisclosedContract(createDelegatedEvent) + } yield TestContext( + ownerParticipant = ownerParticipant, + delegateParticipant = delegateParticipant, + owner = owner, + delegate = delegate, + contractKey = contractKey, + delegationCid = delegationCid, + delegatedCid = delegatedCid, + originalCreateEvent = createDelegatedEvent, + disclosedContract = disclosedContract, + ) + } } object ExplicitDisclosureIT { @@ -597,47 +639,6 @@ object ExplicitDisclosureIT { } - private def initializeTest( - ownerParticipant: ParticipantTestContext, - delegateParticipant: ParticipantTestContext, - owner: Party, - delegate: Party, - transactionFormat: TransactionFormat, - )(implicit ec: ExecutionContext): Future[TestContext] = { - val contractKey = ownerParticipant.nextKeyId() - - for { - // Create a Delegation contract - // Contract is visible both to owner (as signatory) and delegate (as observer) - delegationCid <- ownerParticipant.create( - owner, - new Delegation(owner.getValue, delegate.getValue), - ) - - // Create Delegated contract - // This contract is only visible to the owner - delegatedCid <- ownerParticipant.create(owner, new Delegated(owner.getValue, contractKey)) - - // Get the contract payload from the transaction stream of the owner - txReq <- ownerParticipant.getTransactionsRequest(transactionFormat) - delegatedTx <- ownerParticipant.transactions(txReq) - createDelegatedEvent = createdEvents(delegatedTx.headOption.value).headOption.value - - // Copy the actual Delegated contract to a disclosed contract (which can be shared out of band). - disclosedContract = createEventToDisclosedContract(createDelegatedEvent) - } yield TestContext( - ownerParticipant = ownerParticipant, - delegateParticipant = delegateParticipant, - owner = owner, - delegate = delegate, - contractKey = contractKey, - delegationCid = delegationCid, - delegatedCid = delegatedCid, - originalCreateEvent = createDelegatedEvent, - disclosedContract = disclosedContract, - ) - } - private def formatByPartyAndTemplate( owner: Party, templateId: javaapi.data.Identifier = Delegated.TEMPLATE_ID, diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/GetCompletionsIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/GetCompletionsIT.scala new file mode 100644 index 0000000000..bb837b828c --- /dev/null +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/GetCompletionsIT.scala @@ -0,0 +1,245 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.ledger.api.testtool.suites.v2_2 + +import com.daml.ledger.api.testtool.infrastructure.Allocation.* +import com.daml.ledger.api.testtool.infrastructure.Assertions.* +import com.daml.ledger.api.testtool.infrastructure.participant.ParticipantTestContext +import com.daml.ledger.api.testtool.infrastructure.{LedgerTestSuite, Party, WithTimeout} +import com.daml.ledger.api.v2.completion.Completion +import com.daml.ledger.test.java.model.test.Dummy +import com.digitalasset.canton.time.NonNegativeFiniteDuration + +import scala.concurrent.Future +import scala.concurrent.duration.DurationInt + +final class GetCompletionsIT extends LedgerTestSuite { + private val within: NonNegativeFiniteDuration = NonNegativeFiniteDuration.tryOfSeconds(2L) + + test( + "GCSingleParty", + "Read completions through GetCompletions for a single party", + allocate(SingleParty), + )(implicit ec => { case Participants(Participant(ledger, Seq(alice))) => + val createRequest = ledger.submitRequest(alice, new Dummy(alice).create.commands) + for { + beginExclusive <- ledger.currentEnd() + _ <- ledger.submit(createRequest) + completionO <- WithTimeout(5.seconds)( + ledger.findCompletion(ledger.getCompletionsRequest(beginExclusive)(alice))( + _.commandId == createRequest.getCommands.commandId + ) + ) + } yield { + val completion = assertDefined(completionO, "Expected a completion for alice") + assertEquals( + "Wrong command identifier on completion", + completion.commandId, + createRequest.getCommands.commandId, + ) + assertEquals( + "Single-party GetCompletions should preserve the submitting party", + completion.actAs.toSet, + Set(alice.underlying.getValue), + ) + } + }) + + test( + "GCMultiParty", + "Read completions through GetCompletions for multiple parties", + allocate(TwoParties), + )(implicit ec => { case Participants(Participant(ledger, Seq(alice, bob))) => + val aliceRequest = ledger.submitRequest(alice, new Dummy(alice).create.commands) + val bobRequest = ledger.submitRequest(bob, new Dummy(bob).create.commands) + val aliceBobRequest = multiPartyRequest(ledger, alice, bob) + val request = ledger.getCompletionsRequest(ledger.referenceOffset)(alice, bob) + + for { + _ <- ledger.submit(aliceRequest) + _ <- ledger.submit(bobRequest) + _ <- ledger.submit(aliceBobRequest) + aliceCompletionO <- WithTimeout(5.seconds)( + ledger.findCompletion(request)(_.commandId == aliceRequest.getCommands.commandId) + ) + bobCompletionO <- WithTimeout(5.seconds)( + ledger.findCompletion(request)(_.commandId == bobRequest.getCommands.commandId) + ) + aliceBobCompletionO <- WithTimeout(5.seconds)( + ledger.findCompletion(request)(_.commandId == aliceBobRequest.getCommands.commandId) + ) + } yield { + val aliceCompletion = assertDefined(aliceCompletionO, "Expected a completion for alice") + val bobCompletion = assertDefined(bobCompletionO, "Expected a completion for bob") + val aliceBobCompletion = + assertDefined(aliceBobCompletionO, "Expected a completion for alice and bob") + assertEquals( + "Single-party completion for alice should keep alice as act_as", + aliceCompletion.actAs.toSet, + Set(alice.underlying.getValue), + ) + assertEquals( + "Single-party completion for bob should keep bob as act_as", + bobCompletion.actAs.toSet, + Set(bob.underlying.getValue), + ) + assertEquals( + "Multi-party completion should contain both act_as parties", + aliceBobCompletion.actAs.toSet, + Set(alice.underlying.getValue, bob.underlying.getValue), + ) + } + }) + + test( + "GCWildcardParties", + "Read completions through GetCompletions with wildcard parties", + allocate(TwoParties), + runConcurrently = false, + )(implicit ec => { case Participants(Participant(ledger, Seq(alice, bob))) => + val aliceRequest = ledger.submitRequest(alice, new Dummy(alice).create.commands) + val bobRequest = ledger.submitRequest(bob, new Dummy(bob).create.commands) + val aliceBobRequest = multiPartyRequest(ledger, alice, bob) + + for { + beginExclusive <- ledger.currentEnd() + _ <- ledger.submit(aliceRequest) + _ <- ledger.submit(bobRequest) + _ <- ledger.submit(aliceBobRequest) + explicitResponses <- ledger.completions( + within, + ledger.getCompletionsRequest(beginExclusive)(alice, bob), + ) + wildcardResponses <- ledger.completions( + within, + ledger.getCompletionsRequest(beginExclusive)(), + ) + } yield { + val expectedCommandIds = Set( + aliceRequest.getCommands.commandId, + bobRequest.getCommands.commandId, + aliceBobRequest.getCommands.commandId, + ) + val explicitByCommandId = + completionsByCommandId(explicitResponses.flatMap(_.completion), expectedCommandIds) + val wildcardByCommandId = + completionsByCommandId(wildcardResponses.flatMap(_.completion), expectedCommandIds) + + assertEquals( + "Explicit multi-party and wildcard GetCompletions should return the same command identifiers", + wildcardByCommandId.keySet, + explicitByCommandId.keySet, + ) + assertEquals( + "Explicit multi-party and wildcard GetCompletions should expose the same act_as parties", + wildcardByCommandId, + explicitByCommandId, + ) + } + }) + + test( + "GCBeginExclusive", + "Honor begin_exclusive in GetCompletions", + allocate(SingleParty), + )(implicit ec => { case Participants(Participant(ledger, Seq(alice))) => + val firstRequest = ledger.submitRequest(alice, new Dummy(alice).create.commands) + val secondRequest = ledger.submitRequest(alice, new Dummy(alice).create.commands) + + for { + // submit is asynchronous, so anchor begin_exclusive to the first completion's actual offset + // rather than to currentEnd() (which may still precede that completion). + startOffset <- ledger.currentEnd() + _ <- ledger.submit(firstRequest) + firstCompletionO <- WithTimeout(5.seconds)( + ledger.findCompletion(ledger.getCompletionsRequest(startOffset)(alice))( + _.commandId == firstRequest.getCommands.commandId + ) + ) + firstCompletion = + assertDefined(firstCompletionO, "Expected a completion for the first submission") + _ <- ledger.submit(secondRequest) + secondCompletionO <- WithTimeout(5.seconds)( + ledger.findCompletion(ledger.getCompletionsRequest(firstCompletion.offset)(alice))( + _.commandId == secondRequest.getCommands.commandId + ) + ) + _ = assertDefined(secondCompletionO, "Expected a completion for the second submission") + responses <- ledger.completions( + within, + ledger.getCompletionsRequest(firstCompletion.offset)(alice), + ) + } yield { + val commandIds = responses.flatMap(_.completion).map(_.commandId).toSet + assert( + !commandIds.contains(firstRequest.getCommands.commandId), + "GetCompletions should not return completions at or before begin_exclusive", + ) + assert( + commandIds.contains(secondRequest.getCommands.commandId), + "GetCompletions should return completions after begin_exclusive", + ) + } + }) + + test( + "GCTailing", + "Completions should be served if added during the subscription", + allocate(SingleParty), + )(implicit ec => { case Participants(Participant(ledger, Seq(alice))) => + val completionsToSubmitBefore = 14 + val completionsToRead = completionsToSubmitBefore + 1 + for { + // Settle a batch of completions before opening the stream, then demand one more completion + // than currently exists so the read must block at ledger end and serve the final completion + // live, after the subscription has started. + submittedBefore <- Future.sequence( + Vector.fill(completionsToSubmitBefore)( + ledger.submitAndWait( + ledger.submitAndWaitRequest(alice, new Dummy(alice).create.commands) + ) + ) + ) + completionsF = ledger.completions( + completionsToRead, + ledger.getCompletionsRequest(ledger.referenceOffset)(alice), + ) + _ <- ledger.submitAndWait( + ledger.submitAndWaitRequest(alice, new Dummy(alice).create.commands) + ) + completions <- completionsF + } yield { + assert( + submittedBefore.sizeIs == completionsToSubmitBefore, + s"$completionsToSubmitBefore completions should have been submitted before the subscription but ${submittedBefore.size} were instead", + ) + assert( + completions.sizeIs == completionsToRead, + s"$completionsToRead completions should have been received but ${completions.size} were instead", + ) + } + }) + + private def completionsByCommandId( + completions: Seq[Completion], + expectedCommandIds: Set[String], + ): Map[String, Set[String]] = + completions.iterator + .filter(completion => expectedCommandIds.contains(completion.commandId)) + .map(completion => completion.commandId -> completion.actAs.toSet) + .toMap + + private def multiPartyRequest( + ledger: ParticipantTestContext, + alice: Party, + bob: Party, + ) = { + val request = ledger.submitRequest(alice, new Dummy(alice).create.commands) + request.withCommands( + request.getCommands.copy( + actAs = Seq(alice, bob) + ) + ) + } +} diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InteractiveSubmissionServiceIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InteractiveSubmissionServiceIT.scala index 6f6c14c7db..d77b4809f9 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InteractiveSubmissionServiceIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InteractiveSubmissionServiceIT.scala @@ -3,7 +3,6 @@ package com.daml.ledger.api.testtool.suites.v2_2 -import com.daml.ledger.api.testtool.infrastructure import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.TransactionHelpers.createdEvents @@ -18,7 +17,7 @@ import com.daml.ledger.api.testtool.suites.v2_2.CommandServiceIT.{ createEventToDisclosedContract, formatByPartyAndTemplate, } -import com.daml.ledger.api.testtool.suites.v2_2.CompanionImplicits.* +import com.daml.ledger.api.testtool.{TestDars, infrastructure} import com.daml.ledger.api.v2.commands.DisclosedContract import com.daml.ledger.api.v2.interactive.interactive_submission_service.Metadata.InputContract import com.daml.ledger.api.v2.interactive.interactive_submission_service.Metadata.InputContract.Contract @@ -51,7 +50,11 @@ import com.google.protobuf.timestamp.Timestamp import java.time.Instant import scala.concurrent.{ExecutionContext, Future} -final class InteractiveSubmissionServiceIT extends LedgerTestSuite with CommandSubmissionTestUtils { +final class InteractiveSubmissionServiceIT(override protected val testDars: TestDars) + extends LedgerTestSuite + with CommandSubmissionTestUtils { + import testDars.companionImplicits.* + test( "ISSPrepareSubmissionRequestBasic", "Prepare a submission request", @@ -540,9 +543,15 @@ final class InteractiveSubmissionServiceIT extends LedgerTestSuite with CommandS assertSingleton("expected single event", exerciseTransaction.events).getCreated.templateId, "expected template id", ) - assert(templateId.packageId == Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.getPackageId) - assert(templateId.moduleName == Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.getModuleName) - assert(templateId.entityName == Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.getEntityName) + assert( + templateId.packageId == dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.getPackageId + ) + assert( + templateId.moduleName == dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.getModuleName + ) + assert( + templateId.entityName == dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.getEntityName + ) } }) @@ -703,12 +712,12 @@ final class InteractiveSubmissionServiceIT extends LedgerTestSuite with CommandS result.packageReferences.sortBy(_.packageId), Seq( PackageReference( - packageId = Dummy.PACKAGE_ID, + packageId = dummyCompanion.PACKAGE_ID, packageName = Dummy.PACKAGE_NAME, packageVersion = Dummy.PACKAGE_VERSION.toString, ), PackageReference( - packageId = DivulgenceProposal.PACKAGE_ID, + packageId = divulgenceProposalCompanion.PACKAGE_ID, packageName = DivulgenceProposal.PACKAGE_NAME, packageVersion = DivulgenceProposal.PACKAGE_VERSION.toString, ), @@ -821,7 +830,7 @@ final class InteractiveSubmissionServiceIT extends LedgerTestSuite with CommandS assertEquals( _, PackageReference( - packageId = Dummy.PACKAGE_ID, + packageId = dummyCompanion.PACKAGE_ID, packageName = Dummy.PACKAGE_NAME, packageVersion = Dummy.PACKAGE_VERSION.toString, ), diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InterfaceIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InterfaceIT.scala index a1913cd017..ea4c09ec97 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InterfaceIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InterfaceIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.{ Participant, Participants, @@ -22,9 +23,11 @@ import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors import java.util.List as JList import scala.jdk.CollectionConverters.* -class InterfaceIT extends LedgerTestSuite { - implicit val tCompanion: ContractCompanion.WithoutKey[T.Contract, T.ContractId, T] = - T.COMPANION +class InterfaceIT(testDars: TestDars) extends LedgerTestSuite { + private val semanticTestsPackageId = testDars.SemanticTestDar.packageId + private implicit val tCompanion: ContractCompanion.WithoutKey[T.Contract, T.ContractId, T] = + T.COMPANION.withPackageId(semanticTestsPackageId) + private val interface1Companion = interface1.I.INTERFACE.withPackageId(semanticTestsPackageId) // replace identifier with the wrong identifier for some of these tests private[this] def useWrongId[X]( @@ -67,13 +70,13 @@ class InterfaceIT extends LedgerTestSuite { )(implicit ec => { case Participants(Participant(ledger, Seq(party))) => for { t <- ledger.create(party, new T(party)) - tree <- ledger.exercise(party, t.toInterface(interface1.I.INTERFACE).exerciseMyArchive()) + tree <- ledger.exercise(party, new interface1.I.ContractId(t.contractId).exerciseMyArchive()) } yield { val events = exercisedEvents(tree) assertLength(s"1 successful exercise", 1, events).discard assertEquals( events.headOption.value.interfaceId, - Some(interface1.I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), + Some(interface1Companion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), ) assertEquals(events.headOption.value.getExerciseResult.getText, "Interface1.I") } @@ -91,7 +94,7 @@ class InterfaceIT extends LedgerTestSuite { .submitAndWaitForTransaction( ledger.submitAndWaitForTransactionRequest( party, - useWrongId(t.toInterface(interface1.I.INTERFACE).exerciseChoiceI1(), T.TEMPLATE_ID), + useWrongId(new interface1.I.ContractId(t.contractId).exerciseChoiceI1(), T.TEMPLATE_ID), ) ) .mustFail("unknown choice") @@ -118,7 +121,7 @@ class InterfaceIT extends LedgerTestSuite { ledger.submitAndWaitForTransactionRequest( party, useWrongId( - t.toInterface(interface1.I.INTERFACE).exerciseChoiceI1(), + new interface1.I.ContractId(t.contractId).exerciseChoiceI1(), interface2.I.TEMPLATE_ID, ), ) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InterfaceSubscriptionsIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InterfaceSubscriptionsIT.scala index 66b3da1ead..3e3b850324 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InterfaceSubscriptionsIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/InterfaceSubscriptionsIT.scala @@ -16,7 +16,7 @@ import com.daml.ledger.api.v2.transaction.Transaction import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA import com.daml.ledger.api.v2.transaction_filter.{EventFormat, TransactionFormat} import com.daml.ledger.api.v2.value.{Identifier, Record} -import com.daml.ledger.javaapi +import com.daml.ledger.javaapi.data.Identifier as JavaIdentifier import com.daml.ledger.javaapi.data.codegen.ContractCompanion import com.daml.ledger.test.java.semantic.interfaceviews.{ I, @@ -34,6 +34,7 @@ import com.daml.logging.LoggingContext import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.ledger.api.TransactionShape.LedgerEffects import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors +import com.digitalasset.daml.lf.data.Ref.PackageId import java.util.regex.Pattern import scala.concurrent.duration.* @@ -59,6 +60,10 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) _ <- ledger.exercise(party, c4.exerciseArchive()) } yield () + private val semanticTestsPackageId = testDars.SemanticTestDar.packageId + private val carbonv1TestsPackageId = testDars.Carbonv1TestDar.packageId + private val carbonv2TestsPackageId = testDars.Carbonv2TestDar.packageId + implicit val t1Companion: ContractCompanion.WithoutKey[T1.Contract, T1.ContractId, T1] = T1.COMPANION implicit val t2Companion: ContractCompanion.WithoutKey[T2.Contract, T2.ContractId, T2] = @@ -249,11 +254,11 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertLength("Create event 1 has a view", 1, createdEvent1.interfaceViews).discard assertEquals( "Create event 1 template ID", - createdEvent1.templateId.value.toString, - T1.TEMPLATE_ID_WITH_PACKAGE_ID.toV1.toString, + createdEvent1.templateId.value, + T1.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1, ) assertEquals("Create event 1 contract ID", createdEvent1.contractId, c1) - assertViewEquals(createdEvent1.interfaceViews, I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent1.interfaceViews, I.TEMPLATE_ID) { value => assertLength("View1 has 2 fields", 2, value.fields).discard assertEquals("View1.a", value.fields(0).getValue.getInt64, 1) assertEquals("View1.b", value.fields(1).getValue.getBool, true) @@ -271,7 +276,7 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertEquals( "Archive event 1 has correct implemented_interfaces", exercisedImplementedInterfaces(archivedEventIndexF(0)), - Seq(I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), + Seq(I.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1), ) ) @@ -281,10 +286,10 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertEquals( "Create event 2 template ID", createdEvent2.templateId.value.toString, - T2.TEMPLATE_ID_WITH_PACKAGE_ID.toV1.toString, + T2.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1.toString, ) assertEquals("Create event 2 contract ID", createdEvent2.contractId, c2) - assertViewEquals(createdEvent2.interfaceViews, I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent2.interfaceViews, I.TEMPLATE_ID) { value => assertLength("View2 has 2 fields", 2, value.fields).discard assertEquals("View2.a", value.fields(0).getValue.getInt64, 2) assertEquals("View2.b", value.fields(1).getValue.getBool, false) @@ -294,7 +299,7 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertEquals( "Archive event 2 has correct implemented_interfaces", exercisedImplementedInterfaces(archivedEventIndexF(1)), - Seq(I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), + Seq(I.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1), ) ) @@ -304,16 +309,19 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertEquals( "Create event 3 template ID", createdEvent3.templateId.value.toString, - T3.TEMPLATE_ID_WITH_PACKAGE_ID.toV1.toString, + T3.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1.toString, ) assertEquals("Create event 3 contract ID", createdEvent3.contractId, c3) - assertViewFailed(createdEvent3.interfaceViews, I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) + assertViewFailed( + createdEvent3.interfaceViews, + I.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1, + ) checkArgumentsNonEmpty(createdEvent3, id = 3) exercisedImplementedInterfacesO.foreach(exercisedImplementedInterfaces => assertEquals( "Archive event 3 has correct implemented_interfaces", exercisedImplementedInterfaces(archivedEventIndexF(2)), - Seq(I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), + Seq(I.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1), ) ) @@ -360,12 +368,12 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertLength("single transaction found", 1, mergedTransactions).discard val createdEvent1 = createdEvents(mergedTransactions(0)).headOption.value assertEquals("Create event 1 contract ID", createdEvent1.contractId, c.contractId) - assertViewEquals(createdEvent1.interfaceViews, I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent1.interfaceViews, I.TEMPLATE_ID) { value => assertLength("View1 has 2 fields", 2, value.fields).discard assertEquals("View1.a", value.fields(0).getValue.getInt64, 6) assertEquals("View1.b", value.fields(1).getValue.getBool, true) } - assertViewEquals(createdEvent1.interfaceViews, I2.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent1.interfaceViews, I2.TEMPLATE_ID) { value => assertLength("View2 has 1 field", 1, value.fields).discard assertEquals("View2.c", value.fields(0).getValue.getInt64, 7) } @@ -374,7 +382,7 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) val createdEvent2 = createdEvents(party1Transactions(0)).headOption.value assertEquals("Create event 1 contract ID", createdEvent2.contractId, c.contractId) assertLength("single view found", 1, createdEvent2.interfaceViews).discard - assertViewEquals(createdEvent2.interfaceViews, I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent2.interfaceViews, I.TEMPLATE_ID) { value => assertLength("View1 has 2 fields", 2, value.fields).discard assertEquals("View1.a", value.fields(0).getValue.getInt64, 6) assertEquals("View1.b", value.fields(1).getValue.getBool, true) @@ -404,12 +412,12 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) val createdEvent = createdEvents(transactions(0)).headOption.value val archivedEvent = archivedEvents(transactions(1)).headOption.value assertEquals("Create event with correct contract ID", createdEvent.contractId, c.contractId) - assertViewEquals(createdEvent.interfaceViews, I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent.interfaceViews, I.TEMPLATE_ID) { value => assertLength("View1 has 2 fields", 2, value.fields).discard assertEquals("View1.a", value.fields(0).getValue.getInt64, 31337) assertEquals("View1.b", value.fields(1).getValue.getBool, true) } - assertViewEquals(createdEvent.interfaceViews, I2.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent.interfaceViews, I2.TEMPLATE_ID) { value => assertLength("View2 has 1 field", 1, value.fields).discard assertEquals("View2.c", value.fields(0).getValue.getInt64, 1) } @@ -422,8 +430,8 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) "Archive event has both implemented_interfaces", archivedEvent.implementedInterfaces.toSet, Set( - I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, - I2.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + I.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1, + I2.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1, ), ) } @@ -480,7 +488,7 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertEquals("Create event 2 contract ID", createdEvent2.contractId, c2.contractId) // Expect view to be delivered even though there is an ambiguous // includeInterfaceView flag set to true and false at the same time (true wins) - assertViewEquals(createdEvent2.interfaceViews, I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent2.interfaceViews, I.TEMPLATE_ID) { value => assertLength("View2 has 2 fields", 2, value.fields).discard assertEquals("View2.a", value.fields(0).getValue.getInt64, 2) assertEquals("View2.b", value.fields(1).getValue.getBool, false) @@ -527,7 +535,7 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertEquals("Create event 2 contract ID", createdEvent2.contractId, c2.contractId) // Expect view to be delivered even though there is an ambiguous // includeInterfaceView flag set to true and false at the same time. - assertViewEquals(createdEvent2.interfaceViews, I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) { value => + assertViewEquals(createdEvent2.interfaceViews, I.TEMPLATE_ID) { value => assertLength("View2 has 2 fields", 2, value.fields).discard assertEquals("View2.a", value.fields(0).getValue.getInt64, 2) assertEquals("View2.b", value.fields(1).getValue.getBool, false) @@ -572,7 +580,7 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertEquals( "Create event 1 template ID", createdEvent1.templateId.value.toString, - T1.TEMPLATE_ID_WITH_PACKAGE_ID.toV1.toString, + T1.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1.toString, ) assertEquals("Create event 1 contract ID", createdEvent1.contractId, c1.contractId) } @@ -656,10 +664,8 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) )(implicit ec => { case Participants(Participant(ledger, Seq(party))) => val packageName = I.TEMPLATE_ID.getPackageId val moduleName = I.TEMPLATE_ID.getModuleName - val unknownTemplate = - new javaapi.data.Identifier(packageName, moduleName, "TemplateDoesNotExist") - val unknownInterface = - new javaapi.data.Identifier(packageName, moduleName, "InterfaceDoesNotExist") + val unknownTemplate = new JavaIdentifier(packageName, moduleName, "TemplateDoesNotExist") + val unknownInterface = new JavaIdentifier(packageName, moduleName, "InterfaceDoesNotExist") import ledger.* for { _ <- create(party, new T1(party, 1)) @@ -820,36 +826,28 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) transactions <- transactionFuture - } yield assertSingleContractWithSimpleView( - transactions = transactions, - contractIdentifier = carbonv2.carbonv2.T.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, - viewIdentifier = carbonv1.carbonv1.I.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, - contractId = contract.contractId, - viewValue = 21, - ) - }) - - private def assertSingleContractWithSimpleView( - transactions: Vector[Transaction], - contractIdentifier: Identifier, - viewIdentifier: Identifier, - contractId: String, - viewValue: Long, - ): Unit = { - assertLength("transaction should be found", 1, transactions).discard - val createdEvent = createdEvents(transactions(0)).headOption.value - assertLength("Create event has a view", 1, createdEvent.interfaceViews).discard - assertEquals( - "Create event template ID", - createdEvent.templateId.value.toString, - contractIdentifier.toString, - ) - assertEquals("Create event contract ID", createdEvent.contractId, contractId) - assertViewEquals(createdEvent.interfaceViews, viewIdentifier) { value => - assertLength("View has 1 field", 1, value.fields).discard - assertEquals("View.value", value.fields(0).getValue.getInt64, viewValue) + } yield { + assertLength("transaction should be found", 1, transactions).discard + val createdEvent = createdEvents(transactions(0)).headOption.value + assertLength("Create event has a view", 1, createdEvent.interfaceViews).discard + val contractIdentifier = carbonv2.carbonv2.T.TEMPLATE_ID + val viewIdentifier = carbonv1.carbonv1.I.TEMPLATE_ID + assertEquals( + "Create event template ID", + createdEvent.templateId.value, + contractIdentifier.withPackageId(carbonv2TestsPackageId).toV1, + ) + assertEquals("Create event contract ID", createdEvent.contractId, contract.contractId) + assertViewEquals( + createdEvent.interfaceViews, + viewIdentifier, + packageId = carbonv1TestsPackageId, + ) { value => + assertLength("View has 1 field", 1, value.fields).discard + assertEquals("View.value", value.fields(0).getValue.getInt64, 21) + } } - } + }) private def updateTransaction( emptyView: Boolean = false, @@ -924,21 +922,30 @@ abstract class InterfaceSubscriptionsITBase(testDars: TestDars, prefix: String) assertEquals("Status must be invalid argument", status.code, 9) } - private def assertViewEquals(views: Seq[InterfaceView], interfaceId: Identifier)( + private def assertViewEquals( + views: Seq[InterfaceView], + interfaceId: JavaIdentifier, + packageId: PackageId = semanticTestsPackageId, + )( checkValue: Record => Unit ): Unit = { - val viewSearch = views.find(_.interfaceId.contains(interfaceId)) + val interfaceIdWithPackageId = interfaceId.withPackageId(packageId).toV1 + val viewSearch = views.find(_.interfaceId.contains(interfaceIdWithPackageId)) val view = assertDefined( viewSearch, s"View could not be found, there are: ${views.map(_.interfaceId).mkString("[", ",", "]")}", ) - val viewCount = views.count(_.interfaceId.contains(interfaceId)) - assertEquals(s"Only one view of interfaceId=$interfaceId must be defined", viewCount, 1) + val viewCount = views.count(_.interfaceId.contains(interfaceIdWithPackageId)) + assertEquals( + s"Only one view of interfaceId=$interfaceIdWithPackageId must be defined", + viewCount, + 1, + ) val actualInterfaceId = assertDefined(view.interfaceId, "Interface ID is not defined") - assertEquals("View has correct interface ID", actualInterfaceId, interfaceId) + assertEquals("View has correct interface ID", actualInterfaceId, interfaceIdWithPackageId) val status = assertDefined(view.viewStatus, "Status is not defined") assertEquals("Status must be successful", status.code, 0) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ParticipantPruningIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ParticipantPruningIT.scala index e08d50f91c..5a6e8f28e6 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ParticipantPruningIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/ParticipantPruningIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.{Participant, *} import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.Eventually.eventually @@ -11,10 +12,8 @@ import com.daml.ledger.api.testtool.infrastructure.{FutureAssertions, LedgerTest import com.daml.ledger.api.v2.event_query_service.GetEventsByContractIdRequest import com.daml.ledger.api.v2.transaction.Transaction import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_LEDGER_EFFECTS -import com.daml.ledger.javaapi.data.codegen.ContractCompanion import com.daml.ledger.test.java.model -import com.daml.ledger.test.java.semantic.divulgencetests -import com.daml.ledger.test.java.semantic.divulgencetests.{Contract, Dummy} +import com.daml.ledger.test.java.semantic.divulgencetests.Dummy import com.daml.logging.LoggingContext import com.digitalasset.canton.ledger.api.TransactionShape.{AcsDelta, LedgerEffects} import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors @@ -23,16 +22,8 @@ import java.util.regex.Pattern import scala.concurrent.duration.DurationInt import scala.concurrent.{ExecutionContext, Future} -class ParticipantPruningIT extends LedgerTestSuite { - import CompanionImplicits.* - implicit val contractCompanion - : ContractCompanion.WithoutKey[Contract.Contract$, Contract.ContractId, Contract] = - Contract.COMPANION - implicit val semanticTestsDummyCompanion: ContractCompanion.WithoutKey[ - divulgencetests.Dummy.Contract, - divulgencetests.Dummy.ContractId, - divulgencetests.Dummy, - ] = divulgencetests.Dummy.COMPANION +class ParticipantPruningIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* private implicit val loggingContext: LoggingContext = LoggingContext.ForTesting diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/PartyManagementServiceIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/PartyManagementServiceIT.scala index c41091b4be..5afd2fb5de 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/PartyManagementServiceIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/PartyManagementServiceIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.{ @@ -33,8 +34,8 @@ import java.util.regex.Pattern import scala.concurrent.Future import scala.util.Random -final class PartyManagementServiceIT extends PartyManagementITBase { - import CompanionImplicits.* +final class PartyManagementServiceIT(testDars: TestDars) extends PartyManagementITBase { + import testDars.companionImplicits.* val namePicker: NamePicker = NamePicker( "-_ 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/SemanticTests.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/SemanticTests.scala index bb1cd64eae..693dc9e284 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/SemanticTests.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/SemanticTests.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.Eventually.eventually @@ -30,19 +31,21 @@ import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* import scala.util.Success -final class SemanticTests extends LedgerTestSuite { - import CompanionImplicits.* +final class SemanticTests(testDars: TestDars) extends LedgerTestSuite { + private val semanticTestsPackageId = testDars.SemanticTestDar.packageId + + import testDars.companionImplicits.* implicit val delegationCompanion - : ContractCompanion.WithoutKey[Delegation.Contract, Delegation.ContractId, Delegation] = - Delegation.COMPANION - implicit val sharedContractCompanion: ContractCompanion.WithoutKey[ + : ContractCompanion[Delegation.Contract, Delegation.ContractId, Delegation] = + Delegation.COMPANION.withPackageId(semanticTestsPackageId) + implicit val sharedContractCompanion: ContractCompanion[ SharedContract.Contract, SharedContract.ContractId, SharedContract, - ] = SharedContract.COMPANION + ] = SharedContract.COMPANION.withPackageId(semanticTestsPackageId) implicit val paintOfferCompanion - : ContractCompanion.WithoutKey[PaintOffer.Contract, PaintOffer.ContractId, PaintOffer] = - PaintOffer.COMPANION + : ContractCompanion[PaintOffer.Contract, PaintOffer.ContractId, PaintOffer] = + PaintOffer.COMPANION.withPackageId(semanticTestsPackageId) private[this] val onePound = new Amount(BigDecimal.valueOf(1), "GBP") private[this] val twoPounds = new Amount(BigDecimal.valueOf(2), "GBP") @@ -208,13 +211,15 @@ final class SemanticTests extends LedgerTestSuite { } yield { val agreement = assertSingleton( "SemanticPaintOffer", - createdEvents(tree).filter(_.getTemplateId == PaintAgree.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), + createdEvents(tree).filter( + _.getTemplateId == PaintAgree.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1 + ), ) assertEquals( "Paint agreement parameters", agreement.getCreateArguments, Record( - recordId = Some(PaintAgree.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), + recordId = Some(PaintAgree.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1), fields = Seq( RecordField("painter", Some(Value(Value.Sum.Party(painter)))), RecordField("houseOwner", Some(Value(Value.Sum.Party(houseOwner)))), @@ -288,14 +293,14 @@ final class SemanticTests extends LedgerTestSuite { val agreement = assertSingleton( "SemanticPaintCounterOffer", createdEvents(tx).filter( - _.getTemplateId == PaintAgree.TEMPLATE_ID_WITH_PACKAGE_ID.toV1 + _.getTemplateId == PaintAgree.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1 ), ) assertEquals( "Paint agreement parameters", agreement.getCreateArguments, Record( - recordId = Some(PaintAgree.TEMPLATE_ID_WITH_PACKAGE_ID.toV1), + recordId = Some(PaintAgree.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1), fields = Seq( RecordField("painter", Some(Value(Value.Sum.Party(painter)))), RecordField("houseOwner", Some(Value(Value.Sum.Party(houseOwner)))), @@ -386,7 +391,7 @@ final class SemanticTests extends LedgerTestSuite { tree <- alpha.exercise(houseOwner, offer.exercisePaintOffer_Accept(iou)) (newIouEvents, agreementEvents) = createdEvents(tree).partition( - _.getTemplateId == Iou.TEMPLATE_ID_WITH_PACKAGE_ID.toV1 + _.getTemplateId == Iou.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1 ) newIouEvent <- Future(newIouEvents.headOption.value) agreementEvent <- Future(agreementEvents.headOption.value) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceArgumentsIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceArgumentsIT.scala index cdad64ed77..3ae9a14a05 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceArgumentsIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceArgumentsIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -24,9 +25,9 @@ import java.util.List as JList import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* -class TransactionServiceArgumentsIT extends LedgerTestSuite { +class TransactionServiceArgumentsIT(testDars: TestDars) extends LedgerTestSuite { import ClearIdsImplicits.* - import CompanionImplicits.* + import testDars.companionImplicits.* test( "TXCreateWithAnyType", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceAuthorizationIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceAuthorizationIT.scala index 8f6b239c68..d691996430 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceAuthorizationIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceAuthorizationIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.Eventually.eventually @@ -21,8 +22,8 @@ import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors import scala.jdk.CollectionConverters.* -class TransactionServiceAuthorizationIT extends LedgerTestSuite { - import CompanionImplicits.* +class TransactionServiceAuthorizationIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "TXRequireAuthorization", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceCorrectnessIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceCorrectnessIT.scala index adedf15bfe..192505aec6 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceCorrectnessIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceCorrectnessIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -18,8 +19,8 @@ import java.time.Instant import scala.collection.immutable.Seq import scala.concurrent.Future -class TransactionServiceCorrectnessIT extends LedgerTestSuite { - import CompanionImplicits.* +class TransactionServiceCorrectnessIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "TXProcessInTwoChunks", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceExerciseIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceExerciseIT.scala index 0e6a4d7c38..fc4e8c4c78 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceExerciseIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceExerciseIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -21,8 +22,8 @@ import com.digitalasset.canton.ledger.api.util.TimestampConversion import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors import com.digitalasset.canton.platform.store.utils.EventOps.EventOps -class TransactionServiceExerciseIT extends LedgerTestSuite { - import CompanionImplicits.* +class TransactionServiceExerciseIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "TXUseCreateToExercise", @@ -64,13 +65,13 @@ class TransactionServiceExerciseIT extends LedgerTestSuite { assertEquals( "Create should be of DummyWithParam", create.getTemplateId, - DummyWithParam.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + dummyWithParamCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, ) val archive = assertSingleton("GetArchive", dummyFactory.flatMap(archivedEvents)) assertEquals( "Archive should be of DummyFactory", archive.getTemplateId, - DummyFactory.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + dummyFactoryCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, ) assertEquals( "Mismatching archived contract identifier", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceFiltersIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceFiltersIT.scala index 95fbc35d89..724a7c6aab 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceFiltersIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceFiltersIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.TransactionHelpers.* @@ -24,7 +25,9 @@ import com.digitalasset.canton.ledger.api.TransactionShape.{AcsDelta, LedgerEffe import scala.concurrent.{ExecutionContext, Future} // Allows using deprecated Protobuf fields for testing -class TransactionServiceFiltersIT extends LedgerTestSuite { +class TransactionServiceFiltersIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.dummyCompanion + private val semanticTestsPackageId = testDars.SemanticTestDar.packageId test( "TSFInterfaceTemplatePlainFilters", @@ -510,7 +513,7 @@ class TransactionServiceFiltersIT extends LedgerTestSuite { assertEquals( "Exercised event of Dummy template ID", event.templateId.value, - Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, ) } } @@ -531,7 +534,7 @@ class TransactionServiceFiltersIT extends LedgerTestSuite { assertEquals( "Create event 1 template ID", createdEvent1.templateId.value, - T5.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + T5.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1, ) assertEquals("Create event 1 contract ID", createdEvent1.contractId, c1).discard assertLength("Create event 1 has a view", 1, createdEvent1.interfaceViews).discard @@ -551,7 +554,7 @@ class TransactionServiceFiltersIT extends LedgerTestSuite { assertEquals( "Create event 2 template ID", createdEvent2.templateId.value, - T6.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + T6.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1, ) assertEquals("Create event 2 contract ID", createdEvent2.contractId, c2) assertLength("Create event 2 has a view", 1, createdEvent2.interfaceViews).discard @@ -572,7 +575,7 @@ class TransactionServiceFiltersIT extends LedgerTestSuite { assertEquals( "Create event 3 template ID", createdEvent3.templateId.value.toString, - T3.TEMPLATE_ID_WITH_PACKAGE_ID.toV1.toString, + T3.TEMPLATE_ID.withPackageId(semanticTestsPackageId).toV1.toString, ) assertEquals("Create event 3 contract ID", createdEvent3.contractId, c3) assertLength("Create event 3 has no view", 0, createdEvent3.interfaceViews).discard diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceOutputsIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceOutputsIT.scala index 17e0c3d8a2..16db979162 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceOutputsIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceOutputsIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.TransactionHelpers.* @@ -21,9 +22,9 @@ import java.util.Optional import scala.concurrent.{ExecutionContext, Future} import scala.jdk.OptionConverters.* -class TransactionServiceOutputsIT extends LedgerTestSuite { +class TransactionServiceOutputsIT(testDars: TestDars) extends LedgerTestSuite { import ClearIdsImplicits.* - import CompanionImplicits.* + import testDars.companionImplicits.* import com.daml.ledger.api.testtool.infrastructure.RemoveTrailingNone.Implicits test( diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceStakeholdersIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceStakeholdersIT.scala index a8c66091fb..9e57d3061a 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceStakeholdersIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceStakeholdersIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -13,8 +14,8 @@ import com.digitalasset.canton.ledger.api.TransactionShape.{AcsDelta, LedgerEffe import scala.collection.immutable.Seq import scala.jdk.CollectionConverters.* -class TransactionServiceStakeholdersIT extends LedgerTestSuite { - import CompanionImplicits.* +class TransactionServiceStakeholdersIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test("TXStakeholders", "Expose the correct stakeholders", allocate(SingleParty, SingleParty))( implicit ec => { diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceValidationIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceValidationIT.scala index fa11a9a29b..cffbbad5dc 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceValidationIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceValidationIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -12,8 +13,8 @@ import com.digitalasset.canton.ledger.api.TransactionShape.AcsDelta import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors import com.digitalasset.canton.protocol.TestUpdateId -class TransactionServiceValidationIT extends LedgerTestSuite { - import CompanionImplicits.* +class TransactionServiceValidationIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "TXRejectEmptyFilter", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceVisibilityIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceVisibilityIT.scala index e7bfc4d1b6..82548b6548 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceVisibilityIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/TransactionServiceVisibilityIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.TransactionHelpers.* @@ -36,10 +37,10 @@ import scala.collection.mutable import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* -class TransactionServiceVisibilityIT extends LedgerTestSuite { +class TransactionServiceVisibilityIT(testDars: TestDars) extends LedgerTestSuite { import com.digitalasset.canton.BigDecimalImplicits.* - import CompanionImplicits.* + import testDars.companionImplicits.* implicit val iouTransferCompanion : ContractCompanion.WithoutKey[IouTransfer.Contract, IouTransfer.ContractId, IouTransfer] = diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpdateServiceQueryIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpdateServiceQueryIT.scala index b563204507..ec9a656590 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpdateServiceQueryIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpdateServiceQueryIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -13,8 +14,8 @@ import com.digitalasset.canton.ledger.api.TransactionShape.{AcsDelta, LedgerEffe import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors import com.digitalasset.canton.protocol.TestUpdateId -class UpdateServiceQueryIT extends LedgerTestSuite { - import CompanionImplicits.* +class UpdateServiceQueryIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "TXTransactionByIdLedgerEffectsBasic", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpdateServiceStreamsIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpdateServiceStreamsIT.scala index 77b1f56483..ca57da097f 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpdateServiceStreamsIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpdateServiceStreamsIT.scala @@ -25,7 +25,7 @@ import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* class UpdateServiceStreamsIT(testDars: TestDars) extends LedgerTestSuite { - import CompanionImplicits.* + import testDars.companionImplicits.* private[this] val testPackageResourcePath = testDars.OngoingStreamPackageUploadTestDar.path @@ -388,7 +388,7 @@ class UpdateServiceStreamsIT(testDars: TestDars) extends LedgerTestSuite { assertEquals( "FilterByTemplate", contract.getTemplateId, - Dummy.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + dummyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, ) assertEquals( "FilterByTemplate transactions for party-wildcard should match the specific party", @@ -423,10 +423,11 @@ class UpdateServiceStreamsIT(testDars: TestDars) extends LedgerTestSuite { assertEquals( "FilterByInterface", created.getTemplateId, - Iou.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, + iouCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1, ) - val view = IIou.INTERFACE.fromCreatedEvent(fromProto(toJavaProto(created))) + val interfaceCompanion = IIou.INTERFACE.withPackageId(testDars.ModelTestDar.packageId) + val view = interfaceCompanion.fromCreatedEvent(fromProto(toJavaProto(created))) assertEquals(view.data.icurrency, "USD") } }) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpgradingIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpgradingIT.scala index c2c0ebdea2..24ca32fed0 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpgradingIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/UpgradingIT.scala @@ -71,6 +71,7 @@ import scala.jdk.CollectionConverters.* class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { import testDars.{ + ModelTestDar, UpgradeFetchTestDar1_0_0, UpgradeFetchTestDar2_0_0, UpgradeIfaceDar, @@ -83,6 +84,24 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { private val UA_Ref = ScalaPbIdentifier.fromJavaProto(UA_V1.TEMPLATE_ID.toProto) private val UB_Ref = ScalaPbIdentifier.fromJavaProto(UB_V2.TEMPLATE_ID.toProto) + private implicit val upgradingUA_V1Companion + : ContractCompanion[UA_V1.Contract, UA_V1.ContractId, UA_V1] = + UA_V1.COMPANION.withPackageId(UpgradeTestDar1_0_0.packageId) + private implicit val upgradingUA_V2Companion + : ContractCompanion[UA_V2.Contract, UA_V2.ContractId, UA_V2] = + UA_V2.COMPANION.withPackageId(UpgradeTestDar2_0_0.packageId) + private implicit val upgradingUA_V3Companion + : ContractCompanion[UA_V3.Contract, UA_V3.ContractId, UA_V3] = + UA_V3.COMPANION.withPackageId(UpgradeTestDar3_0_0.packageId) + private implicit val upgradingUB_V2Companion + : ContractCompanion[UB_V2.Contract, UB_V2.ContractId, UB_V2] = + UB_V2.COMPANION.withPackageId(UpgradeTestDar2_0_0.packageId) + private implicit val upgradingUB_V3Companion + : ContractCompanion[UB_V3.Contract, UB_V3.ContractId, UB_V3] = + UB_V3.COMPANION.withPackageId(UpgradeTestDar3_0_0.packageId) + private implicit val dummyCompanion: ContractCompanion[Dummy.Contract, Dummy.ContractId, Dummy] = + Dummy.COMPANION.withPackageId(ModelTestDar.packageId) + test( "USubscriptionsUnknownPackageNames", "Subscriptions are failed if created for package names that are not known to the participant", @@ -195,22 +214,6 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "Template-id and interface-id resolution is updated on package upload during ongoing subscriptions", allocate(SingleParty), )(implicit ec => { case Participants(Participant(ledger, Seq(party))) => - implicit val upgradingUA_V1Companion - : ContractCompanion.WithoutKey[UA_V1.Contract, UA_V1.ContractId, UA_V1] = - UA_V1.COMPANION - implicit val upgradingUA_V2Companion - : ContractCompanion.WithoutKey[UA_V2.Contract, UA_V2.ContractId, UA_V2] = - UA_V2.COMPANION - implicit val upgradingUA_V3Companion - : ContractCompanion.WithoutKey[UA_V3.Contract, UA_V3.ContractId, UA_V3] = - UA_V3.COMPANION - implicit val upgradingUB_V2Companion - : ContractCompanion.WithoutKey[UB_V2.Contract, UB_V2.ContractId, UB_V2] = - UB_V2.COMPANION - implicit val upgradingUB_V3Companion - : ContractCompanion.WithoutKey[UB_V3.Contract, UB_V3.ContractId, UB_V3] = - UB_V3.COMPANION - for { _ <- upload(ledger, UpgradeIfaceDar) // Upload 1.0.0 package (with the first implementation of UA) @@ -309,7 +312,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { ledger, party, payloadUA_3, - Some(PackageRef.Id(Ref.PackageId.assertFromString(UA_V1.PACKAGE_ID))), + Some(PackageRef.Id(Ref.PackageId.assertFromString(UpgradeTestDar1_0_0.packageId))), ) acs_before_v3_upload <- acsF(ledger, party, SubInterface(Iface1_Ref)) @@ -369,7 +372,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { create: CreatedEvent, expectedInterfaceViewValue: Option[String] = None, expectedImplementationPackageId: Option[String] = None, - )(implicit companion: ContractCompanion[?, TCid, T]): Unit = + )(companion: ContractCompanion[?, TCid, T]): Unit = assertPayloadEquals( context, create, @@ -379,7 +382,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { assertCreateArgs = expectedInterfaceViewValue.isEmpty, expectedInterfaceViewValue.toList .map(expectedViewValue => - Identifier.fromProto(iface.Iface1.TEMPLATE_ID_WITH_PACKAGE_ID.toProto) -> { + iface.Iface1.TEMPLATE_ID.withPackageId(UpgradeIfaceDar.packageId) -> { (record: DamlRecord) => assertEquals( s"Iface1 view for create 1 - $context", @@ -398,7 +401,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { context = "GetEventsByContractId at v2", create = create1_fetched_by_contract_id_after_v2_upload.getCreated.getCreatedEvent, expectedInterfaceViewValue = Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( @@ -406,7 +409,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { context = "GetEventsByContractId at v3", create = create1_fetched_by_contract_id_after_v3_upload.getCreated.getCreatedEvent, expectedInterfaceViewValue = Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) // Assert interface subscriptions @@ -416,21 +419,21 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "2 - IFace1 subscription at v1", create2, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_3, "3 - IFace1 subscription at v1", create3, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_4, "4 - IFace1 subscription at v1", create4, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) } @@ -441,21 +444,21 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "2 - IFace1 subscription after v1 create", create2, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_3, "3 - IFace1 subscription after v1 create", create3, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_4, "4 - IFace1 subscription after v1 create", create4, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) } @@ -466,28 +469,28 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "1 - Iface1 subscription at v2", create1, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_2, "2 - Iface1 subscription at v2", create2, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_3, "3 - Iface1 subscription at v2", create3, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_4, "4 - Iface1 subscription at v2", create4, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) } @@ -498,28 +501,28 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "1 - Iface1 subscription at v3", create1, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) assertCreate( payloadUA_2, "2 - Iface1 subscription at v3", create2, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) assertCreate( payloadUA_3, "3 - Iface1 subscription at v3", create3, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) assertCreate( payloadUA_4, "4 - Iface1 subscription at v2", create4, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) } @@ -531,7 +534,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "1 - ACS after v2 upload", create1, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) } inside(acs_before_v3_upload) { case Vector(create1, create2, create3) => @@ -540,21 +543,21 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "1 - ACS after create 3", create1, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_2, "2 - ACS after create 3", create2, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) assertCreate( payloadUA_3, "3 - ACS after create 3", create3, Some("Iface1-UAv2"), - Some(UA_V2.PACKAGE_ID), + Some(UpgradeTestDar2_0_0.packageId), ) } inside(acs_after_v3_upload) { case Vector(create1, create2, create3) => @@ -563,21 +566,21 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "1 - ACS after v3 upload", create1, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) assertCreate( payloadUA_2, "2 - ACS after v3 upload", create2, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) assertCreate( payloadUA_3, "3 - ACS after v3 upload", create3, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) } inside(acs_after_create_4) { case Vector(create1, create2, create3, create4) => @@ -586,28 +589,28 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "1 - ACS after create 4", create1, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) assertCreate( payloadUA_2, "2 - ACS after create 4", create2, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) assertCreate( payloadUA_3, "3 - ACS after create 4", create3, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) assertCreate( payloadUA_4, "4 - ACS after create 4", create4, Some("Iface1-UAv3"), - Some(UA_V3.PACKAGE_ID), + Some(UpgradeTestDar3_0_0.packageId), ) } @@ -636,9 +639,6 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { allocate(SingleParty), )(implicit ec => { case Participants(Participant(ledger, Seq(party))) => val dummy = new Dummy(party) - implicit val dummyCompanion - : ContractCompanion.WithoutKey[Dummy.Contract, Dummy.ContractId, Dummy] = - Dummy.COMPANION val dummyTemplateSubscriptions = new Subscriptions( "Dummy template", @@ -703,9 +703,6 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { allocate(SingleParty), )(implicit ec => { case Participants(Participant(ledger, Seq(party))) => val dummy = new Dummy(party) - implicit val dummyCompanion - : ContractCompanion.WithoutKey[Dummy.Contract, Dummy.ContractId, Dummy] = - Dummy.COMPANION val dummyTemplateSubscriptions = new Subscriptions( "Dummy template", @@ -729,7 +726,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { assertSingleton("Only one create in the ACS", acs).representativePackageId, // For create events stemming from command submissions, // the representative package-id is the same as the contract's package-id - Dummy.PACKAGE_ID, + ModelTestDar.packageId, ) val acsDeltaTx = assertSingleton("Acs Delta transactions for Dummy template", acsDeltaTxs) @@ -741,7 +738,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "ACS delta events", acsDeltaTx.events, ).event.created.value.representativePackageId, - Dummy.PACKAGE_ID, + ModelTestDar.packageId, ) assertEquals( @@ -750,7 +747,7 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { "Ledger Effects events", ledgerFxTx.events, ).event.created.value.representativePackageId, - Dummy.PACKAGE_ID, + ModelTestDar.packageId, ) } }) @@ -782,8 +779,8 @@ class UpgradingIT(testDars: TestDars) extends LedgerTestSuite { .transactions(LedgerEffects, party) .map(_.flatMap(exercisedEvents)) } yield { - val v1TmplId = Fetcher_V1.TEMPLATE_ID_WITH_PACKAGE_ID - val v2TmplId = Fetcher_V2.TEMPLATE_ID_WITH_PACKAGE_ID + val v1TmplId = Fetcher_V1.TEMPLATE_ID.withPackageId(UpgradeFetchTestDar1_0_0.packageId) + val v2TmplId = Fetcher_V2.TEMPLATE_ID.withPackageId(UpgradeFetchTestDar2_0_0.packageId) // The first exercise reports template with package id per v1, and the second per v2 assertEquals(toJavaProto(exercised1.templateId.value), v1TmplId.toProto) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/VettingIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/VettingIT.scala index 2485dd16d4..e65ee6a0ab 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/VettingIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/VettingIT.scala @@ -30,15 +30,7 @@ import com.daml.ledger.api.v2.package_service.{ TopologyStateFilter, } import com.daml.ledger.javaapi.data.codegen.ContractCompanion -import com.daml.ledger.test.java.vetting_alt.alt.AltT -import com.daml.ledger.test.java.vetting_dep.dep.DepT -import com.daml.ledger.test.java.vetting_main_1_0_0.main.{ - MainT as MainT_1_0_0, - MainTSimple as MainTSimple_1_0_0, -} -import com.daml.ledger.test.java.vetting_main_2_0_0.main.MainT as MainT_2_0_0 -import com.daml.ledger.test.java.vetting_main_3_0_0.main.MainT as MainT_3_0_0 -import com.daml.ledger.test.java.vetting_main_split_lineage_2_0_0.main.DifferentMainT as MainT_Split_Lineage_2_0_0 +import com.daml.ledger.test.java.vetting_main_1_0_0.main.MainTSimple as MainTSimple_1_0_0 import com.digitalasset.canton.ProtoDeserializationError.ProtoDeserializationFailure import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.ledger.api.{ @@ -73,14 +65,13 @@ class VettingIT(testDars: TestDars) extends LedgerTestSuite with AppendedClues { VettingMainDar_Split_Lineage_2_0_0, } - private val vettingDepPkgId = Ref.PackageId.assertFromString(DepT.PACKAGE_ID) - private val vettingAltPkgId = Ref.PackageId.assertFromString(AltT.PACKAGE_ID) - private val vettingMainPkgIdV1 = Ref.PackageId.assertFromString(MainT_1_0_0.PACKAGE_ID) - private val vettingMainPkgIdV2 = Ref.PackageId.assertFromString(MainT_2_0_0.PACKAGE_ID) - private val vettingMainPkgIdV2SplitLineage = - Ref.PackageId.assertFromString(MainT_Split_Lineage_2_0_0.PACKAGE_ID) + private val vettingDepPkgId = testDars.VettingDepDar.packageId + private val vettingAltPkgId = testDars.VettingAltDar.packageId + private val vettingMainPkgIdV1 = testDars.VettingMainDar_1_0_0.packageId + private val vettingMainPkgIdV2 = testDars.VettingMainDar_2_0_0.packageId + private val vettingMainPkgIdV2SplitLineage = testDars.VettingMainDar_Split_Lineage_2_0_0.packageId private val vettingMainPkgIdV3UpgradeIncompatible = - Ref.PackageId.assertFromString(MainT_3_0_0.PACKAGE_ID) + testDars.VettingMainDar_3_0_0_Incompatible.packageId private val vettingDepName = "vetting-dep" private val vettingMainName = "vetting-main" @@ -274,7 +265,7 @@ class VettingIT(testDars: TestDars) extends LedgerTestSuite with AppendedClues { alternativeSynchronizer: Option[String] = None, expectedTopologySerial: Option[PriorTopologySerial] = None, allowVetIncompatibleUpgrades: Boolean = false, - allowUnvettedDependencies: Boolean = false, + allowUnvettedDependenciesForceFlag: Boolean = false, ): UpdateVettedPackagesRequest = UpdateVettedPackagesRequest( operations.map(VettedPackagesChange(_)), @@ -285,7 +276,7 @@ class VettingIT(testDars: TestDars) extends LedgerTestSuite with AppendedClues { UpdateVettedPackagesForceFlag.UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_VET_INCOMPATIBLE_UPGRADES ).filter(_ => allowVetIncompatibleUpgrades) ++ Seq( UpdateVettedPackagesForceFlag.UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES - ).filter(_ => allowUnvettedDependencies), + ).filter(_ => allowUnvettedDependenciesForceFlag), ) private def changeOpRequest( @@ -309,7 +300,7 @@ class VettingIT(testDars: TestDars) extends LedgerTestSuite with AppendedClues { alternativeSynchronizer = alternativeSynchronizer, expectedTopologySerial = expectedTopologySerial, allowVetIncompatibleUpgrades = allowVetIncompatibleUpgrades, - allowUnvettedDependencies = allowUnvettedDependencies, + allowUnvettedDependenciesForceFlag = allowUnvettedDependencies, ) private def vetPkgsMatchingRef( @@ -975,42 +966,76 @@ class VettingIT(testDars: TestDars) extends LedgerTestSuite with AppendedClues { test( "PVCheckUnvettedPackagesExceptWithForceFlag", - """Unvetted packages are checked, including during dry run, except when UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES is set.""", + "Unvetted packages are checked, including during dry run, except when UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES is set.", + allocate(NoParties), + runConcurrently = false, + )(implicit ec => { case Participants(Participant(participant, _)) => + packageDependencyUnvetting(participant, supportUnvettingPackageDependencies = false) + }) + + test( + "PVUnvettedDependenciesSupported", + "Unvetting package dependencies is supported even when UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES is not set (starting with PV 35)", allocate(NoParties), runConcurrently = false, )(implicit ec => { case Participants(Participant(participant, _)) => + packageDependencyUnvetting(participant, supportUnvettingPackageDependencies = true) + }) + + // This configurable test is intended to be used in two test cases, toggled based on the protocol version under test. + // For example, supportUnvettingPackageDependencies should be false when testing PVs < 35, and true when testing PVs >= 35. + // + // Ideally, the test logic would toggle dynamically based on the protocol version of the target synchronizer. + // However, since the protocol version of synchronizers is not exposed via the Ledger API, we rely on test exclusions in the Ledger API conformance test suites instead. + private def packageDependencyUnvetting( + participant: ParticipantTestContext, + supportUnvettingPackageDependencies: Boolean, + )(implicit ec: ExecutionContext) = { + def unlessUnvettingPackageDependenciesIsSupported(f: => Future[Unit]): Future[Unit] = + if (supportUnvettingPackageDependencies) Future.unit else f + for { _ <- setNodeIds(participant) _ <- participant.uploadDarFile(uploadDarFileDontVetRequest(VettingDepDar)) _ <- participant.uploadDarFile(uploadDarFileDontVetRequest(VettingMainDar_2_0_0)) vetMainWithoutDepRequest = vetPkgIdsRequest(Seq(vettingMainPkgIdV2)) - forceFlags = Seq( - UpdateVettedPackagesForceFlag.UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES - ) - - // Dry-run vetting without dependencies (should fail) - _ <- participant - .updateVettedPackages(vetMainWithoutDepRequest.copy(dryRun = true)) - .mustFailWith( - "Vetting a package without its dependencies in a dry run should give TOPOLOGY_DEPENDENCIES_NOT_VETTED", - ParticipantTopologyManagerError.DependenciesNotVetted, - ) + forceFlags = + // No force flags should be set if unvetting package dependencies is supported + Option + .unless(supportUnvettingPackageDependencies)( + UpdateVettedPackagesForceFlag.UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES + ) + .toList + + _ <- + unlessUnvettingPackageDependenciesIsSupported { + // Dry-run vetting without dependencies (should fail) + participant + .updateVettedPackages(vetMainWithoutDepRequest.copy(dryRun = true)) + .mustFailWith( + "Vetting a package without its dependencies in a dry run should give TOPOLOGY_DEPENDENCIES_NOT_VETTED", + ParticipantTopologyManagerError.DependenciesNotVetted, + ) + } - // Vet without dependencies (should fail) - _ <- participant - .updateVettedPackages(vetMainWithoutDepRequest) - .mustFailWith( - "Vetting a package without its dependencies should give TOPOLOGY_DEPENDENCIES_NOT_VETTED", - ParticipantTopologyManagerError.DependenciesNotVetted, - ) + _ <- + unlessUnvettingPackageDependenciesIsSupported { + // Vet without dependencies (should fail) + participant + .updateVettedPackages(vetMainWithoutDepRequest) + .mustFailWith( + "Vetting a package without its dependencies should give TOPOLOGY_DEPENDENCIES_NOT_VETTED", + ParticipantTopologyManagerError.DependenciesNotVetted, + ) + } - // Dry-run vetting without dependencies with force flag (should succeed) + // Dry-run vetting without dependencies should succeed _ <- participant.updateVettedPackages( vetMainWithoutDepRequest.copy(dryRun = true, updateVettedPackagesForceFlags = forceFlags) ) - // Vet without dependencies with force flag (should succeed) + // Vet without dependencies should succeed _ <- participant.updateVettedPackages( vetMainWithoutDepRequest.copy(updateVettedPackagesForceFlags = forceFlags) ) @@ -1028,21 +1053,25 @@ class VettingIT(testDars: TestDars) extends LedgerTestSuite with AppendedClues { ), ) - // Dry run vet a package while unvetting its dependencies (should fail) - _ <- participant - .updateVettedPackages(changeOpsRequest(vetMainWhileUnvettingDepOps)) - .mustFailWith( - "Vetting a package while unvetting its dependencies should give TOPOLOGY_DEPENDENCIES_NOT_VETTED", - ParticipantTopologyManagerError.DependenciesNotVetted, - ) + _ <- unlessUnvettingPackageDependenciesIsSupported { + // Dry run vet a package while unvetting its dependencies (should fail) + participant + .updateVettedPackages(changeOpsRequest(vetMainWhileUnvettingDepOps)) + .mustFailWith( + "Vetting a package while unvetting its dependencies should give TOPOLOGY_DEPENDENCIES_NOT_VETTED", + ParticipantTopologyManagerError.DependenciesNotVetted, + ) + } - // Vet a package while unvetting its dependencies (should fail) - _ <- participant - .updateVettedPackages(changeOpsRequest(vetMainWhileUnvettingDepOps)) - .mustFailWith( - "Vetting a package while unvetting its dependencies should give TOPOLOGY_DEPENDENCIES_NOT_VETTED", - ParticipantTopologyManagerError.DependenciesNotVetted, - ) + _ <- unlessUnvettingPackageDependenciesIsSupported { + // Vet a package while unvetting its dependencies (should fail) + participant + .updateVettedPackages(changeOpsRequest(vetMainWhileUnvettingDepOps)) + .mustFailWith( + "Vetting a package while unvetting its dependencies should give TOPOLOGY_DEPENDENCIES_NOT_VETTED", + ParticipantTopologyManagerError.DependenciesNotVetted, + ) + } // Vet a package while unvetting its dependencies, dry run, with force flag (should succeed) _ <- participant @@ -1050,19 +1079,22 @@ class VettingIT(testDars: TestDars) extends LedgerTestSuite with AppendedClues { changeOpsRequest( vetMainWhileUnvettingDepOps, dryRun = true, - allowUnvettedDependencies = true, + allowUnvettedDependenciesForceFlag = !supportUnvettingPackageDependencies, ) ) // Vet a package while unvetting its dependencies, with force flag (should succeed) _ <- participant .updateVettedPackages( - changeOpsRequest(vetMainWhileUnvettingDepOps, allowUnvettedDependencies = true) + changeOpsRequest( + vetMainWhileUnvettingDepOps, + allowUnvettedDependenciesForceFlag = !supportUnvettingPackageDependencies, + ) ) _ <- unvetAllDARMains(participant) } yield () - }) + } test( "PVCheckUpgradeInvariantsExceptWithForceFlag", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/WronglyTypedContractIdIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/WronglyTypedContractIdIT.scala index c50150d244..b3d3d902e9 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/WronglyTypedContractIdIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/WronglyTypedContractIdIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_2 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -11,8 +12,8 @@ import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors import scala.jdk.CollectionConverters.* -final class WronglyTypedContractIdIT extends LedgerTestSuite { - import CompanionImplicits.* +final class WronglyTypedContractIdIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test("WTExerciseFails", "Exercising on a wrong type fails", allocate(SingleParty))( implicit ec => { case Participants(Participant(ledger, Seq(party))) => diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysCommandDeduplicationIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysCommandDeduplicationIT.scala index 9cde427551..7523b7c68d 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysCommandDeduplicationIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysCommandDeduplicationIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_3 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite import com.daml.ledger.api.v2.command_service.SubmitAndWaitRequest @@ -16,9 +17,8 @@ import scala.concurrent.duration.* import scala.jdk.CollectionConverters.* import scala.util.{Failure, Success} -final class ContractKeysCommandDeduplicationIT extends LedgerTestSuite { - - import ContractKeysCompanionImplicits.* +final class ContractKeysCommandDeduplicationIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.contractKeysCompanionImplicits.* test( s"StopOnCompletionFailure", diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysCompanionImplicits.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysCompanionImplicits.scala deleted file mode 100644 index 383d35bb64..0000000000 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysCompanionImplicits.scala +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.ledger.api.testtool.suites.v2_3 - -import com.daml.ledger.javaapi.data.codegen.ContractCompanion -import com.daml.ledger.test.java.keys.da.types -import com.daml.ledger.test.java.keys.test.{ - Delegated, - Delegation, - LocalKeyVisibilityOperations, - MaintainerNotSignatory, - ShowDelegated, - TextKey, - TextKeyOperations, - WithKey, -} -import com.daml.ledger.test.java.model.test.{CallablePayout, Dummy} - -object ContractKeysCompanionImplicits { - - implicit val dummyCompanion - : ContractCompanion.WithoutKey[Dummy.Contract, Dummy.ContractId, Dummy] = Dummy.COMPANION - implicit val textKeyCompanion: ContractCompanion.WithKey[ - TextKey.Contract, - TextKey.ContractId, - TextKey, - types.Tuple2[String, String], - ] = TextKey.COMPANION - implicit val textKeyOperationsCompanion: ContractCompanion.WithoutKey[ - TextKeyOperations.Contract, - TextKeyOperations.ContractId, - TextKeyOperations, - ] = TextKeyOperations.COMPANION - implicit val callablePayoutCompanion: ContractCompanion.WithoutKey[ - CallablePayout.Contract, - CallablePayout.ContractId, - CallablePayout, - ] = CallablePayout.COMPANION - implicit val delegatedCompanion: ContractCompanion.WithKey[ - Delegated.Contract, - Delegated.ContractId, - Delegated, - types.Tuple2[String, String], - ] = Delegated.COMPANION - implicit val delegationCompanion - : ContractCompanion.WithoutKey[Delegation.Contract, Delegation.ContractId, Delegation] = - Delegation.COMPANION - implicit val showDelegatedCompanion: ContractCompanion.WithoutKey[ - ShowDelegated.Contract, - ShowDelegated.ContractId, - ShowDelegated, - ] = ShowDelegated.COMPANION - implicit val maintainerNotSignatoryCompanion: ContractCompanion.WithKey[ - MaintainerNotSignatory.Contract, - MaintainerNotSignatory.ContractId, - MaintainerNotSignatory, - String, - ] = MaintainerNotSignatory.COMPANION - implicit val localKeyVisibilityOperationsCompanion: ContractCompanion.WithoutKey[ - LocalKeyVisibilityOperations.Contract, - LocalKeyVisibilityOperations.ContractId, - LocalKeyVisibilityOperations, - ] = LocalKeyVisibilityOperations.COMPANION - implicit val withKeyCompanion: ContractCompanion.WithKey[ - WithKey.Contract, - WithKey.ContractId, - WithKey, - String, - ] = WithKey.COMPANION -} diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysContractIdIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysContractIdIT.scala index 4987d5957e..35339a43a5 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysContractIdIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysContractIdIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_3 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.{ assertErrorCode, @@ -12,7 +13,6 @@ import com.daml.ledger.api.testtool.infrastructure.Assertions.{ import com.daml.ledger.api.testtool.infrastructure.participant.{Features, ParticipantTestContext} import com.daml.ledger.api.testtool.infrastructure.{LedgerTestSuite, Party} import com.daml.ledger.api.testtool.suites.v2_3.ContractKeysContractIdIT.* -import com.daml.ledger.javaapi.data.codegen.ContractCompanion import com.daml.ledger.javaapi.data.{ContractId, DamlRecord} import com.daml.ledger.test.java.keys.contractidtests.{Contract, ContractRef} import com.digitalasset.base.error.ErrorCode @@ -31,16 +31,8 @@ import scala.util.{Failure, Success, Try} // Check the Ledger API accepts or rejects non-suffixed contract ID. // - Central committer ledger implementations (sandboxes, KV...) may accept non-suffixed CID // - Distributed ledger implementations (e.g. Canton) must reject non-suffixed CID -final class ContractKeysContractIdIT extends LedgerTestSuite { - implicit val contractCompanion - : ContractCompanion.WithoutKey[Contract.Contract$, Contract.ContractId, Contract] = - Contract.COMPANION - implicit val contractRefCompanion: ContractCompanion.WithKey[ - ContractRef.Contract, - ContractRef.ContractId, - ContractRef, - String, - ] = ContractRef.COMPANION +final class ContractKeysContractIdIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.contractKeysCompanionImplicits.* List( TestConfiguration( @@ -180,7 +172,7 @@ final class ContractKeysContractIdIT extends LedgerTestSuite { result <- alpha .exerciseByKey( party, - ContractRef.TEMPLATE_ID_WITH_PACKAGE_ID, + contractRefCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, party, "Change", new DamlRecord( diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysExplicitDisclosureIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysExplicitDisclosureIT.scala index e1fd5a8834..4c237fa451 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysExplicitDisclosureIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysExplicitDisclosureIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_3 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.TransactionHelpers.createdEvents @@ -22,14 +23,16 @@ import com.daml.ledger.api.v2.transaction_filter.{ } import com.daml.ledger.api.v2.value.Identifier import com.daml.ledger.javaapi +import com.daml.ledger.javaapi.data.codegen.ContractCompanion import com.daml.ledger.javaapi.data.{DamlRecord, ExerciseByKeyCommand} import com.daml.ledger.test.java.keys.test.WithKey import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors import java.util.List as JList -final class ContractKeysExplicitDisclosureIT extends LedgerTestSuite { +final class ContractKeysExplicitDisclosureIT(testDars: TestDars) extends LedgerTestSuite { import ContractKeysExplicitDisclosureIT.* + import testDars.contractKeysCompanionImplicits.* test( "EDExerciseByKeyDisclosedContract", @@ -125,13 +128,15 @@ object ContractKeysExplicitDisclosureIT { owner: Party, party: Party, withKeyDisclosedContract: Option[DisclosedContract], + )(implicit + companion: ContractCompanion[WithKey.Contract, WithKey.ContractId, WithKey] ): SubmitAndWaitRequest = ledger .submitAndWaitRequest( party, JList.of( new ExerciseByKeyCommand( - WithKey.TEMPLATE_ID_WITH_PACKAGE_ID, + companion.TEMPLATE_ID_WITH_PACKAGE_ID, owner, "WithKey_NoOp", new DamlRecord( diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysIT.scala index 3f1d6106b9..30890d3285 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_3 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.Eventually.eventually @@ -40,8 +41,8 @@ import java.util.regex.Pattern import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* -final class ContractKeysIT extends LedgerTestSuite { - import ContractKeysCompanionImplicits.* +final class ContractKeysIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.contractKeysCompanionImplicits.* test( "CKNoContractKey", @@ -364,7 +365,7 @@ final class ContractKeysIT extends LedgerTestSuite { failureBeforeCreation <- ledger .exerciseByKey( party, - TextKey.TEMPLATE_ID_WITH_PACKAGE_ID, + textKeyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, expectedKey, "TextKeyChoice", new DamlRecord(), @@ -373,7 +374,7 @@ final class ContractKeysIT extends LedgerTestSuite { _ <- ledger.create(party, new TextKey(party, keyString, List.empty.asJava)) _ <- ledger.exerciseByKey( party, - TextKey.TEMPLATE_ID_WITH_PACKAGE_ID, + textKeyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, expectedKey, "TextKeyChoice", new DamlRecord(), @@ -381,7 +382,7 @@ final class ContractKeysIT extends LedgerTestSuite { failureAfterConsuming <- ledger .exerciseByKey( party, - TextKey.TEMPLATE_ID_WITH_PACKAGE_ID, + textKeyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID, expectedKey, "TextKeyChoice", new DamlRecord(), diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysWronglyTypedContractIdIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysWronglyTypedContractIdIT.scala index 9da9cb5e9a..15b4526ba5 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysWronglyTypedContractIdIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysWronglyTypedContractIdIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_3 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite @@ -10,8 +11,8 @@ import com.daml.ledger.test.java.keys.test.{Delegated, Delegation} import com.daml.ledger.test.java.model.test.Dummy import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors -final class ContractKeysWronglyTypedContractIdIT extends LedgerTestSuite { - import ContractKeysCompanionImplicits.* +final class ContractKeysWronglyTypedContractIdIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.contractKeysCompanionImplicits.* test("WTFetchFails", "Fetching of the wrong type fails", allocate(SingleParty))(implicit ec => { case Participants(Participant(ledger, Seq(party))) => diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/PrefetchContractKeysIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/PrefetchContractKeysIT.scala index 23c56590e7..6d4f26aa94 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/PrefetchContractKeysIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/PrefetchContractKeysIT.scala @@ -3,6 +3,7 @@ package com.daml.ledger.api.testtool.suites.v2_3 +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.{ Participant, Participants, @@ -12,6 +13,7 @@ import com.daml.ledger.api.testtool.infrastructure.Allocation.{ import com.daml.ledger.api.testtool.infrastructure.Assertions.{assertGrpcError, futureAssertions} import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite import com.daml.ledger.api.v2.commands +import com.daml.ledger.api.v2.interactive.interactive_submission_service.HashingSchemeVersion.HASHING_SCHEME_VERSION_V3 import com.daml.ledger.javaapi.data.PrefetchContractKey import com.daml.ledger.test.java.keys.da.types.Tuple2 import com.daml.ledger.test.java.keys.test.{TextKey, TextKeyOperations, WithKey} @@ -28,9 +30,9 @@ object PrefetchContractKeysIT { } } -class PrefetchContractKeysIT extends LedgerTestSuite { - import ContractKeysCompanionImplicits.* +class PrefetchContractKeysIT(testDars: TestDars) extends LedgerTestSuite { import PrefetchContractKeysIT.* + import testDars.contractKeysCompanionImplicits.* test( "CSprefetchContractKeysBasic", @@ -47,7 +49,7 @@ class PrefetchContractKeysIT extends LedgerTestSuite { } yield { assert(active.sizeIs == 1) val dummyTemplateId = active.flatMap(_.templateId.toList).headOption.value - assert(dummyTemplateId == WithKey.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) + assert(dummyTemplateId == withKeyCompanion.TEMPLATE_ID_WITH_PACKAGE_ID.toV1) } }) @@ -59,6 +61,7 @@ class PrefetchContractKeysIT extends LedgerTestSuite { val prefetch = WithKey.byKey(party).toPrefetchKey().toProtoInner val request = ledger .prepareSubmissionRequest(party, new WithKey(party).create.commands) + .update(_.hashingSchemeVersion := HASHING_SCHEME_VERSION_V3) .update(_.prefetchContractKeys := Seq(prefetch)) for { prepareResponse <- ledger.prepareSubmission(request) diff --git a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_dev/EventsDescendantsIT.scala b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_dev/EventsDescendantsIT.scala index c906b7afff..af5046b288 100644 --- a/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_dev/EventsDescendantsIT.scala +++ b/canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_dev/EventsDescendantsIT.scala @@ -3,15 +3,14 @@ package com.daml.ledger.api.testtool.suites.v2_dev +import com.daml.ledger.api.testtool.TestDars import com.daml.ledger.api.testtool.infrastructure.Allocation.* import com.daml.ledger.api.testtool.infrastructure.Assertions.* import com.daml.ledger.api.testtool.infrastructure.Eventually.eventually import com.daml.ledger.api.testtool.infrastructure.{LedgerTestSuite, TransactionHelpers} -import com.daml.ledger.api.testtool.suites.v2_2.CompanionImplicits.* import com.daml.ledger.api.testtool.suites.v2_dev.EventsDescendantsIT.isDescendant import com.daml.ledger.api.v2.event.Event import com.daml.ledger.api.v2.event.Event.Event.Exercised -import com.daml.ledger.javaapi.data.codegen.ContractCompanion import com.daml.ledger.test.java.experimental.exceptions.ExceptionTester import com.daml.ledger.test.java.model.test.{ Agreement, @@ -25,8 +24,8 @@ import com.digitalasset.canton.platform.store.utils.EventOps.EventOps import scala.jdk.CollectionConverters.* -class EventsDescendantsIT extends LedgerTestSuite { - import EventsDescendantsIT.CompanionImplicits.* +class EventsDescendantsIT(testDars: TestDars) extends LedgerTestSuite { + import testDars.companionImplicits.* test( "SingleConsumingExercisedDescendants", @@ -444,12 +443,4 @@ object EventsDescendantsIT { who >= nodeId && who <= lastDescendantNodeId } - - private object CompanionImplicits { - implicit val exceptionTesterCompanion: ContractCompanion.WithoutKey[ - ExceptionTester.Contract, - ExceptionTester.ContractId, - ExceptionTester, - ] = ExceptionTester.COMPANION - } } diff --git a/canton/community/ledger/ledger-api-core/src/.gitattributes b/canton/community/ledger/ledger-api-core/src/.gitattributes deleted file mode 100644 index cc1a7a0815..0000000000 --- a/canton/community/ledger/ledger-api-core/src/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -# Enforce Unix newlines -*.sql text eol=lf -*.sha256 text eol=lf diff --git a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/buf.yaml b/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/buf.yaml deleted file mode 100644 index c126332f30..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/buf.yaml +++ /dev/null @@ -1 +0,0 @@ -version: v1 diff --git a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/acs_continuation.proto b/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/acs_continuation.proto deleted file mode 100644 index ef4a547c15..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/acs_continuation.proto +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -syntax = "proto3"; - -package daml.platform.v1; - -option java_package = "com.daml.platform.v1"; - -message AcsContinuationPointerPayload { - // the sequential_id of the event this pointer is pointing to - int64 sequential_id = 1; - // if the corresponding response is generated from incomplete reassignments, this offset - // helps to filter the incomplete reassignments for continuation - optional int64 offset_for_incomplete_reassignments = 2; -} - -message AcsContinuationTokenPayload { - // the pointer where the stream should continue from - AcsContinuationPointerPayload pointer = 1; - // checksum to validate the token - bytes checksum = 2; -} diff --git a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/acs_page_token.proto b/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/acs_page_token.proto deleted file mode 100644 index d04902d27b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/acs_page_token.proto +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -syntax = "proto3"; - -package daml.platform.v1; - -option java_package = "com.daml.platform.v1"; - -message AcsPageTokenPayload { - // the continuation token of the first element in the next page - bytes continuation_token = 1; - int64 active_at_offset = 2; - int32 version = 3; - bytes participant_id_checksum = 4; - bytes request_checksum = 5; -} diff --git a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/index.proto b/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/index.proto deleted file mode 100644 index 3545d51a84..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/index.proto +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Serialization format for Protocol Buffers values stored in the index. -// -// WARNING: -// As all messages declared here represent values stored to the index database, we MUST ensure that -// they remain backwards-compatible forever. - -syntax = "proto3"; - -package daml.platform.v1; - -import "google/protobuf/any.proto"; - -option java_package = "com.daml.platform.v1"; - -// Serialized status details, conveyed from the driver `ReadService` to the ledger API client. -// To be combined with a status code and message. -message StatusDetails { - repeated google.protobuf.Any details = 1; -} diff --git a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/page_tokens.proto b/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/page_tokens.proto deleted file mode 100644 index 58a47b228c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/protobuf/daml/platform/v1/page_tokens.proto +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -syntax = "proto3"; - -package daml.platform.v1; - -option java_package = "com.daml.platform.v1"; - -// Describes the payload of a page token for listing users. -// Not intended to be handled directly by clients and should be presented to them as an opaque string. -message ListUsersPageTokenPayload { - // Users are ordered by ``user_id``, and the next page starts with users whose ``user_id`` is larger than ``user_id_lower_bound_excl``. - string user_id_lower_bound_excl = 1; -} - -// Describes the payload of a page token for listing parties. -// Not intended to be handled directly by clients and should be presented to them as an opaque string. -message ListPartiesPageTokenPayload { - // Parties are ordered by ``party_id``, and the next page starts with parties whose ``party_id`` is larger than ``party_id_lower_bound_excl``. - string party_id_lower_bound_excl = 1; -} - -// Describes the payload of a page token for listing vetted packages. -// Not intended to be handled directly by clients and should be presented to them as an opaque string. -message ListVettedPackagesPageTokenPayload { - // VettedPackages are ordered by ``synchronizer_id`` and then - // ``participant_id``. The next page starts with VettedPackages messages whose - // synchronizer ID is strictly larger ``synchronizer_id``, or whose - // synchronizer ID is equal to ``synchronizer_id`` and whose participant ID is - // strictly larger than ``participant_id``. - string synchronizer_id = 1; - string participant_id = 2; -} - -// Describes the payload of a page token for fetching update pages -// Not intended to be handled directly by clients and should be presented to them as opaque bytes. -message UpdatesPageToken { - // from the current GetUpdatePageResponse (last returned page) - int64 lowest_page_offset_exclusive = 1; - // from the current GetUpdatePageResponse (last returned page) - int64 highest_page_offset_inclusive = 2; - // to verify version difference - int32 version = 3; - // to verify participant difference - bytes participant_id_checksum = 4; - // to verify difference in request (update_format, begin_offset_exclusive, end_offset_inclusive, descending_order) - bytes request_checksum = 5; -} diff --git a/canton/community/ledger/ledger-api-core/src/main/resources/metering-keys/community.json b/canton/community/ledger/ledger-api-core/src/main/resources/metering-keys/community.json deleted file mode 100644 index 60754d40b5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/resources/metering-keys/community.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "scheme": "community-2022", - "algorithm": "HmacSHA256", - "encoded": "iENTFX4g-fAvOBTXnGjIVfesNzmWFKpo_35zpUnXEsg=" -} \ No newline at end of file diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCategoryDocItem.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCategoryDocItem.scala deleted file mode 100644 index bba5359d0a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCategoryDocItem.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator - -final case class ErrorCategoryDocItem( - description: Option[String], - resolution: Option[String], - retryStrategy: Option[String], -) - -object ErrorCategoryDocItem { - def empty: ErrorCategoryDocItem = ErrorCategoryDocItem(None, None, None) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCategoryInventoryDocsGenerator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCategoryInventoryDocsGenerator.scala deleted file mode 100644 index 68861b8430..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCategoryInventoryDocsGenerator.scala +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator - -import com.digitalasset.base.error.ErrorCategory - -object ErrorCategoryInventoryDocsGenerator { - - def genText(): String = - collectErrorCodesAsReStructuredTextSubsections().mkString("\n\n\n") - - private def collectErrorCodesAsReStructuredTextSubsections(): Seq[String] = - ErrorCategory.all.map { errorCategory => - val annotations = ErrorCodeDocumentationGenerator.getErrorCategoryItem(errorCategory) - - val categoryId: String = errorCategory.asInt.toString - val grpcCode: String = errorCategory.grpcCode.fold("N/A")(_.toString) - val name: String = errorCategory.getClass.getSimpleName.replace("$", "") - val logLevel: String = errorCategory.logLevel.toString - val description: String = annotations.description.getOrElse("").replace("\n", " ") - val resolution: String = annotations.resolution.getOrElse("").replace("\n", " ") - val retryStrategy: String = annotations.retryStrategy.getOrElse("").replace("\n", " ") - - s""".. _error-categories-inventory_$name: - | - |$name - |${"=" * 120} - | **Category id**: $categoryId - | - | **gRPC status code**: $grpcCode - | - | **Default log level**: $logLevel - | - | **Description**: $description - | - | **Resolution**: $resolution - | - | **Retry strategy**: $retryStrategy""".stripMargin - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeDocItem.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeDocItem.scala deleted file mode 100644 index 4f7dc21feb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeDocItem.scala +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator - -import com.digitalasset.base.error.{ErrorClass, Explanation, Resolution} -import com.digitalasset.canton.error.generator.ErrorCodeDocumentationGenerator.DeprecatedItem - -/** Contains error presentation data to be used for documentation rendering on the website. - * - * @param errorCodeClassName - * The error class name (see [[com.digitalasset.base.error.ErrorCode]]). - * @param category - * The error code category (see [[com.digitalasset.base.error.ErrorCategory]]). - * @param hierarchicalGrouping - * The hierarchical code grouping (see [[com.digitalasset.base.error.ErrorClass]] and - * [[com.digitalasset.base.error.ErrorGroup]]). - * @param conveyance - * Provides a statement about the form this error will be returned to the user. - * @param code - * The error identifier. - * @param explanation - * The detailed error explanation. - * @param resolution - * The suggested error resolution. - */ -final case class ErrorCodeDocItem( - errorCodeClassName: String, - category: String, - hierarchicalGrouping: ErrorClass, - conveyance: Option[String], - code: String, - deprecation: Option[DeprecatedItem], - explanation: Option[Explanation], - resolution: Option[Resolution], -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeDocumentationGenerator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeDocumentationGenerator.scala deleted file mode 100644 index 09e43dfb1a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeDocumentationGenerator.scala +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator - -import com.digitalasset.base.error.{ - Description, - ErrorCategory, - ErrorCode, - ErrorGroup, - Explanation, - Resolution, - RetryStrategy, -} -import org.reflections.Reflections - -import scala.jdk.CollectionConverters.* -import scala.reflect.runtime.universe as ru -import scala.reflect.runtime.universe.* - -/** Utility that indexes all error code implementations. - */ -object ErrorCodeDocumentationGenerator { - - final case class DeprecatedItem(message: String, since: Option[String]) - - private final case class ErrorCodeAnnotations( - deprecation: Option[DeprecatedItem], - explanation: Option[Explanation], - resolution: Option[Resolution], - ) - - private final case class ErrorGroupAnnotations( - explanation: Option[Explanation] - ) - - private val runtimeMirror: ru.Mirror = ru.runtimeMirror(getClass.getClassLoader) - - private val ScalaDeprecatedTypeName = classOf[deprecated].getTypeName.replace("scala.", "") - private val ExplanationTypeName = classOf[Explanation].getTypeName.replace("$", ".") - private val ResolutionTypeName = classOf[Resolution].getTypeName.replace("$", ".") - private val DescriptionTypeName = classOf[Description].getTypeName.replace("$", ".") - private val RetryStrategyTypeName = classOf[RetryStrategy].getTypeName.replace("$", ".") - - private val DefaultPackagePrefixes: Array[String] = Array("com.daml") - - def getErrorCodeItems( - searchPackagePrefixes: Array[String] = DefaultPackagePrefixes, - excludePackagePrefixes: Array[String] = Array.empty, - ): Seq[ErrorCodeDocItem] = { - val errorCodes = findInstancesOf[ErrorCode](searchPackagePrefixes, excludePackagePrefixes) - errorCodes.view.map(_.id).groupBy(identity).foreach { - case (code, occurrences) if occurrences.sizeIs > 1 => - sys.error( - s"Error code $code is used ${occurrences.size} times but we require each error code to be unique!" - ) - case _ => - } - errorCodes - .map { errorCode => - val annotations = parseErrorCodeAnnotations(errorCode) - ErrorCodeDocItem( - errorCodeClassName = errorCode.getClass.getName, - category = errorCode.category match { - case ErrorCategory.OverrideDocStringErrorCategory(message) => message - case cat => simpleClassName(cat) - }, - hierarchicalGrouping = errorCode.parent, - conveyance = errorCode.errorConveyanceDocString, - code = errorCode.id, - deprecation = annotations.deprecation, - explanation = annotations.explanation, - resolution = annotations.resolution, - ) - } - .sortBy(_.code) - } - - def getErrorGroupItems( - searchPackagePrefixes: Array[String] = DefaultPackagePrefixes, - excludePackagePrefixes: Array[String] = Array.empty, - ): Seq[ErrorGroupDocItem] = { - val errorGroups = findInstancesOf[ErrorGroup](searchPackagePrefixes, excludePackagePrefixes) - errorGroups.view.map(_.errorClass).groupBy(identity).foreach { - case (group, occurrences) if occurrences.sizeIs > 1 => - sys.error( - s"There are ${occurrences.size} groups named $group but we require each group class name to be unique! " - ) - case _ => - } - errorGroups.map { errorGroup => - ErrorGroupDocItem( - errorClass = errorGroup.errorClass, - className = errorGroup.fullClassName, - explanation = parseErrorGroupAnnotations(errorGroup).explanation, - ) - } - } - - def getErrorCategoryItem(errorCategory: ErrorCategory): ErrorCategoryDocItem = { - val mirroredType = runtimeMirror.reflect(errorCategory) - val annotations: Seq[ru.Annotation] = mirroredType.symbol.annotations - val description = new SettableOnce[String] - val resolution = new SettableOnce[String] - val retryStrategy = new SettableOnce[String] - annotations.foreach { annotation => - getAnnotationTypeName(annotation) match { - case DescriptionTypeName => - description.set(parseAnnotationValue(annotation.tree), DescriptionTypeName) - case ResolutionTypeName => - resolution.set(parseAnnotationValue(annotation.tree), ResolutionTypeName) - case RetryStrategyTypeName => - retryStrategy.set(parseAnnotationValue(annotation.tree), RetryStrategyTypeName) - case otherAnnotationTypeName => - throw new IllegalArgumentException( - s"Unexpected annotation of type: $otherAnnotationTypeName; at error category: $errorCategory" - ) - } - } - ErrorCategoryDocItem( - description = description.get, - resolution = resolution.get, - retryStrategy = retryStrategy.get, - ) - } - - private def parseErrorCodeAnnotations(errorCode: ErrorCode): ErrorCodeAnnotations = { - val mirroredType = runtimeMirror.reflect(errorCode) - val annotations: Seq[ru.Annotation] = mirroredType.symbol.annotations - val deprecatedItem = new SettableOnce[DeprecatedItem] - val explanation = new SettableOnce[Explanation] - val resolution = new SettableOnce[Resolution] - annotations.foreach { annotation => - getAnnotationTypeName(annotation) match { - case ExplanationTypeName => - explanation.set( - Explanation(parseAnnotationValue(annotation.tree)), - context = ExplanationTypeName, - ) - case ResolutionTypeName => - resolution.set( - Resolution(parseAnnotationValue(annotation.tree)), - context = ResolutionTypeName, - ) - case ScalaDeprecatedTypeName => - deprecatedItem.set( - parseScalaDeprecatedAnnotation(annotation), - ScalaDeprecatedTypeName, - ) - case otherAnnotationTypeName => - throw new IllegalArgumentException( - s"Unexpected annotation of type: $otherAnnotationTypeName; at error code $errorCode" - ) - } - } - ErrorCodeAnnotations( - deprecation = deprecatedItem.get, - explanation = explanation.get, - resolution = resolution.get, - ) - } - - private[generator] def parseScalaDeprecatedAnnotation( - annotation: ru.Annotation - ): DeprecatedItem = { - val args: Map[String, String] = annotation.tree.children - .drop(1) - .map { - case ru.NamedArg( - ru.Ident(ru.TermName(argName)), - ru.Literal(ru.Constant(text: String)), - ) => - argName -> text.stripMargin - case other => - sys.error(s"Unexpected tree: $other") - } - .toMap - DeprecatedItem(message = args.getOrElse("message", ""), since = args.get("since")) - } - - private def parseErrorGroupAnnotations(errorGroup: ErrorGroup): ErrorGroupAnnotations = { - val mirroredType = runtimeMirror.reflect(errorGroup) - val annotations = mirroredType.symbol.annotations - val explanation = new SettableOnce[Explanation] - annotations.foreach { annotation => - getAnnotationTypeName(annotation) match { - case ExplanationTypeName => - explanation.set(Explanation(parseAnnotationValue(annotation.tree)), ExplanationTypeName) - case otherAnnotationTypeName => - throw new IllegalArgumentException( - s"Unexpected annotation of type: $otherAnnotationTypeName" - ) - } - } - ErrorGroupAnnotations( - explanation = explanation.get - ) - } - - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - private def findInstancesOf[T: ru.TypeTag]( - packagePrefixes: Array[String], - excludePackagePrefixes: Array[String], - ): Seq[T] = - new Reflections(packagePrefixes) - .getSubTypesOf(runtimeMirror.runtimeClass(ru.typeOf[T])) - .asScala - .view - .filterNot { clazz => - val className = clazz.getName - excludePackagePrefixes.exists(prefix => className.startsWith(prefix)) - } - .filter(_.getDeclaredFields.exists(_.getName == "MODULE$")) - .map(clazz => clazz.getDeclaredField("MODULE$").get(clazz).asInstanceOf[T]) - .toSeq - - private def simpleClassName(any: Any): String = - any.getClass.getSimpleName.replace("$", "") - - private def parseAnnotationValue(tree: ru.Tree): String = - tree.children.drop(1) match { - case ru.Literal(ru.Constant(text: String)) :: Nil => text.stripMargin - case other => - sys.error( - s"Failed to process description (description needs to be a constant-string. e.g. don't apply stripMargin). Unexpected tree: $other" - ) - } - - private def getAnnotationTypeName(annotation: ru.Annotation): String = - annotation.tree.tpe.toString - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private class SettableOnce[T >: Null <: AnyRef] { - private var v: Option[T] = None - - def set(v: T, context: String): Unit = { - if (this.v.nonEmpty) - sys.error(s"Duplicate $context detected. A value |$v| is already present.") - this.v = Some(v) - } - - def get: Option[T] = - this.v - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeInventoryDocsGenerator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeInventoryDocsGenerator.scala deleted file mode 100644 index 1da698360c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorCodeInventoryDocsGenerator.scala +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator - -import com.digitalasset.base.error.{ErrorClass, Grouping} -import com.digitalasset.canton.discard.Implicits.DiscardOps - -import scala.collection.mutable -import scala.collection.mutable.ArrayBuffer - -object ErrorCodeInventoryDocsGenerator { - - def genText(): String = { - val errorDocItems = ErrorCodeDocumentationGenerator.getErrorCodeItems() - val groupDocItems = ErrorCodeDocumentationGenerator.getErrorGroupItems() - - val groupSegmentsToExplanationMap: Map[List[Grouping], Option[String]] = - groupDocItems.map { groupDocItem => - groupDocItem.errorClass.groupings -> groupDocItem.explanation.map(_.explanation) - }.toMap - - val errorCodes: Seq[ErrorCodeValue] = errorDocItems.map { errorCodeDocItem => - ErrorCodeValue( - category = errorCodeDocItem.category, - errorGroupPath = errorCodeDocItem.hierarchicalGrouping, - conveyance = newlineIntoSpace(errorCodeDocItem.conveyance.getOrElse("")), - code = errorCodeDocItem.code, - deprecationO = errorCodeDocItem.deprecation.map(v => - newlineIntoSpace(v.message) + v.since.fold("")(s => s" Since: ${newlineIntoSpace(s)}") - ), - explanation = newlineIntoSpace(errorCodeDocItem.explanation.fold("")(_.explanation)), - resolution = newlineIntoSpace(errorCodeDocItem.resolution.fold("")(_.resolution)), - ) - } - - val root = ErrorGroupTree.empty() - - // Build trie like structure of error groups and error codes. - errorCodes.foreach(errorCode => root.insertErrorCode(errorCode, groupSegmentsToExplanationMap)) - // Traverse the trie to emit error code text. - ErrorGroupTree - .collectErrorCodesAsReStructuredTextSubsections(root) - .mkString("\n\n") - - } - - private def newlineIntoSpace(s: String): String = - s.replace('\n', ' ') - -} - -final case class ErrorCodeValue( - code: String, - errorGroupPath: ErrorClass, - category: String, - explanation: String, - resolution: String, - conveyance: String, - deprecationO: Option[String], -) - -class ErrorGroupTree( - val name: String, - val explanation: Option[String] = None, - children: mutable.Map[Grouping, ErrorGroupTree] = - new mutable.HashMap[Grouping, ErrorGroupTree](), - errorCodes: mutable.Map[String, ErrorCodeValue] = new mutable.HashMap[String, ErrorCodeValue](), -) { - - def sortedSubGroups(): List[ErrorGroupTree] = - children.values.toList.sortBy(_.name) - - def sortedErrorCodes(): List[ErrorCodeValue] = - errorCodes.values.toList.sortBy(_.code) - - def insertErrorCode( - errorCode: ErrorCodeValue, - getExplanation: (List[Grouping]) => Option[String], - ): Unit = - insert( - remaining = errorCode.errorGroupPath.groupings, - path = Nil, - errorCode = errorCode, - getExplanation = getExplanation, - ) - - private def insert( - remaining: List[Grouping], - errorCode: ErrorCodeValue, - path: List[Grouping], - getExplanation: (List[Grouping]) => Option[String], - ): Unit = - remaining match { - case Nil => - assert(!errorCodes.contains(errorCode.code), s"Code: ${errorCode.code} is already present!") - errorCodes.put(errorCode.code, errorCode): Unit - case headGroup :: tail => - val newPath = path :+ headGroup - if (!children.contains(headGroup)) { - children - .put( - headGroup, - new ErrorGroupTree( - name = headGroup.docName, - explanation = getExplanation(newPath), - ), - ) - .discard - } - children(headGroup).insert( - remaining = tail, - errorCode = errorCode, - path = newPath, - getExplanation, - ) - } - -} - -object ErrorGroupTree { - def empty(): ErrorGroupTree = new ErrorGroupTree( - name = "", - explanation = None, - ) - - def collectErrorCodesAsReStructuredTextSubsections(root: ErrorGroupTree): List[String] = { - - // in-order tree traversal - def iter( - tree: ErrorGroupTree, - path: List[String], - groupHierarchicalIndex: List[Int], - ): List[String] = { - val newPath = path :+ tree.name - val textBuffer: mutable.ArrayBuffer[String] = new ArrayBuffer[String]() - - // Add group text - textBuffer.addOne(s"""${groupHierarchicalIndex.mkString(".")}. ${newPath.mkString(" / ")} - |=================================================================================================================== - | - |${tree.explanation.getOrElse("")} - |""".stripMargin) - // Add error codes in this group - textBuffer.addAll( - tree - .sortedErrorCodes() - .map(handleErrorCode) - ) - // Recurse to sub-groups - textBuffer.addAll( - tree - .sortedSubGroups() - .zipWithIndex - .flatMap { case (subGroup: ErrorGroupTree, index: Int) => - iter( - subGroup, - newPath, - groupHierarchicalIndex = groupHierarchicalIndex :+ (index + 1), - ) - } - ) - textBuffer.toList - } - - root - .sortedSubGroups() - .zipWithIndex - .flatMap { case (subGroup, i) => - iter(subGroup, path = List(), groupHierarchicalIndex = List(i + 1)) - } - } - - private def handleErrorCode(e: ErrorCodeValue): String = { - val deprecationText = e.deprecationO.fold("")(d => s""" - | **Deprecation**: $d - | """.stripMargin) - val errorCodeReferenceName = s"error_code_${e.code}" - s""" - |.. _$errorCodeReferenceName: - | - |${e.code} - |--------------------------------------------------------------------------------------------------------------------------------------- - | $deprecationText - | **Explanation**: ${e.explanation} - | - | **Category**: ${e.category} - | - | **Conveyance**: ${e.conveyance} - | - | **Resolution**: ${e.resolution} - | - |""".stripMargin - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorGroupDocItem.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorGroupDocItem.scala deleted file mode 100644 index 75f9f5073b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/ErrorGroupDocItem.scala +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator - -import com.digitalasset.base.error.{ErrorClass, Explanation} - -/** Contains error presentation data to be used for documentation rendering on the website. - * - * @param className - * The group class name (see [[com.digitalasset.base.error.ErrorGroup]]). - * @param explanation - * The detailed error explanation. - * @param errorClass - * Hierarchical grouping of this error group. - */ -final case class ErrorGroupDocItem( - className: String, - explanation: Option[Explanation], - errorClass: ErrorClass, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/ErrorCategoryInventoryDocsGenApp.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/ErrorCategoryInventoryDocsGenApp.scala deleted file mode 100644 index d5a354831e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/ErrorCategoryInventoryDocsGenApp.scala +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator.app - -import com.digitalasset.canton.error.generator.ErrorCategoryInventoryDocsGenerator - -import java.nio.charset.StandardCharsets -import java.nio.file.{Files, Paths, StandardOpenOption} - -/** Generates error categories inventory as a reStructuredText - */ -object ErrorCategoryInventoryDocsGenApp { - - /** to run, in sbt call: ledger-api-core/runMain - * com.digitalasset.canton.error.generator.app.ErrorCategoryInventoryDocsGenApp - */ - def main(args: Array[String]): Unit = { - val outputText = ErrorCategoryInventoryDocsGenerator.genText() - if (args.length >= 1) { - val outputFile = Paths.get(args(0)) - Files.write( - outputFile, - outputText.getBytes(StandardCharsets.UTF_8), - StandardOpenOption.CREATE_NEW, - ): Unit - } else { - println(outputText) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/ErrorCodeInventoryDocsGenApp.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/ErrorCodeInventoryDocsGenApp.scala deleted file mode 100644 index a3c0d9ec7a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/ErrorCodeInventoryDocsGenApp.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator.app - -import com.digitalasset.canton.error.generator.ErrorCodeInventoryDocsGenerator - -import java.nio.charset.StandardCharsets -import java.nio.file.{Files, Paths, StandardOpenOption} - -/** Generates error codes inventory as a reStructuredText - */ -object ErrorCodeInventoryDocsGenApp { - - def main(args: Array[String]): Unit = { - val text = ErrorCodeInventoryDocsGenerator.genText() - if (args.length >= 1) { - val outputFile = Paths.get(args(0)) - val _ = Files.write( - outputFile, - text.getBytes(StandardCharsets.UTF_8), - StandardOpenOption.CREATE_NEW, - ) - } else { - println(text) - } - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/Main.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/Main.scala deleted file mode 100644 index 6407e13f28..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/error/generator/app/Main.scala +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator.app - -import com.digitalasset.base.error.Grouping -import com.digitalasset.canton.error.generator.ErrorCodeDocumentationGenerator.DeprecatedItem -import com.digitalasset.canton.error.generator.{ - ErrorCodeDocItem, - ErrorCodeDocumentationGenerator, - ErrorGroupDocItem, -} -import io.circe.Encoder -import io.circe.syntax.* - -import java.nio.file.{Files, Paths, StandardOpenOption} - -/** Outputs information about self-service error codes needed for generating documentation to a json - * file. - */ -object Main { - - final case class Output(errorCodes: Seq[ErrorCodeDocItem], groups: Seq[ErrorGroupDocItem]) - - implicit val groupingEncode: Encoder[Grouping] = - Encoder.forProduct2( - "docName", - "className", - )(i => - ( - i.docName, - i.fullClassName, - ) - ) - - implicit val deprecatedEncode: Encoder[DeprecatedItem] = - Encoder.forProduct2( - "message", - "since", - )(i => (i.message, i.since)) - - implicit val errorCodeEncode: Encoder[ErrorCodeDocItem] = - Encoder.forProduct8( - "className", - "category", - "hierarchicalGrouping", - "conveyance", - "code", - "deprecation", - "explanation", - "resolution", - )(i => - ( - i.errorCodeClassName, - i.category, - i.hierarchicalGrouping.groupings, - i.conveyance, - i.code, - i.deprecation, - i.explanation.fold("")(_.explanation), - i.resolution.fold("")(_.resolution), - ) - ) - - implicit val groupEncode: Encoder[ErrorGroupDocItem] = - Encoder.forProduct2( - "className", - "explanation", - )(i => - ( - i.className, - i.explanation.fold("")(_.explanation), - ) - ) - - implicit val outputEncode: Encoder[Output] = - Encoder.forProduct2("errorCodes", "groups")(i => (i.errorCodes, i.groups)) - - def main(args: Array[String]): Unit = { - val errorCodes = ErrorCodeDocumentationGenerator.getErrorCodeItems() - val groups = ErrorCodeDocumentationGenerator.getErrorGroupItems() - val output = Output(errorCodes, groups) - val outputText: String = output.asJson.spaces2 - - if (args.length >= 1) { - val outputFile = Paths.get(args(0)) - Files.write(outputFile, outputText.getBytes, StandardOpenOption.CREATE_NEW): Unit - } else { - println(outputText) - } - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/ProxyCloseable.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/ProxyCloseable.scala deleted file mode 100644 index d45f59d40c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/ProxyCloseable.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - -trait ProxyCloseable extends AutoCloseable { - - protected def service: AutoCloseable - - override def close(): Unit = service.close() -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/SubmissionIdGenerator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/SubmissionIdGenerator.scala deleted file mode 100644 index 422dd4d60d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/SubmissionIdGenerator.scala +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.SubmissionId - -import java.util.UUID - -trait SubmissionIdGenerator { - def generate(): Ref.SubmissionId -} - -object SubmissionIdGenerator { - object Random extends SubmissionIdGenerator { - override def generate(): SubmissionId = - Ref.SubmissionId.assertFromString(UUID.randomUUID().toString) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/TraceIdentifiers.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/TraceIdentifiers.scala deleted file mode 100644 index 35d904946a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/TraceIdentifiers.scala +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - -import com.daml.ledger.api.v2.reassignment.Reassignment -import com.daml.ledger.api.v2.topology_transaction.TopologyTransaction -import com.daml.ledger.api.v2.transaction.Transaction -import com.daml.tracing.SpanAttribute - -/** Extracts identifiers from Protobuf messages to correlate traces. - */ -object TraceIdentifiers { - - /** Extract identifiers from a transaction message. - */ - def fromTransaction(transaction: Transaction): Map[SpanAttribute, String] = { - val attributes = Map.newBuilder[SpanAttribute, String] - def setIfNotEmpty(attribute: SpanAttribute, value: String): Unit = - if (value.nonEmpty) attributes += attribute -> value - def setIfNotZero(attribute: SpanAttribute, value: Long): Unit = - if (value != 0) attributes += attribute -> value.toString - - setIfNotZero(SpanAttribute.Offset, transaction.offset) - setIfNotEmpty(SpanAttribute.CommandId, transaction.commandId) - setIfNotEmpty(SpanAttribute.TransactionId, transaction.updateId) - setIfNotEmpty(SpanAttribute.WorkflowId, transaction.workflowId) - - attributes.result() - } - - /** Extract identifiers from a reassignment message. - */ - def fromReassignment(reassignment: Reassignment): Map[SpanAttribute, String] = { - val attributes = Map.newBuilder[SpanAttribute, String] - - def setIfNotEmpty(attribute: SpanAttribute, value: String): Unit = - if (value.nonEmpty) attributes += attribute -> value - def setIfNotZero(attribute: SpanAttribute, value: Long): Unit = - if (value != 0) attributes += attribute -> value.toString - - setIfNotZero(SpanAttribute.Offset, reassignment.offset) - setIfNotEmpty(SpanAttribute.CommandId, reassignment.commandId) - setIfNotEmpty(SpanAttribute.TransactionId, reassignment.updateId) - setIfNotEmpty(SpanAttribute.WorkflowId, reassignment.workflowId) - - attributes.result() - } - - def fromTopologyTransaction( - topologyTransaction: TopologyTransaction - ): Map[SpanAttribute, String] = { - val attributes = Map.newBuilder[SpanAttribute, String] - - def setIfNotEmpty(attribute: SpanAttribute, value: String): Unit = - if (value.nonEmpty) attributes += attribute -> value - def setIfNotZero(attribute: SpanAttribute, value: Long): Unit = - if (value != 0) attributes += attribute -> value.toString - - setIfNotZero(SpanAttribute.Offset, topologyTransaction.offset) - setIfNotEmpty(SpanAttribute.TransactionId, topologyTransaction.updateId) - - attributes.result() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/ValidationLogger.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/ValidationLogger.scala deleted file mode 100644 index 43e85325ef..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/ValidationLogger.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - -import com.digitalasset.base.error.ErrorCode.LoggedApiException -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, TracedLogger} - -object ValidationLogger { - def logFailureWithTrace[Request](logger: TracedLogger, request: Request, t: Throwable)(implicit - loggingContextWithTrace: LoggingContextWithTrace - ): Throwable = { - logger.debug( - s"Request validation failed for $request, message: ${t.getMessage}, ${loggingContextWithTrace.makeString}" - ) - t match { - case _: LoggedApiException => () - case _ => logger.info(t.getMessage) - } - t - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/IdentityProviderAwareAuthService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/IdentityProviderAwareAuthService.scala deleted file mode 100644 index c98aa87e1f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/IdentityProviderAwareAuthService.scala +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.auth0.jwt.JWT -import com.daml.jwt.{ - AuthServiceJWTCodec, - AuthServiceJWTPayload, - DecodedJwt, - Error as JwtError, - JwtFromBearerHeader, - JwtVerifier, - StandardJWTPayload, -} -import com.digitalasset.canton.auth.{AuthService, ClaimSet, JwtVerifierLoader} -import com.digitalasset.canton.ledger.api.IdentityProviderId -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext -import io.circe.parser - -import scala.concurrent.{ExecutionContext, Future} - -class IdentityProviderAwareAuthService( - identityProviderConfigLoader: IdentityProviderConfigLoader, - jwtVerifierLoader: JwtVerifierLoader, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext -) extends AuthService - with NamedLogging { - - def decodeToken( - authToken: Option[String], - serviceName: String, - )(implicit traceContext: TraceContext): Future[ClaimSet] = - authToken match { - case None => Future.successful(ClaimSet.Unauthenticated) - case Some(header) => - parseJWTPayload(header).recover { case error => - // While we failed to authorize the token using IDP, it could still be possible - // to be valid by other means of authorizations, i.e. using default auth service - logger.warn("Failed to authorize the token: " + error.getMessage) - ClaimSet.Unauthenticated - } - } - - private def parseJWTPayload( - header: String - )(implicit traceContext: TraceContext): Future[ClaimSet] = - for { - token <- toFuture(JwtFromBearerHeader(header)) - decodedJWT <- Future(JWT.decode(token)) - claims <- extractClaims( - token, - Option(decodedJWT.getIssuer), - Option(decodedJWT.getKeyId), - ) - } yield claims - - def extractClaims( - token: String, - issuer: Option[String], - keyId: Option[String], - )(implicit traceContext: TraceContext): Future[ClaimSet] = - issuer match { - case None => Future.successful(ClaimSet.Unauthenticated) - case Some(issuer) => - for { - identityProviderConfig <- identityProviderConfigLoader - .getIdentityProviderConfig(issuer)(LoggingContextWithTrace(loggerFactory)) - verifier <- jwtVerifierLoader.loadJwtVerifier( - jwksUrl = identityProviderConfig.jwksUrl, - keyId, - ) - decodedJwt <- verifyToken(token, verifier) - payload <- Future( - parse(decodedJwt.payload, targetAudience = identityProviderConfig.audience) - ) - _ <- checkAudience(payload, identityProviderConfig.audience) - jwtPayload <- parsePayload(payload) - } yield toAuthenticatedUser(jwtPayload, identityProviderConfig.identityProviderId) - } - - private def checkAudience( - payload: AuthServiceJWTPayload, - targetAudience: Option[String], - ): Future[Unit] = - (payload, targetAudience) match { - case (payload: StandardJWTPayload, Some(audience)) if payload.audiences.contains(audience) => - Future.unit - case (_, None) => - Future.unit - case _ => - Future.failed(new Exception(s"JWT token has an audience which is not recognized")) - } - - private def verifyToken(token: String, verifier: JwtVerifier): Future[DecodedJwt[String]] = - toFuture(verifier.verify(com.daml.jwt.Jwt(token))) - - private def toFuture[T](e: Either[JwtError, T]): Future[T] = - e.fold(err => Future.failed(new Exception(err.message)), Future.successful) - - private def parsePayload( - jwtPayload: AuthServiceJWTPayload - ): Future[StandardJWTPayload] = - jwtPayload match { - case payload: StandardJWTPayload => - Future.successful(payload) - } - - private def parse(jwtPayload: String, targetAudience: Option[String]): AuthServiceJWTPayload = - if (targetAudience.isDefined) - parseAudienceBasedPayload(jwtPayload) - else - parseAuthServicePayload(jwtPayload) - - private def parseAuthServicePayload(jwtPayload: String): AuthServiceJWTPayload = { - import AuthServiceJWTCodec.JsonImplicits.* - parser - .decode(jwtPayload) - .fold( - err => throw new RuntimeException("Failed to decode JWT JSON payload", err), - identity, - ) - } - - private[this] def parseAudienceBasedPayload(jwtPayload: String): AuthServiceJWTPayload = { - import AuthServiceJWTCodec.AudienceBasedTokenJsonImplicits.* - parser - .decode(jwtPayload) - .fold( - err => throw new RuntimeException("Failed to decode JWT JSON payload", err), - identity, - ) - } - - private def toAuthenticatedUser(payload: StandardJWTPayload, id: IdentityProviderId.Id) = - ClaimSet.AuthenticatedUser( - identityProviderId = Some(id.value), - participantId = payload.participantId, - userId = payload.userId, - expiration = payload.exp, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/IdentityProviderConfigLoader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/IdentityProviderConfigLoader.scala deleted file mode 100644 index 89ef71082b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/IdentityProviderConfigLoader.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.digitalasset.canton.ledger.api.IdentityProviderConfig -import com.digitalasset.canton.logging.LoggingContextWithTrace - -import scala.concurrent.Future - -trait IdentityProviderConfigLoader { - - def getIdentityProviderConfig(issuer: String)(implicit - loggingContext: LoggingContextWithTrace - ): Future[IdentityProviderConfig] - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaims.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaims.scala deleted file mode 100644 index 809109255d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaims.scala +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.daml.ledger.api.v2.transaction_filter.{EventFormat, TransactionFormat, UpdateFormat} -import com.digitalasset.canton.auth.RequiredClaim -import scalapb.lenses.Lens - -object RequiredClaims { - - def apply[Req](claims: RequiredClaim[Req]*): List[RequiredClaim[Req]] = claims.toList - - def submissionClaims[Req]( - actAs: Set[String], - readAs: Set[String], - userIdL: Lens[Req, String], - ): List[RequiredClaim[Req]] = - RequiredClaim.MatchUserId(userIdL) :: - actAs.view.map(RequiredClaim.ActAs[Req]).toList ::: - readAs.view.map(RequiredClaim.ReadAs[Req]).toList - - def executionClaims[Req]( - executeAs: Set[String], - readAs: Set[String], - userIdL: Lens[Req, String], - ): List[RequiredClaim[Req]] = - RequiredClaim.MatchUserId(userIdL) :: - executeAs.view.map(RequiredClaim.ExecuteAs[Req]).toList ::: - readAs.view.map(RequiredClaim.ReadAs[Req]).toList - - def readAsForAllParties[Req](parties: Iterable[String]): List[RequiredClaim[Req]] = - parties.view.map(RequiredClaim.ReadAs[Req]).toList - - def transactionFormatClaims[Req](transactionFormat: TransactionFormat): List[RequiredClaim[Req]] = - transactionFormat.eventFormat.toList.flatMap(RequiredClaims.eventFormatClaims[Req]) - - def eventFormatClaims[Req](eventFormat: EventFormat): List[RequiredClaim[Req]] = - readAsForAllParties[Req](eventFormat.filtersByParty.keys) ::: - eventFormat.filtersForAnyParty.map(_ => RequiredClaim.ReadAsAnyParty[Req]()).toList - - def updateFormatClaims[Req](updateFormat: UpdateFormat): List[RequiredClaim[Req]] = - List( - updateFormat.includeTransactions.toList - .flatMap(transactionFormatClaims[Req]), - updateFormat.includeReassignments.toList - .flatMap(eventFormatClaims[Req]), - updateFormat.includeTopologyEvents - .flatMap(_.includeParticipantAuthorizationEvents) - .toList - .map(_.parties) - .flatMap { - case empty if empty.isEmpty => List(RequiredClaim.ReadAsAnyParty[Req]()) - case nonEmpty => readAsForAllParties[Req](nonEmpty) - }, - ).flatten.distinct - - def idpAdminClaimsAndMatchingRequestIdpId[Req]( - identityProviderIdL: Lens[Req, String], - mustBeParticipantAdmin: Boolean = false, - ): List[RequiredClaim[Req]] = RequiredClaims( - if (mustBeParticipantAdmin) RequiredClaim.Admin[Req]() - else RequiredClaim.AdminOrIdpAdmin[Req](), - RequiredClaim.MatchIdentityProviderId(identityProviderIdL), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/UserBasedOngoingAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/UserBasedOngoingAuthorization.scala deleted file mode 100644 index 28f03f5d7c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/UserBasedOngoingAuthorization.scala +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.daml.jwt.JwtTimestampLeeway -import com.digitalasset.canton.auth.{ - AuthorizationChecksErrors, - ClaimSet, - OngoingAuthorizationFactory, -} -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Mutex -import io.grpc.StatusRuntimeException -import io.grpc.stub.ServerCallStreamObserver -import org.apache.pekko.actor.Scheduler - -import java.time.{Duration, Instant} -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.ExecutionContext - -private[auth] final class UserBasedOngoingAuthorization[A]( - observer: ServerCallStreamObserver[A], - originalClaims: ClaimSet.Claims, - nowF: () => Instant, - userRightsCheckerO: Option[UserRightsChangeAsyncChecker], - userRightsCheckIntervalInSeconds: Int, - lastUserRightsCheckTime: AtomicReference[Instant], - jwtTimestampLeeway: Option[JwtTimestampLeeway], - tokenExpiryGracePeriodForStreams: Option[Duration], - val loggerFactory: NamedLoggerFactory, -)(implicit traceContext: TraceContext) - extends ServerCallStreamObserver[A] - with NamedLogging { - - private implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace( - loggerFactory - ) - private val errorLogger = ErrorLoggingContext(logger, loggerFactory.properties, traceContext) - private val lock = new Mutex() - - // Guards against propagating calls to delegate observer after either - // [[onComplete]] or [[onError]] has already been called once. - // We need this because [[onError]] can be invoked two concurrent sources: - // 1) scheduled user rights state change task (see [[cancellableO]]), - // 2) upstream component that is translating upstream Pekko stream into [[onNext]] and other signals. - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var afterCompletionOrError = false - - // TODO(i15769) as soon as ServerCallStreamObserver.setOnCloseHandler is not experimental anymore, it would be convenient - // to add respective proxy logic here, and support for this in ServerSubscriber, and drop the cancel handler capture - // workaround from here - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var onCancelHandler: Runnable = () => - logger.error( - "Invalid state: OnCancelHandler was never set. Downstream cancellation cannot be done." + - " This can result in detached/rogue server side stream processing, and a resulting memory leak!" - ) - - private val cancelUserRightsChecksO: Option[() => Unit] = - userRightsCheckerO.map( - _.schedule { () => - if (!isCancelled) { - // Downstream cancellation could race with emittion of errors, therefore we only emit error if the stream is - // not cancelled. - abortGRPCStreamAndCancelUpstream(staleStreamAuthError) - } - } - ) - - override def isCancelled: Boolean = (lock.exclusive(observer.isCancelled)) - - override def setOnCancelHandler(runnable: Runnable): Unit = ( - lock.exclusive { - val newCancelHandler: Runnable = { () => - cancelUserRightsChecksO.foreach(_.apply()) - runnable.run() - } - observer.setOnCancelHandler(newCancelHandler) - onCancelHandler = newCancelHandler - } - ) - - override def setCompression(s: String): Unit = (lock.exclusive(observer.setCompression(s))) - - override def isReady: Boolean = (lock.exclusive(observer.isReady)) - - override def setOnReadyHandler(runnable: Runnable): Unit = ( - lock.exclusive( - observer.setOnReadyHandler(runnable) - ) - ) - - override def disableAutoInboundFlowControl(): Unit = ( - lock.exclusive( - observer.disableAutoInboundFlowControl() - ) - ) - - override def request(i: Int): Unit = (lock.exclusive(observer.request(i))) - - override def setMessageCompression(b: Boolean): Unit = ( - lock.exclusive( - observer.setMessageCompression(b) - ) - ) - - override def onNext(v: A): Unit = onlyBeforeCompletionOrError { - val now = nowF() - (for { - _ <- checkClaimsExpiry(now) - _ <- checkUserRightsRefreshTimeout(now) - } yield ()) match { - case Right(_) => observer.onNext(v) - case Left(e) => abortGRPCStreamAndCancelUpstream(e) - } - } - - override def onError(throwable: Throwable): Unit = onlyBeforeCompletionOrError { - afterCompletionOrError = true - cancelUserRightsChecksO.foreach(_.apply()) - observer.onError(throwable) - } - - override def onCompleted(): Unit = onlyBeforeCompletionOrError { - afterCompletionOrError = true - cancelUserRightsChecksO.foreach(_.apply()) - observer.onCompleted() - } - - private def onlyBeforeCompletionOrError(body: => Unit): Unit = - ( - lock.exclusive( - if (!afterCompletionOrError) { - body - } - ) - ) - - private def checkUserRightsRefreshTimeout(now: Instant): Either[StatusRuntimeException, Unit] = - // Safety switch to abort the stream if the user-rights-state-check task - // fails to refresh within 2*[[userRightsCheckIntervalInSeconds]] seconds. - // In normal conditions we expected the refresh delay to be about [[userRightsCheckIntervalInSeconds]] seconds. - Either.cond( - !(originalClaims.resolvedFromUser && - lastUserRightsCheckTime.get.isBefore( - now.minusSeconds(2 * userRightsCheckIntervalInSeconds.toLong) - )), - (), - staleStreamAuthError, - ) - - private def checkClaimsExpiry(now: Instant): Either[StatusRuntimeException, Unit] = - originalClaims - .notExpired(now, jwtTimestampLeeway, tokenExpiryGracePeriodForStreams) - .left - .map(authorizationError => - AuthorizationChecksErrors.AccessTokenExpired - .Reject(authorizationError.reason)(errorLogger) - .asGrpcError - ) - - private def staleStreamAuthError: StatusRuntimeException = - // Terminate the stream, so that clients will restart their streams - // and claims will be rechecked precisely. - AuthorizationChecksErrors.StaleUserManagementBasedStreamClaims - .Reject()(errorLogger) - .asGrpcError - - private def abortGRPCStreamAndCancelUpstream(error: Throwable): Unit = (lock.exclusive { - onError(error) - onCancelHandler.run() - }) -} - -object UserBasedOngoingAuthorization { - - final case class Factory( - now: () => Instant, - userManagementStore: UserManagementStore, - userRightsCheckIntervalInSeconds: Int, - pekkoScheduler: Scheduler, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - tokenExpiryGracePeriodForStreams: Option[Duration] = None, - loggerFactory: NamedLoggerFactory, - )(implicit - ec: ExecutionContext, - traceContext: TraceContext, - ) extends OngoingAuthorizationFactory { - def apply[A]( - observer: ServerCallStreamObserver[A], - claims: ClaimSet.Claims, - ): ServerCallStreamObserver[A] = UserBasedOngoingAuthorization( - observer = observer, - originalClaims = claims, - nowF = now, - userManagementStore = userManagementStore, - userRightsCheckIntervalInSeconds = userRightsCheckIntervalInSeconds, - pekkoScheduler = pekkoScheduler, - jwtTimestampLeeway = jwtTimestampLeeway, - tokenExpiryGracePeriodForStreams = tokenExpiryGracePeriodForStreams, - loggerFactory = loggerFactory, - ) - } - - /** @param userRightsCheckIntervalInSeconds - * determines the interval at which to check whether user rights state has changed. Also, - * double of this value serves as timeout value for subsequent user rights state checks. - */ - def apply[A]( - observer: ServerCallStreamObserver[A], - originalClaims: ClaimSet.Claims, - nowF: () => Instant, - userManagementStore: UserManagementStore, - userRightsCheckIntervalInSeconds: Int, - pekkoScheduler: Scheduler, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - tokenExpiryGracePeriodForStreams: Option[Duration] = None, - loggerFactory: NamedLoggerFactory, - )(implicit - ec: ExecutionContext, - traceContext: TraceContext, - ): ServerCallStreamObserver[A] = { - - val lastUserRightsCheckTime = new AtomicReference(nowF()) - val userRightsCheckerO = if (originalClaims.resolvedFromUser) { - val checker = new UserRightsChangeAsyncChecker( - lastUserRightsCheckTime = lastUserRightsCheckTime, - originalClaims = originalClaims, - nowF: () => Instant, - userManagementStore: UserManagementStore, - userRightsCheckIntervalInSeconds: Int, - pekkoScheduler: Scheduler, - ) - Some(checker) - } else { - None - } - new UserBasedOngoingAuthorization( - observer = observer, - originalClaims = originalClaims, - nowF = nowF, - userRightsCheckerO = userRightsCheckerO, - userRightsCheckIntervalInSeconds = userRightsCheckIntervalInSeconds, - lastUserRightsCheckTime = lastUserRightsCheckTime, - jwtTimestampLeeway = jwtTimestampLeeway, - tokenExpiryGracePeriodForStreams = tokenExpiryGracePeriodForStreams, - loggerFactory = loggerFactory, - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/UserRightsChangeAsyncChecker.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/UserRightsChangeAsyncChecker.scala deleted file mode 100644 index 7c8b0dd189..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/UserRightsChangeAsyncChecker.scala +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.digitalasset.canton.auth.ClaimSet -import com.digitalasset.canton.ledger.api.auth.interceptor.UserBasedClaimResolver -import com.digitalasset.canton.ledger.api.{IdentityProviderId, User, UserRight} -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Ref -import org.apache.pekko.actor.Scheduler - -import java.time.Instant -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.duration.* -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -private[auth] final class UserRightsChangeAsyncChecker( - lastUserRightsCheckTime: AtomicReference[Instant], - originalClaims: ClaimSet.Claims, - nowF: () => Instant, - userManagementStore: UserManagementStore, - userRightsCheckIntervalInSeconds: Int, - pekkoScheduler: Scheduler, -)(implicit ec: ExecutionContext) { - - /** Schedules an asynchronous and periodic task to check for user rights' state changes - * @param userClaimsMismatchCallback - * called when user rights' state change has been detected. - * @return - * a function to cancel the scheduled task - */ - def schedule( - userClaimsMismatchCallback: () => Unit - )(implicit loggingContext: LoggingContextWithTrace): () => Unit = { - val delay = userRightsCheckIntervalInSeconds.seconds - val identityProviderId = originalClaims.identityProviderId - val userId = originalClaims.userId.fold[Ref.UserId]( - throw new RuntimeException( - "Claims were resolved from a user but userId is missing in the claims." - ) - )(Ref.UserId.assertFromString) - assert( - originalClaims.resolvedFromUser, - "The claims were not resolved from a user. Expected claims resolved from a user.", - ) - // Note: https://doc.akka.io/docs/akka/2.6.13/scheduler.html states that: - // "All scheduled task will be executed when the ActorSystem is terminated, i.e. the task may execute before its timeout." - val cancellable = - pekkoScheduler.scheduleWithFixedDelay(initialDelay = delay, delay = delay) { () => - val idpId = IdentityProviderId.fromOptionalLedgerString(identityProviderId) - val userState: Future[Either[UserManagementStore.Error, (User, Set[UserRight])]] = - for { - userRightsResult <- userManagementStore.listUserRights(userId, idpId) - userResult <- userManagementStore.getUser(userId, idpId) - } yield { - for { - userRights <- userRightsResult - user <- userResult - } yield (user, userRights) - } - userState - .onComplete { - case Failure(_) | Success(Left(_)) => - userClaimsMismatchCallback() - case Success(Right((user, userRights))) => - val updatedClaims = - UserBasedClaimResolver.convertUserRightsToClaims(userRights) - if (updatedClaims.toSet != originalClaims.claims.toSet || user.isDeactivated) { - userClaimsMismatchCallback() - } - lastUserRightsCheckTime.set(nowF()) - } - } - () => (cancellable.cancel(): Unit) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/interceptor/UserBasedClaimResolver.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/interceptor/UserBasedClaimResolver.scala deleted file mode 100644 index 5665c68c6f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/interceptor/UserBasedClaimResolver.scala +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.interceptor - -import com.digitalasset.canton.auth.* -import com.digitalasset.canton.ledger.api.{IdentityProviderId, User, UserRight} -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.logging.{ErrorLoggingContext, LoggingContextWithTrace} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.UserId - -import scala.concurrent.{ExecutionContext, Future} - -/** This interceptor uses the given [[com.digitalasset.canton.auth.AuthService]] to get - * [[com.digitalasset.canton.auth.ClaimSet.Claims]] for the current request, and then stores them - * in the current [[io.grpc.Context]]. - * - * @param userManagementStoreO - * use None if user management is disabled - */ -class UserBasedClaimResolver( - userManagementStoreO: Option[UserManagementStore], - implicit val ec: ExecutionContext, -) extends ClaimResolver { - - override def apply(claimSet: ClaimSet)(implicit - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[ClaimSet] = - resolveAuthenticatedUserRights(claimSet) - - import UserBasedClaimResolver.* - - private[this] def resolveAuthenticatedUserRights( - claimSet: ClaimSet - )(implicit - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[ClaimSet] = - claimSet match { - case ClaimSet.AuthenticatedUser(identityProviderId, userIdStr, participantId, expiration) => - val idpId = IdentityProviderId.fromOptionalLedgerString(identityProviderId) - for { - userManagementStore <- getUserManagementStore(userManagementStoreO) - userId <- getUserId(userIdStr) - user <- verifyUserIsActive(userManagementStore, userId, idpId) - _ <- verifyUserIsWithinIdentityProvider(idpId, user) - userRightsResult <- userManagementStore.listUserRights(userId, idpId) - claimsSet <- userRightsResult match { - case Left(msg) => - Future.failed( - AuthorizationChecksErrors.PermissionDenied - .Reject( - s"Could not resolve rights for user '$userId' due to '$msg'" - ) - .asGrpcError - ) - case Right(userRights: Set[UserRight]) => - Future.successful( - ClaimSet.Claims( - claims = convertUserRightsToClaims(userRights), - participantId = participantId, - userId = Some(userId), - expiration = expiration, - resolvedFromUser = true, - identityProviderId = identityProviderId, - ) - ) - } - } yield { - claimsSet - } - case _ => Future.successful(claimSet) - } - - private def verifyUserIsWithinIdentityProvider( - identityProviderId: IdentityProviderId, - user: User, - )(implicit errorLoggingContext: ErrorLoggingContext): Future[Unit] = - if (user.identityProviderId != identityProviderId) { - Future.failed( - AuthorizationChecksErrors.PermissionDenied - .Reject( - s"User is assigned to another identity provider" - ) - .asGrpcError - ) - } else Future.unit - - private def verifyUserIsActive( - userManagementStore: UserManagementStore, - userId: UserId, - identityProviderId: IdentityProviderId, - )(implicit - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[User] = - for { - userResult <- userManagementStore.getUser(id = userId, identityProviderId) - value <- userResult match { - case Left(msg) => - Future.failed( - AuthorizationChecksErrors.PermissionDenied - .Reject( - s"Could not resolve is_deactivated status for user '$userId' and identity_provider_id '$identityProviderId' due to '$msg'" - ) - .asGrpcError - ) - case Right(user: User) => - if (user.isDeactivated) { - Future.failed( - AuthorizationChecksErrors.PermissionDenied - .Reject( - s"User $userId is deactivated" - ) - .asGrpcError - ) - } else { - Future.successful(user) - } - } - } yield value - - private[this] def getUserManagementStore( - userManagementStoreO: Option[UserManagementStore] - )(implicit errorLoggingContext: ErrorLoggingContext): Future[UserManagementStore] = - userManagementStoreO match { - case None => - Future.failed( - AuthorizationChecksErrors.Unauthenticated - .UserBasedAuthenticationIsDisabled() - .asGrpcError - ) - case Some(userManagementStore) => - Future.successful(userManagementStore) - } - - private[this] def getUserId( - userIdStr: String - )(implicit errorLoggingContext: ErrorLoggingContext): Future[Ref.UserId] = - Ref.UserId.fromString(userIdStr) match { - case Left(err) => - Future.failed( - AuthorizationChecksErrors.InvalidToken - .MissingUserId(s"token $err") - .asGrpcError - ) - case Right(userId) => - Future.successful(userId) - } -} - -object UserBasedClaimResolver { - - def convertUserRightsToClaims(userRights: Set[UserRight]): Seq[Claim] = - userRights.view.map(userRightToClaim).toList.prepended(ClaimPublic) - - private[this] def userRightToClaim(r: UserRight): Claim = r match { - case UserRight.CanActAs(p) => ClaimActAsParty(Ref.Party.assertFromString(p)) - case UserRight.CanReadAs(p) => ClaimReadAsParty(Ref.Party.assertFromString(p)) - case UserRight.CanExecuteAs(p) => ClaimExecuteAsParty(Ref.Party.assertFromString(p)) - case UserRight.IdentityProviderAdmin => ClaimIdentityProviderAdmin - case UserRight.ParticipantAdmin => ClaimAdmin - case UserRight.CanReadAsAnyParty => ClaimReadAsAnyParty - case UserRight.CanExecuteAsAnyParty => ClaimExecuteAsAnyParty - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandCompletionServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandCompletionServiceAuthorization.scala deleted file mode 100644 index 4313a28d3e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandCompletionServiceAuthorization.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.command_completion_service.CommandCompletionServiceGrpc.CommandCompletionService -import com.daml.ledger.api.v2.command_completion_service.{ - CommandCompletionServiceGrpc, - CompletionStreamRequest, - CompletionStreamResponse, -} -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.services.CommandCompletionServiceAuthorization.completionStreamClaims -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition -import io.grpc.stub.StreamObserver -import scalapb.lenses.Lens - -import scala.concurrent.ExecutionContext - -final class CommandCompletionServiceAuthorization( - protected val service: CommandCompletionService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends CommandCompletionService - with ProxyCloseable - with GrpcApiService { - - override def completionStream( - request: CompletionStreamRequest, - responseObserver: StreamObserver[CompletionStreamResponse], - ): Unit = - authorizer.stream(service.completionStream)(completionStreamClaims(request)*)( - request, - responseObserver, - ) - - override def bindService(): ServerServiceDefinition = - CommandCompletionServiceGrpc.bindService(this, executionContext) -} - -object CommandCompletionServiceAuthorization { - def completionStreamClaims( - request: CompletionStreamRequest - ): List[RequiredClaim[CompletionStreamRequest]] = - RequiredClaim.MatchUserId( - requestStringL = Lens.unit[CompletionStreamRequest].userId, - skipUserIdValidationForAnyPartyReaders = true, - ) :: request.parties.view.map(RequiredClaim.ReadAs[CompletionStreamRequest]).toList -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandInspectionServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandInspectionServiceAuthorization.scala deleted file mode 100644 index 1d85ed2f6b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandInspectionServiceAuthorization.scala +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.admin.command_inspection_service.CommandInspectionServiceGrpc.CommandInspectionService -import com.daml.ledger.api.v2.admin.command_inspection_service.{ - CommandInspectionServiceGrpc, - GetCommandStatusRequest, - GetCommandStatusResponse, -} -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -final class CommandInspectionServiceAuthorization( - protected val service: CommandInspectionService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends CommandInspectionService - with ProxyCloseable - with GrpcApiService { - - override def bindService(): ServerServiceDefinition = - CommandInspectionServiceGrpc.bindService(this, executionContext) - - override def getCommandStatus( - request: GetCommandStatusRequest - ): Future[GetCommandStatusResponse] = - authorizer.rpc(service.getCommandStatus)(RequiredClaim.Admin())(request) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandServiceAuthorization.scala deleted file mode 100644 index 33e5ee1249..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandServiceAuthorization.scala +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.command_service.* -import com.daml.ledger.api.v2.command_service.CommandServiceGrpc.CommandService -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.auth.services.CommandServiceAuthorization.{ - getSubmitAndWaitForReassignmentClaims, - getSubmitAndWaitForTransactionClaims, -} -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.CommandsValidator -import io.grpc.ServerServiceDefinition -import scalapb.lenses.Lens - -import scala.concurrent.{ExecutionContext, Future} - -/** Note: the command service internally uses calls to the CommandSubmissionService and - * CommandCompletionService. These calls already require authentication, but it is better to check - * authorization here as well. - */ -final class CommandServiceAuthorization( - protected val service: CommandService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends CommandService - with ProxyCloseable - with GrpcApiService { - - override def submitAndWaitForTransaction( - request: SubmitAndWaitForTransactionRequest - ): Future[SubmitAndWaitForTransactionResponse] = - authorizer.rpc(service.submitAndWaitForTransaction)( - getSubmitAndWaitForTransactionClaims(request)* - )(request) - - override def submitAndWaitForReassignment( - request: SubmitAndWaitForReassignmentRequest - ): Future[SubmitAndWaitForReassignmentResponse] = - authorizer.rpc(service.submitAndWaitForReassignment)( - getSubmitAndWaitForReassignmentClaims(request)* - )(request) - - override def submitAndWait( - request: SubmitAndWaitRequest - ): Future[SubmitAndWaitResponse] = { - val effectiveSubmitters = CommandsValidator.effectiveSubmitters(request.commands) - authorizer.rpc(service.submitAndWait)( - RequiredClaims.submissionClaims( - actAs = effectiveSubmitters.actAs, - readAs = effectiveSubmitters.readAs, - userIdL = Lens.unit[SubmitAndWaitRequest].commands.userId, - )* - )(request) - } - - override def bindService(): ServerServiceDefinition = - CommandServiceGrpc.bindService(this, executionContext) -} - -object CommandServiceAuthorization { - def getSubmitAndWaitForTransactionClaims( - request: SubmitAndWaitForTransactionRequest - ): List[RequiredClaim[SubmitAndWaitForTransactionRequest]] = { - val effectiveSubmitters = CommandsValidator.effectiveSubmitters(request.commands) - (RequiredClaims.submissionClaims( - actAs = effectiveSubmitters.actAs, - readAs = effectiveSubmitters.readAs, - userIdL = userIdForTransactionL, - ) ::: request.transactionFormat.toList - .flatMap( - RequiredClaims.transactionFormatClaims[SubmitAndWaitForTransactionRequest] - )).distinct - } - - def getSubmitAndWaitForReassignmentClaims( - request: SubmitAndWaitForReassignmentRequest - ): List[RequiredClaim[SubmitAndWaitForReassignmentRequest]] = - (RequiredClaims.submissionClaims( - actAs = request.reassignmentCommands.fold(Set.empty[String])(c => Set(c.submitter)), - readAs = Set.empty, - userIdL = userIdForReassignmentL, - ) ::: request.eventFormat.toList - .flatMap( - RequiredClaims.eventFormatClaims[SubmitAndWaitForReassignmentRequest] - )).distinct - - val userIdL: Lens[SubmitAndWaitRequest, String] = - Lens.unit[SubmitAndWaitRequest].commands.userId - - val userIdForTransactionL: Lens[SubmitAndWaitForTransactionRequest, String] = - Lens.unit[SubmitAndWaitForTransactionRequest].commands.userId - - val userIdForReassignmentL: Lens[SubmitAndWaitForReassignmentRequest, String] = - Lens.unit[SubmitAndWaitForReassignmentRequest].reassignmentCommands.userId - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandSubmissionServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandSubmissionServiceAuthorization.scala deleted file mode 100644 index b9b86955b1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/CommandSubmissionServiceAuthorization.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.command_submission_service.* -import com.daml.ledger.api.v2.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionService -import com.digitalasset.canton.auth.Authorizer -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.CommandsValidator -import io.grpc.ServerServiceDefinition -import scalapb.lenses.Lens - -import scala.concurrent.{ExecutionContext, Future} - -final class CommandSubmissionServiceAuthorization( - protected val service: CommandSubmissionService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends CommandSubmissionService - with ProxyCloseable - with GrpcApiService { - - override def submit(request: SubmitRequest): Future[SubmitResponse] = { - val effectiveSubmitters = CommandsValidator.effectiveSubmitters(request.commands) - authorizer.rpc(service.submit)( - RequiredClaims.submissionClaims( - actAs = effectiveSubmitters.actAs, - readAs = effectiveSubmitters.readAs, - userIdL = Lens.unit[SubmitRequest].commands.userId, - )* - )(request) - } - - override def submitReassignment( - request: SubmitReassignmentRequest - ): Future[SubmitReassignmentResponse] = - authorizer.rpc(service.submitReassignment)( - RequiredClaims.submissionClaims( - actAs = request.reassignmentCommands.map(_.submitter).toList.toSet, - readAs = Set.empty, - userIdL = Lens.unit[SubmitReassignmentRequest].reassignmentCommands.userId, - )* - )(request) - - override def bindService(): ServerServiceDefinition = - CommandSubmissionServiceGrpc.bindService(this, executionContext) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/ContractServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/ContractServiceAuthorization.scala deleted file mode 100644 index 8438ee66f9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/ContractServiceAuthorization.scala +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.contract_service.* -import com.daml.ledger.api.v2.contract_service.ContractServiceGrpc.ContractService -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -final class ContractServiceAuthorization( - protected val service: ContractService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends ContractService - with ProxyCloseable - with GrpcApiService { - - override def getContract(request: GetContractRequest): Future[GetContractResponse] = - authorizer.rpc(service.getContract)( - ContractServiceAuthorization.requiredClaims(request)* - )(request) - - override def bindService(): ServerServiceDefinition = - ContractServiceGrpc.bindService(this, executionContext) -} - -object ContractServiceAuthorization { - def requiredClaims(request: GetContractRequest): List[RequiredClaim[GetContractRequest]] = - request.queryingParties match { - case empty if empty.isEmpty => List(RequiredClaim.ReadAsAnyParty[GetContractRequest]()) - case nonEmpty => RequiredClaims.readAsForAllParties[GetContractRequest](nonEmpty) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/EventQueryServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/EventQueryServiceAuthorization.scala deleted file mode 100644 index 73545c3388..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/EventQueryServiceAuthorization.scala +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.event_query_service.EventQueryServiceGrpc.EventQueryService -import com.daml.ledger.api.v2.event_query_service.{ - EventQueryServiceGrpc, - GetEventsByContractIdRequest, - GetEventsByContractIdResponse, -} -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.auth.services.EventQueryServiceAuthorization.getEventsByContractIdClaims -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -final class EventQueryServiceAuthorization( - protected val service: EventQueryService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends EventQueryService - with ProxyCloseable - with GrpcApiService { - - override def getEventsByContractId( - request: GetEventsByContractIdRequest - ): Future[GetEventsByContractIdResponse] = - authorizer.rpc(service.getEventsByContractId)( - getEventsByContractIdClaims(request)* - )(request) - - override def bindService(): ServerServiceDefinition = - EventQueryServiceGrpc.bindService(this, executionContext) -} - -object EventQueryServiceAuthorization { - def getEventsByContractIdClaims( - request: GetEventsByContractIdRequest - ): List[RequiredClaim[GetEventsByContractIdRequest]] = - request.eventFormat.toList.flatMap( - RequiredClaims.eventFormatClaims[GetEventsByContractIdRequest] - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/IdentityProviderConfigServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/IdentityProviderConfigServiceAuthorization.scala deleted file mode 100644 index 91a2fc840d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/IdentityProviderConfigServiceAuthorization.scala +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.admin.identity_provider_config_service.* -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -class IdentityProviderConfigServiceAuthorization( - protected val service: IdentityProviderConfigServiceGrpc.IdentityProviderConfigService - with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends IdentityProviderConfigServiceGrpc.IdentityProviderConfigService - with ProxyCloseable - with GrpcApiService { - - override def createIdentityProviderConfig( - request: CreateIdentityProviderConfigRequest - ): Future[CreateIdentityProviderConfigResponse] = - authorizer.rpc(service.createIdentityProviderConfig)(RequiredClaim.Admin())(request) - - override def getIdentityProviderConfig( - request: GetIdentityProviderConfigRequest - ): Future[GetIdentityProviderConfigResponse] = - authorizer.rpc(service.getIdentityProviderConfig)(RequiredClaim.Admin())(request) - - override def updateIdentityProviderConfig( - request: UpdateIdentityProviderConfigRequest - ): Future[UpdateIdentityProviderConfigResponse] = - authorizer.rpc(service.updateIdentityProviderConfig)(RequiredClaim.Admin())(request) - - override def listIdentityProviderConfigs( - request: ListIdentityProviderConfigsRequest - ): Future[ListIdentityProviderConfigsResponse] = - authorizer.rpc(service.listIdentityProviderConfigs)(RequiredClaim.Admin())(request) - - override def deleteIdentityProviderConfig( - request: DeleteIdentityProviderConfigRequest - ): Future[DeleteIdentityProviderConfigResponse] = - authorizer.rpc(service.deleteIdentityProviderConfig)(RequiredClaim.Admin())(request) - - override def bindService(): ServerServiceDefinition = - IdentityProviderConfigServiceGrpc.bindService(this, executionContext) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/InteractiveSubmissionServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/InteractiveSubmissionServiceAuthorization.scala deleted file mode 100644 index 02e8507bf4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/InteractiveSubmissionServiceAuthorization.scala +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.interactive.interactive_submission_service.InteractiveSubmissionServiceGrpc.InteractiveSubmissionService -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{ - ExecuteSubmissionAndWaitForTransactionRequest, - ExecuteSubmissionAndWaitForTransactionResponse, - ExecuteSubmissionAndWaitRequest, - ExecuteSubmissionAndWaitResponse, - ExecuteSubmissionRequest, - ExecuteSubmissionResponse, - GetPreferredPackageVersionRequest, - GetPreferredPackageVersionResponse, - GetPreferredPackagesRequest, - GetPreferredPackagesResponse, - InteractiveSubmissionServiceGrpc, - PrepareSubmissionRequest, - PrepareSubmissionResponse, - PreparedTransaction, -} -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.CommandsValidator -import io.grpc.ServerServiceDefinition -import scalapb.lenses.Lens - -import scala.concurrent.{ExecutionContext, Future} - -/** Enforce authorization for the interactive submission service using LAPI User management. - */ -final class InteractiveSubmissionServiceAuthorization( - protected val service: InteractiveSubmissionService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends InteractiveSubmissionService - with ProxyCloseable - with GrpcApiService { - - import InteractiveSubmissionServiceAuthorization.* - - override def prepareSubmission( - request: PrepareSubmissionRequest - ): Future[PrepareSubmissionResponse] = - authorizer.rpc(service.prepareSubmission)( - getPreparedSubmissionClaims(request)* - )(request) - - override def executeSubmission( - request: ExecuteSubmissionRequest - ): Future[ExecuteSubmissionResponse] = - authorizer.rpc(service.executeSubmission)( - getExecuteSubmissionClaims( - request, - preparedTransactionForExecuteSubmissionL, - userIdForExecuteSubmissionL, - )* - )(request) - - override def getPreferredPackageVersion( - request: GetPreferredPackageVersionRequest - ): Future[GetPreferredPackageVersionResponse] = - authorizer.rpc(service.getPreferredPackageVersion)(RequiredClaim.Public())(request) - - override def getPreferredPackages( - request: GetPreferredPackagesRequest - ): Future[GetPreferredPackagesResponse] = - authorizer.rpc(service.getPreferredPackages)(RequiredClaim.Public())(request) - - override def bindService(): ServerServiceDefinition = - InteractiveSubmissionServiceGrpc.bindService(this, executionContext) - - override def executeSubmissionAndWait( - request: ExecuteSubmissionAndWaitRequest - ): Future[ExecuteSubmissionAndWaitResponse] = - authorizer.rpc(service.executeSubmissionAndWait)( - getExecuteSubmissionClaims( - request, - preparedTransactionForExecuteSubmissionAndWaitL, - userIdForExecuteSubmissionAndWaitL, - )* - )(request) - - override def executeSubmissionAndWaitForTransaction( - request: ExecuteSubmissionAndWaitForTransactionRequest - ): Future[ExecuteSubmissionAndWaitForTransactionResponse] = - authorizer.rpc(service.executeSubmissionAndWaitForTransaction)( - getExecuteSubmissionAndWaitForTransactionClaims(request)* - )(request) -} - -object InteractiveSubmissionServiceAuthorization { - - def getPreparedSubmissionClaims( - request: PrepareSubmissionRequest - ): List[RequiredClaim[PrepareSubmissionRequest]] = { - val effectiveSubmitters = CommandsValidator.effectiveSubmitters(request) - RequiredClaims.executionClaims( - executeAs = Set.empty, // At preparation time the executeAs parties are only reading - readAs = effectiveSubmitters.readAs ++ effectiveSubmitters.actAs, - userIdL = userIdForPrepareSubmissionL, - ) - } - - def getExecuteSubmissionClaims[Req]( - request: Req, - preparedTransactionL: Lens[Req, Option[PreparedTransaction]], - userIdL: Lens[Req, String], - ): List[RequiredClaim[Req]] = { - val executeAsO = for { - preparedTx <- preparedTransactionL.get(request) - metadata <- preparedTx.metadata - submitterInfo <- metadata.submitterInfo - } yield submitterInfo.actAs - val executeAs = executeAsO.getOrElse(Seq.empty) - RequiredClaims.executionClaims( - executeAs = executeAs.toSet[String], - readAs = Set.empty[String], - userIdL = userIdL, - ) - } - - def getExecuteSubmissionAndWaitForTransactionClaims( - request: ExecuteSubmissionAndWaitForTransactionRequest - ): List[RequiredClaim[ExecuteSubmissionAndWaitForTransactionRequest]] = - (getExecuteSubmissionClaims( - request, - preparedTransactionForExecuteSubmissionAndWaitForTransactionL, - userIdForExecuteSubmissionAndWaitForTransactionL, - ) ::: request.transactionFormat.toList - .flatMap( - RequiredClaims.transactionFormatClaims[ExecuteSubmissionAndWaitForTransactionRequest] - )).distinct - - val userIdForPrepareSubmissionL: Lens[PrepareSubmissionRequest, String] = - Lens.unit[PrepareSubmissionRequest].userId - val preparedTransactionForExecuteSubmissionL - : Lens[ExecuteSubmissionRequest, Option[PreparedTransaction]] = - Lens.unit[ExecuteSubmissionRequest].optionalPreparedTransaction - val userIdForExecuteSubmissionL: Lens[ExecuteSubmissionRequest, String] = - Lens.unit[ExecuteSubmissionRequest].userId - val preparedTransactionForExecuteSubmissionAndWaitL - : Lens[ExecuteSubmissionAndWaitRequest, Option[PreparedTransaction]] = - Lens.unit[ExecuteSubmissionAndWaitRequest].optionalPreparedTransaction - val userIdForExecuteSubmissionAndWaitL: Lens[ExecuteSubmissionAndWaitRequest, String] = - Lens.unit[ExecuteSubmissionAndWaitRequest].userId - val preparedTransactionForExecuteSubmissionAndWaitForTransactionL - : Lens[ExecuteSubmissionAndWaitForTransactionRequest, Option[PreparedTransaction]] = - Lens.unit[ExecuteSubmissionAndWaitForTransactionRequest].optionalPreparedTransaction - val userIdForExecuteSubmissionAndWaitForTransactionL - : Lens[ExecuteSubmissionAndWaitForTransactionRequest, String] = - Lens.unit[ExecuteSubmissionAndWaitForTransactionRequest].userId -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PackageManagementServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PackageManagementServiceAuthorization.scala deleted file mode 100644 index 320004d887..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PackageManagementServiceAuthorization.scala +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.admin.package_management_service.* -import com.daml.ledger.api.v2.admin.package_management_service.PackageManagementServiceGrpc.PackageManagementService -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -final class PackageManagementServiceAuthorization( - protected val service: PackageManagementService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends PackageManagementService - with ProxyCloseable - with GrpcApiService { - - override def listKnownPackages( - request: ListKnownPackagesRequest - ): Future[ListKnownPackagesResponse] = - authorizer.rpc(service.listKnownPackages)(RequiredClaim.Admin())(request) - - override def uploadDarFile(request: UploadDarFileRequest): Future[UploadDarFileResponse] = - authorizer.rpc(service.uploadDarFile)(RequiredClaim.Admin())(request) - - override def validateDarFile(request: ValidateDarFileRequest): Future[ValidateDarFileResponse] = - authorizer.rpc(service.validateDarFile)(RequiredClaim.Admin())(request) - - override def bindService(): ServerServiceDefinition = - PackageManagementServiceGrpc.bindService(this, executionContext) - - override def close(): Unit = service.close() - - override def updateVettedPackages( - request: UpdateVettedPackagesRequest - ): Future[UpdateVettedPackagesResponse] = - authorizer.rpc(service.updateVettedPackages)(RequiredClaim.Admin())(request) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PackageServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PackageServiceAuthorization.scala deleted file mode 100644 index 43680de9fc..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PackageServiceAuthorization.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.package_service.* -import com.daml.ledger.api.v2.package_service.PackageServiceGrpc.PackageService -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -final class PackageServiceAuthorization( - protected val service: PackageService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends PackageService - with ProxyCloseable - with GrpcApiService { - - override def listPackages(request: ListPackagesRequest): Future[ListPackagesResponse] = - authorizer.rpc(service.listPackages)(RequiredClaim.Public())(request) - - override def getPackage(request: GetPackageRequest): Future[GetPackageResponse] = - authorizer.rpc(service.getPackage)(RequiredClaim.Public())(request) - - override def getPackageStatus( - request: GetPackageStatusRequest - ): Future[GetPackageStatusResponse] = - authorizer.rpc(service.getPackageStatus)(RequiredClaim.Public())(request) - - override def listVettedPackages( - request: ListVettedPackagesRequest - ): Future[ListVettedPackagesResponse] = - authorizer.rpc(service.listVettedPackages)(RequiredClaim.Public())(request) - - override def bindService(): ServerServiceDefinition = - PackageServiceGrpc.bindService(this, executionContext) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/ParticipantPruningServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/ParticipantPruningServiceAuthorization.scala deleted file mode 100644 index ca8449357d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/ParticipantPruningServiceAuthorization.scala +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.admin.participant_pruning_service.ParticipantPruningServiceGrpc.ParticipantPruningService -import com.daml.ledger.api.v2.admin.participant_pruning_service.{ - ParticipantPruningServiceGrpc, - PruneRequest, - PruneResponse, -} -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -class ParticipantPruningServiceAuthorization( - protected val service: ParticipantPruningService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends ParticipantPruningService - with ProxyCloseable - with GrpcApiService { - - override def bindService(): ServerServiceDefinition = - ParticipantPruningServiceGrpc.bindService(this, executionContext) - - override def close(): Unit = service.close() - - override def prune(request: PruneRequest): Future[PruneResponse] = - authorizer.rpc(service.prune)(RequiredClaim.Admin())(request) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PartyManagementServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PartyManagementServiceAuthorization.scala deleted file mode 100644 index 725af09bda..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/PartyManagementServiceAuthorization.scala +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.admin.party_management_service.* -import com.daml.ledger.api.v2.admin.party_management_service.PartyManagementServiceGrpc.PartyManagementService -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition -import scalapb.lenses.Lens - -import scala.concurrent.{ExecutionContext, Future} - -final class PartyManagementServiceAuthorization( - protected val service: PartyManagementService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends PartyManagementService - with ProxyCloseable - with GrpcApiService { - import PartyManagementServiceAuthorization.* - - override def getParticipantId( - request: GetParticipantIdRequest - ): Future[GetParticipantIdResponse] = - authorizer.rpc(service.getParticipantId)(RequiredClaim.Public())(request) - - override def getParties(request: GetPartiesRequest): Future[GetPartiesResponse] = - authorizer.rpc(service.getParties)( - getPartiesClaims(request)* - )(request) - - override def listKnownParties( - request: ListKnownPartiesRequest - ): Future[ListKnownPartiesResponse] = - authorizer.rpc(service.listKnownParties)( - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId( - Lens.unit[ListKnownPartiesRequest].identityProviderId - )* - )(request) - - override def allocateParty(request: AllocatePartyRequest): Future[AllocatePartyResponse] = - authorizer.rpc(service.allocateParty)( - allocatePartyClaims* - )(request) - - override def updatePartyDetails( - request: UpdatePartyDetailsRequest - ): Future[UpdatePartyDetailsResponse] = - authorizer.rpc(service.updatePartyDetails)( - updatePartyDetailsClaims(request)* - )(request) - - override def updatePartyIdentityProviderId( - request: UpdatePartyIdentityProviderIdRequest - ): Future[UpdatePartyIdentityProviderIdResponse] = - authorizer.rpc(service.updatePartyIdentityProviderId)(RequiredClaim.Admin())(request) - - override def generateExternalPartyTopology( - request: GenerateExternalPartyTopologyRequest - ): Future[GenerateExternalPartyTopologyResponse] = - authorizer.rpc(service.generateExternalPartyTopology)(RequiredClaim.Public())(request) - - override def bindService(): ServerServiceDefinition = - PartyManagementServiceGrpc.bindService(this, executionContext) - - override def close(): Unit = service.close() - - override def allocateExternalParty( - request: AllocateExternalPartyRequest - ): Future[AllocateExternalPartyResponse] = - authorizer.rpc(service.allocateExternalParty)( - allocateExternalPartyClaims* - )(request) -} - -object PartyManagementServiceAuthorization { - def updatePartyDetailsClaims( - request: UpdatePartyDetailsRequest - ): List[RequiredClaim[UpdatePartyDetailsRequest]] = - request.partyDetails match { - case Some(_) => - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId( - Lens.unit[UpdatePartyDetailsRequest].partyDetails.identityProviderId - ) - case None => - RequiredClaim.AdminOrIdpAdmin[UpdatePartyDetailsRequest]() :: Nil - } - - def getPartiesClaims( - request: GetPartiesRequest - ): List[RequiredClaim[GetPartiesRequest]] = - RequiredClaims( - RequiredClaim.AdminOrIdpAdminOrOperateAsParty[GetPartiesRequest](request.parties), - RequiredClaim.MatchIdentityProviderId(Lens.unit[GetPartiesRequest].identityProviderId), - ) - - def allocatePartyClaims: List[RequiredClaim[AllocatePartyRequest]] = - RequiredClaims( - RequiredClaim.AdminOrIdpAdminOrSelfAdmin[AllocatePartyRequest]( - Lens.unit[AllocatePartyRequest].userId - ), - RequiredClaim.MatchIdentityProviderId(Lens.unit[AllocatePartyRequest].identityProviderId), - ) - - def allocateExternalPartyClaims: List[RequiredClaim[AllocateExternalPartyRequest]] = - RequiredClaims( - RequiredClaim.AdminOrIdpAdminOrSelfAdmin[AllocateExternalPartyRequest]( - Lens.unit[AllocateExternalPartyRequest].userId - ), - RequiredClaim.MatchIdentityProviderId( - Lens.unit[AllocateExternalPartyRequest].identityProviderId - ), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/StateServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/StateServiceAuthorization.scala deleted file mode 100644 index 4117c54f98..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/StateServiceAuthorization.scala +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.state_service.* -import com.daml.ledger.api.v2.state_service.StateServiceGrpc.StateService -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.auth.services.StateServiceAuthorization.{ - getActiveContractsClaims, - getActiveContractsPageClaims, -} -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition -import io.grpc.stub.StreamObserver -import scalapb.lenses.Lens - -import scala.concurrent.{ExecutionContext, Future} - -final class StateServiceAuthorization( - protected val service: StateService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends StateService - with ProxyCloseable - with GrpcApiService { - - override def getActiveContracts( - request: GetActiveContractsRequest, - responseObserver: StreamObserver[GetActiveContractsResponse], - ): Unit = - authorizer.stream(service.getActiveContracts)( - getActiveContractsClaims(request)* - )(request, responseObserver) - - override def getActiveContractsPage( - request: GetActiveContractsPageRequest - ): Future[GetActiveContractsPageResponse] = - authorizer.rpc(service.getActiveContractsPage)( - getActiveContractsPageClaims(request)* - )(request) - - override def getConnectedSynchronizers( - request: GetConnectedSynchronizersRequest - ): Future[GetConnectedSynchronizersResponse] = - if (request.party.isEmpty) - authorizer.rpc(service.getConnectedSynchronizers)(RequiredClaim.Public())(request) - else - authorizer.rpc(service.getConnectedSynchronizers)( - RequiredClaim.AdminOrIdpAdminOrOperateAsParty(Seq(request.party)), - RequiredClaim.MatchIdentityProviderId( - Lens.unit[GetConnectedSynchronizersRequest].identityProviderId - ), - )(request) - - override def getLedgerEnd(request: GetLedgerEndRequest): Future[GetLedgerEndResponse] = - authorizer.rpc(service.getLedgerEnd)(RequiredClaim.Public())(request) - - override def getLatestPrunedOffsets( - request: GetLatestPrunedOffsetsRequest - ): Future[GetLatestPrunedOffsetsResponse] = - authorizer.rpc(service.getLatestPrunedOffsets)(RequiredClaim.Public())(request) - - override def bindService(): ServerServiceDefinition = - StateServiceGrpc.bindService(this, executionContext) -} - -object StateServiceAuthorization { - def getActiveContractsClaims( - request: GetActiveContractsRequest - ): List[RequiredClaim[GetActiveContractsRequest]] = - request.eventFormat.toList.flatMap( - RequiredClaims.eventFormatClaims[GetActiveContractsRequest] - ) - - def getActiveContractsPageClaims( - request: GetActiveContractsPageRequest - ): List[RequiredClaim[GetActiveContractsPageRequest]] = - request.eventFormat.toList.flatMap( - RequiredClaims.eventFormatClaims[GetActiveContractsPageRequest] - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/TimeServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/TimeServiceAuthorization.scala deleted file mode 100644 index 3c87692128..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/TimeServiceAuthorization.scala +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.testing.time_service.* -import com.daml.ledger.api.v2.testing.time_service.TimeServiceGrpc.TimeService -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.google.protobuf.empty.Empty -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -final class TimeServiceAuthorization( - protected val service: TimeService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends TimeService - with ProxyCloseable - with GrpcApiService { - - override def getTime(request: GetTimeRequest): Future[GetTimeResponse] = - authorizer.rpc(service.getTime)(RequiredClaim.Public())(request) - - override def setTime(request: SetTimeRequest): Future[Empty] = - authorizer.rpc(service.setTime)(RequiredClaim.Admin())(request) - - override def bindService(): ServerServiceDefinition = - TimeServiceGrpc.bindService(this, executionContext) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/UpdateServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/UpdateServiceAuthorization.scala deleted file mode 100644 index 7c0d45cf4b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/UpdateServiceAuthorization.scala +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.update_service.* -import com.daml.ledger.api.v2.update_service.UpdateServiceGrpc.UpdateService -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.auth.services.UpdateServiceAuthorization.getUpdatesClaims -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import io.grpc.ServerServiceDefinition -import io.grpc.stub.StreamObserver - -import scala.concurrent.{ExecutionContext, Future} - -final class UpdateServiceAuthorization( - protected val service: UpdateService with AutoCloseable, - private val authorizer: Authorizer, -)(implicit executionContext: ExecutionContext) - extends UpdateService - with ProxyCloseable - with GrpcApiService { - - override def bindService(): ServerServiceDefinition = - UpdateServiceGrpc.bindService(this, executionContext) - - override def getUpdates( - request: GetUpdatesRequest, - responseObserver: StreamObserver[GetUpdatesResponse], - ): Unit = - authorizer.stream(service.getUpdates)( - getUpdatesClaims(request)* - )(request, responseObserver) - - override def getUpdateByOffset( - request: GetUpdateByOffsetRequest - ): Future[GetUpdateResponse] = - authorizer.rpc(service.getUpdateByOffset)( - request.updateFormat.toList.flatMap( - RequiredClaims.updateFormatClaims[GetUpdateByOffsetRequest] - )* - )(request) - - override def getUpdateById( - request: GetUpdateByIdRequest - ): Future[GetUpdateResponse] = - authorizer.rpc(service.getUpdateById)( - request.updateFormat.toList.flatMap( - RequiredClaims.updateFormatClaims[GetUpdateByIdRequest] - )* - )(request) - - def getUpdatesPage(request: GetUpdatesPageRequest): Future[GetUpdatesPageResponse] = - authorizer.rpc(service.getUpdatesPage)( - request.updateFormat.toList.flatMap(RequiredClaims.updateFormatClaims[GetUpdatesPageRequest])* - )(request) -} - -object UpdateServiceAuthorization { - - def getUpdatesClaims(request: GetUpdatesRequest): List[RequiredClaim[GetUpdatesRequest]] = - request.updateFormat.toList.flatMap( - RequiredClaims.updateFormatClaims[GetUpdatesRequest] - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/UserManagementServiceAuthorization.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/UserManagementServiceAuthorization.scala deleted file mode 100644 index 58ea0dcf53..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/UserManagementServiceAuthorization.scala +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.admin.user_management_service.* -import com.digitalasset.canton.auth.{Authorizer, RequiredClaim} -import com.digitalasset.canton.ledger.api.ProxyCloseable -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import io.grpc.ServerServiceDefinition -import scalapb.lenses.Lens - -import scala.concurrent.{ExecutionContext, Future} - -final class UserManagementServiceAuthorization( - protected val service: UserManagementServiceGrpc.UserManagementService with AutoCloseable, - private val authorizer: Authorizer, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends UserManagementServiceGrpc.UserManagementService - with ProxyCloseable - with GrpcApiService - with NamedLogging { - import UserManagementServiceAuthorization.* - - // Only ParticipantAdmin is allowed to grant ParticipantAdmin right - private def containsParticipantAdmin(rights: Seq[Right]): Boolean = - rights.contains(Right(Right.Kind.ParticipantAdmin(Right.ParticipantAdmin()))) - - override def createUser(request: CreateUserRequest): Future[CreateUserResponse] = - authorizer.rpc(service.createUser)( - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId( - identityProviderIdL = Lens.unit[CreateUserRequest].user.identityProviderId, - mustBeParticipantAdmin = containsParticipantAdmin(request.rights), - )* - )(request) - - override def getUser(request: GetUserRequest): Future[GetUserResponse] = - authorizer.rpc(service.getUser)( - userReaderClaims( - userIdL = Lens.unit[GetUserRequest].userId, - identityProviderIdL = Lens.unit[GetUserRequest].identityProviderId, - )* - )(request) - - override def deleteUser(request: DeleteUserRequest): Future[DeleteUserResponse] = - authorizer.rpc(service.deleteUser)( - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId( - Lens.unit[DeleteUserRequest].identityProviderId - )* - )(request) - - override def listUsers(request: ListUsersRequest): Future[ListUsersResponse] = - authorizer.rpc(service.listUsers)( - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId( - Lens.unit[ListUsersRequest].identityProviderId - )* - )(request) - - override def grantUserRights(request: GrantUserRightsRequest): Future[GrantUserRightsResponse] = - authorizer.rpc(service.grantUserRights)( - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId( - identityProviderIdL = Lens.unit[GrantUserRightsRequest].identityProviderId, - mustBeParticipantAdmin = containsParticipantAdmin(request.rights), - )* - )(request) - - override def revokeUserRights( - request: RevokeUserRightsRequest - ): Future[RevokeUserRightsResponse] = - authorizer.rpc(service.revokeUserRights)( - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId( - identityProviderIdL = Lens.unit[RevokeUserRightsRequest].identityProviderId, - mustBeParticipantAdmin = containsParticipantAdmin(request.rights), - )* - )(request) - - override def listUserRights(request: ListUserRightsRequest): Future[ListUserRightsResponse] = - authorizer.rpc(service.listUserRights)( - userReaderClaims( - userIdL = Lens.unit[ListUserRightsRequest].userId, - identityProviderIdL = Lens.unit[ListUserRightsRequest].identityProviderId, - )* - )(request) - - override def updateUser(request: UpdateUserRequest): Future[UpdateUserResponse] = - request.user match { - case Some(_) => - authorizer.rpc(service.updateUser)( - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId( - Lens.unit[UpdateUserRequest].user.identityProviderId - )* - )(request) - case None => - authorizer.rpc(service.updateUser)(RequiredClaim.AdminOrIdpAdmin())(request) - } - - override def updateUserIdentityProviderId( - request: UpdateUserIdentityProviderIdRequest - ): Future[UpdateUserIdentityProviderIdResponse] = - authorizer.rpc(service.updateUserIdentityProviderId)(RequiredClaim.Admin())(request) - - override def bindService(): ServerServiceDefinition = - UserManagementServiceGrpc.bindService(this, executionContext) - - override def close(): Unit = service.close() -} - -object UserManagementServiceAuthorization { - def userReaderClaims[Req]( - userIdL: Lens[Req, String], - identityProviderIdL: Lens[Req, String], - ): List[RequiredClaim[Req]] = List( - RequiredClaim.MatchUserIdForUserManagement(userIdL), - RequiredClaim.MatchIdentityProviderId(identityProviderIdL), - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/DropRepeated.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/DropRepeated.scala deleted file mode 100644 index be08bb31c3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/DropRepeated.scala +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.grpc - -import org.apache.pekko.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler} -import org.apache.pekko.stream.{Attributes, FlowShape, Inlet, Outlet} - -object DropRepeated { - def apply[T](): GraphStage[FlowShape[T, T]] = new DropRepeated -} - -final class DropRepeated[T] extends GraphStage[FlowShape[T, T]] { - private val in = Inlet[T]("input") - private val out = Outlet[T]("DropRepeated output") - - override def shape: FlowShape[T, T] = FlowShape(in, out) - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = - new GraphStageLogic(shape) { - private var currentValue: Option[T] = None - - setHandler( - in, - new InHandler { - override def onPush(): Unit = { - val element = grab(in) - if (currentValue.contains(element)) { - pull(in) - } else { - currentValue = Some(element) - push(out, element) - } - } - }, - ) - - setHandler( - out, - new OutHandler { - override def onPull(): Unit = - pull(in) - }, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/GrpcApiService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/GrpcApiService.scala deleted file mode 100644 index 501baa829d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/GrpcApiService.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.grpc - -import io.grpc.BindableService - -/** Defines a interface that identifies a api service which will be registered with the ledger api - * grpc server. - */ -trait GrpcApiService extends BindableService with AutoCloseable diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/GrpcHealthService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/GrpcHealthService.scala deleted file mode 100644 index fb6346bd93..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/GrpcHealthService.scala +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.grpc - -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.digitalasset.canton.health.HealthChecks -import com.digitalasset.canton.ledger.api.grpc.GrpcHealthService.* -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContextGrpc -import io.grpc.health.v1.health.{HealthCheckRequest, HealthCheckResponse, HealthGrpc} -import io.grpc.stub.StreamObserver -import io.grpc.{ServerServiceDefinition, Status, StatusRuntimeException} -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.duration.{DurationInt, FiniteDuration} -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -class GrpcHealthService( - healthChecks: HealthChecks, - val loggerFactory: NamedLoggerFactory, - maximumWatchFrequency: FiniteDuration = 1.second, -)(implicit - esf: ExecutionSequencerFactory, - mat: Materializer, - executionContext: ExecutionContext, -) extends HealthGrpc.Health - with StreamingServiceLifecycleManagement - with GrpcApiService - with NamedLogging { - - override def bindService(): ServerServiceDefinition = - HealthGrpc.bindService(this, executionContext) - - override def check(request: HealthCheckRequest): Future[HealthCheckResponse] = { - implicit val loggingContext = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - Future.fromTry(matchResponse(serviceFrom(request))) - } - - override def watch( - request: HealthCheckRequest, - responseObserver: StreamObserver[HealthCheckResponse], - ): Unit = { - implicit val loggingContext = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - registerStream(responseObserver) { - Source - .fromIterator(() => - Iterator.continually(matchResponse(serviceFrom(request)).fold(throw _, identity)) - ) - .throttle(1, per = maximumWatchFrequency) - .via(DropRepeated()) - } - } - - private def matchResponse( - componentName: Option[String] - )(implicit errorLogger: LoggingContextWithTrace): Try[HealthCheckResponse] = - componentName - .collect { - case component if !healthChecks.hasComponent(component) => - val notFound = Status.NOT_FOUND.withDescription(s"Component $component does not exist.") - logger.debug(s"Health check requested for unknown component: '$component'. $notFound") - Failure(new StatusRuntimeException(notFound)) - } - .getOrElse { - if (healthChecks.isHealthy(componentName)) Success(servingResponse) - else Success(notServingResponse) - } -} - -object GrpcHealthService { - private[grpc] val servingResponse = - HealthCheckResponse(HealthCheckResponse.ServingStatus.SERVING) - - private[grpc] val notServingResponse = - HealthCheckResponse(HealthCheckResponse.ServingStatus.NOT_SERVING) - - private def serviceFrom(request: HealthCheckRequest): Option[String] = - Option(request.service).filter(_.nonEmpty) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/Logging.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/Logging.scala deleted file mode 100644 index 966c03e36f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/Logging.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.grpc - -import com.daml.logging.entries.LoggingEntry - -object Logging { - - def traceId(id: Option[String]): LoggingEntry = - "tid" -> (id.getOrElse(""): String) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/StreamingServiceLifecycleManagement.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/StreamingServiceLifecycleManagement.scala deleted file mode 100644 index d59f74e805..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/grpc/StreamingServiceLifecycleManagement.scala +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.grpc - -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.daml.grpc.adapter.server.pekko.ServerAdapter -import com.daml.scalautil.Statement.discard -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.ledger.error.CommonErrors -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLogging} -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.{Mutex, TryUtil} -import io.grpc.StatusRuntimeException -import io.grpc.stub.StreamObserver -import org.apache.pekko.stream.scaladsl.{Keep, Source} -import org.apache.pekko.stream.{KillSwitch, KillSwitches, Materializer} -import org.apache.pekko.{Done, NotUsed} - -import scala.collection.concurrent.TrieMap -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, Future} - -trait StreamingServiceLifecycleManagement extends AutoCloseable with NamedLogging { - - private val directEc = DirectExecutionContext(noTracingLogger) - private val StreamAbortTimeout = Duration(10, "seconds") - private val lock = new Mutex() - - @volatile private var _closed = false - private val _killSwitches = TrieMap.empty[KillSwitch, (TraceContext, Future[Done])] - - def close(): Unit = { - implicit val ec = directEc - val completions = (lock.exclusive { - if (!_closed) { - _closed = true - val completionFs = _killSwitches.map { case (killSwitch, (traceContext, completeF)) => - killSwitch.abort( - closingError(errorLoggingContext(traceContext)) - ) - completeF - } - _killSwitches.clear() - completionFs - } else Nil - }) - // waiting for all the pekko-streams to finish - discard( - Await.result( - awaitable = Future.sequence( - completions.map( - // we don't care about failures after abort - _.transform(_ => TryUtil.unit) - ) - ), - atMost = StreamAbortTimeout, - ) - ) - } - - private def errorHandler(throwable: Throwable): StatusRuntimeException = - CommonErrors.ServiceInternalError - .UnexpectedOrUnknownException(throwable)(errorLoggingContext(TraceContext.empty)) - .asGrpcError - - protected def registerStream[RespT]( - responseObserver: StreamObserver[RespT] - )(createSource: => Source[RespT, NotUsed])(implicit - materializer: Materializer, - executionSequencerFactory: ExecutionSequencerFactory, - traceContext: TraceContext, - ): Unit = { - def ifNotClosed(run: () => Unit): Unit = - if (_closed) responseObserver.onError(closingError(errorLoggingContext)) - else run() - - // Double-checked locking to keep the (potentially expensive) - // by-name `source` evaluation out of the synchronized block - ifNotClosed { () => - val sink = ServerAdapter.toSink(responseObserver, errorHandler) - // Force evaluation before synchronized block - val source = createSource - - { - lock.exclusive { - ifNotClosed { () => - val (killSwitch, doneF) = source - .viaMat(KillSwitches.single)(Keep.right) - .watchTermination()(Keep.both) - .toMat(sink)(Keep.left) - .run() - - logger.debug(s"Streaming to gRPC client started") - - _killSwitches += killSwitch -> (traceContext -> doneF) - - // This can complete outside the synchronized block - // maintaining the need of using a concurrent collection for _killSwitches - doneF.onComplete { _ => - logger.debug(s"Streaming to gRPC client finished") - _killSwitches -= killSwitch - }(directEc) - } - } - } - } - } - - private def closingError(errorLogger: ErrorLoggingContext): StatusRuntimeException = - GrpcErrors.AbortedDueToShutdown.Error()(errorLogger).asGrpcError -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/completion/CompletionStreamRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/completion/CompletionStreamRequest.scala deleted file mode 100644 index e977a5ecb1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/completion/CompletionStreamRequest.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.command.completion - -import com.digitalasset.canton.data.Offset -import com.digitalasset.daml.lf.data.Ref - -final case class CompletionStreamRequest( - userId: Ref.UserId, - parties: Set[Ref.Party], - offset: Option[Offset], -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/submission/SubmitReassignmentRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/submission/SubmitReassignmentRequest.scala deleted file mode 100644 index f7142623c7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/submission/SubmitReassignmentRequest.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.command.submission - -import com.digitalasset.canton.protocol.ReassignmentId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.value.Value - -final case class SubmitReassignmentRequest( - submitter: Ref.Party, - userId: Ref.UserId, - commandId: Ref.CommandId, - submissionId: Ref.SubmissionId, - workflowId: Option[Ref.WorkflowId], - reassignmentCommands: Seq[Either[AssignCommand, UnassignCommand]], -) - -final case class UnassignCommand( - sourceSynchronizerId: Source[SynchronizerId], - targetSynchronizerId: Target[SynchronizerId], - contractId: Value.ContractId, -) -final case class AssignCommand( - sourceSynchronizerId: Source[SynchronizerId], - targetSynchronizerId: Target[SynchronizerId], - reassignmentId: ReassignmentId, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/submission/SubmitRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/submission/SubmitRequest.scala deleted file mode 100644 index 241f651315..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/command/submission/SubmitRequest.scala +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.command.submission - -import com.digitalasset.canton.ledger.api.Commands - -final case class SubmitRequest(commands: Commands) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/event/GetEventsByContractIdRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/event/GetEventsByContractIdRequest.scala deleted file mode 100644 index 7d8757e67a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/event/GetEventsByContractIdRequest.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.event - -import com.digitalasset.canton.ledger.api.EventFormat -import com.digitalasset.daml.lf.value.Value.ContractId - -final case class GetEventsByContractIdRequest( - contractId: ContractId, - eventFormat: EventFormat, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/event/GetEventsByContractKeyRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/event/GetEventsByContractKeyRequest.scala deleted file mode 100644 index cb27e606a3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/event/GetEventsByContractKeyRequest.scala +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.event - -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.Party -import com.digitalasset.daml.lf.value.Value - -final case class GetEventsByContractKeyRequest( - contractKey: Value, - templateId: Ref.Identifier, - requestingParties: Set[Party], - endExclusiveSeqId: Option[Long], -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/state/AcsContinuationToken.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/state/AcsContinuationToken.scala deleted file mode 100644 index 00c24a8307..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/state/AcsContinuationToken.scala +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.state - -import cats.syntax.either.* -import com.daml.ledger.api.v2.state_service.GetActiveContractsRequest -import com.daml.platform.v1.acs_continuation.{ - AcsContinuationPointerPayload, - AcsContinuationTokenPayload, -} -import com.digitalasset.canton.LedgerParticipantId -import com.digitalasset.canton.crypto.{Hash, HashAlgorithm, HashPurpose} -import com.digitalasset.canton.ledger.api.util.{PageTokenUtils, UpdateFormatHashUtils} -import com.digitalasset.canton.ledger.api.validation.{FieldValidator, ValidationErrors} -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.google.protobuf.ByteString -import io.grpc.StatusRuntimeException - -import scala.util.chaining.scalaUtilChainingOps - -final case class AcsRangeInfo( - continuationPointer: Option[AcsContinuationPointer], - requestChecksum: AcsContinuationToken.Checksum, - limit: Option[Long], -) - -object AcsRangeInfo { - def empty: AcsRangeInfo = AcsRangeInfo(None, AcsContinuationToken.emptyChecksum, None) -} - -/** ADT representation of AcsContinuationTokenPayload defined in acs_continuation.proto serialized - * form of the proto is used in State Service API for continuations - */ -sealed trait AcsContinuationPointer extends Product with Serializable { - def toPayload: AcsContinuationPointerPayload - def decrease: AcsContinuationPointer -} - -final case class AcsContinuationPointerActiveContracts( - sequentialId: Long -) extends AcsContinuationPointer { - def toPayload: AcsContinuationPointerPayload = AcsContinuationPointerPayload(sequentialId, None) - def decrease: AcsContinuationPointerActiveContracts = - copy(sequentialId = sequentialId - 1) -} -final case class AcsContinuationPointerIncompleteReassignments( - sequentialId: Long, - offset: Long, -) extends AcsContinuationPointer { - def toPayload: AcsContinuationPointerPayload = - AcsContinuationPointerPayload(sequentialId, Some(offset)) - def decrease: AcsContinuationPointerIncompleteReassignments = - copy(sequentialId = sequentialId - 1) -} - -object AcsContinuationToken { - private val TokenVersion = 1 - - final case class Checksum(bytes: ByteString) - - def emptyChecksum: Checksum = Checksum(ByteString.copyFrom(Array.fill(4)(0.toByte))) - - def calcChecksum( - request: GetActiveContractsRequest, - participantId: LedgerParticipantId, - ): Checksum = - Hash - .build(HashPurpose.AcsContinuationToken, HashAlgorithm.Sha256) - .addLong(request.activeAtOffset) - .addOptional(request.eventFormat, UpdateFormatHashUtils.hashEventFormat) - .addInt(TokenVersion) - .addString(participantId) - .finish() - .pipe(PageTokenUtils.toChecksum) - .pipe(Checksum(_)) - - def decodeAndValidate(expectedChecksum: Checksum, token: ByteString)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, AcsContinuationPointer] = - FieldValidator - .validateProtobufEncodedField( - token, - AcsContinuationTokenPayload, - fieldName = "stream_continuation_token", - errorMessage = "Invalid continuation token for GetActiveContractsRequest", - ) - .ensure(ValidationErrors.invalidContinuationToken)(proto => - proto.checksum == expectedChecksum.bytes - ) - .flatMap(proto => - proto.pointer match { - case Some(ptr) => Right(decodePointerPayload(ptr)) - case None => Left(ValidationErrors.invalidContinuationToken) - } - ) - - private def decodePointerPayload(payload: AcsContinuationPointerPayload) = - payload.offsetForIncompleteReassignments match { - case None => AcsContinuationPointerActiveContracts(payload.sequentialId) - case Some(offset) => - AcsContinuationPointerIncompleteReassignments(payload.sequentialId, offset) - } - - @SuppressWarnings(Array("com.digitalasset.canton.ProtobufToByteString")) - def activeContracts(sequentialId: Long, checksum: Checksum): ByteString = - AcsContinuationTokenPayload( - pointer = Some(AcsContinuationPointerActiveContracts(sequentialId).toPayload), - checksum = checksum.bytes, - ).toByteString - - @SuppressWarnings(Array("com.digitalasset.canton.ProtobufToByteString")) - def incompleteReassignments(sequentialId: Long, offset: Long, checksum: Checksum): ByteString = - AcsContinuationTokenPayload( - pointer = Some(AcsContinuationPointerIncompleteReassignments(sequentialId, offset).toPayload), - checksum = checksum.bytes, - ).toByteString -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/state/AcsPageToken.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/state/AcsPageToken.scala deleted file mode 100644 index 2dc8e4d596..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/state/AcsPageToken.scala +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.state - -import cats.syntax.either.* -import com.daml.ledger.api.v2.state_service.GetActiveContractsPageRequest -import com.daml.platform.v1.acs_page_token.AcsPageTokenPayload -import com.digitalasset.canton.LedgerParticipantId -import com.digitalasset.canton.crypto.{Hash, HashAlgorithm, HashPurpose} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.util.{PageTokenUtils, UpdateFormatHashUtils} -import com.digitalasset.canton.ledger.api.validation.{FieldValidator, ValidationErrors} -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.google.protobuf.ByteString -import io.grpc.StatusRuntimeException - -import scala.util.chaining.scalaUtilChainingOps - -object AcsPageToken { - private val TokenVersion = 1 - - @SuppressWarnings(Array("com.digitalasset.canton.ProtobufToByteString")) - def encode( - request: GetActiveContractsPageRequest, - continuationToken: ByteString, - activeAtOffset: Long, - participantId: LedgerParticipantId, - ): ByteString = - AcsPageTokenPayload( - continuationToken = continuationToken, - activeAtOffset = activeAtOffset, - version = TokenVersion, - participantIdChecksum = calcParticipantChecksum(participantId), - requestChecksum = calcRequestChecksum(request), - ).toByteString - - def calcRequestChecksum(request: GetActiveContractsPageRequest): ByteString = - Hash - .build(HashPurpose.AcsContinuationToken, HashAlgorithm.Sha256) - .addOptional(request.activeAtOffset, _.addLong) - .addOptional(request.eventFormat, UpdateFormatHashUtils.hashEventFormat) - .finish() - .pipe(PageTokenUtils.toChecksum) - - def calcParticipantChecksum(participantId: LedgerParticipantId): ByteString = - PageTokenUtils.calcParticipantChecksum(HashPurpose.AcsContinuationToken, participantId) - - @SuppressWarnings(Array("com.digitalasset.canton.ProtobufToByteString")) - def decodeAndValidate( - expectedRequestChecksum: ByteString, - participantIdChecksum: ByteString, - token: ByteString, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, (Offset, AcsContinuationPointer)] = for { - proto <- FieldValidator - .validateProtobufEncodedField( - token, - AcsPageTokenPayload, - fieldName = "page_token", - errorMessage = "Invalid page token for GetActiveContractsPageRequest", - ) - .ensure( - ValidationErrors.invalidAcsPageToken( - "The page token was prepared by a different participant." - ) - )(proto => proto.participantIdChecksum == participantIdChecksum) - .ensure( - ValidationErrors.invalidAcsPageToken( - "The page token was prepared with different page API version." - ) - )(proto => proto.version == TokenVersion) - .ensure( - ValidationErrors.invalidAcsPageToken( - "The page token was prepared with different event_format or active_at_offset." - ) - )(proto => proto.requestChecksum == expectedRequestChecksum) - offset <- Offset - .fromLong(proto.activeAtOffset) - .left - .map(_ => ValidationErrors.invalidAcsPageToken("Invalid token contents.")) - pointerToTheFirstElem <- AcsContinuationToken - .decodeAndValidate(AcsContinuationToken.emptyChecksum, proto.continuationToken) - } yield (offset, pointerToTheFirstElem.decrease) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetLedgerEndRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetLedgerEndRequest.scala deleted file mode 100644 index 256b8485ae..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetLedgerEndRequest.scala +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.update - -final case class GetLedgerEndRequest() diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetTransactionByIdRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetTransactionByIdRequest.scala deleted file mode 100644 index 599ef25c8c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetTransactionByIdRequest.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.update - -import com.digitalasset.canton.ledger.api.TransactionFormat -import com.digitalasset.canton.protocol.UpdateId - -final case class GetTransactionByIdRequest( - updateId: UpdateId, - transactionFormat: TransactionFormat, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetTransactionByOffsetRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetTransactionByOffsetRequest.scala deleted file mode 100644 index 70861f3d89..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetTransactionByOffsetRequest.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.update - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.TransactionFormat - -final case class GetTransactionByOffsetRequest( - offset: Offset, - transactionFormat: TransactionFormat, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdateByIdRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdateByIdRequest.scala deleted file mode 100644 index a6bd09a728..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdateByIdRequest.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.update - -import com.digitalasset.canton.ledger.api.UpdateFormat -import com.digitalasset.canton.protocol.UpdateId - -final case class GetUpdateByIdRequest( - updateId: UpdateId, - updateFormat: UpdateFormat, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdateByOffsetRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdateByOffsetRequest.scala deleted file mode 100644 index 7971db7207..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdateByOffsetRequest.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.update - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.UpdateFormat - -final case class GetUpdateByOffsetRequest( - offset: Offset, - updateFormat: UpdateFormat, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdatesPageRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdatesPageRequest.scala deleted file mode 100644 index 49bcf2c61a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdatesPageRequest.scala +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.update - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.UpdateFormat -import com.google.protobuf.ByteString - -final case class GetUpdatesPageRequest( - startExclusive: Option[Option[Offset]], // Outer None == dynamic bound, inner None == - endInclusive: Option[Offset], - continueStreamFromIncl: Option[Offset], - maxPageSize: Int, - updateFormat: UpdateFormat, - descendingOrder: Boolean, - requestChecksum: ByteString, - participantChecksum: ByteString, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdatesRequest.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdatesRequest.scala deleted file mode 100644 index b69c7dc624..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/GetUpdatesRequest.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.update - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.UpdateFormat - -final case class GetUpdatesRequest( - startExclusive: Option[Offset], - endInclusive: Option[Offset], - updateFormat: UpdateFormat, - descendingOrder: Boolean, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/UpdatesPageToken.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/UpdatesPageToken.scala deleted file mode 100644 index 24708f0702..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/messages/update/UpdatesPageToken.scala +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.update - -import cats.implicits.toTraverseOps -import com.daml.ledger.api.v2.update_service -import com.daml.platform.v1.page_tokens -import com.digitalasset.canton.crypto.{Hash, HashAlgorithm, HashPurpose} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.util.{PageTokenUtils, UpdateFormatHashUtils} -import com.digitalasset.canton.ledger.api.validation.UpdateServiceRequestValidator.Result -import com.digitalasset.canton.ledger.api.validation.{FieldValidator, ParticipantOffsetValidator} -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.data.Ref -import com.google.protobuf.ByteString - -final case class UpdatesPageToken( - lowestPageOffsetExclusive: Option[Offset], - highestPageOffsetInclusive: Option[Offset], - participantIdChecksum: ByteString, - requestChecksum: ByteString, -) { - def toProto: page_tokens.UpdatesPageToken = - page_tokens.UpdatesPageToken( - lowestPageOffsetExclusive = lowestPageOffsetExclusive.fold(0L)(_.unwrap), - highestPageOffsetInclusive = highestPageOffsetInclusive.fold(0L)(_.unwrap), - version = UpdatesPageToken.Version, - participantIdChecksum = participantIdChecksum, - requestChecksum = requestChecksum, - ) - - @SuppressWarnings(Array("com.digitalasset.canton.ProtobufToByteString")) - def toOpaqueByteString: ByteString = - toProto.toByteString -} - -object UpdatesPageToken { - - /** Token version used to detect format mismatch. Must be bumped every time protobuf token - * structure is changed. - */ - val Version: Int = 1 - - def requestChecksum(request: update_service.GetUpdatesPageRequest): ByteString = - PageTokenUtils.toChecksum( - Hash - .build(HashPurpose.UpdatesPageToken, HashAlgorithm.Sha256) - .addOptional(request.beginOffsetExclusive, _.addLong) - .addOptional(request.endOffsetInclusive, _.addLong) - .addOptional(request.maxPageSize, _.addInt) - .addOptional(request.updateFormat, UpdateFormatHashUtils.hashUpdateFormat) - .addBool(request.descendingOrder) - .finish() - ) - - def participantChecksum(participantId: Ref.ParticipantId): ByteString = - PageTokenUtils.calcParticipantChecksum(HashPurpose.UpdatesPageToken, participantId) - - def validateToken( - updatesPageToken: com.daml.platform.v1.page_tokens.UpdatesPageToken, - request: update_service.GetUpdatesPageRequest, - participantId: Ref.ParticipantId, - )(implicit errorLoggingContext: ErrorLoggingContext): Result[UpdatesPageToken] = - for { - _ <- Either.cond( - updatesPageToken.version == Version, - (), - RequestValidationErrors.InvalidUpdatesPageToken - .Reject("Next page token was generated by a different Canton version") - .asGrpcError, - ) - _ <- Either.cond( - updatesPageToken.requestChecksum == requestChecksum(request), - (), - RequestValidationErrors.InvalidUpdatesPageToken - .Reject("Next page token was obtained with different request parameters") - .asGrpcError, - ) - _ <- Either.cond( - updatesPageToken.participantIdChecksum == participantChecksum(participantId), - (), - RequestValidationErrors.InvalidUpdatesPageToken - .Reject("Next page token was obtained from an other participant node") - .asGrpcError, - ) - lowestOffsetExcl <- ParticipantOffsetValidator - .validateNonNegative( - updatesPageToken.lowestPageOffsetExclusive, - "lowestPageOffsetExclusive", - ) - .left - .map(_ => - RequestValidationErrors.InvalidUpdatesPageToken - .Reject("lowestPageOffsetExclusive bound is negative") - .asGrpcError - ) - highestOffsetIncl <- ParticipantOffsetValidator - .validateNonNegative( - updatesPageToken.highestPageOffsetInclusive, - "highestPageOffsetInclusive", - ) - .left - .map(_ => - RequestValidationErrors.InvalidUpdatesPageToken - .Reject("highestPageOffsetInclusive bound is negative") - .asGrpcError - ) - } yield UpdatesPageToken( - lowestOffsetExcl, - highestOffsetIncl, - updatesPageToken.participantIdChecksum, - updatesPageToken.requestChecksum, - ) - - def validateToken( - request: update_service.GetUpdatesPageRequest, - participantId: Ref.ParticipantId, - )(implicit errorLoggingContext: ErrorLoggingContext): Result[Option[UpdatesPageToken]] = - request.pageToken.traverse { token => - for { - parsedToken <- FieldValidator.validateProtobufEncodedField( - token, - page_tokens.UpdatesPageToken, - fieldName = "page_token", - errorMessage = "Invalid page token for GetUpdatesPageRequest", - ) - validated <- validateToken(parsedToken, request, participantId) - } yield validated - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/package.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/package.scala deleted file mode 100644 index aea9748069..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/package.scala +++ /dev/null @@ -1,1044 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger - -import cats.syntax.either.* -import cats.syntax.order.* -import cats.syntax.traverse.* -import com.daml.jwt.JwksUrl -import com.daml.ledger.api.v2.admin.package_management_service -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.{ - TRANSACTION_SHAPE_ACS_DELTA, - TRANSACTION_SHAPE_LEDGER_EFFECTS, -} -import com.daml.ledger.api.v2.{package_reference, package_service} -import com.daml.logging.entries.{LoggingValue, ToLoggingValue} -import com.daml.nonempty.* -import com.daml.platform.v1.page_tokens.ListVettedPackagesPageTokenPayload -import com.digitalasset.canton.ProtoDeserializationError.{ - FieldNotSet, - InvariantViolation, - UnrecognizedEnum, - ValueConversionError, -} -import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.data.{CantonTimestamp, DeduplicationPeriod} -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.canton.serialization.ProtoConverter -import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.topology.transaction.VettedPackage -import com.digitalasset.canton.topology.{ - ForceFlag, - ForceFlags, - ParticipantId as TopoParticipantId, - SynchronizerId, - UniqueIdentifier, -} -import com.digitalasset.canton.util.{EitherUtil, OptionUtil} -import com.digitalasset.canton.{LfPackageId, LfPackageName, LfPackageVersion} -import com.digitalasset.daml.lf.command.{ApiCommands as LfCommands, ApiContractKey} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.logging.* -import com.digitalasset.daml.lf.data.{ImmArray, Ref} -import com.digitalasset.daml.lf.value.Value as Lf -import scalaz.syntax.tag.* -import scalaz.{@@, Tag} - -import java.nio.charset.StandardCharsets -import java.util.Base64 -import scala.collection.immutable -import scala.util.Try - -package object api { - type Value = Lf - - type WorkflowId = Ref.WorkflowId @@ WorkflowIdTag - val WorkflowId: Tag.TagOf[WorkflowIdTag] = Tag.of[WorkflowIdTag] - - type CommandId = Ref.CommandId @@ CommandIdTag - val CommandId: Tag.TagOf[CommandIdTag] = Tag.of[CommandIdTag] - - type UpdateId = Ref.TransactionId @@ UpdateIdTag - val UpdateId: Tag.TagOf[UpdateIdTag] = Tag.of[UpdateIdTag] - - type ParticipantId = Ref.ParticipantId @@ ParticipantIdTag - val ParticipantId: Tag.TagOf[ParticipantIdTag] = Tag.of[ParticipantIdTag] - - type SubmissionId = Ref.SubmissionId @@ SubmissionIdTag - val SubmissionId: Tag.TagOf[SubmissionIdTag] = Tag.of[SubmissionIdTag] -} - -package api { - - sealed trait WorkflowIdTag - - sealed trait CommandIdTag - - sealed trait UpdateIdTag - - sealed trait EventIdTag - - sealed trait ParticipantIdTag - - sealed trait SubmissionIdTag - - sealed trait IdentityProviderId { - def toRequestString: String - - def toDb: Option[IdentityProviderId.Id] - } - - object IdentityProviderId { - final case object Default extends IdentityProviderId { - override def toRequestString: String = "" - - override def toDb: Option[Id] = None - } - - final case class Id(value: Ref.LedgerString) extends IdentityProviderId { - override def toRequestString: String = value - - override def toDb: Option[Id] = Some(this) - } - - object Id { - def fromString(id: String): Either[String, IdentityProviderId.Id] = - Ref.LedgerString.fromString(id).map(Id.apply) - - def assertFromString(id: String): Id = - Id(Ref.LedgerString.assertFromString(id)) - } - - def apply(identityProviderId: String): IdentityProviderId = - Some(identityProviderId).filter(_.nonEmpty) match { - case Some(id) => Id.assertFromString(id) - case None => Default - } - - def fromString(identityProviderId: String): Either[String, IdentityProviderId] = - Some(identityProviderId).filter(_.nonEmpty) match { - case Some(id) => Id.fromString(id) - case None => Right(Default) - } - - def fromDb(identityProviderId: Option[IdentityProviderId.Id]): IdentityProviderId = - identityProviderId match { - case None => IdentityProviderId.Default - case Some(id) => id - } - - def fromOptionalLedgerString( - identityProviderId: Option[Ref.LedgerString] - ): IdentityProviderId = - identityProviderId match { - case None => IdentityProviderId.Default - case Some(id) => IdentityProviderId.Id(id) - } - } - - final case class IdentityProviderConfig( - identityProviderId: IdentityProviderId.Id, - isDeactivated: Boolean = false, - jwksUrl: JwksUrl, - issuer: String, - audience: Option[String], - ) - - final case class ObjectMeta( - resourceVersionO: Option[Long], - annotations: Map[String, String], - ) - - object ObjectMeta { - def empty: ObjectMeta = ObjectMeta( - resourceVersionO = None, - annotations = Map.empty, - ) - } - - final case class User( - id: Ref.UserId, - primaryParty: Option[Ref.Party], - isDeactivated: Boolean = false, - metadata: ObjectMeta = ObjectMeta.empty, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - primaryPartyAuthentication: Boolean = false, - ) { - // Note: this should be replaced by pretty printing once the ledger-api server packages move - // into their proper place - override def toString: String = - s"User(id=$id, primaryParty=$primaryParty, isDeactivated=$isDeactivated, metadata=${metadata.toString - .take(512)}, identityProviderId=${identityProviderId.toRequestString}, primaryPartyAuthentication=$primaryPartyAuthentication)" - } - - final case class PartyDetails( - party: Ref.Party, - isLocal: Boolean, - metadata: ObjectMeta, - identityProviderId: IdentityProviderId, - ) - - sealed abstract class UserRight extends Product with Serializable { - def getParty: Option[Ref.Party] = None - } - - sealed abstract class UserRightForParty(party: Ref.Party) extends UserRight { - override def getParty: Option[Ref.Party] = Some(party) - } - - object UserRight { - final case object ParticipantAdmin extends UserRight - - final case object IdentityProviderAdmin extends UserRight - - final case class CanActAs(party: Ref.Party) extends UserRightForParty(party) - - final case class CanReadAs(party: Ref.Party) extends UserRightForParty(party) - - final case object CanReadAsAnyParty extends UserRight - - final case class CanExecuteAs(party: Ref.Party) extends UserRightForParty(party) - - final case object CanExecuteAsAnyParty extends UserRight - } - - sealed abstract class Feature extends Product with Serializable - - object Feature { - case object UserManagement extends Feature - } - final case class UpdateFormat( - includeTransactions: Option[TransactionFormat], - includeReassignments: Option[EventFormat], - includeTopologyEvents: Option[TopologyFormat], - ) - - final case class TopologyFormat( - participantAuthorizationFormat: Option[ParticipantAuthorizationFormat] - ) - - // The list of parties for which the topology transactions should be sent. If None then all the parties that the - // participant can read as are denoted (wildcard party). - final case class ParticipantAuthorizationFormat(parties: Option[Set[Ref.Party]]) - - final case class TransactionFormat( - eventFormat: EventFormat, - transactionShape: TransactionShape, - ) - - sealed trait TransactionShape - object TransactionShape { - case object LedgerEffects extends TransactionShape - case object AcsDelta extends TransactionShape - - def toProto( - transactionShape: TransactionShape - ): com.daml.ledger.api.v2.transaction_filter.TransactionShape = - transactionShape match { - case TransactionShape.LedgerEffects => TRANSACTION_SHAPE_LEDGER_EFFECTS - case TransactionShape.AcsDelta => TRANSACTION_SHAPE_ACS_DELTA - } - } - - final case class EventFormat( - filtersByParty: immutable.Map[Ref.Party, CumulativeFilter], - filtersForAnyParty: Option[CumulativeFilter], - verbose: Boolean, - ) - - final case class InterfaceFilter( - interfaceTypeRef: Ref.NameTypeConRef, - includeView: Boolean, - includeCreatedEventBlob: Boolean, - ) - - final case class TemplateFilter( - templateTypeRef: Ref.NameTypeConRef, - includeCreatedEventBlob: Boolean, - ) - - final case class TemplateWildcardFilter( - includeCreatedEventBlob: Boolean - ) - - final case class CumulativeFilter( - templateFilters: immutable.Set[TemplateFilter], - interfaceFilters: immutable.Set[InterfaceFilter], - templateWildcardFilter: Option[TemplateWildcardFilter], - ) - - object CumulativeFilter { - def templateWildcardFilter(includeCreatedEventBlob: Boolean = false): CumulativeFilter = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set.empty, - templateWildcardFilter = - Some(TemplateWildcardFilter(includeCreatedEventBlob = includeCreatedEventBlob)), - ) - - } - - final case class Commands( - workflowId: Option[WorkflowId], - userId: Ref.UserId, - commandId: CommandId, - submissionId: Option[SubmissionId], - actAs: Set[Ref.Party], - readAs: Set[Ref.Party], - submittedAt: Timestamp, - deduplicationPeriod: DeduplicationPeriod, - commands: LfCommands, - disclosedContracts: ImmArray[DisclosedContract], - synchronizerId: Option[SynchronizerId], - packagePreferenceSet: Set[Ref.PackageId] = Set.empty, - // Used to indicate the package map against which package resolution was performed. - packageMap: Map[Ref.PackageId, (Ref.PackageName, Ref.PackageVersion)] = Map.empty, - prefetchKeys: Seq[ApiContractKey], - tapsMaxPasses: Option[PositiveInt], - ) extends PrettyPrinting { - - override protected def pretty: Pretty[Commands] = { - import com.digitalasset.canton.logging.pretty.PrettyInstances.* - prettyOfClass( - param("commandId", _.commandId.unwrap), - paramIfDefined("submissionId", _.submissionId.map(_.unwrap)), - param("userId", _.userId), - param("actAs", _.actAs), - paramIfNonEmpty("readAs", _.readAs), - param("submittedAt", _.submittedAt), - param("ledgerEffectiveTime", _.commands.ledgerEffectiveTime), - param("deduplicationPeriod", _.deduplicationPeriod), - paramIfDefined("workflowId", _.workflowId.filter(_ != commandId).map(_.unwrap)), - paramIfDefined("synchronizerId", _.synchronizerId), - paramIfNonEmpty("prefetchKeys", _.prefetchKeys.map(_.toString.unquoted)), - paramIfDefined("tapsMaxPasses", _.tapsMaxPasses.map(_.value)), - indicateOmittedFields, - ) - } - } - - object Commands { - - import Logging.* - - implicit val `Timestamp to LoggingValue`: ToLoggingValue[Timestamp] = - ToLoggingValue.ToStringToLoggingValue - - implicit val `Commands to LoggingValue`: ToLoggingValue[Commands] = commands => { - LoggingValue.Nested.fromEntries( - "workflowId" -> commands.workflowId, - "userId" -> commands.userId, - "submissionId" -> commands.submissionId, - "commandId" -> commands.commandId, - "actAs" -> commands.actAs, - "readAs" -> commands.readAs, - "submittedAt" -> commands.submittedAt, - "deduplicationPeriod" -> commands.deduplicationPeriod, - ) - } - } - - final case class DisclosedContract( - fatContractInstance: LfFatContractInst, - synchronizerIdO: Option[SynchronizerId], - ) extends PrettyPrinting { - override protected def pretty: Pretty[DisclosedContract] = { - import com.digitalasset.canton.logging.pretty.PrettyInstances.* - prettyOfClass( - param("contractId", _.fatContractInstance.contractId), - param("templateId", _.fatContractInstance.templateId), - paramIfDefined("synchronizerId", _.synchronizerIdO), - indicateOmittedFields, - ) - } - } - - // Wrapper used for ordering package ids by version - final case class PackageReference( - pkgId: LfPackageId, - version: LfPackageVersion, - packageName: LfPackageName, - ) extends PrettyPrinting { - - override protected def pretty: Pretty[PackageReference] = - prettyOfString(_ => show"pkg:$packageName:$version/$pkgId") - } - - object PackageReference { - implicit val packageReferenceOrdering: Ordering[PackageReference] = - (x: PackageReference, y: PackageReference) => - if (x.packageName != y.packageName) { - throw new RuntimeException( - s"Cannot compare package-ids with different package names: $x and $y" - ) - } else - Ordering[(LfPackageVersion, LfPackageId)] - .compare(x.version -> x.pkgId, y.version -> y.pkgId) - - implicit class PackageReferenceOps(val pkgId: LfPackageId) extends AnyVal { - def toPackageReference( - packageIdVersionMap: Map[Ref.PackageId, (Ref.PackageName, Ref.PackageVersion)] - ): Option[PackageReference] = - packageIdVersionMap.get(pkgId).map { case (packageName, packageVersion) => - PackageReference(pkgId, packageVersion, packageName) - } - - def unsafeToPackageReference( - packageIdVersionMap: Map[Ref.PackageId, (Ref.PackageName, Ref.PackageVersion)] - ): PackageReference = - toPackageReference(packageIdVersionMap).getOrElse { - throw new NoSuchElementException( - s"Package id $pkgId not found in packageIdVersionMap" - ) - } - } - } - - object Logging { - implicit def `tagged value to LoggingValue`[T: ToLoggingValue, Tag]: ToLoggingValue[T @@ Tag] = - value => value.unwrap - } - - final case class ListVettedPackagesOpts( - packageFilter: Option[PackageMetadataFilter], - topologyStateFilter: Option[TopologyStateFilter], - pageToken: PageToken, - pageSize: PositiveInt, - ) { - def toPackagePredicate(metadata: PackageMetadata): Ref.PackageId => Boolean = { - (pkgId: Ref.PackageId) => - packageFilter.forall(_.toPredicate(metadata)(pkgId)) - } - - def synchronizers: Option[NonEmpty[Set[SynchronizerId]]] = - topologyStateFilter - .flatMap(filter => NonEmpty.from(filter.synchronizerIds.toSet)) - - def participants: Set[TopoParticipantId] = - topologyStateFilter.map(_.participantIds.toSet).getOrElse(Set.empty) - } - - sealed trait PageToken { - def encode: String - - // Given a list of synchronizers, filter them down to synchronizers that - // exceed this page token, then sort by the lowest synchronizer ID. Return the - // participant ID that serves as a exclusive lower bound for this - // synchronizer. - def sortAndFilterSynchronizers( - synchronizerIds: Set[SynchronizerId], - participantIds: Set[TopoParticipantId], - ): Seq[(SynchronizerId, Option[TopoParticipantId], Option[NonEmpty[Set[TopoParticipantId]]])] = - synchronizerIds.toSeq - .flatMap((syncId: SynchronizerId) => - for { - exclusiveBound <- getParticipantBound(syncId) - participantsFilter <- - if (participantIds.isEmpty) - Some(None) - else - NonEmpty - .from( - participantIds - .filter(PageToken.exceedsParticipantBound(exclusiveBound, _)) - ) - .map(Some(_)) - } yield (syncId, exclusiveBound, participantsFilter) - ) - .sortBy(_._1)(SynchronizerId.orderingIdentifierThenNamespace) - - // Get the participant bound for a synchronizer ID. - // None => synchronizer is too low, no participants would exceed the page token - // Some(None) => any participant would exceed the page token - // Some(Some(id)) => only participants strictly greater than `id` would exceed the page token - def getParticipantBound(synchronizerId: SynchronizerId): Option[Option[TopoParticipantId]] - } - - final case class BoundedPageToken( - synchronizerBound: SynchronizerId, - participantBound: TopoParticipantId, - ) extends PageToken { - override def encode: String = { - val bytes = Base64.getUrlEncoder.encode( - ListVettedPackagesPageTokenPayload( - synchronizerId = synchronizerBound.uid.toProtoPrimitive, - participantId = participantBound.uid.toProtoPrimitive, - ).toByteArray - ) - new String(bytes, StandardCharsets.UTF_8) - } - - override def getParticipantBound( - synchronizerId: SynchronizerId - ): Option[Option[TopoParticipantId]] = - if (synchronizerId > synchronizerBound) - Some(None) - else if (synchronizerId == synchronizerBound) - Some(Some(participantBound)) - else - None - } - - final case object InitialPageToken extends PageToken { - override def encode: String = "" - override def getParticipantBound( - synchronizerId: SynchronizerId - ): Option[Option[TopoParticipantId]] = - Some(None) - } - - object PageToken { - implicit val orderingVettedPackages: Ordering[VettedPackages] = - SynchronizerId.orderingIdentifierThenNamespace - .on[VettedPackages](_.synchronizerId) - .orElse(TopoParticipantId.orderingIdentifierThenNamespace.on(_.participantId)) - - def exceedsParticipantBound(bound: Option[TopoParticipantId], id: TopoParticipantId) = - bound match { - case None => true - case Some(bound) => TopoParticipantId.orderingIdentifierThenNamespace.gt(id, bound) - } - - def invalidPageToken(suffix: String): ValueConversionError = - ValueConversionError( - error = s"Invalid page token for ListVettedPackagesRequest: $suffix", - field = "page_token", - ) - - def decode(raw: String): ParsingResult[PageToken] = - if (raw.isEmpty) { - Right(InitialPageToken) - } else { - val bytes = raw.getBytes(StandardCharsets.UTF_8) - for { - decodedBytes <- Try[Array[Byte]](Base64.getUrlDecoder.decode(bytes)).toEither.left - .map(_ => invalidPageToken("Failed base64 decoding")) - - tokenPayload <- Try[ListVettedPackagesPageTokenPayload] { - ListVettedPackagesPageTokenPayload.parseFrom(decodedBytes) - }.toEither.left - .map(_ => invalidPageToken("Failed proto decoding")) - - synchronizerId <- - UniqueIdentifier - .fromProtoPrimitive(tokenPayload.synchronizerId, "page_token") - .leftMap(uniqueIdentifierErr => - invalidPageToken( - s"Couldn't extract token's synchronizer ID: ${uniqueIdentifierErr.message}" - ) - ) - participantId <- - UniqueIdentifier - .fromProtoPrimitive(tokenPayload.participantId, "page_token") - .leftMap(uniqueIdentifierErr => - invalidPageToken( - s"Couldn't extract token's participant ID: ${uniqueIdentifierErr.message}" - ) - ) - } yield BoundedPageToken( - synchronizerBound = SynchronizerId(synchronizerId), - participantBound = TopoParticipantId(participantId), - ) - } - } - - object ListVettedPackagesOpts { - def fromProto( - req: package_service.ListVettedPackagesRequest, - serverPageSize: PositiveInt, - ): ParsingResult[ListVettedPackagesOpts] = - for { - packageMetadataFilter <- req.packageMetadataFilter.traverse(PackageMetadataFilter.fromProto) - topologyStateFilter <- req.topologyStateFilter.traverse(TopologyStateFilter.fromProto) - pageToken <- PageToken.decode(req.pageToken) - requestPageSize <- ProtoConverter.parseNonNegativeInt("page_size", req.pageSize) - _ <- EitherUtil.condUnit( - requestPageSize.value <= serverPageSize.value, - InvariantViolation( - "page_size", - s"Page size must not exceed the server's maximum of $serverPageSize", - ), - ) - pageSize = - if (requestPageSize.value == 0) - serverPageSize - else - PositiveInt.tryCreate(requestPageSize.value) - } yield ListVettedPackagesOpts( - packageMetadataFilter, - topologyStateFilter, - pageToken, - pageSize, - ) - } - - final case class PackageMetadataFilter( - packageIds: Seq[Ref.PackageId], - packageNamePrefixes: Seq[String], - ) { - def toProtoLAPI: package_service.PackageMetadataFilter = - package_service.PackageMetadataFilter( - packageIds.map(_.toString), - packageNamePrefixes, - ) - - def toPredicate(metadata: PackageMetadata): Ref.PackageId => Boolean = { - lazy val noFilters = packageIds.isEmpty && packageNamePrefixes.isEmpty - lazy val allPackageIds = packageIds.toSet - lazy val allNames = (for { - name <- metadata.packageNameMap.keys - if packageNamePrefixes.exists(name.toString.startsWith(_)) - } yield name).toSet - - { (targetPkgId: Ref.PackageId) => - lazy val matchesPkgId = allPackageIds.contains(targetPkgId) - lazy val matchesName = metadata.packageIdVersionMap.get(targetPkgId) match { - case Some((name, _)) => allNames.contains(name) - case None => false // package ID is not known on this participant - } - noFilters || matchesPkgId || matchesName - } - } - } - - object PackageMetadataFilter { - def fromProto( - filter: package_service.PackageMetadataFilter - ): ParsingResult[PackageMetadataFilter] = - filter.packageIds - .traverse( - Ref.PackageId.fromString(_).leftMap(ValueConversionError("package_ids", _)) - ) - .map(PackageMetadataFilter(_, filter.packageNamePrefixes)) - } - - final case class TopologyStateFilter( - participantIds: Seq[TopoParticipantId], - synchronizerIds: Seq[SynchronizerId], - ) { - def toProtoLAPI: package_service.TopologyStateFilter = - package_service.TopologyStateFilter( - participantIds.map(_.uid.toString), - synchronizerIds.map(_.uid.toString), - ) - } - - object TopologyStateFilter { - def fromProto( - filter: package_service.TopologyStateFilter - ): ParsingResult[TopologyStateFilter] = - for { - synchronizerIds <- filter.synchronizerIds.traverse( - UniqueIdentifier - .fromProtoPrimitive(_, "synchronizer_ids") - .map(SynchronizerId(_)) - ) - participantIds <- filter.participantIds.traverse( - UniqueIdentifier - .fromProtoPrimitive(_, "participant_ids") - .map(TopoParticipantId(_)) - ) - } yield TopologyStateFilter( - participantIds = participantIds, - synchronizerIds = synchronizerIds, - ) - } - - final case class UpdateVettedPackagesForceFlags( - forceVetIncompatibleUpgrade: Boolean = false, - forceUnvettedDependencies: Boolean = false, - ) { - def toForceFlags = - ForceFlags( - Set(ForceFlag.AllowVetIncompatibleUpgrades) - .filter(_ => forceVetIncompatibleUpgrade) ++ - Set(ForceFlag.AllowUnvettedDependencies) - .filter(_ => forceUnvettedDependencies) - ) - } - - object UpdateVettedPackagesForceFlags { - def fromProto( - forceFlags: Seq[package_management_service.UpdateVettedPackagesForceFlag] - ): ParsingResult[UpdateVettedPackagesForceFlags] = - Right( - UpdateVettedPackagesForceFlags( - forceVetIncompatibleUpgrade = - forceFlags.exists(_.isUpdateVettedPackagesForceFlagAllowVetIncompatibleUpgrades), - forceUnvettedDependencies = - forceFlags.exists(_.isUpdateVettedPackagesForceFlagAllowUnvettedDependencies), - ) - ) - - } - - final case class UpdateVettedPackagesOpts( - changes: Seq[VettedPackagesChange], - dryRun: Boolean, - synchronizerIdO: Option[SynchronizerId], - expectedTopologySerial: Option[PriorTopologySerial], - forceFlags: UpdateVettedPackagesForceFlags, - ) { - def toTargetStates: Seq[SinglePackageTargetVetting[VettedPackagesRef]] = - for { - change <- changes - ref <- change.packages - } yield change match { - case v: VettedPackagesChange.Vet => - SinglePackageTargetVetting(ref, Some((v.newValidFromInclusive, v.newValidUntilExclusive))) - case v: VettedPackagesChange.Unvet => SinglePackageTargetVetting(ref, None) - } - } - - object UpdateVettedPackagesOpts { - def fromProto( - req: package_management_service.UpdateVettedPackagesRequest - ): ParsingResult[UpdateVettedPackagesOpts] = for { - vettingChanges <- req.changes - .traverse(VettedPackagesChange.fromProto) - synchronizerIdO <- OptionUtil - .emptyStringAsNone(req.synchronizerId) - .traverse(SynchronizerId.fromProtoPrimitive(_, "synchronizer_id")) - expectedTopologySerial <- req.expectedTopologySerial - .flatTraverse(PriorTopologySerial.fromProto("expected_topology_serial", _)) - forceFlags <- UpdateVettedPackagesForceFlags.fromProto(req.updateVettedPackagesForceFlags) - } yield UpdateVettedPackagesOpts( - vettingChanges, - req.dryRun, - synchronizerIdO, - expectedTopologySerial, - forceFlags, - ) - } - - sealed trait VettedPackagesChange { - def packages: Seq[VettedPackagesRef] - } - - object VettedPackagesChange { - final case class Vet( - packages: Seq[VettedPackagesRef], - newValidFromInclusive: Option[CantonTimestamp], - newValidUntilExclusive: Option[CantonTimestamp], - ) extends VettedPackagesChange - - object Vet { - def fromProto( - change: package_management_service.VettedPackagesChange.Vet - ): ParsingResult[Vet] = - for { - packages <- change.packages.traverse(VettedPackagesRef.fromProto) - newValidFromInclusive <- change.newValidFromInclusive.traverse( - CantonTimestamp.fromProtoTimestamp - ) - newValidUntilExclusive <- change.newValidUntilExclusive.traverse( - CantonTimestamp.fromProtoTimestamp - ) - } yield Vet(packages, newValidFromInclusive, newValidUntilExclusive) - } - - final case class Unvet( - packages: Seq[VettedPackagesRef] - ) extends VettedPackagesChange - - object Unvet { - def fromProto( - change: package_management_service.VettedPackagesChange.Unvet - ): ParsingResult[Unvet] = - change.packages - .traverse(VettedPackagesRef.fromProto) - .map(Unvet(_)) - } - - def fromProto( - change: package_management_service.VettedPackagesChange - ): ParsingResult[VettedPackagesChange] = - change.operation match { - case package_management_service.VettedPackagesChange.Operation.Vet(vet) => - Vet.fromProto(vet) - case package_management_service.VettedPackagesChange.Operation.Unvet(unvet) => - Unvet.fromProto(unvet) - case package_management_service.VettedPackagesChange.Operation.Empty => - Left(FieldNotSet("operation")) - } - } - - trait UploadDarVettingChange { - def toProto: package_management_service.UploadDarFileRequest.VettingChange - } - object VetAllPackages extends UploadDarVettingChange { - override def toProto = - package_management_service.UploadDarFileRequest.VettingChange.VETTING_CHANGE_VET_ALL_PACKAGES - } - object DontVetAnyPackages extends UploadDarVettingChange { - override def toProto = - package_management_service.UploadDarFileRequest.VettingChange.VETTING_CHANGE_DONT_VET_ANY_PACKAGES - } - - object UploadDarVettingChange { - val default: UploadDarVettingChange = VetAllPackages - - def fromProto( - fieldName: String, - change: Option[package_management_service.UploadDarFileRequest.VettingChange], - ): ParsingResult[UploadDarVettingChange] = - change.map(fromProto(fieldName, _)).getOrElse(Right(VetAllPackages)) - - def fromProto( - fieldName: String, - change: package_management_service.UploadDarFileRequest.VettingChange, - ): ParsingResult[UploadDarVettingChange] = - change match { - case package_management_service.UploadDarFileRequest.VettingChange.VETTING_CHANGE_UNSPECIFIED => - Right(default) - - case package_management_service.UploadDarFileRequest.VettingChange.VETTING_CHANGE_VET_ALL_PACKAGES => - Right(VetAllPackages) - case package_management_service.UploadDarFileRequest.VettingChange.VETTING_CHANGE_DONT_VET_ANY_PACKAGES => - Right(DontVetAnyPackages) - case package_management_service.UploadDarFileRequest.VettingChange - .Unrecognized(unrecognizedValue) => - Left(UnrecognizedEnum(fieldName, unrecognizedValue)) - } - } - - sealed trait VettedPackagesRef extends PrettyPrinting { - def toProtoLAPI: package_management_service.VettedPackagesRef - def findMatchingPackages( - metadata: PackageMetadata - ): Either[String, NonEmpty[Set[Ref.PackageId]]] - } - - object VettedPackagesRef { - final case class Id( - id: Ref.PackageId - ) extends VettedPackagesRef { - def toProtoLAPI: package_management_service.VettedPackagesRef = - package_management_service.VettedPackagesRef(id.toString, "", "") - - def findMatchingPackages( - metadata: PackageMetadata - ): Either[String, NonEmpty[Set[Ref.PackageId]]] = - if (!metadata.packageIdVersionMap.contains(id)) { - Left(s"No packages with package ID $id") - } else { - Right(NonEmpty(Set, id)) - } - - override protected def pretty: Pretty[Id] = - prettyOfString(id => s"package-id: ${id.id.singleQuoted}") - } - - final case class NameAndVersion( - name: Ref.PackageName, - version: Ref.PackageVersion, - ) extends VettedPackagesRef { - def toProtoLAPI: package_management_service.VettedPackagesRef = - package_management_service.VettedPackagesRef( - "", - name.toString, - version.toString, - ) - - def findMatchingPackages( - metadata: PackageMetadata - ): Either[String, NonEmpty[Set[Ref.PackageId]]] = - metadata.packageNameMap.get(name) match { - case None => Left(s"Name $name did not match any packages.") - case Some(packageResolution) => - val matchingIds: Set[Ref.PackageId] = - packageResolution.allPackageIdsForName.toSet - .filter { matchingId => - val (_, matchingVersion) = metadata.packageIdVersionMap.getOrElse( - matchingId, - sys.error( - s"Unexpectedly missing package ID $matchingId from the package ID version map." - ), - ) - version == matchingVersion - } - NonEmpty.from(matchingIds) match { - case None => Left(s"No packages with name $name have version $version.") - case Some(ne) => Right(ne) - } - } - - override protected def pretty: Pretty[NameAndVersion] = prettyOfClass( - param("name", _.name), - param("version", _.version), - ) - } - - final case class All( - id: Ref.PackageId, - name: Ref.PackageName, - version: Ref.PackageVersion, - ) extends VettedPackagesRef { - def toProtoLAPI: package_management_service.VettedPackagesRef = - package_management_service.VettedPackagesRef( - id.toString, - name.toString, - version.toString, - ) - - def findMatchingPackages( - metadata: PackageMetadata - ): Either[String, NonEmpty[Set[Ref.PackageId]]] = - metadata.packageIdVersionMap.get(id) match { - case None => Left(s"No packages with package ID $id") - case Some((matchingName, matchingVersion)) => - if (name == matchingName && version == matchingVersion) { - Right(NonEmpty(Set, id)) - } else { - Left( - s"Package with package ID $id has name $matchingName and version $matchingVersion, but filter specifies name $name and version $version" - ) - } - } - - override protected def pretty: Pretty[All] = - prettyOfClass( - param("id", _.id), - param("name", _.name), - param("version", _.version), - ) - } - - final case class Name( - name: Ref.PackageName - ) extends VettedPackagesRef { - def toProtoLAPI: package_management_service.VettedPackagesRef = - package_management_service.VettedPackagesRef("", name.toString, "") - - def findMatchingPackages( - metadata: PackageMetadata - ): Either[String, NonEmpty[Set[Ref.PackageId]]] = - metadata.packageNameMap.get(name) match { - case None => Left(s"No packages with name $name") - case Some(packageResolution) => Right(packageResolution.allPackageIdsForName) - } - - override protected def pretty: Pretty[Name] = - prettyOfString(name => s"package-name: ${name.name.singleQuoted}") - } - - private def parseWith[A]( - name: String, - value: String, - f: String => Either[String, A], - ): ParsingResult[Option[A]] = - Some(value) - .filter(_.nonEmpty) - .traverse(f) - .leftMap(ValueConversionError(name, _)) - - private def process( - mbPackageId: Option[Ref.PackageId], - mbPackageName: Option[Ref.PackageName], - mbPackageVersion: Option[Ref.PackageVersion], - ): ParsingResult[VettedPackagesRef] = - (mbPackageId, mbPackageName, mbPackageVersion) match { - case (Some(id), Some(name), Some(version)) => Right(All(id, name, version)) - case (None, Some(name), Some(version)) => Right(NameAndVersion(name, version)) - case (Some(id), None, None) => Right(Id(id)) - case (None, Some(name), None) => Right(Name(name)) - case _ => - Left( - InvariantViolation( - "package_name", - "Either package_id must be set, or package_name and package_version must be set, or all three must be set.", - ) - ) - } - - def fromProto( - raw: package_management_service.VettedPackagesRef - ): ParsingResult[VettedPackagesRef] = - for { - mbPackageId <- parseWith("package_id", raw.packageId, Ref.PackageId.fromString) - mbPackageName <- parseWith("package_name", raw.packageName, Ref.PackageName.fromString) - mbPackageVersion <- parseWith( - "package_version", - raw.packageVersion, - Ref.PackageVersion.fromString, - ) - result <- process(mbPackageId, mbPackageName, mbPackageVersion) - } yield result - } - - final case class SinglePackageTargetVetting[R]( - ref: R, - bounds: Option[(Option[CantonTimestamp], Option[CantonTimestamp])], - ) { - def isVetting: Boolean = !isUnvetting - def isUnvetting: Boolean = bounds.isEmpty - } - - sealed trait VettedPackages { - def participantId: TopoParticipantId - def synchronizerId: SynchronizerId - def toBoundedPageToken: BoundedPageToken = BoundedPageToken(synchronizerId, participantId) - } - - final case class ParticipantVettedPackages( - packages: Seq[VettedPackage], - participantId: TopoParticipantId, - synchronizerId: SynchronizerId, - serial: PositiveInt, - ) extends VettedPackages - - final case class EnrichedVettedPackages( - packages: Seq[EnrichedVettedPackage], - participantId: TopoParticipantId, - synchronizerId: SynchronizerId, - serial: PositiveInt, - ) extends VettedPackages { - def toProtoLAPI: package_reference.VettedPackages = - package_reference.VettedPackages( - packages = packages.map(_.toProtoLAPI), - participantId = participantId.uid.toProtoPrimitive, - synchronizerId = synchronizerId.toProtoPrimitive, - topologySerial = serial.value, - ) - } - - final case class EnrichedVettedPackage( - vetted: VettedPackage, - name: Option[Ref.PackageName], - version: Option[Ref.PackageVersion], - ) { - def toProtoLAPI: package_reference.VettedPackage = package_reference.VettedPackage( - vetted.packageId, - validFromInclusive = vetted.validFromInclusive.map(_.toProtoTimestamp), - validUntilExclusive = vetted.validUntilExclusive.map(_.toProtoTimestamp), - packageName = name.map(_.toString).getOrElse(""), - packageVersion = version.map(_.toString).getOrElse(""), - ) - } - - sealed trait PriorTopologySerial - - object PriorTopologySerial { - def fromProto( - field: String, - proto: package_reference.PriorTopologySerial, - ): ParsingResult[Option[PriorTopologySerial]] = - proto.serial match { - case package_reference.PriorTopologySerial.Serial.Empty => Right(None) - case package_reference.PriorTopologySerial.Serial.NoPrior(_) => - Right(Some(PriorTopologySerialNone)) - case package_reference.PriorTopologySerial.Serial.Prior(serial) => - ProtoConverter - .parsePositiveInt(field, serial) - .map(serial => Some(PriorTopologySerialExists(serial))) - } - } - - final case class PriorTopologySerialExists(serial: PositiveInt) extends PriorTopologySerial - - case object PriorTopologySerialNone extends PriorTopologySerial - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/refinements/ApiTypes.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/refinements/ApiTypes.scala deleted file mode 100644 index 4c9b633818..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/refinements/ApiTypes.scala +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.refinements - -import com.daml.ledger.api.v2.value.Identifier -import scalaz.{@@, Tag} - -object ApiTypes { - - sealed trait UpdateIdTag - type UpdateId = String @@ UpdateIdTag - val UpdateId = Tag.of[UpdateIdTag] - - sealed trait CommandIdTag - type CommandId = String @@ CommandIdTag - val CommandId = Tag.of[CommandIdTag] - - sealed trait WorkflowIdTag - type WorkflowId = String @@ WorkflowIdTag - val WorkflowId = Tag.of[WorkflowIdTag] - - sealed trait TemplateIdTag - type TemplateId = Identifier @@ TemplateIdTag - val TemplateId = Tag.of[TemplateIdTag] - - sealed trait InterfaceIdTag - type InterfaceId = Identifier @@ InterfaceIdTag - val InterfaceId = Tag.of[InterfaceIdTag] - - sealed trait UserIdTag - type UserId = String @@ UserIdTag - val UserId = Tag.of[UserIdTag] - - sealed trait ContractIdTag - type ContractId = String @@ ContractIdTag - val ContractId = Tag.of[ContractIdTag] - - sealed trait ChoiceTag - type Choice = String @@ ChoiceTag - val Choice = Tag.of[ChoiceTag] - - sealed trait PartyTag - type Party = String @@ PartyTag - val Party = Tag.of[PartyTag] - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandInspectionService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandInspectionService.scala deleted file mode 100644 index fc85878efa..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandInspectionService.scala +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.services - -import com.daml.ledger.api.v2.admin.command_inspection_service.CommandState -import com.digitalasset.canton.platform.apiserver.execution.CommandStatus - -import scala.concurrent.Future - -trait CommandInspectionService { - def findCommandStatus( - commandIdPrefix: String, - state: CommandState, - limit: Int, - ): Future[Seq[CommandStatus]] - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandService.scala deleted file mode 100644 index 5b355d3725..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandService.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.services - -import com.daml.ledger.api.v2.command_service.{ - SubmitAndWaitForReassignmentRequest, - SubmitAndWaitForReassignmentResponse, - SubmitAndWaitForTransactionRequest, - SubmitAndWaitForTransactionResponse, - SubmitAndWaitRequest, - SubmitAndWaitResponse, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace - -import scala.concurrent.Future - -trait CommandService { - def submitAndWait(request: SubmitAndWaitRequest)( - loggingContext: LoggingContextWithTrace - ): Future[SubmitAndWaitResponse] - - def submitAndWaitForTransaction( - request: SubmitAndWaitForTransactionRequest - )(loggingContext: LoggingContextWithTrace): Future[SubmitAndWaitForTransactionResponse] - - def submitAndWaitForReassignment( - request: SubmitAndWaitForReassignmentRequest - )(loggingContext: LoggingContextWithTrace): Future[SubmitAndWaitForReassignmentResponse] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandSubmissionService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandSubmissionService.scala deleted file mode 100644 index ca1758c96d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/CommandSubmissionService.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.services - -import com.digitalasset.canton.ledger.api.messages.command.submission.SubmitRequest -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace - -trait CommandSubmissionService { - def submit( - request: SubmitRequest - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[Unit] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/InteractiveSubmissionService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/InteractiveSubmissionService.scala deleted file mode 100644 index 98d7b01f22..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/services/InteractiveSubmissionService.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.services - -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{ - ExecuteSubmissionAndWaitForTransactionResponse, - ExecuteSubmissionAndWaitResponse, - ExecuteSubmissionResponse, - PrepareSubmissionResponse, - PreparedTransaction, -} -import com.daml.ledger.api.v2.transaction_filter.TransactionFormat -import com.digitalasset.canton.LfTimestamp -import com.digitalasset.canton.crypto.Signature -import com.digitalasset.canton.data.{CantonTimestamp, DeduplicationPeriod} -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService.{ - ExecuteRequest, - PrepareRequest, -} -import com.digitalasset.canton.ledger.api.validation.GetPreferredPackagesRequestValidator.PackageVettingRequirements -import com.digitalasset.canton.ledger.api.{Commands, PackageReference} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.apiserver.services.command.interactive.CostEstimationHints -import com.digitalasset.canton.topology.{PartyId, PhysicalSynchronizerId, SynchronizerId} -import com.digitalasset.canton.version.HashingSchemeVersion -import com.digitalasset.daml.lf.data.Ref.{SubmissionId, UserId} - -object InteractiveSubmissionService { - final case class PrepareRequest( - commands: Commands, - verboseHashing: Boolean, - maxRecordTime: Option[LfTimestamp], - costEstimationHints: Option[CostEstimationHints], - hashingSchemeVersion: HashingSchemeVersion, - ) - - final case class ExecuteRequest( - userId: UserId, - submissionId: SubmissionId, - deduplicationPeriod: DeduplicationPeriod, - signatures: Map[PartyId, Seq[Signature]], - preparedTransaction: PreparedTransaction, - serializationVersion: HashingSchemeVersion, - tentativeLedgerEffectiveTime: LfTimestamp, - ) -} - -trait InteractiveSubmissionService { - def prepare(request: PrepareRequest)(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[PrepareSubmissionResponse] - - def execute(request: ExecuteRequest)(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[ExecuteSubmissionResponse] - - def executeAndWait(request: ExecuteRequest)(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[ExecuteSubmissionAndWaitResponse] - - def executeAndWaitForTransaction( - request: ExecuteRequest, - transactionFormat: Option[TransactionFormat], - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[ExecuteSubmissionAndWaitForTransactionResponse] - - def getPreferredPackages( - packageVettingRequirements: PackageVettingRequirements, - synchronizerId: Option[SynchronizerId], - vettingValidAt: Option[CantonTimestamp], - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[Either[String, (Seq[PackageReference], PhysicalSynchronizerId)]] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/DurationConversion.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/DurationConversion.scala deleted file mode 100644 index 8b3f9f4bf7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/DurationConversion.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.util - -import com.google.protobuf.duration.Duration as PDuration - -import java.time.Duration as JDuration - -object DurationConversion { - - def toProto(jDuration: JDuration): PDuration = PDuration(jDuration.getSeconds, jDuration.getNano) - - def fromProto(pDuration: PDuration): JDuration = - JDuration.ofSeconds(pDuration.seconds, pDuration.nanos.toLong) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/LfEngineToApi.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/LfEngineToApi.scala deleted file mode 100644 index e843effb7f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/LfEngineToApi.scala +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.util - -import com.daml.ledger.api.v2.value as api -import com.digitalasset.daml.lf.data.{Numeric, Ref} -import com.digitalasset.daml.lf.value.Value as Lf -import com.digitalasset.daml.lf.value.Value.ValueOptional -import com.google.protobuf.empty.Empty -import com.google.protobuf.timestamp.Timestamp -import scalaz.std.either.* -import scalaz.std.list.* -import scalaz.syntax.traverse.* - -import java.time.Instant - -/** Translates [[com.digitalasset.daml.lf.value.Value]] values to Ledger API values. - * - * All conversion functions are pure and total. - * - * Most conversion functions have a verbose flag: - * - If verbose mode is disabled, then all resulting Api values have missing type identifiers and - * record field names. - * - If verbose mode is enabled, then type identifiers and record field names are copied from the - * input Daml-LF values. The caller is responsible for filling in missing type information - * using [[com.digitalasset.daml.lf.engine.Enricher]], which may involve loading Daml-LF - * packages. - */ -object LfEngineToApi { - - private[this] type LfValue = Lf - - def toApiIdentifier(identifier: Ref.Identifier): api.Identifier = - api.Identifier( - identifier.packageId, - identifier.qualifiedName.module.toString(), - identifier.qualifiedName.name.toString(), - ) - - def toTimestamp(instant: Instant): Timestamp = - Timestamp.apply(instant.getEpochSecond, instant.getNano) - - def lfValueToApiRecord( - verbose: Boolean, - recordValue: LfValue, - ): Either[String, api.Record] = - recordValue match { - case recordLfValue: Lf.ValueRecord => - lfValueToApiValue(verbose, recordLfValue).map(_.getRecord) - case other => - Left(s"Expected value to be record, but got $other") - } - - def lfValueToApiValue( - verbose: Boolean, - lf: Option[LfValue], - ): Either[String, Option[api.Value]] = - lf.fold[Either[String, Option[api.Value]]](Right(None))( - lfValueToApiValue(verbose, _).map(Some(_)) - ) - - def lfValueToApiValue( - verbose: Boolean, - value0: LfValue, - ): Either[String, api.Value] = - value0 match { - case Lf.ValueUnit => Right(api.Value(api.Value.Sum.Unit(Empty()))) - case Lf.ValueNumeric(d) => - Right(api.Value(api.Value.Sum.Numeric(Numeric.toString(d)))) - case Lf.ValueContractId(c) => Right(api.Value(api.Value.Sum.ContractId(c.coid))) - case Lf.ValueBool(b) => Right(api.Value(api.Value.Sum.Bool(b))) - case Lf.ValueDate(d) => Right(api.Value(api.Value.Sum.Date(d.days))) - case Lf.ValueTimestamp(t) => Right(api.Value(api.Value.Sum.Timestamp(t.micros))) - case Lf.ValueInt64(i) => Right(api.Value(api.Value.Sum.Int64(i))) - case Lf.ValueParty(p) => Right(api.Value(api.Value.Sum.Party(p))) - case Lf.ValueText(t) => Right(api.Value(api.Value.Sum.Text(t))) - case Lf.ValueOptional(o) => // TODO(i12289) DEL-7054 add test coverage - o.fold[Either[String, api.Value]]( - Right(api.Value(api.Value.Sum.Optional(api.Optional.defaultInstance))) - )(v => - lfValueToApiValue(verbose, v).map(c => - api.Value(api.Value.Sum.Optional(api.Optional(Some(c)))) - ) - ) - case Lf.ValueTextMap(m) => - m.toImmArray.reverse - .foldLeft[Either[String, List[api.TextMap.Entry]]](Right(List.empty)) { - case (Right(list), (k, v)) => - lfValueToApiValue(verbose, v).map(w => api.TextMap.Entry(k, Some(w)) :: list) - case (left, _) => left - } - .map(list => api.Value(api.Value.Sum.TextMap(api.TextMap(list)))) - case Lf.ValueGenMap(entries) => - entries.reverseIterator - .foldLeft[Either[String, List[api.GenMap.Entry]]](Right(List.empty)) { - case (acc, (k, v)) => - for { - tail <- acc - key <- lfValueToApiValue(verbose, k) - value <- lfValueToApiValue(verbose, v) - } yield api.GenMap.Entry(Some(key), Some(value)) :: tail - } - .map(list => api.Value(api.Value.Sum.GenMap(api.GenMap(list)))) - case Lf.ValueList(vs) => - vs.toImmArray.toList.traverseU(lfValueToApiValue(verbose, _)) map { xs => - api.Value(api.Value.Sum.List(api.List(xs))) - } - case Lf.ValueVariant(tycon, variant, v) => - lfValueToApiValue(verbose, v) map { x => - api.Value( - api.Value.Sum.Variant( - api.Variant( - tycon.filter(_ => verbose).map(toApiIdentifier), - variant, - Some(x), - ) - ) - ) - } - case Lf.ValueEnum(tyCon, value) => - Right( - api.Value( - api.Value.Sum.Enum( - api.Enum( - tyCon.filter(_ => verbose).map(toApiIdentifier), - value, - ) - ) - ) - ) - case Lf.ValueRecord(tycon, fields) => - val trailingNonesSize = fields.reverseIterator.takeWhile { - case (_, ValueOptional(None)) => true - case _ => false - }.size - - fields.iterator - // Since Canton 3.3, trailing None Optionals are omitted in LF normalization. - // For consistency, omit trailing None Optionals on all value reads - // to ensure that pre-Canton 3.3 values are normalized as well - .take(fields.length - trailingNonesSize) - .toList - .traverseU { case (mbLabel, value) => - lfValueToApiValue(verbose, value) map { x => - api.RecordField( - label = if (verbose) mbLabel.getOrElse("") else "", - value = Some(x), - ) - } - } - .map { apiFields => - api.Value( - api.Value.Sum.Record( - api.Record( - if (verbose) - tycon.map(toApiIdentifier) - else - None, - apiFields, - ) - ) - ) - } - } - - @throws[RuntimeException] - def assertOrRuntimeEx[A](failureContext: String, ea: Either[String, A]): A = - ea.fold(e => throw new RuntimeException(s"Unexpected error when $failureContext: $e"), identity) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/PageTokenUtils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/PageTokenUtils.scala deleted file mode 100644 index 7341ecca2f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/PageTokenUtils.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.util - -import com.digitalasset.canton.crypto.{Hash, HashAlgorithm, HashPurpose} -import com.google.protobuf.ByteString - -object PageTokenUtils { - - /** Compute a 4-byte participant ID checksum for use in page tokens. */ - def calcParticipantChecksum(purpose: HashPurpose, participantId: String): ByteString = - toChecksum( - Hash - .build(purpose, HashAlgorithm.Sha256) - .addString(participantId) - .finish() - ) - - def toChecksum(hash: Hash): ByteString = hash.unwrap.substring(0, 4) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimeProvider.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimeProvider.scala deleted file mode 100644 index 493b93468f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimeProvider.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.util - -import com.digitalasset.canton.ledger.api.util.TimeProvider.MappedTimeProvider - -import java.time.{Clock, Instant} - -trait TimeProvider { self => - - def getCurrentTime: Instant - - def map(transform: Instant => Instant): TimeProvider = MappedTimeProvider(this, transform) -} - -object TimeProvider { - final case class MappedTimeProvider(timeProvider: TimeProvider, transform: Instant => Instant) - extends TimeProvider { - override def getCurrentTime: Instant = transform(timeProvider.getCurrentTime) - } - - final case class Constant(getCurrentTime: Instant) extends TimeProvider - - case object UTC extends TimeProvider { - - private val utcClock = Clock.systemUTC() - - override def getCurrentTime: Instant = utcClock.instant() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimeProviderType.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimeProviderType.scala deleted file mode 100644 index 682194fb60..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimeProviderType.scala +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.util - -sealed abstract class TimeProviderType extends Product with Serializable { - def description: String -} - -object TimeProviderType { - - case object Static extends TimeProviderType { - override lazy val description: String = "static time" - } - - case object WallClock extends TimeProviderType { - override lazy val description: String = "wall-clock time" - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimestampConversion.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimestampConversion.scala deleted file mode 100644 index 16d4ba4b44..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/TimestampConversion.scala +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.util - -import com.daml.ledger.api.v2.value.Value -import com.digitalasset.daml.lf.data.Time.Timestamp as LfTimestamp -import com.google.protobuf.timestamp.Timestamp as ProtoTimestamp - -import java.time.Instant -import java.util.concurrent.TimeUnit - -object TimestampConversion { - val MIN = Instant parse "0001-01-01T00:00:00Z" - val MAX = Instant parse "9999-12-31T23:59:59.999999Z" - - def microsToInstant(micros: Value.Sum.Timestamp): Instant = { - val seconds = TimeUnit.MICROSECONDS.toSeconds(micros.value) - val deltaMicros = micros.value - TimeUnit.SECONDS.toMicros(seconds) - Instant.ofEpochSecond(seconds, TimeUnit.MICROSECONDS.toNanos(deltaMicros)) - } - - def instantToMicros(t: Instant): Value.Sum.Timestamp = - if (t.getNano % 1000 != 0) - throw new IllegalArgumentException( - s"Conversion of Instant $t to microsecond granularity would result in loss of precision." - ) - else - Value.Sum.Timestamp( - TimeUnit.SECONDS.toMicros(t.getEpochSecond) + TimeUnit.NANOSECONDS - .toMicros(t.getNano.toLong) - ) - - def roundInstantToMicros(t: Instant): Value.Sum.Timestamp = - instantToMicros(roundToMicros(t, ConversionMode.HalfUp)) - - def toInstant(protoTimestamp: ProtoTimestamp): Instant = - Instant.ofEpochSecond(protoTimestamp.seconds, protoTimestamp.nanos.toLong) - - def fromInstant(instant: Instant): ProtoTimestamp = - new ProtoTimestamp().withSeconds(instant.getEpochSecond).withNanos(instant.getNano) - - def toLf(protoTimestamp: ProtoTimestamp, mode: ConversionMode): LfTimestamp = { - val instant = roundToMicros(toInstant(protoTimestamp), mode) - LfTimestamp.assertFromInstant(instant) - } - - def fromLf(timestamp: LfTimestamp): ProtoTimestamp = - fromInstant(timestamp.toInstant) - - private def roundToMicros(t: Instant, mode: ConversionMode): Instant = { - val fractionNanos = t.getNano % 1000L - if (fractionNanos != 0) { - mode match { - case ConversionMode.Exact => - throw new IllegalArgumentException( - s"Conversion of $t to microsecond granularity would result in loss of precision." - ) - case ConversionMode.HalfUp => - t.plusNanos(if (fractionNanos >= 500L) 1000L - fractionNanos else -fractionNanos) - } - } else { - t - } - } - - sealed trait ConversionMode - object ConversionMode { - - /** Throw an exception if the input can not be represented in microsecond resolution */ - case object Exact extends ConversionMode - - /** Round to the nearest microsecond */ - case object HalfUp extends ConversionMode - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/UpdateFormatHashUtils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/UpdateFormatHashUtils.scala deleted file mode 100644 index 0a02169bd7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/util/UpdateFormatHashUtils.scala +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.util - -import com.daml.ledger.api.v2.transaction_filter.{ - EventFormat, - ParticipantAuthorizationTopologyFormat, - TopologyFormat, - TransactionFormat, - UpdateFormat, -} -import com.digitalasset.canton.crypto.HashBuilder - -object UpdateFormatHashUtils { - - @SuppressWarnings(Array("com.digitalasset.canton.ProtobufToByteString")) - def hashEventFormat[T <: HashBuilder]( - builder: T - )(eventFormat: EventFormat): T = - // since EventFormat contains a Map, we need to be careful with calculating the hash - builder - .addMap(eventFormat.filtersByParty)((hashBuilder, key) => hashBuilder.addString(key))( - (hashBuilder, value) => hashBuilder.addByteString(value.toByteString) - ) - .addOptional(eventFormat.filtersForAnyParty.map(_.toByteString), _.addByteString) - .addBool(eventFormat.verbose) - - def hashTopologyFormat[T <: HashBuilder]( - hashBuilder: T - )(topologyFormat: TopologyFormat): T = - hashBuilder.addOptional( - topologyFormat.includeParticipantAuthorizationEvents, - hashParticipangAuthorizationFormat, - ) - - private def hashParticipangAuthorizationFormat[T <: HashBuilder](hashBuilder: T)( - participantAuthorizationFormat: ParticipantAuthorizationTopologyFormat - ): T = participantAuthorizationFormat.parties.foldLeft(hashBuilder)(_.addString(_)) - - def hashTransactionFormat[T <: HashBuilder](hashBuilder: T)( - transactionFormat: TransactionFormat - ): T = - hashBuilder - .addOptional(transactionFormat.eventFormat, hashEventFormat) - .addInt(transactionFormat.transactionShape.value) - - def hashUpdateFormat[T <: HashBuilder](hashBuilder: T)(uf: UpdateFormat): T = - hashBuilder - .addOptional(uf.includeReassignments, hashEventFormat) - .addOptional( - uf.includeTopologyEvents, - hashTopologyFormat, - ) - .addOptional( - uf.includeTransactions, - hashTransactionFormat, - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CommandInspectionServiceRequestValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CommandInspectionServiceRequestValidator.scala deleted file mode 100644 index 93ad909ac4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CommandInspectionServiceRequestValidator.scala +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.admin.command_inspection_service.GetCommandStatusRequest -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.invalidField -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.data.Ref -import io.grpc.StatusRuntimeException - -object CommandInspectionServiceRequestValidator { - def validateCommandStatusRequest( - request: GetCommandStatusRequest - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, GetCommandStatusRequest] = - if (request.commandIdPrefix.isEmpty) Right(request) - else - Ref.CommandId - .fromString(request.commandIdPrefix) - .map(_ => request) - .left - .map(invalidField("command_id_prefix", _)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CommandsValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CommandsValidator.scala deleted file mode 100644 index 5809d217c8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CommandsValidator.scala +++ /dev/null @@ -1,495 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.syntax.traverse.* -import com.daml.ledger.api.v2.commands.Command.Command.{ - Create as ProtoCreate, - CreateAndExercise as ProtoCreateAndExercise, - Empty as ProtoEmpty, - Exercise as ProtoExercise, - ExerciseByKey as ProtoExerciseByKey, -} -import com.daml.ledger.api.v2.commands.{Command, Commands as ProtoCommands, PrefetchContractKey} -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{ - ExecuteSubmissionRequest, - PrepareSubmissionRequest, -} -import com.daml.ledger.api.v2.reassignment_commands.{ReassignmentCommand, ReassignmentCommands} -import com.digitalasset.canton.LfTimestamp -import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.data.{DeduplicationPeriod, Offset} -import com.digitalasset.canton.ledger.api.messages.command.submission -import com.digitalasset.canton.ledger.api.util.{DurationConversion, TimestampConversion} -import com.digitalasset.canton.ledger.api.validation.CommandsValidator.{ - Submitters, - effectiveSubmitters, -} -import com.digitalasset.canton.ledger.api.{CommandId, Commands} -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.protocol.ReassignmentId -import com.digitalasset.canton.util.OptionUtil -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import com.digitalasset.daml.lf.command.* -import com.digitalasset.daml.lf.data.* -import com.digitalasset.daml.lf.value.Value as Lf -import com.google.protobuf.duration.Duration as DurationP -import com.google.protobuf.timestamp.Timestamp -import io.grpc.StatusRuntimeException -import io.scalaland.chimney.dsl.* -import scalaz.syntax.tag.* - -import java.time.{Duration, Instant} -import scala.Ordering.Implicits.infixOrderingOps -import scala.collection.immutable - -final class CommandsValidator( - validateUpgradingPackageResolutions: ValidateUpgradingPackageResolutions, - validateDisclosedContracts: ValidateDisclosedContracts = ValidateDisclosedContracts, - topologyAwarePackageSelectionEnabled: Boolean = false, -) { - - import FieldValidator.* - import ValidationErrors.* - import ValueValidator.* - - def validatePrepareRequest( - prepareRequest: PrepareSubmissionRequest, - currentLedgerTime: Instant, - currentUtcTime: Instant, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Commands] = - for { - userId <- requireUserId(prepareRequest.userId, "user_id") - commandId <- requireLedgerString(prepareRequest.commandId, "command_id").map( - CommandId(_) - ) - submitters <- validateSubmitters(effectiveSubmitters(prepareRequest)) - synchronizerIdO <- optionalSynchronizerId(prepareRequest.synchronizerId, "synchronizer_id") - commandz <- requireNonEmpty(prepareRequest.commands, "commands") - validatedCommands <- validateInnerCommands(commandz) - ledgerEffectiveTimestamp <- validateLedgerTime( - currentLedgerTime, - prepareRequest.minLedgerTime.flatMap(_.time.minLedgerTimeAbs), - prepareRequest.minLedgerTime.flatMap(_.time.minLedgerTimeRel), - ) - validatedDisclosedContracts <- validateDisclosedContracts.validateDisclosedContracts( - prepareRequest.disclosedContracts - ) - packageResolutions <- validateUpgradingPackageResolutions( - prepareRequest.packageIdSelectionPreference - ) - prefetchKeys <- validatePrefetchContractKeys(prepareRequest.prefetchContractKeys) - tapsMaxPasses <- validateTapsMaxPasses(prepareRequest.tapsMaxPasses) - } yield Commands( - // Not used for external submissions - workflowId = None, - userId = userId, - commandId = commandId, - // Will be provided in "execute" - submissionId = None, - actAs = submitters.actAs, - readAs = submitters.readAs, - submittedAt = Time.Timestamp.assertFromInstant(currentUtcTime), - // Unused for transaction preparation - deduplicationPeriod = DeduplicationPeriod.DeduplicationDuration(Duration.ZERO), - commands = ApiCommands( - commands = validatedCommands.to(ImmArray), - ledgerEffectiveTime = ledgerEffectiveTimestamp, - commandsReference = "", - ), - disclosedContracts = validatedDisclosedContracts, - synchronizerId = synchronizerIdO, - packageMap = packageResolutions.packageMap, - packagePreferenceSet = - if (topologyAwarePackageSelectionEnabled) { - // TODO(#25385): move the decision point into the TopologyAwareCommandExecutor - prepareRequest.packageIdSelectionPreference.map(Ref.PackageId.assertFromString).toSet - } else - packageResolutions.packagePreferenceSet, - prefetchKeys = prefetchKeys, - tapsMaxPasses = tapsMaxPasses, - ) - - def validateCommands( - commands: ProtoCommands, - currentLedgerTime: Instant, - currentUtcTime: Instant, - maxDeduplicationDuration: Duration, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Commands] = - for { - workflowId <- validateWorkflowId(commands.workflowId) - userId <- requireUserId(commands.userId, "user_id") - commandId <- requireLedgerString(commands.commandId, "command_id").map(CommandId(_)) - submissionId <- validateSubmissionId(commands.submissionId) - submitters <- validateSubmitters(effectiveSubmitters(commands)) - synchronizerId <- validateOptional(OptionUtil.emptyStringAsNone(commands.synchronizerId))( - requireSynchronizerId(_, "synchronizer_id") - ) - commandz <- requireNonEmpty(commands.commands, "commands") - validatedCommands <- validateInnerCommands(commandz) - ledgerEffectiveTimestamp <- validateLedgerTime( - currentLedgerTime, - commands.minLedgerTimeAbs, - commands.minLedgerTimeRel, - ) - deduplicationPeriod <- validateDeduplicationPeriod( - commands.deduplicationPeriod, - maxDeduplicationDuration, - ) - validatedDisclosedContracts <- validateDisclosedContracts.validateCommands(commands) - packageResolutions <- validateUpgradingPackageResolutions( - commands.packageIdSelectionPreference - ) - prefetchKeys <- validatePrefetchContractKeys(commands.prefetchContractKeys) - tapsMaxPasses <- validateTapsMaxPasses(commands.tapsMaxPasses) - } yield Commands( - workflowId = workflowId, - userId = userId, - commandId = commandId, - submissionId = submissionId, - actAs = submitters.actAs, - readAs = submitters.readAs, - submittedAt = Time.Timestamp.assertFromInstant(currentUtcTime), - deduplicationPeriod = deduplicationPeriod, - commands = ApiCommands( - commands = validatedCommands.to(ImmArray), - ledgerEffectiveTime = ledgerEffectiveTimestamp, - commandsReference = workflowId.fold("")(_.unwrap), - ), - disclosedContracts = validatedDisclosedContracts, - synchronizerId = synchronizerId, - packageMap = packageResolutions.packageMap, - packagePreferenceSet = - if (topologyAwarePackageSelectionEnabled) { - // TODO(#25385): move the decision point into the TopologyAwareCommandExecutor - commands.packageIdSelectionPreference.map(Ref.PackageId.assertFromString).toSet - } else - packageResolutions.packagePreferenceSet, - prefetchKeys = prefetchKeys, - tapsMaxPasses = tapsMaxPasses, - ) - - def validateReassignmentCommands( - reassignmentCommands: ReassignmentCommands - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, submission.SubmitReassignmentRequest] = - for { - submitter <- requirePartyField(reassignmentCommands.submitter, "submitter") - userId <- requireUserId(reassignmentCommands.userId, "user_id") - commandId <- requireCommandId(reassignmentCommands.commandId, "command_id") - submissionId <- requireSubmissionId(reassignmentCommands.submissionId, "submission_id") - workflowId <- validateOptional(Some(reassignmentCommands.workflowId).filter(_.nonEmpty))( - requireWorkflowId(_, "workflow_id") - ) - reassignmentCommands <- reassignmentCommands.commands.traverse { - _.command match { - case ReassignmentCommand.Command.Empty => - Left(ValidationErrors.missingField("command")) - case assignCommand: ReassignmentCommand.Command.AssignCommand => - for { - sourceSynchronizerId <- requireSynchronizerId(assignCommand.value.source, "source") - targetSynchronizerId <- requireSynchronizerId(assignCommand.value.target, "target") - reassignmentId <- ReassignmentId - .fromProtoPrimitive(assignCommand.value.reassignmentId) - .left - .map(_ => - ValidationErrors.invalidField("reassignment_id", "Invalid reassignment ID") - ) - } yield Left( - submission.AssignCommand( - sourceSynchronizerId = Source(sourceSynchronizerId), - targetSynchronizerId = Target(targetSynchronizerId), - reassignmentId = reassignmentId, - ) - ) - case unassignCommand: ReassignmentCommand.Command.UnassignCommand => - for { - sourceSynchronizerId <- requireSynchronizerId(unassignCommand.value.source, "source") - targetSynchronizerId <- requireSynchronizerId(unassignCommand.value.target, "target") - cid <- requireContractId(unassignCommand.value.contractId, "contract_id") - } yield Right( - submission.UnassignCommand( - sourceSynchronizerId = Source(sourceSynchronizerId), - targetSynchronizerId = Target(targetSynchronizerId), - contractId = cid, - ) - ) - } - } - } yield submission.SubmitReassignmentRequest( - submitter = submitter, - userId = userId, - commandId = commandId, - submissionId = submissionId, - workflowId = workflowId, - reassignmentCommands = reassignmentCommands, - ) - - def validateLedgerTime( - currentTime: Instant, - minLedgerTimeAbs: Option[Timestamp], - minLedgerTimeRel: Option[DurationP], - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Time.Timestamp] = - for { - ledgerEffectiveTime <- (minLedgerTimeAbs, minLedgerTimeRel) match { - case (None, None) => Right(currentTime) - case (Some(minAbs), None) => - Right(currentTime.max(TimestampConversion.toInstant(minAbs))) - case (None, Some(minRel)) => Right(currentTime.plus(DurationConversion.fromProto(minRel))) - case (Some(_), Some(_)) => - Left( - invalidArgument( - "min_ledger_time_abs cannot be specified at the same time as min_ledger_time_rel" - ) - ) - } - ledgerEffectiveTimestamp <- Time.Timestamp - .fromInstant(ledgerEffectiveTime) - .left - .map(_ => - invalidArgument( - s"Can not represent command ledger time $ledgerEffectiveTime as a Daml timestamp" - ) - ) - - } yield ledgerEffectiveTimestamp - - def validateLfTime(protoTimestamp: com.google.protobuf.timestamp.Timestamp)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, LfTimestamp] = - LfTimestamp - .fromInstant(TimestampConversion.toInstant(protoTimestamp)) - .left - .map(_ => - invalidArgument( - s"Can not represent ledger time $protoTimestamp as a Daml timestamp" - ) - ) - - // Public because it is used by Canton. - def validateInnerCommands( - commands: Seq[Command] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, immutable.Seq[ApiCommand]] = - commands.traverse(command => validateInnerCommand(command.command)) - - // Public so that clients have an easy way to convert ProtoCommand.Command to ApiCommand. - def validateInnerCommand( - command: Command.Command - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ApiCommand] = - command match { - case c: ProtoCreate => - for { - templateId <- requirePresence(c.value.templateId, "template_id") - typeConRef <- validateTypeConRef(templateId) - createArguments <- requirePresence(c.value.createArguments, "create_arguments") - recordId <- createArguments.recordId.traverse(validateIdentifier) - validatedRecordField <- validateRecordFields(createArguments.fields) - } yield ApiCommand.Create( - templateRef = typeConRef, - argument = Lf.ValueRecord(recordId, validatedRecordField), - ) - - case e: ProtoExercise => - for { - templateId <- requirePresence(e.value.templateId, "template_id") - templateRef <- validateTypeConRef(templateId) - contractId <- requireContractId(e.value.contractId, "contract_id") - choice <- requireName(e.value.choice, "choice") - value <- requirePresence(e.value.choiceArgument, "value") - validatedValue <- validateValue(value) - } yield ApiCommand.Exercise( - typeRef = templateRef, - contractId = contractId, - choiceId = choice, - argument = validatedValue, - ) - - case ek: ProtoExerciseByKey => - for { - templateId <- requirePresence(ek.value.templateId, "template_id") - templateRef <- validateTypeConRef(templateId) - contractKey <- requirePresence(ek.value.contractKey, "contract_key") - validatedContractKey <- validateValue(contractKey) - choice <- requireName(ek.value.choice, "choice") - value <- requirePresence(ek.value.choiceArgument, "value") - validatedValue <- validateValue(value) - } yield ApiCommand.ExerciseByKey( - templateRef = templateRef, - contractKey = validatedContractKey, - choiceId = choice, - argument = validatedValue, - ) - - case ce: ProtoCreateAndExercise => - for { - templateId <- requirePresence(ce.value.templateId, "template_id") - templateRef <- validateTypeConRef(templateId) - createArguments <- requirePresence(ce.value.createArguments, "create_arguments") - recordId <- createArguments.recordId.traverse(validateIdentifier) - validatedRecordField <- validateRecordFields(createArguments.fields) - choice <- requireName(ce.value.choice, "choice") - value <- requirePresence(ce.value.choiceArgument, "value") - validatedChoiceArgument <- validateValue(value) - } yield ApiCommand.CreateAndExercise( - templateRef = templateRef, - createArgument = Lf.ValueRecord(recordId, validatedRecordField), - choiceId = choice, - choiceArgument = validatedChoiceArgument, - ) - case ProtoEmpty => - Left(missingField("command")) - } - - private def validateSubmitters( - submitters: Submitters[String] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Submitters[Ref.Party]] = { - def actAsMustNotBeEmpty(effectiveActAs: Set[Ref.Party]) = - Either.cond( - effectiveActAs.nonEmpty, - (), - missingField("party or act_as"), - ) - - for { - actAs <- requireParties(submitters.actAs) - readAs <- requireParties(submitters.readAs) - _ <- actAsMustNotBeEmpty(actAs) - } yield Submitters(actAs, readAs) - } - - /** Same as [[validateDeduplicationPeriod]] but for the "ExecuteSubmissionRequest" RPC of the - * interactive submission service. - */ - def validateExecuteDeduplicationPeriod( - deduplicationPeriod: ExecuteSubmissionRequest.DeduplicationPeriod, - maxDeduplicationDuration: Duration, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, DeduplicationPeriod] = - validateDeduplicationPeriod( - deduplicationPeriod.transformInto[ProtoCommands.DeduplicationPeriod], - maxDeduplicationDuration, - ) - - /** We validate only using current time because we set the currentTime as submitTime so no need to - * check both - */ - def validateDeduplicationPeriod( - deduplicationPeriod: ProtoCommands.DeduplicationPeriod, - maxDeduplicationDuration: Duration, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, DeduplicationPeriod] = - deduplicationPeriod match { - case ProtoCommands.DeduplicationPeriod.Empty => - Right(DeduplicationPeriod.DeduplicationDuration(maxDeduplicationDuration)) - case ProtoCommands.DeduplicationPeriod.DeduplicationDuration(duration) => - val deduplicationDuration = DurationConversion.fromProto(duration) - DeduplicationPeriodValidator - .validateNonNegativeDuration(deduplicationDuration) - .map(DeduplicationPeriod.DeduplicationDuration.apply) - case ProtoCommands.DeduplicationPeriod.DeduplicationOffset(offset) => - if (offset < 0L) - Left( - RequestValidationErrors.NegativeOffset - .Error( - fieldName = "deduplication_period", - offsetValue = offset, - message = - s"the deduplication offset has to be a non-negative integer and not $offset", - ) - .asGrpcError - ) - else - Right( - DeduplicationPeriod.DeduplicationOffset( - Offset.tryOffsetOrParticipantBegin(offset) - ) - ) - } - - private def validatePrefetchContractKeys( - keys: Seq[PrefetchContractKey] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Seq[ApiContractKey]] = - keys.traverse(validatePrefetchContractKey) - - private def validatePrefetchContractKey( - key: PrefetchContractKey - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ApiContractKey] = { - val PrefetchContractKey(templateIdO, contractKeyO, limitOpt) = key - for { - templateId <- requirePresence(templateIdO, "template_id") - templateRef <- validateTypeConRef(templateId) - contractKey <- requirePresence(contractKeyO, "contract_key") - validatedKey <- validateValue(contractKey) - validatedLimit <- limitOpt match { - case None => Right(1) - case Some(limit) if limit > 0 => Right(limit) - case Some(0) => Left(invalidArgument("limit must be a positive integer, but got 0")) - case Some(_) => Right(Int.MaxValue) // limit is a proto uint32 capped by (1<<31) - 1 - } - } yield ApiContractKey(templateRef, validatedKey, validatedLimit) - } - - private def validateTapsMaxPasses( - tapsMaxPasses: Option[Int] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[PositiveInt]] = - tapsMaxPasses.traverse(tapsMaxPasses => - PositiveInt - .create(tapsMaxPasses) - .left - .map(_ => - invalidArgument(s"taps_max_passes must be strictly positive, but got $tapsMaxPasses.") - ) - ) -} - -object CommandsValidator { - - /** Effective submitters of a command - * @param actAs - * Guaranteed to be non-empty. Will contain exactly one element in most cases. - * @param readAs - * May be empty. - */ - final case class Submitters[T](actAs: Set[T], readAs: Set[T]) - - def effectiveSubmitters(commands: Option[ProtoCommands]): Submitters[String] = - commands.fold(noSubmitters)(effectiveSubmitters) - - def effectiveSubmitters(prepareRequest: PrepareSubmissionRequest): Submitters[String] = { - val actAs = prepareRequest.actAs.toSet - val readAs = prepareRequest.readAs.toSet -- actAs - Submitters(actAs, readAs) - } - - def effectiveSubmitters(commands: ProtoCommands): Submitters[String] = { - val actAs = commands.actAs.toSet - val readAs = commands.readAs.toSet -- actAs - Submitters(actAs, readAs) - } - - val noSubmitters: Submitters[String] = Submitters(Set.empty, Set.empty) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CompletionServiceRequestValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CompletionServiceRequestValidator.scala deleted file mode 100644 index f5e31bc208..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CompletionServiceRequestValidator.scala +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamRequest as GrpcCompletionStreamRequest -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.messages.command.completion.CompletionStreamRequest -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException - -object CompletionServiceRequestValidator { - - import FieldValidator.* - - def validateGrpcCompletionStreamRequest( - request: GrpcCompletionStreamRequest - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, CompletionStreamRequest] = - for { - userId <- requireUserId(request.userId, "user_id") - parties <- requireParties(request.parties.toSet) - offsetO <- ParticipantOffsetValidator.validateNonNegative( - request.beginExclusive, - "begin_exclusive", - ) - } yield CompletionStreamRequest( - userId, - parties, - offsetO, - ) - - def validateCompletionStreamRequest( - request: CompletionStreamRequest, - ledgerEnd: Option[Offset], - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, CompletionStreamRequest] = - for { - _ <- ParticipantOffsetValidator.offsetIsBeforeEnd( - "Begin", - request.offset, - ledgerEnd, - ) - _ <- requireNonEmpty(request.parties, "parties") - } yield request - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CryptoValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CryptoValidator.scala deleted file mode 100644 index 7ada1b24e9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/CryptoValidator.scala +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.syntax.either.* -import com.daml.ledger.api.v2.crypto -import com.daml.ledger.api.v2.crypto.{ - Signature as LAPISignature, - SignatureFormat as LAPISignatureFormat, -} -import com.digitalasset.canton.crypto.{ - Fingerprint, - Signature, - SignatureFormat, - SigningAlgorithmSpec, -} -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.invalidField -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException - -import scala.annotation.nowarn - -object CryptoValidator { - - def validateSignature( - cryptoSignatureP: crypto.Signature, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Signature] = { - val LAPISignature(formatP, signatureP, signedByP, signingAlgorithmSpecP) = - cryptoSignatureP - for { - format <- validateSignatureFormat(formatP, "format") - signature = signatureP - signedBy <- Fingerprint - .fromProtoPrimitive(signedByP) - .leftMap(err => invalidField(fieldName = fieldName, message = err.message)) - signingAlgorithmSpec <- validateSigningAlgorithmSpec(signingAlgorithmSpecP, fieldName) - } yield Signature.fromExternalSigning(format, signature, signedBy, signingAlgorithmSpec) - } - - private def validateSignatureFormat( - formatP: LAPISignatureFormat, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, SignatureFormat] = - formatP match { - case LAPISignatureFormat.SIGNATURE_FORMAT_DER => Right(SignatureFormat.Der) - case LAPISignatureFormat.SIGNATURE_FORMAT_CONCAT => Right(SignatureFormat.Concat) - case LAPISignatureFormat.SIGNATURE_FORMAT_RAW => - Right(SignatureFormat.Raw: @nowarn("msg=Raw in object SignatureFormat is deprecated")) - case LAPISignatureFormat.SIGNATURE_FORMAT_SYMBOLIC => Right(SignatureFormat.Symbolic) - case LAPISignatureFormat.SIGNATURE_FORMAT_UNSPECIFIED => - Left(invalidField(fieldName, message = "Signature format must be specified")) - case other: LAPISignatureFormat.Unrecognized => - Left(invalidField(fieldName, message = s"Signing algorithm spec $other not supported")) - } - - def validateSigningAlgorithmSpec( - signingAlgorithmSpecP: crypto.SigningAlgorithmSpec, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, SigningAlgorithmSpec] = - signingAlgorithmSpecP match { - case crypto.SigningAlgorithmSpec.SIGNING_ALGORITHM_SPEC_ED25519 => - Right(SigningAlgorithmSpec.Ed25519) - case crypto.SigningAlgorithmSpec.SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256 => - Right(SigningAlgorithmSpec.EcDsaSha256) - case crypto.SigningAlgorithmSpec.SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_384 => - Right(SigningAlgorithmSpec.EcDsaSha384) - case other => - Left(invalidField(fieldName, message = s"Signing algorithm spec $other not supported")) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/DeduplicationPeriodValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/DeduplicationPeriodValidator.scala deleted file mode 100644 index 03b1b3995a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/DeduplicationPeriodValidator.scala +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException - -import java.time.Duration - -object DeduplicationPeriodValidator { - private val fieldName = "deduplication_period" - - def validateNonNegativeDuration(duration: Duration)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Duration] = if (duration.isNegative) - Left( - ValidationErrors - .invalidField( - fieldName, - "Duration must be positive", - ) - ) - else Right(duration) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/EventQueryServiceRequestValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/EventQueryServiceRequestValidator.scala deleted file mode 100644 index acf9319570..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/EventQueryServiceRequestValidator.scala +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.event_query_service.GetEventsByContractIdRequest -import com.digitalasset.canton.ledger.api.messages.event -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException - -object EventQueryServiceRequestValidator { - type Result[X] = Either[StatusRuntimeException, X] - - import FieldValidator.* - import ValidationErrors.* - - def validateEventsByContractId( - req: GetEventsByContractIdRequest - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Result[event.GetEventsByContractIdRequest] = - for { - contractId <- requireContractId(req.contractId, "contract_id") - eventFormat <- req.eventFormat match { - case None => - Left(missingField("event_format")) - case Some(protoEventFormat) => - FormatValidator.validate(protoEventFormat) - } - } yield { - event.GetEventsByContractIdRequest(contractId, eventFormat) - } - - // TODO(i16065): Re-enable getEventsByContractKey tests -// def validateEventsByContractKey( -// req: GetEventsByContractKeyRequest -// )(implicit -// errorLoggingContext: ErrorLoggingContext -// ): Result[event.GetEventsByContractKeyRequest] = { -// -// for { -// apiContractKey <- requirePresence(req.contractKey, "contract_key") -// contractKey <- ValueValidator.validateValue(apiContractKey) -// apiTemplateId <- requirePresence(req.templateId, "template_id") -// templateId <- validateIdentifier(apiTemplateId) -// _ <- requireNonEmpty(req.requestingParties, "requesting_parties") -// requestingParties <- partyValidator.requireKnownParties(req.requestingParties) -// endExclusiveSeqId <- optionalEventSequentialId( -// req.continuationToken, -// "continuation_token", -// "Invalid token", // Don't mention event sequential id as opaque -// ) -// } yield { -// -// event.GetEventsByContractKeyRequest( -// contractKey = contractKey, -// templateId = templateId, -// requestingParties = requestingParties, -// endExclusiveSeqId = endExclusiveSeqId, -// ) -// } -// -// } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/FieldValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/FieldValidator.scala deleted file mode 100644 index 0e24be4268..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/FieldValidator.scala +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.implicits.toBifunctorOps -import com.daml.jwt.JwksUrl -import com.daml.ledger.api.v2.value.Identifier -import com.digitalasset.canton.ledger.api.validation.ResourceAnnotationValidator.{ - AnnotationsSizeExceededError, - EmptyAnnotationsValueError, - InvalidAnnotationsKeyError, -} -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.* -import com.digitalasset.canton.ledger.api.validation.ValueValidator.* -import com.digitalasset.canton.ledger.api.{IdentityProviderId, SubmissionId, WorkflowId} -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.topology.{ - ParticipantId, - PartyId as TopologyPartyId, - PhysicalSynchronizerId, - SynchronizerId, -} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{Party, TypeConRef} -import com.digitalasset.daml.lf.value.Value.ContractId -import com.google.protobuf.ByteString -import io.grpc.StatusRuntimeException -import scalapb.{GeneratedMessage, GeneratedMessageCompanion} - -import scala.util.{Failure, Success, Try} - -object FieldValidator { - - def requireNonEmptyString(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, String] = - Either.cond(s.nonEmpty, s, missingField(fieldName)) - - def requireParty(s: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.Party] = - Ref.Party.fromString(s).left.map(invalidArgument) - - def requireParty( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.Party] = - if (s.isEmpty) Left(missingField(fieldName)) - else Ref.Party.fromString(s).left.map(invalidField(fieldName, _)) - - def requirePartyField(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.Party] = - Ref.Party.fromString(s).left.map(invalidField(fieldName, _)) - - def requireTopologyPartyIdField(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, TopologyPartyId] = for { - lf <- requirePartyField(s, fieldName) - id <- TopologyPartyId - .fromLfParty(lf) - .leftMap(err => invalidField(fieldName = fieldName, message = err)) - } yield id - - def optionalParticipantId(participantId: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[ParticipantId]] = optionalString(participantId) { s => - ParticipantId - .fromProtoPrimitive("PAR::" + s, fieldName) - .left - .map(err => invalidField(fieldName = fieldName, message = err.message)) - } - - def requireResourceVersion(raw: String, fieldName: String)(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, Long] = - Try { - raw.toLong - } match { - case Success(resourceVersionNumber) => Right(resourceVersionNumber) - case Failure(_) => - Left( - invalidField(fieldName = fieldName, message = "Invalid resource version number") - ) - } - - def requireJwksUrl(raw: String, fieldName: String)(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, JwksUrl] = - for { - _ <- requireNonEmptyString(raw, fieldName) - value <- JwksUrl.fromString(raw).left.map { error => - invalidField(fieldName = fieldName, message = s"Malformed URL: $error") - } - } yield value - - def requireParties(parties: Set[String])(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Set[Party]] = - parties.foldLeft[Either[StatusRuntimeException, Set[Party]]](Right(Set.empty)) { - (acc, partyTxt) => - for { - parties <- acc - party <- requireParty(partyTxt) - } yield parties + party - } - - def requireUserId( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.UserId] = - requireNonEmptyParsedId(Ref.UserId.fromString)(s, fieldName) - - def optionalUserId( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[Ref.UserId]] = - if (s.isEmpty) Right(None) - else { - Ref.UserId.fromString(s).map(Some(_)).left.map(invalidField(fieldName, _)) - } - - def requireLedgerString( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.LedgerString] = - requireNonEmptyParsedId(Ref.LedgerString.fromString)(s, fieldName) - - def eventSequentialId(raw: String, fieldName: String, message: String)(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, Long] = - Try { - raw.toLong - } match { - case Success(seqId) => Right(seqId) - case Failure(_) => - // Do not mention event sequential id as this should be opaque externally - Left(invalidField(fieldName = fieldName, message)) - } - - def requireIdentityProviderId( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, IdentityProviderId.Id] = - for { - _ <- requireNonEmptyString(s, fieldName) - value <- IdentityProviderId.Id.fromString(s).left.map(invalidField(fieldName, _)) - } yield value - - def optionalIdentityProviderId( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, IdentityProviderId] = - if (s.isEmpty) Right(IdentityProviderId.Default) - else - IdentityProviderId.Id.fromString(s).left.map(invalidField(fieldName, _)) - - def requireLedgerString(s: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.LedgerString] = - Ref.LedgerString.fromString(s).left.map(invalidArgument) - - def validateWorkflowId(s: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[WorkflowId]] = - if (s.isEmpty) Right(None) - else requireLedgerString(s).map(x => Some(WorkflowId(x))) - - def validateSubmissionId(s: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[SubmissionId]] = - optionalString(s) { nonEmptyString => - Ref.SubmissionId - .fromString(nonEmptyString) - .map(SubmissionId(_)) - .left - .map(invalidField("submission_id", _)) - } - - def requireSubmissionId(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.SubmissionId] = - Ref.SubmissionId - .fromString(s) - .left - .map(invalidField(fieldName, _)) - - def requireCommandId(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.CommandId] = - Ref.CommandId - .fromString(s) - .left - .map(invalidField(fieldName, _)) - - def requireWorkflowId(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.WorkflowId] = - Ref.WorkflowId - .fromString(s) - .left - .map(invalidField(fieldName, _)) - - def requireSynchronizerId(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, SynchronizerId] = - if (s.isEmpty) Left(missingField(fieldName)) - else SynchronizerId.fromString(s).left.map(invalidField(fieldName, _)) - - def requirePhysicalSynchronizerId(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, PhysicalSynchronizerId] = - if (s.isEmpty) Left(missingField(fieldName)) - else PhysicalSynchronizerId.fromString(s).left.map(invalidField(fieldName, _)) - - def optionalSynchronizerId(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[SynchronizerId]] = - if (s.isEmpty) Right(None) - else SynchronizerId.fromString(s).left.map(invalidField(fieldName, _)).map(Some(_)) - - def requirePackageName(s: String, fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.PackageName] = - if (s.isEmpty) Left(missingField(fieldName)) - else Ref.PackageName.fromString(s).left.map(invalidField(fieldName, _)) - - def requireContractId( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ContractId] = - if (s.isEmpty) Left(missingField(fieldName)) - else ContractId.fromString(s).left.map(invalidField(fieldName, _)) - - def requireNonEmpty[M[_] <: Iterable[?], T]( - s: M[T], - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, M[T]] = - if (s.nonEmpty) Right(s) - else Left(missingField(fieldName)) - - def validateTypeConRef(identifier: Identifier)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, TypeConRef] = - for { - qualifiedName <- validateTemplateQualifiedName(identifier.moduleName, identifier.entityName) - pkgRef <- Ref.PackageRef - .fromString(identifier.packageId) - .left - .map(invalidField("package reference", _)) - } yield Ref.TypeConRef(pkgRef, qualifiedName) - - def optionalString[T](s: String)( - someValidation: String => Either[StatusRuntimeException, T] - ): Either[StatusRuntimeException, Option[T]] = - if (s.isEmpty) Right(None) - else someValidation(s).map(Option(_)) - - def requireEmptyString(s: String, fieldName: String)(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, String] = - Either.cond(s.isEmpty, s, invalidArgument(s"field $fieldName must be not set")) - - def verifyMetadataAnnotations( - annotations: Map[String, String], - allowEmptyValues: Boolean, - fieldName: String, - )(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, Map[String, String]] = - ResourceAnnotationValidator.validateAnnotationsFromApiRequest( - annotations, - allowEmptyValues = allowEmptyValues, - ) match { - case Left(AnnotationsSizeExceededError) => - Left( - invalidArgument( - s"annotations from field '$fieldName' are larger than the limit of ${ResourceAnnotationValidator.MaxAnnotationsSizeInKiloBytes}kb" - ) - ) - case Left(e: InvalidAnnotationsKeyError) => Left(invalidArgument(e.reason)) - case Left(e: EmptyAnnotationsValueError) => Left(invalidArgument(e.reason)) - case Right(_) => Right(annotations) - } - - def validatePageSize(limit: Int, defaultPageSize: Int, value: Option[Int])(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, Int] = - value match { - case Some(pageSize) if pageSize > 0 && pageSize <= limit => Right(pageSize) - case Some(pageSize) if pageSize <= 0 => - Left(invalidArgument(s"Requested page size must be positive, but got $pageSize")) - case Some(pageSize) => - Left(invalidArgument(s"Requested page size must not exceed $limit, but got $pageSize")) - case None => Right(defaultPageSize) - } - - def validateOptional[T, U](t: Option[T])( - validation: T => Either[StatusRuntimeException, U] - ): Either[StatusRuntimeException, Option[U]] = - t.map(validation).map(_.map(Some(_))).getOrElse(Right(None)) - - def requireOptional[T, U](t: Option[T], fieldName: String)( - validation: T => Either[StatusRuntimeException, U] - )(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, U] = - t.map(validation).getOrElse(Left(missingField(fieldName))) - - def validateProtobufEncodedField[T <: GeneratedMessage]( - byteString: ByteString, - companion: GeneratedMessageCompanion[T], - fieldName: String, - errorMessage: String, - )(implicit errorLoggingContext: ErrorLoggingContext): Either[StatusRuntimeException, T] = - Try(companion.parseFrom(byteString.toByteArray)).toEither.left.map(_ => - ValidationErrors.invalidField( - fieldName = fieldName, - message = errorMessage, - ) - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/FormatValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/FormatValidator.scala deleted file mode 100644 index 5cd4a43557..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/FormatValidator.scala +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.transaction_filter.CumulativeFilter.IdentifierFilter -import com.daml.ledger.api.v2.transaction_filter.{ - EventFormat as ProtoEventFormat, - Filters, - InterfaceFilter as ProtoInterfaceFilter, - ParticipantAuthorizationTopologyFormat as ProtoParticipantAuthorizationTopologyFormat, - TemplateFilter as ProtoTemplateFilter, - TopologyFormat as ProtoTopologyFormat, - TransactionFormat as ProtoTransactionFormat, - TransactionShape as ProtoTransactionShape, - UpdateFormat as ProtoUpdateFormat, - WildcardFilter, -} -import com.daml.ledger.api.v2.value.Identifier -import com.digitalasset.canton.ledger.api.validation.ValueValidator.* -import com.digitalasset.canton.ledger.api.{ - CumulativeFilter, - EventFormat, - InterfaceFilter, - ParticipantAuthorizationFormat, - TemplateFilter, - TemplateWildcardFilter, - TopologyFormat, - TransactionFormat, - TransactionShape, - UpdateFormat, -} -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.data.Ref.{NameTypeConRef, PackageRef} -import io.grpc.StatusRuntimeException -import scalaz.std.either.* -import scalaz.std.list.* -import scalaz.syntax.traverse.* - -object FormatValidator { - - import FieldValidator.* - import ValidationErrors.* - - def validate(eventFormat: ProtoEventFormat)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, EventFormat] = - if (eventFormat.filtersByParty.isEmpty && eventFormat.filtersForAnyParty.isEmpty) { - Left(invalidArgument("filtersByParty and filtersForAnyParty cannot be empty simultaneously")) - } else { - for { - convertedFilters <- eventFormat.filtersByParty.toList.traverse { case (party, filters) => - for { - key <- requireParty(party) - validatedFilters <- validateFilters( - filters - ) - } yield key -> validatedFilters - } - filtersForAnyParty <- eventFormat.filtersForAnyParty.toList - .traverse(validateFilters) - .map(_.headOption) - } yield EventFormat( - filtersByParty = convertedFilters.toMap, - filtersForAnyParty = filtersForAnyParty, - verbose = eventFormat.verbose, - ) - } - - def validate( - protoParticipantAuthorizationTopologyFormat: ProtoParticipantAuthorizationTopologyFormat - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ParticipantAuthorizationFormat] = - protoParticipantAuthorizationTopologyFormat.parties.toList - .traverse(requirePartyField(_, "parties")) - .map(parties => - ParticipantAuthorizationFormat( - // empty means: for all parties - if (parties.isEmpty) None - else Some(parties.toSet) - ) - ) - - def validate(protoTopologyFormat: ProtoTopologyFormat)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, TopologyFormat] = - for { - participantAuthorizationPartiesO <- validateOptional( - protoTopologyFormat.includeParticipantAuthorizationEvents - )(validate) - } yield TopologyFormat(participantAuthorizationPartiesO) - - def validate(protoTransactionShape: ProtoTransactionShape)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, TransactionShape] = protoTransactionShape match { - case ProtoTransactionShape.TRANSACTION_SHAPE_UNSPECIFIED => - Left(RequestValidationErrors.MissingField.Reject("transaction_shape").asGrpcError) - case ProtoTransactionShape.TRANSACTION_SHAPE_LEDGER_EFFECTS => - Right(TransactionShape.LedgerEffects) - case ProtoTransactionShape.TRANSACTION_SHAPE_ACS_DELTA => - Right(TransactionShape.AcsDelta) - case ProtoTransactionShape.Unrecognized(value) => - Left( - RequestValidationErrors.InvalidArgument - .Reject(s"transaction_shape is defined with invalid value $value") - .asGrpcError - ) - } - - def validate(protoTransactionFormat: ProtoTransactionFormat)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, TransactionFormat] = - for { - transactionShape <- validate(protoTransactionFormat.transactionShape) - eventFormat <- requireOptional(protoTransactionFormat.eventFormat, "event_format")(validate) - } yield TransactionFormat(eventFormat, transactionShape) - - def validate(protoUpdateFormat: ProtoUpdateFormat)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, UpdateFormat] = - for { - includeTransactions <- validateOptional(protoUpdateFormat.includeTransactions)(validate) - includeReassignments <- validateOptional(protoUpdateFormat.includeReassignments)(validate) - includeTopologyEvents <- validateOptional(protoUpdateFormat.includeTopologyEvents)(validate) - } yield UpdateFormat(includeTransactions, includeReassignments, includeTopologyEvents) - - // Allow using deprecated Protobuf fields for backwards compatibility - private def validateFilters(filters: Filters)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, CumulativeFilter] = { - val extractedFilters = filters.cumulative.map(_.identifierFilter) - val empties = extractedFilters.filter(_.isEmpty) - lazy val templateFilters = extractedFilters.collect { case IdentifierFilter.TemplateFilter(f) => - f - } - lazy val interfaceFilters = extractedFilters.collect { - case IdentifierFilter.InterfaceFilter(f) => - f - } - lazy val wildcardFilters = extractedFilters.collect { case IdentifierFilter.WildcardFilter(f) => - f - } - - if (empties.sizeIs == extractedFilters.size) - Right(CumulativeFilter.templateWildcardFilter()) - else { - for { - _ <- validateNonEmptyFilters( - templateFilters, - interfaceFilters, - wildcardFilters, - ) - validatedTemplates <- - templateFilters.toList.traverse(validateTemplateFilter(_)) - validatedInterfaces <- - interfaceFilters.toList.traverse(validateInterfaceFilter(_)) - wildcardO = mergeWildcardFilters(wildcardFilters) - } yield CumulativeFilter( - validatedTemplates.toSet, - validatedInterfaces.toSet, - wildcardO, - ) - } - } - - private def validateTemplateFilter(filter: ProtoTemplateFilter)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, TemplateFilter] = - for { - templateId <- requirePresence(filter.templateId, "templateId") - typeConRef <- validateNameTypeConRef(templateId) - } yield TemplateFilter( - templateTypeRef = typeConRef, - includeCreatedEventBlob = filter.includeCreatedEventBlob, - ) - - private def validateInterfaceFilter(filter: ProtoInterfaceFilter)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, InterfaceFilter] = - for { - interfaceId <- requirePresence(filter.interfaceId, "interfaceId") - typeConRef <- validateNameTypeConRef(interfaceId) - } yield InterfaceFilter( - interfaceTypeRef = typeConRef, - includeView = filter.includeInterfaceView, - includeCreatedEventBlob = filter.includeCreatedEventBlob, - ) - - private def validateNameTypeConRef(identifier: Identifier)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, NameTypeConRef] = - for { - typeConRef <- validateTypeConRef(identifier) - nameTypeConRef <- typeConRef.pkg match { - case n: PackageRef.Name => Right(NameTypeConRef(n, typeConRef.qualifiedName)) - case PackageRef.Id(id) => - Left( - invalidField( - "packageId", - s"Received an identifier with package ID $id, but expected a package name.", - ) - ) - } - } yield nameTypeConRef - - private def validateNonEmptyFilters( - templateFilters: Seq[ProtoTemplateFilter], - interfaceFilters: Seq[ProtoInterfaceFilter], - wildcardFilters: Seq[WildcardFilter], - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Unit] = - Either.cond( - !(templateFilters.isEmpty && interfaceFilters.isEmpty && wildcardFilters.isEmpty), - (), - RequestValidationErrors.InvalidArgument - .Reject( - "requests with empty template, interface and wildcard filters are not supported" - ) - .asGrpcError, - ) - - private def mergeWildcardFilters( - filters: Seq[WildcardFilter] - ): Option[TemplateWildcardFilter] = - if (filters.isEmpty) None - else - Some( - TemplateWildcardFilter( - includeCreatedEventBlob = filters.exists(_.includeCreatedEventBlob) - ) - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/GetPreferredPackagesRequestValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/GetPreferredPackagesRequestValidator.scala deleted file mode 100644 index 6dd9c2d627..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/GetPreferredPackagesRequestValidator.scala +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.implicits.toTraverseOps -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{ - GetPreferredPackagesRequest, - PackageVettingRequirement, -} -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.{LfPackageName, LfPartyId} -import io.grpc.StatusRuntimeException - -object GetPreferredPackagesRequestValidator { - def validate( - request: GetPreferredPackagesRequest - )(implicit errorLoggingContext: ErrorLoggingContext): Either[ - StatusRuntimeException, - (PackageVettingRequirements, Option[SynchronizerId], Option[CantonTimestamp]), - ] = - for { - _ <- FieldValidator - .requireNonEmpty( - s = request.packageVettingRequirements, - fieldName = "package_vetting_requirements/packageVettingRequirements", - ) - packageVettingRequirements <- request.packageVettingRequirements - .traverse(validatePackageVettingRequirement) - .map(_.toMap) - synchronizerIdO <- FieldValidator.optionalSynchronizerId( - s = request.synchronizerId, - fieldName = "synchronizer_id/synchronizerId", - ) - vettingValidAtO <- request.vettingValidAt - .traverse( - CantonTimestamp - .fromProtoTimestamp(_) - .left - .map(protoDeserializationError => - ValidationErrors.invalidField( - fieldName = "vetting_valid_at/vettingValidAt", - message = protoDeserializationError.message, - ) - ) - ) - } yield ( - PackageVettingRequirements(packageVettingRequirements), - synchronizerIdO, - vettingValidAtO, - ) - - private def validatePackageVettingRequirement( - requirement: PackageVettingRequirement - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, (LfPackageName, Set[LfPartyId])] = - for { - packageName <- FieldValidator - .requirePackageName( - s = requirement.packageName, - fieldName = "package_name/packageName", - ) - nonEmptyRawParties <- FieldValidator.requireNonEmpty( - s = requirement.parties, - fieldName = "parties", - ) - parties <- FieldValidator.requireParties(nonEmptyRawParties.toSet) - } yield packageName -> parties - - /** Defines which package-names must have commonly-vetted packages for the provided parties. - */ - final case class PackageVettingRequirements( - value: Map[LfPackageName, Set[LfPartyId]] - ) { - lazy val allPackageNames: Set[LfPackageName] = value.keySet - lazy val allParties: Set[LfPartyId] = value.values.flatten.toSet - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ParticipantOffsetValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ParticipantOffsetValidator.scala deleted file mode 100644 index 9edc397653..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ParticipantOffsetValidator.scala +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException - -object ParticipantOffsetValidator { - - def validateOptionalPositive(offsetO: Option[Long], fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[Offset]] = - offsetO match { - case Some(off) => - validatePositive( - off, - fieldName, - "the offset has to be either a positive integer (>0) or not defined at all", - ).map( - Some(_) - ) - case None => Right(None) - } - - def validatePositive( - offset: Long, - fieldName: String, - errorMsg: String = "the offset has to be a positive integer (>0)", - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Offset] = - Either.cond( - offset > 0, - Offset.tryFromLong(offset), - RequestValidationErrors.NonPositiveOffset - .Error( - fieldName, - offset, - errorMsg, - ) - .asGrpcError, - ) - - def validateNonNegative(offset: Long, fieldName: String)(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[Offset]] = - Either.cond( - offset >= 0, - Offset.tryOffsetOrParticipantBegin(offset), - RequestValidationErrors.NegativeOffset - .Error( - fieldName = fieldName, - offsetValue = offset, - message = s"the offset in $fieldName field has to be a non-negative integer (>=0)", - ) - .asGrpcError, - ) - - def validateOptionalNonNegative(offsetO: Option[Long], fieldName: String)(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[Offset]] = - offsetO match { - case Some(offset) => - validateNonNegative( - offset, - fieldName, - ) - case None => Right(None) - } - - def offsetIsBeforeEnd( - offsetType: String, - offset: Option[Offset], - ledgerEnd: Option[Offset], - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Unit] = - Either.cond( - offset <= ledgerEnd, - (), - RequestValidationErrors.OffsetAfterLedgerEnd - .Reject( - offsetType, - offset.fold(0L)(_.unwrap), - ledgerEnd.fold(0L)(_.unwrap), - ) - .asGrpcError, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ResourceAnnotationValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ResourceAnnotationValidator.scala deleted file mode 100644 index 63b9810951..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ResourceAnnotationValidator.scala +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.syntax.either.* - -import java.nio.charset.StandardCharsets -import scala.util.matching.Regex - -object ResourceAnnotationValidator { - - // NOTE: These constraints are based on constraints K8s uses for their annotations and labels - private val NamePattern = "([a-zA-Z0-9]+[a-zA-Z0-9-]*)?[a-zA-Z0-9]+" - private val KeySegmentRegex: Regex = "^([a-zA-Z0-9]+[a-zA-Z0-9.\\-_]*)?[a-zA-Z0-9]+$".r - private val DnsSubdomainRegex: Regex = ("^(" + NamePattern + "[.])*" + NamePattern + "$").r - val MaxAnnotationsSizeInKiloBytes: Int = 256 - private val MaxAnnotationsSizeInBytes: Int = MaxAnnotationsSizeInKiloBytes * 1024 - - sealed trait MetadataAnnotationsError { - def reason: String - } - case object AnnotationsSizeExceededError extends MetadataAnnotationsError { - override val reason = - s"Max annotations size of ${MaxAnnotationsSizeInKiloBytes}kb has been exceeded" - } - final case class InvalidAnnotationsKeyError(override val reason: String) - extends MetadataAnnotationsError - final case class EmptyAnnotationsValueError(private val key: String) - extends MetadataAnnotationsError { - override val reason = s"The value of an annotation is empty for key: '${shorten(key)}'" - } - - /** @return - * a Left(actualSizeInBytes) in case of a failed validation - */ - def isWithinMaxAnnotationsByteSize(annotations: Map[String, String]): Boolean = { - val totalSizeInBytes = annotations.iterator.foldLeft(0L) { case (size, (key, value)) => - val keySize = key.getBytes(StandardCharsets.UTF_8).length - val valSize = value.getBytes(StandardCharsets.UTF_8).length - size + keySize + valSize - } - totalSizeInBytes <= MaxAnnotationsSizeInBytes - } - - def validateAnnotationsFromApiRequest( - annotations: Map[String, String], - allowEmptyValues: Boolean, - ): Either[MetadataAnnotationsError, Unit] = { - val nonEmptyValued = annotations.view.filter { case (_, value) => value.nonEmpty }.toMap - for { - _ <- - Either.cond( - isWithinMaxAnnotationsByteSize(nonEmptyValued), - (), - AnnotationsSizeExceededError, - ) - _ <- validateAnnotationKeys(annotations) - _ <- if (allowEmptyValues) Either.unit else validateAnnotationsValues(annotations) - } yield () - } - - private def validateAnnotationsValues( - annotations: Map[String, String] - ): Either[MetadataAnnotationsError, Unit] = - annotations.view.iterator.foldLeft(Either.unit[MetadataAnnotationsError]) { - case (acc, (key, value)) => - for { - _ <- acc - _ <- if (value.isEmpty) Left(EmptyAnnotationsValueError(key = key)) else Right(()) - } yield () - } - - private def validateAnnotationKeys( - annotations: Map[String, String] - ): Either[MetadataAnnotationsError, Unit] = - annotations.keys.iterator.foldLeft(Either.unit[MetadataAnnotationsError]) { (acc, key) => - for { - _ <- acc - _ <- isValidKey(key) - } yield () - } - - private def isValidKey(key: String): Either[MetadataAnnotationsError, Unit] = - key.split('/') match { - case Array(name) => isValidKeyNameSegment(name) - case Array(prefix, name) => - for { - _ <- isValidKeyPrefixSegment(prefix) - _ <- isValidKeyNameSegment(name) - } yield () - case _ => - Left( - InvalidAnnotationsKeyError( - s"Key '${shorten(key)}' contains more than one forward slash ('/') character" - ) - ) - } - - private def isValidKeyPrefixSegment( - prefixSegment: String - ): Either[InvalidAnnotationsKeyError, Unit] = - if (prefixSegment.length > 253) { - Left( - InvalidAnnotationsKeyError( - s"Key prefix segment '${shorten(prefixSegment)}' exceeds maximum length of 253 characters" - ) - ) - } else { - Either.cond( - DnsSubdomainRegex.matches(prefixSegment), - (), - InvalidAnnotationsKeyError( - s"Key prefix segment '${shorten(prefixSegment)}' has invalid syntax" - ), - ) - } - - private def isValidKeyNameSegment( - nameSegment: String - ): Either[InvalidAnnotationsKeyError, Unit] = - if (nameSegment.length > 63) { - Left( - InvalidAnnotationsKeyError( - s"Key name segment '${shorten(nameSegment)}' exceeds maximum length of 63 characters" - ) - ) - } else { - Either.cond( - KeySegmentRegex.matches(nameSegment), - (), - InvalidAnnotationsKeyError( - s"Key name segment '${shorten(nameSegment)}' has invalid syntax" - ), - ) - } - - private def shorten(s: String): String = - if (s.length > 53) { s.take(50) + "..." } - else s - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/SubmitAndWaitRequestValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/SubmitAndWaitRequestValidator.scala deleted file mode 100644 index 39c9bc3a86..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/SubmitAndWaitRequestValidator.scala +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.command_service.{ - SubmitAndWaitForReassignmentRequest, - SubmitAndWaitForTransactionRequest, - SubmitAndWaitRequest, -} -import com.digitalasset.canton.ledger.api.validation.ValueValidator.* -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException - -import java.time.{Duration, Instant} - -class SubmitAndWaitRequestValidator( - commandsValidator: CommandsValidator -) { - - def validate( - req: SubmitAndWaitRequest, - currentLedgerTime: Instant, - currentUtcTime: Instant, - maxDeduplicationDuration: Duration, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Unit] = - for { - commands <- requirePresence(req.commands, "commands") - _ <- commandsValidator.validateCommands( - commands, - currentLedgerTime, - currentUtcTime, - maxDeduplicationDuration, - ) - } yield () - - def validate( - req: SubmitAndWaitForTransactionRequest, - currentLedgerTime: Instant, - currentUtcTime: Instant, - maxDeduplicationDuration: Duration, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Unit] = - for { - commands <- requirePresence(req.commands, "commands") - _ <- requirePresence(req.transactionFormat, "transaction_format").flatMap( - FormatValidator.validate - ) - _ <- commandsValidator.validateCommands( - commands, - currentLedgerTime, - currentUtcTime, - maxDeduplicationDuration, - ) - } yield () - - def validate( - req: SubmitAndWaitForReassignmentRequest - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Unit] = - for { - commands <- requirePresence(req.reassignmentCommands, "reassignment_commands") - _ <- req.eventFormat match { - case Some(eventFormat) => FormatValidator.validate(eventFormat).map(_ => ()) - case None => Right(()) - } - _ <- commandsValidator.validateReassignmentCommands( - commands - ) - } yield () - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/SubmitRequestValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/SubmitRequestValidator.scala deleted file mode 100644 index 22decc5d31..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/SubmitRequestValidator.scala +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.implicits.{toBifunctorOps, toTraverseOps} -import com.daml.ledger.api.v2.command_submission_service.{SubmitReassignmentRequest, SubmitRequest} -import com.daml.ledger.api.v2.interactive.interactive_submission_service as iss -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{ - CostEstimationHints as CostEstimationHintsP, - PartySignatures, - PrepareSubmissionRequest, - SinglePartySignatures, -} -import com.digitalasset.base.error.RpcError -import com.digitalasset.canton.crypto.Signature -import com.digitalasset.canton.ledger.api.SubmissionIdGenerator -import com.digitalasset.canton.ledger.api.messages.command.submission -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService.ExecuteRequest -import com.digitalasset.canton.ledger.api.validation.ValueValidator.* -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.platform.apiserver.services.command.interactive.CostEstimationHints -import com.digitalasset.canton.topology.{PartyId as TopologyPartyId, Synchronizer} -import com.digitalasset.canton.version.HashingSchemeVersion -import com.digitalasset.canton.version.HashingSchemeVersion.{V2, V3} -import io.grpc.StatusRuntimeException -import scalaz.syntax.tag.* - -import java.time.{Duration, Instant} - -class SubmitRequestValidator( - commandsValidator: CommandsValidator -) { - import FieldValidator.* - def validate( - req: SubmitRequest, - currentLedgerTime: Instant, - currentUtcTime: Instant, - maxDeduplicationDuration: Duration, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, submission.SubmitRequest] = - for { - commands <- requirePresence(req.commands, "commands") - validatedCommands <- commandsValidator.validateCommands( - commands, - currentLedgerTime, - currentUtcTime, - maxDeduplicationDuration, - ) - } yield submission.SubmitRequest(validatedCommands) - - def validatePrepare( - req: PrepareSubmissionRequest, - currentLedgerTime: Instant, - currentUtcTime: Instant, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, InteractiveSubmissionService.PrepareRequest] = - for { - validatedCommands <- commandsValidator.validatePrepareRequest( - req, - currentLedgerTime, - currentUtcTime, - ) - maxRecordTime <- req.maxRecordTime.traverse(commandsValidator.validateLfTime) - costEstimationHints <- CostEstimationHints.fromProto( - // If not set, defaults to the default instance which enables estimation without hints - req.estimateTrafficCost.getOrElse(CostEstimationHintsP.defaultInstance) - ) - hashingSchemeVersion <- req.hashingSchemeVersion match { - case Some(hashingSchemeVersionP) => - validateHashingSchemeVersion(hashingSchemeVersionP).leftMap(_.asGrpcError) - case None => - Right(HashingSchemeVersion.V2) // Default to V2 for backward compatibility if not set - } - } yield InteractiveSubmissionService.PrepareRequest( - validatedCommands, - req.verboseHashing, - maxRecordTime, - costEstimationHints, - hashingSchemeVersion, - ) - - private def validatePartySignatures( - proto: PartySignatures - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Map[TopologyPartyId, Seq[Signature]]] = - proto.signatures - .traverse { case SinglePartySignatures(partyP, signaturesP) => - for { - partyId <- requireTopologyPartyIdField(partyP, "SinglePartySignatures.party") - signatures <- signaturesP.traverse(s => - CryptoValidator.validateSignature(s, "SinglePartySignatures.signature") - ) - } yield partyId -> signatures - } - .map(_.foldLeft(Map.empty[TopologyPartyId, Seq[Signature]]) { case (m, (p, s)) => - m.updatedWith(p) { - case None => Some(s) - // This covers the test case where a client submits multiple SinglePartySignatures - // objects for a single party (the more usual use case would be to submit all signatures in one go) - case Some(existing) => Some((s.toSet ++ existing.toSet).toSeq) - } - }) - - def validateExecute( - req: iss.ExecuteSubmissionRequest, - currentLedgerTime: Instant, - submissionIdGenerator: SubmissionIdGenerator, - maxDeduplicationDuration: Duration, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ExecuteRequest] = { - val iss.ExecuteSubmissionRequest( - preparedTransactionP, - partySignaturesOP, - deduplicationPeriodP, - submissionIdP, - userIdP, - hashingSchemeVersionP, - minLedgerTimeP, - ) = req - for { - submissionId <- validateSubmissionId(submissionIdP) - .map(_.map(_.unwrap)) - .map( - _.getOrElse(submissionIdGenerator.generate()) - ) - userId <- requireUserId(userIdP, "user_id") - deduplicationPeriod <- commandsValidator.validateExecuteDeduplicationPeriod( - deduplicationPeriodP, - maxDeduplicationDuration, - ) - preparedTransaction <- preparedTransactionP.toRight( - RequestValidationErrors.MissingField - .Reject("prepared_transaction") - .asGrpcError - ) - partySignaturesP <- requirePresence(partySignaturesOP, "parties_signatures") - partySignatures <- validatePartySignatures(partySignaturesP) - hashingSchemeVersion <- validateHashingSchemeVersion(hashingSchemeVersionP).leftMap( - _.asGrpcError - ) - synchronizerIdString <- requirePresence( - preparedTransactionP.flatMap(_.metadata.map(_.synchronizerId)), - "synchronizer_id", - ) - synchronizer <- validateSynchronizer(synchronizerIdString).leftMap(_.asGrpcError) - ledgerEffectiveTime <- commandsValidator.validateLedgerTime( - currentLedgerTime, - minLedgerTimeP.flatMap(_.time.minLedgerTimeAbs), - minLedgerTimeP.flatMap(_.time.minLedgerTimeRel), - ) - } yield { - ExecuteRequest( - userId, - submissionId, - deduplicationPeriod, - partySignatures, - preparedTransaction, - hashingSchemeVersion, - ledgerEffectiveTime, - ) - } - } - - private def validateSynchronizer(string: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[RpcError, Synchronizer] = - Synchronizer - .fromLogicalOrPhysicalString(string, "synchronizer_id") - .leftMap(err => - RequestValidationErrors.InvalidField - .Reject("synchronizer_id", err.message) - ) - private def validateHashingSchemeVersion(protoVersion: iss.HashingSchemeVersion)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[RpcError, HashingSchemeVersion] = protoVersion match { - case iss.HashingSchemeVersion.HASHING_SCHEME_VERSION_V2 => Right(V2) - case iss.HashingSchemeVersion.HASHING_SCHEME_VERSION_V3 => Right(V3) - case iss.HashingSchemeVersion.HASHING_SCHEME_VERSION_UNSPECIFIED => - Left( - RequestValidationErrors.InvalidField - .Reject("hashing_scheme_version", "Unspecified version") - ) - case iss.HashingSchemeVersion.Unrecognized(unrecognizedValue) => - Left( - RequestValidationErrors.InvalidField - .Reject("hashing_scheme_version", s"Unrecognized version $unrecognizedValue") - ) - } - - def validateReassignment( - req: SubmitReassignmentRequest - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, submission.SubmitReassignmentRequest] = - for { - commands <- requirePresence(req.reassignmentCommands, "reassignment_commands") - submitReassignmentRequest <- commandsValidator.validateReassignmentCommands( - commands - ) - } yield submitReassignmentRequest - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/UpdateServiceRequestValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/UpdateServiceRequestValidator.scala deleted file mode 100644 index 39b14cbb59..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/UpdateServiceRequestValidator.scala +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.implicits.{catsSyntaxTuple2Semigroupal, toTraverseOps} -import com.daml.ledger.api.v2.update_service.{ - GetUpdateByIdRequest, - GetUpdateByOffsetRequest, - GetUpdatesPageRequest, - GetUpdatesRequest, -} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.messages.update -import com.digitalasset.canton.ledger.api.messages.update.UpdatesPageToken -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.invalidArgument -import com.digitalasset.canton.ledger.api.validation.ValueValidator.* -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.platform.config.UpdateServiceConfig -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.daml.lf.data.Ref.ParticipantId -import io.grpc.StatusRuntimeException - -object UpdateServiceRequestValidator { - type Result[X] = Either[StatusRuntimeException, X] - - import FieldValidator.* - - final case class PartialValidation( - begin: Option[Offset], - end: Option[Offset], - descendingOrder: Boolean, - ) - - private def commonValidations( - req: GetUpdatesRequest - )(implicit errorLoggingContext: ErrorLoggingContext): Result[PartialValidation] = - for { - begin <- ParticipantOffsetValidator - .validateNonNegative(req.beginExclusive, "begin_exclusive") - endAndDescendingOrder <- - if (req.descendingOrder) { - (req.endInclusive match { - case Some(value) => ParticipantOffsetValidator.validatePositive(value, "end_inclusive") - case None => Left(RequestValidationErrors.DescendingOrderMissingEnd.Error().asGrpcError) - }).map(offset => (Option(offset), true)) - } else { - ParticipantOffsetValidator - .validateOptionalPositive(req.endInclusive, "end_inclusive") - .map((_, false)) - } - } yield PartialValidation( - begin, - endAndDescendingOrder._1, - endAndDescendingOrder._2, - ) - - def validate( - req: GetUpdatesRequest, - ledgerEnd: Option[Offset], - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Result[update.GetUpdatesRequest] = - for { - partial <- commonValidations(req) - _ <- ParticipantOffsetValidator.offsetIsBeforeEnd( - "Begin", - partial.begin, - ledgerEnd, - ) - _ <- ParticipantOffsetValidator.offsetIsBeforeEnd( - "End", - partial.end, - ledgerEnd, - ) - updateFormatProto <- requirePresence(req.updateFormat, "update_format") - updateFormat <- FormatValidator.validate(updateFormatProto) - } yield { - update.GetUpdatesRequest( - partial.begin, - partial.end, - updateFormat, - partial.descendingOrder, - ) - } - - def validateUpdateByOffset( - req: GetUpdateByOffsetRequest - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Result[update.GetUpdateByOffsetRequest] = - for { - offset <- ParticipantOffsetValidator.validatePositive(req.offset, "offset") - updateFormatProto <- requirePresence(req.updateFormat, "update_format") - updateFormat <- FormatValidator.validate(updateFormatProto) - } yield { - update.GetUpdateByOffsetRequest( - offset = offset, - updateFormat = updateFormat, - ) - } - - def validateUpdateById( - req: GetUpdateByIdRequest - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Result[update.GetUpdateByIdRequest] = - for { - _ <- requireNonEmptyString(req.updateId, "update_id") - updateIdStr <- requireLedgerString(req.updateId) - updateId <- UpdateId.fromLedgerString(updateIdStr).left.map(e => invalidArgument(e.message)) - updateFormatProto <- requirePresence(req.updateFormat, "update_format") - updateFormat <- FormatValidator.validate(updateFormatProto) - } yield { - update.GetUpdateByIdRequest( - updateId = updateId, - updateFormat = updateFormat, - ) - } - - def validateUpdatesPageRequest( - req: GetUpdatesPageRequest, - ledgerEnd: Option[Offset], - participantId: ParticipantId, - updateServiceConfig: UpdateServiceConfig, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Result[update.GetUpdatesPageRequest] = for { - token <- UpdatesPageToken.validateToken(req, participantId) - endOffsetInRequest <- req.endOffsetInclusive.traverse( - ParticipantOffsetValidator.validatePositive(_, "endOffsetInclusive") - ) - beginExclInRequest <- req.beginOffsetExclusive.traverse( - ParticipantOffsetValidator - .validateNonNegative(_, "beginOffsetExclusive") - ) - maxPageSize <- FieldValidator.validatePageSize( - updateServiceConfig.maxUpdatesPageSize.value, - updateServiceConfig.defaultUpdatesPageSize.value, - req.maxPageSize, - ) - _ <- ParticipantOffsetValidator.offsetIsBeforeEnd( - "endOffsetInclusive", - endOffsetInRequest, - ledgerEnd, - ) - _ <- ParticipantOffsetValidator.offsetIsBeforeEnd( - "beginOffsetInclusive", - endOffsetInRequest, - ledgerEnd, - ) - _ <- (beginExclInRequest, endOffsetInRequest).tupled.traverse { case (begin, end) => - Either.cond( - begin.forall(_ <= end), - (), - RequestValidationErrors.InvalidArgument - .Reject(s"beginOffsetExclusive is after endOffsetInclusive") - .asGrpcError, - ) - } - updateFormatProto <- requirePresence(req.updateFormat, "update_format") - updateFormat <- FormatValidator.validate(updateFormatProto) - continueStreamFromIncl <- token.traverse(t => - if (req.descendingOrder) { - t.lowestPageOffsetExclusive.toRight( - RequestValidationErrors.InvalidUpdatesPageToken - .Reject("Page token not from descendingOrder=true request") - .asGrpcError - ) - } else { - Either.cond( - endOffsetInRequest.forall(t.highestPageOffsetInclusive < Some(_)), - t.highestPageOffsetInclusive.fold(Offset.firstOffset)(_.increment), - RequestValidationErrors.InvalidUpdatesPageToken - .Reject("Page token not from descendingOrder=false request") - .asGrpcError, - ) - } - ) - } yield update.GetUpdatesPageRequest( - startExclusive = beginExclInRequest, - endInclusive = endOffsetInRequest, - continueStreamFromIncl = continueStreamFromIncl, - maxPageSize = maxPageSize, - updateFormat = updateFormat, - descendingOrder = req.descendingOrder, - requestChecksum = token.map(_.requestChecksum).getOrElse(UpdatesPageToken.requestChecksum(req)), - participantChecksum = token - .map(_.participantIdChecksum) - .getOrElse(UpdatesPageToken.participantChecksum(participantId)), - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidateDisclosedContracts.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidateDisclosedContracts.scala deleted file mode 100644 index deececc4e5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidateDisclosedContracts.scala +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.implicits.{toFoldableOps, toTraverseOps} -import com.daml.ledger.api.v2.commands.{ - Commands as ProtoCommands, - DisclosedContract as ProtoDisclosedContract, -} -import com.digitalasset.canton.ledger.api.DisclosedContract -import com.digitalasset.canton.ledger.api.validation.FieldValidator.{ - requireSynchronizerId, - validateOptional, -} -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.{ - disclosedContractsConflictingPayloads, - invalidArgument, - invalidField, -} -import com.digitalasset.canton.ledger.api.validation.ValueValidator.* -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.canton.util.OptionUtil -import com.digitalasset.daml.lf.data.ImmArray -import com.digitalasset.daml.lf.transaction.{CreationTime, TransactionCoder} -import com.digitalasset.daml.lf.value.Value.ContractId -import io.grpc.StatusRuntimeException - -trait ValidateDisclosedContracts { - - def validateCommands(commands: ProtoCommands)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ImmArray[DisclosedContract]] - - def validateDisclosedContracts(disclosedContracts: Seq[ProtoDisclosedContract])(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ImmArray[DisclosedContract]] -} - -object ValidateDisclosedContracts extends ValidateDisclosedContracts { - - def validateCommands(commands: ProtoCommands)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ImmArray[DisclosedContract]] = - validateDisclosedContracts(commands.disclosedContracts) - - def validateDisclosedContracts(disclosedContracts: Seq[ProtoDisclosedContract])(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ImmArray[DisclosedContract]] = - for { - validatedDisclosedContracts <- validateContracts(disclosedContracts) - _ <- verifyNoDuplicates(validatedDisclosedContracts.map(_.fatContractInstance).toSeq) - } yield validatedDisclosedContracts - - private def verifyNoDuplicates( - disclosedContracts: Seq[LfFatContractInst] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Unit] = { - val contractConflictingPayloads = disclosedContracts - .groupBy(_.contractId.coid) - .flatMap { case (id, contracts) => - val duplicateContracts = contracts.distinct - if (duplicateContracts.sizeIs > 1) - Some( - (id, duplicateContracts.size.toLong, duplicateContracts.mkString(", ")) - ) - else None - } - .toList - - if (contractConflictingPayloads.isEmpty) Right(()) - else Left(disclosedContractsConflictingPayloads(contractConflictingPayloads)) - } - - private def validateContracts( - disclosedContracts: Seq[ProtoDisclosedContract] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ImmArray[DisclosedContract]] = - disclosedContracts.toList - .foldM(ImmArray.newBuilder[DisclosedContract])((acc, contract) => - validateDisclosedContract(contract).map(acc.addOne) - ) - .map(_.result()) - - private def validateDisclosedContract( - disclosedContract: ProtoDisclosedContract - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, DisclosedContract] = - if (disclosedContract.createdEventBlob.isEmpty) - Left(ValidationErrors.missingField("DisclosedContract.createdEventBlob")) - else - for { - validatedTemplateIdO <- validateOptionalIdentifier(disclosedContract.templateId) - validatedContractIdO <- validateOptional( - OptionUtil.emptyStringAsNone(disclosedContract.contractId) - )(ContractId.fromString(_).left.map(invalidField("DisclosedContract.contract_id", _))) - synchronizerIdO <- OptionUtil - .emptyStringAsNone(disclosedContract.synchronizerId) - .map(requireSynchronizerId(_, "DisclosedContract.synchronizer_id").map(Some(_))) - .getOrElse(Right(None)) - fatContractInstance <- TransactionCoder - .decodeFatContractInstance(disclosedContract.createdEventBlob) - .left - .map(decodeError => - invalidArgument(s"Unable to decode disclosed contract event payload: $decodeError") - ) - _ <- validatedContractIdO.traverse(validatedContractId => - Either.cond( - validatedContractId == fatContractInstance.contractId, - (), - invalidArgument( - s"Mismatch between DisclosedContract.contract_id (${validatedContractId.coid}) and contract_id from decoded DisclosedContract.created_event_blob (${fatContractInstance.contractId.coid})" - ), - ) - ) - _ <- validatedTemplateIdO.traverse(validatedTemplateId => - Either.cond( - validatedTemplateId == fatContractInstance.templateId, - (), - invalidArgument( - s"Mismatch between DisclosedContract.template_id ($validatedTemplateId) and template_id from decoded DisclosedContract.created_event_blob (${fatContractInstance.templateId})" - ), - ) - ) - lfFatContractInst <- fatContractInstance.traverseCreateAt { - case time: CreationTime.CreatedAt => Right(time) - case _ => Left(invalidArgument("Contract creation time cannot be 'Now'")) - } - } yield DisclosedContract( - fatContractInstance = lfFatContractInst, - synchronizerIdO = synchronizerIdO, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidateUpgradingPackageResolutions.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidateUpgradingPackageResolutions.scala deleted file mode 100644 index cfaf762f20..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidateUpgradingPackageResolutions.scala +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.syntax.traverse.* -import com.digitalasset.canton.ledger.api.validation.ValidateUpgradingPackageResolutions.ValidatedCommandPackageResolutionsSnapshot -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.invalidArgument -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.store.packagemeta.PackageMetadata.PackageResolution -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{PackageId, PackageName, PackageVersion} -import com.google.common.annotations.VisibleForTesting -import io.grpc.StatusRuntimeException - -trait ValidateUpgradingPackageResolutions { - def apply( - rawUserPackageIdPreferences: Seq[String] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[ - StatusRuntimeException, - ValidatedCommandPackageResolutionsSnapshot, - ] -} - -class ValidateUpgradingPackageResolutionsImpl( - getPackageMetadataSnapshot: ErrorLoggingContext => PackageMetadata -) extends ValidateUpgradingPackageResolutions { - def apply( - rawUserPackageIdPreferences: Seq[String] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[ - StatusRuntimeException, - ValidatedCommandPackageResolutionsSnapshot, - ] = { - val packageMetadataSnapshot = getPackageMetadataSnapshot(errorLoggingContext) - val packageResolutionMapSnapshot = packageMetadataSnapshot.packageIdVersionMap - val participantPackagePreferenceMapSnapshot = - packageMetadataSnapshot.packageNameMap.view.mapValues { - case PackageResolution(preference, _) => preference.packageId - }.toMap - - for { - userPackageIdPreferences <- rawUserPackageIdPreferences - .traverse(Ref.PackageId.fromString) - .left - .map(err => - invalidArgument( - s"package_id_selection_preference parsing failed with `$err`. The package_id_selection_preference field must contain non-empty and valid package ids" - ) - ) - userPackagePreferenceMap <- userPackageIdPreferences - .traverse(pkgId => - packageResolutionMapSnapshot - .get(pkgId) - .map(_._1 -> pkgId) - .toRight(invalidArgument(s"user-specified pkg id ($pkgId) could not be found")) - ) - validatedUserPackagePreferenceMap <- - userPackagePreferenceMap.foldLeft( - Right(Map.empty): Either[StatusRuntimeException, Map[PackageName, PackageId]] - ) { - case (Right(acc), (packageName, userPref)) => - acc.get(packageName) match { - case Some(existing) => - Left( - invalidArgument( - s"duplicate preference for package-name $packageName: $existing vs $userPref" - ) - ) - case None => Right(acc.updated(packageName, userPref)) - } - case (Left(err), _) => Left(err) - } - } yield { - val submissionPackagePreferenceSet = - (participantPackagePreferenceMapSnapshot ++ validatedUserPackagePreferenceMap).values - // It's fine provided that we disallow uploading of unrelated package-ids for the same package-name - .toSet - ValidatedCommandPackageResolutionsSnapshot( - packageResolutionMapSnapshot, - submissionPackagePreferenceSet, - ) - } - } -} - -object ValidateUpgradingPackageResolutions { - final case class ValidatedCommandPackageResolutionsSnapshot( - packageMap: Map[PackageId, (PackageName, PackageVersion)], - packagePreferenceSet: Set[PackageId], - ) - - @VisibleForTesting - val Empty: ValidateUpgradingPackageResolutions = - new ValidateUpgradingPackageResolutions { - override def apply(userPackageIdPreferences: Seq[String])(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ValidatedCommandPackageResolutionsSnapshot] = - Right( - ValidatedCommandPackageResolutionsSnapshot( - Map.empty, - Set.empty, - ) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidationErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidationErrors.scala deleted file mode 100644 index 69dd655053..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValidationErrors.scala +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException - -object ValidationErrors { - - def missingField(fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): StatusRuntimeException = - RequestValidationErrors.MissingField - .Reject(fieldName) - .asGrpcError - - def invalidArgument(message: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): StatusRuntimeException = - RequestValidationErrors.InvalidArgument - .Reject(message) - .asGrpcError - - def disclosedContractsConflictingPayloads( - contractConflictingPayloads: List[(String, Long, String)] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): StatusRuntimeException = - RequestValidationErrors.DisclosedContractsConflictingPayloads - .Reject(contractConflictingPayloads) - .asGrpcError - - def invalidField( - fieldName: String, - message: String, - )(implicit errorLoggingContext: ErrorLoggingContext): StatusRuntimeException = - RequestValidationErrors.InvalidField - .Reject(fieldName = fieldName, message = message) - .asGrpcError - - def invalidContinuationToken(implicit - errorLoggingContext: ErrorLoggingContext - ): StatusRuntimeException = - RequestValidationErrors.InvalidContinuationToken - .Reject() - .asGrpcError - - def invalidAcsPageToken( - message: String - )(implicit errorLoggingContext: ErrorLoggingContext): StatusRuntimeException = - RequestValidationErrors.InvalidAcsPageToken - .Reject(message) - .asGrpcError -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValueValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValueValidator.scala deleted file mode 100644 index 73ef8ef24d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/validation/ValueValidator.scala +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.value as api -import com.daml.ledger.api.v2.value.Value.Sum -import com.digitalasset.canton.ledger.api.Value -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.data.* -import com.digitalasset.daml.lf.value.Value as Lf -import com.digitalasset.daml.lf.value.Value.{ContractId, ValueUnit} -import io.grpc.StatusRuntimeException -import scalaz.std.either.* -import scalaz.syntax.bifunctor.* - -abstract class ValueValidator { - - import ValidationErrors.* - - protected def validateNumeric(s: String): Option[Numeric] - - private[validation] def validateRecordFields( - recordFields: Seq[api.RecordField] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ImmArray[(Option[Ref.Name], Value)]] = - recordFields - .foldLeft[Either[StatusRuntimeException, BackStack[(Option[Ref.Name], Value)]]]( - Right(BackStack.empty) - ) { (acc, rf) => - for { - fields <- acc - v <- requirePresence(rf.value, "value") - value <- validateValue(v) - label <- if (rf.label.isEmpty) Right(None) else requireIdentifier(rf.label).map(Some(_)) - } yield fields :+ label -> value - } - .map(_.toImmArray) - - def validateRecord(rec: api.Record)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Lf.ValueRecord] = - for { - recId <- validateOptionalIdentifier(rec.recordId) - fields <- validateRecordFields(rec.fields) - } yield Lf.ValueRecord(recId, fields) - - def validateValue(v0: api.Value)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Value] = v0.sum match { - case Sum.ContractId(cId) => - ContractId - .fromString(cId) - .bimap(invalidArgument, Lf.ValueContractId(_)) - case Sum.Numeric(value) => - validateNumeric(value) match { - case Some(numeric) => - Right(Lf.ValueNumeric(numeric)) - case None => - Left(invalidArgument(s"""Could not read Numeric string "$value"""")) - } - case Sum.Party(party) => - Ref.Party - .fromString(party) - .left - .map(invalidArgument) - .map(Lf.ValueParty.apply) - case Sum.Bool(b) => Right(Lf.ValueBool(b)) - case Sum.Timestamp(micros) => - Time.Timestamp - .fromLong(micros) - .left - .map(invalidArgument) - .map(Lf.ValueTimestamp.apply) - case Sum.Date(days) => - Time.Date - .fromDaysSinceEpoch(days) - .left - .map(invalidArgument) - .map(Lf.ValueDate.apply) - case Sum.Text(text) => Right(Lf.ValueText(text)) - case Sum.Int64(value) => Right(Lf.ValueInt64(value)) - case Sum.Record(rec) => - validateRecord(rec) - case Sum.Variant(api.Variant(variantId, constructor, value)) => - for { - validatedVariantId <- validateOptionalIdentifier(variantId) - validatedConstructor <- requireName(constructor, "constructor") - v <- requirePresence(value, "value") - validatedValue <- validateValue(v) - } yield Lf.ValueVariant(validatedVariantId, validatedConstructor, validatedValue) - case Sum.Enum(api.Enum(enumId, value)) => - for { - validatedEnumId <- validateOptionalIdentifier(enumId) - validatedValue <- requireName(value, "value") - } yield Lf.ValueEnum(validatedEnumId, validatedValue) - case Sum.List(api.List(elems)) => - elems - .foldLeft[Either[StatusRuntimeException, BackStack[Value]]](Right(BackStack.empty))( - (valuesE, v) => - for { - values <- valuesE - validatedValue <- validateValue(v) - } yield values :+ validatedValue - ) - .map(elements => Lf.ValueList(elements.toFrontStack)) - case _: Sum.Unit => Right(ValueUnit) - case Sum.Optional(o) => - o.value.fold[Either[StatusRuntimeException, Value]](Right(Lf.ValueNone))( - validateValue(_).map(v => Lf.ValueOptional(Some(v))) - ) - case Sum.TextMap(textMap0) => - val map = textMap0.entries - .foldLeft[Either[StatusRuntimeException, FrontStack[(String, Value)]]]( - Right(FrontStack.empty) - ) { case (acc, api.TextMap.Entry(key, value0)) => - for { - tail <- acc - v <- requirePresence(value0, "value") - validatedValue <- validateValue(v) - } yield (key -> validatedValue) +: tail - } - for { - entries <- map - map <- SortedLookupList - .fromImmArray(entries.toImmArray) - .left - .map(invalidArgument) - } yield Lf.ValueTextMap(map) - - case Sum.GenMap(genMap0) => - val genMap = genMap0.entries - .foldLeft[Either[StatusRuntimeException, BackStack[(Value, Value)]]]( - Right(BackStack.empty) - ) { case (acc, api.GenMap.Entry(key0, value0)) => - for { - stack <- acc - key <- requirePresence(key0, "key") - value <- requirePresence(value0, "value") - validatedKey <- validateValue(key) - validatedValue <- validateValue(value) - } yield stack :+ (validatedKey -> validatedValue) - } - genMap.map(entries => Lf.ValueGenMap(entries.toImmArray)) - - case Sum.Empty => Left(missingField("value")) - } - - private[validation] def validateOptionalIdentifier( - variantIdO: Option[api.Identifier] - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[Ref.Identifier]] = - variantIdO.map(validateIdentifier(_).map(Some.apply)).getOrElse(Right(None)) - - def requireIdentifier(s: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.Name] = - Ref.Name.fromString(s).left.map(invalidArgument) - - def requireNonEmptyParsedId[T](parser: String => Either[String, T])( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, T] = - if (s.isEmpty) - Left(missingField(fieldName)) - else - parser(s).left.map(invalidField(fieldName, _)) - - def requireName( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.Name] = - requireNonEmptyParsedId(Ref.Name.fromString)(s, fieldName) - - def requirePackageId( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.PackageId] = - requireNonEmptyParsedId(Ref.PackageId.fromString)(s, fieldName) - - def requireDottedName( - s: String, - fieldName: String, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.DottedName] = - Ref.DottedName.fromString(s).left.map(invalidField(fieldName, _)) - - def requirePresence[T](option: Option[T], fieldName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, T] = - option.fold[Either[StatusRuntimeException, T]]( - Left(missingField(fieldName)) - )(Right(_)) - - def validateIdentifier(identifier: api.Identifier)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.Identifier] = - for { - qualifiedName <- validateTemplateQualifiedName(identifier.moduleName, identifier.entityName) - packageId <- requirePackageId(identifier.packageId, "package_id") - } yield Ref.Identifier(packageId, qualifiedName) - - def validateTemplateQualifiedName(moduleName: String, entityName: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Ref.QualifiedName] = - for { - mn <- requireDottedName(moduleName, "module_name") - en <- requireDottedName(entityName, "entity_name") - } yield Ref.QualifiedName(mn, en) - -} - -// Standard version of the Validator use by the ledger API -object ValueValidator extends ValueValidator { - - private[this] val validNumericPattern = - """[+-]?\d{1,38}(\.\d{0,37})?""".r.pattern - - protected override def validateNumeric(s: String): Option[Numeric] = - if (validNumericPattern.matcher(s).matches()) - Numeric.fromUnscaledBigDecimal(new java.math.BigDecimal(s)).toOption - else - None - -} - -// Version of the ValueValidator that is stricter for syntax for Numeric but preserves their precision. -// Use by canton's Repair service -object StricterValueValidator extends ValueValidator { - protected override def validateNumeric(s: String): Option[Numeric] = - Numeric.fromString(s).toOption -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/GrpcChannel.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/GrpcChannel.scala deleted file mode 100644 index b7d2472c96..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/GrpcChannel.scala +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client - -import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner} -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.client.configuration.LedgerClientChannelConfiguration -import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder -import io.grpc.{Channel, ManagedChannel} - -import java.net.InetAddress -import java.util.concurrent.TimeUnit -import scala.concurrent.Future - -object GrpcChannel { - - final class Owner(builder: NettyChannelBuilder) extends ResourceOwner[ManagedChannel] { - def this(port: Int, configuration: LedgerClientChannelConfiguration) = - this( - configuration.builderFor(InetAddress.getLoopbackAddress.getHostAddress, port) - ) - - override def acquire()(implicit context: ResourceContext): Resource[ManagedChannel] = - Resource(Future(builder.build()))(channel => - Future { - channel.shutdownNow().discard - } - ) - } - - def withShutdownHook( - builder: NettyChannelBuilder - ): ManagedChannel = { - val channel = builder.build() - (sys.addShutdownHook { - DiscardOps(channel.shutdownNow()).discard - }).discard - channel - } - - def close(channel: Channel): Unit = - channel match { - case channel: ManagedChannel => - // This includes closing active connections. - channel.shutdownNow() - channel.awaitTermination(Long.MaxValue, TimeUnit.SECONDS) - () - case _ => // do nothing - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala deleted file mode 100644 index 4ce07c2916..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client - -import com.daml.grpc.AuthCallCredentials.authorizingStub -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.daml.ledger.api.v2.admin.identity_provider_config_service.IdentityProviderConfigServiceGrpc -import com.daml.ledger.api.v2.admin.package_management_service.PackageManagementServiceGrpc -import com.daml.ledger.api.v2.admin.participant_pruning_service.ParticipantPruningServiceGrpc -import com.daml.ledger.api.v2.admin.party_management_service.PartyManagementServiceGrpc -import com.daml.ledger.api.v2.admin.user_management_service.UserManagementServiceGrpc -import com.daml.ledger.api.v2.command_service.CommandServiceGrpc as CommandServiceGrpcV2 -import com.daml.ledger.api.v2.event_query_service.EventQueryServiceGrpc -import com.daml.ledger.api.v2.package_service.PackageServiceGrpc as PackageServiceGrpcV2 -import com.daml.ledger.api.v2.state_service.StateServiceGrpc -import com.daml.ledger.api.v2.trace_context.TraceContext as LedgerApiTraceContext -import com.daml.ledger.api.v2.update_service.UpdateServiceGrpc -import com.daml.ledger.api.v2.version_service.VersionServiceGrpc -import com.digitalasset.canton.ledger.client.LedgerClient.stubWithTracing -import com.digitalasset.canton.ledger.client.configuration.{ - LedgerClientChannelConfiguration, - LedgerClientConfiguration, -} -import com.digitalasset.canton.ledger.client.services.EventQueryServiceClient -import com.digitalasset.canton.ledger.client.services.admin.* -import com.digitalasset.canton.ledger.client.services.commands.CommandServiceClient -import com.digitalasset.canton.ledger.client.services.pkg.PackageClient -import com.digitalasset.canton.ledger.client.services.state.StateServiceClient -import com.digitalasset.canton.ledger.client.services.updates.UpdateServiceClient -import com.digitalasset.canton.ledger.client.services.version.VersionClient -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc, W3CTraceContext} -import io.grpc.Channel -import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder -import io.grpc.stub.AbstractStub - -import java.io.Closeable -import scala.annotation.unused -import scala.concurrent.{ExecutionContext, Future} - -/** GRPC client for the Canton Ledger API. - * - * Tracing support: we use CallOptions, see [[com.digitalasset.canton.tracing.TraceContextGrpc]] - */ -final class LedgerClient private ( - val channel: Channel, - config: LedgerClientConfiguration, - @unused - loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext, esf: ExecutionSequencerFactory) - extends Closeable { - - lazy val commandService = new CommandServiceClient( - CommandServiceGrpcV2.stub(channel), - config.token, - ) - lazy val eventQueryService = new EventQueryServiceClient( - EventQueryServiceGrpc.stub(channel), - config.token, - ) - lazy val packageService = new PackageClient( - PackageServiceGrpcV2.stub(channel), - config.token, - ) - - lazy val stateService = new StateServiceClient( - StateServiceGrpc.stub(channel), - config.token, - ) - - lazy val updateService = new UpdateServiceClient( - UpdateServiceGrpc.stub(channel), - config.token, - ) - - lazy val versionClient: VersionClient = - new VersionClient(VersionServiceGrpc.stub(channel), config.token) - - lazy val identityProviderConfigClient: IdentityProviderConfigClient = - new IdentityProviderConfigClient( - IdentityProviderConfigServiceGrpc.stub(channel), - config.token, - ) - - lazy val packageManagementClient: PackageManagementClient = - new PackageManagementClient( - PackageManagementServiceGrpc.stub(channel), - config.token, - ) - - lazy val partyManagementClient: PartyManagementClient = - new PartyManagementClient( - PartyManagementServiceGrpc.stub(channel), - config.token, - ) - - lazy val userManagementClient: UserManagementClient = - new UserManagementClient( - UserManagementServiceGrpc.stub(channel), - config.token, - ) - - lazy val participantPruningManagementClient: ParticipantPruningManagementClient = - new ParticipantPruningManagementClient( - ParticipantPruningServiceGrpc.stub(channel), - config.token, - ) - - override def close(): Unit = GrpcChannel.close(channel) - - def serviceClient[A <: AbstractStub[A]](stub: Channel => A, token: Option[String])(implicit - traceContext: TraceContext - ): A = - stubWithTracing(stub(channel), token) -} - -object LedgerClient { - - def apply( - channel: Channel, - config: LedgerClientConfiguration, - loggerFactory: NamedLoggerFactory, - )(implicit - ec: ExecutionContext, - esf: ExecutionSequencerFactory, - traceContext: TraceContext, - ): Future[LedgerClient] = - for { - // requesting ledger end validates the token, thus guaranteeing that the client is operable - _ <- new StateServiceClient( - StateServiceGrpc.stub(channel) - ).getLedgerEnd(config.token()) - } yield new LedgerClient(channel, config, loggerFactory) - - def withoutToken( - channel: Channel, - config: LedgerClientConfiguration, - loggerFactory: NamedLoggerFactory, - )(implicit ec: ExecutionContext, esf: ExecutionSequencerFactory): LedgerClient = - new LedgerClient(channel, config, loggerFactory) - - private[client] def stub[A <: AbstractStub[A]](stub: A, token: Option[String]): A = - token.fold(stub)( - authorizingStub(stub, _).withInterceptors(TraceContextGrpc.clientInterceptor()) - ) - - private[client] def stubWithTracing[A <: AbstractStub[A]](stub: A, token: Option[String])(implicit - traceContext: TraceContext - ): A = - token - .fold(stub)(authorizingStub(stub, _)) - .withInterceptors(TraceContextGrpc.clientInterceptor()) - .withOption(TraceContextGrpc.TraceContextCallOptionKey, traceContext) - - /** A convenient shortcut to build a [[LedgerClient]], use [[fromBuilder]] for a more flexible - * alternative. - */ - def singleHost( - hostIp: String, - port: Int, - configuration: LedgerClientConfiguration, - channelConfig: LedgerClientChannelConfiguration, - loggerFactory: NamedLoggerFactory, - )(implicit - ec: ExecutionContext, - esf: ExecutionSequencerFactory, - traceContext: TraceContext, - ): Future[LedgerClient] = - fromBuilder(channelConfig.builderFor(hostIp, port), configuration, loggerFactory) - - def insecureSingleHost( - hostIp: String, - port: Int, - configuration: LedgerClientConfiguration, - loggerFactory: NamedLoggerFactory, - )(implicit - ec: ExecutionContext, - esf: ExecutionSequencerFactory, - traceContext: TraceContext, - ): Future[LedgerClient] = - fromBuilder( - LedgerClientChannelConfiguration.InsecureDefaults.builderFor(hostIp, port), - configuration, - loggerFactory, - ) - - /** Takes a [[io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder]], possibly set up with some - * relevant extra options that cannot be specified though the - * [[com.digitalasset.canton.ledger.client.configuration.LedgerClientConfiguration]] (e.g. a set - * of default [[io.grpc.CallCredentials]] to be used with all calls unless explicitly set on a - * per-call basis), sets the relevant options specified by the configuration (possibly overriding - * the existing builder settings), and returns a [[LedgerClient]]. - * - * A shutdown hook is also added to close the channel when the JVM stops. - */ - def fromBuilder( - builder: NettyChannelBuilder, - configuration: LedgerClientConfiguration, - loggerFactory: NamedLoggerFactory, - )(implicit - ec: ExecutionContext, - esf: ExecutionSequencerFactory, - traceContext: TraceContext, - ): Future[LedgerClient] = - LedgerClient( - GrpcChannel.withShutdownHook(builder), - configuration, - loggerFactory, - ) - - /** Extract a trace context from a transaction and represent it as our TraceContext */ - def traceContextFromLedgerApi(traceContext: Option[LedgerApiTraceContext]): TraceContext = - traceContext match { - case Some(LedgerApiTraceContext(Some(parent), state)) => - W3CTraceContext(parent, state).toTraceContext - case _ => TraceContext.withNewTraceContext("ledger_api")(identity) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClientUtils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClientUtils.scala deleted file mode 100644 index 820edcf8de..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClientUtils.scala +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client - -import com.digitalasset.base.error.utils.DecodedCantonError -import com.google.rpc.code.Code -import com.google.rpc.status.Status - -import scala.concurrent.duration.{DurationInt, FiniteDuration} - -object LedgerClientUtils { - - /** Default retry rules which will retry on retryable known errors and if the ledger api is - * unavailable - */ - def defaultRetryRules: Status => Option[FiniteDuration] = status => - DecodedCantonError - .fromGrpcStatus(status) - .toOption - .flatMap(_.retryIn) - .orElse { - Option.when( - status.code == Code.UNAVAILABLE.value || status.code == Code.DEADLINE_EXCEEDED.value - )(1.second) - } - - /** Convert codegen command to scala proto command */ - def javaCodegenToScalaProto( - command: com.daml.ledger.javaapi.data.Command - ): com.daml.ledger.api.v2.commands.Command = - com.daml.ledger.api.v2.commands.Command.fromJavaProto(command.toProtoCommand) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerSubscription.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerSubscription.scala deleted file mode 100644 index aaa4e4cf61..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerSubscription.scala +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client - -import com.digitalasset.canton.config.ProcessingTimeout -import com.digitalasset.canton.lifecycle.{ - AsyncCloseable, - AsyncOrSyncCloseable, - FlagCloseableAsync, - SyncCloseable, -} -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.util.PekkoUtil -import io.grpc.StatusRuntimeException -import org.apache.pekko.stream.scaladsl.{Flow, Keep, Sink, Source} -import org.apache.pekko.stream.{KillSwitches, Materializer} -import org.apache.pekko.{Done, NotUsed} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -trait LedgerSubscription extends FlagCloseableAsync with NamedLogging { - val completed: Future[Done] -} - -object LedgerSubscription { - - def makeSubscription[S, T]( - source: Source[S, NotUsed], - consumingFlow: Flow[S, T, ?], - subscriptionName: String, - processingTimeouts: ProcessingTimeout, - namedLoggerFactory: NamedLoggerFactory, - )(implicit materializer: Materializer, executionContext: ExecutionContext): LedgerSubscription = - new LedgerSubscription { - override protected def timeouts: ProcessingTimeout = processingTimeouts - - import com.digitalasset.canton.tracing.TraceContext.Implicits.Empty.* - - override val loggerFactory: NamedLoggerFactory = - if (subscriptionName.isEmpty) - namedLoggerFactory - else - namedLoggerFactory.appendUnnamedKey( - "subscription", - subscriptionName, - ) - - val (killSwitch, completed) = PekkoUtil.runSupervised( - source - // we place the kill switch before the map operator, such that - // we can shut down the operator quickly and signal upstream to cancel further sending - .viaMat(KillSwitches.single)(Keep.right) - .viaMat(consumingFlow)(Keep.left) - // and we get the Future[Done] as completed from the sink so we know when the last message - // was processed - .toMat(Sink.ignore)(Keep.both), - errorLogMessagePrefix = "Fatally failed to handle transaction", - ) - - override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = { - import com.digitalasset.canton.tracing.TraceContext.Implicits.Empty.* - List[AsyncOrSyncCloseable]( - SyncCloseable(s"killSwitch.shutdown $subscriptionName", killSwitch.shutdown()), - AsyncCloseable( - s"graph.completed $subscriptionName", - completed.transform { - case Success(v) => Success(v) - case Failure(_: StatusRuntimeException) => - // don't fail to close if there was a grpc status runtime exception - // this can happen (i.e. server not available etc.) - Success(Done) - case Failure(ex) => Failure(ex) - }, - processingTimeouts.shutdownShort, - ), - ) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/ResilientLedgerSubscription.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/ResilientLedgerSubscription.scala deleted file mode 100644 index 4cdc06fe7a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/ResilientLedgerSubscription.scala +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client - -import com.daml.ledger.api.v2.update_service.GetUpdatesResponse -import com.daml.ledger.api.v2.update_service.GetUpdatesResponse.Update -import com.digitalasset.base.error.utils.DecodedCantonError -import com.digitalasset.canton.config.ProcessingTimeout -import com.digitalasset.canton.ledger.error.LedgerApiErrors -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.lifecycle.* -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.{NoTracing, Spanning, TraceContext} -import com.digitalasset.canton.util.Thereafter.syntax.ThereafterOps -import com.digitalasset.canton.util.TryUtil.ForFailedOps -import com.digitalasset.canton.util.retry.AllExceptionRetryPolicy -import com.digitalasset.canton.util.{FutureUtil, retry} -import io.grpc.StatusRuntimeException -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.{Flow, Source} -import org.slf4j.event.Level - -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContextExecutor, Future} -import scala.util.{Failure, Success, Try} - -/** Resilient ledger subscriber, which keeps continuously re-subscribing (on failure) to the Ledger - * API transaction stream and applies the received transactions to the `processTransaction` - * function. - * - * `processTransaction` must not throw. If it does, it must be idempotent (i.e. allow re-processing - * the same transaction twice). - */ -class ResilientLedgerSubscription[S, T]( - makeSource: Long => Source[S, NotUsed], - consumingFlow: Flow[S, T, ?], - subscriptionName: String, - startOffset: Long, - extractOffset: S => Option[Long], - val timeouts: ProcessingTimeout, - val loggerFactory: NamedLoggerFactory, - resubscribeIfPruned: Boolean = false, -)(implicit - ec: ExecutionContextExecutor, - materializer: Materializer, -) extends FlagCloseableAsync - with NamedLogging - with Spanning - with NoTracing { - private val offsetRef = new AtomicReference[Long](startOffset) - private implicit val policyRetry: retry.Success[Any] = retry.Success.always - private val ledgerSubscriptionRef = new AtomicReference[Option[LedgerSubscription]](None) - private[client] val subscriptionF = retry - .Backoff( - logger = logger, - hasSynchronizeWithClosing = this, - maxRetries = retry.Forever, - initialDelay = 1.second, - maxDelay = 5.seconds, - operationName = s"restartable-$subscriptionName", - ) - .apply(resilientSubscription(), AllExceptionRetryPolicy) - - runOnOrAfterClose_(new RunOnClosing { - override def name: String = s"$subscriptionName-shutdown" - - override def done: Boolean = - // Use isClosing to avoid task eviction at the beginning (see runOnShutdown) - isClosing && ledgerSubscriptionRef.get().forall(_.completed.isCompleted) - - override def run()(implicit traceContext: TraceContext): Unit = - ledgerSubscriptionRef.getAndSet(None).foreach(LifeCycle.close(_)(logger)) - }) - - override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = { - val name = s"wait-for-$subscriptionName-completed" - Seq( - AsyncCloseable( - name, - subscriptionF.recover { error => - logger.warn(s"$name finished with an error", error) - () - }, - timeouts.closing, - ) - ) - } - - private def resilientSubscription(): Future[Unit] = - FutureUtil.logOnFailure( - future = { - val newSubscription = createLedgerSubscription() - ledgerSubscriptionRef.set(Some(newSubscription)) - // Check closing again to ensure closing of the new subscription - // in case the shutdown happened before the after the first closing check - // but before the previous reference update - if (isClosing) { - ledgerSubscriptionRef.getAndSet(None).foreach(closeSubscription) - newSubscription.completed.map(_ => ()) - } else { - newSubscription.completed - .map(_ => ()) - .thereafter { result => - // This closing races with the one from runOnShutdown so use getAndSet - // to ensure calling close only once on a subscription - ledgerSubscriptionRef.getAndSet(None).foreach(closeSubscription) - result.forFailed(handlePrunedDataAccessed) - } - } - }, - failureMessage = s"${subject(capitalized = true)} failed with an error", - level = Level.WARN, - ) - - private def closeSubscription(ledgerSubscription: LedgerSubscription): Unit = - Try(ledgerSubscription.close()) match { - case Failure(exception) => - logger.warn( - s"${subject(capitalized = true)} [$ledgerSubscription] failed to close successfully", - exception, - ) - case Success(_) => - logger.info( - s"Successfully closed ${subject(capitalized = false)} [$ledgerSubscription] closed successfully" - ) - } - - private def subject(capitalized: Boolean) = - s"${if (capitalized) "Ledger" else "ledger"} subscription $subscriptionName" - - private def createLedgerSubscription(): LedgerSubscription = { - val currentOffset = offsetRef.get() - logger.debug( - s"Creating new transactions ${subject(capitalized = false)} starting at offset $currentOffset" - ) - LedgerSubscription.makeSubscription( - makeSource(currentOffset), - Flow[S] - .map { item => - extractOffset(item).foreach(offsetRef.set) - item - } - .via(consumingFlow), - subscriptionName, - timeouts, - loggerFactory, - ) - } - - private def handlePrunedDataAccessed: Throwable => Unit = { - case sre: StatusRuntimeException => - DecodedCantonError - .fromStatusRuntimeException(sre) - .toOption - .filter(_.code.id == RequestValidationErrors.ParticipantPrunedDataAccessed.id) - .flatMap(_.context.get(LedgerApiErrors.EarliestOffsetMetadataKey)) - .foreach { earliestOffset => - if (resubscribeIfPruned) { - logger.warn( - s"Setting the ${subject(capitalized = false)} offset to a later offset [$earliestOffset] due to pruning. Some commands might timeout or events might become stale." - ) - offsetRef.set(earliestOffset.toLong) - } else { - logger.error( - s"Connection ${subject(capitalized = false)} failed to resubscribe from ${offsetRef - .get()}, while earliest offset is [$earliestOffset] due to pruning." - ) - close() - } - } - case _ => - // Do nothing for other errors - () - } -} - -object ResilientLedgerSubscription { - def extractOffsetFromGetUpdateResponse(response: GetUpdatesResponse): Option[Long] = - response.update match { - case Update.Transaction(value) => - Some(value.offset) - case Update.Reassignment(value) => - Some(value.offset) - case Update.OffsetCheckpoint(value) => - Some(value.offset) - case Update.TopologyTransaction(value) => - Some(value.offset) - case Update.Empty => None - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/CommandClientConfiguration.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/CommandClientConfiguration.scala deleted file mode 100644 index 606e80a257..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/CommandClientConfiguration.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.configuration - -import java.time.Duration - -/** @param maxCommandsInFlight - * The maximum number of unconfirmed commands the client may track. The client will backpressure - * when this number is reached. - * @param maxParallelSubmissions - * The maximum number of parallel command submissions at a given time. The client will - * backpressure when this number is reached. - * @param defaultDeduplicationTime - * The deduplication time to use for commands that do not have a deduplication time set. The - * deduplication time is also used as the time after which commands time out in the command - * client. - */ -final case class CommandClientConfiguration( - maxCommandsInFlight: Int, - maxParallelSubmissions: Int, - defaultDeduplicationTime: Duration, -) - -object CommandClientConfiguration { - def default: CommandClientConfiguration = CommandClientConfiguration( - maxCommandsInFlight = 1, - maxParallelSubmissions = 1, - defaultDeduplicationTime = Duration.ofSeconds(30L), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/LedgerClientChannelConfiguration.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/LedgerClientChannelConfiguration.scala deleted file mode 100644 index 309e6e3371..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/LedgerClientChannelConfiguration.scala +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.configuration - -import com.digitalasset.canton.config.RequireTypes.PositiveInt -import io.grpc.internal.GrpcUtil -import io.grpc.netty.shaded.io.grpc.netty.{NegotiationType, NettyChannelBuilder} -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext - -/** @param sslContext - * If defined, the context will be passed on to the underlying gRPC code to ensure the - * communication channel is secured by TLS - * @param maxInboundMetadataSize - * The maximum size of the response headers. - * @param maxInboundMessageSize - * The maximum (uncompressed) size of the response body. - */ -final case class LedgerClientChannelConfiguration( - sslContext: Option[SslContext], - maxInboundMetadataSize: Int = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, - maxInboundMessageSize: Int = LedgerClientChannelConfiguration.DefaultMaxInboundMessageSize, - flowControlWindow: PositiveInt = LedgerClientChannelConfiguration.DefaultFlowControlWindow, -) { - - def builderFor(host: String, port: Int): NettyChannelBuilder = { - val builder = NettyChannelBuilder - .forAddress(host, port) - sslContext - .fold(builder.usePlaintext())(builder.sslContext(_).negotiationType(NegotiationType.TLS)) - .maxInboundMetadataSize(maxInboundMetadataSize) - .maxInboundMessageSize(maxInboundMessageSize) - .flowControlWindow(flowControlWindow.unwrap) - } - -} - -object LedgerClientChannelConfiguration { - - val DefaultMaxInboundMessageSize: Int = 10 * 1024 * 1024 - val DefaultFlowControlWindow: PositiveInt = - PositiveInt.tryCreate(1024 * 1024) // 1mb instead of 64kb - val InsecureDefaults: LedgerClientChannelConfiguration = - LedgerClientChannelConfiguration(sslContext = None) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/LedgerClientConfiguration.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/LedgerClientConfiguration.scala deleted file mode 100644 index d9e9bd9dd2..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/configuration/LedgerClientConfiguration.scala +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.configuration - -/** @param userId - * The string that will be used as an user identifier when issuing commands and retrieving - * transactions - * @param commandClient - * The [[CommandClientConfiguration]] that defines how the command client should be setup with - * regards to timeouts, commands in-flight and command TTL - * @param token - * If defined, the access token that will be passed by default, unless overridden in individual - * calls (mostly useful for short-lived applications) - */ -final case class LedgerClientConfiguration( - userId: String, - commandClient: CommandClientConfiguration, - token: () => Option[String] = () => None, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/EventQueryServiceClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/EventQueryServiceClient.scala deleted file mode 100644 index 8971619f96..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/EventQueryServiceClient.scala +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services - -import com.daml.ledger.api.v2.event_query_service.EventQueryServiceGrpc.EventQueryServiceStub -import com.daml.ledger.api.v2.event_query_service.{ - GetEventsByContractIdRequest, - GetEventsByContractIdResponse, -} -import com.daml.ledger.api.v2.transaction_filter.{EventFormat, Filters} -import com.digitalasset.canton.ledger.client.LedgerClient - -import scala.concurrent.Future - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -class EventQueryServiceClient( - service: EventQueryServiceStub, - getDefaultToken: () => Option[String] = () => None, -) { - def getEventsByContractId( - contractId: String, - requestingParties: Seq[String], - token: Option[String] = None, - ): Future[GetEventsByContractIdResponse] = { - val eventFormat = EventFormat( - filtersByParty = requestingParties.map(_ -> Filters(Nil)).toMap, - filtersForAnyParty = None, - verbose = true, - ) - - LedgerClient - .stub(service, token.orElse(getDefaultToken())) - .getEventsByContractId( - GetEventsByContractIdRequest( - contractId = contractId, - eventFormat = Some(eventFormat), - ) - ) - } - -// TODO(#16065) -// def getEventsByContractKey( -// contractKey: com.daml.ledger.api.v2.value.Value, -// templateId: Identifier, -// requestingParties: Seq[String], -// continuationToken: String, -// token: Option[String] = None, -// ): Future[GetEventsByContractKeyResponse] = -// LedgerClient -// .stub(service, token.orElse(getDefaultToken()))) -// .getEventsByContractKey( -// GetEventsByContractKeyRequest( -// contractKey = Some(contractKey), -// templateId = Some(templateId), -// requestingParties = requestingParties, -// continuationToken = continuationToken, -// ) -// ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/IdentityProviderConfigClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/IdentityProviderConfigClient.scala deleted file mode 100644 index 36cd4894c2..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/IdentityProviderConfigClient.scala +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.admin - -import com.daml.jwt.JwksUrl -import com.daml.ledger.api.v2.admin.identity_provider_config_service as proto -import com.daml.ledger.api.v2.admin.identity_provider_config_service.IdentityProviderConfigServiceGrpc.IdentityProviderConfigServiceStub -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import com.google.protobuf.field_mask.FieldMask - -import scala.concurrent.{ExecutionContext, Future} - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -final class IdentityProviderConfigClient( - service: IdentityProviderConfigServiceStub, - getDefaultToken: () => Option[String] = () => None, -)(implicit - ec: ExecutionContext -) { - - import IdentityProviderConfigClient.* - def createIdentityProviderConfig( - config: IdentityProviderConfig, - token: Option[String], - )(implicit traceContext: TraceContext): Future[IdentityProviderConfig] = { - val request = proto.CreateIdentityProviderConfigRequest( - Some(IdentityProviderConfigClient.toProtoConfig(config)) - ) - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .createIdentityProviderConfig(request) - .map(res => fromProtoConfig(res.getIdentityProviderConfig)) - } - - def getIdentityProviderConfig( - identityProviderId: IdentityProviderId.Id, - token: Option[String], - )(implicit traceContext: TraceContext): Future[IdentityProviderConfig] = { - val request = proto.GetIdentityProviderConfigRequest(identityProviderId.toRequestString) - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getIdentityProviderConfig(request) - .map(res => fromProtoConfig(res.getIdentityProviderConfig)) - } - - def updateIdentityProviderConfig( - config: IdentityProviderConfig, - updateMask: FieldMask, - token: Option[String], - )(implicit traceContext: TraceContext): Future[IdentityProviderConfig] = { - val request = proto.UpdateIdentityProviderConfigRequest( - Some(IdentityProviderConfigClient.toProtoConfig(config)), - Some(updateMask), - ) - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .updateIdentityProviderConfig(request) - .map(res => fromProtoConfig(res.getIdentityProviderConfig)) - } - - def listIdentityProviderConfigs( - token: Option[String] - )(implicit traceContext: TraceContext): Future[Seq[IdentityProviderConfig]] = { - val request = proto.ListIdentityProviderConfigsRequest() - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .listIdentityProviderConfigs(request) - .map(res => res.identityProviderConfigs.map(fromProtoConfig)) - } - - def deleteIdentityProviderConfig( - identityProviderId: IdentityProviderId.Id, - token: Option[String], - )(implicit traceContext: TraceContext): Future[Unit] = { - val request = proto.DeleteIdentityProviderConfigRequest(identityProviderId.toRequestString) - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .deleteIdentityProviderConfig(request) - .map(_ => ()) - } - - def serviceStub(token: Option[String] = None)(implicit - traceContext: TraceContext - ): IdentityProviderConfigServiceStub = - LedgerClient.stubWithTracing(service, token.orElse(getDefaultToken())) - -} - -object IdentityProviderConfigClient { - def toProtoConfig(config: IdentityProviderConfig): proto.IdentityProviderConfig = - proto.IdentityProviderConfig( - config.identityProviderId.toRequestString, - config.isDeactivated, - config.issuer, - config.jwksUrl.value, - config.audience.getOrElse(""), - ) - - def fromProtoConfig(config: proto.IdentityProviderConfig): IdentityProviderConfig = - IdentityProviderConfig( - IdentityProviderId.Id(Ref.LedgerString.assertFromString(config.identityProviderId)), - config.isDeactivated, - JwksUrl.assertFromString(config.jwksUrl), - config.issuer, - Option(config.audience).filter(_.trim.nonEmpty), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/PackageManagementClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/PackageManagementClient.scala deleted file mode 100644 index 8a96a2b673..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/PackageManagementClient.scala +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.admin - -import com.daml.ledger.api.v2.admin.package_management_service.PackageManagementServiceGrpc.PackageManagementServiceStub -import com.daml.ledger.api.v2.admin.package_management_service.{ - ListKnownPackagesRequest, - PackageDetails, - UpdateVettedPackagesRequest, - UpdateVettedPackagesResponse, - UploadDarFileRequest, - ValidateDarFileRequest, -} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext -import com.google.protobuf.ByteString - -import scala.concurrent.{ExecutionContext, Future} - -object PackageManagementClient { - - private val listKnownPackagesRequest = ListKnownPackagesRequest() - -} - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -final class PackageManagementClient( - service: PackageManagementServiceStub, - getDefaultToken: () => Option[String] = () => None, -)(implicit - ec: ExecutionContext -) { - - def listKnownPackages( - token: Option[String] = None - )(implicit traceContext: TraceContext): Future[Seq[PackageDetails]] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .listKnownPackages(PackageManagementClient.listKnownPackagesRequest) - .map(_.packageDetails) - - def uploadDarFile( - darFile: ByteString, - token: Option[String] = None, - vetAllPackages: Boolean = true, - synchronizerId: Option[String] = None, - )(implicit traceContext: TraceContext): Future[Unit] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .uploadDarFile( - UploadDarFileRequest( - darFile = darFile, - submissionId = "", - vettingChange = - if (vetAllPackages) - UploadDarFileRequest.VettingChange.VETTING_CHANGE_VET_ALL_PACKAGES - else - UploadDarFileRequest.VettingChange.VETTING_CHANGE_DONT_VET_ANY_PACKAGES, - synchronizerId = synchronizerId.getOrElse(""), - ) - ) - .map(_ => ()) - - def validateDarFile( - darFile: ByteString, - token: Option[String] = None, - synchronizerId: Option[String] = None, - )(implicit traceContext: TraceContext): Future[Unit] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .validateDarFile( - ValidateDarFileRequest( - darFile = darFile, - submissionId = "", - synchronizerId = synchronizerId.getOrElse(""), - ) - ) - .map(_ => ()) - - def updateVettedPackages( - request: UpdateVettedPackagesRequest, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[UpdateVettedPackagesResponse] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .updateVettedPackages(request) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/ParticipantPruningManagementClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/ParticipantPruningManagementClient.scala deleted file mode 100644 index 0c28b4302b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/ParticipantPruningManagementClient.scala +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.admin - -import com.daml.ledger.api.v2.admin.participant_pruning_service.ParticipantPruningServiceGrpc.ParticipantPruningServiceStub -import com.daml.ledger.api.v2.admin.participant_pruning_service.{PruneRequest, PruneResponse} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.Future - -object ParticipantPruningManagementClient { - - private def pruneRequest(pruneUpTo: Long, submissionId: Option[String]) = - PruneRequest( - pruneUpTo = pruneUpTo, - submissionId = submissionId.getOrElse(""), - pruneAllDivulgedContracts = false, - ) - -} - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -final class ParticipantPruningManagementClient( - service: ParticipantPruningServiceStub, - getDefaultToken: () => Option[String] = () => None, -) { - - def prune( - pruneUpTo: Long, - token: Option[String] = None, - submissionId: Option[String] = None, - )(implicit traceContext: TraceContext): Future[PruneResponse] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .prune(ParticipantPruningManagementClient.pruneRequest(pruneUpTo, submissionId)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/PartyManagementClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/PartyManagementClient.scala deleted file mode 100644 index 12b8edb4e6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/PartyManagementClient.scala +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.admin - -import com.daml.ledger.api.v2.admin.party_management_service.PartyManagementServiceGrpc.PartyManagementServiceStub -import com.daml.ledger.api.v2.admin.party_management_service.{ - AllocatePartyRequest, - GetParticipantIdRequest, - GetPartiesRequest, - ListKnownPartiesRequest, - PartyDetails as ApiPartyDetails, -} -import com.digitalasset.canton.ledger.api.{ - IdentityProviderId, - ObjectMeta, - ParticipantId, - PartyDetails, -} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.Party -import scalaz.OneAnd - -import scala.concurrent.{ExecutionContext, Future} - -object PartyManagementClient { - - private def details(d: ApiPartyDetails): PartyDetails = - PartyDetails( - Party.assertFromString(d.party), - d.isLocal, - ObjectMeta.empty, - IdentityProviderId(d.identityProviderId), - ) - - private val getParticipantIdRequest = GetParticipantIdRequest() - - private def listKnownPartiesRequest(pageToken: String, pageSize: Int, filterParty: String) = - ListKnownPartiesRequest( - pageToken = pageToken, - pageSize = pageSize, - identityProviderId = "", - filterParty = filterParty, - ) - - private def getPartiesRequest(parties: OneAnd[Set, Ref.Party]) = { - import scalaz.std.iterable.* - import scalaz.syntax.foldable.* - GetPartiesRequest( - parties = parties.toList, - identityProviderId = "", - ) - } -} - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -final class PartyManagementClient( - service: PartyManagementServiceStub, - getDefaultToken: () => Option[String] = () => None, -)(implicit - ec: ExecutionContext -) { - - def getParticipantId( - token: Option[String] = None - )(implicit traceContext: TraceContext): Future[ParticipantId] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getParticipantId(PartyManagementClient.getParticipantIdRequest) - .map(r => ParticipantId(Ref.ParticipantId.assertFromString(r.participantId))) - - def listKnownParties( - token: Option[String] = None, - pageToken: String = "", - pageSize: Int = 1000, - filterParty: String = "", - )(implicit traceContext: TraceContext): Future[(List[PartyDetails], String)] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .listKnownParties( - PartyManagementClient - .listKnownPartiesRequest(pageToken, pageSize, filterParty = filterParty) - ) - .map(resp => - (resp.partyDetails.view.map(PartyManagementClient.details).toList, resp.nextPageToken) - ) - - def getParties( - parties: OneAnd[Set, Ref.Party], - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[List[PartyDetails]] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getParties(PartyManagementClient.getPartiesRequest(parties)) - .map(_.partyDetails.view.map(PartyManagementClient.details).toList) - - def allocateParty( - hint: Option[String], - synchronizerId: Option[String] = None, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[PartyDetails] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .allocateParty( - AllocatePartyRequest( - partyIdHint = hint.getOrElse(""), - localMetadata = None, - identityProviderId = "", - synchronizerId = synchronizerId.getOrElse(""), - userId = "", - ) - ) - .map(_.partyDetails.getOrElse(sys.error("No PartyDetails in response."))) - .map(PartyManagementClient.details) - - /** Utility method for json services - */ - def serviceStub(token: Option[String] = None)(implicit - traceContext: TraceContext - ): PartyManagementServiceStub = - LedgerClient.stubWithTracing(service, token.orElse(getDefaultToken())) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/UserManagementClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/UserManagementClient.scala deleted file mode 100644 index 3e9ee08732..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/admin/UserManagementClient.scala +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.admin - -import com.daml.ledger.api.v2.admin as admin_proto -import com.daml.ledger.api.v2.admin.user_management_service as proto -import com.daml.ledger.api.v2.admin.user_management_service.UserManagementServiceGrpc.UserManagementServiceStub -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta, User, UserRight} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{Party, UserId} - -import scala.concurrent.{ExecutionContext, Future} - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -final class UserManagementClient( - service: UserManagementServiceStub, - getDefaultToken: () => Option[String] = () => None, -)(implicit - ec: ExecutionContext -) { - import UserManagementClient.* - - def createUser( - user: User, - initialRights: Seq[UserRight] = List.empty, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[User] = { - val request = proto.CreateUserRequest( - Some(UserManagementClient.toProtoUser(user)), - initialRights.view.map(toProtoRight).toList, - ) - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .createUser(request) - .flatMap(res => fromOptionalProtoUser(res.user)) - } - - def getUser(userId: UserId, token: Option[String] = None, identityProviderId: String = "")( - implicit traceContext: TraceContext - ): Future[User] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getUser(proto.GetUserRequest(userId, identityProviderId)) - .flatMap(res => fromOptionalProtoUser(res.user)) - - /** Retrieve the User information for the user authenticated by the token(s) on the call . */ - def getAuthenticatedUser( - token: Option[String] = None - )(implicit traceContext: TraceContext): Future[User] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getUser( - proto.GetUserRequest( - userId = "", - identityProviderId = "", - ) - ) - .flatMap(res => fromOptionalProtoUser(res.user)) - - def deleteUser(userId: UserId, token: Option[String] = None, identityProviderId: String = "")( - implicit traceContext: TraceContext - ): Future[Unit] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .deleteUser(proto.DeleteUserRequest(userId, identityProviderId)) - .map(_ => ()) - - def listUsers( - token: Option[String] = None, - pageToken: String, - pageSize: Int, - identityProviderId: String = "", - )(implicit traceContext: TraceContext): Future[(Seq[User], String)] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .listUsers( - proto.ListUsersRequest( - pageToken = pageToken, - pageSize = pageSize, - identityProviderId = identityProviderId, - ) - ) - .map(res => res.users.view.map(fromProtoUser).toSeq -> res.nextPageToken) - - def grantUserRights( - userId: UserId, - rights: Seq[UserRight], - token: Option[String] = None, - identityProviderId: String = "", - )(implicit traceContext: TraceContext): Future[Seq[UserRight]] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .grantUserRights( - proto.GrantUserRightsRequest(userId, rights.map(toProtoRight), identityProviderId) - ) - .map(_.newlyGrantedRights.view.collect(fromProtoRight.unlift).toSeq) - - def revokeUserRights( - userId: UserId, - rights: Seq[UserRight], - token: Option[String] = None, - identityProviderId: String = "", - )(implicit traceContext: TraceContext): Future[Seq[UserRight]] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .revokeUserRights( - proto.RevokeUserRightsRequest(userId, rights.map(toProtoRight), identityProviderId) - ) - .map(_.newlyRevokedRights.view.collect(fromProtoRight.unlift).toSeq) - - /** List the rights of the given user. Unknown rights are ignored. - */ - def listUserRights(userId: UserId, token: Option[String] = None, identityProviderId: String = "")( - implicit traceContext: TraceContext - ): Future[Seq[UserRight]] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .listUserRights(proto.ListUserRightsRequest(userId, identityProviderId)) - .map(_.rights.view.collect(fromProtoRight.unlift).toSeq) - - /** Retrieve the rights of the user authenticated by the token(s) on the call . Unknown rights are - * ignored. - */ - def listAuthenticatedUserRights( - token: Option[String] = None - )(implicit traceContext: TraceContext): Future[Seq[UserRight]] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .listUserRights( - proto.ListUserRightsRequest( - userId = "", - identityProviderId = "", - ) - ) - .map(_.rights.view.collect(fromProtoRight.unlift).toSeq) - - /** Utility method for json services - */ - def serviceStub(token: Option[String] = None)(implicit - traceContext: TraceContext - ): UserManagementServiceStub = - LedgerClient.stubWithTracing(service, token.orElse(getDefaultToken())) -} - -object UserManagementClient { - - private def fromOptionalProtoUser(userO: Option[proto.User]): Future[User] = - userO.fold(Future.failed[User](new IllegalStateException("empty user")))(u => - Future.successful(fromProtoUser(u)) - ) - - private def fromProtoUser(user: proto.User): User = - User( - id = Ref.UserId.assertFromString(user.id), - primaryParty = - Option.unless(user.primaryParty.isEmpty)(Party.assertFromString(user.primaryParty)), - isDeactivated = user.isDeactivated, - identityProviderId = IdentityProviderId(user.identityProviderId), - metadata = user.metadata.fold(ObjectMeta.empty)(fromProtoMetadata), - primaryPartyAuthentication = user.primaryPartyAuthentication, - ) - - private def fromProtoMetadata( - metadata: com.daml.ledger.api.v2.admin.object_meta.ObjectMeta - ): ObjectMeta = - ObjectMeta( - // It's unfortunate that a client is using the server-side ObjectMeta and has to know how to parse the resource version - resourceVersionO = - Option.when(metadata.resourceVersion.nonEmpty)(metadata.resourceVersion).map(_.toLong), - annotations = metadata.annotations, - ) - - private def toProtoUser(user: User): proto.User = - proto.User( - id = user.id, - primaryParty = user.primaryParty.getOrElse(""), - isDeactivated = user.isDeactivated, - identityProviderId = user.identityProviderId.toRequestString, - metadata = Some(toProtoObjectMeta(user.metadata)), - primaryPartyAuthentication = user.primaryPartyAuthentication, - ) - - private def toProtoObjectMeta(meta: ObjectMeta): admin_proto.object_meta.ObjectMeta = - admin_proto.object_meta.ObjectMeta( - // It's unfortunate that a client is using the server-side ObjectMeta and has to know how to parse the resource version - resourceVersion = meta.resourceVersionO.map(_.toString).getOrElse(""), - annotations = meta.annotations, - ) - - private val toProtoRight: UserRight => proto.Right = { - case UserRight.ParticipantAdmin => - proto.Right(proto.Right.Kind.ParticipantAdmin(proto.Right.ParticipantAdmin())) - case UserRight.IdentityProviderAdmin => - proto.Right(proto.Right.Kind.IdentityProviderAdmin(proto.Right.IdentityProviderAdmin())) - case UserRight.CanActAs(party) => - proto.Right(proto.Right.Kind.CanActAs(proto.Right.CanActAs(party))) - case UserRight.CanReadAs(party) => - proto.Right(proto.Right.Kind.CanReadAs(proto.Right.CanReadAs(party))) - case UserRight.CanReadAsAnyParty => - proto.Right(proto.Right.Kind.CanReadAsAnyParty(proto.Right.CanReadAsAnyParty())) - case UserRight.CanExecuteAs(party) => - proto.Right(proto.Right.Kind.CanExecuteAs(proto.Right.CanExecuteAs(party))) - case UserRight.CanExecuteAsAnyParty => - proto.Right(proto.Right.Kind.CanExecuteAsAnyParty(proto.Right.CanExecuteAsAnyParty())) - } - - private val fromProtoRight: proto.Right => Option[UserRight] = { - case proto.Right(_: proto.Right.Kind.ParticipantAdmin) => - Some(UserRight.ParticipantAdmin) - case proto.Right(_: proto.Right.Kind.IdentityProviderAdmin) => - Some(UserRight.IdentityProviderAdmin) - case proto.Right(proto.Right.Kind.CanActAs(x)) => - // Note: assertFromString is OK here, as the server should deliver valid party identifiers. - Some(UserRight.CanActAs(Ref.Party.assertFromString(x.party))) - case proto.Right(proto.Right.Kind.CanReadAs(x)) => - Some(UserRight.CanReadAs(Ref.Party.assertFromString(x.party))) - case proto.Right(proto.Right.Kind.CanReadAsAnyParty(_)) => - Some(UserRight.CanReadAsAnyParty) - case proto.Right(proto.Right.Kind.CanExecuteAs(x)) => - Some(UserRight.CanExecuteAs(Ref.Party.assertFromString(x.party))) - case proto.Right(proto.Right.Kind.CanExecuteAsAnyParty(_)) => - Some(UserRight.CanExecuteAsAnyParty) - case proto.Right(proto.Right.Kind.Empty) => - None // The server sent a right of a kind that this client doesn't know about. - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/commands/CommandClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/commands/CommandClient.scala deleted file mode 100644 index 30a0efdae9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/commands/CommandClient.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.commands - -import com.daml.ledger.api.v2.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionServiceStub -import com.daml.ledger.api.v2.command_submission_service.{SubmitRequest, SubmitResponse} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.Future - -/** Enables easy access to command services and high level operations on top of them. - * - * @param commandSubmissionService - * gRPC service reference. - * @param commandCompletionService - * gRPC service reference. - * @param userId - * Will be applied to submitted commands. - * @param config - * Options for changing behavior. - */ -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -final class CommandClient( - commandSubmissionService: CommandSubmissionServiceStub, - override protected val loggerFactory: NamedLoggerFactory, -) extends NamedLogging { - - /** Submit a single command. Successful result does not guarantee that the resulting transaction - * has been written to the ledger. - */ - def submitSingleCommand( - submitRequest: SubmitRequest, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[SubmitResponse] = - submit(token)(submitRequest) - - private def submit( - token: Option[String] - )(submitRequest: SubmitRequest)(implicit traceContext: TraceContext): Future[SubmitResponse] = { - noTracingLogger.debug( - "Invoking grpc-submission on commandId={}", - submitRequest.commands.map(_.commandId).getOrElse("no-command-id"), - ) - LedgerClient - .stubWithTracing(commandSubmissionService, token) - .submit(submitRequest) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/commands/CommandServiceClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/commands/CommandServiceClient.scala deleted file mode 100644 index f8204008be..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/commands/CommandServiceClient.scala +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.commands - -import com.daml.ledger.api.v2.command_service.CommandServiceGrpc.CommandServiceStub -import com.daml.ledger.api.v2.command_service.{ - SubmitAndWaitForTransactionRequest, - SubmitAndWaitForTransactionResponse, - SubmitAndWaitRequest, - SubmitAndWaitResponse, -} -import com.daml.ledger.api.v2.commands.Commands -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA -import com.daml.ledger.api.v2.transaction_filter.{ - EventFormat, - Filters, - TransactionFormat, - TransactionShape, -} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.ledger.client.services.commands.CommandServiceClient.statusFromThrowable -import com.digitalasset.canton.tracing.TraceContext -import com.google.rpc.status.Status -import io.grpc.protobuf.StatusProto - -import java.util.concurrent.TimeUnit -import scala.concurrent.duration.Duration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.chaining.scalaUtilChainingOps -import scala.util.{Failure, Success, Using} - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -class CommandServiceClient( - service: CommandServiceStub, - getDefaultToken: () => Option[String] = () => None, -)(implicit - executionContext: ExecutionContext -) { - - private def handleException[R](exception: Throwable): Future[Either[Status, R]] = - statusFromThrowable(exception) match { - case Some(value) => Future.successful(Left(value)) - case None => Future.failed(exception) - } - - /** Submits and waits, optionally with a custom timeout - * - * Note that the [[com.daml.ledger.api.v2.commands.Commands]] argument is scala protobuf. If you - * use java codegen, you need to convert the List[Command] using the codegenToScalaProto method - */ - - private[canton] def submitAndWaitForTransactionForJsonApi( - request: SubmitAndWaitRequest, - timeout: Option[Duration] = None, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[SubmitAndWaitForTransactionResponse] = - serviceWithTokenAndDeadline(timeout, token).submitAndWaitForTransaction( - getSubmitAndWaitForTransactionRequest(request.commands) - ) - - def submitAndWaitForTransaction( - commands: Commands, - transactionShape: TransactionShape = TRANSACTION_SHAPE_ACS_DELTA, - timeout: Option[Duration] = None, - token: Option[String] = None, - )(implicit - traceContext: TraceContext - ): Future[Either[Status, SubmitAndWaitForTransactionResponse]] = - submitAndHandle( - timeout, - token, - withTraceContextInjectedIntoOpenTelemetryContext( - _.submitAndWaitForTransaction( - getSubmitAndWaitForTransactionRequest(Some(commands), transactionShape) - ) - ), - ) - - def submitAndWait( - commands: Commands, - timeout: Option[Duration] = None, - token: Option[String] = None, - )(implicit - traceContext: TraceContext - ): Future[Either[Status, SubmitAndWaitResponse]] = - submitAndHandle( - timeout, - token, - withTraceContextInjectedIntoOpenTelemetryContext( - _.submitAndWait(SubmitAndWaitRequest(commands = Some(commands))) - ), - ) - - private def serviceWithTokenAndDeadline( - timeout: Option[Duration], - token: Option[String], - )(implicit traceContext: TraceContext): CommandServiceStub = { - val withToken: CommandServiceStub = LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - - timeout - .fold(withToken) { timeout => - withToken - .withDeadlineAfter(timeout.toMillis, TimeUnit.MILLISECONDS) - } - } - - private def submitAndHandle[R]( - timeout: Option[Duration], - token: Option[String], - request: CommandServiceStub => Future[R], - )(implicit traceContext: TraceContext): Future[Either[Status, R]] = - request(serviceWithTokenAndDeadline(timeout, token)) - .transformWith { - case Success(value) => Future.successful(Right(value)) - case Failure(exception) => handleException(exception) - } - - private def getSubmitAndWaitForTransactionRequest( - commands: Option[Commands], - transactionShape: TransactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) = - SubmitAndWaitForTransactionRequest( - commands = commands, - transactionFormat = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = commands.toList - .flatMap(_.actAs) - .map( - _ -> Filters( - cumulative = Nil - ) - ) - .toMap, - filtersForAnyParty = None, - verbose = true, - ) - ), - transactionShape = transactionShape, - ) - ), - ) - - private def withTraceContextInjectedIntoOpenTelemetryContext[R]( - request: CommandServiceStub => Future[R] - )(svc: CommandServiceStub)(implicit traceContext: TraceContext): Future[R] = - // Attach the current trace context so the native OpenTelemetry client tracing interceptor - // can extract and propagate it - Using(traceContext.context.makeCurrent())(_ => request(svc)).pipe(Future.fromTry(_).flatten) -} - -object CommandServiceClient { - def statusFromThrowable(throwable: Throwable): Option[Status] = - Option(StatusProto.fromThrowable(throwable)).map(Status.fromJavaProto) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/pkg/PackageClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/pkg/PackageClient.scala deleted file mode 100644 index cb395ab9c9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/pkg/PackageClient.scala +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.pkg - -import com.daml.ledger.api.v2.package_service.PackageServiceGrpc.PackageServiceStub -import com.daml.ledger.api.v2.package_service.{ - GetPackageRequest, - GetPackageResponse, - GetPackageStatusRequest, - GetPackageStatusResponse, - ListPackagesRequest, - ListPackagesResponse, - ListVettedPackagesRequest, - ListVettedPackagesResponse, -} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.Future - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -final class PackageClient( - service: PackageServiceStub, - getDefaultToken: () => Option[String] = () => None, -) { - - def listPackages( - token: Option[String] = None - )(implicit traceContext: TraceContext): Future[ListPackagesResponse] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .listPackages(ListPackagesRequest()) - - def getPackage( - packageId: String, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[GetPackageResponse] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getPackage(GetPackageRequest(packageId = packageId)) - - def getPackageStatus( - packageId: String, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[GetPackageStatusResponse] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getPackageStatus( - GetPackageStatusRequest(packageId = packageId) - ) - - def listVettedPackages( - request: ListVettedPackagesRequest, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[ListVettedPackagesResponse] = - LedgerClient - .stubWithTracing(service, token) - .listVettedPackages(request) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/state/StateServiceClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/state/StateServiceClient.scala deleted file mode 100644 index acea5378c9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/state/StateServiceClient.scala +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.state - -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.daml.grpc.adapter.client.pekko.ClientAdapter -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse.ContractEntry -import com.daml.ledger.api.v2.state_service.StateServiceGrpc.StateServiceStub -import com.daml.ledger.api.v2.state_service.{ - ActiveContract, - GetActiveContractsRequest, - GetActiveContractsResponse, - GetConnectedSynchronizersRequest, - GetConnectedSynchronizersResponse, - GetLedgerEndRequest, - GetLedgerEndResponse, -} -import com.daml.ledger.api.v2.transaction_filter.EventFormat -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.{Sink, Source} - -import scala.concurrent.{ExecutionContext, Future} - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -class StateServiceClient( - service: StateServiceStub, - getDefaultToken: () => Option[String] = () => None, -)(implicit - ec: ExecutionContext, - esf: ExecutionSequencerFactory, -) { - - /** Returns a stream of GetActiveContractsResponse messages. */ - def getActiveContractsSource( - eventFormat: EventFormat, - validAtOffset: Long, - token: Option[String], - )(implicit traceContext: TraceContext): Source[GetActiveContractsResponse, NotUsed] = - ClientAdapter - .serverStreaming( - GetActiveContractsRequest( - activeAtOffset = validAtOffset, - eventFormat = Some(eventFormat), - streamContinuationToken = None, - ), - LedgerClient.stubWithTracing(service, token.orElse(getDefaultToken())).getActiveContracts, - ) - - /** Returns the resulting active contract set */ - def getActiveContracts( - eventFormat: EventFormat, - validAtOffset: Long, - token: Option[String] = None, - )(implicit - materializer: Materializer, - traceContext: TraceContext, - ): Future[Seq[ActiveContract]] = - for { - contracts <- getActiveContractsSource(eventFormat, validAtOffset, token).runWith(Sink.seq) - active = contracts - .map(_.contractEntry) - .collect { case ContractEntry.ActiveContract(value) => - value - } - } yield active - - def getLedgerEnd( - token: Option[String] = None - )(implicit traceContext: TraceContext): Future[GetLedgerEndResponse] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getLedgerEnd(GetLedgerEndRequest()) - - /** Get the current participant offset */ - def getLedgerEndOffset( - token: Option[String] = None - )(implicit traceContext: TraceContext): Future[Long] = - getLedgerEnd(token).map(_.offset) - - def getConnectedSynchronizers( - party: String, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Future[GetConnectedSynchronizersResponse] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getConnectedSynchronizers( - GetConnectedSynchronizersRequest( - party = party, - participantId = "", - identityProviderId = "", - ) - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/updates/UpdateServiceClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/updates/UpdateServiceClient.scala deleted file mode 100644 index 11710112fa..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/updates/UpdateServiceClient.scala +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.updates - -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.daml.grpc.adapter.client.pekko.ClientAdapter -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA -import com.daml.ledger.api.v2.transaction_filter.{EventFormat, TransactionFormat, UpdateFormat} -import com.daml.ledger.api.v2.update_service.UpdateServiceGrpc.UpdateServiceStub -import com.daml.ledger.api.v2.update_service.{GetUpdatesRequest, GetUpdatesResponse} -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -class UpdateServiceClient( - service: UpdateServiceStub, - getDefaultToken: () => Option[String] = () => None, -)(implicit - esf: ExecutionSequencerFactory -) { - def getUpdatesSource( - begin: Long, - eventFormat: EventFormat, - end: Option[Long] = None, - token: Option[String] = None, - )(implicit traceContext: TraceContext): Source[GetUpdatesResponse, NotUsed] = - ClientAdapter - .serverStreaming( - GetUpdatesRequest( - beginExclusive = begin, - endInclusive = end, - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some(eventFormat), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some(eventFormat), - includeTopologyEvents = None, - ) - ), - descendingOrder = false, - ), - LedgerClient.stubWithTracing(service, token.orElse(getDefaultToken())).getUpdates, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/version/VersionClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/version/VersionClient.scala deleted file mode 100644 index 24da8325f9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/services/version/VersionClient.scala +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client.services.version - -import com.daml.ledger.api.v2.version_service.VersionServiceGrpc.VersionServiceStub -import com.daml.ledger.api.v2.version_service.{FeaturesDescriptor, GetLedgerApiVersionRequest} -import com.digitalasset.canton.ledger.api.Feature -import com.digitalasset.canton.ledger.client.LedgerClient -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.{ExecutionContext, Future} - -@SuppressWarnings(Array("com.digitalasset.canton.DirectGrpcServiceInvocation")) -final class VersionClient( - service: VersionServiceStub, - getDefaultToken: () => Option[String] = () => None, -) { - def getApiVersion( - token: Option[String] = None - )(implicit executionContext: ExecutionContext, traceContext: TraceContext): Future[String] = - LedgerClient - .stubWithTracing(service, token.orElse(getDefaultToken())) - .getLedgerApiVersion( - new GetLedgerApiVersionRequest() - ) - .map(_.version) - - /** Utility method for json services - */ - def serviceStub(token: Option[String] = None)(implicit - traceContext: TraceContext - ): VersionServiceStub = - LedgerClient.stubWithTracing(service, token.orElse(getDefaultToken())) -} - -object VersionClient { - // see also com.digitalasset.canton.platform.apiserver.services.ApiVersionService.featuresDescriptor - def fromProto(featuresDescriptor: FeaturesDescriptor): Seq[Feature] = - featuresDescriptor match { - // Note that we do not expose experimental features here, as they are used for internal testing only - // and do not have backwards compatibility guarantees. (They should probably be named 'internalFeatures' ;-) - case FeaturesDescriptor(userManagement, _, _, _, _) => - userManagement.toList map (_ => Feature.UserManagement) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/CommonErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/CommonErrors.scala deleted file mode 100644 index bb6de25d78..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/CommonErrors.scala +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error - -import com.digitalasset.base.error.{ - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.CommonErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext - -import scala.concurrent.duration.Duration - -@Explanation( - "Common errors raised in Daml services and components." -) -object CommonErrors extends CommonErrorGroup { - - @Explanation( - "Another request with the same id is already being processed." - ) - @Resolution( - """Listen to the appropriate stream until a result for the in-flight request is published. - |Alternatively, resubmit the request. - |""" - ) - object RequestAlreadyInFlight - extends ErrorCode( - id = "REQUEST_ALREADY_IN_FLIGHT", - ErrorCategory.ContentionOnSharedResources, - ) { - final case class Reject(requestId: String, details: String)(implicit - errorLogger: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"Request with ID $requestId is already in flight: $details" - ) - } - - @Explanation( - "This rejection is given when a request processing status is not known and a time-out is reached." - ) - @Resolution( - "Retry for transient problems. If non-transient contact the operator as the time-out limit might be too short." - ) - object RequestTimeOut - extends ErrorCode( - id = "REQUEST_TIME_OUT", - ErrorCategory.DeadlineExceededRequestStateUnknown, - ) { - final case class Reject(message: String, override val definiteAnswer: Boolean)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = message, - definiteAnswer = definiteAnswer, - ) - } - - @Explanation( - "The request has not been submitted for processing as its predefined deadline has expired." - ) - @Resolution("Retry the request with a greater deadline.") - object RequestDeadlineExceeded - extends ErrorCode( - id = "REQUEST_DEADLINE_EXCEEDED", - ErrorCategory.DeadlineExceededRequestStateUnknown, - ) { - final case class Reject(deadlineExceededBy: Duration, commandId: String, submissionId: String)( - implicit loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The gRPC deadline for request with commandId=$commandId and submissionId=$submissionId has expired by $deadlineExceededBy. The request will not be processed further.", - definiteAnswer = false, - ) - } - - @Explanation( - "This rejection is given when the requested service is not running. It has not started or has already been shut down." - ) - @Resolution( - "Retry re-submitting the request. If the error persists, contact the participant operator." - ) - object ServiceNotRunning - extends ErrorCode( - id = "SERVICE_NOT_RUNNING", - ErrorCategory.TransientServerFailure, - ) { - final case class Reject(serviceName: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$serviceName is not running.", - extraContext = Map("service_name" -> serviceName), - ) - } - - @Explanation("""This error occurs if one of the services encountered an unexpected exception.""") - @Resolution("Contact support.") - object ServiceInternalError - extends ErrorCode( - id = "SERVICE_INTERNAL_ERROR", - ErrorCategory.SystemInternalAssumptionViolated, - ) { - - final case class UnexpectedOrUnknownException(t: Throwable)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = "Unexpected or unknown exception occurred.", - throwableO = Some(t), - ) - - final case class Generic( - message: String, - override val throwableO: Option[Throwable] = None, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = message, - extraContext = Map("throwableO" -> throwableO.toString), - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/IndexErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/IndexErrors.scala deleted file mode 100644 index 8370d11ea0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/IndexErrors.scala +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error - -import com.digitalasset.base.error.ErrorCode.LoggedApiException -import com.digitalasset.base.error.{ - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - ErrorGroup, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.IndexErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext - -@Explanation("Errors raised by the Participant Index persistence layer.") -object IndexErrors extends IndexErrorGroup { - object DatabaseErrors extends ErrorGroup() { - @Explanation( - "This error occurs if a transient error arises when executing a query against the index database." - ) - @Resolution("Re-submit the request.") - object SqlTransientError - extends ErrorCode( - id = "INDEX_DB_SQL_TRANSIENT_ERROR", - ErrorCategory.TransientServerFailure, - ) { - final case class Reject(throwable: Throwable)(implicit - val loggingContext: ErrorLoggingContext - ) extends DbError( - cause = - s"Processing the request failed due to a transient database error: ${throwable.getMessage}", - throwableO = Some(throwable), - ) - } - - @Explanation( - "This error occurs if a non-transient error arises when executing a query against the index database." - ) - @Resolution("Contact the participant operator.") - object SqlNonTransientError - extends ErrorCode( - id = "INDEX_DB_SQL_NON_TRANSIENT_ERROR", - ErrorCategory.SystemInternalAssumptionViolated, - ) { - final case class Reject(throwable: Throwable)(implicit - val loggingContext: ErrorLoggingContext - ) extends DbError( - cause = - s"Processing the request failed due to a non-transient database error: ${throwable.getMessage}", - throwableO = Some(throwable), - ) - } - } - - // Decorator that returns a specialized StatusRuntimeException (IndexDbException) - // that can be used for precise matching of persistence exceptions (e.g. for index initialization failures that need retrying). - // Without this specialization, internal errors just appear as StatusRuntimeExceptions (see INDEX_DB_SQL_NON_TRANSIENT_ERROR) - // without any marker, impeding us to assert whether they are emitted by the persistence layer or not. - abstract class DbError( - override val cause: String, - override val throwableO: Option[Throwable] = None, - )(implicit - code: ErrorCode, - loggingContext: ErrorLoggingContext, - ) extends DamlErrorWithDefiniteAnswer(cause, throwableO) { - - override def asGrpcError: IndexDbException = { - val err = super.asGrpcError - IndexDbException(err.getStatus, err.getTrailers) - } - } - - final case class IndexDbException(status: io.grpc.Status, metadata: io.grpc.Metadata) - extends LoggedApiException(status, metadata) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/JsonApiErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/JsonApiErrors.scala deleted file mode 100644 index c139457e1b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/JsonApiErrors.scala +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error - -import com.digitalasset.base.error.{ - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup.JsonApiErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext - -@Explanation( - "Errors specific for Json Ledger API." -) -object JsonApiErrors extends JsonApiErrorGroup { - @Explanation( - "This error occurs when the Ledger JSON API server fails to find a suitable set of package-ids for decoding a given set of commands in a request." - ) - @Resolution( - """Inspect the error message and ensure that: - |1. The requested packages are uploaded on the participant node. - |2. The topology state satisfies the required vetting for the interested parties""" - ) - object JsonApiPackageSelectionFailed - extends ErrorCode( - id = "JSON_API_PACKAGE_SELECTION_FAILED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(override val cause: String)(implicit - errorLogger: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = cause) - } - - @Explanation( - s"""This happens when the number of returned elements is equal or greater to the node limit. - |The limit is defined by the node configuration and can be changed by the operator. - | Check 'canton.participants..http-ledger-api.websocket-config.http-list-max-elements-limit'. - | Notice: If configured in the participant node config, the actual returned number is the minimum between the "limit" query parameter in the request and this one . - | If the request query "limit" is the same as the mentioned configuration, it allows to return result set up to server limit without producing error. - |""" - ) - @Resolution(""" - |1. Preferred solution is to use websocket endpoint to get results in chunks. - |2. It is possible to increase the limit by changing the node configuration 'http-list-max-elements-limit' but this may have - |a severe impact on a node performance.""") - object MaximumNumberOfElements - extends ErrorCode( - id = "JSON_API_MAXIMUM_LIST_ELEMENTS_NUMBER_REACHED", - ErrorCategory.ContentionOnSharedResources, - ) { - final case class Reject( - value: Int, - limit: Long, - )(implicit - errorLogger: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The number of matching elements ($value) is greater than the node limit ($limit)." - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/LedgerApiErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/LedgerApiErrors.scala deleted file mode 100644 index b3d5e03e81..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/LedgerApiErrors.scala +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error - -import com.daml.metrics.ExecutorServiceMetrics -import com.digitalasset.base.error.{ - BaseError, - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.engine.Error as LfError -import com.digitalasset.daml.lf.engine.Error.Validation.ReplayMismatch -import org.slf4j.event.Level - -import scala.concurrent.duration.FiniteDuration - -@Explanation( - "Errors raised by or forwarded by the Ledger API." -) -object LedgerApiErrors extends LedgerApiErrorGroup { - - val EarliestOffsetMetadataKey = "earliest_offset" - val LatestOffsetMetadataKey = "latest_offset" - - @Explanation( - """This error occurs when a participant rejects a command due to excessive load. - |Load can be caused by the following factors: - |1. when commands are submitted to the participant through its Ledger API, - |2. when the participant receives validation requests from other participants through a connected synchronizer. - | - |In order to prevent the participant of being overloaded, it will start to reject commands once a - |certain load threshold is reached. The main threshold is the number of in-flight validation requests - |that the participant is currently processing. These requests can be caused either by this participant - |or by other participants. - | - |For a submission to be counted as an in-flight validation request, the participant must first - |observe its sequencing, which means that there is a delay between the submission and the submitted - |command to be counted towards the currently in-flight validation requests. In order to avoid an - |overload situation by a sudden burst of commands, the participant will also enforce a rate limit - |before a submission is accepted for interpretation. This rate limit can be configured with a steady - |state rate and a burst factor. The burst factor is a multiplier of the steady state rate that allows - |for a certain number of commands to be submitted in a burst before the rate limit kicks in. - | - |As an example, with a rate limit of 1000 commands per second and a burst factor of 2, the rate limit - |will kick in once 2000 commands have been submitted on top of the commands allowed by the rate limit. - | - |""" - ) - @Resolution( - """Verify the limits configured, the load and the command latency on the participant and adjust if necessary. - |If the participant is highly loaded, ensure that your application waits some time with the resubmission, preferably with some backoff factor. - |If possible, ask other participants to send fewer requests; the synchronizer operator can enforce this by imposing a rate limit.""" - ) - object ParticipantBackpressure - extends ErrorCode( - id = "PARTICIPANT_BACKPRESSURE", - ErrorCategory.ContentionOnSharedResources, - ) { - override def logLevel: Level = Level.INFO - - final case class Rejection(reason: String)(implicit errorLogger: ErrorLoggingContext) - extends DamlErrorWithDefiniteAnswer( - cause = s"The participant is overloaded: $reason", - extraContext = Map("reason" -> reason), - ) - } - - @Explanation( - "This error happens when the JVM heap memory pool exceeds a pre-configured limit." - ) - @Resolution( - """The following actions can be taken: - |1. Review the historical use of heap space by inspecting the metric given in the message. - |2. Review the current heap space limits configured in the rate limiting configuration. - |3. Try to space out requests that are likely to require a large amount of memory to process.""" - ) - object HeapMemoryOverLimit - extends ErrorCode( - id = "HEAP_MEMORY_OVER_LIMIT", - ErrorCategory.ContentionOnSharedResources, - ) { - final case class Rejection( - memoryPool: String, - limit: Long, - metricPrefix: String, - fullMethodName: String, - )(implicit errorLogger: ErrorLoggingContext) - extends DamlErrorWithDefiniteAnswer( - cause = - s"The $memoryPool collection usage threshold has exceeded the maximum ($limit). Jvm memory metrics are available at $metricPrefix.", - extraContext = Map( - "memoryPool" -> memoryPool, - "limit" -> limit, - "metricPrefix" -> metricPrefix, - "fullMethodName" -> fullMethodName, - ), - ) - } - - @Explanation( - "This error happens when the number of concurrent gRPC streaming requests exceeds the configured limit." - ) - @Resolution( - """The following actions can be taken: - |1. Review the historical need for concurrent streaming by inspecting the metric given in the message. - |2. Review the maximum streams limit configured in the rate limiting configuration. - |3. Try to space out streaming requests such that they do not need to run in parallel with each other.""" - ) - object MaximumNumberOfStreams - extends ErrorCode( - id = "MAXIMUM_NUMBER_OF_STREAMS", - ErrorCategory.ContentionOnSharedResources, - ) { - final case class Rejection( - value: Long, - limit: Int, - metricPrefix: String, - fullMethodName: String, - )(implicit - errorLogger: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The number of streams in use ($value) has reached or exceeded the limit ($limit). Metrics are available at $metricPrefix.", - extraContext = Map( - "value" -> value, - "limit" -> limit, - "metricPrefix" -> metricPrefix, - "fullMethodName" -> fullMethodName, - ), - ) - } - - @Explanation( - "This happens when the rate of submitted gRPC requests requires more CPU or database power than is available." - ) - @Resolution( - """The following actions can be taken: - |Here the 'queue size' for the threadpool is considered as reported by the executor itself. - |1. Review the historical 'queue size' growth by inspecting the metric given in the message. - |2. Review the maximum 'queue size' limits configured in the rate limiting configuration. - |3. Try to space out requests that are likely to require a lot of CPU or database power. - """ - ) - object ThreadpoolOverloaded - extends ErrorCode( - id = "THREADPOOL_OVERLOADED", - ErrorCategory.ContentionOnSharedResources, - ) { - final case class Rejection( - name: String, - metricNameLabel: String, - queued: Long, - limit: Int, - fullMethodName: String, - )(implicit errorLogger: ErrorLoggingContext) - extends DamlErrorWithDefiniteAnswer( - s"The $metricNameLabel ($name) queue size ($queued) has exceeded the maximum ($limit).", - extraContext = Map( - "name" -> name, - "queued" -> queued, - "limit" -> limit, - "name_label" -> metricNameLabel, - "metrics" -> ExecutorServiceMetrics.CommonMetricsName.QueuedTasks, - "fullMethodName" -> fullMethodName, - ), - ) - } - - @Explanation( - """The requested interface view for the template's package-name is unavailable due to a missing vetted package. - |This could be due to a stale stream subscription or the deactivation of an interface implementation - |resulting from the unvetting of its compatible packages.""" - ) - @Resolution( - """Close and re-open the stream subscription to refresh the vetting state used for rendering interface views. - |If the problem persists and it is unexpected, contact the participant operator.""" - ) - object NoVettedInterfaceImplementationPackage - extends ErrorCode( - id = "NO_VETTED_INTERFACE_IMPLEMENTATION_PACKAGE", - category = ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject( - packageName: Ref.PackageName, - reason: String, - )(implicit errorLoggingContext: ErrorLoggingContext) - extends DamlErrorWithDefiniteAnswer( - cause = - s"No vetted package for rendering the interface view for package-name '$packageName'. Reason: $reason" - ) - } - - final case class InterfaceViewUpgradeFailureWrapper( - root: BaseError - )(implicit errorLoggingContext: ErrorLoggingContext) - extends DamlErrorWithDefiniteAnswer( - cause = - s"Could not compute a package-id for rendering the interface view. Root cause: ${root.cause}" - )(root.code, errorLoggingContext) - - @Explanation( - """This error occurs if the topology state of the participant's connected synchronizers cannot satisfy the vetting requirements provided in the request.""" - ) - @Resolution( - """Inspect the error message and refine the request or retry later. If the error persists, inform the involved counterparties about their invalid topology state.""" - ) - object NoPreferredPackagesFound - extends ErrorCode( - id = "NO_PREFERRED_PACKAGES_FOUND", - category = ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(reason: String)(implicit errorLoggingContext: ErrorLoggingContext) - extends DamlErrorWithDefiniteAnswer( - cause = s"Failed to compute package preferences. Reason: $reason" - ) - } - - @Explanation( - """Participant pruning of contracts is blocked, giving up. Please try again later. - |Safe contract pruning is implemented using database locks and with optimistic locking and retries. - |This error happens if all the retry attempts are exhausted.""" - ) - @Resolution( - """As the root cause of this type of lock contention should be automatically resolved in-between retries, please - |retry the operation again. - |If this kind of error emerges more often, please try to increase the number of retries defined by the - |`contract-pruning-max-retries` configuration parameter and/or increase the delay duration defined by the - |`contract-pruning-delay-before-retry` configuration parameter. - |In case this problem persists, please contact technical support.""" - ) - object ParticipantContractPruningBlocked - extends ErrorCode( - id = "PARTICIPANT_CONTRACT_PRUNING_BLOCKED", - category = ErrorCategory.ContentionOnSharedResources, - ) { - final case class Reject( - retries: Int, - delay: FiniteDuration, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"Participant pruning of contracts is blocked, giving up retries. Please try again later. (retires: $retries, delay: $delay)" - ) - } - - @Explanation("""This error occurs if there was an unexpected error in the Ledger API.""") - @Resolution("Contact support.") - object InternalError - extends ErrorCode( - id = "LEDGER_API_INTERNAL_ERROR", - ErrorCategory.SystemInternalAssumptionViolated, - ) { - - final case class UnexpectedOrUnknownException(t: Throwable)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = "Unexpected or unknown exception occurred.", - throwableO = Some(t), - ) - - final case class Generic( - message: String, - override val throwableO: Option[Throwable] = None, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = message, - extraContext = Map("throwableO" -> throwableO.toString), - ) - - final case class PackageSelfConsistency( - err: LfError.Package.DarSelfConsistency - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = err.message - ) - - final case class PackageInternal( - err: LfError.Package.Internal - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = err.message - ) - - final case class Preprocessing( - err: LfError.Preprocessing.Internal - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = err.message) - - final case class Validation(reason: ReplayMismatch)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"Observed un-expected replay mismatch: $reason" - ) - - final case class Interpretation( - where: String, - message: String, - detailMessage: Option[String], - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"Daml-Engine interpretation failed with internal error: $where / $message", - extraContext = Map("detailMessage" -> detailMessage), - ) - - final case class VersionService(message: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = message) - - final case class Buffer(message: String, override val throwableO: Option[Throwable])(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = message, throwableO = throwableO) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/PackageServiceErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/PackageServiceErrors.scala deleted file mode 100644 index cd1850f658..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/PackageServiceErrors.scala +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error - -import com.digitalasset.base.error.{ - ContextualizedDamlError, - ErrorCategory, - ErrorCode, - ErrorGroup, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.archive.Error as LfArchiveError -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.engine.Error -import com.digitalasset.daml.lf.{language, validation} - -import ParticipantErrorGroup.LedgerApiErrorGroup.PackageServiceErrorGroup - -@Explanation( - "Errors raised by the Package Management Service on package uploads." -) -object PackageServiceErrors extends PackageServiceErrorGroup { - - @Explanation("Package parsing errors raised during package upload.") - object Reading extends ErrorGroup { - @Explanation( - """This error indicates that the supplied dar file name did not meet the requirements to be stored in the persistence store.""" - ) - @Resolution("Inspect error message for details and change the file name accordingly") - object InvalidDarFileName - extends ErrorCode( - id = "INVALID_DAR_FILE_NAME", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Error(reason: String)(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Dar file name is invalid", - extraContext = Map("reason" -> reason), - ) - } - - @Explanation("""This error indicates that the supplied dar file was invalid.""") - @Resolution("Inspect the error message for details and contact support.") - object InvalidDar - extends ErrorCode(id = "INVALID_DAR", ErrorCategory.InvalidIndependentOfSystemState) { - final case class Error(entries: Seq[String], throwable: Throwable)(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Dar file is corrupt", - throwableO = Some(throwable), - extraContext = Map( - "entries" -> entries, - "throwable" -> throwable, - ), - ) - } - @Explanation( - """The main package of the uploaded DAR does not match the expected package id.""" - ) - @Resolution( - """Investigate where the DAR is coming from and whether it was manipulated or the provided package id was wrong.""" - ) - object MainPackageInDarDoesNotMatchExpected - extends ErrorCode( - id = "MAIN_PACKAGE_IN_DAR_DOES_NOT_MATCH_EXPECTED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject(found: String, expected: String)(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = - s"The main package of the uploaded DAR is '$found' while the provided expected value is '$expected'" - ) - } - - @Explanation("""This error indicates that the supplied zipped dar file was invalid.""") - @Resolution("Inspect the error message for details and contact support.") - object InvalidZipEntry - extends ErrorCode(id = "INVALID_ZIP_ENTRY", ErrorCategory.InvalidIndependentOfSystemState) { - final case class Error(name: String, entries: Seq[String])(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Dar zip file is corrupt", - extraContext = Map( - "name" -> name, - "entries" -> entries, - ), - ) - } - - @Explanation( - """This error indicates that the supplied zipped dar is an unsupported legacy Dar.""" - ) - @Resolution("Please use a more recent dar version.") - object InvalidLegacyDar - extends ErrorCode( - id = "INVALID_LEGACY_DAR", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Error(entries: Seq[String])(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Unsupported legacy Dar zip file", - extraContext = Map("entries" -> entries), - ) - } - - @Explanation("""This error indicates that the supplied zipped dar is regarded as zip-bomb.""") - @Resolution("Inspect the dar and contact support.") - object ZipBomb - extends ErrorCode(id = "ZIP_BOMB", ErrorCategory.InvalidIndependentOfSystemState) { - final case class Error(msg: String)(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Dar zip file seems to be a zip bomb.", - extraContext = Map("msg" -> msg), - ) - } - - @Explanation( - """This error indicates that the content of the Dar file could not be parsed successfully.""" - ) - @Resolution("Inspect the error message and contact support.") - object ParseError - extends ErrorCode(id = "DAR_PARSE_ERROR", ErrorCategory.InvalidIndependentOfSystemState) { - final case class Error(reason: String, throwable: Option[Throwable] = None)(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Failed to parse the dar file content.", - extraContext = Map("reason" -> reason), - throwableO = throwable, - ) - } - - } - - @Explanation("""This error indicates an internal issue within the package service.""") - @Resolution("Inspect the error message and contact support.") - object InternalError - extends ErrorCode( - id = "PACKAGE_SERVICE_INTERNAL_ERROR", - ErrorCategory.SystemInternalAssumptionViolated, - ) { - final case class Validation(nameOfFunc: String, msg: String, detailMsg: String = "")(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Internal package validation error.", - extraContext = Map( - "nameOfFunc" -> nameOfFunc, - "msg" -> msg, - "detailMsg" -> detailMsg, - ), - ) - final case class Error(missing: Set[Ref.PackageRef])(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Failed to resolve package ids locally.", - extraContext = Map("missing" -> missing), - ) - final case class Generic(reason: String)(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Generic error (please check the reason string).", - extraContext = Map("reason" -> reason), - ) - final case class Unhandled(throwable: Throwable, additionalReason: Option[String] = None)( - implicit val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Failed with an unknown error cause", - throwableO = Some(throwable), - extraContext = { - val context: Map[String, Any] = Map("throwable" -> throwable) - additionalReason match { - case Some(additionalReason) => context + ("additionalReason" -> additionalReason) - case None => context - } - }, - ) - } - - object Validation { - def handleLfArchiveError( - lfArchiveError: LfArchiveError - )(implicit - errorLoggingContext: ErrorLoggingContext - ): ContextualizedDamlError = - lfArchiveError match { - case LfArchiveError.InvalidDar(entries, cause) => - PackageServiceErrors.Reading.InvalidDar - .Error(entries.entries.keys.toSeq, cause) - case LfArchiveError.InvalidZipEntry(name, entries) => - PackageServiceErrors.Reading.InvalidZipEntry - .Error(name, entries.entries.keys.toSeq) - case LfArchiveError.InvalidLegacyDar(entries) => - PackageServiceErrors.Reading.InvalidLegacyDar.Error(entries.entries.keys.toSeq) - case LfArchiveError.ZipBomb => - PackageServiceErrors.Reading.ZipBomb.Error(LfArchiveError.ZipBomb.getMessage) - case e: LfArchiveError => - PackageServiceErrors.Reading.ParseError.Error(e.msg, throwable = Some(e)) - case e => - PackageServiceErrors.InternalError.Unhandled(e) - } - - def handleLfEnginePackageError(err: Error.Package.Error)(implicit - loggingContext: ErrorLoggingContext - ): ContextualizedDamlError = err match { - case Error.Package.Internal(nameOfFunc, msg, _) => - PackageServiceErrors.InternalError.Validation(nameOfFunc, msg) - case Error.Package.Validation(validationError) => - ValidationError.Error(validationError) - case Error.Package.MissingPackage(packageRef, _) => - PackageServiceErrors.InternalError.Error(Set(packageRef)) - case Error.Package - .AllowedLanguageVersion(packageId, languageVersion, allowedLanguageVersions) => - AllowedLanguageMismatchError( - packageId, - languageVersion, - allowedLanguageVersions, - ) - case Error.Package.DarSelfConsistency( - mainPackageId, - missingDependencies, - extraDependencies, - ) => - DarSelfConsistency.Error( - mainPackageId, - missingDependencies, - extraDependencies, - ) - } - - @Explanation("""This error indicates that the validation of the uploaded dar failed.""") - @Resolution("Inspect the error message and contact support.") - object ValidationError - extends ErrorCode( - id = "DAR_VALIDATION_ERROR", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Error(validationError: validation.ValidationError)(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = "Package validation failed.", - extraContext = Map("validationError" -> validationError), - ) - } - - final case class AllowedLanguageMismatchError( - packageId: Ref.PackageId, - languageVersion: language.LanguageVersion, - allowedLanguageVersions: Seq[language.LanguageVersion], - )(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = CommandExecutionErrors.Package.AllowedLanguageVersions - .buildCause(packageId, languageVersion, allowedLanguageVersions), - extraContext = Map( - "packageId" -> packageId, - "languageVersion" -> languageVersion.toString, - "allowedLanguageVersions" -> allowedLanguageVersions.toString, - ), - )( - CommandExecutionErrors.Package.AllowedLanguageVersions, - loggingContext, - ) // reuse error code of ledger api server - - @Explanation( - """This error indicates that the uploaded Dar is broken because it is missing internal dependencies or has unused internal dependencies.""" - ) - @Resolution("Contact the supplier of the Dar.") - object DarSelfConsistency - extends ErrorCode( - id = "DAR_DEPENDENCIES_NOT_VALID", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Error( - mainPackageId: Ref.PackageId, - missingDependencies: Set[Ref.PackageId], - extraDependencies: Set[Ref.PackageId], - )(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = - "The set of packages in the dar is not self-consistent and is missing dependencies or has extra dependencies", - extraContext = Map( - "mainPackageId" -> mainPackageId, - "missingDependencies" -> missingDependencies, - "extraDependencies" -> extraDependencies, - ), - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/ParticipantErrorGroup.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/ParticipantErrorGroup.scala deleted file mode 100644 index ebdbafab47..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/ParticipantErrorGroup.scala +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error - -import com.digitalasset.base.error.{ErrorClass, ErrorGroup} - -object ParticipantErrorGroup extends ErrorGroup()(ErrorClass.root()) { - abstract class CommonErrorGroup extends ErrorGroup() - - abstract class IndexErrorGroup extends ErrorGroup() - - abstract class LedgerApiErrorGroup extends ErrorGroup() - object LedgerApiErrorGroup extends LedgerApiErrorGroup { - abstract class AdminServicesErrorGroup extends ErrorGroup() - object AdminServicesErrorGroup extends AdminServicesErrorGroup { - abstract class UserManagementServiceErrorGroup extends ErrorGroup() - - abstract class PartyManagementServiceErrorGroup extends ErrorGroup() - - abstract class IdentityProviderConfigServiceErrorGroup extends ErrorGroup() - } - - abstract class JsonApiErrorGroup extends ErrorGroup() - - abstract class CommandExecutionErrorGroup extends ErrorGroup() - - abstract class ConsistencyErrorGroup extends ErrorGroup() - - abstract class PackageServiceErrorGroup extends ErrorGroup() - - abstract class RequestValidationErrorGroup extends ErrorGroup() - - abstract class SyncServiceRejectionErrorGroup extends ErrorGroup() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/AdminServiceErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/AdminServiceErrors.scala deleted file mode 100644 index cef1dc4189..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/AdminServiceErrors.scala +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error.groups - -import com.digitalasset.base.error.{ - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup.AdminServicesErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext - -@Explanation("Errors raised by Ledger API admin services.") -object AdminServiceErrors extends AdminServicesErrorGroup { - - val UserManagement: UserManagementServiceErrors.type = - UserManagementServiceErrors - val IdentityProviderConfig: IdentityProviderConfigServiceErrors.type = - IdentityProviderConfigServiceErrors - val PartyManagement: PartyManagementServiceErrors.type = - PartyManagementServiceErrors - - @Explanation("This rejection is given when a new configuration is rejected.") - @Resolution("Fetch newest configuration and/or retry.") - object ConfigurationEntryRejected - extends ErrorCode( - id = "CONFIGURATION_ENTRY_REJECTED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject(_message: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = _message - ) - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/CommandExecutionErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/CommandExecutionErrors.scala deleted file mode 100644 index bb767ff280..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/CommandExecutionErrors.scala +++ /dev/null @@ -1,1157 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error.groups - -import com.digitalasset.base.error.{ - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCategoryRetry, - ErrorCode, - ErrorGroup, - ErrorResource, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.LedgerApiErrors -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup.CommandExecutionErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{Identifier, PackageId} -import com.digitalasset.daml.lf.engine.Error as LfError -import com.digitalasset.daml.lf.interpretation.Error as LfInterpretationError -import com.digitalasset.daml.lf.language -import com.digitalasset.daml.lf.language.{Ast, LanguageVersion, Reference} -import com.digitalasset.daml.lf.transaction.{ - GlobalKey, - GlobalKeyWithMaintainers, - SerializationVersion, -} -import com.digitalasset.daml.lf.value.Value.ContractId -import com.digitalasset.daml.lf.value.{Value, ValueCoder} -import com.google.common.io.BaseEncoding -import org.slf4j.event.Level - -import scala.concurrent.duration.DurationInt - -@Explanation( - "Errors raised during the command execution phase of the command submission evaluation." -) -object CommandExecutionErrors extends CommandExecutionErrorGroup { - def encodeValue(v: Value): Either[ValueCoder.EncodeError, String] = - ValueCoder - .encodeValue(valueVersion = SerializationVersion.VDev, v0 = v) - .map(bs => BaseEncoding.base64().encode(bs.toByteArray)) - - def tryEncodeValue(v: Value)(implicit loggingContext: ErrorLoggingContext): Option[String] = - encodeValue(v).fold( - { case ValueCoder.EncodeError(msg) => - loggingContext.error(msg) - None - }, - Some(_), - ) - - def withEncodedValue( - v: Value - )( - f: String => Seq[(ErrorResource, String)] - )(implicit loggingContext: ErrorLoggingContext): Seq[(ErrorResource, String)] = - tryEncodeValue(v).fold(Seq.empty[(ErrorResource, String)])(f) - - def encodeParties(parties: Set[Ref.Party]): Seq[(ErrorResource, String)] = - Seq((ErrorResource.Parties, parties.mkString(","))) - - @Explanation( - """This error occurs if the participant fails to execute a transaction via the interactive submission service. - |""" - ) - @Resolution("Inspect error details and report the error.") - object InteractiveSubmissionExecuteError - extends ErrorCode( - id = "FAILED_TO_EXECUTE_TRANSACTION", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject(reason: String, throwable: Option[Throwable] = None)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"The participant failed to execute the transaction: $reason", - throwableO = throwable, - ) - } - - @Explanation( - """This error occurs if the participant fails to prepare a transaction via the interactive submission service. - |""" - ) - @Resolution("Inspect error details and report the error.") - object InteractiveSubmissionPreparationError - extends ErrorCode( - id = "FAILED_TO_PREPARE_TRANSACTION", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject(reason: String, throwable: Option[Throwable] = None)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"The participant failed to prepare the transaction: $reason", - throwableO = throwable, - ) - } - - @Explanation( - """This error occurs if the participant fails to determine the max ledger time of the used - |contracts. Most likely, this means that one of the contracts is not active anymore which can - |happen under contention. It can also happen with contract keys. - |""" - ) - @Resolution("Retry the transaction submission.") - object FailedToDetermineLedgerTime - extends ErrorCode( - id = "FAILED_TO_DETERMINE_LEDGER_TIME", - ErrorCategory.ContentionOnSharedResources, - ) { - - final case class Reject(reason: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The participant failed to determine the max ledger time for this command: $reason" - ) - } - - @Explanation( - """This error occurs when the interpretation of a command exceeded the time limit, defined - |as the maximum time that can be assigned by the ledger when it starts processing the command. - |It corresponds to the time assigned upon submission by the participant (the ledger time) + a tolerance - |defined by the `ledgerTimeToRecordTimeTolerance` ledger configuration parameter. - |Reasons for exceeding this limit can vary: the participant may be under high load, the command interpretation - |may be very complex, or even run into an infinite loop due to a mistake in the Daml code. - |""" - ) - @Resolution( - """Due to the halting problem, we cannot determine whether the interpretation will eventually complete. - |As a developer: inspect your code for possible non-terminating loops or consider reducing its complexity. - |As an operator: check and possibly update the resources allocated to the system, as well as the - |time-related configuration parameters (see "Time on Daml Ledgers" in the "Daml Ledger Model Concepts" doc section - |and the `set_ledger_time_record_time_tolerance` console command). - |""" - ) - object TimeExceeded // - extends ErrorCode( - id = "INTERPRETATION_TIME_EXCEEDED", - ErrorCategory.ContentionOnSharedResources, - ) { - - override def logLevel: Level = Level.WARN - - final case class Reject(reason: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = reason) { - override def retryable: Option[ErrorCategoryRetry] = Some( - // As we cannot tell whether the command timed out due to running into an infinite loop, - // because it's too complex, or because the system resources are under heavy load, we need to give - // the application the opportunity to retry. It should not retry "too quickly" though, to avoid entering - // a fast cycle of retry-abort. - // 60 seconds is in the ballpark of the default ledger-time-to-record-time tolerance, so is a reasonable - // amount of time to wait before retrying. - ErrorCategoryRetry(duration = 60.seconds) - ) - } - } - - @Explanation( - """This error occurs if some of the disclosed contracts attached to the command submission that were also used in command interpretation have specified mismatching synchronizer ids. - |This can happen if the synchronizer ids of the disclosed contracts are out of sync OR if the originating contracts are assigned to different synchronizers.""" - ) - @Resolution( - "Retry the submission with an up-to-date set of attached disclosed contracts or re-create a command submission that only uses disclosed contracts residing on the same synchronizer." - ) - object DisclosedContractsSynchronizerIdMismatch - extends ErrorCode( - id = "DISCLOSED_CONTRACTS_SYNCHRONIZER_ID_MISMATCH", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject(mismatchingContractIdToSynchronizerIds: Map[ContractId, String])( - implicit loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"Some disclosed contracts that were used during command interpretation have mismatching synchronizer ids: $mismatchingContractIdToSynchronizerIds" - ) - } - - @Explanation( - """This error occurs when the synchronizer id provided in the command submission mismatches the synchronizer id specified in one of the disclosed contracts used in command interpretation.""" - ) - @Resolution( - "Retry the submission with all disclosed contracts residing on the target submission synchronizer." - ) - object PrescribedSynchronizerIdMismatch - extends ErrorCode( - id = "PRESCRIBED_SYNCHRONIZER_ID_MISMATCH", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject( - usedDisclosedContractsSpecifyingASynchronizerId: Set[ContractId], - disclosedContractsSynchronizerId: String, - prescribedSynchronizerId: String, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The target synchronizer=$prescribedSynchronizerId specified in the command submission mismatches the synchronizer id=$disclosedContractsSynchronizerId of some attached disclosed contracts that have been used in the submission (used-disclosed-contract-ids=$usedDisclosedContractsSpecifyingASynchronizerId)" - ) - } - - @Explanation("Command execution errors raised due to invalid packages.") - object Package extends ErrorGroup() { - @Explanation( - """This error indicates that the uploaded DAR is based on an unsupported language version.""" - ) - @Resolution("Use a DAR compiled with a language version that this participant supports.") - object AllowedLanguageVersions - extends ErrorCode( - id = "ALLOWED_LANGUAGE_VERSIONS", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - def buildCause( - packageId: PackageId, - languageVersion: LanguageVersion, - allowedLanguageVersions: Seq[LanguageVersion], - ): String = - LfError.Package - .AllowedLanguageVersion(packageId, languageVersion, allowedLanguageVersions) - .message - - final case class Error( - packageId: Ref.PackageId, - languageVersion: language.LanguageVersion, - allowedLanguageVersions: Seq[language.LanguageVersion], - )(implicit - val loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = buildCause(packageId, languageVersion, allowedLanguageVersions) - ) - } - - @Explanation( - """This error occurs if a package referred to by a command fails validation. This should not happen as packages are validated when being uploaded.""" - ) - @Resolution("Contact support.") - object PackageValidationFailed - extends ErrorCode( - id = "PACKAGE_VALIDATION_FAILED", - ErrorCategory.SecurityAlert, - ) { - final case class Reject(validationErrorCause: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = validationErrorCause - ) - } - } - - @Explanation( - "Errors raised during command conversion to the internal data representation." - ) - object Preprocessing extends ErrorGroup { - @Explanation("""This error occurs if a command fails during interpreter pre-processing.""") - @Resolution("Inspect error details and correct your application.") - object PreprocessingFailed - extends ErrorCode( - id = "COMMAND_PREPROCESSING_FAILED", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject( - err: LfError.Preprocessing.Error - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = err.message - ) - } - } - - @Explanation( - "Errors raised during the command interpretation phase of the command submission evaluation." - ) - object Interpreter extends ErrorGroup { - @Explanation("""This error occurs if a Daml transaction fails during interpretation.""") - @Resolution("This error type occurs if there is an application error.") - object GenericInterpretationError - extends ErrorCode( - id = "DAML_INTERPRETATION_ERROR", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Error(override val cause: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) - } - - @Explanation( - """This error occurs if a Daml transaction fails during interpretation due to an invalid argument.""" - ) - @Resolution("This error type occurs if there is an application error.") - object InvalidArgumentInterpretationError - extends ErrorCode( - id = "DAML_INTERPRETER_INVALID_ARGUMENT", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Error(override val cause: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) - - } - - @Explanation( - """This error occurs if an exercise or fetch happens on a transaction-locally consumed contract.""" - ) - @Resolution("This error indicates an application error.") - object ContractNotActive - extends ErrorCode( - id = "CONTRACT_NOT_ACTIVE", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - object Reject { - def apply(cause: String, err: LfInterpretationError.ContractNotActive)(implicit - loggingContext: ErrorLoggingContext - ): Reject = Reject( - cause, - err.coid, - Some(err.templateId.toString()), - ) - } - - final case class Reject( - override val cause: String, - coid: ContractId, - templateIdO: Option[String], - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - templateIdO.map(templateId => (ErrorResource.TemplateId, templateId)), - Some((ErrorResource.ContractId, coid.coid)), - ).flatten - } - - } - - @Explanation("Errors raised in lookups during the command interpretation phase.") - object LookupErrors extends ErrorGroup { - @Explanation( - """This error occurs if the Daml engine interpreter cannot resolve a contract key to an active contract. This - |can be caused by either the contract key not being known to the participant, or not being known to - |the submitting parties or the contract representing an already archived key.""" - ) - @Resolution("This error type occurs if there is contention on a contract.") - object ContractKeyNotFound - extends ErrorCode( - id = "CONTRACT_KEY_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - final case class Reject( - override val cause: String, - key: GlobalKey, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - withEncodedValue(key.key) { encodedKey => - Seq( - (ErrorResource.TemplateId, key.templateId.toString), - (ErrorResource.ContractKey, encodedKey), - (ErrorResource.PackageName, key.packageName), - ) - } - } - } - - @Explanation( - """This error occurs if the Daml engine interpreter cannot resolve a package name to any vetted package. This - |can be caused by a commmand using an explicit disclosure produced by a package that hasn't been vetted yet - |by the participant or by a command that uses a contract whose creation package has been force-unvetted.""" - ) - @Resolution( - "Ensure the command doesn't use a package that has not been yet vetted or has been unvetted." - ) - object UnresolvedPackageName - extends ErrorCode( - id = "UNRESOLVED_PACKAGE_NAME", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - final case class Reject(override val cause: String, packageName: Ref.PackageName)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - Seq((ErrorResource.PackageName, packageName)) - } - } - } - @Explanation("""This error occurs if a Daml transaction fails due to an authorization error. - |An authorization means that the Daml transaction computed a different set of required submitters than - |you have provided during the submission as `actAs` parties.""") - @Resolution("This error type occurs if there is an application error.") - object AuthorizationError - extends ErrorCode( - id = "DAML_AUTHORIZATION_ERROR", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject(override val cause: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) - } - - @Explanation( - """This error occurs if a Daml transaction fails due to a rollback of an effectful node.""" - ) - @Resolution("This error type occurs if there is an application error.") - object EffectfulRollback - extends ErrorCode( - id = "DAML_EFFECTFUL_ROLLBACK_ERROR", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) - } - - @Explanation( - """This error occurs when trying to hash an ill-formed contract.""" - ) - @Resolution( - "Ensure that the contract being hashed is a valid contract." - ) - object ContractHashingError - extends ErrorCode( - id = "CONTRACT_HASHING_ERROR", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.ContractHashingError, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - withEncodedValue(err.createArg) { encodedCreateArg => - Seq( - (ErrorResource.ContractId, err.coid.coid), - (ErrorResource.TemplateId, err.dstTemplateId.toString), - (ErrorResource.ContractArg, encodedCreateArg), - ) - } - } - } - - @Explanation( - """This error occurs if a user attempts to provide a key hash for a disclosed contract which we have already cached to be different.""" - ) - @Resolution( - "Ensure the contract ID and contract payload you have provided in your disclosed contract is correct." - ) - object DisclosedContractKeyHashingError - extends ErrorCode( - id = "DISCLOSED_CONTRACT_KEY_HASHING_ERROR", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.DisclosedContractKeyHashingError, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - withEncodedValue(err.key.key) { encodedKey => - Seq( - (ErrorResource.TemplateId, err.key.templateId.toString), - (ErrorResource.ContractId, err.coid.coid), - (ErrorResource.ContractKey, encodedKey), - (ErrorResource.ContractKeyHash, err.declaredHash.toString), - (ErrorResource.PackageName, err.key.packageName), - ) - } - } - } - - private def getTypeIdentifier(t: Ast.Type): Option[Identifier] = - t match { - case Ast.TTyCon(ty) => Some(ty) - case _ => None - } - - @Explanation( - """This error occurs when a user throws an error and does not catch it with try-catch.""" - ) - @Resolution( - "Either your error handling in a choice body is insufficient, or you are using a contract incorrectly." - ) - object UnhandledException - extends ErrorCode( - id = "UNHANDLED_EXCEPTION", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.UnhandledException, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - withEncodedValue(err.value) { encodedValue => - getTypeIdentifier(err.exceptionType) - .map(ty => - Seq( - (ErrorResource.ExceptionType, ty.toString), - (ErrorResource.ExceptionValue, encodedValue), - ) - ) - .getOrElse(Nil) - } - } - } - - @Explanation( - """This error occurs when a user calls abort or error on an LF version before native exceptions were introduced.""" - ) - @Resolution( - "Either remove the call to abort, error or perhaps assert, or ensure you are exercising your contract choice as the author expects." - ) - object InterpretationUserError - extends ErrorCode( - id = "INTERPRETATION_USER_ERROR", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.UserError, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.ExceptionText, err.message) - ) - } - } - - @Explanation( - """This error occurs when a contract's pre-condition (the ensure clause) is violated on contract creation.""" - ) - @Resolution( - "Ensure the contract argument you are passing into your create doesn't violate the conditions of the contract." - ) - object TemplatePreconditionViolated - extends ErrorCode( - id = "TEMPLATE_PRECONDITION_VIOLATED", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) - } - - @Explanation( - """This error occurs when you try to create a contract that has a key, but with empty maintainers.""" - ) - @Resolution( - "Check the definition of the contract key's maintainers, and ensure this list won't be empty given your creation arguments." - ) - object CreateEmptyContractKeyMaintainers - extends ErrorCode( - id = "CREATE_EMPTY_CONTRACT_KEY_MAINTAINERS", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.CreateEmptyContractKeyMaintainers, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - withEncodedValue(err.arg) { encodedArg => - Seq( - (ErrorResource.TemplateId, err.templateId.toString), - (ErrorResource.ContractArg, encodedArg), - ) - } - } - } - - @Explanation( - """This error occurs when you try to fetch a contract by key, but that key would have empty maintainers.""" - ) - @Resolution( - "Check the definition of the contract key's maintainers, and ensure this list won't be empty given the contract key you are fetching." - ) - object FetchEmptyContractKeyMaintainers - extends ErrorCode( - id = "FETCH_EMPTY_CONTRACT_KEY_MAINTAINERS", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.FetchEmptyContractKeyMaintainers, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - withEncodedValue(err.key) { encodedKey => - Seq( - (ErrorResource.TemplateId, err.templateId.toString), - (ErrorResource.ContractKey, encodedKey), - (ErrorResource.PackageName, err.packageName), - ) - } - } - } - - @Explanation( - """This error occurs when you try to fetch/use a contract in some way with a contract ID that doesn't match the template type on the ledger.""" - ) - @Resolution( - "Ensure the contract IDs you are using are of the type we expect on the ledger. Avoid unsafely coercing contract IDs." - ) - object WronglyTypedContract - extends ErrorCode( - id = "WRONGLY_TYPED_CONTRACT", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.WronglyTypedContract, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.ContractId, err.coid.coid), - (ErrorResource.TemplateId, err.expected.toString), - (ErrorResource.TemplateId, err.actual.toString), - ) - } - } - - @Explanation( - """This error occurs when you try to coerce/use a contract via an interface that it does not implement.""" - ) - @Resolution( - "Ensure the contract you are calling does implement the interface you are using to do so. Avoid writing LF/low-level interface implementation classes manually." - ) - object ContractDoesNotImplementInterface - extends ErrorCode( - id = "CONTRACT_DOES_NOT_IMPLEMENT_INTERFACE", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.ContractDoesNotImplementInterface, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.ContractId, err.coid.coid), - (ErrorResource.TemplateId, err.templateId.toString), - (ErrorResource.InterfaceId, err.interfaceId.toString), - ) - } - } - - @Explanation( - """This error occurs when you try to create/use a contract that does not implement the requiring interfaces of some other interface that it does implement.""" - ) - @Resolution( - "Ensure you implement all required interfaces correctly, and avoid writing LF/low-level interface implementation classes manually." - ) - object ContractDoesNotImplementRequiringInterface - extends ErrorCode( - id = "CONTRACT_DOES_NOT_IMPLEMENT_REQUIRING_INTERFACE", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.ContractDoesNotImplementRequiringInterface, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.ContractId, err.coid.coid), - (ErrorResource.TemplateId, err.templateId.toString), - (ErrorResource.InterfaceId, err.requiredInterfaceId.toString), - (ErrorResource.InterfaceId, err.requiringInterfaceId.toString), - ) - } - } - - @Explanation( - """This error occurs when you attempt to compare two values of different types using the built-in comparison types.""" - ) - @Resolution( - "Avoid using the low level comparison build, and instead use the Eq class." - ) - object NonComparableValues - extends ErrorCode( - id = "NON_COMPARABLE_VALUES", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) - } - - @Explanation( - """This error occurs when a contract key contains a contract ID, which is illegal for hashing reasons.""" - ) - @Resolution( - "Ensure your contracts key field cannot contain a contract ID." - ) - object ContractIdInContractKey - extends ErrorCode( - id = "CONTRACT_ID_IN_CONTRACT_KEY", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) - } - - @Explanation( - """This error occurs when you attempt to compare a global and local contract ID of the same discriminator.""" - ) - @Resolution( - "Avoid constructing contract IDs manually." - ) - object ContractIdComparability - extends ErrorCode( - id = "CONTRACT_ID_COMPARABILITY", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.ContractIdComparability, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.ContractId, err.globalCid.coid) - ) - } - } - - @Explanation("This error occurs when you nest values too deeply.") - @Resolution("Restructure your code and reduce value nesting.") - object ValueNesting - extends ErrorCode(id = "VALUE_NESTING", ErrorCategory.InvalidIndependentOfSystemState) { - - final case class Reject(override val cause: String, err: LfInterpretationError.ValueNesting)( - implicit loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = cause) {} - } - - @Explanation( - "This error occurs when a Daml text is not a valid UTF-8 string or contains null characters." - ) - @Resolution("Restructure your code and reduce value nesting.") - object MalformedText - extends ErrorCode(id = "MALFORMED_TEXT", ErrorCategory.InvalidIndependentOfSystemState) { - - final case class Reject(override val cause: String, err: LfInterpretationError.MalformedText)( - implicit loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = cause) {} - } - - @Explanation( - "This error is thrown by use of `failWithStatus` in daml code. The Daml code determines the canton error category, and thus the grpc status code." - ) - @Resolution( - "Ensure that you are using the contract correctly, and that the choice implementation does not have a bug." - ) - object FailureStatus - extends ErrorCode( - id = "DAML_FAILURE", - ErrorCategory.OverrideDocStringErrorCategory(""), - ) { - - // Building conveyance string from OverrideDocStringErrorCategory will fail, and would be wrong - override def errorConveyanceDocString: Option[String] = Some( - "Conveyance is determined by the category, which is selected in daml code. Refer to the documentation of the actual error category encoded in the raised error for details." - ) - - def Reject( - cause: String, - err: LfInterpretationError.FailureStatus, - trace: Option[String], - )(implicit - loggingContext: ErrorLoggingContext - ) = ErrorCategory.fromInt(err.failureCategory) match { - case Some(errorCategory) => - new DamlErrorWithDefiniteAnswer( - cause = cause - )( - code = new ErrorCode(id = FailureStatus.id, errorCategory)(FailureStatus.parent) {}, - loggingContext = loggingContext, - ) { - override def context: Map[String, String] = - // ++ on maps takes last key, we don't want users to override `error_id`, so we add this last - // SerializableErrorCodeComponents also puts `context` first, so fields added by canton cannot be overwritten - super.context ++ err.metadata ++ List(("error_id", err.errorId)) ++ trace - .map(("exercise_trace", _)) - .toList - } - case None => - LedgerApiErrors.InternalError.Generic( - s"Error category ordinal ${err.failureCategory} is not a valid error category. " - + s"This is likely a programming bug. Please report this error. Original raised error cause: $cause" - ) - } - } - - @Explanation("Errors that occur when trying to upgrade a contract") - object UpgradeError extends ErrorGroup { - @Explanation("Validation fails when trying to upgrade the contract") - @Resolution( - "Verify that neither the signatories, nor the observers, nor the contract key, nor the key's maintainers, nor the package name have changed" - ) - object ValidationFailed - extends ErrorCode( - id = "INTERPRETATION_UPGRADE_ERROR_VALIDATION_FAILED", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject( - override val cause: String, - err: LfInterpretationError.Upgrade.ValidationFailed, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = { - def optKeyResources( - keyOpt: Option[GlobalKeyWithMaintainers] - ): Seq[(ErrorResource, String)] = - Seq( - ( - ErrorResource.ContractKey.nullable, - keyOpt.flatMap(key => tryEncodeValue(key.globalKey.key)).getOrElse("NULL"), - ), - ( - ErrorResource.PackageName.nullable, - keyOpt.map(_.globalKey.packageName).getOrElse("NULL"), - ), - ( - ErrorResource.Parties.nullable, - keyOpt.map(_.maintainers.mkString(",")).getOrElse("NULL"), - ), - ) - - Seq( - (ErrorResource.ContractId, err.coid.coid), - (ErrorResource.TemplateId, err.srcTemplateId.toString), - (ErrorResource.TemplateId, err.dstTemplateId.toString), - (ErrorResource.PackageName, err.srcPackageName), - (ErrorResource.PackageName, err.dstPackageName), - ) ++ encodeParties(err.originalSignatories) ++ - encodeParties(err.originalObservers) ++ - optKeyResources(err.originalKeyOpt) ++ - encodeParties(err.recomputedSignatories) ++ - encodeParties(err.recomputedObservers) ++ - optKeyResources(err.recomputedKeyOpt) - } - } - } - - @Explanation("Contract is malformed or doesn't match the expected type") - @Resolution( - "Verify that the template used for loading the contract is upgrade-compatible with the template that created it." - ) - object TranslationFailed - extends ErrorCode( - id = "INTERPRETATION_UPGRADE_ERROR_TRANSLATION_FAILED", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject( - override val cause: String, - err: LfInterpretationError.Upgrade.TranslationFailed, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - withEncodedValue(err.createArg) { encodedArg => - Seq( - (ErrorResource.ContractId.nullable, err.coid.fold("NULL")(_.coid)), - (ErrorResource.TemplateId, err.srcTemplateId.toString), - (ErrorResource.TemplateId, err.dstTemplateId.toString), - (ErrorResource.ContractArg, encodedArg), - ) - } - } - } - - @Explanation("Cannot authenticate contract") - @Resolution( - "Verify that the template used for loading the contract is upgrade-compatible with the template that created it." - ) - object AuthenticationFailed - extends ErrorCode( - id = "INTERPRETATION_UPGRADE_ERROR_AUTHENTICATION_FAILED", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject( - override val cause: String, - err: LfInterpretationError.Upgrade.AuthenticationFailed, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - withEncodedValue(err.createArg) { encodedArg => - Seq( - (ErrorResource.ContractId, err.coid.coid), - (ErrorResource.TemplateId, err.srcTemplateId.toString), - (ErrorResource.TemplateId, err.dstTemplateId.toString), - (ErrorResource.ContractArg, encodedArg), - ) - } - } - } - } - - @Explanation("Errors that occur when using cyptography primitives") - object CryptoError extends ErrorGroup { - @Explanation( - "Hex string is malformed" - ) - @Resolution( - "Ensure string is non-empty, of even length and only contains hex characters (i.e. matches [0-9a-fA-F]+)" - ) - object MalformedByteEncoding - extends ErrorCode( - id = "INTERPRETATION_CRYPTO_ERROR_MALFORMED_BYTE_ENCODING", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject( - override val cause: String, - err: LfInterpretationError.Crypto.MalformedByteEncoding, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.CryptoValue, err.value) - ) - } - } - @Explanation( - "Public key hex encoding is malformed" - ) - @Resolution( - "Ensure public key is a DER encoded Secp256k1 public key" - ) - object MalformedKey - extends ErrorCode( - id = "INTERPRETATION_CRYPTO_ERROR_MALFORMED_KEY", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject( - override val cause: String, - err: LfInterpretationError.Crypto.MalformedKey, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.CryptoValue, err.key) - ) - } - } - @Explanation( - "Signature hex encoding is malformed" - ) - @Resolution( - "Ensure signature is a DER encoded Keccak256 digest" - ) - object MalformedSignature - extends ErrorCode( - id = "INTERPRETATION_CRYPTO_ERROR_MALFORMED_SIGNATURE", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject( - override val cause: String, - err: LfInterpretationError.Crypto.MalformedSignature, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.CryptoValue, err.signature) - ) - } - } - } - - @Explanation( - """This error is a catch-all for errors thrown by in-development features, and should never be thrown in production.""" - ) - @Resolution( - "See the error message for details of the specific in-development feature error. If this is production, avoid using development features." - ) - object InterpretationDevError - extends ErrorCode( - id = "INTERPRETATION_DEV_ERROR", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject( - override val cause: String, - err: LfInterpretationError.Dev.Error, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - - override def resources: Seq[(ErrorResource, String)] = - Seq( - (ErrorResource.DevErrorType, err.getClass.getSimpleName) - ) - } - } - } - - @Explanation( - """This error occurs when topology-aware package selection could not yield a valid candidate set of vetted packages needed for command submission.""" - ) - @Resolution( - "Inspect the error message and adjust the topology state or the submitted command" - ) - object PackageSelectionFailed - extends ErrorCode( - id = "PACKAGE_SELECTION_FAILED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject( - override val cause: String - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) {} - } - - @Explanation( - "A package-name required in command interpretation was discarded in topology-aware package selection due to vetting topology restrictions." - ) - @Resolution( - "Revisit the command submission and ensure it conforms with the vetted topology state of the submitters and informees." - ) - object PackageNameDiscardedDueToUnvettedPackages - extends ErrorCode( - id = "PACKAGE_NAME_DISCARDED_DUE_TO_UNVETTED_PACKAGES", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - final case class Reject( - pkgName: Ref.PackageName, - reference: Reference, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"Command interpretation failed: No packages with valid vetting exist that conform to topology restrictions for $pkgName, encountered in $reference." - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/ConsistencyErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/ConsistencyErrors.scala deleted file mode 100644 index 4fd9237402..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/ConsistencyErrors.scala +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error.groups - -import com.digitalasset.base.error.{ - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - ErrorResource, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup.ConsistencyErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.transaction.GlobalKey -import com.digitalasset.daml.lf.value.Value - -@Explanation( - "Potential consistency errors raised due to race conditions during command submission or returned as submission rejections by the backing ledger." -) -object ConsistencyErrors extends ConsistencyErrorGroup { - - @Explanation("A command with the given command id has already been successfully processed.") - @Resolution( - """The correct resolution depends on the use case. If the error received pertains to a submission retried due to a timeout, - |do nothing, as the previous command has already been accepted. - |If the intent is to submit a new command, re-submit using a distinct command id. - |""" - ) - object DuplicateCommand - extends ErrorCode( - id = "DUPLICATE_COMMAND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, - ) { - - final case class Reject( - override val definiteAnswer: Boolean = false, - existingCommandSubmissionId: Option[String], - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = "A command with the given command id has already been successfully processed", - definiteAnswer = definiteAnswer, - ) { - override def context: Map[String, String] = - super.context ++ existingCommandSubmissionId - .map("existing_submission_id" -> _) - .toList - } - } - - @Explanation("At least one input has been altered by a concurrent transaction submission.") - @Resolution( - "The correct resolution depends on the business flow, for example it may be possible to proceed " + - "without an archived contract as an input, or the transaction submission may be retried " + - "to load the up-to-date value of a contract key." - ) - object Inconsistent - extends ErrorCode( - id = "INCONSISTENT", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class Reject( - details: String - )(implicit loggingContext: ErrorLoggingContext) - extends DamlErrorWithDefiniteAnswer( - cause = s"Inconsistent: $details" - ) - - } - - @Explanation( - """This error occurs if the Daml engine can not find a referenced contract. This - |can be caused by either the contract not being known to the participant, or not being known to - |the submitting parties or already being archived.""" - ) - @Resolution("This error type occurs if there is contention on a contract.") - object ContractNotFound - extends ErrorCode( - id = "CONTRACT_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - final case class Reject( - override val cause: String, - cid: Value.ContractId, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - (ErrorResource.ContractId, cid.coid) - ) - } - - } - - @Explanation( - "An input contract key was re-assigned to a different contract by a concurrent transaction submission." - ) - @Resolution("Retry the transaction submission.") - object InconsistentContractKey - extends ErrorCode( - id = "INCONSISTENT_CONTRACT_KEY", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - - final case class RejectWithContractKeyArg( - override val cause: String, - key: GlobalKey, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - CommandExecutionErrors.withEncodedValue(key.key) { encodedKey => - Seq( - // TODO(i12763): Reconsider the transport format for the contract key. - // If the key is big, it can force chunking other resources. - (ErrorResource.TemplateId, key.templateId.toString), - (ErrorResource.ContractKey, encodedKey), - (ErrorResource.PackageName, key.packageName), - ) - } - } - - final case class Reject(reason: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = reason) - - } - - @Explanation( - """This error signals that within the transaction we got to a point where two contracts with the same key were active.""" - ) - @Resolution("This error indicates an application error.") - object DuplicateContractKey - extends ErrorCode( - id = "DUPLICATE_CONTRACT_KEY", - ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, - ) { - - final case class RejectWithContractKeyArg( - override val cause: String, - key: GlobalKey, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause - ) { - override def resources: Seq[(ErrorResource, String)] = - CommandExecutionErrors.withEncodedValue(key.key) { encodedKey => - Seq( - // TODO(i12763): Reconsider the transport format for the contract key. - // If the key is big, it can force chunking other resources. - (ErrorResource.TemplateId, key.templateId.toString), - (ErrorResource.ContractKey, encodedKey), - (ErrorResource.PackageName, key.packageName), - ) - } - } - - final case class Reject(override val cause: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = cause) - - } - - @Explanation( - "Another command submission with the same change ID (user ID, command ID, actAs) is already being processed." - ) - @Resolution( - """Listen to the command completion stream until a completion for the in-flight command submission is published. - |Alternatively, resubmit the command. If the in-flight submission has finished successfully by then, - |this will return more detailed information about the earlier one. - |If the in-flight submission has failed by then, the resubmission will attempt to record the new transaction on the ledger. - |""" - ) - object SubmissionAlreadyInFlight - extends ErrorCode( - id = "SUBMISSION_ALREADY_IN_FLIGHT", - ErrorCategory.ContentionOnSharedResources, - ) { - // used by Ledger API command tracking - final case class Reject( - override val definiteAnswer: Boolean = false - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - "A submission with the given change ID (user ID, command ID, actAs) and submission ID is already in flight", - definiteAnswer = definiteAnswer, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/IdentityProviderConfigServiceErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/IdentityProviderConfigServiceErrors.scala deleted file mode 100644 index 04302bfaf4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/IdentityProviderConfigServiceErrors.scala +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error.groups - -import com.digitalasset.base.error.{ - ContextualizedDamlError, - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - ErrorResource, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup.AdminServicesErrorGroup.IdentityProviderConfigServiceErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext - -object IdentityProviderConfigServiceErrors extends IdentityProviderConfigServiceErrorGroup { - - @Explanation( - "There was an attempt to update an identity provider config using an invalid update request." - ) - @Resolution( - """|Inspect the error details for specific information on what made the request invalid. - |Retry with an adjusted update request.""" - ) - object InvalidUpdateIdentityProviderConfigRequest - extends ErrorCode( - id = "INVALID_IDENTITY_PROVIDER_UPDATE_REQUEST", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject(identityProviderId: String, reason: String)(implicit - loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = - s"Update operation for identity provider config '$identityProviderId' failed due to: $reason" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.IdentityProviderConfig -> identityProviderId - ) - } - } - - @Explanation("The identity provider config referred to by the request was not found.") - @Resolution( - "Check that you are connecting to the right participant node and the identity provider config is spelled correctly, or create the configuration." - ) - object IdentityProviderConfigNotFound - extends ErrorCode( - id = "IDP_CONFIG_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(operation: String, identityProviderId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$operation failed for unknown identity provider id=\"$identityProviderId\"" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.IdentityProviderConfig -> identityProviderId - ) - } - } - - @Explanation("The identity provider config referred to by the request was not found.") - @Resolution( - "Check that you are connecting to the right participant node and the identity provider config is spelled correctly, or create the configuration." - ) - object IdentityProviderConfigByIssuerNotFound - extends ErrorCode( - id = "IDP_CONFIG_BY_ISSUER_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(operation: String, issuer: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$operation failed for unknown identity provider issuer=\"$issuer\"" - ) - } - - @Explanation( - "There already exists an identity provider configuration with the same identity provider id." - ) - @Resolution( - "Check that you are connecting to the right participant node and the identity provider id is spelled correctly, or use an identity provider that already exists." - ) - object IdentityProviderConfigAlreadyExists - extends ErrorCode( - id = "IDP_CONFIG_ALREADY_EXISTS", - ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, - ) { - final case class Reject(operation: String, identityProviderId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$operation failed, as identity provider \"$identityProviderId\" already exists" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.IdentityProviderConfig -> identityProviderId - ) - } - } - - @Explanation( - "There already exists an identity provider configuration with the same issuer." - ) - @Resolution( - "Check that you are connecting to the right participant node and the identity provider id is spelled correctly, or use an identity provider that already exists." - ) - object IdentityProviderConfigIssuerAlreadyExists - extends ErrorCode( - id = "IDP_CONFIG_ISSUER_ALREADY_EXISTS", - ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, - ) { - final case class Reject(operation: String, issuer: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$operation failed, as identity provider with issuer \"$issuer\" already exists" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.IdentityProviderConfig -> issuer - ) - } - } - - @Explanation( - """|A system can have only a limited number of identity provider configurations. - |There was an attempt to create an identity provider configuration.""" - ) - @Resolution( - """|Delete some of the already existing identity provider configurations. - |Contact the participant operator if the limit is too low.""" - ) - object TooManyIdentityProviderConfigs - extends ErrorCode( - id = "TOO_MANY_IDENTITY_PROVIDER_CONFIGS", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(operation: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$operation failed." - ) {} - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/PartyManagementServiceErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/PartyManagementServiceErrors.scala deleted file mode 100644 index e1eb04fb11..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/PartyManagementServiceErrors.scala +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error.groups - -import com.digitalasset.base.error.{ - ContextualizedDamlError, - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - ErrorResource, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup.AdminServicesErrorGroup.PartyManagementServiceErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext - -object PartyManagementServiceErrors extends PartyManagementServiceErrorGroup { - - @Explanation("There was an attempt to update a party using an invalid update request.") - @Resolution( - """|Inspect the error details for specific information on what made the request invalid. - |Retry with an adjusted update request.""" - ) - object InvalidUpdatePartyDetailsRequest - extends ErrorCode( - id = "INVALID_PARTY_DETAILS_UPDATE_REQUEST", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject(party: String, reason: String)(implicit - loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = s"Update operation for party '$party' failed due to: $reason" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.Party -> party - ) - } - } - - @Explanation( - """|A party can have at most 256kb worth of annotations in total measured in number of bytes in UTF-8 encoding. - |There was an attempt to allocate or update a party such that this limit would have been exceeded.""" - ) - @Resolution( - "Retry with fewer annotations or delete some of the party's existing annotations." - ) - object MaxPartyAnnotationsSizeExceeded - extends ErrorCode( - id = "MAX_PARTY_DETAILS_ANNOTATIONS_SIZE_EXCEEDED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(party: String)(implicit - loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = s"Maximum annotations size for party '$party' has been exceeded" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.Party -> party - ) - } - } - - @Explanation( - """|Concurrent updates to a party can be controlled by supplying an update request with a resource version (this is optional). - |A party's resource version can be obtained by reading the party on the Ledger API. - |There was attempt to update a party using a stale resource version, indicating that a different process had updated the party earlier.""" - ) - @Resolution( - """|Read this party again to obtain its most recent state and - |in particular its most recent resource version. Use the obtained information to build and send a new update request.""" - ) - object ConcurrentPartyDetailsUpdateDetected - extends ErrorCode( - id = "CONCURRENT_PARTY_DETAILS_UPDATE_DETECTED", - ErrorCategory.ContentionOnSharedResources, - ) { - final case class Reject(party: String)(implicit - loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = - s"Update operation for party '$party' failed due to a concurrent update to the same party" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.Party -> party - ) - } - } - - @Explanation("The party referred to by the request was not found.") - @Resolution( - "Check that you are connecting to the right participant node and that the party is spelled correctly." - ) - object PartyNotFound - extends ErrorCode( - id = "PARTY_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(operation: String, party: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"Party: '$party' was not found when $operation" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.Party -> party - ) - } - } - - @Explanation( - """|Each on-ledger party known to this participant node can have a participant's local metadata assigned to it. - |The local information about a party referred to by this request was not found when it should have been found.""" - ) - @Resolution( - "This error can indicate a problem with the server's storage or implementation." - ) - object InternalPartyRecordNotFound - extends ErrorCode( - id = "INTERNAL_PARTY_RECORD_NOT_FOUND", - ErrorCategory.SystemInternalAssumptionViolated, - ) { - final case class Reject(operation: String, party: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"Party record for party: '$party' was not found when $operation" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.Party -> party - ) - } - } - - @Explanation( - """|Each on-ledger party known to this participant node can have a participant's local metadata assigned to it. - |The local information about a party referred to by this request was found when it should have been not found.""" - ) - @Resolution( - "This error can indicate a problem with the server's storage or implementation." - ) - object InternalPartyRecordAlreadyExists - extends ErrorCode( - id = "INTERNAL_PARTY_RECORD_ALREADY_EXISTS", - ErrorCategory.SystemInternalAssumptionViolated, - ) { - final case class Reject(operation: String, party: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"Party record for party: '$party' already exists when $operation" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.Party -> party - ) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/RequestValidationErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/RequestValidationErrors.scala deleted file mode 100644 index f194308ec0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/RequestValidationErrors.scala +++ /dev/null @@ -1,562 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error.groups - -import com.digitalasset.base.error.{ - ContextualizedDamlError, - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - ErrorGroup, - ErrorResource, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.LedgerApiErrors.{ - EarliestOffsetMetadataKey, - LatestOffsetMetadataKey, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup.RequestValidationErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.daml.lf.data.{Ref, Time} -import com.digitalasset.daml.lf.language.{LookupError, Reference} -import com.digitalasset.daml.lf.value.Value.ContractId - -import java.time.Duration - -@Explanation( - "Validation errors raised when evaluating requests in the Ledger API." -) -object RequestValidationErrors extends RequestValidationErrorGroup { - object NotFound extends ErrorGroup() { - @Explanation( - "This rejection is given when a read request tries to access a package which does not exist on the ledger." - ) - @Resolution("Use a package id pertaining to a package existing on the ledger.") - object Package - extends ErrorCode( - id = "PACKAGE_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(packageId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = "Could not find package." - ) { - - override def resources: Seq[(ErrorResource, String)] = - super.resources :+ ((ErrorResource.DalfPackage, packageId)) - } - - final case class InterpretationReject( - pkgRef: Ref.PackageRef, - reference: Reference, - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = LookupError.MissingPackage.pretty(pkgRef, reference) - ) - } - - @Explanation( - "The update does not exist or the update format specified filters it out." - ) - @Resolution( - "Check the update id or offset and verify that the requested update is not being filtered out by the update format." - ) - object Update - extends ErrorCode( - id = "UPDATE_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - final case class RejectWithTxId(updateId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = "Update not found, or not visible.") { - override def resources: Seq[(ErrorResource, String)] = Seq( - (ErrorResource.UpdateId, updateId) - ) - } - - final case class RejectWithOffset(offset: Long)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = "Update not found, or not visible.") { - override def resources: Seq[(ErrorResource, String)] = Seq( - (ErrorResource.Offset, offset.toString) - ) - } - } - - @Explanation( - "Events for the specified contract ID do not exist or the event format specified filters them out." - ) - @Resolution( - "Check the contract ID and verify that the requested events are not being filtered out by the event format." - ) - object ContractEvents - extends ErrorCode( - id = "CONTRACT_EVENTS_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - final case class Reject(contractId: ContractId)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = "Contract events not found, or not visible.") { - override def resources: Seq[(ErrorResource, String)] = Seq( - (ErrorResource.ContractId, contractId.coid) - ) - } - } - - @Explanation( - """This error occurs if the contract cannot be found for the referenced contract. This - |can be caused by either the contract not being known to the participant, or not being known to - |the requesting parties.""" - ) - @Resolution( - "Check the contract ID and verify that the requesting parties have intersection with the contract stakeholders." - ) - object ContractPayload - extends ErrorCode( - id = "CONTRACT_PAYLOAD_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - final case class Reject(contractId: ContractId)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = "Contract payload not found, or not visible.") { - override def resources: Seq[(ErrorResource, String)] = Seq( - (ErrorResource.ContractId, contractId.coid) - ) - } - } - - @Explanation( - "The queried template or interface ids do not exist." - ) - @Resolution( - "Use valid template or interface ids in your query or ask the participant operator to upload the package containing the necessary interfaces/templates." - ) - object TemplateOrInterfaceIdsNotFound - extends ErrorCode( - id = "TEMPLATES_OR_INTERFACES_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - - private def buildCause( - unknownTemplatesOrInterfaces: Seq[Either[Ref.Identifier, Ref.Identifier]] - ): String = { - val unknownTemplateIds = - unknownTemplatesOrInterfaces.collect { case Left(identifier) => identifier.toString } - val unknownInterfaceIds = - unknownTemplatesOrInterfaces.collect { case Right(identifier) => identifier.toString } - - val templatesMessage = if (unknownTemplateIds.nonEmpty) { - s"Templates do not exist: [${unknownTemplateIds.mkString(", ")}]. " - } else "" - val interfacesMessage = if (unknownInterfaceIds.nonEmpty) { - s"Interfaces do not exist: [${unknownInterfaceIds.mkString(", ")}]. " - } else - "" - (templatesMessage + interfacesMessage).trim - } - - final case class Reject( - unknownTemplatesOrInterfaces: Seq[Either[Ref.Identifier, Ref.Identifier]] - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = buildCause(unknownTemplatesOrInterfaces)) { - override def resources: Seq[(ErrorResource, String)] = - unknownTemplatesOrInterfaces.map { - case Left(templateId) => ErrorResource.TemplateId -> templateId.toString - case Right(interfaceId) => ErrorResource.InterfaceId -> interfaceId.toString - } - } - } - - @Explanation( - "The queried package names do not match packages uploaded on this participant." - ) - @Resolution( - "Use valid package names or ask the participant operator to upload the necessary packages." - ) - object PackageNamesNotFound - extends ErrorCode( - id = "PACKAGE_NAMES_NOT_FOUND", - category = ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(unknownPackageNames: Set[Ref.PackageName])(implicit - errorLoggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The following package names do not match upgradable packages uploaded on this participant: [${unknownPackageNames - .mkString(", ")}]." - ) - } - - @Explanation( - "The queried type reference for the specified package name and template qualified-name does not reference any template uploaded on this participant" - ) - @Resolution( - "Use a template qualified-name referencing already uploaded template-ids or ask the participant operator to upload the necessary packages." - ) - object NoTemplatesForPackageNameAndQualifiedName - extends ErrorCode( - id = "NO_TEMPLATES_FOR_PACKAGE_NAME_AND_QUALIFIED_NAME", - category = ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(noKnownReferences: Set[(Ref.PackageName, Ref.QualifiedName)])(implicit - errorLoggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The following package-name/template qualified-name pairs do not reference any template-id uploaded on this participant: [${noKnownReferences - .mkString(", ")}]." - ) - } - - @Explanation( - "The queried type reference for the specified package name and interface qualified-name does not reference any interface uploaded on this participant" - ) - @Resolution( - "Use a interface qualified-name referencing already uploaded interface-ids or ask the participant operator to upload the necessary packages." - ) - object NoInterfaceForPackageNameAndQualifiedName - extends ErrorCode( - id = "NO_INTERFACE_FOR_PACKAGE_NAME_AND_QUALIFIED_NAME", - category = ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(noKnownReferences: Set[(Ref.PackageName, Ref.QualifiedName)])(implicit - errorLoggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The following package-name/interface qualified-name pairs do not reference any interface-id uploaded on this participant: [${noKnownReferences - .mkString(", ")}]." - ) - } - } - - @Explanation("This rejection is given when a read request tries to access pruned data.") - @Resolution("Use an offset that is after the pruning offset.") - object ParticipantPrunedDataAccessed - extends ErrorCode( - id = "PARTICIPANT_PRUNED_DATA_ACCESSED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(override val cause: String, earliestOffset: Long)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause, - extraContext = Map(EarliestOffsetMetadataKey -> earliestOffset), - ) - } - - @Explanation("Pruning in progress.") - @Resolution( - "Only one pruning operation is allowed to execute at a given time. Please try pruning later." - ) - object ParticipantPruningInProgress - extends ErrorCode( - id = "PARTICIPANT_PRUNING_IN_PROGRESS", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject()(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = "Pruning in progress. Please try later.") - } - - @Explanation( - "This rejection is given when a read request tries to access data after the ledger end" - ) - @Resolution("Use an offset that is before the ledger end.") - object ParticipantDataAccessedAfterLedgerEnd - extends ErrorCode( - id = "PARTICIPANT_DATA_ACCESSED_AFTER_LEDGER_END", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(override val cause: String, latestOffset: Long)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = cause, - extraContext = Map(LatestOffsetMetadataKey -> latestOffset), - ) - } - - @Explanation( - "This rejection is given when a read request uses an offset beyond the current ledger end." - ) - @Resolution("Use an offset that is before the ledger end.") - object OffsetAfterLedgerEnd - extends ErrorCode( - id = "OFFSET_AFTER_LEDGER_END", - ErrorCategory.InvalidGivenCurrentSystemStateSeekAfterEnd, - ) { - final case class Reject(offsetType: String, requestedOffset: Long, ledgerEnd: Long)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$offsetType offset ($requestedOffset) is after ledger end ($ledgerEnd)" - ) - } - - @Explanation( - "This rejection is given when a read request uses an offset invalid in the requests' context." - ) - @Resolution("Inspect the error message and use a valid offset.") - object OffsetOutOfRange - extends ErrorCode( - id = "OFFSET_OUT_OF_RANGE", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(message: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer(cause = message) - } - - @Explanation( - """This error is emitted when a mandatory field is not set in a submitted ledger API command.""" - ) - @Resolution("Inspect the reason given and correct your application.") - object MissingField - extends ErrorCode(id = "MISSING_FIELD", ErrorCategory.InvalidIndependentOfSystemState) { - final case class Reject(missingField: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"The submitted command is missing a mandatory field: $missingField", - extraContext = Map("field_name" -> missingField), - ) - } - - @Explanation( - """This error is emitted when an attempt is made to submit a transaction with at a time that is outside the ledger time bounds required by the transaction.""" - ) - @Resolution( - "If the time bounds are in the future then retry within the time bounds, if in the past a new transaction needs to be prepared with a current or future time" - ) - object LedgerTimeOutsideBounds - extends ErrorCode( - id = "LEDGER_TIME_OUTSIDE_BOUNDS", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject(ledgerEffectiveTime: Time.Timestamp, timeBoundaries: Time.Range)( - implicit loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The submitted command with a ledger effective time of $ledgerEffectiveTime is outside of the bounds required by the transaction $timeBoundaries", - extraContext = Map( - "ledger_effective_time" -> ledgerEffectiveTime.toString, - "time_boundaries" -> timeBoundaries.toString, - ), - ) - } - - @Explanation( - """This error is emitted when a submitted ledger API command contains an invalid argument.""" - ) - @Resolution("Inspect the reason given and correct your application.") - object InvalidArgument - extends ErrorCode(id = "INVALID_ARGUMENT", ErrorCategory.InvalidIndependentOfSystemState) { - final case class Reject(reason: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"The submitted request has invalid arguments: $reason" - ) - } - - @Explanation( - """This error is emitted when a submitted ledger API command contains disclosed contracts with conflicting payloads for the same contract ID.""" - ) - @Resolution( - "This may be considered a security incident or a defect on the server. Please contact support or the providers of the disclosed contract payloads." - ) - object DisclosedContractsConflictingPayloads - extends ErrorCode( - id = "DISCLOSED_CONTRACTS_CONFLICTING_PAYLOADS", - ErrorCategory.SecurityAlert, - ) { - final case class Reject(conflictingContractPayloads: List[(String, Long, String)])(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = conflictingContractPayloads - .map { case (contractId, payloadCounts, payloads) => - s"The contractId $contractId for submitted request has conflicting $payloadCounts payloads for disclosed contracts: $payloads" - } - .mkString("\n") - ) - } - - @Explanation( - """This error is emitted when a submitted ledger API command refers to a non-existing resource.""" - ) - @Resolution("Inspect the reason given and correct your application.") - object UnknownResource - extends ErrorCode( - id = "UNKNOWN_RESOURCE", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(errorResource: ErrorResource, items: Seq[String], reason: String)( - implicit loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The submitted request refers to a non-existing ${errorResource.asString}: $reason" - ) { - override def resources: Seq[(ErrorResource, String)] = - super.resources ++ items.map(s => (errorResource, s)) - } - } - - @Explanation( - """This error is emitted when a submitted ledger API command contains a field value that cannot be understood.""" - ) - @Resolution("Inspect the reason given and correct your application.") - object InvalidField - extends ErrorCode(id = "INVALID_FIELD", ErrorCategory.InvalidIndependentOfSystemState) { - final case class Reject(fieldName: String, message: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The submitted command has a field with invalid value: Invalid field $fieldName: $message" - ) - } - - @Explanation( - """This error is emitted when a submitted ledger API command contains a continuation token which cannot be verified. - |When an ACS request is made with a continuation token, the token must be taken from a valid - |GetActiveContractsResponse and used with the same EventFormat settings, with the same Canton participant - |running the same Canton version. These tokens are not intended to be stored and used much later under different circumstances.""" - ) - @Resolution("Inspect the reason given and correct your application.") - object InvalidContinuationToken - extends ErrorCode( - id = "INVALID_CONTINUATION_TOKEN", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject()(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The submitted command contains an invalid continuation token. Tokens used in ACS requests must be taken " + - "from a valid GetActiveContractsResponse and used with the same EventFormat settings, with the same Canton " + - "participant running the same Canton version." - ) - } - - @Explanation( - """This error is emitted when a submitted ledger API page request contains a next page token which cannot be verified. - |When an GetUpdatesPage request is made with a next page token, the token must be taken from a valid - |GetUpdatesPageResponse and used with the same EventFormat settings, the same begin, end and with the same Canton participant - |running the same Canton version. These tokens are not intended to be stored and used much later under different circumstances.""" - ) - @Resolution("Inspect the reason given and correct your application.") - object InvalidUpdatesPageToken - extends ErrorCode( - id = "INVALID_UPDATES_PAGE_TOKEN", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject(message: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = "The submitted command contains an invalid page token. Tokens used in GetUpdatesPage requests must " + - "be taken from a valid GetUpdatesPageResponse and used with the same EventFormat settings, the same " + - s"begin and end with the same Canton participant running the same Canton version. $message" - ) - } - - @Explanation( - """This error is emitted when a submitted ledger API command contains a page token which cannot be verified. - |When an ACS page request is made with a page token, the token must be taken from a valid - |GetActiveContractsPageResponse and used with the same EventFormat settings, with the same Canton participant - |running the same Canton version. These tokens are not intended to be stored and used much later under different circumstances.""" - ) - @Resolution("Inspect the reason given and correct your application.") - object InvalidAcsPageToken - extends ErrorCode( - id = "INVALID_ACS_PAGE_TOKEN", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject(message: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"The submitted command contains an invalid page token. Tokens used in ACS requests must be taken " + - "from a valid GetActiveContractsPageResponse and used with the same EventFormat settings, with the same Canton " + - s"participant running the same Canton version. $message" - ) - } - - @Explanation( - "This error is emitted when a submitted ledger API command specifies an invalid deduplication period." - ) - @Resolution( - "Inspect the error message, adjust the value of the deduplication period or ask the participant operator to increase the maximum deduplication period." - ) - object InvalidDeduplicationPeriodField - extends ErrorCode( - id = "INVALID_DEDUPLICATION_PERIOD", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - val ValidMaxDeduplicationFieldKey = "longest_duration" - final case class Reject( - reason: String, - maxDeduplicationDuration: Option[Duration], - )(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"The submitted command had an invalid deduplication period: $reason" - ) { - override def context: Map[String, String] = - super.context ++ maxDeduplicationDuration - .map(ValidMaxDeduplicationFieldKey -> _.toString) - .toList - } - } - - @Explanation("""The supplied offset is not a positive integer.""") - @Resolution("Ensure the offset specified is a positive (non zero) integer.") - object NonPositiveOffset - extends ErrorCode( - id = "NON_POSITIVE_OFFSET", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Error( - fieldName: String, - offsetValue: Long, - message: String, - )(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = s"Offset $offsetValue in $fieldName is not a positive integer: $message" - ) - } - - @Explanation("""The supplied offset is a negative integer.""") - @Resolution("Ensure the offset specified is a negative integer.") - object NegativeOffset - extends ErrorCode( - id = "NEGATIVE_OFFSET", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Error( - fieldName: String, - offsetValue: Long, - message: String, - )(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = s"Offset $offsetValue in $fieldName is a negative integer: $message" - ) - } - - @Explanation("""Descending order is set to true but end_inclusive is not present.""") - @Resolution("Ensure the end_inclusive is provided when requesting descending order stream") - object DescendingOrderMissingEnd - extends ErrorCode( - id = "DESCENDING_ORDER_MISSING_END", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Error()(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = s"end_inclusive is not provided when descending_order is true" - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/UserManagementServiceErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/UserManagementServiceErrors.scala deleted file mode 100644 index c7743a76df..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/UserManagementServiceErrors.scala +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.error.groups - -import com.digitalasset.base.error.{ - ContextualizedDamlError, - DamlErrorWithDefiniteAnswer, - ErrorCategory, - ErrorCode, - ErrorResource, - Explanation, - Resolution, -} -import com.digitalasset.canton.ledger.error.ParticipantErrorGroup.LedgerApiErrorGroup.AdminServicesErrorGroup.UserManagementServiceErrorGroup -import com.digitalasset.canton.logging.ErrorLoggingContext - -object UserManagementServiceErrors extends UserManagementServiceErrorGroup { - - @Explanation("There was an attempt to update a user using an invalid update request.") - @Resolution( - """|Inspect the error details for specific information on what made the request invalid. - |Retry with an adjusted update request.""" - ) - object InvalidUpdateUserRequest - extends ErrorCode( - id = "INVALID_USER_UPDATE_REQUEST", - ErrorCategory.InvalidIndependentOfSystemState, - ) { - final case class Reject(userId: String, reason: String)(implicit - loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = s"Update operation for user id '$userId' failed due to: $reason" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.User -> userId - ) - } - } - - @Explanation( - """|A user can have at most 256kb worth of annotations in total measured in number of bytes in UTF-8 encoding. - |There was an attempt to create or update a user such that this limit would have been exceeded.""" - ) - @Resolution( - "Retry with fewer annotations or delete some of the user's existing annotations." - ) - object MaxUserAnnotationsSizeExceeded - extends ErrorCode( - id = "MAX_USER_ANNOTATIONS_SIZE_EXCEEDED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(userId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = s"Maximum annotations size for user '$userId' has been exceeded" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.User -> userId - ) - } - } - - @Explanation( - """|Concurrent updates to a user can be controlled by supplying an update request with a resource version (this is optional). - |A user's resource version can be obtained by reading the user on the Ledger API. - |There was attempt to update a user using a stale resource version, indicating that a different process had updated the user earlier.""" - ) - @Resolution( - """|Read this user again to obtain its most recent state and - |in particular its most recent resource version. Use the obtained information to build and send a new update request.""" - ) - object ConcurrentUserUpdateDetected - extends ErrorCode( - id = "CONCURRENT_USER_UPDATE_DETECTED", - ErrorCategory.ContentionOnSharedResources, - ) { - final case class Reject(userId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = - s"Update operation for user '$userId' failed due to a concurrent update to the same user" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.User -> userId - ) - } - } - - @Explanation("The user / idp combination referred to by the request was not found.") - @Resolution( - "Check that you are connecting to the right participant node and the user-id is spelled correctly, if yes, create the user." - ) - object UserNotFound - extends ErrorCode( - id = "USER_NOT_FOUND", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - ) { - final case class Reject(operation: String, userId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$operation failed for unknown user \"$userId\"" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.User -> userId - ) - } - } - - @Explanation( - "There was attempt to update a user while a different process deleted the user at the same time." - ) - @Resolution( - "Read all users again and reattempt the operation with another user." - ) - object UserDeletedWhileUpdating - extends ErrorCode( - id = "USER_DELETED_WHILE_UPDATING", - ErrorCategory.ContentionOnSharedResources, - ) { - final case class Reject(operation: String, userId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = - s"Update operation for user '$userId' failed because user was removed in the meantime" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.User -> userId - ) - } - } - - @Explanation("There already exists a user with the same user-id.") - @Resolution( - "Check that you are connecting to the right participant node and the user-id is spelled correctly, or use the user that already exists." - ) - object UserAlreadyExists - extends ErrorCode( - id = "USER_ALREADY_EXISTS", - ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, - ) { - final case class Reject(operation: String, userId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$operation failed, as user \"$userId\" already exists" - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.User -> userId - ) - } - } - - @Explanation( - """|A user can have only a limited number of user rights. - |There was an attempt to create a user with too many rights or grant too many rights to a user.""" - ) - @Resolution( - """|Retry with a smaller number of rights or delete some of the already existing rights of this user. - |Contact the participant operator if the limit is too low.""" - ) - object TooManyUserRights - extends ErrorCode( - id = "TOO_MANY_USER_RIGHTS", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(operation: String, userId: String)(implicit - loggingContext: ErrorLoggingContext - ) extends DamlErrorWithDefiniteAnswer( - cause = s"$operation failed, as user \"$userId\" would have too many rights." - ) { - override def resources: Seq[(ErrorResource, String)] = Seq( - ErrorResource.User -> userId - ) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/CachedIdentityProviderConfigStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/CachedIdentityProviderConfigStore.scala deleted file mode 100644 index 546c1ddf81..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/CachedIdentityProviderConfigStore.scala +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.caching.ScaffeineCache -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.ledger.localstore.api.{ - IdentityProviderConfigStore, - IdentityProviderConfigUpdate, -} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.github.blemale.scaffeine.Scaffeine - -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -import IdentityProviderConfigStore.Result - -class CachedIdentityProviderConfigStore( - delegate: IdentityProviderConfigStore, - cacheExpiryAfterWrite: FiniteDuration, - maximumCacheSize: Int, - metrics: LedgerApiServerMetrics, - override protected val loggerFactory: NamedLoggerFactory, -)(implicit val executionContext: ExecutionContext, loggingContext: LoggingContextWithTrace) - extends IdentityProviderConfigStore - with NamedLogging { - - private val idpByIssuer - : ScaffeineCache.TunnelledAsyncLoadingCache[Future, String, Result[IdentityProviderConfig]] = - ScaffeineCache.buildAsync[Future, String, Result[IdentityProviderConfig]]( - Scaffeine() - .expireAfterWrite(cacheExpiryAfterWrite) - .maximumSize(maximumCacheSize.toLong), - loader = issuer => delegate.getIdentityProviderConfig(issuer), - metrics = Some(metrics.identityProviderConfigStore.idpConfigCache), - )(logger, "idpByIssuer") - - override def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = - delegate - .createIdentityProviderConfig(identityProviderConfig) - .thereafter(invalidateByIssuerOnSuccess(identityProviderConfig.issuer)) - - override def getIdentityProviderConfig(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = delegate.getIdentityProviderConfig(id) - - override def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Unit]] = - delegate.deleteIdentityProviderConfig(id).thereafter(invalidateAllEntriesOnSuccess()) - - override def listIdentityProviderConfigs()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Seq[IdentityProviderConfig]]] = delegate.listIdentityProviderConfigs() - - override def updateIdentityProviderConfig(update: IdentityProviderConfigUpdate)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = delegate - .updateIdentityProviderConfig(update) - .thereafter(invalidateAllEntriesOnSuccess()) - - override def getIdentityProviderConfig(issuer: String)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = - idpByIssuer.get(issuer) - - override def identityProviderConfigExists(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Boolean] = - delegate.identityProviderConfigExists(id) - - private def invalidateAllEntriesOnSuccess(): Try[Result[Any]] => Unit = - _.foreach(_.foreach(_ => idpByIssuer.invalidateAll())) - - private def invalidateByIssuerOnSuccess( - issuer: String - ): Try[Result[Any]] => Unit = - _.foreach(_.foreach(_ => idpByIssuer.invalidate(issuer))) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/CachedUserManagementStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/CachedUserManagementStore.scala deleted file mode 100644 index 4da9ecd5be..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/CachedUserManagementStore.scala +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.caching.ScaffeineCache -import com.digitalasset.canton.config.FallbackExecutor -import com.digitalasset.canton.ledger.api.{IdentityProviderId, User, UserRight} -import com.digitalasset.canton.ledger.localstore.api.{UserManagementStore, UserUpdate} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.UserId -import com.github.blemale.scaffeine.Scaffeine - -import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Success, Try} - -import CachedUserManagementStore.CacheKey -import UserManagementStore.{Result, UserInfo} - -class CachedUserManagementStore( - delegate: UserManagementStore, - expiryAfterWriteInSeconds: Int, - maximumCacheSize: Int, - metrics: LedgerApiServerMetrics, - override protected val loggerFactory: NamedLoggerFactory, -)(implicit val executionContext: ExecutionContext, loggingContext: LoggingContextWithTrace) - extends UserManagementStore - with NamedLogging { - - private val cache: ScaffeineCache.TunnelledAsyncLoadingCache[Future, CacheKey, Result[UserInfo]] = - ScaffeineCache.buildAsync[Future, CacheKey, Result[UserInfo]]( - cache = Scaffeine() - .expireAfterWrite(expiryAfterWriteInSeconds.seconds) - .maximumSize(maximumCacheSize.toLong) - .executor(new FallbackExecutor(executionContext, loggerFactory)), - loader = key => delegate.getUserInfo(key.id, key.identityProviderId), - metrics = Some(metrics.userManagement.cache), - )(logger, "cache") - - override def getUserInfo(id: UserId, identityProviderId: IdentityProviderId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[UserManagementStore.UserInfo]] = - cache.get(CacheKey(id, identityProviderId)) - - override def createUser(user: User, rights: Set[UserRight])(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[User]] = - delegate - .createUser(user, rights) - .thereafter(invalidateOnSuccess(CacheKey(user.id, user.identityProviderId))) - - override def updateUser( - userUpdate: UserUpdate - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[User]] = - delegate - .updateUser(userUpdate) - .thereafter(invalidateOnSuccess(CacheKey(userUpdate.id, userUpdate.identityProviderId))) - - override def deleteUser( - id: UserId, - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Unit]] = - delegate - .deleteUser(id, identityProviderId) - .thereafter(invalidateOnSuccess(CacheKey(id, identityProviderId))) - - override def grantRights( - id: UserId, - rights: Set[UserRight], - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Set[UserRight]]] = - delegate - .grantRights(id, rights, identityProviderId) - .thereafter(invalidateOnSuccess(CacheKey(id, identityProviderId))) - - override def revokeRights( - id: UserId, - rights: Set[UserRight], - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Set[UserRight]]] = - delegate - .revokeRights(id, rights, identityProviderId) - .thereafter(invalidateOnSuccess(CacheKey(id, identityProviderId))) - - override def listUsers( - fromExcl: Option[Ref.UserId], - maxResults: Int, - identityProviderId: IdentityProviderId, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[UserManagementStore.UsersPage]] = - delegate.listUsers(fromExcl, maxResults, identityProviderId) - - override def updateUserIdp( - id: UserId, - sourceIdp: IdentityProviderId, - targetIdp: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[User]] = { - val keyToInvalidate = CacheKey(id, sourceIdp) - delegate - .updateUserIdp(id, sourceIdp = sourceIdp, targetIdp = targetIdp) - .thereafter(invalidateOnSuccess(keyToInvalidate)) - } - - private def invalidateOnSuccess(key: CacheKey): Try[Result[Any]] => Unit = { - case Success(Right(_)) => cache.invalidate(key) - case _ => - } - -} - -object CachedUserManagementStore { - final case class CacheKey(id: UserId, identityProviderId: IdentityProviderId) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryIdentityProviderConfigStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryIdentityProviderConfigStore.scala deleted file mode 100644 index 6efe24dba9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryIdentityProviderConfigStore.scala +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import cats.syntax.either.* -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.ledger.localstore.api.{ - IdentityProviderConfigStore, - IdentityProviderConfigUpdate, -} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.util.Mutex - -import scala.collection.concurrent.TrieMap -import scala.concurrent.Future - -import IdentityProviderConfigStore.* - -class InMemoryIdentityProviderConfigStore( - override protected val loggerFactory: NamedLoggerFactory, - maxIdentityProviderConfigs: Int = 10, -) extends IdentityProviderConfigStore - with NamedLogging { - - private val state: TrieMap[IdentityProviderId.Id, IdentityProviderConfig] = - TrieMap[IdentityProviderId.Id, IdentityProviderConfig]() - - override def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = withState { - for { - _ <- checkIssuerDoNotExists( - identityProviderConfig.issuer, - identityProviderConfig.identityProviderId, - ) - _ <- checkIdDoNotExists(identityProviderConfig.identityProviderId) - _ <- tooManyIdentityProviderConfigs() - _ = state.put(identityProviderConfig.identityProviderId, identityProviderConfig) - } yield identityProviderConfig - } - - override def getIdentityProviderConfig(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = withState { - state.get(id).toRight(IdentityProviderConfigNotFound(id)) - } - - override def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Unit]] = withState { - for { - _ <- checkIdExists(id) - } yield { - state.remove(id).discard - } - } - - override def listIdentityProviderConfigs()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Seq[IdentityProviderConfig]]] = withState { - Right(state.values.toSeq) - } - - override def updateIdentityProviderConfig(update: IdentityProviderConfigUpdate)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = withState { - val id = update.identityProviderId - for { - currentState <- checkIdExists(id) - _ <- update.issuerUpdate - .map(checkIssuerDoNotExists(_, update.identityProviderId)) - .getOrElse(Either.unit) - } yield { - val updatedValue = currentState - .copy(isDeactivated = update.isDeactivatedUpdate.getOrElse(currentState.isDeactivated)) - .copy(issuer = update.issuerUpdate.getOrElse(currentState.issuer)) - .copy(jwksUrl = update.jwksUrlUpdate.getOrElse(currentState.jwksUrl)) - .copy(audience = update.audienceUpdate.getOrElse(currentState.audience)) - state.put(update.identityProviderId, updatedValue).discard - updatedValue - } - } - - override def getIdentityProviderConfig(issuer: String)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = withState { - state - .collectFirst { case (_, config) if config.issuer == issuer => Right(config) } - .getOrElse(Left(IdentityProviderConfigByIssuerNotFound(issuer))) - } - - override def identityProviderConfigExists(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Boolean] = withState { - state.isDefinedAt(id) - } - - private def checkIssuerDoNotExists( - issuer: String, - idToIgnore: IdentityProviderId.Id, - ): Result[Unit] = - Either.cond( - !state.values.exists(cfg => cfg.issuer == issuer && cfg.identityProviderId != idToIgnore), - (), - IdentityProviderConfigWithIssuerExists(issuer), - ) - - private def checkIdDoNotExists(id: IdentityProviderId.Id): Result[Unit] = - Either.cond( - !state.isDefinedAt(id), - (), - IdentityProviderConfigExists(id), - ) - - @SuppressWarnings(Array("com.digitalasset.canton.ConcurrentMapSize")) - private def tooManyIdentityProviderConfigs(): Result[Unit] = - Either.cond( - state.size + 1 <= maxIdentityProviderConfigs, - (), - TooManyIdentityProviderConfigs(), - ) - - private def checkIdExists(id: IdentityProviderId.Id): Result[IdentityProviderConfig] = - state.get(id).toRight(IdentityProviderConfigNotFound(id)) - - private val lock = new Mutex() - private def withState[T](t: => T): Future[T] = - Future.successful { - lock.exclusive(t) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryPartyRecordStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryPartyRecordStore.scala deleted file mode 100644 index a253e2f5f5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryPartyRecordStore.scala +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.api.validation.ResourceAnnotationValidator -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta} -import com.digitalasset.canton.ledger.localstore.api.PartyRecordStore.{ - MaxAnnotationsSizeExceeded, - PartyRecordExistsFatal, - Result, -} -import com.digitalasset.canton.ledger.localstore.api.{ - PartyRecord, - PartyRecordStore, - PartyRecordUpdate, -} -import com.digitalasset.canton.ledger.localstore.utils.LocalAnnotationsUtils -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.util.Mutex -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.Party - -import scala.collection.mutable -import scala.concurrent.{ExecutionContext, Future} - -object InMemoryPartyRecordStore { - final case class PartyRecordInfo( - party: Ref.Party, - resourceVersion: Long, - annotations: Map[String, String], - identityProviderId: IdentityProviderId, - ) - - def toPartyRecord(info: PartyRecordInfo): PartyRecord = - PartyRecord( - party = info.party, - metadata = ObjectMeta( - resourceVersionO = Some(info.resourceVersion), - annotations = info.annotations, - ), - identityProviderId = info.identityProviderId, - ) - -} - -class InMemoryPartyRecordStore( - executionContext: ExecutionContext, - val loggerFactory: NamedLoggerFactory, -) extends PartyRecordStore - with NamedLogging { - import InMemoryPartyRecordStore.* - - implicit private val ec: ExecutionContext = executionContext - - private val state: mutable.TreeMap[Ref.Party, PartyRecordInfo] = mutable.TreeMap() - private val lock = Mutex() - - override def getPartyRecordO( - party: Party - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Option[PartyRecord]]] = - withState( - state.get(party) match { - case Some(info) => Right(Some(toPartyRecord(info))) - case None => Right(None) - } - ) - - override def createPartyRecord( - partyRecord: PartyRecord - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[PartyRecord]] = - withState(withoutPartyRecord(partyRecord.party) { - for { - info <- doCreatePartyRecord(partyRecord) - } yield toPartyRecord(info) - }) - - override def updatePartyRecord( - partyRecordUpdate: PartyRecordUpdate, - ledgerPartyIsLocal: Boolean, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[PartyRecord]] = { - val party = partyRecordUpdate.party - for { - updatedPartyRecord <- withState( - state.get(party) match { - case Some(info) => - for { - updatedInfo <- doUpdatePartyRecord( - partyRecordUpdate = partyRecordUpdate, - party = party, - info = info, - ) - } yield toPartyRecord(updatedInfo) - case None => - if (ledgerPartyIsLocal) { - val newPartyRecord = PartyRecord( - party = party, - metadata = ObjectMeta( - resourceVersionO = None, - annotations = partyRecordUpdate.metadataUpdate.annotationsUpdateO.getOrElse( - Map.empty[String, String] - ), - ), - identityProviderId = partyRecordUpdate.identityProviderId, - ) - for { - info <- doCreatePartyRecord(newPartyRecord) - } yield toPartyRecord(info) - } else { - Left(PartyRecordStore.PartyNotFound(party)) - } - } - ).map(tapSuccess { updatePartyRecord => - logger.info(s"Updated party record in a participant local store: $updatePartyRecord.") - }) - } yield updatedPartyRecord - } - - override def updatePartyRecordIdp( - party: Party, - ledgerPartyIsLocal: Boolean, - sourceIdp: IdentityProviderId, - targetIdp: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[PartyRecord]] = - withState { - state.get(party) match { - case Some(info) => - if (info.identityProviderId != sourceIdp) { - Left(PartyRecordStore.PartyNotFound(party = party)) - } else { - val updatedInfo = info.copy(identityProviderId = targetIdp) - state.put(party, updatedInfo).discard - Right(toPartyRecord(updatedInfo)) - } - case None => - if (ledgerPartyIsLocal) { - // When party record doesn't exist - // it implicitly means that the party belongs to the default idp. - if (sourceIdp != IdentityProviderId.Default) { - Left(PartyRecordStore.PartyNotFound(party = party)) - } else { - val newPartyRecord = PartyRecord( - party = party, - metadata = ObjectMeta.empty, - identityProviderId = targetIdp, - ) - for { - info <- doCreatePartyRecord(newPartyRecord) - } yield toPartyRecord(info) - } - } else { - Left(PartyRecordStore.PartyNotFound(party)) - } - } - } - - private def doUpdatePartyRecord( - partyRecordUpdate: PartyRecordUpdate, - party: Party, - info: PartyRecordInfo, - ): Result[PartyRecordInfo] = { - val existingAnnotations = info.annotations - val updatedAnnotations = - partyRecordUpdate.metadataUpdate.annotationsUpdateO.fold(existingAnnotations) { - newAnnotations => - LocalAnnotationsUtils.calculateUpdatedAnnotations( - newValue = newAnnotations, - existing = existingAnnotations, - ) - } - val currentResourceVersion = info.resourceVersion - val newResourceVersionEither = partyRecordUpdate.metadataUpdate.resourceVersionO match { - case None => Right(currentResourceVersion + 1) - case Some(requestResourceVersion) => - if (requestResourceVersion == currentResourceVersion) { - Right(currentResourceVersion + 1) - } else { - Left(PartyRecordStore.ConcurrentPartyUpdate(partyRecordUpdate.party)) - } - } - for { - _ <- validateAnnotationsSize(updatedAnnotations, party) - newResourceVersion <- newResourceVersionEither - } yield { - val updatedInfo = PartyRecordInfo( - party = party, - resourceVersion = newResourceVersion, - annotations = updatedAnnotations, - identityProviderId = partyRecordUpdate.identityProviderId, - ) - state.put(party, updatedInfo).discard - updatedInfo - } - } - - private def doCreatePartyRecord( - partyRecord: PartyRecord - ): Result[PartyRecordInfo] = - for { - _ <- validateAnnotationsSize(partyRecord.metadata.annotations, partyRecord.party) - } yield { - val info = PartyRecordInfo( - party = partyRecord.party, - resourceVersion = 0, - annotations = partyRecord.metadata.annotations, - identityProviderId = partyRecord.identityProviderId, - ) - state.update(partyRecord.party, info) - info - } - - private def withState[T](t: => T): Future[T] = - Future.successful { - lock.exclusive(t) - } - - private def withoutPartyRecord[T](party: Ref.Party)(t: => Result[T]): Result[T] = - state.get(party) match { - case Some(_) => Left(PartyRecordExistsFatal(party)) - case None => t - } - - private def tapSuccess[T](f: T => Unit)(r: Result[T]): Result[T] = { - r.foreach(f) - r - } - - private def validateAnnotationsSize( - annotations: Map[String, String], - party: Ref.Party, - ): Result[Unit] = - Either.cond( - ResourceAnnotationValidator.isWithinMaxAnnotationsByteSize(annotations), - (), - MaxAnnotationsSizeExceeded(party), - ) - - override def filterExistingParties(parties: Set[Party], identityProviderId: IdentityProviderId)( - implicit loggingContext: LoggingContextWithTrace - ): Future[Set[Party]] = - withState { - parties.map(party => (party, state.get(party))).collect { - case (party, Some(record)) if record.identityProviderId == identityProviderId => party - } - } - - override def filterExistingParties(parties: Set[Party])(implicit - loggingContext: LoggingContextWithTrace - ): Future[Set[Party]] = - withState { - parties.map(party => (party, state.get(party))).collect { case (party, Some(_)) => - party - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryUserManagementStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryUserManagementStore.scala deleted file mode 100644 index 8683145737..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/InMemoryUserManagementStore.scala +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import cats.syntax.either.* -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.api.validation.ResourceAnnotationValidator -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta, User, UserRight} -import com.digitalasset.canton.ledger.localstore.api.{UserManagementStore, UserUpdate} -import com.digitalasset.canton.ledger.localstore.utils.LocalAnnotationsUtils -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.UserId - -import scala.collection.mutable -import scala.concurrent.{Future, blocking} - -import UserManagementStore.* - -@SuppressWarnings(Array("com.digitalasset.canton.RequireBlocking")) -class InMemoryUserManagementStore( - createAdmin: Boolean = true, - val loggerFactory: NamedLoggerFactory, -) extends UserManagementStore - with NamedLogging { - import InMemoryUserManagementStore.* - - // Underlying mutable map to keep track of UserInfo state. - // Structured so we can use a ConcurrentHashMap (to more closely mimic a real implementation, where performance is key). - // We synchronize on a private object (the mutable map), not the service (which could cause deadlocks). - // (No need to mark state as volatile -- rely on synchronized to establish the JMM's happens-before relation.) - private val state: mutable.TreeMap[Ref.UserId, InMemUserInfo] = mutable.TreeMap() - if (createAdmin) { - state.put(AdminUser.user.id, AdminUser).discard - } - - override def getUserInfo(id: UserId, identityProviderId: IdentityProviderId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[UserManagementStore.UserInfo]] = - withUser(id, identityProviderId)(info => Right(toApiUserInfo(info))) - - override def createUser(user: User, rights: Set[UserRight])(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[User]] = - withoutUser(user.id, user.identityProviderId) { - for { - _ <- validateAnnotationsSize(user.metadata.annotations, user.id) - } yield { - val userWithResourceVersion = - InMemUser( - id = user.id, - primaryParty = user.primaryParty, - isDeactivated = user.isDeactivated, - resourceVersion = 0, - annotations = user.metadata.annotations, - identityProviderId = user.identityProviderId, - primaryPartyAuthentication = user.primaryPartyAuthentication, - ) - state.update(user.id, InMemUserInfo(userWithResourceVersion, rights)) - toApiUser(userWithResourceVersion) - } - } - - override def updateUser( - userUpdate: UserUpdate - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[User]] = - withUser(userUpdate.id, userUpdate.identityProviderId) { userInfo => - val updatedPrimaryParty = userUpdate.primaryPartyUpdateO.getOrElse(userInfo.user.primaryParty) - val updatedIsDeactivated = - userUpdate.isDeactivatedUpdateO.getOrElse(userInfo.user.isDeactivated) - val existingAnnotations = userInfo.user.annotations - val identityProviderId = userInfo.user.identityProviderId - val updatedAnnotations = - userUpdate.metadataUpdate.annotationsUpdateO.fold(existingAnnotations) { newAnnotations => - LocalAnnotationsUtils.calculateUpdatedAnnotations( - newValue = newAnnotations, - existing = existingAnnotations, - ) - } - val updatedPrimaryPartyAuthentication = - userUpdate.primaryPartyAuthenticationUpdateO.getOrElse( - userInfo.user.primaryPartyAuthentication - ) - val currentResourceVersion = userInfo.user.resourceVersion - val newResourceVersionEither = userUpdate.metadataUpdate.resourceVersionO match { - case None => Right(currentResourceVersion + 1) - case Some(requestResourceVersion) => - if (requestResourceVersion == currentResourceVersion) { - Right(currentResourceVersion + 1) - } else { - Left(UserManagementStore.ConcurrentUserUpdate(userUpdate.id)) - } - } - for { - _ <- validateAnnotationsSize(updatedAnnotations, userUpdate.id) - newResourceVersion <- newResourceVersionEither - } yield { - val updatedUserInfo = userInfo.copy( - user = userInfo.user.copy( - primaryParty = updatedPrimaryParty, - isDeactivated = updatedIsDeactivated, - resourceVersion = newResourceVersion, - annotations = updatedAnnotations, - identityProviderId = identityProviderId, - primaryPartyAuthentication = updatedPrimaryPartyAuthentication, - ) - ) - state.update(userUpdate.id, updatedUserInfo) - toApiUser(updatedUserInfo.user) - } - } - - override def deleteUser( - id: Ref.UserId, - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Unit]] = - withUser(id, identityProviderId) { _ => - state.remove(id).discard - Either.unit - } - - override def grantRights( - id: Ref.UserId, - granted: Set[UserRight], - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Set[UserRight]]] = - withUser(id, identityProviderId) { userInfo => - val newlyGranted = granted.diff(userInfo.rights) // faster than filter - // we're not doing concurrent updates -- assert as backstop and a reminder to handle the collision case in the future - assert( - replaceInfo(userInfo, userInfo.copy(rights = userInfo.rights ++ newlyGranted)) - ) - Right(newlyGranted) - } - - override def revokeRights( - id: Ref.UserId, - revoked: Set[UserRight], - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Set[UserRight]]] = - withUser(id, identityProviderId) { userInfo => - val effectivelyRevoked = revoked.intersect(userInfo.rights) // faster than filter - // we're not doing concurrent updates -- assert as backstop and a reminder to handle the collision case in the future - assert( - replaceInfo(userInfo, userInfo.copy(rights = userInfo.rights -- effectivelyRevoked)) - ) - Right(effectivelyRevoked) - } - - override def listUsers( - fromExcl: Option[Ref.UserId], - maxResults: Int, - identityProviderId: IdentityProviderId, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[UsersPage]] = - withState { - val iter: Iterator[InMemUserInfo] = fromExcl match { - case None => state.valuesIterator - case Some(after) => state.valuesIteratorFrom(start = after).dropWhile(_.user.id == after) - } - val users: Seq[User] = iter - .filter(_.user.identityProviderId == identityProviderId) - .take(maxResults) - .map(info => toApiUser(info.user)) - .toSeq - Right(UsersPage(users = users)) - } - - override def updateUserIdp( - id: Ref.UserId, - sourceIdp: IdentityProviderId, - targetIdp: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[User]] = - withUser(id = id, identityProviderId = sourceIdp) { info => - val user = info.user.copy(identityProviderId = targetIdp) - val updated = info.copy(user = user) - state.update(id, updated) - Right(toApiUser(updated.user)) - } - - private def withState[T](t: => T): Future[T] = - Future.successful { - blocking { - state.synchronized(t) - } - } - - private def withUser[T](id: Ref.UserId, identityProviderId: IdentityProviderId)( - f: InMemUserInfo => Result[T] - ): Future[Result[T]] = - withState( - state.get(id) match { - case Some(user) if user.user.identityProviderId == identityProviderId => f(user) - case Some(_) => - Left(PermissionDenied(id)) - case None => - Left(UserNotFound(id)) - } - ) - - private def withoutUser[T](id: Ref.UserId, identityProviderId: IdentityProviderId)( - t: => Result[T] - ): Future[Result[T]] = - withState( - state.get(id) match { - case Some(user) if user.user.identityProviderId != identityProviderId => - Left(PermissionDenied(id)) - case Some(_) => Left(UserExists(id)) - case None => t - } - ) - - private def replaceInfo(oldInfo: InMemUserInfo, newInfo: InMemUserInfo): Boolean = - blocking(state.synchronized { - assert( - oldInfo.user.id == newInfo.user.id, - s"Replace info from if ${oldInfo.user.id} to ${newInfo.user.id} -> ${newInfo.rights}", - ) - state.get(oldInfo.user.id) match { - case Some(`oldInfo`) => state.update(newInfo.user.id, newInfo); true - case _ => false - } - }) - - private def validateAnnotationsSize( - annotations: Map[String, String], - userId: Ref.UserId, - ): Result[Unit] = - Either.cond( - ResourceAnnotationValidator.isWithinMaxAnnotationsByteSize(annotations), - (), - MaxAnnotationsSizeExceeded(userId), - ) - -} - -object InMemoryUserManagementStore { - - final case class InMemUser( - id: Ref.UserId, - primaryParty: Option[Ref.Party], - isDeactivated: Boolean = false, - resourceVersion: Long, - annotations: Map[String, String], - identityProviderId: IdentityProviderId, - primaryPartyAuthentication: Boolean = false, - ) - final case class InMemUserInfo(user: InMemUser, rights: Set[UserRight]) - - def toApiUserInfo(info: InMemUserInfo): UserInfo = - UserInfo( - user = toApiUser(info.user), - rights = info.rights, - ) - - def toApiUser(user: InMemUser): User = - User( - id = user.id, - primaryParty = user.primaryParty, - isDeactivated = user.isDeactivated, - metadata = ObjectMeta( - resourceVersionO = Some(user.resourceVersion), - annotations = user.annotations, - ), - identityProviderId = user.identityProviderId, - primaryPartyAuthentication = user.primaryPartyAuthentication, - ) - - private val AdminUser = InMemUserInfo( - user = InMemUser( - id = Ref.UserId.assertFromString(UserManagementStore.DefaultParticipantAdminUserId), - primaryParty = None, - isDeactivated = false, - resourceVersion = 0, - annotations = Map.empty, - identityProviderId = IdentityProviderId.Default, - primaryPartyAuthentication = false, - ), - rights = Set(UserRight.ParticipantAdmin), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/Ops.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/Ops.scala deleted file mode 100644 index 60698c6016..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/Ops.scala +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.metrics.DatabaseMetrics -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.store.dao.DbDispatcher - -import java.sql.Connection -import scala.concurrent.Future - -object Ops { - - private[localstore] def rollbackOnLeft[E, T](sql: Connection => Either[E, T])( - connection: Connection - ): Either[E, T] = - sql(connection).left.map { error => - connection.rollback() - error - } - - implicit class DbDispatcherLeftOps(val dbDispatcher: DbDispatcher) extends AnyVal { - /* - This method extends DbDispatcher.executeSql to accept a closure which returns Either. - In case of Left value on that Either - transaction is rolled back. - */ - def executeSqlEither[E, T](databaseMetrics: DatabaseMetrics)(sql: Connection => Either[E, T])( - implicit loggingContext: LoggingContextWithTrace - ): Future[Either[E, T]] = - dbDispatcher.executeSql(databaseMetrics)(rollbackOnLeft(sql)) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStore.scala deleted file mode 100644 index b78f7f8bb5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStore.scala +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import cats.syntax.either.* -import com.daml.metrics.DatabaseMetrics -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.ledger.localstore.CachedIdentityProviderConfigStore -import com.digitalasset.canton.ledger.localstore.Ops.* -import com.digitalasset.canton.ledger.localstore.api.IdentityProviderConfigStore.* -import com.digitalasset.canton.ledger.localstore.api.{ - IdentityProviderConfigStore, - IdentityProviderConfigUpdate, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.DbSupport -import com.digitalasset.canton.tracing.TraceContext - -import java.sql.Connection -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future} - -class PersistentIdentityProviderConfigStore( - dbSupport: DbSupport, - metrics: LedgerApiServerMetrics, - maxIdentityProviders: Int, - override protected val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends IdentityProviderConfigStore - with NamedLogging { - - private val backend = dbSupport.storageBackendFactory.createIdentityProviderConfigStorageBackend - private val dbDispatcher = dbSupport.dbDispatcher - - override def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = - inTransaction(_.createIdpConfig) { implicit connection => - val id = identityProviderConfig.identityProviderId - for { - _ <- idpConfigDoesNotExist(id) - _ <- idpConfigByIssuerDoesNotExist( - Some(identityProviderConfig.issuer), - identityProviderConfig.identityProviderId, - ) - _ = backend.createIdentityProviderConfig(identityProviderConfig)(connection) - _ <- tooManyIdentityProviderConfigs()(connection) - apiConfig <- backend - .getIdentityProviderConfig(id)(connection) - .toRight(IdentityProviderConfigNotFound(id)) - } yield apiConfig - }.map(tapSuccess { cfg => - logger.info( - s"Created new identity provider configuration: $cfg" - ) - }) - - override def getIdentityProviderConfig(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = - inTransaction(_.getIdpConfig) { implicit connection => - backend - .getIdentityProviderConfig(id)(connection) - .toRight(IdentityProviderConfigNotFound(id)) - } - - override def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Unit]] = - inTransaction(_.deleteIdpConfig) { implicit connection => - Either.cond( - backend.deleteIdentityProviderConfig(id)(connection), - (), - IdentityProviderConfigNotFound(id), - ) - }.map(tapSuccess { _ => - logger.info( - s"Deleted identity provider configuration with id $id" - ) - }) - - override def listIdentityProviderConfigs()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Seq[IdentityProviderConfig]]] = - inTransaction(_.listIdpConfigs) { implicit connection => - Right(backend.listIdentityProviderConfigs()(connection)) - } - - override def updateIdentityProviderConfig(update: IdentityProviderConfigUpdate)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = - inTransaction(_.updateIdpConfig) { implicit connection => - val id = update.identityProviderId - for { - _ <- idpConfigExists(id) - _ <- idpConfigByIssuerDoesNotExist(update.issuerUpdate, update.identityProviderId) - _ <- updateIssuer(update)(connection) - _ <- updateJwksUrl(update)(connection) - _ <- updateAudience(update)(connection) - _ <- updateIsDeactivated(update)(connection) - identityProviderConfig <- backend - .getIdentityProviderConfig(id)(connection) - .toRight(IdentityProviderConfigNotFound(id)) - } yield identityProviderConfig - }.map(tapSuccess { _ => - logger.info( - s"Updated identity provider configuration with id ${update.identityProviderId}" - ) - }) - - override def getIdentityProviderConfig(issuer: String)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] = inTransaction(_.getIdpConfig) { implicit connection => - for { - identityProviderConfig <- backend - .getIdentityProviderConfigByIssuer(issuer)(connection) - .toRight(IdentityProviderConfigByIssuerNotFound(issuer)) - } yield identityProviderConfig - } - - def identityProviderConfigExists(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Boolean] = - dbDispatcher.executeSql(metrics.identityProviderConfigStore.getIdpConfig) { connection => - backend.idpConfigByIdExists(id)(connection) - } - - private def updateIssuer( - update: IdentityProviderConfigUpdate - )(connection: Connection): Result[Unit] = { - val execute = - update.issuerUpdate.forall(backend.updateIssuer(update.identityProviderId, _)(connection)) - Either.cond(execute, (), IdentityProviderConfigNotFound(update.identityProviderId)) - } - private def updateJwksUrl( - update: IdentityProviderConfigUpdate - )(connection: Connection): Result[Unit] = { - val execute = update.jwksUrlUpdate.forall( - backend.updateJwksUrl(update.identityProviderId, _)(connection) - ) - Either.cond(execute, (), IdentityProviderConfigNotFound(update.identityProviderId)) - } - - private def updateAudience( - update: IdentityProviderConfigUpdate - )(connection: Connection): Result[Unit] = { - val execute = update.audienceUpdate.forall( - backend.updateAudience(update.identityProviderId, _)(connection) - ) - Either.cond(execute, (), IdentityProviderConfigNotFound(update.identityProviderId)) - } - - private def updateIsDeactivated( - update: IdentityProviderConfigUpdate - )(connection: Connection): Result[Unit] = { - val execute = update.isDeactivatedUpdate.forall( - backend.updateIsDeactivated(update.identityProviderId, _)(connection) - ) - Either.cond(execute, (), IdentityProviderConfigNotFound(update.identityProviderId)) - } - - private def tooManyIdentityProviderConfigs()( - connection: Connection - ): Result[Unit] = - Either.cond( - backend.countIdentityProviderConfigs()(connection) <= maxIdentityProviders, - (), - TooManyIdentityProviderConfigs(), - ) - - private def idpConfigExists( - id: IdentityProviderId.Id - )(implicit connection: Connection): Result[Unit] = Either.cond( - backend.idpConfigByIdExists(id)(connection), - (), - IdentityProviderConfigNotFound(id), - ) - - private def idpConfigDoesNotExist( - id: IdentityProviderId.Id - )(implicit connection: Connection): Result[Unit] = Either.cond( - !backend.idpConfigByIdExists(id)(connection), - (), - IdentityProviderConfigExists(id), - ) - - private def idpConfigByIssuerDoesNotExist( - issuer: Option[String], - id: IdentityProviderId.Id, - )(implicit connection: Connection): Result[Unit] = issuer match { - case Some(value) => - Either.cond( - !backend.identityProviderConfigByIssuerExists(id, value)(connection), - (), - IdentityProviderConfigWithIssuerExists(value), - ) - case None => Either.unit - } - - private def inTransaction[T]( - dbMetric: metrics.identityProviderConfigStore.type => DatabaseMetrics - )( - thunk: Connection => Result[T] - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[T]] = - dbDispatcher - .executeSqlEither(dbMetric(metrics.identityProviderConfigStore))(thunk) - - private def tapSuccess[T](f: T => Unit)(r: Result[T]): Result[T] = { - r.foreach(f) - r - } - -} - -object PersistentIdentityProviderConfigStore { - def cached( - dbSupport: DbSupport, - metrics: LedgerApiServerMetrics, - cacheExpiryAfterWrite: FiniteDuration, - maxIdentityProviders: Int, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext, - traceContext: TraceContext, - ) = new CachedIdentityProviderConfigStore( - delegate = new PersistentIdentityProviderConfigStore( - dbSupport, - metrics, - maxIdentityProviders, - loggerFactory, - ), - cacheExpiryAfterWrite = cacheExpiryAfterWrite, - maximumCacheSize = maxIdentityProviders, - metrics = metrics, - loggerFactory, - )(executionContext, LoggingContextWithTrace(loggerFactory)) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStore.scala deleted file mode 100644 index a7ddbf5b18..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStore.scala +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.metrics.DatabaseMetrics -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.api.util.TimeProvider -import com.digitalasset.canton.ledger.api.validation.ResourceAnnotationValidator -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta} -import com.digitalasset.canton.ledger.localstore.PersistentPartyRecordStore.{ - ConcurrentPartyRecordUpdateDetectedRuntimeException, - MaxAnnotationsSizeExceededException, -} -import com.digitalasset.canton.ledger.localstore.api.PartyRecordStore.{ - ConcurrentPartyUpdate, - MaxAnnotationsSizeExceeded, - Result, -} -import com.digitalasset.canton.ledger.localstore.api.{ - PartyRecord, - PartyRecordStore, - PartyRecordUpdate, -} -import com.digitalasset.canton.ledger.localstore.utils.LocalAnnotationsUtils -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.DbSupport -import com.digitalasset.canton.platform.store.backend.localstore.PartyRecordStorageBackend -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.Party - -import java.sql.Connection -import scala.concurrent.{ExecutionContext, Future} - -object PersistentPartyRecordStore { - - final case class ConcurrentPartyRecordUpdateDetectedRuntimeException(party: Ref.Party) - extends RuntimeException - - final case class MaxAnnotationsSizeExceededException(party: Ref.Party) extends RuntimeException -} - -class PersistentPartyRecordStore( - dbSupport: DbSupport, - metrics: LedgerApiServerMetrics, - timeProvider: TimeProvider, - executionContext: ExecutionContext, - val loggerFactory: NamedLoggerFactory, -) extends PartyRecordStore - with NamedLogging { - - private implicit val ec: ExecutionContext = executionContext - - private val directEc = DirectExecutionContext(noTracingLogger) - - private val backend = dbSupport.storageBackendFactory.createPartyRecordStorageBackend - private val dbDispatcher = dbSupport.dbDispatcher - - override def createPartyRecord(partyRecord: PartyRecord)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[PartyRecord]] = - inTransaction(_.createPartyRecord) { implicit connection: Connection => - for { - _ <- withoutPartyRecord(id = partyRecord.party) { - doCreatePartyRecord(partyRecord)(connection) - } - createdPartyRecord <- doFetchStorePartyRecord(party = partyRecord.party)(connection) - } yield createdPartyRecord - }.map(tapSuccess { _ => - logger.info( - s"Created new party record in participant local store: $partyRecord" - ) - })(directEc) - - override def updatePartyRecord( - partyRecordUpdate: PartyRecordUpdate, - ledgerPartyIsLocal: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[PartyRecord]] = { - val party = partyRecordUpdate.party - for { - updatedPartyRecord <- inTransaction(_.updatePartyRecord) { implicit connection => - backend.getPartyRecord(party = party)(connection) match { - // Update an existing party record - case Some(dbPartyRecord) => - doUpdatePartyRecord( - dbPartyRecord = dbPartyRecord, - partyRecordUpdate = partyRecordUpdate, - )(connection) - doFetchStorePartyRecord(party)(connection) - // Party record does not exist, but party is local to participant - case None => - if (ledgerPartyIsLocal) { - for { - _ <- withoutPartyRecord(party) { - val newPartyRecord = PartyRecord( - party = party, - identityProviderId = partyRecordUpdate.identityProviderId, - metadata = ObjectMeta( - resourceVersionO = None, - annotations = partyRecordUpdate.metadataUpdate.annotationsUpdateO.getOrElse( - Map.empty[String, String] - ), - ), - ) - doCreatePartyRecord(newPartyRecord)(connection) - } - updatePartyRecord <- doFetchStorePartyRecord(party)(connection) - } yield updatePartyRecord - } else { - Left(PartyRecordStore.PartyNotFound(party)) - } - } - }.map(tapSuccess { updatePartyRecord => - logger.info(s"Updated party record in participant local store: $updatePartyRecord") - }) - } yield updatedPartyRecord - } - - override def updatePartyRecordIdp( - party: Party, - ledgerPartyIsLocal: Boolean, - sourceIdp: IdentityProviderId, - targetIdp: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[PartyRecord]] = - for { - updatedPartyRecord <- inTransaction(_.updatePartyRecordIdp) { implicit connection => - backend.getPartyRecord(party = party)(connection) match { - // Update an existing party record - case Some(dbPartyRecord) => - if (dbPartyRecord.payload.identityProviderId != sourceIdp.toDb) { - Left(PartyRecordStore.PartyNotFound(party = party)) - } else { - backend - .updatePartyRecordIdp( - internalId = dbPartyRecord.internalId, - identityProviderId = targetIdp.toDb, - )(connection) - .discard - doFetchStorePartyRecord(party)(connection) - } - case None => - // Party record does not exist, but party is local to participant - if (ledgerPartyIsLocal) { - // When party record doesn't exist - // it means that the party implicitly belongs to the default idp. - if (sourceIdp != IdentityProviderId.Default) { - Left(PartyRecordStore.PartyNotFound(party = party)) - } else { - for { - _ <- withoutPartyRecord(party) { - val newPartyRecord = PartyRecord( - party = party, - identityProviderId = targetIdp, - metadata = ObjectMeta.empty, - ) - doCreatePartyRecord(newPartyRecord)(connection) - } - updatePartyRecord <- doFetchStorePartyRecord(party)(connection) - } yield updatePartyRecord - } - } else { - Left(PartyRecordStore.PartyNotFound(party)) - } - } - }.map(tapSuccess { updatePartyRecord => - logger.info(s"Updated party record in participant local store: $updatePartyRecord") - }) - } yield updatedPartyRecord - - override def getPartyRecordO( - party: Party - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Option[PartyRecord]]] = - inTransaction(_.getPartyRecord) { implicit connection => - doFetchStorePartyRecordO(party) - } - - private def doFetchStorePartyRecord( - party: Ref.Party - )(implicit connection: Connection): Result[PartyRecord] = - withPartyRecord(id = party) { dbPartyRecord => - val annotations = backend.getPartyAnnotations(dbPartyRecord.internalId)(connection) - toStorePartyRecord(dbPartyRecord.payload, annotations) - } - - private def doFetchStorePartyRecordO( - party: Ref.Party - )(implicit connection: Connection): Result[Option[PartyRecord]] = - backend.getPartyRecord(party = party)(connection) match { - case Some(dbPartyRecord) => - val annotations = backend.getPartyAnnotations(dbPartyRecord.internalId)(connection) - Right(Some(toStorePartyRecord(dbPartyRecord.payload, annotations))) - case None => Right(None) - } - - private def doCreatePartyRecord( - partyRecord: PartyRecord - )(connection: Connection): Unit = { - if ( - !ResourceAnnotationValidator - .isWithinMaxAnnotationsByteSize(partyRecord.metadata.annotations) - ) { - throw MaxAnnotationsSizeExceededException(partyRecord.party) - } - val now = epochMicroseconds() - val dbParty = PartyRecordStorageBackend.DbPartyRecordPayload( - party = partyRecord.party, - identityProviderId = partyRecord.identityProviderId.toDb, - resourceVersion = 0, - createdAt = now, - ) - val internalId = backend.createPartyRecord( - partyRecord = dbParty - )(connection) - partyRecord.metadata.annotations.foreach { case (key, value) => - backend.addPartyAnnotation( - internalId = internalId, - key = key, - value = value, - updatedAt = now, - )(connection) - } - } - - private def doUpdatePartyRecord( - dbPartyRecord: PartyRecordStorageBackend.DbPartyRecord, - partyRecordUpdate: PartyRecordUpdate, - )(connection: Connection): Unit = { - val now = epochMicroseconds() - // Step 1: Update resource version - // NOTE: We starts by writing to the 'resource_version' attribute - // of 'lapi_party_records' to effectively obtain an exclusive lock for - // updating this party-record for the rest of the transaction. - val _ = partyRecordUpdate.metadataUpdate.resourceVersionO match { - case Some(expectedResourceVersion) => - if ( - !backend.compareAndIncreaseResourceVersion( - internalId = dbPartyRecord.internalId, - expectedResourceVersion = expectedResourceVersion, - )(connection) - ) { - throw ConcurrentPartyRecordUpdateDetectedRuntimeException( - partyRecordUpdate.party - ) - } - case None => - backend.increaseResourceVersion( - internalId = dbPartyRecord.internalId - )(connection) - } - // Step 2: Update annotations - partyRecordUpdate.metadataUpdate.annotationsUpdateO.foreach { newAnnotations => - val existingAnnotations = backend.getPartyAnnotations(dbPartyRecord.internalId)(connection) - val updatedAnnotations = LocalAnnotationsUtils.calculateUpdatedAnnotations( - newValue = newAnnotations, - existing = existingAnnotations, - ) - if ( - !ResourceAnnotationValidator - .isWithinMaxAnnotationsByteSize(updatedAnnotations) - ) { - throw MaxAnnotationsSizeExceededException(partyRecordUpdate.party) - } - backend.deletePartyAnnotations(internalId = dbPartyRecord.internalId)(connection) - updatedAnnotations.iterator.foreach { case (key, value) => - backend.addPartyAnnotation( - internalId = dbPartyRecord.internalId, - key = key, - value = value, - updatedAt = now, - )(connection) - } - } - } - - private def toStorePartyRecord( - payload: PartyRecordStorageBackend.DbPartyRecordPayload, - annotations: Map[String, String], - ): PartyRecord = - PartyRecord( - party = payload.party, - identityProviderId = IdentityProviderId.fromDb(payload.identityProviderId), - metadata = ObjectMeta( - resourceVersionO = Some(payload.resourceVersion), - annotations = annotations, - ), - ) - - private def withPartyRecord[T]( - id: Ref.Party - )( - f: PartyRecordStorageBackend.DbPartyRecord => T - )(implicit connection: Connection): Result[T] = - backend.getPartyRecord(party = id)(connection) match { - case Some(partyRecord) => Right(f(partyRecord)) - case None => Left(PartyRecordStore.PartyRecordNotFoundFatal(party = id)) - } - - private def withoutPartyRecord[T]( - id: Ref.Party - )(t: => T)(implicit connection: Connection): Result[T] = - backend.getPartyRecord(party = id)(connection) match { - case Some(partyRecord) => - Left(PartyRecordStore.PartyRecordExistsFatal(party = partyRecord.payload.party)) - case None => Right(t) - } - - private def inTransaction[T]( - dbMetric: metrics.partyRecordStore.type => DatabaseMetrics - )( - thunk: Connection => Result[T] - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[T]] = - dbDispatcher - .executeSql(dbMetric(metrics.partyRecordStore))(thunk) - .recover[Result[T]] { - case ConcurrentPartyRecordUpdateDetectedRuntimeException(userId) => - Left(ConcurrentPartyUpdate(userId)) - case MaxAnnotationsSizeExceededException(userId) => Left(MaxAnnotationsSizeExceeded(userId)) - }(directEc) - - private def tapSuccess[T](f: T => Unit)(r: Result[T]): Result[T] = { - r.foreach(f) - r - } - - private def epochMicroseconds(): Long = { - val now = timeProvider.getCurrentTime - (now.getEpochSecond * 1000 * 1000) + (now.getNano / 1000) - } - - override def filterExistingParties(parties: Set[Party], identityProviderId: IdentityProviderId)( - implicit loggingContext: LoggingContextWithTrace - ): Future[Set[Party]] = inTransaction(_.partiesExist) { implicit connection => - Right(backend.filterExistingParties(parties, identityProviderId.toDb)(connection)) - }.map { - case Right(value) => value - case Left(_) => Set.empty // It is always `Right`, see few lines above - } - - override def filterExistingParties(parties: Set[Party])(implicit - loggingContext: LoggingContextWithTrace - ): Future[Set[Party]] = inTransaction(_.partiesExist) { implicit connection => - Right(backend.filterExistingParties(parties)(connection)) - }.map { - case Right(value) => value - case Left(_) => Set.empty // It is always `Right`, see few lines above - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentUserManagementStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentUserManagementStore.scala deleted file mode 100644 index 7ae532437b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/PersistentUserManagementStore.scala +++ /dev/null @@ -1,484 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.metrics.DatabaseMetrics -import com.daml.nameof.NameOf.* -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.ledger.api.util.TimeProvider -import com.digitalasset.canton.ledger.api.validation.ResourceAnnotationValidator -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta, User, UserRight} -import com.digitalasset.canton.ledger.localstore.PersistentUserManagementStore.{ - ConcurrentUserUpdateDetectedRuntimeException, - MaxAnnotationsSizeExceededException, - TooManyUserRightsRuntimeException, -} -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore.* -import com.digitalasset.canton.ledger.localstore.api.{UserManagementStore, UserUpdate} -import com.digitalasset.canton.ledger.localstore.utils.LocalAnnotationsUtils -import com.digitalasset.canton.lifecycle.FlagCloseable -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, - TracedLogger, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.DbSupport -import com.digitalasset.canton.platform.store.backend.localstore.UserManagementStorageBackend -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.retry.ErrorKind.{FatalErrorKind, TransientErrorKind} -import com.digitalasset.canton.util.retry.{Backoff, ErrorKind, ExceptionRetryPolicy, Success} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.UserId - -import java.sql.Connection -import scala.concurrent.duration.* -import scala.concurrent.{ExecutionContext, Future} - -class PersistentUserManagementStore( - dbSupport: DbSupport, - metrics: LedgerApiServerMetrics, - timeProvider: TimeProvider, - maxRightsPerUser: Int, - val loggerFactory: NamedLoggerFactory, - flagCloseable: FlagCloseable, -) extends UserManagementStore - with NamedLogging { - - private val directEc = DirectExecutionContext(noTracingLogger) - - private val backend = dbSupport.storageBackendFactory.createUserManagementStorageBackend - private val dbDispatcher = dbSupport.dbDispatcher - - override def getUserInfo(id: UserId, identityProviderId: IdentityProviderId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[UserInfo]] = - inTransaction(_.getUserInfo, functionFullName) { implicit connection => - withUser(id, identityProviderId) { dbUser => - val rights = backend.getUserRights(internalId = dbUser.internalId)(connection) - val annotations = backend.getUserAnnotations(internalId = dbUser.internalId)(connection) - val apiUser = toApiUser(dbUser, annotations) - UserInfo(apiUser, rights.map(_.apiRight)) - } - } - - override def createUser( - user: User, - rights: Set[UserRight], - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[User]] = - inTransaction(_.createUser, functionFullName) { implicit connection: Connection => - withoutUser(user.id, user.identityProviderId) { - val now = epochMicroseconds() - if ( - !ResourceAnnotationValidator - .isWithinMaxAnnotationsByteSize(user.metadata.annotations) - ) { - throw MaxAnnotationsSizeExceededException(userId = user.id) - } - val dbUser = UserManagementStorageBackend.DbUserPayload( - id = user.id, - primaryPartyO = user.primaryParty, - identityProviderId = user.identityProviderId.toDb, - isDeactivated = user.isDeactivated, - primaryPartyAuthentication = user.primaryPartyAuthentication, - resourceVersion = 0, - createdAt = now, - ) - val internalId = retryOnceMore(backend.createUser(user = dbUser)(connection)) - user.metadata.annotations.foreach { case (key, value) => - backend.addUserAnnotation( - internalId = internalId, - key = key, - value = value, - updatedAt = now, - )(connection) - } - rights.foreach(right => - backend.addUserRight(internalId = internalId, right = right, grantedAt = now)( - connection - ) - ) - if (backend.countUserRights(internalId)(connection) > maxRightsPerUser) { - throw TooManyUserRightsRuntimeException(user.id) - } - toApiUser( - dbUser = dbUser, - annotations = user.metadata.annotations, - ) - } - }.map(tapSuccess { _ => - logger.info( - s"Created new user: $user with " + - (if (rights.nonEmpty) - s"${rights.size} rights: ${rightsDigestText(rights)}" - else "no rights") + - s", ${loggingContext.serializeFiltered("submissionId")}." - ) - })(directEc) - - override def updateUser( - userUpdate: UserUpdate - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[User]] = - inTransaction(_.updateUser, functionFullName) { implicit connection => - for { - _ <- withUser(id = userUpdate.id, userUpdate.identityProviderId) { dbUser => - val now = epochMicroseconds() - // Step 1: Update resource version - // NOTE: We starts by writing to the 'resource_version' attribute - // of 'lapi_users' to effectively obtain an exclusive lock for - // updating this user for the rest of the transaction. - val _ = userUpdate.metadataUpdate.resourceVersionO match { - case Some(expectedResourceVersion) => - if ( - !backend.compareAndIncreaseResourceVersion( - internalId = dbUser.internalId, - expectedResourceVersion = expectedResourceVersion, - )(connection) - ) { - throw ConcurrentUserUpdateDetectedRuntimeException( - userUpdate.id - ) - } - case None => - backend.increaseResourceVersion( - internalId = dbUser.internalId - )(connection) - } - // Step 2: Update annotations - userUpdate.metadataUpdate.annotationsUpdateO.foreach { newAnnotations => - val existingAnnotations = - backend.getUserAnnotations(dbUser.internalId)(connection) - val updatedAnnotations = LocalAnnotationsUtils.calculateUpdatedAnnotations( - newValue = newAnnotations, - existing = existingAnnotations, - ) - if ( - !ResourceAnnotationValidator - .isWithinMaxAnnotationsByteSize(updatedAnnotations) - ) { - throw MaxAnnotationsSizeExceededException(userId = userUpdate.id) - } - backend.deleteUserAnnotations(internalId = dbUser.internalId)(connection) - updatedAnnotations.iterator.foreach { case (key, value) => - backend.addUserAnnotation( - internalId = dbUser.internalId, - key = key, - value = value, - updatedAt = now, - )(connection) - } - } - // update is_deactivated - userUpdate.isDeactivatedUpdateO.foreach { newValue => - backend.updateUserIsDeactivated( - internalId = dbUser.internalId, - isDeactivated = newValue, - )(connection) - } - // update primary_party - userUpdate.primaryPartyUpdateO.foreach { newValue => - backend.updateUserPrimaryParty( - internalId = dbUser.internalId, - primaryPartyO = newValue, - )(connection) - } - // update primary_party_authentication - userUpdate.primaryPartyAuthenticationUpdateO.foreach { newValue => - backend.updateUserPrimaryPartyAuthentication( - internalId = dbUser.internalId, - primaryPartyAuthentication = newValue, - )(connection) - } - } - apiUser <- withUser( - id = userUpdate.id, - identityProviderId = userUpdate.identityProviderId, - ) { dbUserAfterUpdates => - val annotations = - backend.getUserAnnotations(internalId = dbUserAfterUpdates.internalId)(connection) - toApiUser(dbUser = dbUserAfterUpdates, annotations = annotations) - } - } yield apiUser - } - - override def updateUserIdp( - id: UserId, - sourceIdp: IdentityProviderId, - targetIdp: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[User]] = - inTransaction(_.updateUserIdp, functionFullName) { implicit connection => - for { - _ <- withUser(id = id, sourceIdp) { dbUser => - val _ = backend.updateUserIdp( - internalId = dbUser.internalId, - identityProviderId = targetIdp.toDb, - )(connection) - } - apiUser <- withUser( - id = id, - identityProviderId = targetIdp, - ) { dbUserAfterUpdates => - val annotations = - backend.getUserAnnotations(internalId = dbUserAfterUpdates.internalId)(connection) - toApiUser(dbUser = dbUserAfterUpdates, annotations = annotations) - } - } yield apiUser - }.map(tapSuccess { _ => - logger.info(s"Updated user $id idp from $sourceIdp to $targetIdp.") - })(directEc) - - override def deleteUser( - id: UserId, - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Unit]] = - inTransaction(_.deleteUser, functionFullName) { implicit connection => - withUser(id, identityProviderId) { _ => - backend.deleteUser(id = id)(connection) - }.flatMap { - Either.cond(_, (), UserNotFound(userId = id)) - } - }.map(tapSuccess { _ => - logger.info( - s"Deleted user with id: $id, ${loggingContext.serializeFiltered("submissionId")}." - ) - })(directEc) - - override def grantRights( - id: UserId, - rights: Set[UserRight], - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Set[UserRight]]] = - inTransaction(_.grantRights, functionFullName) { implicit connection => - withUser(id = id, identityProviderId) { user => - val now = epochMicroseconds() - val addedRights = rights.filter { right => - if (!backend.userRightExists(internalId = user.internalId, right = right)(connection)) { - retryOnceMore( - backend.addUserRight( - internalId = user.internalId, - right = right, - grantedAt = now, - )(connection) - ) - true - } else { - false - } - } - if (backend.countUserRights(user.internalId)(connection) > maxRightsPerUser) { - throw TooManyUserRightsRuntimeException(user.payload.id) - } else { - addedRights - } - } - }.map(tapSuccess { grantedRights => - logger.info( - s"Granted ${grantedRights.size} user rights to user $id: ${rightsDigestText(grantedRights)}, ${loggingContext - .serializeFiltered("submissionId")}." - ) - })(directEc) - - override def revokeRights( - id: UserId, - rights: Set[UserRight], - identityProviderId: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[Set[UserRight]]] = - inTransaction(_.revokeRights, functionFullName) { implicit connection => - withUser(id = id, identityProviderId) { user => - val revokedRights = rights.filter { right => - backend.deleteUserRight(internalId = user.internalId, right = right)(connection) - } - revokedRights - } - }.map(tapSuccess { revokedRights => - logger.info( - s"Revoked ${revokedRights.size} user rights from user $id: ${rightsDigestText(revokedRights)}, ${loggingContext - .serializeFiltered("submissionId")}." - ) - })(directEc) - - override def listUsers( - fromExcl: Option[Ref.UserId], - maxResults: Int, - identityProviderId: IdentityProviderId, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[UsersPage]] = - inTransaction(_.listUsers, functionFullName) { connection => - val dbUsers = fromExcl match { - case None => - backend.getUsersOrderedById(None, maxResults, identityProviderId)(connection) - case Some(fromExcl) => - backend.getUsersOrderedById(Some(fromExcl), maxResults, identityProviderId)( - connection - ) - } - val users = dbUsers.map { dbUser => - val annotations = backend.getUserAnnotations(dbUser.internalId)(connection) - toApiUser(dbUser = dbUser, annotations = annotations) - } - Right(UsersPage(users = users)) - } - - private def inTransaction[T]( - dbMetric: metrics.userManagement.type => DatabaseMetrics, - operationName: String, - )( - thunk: Connection => Result[T] - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[T]] = { - def execute(): Future[Result[T]] = - dbDispatcher.executeSql(dbMetric(metrics.userManagement))(thunk) - implicit val ec: ExecutionContext = directEc - implicit val success = Success.always - val retry = Backoff( - logger = logger, - hasSynchronizeWithClosing = flagCloseable, - maxRetries = 10, - initialDelay = 50.milliseconds, - maxDelay = 1.second, - operationName = operationName, - ) - retry( - execute(), - RetryOnceMoreExceptionRetryPolicy, - ) - .recover[Result[T]] { - case TooManyUserRightsRuntimeException(userId) => Left(TooManyUserRights(userId)) - case ConcurrentUserUpdateDetectedRuntimeException(userId) => - Left(UserManagementStore.ConcurrentUserUpdate(userId)) - case MaxAnnotationsSizeExceededException(userId) => - Left(UserManagementStore.MaxAnnotationsSizeExceeded(userId)) - } - } - - private def toApiUser( - dbUser: UserManagementStorageBackend.DbUserWithId, - annotations: Map[String, String], - ): User = - toApiUser( - dbUser = dbUser.payload, - annotations = annotations, - ) - - private def toApiUser( - dbUser: UserManagementStorageBackend.DbUserPayload, - annotations: Map[String, String], - ): User = { - val payload = dbUser - - User( - id = payload.id, - primaryParty = payload.primaryPartyO, - isDeactivated = payload.isDeactivated, - identityProviderId = IdentityProviderId.fromDb(payload.identityProviderId), - metadata = ObjectMeta( - resourceVersionO = Some(payload.resourceVersion), - annotations = annotations, - ), - primaryPartyAuthentication = payload.primaryPartyAuthentication, - ) - } - - private def withUser[T]( - id: Ref.UserId, - identityProviderId: IdentityProviderId, - )( - f: UserManagementStorageBackend.DbUserWithId => T - )(implicit connection: Connection): Result[T] = - backend.getUser(id = id)(connection) match { - case Some(user) if user.payload.identityProviderId == identityProviderId.toDb => - Right(f(user)) - case Some(_) => Left(PermissionDenied(userId = id)) - case None => Left(UserNotFound(userId = id)) - } - - private def withoutUser[T]( - id: Ref.UserId, - identityProviderId: IdentityProviderId, - )(t: => T)(implicit connection: Connection): Result[T] = - backend.getUser(id = id)(connection) match { - case Some(user) if user.payload.identityProviderId != identityProviderId.toDb => - Left(PermissionDenied(userId = id)) - case Some(user) => - Left(UserExists(userId = user.payload.id)) - case None => Right(t) - } - - private def tapSuccess[T](f: T => Unit)(r: Result[T]): Result[T] = { - r.foreach(f) - r - } - - private def rightsDigestText(rights: Iterable[UserRight]): String = { - val closingBracket = if (rights.sizeIs > 5) ", ..." else "" - rights.take(5).mkString("", ", ", closingBracket) - } - - private def epochMicroseconds(): Long = { - val now = timeProvider.getCurrentTime - (now.getEpochSecond * 1000 * 1000) + (now.getNano / 1000) - } - - private def retryOnceMore[T](body: => T): T = - try { - body - } catch { - case t: Throwable => throw RetryOnceMoreException(t) - } -} - -object PersistentUserManagementStore { - - /** Intended to be thrown within a DB transaction to abort it. The resulting failed future will - * get mapped to a successful future containing scala.util.Left - */ - final case class TooManyUserRightsRuntimeException(userId: Ref.UserId) extends RuntimeException - - final case class ConcurrentUserUpdateDetectedRuntimeException(userId: Ref.UserId) - extends RuntimeException - - final case class MaxAnnotationsSizeExceededException(userId: Ref.UserId) extends RuntimeException - - def cached( - dbSupport: DbSupport, - metrics: LedgerApiServerMetrics, - timeProvider: TimeProvider, - cacheExpiryAfterWriteInSeconds: Int, - maxCacheSize: Int, - maxRightsPerUser: Int, - loggerFactory: NamedLoggerFactory, - flagCloseable: FlagCloseable, - )(implicit - executionContext: ExecutionContext, - traceContext: TraceContext, - ): UserManagementStore = - new CachedUserManagementStore( - delegate = new PersistentUserManagementStore( - dbSupport = dbSupport, - metrics = metrics, - maxRightsPerUser = maxRightsPerUser, - timeProvider = timeProvider, - loggerFactory = loggerFactory, - flagCloseable = flagCloseable, - ), - expiryAfterWriteInSeconds = cacheExpiryAfterWriteInSeconds, - maximumCacheSize = maxCacheSize, - metrics = metrics, - loggerFactory = loggerFactory, - )(executionContext, LoggingContextWithTrace(loggerFactory)) -} - -final case class RetryOnceMoreException(underlying: Throwable) extends RuntimeException - -object RetryOnceMoreExceptionRetryPolicy extends ExceptionRetryPolicy { - override protected def determineExceptionErrorKind( - exception: Throwable, - logger: TracedLogger, - )(implicit tc: TraceContext): ErrorKind = exception match { - case RetryOnceMoreException(t) => TransientErrorKind() - case _ => FatalErrorKind - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/IdentityProviderConfigStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/IdentityProviderConfigStore.scala deleted file mode 100644 index 8d7d03612c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/IdentityProviderConfigStore.scala +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore.api - -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLogging} - -import scala.concurrent.{ExecutionContext, Future} - -import IdentityProviderConfigStore.Result - -trait IdentityProviderConfigStore { self: NamedLogging => - - def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] - - def getIdentityProviderConfig(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] - - def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Unit]] - - def listIdentityProviderConfigs()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Seq[IdentityProviderConfig]]] - - def updateIdentityProviderConfig(update: IdentityProviderConfigUpdate)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] - - def getIdentityProviderConfig(issuer: String)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[IdentityProviderConfig]] - - def identityProviderConfigExists(id: IdentityProviderId.Id)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Boolean] - - final def getActiveIdentityProviderByIssuer(issuer: String)(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[IdentityProviderConfig] = - getIdentityProviderConfig(issuer) - .flatMap { - case Right(value) if !value.isDeactivated => Future.successful(value) - case Right(value) => - // We do not throw here an error code, as this code path is - // handled by IdentityProviderAwareAuthService by transforming the - // exception into a warning in the logs. - Future.failed( - new Exception(s"Identity Provider ${value.identityProviderId.value} is deactivated.") - ) - case Left(error) => - Future.failed(new Exception(error.toString)) - }(executionContext) -} - -object IdentityProviderConfigStore { - - type Result[T] = Either[Error, T] - - sealed trait Error - final case class IdentityProviderConfigNotFound(identityProviderId: IdentityProviderId.Id) - extends Error - final case class IdentityProviderConfigExists(identityProviderId: IdentityProviderId.Id) - extends Error - final case class IdentityProviderConfigWithIssuerExists(issuer: String) extends Error - final case class TooManyIdentityProviderConfigs() extends Error - final case class IdentityProviderConfigByIssuerNotFound(issuer: String) extends Error -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/IdentityProviderConfigUpdate.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/IdentityProviderConfigUpdate.scala deleted file mode 100644 index 80e9f6ee36..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/IdentityProviderConfigUpdate.scala +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore.api - -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.api.IdentityProviderId - -final case class IdentityProviderConfigUpdate( - identityProviderId: IdentityProviderId.Id, - isDeactivatedUpdate: Option[Boolean] = None, - jwksUrlUpdate: Option[JwksUrl] = None, - issuerUpdate: Option[String] = None, - audienceUpdate: Option[Option[String]] = None, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/PartyRecord.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/PartyRecord.scala deleted file mode 100644 index aaec277ae8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/PartyRecord.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore.api - -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta} -import com.digitalasset.daml.lf.data.Ref - -final case class PartyRecord( - party: Ref.Party, - metadata: ObjectMeta, - identityProviderId: IdentityProviderId, -) extends { - override def toString: String = - s"PartyRecord(party=$party, metadata=${metadata.toString.take(500)}, identityProviderId=$identityProviderId)" -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/PartyRecordStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/PartyRecordStore.scala deleted file mode 100644 index 9fc2925ac5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/PartyRecordStore.scala +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore.api - -import com.digitalasset.canton.ledger.api.IdentityProviderId -import com.digitalasset.canton.ledger.api.validation.ResourceAnnotationValidator -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Ref - -import scala.concurrent.Future - -final case class PartyDetailsUpdate( - party: Ref.Party, - identityProviderId: IdentityProviderId, - isLocalUpdate: Option[Boolean], - metadataUpdate: ObjectMetaUpdate, -) - -final case class PartyRecordUpdate( - party: Ref.Party, - identityProviderId: IdentityProviderId, - metadataUpdate: ObjectMetaUpdate, -) - -trait PartyRecordStore { - import PartyRecordStore.* - - def createPartyRecord(partyRecord: PartyRecord)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[PartyRecord]] - - def updatePartyRecord(partyRecordUpdate: PartyRecordUpdate, ledgerPartyIsLocal: Boolean)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[PartyRecord]] - - def updatePartyRecordIdp( - party: Ref.Party, - ledgerPartyIsLocal: Boolean, - sourceIdp: IdentityProviderId, - targetIdp: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[PartyRecord]] - - def getPartyRecordO(party: Ref.Party)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Option[PartyRecord]]] - - def filterExistingParties(parties: Set[Ref.Party], identityProviderId: IdentityProviderId)( - implicit loggingContext: LoggingContextWithTrace - ): Future[Set[Ref.Party]] - - def filterExistingParties(parties: Set[Ref.Party])(implicit - loggingContext: LoggingContextWithTrace - ): Future[Set[Ref.Party]] - -} - -object PartyRecordStore { - type Result[T] = Either[Error, T] - - sealed trait Error - - final case class PartyNotFound(party: Ref.Party) extends Error - final case class PartyRecordNotFoundFatal(party: Ref.Party) extends Error - final case class PartyRecordExistsFatal(party: Ref.Party) extends Error - final case class ConcurrentPartyUpdate(party: Ref.Party) extends Error - final case class MaxAnnotationsSizeExceeded(party: Ref.Party) extends Error { - def getReason: String = ResourceAnnotationValidator.AnnotationsSizeExceededError.reason - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/UserManagementStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/UserManagementStore.scala deleted file mode 100644 index 79268e25f0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/api/UserManagementStore.scala +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore.api - -import com.digitalasset.canton.ledger.api.{IdentityProviderId, User, UserRight} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLogging} -import com.digitalasset.daml.lf.data.Ref - -import scala.concurrent.{ExecutionContext, Future} - -final case class UserUpdate( - id: Ref.UserId, - identityProviderId: IdentityProviderId, - primaryPartyUpdateO: Option[Option[Ref.Party]] = None, - isDeactivatedUpdateO: Option[Boolean] = None, - metadataUpdate: ObjectMetaUpdate, - primaryPartyAuthenticationUpdateO: Option[Boolean] = None, -) - -final case class ObjectMetaUpdate( - resourceVersionO: Option[Long], - annotationsUpdateO: Option[Map[String, String]], -) - -object ObjectMetaUpdate { - def empty: ObjectMetaUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = None, - ) -} - -trait UserManagementStore { self: NamedLogging => - - import UserManagementStore.* - - // read access - - def getUserInfo(id: Ref.UserId, identityProviderId: IdentityProviderId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[UserInfo]] - - /** Always returns `maxResults` if possible, i.e. if a call to this method returned fewer than - * `maxResults` users, then the next page (as of calling this method) was empty. - */ - def listUsers( - fromExcl: Option[Ref.UserId], - maxResults: Int, - identityProviderId: IdentityProviderId, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[UsersPage]] - - // write access - - def createUser(user: User, rights: Set[UserRight])(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[User]] - - def updateUser(userUpdate: UserUpdate)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[User]] - - def deleteUser(id: Ref.UserId, identityProviderId: IdentityProviderId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Result[Unit]] - - def grantRights(id: Ref.UserId, rights: Set[UserRight], identityProviderId: IdentityProviderId)( - implicit loggingContext: LoggingContextWithTrace - ): Future[Result[Set[UserRight]]] - - def revokeRights(id: Ref.UserId, rights: Set[UserRight], identityProviderId: IdentityProviderId)( - implicit loggingContext: LoggingContextWithTrace - ): Future[Result[Set[UserRight]]] - - def updateUserIdp( - id: Ref.UserId, - sourceIdp: IdentityProviderId, - targetIdp: IdentityProviderId, - )(implicit loggingContext: LoggingContextWithTrace): Future[Result[User]] - // read helpers - - final def getUser(id: Ref.UserId, identityProviderId: IdentityProviderId)(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[Result[User]] = - getUserInfo(id, identityProviderId).map(_.map(_.user)) - - final def listUserRights(id: Ref.UserId, identityProviderId: IdentityProviderId)(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[Result[Set[UserRight]]] = - getUserInfo(id, identityProviderId).map(_.map(_.rights)) - -} - -object UserManagementStore { - - val DefaultParticipantAdminUserId = "participant_admin" - - type Result[T] = Either[Error, T] - - final case class UsersPage(users: Seq[User]) { - def lastUserIdOption: Option[Ref.UserId] = users.lastOption.map(_.id) - } - - final case class UserInfo(user: User, rights: Set[UserRight]) - - sealed trait Error - final case class UserNotFound(userId: Ref.UserId) extends Error - final case class UserDeletedWhileUpdating(userId: Ref.UserId) extends Error - final case class UserExists(userId: Ref.UserId) extends Error - final case class TooManyUserRights(userId: Ref.UserId) extends Error - final case class ConcurrentUserUpdate(userId: Ref.UserId) extends Error - final case class MaxAnnotationsSizeExceeded(userId: Ref.UserId) extends Error - final case class PermissionDenied(userId: Ref.UserId) extends Error - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/utils/LocalAnnotationsUtils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/utils/LocalAnnotationsUtils.scala deleted file mode 100644 index 1171a1426f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/localstore/utils/LocalAnnotationsUtils.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore.utils - -object LocalAnnotationsUtils { - - def calculateUpdatedAnnotations( - newValue: Map[String, String], - existing: Map[String, String], - ): Map[String, String] = - existing.concat(newValue).view.filter { case (_, value) => value != "" }.toMap - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/CommandMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/CommandMetrics.scala deleted file mode 100644 index 63192fb7db..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/CommandMetrics.scala +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.HistogramInventory.Item -import com.daml.metrics.api.MetricHandle.{Counter, Histogram, LabeledMetricsFactory, Meter, Timer} -import com.daml.metrics.api.{HistogramInventory, MetricInfo, MetricName, MetricQualification} - -final class CommandHistograms(val prefix: MetricName)(implicit - inventory: HistogramInventory -) { - - val validation: Item = Item( - prefix :+ "validation", - summary = "The time to validate a Daml command.", - description = """The time to validate a submitted Daml command before is fed to the - |interpreter.""", - qualification = MetricQualification.Debug, - ) - - val reassignmentValidation: Item = Item( - prefix :+ "reassignment_validation", - summary = "The time to validate a reassignment command.", - description = """The time to validate a submitted Daml command before is fed to the - |interpreter.""", - qualification = MetricQualification.Debug, - ) - - val submissions: Item = Item( - prefix :+ "submissions", - summary = "The time to fully process a Daml command.", - description = """The time to validate and interpret a command before it is handed over to the - |synchronization services to be finalized (either committed or rejected).""", - qualification = MetricQualification.Latency, - ) - - val interactive_prepares: Item = Item( - prefix :+ "interactive_prepares", - summary = "The time to prepare a transaction for interactive submission.", - description = - """The time to validate and interpret a command before it is returned to the caller - |for external signing.""", - qualification = MetricQualification.Latency, - ) - - val taps_package_selection = Item( - prefix :+ "taps_package_selection", - summary = "The time spent on package selection in a single TAPS pass.", - description = """The time spent on package selection in a single pass of the Topology-Aware - |Package Selection, before the command is handed to the Daml Engine for interpretation.""", - qualification = MetricQualification.Latency, - ) -} - -// Private constructor to avoid being instantiated multiple times by accident -final class CommandMetrics private[metrics] ( - inventory: CommandHistograms, - factory: LabeledMetricsFactory, -) { - - import com.daml.metrics.api.MetricsContext.Implicits.empty - - val validation: Timer = factory.timer(inventory.validation.info) - - val reassignmentValidation: Timer = factory.timer(inventory.reassignmentValidation.info) - - val submissions: Timer = factory.timer(inventory.submissions.info) - val interactivePrepares: Timer = factory.timer(inventory.interactive_prepares.info) - val tapsPackageSelection: Timer = factory.timer(inventory.taps_package_selection.info) - - val submissionsRunning: Counter = factory.counter( - MetricInfo( - inventory.prefix :+ "submissions_running", - summary = - "The number of the Daml commands that are currently being handled by the ledger api server.", - description = - """The number of the Daml commands that are currently being handled by the ledger - |api server (including validation, interpretation, and handing the transaction - |over to the synchronization services).""", - qualification = MetricQualification.Saturation, - ) - ) - - val preparesRunning: Counter = factory.counter( - MetricInfo( - inventory.prefix :+ "prepares_running", - summary = - "The number of the Daml commands for which transactions are currently being prepared by the ledger api server.", - description = - """The number of the Daml commands that are currently being prepared by the ledger - |api server (including validation, interpretation).""", - qualification = MetricQualification.Saturation, - ) - ) - - val failedCommandInterpretations: Meter = - factory.meter( - MetricInfo( - inventory.prefix :+ "failed_command_interpretations", - summary = "The number of Daml commands that failed in interpretation.", - description = """The number of Daml commands that have been rejected by the interpreter - |(e.g. badly authorized action).""", - qualification = MetricQualification.Errors, - ) - ) - - val delayedSubmissions: Meter = factory.meter( - MetricInfo( - inventory.prefix :+ "delayed_submissions", - summary = "The number of the delayed Daml commands.", - description = """The number of Daml commands that have been delayed internally because they - |have been evaluated to require the ledger time further in the future than the - |expected latency.""", - qualification = MetricQualification.Debug, - ) - ) - - val validSubmissions: Meter = factory.meter( - MetricInfo( - inventory.prefix :+ "valid_submissions", - summary = "The total number of the valid Daml commands.", - description = """The total number of the Daml commands that have passed validation and were - |sent to interpretation in this ledger api server process.""", - qualification = MetricQualification.Debug, - ) - ) - - val maxInFlightLength: Counter = factory.counter( - MetricInfo( - inventory.prefix :+ "max_in_flight_length", - summary = "The number of the Daml commands awaiting completion.", - description = - "The number of the currently Daml commands awaiting completion in the Command Service.", - qualification = MetricQualification.Debug, - ) - ) - - val maxInFlightCapacity: Counter = factory.counter( - MetricInfo( - inventory.prefix :+ "max_in_flight_capacity", - summary = "The maximum number of Daml commands that can await completion.", - description = - "The maximum number of Daml commands that can await completion in the Command Service.", - qualification = MetricQualification.Debug, - ) - ) - - val tapsPasses: Histogram = factory.histogram( - MetricInfo( - inventory.prefix :+ "taps_passes", - summary = "The number of TAPS passes during processing of a command.", - description = - """The number of Topology-Aware Package Selection passes during processing of a command.""", - qualification = MetricQualification.Debug, - ) - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ContractStoreMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ContractStoreMetrics.scala deleted file mode 100644 index 1ffef21ba8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ContractStoreMetrics.scala +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.HistogramInventory.Item -import com.daml.metrics.api.MetricHandle.{LabeledMetricsFactory, Timer} -import com.daml.metrics.api.{HistogramInventory, MetricName, MetricQualification} - -class ContractStoreHistograms(val prefix: MetricName)(implicit - inventory: HistogramInventory -) { - val lookupPersisted: Item = Item( - prefix :+ "lookup_persisted", - summary = "The time to lookup persisted contract by LF contract id.", - description = - "The time to enqueue and execute the lookup for persisted contract by LF contract id.", - qualification = MetricQualification.Debug, - ) - val lookupBatched: Item = Item( - prefix :+ "lookup_batched", - summary = "The time to execute batched contract lookup.", - description = "The time to enqueue and execute batched contract lookup.", - qualification = MetricQualification.Debug, - ) - val lookupBatchedContractIds: Item = Item( - prefix :+ "lookup_batched_contract_ids", - summary = "The time to execute batched contract id lookup.", - description = "The time to enqueue and execute batched contract id lookup.", - qualification = MetricQualification.Debug, - ) - val lookupBatchedInternalIds: Item = Item( - prefix :+ "lookup_batched_internal_ids", - summary = "The time to execute batched internal id lookup.", - description = "The time to enqueue and execute batched internal id lookup.", - qualification = MetricQualification.Debug, - ) - val reInsertContracts: Item = Item( - prefix :+ "re_insert_contracts", - summary = - "The time to execute batched contract insertion in DB in case the contracts have got pruned by the time needed by the Indexer.", - description = "The time to conduct the DB operation for storing the contracts missing.", - qualification = MetricQualification.Debug, - ) -} - -class ContractStoreMetrics private[metrics] ( - inventory: ContractStoreHistograms, - factory: LabeledMetricsFactory, -) { - val lookupPersisted: Timer = factory.timer(inventory.lookupPersisted.info) - val lookupBatched: Timer = factory.timer(inventory.lookupBatched.info) - val lookupBatchedContractIds: Timer = factory.timer(inventory.lookupBatchedContractIds.info) - val lookupBatchedInternalIds: Timer = factory.timer(inventory.lookupBatchedInternalIds.info) - val reInsertContracts: Timer = factory.timer(inventory.reInsertContracts.info) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/DatabaseMetricsFactory.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/DatabaseMetricsFactory.scala deleted file mode 100644 index 4bba91e81b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/DatabaseMetricsFactory.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.DatabaseMetrics -import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory -import com.daml.metrics.api.MetricName - -abstract class DatabaseMetricsFactory(prefix: MetricName, factory: LabeledMetricsFactory) { - - protected def createDbMetrics(name: String): DatabaseMetrics = - new DatabaseMetrics(prefix :+ name, factory) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ExecutionMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ExecutionMetrics.scala deleted file mode 100644 index d23a5c9841..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ExecutionMetrics.scala +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.CacheMetrics -import com.daml.metrics.api.HistogramInventory.Item -import com.daml.metrics.api.MetricHandle.* -import com.daml.metrics.api.{HistogramInventory, MetricInfo, MetricName, MetricQualification} - -private[metrics] final class ExecutionHistograms(val prefix: MetricName)(implicit - inventory: HistogramInventory -) { - - private[metrics] val lookupActiveContract: Item = Item( - prefix :+ "lookup_active_contract", - summary = "The time to lookup individual active contracts during interpretation.", - description = """The interpretation of a command in the ledger api server might require - |fetching multiple active contracts. This metric exposes the time to lookup - |individual active contracts.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val lookupActiveContractPerExecution: Item = Item( - prefix :+ "lookup_active_contract_per_execution", - summary = "The compound time to lookup all active contracts in a single Daml command.", - description = """The interpretation of a command in the ledger api server might require - |fetching multiple active contracts. This metric exposes the compound time to - |lookup all the active contracts in a single Daml command.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val lookupActiveContractCountPerExecution: Item = Item( - prefix :+ "lookup_active_contract_count_per_execution", - summary = "The number of the active contracts looked up per Daml command.", - description = """The interpretation of a command in the ledger api server might require - |fetching multiple active contracts. This metric exposes the number of active - |contracts that must be looked up to process a Daml command.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val lookupContractKey: Item = Item( - prefix :+ "lookup_contract_key", - summary = "The time to lookup individual contract keys during interpretation.", - description = """The interpretation of a command in the ledger api server might require - |fetching multiple contract keys. This metric exposes the time needed to lookup - |individual contract keys.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val lookupNContractKey: Item = Item( - prefix :+ "lookup_n_contract_key", - summary = "The time to lookup individual contract keys during interpretation.", - description = """The interpretation of a command in the ledger api server might require - |fetching multiple contract keys. This metric exposes the time needed to lookup - |individual non unique contract keys.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val lookupContractKeyPerExecution: Item = Item( - prefix :+ "lookup_contract_key_per_execution", - summary = "The compound time to lookup all contract keys in a single Daml command.", - description = """The interpretation of a command in the ledger api server might require - |fetching multiple contract keys. This metric exposes the compound time needed - |to lookup all the contract keys in a single Daml command.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val lookupContractKeyCountPerExecution: Item = Item( - prefix :+ "lookup_contract_key_count_per_execution", - summary = "The number of contract keys looked up per Daml command.", - description = """The interpretation of a command in the ledger api server might require - |fetching multiple contract keys. This metric exposes the number of contract - |keys that must be looked up to process a Daml command.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val getLfPackage: Item = Item( - prefix :+ "get_lf_package", - summary = "The time to fetch individual Daml code packages during interpretation.", - description = """The interpretation of a command in the ledger api server might require - |fetching multiple Daml packages. This metric exposes the time needed to fetch - |the packages that are necessary for interpretation.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val total: Item = Item( - prefix :+ "total", - summary = "The overall time spent interpreting a Daml command.", - description = """The time spent interpreting a Daml command in the ledger api server (includes - |executing Daml and fetching data).""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val engine: Item = Item( - prefix :+ "engine", - summary = "The time spent executing a Daml command.", - description = """The time spent by the Daml engine executing a Daml command (excluding fetching - |data).""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val cachePrefix: MetricName = ExecutionHistograms.this.prefix :+ "cache" - - private[metrics] val registerKeyStateCacheUpdate: Item = Item( - cachePrefix :+ "key_state" :+ "register_update", - summary = "The time spent to update the key state cache.", - description = """The total time spent in sequential update steps of the key state caches - |updating logic. This metric is created with debugging purposes in mind.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val registerContractStateCacheUpdate: Item = Item( - cachePrefix :+ "contract_state" :+ "register_update", - summary = "The time spent to update the contract state cache.", - description = """The total time spent in sequential update steps of the contract state caches - |updating logic. This metric is created with debugging purposes in mind.""", - qualification = MetricQualification.Debug, - ) - -} - -// Private constructor to avoid being instantiated multiple times by accident -final class ExecutionMetrics private[metrics] ( - inventory: ExecutionHistograms, - openTelemetryMetricsFactory: LabeledMetricsFactory, -) { - - import com.daml.metrics.api.MetricsContext.Implicits.empty - private val prefix = inventory.prefix - - val lookupActiveContract: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupActiveContract.info) - - val lookupActiveContractPerExecution: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupActiveContractPerExecution.info) - - val lookupActiveContractCountPerExecution: Histogram = - openTelemetryMetricsFactory.histogram(inventory.lookupActiveContractCountPerExecution.info) - - val lookupContractKey: Timer = openTelemetryMetricsFactory.timer(inventory.lookupContractKey.info) - val lookupNContractKey: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupNContractKey.info) - - val lookupContractKeyPerExecution: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupContractKeyPerExecution.info) - - val lookupContractKeyCountPerExecution: Histogram = - openTelemetryMetricsFactory.histogram(inventory.lookupContractKeyCountPerExecution.info) - - val getLfPackage: Timer = openTelemetryMetricsFactory.timer(inventory.getLfPackage.info) - - val retry: Meter = openTelemetryMetricsFactory.meter( - MetricInfo( - prefix :+ "retry", - summary = "The number of the interpretation retries.", - description = - """The total number of interpretation retries attempted due to mismatching ledger - |effective time in this ledger api server process.""", - qualification = MetricQualification.Debug, - ) - ) - - val total: Timer = openTelemetryMetricsFactory.timer(inventory.total.info) - - val totalRunning: Counter = openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "total_running", - summary = "The number of Daml commands currently being interpreted.", - description = """The number of the commands that are currently being interpreted (includes - |executing Daml code and fetching data).""", - qualification = MetricQualification.Debug, - ) - ) - - val engine: Timer = openTelemetryMetricsFactory.timer(inventory.engine.info) - - val engineRunning: Counter = openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "engine_running", - summary = "The number of Daml commands currently being executed.", - description = """The number of the commands that are currently being executed by the Daml - |engine (excluding fetching data).""", - qualification = MetricQualification.Debug, - ) - ) - - // Private constructor to avoid being instantiated multiple times by accident - final class ExecutionCacheMetrics private[ExecutionMetrics] { - val prefix: MetricName = inventory.cachePrefix - - // Private constructor to avoid being instantiated multiple times by accident - final class KeyStateMetrics private[ExecutionCacheMetrics] { - val stateCache: CacheMetrics = - new CacheMetrics(prefix :+ "key_state", openTelemetryMetricsFactory) - - val registerCacheUpdate: Timer = - openTelemetryMetricsFactory.timer(inventory.registerKeyStateCacheUpdate.info) - } - - val keyState: KeyStateMetrics = new KeyStateMetrics - - // Private constructor to avoid being instantiated multiple times by accident - final class ContractStateMetrics private[ExecutionMetrics] { - val stateCache: CacheMetrics = - new CacheMetrics(prefix :+ "contract_state", openTelemetryMetricsFactory) - - val registerCacheUpdate: Timer = - openTelemetryMetricsFactory.timer(inventory.registerContractStateCacheUpdate.info) - } - - val contractState: ContractStateMetrics = new ContractStateMetrics - - } - - val cache: ExecutionCacheMetrics = new ExecutionCacheMetrics -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IdentityProviderConfigStoreMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IdentityProviderConfigStoreMetrics.scala deleted file mode 100644 index bb1ffd3177..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IdentityProviderConfigStoreMetrics.scala +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory -import com.daml.metrics.api.MetricName -import com.daml.metrics.{CacheMetrics, DatabaseMetrics} - -// Private constructor to avoid being instantiated multiple times by accident -final class IdentityProviderConfigStoreMetrics private[metrics] ( - prefix: MetricName, - labeledMetricsFactory: LabeledMetricsFactory, -) extends DatabaseMetricsFactory(prefix, labeledMetricsFactory) { - - val idpConfigCache: CacheMetrics = - new CacheMetrics(prefix :+ "idp_config_cache", labeledMetricsFactory) - val verifierCache: CacheMetrics = - new CacheMetrics(prefix :+ "verifier_cache", labeledMetricsFactory) - val createIdpConfig: DatabaseMetrics = createDbMetrics("create_identity_provider_config") - val getIdpConfig: DatabaseMetrics = createDbMetrics("get_identity_provider_config") - val deleteIdpConfig: DatabaseMetrics = createDbMetrics("delete_identity_provider_config") - val updateIdpConfig: DatabaseMetrics = createDbMetrics("update_identity_provider_config") - val listIdpConfigs: DatabaseMetrics = createDbMetrics("list_identity_provider_configs") - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexDBMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexDBMetrics.scala deleted file mode 100644 index ac89ae552d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexDBMetrics.scala +++ /dev/null @@ -1,562 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.DatabaseMetrics -import com.daml.metrics.api.HistogramInventory.Item -import com.daml.metrics.api.MetricHandle.{Counter, Histogram, LabeledMetricsFactory, Timer} -import com.daml.metrics.api.{HistogramInventory, MetricName, MetricQualification, MetricsContext} - -private[metrics] trait TransactionStreamsDbHistograms { - - protected implicit def inventory: HistogramInventory - protected def prefix: MetricName - - private val flatTxStreamPrefix: MetricName = prefix :+ "flat_transactions_stream" - - private[metrics] val flatTxStreamTranslationTimer: Item = Item( - flatTxStreamPrefix :+ "translation", - summary = "The time needed to turn serialized Daml-LF values into in-memory objects.", - description = """Some index database queries that target contracts and transactions involve a - |Daml-LF translation step. For such queries this metric stands for the time it - |takes to turn the serialized Daml-LF values into in-memory representation.""", - qualification = MetricQualification.Debug, - ) - - private val treeTxStreamPrefix: MetricName = prefix :+ "tree_transactions_stream" - - private[metrics] val treeTxStreamTranslationTimer: Item = Item( - treeTxStreamPrefix :+ "translation", - summary = "The time needed to turn serialized Daml-LF values into in-memory objects.", - description = """Some index database queries that target contracts and transactions involve a - |Daml-LF translation step. For such queries this metric stands for the time it - |takes to turn the serialized Daml-LF values into in-memory representation.""", - qualification = MetricQualification.Debug, - ) - - private val reassignmentStreamPrefix: MetricName = prefix :+ "reassignment_stream" - - private[metrics] val reassignmentStreamTranslationTimer: Item = Item( - reassignmentStreamPrefix :+ "translation", - summary = "The time needed to turn serialized Daml-LF values into in-memory objects.", - description = """Some index database queries that target contracts and transactions involve a - |Daml-LF translation step. For such queries this metric stands for the time it - |takes to turn the serialized Daml-LF values into in-memory representation.""", - qualification = MetricQualification.Debug, - ) - -} - -final class IndexDBHistograms(prefix: MetricName)(implicit - protected val inventory: HistogramInventory -) extends MainIndexDBHistograms(prefix) - with TransactionStreamsDbHistograms - -// Private constructor to avoid being instantiated multiple times by accident -final class IndexDBMetrics private[metrics] ( - override val inventory: IndexDBHistograms, - override val openTelemetryMetricsFactory: LabeledMetricsFactory, -) extends MainIndexDBMetrics(inventory, openTelemetryMetricsFactory) - with UpdateStreamsDbMetrics - with UpdatePointwiseDbMetrics - -trait UpdateStreamsDbMetrics { - self: DatabaseMetricsFactory => - - val inventory: TransactionStreamsDbHistograms - val openTelemetryMetricsFactory: LabeledMetricsFactory - - private implicit val metricsContext: MetricsContext = MetricsContext.Empty - - // Private constructor to avoid being instantiated multiple times by accident - final class UpdatesAcsDeltaStreamMetrics private[UpdateStreamsDbMetrics] { - val fetchEventActivateIdsStakeholder: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_stakeholder" - ) - val fetchEventActivateIdsStakeholderFilteredRange: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_stakeholder_filtered_range" - ) - val fetchEventActivateIdsStakeholderFilteredIds: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_stakeholder_filtered_ids" - ) - val fetchEventDeactivateIdsStakeholder: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_stakeholder" - ) - val fetchEventDeactivateIdsStakeholderFilteredRange: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_stakeholder_filtered_range" - ) - val fetchEventDeactivateIdsStakeholderFilteredIds: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_stakeholder_filtered_ids" - ) - val fetchEventActivatePayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_payloads" - ) - val fetchEventDeactivatePayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_payloads" - ) - - val translationTimer: Timer = - openTelemetryMetricsFactory.timer(inventory.flatTxStreamTranslationTimer.info) - } - - val updatesAcsDeltaStream: UpdatesAcsDeltaStreamMetrics = new UpdatesAcsDeltaStreamMetrics - - // Private constructor to avoid being instantiated multiple times by accident - final class UpdatesLedgerEffectsStreamMetrics private[UpdateStreamsDbMetrics] { - val fetchEventActivateIdsStakeholder: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_stakeholder" - ) - val fetchEventActivateIdsStakeholderFilteredRange: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_stakeholder_filtered_range" - ) - val fetchEventActivateIdsStakeholderFilteredIds: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_stakeholder_filtered_ids" - ) - val fetchEventActivateIdsWitness: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_witness" - ) - val fetchEventActivateIdsWitnessFilteredRange: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_witness_filtered_range" - ) - val fetchEventActivateIdsWitnessFilteredIds: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_ids_witness_filtered_ids" - ) - val fetchEventDeactivateIdsStakeholder: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_stakeholder" - ) - val fetchEventDeactivateIdsStakeholderFilteredRange: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_stakeholder_filtered_range" - ) - val fetchEventDeactivateIdsStakeholderFilteredIds: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_stakeholder_filtered_ids" - ) - val fetchEventDeactivateIdsWitness: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_witness" - ) - val fetchEventDeactivateIdsWitnessFilteredRange: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_witness_filtered_range" - ) - val fetchEventDeactivateIdsWitnessFilteredIds: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_ids_witness_filtered_ids" - ) - val fetchEventVariousIdsWitness: DatabaseMetrics = createDbMetrics( - "fetch_event_various_ids_witness" - ) - val fetchEventVariousIdsWitnessFilteredRange: DatabaseMetrics = createDbMetrics( - "fetch_event_various_ids_witness_filtered_range" - ) - val fetchEventVariousIdsWitnessFilteredIds: DatabaseMetrics = createDbMetrics( - "fetch_event_various_ids_witness_filtered_ids" - ) - val fetchEventActivatePayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_payloads" - ) - val fetchEventDeactivatePayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_payloads" - ) - val fetchEventVariousWitnessedPayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_various_witnessed_payloads" - ) - - val translationTimer: Timer = - openTelemetryMetricsFactory.timer(inventory.treeTxStreamTranslationTimer.info) - - } - - val updatesLedgerEffectsStream: UpdatesLedgerEffectsStreamMetrics = - new UpdatesLedgerEffectsStreamMetrics - - // Private constructor to avoid being instantiated multiple times by accident - final class TopologyTransactionsStreamMetrics private[UpdateStreamsDbMetrics] { - val fetchTopologyPartyEventIds: DatabaseMetrics = - createDbMetrics("fetch_topology_party_event_ids") - - val fetchTopologyPartyEventPayloads: DatabaseMetrics = - createDbMetrics("fetch_topology_party_event_payloads") - } - - val topologyTransactionsStream: TopologyTransactionsStreamMetrics = - new TopologyTransactionsStreamMetrics -} - -trait UpdatePointwiseDbMetrics { - self: DatabaseMetricsFactory => - - val inventory: TransactionStreamsDbHistograms - val openTelemetryMetricsFactory: LabeledMetricsFactory - - private implicit val metricsContext: MetricsContext = MetricsContext.Empty - - // Private constructor to avoid being instantiated multiple times by accident - final class UpdatesAcsDeltaPointwiseMetrics private[UpdatePointwiseDbMetrics] { - val fetchEventActivatePayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_payloads" - ) - val fetchEventDeactivatePayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_payloads" - ) - - val translationTimer: Timer = - openTelemetryMetricsFactory.timer(inventory.flatTxStreamTranslationTimer.info) - } - - val updatesAcsDeltaPointwise: UpdatesAcsDeltaPointwiseMetrics = - new UpdatesAcsDeltaPointwiseMetrics - - // Private constructor to avoid being instantiated multiple times by accident - final class UpdatesLedgerEffectsPointwiseMetrics private[UpdatePointwiseDbMetrics] { - val fetchEventActivatePayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_activate_payloads" - ) - val fetchEventDeactivatePayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_deactivate_payloads" - ) - val fetchEventVariousWitnessedPayloads: DatabaseMetrics = createDbMetrics( - "fetch_event_various_witnessed_payloads" - ) - - val translationTimer: Timer = - openTelemetryMetricsFactory.timer(inventory.treeTxStreamTranslationTimer.info) - } - - val updatesLedgerEffectsPointwise: UpdatesLedgerEffectsPointwiseMetrics = - new UpdatesLedgerEffectsPointwiseMetrics - - // Private constructor to avoid being instantiated multiple times by accident - final class TopologyTransactionsPointwiseMetrics private[UpdatePointwiseDbMetrics] { - val fetchTopologyPartyEventPayloads: DatabaseMetrics = - createDbMetrics("fetch_topology_party_event_payloads") - } - - val topologyTransactionsPointwise: TopologyTransactionsPointwiseMetrics = - new TopologyTransactionsPointwiseMetrics -} - -final class BatchLoaderMetricsInventory(parent: MetricName)(implicit - inventory: HistogramInventory -) { - private val prefix = parent :+ "batch" - - val bufferLength: Item = - Item( - prefix :+ "buffer_length", - summary = "The number of the currently pending lookups.", - description = - "The number of the currently pending lookups in the batch-loading queue of the Contract Service.", - qualification = MetricQualification.Debug, - ) - - val bufferCapacity: Item = - Item( - prefix :+ "buffer_capacity", - summary = "The capacity of the lookup queue.", - description = """The maximum number of elements that can be kept in the queue of lookups - |in the batch-loading queue of the Contract Service.""", - qualification = MetricQualification.Debug, - ) - - val bufferDelay: Item = - Item( - prefix :+ "buffer_delay", - summary = "The queuing delay for the lookup queue.", - description = - "The queuing delay for the pending lookups in the batch-loading queue of the Contract Service.", - qualification = MetricQualification.Debug, - ) - - val batchSize: Item = - Item( - prefix :+ "batch_size", - summary = "The batch sizes in the lookup batch-loading Contract Service.", - description = - """The number of lookups contained in a batch, used in the batch-loading Contract Service.""", - qualification = MetricQualification.Debug, - ) -} - -final class BatchLoaderMetrics( - inventory: BatchLoaderMetricsInventory, - factory: LabeledMetricsFactory, -) { - val bufferLength: Counter = factory.counter(inventory.bufferLength.info) - val bufferCapacity: Counter = factory.counter(inventory.bufferCapacity.info) - val bufferDelay: Timer = factory.timer(inventory.bufferDelay.info) - val batchSize: Histogram = factory.histogram(inventory.batchSize.info) -} - -private[metrics] class MainIndexDBHistograms(val prefix: MetricName)(implicit - inventory: HistogramInventory -) { - - private[metrics] val lookupKey: Item = Item( - prefix :+ "lookup_key", - summary = "The time spent looking up a contract using its key.", - description = """This metric exposes the time spent looking up a contract using its key in the - |index db. It is then used by the Daml interpreter when evaluating a command - |into a transaction.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val lookupNonUniqueKey: Item = Item( - prefix :+ "lookup_non_unique_key", - summary = "The time spent looking up contracts using its key.", - description = """This metric exposes the time spent looking up contracts using its key in the - |index db. It is then used by the Daml interpreter when evaluating a command - |into a transaction.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val lookupActiveContract: Item = Item( - prefix :+ "lookup_active_contract", - summary = "The time spent fetching a contract using its id.", - description = """This metric exposes the time spent fetching a contract using its id from the - |index db. It is then used by the Daml interpreter when evaluating a command - |into a transaction.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val activeContracts = new BatchLoaderMetricsInventory( - prefix :+ "active_contract_lookup" - ) - - private[metrics] val activeContractKeys = new BatchLoaderMetricsInventory( - prefix :+ "active_contract_keys_lookup" - ) - - private val translationPrefix = prefix :+ "translation" - // TODO(#17635): It's not an IndexDB op anymore - private[metrics] val getLfPackage: Item = Item( - translationPrefix :+ "get_lf_package", - summary = "The time needed to deserialize and decode a Daml-LF archive.", - description = """A Daml archive before it can be used in the interpretation needs to be - |deserialized and decoded, in other words converted into the in-memory - |representation. This metric represents time necessary to do that.""", - qualification = MetricQualification.Debug, - ) - - private val compressionPrefix: MetricName = prefix :+ "compression" - - private[metrics] val createArgumentCompressed: Item = Item( - compressionPrefix :+ "create_argument_compressed", - summary = "The size of the compressed arguments of a create event.", - description = """Event information can be compressed by the indexer before storing it in the - |database. This metric collects statistics about the size of compressed - |arguments of a create event.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val createKeyValueCompressed: Item = Item( - compressionPrefix :+ "create_key_value_compressed", - summary = "The size of the compressed key value of a create event.", - description = """Event information can be compressed by the indexer before storing it in the - |database. This metric collects statistics about the size of compressed key - |value of a create event.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val createKeyValueUncompressed: Item = Item( - compressionPrefix :+ "create_key_value_uncompressed", - summary = "The size of the decompressed key value of a create event.", - description = """Event information can be compressed by the indexer before storing it in the - |database. This metric collects statistics about the size of decompressed key - |value of a create event.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val exerciseArgumentCompressed: Item = Item( - compressionPrefix :+ "exercise_argument_compressed", - summary = "The size of the compressed argument of an exercise event.", - description = """Event information can be compressed by the indexer before storing it in the - |database. This metric collects statistics about the size of compressed - |arguments of an exercise event.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val exerciseArgumentUncompressed: Item = Item( - compressionPrefix :+ "exercise_argument_uncompressed", - summary = "The size of the decompressed argument of an exercise event.", - description = """Event information can be compressed by the indexer before storing it in the - |database. This metric collects statistics about the size of decompressed - |arguments of an exercise event.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val exerciseResultCompressed: Item = Item( - compressionPrefix :+ "exercise_result_compressed", - summary = "The size of the compressed result of an exercise event.", - description = """Event information can be compressed by the indexer before storing it in the - |database. This metric collects statistics about the size of compressed - |result of an exercise event.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val exerciseResultUncompressed: Item = Item( - compressionPrefix :+ "exercise_result_uncompressed", - summary = "The size of the decompressed result of an exercise event.", - description = """Event information can be compressed by the indexer before storing it in the - |database. This metric collects statistics about the size of compressed - |result of an exercise event.""", - qualification = MetricQualification.Debug, - ) - -} - -class MainIndexDBMetrics( - inventory: MainIndexDBHistograms, - openTelemetryMetricsFactory: LabeledMetricsFactory, -) extends DatabaseMetricsFactory(inventory.prefix, openTelemetryMetricsFactory) { self => - - implicit val metricsContext: MetricsContext = MetricsContext.Empty - private val prefix = inventory.prefix - - val lookupKey: Timer = openTelemetryMetricsFactory.timer(inventory.lookupKey.info) - val lookupNonUniqueKey: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupNonUniqueKey.info) - - val lookupActiveContract: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupActiveContract.info) - - val activeContracts = - new BatchLoaderMetrics(inventory.activeContracts, openTelemetryMetricsFactory) - val activeContractKeys = - new BatchLoaderMetrics(inventory.activeContractKeys, openTelemetryMetricsFactory) - - private val overall = createDbMetrics("all") - val waitAll: Timer = overall.waitTimer - val execAll: Timer = overall.executionTimer - - val getCompletions: DatabaseMetrics = createDbMetrics("get_completions") - val getParticipantId: DatabaseMetrics = createDbMetrics("get_participant_id") - val getLedgerEnd: DatabaseMetrics = createDbMetrics("get_ledger_end") - val getCleanSynchronizerIndex: DatabaseMetrics = createDbMetrics("get_clean_synchronizer_index") - val getTopologyEventOffsetPublishedOnRecordTime: DatabaseMetrics = createDbMetrics( - "get_topology_event_offset_published_on_record_time" - ) - val getPostProcessingEnd: DatabaseMetrics = createDbMetrics("get_post_processing_end") - val initializeLedgerParameters: DatabaseMetrics = createDbMetrics( - "initialize_ledger_parameters" - ) - val lookupConfiguration: DatabaseMetrics = createDbMetrics("lookup_configuration") - - val storePartyEntryDbMetrics: DatabaseMetrics = createDbMetrics( - "store_party_entry" - ) - val loadPartyEntries: DatabaseMetrics = createDbMetrics("load_party_entries") - - val storeTransactionDbMetrics: DatabaseMetrics = createDbMetrics("store_ledger_entry") - - val storeRejectionDbMetrics: DatabaseMetrics = createDbMetrics( - "store_rejection" - ) - val loadParties: DatabaseMetrics = createDbMetrics("load_parties") - val loadAllParties: DatabaseMetrics = createDbMetrics("load_all_parties") - val pruneDbMetrics: DatabaseMetrics = createDbMetrics("prune") - val cleanPruningCandidateContractsDbMetrics: DatabaseMetrics = createDbMetrics( - "prune_clean_contract_candidates" - ) - val pruneContractsDbMetrics: DatabaseMetrics = createDbMetrics("prune_contracts") - val fetchPruningOffsetsMetrics: DatabaseMetrics = createDbMetrics("fetch_pruning_offsets") - val lookupActiveContractsDbMetrics: DatabaseMetrics = createDbMetrics( - "lookup_active_contracts" - ) - val lookupContractByKeyDbMetrics: DatabaseMetrics = createDbMetrics( - "lookup_contract_by_key" - ) - val lookupLastActivationsDbMetrics: DatabaseMetrics = createDbMetrics( - "lookup_last_activations" - ) - - val lookupPointwiseUpdateFetchEventIds: DatabaseMetrics = createDbMetrics( - "fetch_event_ids" - ) - - val getEventsByContractId: DatabaseMetrics = createDbMetrics("get_events_by_contract_id") - val getActiveContracts: DatabaseMetrics = createDbMetrics("get_active_contracts") - val getActiveContractIdRanges: DatabaseMetrics = createDbMetrics( - "get_active_contract_id_ranges" - ) - val getFilteredActiveContractIds: DatabaseMetrics = createDbMetrics( - "get_filtered_active_contract_ids" - ) - val getAchsIdRanges: DatabaseMetrics = createDbMetrics( - "get_achs_id_ranges" - ) - val getAchsFilteredIds: DatabaseMetrics = createDbMetrics( - "get_achs_filtered_ids" - ) - val getActiveContractBatch: DatabaseMetrics = createDbMetrics( - "get_active_contract_batch" - ) - val getEventSeqIdRange: DatabaseMetrics = createDbMetrics("get_event_sequential_id_range") - val getAcsEventSeqIdRange: DatabaseMetrics = - createDbMetrics("get_acs_event_sequential_id_range") - val loadStringInterningEntries: DatabaseMetrics = createDbMetrics( - "load_string_interning_entries" - ) - - val getAssingIdsForOffsets: DatabaseMetrics = createDbMetrics( - "get_assign_ids_for_offsets" - ) - val getUnassingIdsForOffsets: DatabaseMetrics = createDbMetrics( - "get_unassign_ids_for_offsets" - ) - - val firstSynchronizerOffsetAfterOrAt: DatabaseMetrics = createDbMetrics( - "first_synchronizer_offset_after_or_at" - ) - val lastSynchronizerOffsetBeforeOrAt: DatabaseMetrics = createDbMetrics( - "last_synchronizer_offset_before_or_at" - ) - val synchronizerOffset: DatabaseMetrics = createDbMetrics("synchronizer_offset") - val firstSynchronizerOffsetAfterOrAtPublicationTime: DatabaseMetrics = createDbMetrics( - "first_synchronizer_offset_after_or_at_publication_time" - ) - val lastSynchronizerOffsetBeforeOrAtPublicationTime: DatabaseMetrics = createDbMetrics( - "last_synchronizer_offset_before_or_at_publication_time" - ) - val lastSynchronizerOffsetBeforeOrAtRecordTime: DatabaseMetrics = createDbMetrics( - "last_synchronizer_offset_before_or_at_record_time" - ) - - val lastRecordTimeBeforeOrAtSynchronizerOffset: DatabaseMetrics = createDbMetrics( - "last_record_time_before_or_at_synchronizer_offset" - ) - - object translation { - val getLfPackage: Timer = openTelemetryMetricsFactory.timer(inventory.getLfPackage.info) - } - - object compression { - - val createArgumentCompressed: Histogram = - openTelemetryMetricsFactory.histogram(inventory.createArgumentCompressed.info) - - val createArgumentUncompressed: Histogram = - openTelemetryMetricsFactory.histogram(inventory.createArgumentCompressed.info) - - val createKeyValueCompressed: Histogram = - openTelemetryMetricsFactory.histogram(inventory.createKeyValueCompressed.info) - - val createKeyValueUncompressed: Histogram = - openTelemetryMetricsFactory.histogram(inventory.createKeyValueUncompressed.info) - - val exerciseArgumentCompressed: Histogram = - openTelemetryMetricsFactory.histogram(inventory.exerciseArgumentCompressed.info) - - val exerciseArgumentUncompressed: Histogram = - openTelemetryMetricsFactory.histogram(inventory.exerciseArgumentUncompressed.info) - - val exerciseResultCompressed: Histogram = - openTelemetryMetricsFactory.histogram(inventory.exerciseResultCompressed.info) - - val exerciseResultUncompressed: Histogram = - openTelemetryMetricsFactory.histogram(inventory.exerciseResultUncompressed.info) - } - - object threadpool { - private val prefix: MetricName = MainIndexDBMetrics.this.prefix :+ "threadpool" - - val connection: MetricName = prefix :+ "connection" - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexMetrics.scala deleted file mode 100644 index 88737b0c95..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexMetrics.scala +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.HistogramInventory.Item -import com.daml.metrics.api.MetricHandle.{Counter, Gauge, LabeledMetricsFactory, Timer} -import com.daml.metrics.api.{ - HistogramInventory, - MetricInfo, - MetricName, - MetricQualification, - MetricsContext, -} - -class IndexHistograms(val prefix: MetricName)(implicit - inventory: HistogramInventory -) { - - private val lfValuePrefix = prefix :+ "lf_value" - private[metrics] val db = new IndexDBHistograms(prefix :+ "db") - - private[metrics] val computeInterfaceView: Item = Item( - lfValuePrefix :+ "compute_interface_view", - summary = "The time to compute an interface view while serving transaction streams.", - description = """Transaction API allows clients to request events by interface-id. When an - |event matches the interface - an interface view is computed, which adds to - |the latency. This metric represents the time for each such computation.""", - qualification = MetricQualification.Debug, - ) -} - -class IndexMetrics( - inventory: IndexHistograms, - openTelemetryMetricsFactory: LabeledMetricsFactory, -) { - - import MetricsContext.Implicits.empty - private val prefix = inventory.prefix - - val transactionTreesBufferSize: Counter = - openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "transaction_trees_buffer_size", - summary = "The buffer size for transaction trees requests.", - description = - """An Pekko stream buffer is added at the end of all streaming queries, allowing - |to absorb temporary downstream backpressure (e.g. when the client is - |slower than upstream delivery throughput). This metric gauges the - |size of the buffer for queries requesting transaction trees.""", - qualification = MetricQualification.Debug, - ) - ) - - val updatesBufferSize: Counter = - openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "updates_buffer_size", - summary = "The buffer size for streaming updates requests.", - description = - """An Pekko stream buffer is added at the end of all streaming queries, allowing - |to absorb temporary downstream backpressure (e.g. when the client is - |slower than upstream delivery throughput). This metric gauges the - |size of the buffer for queries requesting updates in a specific - |period of time that satisfy a given predicate.""", - qualification = MetricQualification.Debug, - ) - ) - - val activeContractsBufferSize: Counter = - openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "active_contracts_buffer_size", - summary = "The buffer size for active contracts requests.", - description = - """An Pekko stream buffer is added at the end of all streaming queries, allowing - |to absorb temporary downstream backpressure (e.g. when the client is - |slower than upstream delivery throughput). This metric gauges the - |size of the buffer for queries requesting active contracts that transactions - |satisfying a given predicate.""", - qualification = MetricQualification.Debug, - ) - ) - - val completionsBufferSize: Counter = - openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "completions_buffer_size", - summary = "The buffer size for completions requests.", - description = - """An Pekko stream buffer is added at the end of all streaming queries, allowing - |to absorb temporary downstream backpressure (e.g. when the client is - |slower than upstream delivery throughput). This metric gauges the - |size of the buffer for queries requesting the completed commands in a specific - |period of time.""", - qualification = MetricQualification.Debug, - ) - ) - - val db = new IndexDBMetrics(inventory.db, openTelemetryMetricsFactory) - - val achsMidstreamFallbacks: Counter = - openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "achs_midstream_fallbacks", - summary = "The number of mid-stream fallbacks from ACHS to filter tables.", - description = """Counts the number of times the active contracts stream fell back from the - |ACHS to the filter tables because the ACHS validAt was bumped past the - |requested activeAt while streaming was in progress.""", - qualification = MetricQualification.Debug, - ) - ) - - val achsSkips: Counter = - openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "achs_skips", - summary = "The number of times the ACHS was skipped entirely.", - description = """Counts the number of times the active contracts stream skipped the ACHS - |entirely because the ACHS validAt had already surpassed the requested - |activeAtEventSeqId before streaming started.""", - qualification = MetricQualification.Debug, - ) - ) - - val ledgerEndSequentialId: Gauge[Long] = - openTelemetryMetricsFactory.gauge( - MetricInfo( - prefix :+ "ledger_end_sequential_id", - summary = "The sequential id of the current ledger end kept in memory.", - description = """The ledger end's sequential id is a monotonically increasing integer value - |representing the sequential id ascribed to the most recent ledger event - |ingested by the index db. Please note, that only a subset of all ledger events - |are ingested and given a sequential id. These are: creates, consuming - |exercises, non-consuming exercises and divulgence events. This value can - |be treated as a counter of all such events visible to a given participant. - |This metric exposes the latest ledger end's sequential id registered in the - |in-memory data set.""", - qualification = MetricQualification.Debug, - ), - 0L, - ) - - object lfValue { - - val computeInterfaceView: Timer = - openTelemetryMetricsFactory.timer(inventory.computeInterfaceView.info) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexerMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexerMetrics.scala deleted file mode 100644 index b54d13e3e1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/IndexerMetrics.scala +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.DatabaseMetrics -import com.daml.metrics.api.HistogramInventory.Item -import com.daml.metrics.api.MetricHandle.{Counter, Gauge, Histogram, LabeledMetricsFactory, Timer} -import com.daml.metrics.api.{ - HistogramInventory, - MetricHandle, - MetricInfo, - MetricName, - MetricQualification, - MetricsContext, -} - -class IndexerHistograms(val prefix: MetricName)(implicit - inventory: HistogramInventory -) { - - private[metrics] val inputMappingPrefix: MetricName = prefix :+ "inputmapping" - - private[metrics] val inputMappingBatchSize: Item = Item( - inputMappingPrefix :+ "batch_size", - summary = "The batch sizes in the indexer.", - description = """The number of state updates contained in a batch used in the indexer for - |database submission.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val inputMappingBatchWeight: Item = Item( - inputMappingPrefix :+ "batch_weight", - summary = "The batch weights in the indexer.", - description = - """The calculated weights of state updates contained in a batch used in the indexer for - |database submission, if useWeightedBatching was specified.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val seqMappingDuration: Item = Item( - prefix :+ "seqmapping" :+ "duration", - summary = "The duration of the seq-mapping stage.", - description = """The time that a batch of updates spends in the seq-mapping stage of the - |indexer.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val ingestionBlockeByPruningDuration: Item = Item( - prefix :+ "ingestion_blocked_by_pruning" :+ "duration", - summary = "The duration of ingestions DB execution is blocked by pruning.", - description = - "The time that a batch of updates spends in blocked waiting for the pruning DB operation to finish.", - qualification = MetricQualification.Debug, - ) - - private[metrics] val deactivationDistances: Item = Item( - prefix :+ "deactivation_distances", - summary = "Event sequence id distances between activations and deactivations.", - description = "Histogram to collect the statistics of how long individual contracts lived.", - qualification = MetricQualification.Debug, - ) -} - -class IndexerMetrics( - histograms: IndexerHistograms, - factory: LabeledMetricsFactory, -) { - - import MetricsContext.Implicits.empty - - private val prefix = histograms.prefix - - val initialization = new DatabaseMetrics(prefix :+ "initialization", factory) - - // Number of state updates persisted to the database - // (after the effect of the corresponding Update is persisted into the database, - // and before this effect is visible via moving the ledger end forward) - val updates: Counter = factory.counter( - MetricInfo( - prefix :+ "updates", - summary = "The number of the state updates persisted to the database.", - description = """The number of the state updates persisted to the database. There are - |updates such as accepted transactions, configuration changes, - |party allocations, rejections, etc, but they also include synthetic events - |when the node learned about the sequencer clock advancing without any actual - |ledger event such as due to submission receipts or time proofs.""", - qualification = MetricQualification.Traffic, - ) - ) - - val deactivationDistances: Histogram = factory.histogram(histograms.deactivationDistances.info) - - val outputBatchedBufferLength: Counter = - factory.counter( - MetricInfo( - prefix :+ "output_batched_buffer_length", - summary = - "The size of the queue between the indexer and the in-memory state updating flow.", - description = - """This counter counts batches of updates passed to the in-memory flow. Batches - |are dynamically-sized based on amount of backpressure exerted by the - |downstream stages of the flow.""", - qualification = MetricQualification.Debug, - ) - ) - - // Number of times the Indexer needed to restart due to missing referenced contracts (likely because of pruning) - val indexerRestartDueToMissingReferencedContracts: Counter = factory.counter( - MetricInfo( - prefix :+ "indexer_restart_due_to_missing_contract", - summary = - "Number of times the Indexer needed to restart due to missing referenced contracts.", - description = """Under seldom circumstances the indexer could be forced to restart if pruning removed - |referenced contracts. If this happens the missing contracts will be re-inserted to the DB - |and indexing continues. This is part for the normal operation and should happen very rarely.""", - qualification = MetricQualification.Traffic, - ) - ) - - val ingestionBlockeByPruningDuration: Timer = - factory.timer(histograms.ingestionBlockeByPruningDuration.info) - - // Input mapping stage - // Translating state updates to data objects corresponding to individual SQL insert statements - object inputMapping { - - // Bundle of metrics coming from instrumentation of the underlying thread-pool - val executor: MetricName = histograms.inputMappingPrefix :+ "executor" - - val batchSize: Histogram = - factory.histogram(histograms.inputMappingBatchSize.info) - - val batchWeight: Histogram = - factory.histogram(histograms.inputMappingBatchWeight.info) - - val submissionBatchConfiguredWeight: Gauge[Long] = - factory.gauge( - MetricInfo( - prefix :+ "submission_batch_configured_weight", - summary = "The configured weight of each submission batch.", - description = - """This value is calculated from `submissionBatchInsertionSize` of the indexer config by multiplying - |the default weight of one insert operation.""".stripMargin, - qualification = MetricQualification.Debug, - ), - 0L, - ) - - } - - // Batching stage - // Translating batch data objects to db-specific DTO batches - object batching { - private val prefix: MetricName = IndexerMetrics.this.prefix :+ "batching" - - // Bundle of metrics coming from instrumentation of the underlying thread-pool - val executor: MetricName = prefix :+ "executor" - } - - // Sequence Mapping stage - object seqMapping { - - val duration: Timer = factory.timer(histograms.seqMappingDuration.info) - } - - // Ingestion stage - // Parallel ingestion of prepared data into the database - val ingestion = new DatabaseMetrics(prefix :+ "ingestion", factory) - - // Tail ingestion stage - // The throttled update of ledger end parameters - val tailIngestion = new DatabaseMetrics(prefix :+ "tail_ingestion", factory) - - // Post Processing end ingestion stage - // The throttled update of post processing end parameter - val postProcessingEndIngestion = - new DatabaseMetrics(prefix :+ "post_processing_end_ingestion", factory) - - val achsProcessing = - new DatabaseMetrics(prefix :+ "achs_processing", factory) - - val achsBufferLength: Counter = - factory.counter( - MetricInfo( - prefix :+ "achs_buffer_length", - summary = "The size of the queue between the indexer and the ACHS maintenance pipe.", - description = - """This counter counts batches of updates queued before the ACHS maintenance pipe. - |When the buffer is mostly full, it indicates that ACHS maintenance is creating - |backpressure on the indexing pipeline.""", - qualification = MetricQualification.Debug, - ) - ) - - val achsValidAt: Gauge[Long] = - factory.gauge( - MetricInfo( - prefix :+ "achs_valid_at", - summary = "The event sequential id at which the ACHS is valid.", - description = - """The event sequential id at which the ACHS is currently valid. It may contain some - |deactivated events but they will anyway be removed when fetched.""", - qualification = MetricQualification.Debug, - ), - 0L, - ) - - val achsLastPopulated: Gauge[Long] = - factory.gauge( - MetricInfo( - prefix :+ "achs_last_populated", - summary = "The last event sequential id populated into the ACHS.", - description = - """The last event sequential id for which activations were added to the ACHS.""", - qualification = MetricQualification.Debug, - ), - 0L, - ) - - val achsLastRemoved: Gauge[Long] = - factory.gauge( - MetricInfo( - prefix :+ "achs_last_removed", - summary = - "The last event sequential id for which deactivations were removed from the ACHS.", - description = """The last event sequential id for which deactivations were looked up and the - |corresponding activations were removed from the ACHS.""", - qualification = MetricQualification.Debug, - ), - 0L, - ) - - val indexerQueueBlocked: MetricHandle.Meter = factory.meter( - MetricInfo( - prefix :+ "indexer_queue_blocked", - summary = "The amount of blocked enqueue operations for the indexer queue.", - description = - """Indexer queue exerts backpressure by blocking asynchronous enqueue operations. - |This meter measures the amount of such blocked operations, signalling backpressure - |materializing from downstream.""", - qualification = MetricQualification.Debug, - ) - ) - - val indexerQueueBuffered: MetricHandle.Meter = factory.meter( - MetricInfo( - prefix :+ "indexer_queue_buffered", - summary = "The size of the buffer before the indexer.", - description = - """This buffer is located before the indexer, increasing amount signals backpressure mounting.""", - qualification = MetricQualification.Debug, - ) - ) - - val indexerQueueUncommitted: MetricHandle.Meter = factory.meter( - MetricInfo( - prefix :+ "indexer_queue_uncommitted", - summary = "The amount of entries which are uncommitted for the indexer.", - description = - """Uncommitted entries contain all blocked, buffered and submitted, but not yet committed entries. - |This amount signals the momentum of stream processing, and has a theoretical maximum defined by all - |the queue perameters.""".stripMargin, - qualification = MetricQualification.Debug, - ) - ) - - val ledgerEndSequentialId: Gauge[Long] = - factory.gauge( - MetricInfo( - prefix :+ "ledger_end_sequential_id", - summary = "The sequential id of the current ledger end kept in the database.", - description = """The ledger end's sequential id is a monotonically increasing integer value - |representing the sequential id ascribed to the most recent ledger event - |ingested by the index db. Please note, that only a subset of all ledger events - |are ingested and given a sequential id. These are: creates, consuming - |exercises, non-consuming exercises and divulgence events. This value can be - |treated as a counter of all such events visible to a given participant. This - |metric exposes the latest ledger end's sequential id registered in the - |database.""", - qualification = MetricQualification.Debug, - ), - 0L, - ) - - val meteredEventsMeter: MetricHandle.Meter = factory.meter( - MetricInfo( - prefix :+ "metered_events", - summary = "Number of individual ledger events (create, exercise, archive).", - description = - """Represents the number of individual ledger events constituting a transaction.""", - qualification = MetricQualification.Debug, - labelsWithDescription = Map( - "participant_id" -> "The id of the participant.", - "user_id" -> "The user generating the events.", - ), - ) - ) - - val eventsMeter: MetricHandle.Meter = - factory.meter( - MetricInfo( - prefix :+ "events", - summary = "Number of ledger events processed.", - description = - "Represents the total number of ledger events processed (transactions, reassignments, party allocations).", - qualification = MetricQualification.Debug, - labelsWithDescription = Map( - "participant_id" -> "The id of the participant.", - "user_id" -> "The user generating the events.", - "event_type" -> "The type of ledger event processed (transaction, reassignment, party_allocation).", - "status" -> "Indicates if the event was accepted or not. Possible values accepted|rejected.", - ), - ) - ) -} - -object IndexerMetrics { - - object Labels { - val userId = "user_id" - val grpcCode = "grpc_code" - object eventType { - - val key = "event_type" - - val partyAllocation = "party_allocation" - val transaction = "transaction" - val reassignment = "reassignment" - val topologyTransaction = "topology_transaction" - } - - object status { - val key = "status" - - val accepted = "accepted" - val rejected = "rejected" - } - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/LAPIMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/LAPIMetrics.scala deleted file mode 100644 index 59a0271836..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/LAPIMetrics.scala +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.MetricHandle.{Counter, LabeledMetricsFactory} -import com.daml.metrics.api.{MetricInfo, MetricName, MetricQualification, MetricsContext} - -class LAPIMetrics private[metrics] ( - val prefix: MetricName, - val metricsFactory: LabeledMetricsFactory, -) { - - import MetricsContext.Implicits.empty - - // Private constructor to avoid being instantiated multiple times by accident - final class ThreadpoolMetrics private[LAPIMetrics] { - private val prefix: MetricName = LAPIMetrics.this.prefix :+ "threadpool" - - val apiQueryServices: MetricName = prefix :+ "api_query_services" - - // Private constructor to avoid being instantiated multiple times by accident - final class IndexBypassMetrics private[ThreadpoolMetrics] { - private val prefix: MetricName = ThreadpoolMetrics.this.prefix :+ "index_bypass" - val prepareUpdates: MetricName = prefix :+ "prepare_updates" - val updateInMemoryState: MetricName = prefix :+ "update_in_memory_state" - } - val indexBypass = new IndexBypassMetrics - } - - val threadpool: ThreadpoolMetrics = new ThreadpoolMetrics - - // Private constructor to avoid being instantiated multiple times by accident - final class StreamsMetrics private[LAPIMetrics] { - private val prefix: MetricName = LAPIMetrics.this.prefix :+ "streams" - - val transactionTrees: Counter = metricsFactory.counter( - MetricInfo( - prefix :+ "transaction_trees_sent", - summary = "The number of the transaction trees sent over the ledger api.", - description = """The total number of the transaction trees sent over the ledger api streams - |to all clients.""", - qualification = MetricQualification.Traffic, - ) - ) - - val updates: Counter = metricsFactory.counter( - MetricInfo( - prefix :+ "updates_sent", - summary = "The number of the flat updates sent over the ledger api.", - description = """The total number of the flat updates sent over the ledger api streams to - |all clients.""", - qualification = MetricQualification.Traffic, - ) - ) - - val completions: Counter = metricsFactory.counter( - MetricInfo( - prefix :+ "completions_sent", - summary = "The number of the command completions sent by the ledger api.", - description = """The total number of completions sent over the ledger api streams to all - |clients.""", - qualification = MetricQualification.Traffic, - ) - ) - - val acs: Counter = metricsFactory.counter( - MetricInfo( - prefix :+ "acs_sent", - summary = "The number of the active contracts sent by the ledger api.", - description = - """The total number of active contracts sent over the ledger api streams to all - |clients.""", - qualification = MetricQualification.Traffic, - ) - ) - - } - - val streams: StreamsMetrics = new StreamsMetrics -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/PartyRecordStoreMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/PartyRecordStoreMetrics.scala deleted file mode 100644 index 8386dff218..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/PartyRecordStoreMetrics.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.DatabaseMetrics -import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory -import com.daml.metrics.api.MetricName - -// Private constructor to avoid being instantiated multiple times by accident -class PartyRecordStoreMetrics private[metrics] ( - prefix: MetricName, - labeledMetricsFactory: LabeledMetricsFactory, -) extends DatabaseMetricsFactory(prefix, labeledMetricsFactory) { - - val getPartyRecord: DatabaseMetrics = createDbMetrics("get_party_record") - val partiesExist: DatabaseMetrics = createDbMetrics("parties_exist") - val createPartyRecord: DatabaseMetrics = createDbMetrics("create_party_record") - val updatePartyRecord: DatabaseMetrics = createDbMetrics("update_party_record") - val updatePartyRecordIdp: DatabaseMetrics = createDbMetrics("update_party_record_idp") - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/PruningMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/PruningMetrics.scala deleted file mode 100644 index ace8bb4db8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/PruningMetrics.scala +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory -import com.daml.metrics.api.{MetricHandle, MetricInfo, MetricName, MetricQualification} - -// Private constructor to avoid being instantiated multiple times by accident -class PruningMetrics private[metrics] (prefix: MetricName, factory: LabeledMetricsFactory) { - import com.daml.metrics.api.MetricsContext.Implicits.empty - - // Using a meter, which can keep track of how many times the operation was executed, even if the - // operation is fully executed between 2 metric fetches by the monitoring. - // With a (boolean) gauge, there is a large risk that some operation invocation would be missed. - val pruneCommandStarted: MetricHandle.Meter = - factory.meter( - MetricInfo( - prefix :+ "prune" :+ "started", - "Total number of started pruning processes.", - MetricQualification.Debug, - ) - ) - val pruneCommandCompleted: MetricHandle.Meter = - factory.meter( - MetricInfo( - prefix :+ "prune" :+ "completed", - "Total number of completed pruning processes.", - MetricQualification.Debug, - ) - ) - val contractPruningRetried: MetricHandle.Meter = - factory.meter( - MetricInfo( - prefix :+ "contract_pruning_retried", - """Optimistic locking with contention might result in retries. This metric tracks if the operation was retried this many times.""", - MetricQualification.Debug, - ) - ) - val contractPruningBlocked: MetricHandle.Counter = - factory.counter( - MetricInfo( - prefix :+ "contract_pruning_blocked", - """Optimistic locking with contention might result in retries. This metric tracks if maximum amount of configured retries reached.""", - MetricQualification.Debug, - ) - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ServicesMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ServicesMetrics.scala deleted file mode 100644 index 8fa610ebd9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/ServicesMetrics.scala +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.HistogramInventory.Item -import com.daml.metrics.api.MetricHandle.* -import com.daml.metrics.api.{ - HistogramInventory, - MetricInfo, - MetricName, - MetricQualification, - MetricsContext, -} - -private[metrics] final class ServicesHistograms(val prefix: MetricName)(implicit - inventory: HistogramInventory -) { - - private[metrics] val indexPrefix: MetricName = prefix :+ "index" - private[metrics] val fanoutPrefix: MetricName = indexPrefix :+ "in_memory_fan_out_buffer" - - private[metrics] val fanoutBufferSize: Item = - Item( - fanoutPrefix :+ "size", - summary = "The size of the in-memory fan-out buffer.", - description = """The actual size of the in-memory fan-out buffer. This metric is mostly - |targeted for debugging purposes.""", - qualification = MetricQualification.Saturation, - ) - private[metrics] val fanoutPush: Item = - Item( - fanoutPrefix :+ "push", - summary = "The time to add a new event into the buffer.", - description = """The in-memory fan-out buffer is a buffer that stores the last ingested - |maxBufferSize accepted and rejected submission updates as - |TransactionLogUpdate. It allows bypassing IndexDB persistence fetches for - |recent updates for flat and transaction tree streams, command completion - |streams and by-event-id and by-transaction-id flat and transaction tree - |lookups. This metric exposes the time spent on adding a new event into the - |buffer.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val fanoutPruning: Item = - Item( - fanoutPrefix :+ "prune", - summary = "The time to remove all elements from the in-memory fan-out buffer.", - description = """It is possible to remove the oldest entries of the in-memory fan out - |buffer. This metric exposes the time needed to prune the buffer.""", - qualification = MetricQualification.Debug, - ) - - private val baseInfo = MetricInfo( - prefix, - summary = "The time to execute an index service operation.", - description = """The index service is an internal component responsible for access to the - |index db data. Its operations are invoked whenever a client request received - |over the ledger api requires access to the index db. This metric captures - |time statistics of such operations.""", - qualification = MetricQualification.Debug, - ) - private def extend(name: String, template: MetricInfo): Item = { - val info = template.extend(name) - val item = new Item( - info.name, - info.summary, - info.qualification, - info.description, - info.labelsWithDescription, - ) - inventory.register(item) - item - } - - private[metrics] val listLfPackages: Item = extend("list_lf_packages", baseInfo) - private[metrics] val getLfArchive: Item = extend("get_lf_archive", baseInfo) - private[metrics] val currentLedgerEnd: Item = extend("current_ledger_end", baseInfo) - private[metrics] val latestPrunedOffsets: Item = extend("latest_pruned_offsets", baseInfo) - private[metrics] val getCompletions: Item = extend("get_completions", baseInfo) - private[metrics] val transactions: Item = extend("transactions", baseInfo) - private[metrics] val transactionTrees: Item = extend("transaction_trees", baseInfo) - private[metrics] val getUpdateByOffset: Item = extend("get_update_by_offset", baseInfo) - private[metrics] val getUpdateById: Item = extend("get_update_by_id", baseInfo) - private[metrics] val getUpdatesPage: Item = extend("get_updates_page", baseInfo) - private[metrics] val getActiveContracts: Item = extend("get_active_contracts", baseInfo) - private[metrics] val lookupActiveContract: Item = extend("lookup_active_contract", baseInfo) - private[metrics] val lookupContractState: Item = extend("lookup_contract_state", baseInfo) - private[metrics] val lookupContractKey: Item = extend("lookup_contract_key", baseInfo) - private[metrics] val lookupNonUniqueContractKey: Item = - extend("lookup_non_unique_contract_key", baseInfo) - private[metrics] val getEventsByContractId: Item = extend("get_events_by_contract_id", baseInfo) - private[metrics] val lookupMaximumLedgerTime: Item = - extend("lookup_maximum_ledger_time", baseInfo) - private[metrics] val getParticipantId: Item = extend("get_participant_id", baseInfo) - private[metrics] val getParties: Item = extend("get_parties", baseInfo) - private[metrics] val listKnownParties: Item = extend("list_known_parties", baseInfo) - private[metrics] val partyEntries: Item = extend("party_entries", baseInfo) - private[metrics] val lookupConfiguration: Item = extend("lookup_configuration", baseInfo) - private[metrics] val prune: Item = extend("prune", baseInfo) - - private[metrics] val bufferedReaderPrefix: MetricName = indexPrefix :+ "buffer_reader" - - private[metrics] val bufferedReaderFetchTimer: Item = Item( - bufferedReaderPrefix :+ "fetch", - summary = "The time needed to fetch an event (either from the buffer or the persistence).", - description = """The buffer reader serves stream events in chunks downstream to the gRPC - |layer and to the client. This metric times the duration needed for serving - |a chunk of events. This metric has been created with debugging purposes - |in mind.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val bufferedReaderConversion: Item = Item( - bufferedReaderPrefix :+ "conversion", - summary = "The time to convert a buffered fetched event to a ledger api stream response.", - description = """Entries are stored in the buffer in a custom deserialized representation. - |When served to the gRPC layer, the entries are processed and serialized. - |This metric times this operation.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val bufferedReaderSlice: Item = Item( - bufferedReaderPrefix :+ "slice", - summary = "The time to fetch a chunk of events from the buffer", - description = """The events are served from the buffer in chunks, respecting the input - |bounds and a predicate filter. This metric times this operation.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val bufferedReaderSliceSize: Item = Item( - bufferedReaderPrefix :+ "slice_size", - summary = "The size of the slice requested.", - description = """The events are served from the buffer in chunks. This metric gauges the - |chunk size delivered downstream.""", - qualification = MetricQualification.Debug, - ) - - private val readBaseInfo = MetricInfo( - prefix :+ "read", - summary = "The time to execute a read service operation.", - description = """The read service is an internal interface for reading the events from the - |synchronization interfaces. The metrics expose the time needed to execute - |each operation.""", - qualification = MetricQualification.Debug, - ) - private[metrics] val readStateUpdates: Item = extend("state_updates", readBaseInfo) - - private[metrics] val readGetConnectedSynchronizers: Item = - extend("get_connected_synchronizers", readBaseInfo) - - private[metrics] val readIncompleteReassignmentOffsets: Item = - extend("incomplete_reassignment_offsets", readBaseInfo) - - private[metrics] val readListLfPackages: Item = extend("list_lf_packages", readBaseInfo) - private[metrics] val readGetLfArchive: Item = extend("get_lf_archive", readBaseInfo) - private[metrics] val readValidateDar: Item = extend("validate_dar", readBaseInfo) - private[metrics] val readListVettedPackages: Item = extend("list_vetted_packages", readBaseInfo) - private[metrics] val computePartyVettingMap: Item = - extend("compute_party_vetting_map", readBaseInfo) - private[metrics] val computeHighestRankedSynchronizerFromAdmissible: Item = - extend("compute_highest_ranked_synchronizer_from_admissible", readBaseInfo) - private[metrics] val selectRoutingSynchronizer: Item = - extend("select_routing_synchronizer", readBaseInfo) - - private[metrics] val writeBaseInfo = MetricInfo( - indexPrefix :+ "write", - summary = "The time to execute a write service operation.", - description = """The write service is an internal interface for changing the state through - |the synchronization services. The methods in this interface are all methods - |that are supported uniformly across all ledger implementations. This metric - |exposes the time needed to execute each operation.""", - qualification = MetricQualification.Debug, - ) - - private[metrics] val writeSubmitTransaction: Item = extend("submit_transaction", writeBaseInfo) - - private[metrics] val writeSubmitReassignment: Item = extend("submit_reassignment", writeBaseInfo) - - private[metrics] val writeUploadPackages: Item = extend("upload_packages", writeBaseInfo) - - private[metrics] val writeAllocateParty: Item = extend("allocate_party", writeBaseInfo) - - private[metrics] val writePrune: Item = extend("prune", writeBaseInfo) - - private[metrics] val writeUpdateVettedPackages: Item = - extend("update_vetted_packages", writeBaseInfo) - -} - -// Private constructor to avoid being instantiated multiple times by accident -final class ServicesMetrics private[metrics] ( - inventory: ServicesHistograms, - openTelemetryMetricsFactory: LabeledMetricsFactory, -) { - - private val prefix = inventory.prefix - private implicit val metricsContext: MetricsContext = MetricsContext.Empty - - // Private constructor to avoid being instantiated multiple times by accident - final class IndexMetrics private[ServicesMetrics] { - private val prefix = inventory.indexPrefix - - val listLfPackages: Timer = openTelemetryMetricsFactory.timer(inventory.listLfPackages.info) - val getLfArchive: Timer = openTelemetryMetricsFactory.timer(inventory.getLfArchive.info) - val currentLedgerEnd: Timer = openTelemetryMetricsFactory.timer(inventory.currentLedgerEnd.info) - val latestPrunedOffsets: Timer = - openTelemetryMetricsFactory.timer(inventory.latestPrunedOffsets.info) - val getCompletions: Timer = openTelemetryMetricsFactory.timer(inventory.getCompletions.info) - val transactions: Timer = openTelemetryMetricsFactory.timer(inventory.transactions.info) - val transactionTrees: Timer = openTelemetryMetricsFactory.timer(inventory.transactionTrees.info) - val getUpdateByOffset: Timer = - openTelemetryMetricsFactory.timer(inventory.getUpdateByOffset.info) - val getUpdateById: Timer = - openTelemetryMetricsFactory.timer(inventory.getUpdateById.info) - val getUpdatesPage: Timer = openTelemetryMetricsFactory.timer(inventory.getUpdatesPage.info) - val getActiveContracts: Timer = - openTelemetryMetricsFactory.timer(inventory.getActiveContracts.info) - val lookupActiveContract: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupActiveContract.info) - - val lookupContractState: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupContractState.info) - - val lookupContractKey: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupContractKey.info) - - val lookupNonUniqueContractKey: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupNonUniqueContractKey.info) - - val getEventsByContractId: Timer = - openTelemetryMetricsFactory.timer(inventory.getEventsByContractId.info) - - val lookupMaximumLedgerTime: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupMaximumLedgerTime.info) - - val getParticipantId: Timer = - openTelemetryMetricsFactory.timer(inventory.getParticipantId.info) - - val getParties: Timer = - openTelemetryMetricsFactory.timer(inventory.getParties.info) - - val listKnownParties: Timer = - openTelemetryMetricsFactory.timer(inventory.listKnownParties.info) - - val partyEntries: Timer = - openTelemetryMetricsFactory.timer(inventory.partyEntries.info) - - val lookupConfiguration: Timer = - openTelemetryMetricsFactory.timer(inventory.lookupConfiguration.info) - - val prune: Timer = openTelemetryMetricsFactory.timer(inventory.prune.info) - - // Private constructor to avoid being instantiated multiple times by accident - final class InMemoryFanoutBufferMetrics private[IndexMetrics] { - val prefix: MetricName = inventory.fanoutPrefix - - val push: Timer = openTelemetryMetricsFactory.timer( - inventory.fanoutPush.info - ) - - val prune: Timer = openTelemetryMetricsFactory.timer( - inventory.fanoutPruning.info - ) - - val bufferSize: Histogram = openTelemetryMetricsFactory.histogram( - inventory.fanoutBufferSize.info - ) - } - val inMemoryFanoutBuffer: InMemoryFanoutBufferMetrics = new InMemoryFanoutBufferMetrics - - case class BufferedReader(streamName: String) { - implicit val metricsContext: MetricsContext = MetricsContext("stream" -> streamName) - val prefix: MetricName = index.prefix :+ "buffer_reader" - - val fetchedTotal: Counter = openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "fetched_total", - summary = - "The total number of the events fetched (either from the buffer or the persistence).", - description = """The buffer reader serves processed and filtered events from the buffer, - |with fallback to persistence fetches if the bounds are not within the - |buffer's range bounds. This metric exposes the total number of the fetched - |events.""", - qualification = MetricQualification.Debug, - ) - ) - - val fetchedBuffered: Counter = - openTelemetryMetricsFactory.counter( - MetricInfo( - prefix :+ "fetched_buffered", - summary = "The total number of the events fetched from the buffer.", - description = """The buffer reader serves processed and filtered events from the buffer, - |with fallback to persistence fetches if the bounds are not within the - |buffer's range bounds. This metric counts the number of events delivered - |exclusively from the buffer.""", - qualification = MetricQualification.Debug, - ) - ) - - val fetchTimer: Timer = - openTelemetryMetricsFactory.timer(inventory.bufferedReaderFetchTimer.info) - - val conversion: Timer = - openTelemetryMetricsFactory.timer(inventory.bufferedReaderConversion.info) - - val slice: Timer = openTelemetryMetricsFactory.timer(inventory.bufferedReaderSlice.info) - - val sliceSize: Histogram = - openTelemetryMetricsFactory.histogram(inventory.bufferedReaderSliceSize.info) - } - } - val index = new IndexMetrics - - // Private constructor to avoid being instantiated multiple times by accident - final class ReadMetrics private[ServicesMetrics] { - - val stateUpdates: Timer = openTelemetryMetricsFactory.timer(inventory.readStateUpdates.info) - - val getConnectedSynchronizers: Timer = - openTelemetryMetricsFactory.timer(inventory.readGetConnectedSynchronizers.info) - - val incompleteReassignmentOffsets: Timer = - openTelemetryMetricsFactory.timer(inventory.readIncompleteReassignmentOffsets.info) - - val listLfPackages: Timer = openTelemetryMetricsFactory.timer(inventory.readListLfPackages.info) - val getLfArchive: Timer = openTelemetryMetricsFactory.timer(inventory.readGetLfArchive.info) - val validateDar: Timer = openTelemetryMetricsFactory.timer(inventory.readValidateDar.info) - val listVettedPackages: Timer = - openTelemetryMetricsFactory.timer(inventory.readListVettedPackages.info) - - val computePartyVettingMap: Timer = - openTelemetryMetricsFactory.timer(inventory.computePartyVettingMap.info) - val computeHighestRankedSynchronizerFromAdmissible: Timer = - openTelemetryMetricsFactory.timer( - inventory.computeHighestRankedSynchronizerFromAdmissible.info - ) - val selectRoutingSynchronizer: Timer = - openTelemetryMetricsFactory.timer(inventory.selectRoutingSynchronizer.info) - } - - val read: ReadMetrics = new ReadMetrics - - // Private constructor to avoid being instantiated multiple times by accident - final class WriteMetrics private[ServicesMetrics] { - - val submitTransaction: Timer = - openTelemetryMetricsFactory.timer(inventory.writeSubmitTransaction.info) - - val submitTransactionRunning: Counter = - openTelemetryMetricsFactory.counter( - inventory.writeBaseInfo.extend("submit_transaction_running") - ) - - val submitReassignment: Timer = - openTelemetryMetricsFactory.timer(inventory.writeSubmitReassignment.info) - - val submitReassignmentRunning: Counter = - openTelemetryMetricsFactory.counter( - inventory.writeBaseInfo.extend("submit_reassignment_running") - ) - - val uploadPackages: Timer = - openTelemetryMetricsFactory.timer(inventory.writeUploadPackages.info) - - val allocateParty: Timer = openTelemetryMetricsFactory.timer(inventory.writeAllocateParty.info) - - val prune: Timer = openTelemetryMetricsFactory.timer(inventory.writePrune.info) - - val updateVettedPackages: Timer = - openTelemetryMetricsFactory.timer(inventory.writeUpdateVettedPackages.info) - } - - val write: WriteMetrics = new WriteMetrics - - val pruning = new PruningMetrics(prefix :+ "pruning", openTelemetryMetricsFactory) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/UserManagementMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/UserManagementMetrics.scala deleted file mode 100644 index 682f3b3ae9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/metrics/UserManagementMetrics.scala +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory -import com.daml.metrics.api.MetricName -import com.daml.metrics.{CacheMetrics, DatabaseMetrics} - -// Private constructor to avoid being instantiated multiple times by accident -class UserManagementMetrics private[metrics] ( - prefix: MetricName, - labeledFactory: LabeledMetricsFactory, -) extends DatabaseMetricsFactory(prefix, labeledFactory) { - - val cache = new CacheMetrics(prefix :+ "cache", labeledFactory) - - val getUserInfo: DatabaseMetrics = createDbMetrics("get_user_info") - val createUser: DatabaseMetrics = createDbMetrics("create_user") - val deleteUser: DatabaseMetrics = createDbMetrics("delete_user") - val updateUser: DatabaseMetrics = createDbMetrics("update_user") - val updateUserIdp: DatabaseMetrics = createDbMetrics("update_user_idp") - val grantRights: DatabaseMetrics = createDbMetrics("grant_rights") - val revokeRights: DatabaseMetrics = createDbMetrics("revoke_rights") - val listUsers: DatabaseMetrics = createDbMetrics("list_users") - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/AcsChange.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/AcsChange.scala deleted file mode 100644 index 7411c51f4b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/AcsChange.scala +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.logging.{HasLoggerName, NamedLoggingContext} -import com.digitalasset.canton.protocol.LfContractId -import com.digitalasset.canton.{InternedPartyId, LfPartyId, ReassignmentCounter} - -/** Represents a change to the ACS. The contracts are accompanied by their stakeholders. - * - * Note that we include the LfContractId (for uniqueness), but we do not include the contract hash - * because the contract id already authenticates the contract contents. - */ - -trait GenericAcsChange[T] { - def activations: Map[LfContractId, GenericContractStakeholdersAndReassignmentCounter[T]] - def deactivations: Map[LfContractId, GenericContractStakeholdersAndReassignmentCounter[T]] -} - -final case class AcsChange( - activations: Map[LfContractId, ContractStakeholdersAndReassignmentCounter], - deactivations: Map[LfContractId, ContractStakeholdersAndReassignmentCounter], -) extends GenericAcsChange[LfPartyId] - with PrettyPrinting { - override protected def pretty: Pretty[AcsChange] = prettyOfClass( - param("activations", _.activations), - param("deactivations", _.deactivations), - ) -} - -final case class InternalizedAcsChange( - activations: Map[LfContractId, InternalizedContractStakeholdersAndReassignmentCounter], - deactivations: Map[LfContractId, InternalizedContractStakeholdersAndReassignmentCounter], -) extends GenericAcsChange[InternedPartyId] - -trait GenericContractStakeholdersAndReassignmentCounter[T] { - def stakeholders: Set[T] - def reassignmentCounter: ReassignmentCounter -} - -final case class ContractStakeholdersAndReassignmentCounter( - stakeholders: Set[LfPartyId], - reassignmentCounter: ReassignmentCounter, -) extends GenericContractStakeholdersAndReassignmentCounter[LfPartyId] - with PrettyPrinting { - override protected def pretty: Pretty[ContractStakeholdersAndReassignmentCounter] = prettyOfClass( - param("stakeholders", _.stakeholders), - param("reassignment counter", _.reassignmentCounter), - ) -} - -final case class InternalizedContractStakeholdersAndReassignmentCounter( - stakeholders: Set[InternedPartyId], - reassignmentCounter: ReassignmentCounter, -) extends GenericContractStakeholdersAndReassignmentCounter[InternedPartyId] - with PrettyPrinting { - override protected def pretty: Pretty[InternalizedContractStakeholdersAndReassignmentCounter] = - prettyOfClass( - param("stakeholders", _.stakeholders), - param("reassignment counter", _.reassignmentCounter), - ) -} - -object AcsChange { - val empty: AcsChange = AcsChange(Map.empty, Map.empty) -} - -/** Staged version of AcsChange, which requires reassignment counters for the archived contracts. - * This AcsChange refers to one single Update, and should not contain transient contracts. - */ -sealed trait AcsChangeFactory { - - /** @return - * The non-transient archivals which for the reassignmentCounterForArchivals is needed - */ - def archivalCids: Set[LfContractId] - - /** Building the final AcsChange. - * - * If a contract in archivalCids is missing in reassignmentCounterForArchivals, it will be - * omitted from the result. - * - * @param reassignmentCounterForArchivals - * reassignment counters for archivalCids - */ - def acsChange( - reassignmentCounterForArchivals: Map[LfContractId, ReassignmentCounter] - )(implicit loggingContext: NamedLoggingContext): AcsChange - - /** Whether contractId is affected by this AcsChange. For transient contracts this returns false. - */ - def contractActivenessChanged(contractId: LfContractId): Boolean -} - -final case class AcsChangeFactoryImpl( - initialAcsChange: AcsChange, - archivalDeactivations: Map[LfContractId, Set[LfPartyId]], -) extends AcsChangeFactory - with HasLoggerName { - // This invariant is used in contractActivenessChanged computation - assert( - !initialAcsChange.activations.keysIterator.exists(archivalDeactivations.contains) && - !initialAcsChange.activations.keysIterator.exists(initialAcsChange.deactivations.contains), - "AcsChange should not include transients", - ) - - override def archivalCids: Set[LfContractId] = archivalDeactivations.keySet - - override def acsChange( - reassignmentCounterForArchivals: Map[LfContractId, ReassignmentCounter] - )(implicit loggingContext: NamedLoggingContext): AcsChange = { - val enrichedArchivalDeactivations - : Map[LfContractId, ContractStakeholdersAndReassignmentCounter] = - archivalDeactivations.collect { - case (contractId, stakeholders) if reassignmentCounterForArchivals.contains(contractId) => - contractId -> ContractStakeholdersAndReassignmentCounter( - stakeholders = stakeholders, - reassignmentCounter = reassignmentCounterForArchivals(contractId), - ) - } - initialAcsChange.copy( - deactivations = initialAcsChange.deactivations ++ enrichedArchivalDeactivations - ) - } - - override def contractActivenessChanged(contractId: LfContractId): Boolean = - initialAcsChange.activations.contains(contractId) || - initialAcsChange.deactivations.contains(contractId) || - archivalDeactivations.contains(contractId) -} - -final case class TestAcsChangeFactory( - contractActivenessChanged: Boolean = true -) extends AcsChangeFactory { - override def archivalCids: Set[LfContractId] = Set.empty - override def acsChange( - reassignmentCounterForArchivals: Map[LfContractId, ReassignmentCounter] - )(implicit loggingContext: NamedLoggingContext): AcsChange = AcsChange.empty - override def contractActivenessChanged(contractId: LfContractId): Boolean = - contractActivenessChanged -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ChangeId.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ChangeId.scala deleted file mode 100644 index 79e129c624..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ChangeId.scala +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref - -/** Identifier for ledger changes used by command deduplication. Equality is defined in terms of the - * cryptographic hash. - * - * @see - * ReadService.stateUpdates for the command deduplication guarantee - */ -final case class ChangeId( - userId: Ref.UserId, - commandId: Ref.CommandId, - actAs: Set[Ref.Party], -) { - - /** A stable hash of the change id. Suitable for storing in persistent storage. - */ - lazy val hash: Hash = - Hash.hashChangeId(userId, commandId, actAs) - - override def equals(that: Any): Boolean = that match { - case other: ChangeId => - if (this eq other) true - else other.canEqual(this) && this.hash == other.hash - case _ => false - } - - override def hashCode(): Int = hash.hashCode() -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/CompletionInfo.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/CompletionInfo.scala deleted file mode 100644 index 12aa5b61c7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/CompletionInfo.scala +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.daml.logging.entries.{LoggingValue, ToLoggingValue} -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.DeduplicationPeriod -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.daml.lf.data.Ref - -/** Information about a completion for a submission. - * - * @param actAs - * the non-empty set of parties that submitted the change. - * @param userId - * an identifier for the user that submitted the command. - * @param commandId - * a submitter-provided identifier to identify an intended ledger change within all the - * submissions by the same parties and application. - * @param optDeduplicationPeriod - * The deduplication period that the [[SyncService]] actually uses for the command submission. It - * may differ from the suggested deduplication period given to [[SyncService.submitTransaction]]. - * - * For example, the suggested deduplication period may have been converted into a different kind or - * extended. The particular choice depends on the particular implementation. - * - * This allows auditing the deduplication guarantee described in the [[Update]]. - * - * Optional as some implementations may not be able to provide this deduplication information. If - * an implementation does not provide this deduplication information, it MUST adhere to the - * deduplication guarantee under a sensible interpretation of the corresponding - * [[CompletionInfo.optDeduplicationPeriod]]. - * @param submissionId - * An identifier for the submission that allows an application to correlate completions to its - * submissions. - * - * Optional as entries created by the participant.state.v1 API do not have this filled. Only set - * for participant.state.v2 created entries - */ -final case class CompletionInfo( - actAs: List[Ref.Party], - userId: Ref.UserId, - commandId: Ref.CommandId, - optDeduplicationPeriod: Option[DeduplicationPeriod], - submissionId: Option[Ref.SubmissionId], - paidTrafficCost: NonNegativeLong, -) extends PrettyPrinting { - def changeId: ChangeId = ChangeId(userId, commandId, actAs.toSet) - - override protected def pretty: Pretty[CompletionInfo.this.type] = prettyOfClass( - param("actAs", _.actAs.mkShow()), - param("commandId", _.commandId), - param("userId", _.userId), - paramIfDefined("deduplication period", _.optDeduplicationPeriod), - param("submissionId", _.submissionId), - param("paidTrafficCost", _.paidTrafficCost), - indicateOmittedFields, - ) -} - -object CompletionInfo { - implicit val `CompletionInfo to LoggingValue`: ToLoggingValue[CompletionInfo] = { - case CompletionInfo( - actAs, - userId, - commandId, - deduplicationPeriod, - submissionId, - paidTrafficCost, - ) => - LoggingValue.Nested.fromEntries( - "actAs " -> actAs, - "userId " -> userId, - "commandId " -> commandId, - "deduplicationPeriod " -> deduplicationPeriod, - "submissionId" -> submissionId, - "paidTrafficCost" -> paidTrafficCost.value, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/InternalIndexService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/InternalIndexService.scala deleted file mode 100644 index 7132327b19..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/InternalIndexService.scala +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.daml.ledger.api.v2.topology_transaction.TopologyTransaction -import com.digitalasset.canton.LfPartyId -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -import java.util.concurrent.atomic.AtomicReference - -trait InternalIndexService { - def activeContracts( - partyIds: Set[LfPartyId], - validAt: Option[Offset], - )(implicit traceContext: TraceContext): Source[GetActiveContractsResponse, NotUsed] - - def topologyTransactions( - partyId: LfPartyId, - fromExclusive: Offset, - )(implicit traceContext: TraceContext): Source[TopologyTransaction, NotUsed] -} - -trait InternalIndexServiceProvider { - def internalIndexService: Option[InternalIndexService] - def registerInternalIndexService(internalIndexService: InternalIndexService): Unit - def unregisterInternalIndexService(): Unit -} - -trait InternalIndexServiceProviderImpl extends InternalIndexServiceProvider { - private val internalIndexServiceRef: AtomicReference[Option[InternalIndexService]] = - new AtomicReference(None) - - override def internalIndexService: Option[InternalIndexService] = - internalIndexServiceRef.get() - - override def registerInternalIndexService(internalIndexService: InternalIndexService): Unit = - internalIndexServiceRef.set(Some(internalIndexService)) - - override def unregisterInternalIndexService(): Unit = - internalIndexServiceRef.set(None) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PackageDescription.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PackageDescription.scala deleted file mode 100644 index 975d6becbe..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PackageDescription.scala +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.LfPackageId -import com.digitalasset.canton.config.CantonRequireTypes.String255 -import com.digitalasset.canton.data.CantonTimestamp -import slick.jdbc.GetResult -import slick.jdbc.GetResult.GetInt - -/** @param packageId - * the unique identifier for the package - * @param name - * name of the package (from package metadata]) - * @param version - * version of package (from package metadata) - * @param uploadedAt - * The package upload timestamp - * @param packageSize - * The LF archive protobuf-serialized size in bytes - */ -final case class PackageDescription( - packageId: LfPackageId, - name: String255, - version: String255, - uploadedAt: CantonTimestamp, - packageSize: Int, -) - -object PackageDescription { - - import com.digitalasset.canton.resource.DbStorage.Implicits.* - implicit val getResult: GetResult[PackageDescription] = GetResult { r => - val packageId = r.<<[LfPackageId] - val name = r.<<[String255] - val version = r.<<[String255] - val uploadedAt = r.<<[CantonTimestamp] - val packageSize = r.<<[Int] - PackageDescription( - packageId = packageId, - name = name, - version = version, - uploadedAt = uploadedAt, - packageSize = packageSize, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PackageSyncService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PackageSyncService.scala deleted file mode 100644 index f4d7f801b0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PackageSyncService.scala +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.ledger.api.{ - EnrichedVettedPackages, - ListVettedPackagesOpts, - UpdateVettedPackagesOpts, - UploadDarVettingChange, -} -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.archive.DamlLf.Archive -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.PackageId -import com.google.protobuf.ByteString - -import scala.concurrent.Future - -/** An interface for uploading and validating packages via a participant. */ -trait PackageSyncService { - - /** Upload a DAR to the ledger. - * - * This method must be thread-safe, not throw, and not block on IO. It is though allowed to - * perform significant computation. - * - * @param dar - * The DAR payload as ByteString. - * @param submissionId - * Submitter chosen submission identifier. - * - * @return - * an async result of a [[com.digitalasset.canton.ledger.participant.state.SubmissionResult]] - */ - def uploadDar( - dars: Seq[ByteString], - submissionId: Ref.SubmissionId, - vettingChange: UploadDarVettingChange, - synchronizerId: Option[SynchronizerId], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] - - def getPackageMetadataSnapshot(implicit - errorLoggingContext: ErrorLoggingContext - ): PackageMetadata = - throw new UnsupportedOperationException() - - def listLfPackages()(implicit - traceContext: TraceContext - ): Future[Seq[PackageDescription]] = - throw new UnsupportedOperationException() - - def getLfArchive(packageId: PackageId)(implicit - traceContext: TraceContext - ): Future[Option[Archive]] = - throw new UnsupportedOperationException() - - def validateDar( - dar: ByteString, - darName: String, - synchronizerId: Option[SynchronizerId], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] = - throw new UnsupportedOperationException() - - def updateVettedPackages( - opts: UpdateVettedPackagesOpts - )(implicit - traceContext: TraceContext - ): Future[(Option[EnrichedVettedPackages], Option[EnrichedVettedPackages])] - - def listVettedPackages( - opts: ListVettedPackagesOpts - )(implicit - traceContext: TraceContext - ): Future[Seq[EnrichedVettedPackages]] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ParticipantPruningSyncService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ParticipantPruningSyncService.scala deleted file mode 100644 index 293c8d3e81..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ParticipantPruningSyncService.scala +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.scheduler.SafeToPruneCommitmentState -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.Future - -/** An interface to prune participant ledger updates to manage participant ledger space and enable - * GDPR-style right-to-be-forgotten support. - */ -trait ParticipantPruningSyncService { - - /** Prune the participant ledger specifying the offset up to which participant ledger events can - * be removed. - * - * As this interface applies only to the local participant unlike other administrator services, - * returns a (completion stage of a) PruningResult rather than a SubmissionResult. - * - * Ledgers that do not elect to support participant pruning, return - * NotPruned(Status.UNIMPLEMENTED). Returning an error also keeps the ledger api server from - * pruning its index. - * - * Ledgers whose participants hold no participant-local state, but want the ledger api server to - * prune, return ParticipantPruned. - * - * For pruning implementations to be fault tolerant, the following aspects are important: - * - Consider failing a prune request before embarking on destructive operations for example if - * certain safety conditions are not met (such as being low on resources). This helps - * minimize the chances of partially performed prune operations. If the system cannot prune - * up to the specified offset, the call should not alter the system and return NotPruned - * rather than prune partially. - * - Implement pruning either atomically (performing all operations or none), or break down - * pruning steps into idempotent pieces that pick up after retries or system recovery in case - * of a mid-pruning crash. - * - To the last point, be aware that pruning of the ledger api server index happens in such an - * idempotent follow-up step upon successful completion of each prune call. To reach eventual - * consistency upon failures, be sure to return ParticipantPruned even if the specified - * offset has already been pruned to allow ledger api server index pruning to proceed in case - * of an earlier failure. - * - * @param pruneUpToInclusive - * The offset up to which contracts should be pruned. - * @param safeToPruneCommitmentState - * Optionally specify in which conditions counter-participants that have not sent matching - * commitments cannot block pruning. - * @return - * The pruning result. - */ - def prune( - pruneUpToInclusive: Offset, - safeToPruneCommitmentState: Option[SafeToPruneCommitmentState], - )(implicit traceContext: TraceContext): Future[PruningResult] - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PartySyncService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PartySyncService.scala deleted file mode 100644 index e8dc2cb8d9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PartySyncService.scala +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.crypto.HashOps -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.topology.{ - ExternalPartyOnboardingDetails, - ParticipantId, - PartyId, - PhysicalSynchronizerId, - SynchronizerId, -} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref - -/** An interface for on-boarding parties via a participant. */ -trait PartySyncService { - - /** Adds a new party to the set managed by the ledger. - * - * Caller specifies a party identifier suggestion, the actual identifier allocated might be - * different and is implementation specific. - * - * In particular, a ledger may: - * - Disregard the given hint and choose a completely new party identifier - * - Construct a new unique identifier from the given hint, e.g., by appending a UUID - * - Use the given hint as is, and reject the call if such a party already exists - * - * Successful party allocations will result in a - * [[com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective]] - * message. See the comments on [[com.digitalasset.canton.ledger.participant.state.Update]] for - * further details. - * - * @param hint - * A party identifier suggestion - * @param submissionId - * Client picked submission identifier for matching the responses with the request. - * @param synchronizerIdO - * The synchronizer on which the party should be allocated. Can be omitted if the participant - * is connected to only one synchronizer. - * @param externalPartyOnboardingDetails - * Onboarding information when allocating an external party - * @return - * an async result of a SubmissionResult - */ - def allocateParty( - partyId: PartyId, - submissionId: Ref.SubmissionId, - synchronizerIdO: Option[SynchronizerId], - externalPartyOnboardingDetails: Option[ExternalPartyOnboardingDetails], - )(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[SubmissionResult] - - /** Return the physical synchronizer ID for a synchronizer ID if the node is connected to it. - */ - def physicalSynchronizerIdForSynchronizerId( - synchronizerId: SynchronizerId - ): Option[PhysicalSynchronizerId] - - /** The participant id */ - def participantId: ParticipantId - - /** Hash ops of the participant */ - def hashOps: HashOps - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PruningResult.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PruningResult.scala deleted file mode 100644 index 7b5e404322..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/PruningResult.scala +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.google.rpc.Status - -sealed trait PruningResult extends Product with Serializable - -object PruningResult { - - /** Pruning has been performed. Commits the ledger api server to prune at the same offset as - * passed to the SyncService. - */ - case object ParticipantPruned extends PruningResult - - /** Pruning was not performed. Indicates to ledger api server not to proceed with pruning either. - * - * @param grpcStatus - * grpcStatus created using error codes API (see [[com.digitalasset.base.error.ErrorCode]]). - * Examples of gRPC status codes specific to pruning: - * - * OUT_OF_RANGE: If the specified offset cannot be pruned at, but will eventually be possible to - * prune at without user intervention. - * - * FAILED_PRECONDITION: If the specified offset cannot be pruned at, and enabling pruning at the - * offset requires user intervention (such as submitting a command via the submission service). - * - * INTERNAL: If a severe error has been encountered, particularly indicating that pruning has - * only been partially applied. - */ - final case class NotPruned(grpcStatus: Status) extends PruningResult - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/Reassignment.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/Reassignment.scala deleted file mode 100644 index 819be1a196..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/Reassignment.scala +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.protocol.ReassignmentId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.transaction.Node -import com.digitalasset.daml.lf.value.Value - -sealed trait Reassignment { - def templateId: Ref.Identifier - def packageName: Ref.PackageName - def stakeholders: Set[Ref.Party] - def nodeId: Int -} - -object Reassignment { - - final case class Batch private ( - reassignments: NonEmpty[Seq[Reassignment]] - ) extends Iterable[Reassignment] { - def iterator = reassignments.iterator - } - - object Batch { - def apply(first: Reassignment, rest: Reassignment*): Batch = - new Batch(NonEmpty(Seq, first, rest*)) - - def apply(reassignments: NonEmpty[Seq[Reassignment]]): Batch = - apply(reassignments.head1, reassignments.tail1*) - } - - /** Represent the update of unassigning a contract from a synchronizer. - * - * @param contractId - * Contract ID of the underlying contract. - * @param templateId - * Template ID of the underlying contract. - * @param packageName - * Package name of the underlying contract's template. - * @param stakeholders - * Stakeholders of the underlying contract. - * @param assignmentExclusivity - * Before this time (measured on the target synchronizer), only the submitter of the - * unassignment can initiate the assignment. Defined for reassigning participants. - * @param reassignmentCounter - * The reassignment counter of the underlying contract. - * @param nodeId - * The node ID of the unassign node. - */ - final case class Unassign( - contractId: Value.ContractId, - templateId: Ref.Identifier, - packageName: Ref.PackageName, - stakeholders: Set[Ref.Party], - assignmentExclusivity: Option[Timestamp], - reassignmentCounter: Long, - nodeId: Int, - ) extends Reassignment {} - - /** Represents the update of assigning a contract to a synchronizer. - * - * @param reassignmentCounter - * The reassignment counter of the underlying contract. - * @param nodeId - * The node ID of the create node. - * @param persistedContractInstance - * The persisted contract - */ - final case class Assign( - reassignmentCounter: Long, - nodeId: Int, - persistedContractInstance: PersistedContractInstance, - ) extends Reassignment { - def createNode: Node.Create = persistedContractInstance.inst.toCreateNode - def ledgerEffectiveTime: Timestamp = persistedContractInstance.inst.createdAt.time - def contractAuthenticationData: Bytes = persistedContractInstance.inst.authenticationData - def templateId: Ref.Identifier = persistedContractInstance.inst.templateId - def packageName: Ref.PackageName = persistedContractInstance.inst.packageName - def stakeholders: Set[Ref.Party] = persistedContractInstance.inst.stakeholders - def internalContractId: Long = persistedContractInstance.internalContractId - } -} - -/** The common information for all reassignments. Except from the hosted and reassigning - * stakeholders, all fields are the same for reassign and assign updates, which belong to the same - * reassignment. - * - * @param sourceSynchronizer - * The synchronizer ID from which the contract is unassigned. - * @param targetSynchronizer - * The synchronizer ID to which the contract is assigned. - * @param submitter - * Submitter of the command, unless the operation is performed offline. - * @param reassignmentId - * The ID of the unassign event. This should be used for the assign command. - * @param isReassigningParticipant - * Whether the participant is reassigning for the reassignment. - */ -final case class ReassignmentInfo( - sourceSynchronizer: Source[SynchronizerId], - targetSynchronizer: Target[SynchronizerId], - submitter: Option[Ref.Party], - reassignmentId: ReassignmentId, - isReassigningParticipant: Boolean, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommand.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommand.scala deleted file mode 100644 index df1d5a926a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommand.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.protocol.ReassignmentId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import com.digitalasset.daml.lf.value.Value - -sealed trait ReassignmentCommand { - def sourceSynchronizer: Source[SynchronizerId] - def targetSynchronizer: Target[SynchronizerId] -} - -object ReassignmentCommand { - final case class Unassign( - sourceSynchronizer: Source[SynchronizerId], - targetSynchronizer: Target[SynchronizerId], - contractId: Value.ContractId, - ) extends ReassignmentCommand - - final case class Assign( - sourceSynchronizer: Source[SynchronizerId], - targetSynchronizer: Target[SynchronizerId], - reassignmentId: ReassignmentId, - ) extends ReassignmentCommand -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommandsBatch.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommandsBatch.scala deleted file mode 100644 index 4d6a7ad546..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommandsBatch.scala +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.ledger.participant.state.ReassignmentCommand.{Assign, Unassign} -import com.digitalasset.canton.protocol.{LfContractId, ReassignmentId} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} - -import scala.annotation.tailrec - -sealed trait ReassignmentCommandsBatch - -object ReassignmentCommandsBatch { - - final case class Unassignments( - source: Source[SynchronizerId], - target: Target[SynchronizerId], - contractIds: NonEmpty[Seq[LfContractId]], - ) extends ReassignmentCommandsBatch - - final case class Assignments(target: Target[SynchronizerId], reassignmentId: ReassignmentId) - extends ReassignmentCommandsBatch - - abstract class InvalidBatch(val error: String) - case object NoCommands extends InvalidBatch("no commands") - case object MixedAssignWithOtherCommands extends InvalidBatch("mixed assign with other commands") - case object DifferingSynchronizers extends InvalidBatch("differing synchronizers") - - def create(commands: Seq[ReassignmentCommand]): Either[InvalidBatch, ReassignmentCommandsBatch] = - commands match { - case Nil => Left(NoCommands) - case Seq(assign: Assign) => - Right( - Assignments( - target = assign.targetSynchronizer, - reassignmentId = assign.reassignmentId, - ) - ) - case (head: Unassign) +: tail => - validateUnassigns( - Unassignments( - source = head.sourceSynchronizer, - target = head.targetSynchronizer, - contractIds = NonEmpty.mk(Seq, head.contractId), - ), - tail, - ) - case _ => Left(MixedAssignWithOtherCommands) - } - - @tailrec - private def validateUnassigns( - soFar: Unassignments, - rest: Seq[ReassignmentCommand], - ): Either[InvalidBatch, Unassignments] = rest match { - case Nil => Right(soFar.copy(contractIds = soFar.contractIds.reverse)) - case (head: Unassign) +: tail => - if (head.sourceSynchronizer == soFar.source && head.targetSynchronizer == soFar.target) - validateUnassigns(soFar.copy(contractIds = head.contractId +: soFar.contractIds), tail) - else - Left(DifferingSynchronizers) - case _ => Left(MixedAssignWithOtherCommands) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/RoutingSynchronizerState.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/RoutingSynchronizerState.scala deleted file mode 100644 index 318e3fa47c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/RoutingSynchronizerState.scala +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.crypto.SynchronizerCryptoPureApi -import com.digitalasset.canton.error.TransactionRoutingError.{ - UnableToGetStaticParameters, - UnableToQueryTopologySnapshot, -} -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.protocol.LfContractId -import com.digitalasset.canton.topology.client.TopologySnapshotLoader -import com.digitalasset.canton.topology.{PhysicalSynchronizerId, SynchronizerId} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ReassignmentTag.Target - -import scala.concurrent.ExecutionContext - -/** Provides state information about a synchronizer. */ -trait RoutingSynchronizerState { - - val topologySnapshots: Map[PhysicalSynchronizerId, TopologySnapshotLoader] - - /** @return - * true if there is at least a ready connected synchronizer, false otherwise - */ - def existsReadySynchronizer(): Boolean - - def getPhysicalId(synchronizerId: SynchronizerId): Option[PhysicalSynchronizerId] = - topologySnapshots.keys.filter(_.logical == synchronizerId).maxOption - - /** @return - * Right containing the topology snapshot for the given ``synchronizerId``, or Left with an - * error if the requested synchronizer is not connected - */ - def getTopologySnapshotFor( - psid: PhysicalSynchronizerId - ): Either[UnableToQueryTopologySnapshot.Failed, TopologySnapshotLoader] - - def getTopologySnapshotFor( - targetPsid: Target[PhysicalSynchronizerId] - ): Either[UnableToQueryTopologySnapshot.Failed, Target[TopologySnapshotLoader]] = - getTopologySnapshotFor(targetPsid.unwrap).map(Target(_)) - - def getSynchronizersOfContracts( - coids: Seq[LfContractId] - )(implicit - ec: ExecutionContext, - traceContext: TraceContext, - ): FutureUnlessShutdown[Map[LfContractId, (PhysicalSynchronizerId, ContractStateStatus)]] - - /** SyncCryptoPureApi for this synchronizer. - */ - def getSyncCryptoPureApi( - synchronizerId: PhysicalSynchronizerId - ): Either[UnableToGetStaticParameters.Failed, Option[SynchronizerCryptoPureApi]] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmissionResult.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmissionResult.scala deleted file mode 100644 index abfca92c21..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmissionResult.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.daml.grpc.GrpcStatus -import com.daml.logging.entries.{LoggingValue, ToLoggingValue} -import com.google.rpc.status.Status as ProtoStatus -import io.grpc.{StatusRuntimeException, protobuf} - -sealed abstract class SubmissionResult extends Product with Serializable { - def description: String -} - -object SubmissionResult { - - /** The request has been received */ - case object Acknowledged extends SubmissionResult { - override val description: String = "The request has been received" - } - - /** The submission has failed with a synchronous error. - * - * Asynchronous errors are reported via the command completion stream as a - * [[Update.CommandRejected]] - * - * See the documentation in `error.proto` for how to report common submission errors. - */ - final case class SynchronousError(status: com.google.rpc.status.Status) extends SubmissionResult { - override val description: String = s"Submission failed with error ${status.message}" - - def exception: StatusRuntimeException = - protobuf.StatusProto.toStatusRuntimeException(GrpcStatus.toJavaProto(status)) - } - - object SynchronousError { - implicit val `SynchronousError to LoggingValue`: ToLoggingValue[SynchronousError] = - error => - LoggingValue.Nested.fromEntries( - "status" -> LoggingValue.Nested.fromEntries( - "code" -> error.status.code, - "message" -> error.status.message, - ) - ) - - def apply(status: com.google.rpc.Status): SynchronousError = new SynchronousError( - ProtoStatus.fromJavaProto(status) - ) - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmissionSyncService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmissionSyncService.scala deleted file mode 100644 index d482416604..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmissionSyncService.scala +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.LfGlobalKeyMapping -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.{ImmArray, Ref} -import com.digitalasset.daml.lf.transaction.SubmittedTransaction - -import scala.concurrent.Future - -trait SubmissionSyncService { - - /** Submit a transaction for acceptance to the ledger. - * - * This method must be thread-safe. - * - * The result of the transaction submission is communicated asynchronously via a sequence of - * [[com.digitalasset.canton.ledger.participant.state.Update]] implementation backed by the same - * participant state as this [[com.digitalasset.canton.ledger.participant.state.SyncService]]. - * Successful transaction acceptance is communicated using a - * [[com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted]] message. - * Failed transaction acceptance is communicated when possible via a - * [[com.digitalasset.canton.ledger.participant.state.Update.CommandRejected]] message - * referencing the same `submitterInfo` as provided in the submission. There can be failure modes - * where a transaction submission is lost in transit, and no - * [[com.digitalasset.canton.ledger.participant.state.Update.CommandRejected]] is generated. See - * the comments on [[com.digitalasset.canton.ledger.participant.state.Update]] for further - * details. - * - * A note on ledger time and record time: transactions are submitted together with a `ledgerTime` - * provided as part of the `transactionMeta` information. The ledger time is used by the Daml - * Engine to resolve calls to the `getTime :: Update Time` function. Letting the submitter freely - * choose the ledger time is though a problem for the other stakeholders in the contracts - * affected by the submitted transaction. The submitter can in principle choose to submit - * transactions that are effective far in the past or future relative to the wall-clock time of - * the other participants. This gives the submitter an unfair advantage and make the semantics of - * `getTime` quite surprising. We've chosen the following solution to provide useful guarantees - * for contracts relying on `getTime`. - * - * The ledger is charged with (1) associating record-time stamps to accepted transactions and (2) - * to provide a guarantee on the maximal skew between the ledger effective time and the record - * time stamp associated to an accepted transaction. The ledger is also expected to provide - * guarantees on the distribution of the maximal skew between record time stamps on accepted - * transactions and the wall-clock time at delivery of accepted transactions to a ledger - * participant. Thereby providing ledger participants with a guarantee on the maximal skew - * between the ledger effective time of an accepted transaction and the wall-clock time at - * delivery to these participants. - * - * Concretely, we typically expect the allowed skew between record time and ledger time to be in - * the minute range. Thereby leaving ample time for submitting and validating large transactions - * before they are timestamped with their record time. - * - * The [[com.digitalasset.canton.ledger.participant.state.SyncService]] is responsible for - * deduplicating commands with the same - * [[com.digitalasset.canton.ledger.participant.state.SubmitterInfo.changeId]] within the - * [[com.digitalasset.canton.ledger.participant.state.SubmitterInfo.deduplicationPeriod]]. - * - * @param transaction - * the submitted transaction. This transaction can contain local contract-ids that need - * suffixing. The participant state may have to suffix those contract-ids in order to - * guaranteed their global uniqueness. See the Contract Id specification for more detail - * daml-lf/spec/contract-id.rst. - * @param synchronizerRank - * The synchronizer rank based on which: - * - the participant performs the required reassignments of the transaction's input contracts - * - the participant routes the transaction to the synchronizer - * @param routingSynchronizerState - * The synchronizer state used for synchronizer selection. This is subsequently used for - * synchronizer routing. - * @param submitterInfo - * the information provided by the submitter for correlating this submission with its - * acceptance or rejection on the associated - * [[com.digitalasset.canton.ledger.participant.state.Update]]. - * @param transactionMeta - * the meta-data accessible to all consumers of the transaction. See - * [[com.digitalasset.canton.ledger.participant.state.TransactionMeta]] for more information. - * @param _estimatedInterpretationCost - * Estimated cost of interpretation that may be used for handling submitted transactions - * differently. - * @param keyResolver - * Input key mapping inferred by interpretation. The map should contain all contract keys that - * were used during interpretation. A value of None means no contract was found with this - * contract key. - * @param processedDisclosedContracts - * Explicitly disclosed contracts used during interpretation. - */ - def submitTransaction( - transaction: SubmittedTransaction, - synchronizerRank: SynchronizerRank, - routingSynchronizerState: RoutingSynchronizerState, - submitterInfo: SubmitterInfo, - transactionMeta: TransactionMeta, - // TODO(#25385): Consider removing since it's currently not used - _estimatedInterpretationCost: Long, - keyResolver: LfGlobalKeyMapping, - processedDisclosedContracts: ImmArray[LfFatContractInst], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] - - /** Submit a reassignment command for acceptance to the ledger. - * - * To complete a reassignment, first a submission of an unassign command followed by an assign - * command is required. The - * [[com.digitalasset.canton.ledger.participant.state.ReassignmentCommand.Assign]] command must - * include the unassign ID which can be observed in the accepted event marking the corresponding - * successful unassign command. - * - * @param submitter - * The submitter of the reassignment. - * @param userId - * An identifier for the user that submitted the command. This is used for monitoring, command - * deduplication, and to allow Daml applications subscribe to their own submissions only. - * @param commandId - * A submitter-provided identifier to identify an intended ledger change within all the - * submissions by the same parties and application. - * @param submissionId - * An identifier for the submission that allows an application to correlate completions to its - * submissions. - * @param workflowId - * A submitter-provided identifier used for monitoring and to traffic-shape the work handled by - * Daml applications communicating over the ledger. - * @param reassignmentCommands - * The commands specifying this reassignment further. - */ - def submitReassignment( - submitter: Ref.Party, - userId: Ref.UserId, - commandId: Ref.CommandId, - submissionId: Option[Ref.SubmissionId], - workflowId: Option[Ref.WorkflowId], - reassignmentCommands: Seq[ReassignmentCommand], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmitterInfo.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmitterInfo.scala deleted file mode 100644 index 571f4c2bdc..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SubmitterInfo.scala +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.daml.logging.entries.{LoggingValue, ToLoggingValue} -import com.digitalasset.canton.LfTimestamp -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.crypto.Signature -import com.digitalasset.canton.data.DeduplicationPeriod -import com.digitalasset.canton.ledger.participant.state.SubmitterInfo.ExternallySignedSubmission -import com.digitalasset.canton.topology.MediatorGroup.MediatorGroupIndex -import com.digitalasset.canton.topology.PartyId -import com.digitalasset.canton.version.HashingSchemeVersion -import com.digitalasset.daml.lf.data.Ref - -import java.util.UUID - -/** Collects context information for a submission. - * - * Note that this is used for party-originating changes only. They are usually issued via the - * Ledger API. - * - * @param actAs - * the non-empty set of parties that submitted the change. - * @param readAs - * the parties on whose behalf (in addition to all parties listed in [[actAs]]) contracts can be - * retrieved. - * @param userId - * an identifier for the user that submitted the command. This is used for monitoring, command - * deduplication, and to allow Daml applications subscribe to their own submissions only. - * @param commandId - * a submitter-provided identifier to identify an intended ledger change within all the - * submissions by the same parties and application. - * @param deduplicationPeriod - * The deduplication period for the command submission. Used for the deduplication guarantee - * described in the [[Update]]. - * @param submissionId - * An identifier for the submission that allows an application to correlate completions to its - * submissions. - * @param externallySignedSubmission - * If this is provided then the authorization for all acting parties will be provided by the - * enclosed signatures. - */ -final case class SubmitterInfo( - actAs: List[Ref.Party], - readAs: List[Ref.Party], - userId: Ref.UserId, - commandId: Ref.CommandId, - deduplicationPeriod: DeduplicationPeriod, - submissionId: Option[Ref.SubmissionId], - externallySignedSubmission: Option[ExternallySignedSubmission], -) { - - /** The ID for the ledger change */ - val changeId: ChangeId = ChangeId(userId, commandId, actAs.toSet) - - def toCompletionInfo(paidTrafficCost: NonNegativeLong): CompletionInfo = - CompletionInfo( - actAs, - userId, - commandId, - Some(deduplicationPeriod), - submissionId, - paidTrafficCost, - ) - -} - -object SubmitterInfo { - import com.digitalasset.canton.ledger.api.Commands.`Timestamp to LoggingValue` - - implicit val `ExternallySignedSubmission to LoggingValue` - : ToLoggingValue[ExternallySignedSubmission] = { - case ExternallySignedSubmission( - version, - signatures, - transactionUUID, - mediatorGroup, - maxRecordTime, - ) => - LoggingValue.Nested.fromEntries( - "version" -> version.index, - "signatures" -> signatures.keys.map(_.toProtoPrimitive), - "transactionUUID" -> transactionUUID.toString, - "mediatorGroup" -> mediatorGroup.toString, - "maxRecordTime" -> maxRecordTime, - ) - } - implicit val `SubmitterInfo to LoggingValue`: ToLoggingValue[SubmitterInfo] = { - case SubmitterInfo( - actAs, - readAs, - userId, - commandId, - deduplicationPeriod, - submissionId, - externallySignedSubmission, - ) => - LoggingValue.Nested.fromEntries( - "actAs " -> actAs, - "readAs" -> readAs, - "userId " -> userId, - "commandId " -> commandId, - "deduplicationPeriod " -> deduplicationPeriod, - "submissionId" -> submissionId, - "externallySignedSubmission" -> externallySignedSubmission, - ) - } - - final case class ExternallySignedSubmission( - version: HashingSchemeVersion, - signatures: Map[PartyId, Seq[Signature]], - transactionUUID: UUID, - mediatorGroup: MediatorGroupIndex, - maxRecordTime: Option[LfTimestamp], - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SyncService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SyncService.scala deleted file mode 100644 index e6ff02a92e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SyncService.scala +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import cats.data.EitherT -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.error.{TransactionError, TransactionRoutingError} -import com.digitalasset.canton.health.ReportsHealth -import com.digitalasset.canton.ledger.participant.state.SyncService.{ - ConnectedSynchronizerRequest, - ConnectedSynchronizerResponse, - SubmissionCostEstimation, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.platform.apiserver.services.command.interactive.CostEstimationHints -import com.digitalasset.canton.protocol.{ - LfContractId, - LfFatContractInst, - LfSubmittedTransaction, - LfVersionedTransaction, -} -import com.digitalasset.canton.topology.transaction.ParticipantPermission -import com.digitalasset.canton.topology.{ParticipantId, PhysicalSynchronizerId, SynchronizerId} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.{LfGlobalKeyMapping, LfPackageId, LfPartyId, SynchronizerAlias} - -/** An interface to change a ledger via a participant. '''Please note that this interface is - * unstable and may significantly change.''' - * - * The methods in this interface are all methods that are supported *uniformly* across all ledger - * participant implementations. Methods for uploading packages, on-boarding parties, and changing - * ledger-wide configuration are specific to a ledger and therefore to a participant - * implementation. Moreover, these methods usually require admin-level privileges, whose granting - * is also specific to a ledger. - * - * If a ledger is run for testing only, there is the option for quite freely allowing the - * on-boarding of parties and uploading of packages. There are plans to make this functionality - * uniformly available: see the roadmap for progress information - * https://github.com/digital-asset/daml/issues/121. - * - * The following methods are currently available for changing the state of a Daml ledger: - * - submitting a transaction using [[SyncService!.submitTransaction]] - * - allocating a new party using [[PartySyncService!.allocateParty]] - * - pruning a participant ledger using [[ParticipantPruningSyncService!.prune]] - */ -trait SyncService - extends SubmissionSyncService - with PackageSyncService - with PartySyncService - with ParticipantPruningSyncService - with ReportsHealth - with InternalIndexServiceProvider { - - // temporary implementation, will be removed as topology events on Ledger API proceed - def getConnectedSynchronizers(request: ConnectedSynchronizerRequest)(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[ConnectedSynchronizerResponse] = - throw new UnsupportedOperationException() - - // temporary implementation, will be removed as topology events on Ledger API proceed - /** Get the offsets of the incomplete assigned/unassigned events for a set of stakeholders. - * - * @param validAt - * The offset of validity in participant offset terms. - * @param stakeholders - * Only offsets are returned which have at least one stakeholder from this set. - * @return - * All the offset of assigned/unassigned events which do not have their counterparts visible at - * the validAt offset, and only for the reassignments for which this participant is - * reassigning. - */ - def incompleteReassignmentOffsets( - validAt: Offset, - stakeholders: Set[LfPartyId], - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Vector[Offset]] = { - val _ = validAt - val _ = stakeholders - val _ = traceContext - FutureUnlessShutdown.pure(Vector.empty) - } - - // temporary implementation, will be removed with the refactoring of the SyncService interface - /** Computes a SynchronizerId -> PartyId -> PackageId relation that describes: - * - for each synchronizer that hosts all the provided `submitters` that can submit. The - * provided `submitters` can be empty (for externally signed transactions), in which case - * synchronizers are not restricted by parties with submission rights on the local - * participant - * - which package-ids can be accepted (i.e. they are vetting-valid) in a transaction by each - * of the informees provided - * - if the prescribed synchronizer is provided, only that one is considered - */ - def computePartyVettingMap( - submitters: Set[LfPartyId], - informees: Set[LfPartyId], - vettingValidityTimestamp: CantonTimestamp, - prescribedSynchronizer: Option[SynchronizerId], - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[Map[PhysicalSynchronizerId, Map[LfPartyId, Set[LfPackageId]]]] - - // temporary implementation, will be removed with the refactoring of the SyncService interface - /** Computes the highest ranked synchronizer from the given admissible synchronizers without - * performing topology checks. - * - * This method is used internally in command processing to pre-select a synchronizer for - * determining the package preference set used in command interpretation. - * - * For the definitive synchronizer selection to be used for routing of a submitted transaction, - * use [[selectRoutingSynchronizer]]. - * - * @param submitterInfo - * The submitter info - * @param transaction - * The submitted transaction - * @param transactionMeta - * The transaction metadata - * @param admissibleSynchronizers - * The list of synchronizers from which the best one should be selected - * @param disclosedContractIds - * The list of disclosed contracts used in command interpretation - * @param routingSynchronizerState - * The routing synchronizer state the computation should be based on - * @return - * The ID of the best ranked synchronizer - */ - def computeHighestRankedSynchronizerFromAdmissible( - submitterInfo: SubmitterInfo, - transaction: LfSubmittedTransaction, - transactionMeta: TransactionMeta, - admissibleSynchronizers: NonEmpty[Set[PhysicalSynchronizerId]], - disclosedContractIds: List[LfContractId], - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, TransactionRoutingError, PhysicalSynchronizerId] - - // temporary implementation, will be removed with the refactoring of the SyncService interface - /** Computes the best synchronizer for a submitted transaction by checking the submitted - * transaction against the topology of the connected synchronizers and ranking the admissible - * ones using the synchronizer ranking (by priority, minimum number of reassignments and - * synchronizer-id). - * - * @param submitterInfo - * The submitter info - * @param transaction - * The submitted transaction - * @param transactionMeta - * The transaction metadata - * @param disclosedContractIds - * The list of disclosed contracts used in command interpretation - * @param optSynchronizerId - * If provided, only this synchronizer id is considered as a candidate for routing - * @param transactionUsedForExternalSigning - * If true, the topology checks do not required that the submitters of the transaction have - * submission rights on the local participant since they are supposed to externally sign the - * transaction. - * @return - * The rank of the routing synchronizer - */ - def selectRoutingSynchronizer( - submitterInfo: SubmitterInfo, - transaction: LfSubmittedTransaction, - transactionMeta: TransactionMeta, - disclosedContractIds: List[LfContractId], - optSynchronizerId: Option[SynchronizerId], - transactionUsedForExternalSigning: Boolean, - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, TransactionError, SynchronizerRank] - - // temporary implementation, will be removed with the refactoring of the SyncService interface - /** Constructs and fetches the current synchronizer state, to be used throughout command execution - */ - def getRoutingSynchronizerState(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[RoutingSynchronizerState] - - /** Estimate the associated traffic cost to submitting and confirming a transaction - */ - def estimateTrafficCost( - synchronizerId: SynchronizerId, - transaction: LfVersionedTransaction, - transactionMetadata: TransactionMeta, - submitterInfo: SubmitterInfo, - keyResolver: LfGlobalKeyMapping, - disclosedContracts: Map[LfContractId, LfFatContractInst], - costHints: CostEstimationHints, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, String, SubmissionCostEstimation] -} - -object SyncService { - final case class SubmissionCostEstimation( - estimationTimestamp: CantonTimestamp, - confirmationRequestCost: NonNegativeLong, - confirmationResponseCost: NonNegativeLong, - ) { - def totalCost: NonNegativeLong = - confirmationRequestCost + confirmationResponseCost - } - final case class ConnectedSynchronizerRequest( - party: Option[LfPartyId], - participantId: Option[ParticipantId], - ) - - final case class ConnectedSynchronizerResponse( - connectedSynchronizers: Seq[ConnectedSynchronizerResponse.ConnectedSynchronizer] - ) - - object ConnectedSynchronizerResponse { - final case class ConnectedSynchronizer( - synchronizerAlias: SynchronizerAlias, - synchronizerId: PhysicalSynchronizerId, - permission: Option[ParticipantPermission], - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SynchronizerIndex.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SynchronizerIndex.scala deleted file mode 100644 index cf472d7f36..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SynchronizerIndex.scala +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.RepairCounter -import com.digitalasset.canton.data.CantonTimestamp - -/** SynchronizerIndex is a composite type describing the index for a synchronizer. - * - * SynchronizerIndex-es for the various type of updates can be created with the respective factory - * function: - * - [[SynchronizerIndex.forRepairUpdate]] creates a SynchronizerIndex for repair update and - * setting the recordTime to the timestamp of the repair. - * - * - [[SynchronizerIndex.forSequencedUpdate]] creates a SynchronizerIndex for sequenced update - * and setting the recordTime to the sequencer timestamp. - * - * - [[SynchronizerIndex.forFloatingUpdate]] creates a SynchronizerIndex for floating update and - * setting the recordTime to the timestamp of the floating update. Floating event are for - * example: topology transactions emitted at effective time, timely rejection events. - * - * SynchronizerIndex-es can be aggregated with the [[max]] function which builds the maximum of - * each parameter individually. This also means that all parameters can increase independently of - * each other. For example - * - * {{{ - * SynchronizerIndex( - * repairIndex = Some(RepairIndex(T2, 13)) - * sequencerIndex = Some(T1) - * recordTime = Some(T3) - * ) - * }}} - * - * with timestamps T1 < T2 < T3 means: - * - the last sequenced event observed by the indexer is at T1 - * - * - after that record time moved ahead because of some floating events observed to T2 - * - * - then repair operations happened pushing the RepairCounter to 13 - * - * - and then additional floating events pushed the record time to T3 - * - * @param repairIndex - * references the last known timestamp and repair counter for a repair update. The repair - * timestamp must be less or equal to the recordTime. - * @param sequencerIndex - * references the last known timestamp for a sequenced update. This timestamp must be less or - * equal to the recordTime. - * @param recordTime - * references the timestamp of the last update. - */ -final case class SynchronizerIndex( - repairIndex: Option[RepairIndex], - sequencerIndex: Option[CantonTimestamp], - recordTime: CantonTimestamp, -) { - def max(otherSynchronizerIndex: SynchronizerIndex): SynchronizerIndex = - new SynchronizerIndex( - repairIndex = repairIndex.iterator - .++(otherSynchronizerIndex.repairIndex.iterator) - .maxByOption(identity), - sequencerIndex = sequencerIndex.iterator - .++(otherSynchronizerIndex.sequencerIndex.iterator) - .maxOption, - recordTime = recordTime max otherSynchronizerIndex.recordTime, - ) - - override def toString: String = - s"SynchronizerIndex(sequencerIndex=$sequencerIndex, repairIndex=$repairIndex, recordTime=$recordTime)" -} - -object SynchronizerIndex { - def forRepairUpdate(repairIndex: RepairIndex): SynchronizerIndex = - SynchronizerIndex( - Some(repairIndex), - None, - repairIndex.timestamp, - ) - - def forSequencedUpdate(sequencerTimestamp: CantonTimestamp): SynchronizerIndex = - SynchronizerIndex( - None, - Some(sequencerTimestamp), - sequencerTimestamp, - ) - - def forFloatingUpdate(recordTime: CantonTimestamp): SynchronizerIndex = - SynchronizerIndex( - None, - None, - recordTime, - ) -} - -final case class RepairIndex(timestamp: CantonTimestamp, counter: RepairCounter) - -object RepairIndex { - implicit val orderingRepairIndex: Ordering[RepairIndex] = - Ordering.by[RepairIndex, (CantonTimestamp, RepairCounter)](repairIndex => - (repairIndex.timestamp, repairIndex.counter) - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SynchronizerRank.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SynchronizerRank.scala deleted file mode 100644 index 9eb5adaa5d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/SynchronizerRank.scala +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import cats.Order.* -import com.digitalasset.canton.LfPartyId -import com.digitalasset.canton.protocol.LfContractId -import com.digitalasset.canton.topology.PhysicalSynchronizerId - -final case class SynchronizerRank( - reassignments: Map[ - LfContractId, - (LfPartyId, PhysicalSynchronizerId), - ], // (cid, (submitter, current synchronizer)) - priority: Int, - synchronizerId: PhysicalSynchronizerId, // synchronizer for submission -) - -object SynchronizerRank { - // The highest priority synchronizer should be picked first, so negate the priority - implicit val synchronizerRanking: Ordering[SynchronizerRank] = - Ordering.by(x => (-x.priority, x.reassignments.size, x.synchronizerId)) - - def single(synchronizerId: PhysicalSynchronizerId): SynchronizerRank = - SynchronizerRank(Map.empty, 0, synchronizerId) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/TransactionMeta.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/TransactionMeta.scala deleted file mode 100644 index 7717ffb974..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/TransactionMeta.scala +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.digitalasset.canton.data.LedgerTimeBoundaries -import com.digitalasset.canton.logging.pretty.PrettyInstances.prettyTimeBoundaries -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time} -import com.digitalasset.daml.lf.transaction.NodeId - -/** Meta-data of a transaction visible to all parties that can see a part of the transaction. - * - * @param ledgerEffectiveTime: - * the submitter-provided time at which the transaction should be interpreted. This is the time - * returned by the Daml interpreter on a `getTime :: Update Time` call. See the docs on - * [[SyncService.submitTransaction]] for how it relates to the notion of `recordTime`. - * - * @param workflowId: - * a submitter-provided identifier used for monitoring and to traffic-shape the work handled by - * Daml applications communicating over the ledger. - * - * @param preparationTime: - * the transaction prepartion time - * - * @param submissionSeed: - * the seed used to derive the transaction contract IDs. - * - * @param timeBoundaries: - * the time boundaries associated with the transaction - * - * @param optUsedPackages: - * the set of package IDs the transaction is depending on. Undefined means 'not known'. - * - * @param optNodeSeeds: - * an association list that maps to each ID if create and exercise nodes its respective seed. - * Undefined is not known. - * - * @param optByKeyNodes: - * the list of each ID of the fetch and exercise nodes that correspond to a fetch-by-key, - * lookup-by-key, or exercise-by-key command. Undefined is not known. - */ -final case class TransactionMeta( - ledgerEffectiveTime: Time.Timestamp, - workflowId: Option[Ref.WorkflowId], - preparationTime: Time.Timestamp, - submissionSeed: crypto.Hash, - timeBoundaries: LedgerTimeBoundaries, - optUsedPackages: Option[Set[Ref.PackageId]], - optNodeSeeds: Option[ImmArray[(NodeId, crypto.Hash)]], - optByKeyNodes: Option[ImmArray[NodeId]], -) extends PrettyPrinting { - - override protected def pretty: Pretty[TransactionMeta.this.type] = prettyOfClass( - param("ledgerEffectiveTime", _.ledgerEffectiveTime), - paramIfDefined("workflowId", _.workflowId), - param("preparationTime", _.preparationTime), - param("timeBoundaries", _.timeBoundaries), - indicateOmittedFields, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/Update.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/Update.scala deleted file mode 100644 index 1862c5ab47..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/Update.scala +++ /dev/null @@ -1,778 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.daml.logging.entries.{LoggingEntry, LoggingValue, ToLoggingValue} -import com.digitalasset.base.error.GrpcStatuses -import com.digitalasset.canton.RepairCounter -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.crypto.Hash -import com.digitalasset.canton.data.{CantonTimestamp, DeduplicationPeriod} -import com.digitalasset.canton.ledger.participant.state.Update.CommandRejected.RejectionReasonTemplate -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting, PrettyUtil} -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.platform.indexer.TransactionTraversalUtils -import com.digitalasset.canton.protocol.{LfHash, UpdateId} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.{HasTraceContext, TraceContext} -import com.digitalasset.canton.util.ShowUtil -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.engine.Blinding -import com.digitalasset.daml.lf.transaction.{ - BlindingInfo, - CommittedTransaction, - TransactionNodeStatistics, -} -import com.digitalasset.daml.lf.value.Value -import com.google.rpc.status.Status as RpcStatus - -import java.util.UUID -import scala.concurrent.Promise - -/** An update to the (abstract) participant state. - * - * [[Update]]'s are used in to communicate changes to abstract participant state to consumers. - * - * We describe the possible updates in the comments of each of the case classes implementing - * [[Update]]. - * - * Deduplication guarantee: Let there be a [[Update.TransactionAccepted]] with [[CompletionInfo]] - * or a [[Update.CommandRejected]] with [[CompletionInfo]] at offset `off2`. If `off2`'s - * [[CompletionInfo.optDeduplicationPeriod]] is a - * [[com.digitalasset.canton.data.DeduplicationPeriod.DeduplicationOffset]], let `off1` be the - * first offset after the deduplication offset. If the deduplication period is a - * [[com.digitalasset.canton.data.DeduplicationPeriod.DeduplicationDuration]], let `off1` be the - * first offset whose record time is at most the duration before `off2`'s record time (inclusive). - * Then there is no other [[Update.TransactionAccepted]] with [[CompletionInfo]] for the same - * [[CompletionInfo.changeId]] between the offsets `off1` and `off2` inclusive. - * - * So if a command submission has resulted in a [[Update.TransactionAccepted]], other command - * submissions with the same [[SubmitterInfo.changeId]] must be deduplicated if the earlier's - * [[Update.TransactionAccepted]] falls within the latter's - * [[CompletionInfo.optDeduplicationPeriod]]. - * - * Implementations MAY extend the deduplication period from [[SubmitterInfo]] arbitrarily and - * reject a command submission as a duplicate even if its deduplication period does not include the - * earlier's [[Update.TransactionAccepted]]. A [[Update.CommandRejected]] completion does not - * trigger deduplication and implementations SHOULD process such resubmissions normally. - */ -sealed trait Update extends Product with Serializable with PrettyPrinting with HasTraceContext - -/** Update which defines a recordTime, synchronizerId and SynchronizerIndex. - */ -sealed trait SynchronizerUpdate extends Update { - - /** The record time at which the state change was committed. */ - def recordTime: CantonTimestamp - - def synchronizerId: SynchronizerId - - def synchronizerIndex: SynchronizerIndex -} - -sealed trait SequencedUpdate extends SynchronizerUpdate { - final override def synchronizerIndex: SynchronizerIndex = - SynchronizerIndex.forSequencedUpdate(recordTime) -} - -sealed trait FloatingUpdate extends SynchronizerUpdate { - final override def synchronizerIndex: SynchronizerIndex = - SynchronizerIndex.forFloatingUpdate(recordTime) -} - -sealed trait SequencedEventUpdate extends SequencedUpdate - -sealed trait RepairUpdate extends SynchronizerUpdate { - def repairCounter: RepairCounter - - final override def synchronizerIndex: SynchronizerIndex = - SynchronizerIndex.forRepairUpdate( - RepairIndex( - timestamp = recordTime, - counter = repairCounter, - ) - ) -} - -object Update { - - /** Produces a constant dummy transaction seed for transactions in which we cannot expose a seed. - * Essentially all of them. TransactionMeta.submissionSeed can no longer be set to None starting - * with Daml 1.3 - */ - def noOpSeed: LfHash = - LfHash.assertFromString("00" * LfHash.underlyingHashLength) - - final case class TopologyTransactionEffective( - updateId: UpdateId, - events: Set[TopologyTransactionEffective.TopologyEvent], - synchronizerId: SynchronizerId, - effectiveTime: CantonTimestamp, - )(implicit override val traceContext: TraceContext) - extends FloatingUpdate { - - // Topology transactions emitted to the update stream at effective time - override def recordTime: CantonTimestamp = effectiveTime - - override def pretty: Pretty[TopologyTransactionEffective] = - TopologyTransactionEffective.pretty - } - - object TopologyTransactionEffective extends PrettyUtil { - - sealed trait AuthorizationLevel - object AuthorizationLevel { - final case object Submission extends AuthorizationLevel - - final case object Confirmation extends AuthorizationLevel - - final case object Observation extends AuthorizationLevel - } - - sealed trait AuthorizationEvent - object AuthorizationEvent { - sealed trait ActiveAuthorization extends AuthorizationEvent { - def level: AuthorizationLevel - } - - final case class Added(level: AuthorizationLevel) extends ActiveAuthorization - final case class ChangedTo(level: AuthorizationLevel) extends ActiveAuthorization - final case object Revoked extends AuthorizationEvent - final case class Onboarding(level: AuthorizationLevel) extends ActiveAuthorization - } - - sealed trait TopologyEvent - - object TopologyEvent { - final case class PartyToParticipantAuthorization( - party: Ref.Party, - participant: Ref.ParticipantId, - authorizationEvent: AuthorizationEvent, - ) extends TopologyEvent - } - implicit val `TopologyTransactionEffective to LoggingValue` - : ToLoggingValue[TopologyTransactionEffective] = { topologyTransactionEffective => - LoggingValue.Nested.fromEntries( - Logging.updateId(topologyTransactionEffective.updateId), - Logging.recordTime(topologyTransactionEffective.recordTime.toLf), - Logging.synchronizerId(topologyTransactionEffective.synchronizerId), - ) - } - - val pretty: Pretty[TopologyTransactionEffective] = - prettyOfClass( - param("effectiveTime", _.effectiveTime), - param("synchronizerId", _.synchronizerId), - param("updateId", _.updateId.tryAsLedgerTransactionId), - indicateOmittedFields, - ) - } - - sealed trait AcsChangeSequencedUpdate extends SynchronizerUpdate { - def acsChangeFactory: AcsChangeFactory - } - - /** Signal the acceptance of a transaction. - */ - sealed trait TransactionAccepted extends SynchronizerUpdate { - - /** The information provided by the submitter of the command that created this transaction. It - * must be provided if this participant hosts one of the [[SubmitterInfo.actAs]] parties and - * shall output a completion event for this transaction. This in particular applies if this - * participant has submitted the command to the [[SyncService]]. - * - * The Offset-order of Updates must ensure that command deduplication guarantees are met. - */ - def completionInfoO: Option[CompletionInfo] - - /** Traffic cost paid by this node for the sequencing of the corresponding transaction event - */ - def paidTrafficCost: Option[NonNegativeLong] = completionInfoO.map(_.paidTrafficCost) - - /** The metadata of the transaction that was provided by the submitter. It is visible to all - * parties that can see the transaction. - */ - def transactionMeta: TransactionMeta - - def transactionInfo: TransactionAccepted.TransactionInfo - - def updateId: UpdateId - - /** Transaction hash signed by the external party to authorize the transaction. Only on - * externally signed transactions - */ - def externalTransactionHash: Option[Hash] - - def isAcsDelta(contractId: Value.ContractId): Boolean - - /** Maps each contract id (of created or archived events of the transaction) to the - * corresponding [[ContractInfo]]. - */ - def contractInfos: Map[Value.ContractId, ContractInfo] - } - - object TransactionAccepted { - implicit val `TransactionAccepted to LoggingValue`: ToLoggingValue[TransactionAccepted] = { - case txAccepted: TransactionAccepted => - LoggingValue.Nested.fromEntries( - Logging.recordTime(txAccepted.recordTime.toLf), - Logging.completionInfo(txAccepted.completionInfoO), - Logging.updateId(txAccepted.updateId), - Logging.ledgerTime(txAccepted.transactionMeta.ledgerEffectiveTime), - Logging.workflowIdOpt(txAccepted.transactionMeta.workflowId), - Logging.preparationTime(txAccepted.transactionMeta.preparationTime), - Logging.synchronizerId(txAccepted.synchronizerId), - ) - } - - /** For each contract created in a transaction, a representative package exists in the - * Participant package store that is guaranteed to type-check the contract's argument. Such a - * package-id guarantee is required for ensuring correct rendering of contract create values on - * the gRPC/JSON Ledger API read queries. - */ - sealed trait RepresentativePackageId extends Product with Serializable - object RepresentativePackageId { - - /** Signals that the representative package-id of the created contract referenced in this - * transaction are the same as the contract's creation package-id. - */ - case object SameAsContractPackageId extends RepresentativePackageId - - final case class DedicatedRepresentativePackageId( - representativePackageId: Ref.PackageId - ) extends RepresentativePackageId - } - - final case class TransactionInfo( - blindingInfo: BlindingInfo, - executionOrder: Seq[TransactionTraversalUtils.NodeInfo], - statistics: TransactionNodeStatistics, - noOfNodes: Int, - noOfRootNodes: Int, - ) - object TransactionInfo { - def apply(transaction: CommittedTransaction): TransactionInfo = TransactionInfo( - blindingInfo = Blinding.blind(transaction), - executionOrder = TransactionTraversalUtils - .executionOrderTraversalForIngestion(transaction.transaction) - .toVector, - statistics = TransactionNodeStatistics( - transaction, - Set.empty[Ref.PackageId], - ), - noOfNodes = transaction.nodes.size, - noOfRootNodes = transaction.roots.length, - ) - } - } - - /** Information about a contract needed for indexing. - * - * @param persistedContractInstance - * The persisted contract. - * @param representativePackageId - * The representative package-id for the contract, if the contract is created in this - * transaction. See [[TransactionAccepted.RepresentativePackageId]] for more details. - */ - final case class ContractInfo( - persistedContractInstance: PersistedContractInstance, - representativePackageId: RepresentativePackageId, - ) { - def internalContractId: Long = persistedContractInstance.internalContractId - def contractAuthenticationData: Bytes = persistedContractInstance.inst.authenticationData - } - - final case class SequencedTransactionAccepted( - completionInfoO: Option[CompletionInfo], - transactionMeta: TransactionMeta, - transactionInfo: TransactionAccepted.TransactionInfo, - updateId: UpdateId, - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - acsChangeFactory: AcsChangeFactory, - contractInfos: Map[Value.ContractId, ContractInfo], - externalTransactionHash: Option[Hash] = None, - )(implicit override val traceContext: TraceContext) - extends TransactionAccepted - with SequencedEventUpdate - with AcsChangeSequencedUpdate { - override def isAcsDelta(contractId: Value.ContractId): Boolean = - acsChangeFactory.contractActivenessChanged(contractId) - - override protected def pretty: Pretty[TransactionAccepted] = - SequencedTransactionAccepted.pretty - } - - object SequencedTransactionAccepted extends PrettyUtil { - val pretty: Pretty[TransactionAccepted] = - prettyOfClass( - param("recordTime", _.recordTime), - param("updateId", _.updateId.tryAsLedgerTransactionId), - param("transactionMeta", _.transactionMeta), - paramIfDefined("completion", _.completionInfoO), - param("nodes", _.transactionInfo.noOfNodes), - param("roots", _.transactionInfo.noOfRootNodes), - indicateOmittedFields, - ) - } - - final case class RepairTransactionAccepted( - transactionMeta: TransactionMeta, - transactionInfo: TransactionAccepted.TransactionInfo, - updateId: UpdateId, - synchronizerId: SynchronizerId, - repairCounter: RepairCounter, - recordTime: CantonTimestamp, - contractInfos: Map[Value.ContractId, ContractInfo], - )(implicit override val traceContext: TraceContext) - extends TransactionAccepted - with RepairUpdate { - - override def externalTransactionHash: Option[Hash] = None - override def completionInfoO: Option[CompletionInfo] = None - - // Repair transactions have only contracts which affect the ACS. - override def isAcsDelta(contractId: Value.ContractId): Boolean = true - - override protected def pretty: Pretty[RepairTransactionAccepted] = - RepairTransactionAccepted.pretty - } - - object RepairTransactionAccepted extends PrettyUtil { - val pretty: Pretty[RepairTransactionAccepted] = - prettyOfClass( - param("recordTime", _.recordTime), - param("repairCounter", _.repairCounter), - param("updateId", _.updateId.tryAsLedgerTransactionId), - param("transactionMeta", _.transactionMeta), - paramIfDefined("completion", _.completionInfoO), - param("nodes", _.transactionInfo.noOfNodes), - param("roots", _.transactionInfo.noOfRootNodes), - indicateOmittedFields, - ) - } - - sealed trait ReassignmentAccepted extends SynchronizerUpdate { - - /** The information provided by the submitter of the command that created this reassignment. It - * must be provided if this participant hosts the submitter and shall output a completion event - * for this reassignment. This in particular applies if this participant has submitted the - * command to the [[SyncService]]. - */ - def optCompletionInfo: Option[CompletionInfo] - - /** Traffic cost paid by this node for the sequencing of the corresponding transaction event - */ - def paidTrafficCost: Option[NonNegativeLong] = optCompletionInfo.map(_.paidTrafficCost) - - /** A submitter-provided identifier used for monitoring and to traffic-shape the work handled by - * Daml applications - */ - def workflowId: Option[Ref.WorkflowId] - - /** A unique identifier for this update assigned by the ledger. - */ - def updateId: UpdateId - - /** Common part of all type of reassignments. - */ - def reassignmentInfo: ReassignmentInfo - - def reassignment: Reassignment.Batch - - def kind: String = if (reassignmentInfo.sourceSynchronizer.unwrap == synchronizerId) - "unassignment" - else "assignment" - } - - final case class SequencedReassignmentAccepted( - optCompletionInfo: Option[CompletionInfo], - workflowId: Option[Ref.WorkflowId], - updateId: UpdateId, - reassignmentInfo: ReassignmentInfo, - reassignment: Reassignment.Batch, - recordTime: CantonTimestamp, - override val synchronizerId: SynchronizerId, - acsChangeFactory: AcsChangeFactory, - )(implicit override val traceContext: TraceContext) - extends ReassignmentAccepted - with SequencedEventUpdate - with AcsChangeSequencedUpdate { - - override protected def pretty: Pretty[SequencedReassignmentAccepted] = - SequencedReassignmentAccepted.pretty - } - - object SequencedReassignmentAccepted extends PrettyUtil with ShowUtil { - val pretty: Pretty[SequencedReassignmentAccepted] = - prettyOfClass( - param("recordTime", _.recordTime), - param("updateId", _.updateId.tryAsLedgerTransactionId), - paramIfDefined("completion", _.optCompletionInfo), - param("source", _.reassignmentInfo.sourceSynchronizer), - param("target", _.reassignmentInfo.targetSynchronizer), - param("kind", _.kind.unquoted), - indicateOmittedFields, - ) - } - - final case class RepairReassignmentAccepted( - workflowId: Option[Ref.WorkflowId], - updateId: UpdateId, - reassignmentInfo: ReassignmentInfo, - reassignment: Reassignment.Batch, - repairCounter: RepairCounter, - recordTime: CantonTimestamp, - override val synchronizerId: SynchronizerId, - )(implicit override val traceContext: TraceContext) - extends ReassignmentAccepted - with RepairUpdate { - override def optCompletionInfo: Option[CompletionInfo] = None - - override protected def pretty: Pretty[RepairReassignmentAccepted] = - RepairReassignmentAccepted.pretty - } - - object RepairReassignmentAccepted extends PrettyUtil with ShowUtil { - val pretty: Pretty[RepairReassignmentAccepted] = - prettyOfClass( - param("recordTime", _.recordTime), - param("repairCounter", _.repairCounter), - param("updateId", _.updateId.tryAsLedgerTransactionId), - paramIfDefined("completion", _.optCompletionInfo), - param("source", _.reassignmentInfo.sourceSynchronizer), - param("target", _.reassignmentInfo.targetSynchronizer), - param("kind", _.kind.unquoted), - indicateOmittedFields, - ) - } - - final case class OnPRReassignmentAccepted( - workflowId: Option[Ref.WorkflowId], - updateId: UpdateId, - reassignmentInfo: ReassignmentInfo, - reassignment: Reassignment.Batch, - repairCounter: RepairCounter, - recordTime: CantonTimestamp, - override val synchronizerId: SynchronizerId, - acsChangeFactory: AcsChangeFactory, - )(implicit override val traceContext: TraceContext) - extends ReassignmentAccepted - with RepairUpdate - with AcsChangeSequencedUpdate { - override def optCompletionInfo: Option[CompletionInfo] = None - - override protected def pretty: Pretty[OnPRReassignmentAccepted] = - OnPRReassignmentAccepted.pretty - } - - object OnPRReassignmentAccepted extends PrettyUtil with ShowUtil { - val pretty: Pretty[OnPRReassignmentAccepted] = - prettyOfClass( - param("recordTime", _.recordTime), - param("repairCounter", _.repairCounter), - param("updateId", _.updateId.tryAsLedgerTransactionId), - paramIfDefined("completion", _.optCompletionInfo), - param("source", _.reassignmentInfo.sourceSynchronizer), - param("target", _.reassignmentInfo.targetSynchronizer), - param("kind", _.kind.unquoted), - indicateOmittedFields, - ) - } - - object ReassignmentAccepted { - implicit val `ReassignmentAccepted to LoggingValue`: ToLoggingValue[ReassignmentAccepted] = { - case reassignmentAccepted: ReassignmentAccepted => - LoggingValue.Nested.fromEntries( - Logging.recordTime(reassignmentAccepted.recordTime.toLf), - Logging.completionInfo(reassignmentAccepted.optCompletionInfo), - Logging.updateId(reassignmentAccepted.updateId), - Logging.workflowIdOpt(reassignmentAccepted.workflowId), - Logging.optionalTrafficCost(reassignmentAccepted.paidTrafficCost), - ) - } - } - - /** Signal that a command submitted via [[SyncService]] was rejected. - */ - sealed trait CommandRejected extends SynchronizerUpdate { - - /** The completion information for the submission - */ - def completionInfo: CompletionInfo - - /** A template for generating the gRPC status code with error details. See ``error.proto`` for - * the status codes of common rejection reasons. - */ - def reasonTemplate: RejectionReasonTemplate - - /** If true, the deduplication guarantees apply to this rejection. The participant state - * implementations should strive to set this flag to true as often as possible so that - * applications get better guarantees. - */ - final def definiteAnswer: Boolean = reasonTemplate.definiteAnswer - - override protected def pretty: Pretty[CommandRejected] = - CommandRejected.pretty - - def isTransaction: Boolean - } - - final case class SequencedCommandRejected( - completionInfo: CompletionInfo, - reasonTemplate: RejectionReasonTemplate, - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - isTransaction: Boolean, - )(implicit override val traceContext: TraceContext) - extends CommandRejected - with SequencedEventUpdate - - final case class UnSequencedCommandRejected( - completionInfo: CompletionInfo, - reasonTemplate: RejectionReasonTemplate, - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - messageUuid: UUID, - isTransaction: Boolean, - )(implicit override val traceContext: TraceContext) - extends CommandRejected - with FloatingUpdate - - object CommandRejected extends PrettyUtil with ShowUtil { - - implicit val `CommandRejected to LoggingValue`: ToLoggingValue[CommandRejected] = { - case commandRejected: CommandRejected => - LoggingValue.Nested.fromEntries( - Logging.recordTime(commandRejected.recordTime.toLf), - Logging.submitter(commandRejected.completionInfo.actAs), - Logging.userId(commandRejected.completionInfo.userId), - Logging.commandId(commandRejected.completionInfo.commandId), - Logging.deduplicationPeriod(commandRejected.completionInfo.optDeduplicationPeriod), - Logging.rejectionReason(commandRejected.reasonTemplate), - Logging.synchronizerId(commandRejected.synchronizerId), - ) - } - - val pretty: Pretty[CommandRejected] = - prettyOfClass( - param("recordTime", _.recordTime), - param("completion", _.completionInfo), - paramIfTrue("definiteAnswer", _.definiteAnswer), - param("reason", _.reasonTemplate.message.singleQuoted), - param("synchronizerId", _.synchronizerId.uid), - ) - - /** A template for generating gRPC status codes. - */ - sealed trait RejectionReasonTemplate { - - /** A human-readable description of the error */ - def message: String - - /** A gRPC status code representing the error. */ - def code: Int - - /** A protobuf gRPC status representing the error. */ - def status: RpcStatus - - /** Whether the rejection is a definite answer for the deduplication guarantees specified for - * [[Update]]. - */ - def definiteAnswer: Boolean - } - - object RejectionReasonTemplate { - implicit val `RejectionReasonTemplate to LoggingValue` - : ToLoggingValue[RejectionReasonTemplate] = - reason => - LoggingValue.Nested.fromEntries( - "code" -> reason.code, - "message" -> reason.message, - "definiteAnswer" -> reason.definiteAnswer, - ) - } - - /** The status code for the command rejection. */ - final case class FinalReason(override val status: RpcStatus) extends RejectionReasonTemplate { - override def message: String = status.message - - override def code: Int = status.code - - override def definiteAnswer: Boolean = GrpcStatuses.isDefiniteAnswer(status) - } - } - - final case class SequencerIndexMoved( - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - )(implicit override val traceContext: TraceContext) - extends SequencedUpdate { - override protected def pretty: Pretty[SequencerIndexMoved] = - prettyOfClass( - param("synchronizerId", _.synchronizerId.uid), - param("sequencerTimestamp", _.recordTime), - ) - } - - object SequencerIndexMoved extends PrettyUtil { - implicit val `SequencerIndexMoved to LoggingValue`: ToLoggingValue[SequencerIndexMoved] = - seqIndexMoved => - LoggingValue.Nested.fromEntries( - Logging.synchronizerId(seqIndexMoved.synchronizerId), - "sequencerTimestamp" -> seqIndexMoved.recordTime.toInstant, - ) - - val pretty: Pretty[SequencerIndexMoved] = - prettyOfClass( - param("synchronizerId", _.synchronizerId.uid), - param("sequencerTimestamp", _.recordTime), - ) - } - - final case class LsuTimeReached( - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - )(implicit override val traceContext: TraceContext) - extends FloatingUpdate { - override protected def pretty: Pretty[LsuTimeReached] = - LsuTimeReached.pretty - } - - object LsuTimeReached extends PrettyUtil { - implicit val `LsuTimeReached to LoggingValue`: ToLoggingValue[LsuTimeReached] = - lsuTimeReached => - LoggingValue.Nested.fromEntries( - Logging.synchronizerId(lsuTimeReached.synchronizerId), - "sequencerTimestamp" -> lsuTimeReached.recordTime.toInstant, - ) - - val pretty: Pretty[LsuTimeReached] = - prettyOfClass( - param("synchronizerId", _.synchronizerId.uid), - param("sequencerTimestamp", _.recordTime), - ) - } - - final case class EmptyAcsPublicationRequired( - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - )(implicit override val traceContext: TraceContext) - extends FloatingUpdate { - override protected def pretty: Pretty[EmptyAcsPublicationRequired] = - EmptyAcsPublicationRequired.pretty - } - - object EmptyAcsPublicationRequired extends PrettyUtil { - implicit val `EmptyAcsPublicationRequired to LoggingValue` - : ToLoggingValue[EmptyAcsPublicationRequired] = - emptyAcsPublicationRequired => - LoggingValue.Nested.fromEntries( - Logging.synchronizerId(emptyAcsPublicationRequired.synchronizerId), - "sequencerTimestamp" -> emptyAcsPublicationRequired.recordTime.toInstant, - ) - - val pretty: Pretty[EmptyAcsPublicationRequired] = - prettyOfClass( - param("synchronizerId", _.synchronizerId.uid), - param("sequencerTimestamp", _.recordTime), - ) - } - - final case class CommitRepair()(implicit override val traceContext: TraceContext) extends Update { - val persisted: Promise[Unit] = Promise() - - override protected def pretty: Pretty[CommitRepair] = prettyOfClass() - } - - implicit val `Update to LoggingValue`: ToLoggingValue[Update] = { - case update: TopologyTransactionEffective => - TopologyTransactionEffective.`TopologyTransactionEffective to LoggingValue`.toLoggingValue( - update - ) - case update: TransactionAccepted => - TransactionAccepted.`TransactionAccepted to LoggingValue`.toLoggingValue(update) - case update: CommandRejected => - CommandRejected.`CommandRejected to LoggingValue`.toLoggingValue(update) - case update: ReassignmentAccepted => - ReassignmentAccepted.`ReassignmentAccepted to LoggingValue`.toLoggingValue(update) - case update: EmptyAcsPublicationRequired => - EmptyAcsPublicationRequired.`EmptyAcsPublicationRequired to LoggingValue`.toLoggingValue( - update - ) - case update: LsuTimeReached => - LsuTimeReached.`LsuTimeReached to LoggingValue` - .toLoggingValue( - update - ) - case update: SequencerIndexMoved => - SequencerIndexMoved.`SequencerIndexMoved to LoggingValue`.toLoggingValue(update) - case _: CommitRepair => - LoggingValue.Empty - } - - private object Logging { - def recordTime(timestamp: Timestamp): LoggingEntry = - "recordTime" -> timestamp.toInstant - - def submissionId(id: Ref.SubmissionId): LoggingEntry = - "submissionId" -> id - - def submissionIdOpt(id: Option[Ref.SubmissionId]): LoggingEntry = - "submissionId" -> id - - def participantId(id: Ref.ParticipantId): LoggingEntry = - "participantId" -> id - - def commandId(id: Ref.CommandId): LoggingEntry = - "commandId" -> id - - def party(party: Ref.Party): LoggingEntry = - "party" -> party - - def updateId(id: UpdateId): LoggingEntry = - "updateId" -> id.toHexString - - def userId(id: Ref.UserId): LoggingEntry = - "userId" -> id - - def workflowIdOpt(id: Option[Ref.WorkflowId]): LoggingEntry = - "workflowId" -> id - - def ledgerTime(time: Timestamp): LoggingEntry = - "ledgerTime" -> time.toInstant - - def preparationTime(time: Timestamp): LoggingEntry = - "preparationTime" -> time.toInstant - - def deduplicationPeriod(period: Option[DeduplicationPeriod]): LoggingEntry = - "deduplicationPeriod" -> period - - def rejectionReason(rejectionReason: String): LoggingEntry = - "rejectionReason" -> rejectionReason - - def rejectionReason( - rejectionReasonTemplate: CommandRejected.RejectionReasonTemplate - ): LoggingEntry = - "rejectionReason" -> rejectionReasonTemplate - - def submitter(parties: List[Ref.Party]): LoggingEntry = - "submitter" -> parties - - def completionInfo(info: Option[CompletionInfo]): LoggingEntry = - "completion" -> info - - def synchronizerId(synchronizerId: SynchronizerId): LoggingEntry = - "synchronizerId" -> synchronizerId.toString - - def trafficCost(trafficCost: NonNegativeLong): LoggingEntry = - "trafficCost" -> trafficCost.value.toString - - def optionalTrafficCost(trafficCost: Option[NonNegativeLong]): LoggingEntry = - "trafficCost" -> trafficCost.map(_.value.toString) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/ContractStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/ContractStore.scala deleted file mode 100644 index b353de13b7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/ContractStore.scala +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.transaction.GlobalKey -import com.digitalasset.daml.lf.value.Value.ContractId - -import scala.concurrent.Future - -/** Meant be used for optimistic contract lookups before command submission. - */ -trait ContractStore { - - /** Looking up an active contract. - */ - def lookupActiveContract( - readers: Set[Ref.Party], - contractId: ContractId, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[LfFatContractInst]] - - def lookupContractKey(readers: Set[Ref.Party], key: GlobalKey)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ContractId]] - - def lookupNonUniqueContractKey( - readers: Set[Ref.Party], - key: GlobalKey, - pageToken: Option[Long], - limit: Int, - )(implicit loggingContext: LoggingContextWithTrace): Future[ContractKeyPage] - - /** Querying the state of the contracts. - */ - def lookupContractState( - contractId: ContractId - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[ContractState] -} - -final case class ContractKeyPage( - contracts: Vector[LfFatContractInst], - nextPageToken: Option[Long], -) - -sealed trait ContractState { - def toContractOption: Option[LfFatContractInst] = None -} - -sealed trait ContractStateStatus extends Product with Serializable { - def isActive: Boolean = this match { - case ContractStateStatus.Active => true - case _ => false - } - - def isArchived: Option[Boolean] = this match { - case ContractStateStatus.Archived => Some(true) - case ContractStateStatus.Active => Some(false) - case ContractStateStatus.NotFound => None - } -} -object ContractStateStatus { - case object NotFound extends ContractStateStatus - sealed trait ExistingContractStatus extends ContractStateStatus - case object Archived extends ExistingContractStatus - case object Active extends ExistingContractStatus -} - -object ContractState { - case object NotFound extends ContractState - case object Archived extends ContractState - final case class Active(contractInstance: LfFatContractInst) extends ContractState { - override def toContractOption: Option[LfFatContractInst] = Some(contractInstance) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexActiveContractsService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexActiveContractsService.scala deleted file mode 100644 index 576c5458f9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexActiveContractsService.scala +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.EventFormat -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.logging.LoggingContextWithTrace -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -/** Serves as a backend to implement - * [[com.daml.ledger.api.v2.state_service.StateServiceGrpc.StateService]] - */ -trait IndexActiveContractsService { - - def getActiveContracts( - eventFormat: EventFormat, - activeAt: Option[Offset], - rangeInfo: AcsRangeInfo, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[GetActiveContractsResponse, NotUsed] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexCompletionsService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexCompletionsService.scala deleted file mode 100644 index f9c03f20ad..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexCompletionsService.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Ref -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -/** Serves as a backend to implement - * [[com.daml.ledger.api.v2.command_completion_service.CommandCompletionServiceGrpc.CommandCompletionService]] - */ -trait IndexCompletionsService extends LedgerEndService { - def getCompletions( - begin: Option[Offset], - userId: Ref.UserId, - parties: Set[Ref.Party], - )(implicit loggingContext: LoggingContextWithTrace): Source[CompletionStreamResponse, NotUsed] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexEventQueryService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexEventQueryService.scala deleted file mode 100644 index 5e0885c160..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexEventQueryService.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.daml.ledger.api.v2.event_query_service.GetEventsByContractIdResponse -import com.digitalasset.canton.ledger.api.EventFormat -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.value.Value.ContractId - -import scala.concurrent.Future - -/** Serves as a backend to implement - * [[com.daml.ledger.api.v2.event_query_service.EventQueryServiceGrpc.EventQueryService]] - */ -trait IndexEventQueryService extends LedgerEndService { - - def getEventsByContractId( - contractId: ContractId, - eventFormat: EventFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractIdResponse] - - // TODO(i16065): Re-enable getEventsByContractKey tests -// def getEventsByContractKey( -// contractKey: Value, -// templateId: Ref.Identifier, -// requestingParties: Set[Ref.Party], -// endExclusiveSeqId: Option[Long], -// )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractKeyResponse] - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexParticipantPruningService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexParticipantPruningService.scala deleted file mode 100644 index d1d7b9f88b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexParticipantPruningService.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.LoggingContextWithTrace - -import scala.concurrent.Future - -/** Serves as a backend to implement ParticipantPruningService. - */ -trait IndexParticipantPruningService { - def prune( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusive: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Unit] - - def indexDbPrunedUpto(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] - - def isPruningInProgress: Boolean -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexPartyManagementService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexPartyManagementService.scala deleted file mode 100644 index 8578404b66..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexPartyManagementService.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Ref.{ParticipantId, Party} - -import scala.concurrent.Future - -/** Serves as a backend to implement - * [[com.daml.ledger.api.v2.admin.party_management_service.PartyManagementServiceGrpc]] - */ -trait IndexPartyManagementService { - - def getParticipantId(): Future[ParticipantId] - - def getParties( - parties: Seq[Party] - )(implicit loggingContext: LoggingContextWithTrace): Future[List[IndexerPartyDetails]] - - def listKnownParties( - fromExcl: Option[Party], - filterString: Option[String185], - maxResults: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexService.scala deleted file mode 100644 index 2dde176983..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexService.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.digitalasset.canton.health.ReportsHealth - -trait IndexService - extends IndexCompletionsService - with IndexUpdateService - with IndexEventQueryService - with IndexActiveContractsService - with ContractStore - with MaximumLedgerTimeService - with IndexPartyManagementService - with IndexParticipantPruningService - with ReportsHealth diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexUpdateService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexUpdateService.scala deleted file mode 100644 index c789b65bd9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexUpdateService.scala +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.daml.ledger.api.v2.update_service.{ - GetUpdateResponse, - GetUpdatesPageResponse, - GetUpdatesResponse, -} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.UpdateFormat -import com.digitalasset.canton.ledger.api.messages.update.GetUpdatesPageRequest -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.Future - -/** Serves as a backend to implement - * [[com.daml.ledger.api.v2.update_service.UpdateServiceGrpc.UpdateService]] - */ -trait IndexUpdateService extends LedgerEndService { - def updates( - begin: Option[Offset], - endAt: Option[Offset], - updateFormat: UpdateFormat, - descendingOrder: Boolean, - skipPruningChecks: Boolean, - )(implicit loggingContext: LoggingContextWithTrace): Source[GetUpdatesResponse, NotUsed] - - def getUpdateBy( - lookupKey: LookupKey, - updateFormat: UpdateFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] - - def latestPrunedOffset()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] - - def updatesPage( - getUpdatesPageRequest: GetUpdatesPageRequest - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[GetUpdatesPageResponse] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexerPartyDetails.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexerPartyDetails.scala deleted file mode 100644 index 4851cc6e6e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/IndexerPartyDetails.scala +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.digitalasset.daml.lf.data.Ref - -/** Represents a party with additional known information. - * - * @param party - * The stable unique identifier of a Daml party. - * @param isLocal - * True if party is hosted by the backing participant. - */ -final case class IndexerPartyDetails( - party: Ref.Party, - isLocal: Boolean, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/LedgerEndService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/LedgerEndService.scala deleted file mode 100644 index f7d319c182..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/LedgerEndService.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.digitalasset.canton.data.Offset - -import scala.concurrent.Future - -/** Serves as a backend to implement ledger end related API calls. - */ -trait LedgerEndService { - def currentLedgerEnd(): Future[Option[Offset]] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/MaximumLedgerTimeService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/MaximumLedgerTimeService.scala deleted file mode 100644 index fe6681cddd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/index/MaximumLedgerTimeService.scala +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.index - -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.value.Value.ContractId - -import scala.concurrent.Future - -trait MaximumLedgerTimeService { - - /** This method serves two purposes: - * 1. Verify that none of the specified contracts are archived - * 1. Calculate the maximum ledger time of all the specified contracts - * - * Important note: existence of the contracts is not checked, only the fact of archival, - * therefore this method is intended to be used after interpretation, which guarantees that all - * the used ids were visible once. - * - * @return - * NotAvailable, if the specified set is empty. Max, if none of the specified contracts are - * archived, and the maximum ledger-time of all specified contracts is known Archived, if there - * was at least one contract specified which is archived (this list is not necessarily - * exhaustive) - */ - def lookupMaximumLedgerTimeAfterInterpretation(ids: Set[ContractId])(implicit - loggingContext: LoggingContextWithTrace - ): Future[MaximumLedgerTime] -} - -/** The outcome of determining the maximum ledger time of a set of contracts. - */ -sealed trait MaximumLedgerTime - -object MaximumLedgerTime { - - /** None of the contracts is archived, but none has a known ledger time (also when no contracts - * specified). - */ - case object NotAvailable extends MaximumLedgerTime - - /** None of the contracts is archived, and this is the maximum of the known ledger times. */ - final case class Max(ledgerTime: Timestamp) extends MaximumLedgerTime - - /** The given contracts are archived. The remaining contracts may or may not have a known ledger - * time. At least one contract specified which is archived, but this Set is not necessarily - * exhaustive. - */ - final case class Archived(contracts: Set[ContractId]) extends MaximumLedgerTime - - def from(optionalMaximumLedgerTime: Option[Timestamp]): MaximumLedgerTime = - optionalMaximumLedgerTime.fold[MaximumLedgerTime](NotAvailable)(Max.apply) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/metrics/TimedSyncService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/metrics/TimedSyncService.scala deleted file mode 100644 index 142fa666a2..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/metrics/TimedSyncService.scala +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state.metrics - -import cats.data.EitherT -import com.daml.metrics.Timed -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.crypto.HashOps -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.error.{TransactionError, TransactionRoutingError} -import com.digitalasset.canton.health.HealthStatus -import com.digitalasset.canton.ledger.api.{ - EnrichedVettedPackages, - ListVettedPackagesOpts, - UpdateVettedPackagesOpts, - UploadDarVettingChange, -} -import com.digitalasset.canton.ledger.participant.state.* -import com.digitalasset.canton.ledger.participant.state.SyncService.{ - ConnectedSynchronizerRequest, - ConnectedSynchronizerResponse, - SubmissionCostEstimation, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.services.command.interactive.CostEstimationHints -import com.digitalasset.canton.protocol.{ - LfContractId, - LfFatContractInst, - LfSubmittedTransaction, - LfVersionedTransaction, -} -import com.digitalasset.canton.scheduler.SafeToPruneCommitmentState -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.topology.{ - ExternalPartyOnboardingDetails, - ParticipantId, - PartyId, - PhysicalSynchronizerId, - SynchronizerId, -} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.{LfGlobalKeyMapping, LfPartyId} -import com.digitalasset.daml.lf.archive.DamlLf.Archive -import com.digitalasset.daml.lf.data.Ref.PackageId -import com.digitalasset.daml.lf.data.{ImmArray, Ref} -import com.digitalasset.daml.lf.transaction.SubmittedTransaction -import com.google.protobuf.ByteString - -import scala.concurrent.Future - -final class TimedSyncService(delegate: SyncService, metrics: LedgerApiServerMetrics) - extends SyncService { - - import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.TimerOnShutdownSyntax - - override def submitTransaction( - transaction: SubmittedTransaction, - synchronizerRank: SynchronizerRank, - routingSynchronizerState: RoutingSynchronizerState, - submitterInfo: SubmitterInfo, - transactionMeta: TransactionMeta, - // Currently, the estimated interpretation cost is not used - _estimatedInterpretationCost: Long, - keyResolver: LfGlobalKeyMapping, - processedDisclosedContracts: ImmArray[LfFatContractInst], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] = - Timed.timedAndTrackedFuture( - metrics.services.write.submitTransaction, - metrics.services.write.submitTransactionRunning, - delegate.submitTransaction( - transaction, - synchronizerRank, - routingSynchronizerState, - submitterInfo, - transactionMeta, - _estimatedInterpretationCost, - keyResolver, - processedDisclosedContracts, - ), - ) - - def submitReassignment( - submitter: Ref.Party, - userId: Ref.UserId, - commandId: Ref.CommandId, - submissionId: Option[Ref.SubmissionId], - workflowId: Option[Ref.WorkflowId], - reassignmentCommands: Seq[ReassignmentCommand], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] = - Timed.timedAndTrackedFuture( - metrics.services.write.submitReassignment, - metrics.services.write.submitReassignmentRunning, - delegate.submitReassignment( - submitter, - userId, - commandId, - submissionId, - workflowId, - reassignmentCommands, - ), - ) - - override def uploadDar( - dar: Seq[ByteString], - submissionId: Ref.SubmissionId, - vettingChange: UploadDarVettingChange, - synchronizerId: Option[SynchronizerId], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] = - Timed.future( - metrics.services.write.uploadPackages, - delegate.uploadDar(dar, submissionId, vettingChange, synchronizerId), - ) - - override def allocateParty( - partyId: PartyId, - submissionId: Ref.SubmissionId, - synchronizerIdO: Option[SynchronizerId], - externalPartyOnboardingDetails: Option[ExternalPartyOnboardingDetails], - )(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[SubmissionResult] = - Timed.futureUS( - metrics.services.write.allocateParty, - delegate.allocateParty(partyId, submissionId, synchronizerIdO, externalPartyOnboardingDetails), - ) - - override def prune( - pruneUpToInclusive: Offset, - safeToPruneCommitmentState: Option[SafeToPruneCommitmentState], - )(implicit traceContext: TraceContext): Future[PruningResult] = - Timed.future( - metrics.services.write.prune, - delegate.prune(pruneUpToInclusive, safeToPruneCommitmentState), - ) - - override def currentHealth(): HealthStatus = - delegate.currentHealth() - - override def getConnectedSynchronizers( - request: ConnectedSynchronizerRequest - )(implicit traceContext: TraceContext): FutureUnlessShutdown[ConnectedSynchronizerResponse] = - Timed.futureUS( - metrics.services.read.getConnectedSynchronizers, - delegate.getConnectedSynchronizers(request), - ) - - override def incompleteReassignmentOffsets(validAt: Offset, stakeholders: Set[LfPartyId])(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[Vector[Offset]] = - Timed.futureUS( - metrics.services.read.getConnectedSynchronizers, - delegate.incompleteReassignmentOffsets(validAt, stakeholders), - ) - - override def registerInternalIndexService(internalIndexService: InternalIndexService): Unit = - delegate.registerInternalIndexService(internalIndexService) - - override def internalIndexService: Option[InternalIndexService] = - delegate.internalIndexService - - override def unregisterInternalIndexService(): Unit = - delegate.unregisterInternalIndexService() - - override def getPackageMetadataSnapshot(implicit - errorLoggingContext: ErrorLoggingContext - ): PackageMetadata = - delegate.getPackageMetadataSnapshot - - override def listLfPackages()(implicit - traceContext: TraceContext - ): Future[Seq[PackageDescription]] = - Timed.future( - metrics.services.read.listLfPackages, - delegate.listLfPackages(), - ) - - override def getLfArchive( - packageId: PackageId - )(implicit traceContext: TraceContext): Future[Option[Archive]] = - Timed.future( - metrics.services.read.getLfArchive, - delegate.getLfArchive(packageId), - ) - - override def validateDar( - dar: ByteString, - darName: String, - synchronizerId: Option[SynchronizerId], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] = - Timed.future( - metrics.services.read.validateDar, - delegate.validateDar(dar, darName, synchronizerId), - ) - - override def updateVettedPackages( - opts: UpdateVettedPackagesOpts - )(implicit - traceContext: TraceContext - ): Future[(Option[EnrichedVettedPackages], Option[EnrichedVettedPackages])] = - Timed.future( - metrics.services.write.updateVettedPackages, - delegate.updateVettedPackages(opts), - ) - - override def listVettedPackages( - opts: ListVettedPackagesOpts - )(implicit - traceContext: TraceContext - ): Future[Seq[EnrichedVettedPackages]] = - Timed.future( - metrics.services.read.listVettedPackages, - delegate.listVettedPackages(opts), - ) - - override def computePartyVettingMap( - submitters: Set[LfPartyId], - informees: Set[LfPartyId], - vettingValidityTimestamp: CantonTimestamp, - prescribedSynchronizer: Option[SynchronizerId], - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[Map[PhysicalSynchronizerId, Map[LfPartyId, Set[PackageId]]]] = - Timed.futureUS( - metrics.services.read.computePartyVettingMap, - delegate.computePartyVettingMap( - submitters, - informees, - vettingValidityTimestamp, - prescribedSynchronizer, - routingSynchronizerState, - ), - ) - - override def computeHighestRankedSynchronizerFromAdmissible( - submitterInfo: SubmitterInfo, - transaction: LfSubmittedTransaction, - transactionMeta: TransactionMeta, - admissibleSynchronizers: NonEmpty[Set[PhysicalSynchronizerId]], - disclosedContractIds: List[LfContractId], - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, TransactionRoutingError, PhysicalSynchronizerId] = - EitherT( - Timed.futureUS( - metrics.services.read.computeHighestRankedSynchronizerFromAdmissible, - delegate - .computeHighestRankedSynchronizerFromAdmissible( - submitterInfo, - transaction, - transactionMeta, - admissibleSynchronizers, - disclosedContractIds, - routingSynchronizerState, - ) - .value, - ) - ) - - override def selectRoutingSynchronizer( - submitterInfo: SubmitterInfo, - transaction: LfSubmittedTransaction, - transactionMeta: TransactionMeta, - disclosedContractIds: List[LfContractId], - optSynchronizerId: Option[SynchronizerId], - transactionUsedForExternalSigning: Boolean, - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, TransactionError, SynchronizerRank] = - EitherT( - Timed.futureUS( - metrics.services.read.selectRoutingSynchronizer, - delegate - .selectRoutingSynchronizer( - submitterInfo, - transaction, - transactionMeta, - disclosedContractIds, - optSynchronizerId, - transactionUsedForExternalSigning, - routingSynchronizerState, - ) - .value, - ) - ) - - override def getRoutingSynchronizerState(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[RoutingSynchronizerState] = - delegate.getRoutingSynchronizerState - - override def estimateTrafficCost( - synchronizerId: SynchronizerId, - transaction: LfVersionedTransaction, - transactionMetadata: TransactionMeta, - submitterInfo: SubmitterInfo, - keyResolver: LfGlobalKeyMapping, - disclosedContracts: Map[LfContractId, LfFatContractInst], - costHints: CostEstimationHints, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, String, SubmissionCostEstimation] = - delegate.estimateTrafficCost( - synchronizerId, - transaction, - transactionMetadata, - submitterInfo, - keyResolver, - disclosedContracts, - costHints, - ) - - override def physicalSynchronizerIdForSynchronizerId( - synchronizerId: SynchronizerId - ): Option[PhysicalSynchronizerId] = - delegate.physicalSynchronizerIdForSynchronizerId(synchronizerId) - - override def hashOps: HashOps = delegate.hashOps - - override def participantId: ParticipantId = delegate.participantId - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/package.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/package.scala deleted file mode 100644 index 98b863c907..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/participant/state/package.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant - -/** Interfaces to read from and write to an (abstract) participant state. - * - * A Daml ledger participant is code that allows to actively participate in the evolution of a - * shared Daml ledger. Each such participant maintains a particular view onto the state of the Daml - * ledger. We call this view the participant state. - * - * Actual implementations of a Daml ledger participant will likely maintain more state than what is - * exposed through the interfaces in this package, which is why we talk about an abstract - * participant state. It abstracts over the different implementations of Daml ledger participants. - * - * The interfaces are optimized for easy implementation. The [[SyncService]] interface contains the - * methods for changing the participant state (and potentially the state of the Daml ledger), which - * all ledger participants must support. These methods are for example exposed via the Daml Ledger - * API. Actual ledger participant implementations likely support more implementation-specific - * methods. They are however not exposed via the Daml Ledger API. - */ -package object state diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/runner/common/OptConfigValue.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/runner/common/OptConfigValue.scala deleted file mode 100644 index d8b7cd0184..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/runner/common/OptConfigValue.scala +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.runner.common - -import com.typesafe.config.{ConfigObject, ConfigValue, ConfigValueFactory} -import pureconfig.error.{ConfigReaderFailures, UnknownKey} -import pureconfig.generic.ProductHint -import pureconfig.{ConfigConvert, ConfigCursor, ConfigObjectCursor, ConfigReader, ConfigWriter} - -object OptConfigValue { - val enabledKey = "enabled" - - /** Reads configuration object of `T` and `enabled` flag to find out if this object has values. - */ - def optReaderEnabled[T](reader: ConfigReader[T]): ConfigReader[Option[T]] = - (cursor: ConfigCursor) => - for { - objCur <- cursor.asObjectCursor - enabledCur <- objCur.atKey(enabledKey) - enabled <- enabledCur.asBoolean - value <- - if (enabled) { - reader.from(cursor).map(x => Some(x)) - } else { - Right(None) - } - } yield value - - /** Writes object of `T` and adds `enabled` flag for configuration which contains value. - */ - def optWriterEnabled[T](writer: ConfigWriter[T]): ConfigWriter[Option[T]] = { - import scala.jdk.CollectionConverters.* - def toConfigValue(enabled: Boolean) = - ConfigValueFactory.fromMap(Map(enabledKey -> enabled).asJava) - - (optValue: Option[T]) => - optValue match { - case Some(value) => - writer.to(value) match { - // if serialised object of `T` is `ConfigObject` and - // has `enabled` inside, it cannot be supported by this writer - case configObject: ConfigObject if configObject.toConfig.hasPath(enabledKey) => - throw new IllegalArgumentException( - s"Ambiguous configuration, object contains `$enabledKey` flag" - ) - case _ => - writer.to(value).withFallback(toConfigValue(enabled = true)) - } - case None => toConfigValue(enabled = false) - } - } - - def optConvertEnabled[T]( - reader: ConfigReader[T], - writer: ConfigWriter[T], - ): ConfigConvert[Option[T]] = - ConfigConvert.apply(optReaderEnabled(reader), optWriterEnabled(writer)) - - def optConvertEnabled[T](convert: ConfigConvert[T]): ConfigConvert[Option[T]] = - optConvertEnabled(convert, convert) - - class OptProductHint[T](allowUnknownKeys: Boolean) extends ProductHint[T] { - val hint = ProductHint[T](allowUnknownKeys = allowUnknownKeys) - - override def from(cursor: ConfigObjectCursor, fieldName: String): ProductHint.Action = - hint.from(cursor, fieldName) - - override def bottom( - cursor: ConfigObjectCursor, - usedFields: Set[String], - ): Option[ConfigReaderFailures] = if (allowUnknownKeys) - None - else { - val unknownKeys = cursor.map.toList.collect { - case (k, keyCur) if !usedFields.contains(k) && k != enabledKey => - keyCur.failureFor(UnknownKey(k)) - } - unknownKeys match { - case h :: t => Some(ConfigReaderFailures(h, t*)) - case Nil => None - } - } - - override def to(value: Option[ConfigValue], fieldName: String): Option[(String, ConfigValue)] = - hint.to(value, fieldName) - } - - def optProductHint[T](allowUnknownKeys: Boolean): OptProductHint[T] = new OptProductHint[T]( - allowUnknownKeys = allowUnknownKeys - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/runner/common/PureConfigReaderWriter.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/runner/common/PureConfigReaderWriter.scala deleted file mode 100644 index 9b4f21928f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/runner/common/PureConfigReaderWriter.scala +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.runner.common - -import com.daml.jwt.JwtTimestampLeeway -import com.daml.ports.Port -import com.digitalasset.canton.ledger.runner.common.OptConfigValue.{ - optConvertEnabled, - optProductHint, -} -import com.digitalasset.canton.platform.apiserver.SeedService.Seeding -import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig -import com.digitalasset.canton.platform.config.{ - ActiveContractsServiceStreamsConfig, - CommandServiceConfig, - IdentityProviderManagementConfig, - IndexServiceConfig, - PackageServiceConfig, - PartyManagementServiceConfig, - UpdatesStreamsConfig, - UserManagementServiceConfig, -} -import com.digitalasset.canton.platform.indexer.IndexerConfig -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.indexer.ha.HaConfig -import com.digitalasset.canton.platform.store.DbSupport.{ - ConnectionPoolConfig, - DataSourceProperties, - ParticipantDataSourceConfig, -} -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig.SynchronousCommitValue -import com.digitalasset.daml.lf.data.Ref -import pureconfig.configurable.{genericMapReader, genericMapWriter} -import pureconfig.error.CannotConvert -import pureconfig.generic.ProductHint -import pureconfig.generic.semiauto.* -import pureconfig.{ConfigConvert, ConfigReader, ConfigWriter} - -import scala.concurrent.duration.{Duration, FiniteDuration} -import scala.jdk.DurationConverters.{JavaDurationOps, ScalaDurationOps} - -class PureConfigReaderWriter(secure: Boolean = true) { - - private val ReplaceSecretWithString = "" - - implicit val javaDurationWriter: ConfigWriter[java.time.Duration] = - ConfigWriter.stringConfigWriter.contramap[java.time.Duration] { duration => - duration.toScala.toString() - } - - implicit val javaDurationReader: ConfigReader[java.time.Duration] = - ConfigReader.fromString[java.time.Duration] { str => - Some(Duration.apply(str)) - .collect { case d: FiniteDuration => d } - .map(_.toJava) - .toRight(CannotConvert(str, Duration.getClass.getName, s"Could not convert $str")) - } - - implicit val portReader: ConfigReader[Port] = ConfigReader.intConfigReader.map(Port.apply) - implicit val portWriter: ConfigWriter[Port] = ConfigWriter.intConfigWriter.contramap[Port] { - _.value - } - - implicit val seedingReader: ConfigReader[Seeding] = - // Not using deriveEnumerationReader[Seeding] as we prefer "testing-static" over static (that appears - // in Seeding.name, but not in the case object name). - ConfigReader.fromString[Seeding] { - case Seeding.Strong.name => Right(Seeding.Strong) - case Seeding.Weak.name => Right(Seeding.Weak) - case Seeding.Static.name => Right(Seeding.Static) - case unknownSeeding => - Left( - CannotConvert( - unknownSeeding, - Seeding.getClass.getName, - s"Seeding is neither ${Seeding.Strong.name}, ${Seeding.Weak.name}, nor ${Seeding.Static.name}: $unknownSeeding", - ) - ) - } - - implicit val seedingWriter: ConfigWriter[Seeding] = ConfigWriter.toString(_.name) - - implicit val userManagementServiceConfigHint: ProductHint[UserManagementServiceConfig] = - ProductHint[UserManagementServiceConfig](allowUnknownKeys = false) - - implicit val userManagementServiceConfigConvert: ConfigConvert[UserManagementServiceConfig] = - deriveConvert[UserManagementServiceConfig] - - implicit val partyManagementServiceConfigHint: ProductHint[PartyManagementServiceConfig] = - ProductHint[PartyManagementServiceConfig](allowUnknownKeys = false) - - implicit val partyManagementServiceConfigConvert: ConfigConvert[PartyManagementServiceConfig] = - deriveConvert[PartyManagementServiceConfig] - - implicit val packageServiceConfigHint: ProductHint[PackageServiceConfig] = - ProductHint[PackageServiceConfig](allowUnknownKeys = false) - - implicit val packageServiceConfigConvert: ConfigConvert[PackageServiceConfig] = - deriveConvert[PackageServiceConfig] - - implicit val identityProviderManagementConfigHint: ProductHint[IdentityProviderManagementConfig] = - ProductHint[IdentityProviderManagementConfig](allowUnknownKeys = false) - - implicit val identityProviderManagementConfigConvert - : ConfigConvert[IdentityProviderManagementConfig] = - deriveConvert[IdentityProviderManagementConfig] - - implicit val jwtTimestampLeewayConfigHint: OptConfigValue.OptProductHint[JwtTimestampLeeway] = - optProductHint[JwtTimestampLeeway](allowUnknownKeys = false) - - implicit val jwtTimestampLeewayConfigConvert: ConfigConvert[Option[JwtTimestampLeeway]] = - optConvertEnabled(deriveConvert[JwtTimestampLeeway]) - - implicit val commandConfigurationHint: ProductHint[CommandServiceConfig] = - ProductHint[CommandServiceConfig](allowUnknownKeys = false) - - implicit val commandConfigurationConvert: ConfigConvert[CommandServiceConfig] = - deriveConvert[CommandServiceConfig] - - implicit val dbConfigSynchronousCommitValueConvert: ConfigConvert[SynchronousCommitValue] = - deriveEnumerationConvert[SynchronousCommitValue] - - implicit val dbConfigConnectionPoolConfigHint: ProductHint[ConnectionPoolConfig] = - ProductHint[ConnectionPoolConfig](allowUnknownKeys = false) - - implicit val dbConfigConnectionPoolConfigConvert: ConfigConvert[ConnectionPoolConfig] = - deriveConvert[ConnectionPoolConfig] - - implicit val dbConfigPostgresDataSourceConfigHint: ProductHint[PostgresDataSourceConfig] = - ProductHint[PostgresDataSourceConfig](allowUnknownKeys = false) - - implicit val dbConfigPostgresDataSourceConfigConvert: ConfigConvert[PostgresDataSourceConfig] = - deriveConvert[PostgresDataSourceConfig] - - implicit val dataSourcePropertiesHint: ProductHint[DataSourceProperties] = - ProductHint[DataSourceProperties](allowUnknownKeys = false) - - implicit val dataSourcePropertiesConvert: ConfigConvert[DataSourceProperties] = - deriveConvert[DataSourceProperties] - - implicit val rateLimitingConfigHint: OptConfigValue.OptProductHint[RateLimitingConfig] = - optProductHint[RateLimitingConfig](allowUnknownKeys = false) - - implicit val rateLimitingConfigConvert: ConfigConvert[Option[RateLimitingConfig]] = - optConvertEnabled(deriveConvert[RateLimitingConfig]) - - implicit val haConfigHint: ProductHint[HaConfig] = - ProductHint[HaConfig](allowUnknownKeys = false) - - implicit val haConfigConvert: ConfigConvert[HaConfig] = deriveConvert[HaConfig] - - private def createParticipantId(participantId: String) = - Ref.ParticipantId - .fromString(participantId) - .left - .map(err => CannotConvert(participantId, Ref.ParticipantId.getClass.getName, err)) - - implicit val participantIdReader: ConfigReader[Ref.ParticipantId] = ConfigReader - .fromString[Ref.ParticipantId](createParticipantId) - - implicit val participantIdWriter: ConfigWriter[Ref.ParticipantId] = - ConfigWriter.toString[Ref.ParticipantId](identity) - - implicit val indexerConfigHint: ProductHint[IndexerConfig] = - ProductHint[IndexerConfig](allowUnknownKeys = false) - - implicit val indexerConfigConvert: ConfigConvert[IndexerConfig] = deriveConvert[IndexerConfig] - - implicit val indexServiceConfigHint: ProductHint[IndexServiceConfig] = - ProductHint[IndexServiceConfig](allowUnknownKeys = false) - - implicit val achsConfigHint: ProductHint[AchsConfig] = - ProductHint[AchsConfig](allowUnknownKeys = false) - - implicit val achsConfigConvert: ConfigConvert[AchsConfig] = deriveConvert[AchsConfig] - - implicit val activeContractsServiceStreamsConfigConvert - : ConfigConvert[ActiveContractsServiceStreamsConfig] = - deriveConvert[ActiveContractsServiceStreamsConfig] - - implicit val transactionFlatStreamsConfigConvert: ConfigConvert[UpdatesStreamsConfig] = - deriveConvert[UpdatesStreamsConfig] - - implicit val indexServiceConfigConvert: ConfigConvert[IndexServiceConfig] = - deriveConvert[IndexServiceConfig] - - implicit val participantDataSourceConfigReader: ConfigReader[ParticipantDataSourceConfig] = - ConfigReader.fromString[ParticipantDataSourceConfig] { url => - Right(ParticipantDataSourceConfig(url)) - } - - implicit val participantDataSourceConfigWriter: ConfigWriter[ParticipantDataSourceConfig] = - ConfigWriter.toString { - case _ if secure => ReplaceSecretWithString - case dataSourceConfig => dataSourceConfig.jdbcUrl - } - - implicit val participantDataSourceConfigMapReader - : ConfigReader[Map[Ref.ParticipantId, ParticipantDataSourceConfig]] = - genericMapReader[Ref.ParticipantId, ParticipantDataSourceConfig]((s: String) => - createParticipantId(s) - ) - implicit val participantDataSourceConfigMapWriter - : ConfigWriter[Map[Ref.ParticipantId, ParticipantDataSourceConfig]] = - genericMapWriter[Ref.ParticipantId, ParticipantDataSourceConfig](identity) - -} - -object PureConfigReaderWriter { - implicit val Secure: PureConfigReaderWriter = new PureConfigReaderWriter(secure = true) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/metrics/LedgerApiServerMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/metrics/LedgerApiServerMetrics.scala deleted file mode 100644 index c03157001c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/metrics/LedgerApiServerMetrics.scala +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.metrics - -import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory -import com.daml.metrics.api.noop.NoOpMetricsFactory -import com.daml.metrics.api.opentelemetry.OpenTelemetryMetricsFactory -import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} -import com.daml.metrics.grpc.{DamlGrpcServerHistograms, DamlGrpcServerMetrics} -import com.daml.metrics.{DatabaseMetricsHistograms, HealthMetrics} -import com.typesafe.scalalogging.LazyLogging -import io.opentelemetry.api.metrics.Meter - -import scala.annotation.unused - -object LedgerApiServerMetrics extends LazyLogging { - - def apply(prefix: MetricName, otelMeter: Meter): LedgerApiServerMetrics = { - val inventory = new HistogramInventory - val histograms = new LedgerApiServerHistograms(prefix)(inventory) - new LedgerApiServerMetrics( - histograms, - new OpenTelemetryMetricsFactory( - otelMeter, - inventory.registered().map(_.name.toString()).toSet, - Some(logger.underlying), - ), - ) - } - - lazy val ForTesting: LedgerApiServerMetrics = { - val prefix = MetricName("test") - val histograms = new LedgerApiServerHistograms(prefix)(new HistogramInventory) - new LedgerApiServerMetrics( - histograms, - NoOpMetricsFactory, - ) - } -} - -final class LedgerApiServerMetrics( - inventory: LedgerApiServerHistograms, - val openTelemetryMetricsFactory: LabeledMetricsFactory, -) { - - private val prefix = inventory.prefix - - val commands: CommandMetrics = new CommandMetrics(inventory.commands, openTelemetryMetricsFactory) - - val execution: ExecutionMetrics = new ExecutionMetrics( - inventory.execution, - openTelemetryMetricsFactory, - ) - - val lapi = new LAPIMetrics(prefix :+ "lapi", openTelemetryMetricsFactory) - - val userManagement = new UserManagementMetrics( - prefix :+ "user_management", - openTelemetryMetricsFactory, - ) - - val partyRecordStore = new PartyRecordStoreMetrics( - prefix :+ "party_record_store", - openTelemetryMetricsFactory, - ) - - val identityProviderConfigStore = new IdentityProviderConfigStoreMetrics( - prefix :+ "identity_provider_config_store", - openTelemetryMetricsFactory, - ) - - val index = new IndexMetrics( - inventory.index, - openTelemetryMetricsFactory, - ) - val contractStore = new ContractStoreMetrics(inventory.contractStore, openTelemetryMetricsFactory) - - val indexer = new IndexerMetrics(inventory.indexer, openTelemetryMetricsFactory) - - val services = new ServicesMetrics( - inventory = inventory.services, - openTelemetryMetricsFactory, - ) - - val grpc = new DamlGrpcServerMetrics(openTelemetryMetricsFactory, "participant") - val requests = new ActiveRequestsMetrics(openTelemetryMetricsFactory, "participant")( - MetricsContext.Empty - ) - - val health = new HealthMetrics(openTelemetryMetricsFactory) - -} - -final class LedgerApiServerHistograms(val prefix: MetricName)(implicit - inventory: HistogramInventory -) { - - private[metrics] val services = new ServicesHistograms(prefix :+ "services") - private[metrics] val commands = new CommandHistograms(prefix :+ "commands") - private[metrics] val execution = new ExecutionHistograms(prefix :+ "execution") - private[metrics] val index = new IndexHistograms(prefix :+ "index") - private[metrics] val indexer = new IndexerHistograms(prefix :+ "indexer") - private[metrics] val contractStore = new ContractStoreHistograms(prefix :+ "contract_store") - - @unused - private val _grpc = new DamlGrpcServerHistograms() - // the ledger api server creates these metrics all over the place, but their prefix - // is anyway hardcoded - @unused - private val _db = new DatabaseMetricsHistograms() - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/DispatcherState.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/DispatcherState.scala deleted file mode 100644 index 528e846c03..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/DispatcherState.scala +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.concurrent.{DirectExecutionContext, FutureSupervisor} -import com.digitalasset.canton.config.NonNegativeDuration -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.error.CommonErrors -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.pekkostreams.dispatcher.Dispatcher -import com.digitalasset.canton.platform.DispatcherState.{ - DispatcherNotRunning, - DispatcherRunning, - DispatcherStateShutdown, -} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Mutex -import io.grpc.StatusRuntimeException - -import java.util.concurrent.ScheduledExecutorService -import scala.concurrent.Future -import scala.concurrent.duration.{Duration, DurationInt} -import scala.util.{Failure, Success} - -/** Life-cycle manager for the Ledger API streams offset dispatcher. */ -class DispatcherState( - dispatcherShutdownTimeout: Duration, - override protected val loggerFactory: NamedLoggerFactory, -)(implicit - traceContext: TraceContext, - scheduler: ScheduledExecutorService, -) extends NamedLogging { - - private val ServiceName = "Ledger API offset dispatcher" - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var dispatcherStateRef: DispatcherState.State = DispatcherNotRunning - private val lock = new Mutex() - private val directEc = DirectExecutionContext(noTracingLogger) - - def isRunning: Boolean = (lock.exclusive { - dispatcherStateRef match { - case DispatcherRunning(_) => true - case DispatcherNotRunning | DispatcherStateShutdown => false - } - }) - - def getDispatcher: Dispatcher[Offset] = (lock.exclusive { - dispatcherStateRef match { - case DispatcherStateShutdown | DispatcherNotRunning => throw dispatcherNotRunning - case DispatcherRunning(dispatcher) => dispatcher - } - }) - - def startDispatcher(initializationOffset: Option[Offset]): Unit = (lock.exclusive { - dispatcherStateRef match { - case DispatcherNotRunning => - val activeDispatcher = buildDispatcher(initializationOffset) - dispatcherStateRef = DispatcherRunning(activeDispatcher) - logger.info( - s"Started a $ServiceName at initialization offset: $initializationOffset." - ) - case DispatcherStateShutdown => - throw new IllegalStateException(s"$ServiceName state has already shut down.") - case DispatcherRunning(_) => - throw new IllegalStateException( - "Dispatcher startup triggered while an existing dispatcher is still active." - ) - } - }) - - def stopDispatcher(): Future[Unit] = { - val dispatcherToCancel = (lock.exclusive { - dispatcherStateRef match { - case DispatcherNotRunning | DispatcherStateShutdown => - logger.debug(s"$ServiceName already stopped, shutdown or never started.") - None - case DispatcherRunning(dispatcher) => - logger.info(s"Stopping active $ServiceName.") - dispatcherStateRef = DispatcherNotRunning - Some(dispatcher) - } - }) - dispatcherToCancel match { - case None => Future.unit - case Some(dispatcher) => - dispatcher - .cancel(() => dispatcherNotRunning) - .transform { - case success @ Success(_) => - logger.debug(s"Active $ServiceName stopped.") - success - case f @ Failure(failure) => - logger.warn(s"Failed stopping active $ServiceName", failure) - f - }(directEc) - } - } - - private[platform] def shutdown(): Future[Unit] = { - logger.info(s"Shutting down $ServiceName state.") - val currentDispatcherState = (lock.exclusive { - val currentDispatcherState = dispatcherStateRef - dispatcherStateRef = DispatcherStateShutdown - currentDispatcherState - }) - currentDispatcherState match { - case DispatcherNotRunning => - logger.info(s"$ServiceName not running. Transitioned to shutdown.") - Future.unit - case DispatcherStateShutdown => - logger.info(s"$ServiceName already shutdown.") - Future.unit - case DispatcherRunning(dispatcher) => - new FutureSupervisor.Impl( - NonNegativeDuration(dispatcherShutdownTimeout + 1.seconds), - loggerFactory, - ).supervised( - description = s"Shutdown $ServiceName", - warnAfter = dispatcherShutdownTimeout, - )( - dispatcher - .shutdown() - .transform { - case success @ Success(_) => - logger.info(s"Shutdown $ServiceName.") - success - case f @ Failure(failure) => - logger.warn(s"Error during $ServiceName shutdown", failure) - f - }(directEc) - ) - } - } - - private def buildDispatcher( - initializationOffset: Option[Offset] - ): Dispatcher[Offset] = - Dispatcher( - name = ServiceName, - firstIndex = Offset.firstOffset, - headAtInitialization = initializationOffset, - ) - - private def dispatcherNotRunning: StatusRuntimeException = { - val errorLoggingContext = ErrorLoggingContext( - logger = logger, - loggerFactory.properties, - traceContext, - ) - CommonErrors.ServiceNotRunning.Reject(ServiceName)(errorLoggingContext).asGrpcError - } -} - -object DispatcherState { - private sealed trait State extends Product with Serializable - - private final case object DispatcherNotRunning extends State - private final case object DispatcherStateShutdown extends State - private final case class DispatcherRunning(dispatcher: Dispatcher[Offset]) extends State - - def owner( - apiStreamShutdownTimeout: Duration, - loggerFactory: NamedLoggerFactory, - )(implicit - traceContext: TraceContext, - scheduler: ScheduledExecutorService, - ): ResourceOwner[DispatcherState] = ResourceOwner.forReleasable(() => - new DispatcherState(apiStreamShutdownTimeout, loggerFactory) - )(_.shutdown()) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/InMemoryState.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/InMemoryState.scala deleted file mode 100644 index 81e74f23cb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/InMemoryState.scala +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker -import com.digitalasset.canton.platform.apiserver.services.admin.PartyAllocation -import com.digitalasset.canton.platform.apiserver.services.tracking.{ - InFlight, - StreamTracker, - SubmissionTracker, -} -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{AchsState, LedgerEnd} -import com.digitalasset.canton.platform.store.cache.{ - AchsStateCache, - ContractStateCaches, - InMemoryFanoutBuffer, - MutableLedgerEndCache, - OffsetCheckpointCache, -} -import com.digitalasset.canton.platform.store.interning.StringInterningView -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import io.opentelemetry.api.trace.Tracer - -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.duration.Duration -import scala.concurrent.{ExecutionContext, Future} - -/** Wrapper and life-cycle manager for the in-memory Ledger API state. */ -class InMemoryState( - val participantId: Ref.ParticipantId, - val ledgerEndCache: MutableLedgerEndCache, - val contractStateCaches: ContractStateCaches, - val offsetCheckpointCache: OffsetCheckpointCache, - val achsStateCache: AchsStateCache, - val inMemoryFanoutBuffer: InMemoryFanoutBuffer, - val stringInterningView: StringInterningView, - val dispatcherState: DispatcherState, - val transactionSubmissionTracker: SubmissionTracker, - val reassignmentSubmissionTracker: SubmissionTracker, - val partyAllocationTracker: PartyAllocation.Tracker, - val commandProgressTracker: CommandProgressTracker, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends NamedLogging { - - final def initialized: Boolean = dispatcherState.isRunning - - val cachesUpdatedUpto: AtomicReference[Option[Offset]] = - new AtomicReference[Option[Offset]](None) - - /** (Re-)initializes the participant in-memory state to a specific ledger end. - * - * NOTE: This method is not thread-safe. Calling it concurrently leads to undefined behavior. - */ - final def initializeTo( - ledgerEndO: Option[LedgerEnd], - achsState: AchsState, - )(implicit traceContext: TraceContext): Future[Unit] = { - def clearCaches(): Unit = { - contractStateCaches.reset(ledgerEndO) - inMemoryFanoutBuffer.flush() - ledgerEndCache.set(ledgerEndO) - achsStateCache.set(achsState) - } - def resetInMemoryState(): Future[Unit] = - for { - // First stop the active dispatcher (if exists) to ensure - // termination of existing Ledger API subscriptions and to also ensure - // that new Ledger API subscriptions racing with `initializeTo` - // do not observe an inconsistent state. - _ <- dispatcherState.stopDispatcher() - // Reset the Ledger API caches to the latest ledger end - _ <- Future { - clearCaches() - transactionSubmissionTracker.close() - reassignmentSubmissionTracker.close() - } - // Start a new Ledger API offset dispatcher - _ = dispatcherState.startDispatcher(ledgerEndO.map(_.lastOffset)) - } yield () - - def inMemoryStateIsUptodate: Boolean = - ledgerEndCache() == ledgerEndO && - dispatcherState.getDispatcher.getHead() == ledgerEndO.map(_.lastOffset) && - cachesUpdatedUpto.get() == ledgerEndO.map(_.lastOffset) && - achsStateCache.get() == achsState - - def ledgerEndComparisonLog: String = - s"inMemoryLedgerEnd:$ledgerEndCache persistedLedgerEnd:$ledgerEndO dispatcher-head:${dispatcherState.getDispatcher - .getHead()} cachesAreUpdateUpto:${cachesUpdatedUpto.get()} achsState:${achsStateCache - .get()} persistedAchsState:$achsState" - - if (!dispatcherState.isRunning) { - logger.info(s"Initializing participant in-memory state to ledger end: $ledgerEndO") - resetInMemoryState() - } else if (!inMemoryStateIsUptodate) { - logger.info( - s"Participant in-memory state/persisted ledger end mismatch: resetting in-memory state. $ledgerEndComparisonLog" - ) - resetInMemoryState() - } else if (cachesUpdatedUpto.get().isEmpty) { - // cachesUpdatedUpto can signal an incomplete cache state update, therefore we clear the caches - logger.info( - s"Participant in-memory state with empty cachesUpdatedUpTo: resetting caches. $ledgerEndComparisonLog" - ) - Future(clearCaches()) - } else { - logger.info( - s"Participant in-memory state is up-to-date, continue without reset. $ledgerEndComparisonLog" - ) - Future.unit - } - } -} - -object InMemoryState { - def owner( - participantId: Ref.ParticipantId, - commandProgressTracker: CommandProgressTracker, - apiStreamShutdownTimeout: Duration, - bufferedStreamsPageSize: Int, - maxContractStateCacheSize: Long, - maxContractKeyStateCacheSize: Long, - maxTransactionsInMemoryFanOutBufferSize: Int, - maxCommandsInFlight: Int, - metrics: LedgerApiServerMetrics, - executionContext: ExecutionContext, - tracer: Tracer, - loggerFactory: NamedLoggerFactory, - )( - mutableLedgerEndCache: MutableLedgerEndCache, - stringInterningView: StringInterningView, - )(implicit - traceContext: TraceContext, - scheduler: ScheduledExecutorService, - ): ResourceOwner[InMemoryState] = { - val initialLedgerEnd = LedgerEnd.beforeBegin - - for { - dispatcherState <- DispatcherState.owner( - apiStreamShutdownTimeout, - loggerFactory, - ) - transactionSubmissionTracker <- SubmissionTracker.owner( - maxCommandsInFlight, - metrics, - tracer, - loggerFactory, - ) - reassignmentSubmissionTracker <- SubmissionTracker.owner( - maxCommandsInFlight, - metrics, - tracer, - loggerFactory, - ) - partyAllocationTracker <- StreamTracker - .owner( - "party-added", - (item: PartyAllocation.Completed) => Some(item.submissionId), - InFlight.Unlimited, - loggerFactory, - ) - } yield new InMemoryState( - participantId = participantId, - ledgerEndCache = mutableLedgerEndCache, - achsStateCache = new AchsStateCache(loggerFactory), - dispatcherState = dispatcherState, - contractStateCaches = ContractStateCaches.build( - initialLedgerEnd.map(_.lastEventSeqId).getOrElse(0L), - maxContractStateCacheSize, - maxContractKeyStateCacheSize, - metrics, - loggerFactory, - )(executionContext), - offsetCheckpointCache = new OffsetCheckpointCache, - inMemoryFanoutBuffer = new InMemoryFanoutBuffer( - maxBufferSize = maxTransactionsInMemoryFanOutBufferSize, - metrics = metrics, - maxBufferedChunkSize = bufferedStreamsPageSize, - loggerFactory = loggerFactory, - ), - stringInterningView = stringInterningView, - transactionSubmissionTracker = transactionSubmissionTracker, - reassignmentSubmissionTracker = reassignmentSubmissionTracker, - partyAllocationTracker = partyAllocationTracker, - commandProgressTracker = commandProgressTracker, - loggerFactory = loggerFactory, - )(executionContext) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/InternalUpdateFormat.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/InternalUpdateFormat.scala deleted file mode 100644 index 93014208c2..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/InternalUpdateFormat.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.digitalasset.canton.ledger.api.{TopologyFormat, TransactionShape} -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties - -final case class InternalUpdateFormat( - includeTransactions: Option[InternalTransactionFormat], - includeReassignments: Option[InternalEventFormat], - includeTopologyEvents: Option[TopologyFormat], -) - -final case class InternalTransactionFormat( - internalEventFormat: InternalEventFormat, - transactionShape: TransactionShape, -) - -final case class InternalEventFormat( - templatePartiesFilter: TemplatePartiesFilter, - eventProjectionProperties: EventProjectionProperties, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/LedgerApiServerInternals.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/LedgerApiServerInternals.scala deleted file mode 100644 index 16bff748dd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/LedgerApiServerInternals.scala +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker -import com.digitalasset.canton.platform.config.IndexServiceConfig -import com.digitalasset.canton.platform.index.InMemoryStateUpdater -import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache -import com.digitalasset.canton.platform.store.interning.StringInterningView -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import io.opentelemetry.api.trace.Tracer - -import java.util.concurrent.ScheduledExecutorService -import scala.concurrent.ExecutionContext - -object LedgerApiServerInternals { - def createInMemoryStateAndUpdater( - participantId: Ref.ParticipantId, - commandProgressTracker: CommandProgressTracker, - indexServiceConfig: IndexServiceConfig, - maxCommandsInFlight: Int, - metrics: LedgerApiServerMetrics, - executionContext: ExecutionContext, - tracer: Tracer, - loggerFactory: NamedLoggerFactory, - )( - mutableLedgerEndCache: MutableLedgerEndCache, - stringInterningView: StringInterningView, - )(implicit - traceContext: TraceContext, - scheduler: ScheduledExecutorService, - ): ResourceOwner[(InMemoryState, InMemoryStateUpdater.UpdaterFlow)] = - for { - inMemoryState <- InMemoryState.owner( - participantId = participantId, - commandProgressTracker = commandProgressTracker, - apiStreamShutdownTimeout = indexServiceConfig.apiStreamShutdownTimeout, - bufferedStreamsPageSize = indexServiceConfig.bufferedStreamsPageSize, - maxContractStateCacheSize = indexServiceConfig.maxContractStateCacheSize, - maxContractKeyStateCacheSize = indexServiceConfig.maxContractKeyStateCacheSize, - maxTransactionsInMemoryFanOutBufferSize = - indexServiceConfig.maxTransactionsInMemoryFanOutBufferSize, - executionContext = executionContext, - maxCommandsInFlight = maxCommandsInFlight, - metrics = metrics, - tracer = tracer, - loggerFactory = loggerFactory, - )(mutableLedgerEndCache, stringInterningView) - - inMemoryStateUpdater <- InMemoryStateUpdater.owner( - inMemoryState = inMemoryState, - prepareUpdatesParallelism = indexServiceConfig.inMemoryStateUpdaterParallelism, - offsetCheckpointCacheUpdateInterval = - indexServiceConfig.offsetCheckpointCacheUpdateInterval.underlying, - metrics = metrics, - loggerFactory = loggerFactory, - ) - } yield inMemoryState -> inMemoryStateUpdater -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/PackagePreferenceBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/PackagePreferenceBackend.scala deleted file mode 100644 index be33ad872f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/PackagePreferenceBackend.scala +++ /dev/null @@ -1,605 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import cats.implicits.{catsSyntaxAlternativeSeparate, toFoldableOps} -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.ledger.api.PackageReference -import com.digitalasset.canton.ledger.api.PackageReference.* -import com.digitalasset.canton.ledger.api.validation.GetPreferredPackagesRequestValidator.PackageVettingRequirements -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.NotFound.PackageNamesNotFound -import com.digitalasset.canton.ledger.participant.state.SyncService -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.logging.{ - HasLoggerName, - NamedLoggerFactory, - NamedLogging, - NamedLoggingContext, -} -import com.digitalasset.canton.platform.PackagePreferenceBackend.{ - Candidate, - PackageFilter, - SupportedPackagesFilter, -} -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.time.Clock -import com.digitalasset.canton.topology.{PhysicalSynchronizerId, SynchronizerId} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.MonadUtil -import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.util.collection.MapsUtil -import com.digitalasset.canton.version.ProtocolVersion -import com.digitalasset.daml.lf.data.Ref.PackageVersion -import com.digitalasset.daml.lf.language.Ast -import com.digitalasset.daml.lf.language.Ast.PackageSignature - -import scala.collection.immutable.SortedSet -import scala.collection.{MapView, mutable} -import scala.concurrent.ExecutionContext -import scala.util.chaining.scalaUtilChainingOps - -class PackagePreferenceBackend( - clock: Clock, - adminParty: Party, - syncService: SyncService, - val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext) - extends NamedLogging { - - /** Computes the preferred package versions for the provided package vetting requirements. - * - * In detail, the method outputs the most preferred package for each package-name specified in - * the package vetting requirements. The algorithm proceeds as follows: - * - * 1. The input package vetting requirements for each party are extended to incorporate - * requirements derived from other parties: for each party, and for each of the party's - * required package-names, the vetted packages with that package-name are resolved, and the - * package-names of their transitive dependencies are added to the party's requirements if - * those package-names are required by any other party. - * - * 1. For each required package-name, the set of candidate package-ids is computed as the - * intersection of the vetted package-ids across all parties that require that package-name - * (after extension). - * - * 1. Any candidate whose transitive dependencies include a package-id pertaining to a required - * (by any party) package-name that is not in the corresponding intersection is discarded. - * - * 1. The highest-versioned remaining candidate for each originally requested package-name is - * selected. - * - * Note: - * - For brevity, we refer here to a party vetting a package if all its hosting participants - * have vetted the package. - * - * @param packageVettingRequirements - * The package vetting requirements for which the package preferences should be computed. - * @param packageFilter - * Filters which package IDs are eligible for consideration in the preference computation for - * each package-name specified in the provided requirements. - * @param synchronizerId - * If provided, only this synchronizer's vetting topology state is considered in the - * computation. Otherwise, the highest package version from all the connected synchronizers is - * returned. - * @param vettingValidAt - * If provided, used to compute the package vetting state at this timestamp. - * @return - * if a solution exists, the best package preference coupled with the synchronizer id that it - * pertains to. - */ - def getPreferredPackages( - packageVettingRequirements: PackageVettingRequirements, - packageFilter: PackageFilter, - synchronizerId: Option[SynchronizerId], - vettingValidAt: Option[CantonTimestamp], - )(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[Either[String, (Seq[PackageReference], PhysicalSynchronizerId)]] = { - val packageMetadataSnapshot = syncService.getPackageMetadataSnapshot - - for { - routingSynchronizerState <- syncService.getRoutingSynchronizerState - _ <- ensurePackageNamesKnown(packageVettingRequirements, packageMetadataSnapshot) - packageMapForRequest <- syncService.computePartyVettingMap( - submitters = Set.empty, - informees = packageVettingRequirements.allParties, - vettingValidityTimestamp = vettingValidAt.getOrElse(clock.now), - prescribedSynchronizer = synchronizerId, - routingSynchronizerState = routingSynchronizerState, - ) - } yield { - val partyToRequiredPackages = MapsUtil.transpose(packageVettingRequirements.value) - val allCandidates = packageVettingRequirements.allPackageNames - .flatMap(packageMetadataSnapshot.packageNameMap(_).allPackageIdsForName) - - // Mapping from a candidate package-id to the package names of its transitive dependencies - // that are also candidates - val candidateToRequiredDependencyNames: Map[PackageId, Set[PackageName]] = - packageMetadataSnapshot - .allDependencySetsRecursively(allCandidates) - .view - .mapValues(deps => deps.filter(allCandidates)) - .mapValues(deps => deps.map(packageMetadataSnapshot.packageIdVersionMap(_)._1)) - .toMap - val synchronizerCandidates = packageMapForRequest.map { case (syncId, partiesVettingState) => - val extendedRequirements = partyToRequiredPackages.map { - case (partyId, requiredPackageNames) => - // For each party, extend its required package names to include - // the package names of the recursive dependencies of package names required by other parties - val vettedPackagesForParty = partiesVettingState(partyId) - val extendedRequiredPackageNames = - requiredPackageNames.foldLeft(requiredPackageNames) { (packageNames, packageName) => - packageNames ++ packageMetadataSnapshot - .packageNameMap(packageName) - .allPackageIdsForName - // Only consider required package-name dependencies of vetted packages - .filter(vettedPackagesForParty.contains) - .flatMap(candidateToRequiredDependencyNames) - } - partyId -> extendedRequiredPackageNames - } - val candidates = PackagePreferenceBackend.computePerSynchronizerPackageCandidates( - partiesVettingState = partiesVettingState, - packageMetadataSnapshot = packageMetadataSnapshot, - packageFilter = packageFilter, - requirements = extendedRequirements, - synchronizerProtocolVersion = syncId.protocolVersion, - ) - syncId -> PackagePreferenceBackend.selectRequestedPackages( - candidates, - packageVettingRequirements.allPackageNames, - ) - } - findValidCandidate(synchronizerCandidates) - } - } - - private def findValidCandidate( - synchronizerCandidates: Map[PhysicalSynchronizerId, Candidate[Set[PackageReference]]] - )(implicit - traceContext: TraceContext - ): Either[String, (Seq[PackageReference], PhysicalSynchronizerId)] = { - val (discardedCandidates, validCandidates) = synchronizerCandidates.view - .map { case (sync, candidateE) => - candidateE.left.map(sync -> _).map(sync -> _) - } - .toList - .separate - - validCandidates - .maxByOption(_._1)( - // TODO(#25385): Order by the package version with the package precedence set by the order of the vetting requirements - // Follow the pattern used for SynchronizerRank ordering, - // where lexicographic order picks the most preferred synchronizer by id - implicitly[Ordering[PhysicalSynchronizerId]].reverse - ) - .map { case (syncId, packageRefs) => - // Valid candidate found - // Log discarded candidates and return the package references and synchronizer id of the valid candidate - if (discardedCandidates.nonEmpty) { - logger.debug(show"Discarded synchronizers: $discardedCandidates") - } - packageRefs.toSeq -> syncId - } - .toRight( - show"No synchronizer satisfies the vetting requirements. Discarded synchronizers: $discardedCandidates" - ) - } - - def getPreferredPackageVersionForParticipant( - packageName: PackageName, - supportedPackageIds: Set[PackageId], - supportedPackageIdsDescription: String, - )(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[Either[String, PackageId]] = - getPreferredPackages( - packageVettingRequirements = PackageVettingRequirements( - value = Map(packageName -> Set(adminParty)) - ), - packageFilter = SupportedPackagesFilter( - Map(packageName -> supportedPackageIds), - supportedPackageIdsDescription, - ), - synchronizerId = None, - vettingValidAt = Some(CantonTimestamp.MaxValue), - ) - .map(_.map { - case (Seq(pkgRef), _) => pkgRef.pkgId - case (invalidSeq, syncId) => - throw new RuntimeException( - s"Expected exactly one package reference for package name $packageName and $syncId, but got $invalidSeq. This is likely a programming error. Please contact support" - ) - }) - - private def ensurePackageNamesKnown( - packageVettingRequirements: PackageVettingRequirements, - packageMetadataSnapshot: PackageMetadata, - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { - val requestPackageNames = packageVettingRequirements.allPackageNames - val knownPackageNames = packageMetadataSnapshot.packageNameMap.keySet - val unknownPackageNames = requestPackageNames.diff(knownPackageNames) - - if (unknownPackageNames.isEmpty) FutureUnlessShutdown.unit - else - FutureUnlessShutdown.failed( - PackageNamesNotFound.Reject(unknownPackageNames).asGrpcError - ) - } -} - -object PackagePreferenceBackend extends HasLoggerName { - type SortedPreferences = NonEmpty[SortedSet[PackageReference]] // most preferred last - // A candidate refers to a value T that: - // - wraps the value in a Right if it is valid for package preferences computation OR - // - wraps the value's discarded reason in a Left - type Candidate[T] = Either[String, T] - - sealed trait PackageFilter extends Product with Serializable with PrettyPrinting { - def apply(packageName: PackageName, packageId: PackageId): Boolean - } - - case object AllowAllPackageIds extends PackageFilter { - def apply(packageName: PackageName, packageId: PackageId): Boolean = true - - override protected def pretty: Pretty[AllowAllPackageIds.this.type] = - Pretty.prettyOfString(_ => "All package-ids supported") - } - - final case class SupportedPackagesFilter( - supportedPackagesPerPackageName: Map[PackageName, Set[PackageId]], - restrictionDescription: String, - ) extends PackageFilter { - def apply(packageName: PackageName, packageId: PackageId): Boolean = - supportedPackagesPerPackageName.get(packageName).forall(_.contains(packageId)) - - override protected def pretty: Pretty[SupportedPackagesFilter] = - Pretty.prettyOfString(c => - show"${c.restrictionDescription.singleQuoted}=$supportedPackagesPerPackageName" - ) - } - - def computePerSynchronizerPackageCandidates( - partiesVettingState: Map[Party, Set[PackageId]], - packageMetadataSnapshot: PackageMetadata, - requirements: Map[Party, Set[PackageName]], - packageFilter: PackageFilter, - synchronizerProtocolVersion: ProtocolVersion, - )(implicit - loggingContext: NamedLoggingContext - ): MapView[PackageName, Candidate[SortedPreferences]] = - partiesVettingState.view - // Resolve to full package references - .mapValues(groupAndSortPackageLineages(packageMetadataSnapshot.packageIdVersionMap, _)) - .toMap - .pipe((candidates: Map[Party, Map[PackageName, SortedPreferences]]) => - // Mark candidates for which there is no vetted package satisfying a vetting requirement (party <-> package-name) - documentUnsatisfiedPackageNameRequirements(candidates, requirements) - ) - .pipe { (candidates: Map[Party, Map[PackageName, Candidate[SortedPreferences]]]) => - // At this point we are reducing the party dimension: - // - for required package names, we intersect the sets of package-ids of the requiring parties. - // - for other package names, we sum the sets of available package-ids of all parties. - computePartyPackageCandidatesIntersection(requirements, candidates) - } - // Preserve only the candidate package-ids that are commonly-vetted and all their dependencies are commonly-vetted - .pipe( - preserveDeeplyVetted( - packageMetadataSnapshot, - _, - requirements.values.flatten.toSet, - synchronizerProtocolVersion, - ) - ) - // Apply package-id filter restriction to the candidate package-ids - // Note: the filter can discard packages that are dependencies of other candidates - .pipe(filterPackages(packageFilter, _)) - - // Preserve for each package-name all the package-ids that are vetted and all their dependencies are vetted - private def preserveDeeplyVetted( - packageMetadataSnapshot: PackageMetadata, - candidatesForPackageName: Map[PackageName, Candidate[SortedPreferences]], - requiredPackageNames: Set[PackageName], - protocolVersion: ProtocolVersion, - ): MapView[PackageName, Candidate[SortedPreferences]] = { - val packageIndex: Map[PackageId, (PackageName, PackageVersion)] = - packageMetadataSnapshot.packageIdVersionMap - val dependencyGraph: Map[PackageId, Set[PackageId]] = - packageMetadataSnapshot.packages.view.mapValues(_.directDeps).toMap - - val allVettedPackages = candidatesForPackageName.view.values - .collect { case Right(vettedCandidates) => - vettedCandidates.view.map(_.pkgId) - } - .flatten - .toSet - - val getPackageAst: PackageId => PackageSignature = pkgId => - packageMetadataSnapshot.packages.getOrElse( - pkgId, - throw new NoSuchElementException( - s"Package with id $pkgId not found in the package metadata snapshot" - ), - ) - - if (protocolVersion <= ProtocolVersion.v34) - preserveDeeplyVettedPV34( - candidatesForPackageName, - dependencyGraph, - allVettedPackages, - packageIndex, - getPackageAst, - ) - else - preserveDeeplyRequiredVetted( - requiredPackageNames, - candidatesForPackageName, - dependencyGraph, - allVettedPackages, - packageIndex, - getPackageAst, - ) - } - - // Discard packages that have an unvetted direct or transitive dependency package-id - // pertaining to a required package-name - private def preserveDeeplyRequiredVetted( - requiredPackageNames: Set[PackageName], - candidatesForPackageName: Map[PackageName, Candidate[SortedPreferences]], - dependencyGraph: Map[PackageId, Set[PackageId]], - allVettedPackages: Set[PackageId], - packageIndex: Map[PackageId, (PackageName, PackageVersion)], - getPackageAst: PackageId => PackageSignature, - ): MapView[PackageName, Candidate[NonEmpty[SortedSet[PackageReference]]]] = { - val deepVettingCache: mutable.HashMap[PackageId, Either[PackageId, Unit]] = - mutable.HashMap.empty - - // Note: Keeping it simple without tailrec since the dependency graph depth should be limited - def checkRequiredVettingRecursively(pkgId: PackageId): Either[PackageId, Unit] = - // Note: This re-entrant call to `mutable.HashMap.getOrElseUpdate` is safe - // since the call is single-threaded and the Daml package dependency graph - // is acyclic (a key cannot be re-visited along a recursive call-stack) - deepVettingCache.getOrElseUpdate( - pkgId, { - val pkg = getPackageAst(pkgId) - if (allVettedPackages(pkgId) || !requiredPackageNames(pkg.pkgName)) { - val dependencies = dependencyGraph(pkgId) - MonadUtil.sequentialTraverse_(dependencies.toSeq)(checkRequiredVettingRecursively) - } else Left(pkgId) - }, - ) - - candidatesForPackageName.view - .mapValues( - _.flatMap { candidates => - val (packagesWithUnvettedRequiredDeps, validCandidates) = candidates.view - .map(pkgRef => - checkRequiredVettingRecursively(pkgRef.pkgId).left - .map(pkgRef.pkgId -> _) - .map(_ => pkgRef) - ) - .toList - .separate - - def showPkg(pkgId: PackageId): String = - pkgId.toPackageReference(packageIndex).map(_.show).getOrElse(pkgId.show) - - lazy val packageRefsWithUnvettedDepsForError = packagesWithUnvettedRequiredDeps.map { - case (pkg, unvettedDep) => s"${showPkg(pkg)} -> ${showPkg(unvettedDep)}" - } - - NonEmpty - .from(validCandidates.to(SortedSet)) - .toRight( - show"Packages with required dependencies not vetted by all interested parties: $packageRefsWithUnvettedDepsForError" - ) - } - ) - } - - // TODO(#25385): Legacy behavior, only supported for backwards compatibility with Protocol Version 34. - // Remove once PV34 support is dropped. - private def preserveDeeplyVettedPV34( - candidatesForPackageName: Map[PackageName, Candidate[SortedPreferences]], - dependencyGraph: Map[PackageId, Set[PackageId]], - allVettedPackages: Set[PackageId], - packageIndex: Map[PackageId, (PackageName, PackageVersion)], - getPackageAst: PackageId => Ast.PackageSignature, - ): MapView[PackageName, Candidate[NonEmpty[SortedSet[PackageReference]]]] = { - val deepVettingCache: mutable.Map[PackageId, Either[PackageId, Unit]] = - mutable.Map.empty - - // Note: Keeping it simple without tailrec since the dependency graph depth should be limited - def isDeeplyVetted(pkgId: PackageId): Either[PackageId, Unit] = { - val pkg = getPackageAst(pkgId) - if ( - // If a package is vetted or it is not a schema package, we continue with checking its dependencies. - // We ignore unvetted non-schema packages to support - // disjoint versions across informees (e.g. Daml stdlib packages) - allVettedPackages(pkgId) || !isSchemaPackage(pkg) - ) { - val dependencies = dependencyGraph(pkgId) - - dependencies.foldLeft(Right(()): Either[PackageId, Unit]) { - case (Right(()), dep) => deepVettingCache.getOrElseUpdate(dep, isDeeplyVetted(dep)) - case (left, _) => left - } - } else { - // If the schema package is not vetted, return it as an error - Left(pkgId) - } - } - - candidatesForPackageName.view - .mapValues( - _.flatMap { candidates => - val (packagesWithUnvettedDeps, candidatesWithVettedDeps) = candidates.view - .map(pkgRef => - isDeeplyVetted(pkgRef.pkgId).left.map(pkgRef.pkgId -> _).map(_ => pkgRef) - ) - .toList - .separate - - val lazyPackageRefsWithUnvettedDepsForError = packagesWithUnvettedDeps.view - .map { case (pkg, unvettedDep) => - s"${pkg.toPackageReference(packageIndex).map(_.show).getOrElse(pkg.show)} -> ${unvettedDep.toPackageReference(packageIndex).map(_.show).getOrElse(pkg.show)}" - } - - NonEmpty - .from(candidatesWithVettedDeps.to(SortedSet)) - .toRight( - show"Packages with required dependencies not vetted by all interested parties: ${lazyPackageRefsWithUnvettedDepsForError.toList}" - ) - } - ) - } - - private def filterPackages( - packageFilter: PackageFilter, - candidatePackagesForName: MapView[PackageName, Candidate[SortedPreferences]], - ): MapView[PackageName, Candidate[SortedPreferences]] = - candidatePackagesForName.view - .mapValues(_.flatMap { packageRefs => - NonEmpty - .from(packageRefs.forgetNE.filter(ref => packageFilter(ref.packageName, ref.pkgId))) - .toRight( - // TODO(#25385): Improve error message by making it explicit that these packages are not vetted by the requested parties - show"No vetted package candidate satisfies the package-id filter $packageFilter.\nCandidates: ${packageRefs - .map(_.pkgId)}" - ) - }) - - private def groupAndSortPackageLineages( - packageIdVersionMap: Map[PackageId, (PackageName, PackageVersion)], - packageIds: Set[PackageId], - )(implicit loggingContext: NamedLoggingContext): Map[PackageName, SortedPreferences] = - packageIds.view - .flatMap { pkgId => - val pkgRef = pkgId.toPackageReference(packageIdVersionMap) - if (pkgRef.isEmpty) - loggingContext.trace( - show"Discarding package ID $pkgId as it doesn't exist in the participant's package store." - ) - pkgRef - } - .groupBy(_.packageName) - .view - .map { case (pkgName, pkgRefs) => - pkgName -> - NonEmpty - .from(SortedSet.from(pkgRefs)) - // The groupBy in this chain ensures non-empty pkgRefs - .getOrElse( - sys.error( - "Empty package references. This is likely a programming error. Please contact support" - ) - ) - } - .toMap - - private def computePartyPackageCandidatesIntersection( - requirements: Map[Party, Set[PackageName]], - candidatesPerParty: Map[Party, Map[PackageName, Candidate[SortedPreferences]]], - ): Map[PackageName, Candidate[SortedPreferences]] = { - val requiredNames = requirements.values.flatten.toSet - candidatesPerParty.view - .flatMap { case (party, pkgNameCandidates) => pkgNameCandidates.view.map(party -> _) } - .foldLeft(Map.empty[PackageName, Candidate[SortedPreferences]]) { - case (acc, (party, (pkgName, newCandidates))) => - if (requiredNames.contains(pkgName)) { - if (requirements.get(party).exists(_.contains(pkgName))) { - // The current package name is required by the current party: we compute the - // intersection of candidates. - acc.updatedWith(pkgName) { - case None => Some(newCandidates) - case Some(existingCandidates) => - Some( - for { - existingPkgRefs <- existingCandidates - pkgRefs <- newCandidates - newPkgRefs <- NonEmpty - .from(existingPkgRefs.forgetNE.intersect(pkgRefs)) - .toRight( - show"""|No package candidates for '$pkgName' after considering candidates for party $party. - |Current candidates: $existingPkgRefs. - |Candidates for party $party: $pkgRefs""".stripMargin - ) - } yield newPkgRefs - ) - } - } else acc - } else { - // This package is not a known requirement. It won't be used to compute GetPreferredPackages. - // However TAPS may need it during interpretation. We retain all candidates to maximize - // the options. If routing fails, the package will appear as requirement of the next TAPS pass. - acc.updatedWith(pkgName) { - case None => Some(newCandidates) - case Some(existingCandidates) => - (existingCandidates, newCandidates) match { - case (Right(existingCandidates), Right(newCandidates)) => - NonEmpty.from(existingCandidates.forgetNE ++ newCandidates).map(Right(_)) - case (existingCandidates, newCandidates) => - Some(existingCandidates.orElse(newCandidates)) - } - } - } - } - } - - private def documentUnsatisfiedPackageNameRequirements( - candidatesPerParty: Map[Party, Map[PackageName, SortedPreferences]], - requirementsPerParty: Map[Party, Set[PackageName]], - ): Map[Party, Map[PackageName, Candidate[SortedPreferences]]] = - requirementsPerParty.map { case (party, pkgNameReqs) => - lazy val noPartyBackfillMsg = - show"No package is consistently by all hosting participants of $party." - - val backfillForMissingPartyCandidates = pkgNameReqs.view - .map(_ -> Left(noPartyBackfillMsg)) - .toMap - - val candidatesForParty = candidatesPerParty - .get(party) - .map(_.view.mapValues(Right(_)).toMap) - .getOrElse(backfillForMissingPartyCandidates) - - val packagesWithNoCandidates = pkgNameReqs.diff(candidatesForParty.keySet) - val pkgNameWithNoVettedCandidates = packagesWithNoCandidates.view - .map(pkgName => - pkgName -> Left( - show"No package with package-name '$pkgName' is consistently vetted by all hosting participants of party $party." - ) - ) - .toMap - - // If all the required package-names are present in the candidates for the party, all good. - // If there are required package-names that are not present in the party's candidates, - // back-fill the candidates for the party with an error reason for each missing package-name - party -> (candidatesForParty ++ pkgNameWithNoVettedCandidates) - } - - // Select the highest version package for each requested package-name - // or discard the preference set if there is a requirement not satisfied - private def selectRequestedPackages( - candidates: MapView[PackageName, Candidate[SortedPreferences]], - requiredPackageNames: Set[PackageName], - ): Candidate[Set[PackageReference]] = - requiredPackageNames.toList - .foldM(Set.empty[PackageReference]) { case (acc, requestedPackageName) => - for { - preferencesForNameE <- candidates - .get(requestedPackageName) - .toRight( - show"No package is consistently vetted by all hosting participants of the requested parties for package-name '$requestedPackageName'" - ) - preferencesForName <- preferencesForNameE - } yield acc + preferencesForName.last1 - } - - private def isSchemaPackage(pkg: Ast.PackageSignature): Boolean = - pkg.modules.exists { case (_, module) => - module.interfaces.nonEmpty || module.templates.nonEmpty - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/ResourceCloseable.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/ResourceCloseable.scala deleted file mode 100644 index f8bbf3ac6c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/ResourceCloseable.scala +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.daml.ledger.resources.Resource -import com.digitalasset.canton.lifecycle.{AsyncCloseable, AsyncOrSyncCloseable, FlagCloseableAsync} -import com.digitalasset.canton.logging.NamedLogging -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Mutex - -/** This helper class serves as a bridge between FlagCloseable (canton's shutdown/resource manager - * trait) and Resource (daml's shutdown/resource management container). Recommended usage: - * 1. The service class needs to be prepared with subclassing the ResourceCloseable. - * 1. As the service instance is created, the ResourceOwnerFlagCloseableOps should be used to - * instantiate a FlagCloseable with the ResourceOwner[ServiceClass].acquireFlagCloseable. - * 1. The resulting ServiceClass instance can be used as FlagCloseable: as it is getting closed, - * the wrapped Resource will be released. - */ -@SuppressWarnings(Array("org.wartremover.warts.Var")) -abstract class ResourceCloseable extends FlagCloseableAsync with NamedLogging { - private var closeableResource: Option[AsyncCloseable] = None - - private val lock = new Mutex() - - override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = (lock.exclusive { - List( - closeableResource.getOrElse( - throw new IllegalStateException( - "Programming error: resource not registered. Please use ResourceOwnerOps.toCloseable." - ) - ) - ) - }) - - def registerResource(resource: Resource[?], name: String)(implicit - traceContext: TraceContext - ): this.type = (lock.exclusive { - this.closeableResource.foreach(_ => - throw new IllegalStateException( - "Programming error: resource registered multiple times. Please use ResourceOwnerFlagCloseableOps.acquireFlagCloseable." - ) - ) - this.closeableResource = Some( - AsyncCloseable( - name = name, - closeFuture = resource.release(), - timeout = timeouts.shutdownNetwork, - onTimeout = err => - logger.warn(s"Resource $name failed to close within ${timeouts.shutdownNetwork}.", err), - ) - ) - this - }) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/TemplatePartiesFilter.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/TemplatePartiesFilter.scala deleted file mode 100644 index cec79aa6fe..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/TemplatePartiesFilter.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.digitalasset.daml.lf.data.Ref.NameTypeConRef - -/** This class represents the filters used in transactions and contracts fetching based on the - * templates or interfaces they implement and the parties included. - * - * @param relation - * holds the per template filters, if the value of a specific key (identifier) is defined then - * the filter corresponds only to the specific set of parties, if None then all the parties known - * to the participant are included - * @param templateWildcardParties - * represents all the templates (template-wildcard) for the set of parties specified if defined - * or all the parties known to the participant if None. - */ -final case class TemplatePartiesFilter( - relation: Map[NameTypeConRef, Option[Set[Party]]], - templateWildcardParties: Option[Set[Party]], -) { - val allFilterParties: Option[Set[Party]] = { - val partiesO = Seq(templateWildcardParties) ++ relation.values - if (partiesO.exists(_.isEmpty)) { - None - } else { - Some(partiesO.flatten.flatten.toSet) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiException.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiException.scala deleted file mode 100644 index 2a425469a1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiException.scala +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import io.grpc.StatusRuntimeException - -import scala.util.control.NoStackTrace - -/** The sole purpose of this class is to give StatusRuntimeException with NoStacktrace a nice name - * in logs. - */ -class ApiException(exception: StatusRuntimeException) - extends StatusRuntimeException(exception.getStatus, exception.getTrailers) - with NoStackTrace diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiService.scala deleted file mode 100644 index 37409cdf46..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiService.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.digitalasset.canton.config.RequireTypes.Port - -trait ApiService { - - /** the API port the server is listening on */ - def port: Port - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiServiceOwner.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiServiceOwner.scala deleted file mode 100644 index aa4213b8d8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiServiceOwner.scala +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.jwt.JwtTimestampLeeway -import com.daml.ledger.resources.ResourceOwner -import com.daml.tls.TlsServerConfig -import com.digitalasset.canton.auth.* -import com.digitalasset.canton.config.* -import com.digitalasset.canton.config.RequireTypes.Port -import com.digitalasset.canton.health.HealthChecks -import com.digitalasset.canton.interactive.InteractiveSubmissionEnricher -import com.digitalasset.canton.ledger.api.IdentityProviderConfig -import com.digitalasset.canton.ledger.api.auth.* -import com.digitalasset.canton.ledger.api.auth.interceptor.UserBasedClaimResolver -import com.digitalasset.canton.ledger.api.util.{TimeProvider, TimeProviderType} -import com.digitalasset.canton.ledger.localstore.api.{ - IdentityProviderConfigStore, - PartyRecordStore, - UserManagementStore, -} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.index.IndexService -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.PackagePreferenceBackend -import com.digitalasset.canton.platform.apiserver.SeedService.Seeding -import com.digitalasset.canton.platform.apiserver.execution.{ - CommandProgressTracker, - DynamicSynchronizerParameterGetter, -} -import com.digitalasset.canton.platform.apiserver.services.ApiContractService -import com.digitalasset.canton.platform.apiserver.services.admin.PartyAllocation -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker -import com.digitalasset.canton.platform.config.{ - CommandServiceConfig, - IdentityProviderManagementConfig, - InteractiveSubmissionServiceConfig, - PackageServiceConfig, - PartyManagementServiceConfig, - StateServiceConfig, - UpdateServiceConfig, - UserManagementServiceConfig, -} -import com.digitalasset.canton.scheduler.SafeToPruneCommitmentState -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ContractValidator.ContractAuthenticatorFn -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.engine.Engine -import io.grpc.{BindableService, ServerInterceptor} -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.stream.Materializer - -import java.time.Clock -import scala.collection.immutable -import scala.concurrent.{ExecutionContextExecutor, Future} - -object ApiServiceOwner { - - def apply( - // configuration parameters - address: Option[String] = DefaultAddress, // This defaults to "localhost" when set to `None`. - maxInboundMessageSize: Int = DefaultMaxInboundMessageSize, - maxInboundMetadataSize: Int = ServerConfig.defaultMaxInboundMetadataSize.unwrap, - maxConcurrentCallsPerConnection: Int = - ServerConfig.defaultMaxConcurrentCallsPerConnection.unwrap, - port: Port = DefaultPort, - tls: Option[TlsServerConfig] = DefaultTls, - seeding: Seeding = DefaultSeeding, - managementServiceTimeout: NonNegativeFiniteDuration = - ApiServiceOwner.DefaultManagementServiceTimeout, - ledgerFeatures: LedgerFeatures, - maxDeduplicationDuration: NonNegativeFiniteDuration, - jwtTimestampLeeway: Option[JwtTimestampLeeway], - tokenExpiryGracePeriodForStreams: Option[NonNegativeDuration], - // immutable configuration parameters - participantId: Ref.ParticipantId, - // objects - indexService: IndexService, - transactionSubmissionTracker: SubmissionTracker, - reassignmentSubmissionTracker: SubmissionTracker, - partyAllocationTracker: PartyAllocation.Tracker, - commandProgressTracker: CommandProgressTracker, - userManagementStore: UserManagementStore, - identityProviderConfigStore: IdentityProviderConfigStore, - partyRecordStore: PartyRecordStore, - command: CommandServiceConfig = ApiServiceOwner.DefaultCommandServiceConfig, - syncService: state.SyncService, - healthChecks: HealthChecks, - metrics: LedgerApiServerMetrics, - timeServiceBackend: Option[TimeServiceBackend] = None, - otherServices: immutable.Seq[BindableService] = immutable.Seq.empty, - otherInterceptors: List[ServerInterceptor] = List.empty, - engine: Engine, - queryExecutionContext: ExecutionContextExecutor, - commandExecutionContext: ExecutionContextExecutor, - checkOverloaded: TraceContext => Option[state.SubmissionResult] = - _ => None, // Used for Canton rate-limiting, - authServices: Seq[AuthService], - jwtVerifierLoader: JwtVerifierLoader, - userManagement: UserManagementServiceConfig = ApiServiceOwner.DefaultUserManagement, - partyManagementServiceConfig: PartyManagementServiceConfig = - ApiServiceOwner.DefaultPartyManagementServiceConfig, - packageServiceConfig: PackageServiceConfig = ApiServiceOwner.DefaultPackageServiceConfig, - updateServiceConfig: UpdateServiceConfig, - stateServiceConfig: StateServiceConfig, - loggerFactory: NamedLoggerFactory, - contractAuthenticator: ContractAuthenticatorFn, - dynParamGetter: DynamicSynchronizerParameterGetter, - interactiveSubmissionServiceConfig: InteractiveSubmissionServiceConfig, - interactiveSubmissionEnricher: InteractiveSubmissionEnricher, - keepAlive: Option[KeepAliveServerConfig], - packagePreferenceBackend: PackagePreferenceBackend, - apiLoggingConfig: ApiLoggingConfig, - apiContractService: ApiContractService, - safeToPruneCommitmentState: Option[SafeToPruneCommitmentState], - )(implicit - actorSystem: ActorSystem, - materializer: Materializer, - traceContext: TraceContext, - tracer: Tracer, - ): ResourceOwner[(ApiService, AuthInterceptor)] = { - import com.digitalasset.canton.platform.ResourceOwnerOps - val logger = loggerFactory.getTracedLogger(getClass) - - val authorizer = new Authorizer( - now = Clock.systemUTC.instant _, - participantId = participantId, - ongoingAuthorizationFactory = UserBasedOngoingAuthorization.Factory( - now = Clock.systemUTC.instant _, - userManagementStore = userManagementStore, - userRightsCheckIntervalInSeconds = userManagement.cacheExpiryAfterWriteInSeconds, - pekkoScheduler = actorSystem.scheduler, - jwtTimestampLeeway = jwtTimestampLeeway, - tokenExpiryGracePeriodForStreams = - tokenExpiryGracePeriodForStreams.map(_.asJavaApproximation), - loggerFactory = loggerFactory, - )(commandExecutionContext, traceContext), - jwtTimestampLeeway = jwtTimestampLeeway, - loggerFactory = loggerFactory, - ) - val healthChecksWithIndexService = healthChecks + ("index" -> indexService) - - val identityProviderConfigLoader = new IdentityProviderConfigLoader { - override def getIdentityProviderConfig(issuer: String)(implicit - loggingContext: LoggingContextWithTrace - ): Future[IdentityProviderConfig] = - identityProviderConfigStore.getActiveIdentityProviderByIssuer(issuer)( - loggingContext, - commandExecutionContext, - ) - } - val userAuthInterceptor = new AuthInterceptor( - authServices = authServices :+ new IdentityProviderAwareAuthService( - identityProviderConfigLoader = identityProviderConfigLoader, - jwtVerifierLoader = jwtVerifierLoader, - loggerFactory = loggerFactory, - )(commandExecutionContext), - loggerFactory = loggerFactory, - ec = commandExecutionContext, - claimResolver = new UserBasedClaimResolver( - userManagementStoreO = Option.when(userManagement.enabled)(userManagementStore), - ec = commandExecutionContext, - ), - ) - for { - executionSequencerFactory <- new ExecutionSequencerFactoryOwner() - .afterReleased(logger.info(s"ExecutionSequencerFactory is released for LedgerApiService")) - apiServicesOwner = ApiServices( - participantId = participantId, - syncService = syncService, - indexService = indexService, - authorizer = authorizer, - engine = engine, - timeProvider = timeServiceBackend.getOrElse(TimeProvider.UTC), - timeProviderType = - timeServiceBackend.fold[TimeProviderType](TimeProviderType.WallClock)(_ => - TimeProviderType.Static - ), - transactionSubmissionTracker = transactionSubmissionTracker, - reassignmentSubmissionTracker = reassignmentSubmissionTracker, - partyAllocationTracker = partyAllocationTracker, - commandProgressTracker = commandProgressTracker, - commandConfig = command, - optTimeServiceBackend = timeServiceBackend, - queryExecutionContext = queryExecutionContext, - commandExecutionContext = commandExecutionContext, - metrics = metrics, - healthChecks = healthChecksWithIndexService, - seedService = SeedService(seeding), - managementServiceTimeout = managementServiceTimeout.underlying, - checkOverloaded = checkOverloaded, - userManagementStore = userManagementStore, - identityProviderConfigStore = identityProviderConfigStore, - partyRecordStore = partyRecordStore, - ledgerFeatures = ledgerFeatures, - maxDeduplicationDuration = maxDeduplicationDuration, - userManagementServiceConfig = userManagement, - partyManagementServiceConfig = partyManagementServiceConfig, - packageServiceConfig = packageServiceConfig, - updateServiceConfig = updateServiceConfig, - stateServiceConfig = stateServiceConfig, - loggerFactory = loggerFactory, - contractAuthenticator = contractAuthenticator, - dynParamGetter = dynParamGetter, - interactiveSubmissionServiceConfig = interactiveSubmissionServiceConfig, - interactiveSubmissionEnricher = interactiveSubmissionEnricher, - packagePreferenceBackend = packagePreferenceBackend, - safeToPruneCommitmentState = safeToPruneCommitmentState, - logger = loggerFactory.getTracedLogger(this.getClass), - apiContractService = apiContractService, - )(materializer, executionSequencerFactory, tracer).withServices(otherServices) - // for all the top level gRPC servicing apparatus we use the writeApiServicesExecutionContext - apiService <- LedgerApiService( - apiServicesOwner, - port, - maxInboundMessageSize, - maxInboundMetadataSize, - maxConcurrentCallsPerConnection, - address, - tls, - // TODO (i28340) fix order of interceptors - new GrpcAuthInterceptor( - userAuthInterceptor, - loggerFactory, - apiLoggingConfig = apiLoggingConfig, - commandExecutionContext, - ) - :: otherInterceptors, - commandExecutionContext, - metrics, - keepAlive, - loggerFactory, - ).afterReleased(logger.info(s"LedgerApiService is released")) - } yield { - logger.info( - s"Initialized API server listening to port = ${apiService.port} ${if (tls.isDefined) "using tls" - else "without tls"}." - ) - (apiService, userAuthInterceptor) - } - } - - val DefaultPort: Port = Port.tryCreate(6865) - val DefaultAddress: Option[String] = None - val DefaultTls: Option[TlsServerConfig] = None - val DefaultMaxInboundMessageSize: Int = 64 * 1024 * 1024 // Larger than ServerConfig default - val DefaultSeeding: Seeding = Seeding.Strong - val DefaultManagementServiceTimeout: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofMinutes(2) - val DefaultUserManagement: UserManagementServiceConfig = - UserManagementServiceConfig.default(enabled = false) - val DefaultPartyManagementServiceConfig: PartyManagementServiceConfig = - PartyManagementServiceConfig.default - val DefaultPackageServiceConfig: PackageServiceConfig = - PackageServiceConfig.default - val DefaultIdentityProviderManagementConfig: IdentityProviderManagementConfig = - IdentityProviderManagementConfig() - val DefaultCommandServiceConfig: CommandServiceConfig = CommandServiceConfig.Default -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiServices.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiServices.scala deleted file mode 100644 index c77398d4a3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ApiServices.scala +++ /dev/null @@ -1,431 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.digitalasset.canton.auth.Authorizer -import com.digitalasset.canton.config -import com.digitalasset.canton.health.HealthChecks -import com.digitalasset.canton.interactive.InteractiveSubmissionEnricher -import com.digitalasset.canton.ledger.api.SubmissionIdGenerator -import com.digitalasset.canton.ledger.api.auth.services.* -import com.digitalasset.canton.ledger.api.grpc.GrpcHealthService -import com.digitalasset.canton.ledger.api.util.{TimeProvider, TimeProviderType} -import com.digitalasset.canton.ledger.api.validation.* -import com.digitalasset.canton.ledger.localstore.api.{ - IdentityProviderConfigStore, - PartyRecordStore, - UserManagementStore, -} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.index.* -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.PackagePreferenceBackend -import com.digitalasset.canton.platform.apiserver.execution.* -import com.digitalasset.canton.platform.apiserver.services.* -import com.digitalasset.canton.platform.apiserver.services.admin.* -import com.digitalasset.canton.platform.apiserver.services.command.interactive.InteractiveSubmissionServiceImpl -import com.digitalasset.canton.platform.apiserver.services.command.{ - CommandInspectionServiceImpl, - CommandServiceImpl, - CommandSubmissionServiceImpl, -} -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker -import com.digitalasset.canton.platform.config.* -import com.digitalasset.canton.platform.packages.DeduplicatingPackageLoader -import com.digitalasset.canton.scheduler.SafeToPruneCommitmentState -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ContractValidator.ContractAuthenticatorFn -import com.digitalasset.canton.util.PackageConsumer.PackageResolver -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.PackageId -import com.digitalasset.daml.lf.engine.* -import com.digitalasset.daml.lf.language.Ast -import io.grpc.BindableService -import io.grpc.protobuf.services.ProtoReflectionServiceV1 -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.stream.Materializer - -import java.time.Instant -import scala.collection.immutable -import scala.concurrent.ExecutionContext -import scala.concurrent.duration.FiniteDuration - -trait ApiServices extends AutoCloseable { - val services: Iterable[BindableService] - - def withServices(otherServices: immutable.Seq[BindableService]): ApiServices -} - -private final case class ApiServicesBundle( - services: immutable.Seq[BindableService], - loggerFactory: NamedLoggerFactory, -) extends ApiServices - with NamedLogging { - - override def withServices(otherServices: immutable.Seq[BindableService]): ApiServices = - copy(services = services ++ otherServices) - - override def close(): Unit = { - services.foreach { - case closeable: AutoCloseable => - noTracingLogger.debug(s"Closing $closeable") - closeable.close() - noTracingLogger.debug(s"Successfully closed $closeable") - case nonCloseable => - noTracingLogger.debug(s"Omit closing $nonCloseable, as it is not closeable") - } - noTracingLogger.info(s"Successfully closed all API services") - } - -} - -object ApiServices { - def apply( - participantId: Ref.ParticipantId, - syncService: state.SyncService, - indexService: IndexService, - userManagementStore: UserManagementStore, - identityProviderConfigStore: IdentityProviderConfigStore, - partyRecordStore: PartyRecordStore, - authorizer: Authorizer, - engine: Engine, - timeProvider: TimeProvider, - timeProviderType: TimeProviderType, - transactionSubmissionTracker: SubmissionTracker, - reassignmentSubmissionTracker: SubmissionTracker, - partyAllocationTracker: PartyAllocation.Tracker, - commandProgressTracker: CommandProgressTracker, - commandConfig: CommandServiceConfig, - optTimeServiceBackend: Option[TimeServiceBackend], - queryExecutionContext: ExecutionContext, - commandExecutionContext: ExecutionContext, - metrics: LedgerApiServerMetrics, - healthChecks: HealthChecks, - seedService: SeedService, - managementServiceTimeout: FiniteDuration, - checkOverloaded: TraceContext => Option[state.SubmissionResult], - ledgerFeatures: LedgerFeatures, - maxDeduplicationDuration: config.NonNegativeFiniteDuration, - userManagementServiceConfig: UserManagementServiceConfig, - partyManagementServiceConfig: PartyManagementServiceConfig, - packageServiceConfig: PackageServiceConfig, - updateServiceConfig: UpdateServiceConfig, - stateServiceConfig: StateServiceConfig, - contractAuthenticator: ContractAuthenticatorFn, - loggerFactory: NamedLoggerFactory, - dynParamGetter: DynamicSynchronizerParameterGetter, - interactiveSubmissionServiceConfig: InteractiveSubmissionServiceConfig, - interactiveSubmissionEnricher: InteractiveSubmissionEnricher, - logger: TracedLogger, - packagePreferenceBackend: PackagePreferenceBackend, - apiContractService: ApiContractService, - safeToPruneCommitmentState: Option[SafeToPruneCommitmentState], - )(implicit - materializer: Materializer, - esf: ExecutionSequencerFactory, - tracer: Tracer, - ): ApiServices = { - implicit val traceContext: TraceContext = TraceContext.empty - val activeContractsService: IndexActiveContractsService = indexService - val updateService: IndexUpdateService = indexService - val eventQueryService: IndexEventQueryService = indexService - val contractStore: ContractStore = indexService - val maximumLedgerTimeService: MaximumLedgerTimeService = indexService - val completionsService: IndexCompletionsService = indexService - val partyManagementService: IndexPartyManagementService = indexService - - val (readServices, ledgerApiUpdateService) = { - implicit val ec: ExecutionContext = queryExecutionContext - - val apiInspectionServiceOpt = - Option - .when(ledgerFeatures.commandInspectionService.supported)( - new CommandInspectionServiceAuthorization( - CommandInspectionServiceImpl.createApiService( - commandProgressTracker, - loggerFactory, - ), - authorizer, - ) - ) - - val (ledgerApiServices, ledgerApiUpdateService) = { - val apiTimeServiceOpt = - optTimeServiceBackend.map(tsb => - new TimeServiceAuthorization( - new ApiTimeService(tsb, loggerFactory), - authorizer, - ) - ) - val apiCommandCompletionService = new ApiCommandCompletionService( - completionsService, - metrics, - loggerFactory, - ) - val apiEventQueryService = - new ApiEventQueryService(eventQueryService, loggerFactory) - val apiPackageService = new ApiPackageService( - syncService, - packageServiceConfig, - loggerFactory, - ) - val apiUpdateService = - new ApiUpdateService( - updateService = updateService, - metrics = metrics, - loggerFactory = loggerFactory, - participantId = participantId, - updateServiceConfig = updateServiceConfig, - ) - val apiStateService = - new ApiStateService( - acsService = activeContractsService, - syncService = syncService, - updateService = updateService, - participantId = participantId, - config = stateServiceConfig, - metrics = metrics, - loggerFactory = loggerFactory, - ) - val apiVersionService = - new ApiVersionService( - ledgerFeatures, - userManagementServiceConfig, - partyManagementServiceConfig, - packageServiceConfig, - loggerFactory, - ) - - val services = apiTimeServiceOpt.toList ::: - List( - new CommandCompletionServiceAuthorization(apiCommandCompletionService, authorizer), - new EventQueryServiceAuthorization(apiEventQueryService, authorizer), - new PackageServiceAuthorization(apiPackageService, authorizer), - new UpdateServiceAuthorization(apiUpdateService, authorizer), - new StateServiceAuthorization(apiStateService, authorizer), - new ContractServiceAuthorization(apiContractService, authorizer), - apiVersionService, - ) - - services -> apiUpdateService - } - - val apiReflectionService = ProtoReflectionServiceV1.newInstance() - - val apiHealthService = new GrpcHealthService(healthChecks, loggerFactory) - - val userManagementServices: List[BindableService] = - if (userManagementServiceConfig.enabled) { - val apiUserManagementService = - new ApiUserManagementService( - userManagementStore = userManagementStore, - maxUsersPageSize = userManagementServiceConfig.maxUsersPageSize, - submissionIdGenerator = SubmissionIdGenerator.Random, - identityProviderExists = new IdentityProviderExists(identityProviderConfigStore), - partyRecordExist = new PartyRecordsExist(partyRecordStore), - loggerFactory = loggerFactory, - ) - val identityProvider = - new ApiIdentityProviderConfigService( - identityProviderConfigStore, - loggerFactory, - ) - List( - new UserManagementServiceAuthorization( - apiUserManagementService, - authorizer, - loggerFactory, - ), - new IdentityProviderConfigServiceAuthorization(identityProvider, authorizer), - ) - } else { - List.empty - } - - val readServices = ledgerApiServices ::: - apiInspectionServiceOpt.toList ::: - List( - apiReflectionService, - apiHealthService, - ) ::: userManagementServices - readServices -> ledgerApiUpdateService - } - - val writeServices = { - implicit val ec: ExecutionContext = commandExecutionContext - - val packageLoader = new DeduplicatingPackageLoader() - - val packageResolver: PackageResolver = new PackageResolver { - override protected def resolveInternal( - packageId: PackageId - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Option[Ast.Package]] = - FutureUnlessShutdown.outcomeF( - packageLoader.loadPackage( - packageId, - syncService.getLfArchive(_), - metrics.execution.getLfPackage, - ) - ) - } - - val commandInterpreter = - new StoreBackedCommandInterpreter( - engine = engine, - participant = participantId, - packageResolver = packageResolver, - contractStore = contractStore, - contractAuthenticator = contractAuthenticator, - metrics = metrics, - prefetchingRecursionLevel = commandConfig.contractPrefetchingDepth, - loggerFactory = loggerFactory, - dynParamGetter = dynParamGetter, - timeProvider = timeProvider, - ) - - val commandExecutor = - new TimedCommandExecutor( - new LedgerTimeAwareCommandExecutor( - delegate = CommandExecutor( - syncService = syncService, - commandInterpreter = commandInterpreter, - topologyAwarePackageSelectionEnabled = ledgerFeatures.topologyAwarePackageSelection, - tapsMaxPassesDefault = ledgerFeatures.tapsMaxPassesDefault, - tapsMaxPassesLimit = ledgerFeatures.tapsMaxPassesLimit, - metrics = metrics, - loggerFactory = loggerFactory, - ), - new ResolveMaximumLedgerTime(maximumLedgerTimeService, loggerFactory), - maxRetries = 3, - metrics, - loggerFactory, - ), - metrics, - ) - - val validateUpgradingPackageResolutions = - new ValidateUpgradingPackageResolutionsImpl( - getPackageMetadataSnapshot = syncService.getPackageMetadataSnapshot(_) - ) - val commandsValidator = new CommandsValidator( - validateUpgradingPackageResolutions = validateUpgradingPackageResolutions, - topologyAwarePackageSelectionEnabled = ledgerFeatures.topologyAwarePackageSelection, - ) - val commandSubmissionService = - CommandSubmissionServiceImpl.createApiService( - syncService, - timeProvider, - timeProviderType, - seedService, - commandExecutor, - checkOverloaded, - metrics, - loggerFactory, - ) - val apiPartyManagementService = ApiPartyManagementService.createApiService( - partyManagementService, - userManagementStore, - new IdentityProviderExists(identityProviderConfigStore), - partyManagementServiceConfig.maxPartiesPageSize, - partyManagementServiceConfig.maxSelfAllocatedParties, - partyRecordStore, - syncService, - managementServiceTimeout, - partyAllocationTracker = partyAllocationTracker, - submissionIdGenerator = - ApiPartyManagementService.CreateSubmissionId.forParticipant(participantId), - loggerFactory = loggerFactory, - ) - - val apiPackageManagementService = - ApiPackageManagementService.createApiService( - packageSyncService = syncService, - loggerFactory = loggerFactory, - ) - - val participantPruningService = ApiParticipantPruningService.createApiService( - indexService, - syncService, - metrics, - safeToPruneCommitmentState, - loggerFactory, - ) - - val apiSubmissionService = new ApiCommandSubmissionService( - commandsValidator = commandsValidator, - commandSubmissionService = commandSubmissionService, - submissionSyncService = syncService, - currentLedgerTime = () => timeProvider.getCurrentTime, - currentUtcTime = () => Instant.now, - maxDeduplicationDuration = maxDeduplicationDuration.asJava, - submissionIdGenerator = SubmissionIdGenerator.Random, - tracker = commandProgressTracker, - metrics = metrics, - loggerFactory = loggerFactory, - ) - val updateServices = new CommandServiceImpl.UpdateServices( - getUpdateById = ledgerApiUpdateService.getUpdateById - ) - val apiCommandService = CommandServiceImpl.createApiService( - commandsValidator = commandsValidator, - transactionSubmissionTracker = transactionSubmissionTracker, - reassignmentSubmissionTracker = reassignmentSubmissionTracker, - // Using local services skips the gRPC layer, improving performance. - submit = apiSubmissionService.submitWithTraceContext, - submitReassignment = apiSubmissionService.submitReassignmentWithTraceContext, - defaultTrackingTimeout = commandConfig.defaultTrackingTimeout, - updateServices = updateServices, - timeProvider = timeProvider, - maxDeduplicationDuration = maxDeduplicationDuration, - loggerFactory = loggerFactory, - ) - - val apiInteractiveSubmissionService = { - val interactiveSubmissionService = - InteractiveSubmissionServiceImpl.createApiService( - updateServices, - syncService, - seedService, - commandExecutor, - metrics, - checkOverloaded, - interactiveSubmissionEnricher, - interactiveSubmissionServiceConfig, - contractStore, - packagePreferenceBackend, - transactionSubmissionTracker, - commandConfig.defaultTrackingTimeout, - loggerFactory, - ) - - new ApiInteractiveSubmissionService( - commandsValidator = commandsValidator, - interactiveSubmissionService = interactiveSubmissionService, - currentLedgerTime = () => timeProvider.getCurrentTime, - currentUtcTime = () => Instant.now, - maxDeduplicationDuration = maxDeduplicationDuration.asJava, - submissionIdGenerator = SubmissionIdGenerator.Random, - tracker = commandProgressTracker, - metrics = metrics, - loggerFactory = loggerFactory, - ) - } - - List( - new CommandSubmissionServiceAuthorization(apiSubmissionService, authorizer), - new CommandServiceAuthorization(apiCommandService, authorizer), - new PartyManagementServiceAuthorization(apiPartyManagementService, authorizer), - new PackageManagementServiceAuthorization(apiPackageManagementService, authorizer), - new ParticipantPruningServiceAuthorization(participantPruningService, authorizer), - new InteractiveSubmissionServiceAuthorization(apiInteractiveSubmissionService, authorizer), - ) - } - - logger.info(engine.info.toString) - ApiServicesBundle(readServices ::: writeServices, loggerFactory) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ExecutionSequencerFactoryOwner.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ExecutionSequencerFactoryOwner.scala deleted file mode 100644 index 8df8b67659..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ExecutionSequencerFactoryOwner.scala +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.grpc.adapter.{ExecutionSequencerFactory, PekkoExecutionSequencerPool} -import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner} -import org.apache.pekko.actor.ActorSystem - -import java.util.UUID -import scala.concurrent.Future - -final class ExecutionSequencerFactoryOwner(implicit actorSystem: ActorSystem) - extends ResourceOwner[ExecutionSequencerFactory] { - // NOTE: Pick a unique pool name as we want to allow multiple LedgerApiServer instances, - // and it's pretty difficult to wait for the name to become available again. - // The name deregistration is asynchronous and the close method does not wait, and it isn't - // trivial to implement. - // https://doc.akka.io/docs/akka/2.5/actors.html#graceful-stop - private val poolName = s"ledger-api-server-rs-grpc-bridge-${UUID.randomUUID}" - - private val ActorCount = Runtime.getRuntime.availableProcessors() * 8 - - override def acquire()(implicit context: ResourceContext): Resource[ExecutionSequencerFactory] = - Resource(Future(new PekkoExecutionSequencerPool(poolName, ActorCount)))(_.closeAsync()) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcConnectionLogger.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcConnectionLogger.scala deleted file mode 100644 index f3ca7461a4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcConnectionLogger.scala +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext -import io.grpc.{Attributes, ServerTransportFilter} - -final case class GrpcConnectionLogger(loggerFactory: NamedLoggerFactory) - extends ServerTransportFilter - with NamedLogging { - override def transportReady(transportAttrs: Attributes): Attributes = { - val attributes = if (transportAttrs == null) "" else transportAttrs.toString - logger.debug(s"Grpc connection open: $attributes")(TraceContext.empty) - super.transportReady(transportAttrs) - } - - override def transportTerminated(transportAttrs: Attributes): Unit = { - val attributes = if (transportAttrs == null) "" else transportAttrs.toString - logger.debug(s"Grpc connection closed: $attributes")(TraceContext.empty) - super.transportTerminated(transportAttrs) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcInterceptors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcInterceptors.scala deleted file mode 100644 index 6cf431cd70..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcInterceptors.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.metrics.grpc.GrpcMetricsServerInterceptor -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.error.ErrorInterceptor -import io.grpc.ServerInterceptor - -object GrpcInterceptors { - // Unfortunately, we can't get the maximum inbound message size from the client, so we don't know - // how big this should be. This seems long enough to contain useful data, but short enough that it - // won't break most well-configured clients. - // As the default response header limit for a Netty client is 8 KB, we set our limit to 4 KB to - // allow for extra information such as the exception stack trace. - private val MaximumStatusDescriptionLength = 4 * 1024 // 4 KB - def apply( - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - interceptors: List[ServerInterceptor] = List.empty, - ): List[ServerInterceptor] = interceptors ::: List( - new GrpcMetricsServerInterceptor(metrics.grpc), - new TruncatedStatusInterceptor(MaximumStatusDescriptionLength), - new ErrorInterceptor(loggerFactory), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcServer.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcServer.scala deleted file mode 100644 index e7b2f962c5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcServer.scala +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.digitalasset.canton.config.KeepAliveServerConfig -import com.digitalasset.canton.config.RequireTypes.Port -import com.digitalasset.canton.logging.NamedLoggerFactory -import io.grpc.* -import io.grpc.inprocess.InProcessServerBuilder -import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext - -import java.net.{InetAddress, InetSocketAddress} -import java.util.concurrent.{Executor, TimeUnit} - -@SuppressWarnings(Array("org.wartremover.warts.Null")) -object GrpcServer { - def netty( - address: Option[String], - desiredPort: Port, - maxInboundMessageSize: Int, - maxInboundMetadataSize: Int, - maxConcurrentCallsPerConnection: Int, - sslContext: Option[SslContext] = None, - keepAlive: Option[KeepAliveServerConfig], - interceptors: List[ServerInterceptor] = List.empty, - services: Iterable[BindableService], - servicesExecutor: Executor, - loggerFactory: NamedLoggerFactory, - ): NettyServerBuilder = { - val host = address.map(InetAddress.getByName).getOrElse(InetAddress.getLoopbackAddress) - val builder = - NettyServerBuilder - .forAddress(new InetSocketAddress(host, desiredPort.unwrap)) - .sslContext(sslContext.orNull) - .executor(servicesExecutor) - .maxInboundMessageSize(maxInboundMessageSize) - .maxInboundMetadataSize(maxInboundMetadataSize) - .maxConcurrentCallsPerConnection(maxConcurrentCallsPerConnection) - .addTransportFilter(GrpcConnectionLogger(loggerFactory)) - val builderWithKeepAlive = configureKeepAlive(keepAlive, builder) - addServicesAndInterceptors(builderWithKeepAlive, interceptors, services) - } - def inProc( - desiredPort: Port, - interceptors: List[ServerInterceptor] = List.empty, - services: Iterable[BindableService], - servicesExecutor: Executor, - ): InProcessServerBuilder = { - val inProcessBuilder: InProcessServerBuilder = - InProcessServerBuilder - .forName(InProcessGrpcName.forPort(desiredPort)) - .executor(servicesExecutor) - addServicesAndInterceptors(inProcessBuilder, interceptors, services) - } - - private def addServicesAndInterceptors[T <: ForwardingServerBuilder[T]]( - builder: T, - interceptors: List[ServerInterceptor], - services: Iterable[BindableService], - ) = { - // NOTE: Interceptors run in the reverse order in which they were added. - val builderWithInterceptors = interceptors.foldLeft(builder) { case (builder, interceptor) => - builder.intercept(interceptor) - } - services.foldLeft(builderWithInterceptors) { case (builder, service) => - builder.addService(service) - } - } - - private def configureKeepAlive( - keepAlive: Option[KeepAliveServerConfig], - builder: NettyServerBuilder, - ): NettyServerBuilder = - keepAlive.fold(builder) { ka => - val time = ka.time.unwrap.toMillis - val timeout = ka.timeout.unwrap.toMillis - val permitTime = ka.permitKeepAliveTime.unwrap.toMillis - val permitKAWOCalls = ka.permitKeepAliveWithoutCalls - builder - .keepAliveTime(time, TimeUnit.MILLISECONDS) - .keepAliveTimeout(timeout, TimeUnit.MILLISECONDS) - .permitKeepAliveTime( - permitTime, - TimeUnit.MILLISECONDS, - ) // gracefully allowing a bit more aggressive keep alives from clients - .permitKeepAliveWithoutCalls(permitKAWOCalls) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcServerOwner.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcServerOwner.scala deleted file mode 100644 index 3825d79fa7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/GrpcServerOwner.scala +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.config.KeepAliveServerConfig -import com.digitalasset.canton.config.RequireTypes.Port -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext -import io.grpc.{BindableService, ForwardingServerBuilder, Server, ServerInterceptor} - -import java.io.IOException -import java.net.BindException -import java.util.concurrent.Executor -import scala.concurrent.duration.DurationInt -import scala.util.Failure -import scala.util.control.NoStackTrace - -object GrpcServerOwner { - - @SuppressWarnings(Array("org.wartremover.warts.IsInstanceOf")) - def apply( - address: Option[String], - desiredPort: Port, - maxInboundMessageSize: Int, - maxInboundMetadataSize: Int, - maxConcurrentCallsPerConnection: Int, - sslContext: Option[SslContext] = None, - interceptors: List[ServerInterceptor] = List.empty, - metrics: LedgerApiServerMetrics, - servicesExecutor: Executor, - services: Iterable[BindableService], - loggerFactory: NamedLoggerFactory, - keepAlive: Option[KeepAliveServerConfig], - ): ResourceOwner[Server] = { - - val allInterceptors = GrpcInterceptors(metrics, loggerFactory, interceptors) - - def build[T <: ForwardingServerBuilder[T]](builderWithServices: T) = - ResourceOwner - .forServer(builderWithServices, shutdownTimeout = 1.second) - .transform(_.recoverWith { - case e: IOException if e.getCause != null && e.getCause.isInstanceOf[BindException] => - Failure(new UnableToBind(desiredPort, e.getCause)) - }) - - for { - _ <- build(GrpcServer.inProc(desiredPort, allInterceptors, services, servicesExecutor)) - httpServer <- build( - GrpcServer.netty( - address, - desiredPort, - maxInboundMessageSize, - maxInboundMetadataSize, - maxConcurrentCallsPerConnection, - sslContext, - keepAlive, - allInterceptors, - services, - servicesExecutor, - loggerFactory, - ) - ) - } yield httpServer - } - - final class UnableToBind(port: Port, cause: Throwable) - extends RuntimeException( - s"The API server was unable to bind to port $port. Terminate the process occupying the port, or choose a different one.", - cause, - ) - with NoStackTrace - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/InProcessGrpcName.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/InProcessGrpcName.scala deleted file mode 100644 index 9681fc8ab0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/InProcessGrpcName.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.digitalasset.canton.config.RequireTypes.Port - -object InProcessGrpcName { - def forPort(port: Port): String = s"inprocess-grpc-${port.unwrap}" -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/LedgerApiService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/LedgerApiService.scala deleted file mode 100644 index 3c18877395..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/LedgerApiService.scala +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.ledger.resources.ResourceOwner -import com.daml.tls.TlsServerConfig -import com.digitalasset.canton.config.KeepAliveServerConfig -import com.digitalasset.canton.config.RequireTypes.Port -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonServerBuilder -import com.digitalasset.canton.tracing.TraceContext -import io.grpc.ServerInterceptor - -import java.util.concurrent.Executor -import scala.util.{Failure, Success} - -object LedgerApiService { - - def apply( - apiServices: ApiServices, - desiredPort: Port, - maxInboundMessageSize: Int, - maxInboundMetadataSize: Int, - maxConcurrentCallsPerConnection: Int, - address: Option[String], - tlsConfiguration: Option[TlsServerConfig], - interceptors: List[ServerInterceptor] = List.empty, - servicesExecutor: Executor, - metrics: LedgerApiServerMetrics, - keepAlive: Option[KeepAliveServerConfig], - loggerFactory: NamedLoggerFactory, - ): ResourceOwner[ApiService] = { - import com.digitalasset.canton.platform.ResourceOwnerOps - val logger = loggerFactory.getTracedLogger(this.getClass) - implicit val traceContext = TraceContext.empty - val _ = tlsConfiguration.map(_.setJvmTlsProperties()) - val sslContext = tlsConfiguration.map( - CantonServerBuilder.sslContext(_, logTlsProtocolAndCipherSuites = true) - ) - ResourceOwner - .forCloseable(() => apiServices) - .flatMap(_ => - GrpcServerOwner( - address, - desiredPort, - maxInboundMessageSize, - maxInboundMetadataSize, - maxConcurrentCallsPerConnection, - sslContext, - interceptors, - metrics, - servicesExecutor, - apiServices.services, - loggerFactory, - keepAlive = keepAlive, - ) - .afterReleased(logger.info("GrpcServer released")) - ) - .map { server => - val host = address.getOrElse("localhost") - val actualPort = server.getPort - val transportMedium = if (sslContext.isDefined) "TLS" else "plain text" - val withKeepAlive = keepAlive.fold("")(ka => s" with $ka") - logger.info(s"Listening on $host:$actualPort over $transportMedium$withKeepAlive.") - new ApiService { - override val port: Port = - Port.tryCreate(server.getPort) - } - } - .transformWith { - case Failure(ex) => - logger.error("Failed to create LedgerApiServer", ex) - ResourceOwner.failed(ex) - case Success(s) => ResourceOwner.successful(s) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/LedgerFeatures.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/LedgerFeatures.scala deleted file mode 100644 index a3d8be692a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/LedgerFeatures.scala +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.ledger.api.v2.experimental_features.ExperimentalCommandInspectionService -import com.daml.ledger.api.v2.version_service.OffsetCheckpointFeature -import com.digitalasset.canton.config.RequireTypes.PositiveInt - -final case class LedgerFeatures( - staticTime: Boolean = false, - commandInspectionService: ExperimentalCommandInspectionService = - ExperimentalCommandInspectionService(supported = true), - offsetCheckpointFeature: OffsetCheckpointFeature = OffsetCheckpointFeature(None), - topologyAwarePackageSelection: Boolean = true, - tapsMaxPassesDefault: PositiveInt, - tapsMaxPassesLimit: PositiveInt, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/SeedService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/SeedService.scala deleted file mode 100644 index a8f87e2fd9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/SeedService.scala +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.digitalasset.daml.lf.crypto - -import java.security.SecureRandom -import java.util.concurrent.TimeUnit - -final class SeedService(seed: crypto.Hash) { - val nextSeed: () => crypto.Hash = crypto.Hash.secureRandom(seed) -} - -object SeedService { - - sealed abstract class Seeding(val name: String) extends Product with Serializable { - override def toString: String = name - } - - object Seeding { - - case object Strong extends Seeding("strong") - - case object Weak extends Seeding("testing-weak") - - case object Static extends Seeding("testing-static") - - } - - def apply(seeding: Seeding): SeedService = - seeding match { - case Seeding.Strong => StrongRandom - case Seeding.Weak => WeakRandom - case Seeding.Static => staticRandom("static random seed service") - } - - /** Pseudo random generator seeded with high entropy seed. May block while gathering entropy from - * the underlying operating system Thread safe. - */ - // lazy to avoid gathering unnecessary entropy. - lazy val StrongRandom: SeedService = { - val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) - val timer = new java.util.Timer() - val task = new java.util.TimerTask { - def run(): Unit = { - logger.warn( - """Trying to gather entropy from the underlying operating system to initialize the contract ID seeding, but the entropy pool seems empty.""" - ) - logger.warn( - s"""In CI environments environment consider using the "${Seeding.Weak.name}" mode, that may produce insecure contract IDs but does not block on startup.""" - ) - } - } - timer.schedule(task, TimeUnit.SECONDS.toMillis(5)) - - val seed = SecureRandom.getInstanceStrong.generateSeed(crypto.Hash.underlyingHashLength) - - timer.cancel() - - new SeedService(crypto.Hash.assertFromByteArray(seed)) - } - - /** Pseudo random generator seeded with a possibly low entropy seed. Do not block. Thread safe. Do - * not use in production mode. - */ - lazy val WeakRandom: SeedService = { - val seed = new Array[Byte](crypto.Hash.underlyingHashLength) - // uses `nextBytes` on a default SecureRandom, that should not block - new SecureRandom().nextBytes(seed) - new SeedService(crypto.Hash.assertFromByteArray(seed)) - } - - /** Pseudo random generator seeded with a given seed. Do not block. Thread safe. Can be use to get - * reproducible run. Do not use in production mode. - */ - def staticRandom(seed: String) = new SeedService(crypto.Hash.hashPrivateKey(seed)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TimeServiceBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TimeServiceBackend.scala deleted file mode 100644 index 8a0dfedbe1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TimeServiceBackend.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.digitalasset.canton.ledger.api.util.TimeProvider - -import java.time.Instant -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.Future - -trait TimeServiceBackend extends TimeProvider { - def setCurrentTime(currentTime: Instant, newTime: Instant): Future[Boolean] -} - -object TimeServiceBackend { - def simple(startTime: Instant): TimeServiceBackend = - new SimpleTimeServiceBackend(startTime) - - private final class SimpleTimeServiceBackend(startTime: Instant) extends TimeServiceBackend { - private val timeRef = new AtomicReference[Instant](startTime) - - override def getCurrentTime: Instant = timeRef.get - - override def setCurrentTime(expectedTime: Instant, newTime: Instant): Future[Boolean] = { - val currentTime = timeRef.get - val res = currentTime == expectedTime && timeRef.compareAndSet(currentTime, newTime) - Future.successful(res) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TimedIndexService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TimedIndexService.scala deleted file mode 100644 index 5b72a8e11d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TimedIndexService.scala +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.ledger.api.v2.event_query_service.GetEventsByContractIdResponse -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.daml.ledger.api.v2.update_service.{ - GetUpdateResponse, - GetUpdatesPageResponse, - GetUpdatesResponse, -} -import com.daml.metrics.Timed -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.health.HealthStatus -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.ledger.api.messages.update.GetUpdatesPageRequest -import com.digitalasset.canton.ledger.api.{EventFormat, UpdateFormat} -import com.digitalasset.canton.ledger.participant.state.index.* -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.Party -import com.digitalasset.daml.lf.transaction.GlobalKey -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.ContractId -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.Future - -final class TimedIndexService(delegate: IndexService, metrics: LedgerApiServerMetrics) - extends IndexService { - - override def currentLedgerEnd(): Future[Option[Offset]] = - Timed.future(metrics.services.index.currentLedgerEnd, delegate.currentLedgerEnd()) - - override def getCompletions( - begin: Option[Offset], - userId: Ref.UserId, - parties: Set[Ref.Party], - )(implicit loggingContext: LoggingContextWithTrace): Source[CompletionStreamResponse, NotUsed] = - Timed.source( - metrics.services.index.getCompletions, - delegate.getCompletions(begin, userId, parties), - ) - - override def updates( - begin: Option[Offset], - endAt: Option[Offset], - updateFormat: UpdateFormat, - descendingOrder: Boolean, - skipPruningChecks: Boolean, - )(implicit loggingContext: LoggingContextWithTrace): Source[GetUpdatesResponse, NotUsed] = - Timed.source( - metrics.services.index.transactions, - delegate.updates(begin, endAt, updateFormat, descendingOrder, skipPruningChecks), - ) - - def getUpdateBy( - lookupKey: LookupKey, - updateFormat: UpdateFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] = - Timed.future( - metrics.services.index.getUpdateByOffset, - delegate.getUpdateBy(lookupKey, updateFormat), - ) - - override def getActiveContracts( - eventFormat: EventFormat, - activeAt: Option[Offset], - rangeInfo: AcsRangeInfo, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[GetActiveContractsResponse, NotUsed] = - Timed.source( - metrics.services.index.getActiveContracts, - delegate.getActiveContracts(eventFormat, activeAt, rangeInfo), - ) - - override def lookupActiveContract( - readers: Set[Ref.Party], - contractId: Value.ContractId, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[FatContract]] = - Timed.future( - metrics.services.index.lookupActiveContract, - delegate.lookupActiveContract(readers, contractId), - ) - - override def lookupContractKey( - readers: Set[Ref.Party], - key: GlobalKey, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[Value.ContractId]] = - Timed.future( - metrics.services.index.lookupContractKey, - delegate.lookupContractKey(readers, key), - ) - - override def lookupMaximumLedgerTimeAfterInterpretation( - ids: Set[Value.ContractId] - )(implicit loggingContext: LoggingContextWithTrace): Future[MaximumLedgerTime] = - Timed.future( - metrics.services.index.lookupMaximumLedgerTime, - delegate.lookupMaximumLedgerTimeAfterInterpretation(ids), - ) - - override def getParticipantId(): Future[Ref.ParticipantId] = - Timed.future(metrics.services.index.getParticipantId, delegate.getParticipantId()) - - override def getParties(parties: Seq[Ref.Party])(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] = - Timed.future(metrics.services.index.getParties, delegate.getParties(parties)) - - override def listKnownParties( - fromExcl: Option[Party], - filterString: Option[String185], - maxResults: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] = - Timed.future( - metrics.services.index.listKnownParties, - delegate.listKnownParties(fromExcl, filterString, maxResults), - ) - - override def prune( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusive: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit loggingContext: LoggingContextWithTrace): Future[Unit] = - Timed.future( - metrics.services.index.prune, - delegate.prune( - previousPruneUpToInclusive = previousPruneUpToInclusive, - previousIncompleteReassignmentOffsets = previousIncompleteReassignmentOffsets, - pruneUpToInclusive = pruneUpToInclusive, - incompleteReassignmentOffsets = incompleteReassignmentOffsets, - ), - ) - - def indexDbPrunedUpto(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] = - delegate.indexDbPrunedUpto - - override def isPruningInProgress: Boolean = delegate.isPruningInProgress - - override def currentHealth(): HealthStatus = - delegate.currentHealth() - - override def lookupContractState(contractId: Value.ContractId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[ContractState] = - Timed.future( - metrics.services.index.lookupContractState, - delegate.lookupContractState(contractId), - ) - - override def latestPrunedOffset()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] = - Timed.future(metrics.services.index.latestPrunedOffsets, delegate.latestPrunedOffset()) - - override def getEventsByContractId( - contractId: ContractId, - eventFormat: EventFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractIdResponse] = - Timed.future( - metrics.services.index.getEventsByContractId, - delegate.getEventsByContractId(contractId, eventFormat), - ) - - override def lookupNonUniqueContractKey( - readers: Set[Party], - key: Key, - pageToken: Option[Long], - limit: Int, - )(implicit loggingContext: LoggingContextWithTrace): Future[ContractKeyPage] = - Timed.future( - metrics.services.index.lookupNonUniqueContractKey, - delegate.lookupNonUniqueContractKey(readers, key, pageToken, limit), - ) - - // TODO(i16065): Re-enable getEventsByContractKey tests -// override def getEventsByContractKey( -// contractKey: Value, -// templateId: Ref.Identifier, -// requestingParties: Set[Ref.Party], -// endExclusiveSeqId: Option[Long], -// )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractKeyResponse] = -// Timed.future( -// metrics.services.index.getEventsByContractKey, -// delegate.getEventsByContractKey( -// contractKey, -// templateId, -// requestingParties, -// endExclusiveSeqId, -// ), -// ) - - override def updatesPage(getUpdatesPageRequest: GetUpdatesPageRequest)(implicit - loggingContext: LoggingContextWithTrace - ): Future[GetUpdatesPageResponse] = Timed.future( - metrics.services.index.getUpdatesPage, - delegate.updatesPage(getUpdatesPageRequest), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TruncatedStatusInterceptor.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TruncatedStatusInterceptor.scala deleted file mode 100644 index af6511bead..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/TruncatedStatusInterceptor.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import io.grpc.ForwardingServerCall.SimpleForwardingServerCall -import io.grpc.{Metadata, ServerCall, ServerCallHandler, ServerInterceptor, Status} - -class TruncatedStatusInterceptor(maximumDescriptionLength: Int) extends ServerInterceptor { - override def interceptCall[ReqT, RespT]( - call: ServerCall[ReqT, RespT], - headers: Metadata, - next: ServerCallHandler[ReqT, RespT], - ): ServerCall.Listener[ReqT] = - next.startCall( - new SimpleForwardingServerCall[ReqT, RespT](call) { - override def close(status: Status, trailers: Metadata): Unit = { - val truncatedStatus = status.withDescription(truncate(status.getDescription)) - super.close(truncatedStatus, trailers) - } - }, - headers, - ) - - private def truncate(description: String): String = - if (description != null && description.length > maximumDescriptionLength) - description.substring(0, maximumDescriptionLength - 3) + "..." - else - description -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/configuration/RateLimitingConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/configuration/RateLimitingConfig.scala deleted file mode 100644 index a735bd5e44..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/configuration/RateLimitingConfig.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.configuration - -/** The memory based rate limiting parameters ([[maxUsedHeapSpacePercentage]] and - * [[minFreeHeapSpaceBytes]] are highly sensitive to the operating environment and should only be - * configured where memory profiling has highlighted spikes in memory usage that need to be - * flattened. - * - * @param maxApiServicesQueueSize - * The maximum number of non-running items in the ApiServices execution service - * @param maxApiServicesIndexDbQueueSize - * The maximum number of non-running items in the IndexDb execution service - * @param maxUsedHeapSpacePercentage - * If, following a garbage collection of the 'tenured' memory pool, the percentage of used pool - * memory is above this percentage the system will be rate limited until additional space is - * freed up. - * @param minFreeHeapSpaceBytes - * If, following a garbage collection of the 'tenured' memory pool, the amount of free space is - * below this value the system will be rate limited until additional space is freed up. - */ -final case class RateLimitingConfig( - maxApiServicesQueueSize: Int = 10000, - maxApiServicesIndexDbQueueSize: Int = 1000, - maxUsedHeapSpacePercentage: Int = 100, - minFreeHeapSpaceBytes: Long = 0, -) { - def calculateCollectionUsageThreshold(maxPoolBytes: Long): Long = { - val thresholdBasedOnUsedPercentage = (maxUsedHeapSpacePercentage * maxPoolBytes) / 100 - val thresholdBasedOnMinFreeSpace = maxPoolBytes - minFreeHeapSpaceBytes - Math.max(thresholdBasedOnUsedPercentage, thresholdBasedOnMinFreeSpace) - } -} - -case object RateLimitingConfig { - - val Megabyte: Long = 1024L * 1024L - - val Default: RateLimitingConfig = RateLimitingConfig() -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/error/ErrorInterceptor.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/error/ErrorInterceptor.scala deleted file mode 100644 index 9179bdff8f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/error/ErrorInterceptor.scala +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.error - -import com.digitalasset.base.error.BaseError -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.error.LedgerApiErrors -import com.digitalasset.canton.logging.* -import io.grpc.* -import io.grpc.ForwardingServerCall.SimpleForwardingServerCall - -import scala.util.control.NonFatal - -final class ErrorInterceptor(val loggerFactory: NamedLoggerFactory) - extends ServerInterceptor - with NamedLogging { - - override def interceptCall[ReqT, RespT]( - call: ServerCall[ReqT, RespT], - headers: Metadata, - next: ServerCallHandler[ReqT, RespT], - ): ServerCall.Listener[ReqT] = { - - val forwardingCall = new SimpleForwardingServerCall[ReqT, RespT](call) { - - /** Here we are trying to detect status/trailers pairs that: - * - originated from the server implementation (i.e. Participant services as opposed to - * internal to gRPC implementation) AND - * - did not originate from LAPI error codes. - * - * NOTE: We are not attempting to detect if a status/trailers pair originates from - * exceptional conditions within gRPC implementation itself. - * - * We are handling unary endpoints that returned failed Futures or other direct invocation of - * [[io.grpc.stub.StreamObserver#onError]]. We are NOT handling here exceptions thrown - * outside of Futures or Pekko streams. These are handled separately in - * [[com.digitalasset.canton.platform.apiserver.error.ErrorListener]]. We are NOT handling - * here exceptions thrown inside Pekko streaming. These are handled in - * [[com.daml.grpc.adapter.server.pekko.ServerAdapter.toSink]] - * - * Handling of Status.INTERNAL: The gRPC services that we generate via scalapb are using - * [[scalapb.grpc.Grpc.completeObserver]] when bridging from Future[T] and into - * io.grpc.stub.StreamObserver[T]. [[scalapb.grpc.Grpc.completeObserver]] does the following: - * a. propagates instances of StatusException and StatusRuntimeException without changes, - * a. translates other throwables into a StatusException with Status.INTERNAL. - * - * We assume that we don't need to deal with a) but need to detect and deal with b). Knowing - * that Status.INTERNAL is used only by - * [[com.digitalasset.base.error.ErrorCategory.SystemInternalAssumptionViolated]], which is - * marked a security sensitive, we have the following heuristic: check whether gRPC status is - * Status.INTERNAL and gRPC status description is not security sanitized. - * - * Handling of Status.UNKNOWN: We do not have an error category that uses UNKNOWN so there is - * no risk of catching a legitimate Ledger API error code. A Status.UNKNOWN can arise when - * someone (us or a library): - * - calls [[io.grpc.stub.StreamObserver#onError]] providing an exception that is not a - * Status(Runtime)Exception (see - * [[io.grpc.stub.ServerCalls.ServerCallStreamObserverImpl#onError]] and - * [[io.grpc.Status#fromThrowable]]), - * - calls [[io.grpc.ServerCall#close]] providing status.UNKNOWN. - */ - override def close(status: Status, trailers: Metadata): Unit = - if (isUnsanitizedInternal(status) || status.getCode == Status.Code.UNKNOWN) { - val recreatedException = status.asRuntimeException(trailers) - val errorCodeException = LedgerApiErrors.InternalError - .UnexpectedOrUnknownException(t = recreatedException)(NoLogging) - .asGrpcError - // Retrieving status and metadata in the same way as in `io.grpc.stub.ServerCalls.ServerCallStreamObserverImpl.onError`. - val newMetadata = - Option(Status.trailersFromThrowable(errorCodeException)).getOrElse(new Metadata()) - val newStatus = Status.fromThrowable(errorCodeException) - LogOnUnhandledFailureInClose(logger, superClose(newStatus, newMetadata)) - } else { - LogOnUnhandledFailureInClose(logger, superClose(status, trailers)) - } - - /** This method serves as an accessor to the super.close() which facilitates its access from - * the outside of this class. This is needed in order to allow the call to be captured in the - * closure passed to the [[LogOnUnhandledFailureInClose]] error handler. - * - * As at Scala 2.13.8, not using this redirection results in a runtime IllegalAccessError. - * Remove this redirection once the runtime exception can be avoided. - */ - private def superClose(status: Status, trailers: Metadata): Unit = - super.close(status, trailers) - } - - val listener = next.startCall(forwardingCall, headers) - new ErrorListener( - loggerFactory = loggerFactory, - delegate = listener, - call = call, - ) - } - - private def isUnsanitizedInternal(status: Status): Boolean = - status.getCode == Status.Code.INTERNAL && - (status.getDescription == null || - !BaseError.isRedactedMessage( - status.getDescription - )) -} - -class ErrorListener[ReqT, RespT]( - val loggerFactory: NamedLoggerFactory, - delegate: ServerCall.Listener[ReqT], - call: ServerCall[ReqT, RespT], -) extends ForwardingServerCallListener.SimpleForwardingServerCallListener[ReqT](delegate) - with NamedLogging { - - /** Handles errors arising outside Futures or Pekko streaming. - * - * NOTE: We don't override other listener methods: onCancel, onComplete, onReady and onMessage; - * as it seems overriding only onHalfClose is sufficient. - */ - override def onHalfClose(): Unit = - try { - super.onHalfClose() - } catch { - // For StatusException and StatusRuntimeException: - // 1. Assuming `t` was produced by self-service error codes and, thus, deeming the corresponding status and - // trailers do not need security sanitization. - // 2. We need to catch it and call `call.close` as otherwise gRPC will close the stream with a Status.UNKNOWN - // (see io.grpc.internal.ServerImpl.JumpToApplicationThreadServerStreamListener.internalClose) - case t: StatusException => - LogOnUnhandledFailureInClose(logger, call.close(t.getStatus, t.getTrailers)) - case t: StatusRuntimeException => - LogOnUnhandledFailureInClose(logger, call.close(t.getStatus, t.getTrailers)) - case NonFatal(t) => - val e = LedgerApiErrors.InternalError - .UnexpectedOrUnknownException(t = t)(NoLogging) - .asGrpcError - LogOnUnhandledFailureInClose(logger, call.close(e.getStatus, e.getTrailers)) - } -} - -private[error] object LogOnUnhandledFailureInClose { - - def apply[T](logger: TracedLogger, close: => T): T = - // If close throws, we can't call ServerCall.close a second time - // since it might have already been marked internally as closed. - // In this situation, we can't do much about it except for notifying the participant operator. - try close - catch { - case NonFatal(e) => - // Instantiate the self-service error code as it logs the error on creation. - // This error is considered security-sensitive and can't be propagated to the client. - LedgerApiErrors.InternalError - .Generic( - s"Unhandled error in ${classOf[ServerCall[?, ?]].getSimpleName}.close(). " + - s"The gRPC client might have not been notified about the call/stream termination. " + - s"Either notify clients to retry pending unary/streaming calls or restart the participant server.", - Some(e), - )(ErrorLoggingContext(logger, LoggingContextWithTrace.empty)) - .discard - throw e - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandExecutionResult.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandExecutionResult.scala deleted file mode 100644 index 4aed6250d8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandExecutionResult.scala +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.{RoutingSynchronizerState, SynchronizerRank} -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.ImmArray -import com.digitalasset.daml.lf.transaction.{GlobalKey, SubmittedTransaction} -import com.digitalasset.daml.lf.value.Value - -/** The result of command execution. - * - * @param submitterInfo - * The submitter info - * @param optSynchronizerId - * The ID of the synchronizer where the submitter wants the transaction to be sequenced - * @param transactionMeta - * The transaction meta-data - * @param transaction - * The transaction - * @param dependsOnLedgerTime - * True if the output of command execution depends in any way on the ledger time, as specified - * through [[com.digitalasset.daml.lf.command.ApiCommands.ledgerEffectiveTime]]. If this value is - * false, then the ledger time of the resulting transaction - * ([[state.TransactionMeta.ledgerEffectiveTime]]) can safely be changed after command - * interpretation. - * @param interpretationTimeNanos - * Wall-clock time that interpretation took for the engine. - * @param globalKeyMapping - * Input key mapping inferred by interpretation. The map should contain all contract keys that - * were used during interpretation. A value of None means no contract was found with this - * contract key. - * @param processedDisclosedContracts - * The disclosed contracts used as part of command interpretation. Note that this may be a subset - * of the `disclosed_contracts` provided as part of the command submission by the client, as - * superfluously-provided contracts are discarded by the Daml engine. - */ -private[canton] final case class CommandInterpretationResult( - submitterInfo: state.SubmitterInfo, - transactionMeta: state.TransactionMeta, - transaction: SubmittedTransaction, - dependsOnLedgerTime: Boolean, - interpretationTimeNanos: Long, - globalKeyMapping: Map[GlobalKey, Vector[Value.ContractId]], - processedDisclosedContracts: ImmArray[LfFatContractInst], - // TODO(#25385): Consider removing the prescribed synchronizer decision from command interpreter - // and factor this field out of here as well. - optSynchronizerId: Option[SynchronizerId], -) { - def toCommandExecutionResult( - synchronizerRank: SynchronizerRank, - routingSynchronizerState: RoutingSynchronizerState, - ): CommandExecutionResult = - CommandExecutionResult(this, synchronizerRank, routingSynchronizerState) -} - -/** The result of command execution. - * - * @param commandInterpretationResult - * The result of command interpretation - * @param synchronizerRank - * The rank of the synchronizer that should be used for routing - * @param routingSynchronizerState - * the synchronizer state that was used for computing the synchronizer rank and should be used - * for the rest of phase 1 of the transaction protocol. - */ -private[apiserver] final case class CommandExecutionResult( - commandInterpretationResult: CommandInterpretationResult, - synchronizerRank: SynchronizerRank, - routingSynchronizerState: RoutingSynchronizerState, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandExecutor.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandExecutor.scala deleted file mode 100644 index 946a700d72..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandExecutor.scala +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.data.EitherT -import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.ledger.api.Commands -import com.digitalasset.canton.ledger.participant.state.{RoutingSynchronizerState, SyncService} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.canton.topology.{PhysicalSynchronizerId, SynchronizerId} -import com.digitalasset.canton.version.EngineMode -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.transaction.NextGenContractStateMachine - -import scala.concurrent.ExecutionContext - -trait CommandExecutor { - - /** Executes the command and returns the command execution result with the rank of the - * synchronizer that should be used for routing. - * - * @param commands - * The commands to be processed - * @param submissionSeed - * The submission seed - * @param routingSynchronizerState - * The synchronizer state that should be used throughout the command execution - * @param forExternallySigned - * Whether the command should be processed for external signing. If true, the command's - * submitters are not required to have submission rights on the participant. - * @return - * the command execution result with the routing synchronizer - */ - def execute( - commands: Commands, - submissionSeed: Hash, - routingSynchronizerState: RoutingSynchronizerState, - forExternallySigned: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): EitherT[FutureUnlessShutdown, ErrorCause, CommandExecutionResult] -} - -object CommandExecutor { - def apply( - syncService: SyncService, - commandInterpreter: CommandInterpreter, - topologyAwarePackageSelectionEnabled: Boolean, - tapsMaxPassesDefault: PositiveInt, - tapsMaxPassesLimit: PositiveInt, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - )(implicit ec: ExecutionContext): CommandExecutor = - if (topologyAwarePackageSelectionEnabled) - new TopologyAwareCommandExecutor( - syncService = syncService, - commandInterpreter = commandInterpreter, - maxPassesDefault = tapsMaxPassesDefault, - maxPassesLimit = tapsMaxPassesLimit, - metrics = metrics, - loggerFactory = loggerFactory, - ) - else - new DefaultCommandExecutor( - syncService = syncService, - commandInterpreter = commandInterpreter, - loggerFactory = loggerFactory, - ) -} - -private[execution] class DefaultCommandExecutor( - syncService: SyncService, - commandInterpreter: CommandInterpreter, - val loggerFactory: NamedLoggerFactory, -)(implicit - ec: ExecutionContext -) extends NamedLogging - with CommandExecutor { - - private def establishMode( - synchronizerId: Option[SynchronizerId], - candidateSynchronizers: Set[PhysicalSynchronizerId], - ): NextGenContractStateMachine.Mode = - synchronizerId - .fold(candidateSynchronizers)(logical => candidateSynchronizers.filter(_.logical == logical)) - .map(_.protocolVersion) - .maxOption - .map(pv => EngineMode.forProtocolVersion(pv)) - .getOrElse(NextGenContractStateMachine.Mode.default) - - override def execute( - commands: Commands, - submissionSeed: Hash, - routingSynchronizerState: RoutingSynchronizerState, - forExternallySigned: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): EitherT[FutureUnlessShutdown, ErrorCause, CommandExecutionResult] = { - logger.debug("Processing command with the default package preference selection algorithm") - commands.tapsMaxPasses.foreach { value => - logger.info( - s"Ignoring taps_max_passes=$value because Topology-Aware Package Selection is disabled." - ) - } - - val mode: NextGenContractStateMachine.Mode = establishMode( - commands.synchronizerId, - routingSynchronizerState.topologySnapshots.keySet, - ) - - for { - - commandInterpretationResult <- EitherT( - commandInterpreter.interpret(commands, mode, submissionSeed) - ) - synchronizerRank <- syncService - .selectRoutingSynchronizer( - commandInterpretationResult.submitterInfo, - commandInterpretationResult.transaction, - commandInterpretationResult.transactionMeta, - commandInterpretationResult.processedDisclosedContracts.map(_.contractId).toList, - commandInterpretationResult.optSynchronizerId, - transactionUsedForExternalSigning = forExternallySigned, - routingSynchronizerState = routingSynchronizerState, - ) - .leftMap[ErrorCause](ErrorCause.RoutingFailed(_)) - } yield commandInterpretationResult.toCommandExecutionResult( - synchronizerRank, - routingSynchronizerState, - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandProgressTracker.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandProgressTracker.scala deleted file mode 100644 index e453045c65..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/CommandProgressTracker.scala +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import com.daml.ledger.api.v2.admin.command_inspection_service.{ - CommandState, - CommandStatus as ApiCommandStatus, - CommandUpdates, - RequestStatistics, - Timing, -} -import com.daml.ledger.api.v2.commands.Command -import com.daml.ledger.api.v2.completion.Completion -import com.digitalasset.base.error.utils.DecodedCantonError -import com.digitalasset.canton.ProtoDeserializationError -import com.digitalasset.canton.crypto.Hash -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.ledger.participant.state.Update -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.protocol.RootHash -import com.digitalasset.canton.serialization.ProtoConverter -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import io.grpc.StatusRuntimeException - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Failure -import scala.util.control.NonFatal - -final case class CommandStatus( - started: CantonTimestamp, - completed: Option[CantonTimestamp], - completion: Completion, - state: CommandState, - commands: Seq[Command], - requestStatistics: RequestStatistics, - updates: CommandUpdates, - synchronizerId: Option[SynchronizerId], - rootHash: Option[Hash], - timings: Seq[(String, Int)], -) extends PrettyPrinting { - def toProto: ApiCommandStatus = - ApiCommandStatus( - started = Some(started.toProtoTimestamp), - completed = completed.map(_.toProtoTimestamp), - completion = Some(completion), - state = state, - commands = commands, - requestStatistics = Some(requestStatistics), - updates = Some(updates), - synchronizerId = synchronizerId.map(_.toProtoPrimitive).getOrElse(""), - timings = timings.reverse.map { case (desc, millis) => Timing(desc, millis) }, - ) - - override def pretty: Pretty[CommandStatus] = CommandStatus.pretty - - def decodedError: Option[DecodedCantonError] = - completion.status.flatMap(s => DecodedCantonError.fromGrpcStatus(s).toOption) - -} - -object CommandStatus { - - import com.digitalasset.canton.logging.pretty.PrettyUtil.* - import com.digitalasset.canton.util.ShowUtil.* - - private implicit val prettyRequestStats: Pretty[RequestStatistics] = prettyOfClass( - param("requestSize", _.requestSize), - param("recipients", _.recipients), - param("envelopes", _.envelopes), - ) - - private implicit val prettyUpdateStats: Pretty[CommandUpdates] = prettyOfClass( - param("created", _.created.length), - param("archived", _.archived.length), - param("exercised", _.exercised), - param("fetched", _.fetched), - param("lookedUpByKey", _.lookedUpByKey), - ) - private def nonEmptyUpdate(update: CommandUpdates): Boolean = - update.created.nonEmpty || update.archived.nonEmpty || update.exercised > 0 || update.fetched > 0 || update.lookedUpByKey > 0 - - private val pretty: Pretty[CommandStatus] = prettyOfClass( - param("commandId", _.completion.commandId.singleQuoted), - param("started", _.started), - paramIfDefined("completed", _.completed), - param("state", _.state.toString().singleQuoted), - param("completion", _.completion.status), - paramIfDefined( - "updateId", - x => Option.when(x.completion.updateId.nonEmpty)(x.completion.updateId.singleQuoted), - ), - paramIfDefined( - "request", - x => Option.when(x.requestStatistics.requestSize > 0)(x.requestStatistics), - ), - paramIfDefined( - "update", - x => Option.when(nonEmptyUpdate(x.updates))(x.updates), - ), - param("timings", _.timings.map { case (desc, ms) => s"$ms ms - $desc".unquoted }), - ) - - def fromProto( - proto: ApiCommandStatus - ): Either[ProtoDeserializationError, CommandStatus] = { - val ApiCommandStatus( - startedP, - completedP, - completionP, - stateP, - commandsP, - requestStatisticsP, - updatesP, - synchronizerIdP, - timings, - ) = proto - for { - started <- ProtoConverter.parseRequired( - CantonTimestamp.fromProtoTimestamp, - "started", - startedP, - ) - completed <- completedP - .map(CantonTimestamp.fromProtoTimestamp(_).map(Some(_))) - .getOrElse(Right(None)) - completion <- ProtoConverter.required("completion", completionP) - requestsStatistics <- ProtoConverter.required("requestStatistics", requestStatisticsP) - updates <- ProtoConverter.required("updates", updatesP) - synchronizerId <- - if (synchronizerIdP.nonEmpty) - SynchronizerId.fromProtoPrimitive(synchronizerIdP, "synchronizer_id").map(Some(_)) - else Right(None) - } yield CommandStatus( - started = started, - completed = completed, - completion = completion, - state = stateP, - commands = commandsP, - requestStatistics = requestsStatistics, - updates = updates, - synchronizerId = synchronizerId, - rootHash = None, - timings = timings.map(tt => (tt.description, tt.durationMs)), - ) - } -} - -/** Result handle that allows to update a command with a respective result */ -trait CommandResultHandle { - - def failedSync(err: StatusRuntimeException): Unit - def internalErrorSync(err: Throwable): Unit - def transactionSequenced(): Unit - def extractFailure[T]( - f: FutureUnlessShutdown[T] - )(implicit executionContext: ExecutionContext): FutureUnlessShutdown[T] = - f.transform { - case ff @ Failure(err: StatusRuntimeException) => - failedSync(err) - ff - case ff @ Failure(NonFatal(err)) => - internalErrorSync(err) - ff - case rr => rr - } - - def recordEnvelopeSizes( - rootHash: RootHash, - batchSize: Int, - numRecipients: Int, - numEnvelopes: Int, - ): Unit - - def recordTransactionImpact( - transaction: com.digitalasset.daml.lf.transaction.SubmittedTransaction - ): Unit - -} - -object CommandResultHandle { - lazy val NoOp: CommandResultHandle = new CommandResultHandle { - override def failedSync(err: StatusRuntimeException): Unit = () - override def internalErrorSync(err: Throwable): Unit = () - override def transactionSequenced(): Unit = () - override def recordEnvelopeSizes( - rootHash: RootHash, - batchSize: Int, - numRecipients: Int, - numEnvelopes: Int, - ): Unit = - () - override def recordTransactionImpact( - transaction: com.digitalasset.daml.lf.transaction.SubmittedTransaction - ): Unit = () - } -} - -/** Command progress tracker for debugging - * - * In order to track the progress of a command, we internally update the progress of the command - * using this tracker trait, and expose the information on the API. - * - * This is in total violation of the CQRS pattern, but it is a necessary evil for debugging. - */ -trait CommandProgressTracker { - - def findCommandStatus( - commandIdPrefix: String, - state: CommandState, - limit: Int, - ): Future[Seq[CommandStatus]] - - def registerCommand( - commandId: String, - submissionId: Option[String], - userId: String, - commands: Seq[Command], - actAs: Set[String], - )(implicit traceContext: TraceContext): CommandResultHandle - - def findHandle( - commandId: String, - userId: String, - actAs: Seq[String], - submissionId: Option[String], - ): CommandResultHandle - - def validationStarts(rootHash: RootHash): Unit = () - def validationCompleted(rootHash: RootHash): Unit = () - def validationResponseCompleted(rootHash: RootHash): Unit = () - def validationVerdict(rootHash: RootHash): Unit = () - def processLedgerUpdate(update: TransactionLogUpdate): Unit - def indexingStarts(update: Update): Unit -} - -object CommandProgressTracker { - lazy val NoOp: CommandProgressTracker = new CommandProgressTracker { - override def findCommandStatus( - commandId: String, - state: CommandState, - limit: Int, - ): Future[Seq[CommandStatus]] = Future.successful(Seq.empty) - - override def registerCommand( - commandId: String, - submissionId: Option[String], - userId: String, - commands: Seq[Command], - actAs: Set[String], - )(implicit traceContext: TraceContext): CommandResultHandle = CommandResultHandle.NoOp - - override def findHandle( - commandId: String, - userId: String, - actAs: Seq[String], - submissionId: Option[String], - ): CommandResultHandle = - CommandResultHandle.NoOp - - override def processLedgerUpdate(update: TransactionLogUpdate): Unit = () - override def indexingStarts(update: Update): Unit = () - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/DynamicSynchronizerParameterGetter.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/DynamicSynchronizerParameterGetter.scala deleted file mode 100644 index 4517781234..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/DynamicSynchronizerParameterGetter.scala +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.data.EitherT -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.time.NonNegativeFiniteDuration -import com.digitalasset.canton.topology.PhysicalSynchronizerId -import com.digitalasset.canton.tracing.TraceContext - -/** Class for retrieving dynamic synchronizer parameters. - * - * Because of the current organisation of code between the ledger API and Canton, the ledger API - * code does not have direct access to Canton concepts such as dynamic synchronizer parameters (it - * only sees the `CantonSyncService` as an instance of - * [[com.digitalasset.canton.ledger.participant.state.SyncService]]). - * - * An instance of this trait is therefore provided as a "hook" to the ledger API to retrieve - * dynamic synchronizer parameters. - */ -trait DynamicSynchronizerParameterGetter { - def getLedgerTimeRecordTimeTolerance(synchronizerIdO: Option[PhysicalSynchronizerId])(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, String, NonNegativeFiniteDuration] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/LedgerTimeAwareCommandExecutor.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/LedgerTimeAwareCommandExecutor.scala deleted file mode 100644 index 2313f4b276..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/LedgerTimeAwareCommandExecutor.scala +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.data.EitherT -import com.digitalasset.canton.ledger.api.Commands -import com.digitalasset.canton.ledger.participant.state.RoutingSynchronizerState -import com.digitalasset.canton.ledger.participant.state.index.MaximumLedgerTime -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.Time -import com.digitalasset.daml.lf.value.Value.ContractId -import monocle.Monocle.toAppliedFocusOps - -import scala.concurrent.ExecutionContext -import scala.util.{Failure, Success} - -private[apiserver] final class LedgerTimeAwareCommandExecutor( - delegate: CommandExecutor, - resolveMaximumLedgerTime: ResolveMaximumLedgerTime, - maxRetries: Int, - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, -)(implicit - ec: ExecutionContext -) extends CommandExecutor - with NamedLogging { - - /** Executes a command, advancing the ledger time as necessary. - * - * The command execution result is guaranteed to satisfy causal monotonicity, i.e., the resulting - * transaction has a ledger time greater than or equal to the ledger time of any used contract. - */ - override def execute( - commands: Commands, - submissionSeed: crypto.Hash, - routingSynchronizerState: RoutingSynchronizerState, - usedForExternallySigningTransaction: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): EitherT[FutureUnlessShutdown, ErrorCause, CommandExecutionResult] = - EitherT( - loop( - commands = commands, - submissionSeed = submissionSeed, - routingSynchronizerState = routingSynchronizerState, - retriesLeft = maxRetries, - forExternallySigned = usedForExternallySigningTransaction, - ) - ) - - private[this] def loop( - commands: Commands, - submissionSeed: crypto.Hash, - routingSynchronizerState: RoutingSynchronizerState, - retriesLeft: Int, - forExternallySigned: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[Either[ErrorCause, CommandExecutionResult]] = - delegate - .execute(commands, submissionSeed, routingSynchronizerState, forExternallySigned) - .value - .flatMap { - case e @ Left(_) => - // Permanently failed - FutureUnlessShutdown.pure(e) - case Right(cer) => - // Command execution was successful. - // Check whether the ledger time used for input is consistent with the output, - // and advance output time or re-execute the command if necessary. - val usedContractIds: Set[ContractId] = cer.commandInterpretationResult.transaction - .inputContracts[ContractId] - .collect { case id: ContractId => id } - - def failed = - FutureUnlessShutdown.pure(Left(ErrorCause.LedgerTime(maxRetries - retriesLeft))) - def success(c: CommandExecutionResult) = - FutureUnlessShutdown.pure(Right(c)) - def retry(c: Commands) = { - metrics.execution.retry.mark() - loop(c, submissionSeed, routingSynchronizerState, retriesLeft - 1, forExternallySigned) - } - - resolveMaximumLedgerTime( - cer.commandInterpretationResult.processedDisclosedContracts, - usedContractIds, - ) - .transformWithHandledAborted { - case Success(MaximumLedgerTime.NotAvailable) => - success(cer) - - case Success(MaximumLedgerTime.Max(maxUsedTime)) - if maxUsedTime <= commands.commands.ledgerEffectiveTime => - success(cer) - - case Success(MaximumLedgerTime.Max(maxUsedTime)) - if !cer.commandInterpretationResult.dependsOnLedgerTime => - logger.debug( - s"Advancing ledger effective time for the output from ${commands.commands.ledgerEffectiveTime} to $maxUsedTime" - ) - success(advanceOutputTime(cer, maxUsedTime)) - - case Success(MaximumLedgerTime.Max(maxUsedTime)) => - if (retriesLeft > 0) { - logger.debug( - s"Restarting the computation with new ledger effective time $maxUsedTime" - ) - retry(advanceInputTime(commands, maxUsedTime)) - } else { - failed - } - - case Success(MaximumLedgerTime.Archived(contracts)) => - if (retriesLeft > 0) { - logger.info( - s"Some input contracts are archived: ${contracts.mkString("[", ", ", "]")}. Restarting the computation." - ) - retry(commands) - } else { - logger.info( - s"Lookup of maximum ledger time failed after ${maxRetries - retriesLeft}. Used contracts: ${usedContractIds - .mkString("[", ", ", "]")}." - ) - failed - } - - // An error while looking up the maximum ledger time for the used contracts. The nature of this error is not known. - // Not retrying automatically. All other automatically retry-able cases are covered by the logic above. - case Failure(error) => - logger.info( - s"Lookup of maximum ledger time failed after ${maxRetries - retriesLeft}. Used contracts: ${usedContractIds - .mkString("[", ", ", "]")}. Details: $error" - ) - failed - } - } - - private[this] def advanceOutputTime( - res: CommandExecutionResult, - newTime: Time.Timestamp, - ): CommandExecutionResult = - res - .focus(_.commandInterpretationResult.transactionMeta.ledgerEffectiveTime) - .replace(newTime) - - private[this] def advanceInputTime(cmd: Commands, newTime: Time.Timestamp): Commands = - cmd.copy(commands = cmd.commands.copy(ledgerEffectiveTime = newTime)) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/ResolveMaximumLedgerTime.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/ResolveMaximumLedgerTime.scala deleted file mode 100644 index b0166a2f57..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/ResolveMaximumLedgerTime.scala +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.ledger.participant.state.index.{ - MaximumLedgerTime, - MaximumLedgerTimeService, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.daml.lf.data.ImmArray -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.transaction.{CreationTime, FatContractInstance} -import com.digitalasset.daml.lf.value.Value.ContractId - -import scala.concurrent.ExecutionContext - -/** Computes the maximum ledger time of all used contracts in a submission by: * Using the - * client-provided disclosed contracts `createdAt` timestamp * Falling back to contractStore - * lookups for contracts that have not been provided as part of submissions' `disclosed_contracts` - * - * @param maximumLedgerTimeService - * The MaximumLedgerTimeService. - */ -class ResolveMaximumLedgerTime( - maximumLedgerTimeService: MaximumLedgerTimeService, - override protected val loggerFactory: NamedLoggerFactory, -) extends NamedLogging { - - private val directEc = DirectExecutionContext(noTracingLogger) - - def apply( - processedDisclosedContracts: ImmArray[FatContractInstance], - usedContractIds: Set[ContractId], - )(implicit - lc: LoggingContextWithTrace, - ec: ExecutionContext, - ): FutureUnlessShutdown[MaximumLedgerTime] = FutureUnlessShutdown.outcomeF { - val usedDisclosedContractIds = processedDisclosedContracts.iterator.map(_.contractId).toSet - - val contractIdsToBeLookedUp = usedContractIds -- usedDisclosedContractIds - - maximumLedgerTimeService - .lookupMaximumLedgerTimeAfterInterpretation(contractIdsToBeLookedUp) - .map(adjustTimeForDisclosedContracts(_, processedDisclosedContracts))(directEc) - } - - private def adjustTimeForDisclosedContracts( - lookupMaximumLet: MaximumLedgerTime, - processedDisclosedContracts: ImmArray[FatContractInstance], - ): MaximumLedgerTime = - processedDisclosedContracts.iterator - .map(_.createdAt) - .collect { case CreationTime.CreatedAt(time) => time } - .maxOption - .fold(lookupMaximumLet)(adjust(lookupMaximumLet, _)) - - private def adjust( - lookedMaximumLet: MaximumLedgerTime, - maxDisclosedContractTime: Timestamp, - ): MaximumLedgerTime = lookedMaximumLet match { - case MaximumLedgerTime.Max(maxUsedTime) => - MaximumLedgerTime.Max(Ordering[Timestamp].max(maxDisclosedContractTime, maxUsedTime)) - case MaximumLedgerTime.NotAvailable => - MaximumLedgerTime.Max(maxDisclosedContractTime) - case other => other - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreter.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreter.scala deleted file mode 100644 index 1348ff5f28..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreter.scala +++ /dev/null @@ -1,691 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.syntax.all.* -import com.daml.metrics.{Timed, Tracked} -import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.data.LedgerTimeBoundaries -import com.digitalasset.canton.ledger.api -import com.digitalasset.canton.ledger.api.DisclosedContract -import com.digitalasset.canton.ledger.api.util.TimeProvider -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.index.ContractStore -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, - TracedLogger, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.canton.protocol.{CantonContractIdVersion, LfFatContractInst} -import com.digitalasset.canton.time.NonNegativeFiniteDuration -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ContractValidator.ContractAuthenticatorFn -import com.digitalasset.canton.util.PackageConsumer.PackageResolver -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time} -import com.digitalasset.daml.lf.engine.* -import com.digitalasset.daml.lf.engine.ResultNeedContract.Response -import com.digitalasset.daml.lf.transaction.{ - GlobalKey, - NeedKeyProgression, - NextGenContractStateMachine, - Node, - SubmittedTransaction, - Transaction, -} -import com.digitalasset.daml.lf.value.ContractIdVersion -import scalaz.syntax.tag.* - -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicLong -import scala.collection.View -import scala.concurrent.{ExecutionContext, Future} -import scala.util.chaining.scalaUtilChainingOps - -private[apiserver] trait CommandInterpreter { - - def interpret( - commands: api.Commands, - mode: NextGenContractStateMachine.Mode, - submissionSeed: crypto.Hash, - )(implicit - loggingContext: LoggingContextWithTrace, - ec: ExecutionContext, - ): FutureUnlessShutdown[Either[ErrorCause, CommandInterpretationResult]] -} - -/** @param ec - * [[scala.concurrent.ExecutionContext]] that will be used for scheduling CPU-intensive - * computations performed by an [[com.digitalasset.daml.lf.engine.Engine]]. - */ -final class StoreBackedCommandInterpreter( - engine: Engine, - participant: Ref.ParticipantId, - packageResolver: PackageResolver, - contractStore: ContractStore, - metrics: LedgerApiServerMetrics, - contractAuthenticator: ContractAuthenticatorFn, - prefetchingRecursionLevel: PositiveInt, - val loggerFactory: NamedLoggerFactory, - dynParamGetter: DynamicSynchronizerParameterGetter, - timeProvider: TimeProvider, -)(implicit - ec: ExecutionContext -) extends CommandInterpreter - with NamedLogging { - - import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.* - - override def interpret( - commands: api.Commands, - mode: NextGenContractStateMachine.Mode, - submissionSeed: crypto.Hash, - )(implicit - loggingContext: LoggingContextWithTrace, - ec: ExecutionContext, - ): FutureUnlessShutdown[Either[ErrorCause, CommandInterpretationResult]] = { - val interpretationTimeNanos = new AtomicLong(0L) - val start = System.nanoTime() - for { - ledgerTimeRecordTimeToleranceO <- dynParamGetter - // TODO(i15313): - // We should really pass the synchronizerId here, but it is not available within the ledger API for 2.x. - .getLedgerTimeRecordTimeTolerance(None) - .leftMap { error => - logger.info( - s"Cannot retrieve ledgerTimeRecordTimeTolerance: $error. Command interpretation time will not be limited." - ) - } - .value - .map(_.toOption) - submissionResult <- submitToEngine(commands, mode, submissionSeed, interpretationTimeNanos) - submission <- consume( - commands.actAs, - commands.readAs, - submissionResult, - commands.disclosedContracts, - interpretationTimeNanos, - commands.commands.ledgerEffectiveTime, - ledgerTimeRecordTimeToleranceO, - ) - - } yield submission.flatMap { case (updateTx, meta) => - val interpretationTimeNanos = System.nanoTime() - start - commandInterpretationResult( - commands, - submissionSeed, - updateTx, - meta, - interpretationTimeNanos, - ) - } - } - - private def commandInterpretationResult( - commands: api.Commands, - submissionSeed: crypto.Hash, - updateTx: SubmittedTransaction, - meta: Transaction.Metadata, - interpretationTimeNanos: Long, - )(implicit - tc: TraceContext - ): Either[ErrorCause.DisclosedContractsSynchronizerIdMismatch, CommandInterpretationResult] = { - - val usedDisclosedContracts = { - val inputContractIds = updateTx.inputContracts - commands.disclosedContracts.filter(c => - inputContractIds.contains(c.fatContractInstance.contractId) - ) - } - - StoreBackedCommandInterpreter - .considerDisclosedContractsSynchronizerId( - commands.synchronizerId, - usedDisclosedContracts.map { disclosed => - disclosed.fatContractInstance.contractId -> disclosed.synchronizerIdO - }, - logger, - ) - .map { prescribedSynchronizerIdO => - CommandInterpretationResult( - submitterInfo = state.SubmitterInfo( - commands.actAs.toList, - commands.readAs.toList, - commands.userId, - commands.commandId.unwrap, - commands.deduplicationPeriod, - commands.submissionId.map(_.unwrap), - externallySignedSubmission = None, - ), - optSynchronizerId = prescribedSynchronizerIdO, - transactionMeta = state.TransactionMeta( - commands.commands.ledgerEffectiveTime, - commands.workflowId.map(_.unwrap), - meta.preparationTime, - submissionSeed, - LedgerTimeBoundaries(meta.timeBoundaries), - Some(meta.usedPackages), - Some(meta.nodeSeeds), - Some( - updateTx.nodes - .collect { case (nodeId, node: Node.Action) if node.byKey => nodeId } - .to(ImmArray) - ), - ), - transaction = updateTx, - dependsOnLedgerTime = meta.dependsOnTime, - interpretationTimeNanos = interpretationTimeNanos, - globalKeyMapping = meta.globalKeyMapping, - processedDisclosedContracts = usedDisclosedContracts.map(_.fatContractInstance), - ) - } - } - - private def submitToEngine( - commands: api.Commands, - mode: NextGenContractStateMachine.Mode, - submissionSeed: crypto.Hash, - interpretationTimeNanos: AtomicLong, - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[Result[(SubmittedTransaction, Transaction.Metadata)]] = - Tracked.futureUS( - metrics.execution.engineRunning, - FutureUnlessShutdown.outcomeF(Future(trackSyncExecution(interpretationTimeNanos) { - // The actAs and readAs parties are used for two kinds of checks by the ledger API server: - // When looking up contracts during command interpretation, the engine should only see contracts - // that are visible to at least one of the actAs or readAs parties. This visibility check is not part of the - // Daml ledger model. - // When checking Daml authorization rules, the engine verifies that the actAs parties are sufficient to - // authorize the resulting transaction. - val commitAuthorizers = commands.actAs - engine.submit( - packageMap = commands.packageMap, - packagePreference = commands.packagePreferenceSet, - submitters = commitAuthorizers, - readAs = commands.readAs, - cmds = commands.commands, - participantId = participant, - submissionSeed = submissionSeed, - prefetchKeys = commands.prefetchKeys, - contractIdVersion = ContractIdVersion.V1, - contractStateMode = mode, - ) - })), - ) - - // TODO(#30398): add unit testing of the NUCK lookups (especially the intersection with explicit disclosure) - private def consume[A]( - actAs: Set[Ref.Party], - readAs: Set[Ref.Party], - result: Result[A], - disclosedContracts: ImmArray[DisclosedContract], - interpretationTimeNanos: AtomicLong, - ledgerEffectiveTime: Time.Timestamp, - ledgerTimeRecordTimeToleranceO: Option[NonNegativeFiniteDuration], - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[Either[ErrorCause, A]] = { - import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.TimerOnShutdownSyntax - val readers = actAs ++ readAs - - val lookupActiveContractTime = new AtomicLong(0L) - val lookupActiveContractCount = new AtomicLong(0L) - - val lookupContractKeyTime = new AtomicLong(0L) - val lookupContractKeyCount = new AtomicLong(0L) - - val disclosedContractsByKey: Map[GlobalKey, Vector[LfFatContractInst]] = - disclosedContracts.foldLeft(Map.empty[GlobalKey, Vector[LfFatContractInst]]) { - case (map, disclosedContract) => - disclosedContract.fatContractInstance.contractKeyWithMaintainers match { - case Some(key) => - map.+( - key.globalKey -> map - .getOrElse(key.globalKey, Vector.empty) - .appended(disclosedContract.fatContractInstance) - ) - case None => - map - } - } - - val disclosedContractsById: Map[ContractId, LfFatContractInst] = - disclosedContracts.iterator - .map(c => c.fatContractInstance.contractId -> c.fatContractInstance) - .toMap - - def disclosedOrStoreLookup(acoid: ContractId): FutureUnlessShutdown[Option[LfFatContractInst]] = - disclosedContractsById.get(acoid) match { - case Some(fatContract) => FutureUnlessShutdown.pure(Some(fatContract)) - case None => timedLookup(acoid) - } - - def timedLookup(acoid: ContractId): FutureUnlessShutdown[Option[LfFatContractInst]] = { - val start = System.nanoTime - Timed - .futureUS( - metrics.execution.lookupActiveContract, - FutureUnlessShutdown.outcomeF(contractStore.lookupActiveContract(readers, acoid)), - ) - .map { - _.tap { _ => - lookupActiveContractTime.addAndGet(System.nanoTime() - start) - lookupActiveContractCount.incrementAndGet() - } - } - } - - def disclosedOrStoreNKeyLookup( - key: GlobalKey, - limit: Int, - progression: NeedKeyProgression.CanContinue, - ): FutureUnlessShutdown[(Vector[LfFatContractInst], NeedKeyProgression.HasStarted)] = - StoreBackedCommandInterpreter.disclosedOrStoreNKeyLookup( - key = key, - limit = limit, - progression = progression, - disclosedContracts = disclosedContractsByKey.getOrElse(key, Vector.empty), - disclosedContractsById = disclosedContractsById, - contractStore = contractStore, - metrics = metrics, - readers = readers, - lookupContractKeyTime = lookupContractKeyTime, - lookupContractKeyCount = lookupContractKeyCount, - ) - - def resolveStep(result: Result[A]): FutureUnlessShutdown[Either[ErrorCause, A]] = - result match { - case ResultDone(r) => FutureUnlessShutdown.pure(Right(r)) - - case ResultError(err) => FutureUnlessShutdown.pure(Left(ErrorCause.DamlLf(err))) - - case ResultNeedContract(acoid, resume) => - (CantonContractIdVersion.extractCantonContractIdVersion(acoid) match { - case Right(version) => - disclosedOrStoreLookup(acoid).map[Response] { - case Some(contract) => - Response.ContractFound( - contract, - version.contractHashingMethod, - hash => contractAuthenticator(contract, hash).isRight, - ) - case None => Response.ContractNotFound - } - - case Left(_) => - FutureUnlessShutdown.pure[Response](Response.UnsupportedContractIdVersion) - - }).flatMap(response => - resolveStep( - Tracked.value( - metrics.execution.engineRunning, - trackSyncExecution(interpretationTimeNanos)(resume(response)), - ) - ) - ) - - case ResultNeedKey(key, limit, continuationToken, resume) => - disclosedOrStoreNKeyLookup(key, limit, continuationToken) - .flatMap { case (fcis, token) => - val entries: Vector[ResultNeedKey.Response.ContractEntry] = fcis.map { fci => - CantonContractIdVersion.extractCantonContractIdVersion(fci.contractId) match { - case Right(version) => - ResultNeedKey.Response.AuthenticableFatContractInstance( - fci, - version.contractHashingMethod, - hash => contractAuthenticator(fci, hash).isRight, - ) - case Left(_) => - ResultNeedKey.Response.UnsupportedContractIdVersion(fci.contractId) - } - } - resolveStep( - Tracked.value( - metrics.execution.engineRunning, - trackSyncExecution(interpretationTimeNanos)( - resume(ResultNeedKey.Response(entries, token)) - ), - ) - ) - } - - case ResultNeedPackage(packageId, resume) => - packageResolver - .resolve(packageId, PackageResolver.ignoreMissingPackage) - .flatMap { maybePackage => - resolveStep( - Tracked.value( - metrics.execution.engineRunning, - trackSyncExecution(interpretationTimeNanos)(resume(maybePackage)), - ) - ) - } - - case ResultInterruption(continue, abort) => - // We want to prevent the interpretation to run indefinitely and use all the resources. - // For this purpose, we check the following condition: - // - // Ledger Effective Time + skew > wall clock - // - // The skew is given by the dynamic synchronizer parameter `ledgerTimeRecordTimeTolerance`. - // - // As defined in the "Time on Daml Ledgers" chapter of the documentation, if this condition - // is true, then the Record Time (assigned later on when the transaction is sequenced) is already - // out of bounds, and the sequencer will reject the transaction. We can therefore abort the - // interpretation and return an error to the application. - - // Using a `Future` as a trampoline to make the recursive call to `resolveStep` stack safe. - def resume(): FutureUnlessShutdown[Either[ErrorCause, A]] = - FutureUnlessShutdown - .outcomeF { - Future { - Tracked.value( - metrics.execution.engineRunning, - trackSyncExecution(interpretationTimeNanos)(continue()), - ) - } - } - .flatMap(resolveStep) - - ledgerTimeRecordTimeToleranceO match { - // Fall back to not checking if the tolerance could not be retrieved - case None => resume() - - case Some(ledgerTimeRecordTimeTolerance) => - val let = ledgerEffectiveTime.toInstant - val currentTime = timeProvider.getCurrentTime - - val limitExceeded = - currentTime.isAfter(let.plus(ledgerTimeRecordTimeTolerance.duration)) - - if (limitExceeded) { - val error: ErrorCause = ErrorCause - .InterpretationTimeExceeded( - ledgerEffectiveTime, - ledgerTimeRecordTimeTolerance, - abort(), - ) - FutureUnlessShutdown.pure(Left(error)) - } else resume() - } - - case ResultPrefetch(coids, keys, resume) => - // Trigger loading through the state cache and the batch aggregator. - // Loading of contracts is a multi-stage process. - // - start with N items - // - trigger a single load in contractStore (1:1) - // - visit the mutableStateCache which will use the read through lookup - // - the read through lookup will ask the contract reader - // - the contract reader will ask the batchLoader - // - the batch loader will put independent requests together into db batches and respond - val disclosedCids = disclosedContractsById.keySet - val initialCids = coids.toSet.diff(disclosedCids) - import com.digitalasset.canton.util.FutureInstances.* - // load all contracts - - val loadContractsF = for { - contractsById <- initialCids.toSeq - .parTraverse(contractStore.lookupContractState(_)) - .map(_.flatMap(_.toContractOption.toList)) - contractsByKey <- keys.toSeq - .parTraverse { case (key, limit) => - contractStore.lookupNonUniqueContractKey(Set.empty, key, None, limit) - } - .map(_.flatMap(_.contracts)) - contracts = contractsById ++ contractsByKey - res <- recursiveLoad( - prefetchingRecursionLevel.value - 1, - disclosedCids, - contracts, - ) - } yield res - - FutureUnlessShutdown - .outcomeF(loadContractsF) - .flatMap(_ => resolveStep(resume())) - } - - resolveStep(result).thereafter { _ => - metrics.execution.lookupActiveContractPerExecution - .update(lookupActiveContractTime.get(), TimeUnit.NANOSECONDS) - metrics.execution.lookupActiveContractCountPerExecution - .update(lookupActiveContractCount.get) - metrics.execution.lookupContractKeyPerExecution - .update(lookupContractKeyTime.get(), TimeUnit.NANOSECONDS) - metrics.execution.lookupContractKeyCountPerExecution - .update(lookupContractKeyCount.get()) - metrics.execution.engine - .update(interpretationTimeNanos.get(), TimeUnit.NANOSECONDS) - } - } - - /** recursively prefetch contract ids up to a certain level */ - private def recursiveLoad( - depth: Int, - loaded: Set[ContractId], - justLoaded: Seq[LfFatContractInst], - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Set[ContractId]] = - if (justLoaded.isEmpty || depth <= 0) { - Future.successful(loaded) - } else { - import com.digitalasset.canton.util.FutureInstances.* - val found = - justLoaded.foldLeft(Set.empty[ContractId])((acc, contract) => contract.collectCids(acc)) - val fresh = found -- loaded - fresh.toSeq - .parTraverse(contractStore.lookupContractState) - .map(_.flatMap(_.toContractOption.toList)) - .flatMap(recursiveLoad(depth - 1, loaded ++ fresh, _)) - } - - private def trackSyncExecution[T](atomicNano: AtomicLong)(computation: => T): T = { - val start = System.nanoTime() - val result = computation - atomicNano.addAndGet(System.nanoTime() - start) - result - } - -} - -object StoreBackedCommandInterpreter { - - sealed trait StoreNeedKeyContinuationToken extends NeedKeyProgression.Token - object StoreNeedKeyContinuationToken { - final case class ContinueDisclosed(usedFromDisclosed: Int) extends StoreNeedKeyContinuationToken - final case class ContinueFromStore(token: Option[Long]) extends StoreNeedKeyContinuationToken - } - - def disclosedOrStoreNKeyLookup( - key: GlobalKey, - limit: Int, - progression: NeedKeyProgression.CanContinue, - disclosedContracts: Vector[LfFatContractInst], - disclosedContractsById: Map[ContractId, LfFatContractInst], - contractStore: ContractStore, - metrics: LedgerApiServerMetrics, - readers: Set[Ref.Party], - lookupContractKeyTime: AtomicLong, - lookupContractKeyCount: AtomicLong, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): FutureUnlessShutdown[(Vector[LfFatContractInst], NeedKeyProgression.HasStarted)] = { - def storeLookup( - key: GlobalKey, - limit: Int, - token: StoreNeedKeyContinuationToken.ContinueFromStore, - ): FutureUnlessShutdown[(Vector[LfFatContractInst], NeedKeyProgression.HasStarted)] = - timedNKeyLookup( - key, - limit, - token, - contractStore = contractStore, - metrics = metrics, - readers = readers, - lookupContractKeyTime = lookupContractKeyTime, - lookupContractKeyCount = lookupContractKeyCount, - ) - - val token = progression match { - case NeedKeyProgression.Unstarted => StoreNeedKeyContinuationToken.ContinueDisclosed(0) - case NeedKeyProgression.InProgress(t: StoreNeedKeyContinuationToken) => t - case NeedKeyProgression.InProgress(invalidToken) => - throw new IllegalArgumentException(s"Invalid token provided $invalidToken") - } - def filterNotDisclosedAndPrepend(prefix: Vector[LfFatContractInst])( - result: (Vector[LfFatContractInst], NeedKeyProgression.HasStarted) - ): (Vector[LfFatContractInst], NeedKeyProgression.HasStarted) = { - val (contracts, hasStarted) = result - val contractsNotDisclosed = - contracts.filterNot(contract => disclosedContractsById.contains(contract.contractId)) - (prefix ++ contractsNotDisclosed, hasStarted) - } - - token match { - case StoreNeedKeyContinuationToken.ContinueDisclosed(usedFromDisclosed) => - val (fromDisclosed, remainingFromDisclosed) = - disclosedContracts.drop(usedFromDisclosed).splitAt(limit) - if (remainingFromDisclosed.nonEmpty) { - FutureUnlessShutdown.pure( - fromDisclosed -> NeedKeyProgression.InProgress( - StoreNeedKeyContinuationToken.ContinueDisclosed(usedFromDisclosed + limit) - ) - ) - } else { - storeLookup( - key, - limit - fromDisclosed.size, - StoreNeedKeyContinuationToken.ContinueFromStore(None), - ).map(filterNotDisclosedAndPrepend(fromDisclosed)) - } - - case storeToken: StoreNeedKeyContinuationToken.ContinueFromStore => - storeLookup( - key, - limit, - storeToken, - ).map(filterNotDisclosedAndPrepend(Vector.empty)) - } - } - - def timedNKeyLookup( - key: GlobalKey, - limit: Int, - continuationToken: StoreNeedKeyContinuationToken.ContinueFromStore, - contractStore: ContractStore, - metrics: LedgerApiServerMetrics, - readers: Set[Ref.Party], - lookupContractKeyTime: AtomicLong, - lookupContractKeyCount: AtomicLong, - )(implicit - executionContext: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): FutureUnlessShutdown[(Vector[LfFatContractInst], NeedKeyProgression.HasStarted)] = { - import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.TimerOnShutdownSyntax - - if (limit <= 0) - FutureUnlessShutdown.pure( - Vector.empty -> NeedKeyProgression.InProgress( - StoreNeedKeyContinuationToken.ContinueFromStore(None) - ) - ) - else { - val start = System.nanoTime - Timed - .futureUS( - metrics.execution.lookupNContractKey, - FutureUnlessShutdown.outcomeF( - contractStore - .lookupNonUniqueContractKey( - readers = readers, - key = key, - pageToken = continuationToken.token, - limit = limit, - ) - .map(contractKeyPage => - ( - contractKeyPage.contracts, - contractKeyPage.nextPageToken.fold[NeedKeyProgression.HasStarted]( - NeedKeyProgression.Finished - )(token => - NeedKeyProgression.InProgress( - StoreNeedKeyContinuationToken.ContinueFromStore(Some(token)) - ) - ), - ) - ) - ), - ) - .map { - _.tap { _ => - lookupContractKeyTime.addAndGet(System.nanoTime() - start) - lookupContractKeyCount.incrementAndGet() - } - } - } - } - - def considerDisclosedContractsSynchronizerId( - prescribedSynchronizerIdO: Option[SynchronizerId], - disclosedContractsUsedInInterpretation: ImmArray[(ContractId, Option[SynchronizerId])], - logger: TracedLogger, - )(implicit - tc: TraceContext - ): Either[ErrorCause.DisclosedContractsSynchronizerIdMismatch, Option[SynchronizerId]] = { - val disclosedContractsSynchronizerIds: View[(ContractId, SynchronizerId)] = - disclosedContractsUsedInInterpretation.toSeq.view.collect { - case (contractId, Some(synchronizerId)) => contractId -> synchronizerId - } - - val synchronizerIdsOfDisclosedContracts = disclosedContractsSynchronizerIds.map(_._2).toSet - if (synchronizerIdsOfDisclosedContracts.sizeIs > 1) { - // Reject on diverging synchronizer ids for used disclosed contracts - Left( - ErrorCause.DisclosedContractsSynchronizerIdsMismatch( - disclosedContractsSynchronizerIds.toMap - ) - ) - } else - disclosedContractsSynchronizerIds.headOption match { - case None => - // If no disclosed contracts with a specified synchronizer id, use the prescribed one (if specified) - Right(prescribedSynchronizerIdO) - case Some((_, synchronizerIdOfDisclosedContracts)) => - prescribedSynchronizerIdO - .map { - // Both prescribed and from disclosed contracts synchronizer id - check for equality - case prescribed if synchronizerIdOfDisclosedContracts == prescribed => - Right(Some(prescribed)) - case mismatchingPrescribed => - Left( - ErrorCause.PrescribedSynchronizerIdMismatch( - disclosedContractIds = disclosedContractsSynchronizerIds.map(_._1).toSet, - synchronizerIdOfDisclosedContracts = synchronizerIdOfDisclosedContracts, - commandsSynchronizerId = mismatchingPrescribed, - ) - ) - } - // If the prescribed synchronizer id is not specified, use the synchronizer id of the disclosed contracts - .getOrElse { - logger.debug( - s"Using the synchronizer id ($synchronizerIdOfDisclosedContracts) of the disclosed contracts used in command interpretation (${disclosedContractsSynchronizerIds - .map(_._1) - .mkString("[", ",", "]")}) as the prescribed synchronizer id." - ) - Right(Some(synchronizerIdOfDisclosedContracts)) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TapsCommandExecutionFactory.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TapsCommandExecutionFactory.scala deleted file mode 100644 index b775ca12d7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TapsCommandExecutionFactory.scala +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.implicits.{catsSyntaxAlternativeSeparate, toFoldableOps} -import com.daml.metrics.Timed -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.error.TransactionRoutingError.ConfigurationErrors.InvalidPrescribedSynchronizerId -import com.digitalasset.canton.ledger.api.{Commands, PackageReference} -import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors -import com.digitalasset.canton.ledger.participant.state.{RoutingSynchronizerState, SyncService} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.PackagePreferenceBackend -import com.digitalasset.canton.platform.PackagePreferenceBackend.{ - Candidate, - SortedPreferences, - SupportedPackagesFilter, -} -import com.digitalasset.canton.platform.apiserver.execution.TapsCommandExecutionFactory.{ - PackagesForName, - TapsDescription, -} -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.topology.{PhysicalSynchronizerId, SynchronizerId} -import com.digitalasset.canton.util.EitherUtil.RichEither -import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.version.{EngineMode, ProtocolVersion} -import com.digitalasset.canton.{LfPackageId, LfPackageName, LfPackageVersion, LfPartyId} -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{PackageId, Party} -import com.digitalasset.daml.lf.engine.Error.{Package, Preprocessing} -import io.grpc.StatusRuntimeException - -import scala.collection.MapView -import scala.collection.immutable.SortedSet -import scala.concurrent.ExecutionContext -import scala.util.chaining.scalaUtilChainingOps - -import PackageReference.PackageReferenceOps - -/** Factory for creating and chaining a series of TAPS steps to be used in a command execution (see - * [[TopologyAwareCommandExecutor]]). - * - * Note: All the submission-related context parameters are shared across all TAPS passes created by - * this factory, including the execution and logging contexts . Do not reuse objects of this class - * across different submissions. - */ -private[execution] class TapsCommandExecutionFactory( - commands: Commands, - commandInterpreter: CommandInterpreter, - forExternallySigned: Boolean, - override val loggerFactory: NamedLoggerFactory, - packageMetadataSnapshot: PackageMetadata, - rootLevelPackageNames: Set[LfPackageName], - routingSynchronizerState: RoutingSynchronizerState, - submissionSeed: Hash, - syncService: SyncService, - metrics: LedgerApiServerMetrics, -)(implicit ec: ExecutionContext, loggingContextWithTrace: LoggingContextWithTrace) - extends NamedLogging { - - private val userSpecifiedPreference: PackagesForName = - orderUserSpecifiedPreferences( - pkgIds = commands.packagePreferenceSet, - packageVersionMap = packageMetadataSnapshot.packageIdVersionMap, - ) - - /** Execute a single TAPS pass (see [[TopologyAwareCommandExecutor]] ScalaDoc for more details) - * - * @param tapsPassDescription - * The description of this TAPS pass (used for logging) - * @param requiredSubmitters - * The parties expected to require submission rights on the preparing participant on the - * selected synchronizer - * @param partyPackageRequirements - * The package-names required to be vetted by each transaction informee party involved in the - * command. Only root-level package-names introduce strict requirements for the transaction's - * informees. Other package-names appearing in non-root nodes are only evaluated for debugging - * purposes (if their restriction cannot be satisfied, a debug log is emitted, but the - * synchronizer is not discarded). - * @param computePackagePreferenceSet - * Computes the package preference set to be used for interpreting the command, given the - * per-synchronizer package preference sets and the pass input - */ - def executePass( - tapsPassDescription: String, - requiredSubmitters: Set[Party], - partyPackageRequirements: Map[LfPartyId, Set[LfPackageName]], - computePackagePreferenceSet: NonEmpty[ - Map[PhysicalSynchronizerId, Set[LfPackageId]] - ] => FutureUnlessShutdown[(Set[LfPackageId], ProtocolVersion)], - ): FutureUnlessShutdown[TapsResult] = - new TapsPass(tapsPassDescription).execute( - requiredSubmitters, - partyPackageRequirements, - computePackagePreferenceSet, - ) - - private class TapsPass(tapsPassDescription: String) { - def execute( - requiredSubmitters: Set[Party], - partyPackageRequirements: Map[LfPartyId, Set[LfPackageName]], - computePackagePreferenceSet: NonEmpty[ - Map[PhysicalSynchronizerId, Set[LfPackageId]] - ] => FutureUnlessShutdown[(Set[LfPackageId], ProtocolVersion)], - ): FutureUnlessShutdown[TapsResult] = { - import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.TimerOnShutdownSyntax - logDebug(s"Attempting $tapsPassDescription of $TapsDescription") - for { - // Compute package preference set - packagePreferenceSetPV <- Timed.futureUS( - metrics.commands.tapsPackageSelection, - computePerSynchronizerPackagePreferenceSet(requiredSubmitters, partyPackageRequirements) - .flatMap(computePackagePreferenceSet), - ) - (packagePreferenceSet, protocolVersion) = packagePreferenceSetPV - - // TODO(#32356): Engine contract state mode selection for TAPS - mode = EngineMode.forProtocolVersion(protocolVersion) - - _ = logDebug( - show"Using package preference set: $packagePreferenceSet, protocol version: $protocolVersion" - ) - - // Interpret command with the computed package preference set - commandInterpretationResult: Either[ErrorCause, CommandInterpretationResult] <- - commandInterpreter.interpret( - commands.copy(packagePreferenceSet = packagePreferenceSet), - mode, - submissionSeed, - ) - - passResult: TapsResult <- commandInterpretationResult match { - case Left(error) => - val refinedError = - refinePackageNotFoundError(error, packageMetadataSnapshot.packageNameMap.keySet) - logDebug( - s"$TapsDescription failed before synchronizer routing. Aborting submission. Error: $refinedError" - ) - FutureUnlessShutdown.pure(TapsResult.InterpretationFailed(refinedError)) - case Right(commandInterpretationResult) => - // Try to route the interpreted command - syncService - .selectRoutingSynchronizer( - submitterInfo = commandInterpretationResult.submitterInfo, - transaction = commandInterpretationResult.transaction, - transactionMeta = commandInterpretationResult.transactionMeta, - disclosedContractIds = - commandInterpretationResult.processedDisclosedContracts.map(_.contractId).toList, - optSynchronizerId = commandInterpretationResult.optSynchronizerId, - transactionUsedForExternalSigning = forExternallySigned, - routingSynchronizerState = routingSynchronizerState, - ) - .value - .map { - case Right(synchronizerRank) => - // Pass succeeded - return the command execution result - logDebug( - s"$TapsDescription succeeded. Routing transaction for synchronization to ${synchronizerRank.synchronizerId}" - ) - TapsResult.Succeeded( - commandInterpretationResult.toCommandExecutionResult( - synchronizerRank, - routingSynchronizerState, - ) - ) - case Left(error) => - // Pass failed at routing stage - return the command interpretation result - val errorMessage = error.code.toMsg( - cause = error.cause, - correlationId = loggingContextWithTrace.traceContext.traceId, - limit = None, - ) - logDebug(s"Failed synchronizer routing: $errorMessage") - TapsResult.RoutingFailed( - commandInterpretationResult, - ErrorCause.RoutingFailed(error), - ) - } - } - } yield passResult - } - - private def computePerSynchronizerPackagePreferenceSet( - requiredSubmitters: Set[Party], - partyPackageRequirements: Map[LfPartyId, Set[LfPackageName]], - )(implicit - loggingContextWithTrace: LoggingContextWithTrace - ): FutureUnlessShutdown[NonEmpty[Map[PhysicalSynchronizerId, Set[LfPackageId]]]] = - for { - partyVettingMap: Map[PhysicalSynchronizerId, Map[LfPartyId, Set[PackageId]]] <- - syncService.computePartyVettingMap( - submitters = - Option.unless(forExternallySigned)(requiredSubmitters).getOrElse(Set.empty), - informees = partyPackageRequirements.keySet, - vettingValidityTimestamp = CantonTimestamp(commands.submittedAt), - prescribedSynchronizer = commands.synchronizerId, - routingSynchronizerState = routingSynchronizerState, - ) - - _ = logDebug( - show"Computing per-synchronizer package preference sets using the party-package requirements ($partyPackageRequirements) and root package-names ($rootLevelPackageNames)" - ) - - packageFilter = SupportedPackagesFilter( - supportedPackagesPerPackageName = - userSpecifiedPreference.view.mapValues(_.map(_.pkgId).toSet).toMap, - restrictionDescription = "Commands.package_id_selection_preference", - ) - perSynchronizerCandidates = partyVettingMap.view.map { case (syncId, partiesVettingState) => - val candidates = PackagePreferenceBackend.computePerSynchronizerPackageCandidates( - partiesVettingState = partiesVettingState, - packageMetadataSnapshot = packageMetadataSnapshot, - packageFilter = packageFilter, - requirements = partyPackageRequirements, - synchronizerProtocolVersion = syncId.protocolVersion, - ) - syncId -> applyRootPackageNamesRestriction(candidates, rootLevelPackageNames) - } - - (discardedSyncs, availableSyncs) = perSynchronizerCandidates - .map { case (sync, candidates) => candidates.map(sync -> _).left.map(sync -> _) } - .toSeq - .separate - - perSynchronizerPreferenceSet <- - NonEmpty - .from(availableSyncs.toMap) - .toRight( - buildSelectionFailedError( - prescribedSynchronizerIdO = commands.synchronizerId, - discardedSynchronizers = discardedSyncs, - partyPackageRequirements = partyPackageRequirements, - ) - ) - .toFutureUS(identity) - } yield perSynchronizerPreferenceSet - - private def applyRootPackageNamesRestriction( - packageNameCandidates: MapView[LfPackageName, Candidate[SortedPreferences]], - rootPackageNames: Set[LfPackageName], - )(implicit - loggingContextWithTrace: LoggingContextWithTrace - ): Either[String, Set[LfPackageId]] = { - val unavailablePackageNames = rootPackageNames.diff(packageNameCandidates.keySet) - // Discard a synchronizer if there are unavailable package-names pertaining to root nodes - // This can happen if no one vetted any package from a specific package-name - if (unavailablePackageNames.nonEmpty) { - Left( - show"Unable to find some package-names used in command root nodes: $unavailablePackageNames. Either these packages are not known on this participant or they are not vetted by the required informee participants. Please upload and vet the missing packages to proceed." - ) - } else { - packageNameCandidates.toSeq.foldM(Set.empty[LfPackageId]) { - case (acc, (_, Right(pkgIdCandidates))) => - Right(acc + pkgIdCandidates.last1.pkgId) - case (_, (pkgName, Left(pkgNameDiscardReason))) if rootPackageNames(pkgName) => - // Discard a synchronizer if there are package-names pertaining to root nodes that have no preferences - // This can happen if a package-name had vetted packages for some party, but it has been discarded due to some restrictions - Left( - show"Failed to select package-id for package-name '$pkgName' appearing in a command root node due to: $pkgNameDiscardReason" - ) - case (acc, (pkgName, Left(pkgNameDiscardReason))) => - // If not a root-node package-name, just log the discard reason and continue - logDebug( - show"No vetted package selection possible for '$pkgName': $pkgNameDiscardReason" - ) - Right(acc) - } - } - } - - private def buildSelectionFailedError( - prescribedSynchronizerIdO: Option[SynchronizerId], - discardedSynchronizers: Seq[(PhysicalSynchronizerId, String)], - partyPackageRequirements: Map[LfPartyId, Set[LfPackageName]], - ): StatusRuntimeException = { - val reason = show"Discarded synchronizers: ${discardedSynchronizers - .map { case (sync, discardReason) => s"$sync: $discardReason" } - .mkString("\n\t", "\n\t", "")}" - - prescribedSynchronizerIdO - .map { prescribedSynchronizerId => - InvalidPrescribedSynchronizerId - .Generic(prescribedSynchronizerId, reason) - .asGrpcError - } - .getOrElse( - CommandExecutionErrors.PackageSelectionFailed - .Reject( - s"No synchronizers satisfy the topology requirements for the submitted command: $reason" - ) - .asGrpcError - ) - .tap { _ => - logInfo( - show"Party-package requirements used for the failed package selection: $partyPackageRequirements" - ) - } - } - - // TODO(#25385): Ideally the Engine already returns a specialized error instead - // of having the need to decide here whether the package-name was discarded or not - private def refinePackageNotFoundError( - errorCause: ErrorCause, - locallyStoredPackageNames: Set[LfPackageName], - )(implicit errorLoggingContext: ErrorLoggingContext): ErrorCause = - // It can be that a missing or unresolved package name is due to package selection algorithm - // removing it from the package-map provided to the engine due to topology constraints. - // In these cases, report a dedicated error to the client to aid debugging. - errorCause match { - case ErrorCause.DamlLf( - Package(Package.MissingPackage(Ref.PackageRef.Name(pkgName), context)) - ) if locallyStoredPackageNames(pkgName) => - ErrorCause.RoutingFailed( - CommandExecutionErrors.PackageNameDiscardedDueToUnvettedPackages - .Reject(pkgName, context) - ) - - case ErrorCause.DamlLf(Preprocessing(Preprocessing.UnresolvedPackageName(pkgName, context))) - if locallyStoredPackageNames(pkgName) => - ErrorCause.RoutingFailed( - CommandExecutionErrors.PackageNameDiscardedDueToUnvettedPackages - .Reject(pkgName, context) - ) - - case other => other - } - - private def logDebug(msg: => String)(implicit - loggingContext: LoggingContextWithTrace - ): Unit = logger.debug(s"Phase 1 [$tapsPassDescription]: $msg")(loggingContext.traceContext) - - private def logInfo(msg: => String)(implicit - loggingContext: LoggingContextWithTrace - ): Unit = logger.info(s"Phase 1 [$tapsPassDescription]: $msg")(loggingContext.traceContext) - } - - private def orderUserSpecifiedPreferences( - pkgIds: Set[LfPackageId], - packageVersionMap: Map[LfPackageId, (LfPackageName, LfPackageVersion)], - ): PackagesForName = - pkgIds.view - .flatMap(pkgId => - // TODO(#25385): Consider rejecting submissions where the resolution does not yield a package name for user-specified package-id - pkgId.toPackageReference(packageVersionMap).map(pkgId -> _).orElse { - logger.debug( - show"Package $pkgId is not known. Discarding from user-specified package preferences in commands" - ) - None - } - ) - .groupMap { case (_, PackageReference(_, _, pkgName)) => pkgName }(_._2) - .view - .mapValues(SortedSet.from[PackageReference]) - .toMap -} - -object TapsCommandExecutionFactory { - type PackagesForName = - Map[LfPackageName, SortedSet[PackageReference] /* least preferred first */ ] - private[execution] val TapsDescription = "Topology-aware package selection for command submission" -} - -sealed trait TapsResult extends Product with Serializable { - def toSubmissionResult: Either[ErrorCause, CommandExecutionResult] -} - -object TapsResult { - // Command execution failed at the interpretation stage - // and the submission should be rejected - final case class InterpretationFailed(cause: ErrorCause) extends TapsResult { - override def toSubmissionResult: Either[ErrorCause, CommandExecutionResult] = Left(cause) - } - final case class RoutingFailed( - interpretation: CommandInterpretationResult, - cause: ErrorCause.RoutingFailed, - ) extends TapsResult { - override def toSubmissionResult: Either[ErrorCause, CommandExecutionResult] = Left(cause) - } - - final case class Succeeded(commandExecutionResult: CommandExecutionResult) extends TapsResult { - override def toSubmissionResult: Either[ErrorCause, CommandExecutionResult] = Right( - commandExecutionResult - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TimedCommandExecutor.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TimedCommandExecutor.scala deleted file mode 100644 index 6775463982..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TimedCommandExecutor.scala +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.data.EitherT -import com.daml.metrics.Timed -import com.digitalasset.canton.ledger.api.Commands -import com.digitalasset.canton.ledger.participant.state.RoutingSynchronizerState -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.TimerAndTrackOnShutdownSyntax -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.daml.lf.crypto.Hash - -private[apiserver] class TimedCommandExecutor( - delegate: CommandExecutor, - metrics: LedgerApiServerMetrics, -) extends CommandExecutor { - - override def execute( - commands: Commands, - submissionSeed: Hash, - routingSynchronizerState: RoutingSynchronizerState, - forExternallySigned: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): EitherT[FutureUnlessShutdown, ErrorCause, CommandExecutionResult] = - EitherT( - Timed.timedAndTrackedFutureUS( - metrics.execution.total, - metrics.execution.totalRunning, - delegate - .execute(commands, submissionSeed, routingSynchronizerState, forExternallySigned) - .value, - ) - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TopologyAwareCommandExecutor.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TopologyAwareCommandExecutor.scala deleted file mode 100644 index 418db1e5e4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/TopologyAwareCommandExecutor.scala +++ /dev/null @@ -1,358 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.data.EitherT -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.* -import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.ledger.api.Commands -import com.digitalasset.canton.ledger.api.PackageReference.* -import com.digitalasset.canton.ledger.participant.state.{RoutingSynchronizerState, SyncService} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.daml.lf.command.ApiCommand -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.{FullReference, PackageName, Party} -import com.digitalasset.daml.lf.engine.Blinding - -import scala.concurrent.ExecutionContext - -// TODO(#25385): Consider introducing performance observability metrics -// due to the high computational complexity of the algorithm -/** Command executor that uses the '''topology-aware package selection''' algorithm for computing - * the package preference set to-be-used by the Daml Engine in command interpretation. - * - * =Topology-Aware Package Selection (TAPS)= - * - * The topology-aware package selection (abbreviated further as '''TAPS''') algorithm is a - * heuristic that computes a package-name-to-package-ID map to be used by the Daml Engine in - * command interpretation for resolving the packages that contracts must be interpreted with in for - * the purpose of implementing up/downgrading. - * - * TAPS uses approximations of topology requirements as input, because the exact requirements (e.g. - * which informees, on which synchronizers, require which packages) can only be precisely derived - * from the Daml transaction resulted from command interpretation. - * - * The situations in which the Engine needs to resolve package names to package IDs during command - * interpretation for up/downgrading are: - * - * - A top-level command is submitted for a contract with a template-id specified using the - * package-name reference format (see [[com.daml.ledger.api.v2.value.Identifier]]) - * - * - An exercise-by-interface or fetch-by-interface in an action node in the interpreted - * transaction - * - * The algorithm depends on the following key definitions: - * - * - '''Party-level vetted package''': A package that is vetted by every participant hosting a - * specific party. Also referred to as a consistently-vetted package for a party. - * - * - '''Submitter interest in a package-name''': A submitter is interested in a package-name if - * it has vetted any package ID pertaining to that package name. - * - * - '''Informee interest in a package-name''': An informee, other than a submitter, is - * interested in a package-name if it requires the package-name in the transaction, as - * interpreted from the previous pass. - * - * - '''Commonly-vetted package''': A package that is vetted by all parties interested in the - * package lineage and whose direct and transitive dependencies are commonly-vetted. - * - * Since each package ID has a unique package name, the package-name-to-package ID resolution can - * simply be represented by its image: a set of package IDs whose package names are all different. - * We refer to this set as the '''package preference set''' - * - * TAPS chooses the synchronizer during package preference set computation, as package vetting and - * party hosting are topology information tied to specific synchronizers. While the synchronizer - * choice remains hidden from the Engine, the provided package preference set ensures compliance - * with topology constraints of the selected synchronizer. - * - * As part of command execution, there are several TAPS attempts: - * - An initial - approximation - pass that uses information derived only from the submitted - * command for computing the package preference set. - * - Subsequent - refinement - passes that use the information derived from the interpreted - * transaction obtained from the previous passes to compute a more accurate package preference - * set. - * - * Each TAPS pass generally performs the following steps: - * - * 1. '''Define Input Topology Requirements''' - find the topology constraints that the - * interpreted Daml transaction should satisfy when evaluating submission candidate - * synchronizers: - * a. Expected transaction submitters: which parties should have submission rights on the - * preparing participant - * a. Expected transaction informees: which parties should have at least observation rights on - * some participant - * a. Root-node package-names vetting requirements: the package-names of the command's root - * nodes for which there must exist commonly-vetted package IDs for the expected informees - * of these nodes. - * - * 1. '''Compute Per-Synchronizer Package Preference Set''': Based on the Input Topology - * transaction informees requirement, a package preference set is computed for each - * synchronizer connected to the preparing participant. This set contains the - * highest-versioned ''commonly-vetted'' package ID for each package name the expected - * transaction's informees are interested in. '''Note''': Only synchronizers with valid - * package candidates in the package preference set for all the command's root package-names - * are admissible and used for further TAPS processing. - * - * 1. '''Process Package Preference Set''': The per-synchronizer package preference set is then - * processed into the package preference set used for interpretation (which differs between - * Initial Pass and Subsequent Passes). - * - * 1. '''Interpret Commands''': The Daml Engine interprets the submitted commands using the - * computed package preference set (see [[com.digitalasset.daml.lf.engine.Engine.submit]] - * - * 1. '''Route Transaction''': If interpretation is successful, synchronizer routing searches for - * a suitable synchronizer that satisfies the interpreted transaction's topology constraints - * (see - * [[com.digitalasset.canton.ledger.participant.state.SyncService.selectRoutingSynchronizer]]). - * If found, the transaction is routed for protocol synchronization. - * - * The two types of passes of the algorithm differ as follows: - * - * ==Initial Pass== - * - * The input topology requirements are derived from the submitter party, which is conventionally - * the first party of `Commands.act_as`. This party is considered the submitter and sole informee - * of the transaction. The package-name requirements are derived from the package-names of all of - * the submitted command’s root nodes, leading to an input topology requirement modelled as - * submitter-party -> Set[command_root_nodes_packages]. The resulting per-synchronizer preference - * set is merged into a single package preference set by selecting the highest-versioned package - * across all admissible synchronizers for the submitter's vetted package names. If synchronizer - * routing in this pass doesn't yield a valid synchronizer, the algorithm proceeds to the next - * pass. - * - * ==Subsequent Passes== - * - * In this pass, the topology requirements are derived from the Daml transactions obtained during - * the command interpretation from the previous passes, referred to below as the '''draft - * transactions'''. These new requirements stipulate that every informee of the previous draft - * transactions has expressed interest in specific packages. As such their vetting state will - * constrain the selection of those packages. This ensures that TAPS converges after a finite - * number of passes. The package preference set for interpretation in this pass is derived from the - * per-synchronizer package preference sets by selecting the one associated with the highest-ranked - * admissible synchronizer (see - * [[com.digitalasset.canton.ledger.participant.state.SyncService.computeHighestRankedSynchronizerFromAdmissible]]). - * If the resulting Daml transaction after interpretation in this pass cannot be routed to a valid - * synchronizer, the algorithm loop with this transaction as the new draft transaction for the next - * pass. - * - * The algorithm terminates successfully as soon as the interpreted transaction can be routed to a - * valid synchronizer. - * - * The algorithm fails if: - * - Interpretation fails. - * - A pass fails to make progress (the set of required packages is not growing). - * - The number of passes has reached the configured maximum (default: 3). - * - * '''Note''': When `Commands.package_id_selection_preference` is specified, it acts as a - * restriction in the package preference set computation for both passes. If this restriction - * cannot be honored, command submission fails. - */ -private[execution] class TopologyAwareCommandExecutor( - syncService: SyncService, - commandInterpreter: CommandInterpreter, - maxPassesDefault: PositiveInt, - maxPassesLimit: PositiveInt, - metrics: LedgerApiServerMetrics, - override val loggerFactory: NamedLoggerFactory, -)(implicit - ec: ExecutionContext -) extends NamedLogging - with CommandExecutor { - - override def execute( - commands: Commands, - submissionSeed: Hash, - routingSynchronizerState: RoutingSynchronizerState, - forExternallySigned: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): EitherT[FutureUnlessShutdown, ErrorCause, CommandExecutionResult] = { - val packageMetadataSnapshot = syncService.getPackageMetadataSnapshot - val packageIndex = packageMetadataSnapshot.packageIdVersionMap - - val maxNumberOfPass = commands.tapsMaxPasses match { - case None => maxPassesDefault - case Some(commandMaxPasses) => - if (commandMaxPasses > maxPassesLimit) { - logger.info( - s"Requested max TAPS passes $commandMaxPasses in `max_taps_passes` of the submitted command exceeds the participant-configured limit $maxPassesLimit. Using the participant-configured limit." - ) - maxPassesLimit - } else commandMaxPasses - } - val rootLevelPackageNames = apiCommandsRootPackageNames(commands) - - val tapsExecutionFactory = new TapsCommandExecutionFactory( - commands = commands, - commandInterpreter = commandInterpreter, - forExternallySigned = forExternallySigned, - loggerFactory = loggerFactory, - packageMetadataSnapshot = packageMetadataSnapshot, - rootLevelPackageNames = rootLevelPackageNames, - routingSynchronizerState = routingSynchronizerState, - submissionSeed = submissionSeed, - syncService = syncService, - metrics = metrics, - ) - def firstPass(passInput: PassInput): FutureUnlessShutdown[TapsResult] = - tapsExecutionFactory.executePass( - tapsPassDescription = "TAPS pass 1", - requiredSubmitters = passInput.requiredSubmitters, - partyPackageRequirements = passInput.partyPackageRequirements, - computePackagePreferenceSet = perSynchronizerPreferenceSet => - FutureUnlessShutdown.pure( - ( - perSynchronizerPreferenceSet.values.flatten - .map(_.unsafeToPackageReference(packageIndex)) - .groupBy(_.packageName) - .valuesIterator - .map( - _.maxOption - .getOrElse(sys.error("Unexpected empty references set after groupBy")) - .pkgId - ) - .toSet, - perSynchronizerPreferenceSet.keySet.max1.protocolVersion, - ) - ), - ) - - def loop( - previousInput: PassInput, - passResult: TapsResult, - passNumber: PositiveInt, - ): FutureUnlessShutdown[(TapsResult, PositiveInt)] = - passResult match { - case routingFailed: TapsResult.RoutingFailed if passNumber >= maxNumberOfPass => - logger.info( - s"Phase 1 [TAPS pass $passNumber]: Stopping after routing failure because it reached the maximum number of passes." - ) - FutureUnlessShutdown.pure((routingFailed, passNumber)) - case routingFailed @ TapsResult.RoutingFailed(interpretationResult, _) => - val nextInput = buildNextInput(previousInput, interpretationResult, packageIndex) - if (nextInput.hasMorePackageRequirementsThan(previousInput)) { - val nextPassNumber = passNumber + PositiveInt.one - tapsExecutionFactory - .executePass( - tapsPassDescription = s"TAPS pass $nextPassNumber", - requiredSubmitters = nextInput.requiredSubmitters, - partyPackageRequirements = nextInput.partyPackageRequirements, - computePackagePreferenceSet = perSynchronizerPreferenceSet => - syncService - .computeHighestRankedSynchronizerFromAdmissible( - submitterInfo = interpretationResult.submitterInfo, - transaction = interpretationResult.transaction, - transactionMeta = interpretationResult.transactionMeta, - admissibleSynchronizers = perSynchronizerPreferenceSet.keySet, - disclosedContractIds = - interpretationResult.processedDisclosedContracts.map(_.contractId).toList, - routingSynchronizerState = routingSynchronizerState, - ) - .leftSemiflatMap(err => FutureUnlessShutdown.failed(err.asGrpcError)) - .merge - .map(highestRankedSync => - ( - checked(perSynchronizerPreferenceSet(highestRankedSync)), - highestRankedSync.protocolVersion, - ) - ), - ) - .flatMap(nextResult => loop(nextInput, nextResult, nextPassNumber)) - } else { - logger.info( - s"Phase 1 [TAPS pass $passNumber]: Stopping after routing failure because no new package requirements were found. TAPS cannot make further progress." - ) - FutureUnlessShutdown.pure((routingFailed, passNumber)) - } - case successOrFailure => FutureUnlessShutdown.pure((successOrFailure, passNumber)) - } - - val requiredSubmitter = - commands.actAs.headOption.getOrElse(sys.error("act_as must be non-empty")) - val initialInput = PassInput( - requiredSubmitters = Set(requiredSubmitter), - partyPackageRequirements = Map(requiredSubmitter -> rootLevelPackageNames), - ) - val result = for { - firstResult <- firstPass(initialInput) - (finalResult, passNumber) <- loop(initialInput, firstResult, PositiveInt.one) - } yield { - val metricsContext = finalResult match { - case _: TapsResult.Succeeded => MetricsContext("status" -> "success") - case _: TapsResult.InterpretationFailed => - MetricsContext("status" -> "interpretation_failed") - case _: TapsResult.RoutingFailed => MetricsContext("status" -> "routing_failed") - } - metrics.commands.tapsPasses.update(passNumber.value)(metricsContext) - finalResult.toSubmissionResult - } - EitherT(result) - } - - private def buildNextInput( - previousInput: PassInput, - interpretation: CommandInterpretationResult, - packageIndex: Map[LfPackageId, (PackageName, LfPackageVersion)], - ): PassInput = - // Merge with previous requirements to avoid oscillating between two different failing - // requirement sets. Narrowing down the number of package candidates ensures we reach either - // a valid configuration or a terminal error state. - previousInput - .addPartyPackageRequirements( - Blinding.partyPackages(interpretation.transaction).view.mapValues { pkgIds => - pkgIds.map( - // It is fine to use unsafe here since the package must have been indexed on the participant - // if it appeared in the draft transaction. - _.unsafeToPackageReference(packageIndex).packageName - ) - } - ) - .withRequiredSubmitters( - interpretation.transaction.rootNodes.iterator.flatMap(_.requiredAuthorizers).toSet - ) - - private case class PassInput( - requiredSubmitters: Set[Party], - partyPackageRequirements: Map[LfPartyId, Set[LfPackageName]], - ) { - def withRequiredSubmitters(newRequiredSubmitters: Set[Party]): PassInput = - copy(requiredSubmitters = newRequiredSubmitters) - - def addPartyPackageRequirements( - newPartyPackageRequirements: Iterable[(LfPartyId, Set[LfPackageName])] - ): PassInput = - copy( - partyPackageRequirements = newPartyPackageRequirements - .foldLeft(partyPackageRequirements) { case (acc, (k, v)) => - acc.updated(k, acc.getOrElse(k, Set.empty) ++ v) - } - ) - - def hasMorePackageRequirementsThan(other: PassInput): Boolean = - partyPackageRequirements.exists { case (party, packages) => - (packages -- other.partyPackageRequirements.getOrElse(party, Set.empty)).nonEmpty - } - } - - // Note: This method also collect package-names for interfaces in case of exercise-by-interface commands - // even though interfaces are not upgradeable. However, this is fine - // since the package selection algorithm merely should still be able to find a commonly-vetted package - // for the package-names used, even though this preference is ignored by the Engine - private def apiCommandsRootPackageNames(commands: Commands): Set[PackageName] = - commands.commands.commands.iterator.collect { - case ApiCommand.Create(FullReference(LfPackageRef.Name(pkgName), _), _) => - pkgName - case ApiCommand.Exercise(FullReference(LfPackageRef.Name(pkgName), _), _, _, _) => - pkgName - case ApiCommand.ExerciseByKey(FullReference(LfPackageRef.Name(pkgName), _), _, _, _) => - pkgName - case ApiCommand.CreateAndExercise(FullReference(LfPackageRef.Name(pkgName), _), _, _, _) => - pkgName - }.toSet -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheck.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheck.scala deleted file mode 100644 index 5ed31d6137..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheck.scala +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.ratelimiting - -import com.digitalasset.canton.ledger.error.LedgerApiErrors.HeapMemoryOverLimit -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory} -import com.digitalasset.canton.networking.grpc.ratelimiting.LimitResult -import com.digitalasset.canton.networking.grpc.ratelimiting.LimitResult.{LimitResultCheck, OverLimit, UnderLimit} -import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig - -import java.lang.management.{MemoryMXBean, MemoryPoolMXBean, MemoryType, MemoryUsage} -import java.util.concurrent.atomic.AtomicLong -import javax.management.ObjectName -import scala.annotation.nowarn -import scala.concurrent.duration.{Duration, DurationInt} - -object MemoryCheck { - - def apply( - tenuredMemoryPools: List[MemoryPoolMXBean], - memoryMxBean: MemoryMXBean, - config: RateLimitingConfig, - loggerFactory: NamedLoggerFactory, - ): LimitResultCheck = { - implicit val logger = ErrorLoggingContext.forClass(loggerFactory, getClass) - - apply( - findTenuredMemoryPool(config, tenuredMemoryPools, logger), - new GcThrottledMemoryBean(memoryMxBean), - config, - ) - } - - def apply( - tenuredMemoryPool: Option[MemoryPoolMXBean], - memoryMxBean: GcThrottledMemoryBean, - config: RateLimitingConfig, - )(implicit logger: ErrorLoggingContext): LimitResultCheck = (fullMethodName, _) => { - - tenuredMemoryPool.fold[LimitResult](UnderLimit) { p => - if (p.isCollectionUsageThresholdExceeded) { - val expectedThreshold = - config.calculateCollectionUsageThreshold(p.getCollectionUsage.getMax) - if (p.getCollectionUsageThreshold == expectedThreshold) { - // Based on a combination of JvmMetricSet and MemoryUsageGaugeSet - val poolBeanMetricPrefix = - "jvm_memory_usage_pools_%s".format(p.getName.replaceAll("\\s+", "_")) - val damlError = HeapMemoryOverLimit.Rejection( - memoryPool = p.getName, - limit = p.getCollectionUsageThreshold, - metricPrefix = poolBeanMetricPrefix, - fullMethodName = fullMethodName, - ) - gc(memoryMxBean) - OverLimit(damlError) - } else { - // In experimental testing the size of the tenured memory pool did not change. However the API docs, - // see https://docs.oracle.com/javase/8/docs/api/java/lang/management/MemoryUsage.html - // say 'The maximum amount of memory may change over time'. If we detect this situation we - // recalculate and reset the threshold - logger.warn( - s"Detected change in max pool memory, updating collection usage threshold from ${p.getCollectionUsageThreshold} to $expectedThreshold" - ) - p.setCollectionUsageThreshold(expectedThreshold) - UnderLimit - } - } else { - UnderLimit - } - } - } - - /** When the collected tenured memory pool usage exceeds the threshold this state will continue - * even if memory has been freed up if no garbage collection takes place. For this reason when we - * are over limit we also run garbage collection on every request to ensure the collection usage - * stats are as up to date as possible to thus stop rate limiting as soon as possible. - * - * We use a throttled memory bean to ensure that even if the server is under heavy rate limited - * load calls to the underlying system gc are limited. - */ - - private def gc(memoryMxBean: GcThrottledMemoryBean): Unit = - memoryMxBean.gc() - - private[ratelimiting] class GcThrottledMemoryBean( - delegate: MemoryMXBean, - delayBetweenCalls: Duration = 1.seconds, - ) extends MemoryMXBean { - - private val lastCall = new AtomicLong() - - /** Only GC if we have not called gc for at least [[delayBetweenCalls]] - */ - override def gc(): Unit = { - val last = lastCall.get() - val now = System.currentTimeMillis() - if (now - last > delayBetweenCalls.toMillis && lastCall.compareAndSet(last, now)) - delegate.gc() - } - - // Delegated methods - @nowarn("cat=deprecation") - override def getObjectPendingFinalizationCount: Int = delegate.getObjectPendingFinalizationCount - override def getHeapMemoryUsage: MemoryUsage = delegate.getHeapMemoryUsage - override def getNonHeapMemoryUsage: MemoryUsage = delegate.getNonHeapMemoryUsage - override def isVerbose: Boolean = delegate.isVerbose - override def setVerbose(value: Boolean): Unit = delegate.setVerbose(value) - override def getObjectName: ObjectName = delegate.getObjectName - } - - @SuppressWarnings(Array("org.wartremover.warts.SortedMaxMinOption")) - private[ratelimiting] def findTenuredMemoryPool( - config: RateLimitingConfig, - memoryPoolMxBeans: List[MemoryPoolMXBean], - logger: ErrorLoggingContext, - ): Option[MemoryPoolMXBean] = - candidates(memoryPoolMxBeans).sortBy(_.getCollectionUsage.getMax).lastOption match { - case None => - logger.error("Could not find tenured memory pool") - None - case Some(pool) => - val threshold = config.calculateCollectionUsageThreshold(pool.getCollectionUsage.getMax) - logger.info( - s"Using 'tenured' memory pool ${pool.getName}. Setting its collection pool threshold to $threshold" - ) - pool.setCollectionUsageThreshold(threshold) - Some(pool) - } - - private def candidates(memoryPoolMxBeans: List[MemoryPoolMXBean]): List[MemoryPoolMXBean] = - memoryPoolMxBeans.filter(p => - p.getType == MemoryType.HEAP && p.isCollectionUsageThresholdSupported - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/RateLimitingInterceptorFactory.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/RateLimitingInterceptorFactory.scala deleted file mode 100644 index 2cd9fb0be5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/RateLimitingInterceptorFactory.scala +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.ratelimiting - -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.networking.grpc.ratelimiting.LimitResult.LimitResultCheck -import com.digitalasset.canton.networking.grpc.ratelimiting.RateLimitingInterceptor -import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig - -import java.lang.management.{ManagementFactory, MemoryMXBean, MemoryPoolMXBean} -import scala.jdk.CollectionConverters.ListHasAsScala - -object RateLimitingInterceptorFactory { - - def create( - loggerFactory: NamedLoggerFactory, - config: RateLimitingConfig, - additionalChecks: List[LimitResultCheck] = List.empty, - ): RateLimitingInterceptor = - createWithMXBeans( - loggerFactory = loggerFactory, - config = config, - tenuredMemoryPools = ManagementFactory.getMemoryPoolMXBeans.asScala.toList, - memoryMxBean = ManagementFactory.getMemoryMXBean, - additionalChecks = additionalChecks, - ) - - def createWithMXBeans( - loggerFactory: NamedLoggerFactory, - config: RateLimitingConfig, - tenuredMemoryPools: List[MemoryPoolMXBean], - memoryMxBean: MemoryMXBean, - additionalChecks: List[LimitResultCheck], - ): RateLimitingInterceptor = - new RateLimitingInterceptor( - checks = List[LimitResultCheck]( - MemoryCheck(tenuredMemoryPools, memoryMxBean, config, loggerFactory) - ) ::: additionalChecks - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandCompletionService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandCompletionService.scala deleted file mode 100644 index 501fd441de..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandCompletionService.scala +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.daml.ledger.api.v2.command_completion_service.{ - CommandCompletionServiceGrpc, - CompletionStreamRequest, - CompletionStreamResponse, -} -import com.daml.logging.entries.LoggingEntries -import com.digitalasset.canton.ledger.api.ValidationLogger -import com.digitalasset.canton.ledger.api.grpc.StreamingServiceLifecycleManagement -import com.digitalasset.canton.ledger.api.validation.CompletionServiceRequestValidator -import com.digitalasset.canton.ledger.participant.state.index.IndexCompletionsService -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.tracing.TraceContextGrpc -import io.grpc.stub.StreamObserver -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.Source - -final class ApiCommandCompletionService( - completionsService: IndexCompletionsService, - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, -)(implicit - esf: ExecutionSequencerFactory, - mat: Materializer, -) extends CommandCompletionServiceGrpc.CommandCompletionService - with StreamingServiceLifecycleManagement - with NamedLogging { - - override def completionStream( - request: CompletionStreamRequest, - responseObserver: StreamObserver[CompletionStreamResponse], - ): Unit = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - registerStream(responseObserver) { - implicit val errorLoggingContext: ErrorLoggingContext = ErrorLoggingContext( - logger, - loggingContextWithTrace.toPropertiesMap, - loggingContextWithTrace.traceContext, - ) - logger.debug(s"Received new completion request $request.") - Source.future(completionsService.currentLedgerEnd()).flatMapConcat { ledgerEnd => - CompletionServiceRequestValidator - .validateGrpcCompletionStreamRequest(request) - .flatMap(CompletionServiceRequestValidator.validateCompletionStreamRequest(_, ledgerEnd)) - .fold( - t => - Source.failed[CompletionStreamResponse]( - ValidationLogger.logFailureWithTrace(logger, request, t) - ), - request => { - logger.info( - s"Received request for completion subscription, ${loggingContextWithTrace - .serializeFiltered("parties", "offset")}" - ) - - completionsService - .getCompletions( - request.offset, - request.userId, - request.parties, - ) - .via( - logger.enrichedDebugStream( - "Responding with completions.", - response => - response.completionResponse.completion match { - case Some(completion) => - LoggingEntries( - "commandId" -> completion.commandId, - "statusCode" -> completion.status.map(_.code), - ) - case None => - LoggingEntries() - }, - ) - ) - .via(logger.logErrorsOnStream) - .via(StreamMetrics.countElements(metrics.lapi.streams.completions)) - }, - ) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandService.scala deleted file mode 100644 index ebbe846fbc..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandService.scala +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.command_service.* -import com.daml.ledger.api.v2.command_service.CommandServiceGrpc.CommandService as CommandServiceGrpc -import com.daml.ledger.api.v2.commands.Commands -import com.daml.ledger.api.v2.reassignment_commands.ReassignmentCommands -import com.daml.ledger.api.v2.transaction_filter.CumulativeFilter.IdentifierFilter -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA -import com.daml.ledger.api.v2.transaction_filter.{ - CumulativeFilter, - EventFormat, - Filters, - TransactionFormat, - WildcardFilter, -} -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.services.CommandService -import com.digitalasset.canton.ledger.api.validation.{ - CommandsValidator, - SubmitAndWaitRequestValidator, -} -import com.digitalasset.canton.ledger.api.{ProxyCloseable, SubmissionIdGenerator, ValidationLogger} -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.platform.apiserver.services.ApiCommandService.generateTransactionFormatIfEmpty -import io.grpc.ServerServiceDefinition - -import java.time.{Duration, Instant} -import scala.concurrent.{ExecutionContext, Future} - -class ApiCommandService( - protected val service: CommandService & AutoCloseable, - commandsValidator: CommandsValidator, - currentLedgerTime: () => Instant, - currentUtcTime: () => Instant, - maxDeduplicationDuration: Duration, - generateSubmissionId: SubmissionIdGenerator, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends CommandServiceGrpc - with GrpcApiService - with ProxyCloseable - with NamedLogging { - - private[this] val validator = new SubmitAndWaitRequestValidator(commandsValidator) - - override def submitAndWait(request: SubmitAndWaitRequest): Future[SubmitAndWaitResponse] = - enrichRequestAndSubmit(request = request)(service.submitAndWait) - - override def submitAndWaitForTransaction( - request: SubmitAndWaitForTransactionRequest - ): Future[SubmitAndWaitForTransactionResponse] = - enrichRequestAndSubmit(request = request)(service.submitAndWaitForTransaction) - - override def submitAndWaitForReassignment( - request: SubmitAndWaitForReassignmentRequest - ): Future[SubmitAndWaitForReassignmentResponse] = - enrichRequestAndSubmit(request = request)(service.submitAndWaitForReassignment) - - override def bindService(): ServerServiceDefinition = - CommandServiceGrpc.bindService(this, executionContext) - - private def enrichRequestAndSubmit[T]( - request: SubmitAndWaitRequest - )(submit: SubmitAndWaitRequest => LoggingContextWithTrace => Future[T]): Future[T] = { - val traceContext = getAnnotatedCommandTraceContext(request.commands) - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(traceContext) - val requestWithSubmissionId = - request.update(_.optionalCommands.modify(generateSubmissionIdIfEmpty)) - validator - .validate( - requestWithSubmissionId, - currentLedgerTime(), - currentUtcTime(), - maxDeduplicationDuration, - )(errorLoggingContext(requestWithSubmissionId)) - .fold( - t => - Future.failed(ValidationLogger.logFailureWithTrace(logger, requestWithSubmissionId, t)), - _ => submit(requestWithSubmissionId)(loggingContext), - ) - } - - private def enrichRequestAndSubmit( - request: SubmitAndWaitForTransactionRequest - )( - submit: SubmitAndWaitForTransactionRequest => LoggingContextWithTrace => Future[ - SubmitAndWaitForTransactionResponse - ] - ): Future[SubmitAndWaitForTransactionResponse] = { - val traceContext = getAnnotatedCommandTraceContext(request.commands) - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(traceContext) - val requestWithSubmissionIdAndFormat = - request - .update(_.optionalCommands.modify(generateSubmissionIdIfEmpty)) - .update( - _.optionalTransactionFormat - .modify( - generateTransactionFormatIfEmpty( - request.commands.toList.flatMap(cmds => cmds.actAs ++ cmds.readAs) - ) - ) - ) - validator - .validate( - requestWithSubmissionIdAndFormat, - currentLedgerTime(), - currentUtcTime(), - maxDeduplicationDuration, - )(errorLoggingContext(requestWithSubmissionIdAndFormat)) - .fold( - t => - Future.failed( - ValidationLogger.logFailureWithTrace(logger, requestWithSubmissionIdAndFormat, t) - ), - _ => submit(requestWithSubmissionIdAndFormat)(loggingContext), - ) - } - - private def enrichRequestAndSubmit( - request: SubmitAndWaitForReassignmentRequest - )( - submit: SubmitAndWaitForReassignmentRequest => LoggingContextWithTrace => Future[ - SubmitAndWaitForReassignmentResponse - ] - ): Future[SubmitAndWaitForReassignmentResponse] = { - val traceContext = - getAnnotatedReassignmentCommandTraceContext(request.reassignmentCommands) - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(traceContext) - val requestWithSubmissionId = - request.update(_.optionalReassignmentCommands.modify(generateSubmissionIdIfEmptyReassignment)) - validator - .validate( - requestWithSubmissionId - )(errorLoggingContext(requestWithSubmissionId)) - .fold( - t => - Future.failed(ValidationLogger.logFailureWithTrace(logger, requestWithSubmissionId, t)), - _ => - requestWithSubmissionId.eventFormat match { - case Some(_) => - submit(requestWithSubmissionId)(loggingContext) - case None => - // request reassignment for all parties and remove the events - submit( - requestWithSubmissionId.copy(eventFormat = - Some( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(Filters(Nil)), - verbose = false, - ) - ) - ) - )( - loggingContext - ).map(_.update(_.reassignment.modify(_.clearEvents))) - }, - ) - } - - private def generateSubmissionIdIfEmpty(commands: Option[Commands]): Option[Commands] = - if (commands.exists(_.submissionId.isEmpty)) { - commands.map(_.copy(submissionId = generateSubmissionId.generate())) - } else { - commands - } - - private def generateSubmissionIdIfEmptyReassignment( - commands: Option[ReassignmentCommands] - ): Option[ReassignmentCommands] = - if (commands.exists(_.submissionId.isEmpty)) { - commands.map(_.copy(submissionId = generateSubmissionId.generate())) - } else { - commands - } - - private def errorLoggingContext(request: SubmitAndWaitRequest)(implicit - loggingContext: LoggingContextWithTrace - ) = - ErrorLoggingContext.fromOption(logger, loggingContext, request.commands.map(_.submissionId)) - - private def errorLoggingContext(request: SubmitAndWaitForTransactionRequest)(implicit - loggingContext: LoggingContextWithTrace - ) = - ErrorLoggingContext.fromOption(logger, loggingContext, request.commands.map(_.submissionId)) - - private def errorLoggingContext(request: SubmitAndWaitForReassignmentRequest)(implicit - loggingContext: LoggingContextWithTrace - ) = - ErrorLoggingContext.fromOption( - logger, - loggingContext, - request.reassignmentCommands.map(_.submissionId), - ) -} - -object ApiCommandService { - def generateTransactionFormatIfEmpty( - actAs: Seq[String] - )(transactionFormat: Option[TransactionFormat]): Option[TransactionFormat] = { - val wildcard = Filters( - cumulative = Seq( - CumulativeFilter( - IdentifierFilter.WildcardFilter( - WildcardFilter(false) - ) - ) - ) - ) - transactionFormat.orElse( - Some( - TransactionFormat( - eventFormat = - Some(EventFormat(actAs.map(party => party -> wildcard).toMap, None, verbose = true)), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandSubmissionService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandSubmissionService.scala deleted file mode 100644 index e26414eae3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandSubmissionService.scala +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.command_submission_service.{ - CommandSubmissionServiceGrpc, - SubmitReassignmentRequest, - SubmitReassignmentResponse, - SubmitRequest, - SubmitResponse, -} -import com.daml.ledger.api.v2.commands.Commands -import com.daml.metrics.Timed -import com.digitalasset.base.error.ErrorCode.LoggedApiException -import com.digitalasset.canton.ledger.api.services.CommandSubmissionService -import com.digitalasset.canton.ledger.api.validation.{CommandsValidator, SubmitRequestValidator} -import com.digitalasset.canton.ledger.api.{SubmissionIdGenerator, ValidationLogger} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.{ReassignmentCommand, SubmissionSyncService} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.TimerAndTrackOnShutdownSyntax -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcFUSExtended -import com.digitalasset.canton.platform.apiserver.execution.{ - CommandProgressTracker, - CommandResultHandle, -} -import com.digitalasset.canton.tracing.{TraceContext, Traced} -import com.digitalasset.canton.util.Thereafter.syntax.* - -import java.time.{Duration, Instant} -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -final class ApiCommandSubmissionService( - commandSubmissionService: CommandSubmissionService & AutoCloseable, - commandsValidator: CommandsValidator, - submissionSyncService: SubmissionSyncService, - currentLedgerTime: () => Instant, - currentUtcTime: () => Instant, - maxDeduplicationDuration: Duration, - submissionIdGenerator: SubmissionIdGenerator, - tracker: CommandProgressTracker, - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends CommandSubmissionServiceGrpc.CommandSubmissionService - with AutoCloseable - with NamedLogging { - - private val validator = new SubmitRequestValidator(commandsValidator) - - override def submit(request: SubmitRequest): Future[SubmitResponse] = { - implicit val traceContext = getAnnotatedCommandTraceContext(request.commands) - submitWithTraceContext(Traced(request)).asGrpcResponse - } - - def submitWithTraceContext( - request: Traced[SubmitRequest] - ): FutureUnlessShutdown[SubmitResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(request.traceContext) - val requestWithSubmissionId = generateSubmissionIdIfEmpty(request.value) - val errorLogger: ErrorLoggingContext = - ErrorLoggingContext.fromOption( - logger, - loggingContextWithTrace, - requestWithSubmissionId.commands.map(_.submissionId), - ) - val resultHandle = requestWithSubmissionId.commands - .map { - case allCommands @ Commands( - workflowId, - userId, - commandId, - commands, - deduplicationPeriod, - minLedgerTimeAbs, - minLedgerTimeRel, - actAs, - readAs, - submissionId, - disclosedContracts, - synchronizerId, - packageIdSelectionPreference, - prefetchKeys, - tapsMaxPasses, - ) => - tracker.registerCommand( - commandId, - Option.when(submissionId.nonEmpty)(submissionId), - userId, - commands, - actAs = allCommands.actAs.toSet, - )(loggingContextWithTrace.traceContext) - } - .getOrElse(CommandResultHandle.NoOp) - - val result = Timed.timedAndTrackedFutureUS( - metrics.commands.submissions, - metrics.commands.submissionsRunning, - Timed - .value( - metrics.commands.validation, - validator.validate( - req = requestWithSubmissionId, - currentLedgerTime = currentLedgerTime(), - currentUtcTime = currentUtcTime(), - maxDeduplicationDuration = maxDeduplicationDuration, - )(errorLogger), - ) - .fold( - t => - FutureUnlessShutdown - .failed(ValidationLogger.logFailureWithTrace(logger, requestWithSubmissionId, t)), - commandSubmissionService.submit(_).map(_ => SubmitResponse()), - ), - ) - resultHandle.extractFailure(result) - } - - override def submitReassignment( - request: SubmitReassignmentRequest - ): Future[SubmitReassignmentResponse] = { - implicit val traceContext: TraceContext = - getAnnotatedReassignmentCommandTraceContext(request.reassignmentCommands) - submitReassignmentWithTraceContext(Traced(request)).asGrpcResponse - } - - def submitReassignmentWithTraceContext( - request: Traced[SubmitReassignmentRequest] - ): FutureUnlessShutdown[SubmitReassignmentResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(request.traceContext) - val requestWithSubmissionId = generateSubmissionIdIfEmpty(request.value) - val errorLogger: ErrorLoggingContext = - ErrorLoggingContext.fromOption( - logger, - loggingContextWithTrace, - requestWithSubmissionId.reassignmentCommands.map(_.submissionId), - ) - Timed - .value( - metrics.commands.reassignmentValidation, - validator.validateReassignment(requestWithSubmissionId)(errorLogger), - ) - .fold( - t => - FutureUnlessShutdown - .failed(ValidationLogger.logFailureWithTrace(logger, requestWithSubmissionId, t)), - request => - FutureUnlessShutdown.outcomeF( - submissionSyncService - .submitReassignment( - submitter = request.submitter, - userId = request.userId, - commandId = request.commandId, - submissionId = Some(request.submissionId), - workflowId = request.workflowId, - reassignmentCommands = request.reassignmentCommands.map { - case Left(assignCommand) => - ReassignmentCommand.Assign( - sourceSynchronizer = assignCommand.sourceSynchronizerId, - targetSynchronizer = assignCommand.targetSynchronizerId, - reassignmentId = assignCommand.reassignmentId, - ) - case Right(unassignCommand) => - ReassignmentCommand.Unassign( - sourceSynchronizer = unassignCommand.sourceSynchronizerId, - targetSynchronizer = unassignCommand.targetSynchronizerId, - contractId = unassignCommand.contractId, - ) - }, - ) - .transform(handleSubmissionResult) - .thereafter(logger.logErrorsOnCall[SubmitReassignmentResponse]) - ), - ) - } - - private def generateSubmissionIdIfEmpty(request: SubmitRequest): SubmitRequest = - if (request.commands.exists(_.submissionId.isEmpty)) - request.update(_.commands.submissionId := submissionIdGenerator.generate()) - else - request - - private def generateSubmissionIdIfEmpty( - request: SubmitReassignmentRequest - ): SubmitReassignmentRequest = - if (request.reassignmentCommands.exists(_.submissionId.isEmpty)) - request.update(_.reassignmentCommands.submissionId := submissionIdGenerator.generate()) - else - request - - private def handleSubmissionResult(result: Try[state.SubmissionResult])(implicit - loggingContext: LoggingContextWithTrace - ): Try[SubmitReassignmentResponse] = { - import state.SubmissionResult.* - result match { - case Success(Acknowledged) => - logger.debug("Success") - Success(SubmitReassignmentResponse()) - - case Success(result: SynchronousError) => - logger.info(s"Rejected: ${result.description}") - Failure(result.exception) - - // Do not log again on errors that are logging on creation - case Failure(error: LoggedApiException) => Failure(error) - case Failure(error) => - logger.info(s"Rejected: ${error.getMessage}") - Failure(error) - } - } - - override def close(): Unit = commandSubmissionService.close() -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiContractService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiContractService.scala deleted file mode 100644 index 5276ec9eff..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiContractService.scala +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.contract_service.{ - ContractServiceGrpc, - GetContractRequest, - GetContractResponse, -} -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.LfPartyId -import com.digitalasset.canton.ledger.api.ValidationLogger -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.FieldValidator -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties.Projection -import com.digitalasset.canton.platform.store.dao.events.LfValueTranslation -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.value.Value.ContractId -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -final class ApiContractService( - ledgerApiContractStore: LedgerApiContractStore, - lfValueTranslation: LfValueTranslation, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends ContractServiceGrpc.ContractService - with GrpcApiService - with NamedLogging { - - override def getContract(request: GetContractRequest): Future[GetContractResponse] = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - (for { - contractId <- FieldValidator.requireContractId(request.contractId, "contract_id") - queryingParties <- FieldValidator.requireParties(request.queryingParties.toSet) - } yield { - ledgerApiContractStore - .lookupPersisted(contractId) - .flatMap(toApiResult(queryingParties, contractId)) - }).fold( - t => Future.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - identity, - ) - } - - private def toApiResult( - queryingParties: Set[LfPartyId], - contractId: ContractId, - )( - persistedContractInstanceO: Option[PersistedContractInstance] - )(implicit loggingContext: LoggingContextWithTrace): Future[GetContractResponse] = { - for { - contactInstance <- persistedContractInstanceO - witnesses <- NonEmpty.from( - contactInstance.inst.stakeholders.iterator - .filter(stakeholder => queryingParties.isEmpty || queryingParties(stakeholder)) - .map(_.toString) - .toSet - ) - } yield lfValueTranslation - .toApiCreatedEvent( - eventProjectionProperties = EventProjectionProperties( - verbose = false, - witnessTemplateProjections = Map( - // witness wildcard - None -> Map( - // template wildcard - None -> Projection(createdEventBlob = true) - ) - ), - )( - interfaceViewPackageUpgrade = - // using the original template implementation - no interface views are populated for this endpoint - this limitation is stated on the API - (_: Ref.ValueRef, originalTemplateImplementation: Ref.ValueRef) => - Future.successful(Right(originalTemplateImplementation)) - ), - fatContractInstance = contactInstance.inst, - // setting one so is not breaking validation only - this should be not used by the client - offset = 1, - // this should be not used by the client - nodeId = 0, - // this is only valid if it is the same as the contract's package ID - this limitation is stated on the API - representativePackageId = contactInstance.inst.templateId.packageId, - witnesses = witnesses, - // this cannot be determined so hardcoded - this limitation is state on the API - acsDelta = false, - ) - .map(Some(_)) - .map(GetContractResponse(_)) - }.getOrElse( - Future.failed( - RequestValidationErrors.NotFound.ContractPayload - .Reject(contractId) - .asGrpcError - ) - ) - - override def bindService(): ServerServiceDefinition = - ContractServiceGrpc.bindService(this, executionContext) - - override def close(): Unit = () -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiEventQueryService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiEventQueryService.scala deleted file mode 100644 index e742803de7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiEventQueryService.scala +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.event_query_service.{GetEventsByContractIdRequest, *} -import com.digitalasset.canton.ledger.api.ValidationLogger -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.EventQueryServiceRequestValidator -import com.digitalasset.canton.ledger.participant.state.index.IndexEventQueryService -import com.digitalasset.canton.logging.LoggingContextWithTrace.{ - implicitExtractTraceContext, - withEnrichedLoggingContext, -} -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.canton.util.Thereafter.syntax.* -import io.grpc.* - -import scala.concurrent.{ExecutionContext, Future} - -final class ApiEventQueryService( - eventQueryService: IndexEventQueryService, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext -) extends EventQueryServiceGrpc.EventQueryService - with GrpcApiService - with NamedLogging { - - override def getEventsByContractId( - req: GetEventsByContractIdRequest - ): Future[GetEventsByContractIdResponse] = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - EventQueryServiceRequestValidator - .validateEventsByContractId(req) - .fold( - t => Future.failed(ValidationLogger.logFailureWithTrace(logger, req, t)), - request => { - withEnrichedLoggingContext( - logging.contractId(request.contractId), - logging.eventFormat(request.eventFormat), - ) { implicit loggingContext => - logger.info("Received request for events by contract ID") - } - logger.trace(s"Events by contract ID request: $request") - eventQueryService - .getEventsByContractId( - request.contractId, - request.eventFormat, - ) - .thereafter(logger.logErrorsOnCall[GetEventsByContractIdResponse]) - }, - ) - } - - override def close(): Unit = () - - override def bindService(): ServerServiceDefinition = - EventQueryServiceGrpc.bindService(this, executionContext) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiInteractiveSubmissionService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiInteractiveSubmissionService.scala deleted file mode 100644 index b67b4ba954..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiInteractiveSubmissionService.scala +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.interactive.interactive_submission_service.InteractiveSubmissionServiceGrpc.InteractiveSubmissionService as InteractiveSubmissionServiceGrpc -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{ - ExecuteSubmissionAndWaitForTransactionRequest, - ExecuteSubmissionAndWaitForTransactionResponse, - ExecuteSubmissionAndWaitRequest, - ExecuteSubmissionAndWaitResponse, - ExecuteSubmissionRequest, - ExecuteSubmissionResponse, - GetPreferredPackageVersionRequest, - GetPreferredPackageVersionResponse, - GetPreferredPackagesRequest, - GetPreferredPackagesResponse, - PackagePreference, - PackageVettingRequirement, - PrepareSubmissionRequest as PrepareRequestP, - PrepareSubmissionResponse as PrepareResponseP, -} -import com.daml.ledger.api.v2.package_reference.PackageReference -import com.daml.metrics.Timed -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService.ExecuteRequest -import com.digitalasset.canton.ledger.api.validation.{ - CommandsValidator, - GetPreferredPackagesRequestValidator, - SubmitRequestValidator, -} -import com.digitalasset.canton.ledger.api.{SubmissionIdGenerator, ValidationLogger} -import com.digitalasset.canton.ledger.error.LedgerApiErrors.NoPreferredPackagesFound -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.TimerAndTrackOnShutdownSyntax -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcFUSExtended -import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker -import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc, Traced} -import com.digitalasset.canton.util.OptionUtil -import io.grpc.ServerServiceDefinition -import io.scalaland.chimney.auto.* -import io.scalaland.chimney.syntax.* - -import java.time.{Duration, Instant} -import scala.concurrent.{ExecutionContext, Future} - -class ApiInteractiveSubmissionService( - interactiveSubmissionService: InteractiveSubmissionService & AutoCloseable, - commandsValidator: CommandsValidator, - currentLedgerTime: () => Instant, - currentUtcTime: () => Instant, - maxDeduplicationDuration: Duration, - submissionIdGenerator: SubmissionIdGenerator, - tracker: CommandProgressTracker, - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends InteractiveSubmissionServiceGrpc - with GrpcApiService - with NamedLogging { - - private val validator = new SubmitRequestValidator(commandsValidator) - - override def prepareSubmission(request: PrepareRequestP): Future[PrepareResponseP] = { - implicit val traceContext = getPrepareRequestTraceContext( - request.userId, - request.commandId, - request.actAs, - ) - prepareWithTraceContext(Traced(request)).asGrpcResponse - } - - private def prepareWithTraceContext( - request: Traced[PrepareRequestP] - ): FutureUnlessShutdown[PrepareResponseP] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(request.traceContext) - implicit val errorLogger: ErrorLoggingContext = - ErrorLoggingContext.fromOption( - logger, - loggingContextWithTrace, - None, - ) - - Timed.timedAndTrackedFutureUS( - metrics.commands.interactivePrepares, - metrics.commands.preparesRunning, - Timed - .value( - metrics.commands.validation, - validator.validatePrepare( - req = request.value, - currentLedgerTime = currentLedgerTime(), - currentUtcTime = currentUtcTime(), - )(errorLogger), - ) - .fold( - t => - FutureUnlessShutdown.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - interactiveSubmissionService.prepare(_), - ), - ) - } - - override def executeSubmission( - request: ExecuteSubmissionRequest - ): Future[ExecuteSubmissionResponse] = { - val submitterInfo = request.preparedTransaction.flatMap(_.metadata.flatMap(_.submitterInfo)) - implicit val traceContext: TraceContext = getExecuteRequestTraceContext( - request.userId, - submitterInfo.map(_.commandId), - submitterInfo.map(_.actAs).toList.flatten, - ) - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory) - val errorLogger: ErrorLoggingContext = - ErrorLoggingContext.fromOption( - logger, - loggingContextWithTrace, - OptionUtil.emptyStringAsNone(request.submissionId), - ) - submitterInfo match { - case Some(value) => - val _ = tracker.registerCommand( - value.commandId, - OptionUtil.emptyStringAsNone(request.submissionId), - request.userId, - Seq.empty, // needs change in the API to support extracting root nodes here - actAs = value.actAs.toSet, - )(loggingContextWithTrace.traceContext) - case _ => - } - validator - .validateExecute( - request, - currentLedgerTime(), - submissionIdGenerator, - maxDeduplicationDuration, - )(errorLogger) - .fold( - t => FutureUnlessShutdown.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - interactiveSubmissionService.execute(_), - ) - .asGrpcResponse - } - - override def getPreferredPackageVersion( - request: GetPreferredPackageVersionRequest - ): Future[GetPreferredPackageVersionResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - implicit val traceContext: TraceContext = loggingContextWithTrace.traceContext - - getPreferredPackagesInternal( - GetPreferredPackagesRequest( - packageVettingRequirements = - Seq(PackageVettingRequirement(request.parties, request.packageName)), - synchronizerId = request.synchronizerId, - vettingValidAt = request.vettingValidAt, - ) - ).map { - case Right((Seq(packageReference), synchronizerId)) => - GetPreferredPackageVersionResponse( - packagePreference = Some(PackagePreference(Some(packageReference), synchronizerId)) - ) - case Right((unexpectedPackageReferences, _)) => - throw new RuntimeException( - s"Expected exactly one package reference but got: $unexpectedPackageReferences" - ) - case Left(failureReason) => - logger.debug(s"Could not compute the preferred package versions: $failureReason") - GetPreferredPackageVersionResponse(packagePreference = None) - } - } - - override def getPreferredPackages( - request: GetPreferredPackagesRequest - ): Future[GetPreferredPackagesResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - implicit val traceContext: TraceContext = loggingContextWithTrace.traceContext - - for { - preferencesEUS <- getPreferredPackagesInternal(request) - response <- preferencesEUS.fold( - preferenceSelectionFailed => - Future.failed(NoPreferredPackagesFound.Reject(preferenceSelectionFailed).asGrpcError), - { case (packageReferences, synchronizerId) => - Future.successful(GetPreferredPackagesResponse(packageReferences, synchronizerId)) - }, - ) - } yield response - } - - private def getPreferredPackagesInternal( - request: GetPreferredPackagesRequest - ): Future[Either[String, (Seq[PackageReference], String)]] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - implicit val traceContext: TraceContext = loggingContextWithTrace.traceContext - - GetPreferredPackagesRequestValidator - .validate(request) - .fold( - t => FutureUnlessShutdown.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - { case (packageVettingRequirements, synchronizerIdO, vettingValidAtO) => - interactiveSubmissionService.getPreferredPackages( - packageVettingRequirements = packageVettingRequirements, - synchronizerId = synchronizerIdO, - vettingValidAt = vettingValidAtO, - ) - }, - ) - .map(_.map { case (domainPackageReferences, synchronizerId) => - ( - domainPackageReferences.map(packageReference => - PackageReference( - packageId = packageReference.pkgId, - packageName = packageReference.packageName, - packageVersion = packageReference.version.toString(), - ) - ), - synchronizerId.logical.toProtoPrimitive, - ) - }) - .asGrpcResponse - } - - override def close(): Unit = {} - - override def bindService(): ServerServiceDefinition = - InteractiveSubmissionServiceGrpc.bindService(this, executionContext) - - private def executeAndWaitInternal[A]( - request: ExecuteSubmissionRequest, - execute: (ExecuteRequest, LoggingContextWithTrace) => FutureUnlessShutdown[A], - ) = { - val submitterInfo = request.preparedTransaction.flatMap(_.metadata.flatMap(_.submitterInfo)) - implicit val traceContext: TraceContext = getExecuteRequestTraceContext( - request.userId, - submitterInfo.map(_.commandId), - submitterInfo.map(_.actAs).toList.flatten, - ) - - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory) - - val errorLogger: ErrorLoggingContext = - ErrorLoggingContext.fromOption( - logger, - loggingContextWithTrace, - OptionUtil.emptyStringAsNone(request.submissionId), - ) - validator - .validateExecute( - request.transformInto[ExecuteSubmissionRequest], - currentLedgerTime(), - submissionIdGenerator, - maxDeduplicationDuration, - )(errorLogger) - .fold( - t => FutureUnlessShutdown.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - execute(_, loggingContextWithTrace), - ) - .asGrpcResponse - } - - override def executeSubmissionAndWait( - request: ExecuteSubmissionAndWaitRequest - ): Future[ExecuteSubmissionAndWaitResponse] = - executeAndWaitInternal( - // Convert the ExecuteSubmissionAndWaitRequest request into an ExecuteSubmissionRequest - // They are duplicated for better UX on the API but their fields are identical - request.transformInto[ExecuteSubmissionRequest], - (executeRequest, loggingContext) => - interactiveSubmissionService.executeAndWait(executeRequest)(loggingContext), - ) - - override def executeSubmissionAndWaitForTransaction( - request: ExecuteSubmissionAndWaitForTransactionRequest - ): Future[ExecuteSubmissionAndWaitForTransactionResponse] = - executeAndWaitInternal( - request.transformInto[ExecuteSubmissionRequest], - (executeRequest, loggingContext) => - interactiveSubmissionService.executeAndWaitForTransaction( - executeRequest, - ApiCommandService.generateTransactionFormatIfEmpty( - executeRequest.preparedTransaction.metadata.toList - .flatMap(_.submitterInfo) - .flatMap(_.actAs) - )(request.transactionFormat), - )(loggingContext), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiPackageService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiPackageService.scala deleted file mode 100644 index dd355bbf3b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiPackageService.scala +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.package_service.PackageServiceGrpc.PackageService -import com.daml.ledger.api.v2.package_service.{ - GetPackageRequest, - GetPackageResponse, - GetPackageStatusRequest, - GetPackageStatusResponse, - HashFunction as APIHashFunction, - ListPackagesRequest, - ListPackagesResponse, - ListVettedPackagesRequest, - ListVettedPackagesResponse, - PackageServiceGrpc, - PackageStatus, -} -import com.daml.logging.LoggingContext -import com.digitalasset.canton.ProtoDeserializationError.ProtoDeserializationFailure -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.ValidationErrors -import com.digitalasset.canton.ledger.api.{ - InitialPageToken, - ListVettedPackagesOpts, - PageToken, - ValidationLogger, -} -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.ledger.participant.state.PackageSyncService -import com.digitalasset.canton.logging.LoggingContextUtil.createLoggingContext -import com.digitalasset.canton.logging.LoggingContextWithTrace.{ - implicitExtractTraceContext, - withEnrichedLoggingContext, -} -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.platform.config.PackageServiceConfig -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.canton.util.EitherUtil.RichEither -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.daml.lf.archive.DamlLf.{Archive, HashFunction} -import com.digitalasset.daml.lf.data.Ref -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -private[apiserver] final class ApiPackageService( - packageSyncService: PackageSyncService, - packageServiceConfig: PackageServiceConfig, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends PackageService - with GrpcApiService - with NamedLogging { - - private implicit val loggingContext: LoggingContext = - createLoggingContext(loggerFactory)(identity) - - override def bindService(): ServerServiceDefinition = - PackageServiceGrpc.bindService(this, executionContext) - - override def close(): Unit = () - - override def listPackages(request: ListPackagesRequest): Future[ListPackagesResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - logger.info(s"Received request to list packages: $request") - packageSyncService - .listLfPackages() - .map(p => ListPackagesResponse(p.map(_.packageId))) - .thereafter(logger.logErrorsOnCall[ListPackagesResponse]) - } - - override def getPackage(request: GetPackageRequest): Future[GetPackageResponse] = - withEnrichedLoggingContext(TraceContextGrpc.fromGrpcContext)( - logging.packageId(request.packageId) - ) { implicit loggingContext => - logger.info(s"Received request for a package: $request") - withValidatedPackageId(request.packageId, request) { packageId => - packageSyncService - .getLfArchive(packageId) - .flatMap { - case None => - Future.failed[GetPackageResponse]( - RequestValidationErrors.NotFound.Package - .Reject(packageId = packageId)( - createerrorLoggingContext - ) - .asGrpcError - ) - case Some(archive) => Future.successful(toGetPackageResponse(archive)) - } - .thereafter(logger.logErrorsOnCall[GetPackageResponse]) - } - } - - override def getPackageStatus( - request: GetPackageStatusRequest - ): Future[GetPackageStatusResponse] = - LoggingContextWithTrace.withEnrichedLoggingContext(TraceContextGrpc.fromGrpcContext)( - logging.packageId(request.packageId) - ) { implicit loggingContext => - logger.info(s"Received request for a package status: $request") - withValidatedPackageId(request.packageId, request) { packageId => - Future { - val result = - if ( - packageSyncService.getPackageMetadataSnapshot.packageIdVersionMap.keySet.contains( - packageId - ) - ) { - PackageStatus.PACKAGE_STATUS_REGISTERED - } else { - PackageStatus.PACKAGE_STATUS_UNSPECIFIED - } - GetPackageStatusResponse(result) - } - .thereafter(logger.logErrorsOnCall[GetPackageStatusResponse]) - } - } - - override def listVettedPackages( - request: ListVettedPackagesRequest - ): Future[ListVettedPackagesResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - for { - opts <- ListVettedPackagesOpts - .fromProto(request, packageServiceConfig.maxVettedPackagesPageSize) - .toFuture(ProtoDeserializationFailure.Wrap(_).asGrpcError) - results <- packageSyncService.listVettedPackages(opts) - } yield ListVettedPackagesResponse( - vettedPackages = results.map(_.toProtoLAPI), - nextPageToken = - results.lastOption.map(_.toBoundedPageToken: PageToken).getOrElse(InitialPageToken).encode, - ) - } - - private def withValidatedPackageId[T, R](packageId: String, request: R)( - block: Ref.PackageId => Future[T] - )(implicit loggingContext: LoggingContextWithTrace): Future[T] = - Ref.PackageId - .fromString(packageId) - .fold( - errorMessage => - Future.failed[T]( - ValidationLogger.logFailureWithTrace( - logger, - request, - ValidationErrors - .invalidArgument(s"Invalid package id: $errorMessage")( - createerrorLoggingContext - ), - ) - ), - packageId => block(packageId), - ) - - private def toGetPackageResponse(archive: Archive): GetPackageResponse = { - val hashFunction = archive.getHashFunction match { - case HashFunction.SHA256 => APIHashFunction.HASH_FUNCTION_SHA256 - case _ => APIHashFunction.Unrecognized(-1) - } - GetPackageResponse( - hashFunction = hashFunction, - archivePayload = archive.getPayload, - hash = archive.getHash, - ) - } - - private def createerrorLoggingContext(implicit - loggingContext: LoggingContextWithTrace - ): ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContext) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiStateService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiStateService.scala deleted file mode 100644 index 92275d03a0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiStateService.scala +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import cats.syntax.traverse.* -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.daml.ledger.api.v2.state_service.* -import com.daml.logging.entries.LoggingEntries -import com.digitalasset.canton.LedgerParticipantId -import com.digitalasset.canton.ledger.api.ValidationLogger -import com.digitalasset.canton.ledger.api.grpc.{GrpcApiService, StreamingServiceLifecycleManagement} -import com.digitalasset.canton.ledger.api.messages.state.{ - AcsContinuationToken, - AcsPageToken, - AcsRangeInfo, -} -import com.digitalasset.canton.ledger.api.validation.ValueValidator.requirePresence -import com.digitalasset.canton.ledger.api.validation.{ - FieldValidator, - FormatValidator, - ParticipantOffsetValidator, -} -import com.digitalasset.canton.ledger.participant.state.SyncService -import com.digitalasset.canton.ledger.participant.state.index.{ - IndexActiveContractsService as ACSBackend, - IndexUpdateService, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.{ - implicitExtractTraceContext, - withEnrichedLoggingContext, -} -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.shutdownAsGrpcError -import com.digitalasset.canton.platform.config.StateServiceConfig -import com.digitalasset.canton.topology.transaction.ParticipantPermission as TopologyParticipantPermission -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.google.protobuf.ByteString -import io.grpc.ServerServiceDefinition -import io.grpc.stub.StreamObserver -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.{Sink, Source} - -import scala.concurrent.{ExecutionContext, Future} - -final class ApiStateService( - acsService: ACSBackend, - syncService: SyncService, - updateService: IndexUpdateService, - metrics: LedgerApiServerMetrics, - participantId: LedgerParticipantId, - config: StateServiceConfig, - val loggerFactory: NamedLoggerFactory, -)(implicit - mat: Materializer, - esf: ExecutionSequencerFactory, - executionContext: ExecutionContext, -) extends StateServiceGrpc.StateService - with StreamingServiceLifecycleManagement - with GrpcApiService - with NamedLogging { - - override def getActiveContracts( - request: GetActiveContractsRequest, - responseObserver: StreamObserver[GetActiveContractsResponse], - ): Unit = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - registerStream(responseObserver) { - - val result = for { - eventFormatProto <- requirePresence(request.eventFormat, "event_format") - eventFormat <- FormatValidator.validate(eventFormatProto) - checksum = AcsContinuationToken.calcChecksum(request, participantId) - continuationPointer <- request.streamContinuationToken.traverse( - AcsContinuationToken.decodeAndValidate(checksum, _) - ) - - activeAt <- ParticipantOffsetValidator.validateNonNegative( - request.activeAtOffset, - "active_at_offset", - ) - } yield { - withEnrichedLoggingContext( - logging.eventFormat(eventFormat) - ) { implicit loggingContext => - logger.info( - s"Received request for active contracts: $request, ${loggingContext.serializeFiltered("filters")}." - ) - acsService - .getActiveContracts( - eventFormat = eventFormat, - activeAt = activeAt, - rangeInfo = AcsRangeInfo( - continuationPointer = continuationPointer, - requestChecksum = checksum, - limit = None, - ), - ) - } - } - result - .fold( - t => - Source.failed( - ValidationLogger.logFailureWithTrace(logger, request, t) - ), - identity, - ) - .via( - logger.enrichedDebugStream("Responding with active contracts.", activeContractsLoggable) - ) - .via(logger.logErrorsOnStream) - .via(StreamMetrics.countElements(metrics.lapi.streams.acs)) - } - } - - override def getActiveContractsPage( - request: GetActiveContractsPageRequest - ): Future[GetActiveContractsPageResponse] = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - (for { - eventFormatProto <- requirePresence(request.eventFormat, "event_format") - eventFormat <- FormatValidator.validate(eventFormatProto) - maxPageSize <- FieldValidator.validatePageSize( - limit = config.maxAcsPageSize.unwrap, - defaultPageSize = config.defaultAcsPageSize.unwrap, - request.maxPageSize, - ) - requestChecksum = AcsPageToken.calcRequestChecksum(request) - participantChecksum = AcsPageToken.calcParticipantChecksum(participantId) - nextPageOpt <- request.pageToken.traverse( - AcsPageToken.decodeAndValidate(requestChecksum, participantChecksum, _) - ) - requestActiveAt <- ParticipantOffsetValidator.validateOptionalNonNegative( - request.activeAtOffset, - "active_at_offset", - ) - consolidatedActiveAt = nextPageOpt.map(_._1).orElse(requestActiveAt) - } yield { - val pointer = nextPageOpt.map(_._2) - for { - activeAtOffset <- consolidatedActiveAt match { - case Some(offset) => Future(Some(offset)) - case None => updateService.currentLedgerEnd() - } - activeContractsWithPointer <- withEnrichedLoggingContext( - logging.eventFormat(eventFormat), - logging.maxPageSize(maxPageSize), - logging.activeAtOffset(activeAtOffset), - ) { implicit loggingContext => - acsService - .getActiveContracts( - eventFormat, - activeAtOffset, - rangeInfo = AcsRangeInfo( - continuationPointer = pointer, - requestChecksum = AcsContinuationToken.emptyChecksum, - limit = Some(maxPageSize + 1L), - ), - ) - .runWith( - Sink.collection[GetActiveContractsResponse, Vector[GetActiveContractsResponse]] - ) - } - } yield { - val responses = activeContractsWithPointer.take(maxPageSize) - val moreItems = activeContractsWithPointer.sizeIs > maxPageSize - val continuationPointerForTheNextElem = - if (moreItems) { - // returning the pointer to the first element of the next page - activeContractsWithPointer.lastOption.map(_.streamContinuationToken) - } else { - None - } - val activeAt = activeAtOffset.fold(0L)(_.unwrap) - val nextPageToken = continuationPointerForTheNextElem.map(pointer => - AcsPageToken.encode(request, pointer, activeAt, participantId) - ) - - GetActiveContractsPageResponse( - activeContracts = responses.map(_.copy(streamContinuationToken = ByteString.empty())), - activeAtOffset = activeAt, - nextPageToken = nextPageToken, - ) - } - }) - .fold( - t => Future.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - identity, - ) - .thereafter(logger.logErrorsOnCall[GetActiveContractsPageResponse]) - } - - override def getConnectedSynchronizers( - request: GetConnectedSynchronizersRequest - ): Future[GetConnectedSynchronizersResponse] = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - val result = (for { - partyO <- FieldValidator - .optionalString(request.party)(FieldValidator.requirePartyField(_, "party")) - participantId <- FieldValidator - .optionalParticipantId(request.participantId, "participant_id") - } yield SyncService.ConnectedSynchronizerRequest(partyO, participantId)) - .fold( - t => FutureUnlessShutdown.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - request => - syncService - .getConnectedSynchronizers(request) - .map(response => - GetConnectedSynchronizersResponse( - response.connectedSynchronizers.map { connectedSynchronizer => - val permissions = connectedSynchronizer.permission match { - case Some(TopologyParticipantPermission.Submission) => - Some(ParticipantPermission.PARTICIPANT_PERMISSION_SUBMISSION) - case Some(TopologyParticipantPermission.Observation) => - Some(ParticipantPermission.PARTICIPANT_PERMISSION_OBSERVATION) - case Some(TopologyParticipantPermission.Confirmation) => - Some(ParticipantPermission.PARTICIPANT_PERMISSION_CONFIRMATION) - case _ => None - } - permissions - .map(permission => - GetConnectedSynchronizersResponse.ConnectedSynchronizer( - synchronizerAlias = - connectedSynchronizer.synchronizerAlias.toProtoPrimitive, - synchronizerId = - connectedSynchronizer.synchronizerId.logical.toProtoPrimitive, - permission = permission, - ) - ) - .getOrElse( - GetConnectedSynchronizersResponse.ConnectedSynchronizer( - synchronizerAlias = - connectedSynchronizer.synchronizerAlias.toProtoPrimitive, - synchronizerId = - connectedSynchronizer.synchronizerId.logical.toProtoPrimitive, - permission = ParticipantPermission.PARTICIPANT_PERMISSION_UNSPECIFIED, - ) - ) - } - ) - ), - ) - shutdownAsGrpcError(result) - } - - override def getLedgerEnd(request: GetLedgerEndRequest): Future[GetLedgerEndResponse] = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - updateService - .currentLedgerEnd() - .map(offset => - GetLedgerEndResponse( - offset.fold(0L)(_.unwrap) - ) - ) - .thereafter(logger.logErrorsOnCall[GetLedgerEndResponse]) - } - - override def getLatestPrunedOffsets( - request: GetLatestPrunedOffsetsRequest - ): Future[GetLatestPrunedOffsetsResponse] = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - updateService - .latestPrunedOffset() - .map { prunedUptoInclusive => - GetLatestPrunedOffsetsResponse( - participantPrunedUpToInclusive = prunedUptoInclusive.fold(0L)(_.unwrap), - allDivulgedContractsPrunedUpToInclusive = prunedUptoInclusive.fold(0L)(_.unwrap), - ) - } - .thereafter(logger.logErrorsOnCall[GetLatestPrunedOffsetsResponse]) - } - - override def bindService(): ServerServiceDefinition = - StateServiceGrpc.bindService(this, executionContext) - - private def activeContractsLoggable( - activeContractsResponse: GetActiveContractsResponse - ): LoggingEntries = - Option(activeContractsResponse.workflowId) - .filter(_.nonEmpty) - .map(workflowId => LoggingEntries(logging.workflowId(workflowId))) - .getOrElse(LoggingEntries()) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiTimeService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiTimeService.scala deleted file mode 100644 index ceec8b1403..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiTimeService.scala +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import cats.syntax.either.* -import com.daml.ledger.api.v2.testing.time_service.TimeServiceGrpc.TimeService -import com.daml.ledger.api.v2.testing.time_service.{ - GetTimeRequest, - GetTimeResponse, - SetTimeRequest, - TimeServiceGrpc, -} -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.util.TimestampConversion.* -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.invalidArgument -import com.digitalasset.canton.ledger.api.validation.ValueValidator.* -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.apiserver.TimeServiceBackend -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.google.protobuf.empty.Empty -import io.grpc.{ServerServiceDefinition, StatusRuntimeException} - -import java.time.Instant -import scala.concurrent.{ExecutionContext, Future} - -private[apiserver] final class ApiTimeService( - backend: TimeServiceBackend, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext -) extends TimeService - with GrpcApiService - with NamedLogging { - - def getTime(request: GetTimeRequest): Future[GetTimeResponse] = { - implicit val loggingContext = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - logger.info(s"Received request for time.") - Future.successful(GetTimeResponse(Some(fromInstant(backend.getCurrentTime)))) - } - - @SuppressWarnings(Array("org.wartremover.warts.JavaSerializable")) - override def setTime(request: SetTimeRequest): Future[Empty] = { - implicit val loggingContext = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - def updateTime( - expectedTime: Instant, - requestedTime: Instant, - ): Future[Either[StatusRuntimeException, Instant]] = { - logger.info(s"Setting time to $requestedTime") - backend - .setCurrentTime(expectedTime, requestedTime) - .map(success => - if (success) Right(requestedTime) - else - Left( - invalidArgument( - s"current_time mismatch. Provided: $expectedTime. Actual: ${backend.getCurrentTime}" - ) - ) - ) - } - - val validatedInput: Either[StatusRuntimeException, (Instant, Instant)] = for { - expectedTime <- requirePresence(request.currentTime, "current_time") - .map(toInstant) - requestedTime <- requirePresence(request.newTime, "new_time").map(toInstant) - _ <- - if (!requestedTime.isBefore(expectedTime)) - Either.unit - else - Left( - invalidArgument( - s"new_time [$requestedTime] is before current_time [$expectedTime]. Setting time backwards is not allowed." - ) - ) - } yield (expectedTime, requestedTime) - - val result = validatedInput match { - case Left(err) => Future.failed(err) - case Right((expectedTime, requestedTime)) => - updateTime(expectedTime, requestedTime).flatMap(resultET => - Future.fromTry(resultET.map(_ => Empty()).toTry) - ) - } - - result.thereafter(logger.logErrorsOnCall) - } - - override def bindService(): ServerServiceDefinition = - TimeServiceGrpc.bindService(this, executionContext) - - def getCurrentTime: Instant = backend.getCurrentTime - - override def close(): Unit = () -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiUpdateService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiUpdateService.scala deleted file mode 100644 index 20f2ddb878..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiUpdateService.scala +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import cats.data.OptionT -import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.daml.ledger.api.v2.update_service.* -import com.daml.logging.entries.LoggingEntries -import com.digitalasset.canton.ledger.api.grpc.StreamingServiceLifecycleManagement -import com.digitalasset.canton.ledger.api.validation.UpdateServiceRequestValidator -import com.digitalasset.canton.ledger.api.{UpdateFormat, ValidationLogger} -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.ledger.participant.state.index.IndexUpdateService -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.config.UpdateServiceConfig -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.daml.lf.data.Ref -import io.grpc.stub.StreamObserver -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.{ExecutionContext, Future} - -final class ApiUpdateService( - updateService: IndexUpdateService, - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, - participantId: Ref.ParticipantId, - updateServiceConfig: UpdateServiceConfig, -)(implicit - esf: ExecutionSequencerFactory, - executionContext: ExecutionContext, - mat: Materializer, -) extends UpdateServiceGrpc.UpdateService - with StreamingServiceLifecycleManagement - with NamedLogging { - - override def getUpdates( - request: GetUpdatesRequest, - responseObserver: StreamObserver[GetUpdatesResponse], - ): Unit = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - registerStream(responseObserver) { - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContextWithTrace) - - logger.debug(s"Received new update request $request.") - Source.future(updateService.currentLedgerEnd()).flatMapConcat { ledgerEnd => - val validation = UpdateServiceRequestValidator.validate(request, ledgerEnd) - - validation.fold( - t => Source.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - req => { - LoggingContextWithTrace.withEnrichedLoggingContext( - logging.startExclusive(req.startExclusive), - logging.endInclusive(req.endInclusive), - logging.updateFormat(req.updateFormat), - logging.descendingOrder(req.descendingOrder), - ) { implicit loggingContext => - logger.info( - s"Received request for updates, ${loggingContext - .serializeFiltered("startExclusive", "endInclusive", "updateFormat", "descendingOrder")}." - )(loggingContext.traceContext) - } - logger.trace(s"Update request: $req.") - updateService - .updates( - req.startExclusive, - req.endInclusive, - req.updateFormat, - req.descendingOrder, - skipPruningChecks = false, - ) - .via(logger.enrichedDebugStream("Responding with updates.", updatesLoggable)) - .via(logger.logErrorsOnStream) - .via(StreamMetrics.countElements(metrics.lapi.streams.updates)) - }, - ) - } - } - } - - override def getUpdateByOffset( - req: GetUpdateByOffsetRequest - ): Future[GetUpdateResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContextWithTrace) - - UpdateServiceRequestValidator - .validateUpdateByOffset(req) - .fold( - t => Future.failed(ValidationLogger.logFailureWithTrace(logger, req, t)), - request => { - implicit val enrichedLoggingContext: LoggingContextWithTrace = - LoggingContextWithTrace.enriched( - logging.offset(request.offset.unwrap), - logging.updateFormat(request.updateFormat), - )(loggingContextWithTrace) - logger.info(s"Received request for update by offset, ${enrichedLoggingContext - .serializeFiltered("offset", "updateFormat")}.")(loggingContextWithTrace.traceContext) - logger.trace(s"Update by offset request: $request")( - loggingContextWithTrace.traceContext - ) - val offset = request.offset - OptionT( - updateService.getUpdateBy(LookupKey.ByOffset(offset), request.updateFormat)( - loggingContextWithTrace - ) - ) - .getOrElseF( - Future.failed( - RequestValidationErrors.NotFound.Update.RejectWithOffset(offset.unwrap).asGrpcError - ) - ) - .thereafter( - logger.logErrorsOnCall[GetUpdateResponse](loggingContextWithTrace.traceContext) - ) - }, - ) - } - - override def getUpdateById( - req: GetUpdateByIdRequest - ): Future[GetUpdateResponse] = { - val loggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - val errorLoggingContext = ErrorLoggingContext(logger, loggingContextWithTrace) - - UpdateServiceRequestValidator - .validateUpdateById(req)(errorLoggingContext) - .fold( - t => - Future - .failed(ValidationLogger.logFailureWithTrace(logger, req, t)(loggingContextWithTrace)), - request => { - implicit val enrichedLoggingContext: LoggingContextWithTrace = - LoggingContextWithTrace.enriched( - logging.updateId(request.updateId), - logging.updateFormat(request.updateFormat), - )(loggingContextWithTrace) - logger.info( - s"Received request for update by ID, ${enrichedLoggingContext - .serializeFiltered("eventId", "updateFormat")}." - )(loggingContextWithTrace.traceContext) - logger.trace(s"Update by ID request: $request")(loggingContextWithTrace.traceContext) - - internalGetUpdateById(request.updateId, request.updateFormat) - .thereafter( - logger.logErrorsOnCall[GetUpdateResponse](loggingContextWithTrace.traceContext) - ) - }, - ) - } - - override def getUpdatesPage(request: GetUpdatesPageRequest): Future[GetUpdatesPageResponse] = { - val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContextWithTrace) - logger.debug(s"Received new update request $request.")(loggingContextWithTrace.traceContext) - for { - ledgerEnd <- updateService.currentLedgerEnd() - validation = UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = ledgerEnd, - participantId = participantId, - updateServiceConfig = updateServiceConfig, - ) - res <- validation.fold( - t => - Future.failed( - ValidationLogger.logFailureWithTrace(logger, request, t)( - loggingContextWithTrace - ) - ), - request => { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace.enriched( - logging.startExclusiveOpt(request.startExclusive), - logging.endInclusive(request.endInclusive), - logging.maxPageSize(request.maxPageSize), - logging.updateFormat(request.updateFormat), - logging.descendingOrder(request.descendingOrder), - logging.continueStreamFromIncl(request.continueStreamFromIncl), - )(loggingContextWithTrace) - internalGetUpdatesPage(request) - }, - ) - } yield res - } - private def internalGetUpdatesPage( - request: com.digitalasset.canton.ledger.api.messages.update.GetUpdatesPageRequest - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[GetUpdatesPageResponse] = - updateService.updatesPage(request) - - private def internalGetUpdateById( - updateId: UpdateId, - updateFormat: UpdateFormat, - )(implicit - loggingContextWithTrace: LoggingContextWithTrace - ): Future[GetUpdateResponse] = - OptionT(updateService.getUpdateBy(LookupKey.ByUpdateId(updateId), updateFormat)) - .getOrElseF( - Future.failed( - RequestValidationErrors.NotFound.Update - .RejectWithTxId(updateId.toHexString) - .asGrpcError - ) - ) - - private def updatesLoggable(updates: GetUpdatesResponse): LoggingEntries = - updates.update match { - case GetUpdatesResponse.Update.Transaction(t) => - entityLoggable(t.commandId, t.updateId, t.workflowId, t.offset) - case GetUpdatesResponse.Update.Reassignment(r) => - entityLoggable(r.commandId, r.updateId, r.workflowId, r.offset) - case GetUpdatesResponse.Update.OffsetCheckpoint(c) => - LoggingEntries(logging.offset(c.offset)) - case GetUpdatesResponse.Update.TopologyTransaction(tt) => - LoggingEntries(logging.offset(tt.offset)) - case GetUpdatesResponse.Update.Empty => - LoggingEntries() - } - - private def entityLoggable( - commandId: String, - updateId: String, - workflowId: String, - offset: Long, - ): LoggingEntries = - LoggingEntries( - logging.commandId(commandId), - logging.updateId(updateId), - logging.workflowId(workflowId), - logging.offset(offset), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiVersionService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiVersionService.scala deleted file mode 100644 index e7d594077c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/ApiVersionService.scala +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.experimental_features.* -import com.daml.ledger.api.v2.version_service.VersionServiceGrpc.VersionService -import com.daml.ledger.api.v2.version_service.{ - FeaturesDescriptor, - GetLedgerApiVersionResponse, - UserManagementFeature, - *, -} -import com.digitalasset.canton.buildinfo.BuildInfo -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.apiserver.LedgerFeatures -import com.digitalasset.canton.platform.config.{ - PackageServiceConfig, - PartyManagementServiceConfig, - UserManagementServiceConfig, -} -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -private[apiserver] final class ApiVersionService( - ledgerFeatures: LedgerFeatures, - userManagementServiceConfig: UserManagementServiceConfig, - partyManagementServiceConfig: PartyManagementServiceConfig, - packageServiceConfig: PackageServiceConfig, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext -) extends VersionService - with GrpcApiService - with NamedLogging { - - private val apiVersion: String = BuildInfo.version - - private val featuresDescriptor = - FeaturesDescriptor.of( - userManagement = Some( - if (userManagementServiceConfig.enabled) { - UserManagementFeature( - supported = true, - maxRightsPerUser = userManagementServiceConfig.maxRightsPerUser, - maxUsersPageSize = userManagementServiceConfig.maxUsersPageSize, - ) - } else { - UserManagementFeature( - supported = false, - maxRightsPerUser = 0, - maxUsersPageSize = 0, - ) - } - ), - partyManagement = Some( - PartyManagementFeature( - maxPartiesPageSize = partyManagementServiceConfig.maxPartiesPageSize.value - ) - ), - experimental = Some( - ExperimentalFeatures.of( - staticTime = Some(ExperimentalStaticTime(supported = ledgerFeatures.staticTime)), - commandInspectionService = Some(ledgerFeatures.commandInspectionService), - ) - ), - offsetCheckpoint = Some(ledgerFeatures.offsetCheckpointFeature), - packageFeature = Some( - PackageFeature.of( - maxVettedPackagesPageSize = packageServiceConfig.maxVettedPackagesPageSize.value - ) - ), - ) - - override def getLedgerApiVersion( - request: GetLedgerApiVersionRequest - ): Future[GetLedgerApiVersionResponse] = - Future.successful(apiVersionResponse(apiVersion)) - private def apiVersionResponse(version: String) = - GetLedgerApiVersionResponse(version, Some(featuresDescriptor)) - - override def bindService(): ServerServiceDefinition = - VersionServiceGrpc.bindService(this, executionContext) - - override def close(): Unit = () - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/RejectionGenerators.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/RejectionGenerators.scala deleted file mode 100644 index 046dafc99f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/RejectionGenerators.scala +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.digitalasset.base.error.{BaseError, DamlErrorWithDefiniteAnswer, RpcError} -import com.digitalasset.canton.ledger.error.LedgerApiErrors -import com.digitalasset.canton.ledger.error.groups.{ - CommandExecutionErrors, - ConsistencyErrors, - RequestValidationErrors, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{ErrorLoggingContext, NoLogging} -import com.digitalasset.canton.protocol.LfContractId -import com.digitalasset.canton.time.NonNegativeFiniteDuration -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.{Ref, Time} -import com.digitalasset.daml.lf.engine.Error as LfError -import com.digitalasset.daml.lf.engine.Error.{Interpretation, Package, Preprocessing, Validation} -import com.digitalasset.daml.lf.interpretation.Error as LfInterpretationError - -sealed abstract class ErrorCause extends Product with Serializable - -object ErrorCause { - final case class DamlLf(error: LfError) extends ErrorCause - final case class LedgerTime(retries: Int) extends ErrorCause - sealed abstract class DisclosedContractsSynchronizerIdMismatch extends ErrorCause - final case class DisclosedContractsSynchronizerIdsMismatch( - mismatchingDisclosedContractSynchronizerIds: Map[LfContractId, SynchronizerId] - ) extends DisclosedContractsSynchronizerIdMismatch - final case class PrescribedSynchronizerIdMismatch( - disclosedContractIds: Set[LfContractId], - synchronizerIdOfDisclosedContracts: SynchronizerId, - commandsSynchronizerId: SynchronizerId, - ) extends DisclosedContractsSynchronizerIdMismatch - - final case class InterpretationTimeExceeded( - ledgerEffectiveTime: Time.Timestamp, // the Ledger Effective Time of the submitted command - tolerance: NonNegativeFiniteDuration, - transactionTrace: Option[String], - ) extends ErrorCause - - final case class RoutingFailed(err: BaseError) extends ErrorCause -} - -object RejectionGenerators { - def commandExecutorErrorFUS[R]( - error: ErrorCause - )(implicit - errorLoggingContext: ErrorLoggingContext - ): FutureUnlessShutdown[R] = - FutureUnlessShutdown.failed(commandExecutorError(error).asGrpcError) - - def commandExecutorError(cause: ErrorCause)(implicit - errorLoggingContext: ErrorLoggingContext - ): RpcError = { - - def processPackageError(err: LfError.Package.Error): RpcError = err match { - case e: Package.Internal => LedgerApiErrors.InternalError.PackageInternal(e) - case Package.Validation(validationError) => - CommandExecutionErrors.Package.PackageValidationFailed - .Reject(validationError.pretty) - case Package.MissingPackage(packageRef, context) => - RequestValidationErrors.NotFound.Package - .InterpretationReject(packageRef, context) - case Package.AllowedLanguageVersion(packageId, languageVersion, allowedLanguageVersions) => - CommandExecutionErrors.Package.AllowedLanguageVersions.Error( - packageId, - languageVersion, - allowedLanguageVersions, - ) - case e: Package.DarSelfConsistency => - LedgerApiErrors.InternalError.PackageSelfConsistency(e) - } - - def processPreprocessingError(err: LfError.Preprocessing.Error): RpcError = err match { - case e: Preprocessing.Internal => LedgerApiErrors.InternalError.Preprocessing(e) - case Preprocessing.UnresolvedPackageName(pkgName, context) => - RequestValidationErrors.NotFound.Package - .InterpretationReject(Ref.PackageRef.Name(pkgName), context) - case e => CommandExecutionErrors.Preprocessing.PreprocessingFailed.Reject(e) - } - - def processValidationError(err: LfError.Validation.Error): RpcError = err match { - // we shouldn't see such errors during submission - case e: Validation.ReplayMismatch => LedgerApiErrors.InternalError.Validation(e) - } - - def processDamlException( - err: com.digitalasset.daml.lf.interpretation.Error, - renderedMessage: String, - transactionTrace: Option[String], - ): RpcError = - // detailMessage is only suitable for server side debugging but not for the user, so don't pass except on internal errors - - err match { - case LfInterpretationError.ContractNotFound(cid) => - ConsistencyErrors.ContractNotFound - .Reject(renderedMessage, cid) - case LfInterpretationError.ContractKeyNotFound(key) => - CommandExecutionErrors.Interpreter.LookupErrors.ContractKeyNotFound - .Reject(renderedMessage, key) - case _: LfInterpretationError.FailedAuthorization => - CommandExecutionErrors.Interpreter.AuthorizationError - .Reject(renderedMessage) - case e: LfInterpretationError.EffectfulRollback => - CommandExecutionErrors.Interpreter.EffectfulRollback - .Reject(renderedMessage) - case LfInterpretationError.UnresolvedPackageName(packageName) => - CommandExecutionErrors.Interpreter.LookupErrors.UnresolvedPackageName - .Reject(renderedMessage, packageName) - case e: LfInterpretationError.ContractNotActive => - CommandExecutionErrors.Interpreter.ContractNotActive - .Reject(renderedMessage, e) - case e: LfInterpretationError.ContractHashingError => - CommandExecutionErrors.Interpreter.ContractHashingError - .Reject(renderedMessage, e) - case e: LfInterpretationError.DisclosedContractKeyHashingError => - CommandExecutionErrors.Interpreter.DisclosedContractKeyHashingError - .Reject(renderedMessage, e) - case LfInterpretationError.DuplicateContractKey(key) => - ConsistencyErrors.DuplicateContractKey - .RejectWithContractKeyArg(renderedMessage, key) - case LfInterpretationError.InconsistentContractKey(key) => - ConsistencyErrors.InconsistentContractKey - .RejectWithContractKeyArg(renderedMessage, key) - case e: LfInterpretationError.UnhandledException => - CommandExecutionErrors.Interpreter.UnhandledException.Reject( - renderedMessage + transactionTrace.fold("")("\n" + _) + ".", - e, - ) - case e: LfInterpretationError.UserError => - CommandExecutionErrors.Interpreter.InterpretationUserError - .Reject(renderedMessage, e) - case _: LfInterpretationError.TemplatePreconditionViolated => - CommandExecutionErrors.Interpreter.TemplatePreconditionViolated - .Reject(renderedMessage) - case e: LfInterpretationError.CreateEmptyContractKeyMaintainers => - CommandExecutionErrors.Interpreter.CreateEmptyContractKeyMaintainers - .Reject(renderedMessage, e) - case e: LfInterpretationError.FetchEmptyContractKeyMaintainers => - CommandExecutionErrors.Interpreter.FetchEmptyContractKeyMaintainers - .Reject(renderedMessage, e) - case e: LfInterpretationError.WronglyTypedContract => - CommandExecutionErrors.Interpreter.WronglyTypedContract - .Reject(renderedMessage, e) - case e: LfInterpretationError.ContractDoesNotImplementInterface => - CommandExecutionErrors.Interpreter.ContractDoesNotImplementInterface - .Reject(renderedMessage, e) - case e: LfInterpretationError.ContractDoesNotImplementRequiringInterface => - CommandExecutionErrors.Interpreter.ContractDoesNotImplementRequiringInterface - .Reject(renderedMessage, e) - case LfInterpretationError.NonComparableValues => - CommandExecutionErrors.Interpreter.NonComparableValues - .Reject(renderedMessage) - case _: LfInterpretationError.ContractIdInContractKey => - CommandExecutionErrors.Interpreter.ContractIdInContractKey - .Reject(renderedMessage) - case e: LfInterpretationError.ContractIdComparability => - CommandExecutionErrors.Interpreter.ContractIdComparability - .Reject(renderedMessage, e) - case e: LfInterpretationError.ValueNesting => - CommandExecutionErrors.Interpreter.ValueNesting - .Reject(renderedMessage, e) - case e: LfInterpretationError.MalformedText => - CommandExecutionErrors.Interpreter.MalformedText - .Reject(renderedMessage, e) - case e: LfInterpretationError.FailureStatus => - CommandExecutionErrors.Interpreter.FailureStatus - .Reject(renderedMessage, e, transactionTrace) - case LfInterpretationError.Upgrade(error: LfInterpretationError.Upgrade.ValidationFailed) => - CommandExecutionErrors.Interpreter.UpgradeError.ValidationFailed - .Reject(renderedMessage, error) - case LfInterpretationError.Upgrade( - error: LfInterpretationError.Upgrade.TranslationFailed - ) => - CommandExecutionErrors.Interpreter.UpgradeError.TranslationFailed - .Reject(renderedMessage, error) - case LfInterpretationError.Upgrade( - error: LfInterpretationError.Upgrade.AuthenticationFailed - ) => - CommandExecutionErrors.Interpreter.UpgradeError.AuthenticationFailed - .Reject(renderedMessage, error) - case LfInterpretationError.Crypto( - error: LfInterpretationError.Crypto.MalformedByteEncoding - ) => - CommandExecutionErrors.Interpreter.CryptoError.MalformedByteEncoding - .Reject(renderedMessage, error) - case LfInterpretationError.Crypto( - error: LfInterpretationError.Crypto.MalformedKey - ) => - CommandExecutionErrors.Interpreter.CryptoError.MalformedKey - .Reject(renderedMessage, error) - case LfInterpretationError.Crypto( - error: LfInterpretationError.Crypto.MalformedSignature - ) => - CommandExecutionErrors.Interpreter.CryptoError.MalformedSignature - .Reject(renderedMessage, error) - case LfInterpretationError.Dev(_, err) => - CommandExecutionErrors.Interpreter.InterpretationDevError - .Reject(renderedMessage, err) - } - - def processInterpretationError( - err: LfError.Interpretation.Error, - detailMessage: Option[String], - ): RpcError = - err match { - case Interpretation.Internal(location, message, _) => - LedgerApiErrors.InternalError.Interpretation(location, message, detailMessage) - case m @ Interpretation.DamlException(error) => - processDamlException(error, m.message, detailMessage) - } - - def processLfError(error: LfError) = { - val transformed = error match { - case LfError.Package(packageError) => processPackageError(packageError) - case LfError.Preprocessing(processingError) => processPreprocessingError(processingError) - case LfError.Interpretation(interpretationError, detailMessage) => - processInterpretationError(interpretationError, detailMessage) - case LfError.Validation(validationError) => processValidationError(validationError) - case e - if e.message.contains( - "requires authorizers" - ) => // Keeping this around as a string match as daml is not yet generating LfError.InterpreterErrors.Validation - CommandExecutionErrors.Interpreter.AuthorizationError.Reject(e.message) - } - transformed - } - - cause match { - case ErrorCause.DamlLf(error) => processLfError(error) - case ErrorCause.LedgerTime(retries) => - CommandExecutionErrors.FailedToDetermineLedgerTime - .Reject(s"Could not find a suitable ledger time after $retries retries") - case ErrorCause.InterpretationTimeExceeded(let, tolerance, transactionTrace) => - CommandExecutionErrors.TimeExceeded.Reject( - s"Time exceeds limit of Ledger Effective Time ($let) + tolerance ($tolerance). Interpretation aborted" + transactionTrace - .fold("")("\n" + _) + "." - ) - case ErrorCause.DisclosedContractsSynchronizerIdsMismatch( - mismatchingDisclosedContractSynchronizerIds - ) => - CommandExecutionErrors.DisclosedContractsSynchronizerIdMismatch.Reject( - mismatchingDisclosedContractSynchronizerIds.view.mapValues(_.toProtoPrimitive).toMap - ) - case ErrorCause.PrescribedSynchronizerIdMismatch( - disclosedContractsWithSynchronizerId, - synchronizerIdOfDisclosedContracts, - commandsSynchronizerId, - ) => - CommandExecutionErrors.PrescribedSynchronizerIdMismatch.Reject( - disclosedContractsWithSynchronizerId, - synchronizerIdOfDisclosedContracts.toProtoPrimitive, - commandsSynchronizerId.toProtoPrimitive, - ) - case ErrorCause.RoutingFailed(baseError) => - // TODO(#25385) Streamline ErrorCause usage - // TODO(#25385) This is logged again on this creation - new DamlErrorWithDefiniteAnswer(baseError.cause, baseError.throwableO)( - baseError.code, - NoLogging, - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/StreamMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/StreamMetrics.scala deleted file mode 100644 index c92b66f71e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/StreamMetrics.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.metrics.api.MetricHandle.Counter -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Flow - -object StreamMetrics { - def countElements[Out](counter: Counter): Flow[Out, Out, NotUsed] = - Flow[Out].map { item => - counter.inc() - item - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiCommandInspectionService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiCommandInspectionService.scala deleted file mode 100644 index 4c73901bed..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiCommandInspectionService.scala +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.daml.ledger.api.v2.admin.command_inspection_service.* -import com.digitalasset.canton.ledger.api.ValidationLogger -import com.digitalasset.canton.ledger.api.grpc.StreamingServiceLifecycleManagement -import com.digitalasset.canton.ledger.api.services.CommandInspectionService -import com.digitalasset.canton.ledger.api.validation.CommandInspectionServiceRequestValidator -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} - -import scala.concurrent.{ExecutionContext, Future} - -class ApiCommandInspectionService( - service: CommandInspectionService, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext -) extends CommandInspectionServiceGrpc.CommandInspectionService - with StreamingServiceLifecycleManagement - with NamedLogging { - - protected implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext( - logger, - loggerFactory.properties, - TraceContext.empty, - ) - - override def getCommandStatus( - request: GetCommandStatusRequest - ): Future[GetCommandStatusResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - logger.info(s"Received new command status request $request.") - CommandInspectionServiceRequestValidator - .validateCommandStatusRequest(request)( - ErrorLoggingContext( - logger, - loggingContextWithTrace.toPropertiesMap, - loggingContextWithTrace.traceContext, - ) - ) - .fold( - t => - Future.failed[GetCommandStatusResponse]( - ValidationLogger.logFailureWithTrace(logger, request, t) - ), - _ => - service - .findCommandStatus(request.commandIdPrefix, request.state, request.limit) - .map(statuses => GetCommandStatusResponse(commandStatus = statuses.map(_.toProto))), - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiIdentityProviderConfigService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiIdentityProviderConfigService.scala deleted file mode 100644 index 2dc904227a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiIdentityProviderConfigService.scala +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.daml.ledger.api.v2.admin.identity_provider_config_service as proto -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.ledger.error.groups.IdentityProviderConfigServiceErrors -import com.digitalasset.canton.ledger.localstore.api.{ - IdentityProviderConfigStore, - IdentityProviderConfigUpdate, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.apiserver.services.admin.ApiIdentityProviderConfigService.toProto -import com.digitalasset.canton.platform.apiserver.update -import com.digitalasset.canton.platform.apiserver.update.IdentityProviderConfigUpdateMapper -import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} -import io.grpc.{ServerServiceDefinition, StatusRuntimeException} - -import scala.concurrent.{ExecutionContext, Future} - -class ApiIdentityProviderConfigService( - identityProviderConfigStore: IdentityProviderConfigStore, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext -) extends proto.IdentityProviderConfigServiceGrpc.IdentityProviderConfigService - with GrpcApiService - with NamedLogging { - - import com.digitalasset.canton.ledger.api.validation.ValueValidator.* - import com.digitalasset.canton.ledger.api.validation.FieldValidator.* - - private def withValidation[A, B](validatedResult: Either[StatusRuntimeException, A])( - f: A => Future[B] - ): Future[B] = - validatedResult.fold(Future.failed, Future.successful).flatMap(f) - - override def createIdentityProviderConfig( - request: proto.CreateIdentityProviderConfigRequest - ): Future[proto.CreateIdentityProviderConfigResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - logger.info("Creating identity provider config.") - withValidation { - for { - config <- requirePresence(request.identityProviderConfig, "identity_provider_config") - identityProviderId <- requireIdentityProviderId( - config.identityProviderId, - "identity_provider_id", - ) - jwksUrl <- requireJwksUrl(config.jwksUrl, "jwks_url") - issuer <- requireNonEmptyString(config.issuer, "issuer") - audience <- optionalString(config.audience)(Right(_)) - } yield IdentityProviderConfig( - identityProviderId, - config.isDeactivated, - jwksUrl, - issuer, - audience, - ) - } { config => - identityProviderConfigStore - .createIdentityProviderConfig(config) - .flatMap(handleResult("creating identity provider config")) - .map(config => proto.CreateIdentityProviderConfigResponse(Some(toProto(config)))) - } - } - - override def getIdentityProviderConfig( - request: proto.GetIdentityProviderConfigRequest - ): Future[proto.GetIdentityProviderConfigResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - withValidation( - requireIdentityProviderId(request.identityProviderId, "identity_provider_id") - )(identityProviderId => - identityProviderConfigStore - .getIdentityProviderConfig(identityProviderId) - .flatMap(handleResult("getting identity provider config")) - .map(cfg => proto.GetIdentityProviderConfigResponse(Some(toProto(cfg)))) - ) - } - override def updateIdentityProviderConfig( - request: proto.UpdateIdentityProviderConfigRequest - ): Future[proto.UpdateIdentityProviderConfigResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - withValidation { - for { - config <- requirePresence(request.identityProviderConfig, "identity_provider_config") - identityProviderId <- requireIdentityProviderId( - config.identityProviderId, - "identity_provider_id", - ) - jwksUrl <- optionalString(config.jwksUrl)(requireJwksUrl(_, "jwks_url")) - issuer <- optionalString(config.issuer)(requireNonEmptyString(_, "issuer")) - updateMask <- requirePresence( - request.updateMask, - "update_mask", - ) - audience <- optionalString(config.audience)(Right(_)) - } yield ( - IdentityProviderConfigUpdate( - identityProviderId, - Some(config.isDeactivated), - jwksUrl, - issuer, - Some(audience), - ), - updateMask, - ) - } { case (identityProviderConfig, updateMask) => - for { - identityProviderConfigUpdate: IdentityProviderConfigUpdate <- handleUpdatePathResult( - identityProviderId = identityProviderConfig.identityProviderId, - IdentityProviderConfigUpdateMapper.toUpdate( - apiObject = identityProviderConfig, - updateMask = updateMask, - ), - ) - updateResult <- identityProviderConfigStore.updateIdentityProviderConfig( - identityProviderConfigUpdate - ) - updatedIdentityProviderConfig <- handleResult("updating identity provider config")( - updateResult - ) - } yield proto.UpdateIdentityProviderConfigResponse( - Some(toProto(updatedIdentityProviderConfig)) - ) - } - } - - override def listIdentityProviderConfigs( - request: proto.ListIdentityProviderConfigsRequest - ): Future[proto.ListIdentityProviderConfigsResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - identityProviderConfigStore - .listIdentityProviderConfigs() - .flatMap(handleResult("listing identity provider configs")) - .map(result => proto.ListIdentityProviderConfigsResponse(result.map(toProto))) - } - override def deleteIdentityProviderConfig( - request: proto.DeleteIdentityProviderConfigRequest - ): Future[proto.DeleteIdentityProviderConfigResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - withValidation( - requireIdentityProviderId(request.identityProviderId, "identity_provider_id") - )(identityProviderId => - identityProviderConfigStore - .deleteIdentityProviderConfig(identityProviderId) - .flatMap(handleResult("deleting identity provider config")) - .map { _ => - proto.DeleteIdentityProviderConfigResponse() - } - ) - } - - private def handleResult[T](operation: String)( - result: IdentityProviderConfigStore.Result[T] - )(implicit traceContext: TraceContext): Future[T] = result match { - case Left(IdentityProviderConfigStore.IdentityProviderConfigNotFound(id)) => - Future.failed( - IdentityProviderConfigServiceErrors.IdentityProviderConfigNotFound - .Reject(operation, id.value) - .asGrpcError - ) - case Left(IdentityProviderConfigStore.IdentityProviderConfigExists(id)) => - Future.failed( - IdentityProviderConfigServiceErrors.IdentityProviderConfigAlreadyExists - .Reject(operation, id.value) - .asGrpcError - ) - case Left(IdentityProviderConfigStore.IdentityProviderConfigWithIssuerExists(issuer)) => - Future.failed( - IdentityProviderConfigServiceErrors.IdentityProviderConfigIssuerAlreadyExists - .Reject(operation, issuer) - .asGrpcError - ) - case Left(IdentityProviderConfigStore.TooManyIdentityProviderConfigs()) => - Future.failed( - IdentityProviderConfigServiceErrors.TooManyIdentityProviderConfigs - .Reject(operation) - .asGrpcError - ) - case Left(IdentityProviderConfigStore.IdentityProviderConfigByIssuerNotFound(issuer)) => - Future.failed( - IdentityProviderConfigServiceErrors.IdentityProviderConfigByIssuerNotFound - .Reject(operation, issuer) - .asGrpcError - ) - case scala.util.Right(t) => - Future.successful(t) - } - - private def handleUpdatePathResult[T]( - identityProviderId: IdentityProviderId.Id, - result: update.Result[T], - )(implicit traceContext: TraceContext): Future[T] = - result match { - case Left(e: update.UpdatePathError) => - Future.failed( - IdentityProviderConfigServiceErrors.InvalidUpdateIdentityProviderConfigRequest - .Reject(identityProviderId.value, reason = e.getReason) - .asGrpcError - ) - case Right(t) => - Future.successful(t) - } - - override def close(): Unit = () - - override def bindService(): ServerServiceDefinition = - proto.IdentityProviderConfigServiceGrpc.bindService(this, executionContext) -} - -object ApiIdentityProviderConfigService { - private def toProto( - identityProviderConfig: IdentityProviderConfig - ): proto.IdentityProviderConfig = - proto.IdentityProviderConfig( - identityProviderId = identityProviderConfig.identityProviderId.toRequestString, - isDeactivated = identityProviderConfig.isDeactivated, - jwksUrl = identityProviderConfig.jwksUrl.value, - issuer = identityProviderConfig.issuer, - audience = identityProviderConfig.audience.getOrElse(""), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPackageManagementService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPackageManagementService.scala deleted file mode 100644 index a66e6889f6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPackageManagementService.scala +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import cats.data.EitherT -import cats.implicits.{toBifunctorOps, toTraverseOps} -import com.daml.ledger.api.v2.admin.package_management_service.* -import com.daml.ledger.api.v2.admin.package_management_service.PackageManagementServiceGrpc.PackageManagementService -import com.daml.logging.LoggingContext -import com.digitalasset.base.error.RpcError -import com.digitalasset.canton.ProtoDeserializationError.ProtoDeserializationFailure -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.util.TimestampConversion -import com.digitalasset.canton.ledger.api.{ - UpdateVettedPackagesOpts, - UploadDarVettingChange as UploadDarOpts, -} -import com.digitalasset.canton.ledger.participant.state.{PackageSyncService, SubmissionResult} -import com.digitalasset.canton.logging.LoggingContextUtil.createLoggingContext -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil -import com.digitalasset.canton.platform.apiserver.services.logging -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.canton.util.EitherUtil.* -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.canton.util.{EitherTUtil, OptionUtil} -import com.digitalasset.daml.lf.data.Ref -import io.grpc.{ServerServiceDefinition, StatusRuntimeException} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -private[apiserver] final class ApiPackageManagementService private ( - packageSyncService: PackageSyncService, - submissionIdGenerator: String => Ref.SubmissionId, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends PackageManagementService - with GrpcApiService - with NamedLogging { - - private implicit val loggingContext: LoggingContext = - createLoggingContext(loggerFactory)(identity) - - override def close(): Unit = { - // Nothing to do in this service's close. - // All backend operations are guarded - } - - override def bindService(): ServerServiceDefinition = - PackageManagementServiceGrpc.bindService(this, executionContext) - - override def listKnownPackages( - request: ListKnownPackagesRequest - ): Future[ListKnownPackagesResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - logger.info("Listing known packages.") - packageSyncService - .listLfPackages() - .map { pkgs => - ListKnownPackagesResponse(pkgs.map { pkgDescription => - PackageDetails( - pkgDescription.packageId, - pkgDescription.packageSize.toLong, - Some(TimestampConversion.fromLf(pkgDescription.uploadedAt.underlying)), - name = pkgDescription.name.unwrap, - version = pkgDescription.version.unwrap, - ) - }) - } - .thereafter(logger.logErrorsOnCall[ListKnownPackagesResponse]) - } - - override def validateDarFile(request: ValidateDarFileRequest): Future[ValidateDarFileResponse] = - LoggingContextWithTrace.withEnrichedLoggingContext(TraceContextGrpc.fromGrpcContext)( - logging.submissionId(submissionIdGenerator(request.submissionId)) - ) { implicit loggingContext: LoggingContextWithTrace => - logger.info(s"Validating DAR file, ${loggingContext.serializeFiltered("submissionId")}.") - for { - synchronizerIdO <- - EitherTUtil.toFuture( - CantonGrpcUtil.mapErrNew( - OptionUtil - .emptyStringAsNone(request.synchronizerId) - .traverse(SynchronizerId.fromProtoPrimitive(_, "synchronizer_id")) - .leftMap(ProtoDeserializationFailure.Wrap(_)) - ) - ) - result <- packageSyncService - .validateDar( - dar = request.darFile, - darName = "defaultDarName", - synchronizerId = synchronizerIdO, - ) - .flatMap { - case SubmissionResult.Acknowledged => Future.successful(ValidateDarFileResponse()) - case err: SubmissionResult.SynchronousError => Future.failed(err.exception) - } - } yield result - } - - override def uploadDarFile(request: UploadDarFileRequest): Future[UploadDarFileResponse] = { - val submissionId = submissionIdGenerator(request.submissionId) - LoggingContextWithTrace.withEnrichedLoggingContext(TraceContextGrpc.fromGrpcContext)( - logging.submissionId(submissionId) - ) { implicit loggingContext: LoggingContextWithTrace => - logger.info(s"Uploading DAR file, ${loggingContext.serializeFiltered("submissionId")}.") - - val resultET = for { - synchronizerIdO <- - CantonGrpcUtil.mapErrNew( - OptionUtil - .emptyStringAsNone(request.synchronizerId) - .traverse(SynchronizerId.fromProtoPrimitive(_, "synchronizer_id")) - .leftMap(ProtoDeserializationFailure.Wrap(_)) - ) - uploadDarVettingChange <- CantonGrpcUtil - .mapErrNew( - UploadDarOpts - .fromProto("vetting_change", request.vettingChange) - .leftMap(ProtoDeserializationFailure.Wrap(_)) - ) - uploadResult <- EitherT.right( - packageSyncService - .uploadDar(Seq(request.darFile), submissionId, uploadDarVettingChange, synchronizerIdO) - ) - response <- uploadResult match { - case SubmissionResult.Acknowledged => - EitherT.rightT[Future, StatusRuntimeException](UploadDarFileResponse()) - case err: SubmissionResult.SynchronousError => - EitherT.leftT[Future, UploadDarFileResponse](err.exception) - } - } yield response - EitherTUtil.toFuture(resultET).thereafter(logger.logErrorsOnCall[UploadDarFileResponse]) - } - } - - override def updateVettedPackages( - request: UpdateVettedPackagesRequest - ): Future[UpdateVettedPackagesResponse] = { - val submissionId = submissionIdGenerator("") - LoggingContextWithTrace.withEnrichedLoggingContext(TraceContextGrpc.fromGrpcContext)( - logging.submissionId(submissionId) - ) { implicit loggingContext: LoggingContextWithTrace => - for { - updateVettedPackagesOpts <- UpdateVettedPackagesOpts - .fromProto(request) - .toFuture(ProtoDeserializationFailure.Wrap(_).asGrpcError) - result <- packageSyncService.updateVettedPackages(updateVettedPackagesOpts) - } yield result match { - case (previousState, newState) => - UpdateVettedPackagesResponse( - pastVettedPackages = previousState.map(_.toProtoLAPI), - newVettedPackages = newState.map(_.toProtoLAPI), - ) - } - } - } -} - -private[apiserver] object ApiPackageManagementService { - - def createApiService( - packageSyncService: PackageSyncService, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext - ): PackageManagementServiceGrpc.PackageManagementService & GrpcApiService = - new ApiPackageManagementService( - packageSyncService, - augmentSubmissionId, - loggerFactory, - ) - - implicit class ErrorValidations[E, R](result: Either[E, R]) { - def handleError(toSelfServiceErrorCode: E => RpcError): Try[R] = - result.left.map { err => - toSelfServiceErrorCode(err).asGrpcError - }.toTry - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiParticipantPruningService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiParticipantPruningService.scala deleted file mode 100644 index 170e29e131..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiParticipantPruningService.scala +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.daml.ledger.api.v2.admin.participant_pruning_service.{ - ParticipantPruningServiceGrpc, - PruneRequest, - PruneResponse, -} -import com.daml.metrics.Tracked -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.ValidationLogger -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.ParticipantOffsetValidator -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.* -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.SyncService -import com.digitalasset.canton.ledger.participant.state.index.{ - IndexParticipantPruningService, - IndexUpdateService, - LedgerEndService, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors.AbortedDueToShutdown -import com.digitalasset.canton.platform.apiserver.ApiException -import com.digitalasset.canton.scheduler.SafeToPruneCommitmentState -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.canton.util.Thereafter.syntax.* -import io.grpc.protobuf.StatusProto -import io.grpc.{ServerServiceDefinition, StatusRuntimeException} - -import scala.concurrent.{ExecutionContext, Future} - -final class ApiParticipantPruningService private ( - readBackend: IndexParticipantPruningService with LedgerEndService, - syncService: SyncService, - metrics: LedgerApiServerMetrics, - safeToPruneCommitmentState: Option[SafeToPruneCommitmentState], - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends ParticipantPruningServiceGrpc.ParticipantPruningService - with GrpcApiService - with NamedLogging { - - override def bindService(): ServerServiceDefinition = - ParticipantPruningServiceGrpc.bindService(this, executionContext) - - override def close(): Unit = () - - override def prune(request: PruneRequest): Future[PruneResponse] = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - - checkPruningIsNotInProgress { () => - logger.info(s"Pruning up to ${request.pruneUpTo}.") - (for { - pruneUpTo <- validateRequest(request) - - // If write service pruning succeeds but ledger api server index pruning fails, the user can bring the - // systems back in sync by reissuing the prune request at the currently specified or later offset. - _ = logger.debug("Pruning write service") - _ <- Tracked.future( - metrics.services.pruning.pruneCommandStarted, - metrics.services.pruning.pruneCommandCompleted, - pruneSyncService(pruneUpTo), - )(MetricsContext(("phase", "underlyingLedger"))) - - _ = logger.debug("Getting incomplete reassignments") - getIncompleteReassignmentOffsets = (offset: Offset) => - syncService - .incompleteReassignmentOffsets( - validAt = offset, - stakeholders = Set.empty, // getting all incomplete reassignments - ) - .failOnShutdownTo(AbortedDueToShutdown.Error().asGrpcError) - previousPrunedOffset <- readBackend.indexDbPrunedUpto - incompleteReassignmentOffsets <- - getIncompleteReassignmentOffsets(pruneUpTo) - previousIncompleteReassignmentOffsets <- - previousPrunedOffset - .map(getIncompleteReassignmentOffsets) - .getOrElse(Future.successful(Vector.empty)) - - _ = logger.debug("Pruning Ledger API Server") - pruneResponse <- Tracked.future( - metrics.services.pruning.pruneCommandStarted, - metrics.services.pruning.pruneCommandCompleted, - pruneLedgerApiServerIndex( - previousPrunedOffset, - previousIncompleteReassignmentOffsets, - pruneUpTo, - incompleteReassignmentOffsets, - ), - )(MetricsContext(("phase", "ledgerApiServerIndex"))) - - } yield pruneResponse) - .thereafter(logger.logErrorsOnCall[PruneResponse](loggingContext.traceContext)) - } - } - - private def validateRequest( - request: PruneRequest - )(implicit - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[Offset] = - (for { - _ <- checkOffsetIsSpecified(request.pruneUpTo) - pruneUpTo <- ParticipantOffsetValidator.validatePositive(request.pruneUpTo, "prune_up_to") - } yield pruneUpTo) - .fold( - t => Future.failed(ValidationLogger.logFailureWithTrace(logger, request, t)), - checkOffsetIsBeforeLedgerEnd, - ) - - private def pruneSyncService( - pruneUpTo: Offset - )(implicit loggingContext: LoggingContextWithTrace): Future[Unit] = { - import state.PruningResult.* - logger.info( - s"About to prune participant ledger up to ${pruneUpTo.unwrap} inclusively starting with the write service." - ) - syncService - .prune(pruneUpTo, safeToPruneCommitmentState) - .flatMap { - case NotPruned(status) => - Future.failed(new ApiException(StatusProto.toStatusRuntimeException(status))) - case ParticipantPruned => - logger.info(s"Pruned participant ledger up to ${pruneUpTo.unwrap} inclusively.") - Future.unit - } - } - - private def pruneLedgerApiServerIndex( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpTo: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit loggingContext: LoggingContextWithTrace): Future[PruneResponse] = { - logger.info(s"About to prune ledger api server index to ${pruneUpTo.unwrap} inclusively.") - readBackend - .prune( - previousPruneUpToInclusive = previousPruneUpToInclusive, - previousIncompleteReassignmentOffsets = previousIncompleteReassignmentOffsets, - pruneUpToInclusive = pruneUpTo, - incompleteReassignmentOffsets = incompleteReassignmentOffsets, - ) - .map { _ => - logger.info(s"Pruned ledger api server index up to ${pruneUpTo.unwrap} inclusively.") - PruneResponse() - } - } - - private def checkOffsetIsSpecified( - offset: Long - )(implicit errorLogger: ErrorLoggingContext): Either[StatusRuntimeException, Unit] = - Either.cond( - offset != 0, - (), - invalidArgument("prune_up_to not specified or zero"), - ) - - private def checkOffsetIsBeforeLedgerEnd( - pruneUpTo: Offset - )(implicit - errorLogger: ErrorLoggingContext - ): Future[Offset] = - for { - ledgerEnd <- readBackend.currentLedgerEnd() - _ <- - if (Option(pruneUpTo) < ledgerEnd) Future.unit - else - Future.failed( - RequestValidationErrors.OffsetOutOfRange - .Reject( - s"prune_up_to needs to be before ledger end $ledgerEnd" - ) - .asGrpcError - ) - } yield pruneUpTo - - // Fast-path check to reject early if pruning is already in progress. - // The actual serialization guard lives in JdbcLedgerDao.ensurePruningIsNotInProgress. - private def checkPruningIsNotInProgress[T](f: () => Future[T])(implicit - errorLogger: ErrorLoggingContext - ): Future[T] = - if (!readBackend.isPruningInProgress) { - f() - } else { - Future.failed( - RequestValidationErrors.ParticipantPruningInProgress.Reject().asGrpcError - ) - } -} - -object ApiParticipantPruningService { - def createApiService( - readBackend: IndexParticipantPruningService with LedgerEndService with IndexUpdateService, - syncService: SyncService, - metrics: LedgerApiServerMetrics, - safeToPruneCommitmentState: Option[SafeToPruneCommitmentState], - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext - ): ParticipantPruningServiceGrpc.ParticipantPruningService with GrpcApiService = - new ApiParticipantPruningService( - readBackend, - syncService, - metrics, - safeToPruneCommitmentState, - loggerFactory, - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPartyManagementService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPartyManagementService.scala deleted file mode 100644 index 1ed69f00cd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPartyManagementService.scala +++ /dev/null @@ -1,1274 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import cats.syntax.either.* -import com.daml.ledger.api.v2.admin.object_meta.ObjectMeta as ProtoObjectMeta -import com.daml.ledger.api.v2.admin.party_management_service.AllocateExternalPartyRequest.SignedTransaction -import com.daml.ledger.api.v2.admin.party_management_service.PartyManagementServiceGrpc.PartyManagementService -import com.daml.ledger.api.v2.admin.party_management_service.{ - AllocateExternalPartyRequest, - AllocateExternalPartyResponse, - AllocatePartyRequest, - AllocatePartyResponse, - GenerateExternalPartyTopologyRequest, - GenerateExternalPartyTopologyResponse, - GetParticipantIdRequest, - GetParticipantIdResponse, - GetPartiesRequest, - GetPartiesResponse, - ListKnownPartiesRequest, - ListKnownPartiesResponse, - PartyDetails as ProtoPartyDetails, - PartyManagementServiceGrpc, - UpdatePartyDetailsRequest, - UpdatePartyDetailsResponse, - UpdatePartyIdentityProviderIdRequest, - UpdatePartyIdentityProviderIdResponse, -} -import com.daml.logging.LoggingContext -import com.daml.nonempty.NonEmpty -import com.daml.platform.v1.page_tokens.ListPartiesPageTokenPayload -import com.digitalasset.canton.LfPartyId -import com.digitalasset.canton.auth.AuthorizationChecksErrors -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} -import com.digitalasset.canton.crypto.v30.{SigningKeyScheme, SigningKeyUsage} -import com.digitalasset.canton.crypto.{Signature, SigningKeysWithThreshold, SigningPublicKey, v30} -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.FieldValidator.{requireParty, *} -import com.digitalasset.canton.ledger.api.validation.ValidationErrors.invalidArgument -import com.digitalasset.canton.ledger.api.validation.ValueValidator.requirePresence -import com.digitalasset.canton.ledger.api.validation.{CryptoValidator, ValidationErrors} -import com.digitalasset.canton.ledger.api.{ - IdentityProviderId, - ObjectMeta, - PartyDetails, - User, - UserRight, -} -import com.digitalasset.canton.ledger.error.CommonErrors -import com.digitalasset.canton.ledger.error.groups.{ - PartyManagementServiceErrors, - RequestValidationErrors, -} -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore.UserInfo -import com.digitalasset.canton.ledger.localstore.api.{ - ObjectMetaUpdate, - PartyDetailsUpdate, - PartyRecord, - PartyRecordStore, - PartyRecordUpdate, - UserManagementStore, -} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel.Observation -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.{ - AuthorizationEvent, - AuthorizationLevel, -} -import com.digitalasset.canton.ledger.participant.state.index.* -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.* -import com.digitalasset.canton.logging.LoggingContextUtil.createLoggingContext -import com.digitalasset.canton.logging.LoggingContextWithTrace.{ - implicitExtractTraceContext, - withEnrichedLoggingContext, -} -import com.digitalasset.canton.platform.apiserver.services.admin.ApiPartyManagementService.* -import com.digitalasset.canton.platform.apiserver.services.admin.AuthenticatedUserContextResolver.AuthenticatedUserContext -import com.digitalasset.canton.platform.apiserver.services.admin.{ - PartyAllocation, - PendingPartyAllocations, -} -import com.digitalasset.canton.platform.apiserver.services.logging -import com.digitalasset.canton.platform.apiserver.services.tracking.StreamTracker -import com.digitalasset.canton.platform.apiserver.update -import com.digitalasset.canton.platform.apiserver.update.PartyRecordUpdateMapper -import com.digitalasset.canton.serialization.ProtoConverter -import com.digitalasset.canton.topology.* -import com.digitalasset.canton.topology.transaction.* -import com.digitalasset.canton.topology.transaction.TopologyTransaction.PositiveTopologyTransaction -import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} -import com.digitalasset.canton.version.{ProtocolVersion, ProtocolVersionValidation} -import com.digitalasset.daml.lf.data.Ref -import io.grpc.Status.Code.ALREADY_EXISTS -import io.grpc.{ServerServiceDefinition, StatusRuntimeException} -import io.opentelemetry.api.trace.Tracer -import scalaz.std.either.* -import scalaz.std.list.* -import scalaz.syntax.traverse.* - -import java.nio.charset.StandardCharsets -import java.util.Base64 -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Try} - -private[apiserver] final class ApiPartyManagementService private ( - partyManagementService: IndexPartyManagementService, - userManagementStore: UserManagementStore, - identityProviderExists: IdentityProviderExists, - maxPartiesPageSize: PositiveInt, - maxSelfAllocatedParties: NonNegativeInt, - partyRecordStore: PartyRecordStore, - syncService: state.PartySyncService, - managementServiceTimeout: FiniteDuration, - submissionIdGenerator: CreateSubmissionId, - partyAllocationTracker: PartyAllocation.Tracker, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext, - tracer: Tracer, -) extends PartyManagementService - with GrpcApiService - with NamedLogging - with AuthenticatedUserContextResolver { - - private val pendingPartyAllocations = new PendingPartyAllocations() - - private implicit val loggingContext: LoggingContext = - createLoggingContext(loggerFactory)(identity) - - override def close(): Unit = partyAllocationTracker.close() - - override def bindService(): ServerServiceDefinition = - PartyManagementServiceGrpc.bindService(this, executionContext) - - override def getParticipantId( - request: GetParticipantIdRequest - ): Future[GetParticipantIdResponse] = { - implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext - - logger.info("Getting Participant ID.") - partyManagementService - .getParticipantId() - .map(GetParticipantIdResponse.apply) - } - - override def getParties(request: GetPartiesRequest): Future[GetPartiesResponse] = - withEnrichedLoggingContext(TraceContextGrpc.fromGrpcContext)( - logging.partyStrings(request.parties) - ) { implicit loggingContextWithTrace => - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext( - logger, - loggingContextWithTrace.toPropertiesMap, - loggingContextWithTrace.traceContext, - ) - logger.info(s"Getting parties, ${loggingContextWithTrace.serializeFiltered("parties")}.") - withValidation { - for { - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - parties <- request.parties.toList.traverse(requireParty) - } yield (parties, identityProviderId) - } { case (parties: Seq[Ref.Party], identityProviderId: IdentityProviderId) => - for { - partyDetailsSeq <- partyManagementService.getParties(parties) - partyRecordOptions <- fetchPartyRecords(partyDetailsSeq) - } yield { - val protoDetails = - partyDetailsSeq - .zip(partyRecordOptions) - .map(blindAndConvertToProto(identityProviderId)) - GetPartiesResponse(partyDetails = protoDetails) - } - } - } - - private def parsePartyFilter( - unsafeString: String - )(implicit traceContext: TraceContext): Either[StatusRuntimeException, Option[String185]] = if ( - unsafeString.isEmpty - ) - Either.right(None) - else - (for { - validated <- Ref.Party.fromString(unsafeString) - limited <- String185.create(validated) - } yield Some(limited)).leftMap(err => - RequestValidationErrors.InvalidField.Reject("filterString", err).asGrpcError - ) - - override def listKnownParties( - request: ListKnownPartiesRequest - ): Future[ListKnownPartiesResponse] = { - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(TraceContextGrpc.fromGrpcContext)(this.loggingContext) - - val ListKnownPartiesRequest(pageToken, pageSize, identityProviderId, filterString) = request - logger.info("Listing known parties.") - withValidation( - for { - fromExcl <- decodePartyFromPageToken(pageToken) - _ <- Either.cond( - pageSize >= 0, - pageSize, - RequestValidationErrors.InvalidArgument - .Reject("Page size must be non-negative") - .asGrpcError, - ) - _ <- Either.cond( - pageSize <= maxPartiesPageSize.value, - pageSize, - RequestValidationErrors.InvalidArgument - .Reject(s"Page size must not exceed the server's maximum of $maxPartiesPageSize") - .asGrpcError, - ) - identityProviderId <- optionalIdentityProviderId( - identityProviderId, - "identity_provider_id", - ) - pageSizeOrDefault = - if (pageSize == 0) maxPartiesPageSize.value - else pageSize - filterStringParsed <- parsePartyFilter(filterString) - } yield { - (fromExcl, pageSizeOrDefault, identityProviderId, filterStringParsed) - } - ) { case (fromExcl, pageSizeOrDefault, identityProviderId, filterStringParsed) => - for { - partyDetailsSeq <- partyManagementService.listKnownParties( - fromExcl, - filterStringParsed, - pageSizeOrDefault, - ) - partyRecords <- fetchPartyRecords(partyDetailsSeq) - } yield { - val protoDetails = partyDetailsSeq - .zip(partyRecords) - .map(blindAndConvertToProto(identityProviderId)) - val lastParty = - if (partyDetailsSeq.sizeIs < pageSizeOrDefault) None - else partyDetailsSeq.lastOption.map(_.party) - ListKnownPartiesResponse(protoDetails, encodeNextPageToken(lastParty)) - } - } - } - - implicit object PartyAllocationErrors extends StreamTracker.Errors[PartyAllocation.TrackerKey] { - import com.digitalasset.canton.ledger.error.CommonErrors - - def timedOut(key: PartyAllocation.TrackerKey)(implicit - errorLogger: ErrorLoggingContext - ): StatusRuntimeException = - CommonErrors.RequestTimeOut - .Reject( - s"Timed out while awaiting item corresponding to ${key.submissionId}.", - definiteAnswer = false, - ) - .asGrpcError - - def duplicated( - key: PartyAllocation.TrackerKey - )(implicit errorLogger: ErrorLoggingContext): StatusRuntimeException = - CommonErrors.RequestAlreadyInFlight - .Reject( - requestId = key.submissionId, - details = s"Party ${key.partyId} is in the process of being allocated on this node.", - ) - .asGrpcError - } - - private def generatePartyName: Ref.Party = { - import java.util.UUID - Ref.Party.assertFromString(s"party-${UUID.randomUUID().toString}") - } - - override def allocateParty(request: AllocatePartyRequest): Future[AllocatePartyResponse] = { - withEnrichedLoggingContext(TraceContextGrpc.fromGrpcContext)( - logging.partyString(request.partyIdHint) - ) { implicit loggingContextWithTrace => - logger.info( - s"Allocating party, ${loggingContextWithTrace.serializeFiltered("submissionId", "parties")}." - ) - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext( - logger, - loggingContextWithTrace.toPropertiesMap, - loggingContextWithTrace.traceContext, - ) - // Retrieving the authenticated user context from the thread-local context - val authenticatedUserContextF: Future[AuthenticatedUserContext] = - resolveAuthenticatedUserContext - import com.digitalasset.canton.config.NonNegativeFiniteDuration - - withValidation { - for { - partyIdHintO <- optionalString( - request.partyIdHint - )(requireParty) - metadata = request.localMetadata.getOrElse( - ProtoObjectMeta( - resourceVersion = "", - annotations = Map.empty, - ) - ) - _ <- requireEmptyString( - metadata.resourceVersion, - "local_metadata.resource_version", - ) - annotations <- verifyMetadataAnnotations( - metadata.annotations, - allowEmptyValues = false, - "local_metadata.annotations", - ) - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - synchronizerIdO <- optionalSynchronizerId(request.synchronizerId, "synchronizer_id") - userId <- optionalUserId(request.userId, "user_id") - partyName = partyIdHintO.getOrElse(generatePartyName) - partyId <- UniqueIdentifier - .create(partyName, syncService.participantId.namespace) - .map(PartyId(_)) - .leftMap(err => invalidArgument(s"Invalid party ID: $err")) - } yield (partyId, annotations, identityProviderId, synchronizerIdO, userId) - } { case (partyId, annotations, identityProviderId, synchronizerIdO, userId) => - val trackerKey = submissionIdGenerator(partyId.toLf, AuthorizationLevel.Submission) - withEnrichedLoggingContext(logging.submissionId(trackerKey.submissionId)) { - implicit loggingContextWithTrace => - pendingPartyAllocations.withUser(userId) { outstandingCalls => - for { - _ <- identityProviderExistsOrError(identityProviderId) - userInfo <- getUserIfUserSpecified(userId, identityProviderId) - _ <- checkUserLimitsIfUserSpecified( - userInfo.map(_.rights), - outstandingCalls, - authenticatedUserContextF, - ) - _ <- verifyPartyIsNonExistentOrInIdp( - identityProviderId, - partyId.toLf, - ) - allocated <- partyAllocationTracker - .track( - trackerKey, - NonNegativeFiniteDuration(managementServiceTimeout), - ) { _ => - for { - result <- syncService.allocateParty( - partyId, - trackerKey.submissionId, - synchronizerIdO, - externalPartyOnboardingDetails = None, - ) - _ <- checkSubmissionResult(result) - } yield () - } - .transform(alreadyExistsError(trackerKey.submissionId, loggingContextWithTrace)) - existingPartyRecord <- partyRecordStore.getPartyRecordO( - allocated.partyDetails.party - ) - partyRecord <- updateOrCreatePartyRecord( - existingPartyRecord, - allocated.partyDetails.party, - identityProviderId, - annotations, - ) - _ <- updateUserInfoIfUserSpecified( - allocated.partyDetails.party, - userInfo.map(_.user), - ) - } yield { - val details = toProtoPartyDetails( - partyDetails = allocated.partyDetails, - metadataO = Some(partyRecord.metadata), - identityProviderId = Some(identityProviderId), - ) - AllocatePartyResponse(Some(details)) - } - } - } - } - } - } - - private def updateUserInfoIfUserSpecified( - party: Ref.Party, - user: Option[User], - )(implicit loggingContextWithTrace: LoggingContextWithTrace): Future[Unit] = - user.fold(Future.successful(())) { u => - userManagementStore - .grantRights( - u.id, - Set(UserRight.CanActAs(party)), - u.identityProviderId, - ) - .map(_.left.map { - case UserManagementStore.UserNotFound(id) => - UserManagementStore.UserDeletedWhileUpdating(id) - case other => other - }) - .flatMap(Utils.handleResult("granting user rights for a new party")) - .map(_ => ()) - } - - private def updateOrCreatePartyRecord( - existingPartyRecord: PartyRecordStore.Result[Option[PartyRecord]], - party: Ref.Party, - identityProviderId: IdentityProviderId, - annotations: Map[String, String], - )(implicit loggingContextWithTrace: LoggingContextWithTrace): Future[PartyRecord] = - if (existingPartyRecord.exists(_.nonEmpty)) { - partyRecordStore - .updatePartyRecord( - PartyRecordUpdate( - party = party, - identityProviderId = identityProviderId, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = Some(annotations), - ), - ), - ledgerPartyIsLocal = true, - ) - .flatMap(handlePartyRecordStoreResult("updating a party record")(_)) - } else { - partyRecordStore - .createPartyRecord( - PartyRecord( - party = party, - metadata = ObjectMeta(resourceVersionO = None, annotations = annotations), - identityProviderId = identityProviderId, - ) - ) - .flatMap(handlePartyRecordStoreResult("creating a party record")(_)) - } - - private def checkSubmissionResult(r: state.SubmissionResult) = r match { - case state.SubmissionResult.Acknowledged => FutureUnlessShutdown.unit - case synchronousError: state.SubmissionResult.SynchronousError => - FutureUnlessShutdown.failed(synchronousError.exception) - } - - private def alreadyExistsError[R]( - submissionId: Ref.SubmissionId, - loggingContextWithTrace: LoggingContextWithTrace, - )(r: Try[R]) = r match { - case Failure(e: StatusRuntimeException) if e.getStatus.getCode == ALREADY_EXISTS => - Failure( - ValidationErrors.invalidArgument(e.getStatus.getDescription)( - ErrorLoggingContext.withExplicitCorrelationId( - logger, - loggingContextWithTrace.toPropertiesMap, - loggingContextWithTrace.traceContext, - submissionId, - ) - ) - ) - case x => x - } - - override def updatePartyDetails( - request: UpdatePartyDetailsRequest - ): Future[UpdatePartyDetailsResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(TraceContextGrpc.fromGrpcContext)(this.loggingContext) - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContextWithTrace) - withValidation { - for { - partyDetails <- requirePresence( - request.partyDetails, - "party_details", - ) - party <- requireParty(partyDetails.party) - metadata = partyDetails.localMetadata.getOrElse( - ProtoObjectMeta( - resourceVersion = "", - annotations = Map.empty, - ) - ) - resourceVersionNumberO <- optionalString(metadata.resourceVersion)( - requireResourceVersion( - _, - "party_details.local_metadata", - ) - ) - annotations <- verifyMetadataAnnotations( - metadata.annotations, - allowEmptyValues = true, - "party_details.local_metadata.annotations", - ) - updateMask <- requirePresence( - request.updateMask, - "update_mask", - ) - identityProviderId <- optionalIdentityProviderId( - partyDetails.identityProviderId, - "identity_provider_id", - ) - partyRecord = PartyDetails( - party = party, - isLocal = partyDetails.isLocal, - metadata = ObjectMeta( - resourceVersionO = resourceVersionNumberO, - annotations = annotations, - ), - identityProviderId = identityProviderId, - ) - } yield (partyRecord, updateMask) - } { case (partyRecord, updateMask) => - for { - _ <- identityProviderExistsOrError(partyRecord.identityProviderId) - _ = logger.info( - s"Updating party: ${request.getPartyDetails.party}, ${loggingContextWithTrace.serializeFiltered("submissionId")}." - ) - partyDetailsUpdate: PartyDetailsUpdate <- handleUpdatePathResult( - party = partyRecord.party, - PartyRecordUpdateMapper.toUpdate( - apiObject = partyRecord, - updateMask = updateMask, - ), - ) - fetchedPartyDetailsO <- partyManagementService - .getParties(parties = Seq(partyRecord.party)) - .map(_.headOption) - fetchedPartyDetails <- fetchedPartyDetailsO match { - case Some(partyDetails) => Future.successful(partyDetails) - case None => - Future.failed( - PartyManagementServiceErrors.PartyNotFound - .Reject( - operation = "updating a party record", - party = partyRecord.party, - ) - .asGrpcError - ) - } - partyRecordUpdate: PartyRecordUpdate <- { - if (partyDetailsUpdate.isLocalUpdate.exists(_ != fetchedPartyDetails.isLocal)) { - Future.failed( - PartyManagementServiceErrors.InvalidUpdatePartyDetailsRequest - .Reject( - party = partyRecord.party, - reason = s"Update request attempted to modify not-modifiable 'is_local' attribute", - ) - .asGrpcError - ) - } else { - // NOTE: In the current implementation (as of 2022.10.13) a no-op update request - // will still cause an update of the resourceVersion's value. - Future.successful( - PartyRecordUpdate( - party = partyDetailsUpdate.party, - metadataUpdate = partyDetailsUpdate.metadataUpdate, - identityProviderId = partyRecord.identityProviderId, - ) - ) - } - } - _ <- verifyPartyIsNonExistentOrInIdp( - partyRecordUpdate.identityProviderId, - partyRecordUpdate.party, - ) - updatedPartyRecordResult <- partyRecordStore.updatePartyRecord( - partyRecordUpdate = partyRecordUpdate, - ledgerPartyIsLocal = fetchedPartyDetailsO.exists(_.isLocal), - ) - updatedPartyRecord: PartyRecord <- handlePartyRecordStoreResult( - "updating a participant party record" - )(updatedPartyRecordResult) - } yield UpdatePartyDetailsResponse( - Some( - toProtoPartyDetails( - partyDetails = fetchedPartyDetails, - metadataO = Some(updatedPartyRecord.metadata), - identityProviderId = Some(updatedPartyRecord.identityProviderId), - ) - ) - ) - } - } - - override def updatePartyIdentityProviderId( - request: UpdatePartyIdentityProviderIdRequest - ): Future[UpdatePartyIdentityProviderIdResponse] = { - implicit val loggingContextWithTrace = LoggingContextWithTrace(TraceContextGrpc.fromGrpcContext) - - logger.info("Updating party identity provider.") - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContextWithTrace) - withValidation { - for { - party <- requireParty(request.party) - sourceIdentityProviderId <- optionalIdentityProviderId( - request.sourceIdentityProviderId, - "source_identity_provider_id", - ) - targetIdentityProviderId <- optionalIdentityProviderId( - request.targetIdentityProviderId, - "target_identity_provider_id", - ) - } yield (party, sourceIdentityProviderId, targetIdentityProviderId) - } { case (party, sourceIdentityProviderId, targetIdentityProviderId) => - for { - _ <- identityProviderExistsOrError(sourceIdentityProviderId) - _ <- identityProviderExistsOrError(targetIdentityProviderId) - fetchedPartyDetailsO <- partyManagementService - .getParties(parties = Seq(party)) - .map(_.headOption) - _ <- fetchedPartyDetailsO match { - case Some(_) => Future.unit - case None => - Future.failed( - PartyManagementServiceErrors.PartyNotFound - .Reject( - operation = "updating party's identity provider", - party = party, - ) - .asGrpcError - ) - } - result <- partyRecordStore - .updatePartyRecordIdp( - party = party, - ledgerPartyIsLocal = fetchedPartyDetailsO.exists(_.isLocal), - sourceIdp = sourceIdentityProviderId, - targetIdp = targetIdentityProviderId, - ) - .flatMap(handlePartyRecordStoreResult("updating party's identity provider")) - .map(_ => UpdatePartyIdentityProviderIdResponse()) - } yield result - } - } - - // Check if party either doesn't exist or exists and belongs to the requested Identity Provider - private def verifyPartyIsNonExistentOrInIdp( - identityProviderId: IdentityProviderId, - party: Ref.Party, - )(implicit - loggingContextWithTrace: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[Unit] = - partyRecordStore.getPartyRecordO(party).flatMap { - case Right(Some(party)) if party.identityProviderId != identityProviderId => - Future.failed( - AuthorizationChecksErrors.PermissionDenied - .Reject( - s"Party $party belongs to an identity provider that differs from the one specified in the request" - ) - .asGrpcError - ) - case _ => Future.unit - } - - private def fetchPartyRecords( - partyDetails: List[IndexerPartyDetails] - )(implicit - loggingContextWithTrace: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[List[Option[PartyRecord]]] = - // Future optimization: Fetch party records from the DB in a batched fashion rather than one-by-one. - partyDetails.foldLeft(Future.successful(List.empty[Option[PartyRecord]])) { - (axF: Future[List[Option[PartyRecord]]], partyDetails: IndexerPartyDetails) => - for { - ax <- axF - next <- partyRecordStore - .getPartyRecordO(party = partyDetails.party) - .flatMap(handlePartyRecordStoreResult(operation = "retrieving a party record")(_)) - } yield ax :+ next - } - - private def withValidation[A, B](validatedResult: Either[StatusRuntimeException, A])( - f: A => Future[B] - ): Future[B] = - validatedResult.fold(Future.failed, Future.successful).flatMap(f) - - private def handleUpdatePathResult[T](party: Ref.Party, result: update.Result[T])(implicit - errorLogger: ErrorLoggingContext - ): Future[T] = - result match { - case Left(e: update.UpdatePathError) => - Future.failed( - PartyManagementServiceErrors.InvalidUpdatePartyDetailsRequest - .Reject(party, reason = e.getReason) - .asGrpcError - ) - case Right(t) => - Future.successful(t) - } - - private def handlePartyRecordStoreResult[T](operation: String)( - result: PartyRecordStore.Result[T] - )(implicit errorLogger: ErrorLoggingContext): Future[T] = - result match { - case Left(PartyRecordStore.PartyNotFound(party)) => - Future.failed( - PartyManagementServiceErrors.PartyNotFound - .Reject(operation, party = party) - .asGrpcError - ) - - case Left(PartyRecordStore.PartyRecordNotFoundFatal(party)) => - Future.failed( - PartyManagementServiceErrors.InternalPartyRecordNotFound - .Reject(operation, party = party) - .asGrpcError - ) - - case Left(PartyRecordStore.PartyRecordExistsFatal(party)) => - Future.failed( - PartyManagementServiceErrors.InternalPartyRecordAlreadyExists - .Reject(operation, party = party) - .asGrpcError - ) - - case Left(PartyRecordStore.ConcurrentPartyUpdate(party)) => - Future.failed( - PartyManagementServiceErrors.ConcurrentPartyDetailsUpdateDetected - .Reject(party = party) - .asGrpcError - ) - - case Left(PartyRecordStore.MaxAnnotationsSizeExceeded(party)) => - Future.failed( - PartyManagementServiceErrors.MaxPartyAnnotationsSizeExceeded - .Reject(party = party) - .asGrpcError - ) - - case Right(t) => - Future.successful(t) - } - - private def getUserIfUserSpecified( - userId: Option[Ref.UserId], - identityProviderId: IdentityProviderId, - )(implicit loggingContextWithTrace: LoggingContextWithTrace): Future[Option[UserInfo]] = - userId.fold[Future[Option[UserInfo]]](Future.successful(None))( - userManagementStore - .getUserInfo(_, identityProviderId) - .flatMap(result => Utils.handleResult("checking user's existence")(result).map(Some(_))) - ) - - private def checkUserLimitsIfUserSpecified( - userRights: Option[Set[UserRight]], - outstandingCalls: Int, - authenticatedUserContextF: Future[AuthenticatedUserContext], - )(implicit loggingContextWithTrace: LoggingContextWithTrace): Future[Unit] = - userRights match { - case None => Future.unit - case Some(rights) => - for { - authenticatedUserContext <- authenticatedUserContextF - resultingRightsCount = rights.flatMap(_.getParty).size + outstandingCalls - _ <- - if ( - authenticatedUserContext.isRegularUser && resultingRightsCount > maxSelfAllocatedParties.unwrap - ) - Future.failed( - AuthorizationChecksErrors.PermissionDenied - .Reject(s"User quota of party allocations exhausted") - .asGrpcError - ) - else - Future.unit - } yield () - } - - private def identityProviderExistsOrError( - id: IdentityProviderId - )(implicit - loggingContextWithTrace: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[Unit] = - identityProviderExists(id) - .flatMap { idpExists => - if (idpExists) - Future.unit - else - Future.failed( - RequestValidationErrors.InvalidArgument - .Reject(s"Provided identity_provider_id $id has not been found.") - .asGrpcError - ) - } - - private def parseSignedTransaction( - protocolVersion: ProtocolVersion, - signedTransaction: SignedTransaction, - )(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[ - StatusRuntimeException, - (PositiveTopologyTransaction, List[Signature]), - ] = - for { - transaction <- TopologyTransaction - .fromByteString( - // TODO(i27619): We may be able to not validate the protocol version here - // depending on the trust we put in the input - // Note that pinning to a protocol version makes it not possible to use transactions - // generated with an earlier protocol version (e.g in between synchronizer updates) - ProtocolVersionValidation(protocolVersion), - signedTransaction.transaction, - ) - .leftMap(error => - ValidationErrors.invalidField( - "onboarding_transactions.transaction", - s"Invalid transaction: ${error.message}", - ) - ) - positiveTransaction <- transaction - .selectOp[TopologyChangeOp.Replace] - .toRight( - ValidationErrors.invalidField( - "onboarding_transactions.transaction", - s"Onboarding topology transactions must be Replace operations", - ) - ) - _ <- Either.cond( - positiveTransaction.serial == PositiveInt.one, - (), - ValidationErrors.invalidField( - "onboarding_transactions.transaction.serial", - "Onboarding transaction serial must be 1", - ), - ) - signatures <- signedTransaction.signatures.toList.traverse( - CryptoValidator.validateSignature(_, "onboarding_transaction.signatures") - ) - } yield (positiveTransaction, signatures) - - override def allocateExternalParty( - request: AllocateExternalPartyRequest - ): Future[AllocateExternalPartyResponse] = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(TraceContextGrpc.fromGrpcContext)(this.loggingContext) - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext( - logger, - loggingContextWithTrace.toPropertiesMap, - loggingContextWithTrace.traceContext, - ) - import com.digitalasset.canton.config.NonNegativeFiniteDuration - // Retrieving the authenticated user context from the thread-local context - val authenticatedUserContextF: Future[AuthenticatedUserContext] = - resolveAuthenticatedUserContext - - // The default value (empty) should default to true (pre-existing behavior) - // So this is only false if explicitly set to false - val waitForAllocation = !request.waitForAllocation.contains(false) - - withValidation { - for { - synchronizerId <- requireSynchronizerId(request.synchronizer, "synchronizer") - .orElse( - // Take a physical synchronizer ID too - requirePhysicalSynchronizerId(request.synchronizer, "synchronizer").map(_.logical) - ) - protocolVersion <- syncService - .physicalSynchronizerIdForSynchronizerId(synchronizerId) - .map(_.protocolVersion) - .toRight( - ValidationErrors.invalidArgument( - s"This node is not connected to the requested synchronizer $synchronizerId." - ) - ) - transactionsWithSignatures <- request.onboardingTransactions.toList.traverse( - parseSignedTransaction(protocolVersion, _) - ) - signedTransactionsNE <- NonEmpty - .from(transactionsWithSignatures) - .toRight( - ValidationErrors - .invalidField("onboarding_transactions.transactions", "Transactions field is empty") - ) - parsedMultiSignatures <- request.multiHashSignatures.toList.traverse( - CryptoValidator.validateSignature(_, "multi_hash_signatures.signatures") - ) - _ = logger.debug( - s"External party allocation input transactions:\n ${signedTransactionsNE.map(_._1).mkString("\n")}" - ) - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - userId <- optionalUserId(request.userId, "user_id") - cantonParticipantId = this.syncService.participantId - externalPartyDetails <- ExternalPartyOnboardingDetails - .create(signedTransactionsNE, parsedMultiSignatures, protocolVersion, cantonParticipantId) - .leftMap(ValidationErrors.invalidArgument(_)) - } yield (synchronizerId, externalPartyDetails, identityProviderId, userId) - } { case (synchronizerId, externalPartyOnboardingDetails, identityProviderId, userId) => - if (externalPartyOnboardingDetails.signedPartyToKeyMappingTransaction.isDefined) { - logger.info( - "PartyToKeyMapping has been deprecated. Please use PartyToParticipant directly to configure" + - " the party's protocol signing keys and threshold instead." - ) - } - - val hostingParticipantsString = externalPartyOnboardingDetails.hostingParticipants - .map { case HostingParticipant(participantId, permission, _onboarding) => - s"$participantId -> $permission" - } - .mkString("[", ", ", "]") - val signingKeysString = - externalPartyOnboardingDetails.optionallySignedPartyToParticipant.mapping.partySigningKeysWithThreshold - .map { case SigningKeysWithThreshold(keys, threshold) => - s" and ${keys.size} signing keys with threshold ${threshold.value}" - } - .getOrElse("") - logger.info( - s"Allocating external party ${externalPartyOnboardingDetails.partyId.toProtoPrimitive} on" + - s" $hostingParticipantsString with confirmation threshold ${externalPartyOnboardingDetails.confirmationThreshold.value}" + signingKeysString - ) - val trackerKey = - submissionIdGenerator( - externalPartyOnboardingDetails.partyId.toLf, - authorizationLevel = - if (externalPartyOnboardingDetails.isConfirming) AuthorizationLevel.Confirmation - else Observation, - ) - withEnrichedLoggingContext(logging.submissionId(trackerKey.submissionId)) { - implicit loggingContextWithTrace => - pendingPartyAllocations.withUser(userId) { outstandingCalls => - def allocateFn = for { - result <- syncService.allocateParty( - externalPartyOnboardingDetails.partyId, - trackerKey.submissionId, - Some(synchronizerId), - Some(externalPartyOnboardingDetails), - ) - _ <- checkSubmissionResult(result) - } yield () - - for { - _ <- identityProviderExistsOrError(identityProviderId) - userInfo <- getUserIfUserSpecified(userId, identityProviderId) - _ <- checkUserLimitsIfUserSpecified( - userInfo.map(_.rights), - outstandingCalls, - authenticatedUserContextF, - ) - _ <- verifyPartyIsNonExistentOrInIdp( - identityProviderId, - externalPartyOnboardingDetails.partyId.toLf, - ) - // Only track the party if we expect it to be fully authorized (and it hasn't explicitly been disabled in the request) - // Otherwise the party won't be fully onboarded here so this would time out - allocated <- - (if (externalPartyOnboardingDetails.fullyAllocatesParty && waitForAllocation) { - partyAllocationTracker - .track( - trackerKey, - NonNegativeFiniteDuration(managementServiceTimeout), - )(_ => allocateFn) - .map(_.partyDetails.party) - } else { - allocateFn - .map(_ => externalPartyOnboardingDetails.partyId.toLf) - .failOnShutdownTo( - CommonErrors.ServiceNotRunning.Reject("PartyManagementService").asGrpcError - ) - }).transform(alreadyExistsError(trackerKey.submissionId, loggingContextWithTrace)) - existingPartyRecord <- partyRecordStore.getPartyRecordO( - allocated - ) - _ <- updateOrCreatePartyRecord( - existingPartyRecord, - allocated, - identityProviderId, - Map.empty, - ) - _ <- updateUserInfoIfUserSpecified( - allocated, - userInfo.map(_.user), - ) - } yield AllocateExternalPartyResponse(allocated) - } - }(loggingContextWithTrace) - } - } - - override def generateExternalPartyTopology( - request: GenerateExternalPartyTopologyRequest - ): Future[GenerateExternalPartyTopologyResponse] = { - import io.scalaland.chimney.dsl.* - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, LoggingContextWithTrace(TraceContextGrpc.fromGrpcContext)) - val GenerateExternalPartyTopologyRequest( - synchronizerIdP, - partyHint, - publicKeyO, - localParticipantObservationOnly, - otherConfirmingParticipantUids, - confirmationThreshold, - observingParticipantUids, - ) = request - - val participantId = syncService.participantId - - val availableConfirmers = - (if (localParticipantObservationOnly) 0 else 1) + otherConfirmingParticipantUids.size - - val response = for { - publicKeyP <- ProtoConverter.required("public_key", publicKeyO).leftMap(_.message) - publicKeyT <- publicKeyP - .intoPartial[v30.SigningPublicKey] - .withFieldConst(_.scheme, SigningKeyScheme.SIGNING_KEY_SCHEME_UNSPECIFIED) - .withFieldConst( - _.usage, - Seq( - SigningKeyUsage.SIGNING_KEY_USAGE_NAMESPACE, - SigningKeyUsage.SIGNING_KEY_USAGE_PROOF_OF_OWNERSHIP, - SigningKeyUsage.SIGNING_KEY_USAGE_PROTOCOL, - ), - ) - .withFieldRenamed(_.keyData, _.publicKey) - .transform - .asEither - .leftMap(_.asErrorPathMessages.map { case (p, e) => s"$p: $e" }.mkString(", ")) - pubKey <- SigningPublicKey - .fromProtoV30(publicKeyT) - .leftMap(_.message) - namespace = Namespace(pubKey.fingerprint) - protocolVersion <- UniqueIdentifier - .fromProtoPrimitive_(synchronizerIdP) - .map(SynchronizerId(_)) - .leftMap(_.message) - .flatMap(synchronizerId => - syncService - .physicalSynchronizerIdForSynchronizerId(synchronizerId) - .map(_.protocolVersion) - .toRight(s"Unknown or not connected synchronizer $synchronizerId") - ) - _ <- Either.cond(partyHint.nonEmpty, (), "Party hint is empty") - _ <- UniqueIdentifier.verifyValidString(partyHint).leftMap(x => "party_hint: " + x) - uid <- UniqueIdentifier.create(partyHint, namespace) - _ <- Either.cond(confirmationThreshold >= 0, (), "Negative confirmation threshold observed") - confirmingPids <- otherConfirmingParticipantUids.toList - .traverse(UniqueIdentifier.fromProtoPrimitive_) - .leftMap(_.message) - observingPids <- observingParticipantUids.toList - .traverse(UniqueIdentifier.fromProtoPrimitive_) - .leftMap(_.message) - _ <- Either.cond( - !confirmingPids.contains(participantId.uid), - (), - s"This participant node ($participantId) is also listed in 'otherConfirmingParticipantUids'." + - s" By sending the request to this node, it is de facto a hosting node and must not be listed in 'otherConfirmingParticipantUids'.", - ) - _ <- Either.cond( - !observingPids.contains(participantId.uid), - (), - s"This participant node ($participantId) is also listed in 'observingParticipantUids'." + - s" By sending the request to this node, it is de facto a hosting node and must not be listed in 'observingParticipantUids'.", - ) - allParticipantIds = (confirmingPids ++ observingPids) - _ <- Either.cond( - allParticipantIds.distinct.sizeIs == allParticipantIds.size, - (), { - val duplicates = - allParticipantIds.groupBy(identity).collect { case (x, ys) if ys.sizeIs > 1 => x } - s"The following participant IDs are referenced multiple times in the request: ${duplicates - .mkString(", ")}." + - s" Please ensure all IDs are referenced only once" + - s" across 'otherConfirmingParticipantUids' and 'observingParticipantUids' fields." - }, - ) - _ <- Either.cond( - confirmationThreshold <= availableConfirmers, - (), - "Confirmation threshold exceeds number of confirming participants", - ) - threshold = - if (confirmationThreshold == 0) availableConfirmers - else confirmationThreshold - party = PartyId(uid) - p2p <- PartyToParticipant.create( - party, - threshold = PositiveInt.tryCreate(threshold), - HostingParticipant( - participantId, - if (localParticipantObservationOnly) ParticipantPermission.Observation - else ParticipantPermission.Confirmation, - ) +: (confirmingPids.map(uid => - HostingParticipant(ParticipantId(uid), ParticipantPermission.Confirmation) - ) ++ observingPids.map(uid => - HostingParticipant(ParticipantId(uid), ParticipantPermission.Observation) - )), - partySigningKeysWithThreshold = Some( - SigningKeysWithThreshold.tryCreate( - keys = NonEmpty.mk(Seq, pubKey), - threshold = PositiveInt.one, - ) - ), - ) - } yield { - val transactions = - NonEmpty - .mk(List, p2p) - .map(mapping => - TopologyTransaction( - op = TopologyChangeOp.Replace, - serial = PositiveInt.one, - mapping = mapping, - protocolVersion = protocolVersion, - ) - ) - - GenerateExternalPartyTopologyResponse( - partyId = party.toProtoPrimitive, - publicKeyFingerprint = pubKey.fingerprint.toProtoPrimitive, - topologyTransactions = transactions.map(_.toByteString), - multiHash = MultiTransactionSignature - .computeCombinedHash( - transactions.map(_.hash).toSet, - syncService.hashOps, - ) - .getCryptographicEvidence, - ) - - } - response match { - case Left(err) => - Future.failed( - RequestValidationErrors.InvalidArgument - .Reject(err) - .asGrpcError - ) - case Right(resp) => Future.successful(resp) - } - } - -} - -private[apiserver] object ApiPartyManagementService { - - def blindAndConvertToProto( - identityProviderId: IdentityProviderId - ): ((IndexerPartyDetails, Option[PartyRecord])) => ProtoPartyDetails = { - case (details, recordO) if recordO.map(_.identityProviderId).contains(identityProviderId) => - toProtoPartyDetails( - partyDetails = details, - metadataO = recordO.map(_.metadata), - recordO.map(_.identityProviderId), - ) - case (details, _) if identityProviderId == IdentityProviderId.Default => - // For the Default IDP, `isLocal` flag is delivered as is. - toProtoPartyDetails(partyDetails = details, metadataO = None, identityProviderId = None) - case (details, _) => - // Expose the party, but blind the identity provider and report it as non-local. - toProtoPartyDetails( - partyDetails = details.copy(isLocal = false), - metadataO = None, - identityProviderId = None, - ) - } - - private def toProtoPartyDetails( - partyDetails: IndexerPartyDetails, - metadataO: Option[ObjectMeta], - identityProviderId: Option[IdentityProviderId], - ): ProtoPartyDetails = - ProtoPartyDetails( - party = partyDetails.party, - isLocal = partyDetails.isLocal, - localMetadata = Some(Utils.toProtoObjectMeta(metadataO.getOrElse(ObjectMeta.empty))), - identityProviderId = identityProviderId.map(_.toRequestString).getOrElse(""), - ) - - def createApiService( - partyManagementServiceBackend: IndexPartyManagementService, - userManagementStore: UserManagementStore, - identityProviderExists: IdentityProviderExists, - maxPartiesPageSize: PositiveInt, - maxSelfAllocatedParties: NonNegativeInt, - partyRecordStore: PartyRecordStore, - writeBackend: state.PartySyncService, - managementServiceTimeout: FiniteDuration, - submissionIdGenerator: CreateSubmissionId, - partyAllocationTracker: PartyAllocation.Tracker, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext, - tracer: Tracer, - ): PartyManagementServiceGrpc.PartyManagementService & GrpcApiService = - new ApiPartyManagementService( - partyManagementServiceBackend, - userManagementStore, - identityProviderExists, - maxPartiesPageSize, - maxSelfAllocatedParties, - partyRecordStore, - writeBackend, - managementServiceTimeout, - submissionIdGenerator, - partyAllocationTracker, - loggerFactory, - ) - - def decodePartyFromPageToken(pageToken: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[Ref.Party]] = - if (pageToken.isEmpty) { - Right(None) - } else { - val bytes = pageToken.getBytes(StandardCharsets.UTF_8) - for { - decodedBytes <- Try[Array[Byte]](Base64.getUrlDecoder.decode(bytes)).toEither.left - .map(_ => invalidPageToken("failed base64 decoding")) - tokenPayload <- Try[ListPartiesPageTokenPayload] { - ListPartiesPageTokenPayload.parseFrom(decodedBytes) - }.toEither.left - .map(_ => invalidPageToken("failed proto decoding")) - party <- Ref.Party - .fromString(tokenPayload.partyIdLowerBoundExcl) - .map(Some(_)) - .left - .map(_ => invalidPageToken("invalid party string in the token")) - } yield { - party - } - } - - private def invalidPageToken(details: String)(implicit - errorLogger: ErrorLoggingContext - ): StatusRuntimeException = { - errorLogger.info(s"Invalid page token: $details") - RequestValidationErrors.InvalidArgument - .Reject("Invalid page token") - .asGrpcError - } - - def encodeNextPageToken(token: Option[Ref.Party]): String = - token - .map { id => - val bytes = Base64.getUrlEncoder.encode( - ListPartiesPageTokenPayload(partyIdLowerBoundExcl = id).toByteArray - ) - new String(bytes, StandardCharsets.UTF_8) - } - .getOrElse("") - - trait CreateSubmissionId { - def apply( - partyId: LfPartyId, - authorizationLevel: AuthorizationLevel, - ): PartyAllocation.TrackerKey - } - - object CreateSubmissionId { - import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel - - def forParticipant(participantId: Ref.ParticipantId) = new CreateSubmissionId() { - override def apply( - partyId: LfPartyId, - authorizationLevel: AuthorizationLevel, - ): PartyAllocation.TrackerKey = - PartyAllocation.TrackerKey( - partyId, - participantId, - AuthorizationEvent.Added(authorizationLevel), - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiUserManagementService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiUserManagementService.scala deleted file mode 100644 index 1d1297c13e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiUserManagementService.scala +++ /dev/null @@ -1,672 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.daml.ledger.api.v2.admin.user_management_service as proto -import com.daml.ledger.api.v2.admin.user_management_service.{ - CreateUserResponse, - GetUserResponse, - UpdateUserIdentityProviderIdRequest, - UpdateUserIdentityProviderIdResponse, - UpdateUserRequest, - UpdateUserResponse, -} -import com.daml.platform.v1.page_tokens.ListUsersPageTokenPayload -import com.digitalasset.base.error.ErrorResource -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.validation.{FieldValidator, ValueValidator} -import com.digitalasset.canton.ledger.api.{ - IdentityProviderId, - ObjectMeta, - SubmissionIdGenerator, - User, - UserRight, -} -import com.digitalasset.canton.ledger.error.groups.{ - RequestValidationErrors, - UserManagementServiceErrors, -} -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.logging.LoggingContextUtil.createLoggingContext -import com.digitalasset.canton.logging.LoggingContextWithTrace.withEnrichedLoggingContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.platform.apiserver.update -import com.digitalasset.canton.platform.apiserver.update.UserUpdateMapper -import com.digitalasset.canton.tracing.TraceContextGrpc -import com.digitalasset.daml.lf.data.Ref -import io.grpc.{ServerServiceDefinition, StatusRuntimeException} -import scalaz.std.either.* -import scalaz.std.list.* -import scalaz.syntax.traverse.* - -import java.nio.charset.StandardCharsets -import java.util.Base64 -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -private[apiserver] final class ApiUserManagementService( - userManagementStore: UserManagementStore, - identityProviderExists: IdentityProviderExists, - partyRecordExist: PartyRecordsExist, - maxUsersPageSize: Int, - submissionIdGenerator: SubmissionIdGenerator, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext -) extends proto.UserManagementServiceGrpc.UserManagementService - with GrpcApiService - with NamedLogging - with AuthenticatedUserContextResolver { - - import ApiUserManagementService.* - import AuthenticatedUserContextResolver.* - import FieldValidator.* - import ValueValidator.* - - override def close(): Unit = () - - override def bindService(): ServerServiceDefinition = - proto.UserManagementServiceGrpc.bindService(this, executionContext) - - override def createUser(request: proto.CreateUserRequest): Future[CreateUserResponse] = - withSubmissionId(loggerFactory) { implicit loggingContext => - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContext) - // Retrieving the authenticated user context from the thread-local context - val authorizedUserContextF: Future[AuthenticatedUserContext] = - resolveAuthenticatedUserContext - withValidation { - for { - pUser <- requirePresence(request.user, "user") - pUserId <- requireUserId(pUser.id, "id") - pMetadata = pUser.metadata.getOrElse( - com.daml.ledger.api.v2.admin.object_meta.ObjectMeta( - resourceVersion = "", - annotations = Map.empty, - ) - ) - _ <- requireEmptyString( - pMetadata.resourceVersion, - "user.metadata.resource_version", - ) - pAnnotations <- verifyMetadataAnnotations( - pMetadata.annotations, - allowEmptyValues = false, - "user.metadata.annotations", - ) - pOptPrimaryParty <- optionalString(pUser.primaryParty)(requireParty) - pRights <- fromProtoRights(request.rights) - identityProviderId <- optionalIdentityProviderId( - pUser.identityProviderId, - "identity_provider_id", - ) - } yield ( - User( - id = pUserId, - primaryParty = pOptPrimaryParty, - isDeactivated = pUser.isDeactivated, - metadata = ObjectMeta( - resourceVersionO = None, - annotations = pAnnotations, - ), - identityProviderId = identityProviderId, - primaryPartyAuthentication = pUser.primaryPartyAuthentication, - ), - pRights, - ) - } { case (user, pRights) => - for { - _ <- identityProviderExistsOrError(user.identityProviderId) - authorizedUserContext <- authorizedUserContextF - _ <- verifyPartiesExistInIdp( - pRights, - user.identityProviderId, - authorizedUserContext.isParticipantAdmin, - ) - result <- userManagementStore - .createUser( - user = user, - rights = pRights, - ) - createdUser <- Utils.handleResult("creating user")(result) - } yield CreateUserResponse(Some(toProtoUser(createdUser))) - } - } - override def updateUser(request: UpdateUserRequest): Future[UpdateUserResponse] = - withSubmissionId(loggerFactory) { implicit loggingContext => - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContext) - val authorizedUserContextF: Future[AuthenticatedUserContext] = - resolveAuthenticatedUserContext - withValidation { - for { - pUser <- requirePresence(request.user, "user") - pUserId <- requireUserId(pUser.id, "user.id") - pMetadata = pUser.metadata.getOrElse( - com.daml.ledger.api.v2.admin.object_meta.ObjectMeta( - resourceVersion = "", - annotations = Map.empty, - ) - ) - pFieldMask <- requirePresence(request.updateMask, "update_mask") - pOptPrimaryParty <- optionalString(pUser.primaryParty)(requireParty) - identityProviderId <- optionalIdentityProviderId( - pUser.identityProviderId, - "identity_provider_id", - ) - pResourceVersion <- optionalString(pMetadata.resourceVersion)( - FieldValidator.requireResourceVersion(_, "user.metadata.resource_version") - ) - pAnnotations <- verifyMetadataAnnotations( - pMetadata.annotations, - allowEmptyValues = true, - "user.metadata.annotations", - ) - } yield ( - User( - id = pUserId, - primaryParty = pOptPrimaryParty, - isDeactivated = pUser.isDeactivated, - metadata = ObjectMeta( - resourceVersionO = pResourceVersion, - annotations = pAnnotations, - ), - identityProviderId = identityProviderId, - primaryPartyAuthentication = pUser.primaryPartyAuthentication, - ), - pFieldMask, - ) - } { case (user, fieldMask) => - for { - userUpdate <- handleUpdatePathResult(user.id, UserUpdateMapper.toUpdate(user, fieldMask)) - _ <- identityProviderExistsOrError(user.identityProviderId) - authorizedUserContext <- authorizedUserContextF - _ <- verifyPartiesExistInIdp( - Set(), - user.identityProviderId, - authorizedUserContext.isParticipantAdmin, - ) - _ <- - if ( - authorizedUserContext.userId - .contains(userUpdate.id) && userUpdate.isDeactivatedUpdateO.contains(true) - ) { - Future.failed( - RequestValidationErrors.InvalidArgument - .Reject( - "Requesting user cannot self-deactivate" - ) - .asGrpcError - ) - } else { - Future.unit - } - resp <- userManagementStore - .updateUser(userUpdate = userUpdate) - .flatMap(Utils.handleResult("updating user")) - .map { u => - UpdateUserResponse(user = Some(toProtoUser(u))) - } - } yield resp - } - } - - override def getUser(request: proto.GetUserRequest): Future[GetUserResponse] = { - implicit val loggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContextWithTrace) - withValidation { - for { - userId <- requireUserId(request.userId, "user_id") - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - } yield (userId, identityProviderId) - } { case (userId, identityProviderId) => - userManagementStore - .getUser(userId, identityProviderId) - .flatMap(Utils.handleResult("getting user")) - .map(u => GetUserResponse(Some(toProtoUser(u)))) - } - } - - override def deleteUser(request: proto.DeleteUserRequest): Future[proto.DeleteUserResponse] = - withSubmissionId(loggerFactory) { implicit loggingContext => - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContext) - val authorizedUserContextF: Future[AuthenticatedUserContext] = - resolveAuthenticatedUserContext - withValidation { - for { - userId <- requireUserId(request.userId, "user_id") - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - } yield (userId, identityProviderId) - } { case (userId, identityProviderId) => - for { - authorizedUserContext <- authorizedUserContextF - _ <- - if (authorizedUserContext.userId.contains(userId)) { - Future.failed( - RequestValidationErrors.InvalidArgument - .Reject( - "Requesting user cannot delete itself" - ) - .asGrpcError - ) - } else { - Future.unit - } - resp <- userManagementStore - .deleteUser(userId, identityProviderId) - .flatMap(Utils.handleResult("deleting user")) - .map(_ => proto.DeleteUserResponse()) - } yield resp - } - } - - override def listUsers(request: proto.ListUsersRequest): Future[proto.ListUsersResponse] = { - implicit val loggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContextWithTrace) - withValidation( - for { - fromExcl <- decodeUserIdFromPageToken(request.pageToken) - rawPageSize <- Either.cond( - request.pageSize >= 0, - request.pageSize, - RequestValidationErrors.InvalidArgument - .Reject("Max page size must be non-negative") - .asGrpcError, - ) - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - pageSize = - if (rawPageSize == 0) maxUsersPageSize - else Math.min(request.pageSize, maxUsersPageSize) - } yield { - (fromExcl, pageSize, identityProviderId) - } - ) { case (fromExcl, pageSize, identityProviderId) => - userManagementStore - .listUsers(fromExcl, pageSize, identityProviderId) - .flatMap(Utils.handleResult("listing users")) - .map { page => - val protoUsers = page.users.map(toProtoUser) - proto.ListUsersResponse( - protoUsers, - encodeNextPageToken(if (page.users.sizeIs < pageSize) None else page.lastUserIdOption), - ) - } - } - } - - override def grantUserRights( - request: proto.GrantUserRightsRequest - ): Future[proto.GrantUserRightsResponse] = withSubmissionId(loggerFactory) { - implicit loggingContext => - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContext) - // Retrieving the authenticated user context from the thread-local context - val authorizedUserContextF: Future[AuthenticatedUserContext] = - resolveAuthenticatedUserContext - withValidation( - for { - userId <- requireUserId(request.userId, "user_id") - rights <- fromProtoRights(request.rights) - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - } yield (userId, rights, identityProviderId) - ) { case (userId, rights, identityProviderId) => - for { - authorizedUserContext <- authorizedUserContextF - _ <- verifyPartiesExistInIdp( - rights, - identityProviderId, - authorizedUserContext.isParticipantAdmin, - ) - result <- userManagementStore - .grantRights( - id = userId, - rights = rights, - identityProviderId = identityProviderId, - ) - handledResult <- Utils.handleResult("grant user rights")(result) - } yield proto.GrantUserRightsResponse(handledResult.view.map(toProtoRight).toList) - } - } - - override def revokeUserRights( - request: proto.RevokeUserRightsRequest - ): Future[proto.RevokeUserRightsResponse] = withSubmissionId(loggerFactory) { - implicit loggingContext => - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContext) - // Retrieving the authenticated user context from the thread-local context - val authorizedUserContextF: Future[AuthenticatedUserContext] = - resolveAuthenticatedUserContext - withValidation( - for { - userId <- FieldValidator.requireUserId(request.userId, "user_id") - rights <- fromProtoRights(request.rights) - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - } yield (userId, rights, identityProviderId) - ) { case (userId, rights, identityProviderId) => - for { - authorizedUserContext <- authorizedUserContextF - _ <- verifyNotRemovingOwnAdminRights( - authorizedUserId = authorizedUserContext.userId.getOrElse(""), - userId = userId, - rights = rights, - ) - _ <- verifyPartiesExistInIdp( - rights, - identityProviderId, - authorizedUserContext.isParticipantAdmin, - ) - result <- userManagementStore - .revokeRights( - id = userId, - rights = rights, - identityProviderId = identityProviderId, - ) - handledResult <- Utils.handleResult("revoke user rights")(result) - } yield proto.RevokeUserRightsResponse(handledResult.view.map(toProtoRight).toList) - } - } - - override def listUserRights( - request: proto.ListUserRightsRequest - ): Future[proto.ListUserRightsResponse] = { - implicit val loggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContextWithTrace) - withValidation { - for { - userId <- requireUserId(request.userId, "user_id") - identityProviderId <- optionalIdentityProviderId( - request.identityProviderId, - "identity_provider_id", - ) - } yield (userId, identityProviderId) - } { case (userId, identityProviderId) => - userManagementStore - .listUserRights(userId, identityProviderId) - .flatMap(Utils.handleResult("list user rights")) - .map(_.view.map(toProtoRight).toList) - .map(proto.ListUserRightsResponse(_)) - } - } - override def updateUserIdentityProviderId( - request: UpdateUserIdentityProviderIdRequest - ): Future[UpdateUserIdentityProviderIdResponse] = { - implicit val loggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContextGrpc.fromGrpcContext) - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContextWithTrace) - withValidation { - for { - userId <- requireUserId(request.userId, "user_id") - sourceIdentityProviderId <- optionalIdentityProviderId( - request.sourceIdentityProviderId, - "source_identity_provider_id", - ) - targetIdentityProviderId <- optionalIdentityProviderId( - request.targetIdentityProviderId, - "target_identity_provider_id", - ) - } yield (userId, sourceIdentityProviderId, targetIdentityProviderId) - } { case (userId, sourceIdentityProviderId, targetIdentityProviderId) => - for { - _ <- identityProviderExistsOrError(sourceIdentityProviderId) - _ <- identityProviderExistsOrError(targetIdentityProviderId) - result <- userManagementStore - .updateUserIdp( - sourceIdp = sourceIdentityProviderId, - targetIdp = targetIdentityProviderId, - id = userId, - ) - .flatMap(Utils.handleResult("update user identity provider")) - .map(_ => proto.UpdateUserIdentityProviderIdResponse()) - } yield result - } - } - - private def handleUpdatePathResult[T](userId: Ref.UserId, result: update.Result[T])(implicit - errorLogger: ErrorLoggingContext - ): Future[T] = - result match { - case Left(e: update.UpdatePathError) => - Future.failed( - UserManagementServiceErrors.InvalidUpdateUserRequest - .Reject(userId = userId, e.getReason) - .asGrpcError - ) - case scala.util.Right(t) => - Future.successful(t) - } - - private def verifyPartiesExistInIdp( - rights: Set[UserRight], - identityProviderId: IdentityProviderId, - isParticipantAdmin: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace, - errorLogger: ErrorLoggingContext, - ): Future[Unit] = { - val parties = userParties(rights) - val partiesKnownF = - if (isParticipantAdmin) - Future.successful(parties) - else - partyRecordExist - .filterPartiesExistingInPartyRecordStore(identityProviderId, parties) - - partiesKnownF - .flatMap { partiesKnown => - val unknownParties = parties -- partiesKnown - if (unknownParties.isEmpty) - Future.unit - else - partiesNotExistsError(unknownParties, identityProviderId) - } - } - - private def verifyNotRemovingOwnAdminRights( - authorizedUserId: String, - userId: String, - rights: Set[UserRight], - )(implicit errorLogger: ErrorLoggingContext): Future[Unit] = - if ( - authorizedUserId == userId && rights.collect { case UserRight.ParticipantAdmin => - true - }.nonEmpty - ) - Future.failed( - RequestValidationErrors.InvalidArgument - .Reject( - "Requesting user cannot remove own admin rights" - ) - .asGrpcError - ) - else - Future.successful(()) - - private def partiesNotExistsError( - unknownParties: Set[Ref.Party], - identityProviderId: IdentityProviderId, - )(implicit errorLogger: ErrorLoggingContext) = { - val message = - s"Provided parties have not been found in " + - s"identity_provider_id=`${identityProviderId.toRequestString}`: [${unknownParties.mkString(",")}]." - Future.failed( - RequestValidationErrors.UnknownResource - .Reject(ErrorResource.Parties, unknownParties.toSeq, message) - .asGrpcError - ) - } - - private def identityProviderExistsOrError( - id: IdentityProviderId - )(implicit - loggingContext: LoggingContextWithTrace, - errorLogger: ErrorLoggingContext, - ): Future[Unit] = - identityProviderExists(id) - .flatMap { idpExists => - if (idpExists) - Future.unit - else - Future.failed( - RequestValidationErrors.InvalidArgument - .Reject(s"Provided identity_provider_id $id has not been found.") - .asGrpcError - ) - } - - private def userParties(rights: Set[UserRight]): Set[Ref.Party] = rights.collect { - case UserRight.CanActAs(party) => party - case UserRight.CanReadAs(party) => party - } - - private def withValidation[A, B](validatedResult: Either[StatusRuntimeException, A])( - f: A => Future[B] - ): Future[B] = - validatedResult.fold(Future.failed, Future.successful).flatMap(f) - - private def fromProtoRight( - right: proto.Right - )(implicit errorLogger: ErrorLoggingContext): Either[StatusRuntimeException, UserRight] = - right match { - case proto.Right(_: proto.Right.Kind.ParticipantAdmin) => - Right(UserRight.ParticipantAdmin) - - case proto.Right(_: proto.Right.Kind.IdentityProviderAdmin) => - Right(UserRight.IdentityProviderAdmin) - - case proto.Right(proto.Right.Kind.CanActAs(r)) => - requireParty(r.party).map(UserRight.CanActAs(_)) - - case proto.Right(proto.Right.Kind.CanReadAs(r)) => - requireParty(r.party).map(UserRight.CanReadAs(_)) - - case proto.Right(_: proto.Right.Kind.CanReadAsAnyParty) => - Right(UserRight.CanReadAsAnyParty) - - case proto.Right(proto.Right.Kind.CanExecuteAs(r)) => - requireParty(r.party).map(UserRight.CanExecuteAs(_)) - - case proto.Right(_: proto.Right.Kind.CanExecuteAsAnyParty) => - Right(UserRight.CanExecuteAsAnyParty) - - case proto.Right(proto.Right.Kind.Empty) => - Left( - RequestValidationErrors.InvalidArgument - .Reject( - "unknown kind of right - check that the Ledger API version of the server is recent enough" - ) - .asGrpcError - ) - } - - private def fromProtoRights( - rights: Seq[proto.Right] - )(implicit - errorLogger: ErrorLoggingContext - ): Either[StatusRuntimeException, Set[UserRight]] = - rights.toList.traverse(fromProtoRight).map(_.toSet) - - private def withSubmissionId[A](loggerFactory: NamedLoggerFactory)( - f: LoggingContextWithTrace => A - ): A = { - val loggingContext = createLoggingContext(loggerFactory)(identity) - withEnrichedLoggingContext(TraceContextGrpc.fromGrpcContext)( - "submissionId" -> submissionIdGenerator.generate() - )(f)( - loggingContext - ) - } - -} - -object ApiUserManagementService { - - private def toProtoUser(user: User): proto.User = - proto.User( - id = user.id, - primaryParty = user.primaryParty.getOrElse(""), - isDeactivated = user.isDeactivated, - metadata = Some(Utils.toProtoObjectMeta(user.metadata)), - identityProviderId = user.identityProviderId.toRequestString, - primaryPartyAuthentication = user.primaryPartyAuthentication, - ) - - private val toProtoRight: UserRight => proto.Right = { - case UserRight.ParticipantAdmin => - proto.Right(proto.Right.Kind.ParticipantAdmin(proto.Right.ParticipantAdmin())) - case UserRight.IdentityProviderAdmin => - proto.Right(proto.Right.Kind.IdentityProviderAdmin(proto.Right.IdentityProviderAdmin())) - case UserRight.CanActAs(party) => - proto.Right(proto.Right.Kind.CanActAs(proto.Right.CanActAs(party))) - case UserRight.CanReadAs(party) => - proto.Right(proto.Right.Kind.CanReadAs(proto.Right.CanReadAs(party))) - case UserRight.CanReadAsAnyParty => - proto.Right(proto.Right.Kind.CanReadAsAnyParty(proto.Right.CanReadAsAnyParty())) - case UserRight.CanExecuteAs(party) => - proto.Right(proto.Right.Kind.CanExecuteAs(proto.Right.CanExecuteAs(party))) - case UserRight.CanExecuteAsAnyParty => - proto.Right(proto.Right.Kind.CanExecuteAsAnyParty(proto.Right.CanExecuteAsAnyParty())) - } - - def encodeNextPageToken(token: Option[Ref.UserId]): String = - token - .map { id => - val bytes = Base64.getUrlEncoder.encode( - ListUsersPageTokenPayload(userIdLowerBoundExcl = id).toByteArray - ) - new String(bytes, StandardCharsets.UTF_8) - } - .getOrElse("") - - def decodeUserIdFromPageToken(pageToken: String)(implicit - loggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[Ref.UserId]] = - if (pageToken.isEmpty) { - Right(None) - } else { - val bytes = pageToken.getBytes(StandardCharsets.UTF_8) - for { - decodedBytes <- Try[Array[Byte]](Base64.getUrlDecoder.decode(bytes)).toEither.left - .map(_ => invalidPageToken) - tokenPayload <- Try[ListUsersPageTokenPayload] { - ListUsersPageTokenPayload.parseFrom(decodedBytes) - }.toEither.left - .map(_ => invalidPageToken) - userId <- Ref.UserId - .fromString(tokenPayload.userIdLowerBoundExcl) - .map(Some(_)) - .left - .map(_ => invalidPageToken) - } yield { - userId - } - } - - private def invalidPageToken(implicit - errorLogger: ErrorLoggingContext - ): StatusRuntimeException = - RequestValidationErrors.InvalidArgument - .Reject("Invalid page token") - .asGrpcError -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/AuthenticatedUserContextResolver.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/AuthenticatedUserContextResolver.scala deleted file mode 100644 index 85d0e69e25..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/AuthenticatedUserContextResolver.scala +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.digitalasset.canton.auth.ClaimSet.Claims -import com.digitalasset.canton.auth.{AuthInterceptor, ClaimAdmin, ClaimIdentityProviderAdmin} -import com.digitalasset.canton.ledger.error.LedgerApiErrors -import com.digitalasset.canton.logging.ErrorLoggingContext - -import scala.concurrent.Future - -trait AuthenticatedUserContextResolver { - import AuthenticatedUserContextResolver.* - def resolveAuthenticatedUserContext(implicit - errorLogger: ErrorLoggingContext - ): Future[AuthenticatedUserContext] = - AuthInterceptor - .extractClaimSetFromContext() - .fold( - fa = error => - Future.failed( - LedgerApiErrors.InternalError - .Generic("Could not extract a claim set from the context", throwableO = Some(error)) - .asGrpcError - ), - fb = { - case claims: Claims => - Future.successful(AuthenticatedUserContext(claims)) - case claimsSet => - Future.failed( - LedgerApiErrors.InternalError - .Generic( - s"Unexpected claims when trying to resolve the authenticated user: $claimsSet" - ) - .asGrpcError - ) - }, - ) -} - -object AuthenticatedUserContextResolver { - final case class AuthenticatedUserContext(claims: Claims) { - def userId: Option[String] = if (claims.resolvedFromUser) claims.userId else None - def isParticipantAdmin: Boolean = claims.claims.contains(ClaimAdmin) - def isIdpAdmin: Boolean = claims.claims.contains(ClaimIdentityProviderAdmin) - def isRegularUser: Boolean = claims.resolvedFromUser && !isParticipantAdmin && !isIdpAdmin - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/IdentityProviderExists.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/IdentityProviderExists.scala deleted file mode 100644 index ccccb81f5b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/IdentityProviderExists.scala +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.digitalasset.canton.ledger.api.IdentityProviderId -import com.digitalasset.canton.ledger.localstore.api.IdentityProviderConfigStore -import com.digitalasset.canton.logging.LoggingContextWithTrace - -import scala.concurrent.Future - -class IdentityProviderExists(identityProviderConfigStore: IdentityProviderConfigStore) { - def apply(id: IdentityProviderId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Boolean] = - id match { - case IdentityProviderId.Default => Future.successful(true) - case id: IdentityProviderId.Id => - identityProviderConfigStore.identityProviderConfigExists(id) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageUpgradeValidator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageUpgradeValidator.scala deleted file mode 100644 index 8cd76e7f22..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageUpgradeValidator.scala +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import cats.syntax.traverse.* -import com.digitalasset.canton.config.CacheConfigWithSizeOnly -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.topology.TopologyManagerError -import com.digitalasset.canton.topology.TopologyManagerError.ParticipantTopologyManagerError.* -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.PackageId -import com.digitalasset.daml.lf.language.Ast.PackageSignature -import com.digitalasset.daml.lf.language.Util.{ - PkgIdWithNameAndVersion, - dependenciesInTopologicalOrder, -} -import com.digitalasset.daml.lf.validation.{TypecheckUpgrades, UpgradeError} - -import scala.concurrent.ExecutionContext - -class PackageUpgradeValidator( - cacheConfig: CacheConfigWithSizeOnly, - val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext) - extends NamedLogging { - - private case class PackageIdAndSignature(packageId: PackageId, signature: PackageSignature) { - def version: Ref.PackageVersion = signature.metadata.version - def name: Ref.PackageName = signature.metadata.name - def supportsUpgrades: Boolean = signature.supportsUpgrades(packageId) - def directDeps: Set[PackageId] = signature.directDeps - def pkgIdWithNameAndVersion: PkgIdWithNameAndVersion = PkgIdWithNameAndVersion( - (packageId, signature) - ) - override def toString: String = s"$packageId ($name v$version)" - } - - private val upgradeCompatCache = - cacheConfig - .buildScaffeine(loggerFactory) - .build[(PackageId, PackageId), Either[TopologyManagerError, Unit]]() - - /** Validate the upgrade-compatibility of the vetted lineages that are affected by a new package - * to vet. That is, - * - the lineage of the new package itself - * - the lineage of each dependency, direct and transitive, of the new package - * - * This validation fails if: - * - a dependency is unknown (not in the package store) - * - a package claims to be daml-prim or daml-stdlib, but it is not a utility package - * - two distinct packages have the same name and version - * - a package in the affected lineages is upgrade-incompatible - * - * @param newPackagesToVet - * new packages to vet - * @param targetVettedPackages - * all packages in the next vetting state, including the new ones - * @param storedPackageMap - * all packages in the package store - * @param loggingContext - * @return - * a topology manager error if the validation fails, unit otherwise - */ - def validateUpgrade( - newPackagesToVet: Set[PackageId], - targetVettedPackages: Set[PackageId], - storedPackageMap: Map[PackageId, PackageSignature], - )(implicit - loggingContext: LoggingContextWithTrace - ): Either[TopologyManagerError, Unit] = { - // Sort the packages in topological order to get the dependencies first. This is useful to get - // the upgrade errors in a deterministic way. - val packagesInTopologicalOrder = - dependenciesInTopologicalOrder(newPackagesToVet.toList, storedPackageMap) - - // We ignore the dependencies that are not in the store. This is acceptable because - // later we check that all required dependencies are known. - val packageNamesToCheckInTopologicalOrder: Seq[Ref.PackageName] = - packagesInTopologicalOrder.flatMap(storedPackageMap.get).map(_.metadata.name).distinct - - // We don't know yet if the dependencies are upgrade-compatible because of force flags. - // Therefore we keep them all and check them. - val packageNamesToCheck = packageNamesToCheckInTopologicalOrder.toSet - - def getPackageToCheck(packageId: PackageId): Option[PackageIdAndSignature] = - // Here we ignore the package if it is not in the store. This is acceptable because - // later we check that all required dependencies are known. - storedPackageMap.get(packageId).collect { - case packageSig if packageNamesToCheck.contains(packageSig.metadata.name) => - PackageIdAndSignature(packageId, packageSig) - } - - // Get packages to check, group them by name, and sort them by version, to create their lineages - val lineagesToCheck: Map[Ref.PackageName, List[PackageIdAndSignature]] = - targetVettedPackages.toList - .flatMap(getPackageToCheck) - .groupBy(_.name) - .view - .mapValues(_.sortBy(pkg => (pkg.version, pkg.packageId))) - .toMap - - // validate the upgradeability of each lineage - packageNamesToCheckInTopologicalOrder - .filter( - lineagesToCheck.contains - ) // some lineage can be missing if unvetted dependencies are allowed - .traverse(name => validatePackageLineage(name, lineagesToCheck(name), storedPackageMap)) - .map(_ => ()) - } - - private def validatePackageLineage( - name: Ref.PackageName, - lineage: List[PackageIdAndSignature], - storedPackageMap: Map[PackageId, PackageSignature], - )(implicit - loggingContext: LoggingContextWithTrace - ): Either[TopologyManagerError, Unit] = { - logger.info( - s"Typechecking upgrades for lineage of package-name $name." - ) - val upgradingPairs: List[(PackageIdAndSignature, PackageIdAndSignature)] = - lineage - .filter(_.supportsUpgrades) - .sliding(2) - .collect { case fst :: snd :: Nil => (fst, snd) } - .toList - for { - _ <- lineage.traverse(validateDependencies(_, storedPackageMap)) - _ <- lineage.traverse(validateDamlPrimOrStdLib) - _ <- upgradingPairs.traverse { case (fst, snd) => validateVersion(fst, snd) } - _ <- upgradingPairs.traverse { case (fst, snd) => - cachedTypecheckUpgrades(fst, snd, storedPackageMap) - } - _ = logger.info(s"Typechecking upgrades for lineage of package-name $name succeeded.") - } yield () - } - - private def validateDependencies( - pkg: PackageIdAndSignature, - storedPackageMap: Map[PackageId, PackageSignature], - )(implicit loggingContext: LoggingContextWithTrace): Either[TopologyManagerError, Unit] = - pkg.directDeps.toSeq - .traverse { packageId => - // we cannot check the upgradability of a package if one of its dependency is unknown - storedPackageMap - .get(packageId) - .toRight(CannotVetDueToMissingPackages.Missing(Set(packageId))) - } - .map(_ => ()) - - private def validateDamlPrimOrStdLib( - pkg: PackageIdAndSignature - )(implicit loggingContext: LoggingContextWithTrace): Either[TopologyManagerError, Unit] = - Either.cond( - !pkg.signature.isInvalidDamlPrimOrStdlib(pkg.packageId), - (), - UpgradeDamlPrimIsNotAUtilityPackage.Error(pkg.pkgIdWithNameAndVersion), - ) - - private def validateVersion( - fst: PackageIdAndSignature, - snd: PackageIdAndSignature, - )(implicit loggingContext: LoggingContextWithTrace): Either[TopologyManagerError, Unit] = - Either.cond( - fst.version != snd.version, - (), - UpgradeVersion.Error(fst.pkgIdWithNameAndVersion, snd.pkgIdWithNameAndVersion), - ) - - private def cachedTypecheckUpgrades( - oldPackage: PackageIdAndSignature, - newPackage: PackageIdAndSignature, - storedPackageMap: Map[PackageId, PackageSignature], - )(implicit - loggingContext: LoggingContextWithTrace - ): Either[TopologyManagerError, Unit] = - upgradeCompatCache.get( - (oldPackage.packageId, newPackage.packageId), - _ => strictTypecheckUpgrades(oldPackage, newPackage, storedPackageMap), - ) - - private def strictTypecheckUpgrades( - oldPackage: PackageIdAndSignature, - newPackage: PackageIdAndSignature, - storedPackageMap: Map[PackageId, PackageSignature], - )(implicit - loggingContext: LoggingContextWithTrace - ): Either[TopologyManagerError, Unit] = { - logger.info(s"Package $newPackage claims to upgrade package $oldPackage") - TypecheckUpgrades - .typecheckUpgrades( - storedPackageMap, - (newPackage.packageId, newPackage.signature), - oldPackage.packageId, - Some(oldPackage.signature), - ) - .toEither - .left - .map { - case err: UpgradeError => - Upgradeability.Error( - newPackage = newPackage.pkgIdWithNameAndVersion, - oldPackage = oldPackage.pkgIdWithNameAndVersion, - upgradeError = err.prettyInternal, - ) - case unhandledErr => - TopologyManagerError.InternalError.Unhandled( - s"Typechecking upgrades from $oldPackage to $newPackage failed", - unhandledErr, - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PartyAllocation.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PartyAllocation.scala deleted file mode 100644 index ac58de1c5c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PartyAllocation.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.digitalasset.canton.LfPartyId -import com.digitalasset.canton.crypto.{Hash, HashAlgorithm, HashPurpose} -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.platform.apiserver.services.tracking.StreamTracker -import com.digitalasset.daml.lf.data.Ref - -object PartyAllocation { - - final case class TrackerKey( - partyId: LfPartyId, - participantId: Ref.ParticipantId, - authorizationEvent: AuthorizationEvent, - ) { - lazy val submissionId = { - val builder = Hash.build(HashPurpose.PartyUpdateId, HashAlgorithm.Sha256) - builder.addString(partyId) - builder.addString(participantId) - builder.addString(authorizationEvent.toString) - val hash = builder.finish() - - Ref.SubmissionId.assertFromString(hash.toHexString) - } - - // Override hashCode and equals to only consider submissionId for equality and hashing - // Needed for when they key is used in HashMaps etc... - override def hashCode(): Int = submissionId.hashCode - override def equals(obj: Any): Boolean = obj match { - case otherTrackerKey: TrackerKey => submissionId.equals(otherTrackerKey.submissionId) - case _ => false - } - } - final case class Completed(submissionId: TrackerKey, partyDetails: IndexerPartyDetails) - - type Tracker = StreamTracker[TrackerKey, Completed] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PartyRecordsExist.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PartyRecordsExist.scala deleted file mode 100644 index 9ebd5c06d1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PartyRecordsExist.scala +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.digitalasset.canton.ledger.api.IdentityProviderId -import com.digitalasset.canton.ledger.localstore.api.PartyRecordStore -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Ref - -import scala.concurrent.Future - -class PartyRecordsExist(partyRecordStore: PartyRecordStore) { - - def filterPartiesExistingInPartyRecordStore(id: IdentityProviderId, parties: Set[Ref.Party])( - implicit loggingContext: LoggingContextWithTrace - ): Future[Set[Ref.Party]] = - partyRecordStore.filterExistingParties(parties, id) - - def filterPartiesExistingInPartyRecordStore(parties: Set[Ref.Party])(implicit - loggingContext: LoggingContextWithTrace - ): Future[Set[Ref.Party]] = - partyRecordStore.filterExistingParties(parties) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PendingPartyAllocations.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PendingPartyAllocations.scala deleted file mode 100644 index 297b30b079..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/PendingPartyAllocations.scala +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.daml.lf.data.Ref - -import scala.collection.concurrent.{Map, TrieMap} -import scala.concurrent.{ExecutionContext, Future} - -class PendingPartyAllocations { - - private val pendingAllocations: Map[Ref.UserId, Int] = TrieMap.empty - - private def increment(user: Ref.UserId): Int = - pendingAllocations - .updateWith(user) { - case None => Some(1) - case Some(n) => Some(n + 1) - } - .fold(0)(identity) - - private def decrement(user: Ref.UserId): Int = - pendingAllocations - .updateWith(user) { - case Some(n) if n > 1 => Some(n - 1) - case _ => None - } - .fold(0)(identity) - - def withUser[T]( - user: Option[Ref.UserId] - )(f: Int => Future[T])(implicit executor: ExecutionContext): Future[T] = user match { - case None => f(0) - case Some(userId) => f(increment(userId)).thereafter(_ => decrement(userId).discard) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/Utils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/Utils.scala deleted file mode 100644 index 6e4bc9381b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/Utils.scala +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.daml.ledger.api.v2.admin as proto_admin -import com.digitalasset.canton.auth.AuthorizationChecksErrors -import com.digitalasset.canton.ledger.api.ObjectMeta -import com.digitalasset.canton.ledger.error.groups.UserManagementServiceErrors -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.logging.ErrorLoggingContext - -import scala.concurrent.Future - -object Utils { - def toProtoObjectMeta(meta: ObjectMeta): proto_admin.object_meta.ObjectMeta = - proto_admin.object_meta.ObjectMeta( - resourceVersion = serializeResourceVersion(meta.resourceVersionO), - annotations = meta.annotations, - ) - - private def serializeResourceVersion(resourceVersionO: Option[Long]): String = - resourceVersionO.fold("")(_.toString) - - def handleResult[T](operation: String)( - result: UserManagementStore.Result[T] - )(implicit errorLogger: ErrorLoggingContext): Future[T] = - result match { - case Left(UserManagementStore.PermissionDenied(id)) => - Future.failed( - AuthorizationChecksErrors.PermissionDenied - .Reject(s"User $id belongs to another Identity Provider") - .asGrpcError - ) - case Left(UserManagementStore.UserNotFound(id)) => - Future.failed( - UserManagementServiceErrors.UserNotFound - .Reject(operation, id) - .asGrpcError - ) - - case Left(UserManagementStore.UserDeletedWhileUpdating(id)) => - Future.failed( - UserManagementServiceErrors.UserDeletedWhileUpdating - .Reject(operation, id) - .asGrpcError - ) - - case Left(UserManagementStore.UserExists(id)) => - Future.failed( - UserManagementServiceErrors.UserAlreadyExists - .Reject(operation, id) - .asGrpcError - ) - - case Left(UserManagementStore.TooManyUserRights(id)) => - Future.failed( - UserManagementServiceErrors.TooManyUserRights - .Reject(operation, id: String) - .asGrpcError - ) - case Left(e: UserManagementStore.ConcurrentUserUpdate) => - Future.failed( - UserManagementServiceErrors.ConcurrentUserUpdateDetected - .Reject(userId = e.userId) - .asGrpcError - ) - - case Left(e: UserManagementStore.MaxAnnotationsSizeExceeded) => - Future.failed( - UserManagementServiceErrors.MaxUserAnnotationsSizeExceeded - .Reject(userId = e.userId) - .asGrpcError - ) - - case scala.util.Right(t) => - Future.successful(t) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/package.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/package.scala deleted file mode 100644 index 8b5ee92856..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/admin/package.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.digitalasset.daml.lf.data.Ref - -import java.util.UUID - -package object admin { - private[admin] def augmentSubmissionId(submissionId: String): Ref.SubmissionId = { - val uuid = UUID.randomUUID().toString - val raw = if (submissionId.isEmpty) uuid else s"$submissionId-$uuid" - Ref.SubmissionId.assertFromString(raw) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandInspectionServiceImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandInspectionServiceImpl.scala deleted file mode 100644 index 83307b15c5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandInspectionServiceImpl.scala +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command - -import com.daml.ledger.api.v2.admin.command_inspection_service.{ - CommandInspectionServiceGrpc, - CommandState, -} -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.services.CommandInspectionService -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.apiserver.execution.{CommandProgressTracker, CommandStatus} -import com.digitalasset.canton.platform.apiserver.services.admin.ApiCommandInspectionService -import io.grpc.ServerServiceDefinition - -import scala.concurrent.{ExecutionContext, Future} - -private[apiserver] final class CommandInspectionServiceImpl private ( - tracker: CommandProgressTracker, - val loggerFactory: NamedLoggerFactory, -) extends CommandInspectionService - with NamedLogging { - - override def findCommandStatus( - commandIdPrefix: String, - state: CommandState, - limit: Int, - ): Future[Seq[CommandStatus]] = - tracker.findCommandStatus(commandIdPrefix, state, limit) -} - -private[apiserver] object CommandInspectionServiceImpl { - - def createApiService( - tracker: CommandProgressTracker, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext - ): ApiCommandInspectionService & GrpcApiService = { - val impl: CommandInspectionService = - new CommandInspectionServiceImpl( - tracker, - loggerFactory, - ) - - new ApiCommandInspectionService( - impl, - loggerFactory, - ) with GrpcApiService { - override def bindService(): ServerServiceDefinition = - CommandInspectionServiceGrpc.bindService(this, executionContext) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandServiceImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandServiceImpl.scala deleted file mode 100644 index f094aa40dd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandServiceImpl.scala +++ /dev/null @@ -1,406 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command - -import com.daml.ledger.api.v2.command_service.* -import com.daml.ledger.api.v2.command_submission_service.{ - SubmitReassignmentRequest, - SubmitReassignmentResponse, - SubmitRequest, - SubmitResponse, -} -import com.daml.ledger.api.v2.commands.Commands -import com.daml.ledger.api.v2.reassignment_commands.ReassignmentCommands -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_LEDGER_EFFECTS -import com.daml.ledger.api.v2.transaction_filter.{Filters, TransactionFormat, UpdateFormat} -import com.daml.ledger.api.v2.update_service.{GetUpdateByIdRequest, GetUpdateResponse} -import com.digitalasset.canton.config -import com.digitalasset.canton.ledger.api.SubmissionIdGenerator -import com.digitalasset.canton.ledger.api.grpc.GrpcApiService -import com.digitalasset.canton.ledger.api.services.CommandService -import com.digitalasset.canton.ledger.api.util.TimeProvider -import com.digitalasset.canton.ledger.api.validation.CommandsValidator -import com.digitalasset.canton.ledger.error.CommonErrors -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, - TracedLogger, -} -import com.digitalasset.canton.platform.apiserver.services.command.CommandServiceImpl.* -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker.SubmissionKey -import com.digitalasset.canton.platform.apiserver.services.tracking.{ - CompletionResponse, - SubmissionTracker, -} -import com.digitalasset.canton.platform.apiserver.services.{ApiCommandService, logging} -import com.digitalasset.canton.tracing.{TraceContext, Traced} -import io.grpc.{Context, Deadline, Status} - -import java.time.Instant -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import scala.concurrent.duration.Duration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -private[apiserver] final class CommandServiceImpl private[services] ( - updateServices: UpdateServices, - transactionSubmissionTracker: SubmissionTracker, - reassignmentSubmissionTracker: SubmissionTracker, - submit: Traced[SubmitRequest] => FutureUnlessShutdown[SubmitResponse], - submitReassignment: Traced[SubmitReassignmentRequest] => FutureUnlessShutdown[ - SubmitReassignmentResponse - ], - defaultTrackingTimeout: config.NonNegativeFiniteDuration, - val loggerFactory: NamedLoggerFactory, -)(implicit - executionContext: ExecutionContext -) extends CommandService - with AutoCloseable - with NamedLogging { - - private val running = new AtomicBoolean(true) - - override def close(): Unit = { - logger.info("Shutting down Command Service.")(TraceContext.empty) - running.set(false) - transactionSubmissionTracker.close() - reassignmentSubmissionTracker.close() - } - - def submitAndWait( - request: SubmitAndWaitRequest - )(loggingContext: LoggingContextWithTrace): Future[SubmitAndWaitResponse] = - withCommandsLoggingContext(request.getCommands, loggingContext) { (errorLogger, traceContext) => - submitAndWaitInternal(request.commands)(errorLogger, traceContext).map { response => - SubmitAndWaitResponse.of( - updateId = response.completion.updateId, - completionOffset = response.completion.offset, - ) - } - } - - def submitAndWaitForTransaction( - request: SubmitAndWaitForTransactionRequest - )(loggingContext: LoggingContextWithTrace): Future[SubmitAndWaitForTransactionResponse] = - withCommandsLoggingContext(request.getCommands, loggingContext) { (errorLogger, traceContext) => - implicit val implicitTraceContext = traceContext - submitAndWaitInternal(request.commands)(errorLogger, traceContext).flatMap { resp => - CommandServiceImpl - .fetchTransactionFromCompletion( - resp = resp, - transactionFormat = request.transactionFormat, - updateServices = updateServices, - logger = logger, - ) - .map { updateResponse => - SubmitAndWaitForTransactionResponse - .of( - updateResponse.update.transaction - ) - } - } - } - - def submitAndWaitForReassignment( - request: SubmitAndWaitForReassignmentRequest - )(loggingContext: LoggingContextWithTrace): Future[SubmitAndWaitForReassignmentResponse] = - withReassignmentCommandsLoggingContext(request.getReassignmentCommands, loggingContext) { - (errorLogger, traceContext) => - submitAndWaitForReassignmentInternal(request.reassignmentCommands)( - errorLogger, - traceContext, - ) - .flatMap { resp => - val updateId = resp.completion.updateId - val txRequest = GetUpdateByIdRequest( - updateId = updateId, - updateFormat = Some( - UpdateFormat( - includeTransactions = None, - includeReassignments = request.eventFormat, - includeTopologyEvents = None, - ) - ), - ) - updateServices - .getUpdateById(txRequest) - .map(updateResponse => - SubmitAndWaitForReassignmentResponse - .of( - updateResponse.update.reassignment - ) - ) - } - } - - private def submitAndWaitInternal( - commands: Option[Commands] - )(implicit - errorLogger: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[CompletionResponse] = { - def ifServiceRunning: Future[Unit] = - if (!running.get()) - Future.failed( - CommonErrors.ServiceNotRunning.Reject("Command Service")(errorLogger).asGrpcError - ) - else Future.unit - - def ensureCommandsPopulated: Commands = - commands.getOrElse( - throw new IllegalArgumentException("Missing commands field in request") - ) - - def submitAndTrack( - commands: Commands, - nonNegativeTimeout: config.NonNegativeFiniteDuration, - ): Future[CompletionResponse] = - transactionSubmissionTracker.track( - submissionKey = SubmissionKey( - commandId = commands.commandId, - submissionId = commands.submissionId, - userId = commands.userId, - parties = commands.actAs.toSet, - ), - timeout = nonNegativeTimeout, - submit = childContext => submit(Traced(SubmitRequest(Some(commands)))(childContext)), - )(errorLogger, traceContext) - - // Capture deadline before thread switching in Future for-comprehension - val deadlineO = Option(Context.current().getDeadline) - for { - _ <- ifServiceRunning - commands = ensureCommandsPopulated - nonNegativeTimeout <- Future.fromTry( - validateRequestTimeout( - deadlineO, - commands.commandId, - commands.submissionId, - defaultTrackingTimeout, - )( - errorLogger - ) - ) - result <- submitAndTrack(commands, nonNegativeTimeout) - } yield result - } - - private def submitAndWaitForReassignmentInternal( - commands: Option[ReassignmentCommands] - )(implicit - errorLogger: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[CompletionResponse] = { - def ifServiceRunning: Future[Unit] = - if (!running.get()) - Future.failed( - CommonErrors.ServiceNotRunning.Reject("Command Service")(errorLogger).asGrpcError - ) - else Future.unit - - def ensureCommandsPopulated: ReassignmentCommands = - commands.getOrElse( - throw new IllegalArgumentException("Missing commands field in request") - ) - - def submitAndTrack( - commands: ReassignmentCommands, - nonNegativeTimeout: config.NonNegativeFiniteDuration, - ): Future[CompletionResponse] = - reassignmentSubmissionTracker.track( - submissionKey = SubmissionKey.fromReassignmentCommands(commands), - timeout = nonNegativeTimeout, - submit = childContext => - submitReassignment(Traced(SubmitReassignmentRequest(Some(commands)))(childContext)), - )(errorLogger, traceContext) - - // Capture deadline before thread switching in Future for-comprehension - val deadlineO = Option(Context.current().getDeadline) - for { - _ <- ifServiceRunning - commands = ensureCommandsPopulated - nonNegativeTimeout <- Future.fromTry( - validateRequestTimeout( - deadlineO, - commands.commandId, - commands.submissionId, - defaultTrackingTimeout, - )( - errorLogger - ) - ) - result <- submitAndTrack(commands, nonNegativeTimeout) - } yield result - } - - private def withCommandsLoggingContext[T]( - commands: Commands, - loggingContextWithTrace: LoggingContextWithTrace, - )( - submitWithContext: (ErrorLoggingContext, TraceContext) => Future[T] - ): Future[T] = - LoggingContextWithTrace.withEnrichedLoggingContext( - logging.submissionId(commands.submissionId), - logging.commandId(commands.commandId), - logging.actAsStrings(commands.actAs), - logging.readAsStrings(commands.readAs), - ) { loggingContext => - submitWithContext( - ErrorLoggingContext.withExplicitCorrelationId( - logger, - loggingContext.toPropertiesMap, - loggingContext.traceContext, - commands.submissionId, - ), - loggingContext.traceContext, - ) - }(loggingContextWithTrace) - - private def withReassignmentCommandsLoggingContext[T]( - commands: ReassignmentCommands, - loggingContextWithTrace: LoggingContextWithTrace, - )( - submitWithContext: (ErrorLoggingContext, TraceContext) => Future[T] - ): Future[T] = - LoggingContextWithTrace.withEnrichedLoggingContext( - logging.submissionId(commands.submissionId), - logging.commandId(commands.commandId), - logging.submitter(commands.submitter), - ) { loggingContext => - submitWithContext( - ErrorLoggingContext.withExplicitCorrelationId( - logger, - loggingContext.toPropertiesMap, - loggingContext.traceContext, - commands.submissionId, - ), - loggingContext.traceContext, - ) - }(loggingContextWithTrace) -} - -private[apiserver] object CommandServiceImpl { - - def createApiService( - transactionSubmissionTracker: SubmissionTracker, - reassignmentSubmissionTracker: SubmissionTracker, - commandsValidator: CommandsValidator, - submit: Traced[SubmitRequest] => FutureUnlessShutdown[SubmitResponse], - submitReassignment: Traced[SubmitReassignmentRequest] => FutureUnlessShutdown[ - SubmitReassignmentResponse - ], - defaultTrackingTimeout: config.NonNegativeFiniteDuration, - updateServices: UpdateServices, - timeProvider: TimeProvider, - maxDeduplicationDuration: config.NonNegativeFiniteDuration, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext - ): CommandServiceGrpc.CommandService & GrpcApiService = - new ApiCommandService( - service = new CommandServiceImpl( - updateServices, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - submit, - submitReassignment, - defaultTrackingTimeout, - loggerFactory, - ), - commandsValidator = commandsValidator, - currentLedgerTime = () => timeProvider.getCurrentTime, - currentUtcTime = () => Instant.now, - maxDeduplicationDuration = maxDeduplicationDuration.asJava, - generateSubmissionId = SubmissionIdGenerator.Random, - loggerFactory = loggerFactory, - ) - - final class UpdateServices( - val getUpdateById: GetUpdateByIdRequest => Future[GetUpdateResponse] - ) - - private[apiserver] def validateRequestTimeout( - grpcRequestDeadline: Option[Deadline], - commandId: String, - submissionId: String, - defaultTrackingTimeout: config.NonNegativeFiniteDuration, - )(implicit errorLogger: ErrorLoggingContext): Try[config.NonNegativeFiniteDuration] = - grpcRequestDeadline.map(_.timeRemaining(TimeUnit.NANOSECONDS)) match { - case None => Success(defaultTrackingTimeout) - case Some(remainingDeadlineNanos) if remainingDeadlineNanos >= 0 => - Success( - config.NonNegativeFiniteDuration(Duration(remainingDeadlineNanos, TimeUnit.NANOSECONDS)) - ) - case Some(remainingDeadlineNanos) => - Failure( - CommonErrors.RequestDeadlineExceeded - .Reject( - Duration.fromNanos(Math.abs(remainingDeadlineNanos)), - commandId = commandId, - submissionId = submissionId, - )(errorLogger) - .asGrpcError - ) - } - - private[apiserver] def fetchTransactionFromCompletion( - resp: CompletionResponse, - transactionFormat: Option[TransactionFormat], - updateServices: UpdateServices, - logger: TracedLogger, - )(implicit - traceContext: TraceContext, - executionContext: ExecutionContext, - ): Future[GetUpdateResponse] = { - val updateId = resp.completion.updateId - val txRequest = GetUpdateByIdRequest( - updateId = updateId, - updateFormat = Some( - UpdateFormat( - includeTransactions = transactionFormat, - includeReassignments = None, - includeTopologyEvents = None, - ) - ), - ) - updateServices - .getUpdateById(txRequest) - .recoverWith { - case e: io.grpc.StatusRuntimeException - if e.getStatus.getCode == Status.Code.NOT_FOUND - && e.getStatus.getDescription.contains( - RequestValidationErrors.NotFound.Update.id - ) => - logger.debug( - s"Transaction not found in update lookup for updateId $updateId, falling back to LedgerEffects lookup without events." - )(traceContext) - // When a command submission completes successfully, - // the submitters can end up getting an UPDATE_NOT_FOUND when querying its corresponding AcsDelta - // transaction that either: - // * has only non-consuming events - // * has only events of contracts which have stakeholders that are not amongst the requesting parties - // or in general when filters defined in the transactionFormat exclude all the events from the - // transaction. - // In these situations, we fallback to a LedgerEffects transaction lookup with a wildcard filter and - // populate the transaction response with its details but no events. - updateServices - .getUpdateById( - txRequest - .update( - _.updateFormat.includeTransactions.transactionShape := TRANSACTION_SHAPE_LEDGER_EFFECTS, - _.updateFormat.includeTransactions.eventFormat.filtersForAnyParty := Filters( - Nil - ), - ) - ) - .map(_.update(_.transaction.modify(_.clearEvents))) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandSubmissionServiceImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandSubmissionServiceImpl.scala deleted file mode 100644 index 5dfec8eed8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandSubmissionServiceImpl.scala +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command - -import cats.data.EitherT -import com.daml.timer.Delayed -import com.digitalasset.base.error.ErrorCode.LoggedApiException -import com.digitalasset.canton.ledger.api.messages.command.submission.SubmitRequest -import com.digitalasset.canton.ledger.api.services.CommandSubmissionService -import com.digitalasset.canton.ledger.api.util.{TimeProvider, TimeProviderType} -import com.digitalasset.canton.ledger.api.{Commands as ApiCommands, SubmissionId} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, UnlessShutdown} -import com.digitalasset.canton.logging.LoggingContextWithTrace.{ - implicitExtractTraceContext, - withEnrichedLoggingContext, -} -import com.digitalasset.canton.logging.TracedLoggerOps.TracedLoggerOps -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.SeedService -import com.digitalasset.canton.platform.apiserver.execution.{ - CommandExecutionResult, - CommandExecutor, -} -import com.digitalasset.canton.platform.apiserver.services.{RejectionGenerators, logging} -import com.digitalasset.canton.tracing.{Spanning, TraceContext} -import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.daml.lf.command.ApiCommand -import com.digitalasset.daml.lf.crypto -import io.opentelemetry.api.trace.Tracer - -import java.time.Duration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -private[apiserver] object CommandSubmissionServiceImpl { - - def createApiService( - syncService: state.SyncService, - timeProvider: TimeProvider, - timeProviderType: TimeProviderType, - seedService: SeedService, - commandExecutor: CommandExecutor, - checkOverloaded: TraceContext => Option[state.SubmissionResult], - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext, - tracer: Tracer, - ): CommandSubmissionService & AutoCloseable = new CommandSubmissionServiceImpl( - syncService, - timeProvider, - timeProviderType, - seedService, - commandExecutor, - checkOverloaded, - metrics, - loggerFactory, - ) -} - -private[apiserver] final class CommandSubmissionServiceImpl private[services] ( - syncService: state.SyncService, - timeProvider: TimeProvider, - timeProviderType: TimeProviderType, - seedService: SeedService, - commandExecutor: CommandExecutor, - checkOverloaded: TraceContext => Option[state.SubmissionResult], - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext, tracer: Tracer) - extends CommandSubmissionService - with AutoCloseable - with Spanning - with NamedLogging { - - override def submit( - request: SubmitRequest - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[Unit] = - withEnrichedLoggingContext(logging.commands(request.commands)) { implicit loggingContext => - logger.info( - show"Phase 1 started: Submitting ${request.commands.commands.commands.length} command(s) for interpretation on behalf of ${request.commands.actAs - .limit(5) - .toSeq}." - ) - val cmds = request.commands.commands.commands - logger.debug(show"Submitted commands are: ${if (cmds.length > 1) "\n " else ""}${cmds - .map { - case ApiCommand.Create(templateRef, _) => - s"create ${templateRef.qualifiedName}" - case ApiCommand.Exercise(templateRef, _, choiceId, _) => - s"exercise @${templateRef.qualifiedName} $choiceId" - case ApiCommand.ExerciseByKey(templateRef, _, choiceId, _) => - s"exerciseByKey @${templateRef.qualifiedName} $choiceId" - case ApiCommand.CreateAndExercise(templateRef, _, choiceId, _) => - s"createAndExercise ${templateRef.qualifiedName} ... $choiceId ..." - } - .map(_.singleQuoted) - .toSeq - .mkString("\n ")}") - - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext.fromOption( - logger, - loggingContext, - request.commands.submissionId.map(SubmissionId.unwrap), - ) - - val evaluatedCommand = - evaluateAndSubmit(seedService.nextSeed(), request.commands) - .transform(handleSubmissionResult) - evaluatedCommand.thereafter(logger.logErrorsOnCall[UnlessShutdown[Unit]]) - } - - private def handleSubmissionResult(result: Try[UnlessShutdown[state.SubmissionResult]])(implicit - loggingContext: LoggingContextWithTrace - ): Try[UnlessShutdown[Unit]] = { - import state.SubmissionResult.* - result match { - case Success(UnlessShutdown.Outcome(Acknowledged)) => - logger.debug("Submission acknowledged by sync-service.") - Success(UnlessShutdown.unit) - - case Success(UnlessShutdown.Outcome(result: SynchronousError)) => - logger.info(s"Rejected: ${result.description}") - Failure(result.exception) - - case Success(UnlessShutdown.AbortedDueToShutdown) => - Success(UnlessShutdown.AbortedDueToShutdown) - // Do not log again on errors that are logging on creation - case Failure(error: LoggedApiException) => Failure(error) - case Failure(error) => - logger.info(s"Rejected: ${error.getMessage}") - Failure(error) - } - } - - private def evaluateAndSubmit( - submissionSeed: crypto.Hash, - commands: ApiCommands, - )(implicit - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): FutureUnlessShutdown[state.SubmissionResult] = - checkOverloaded(loggingContext.traceContext) - .map(FutureUnlessShutdown.pure) - .getOrElse( - withSpan("ApiSubmissionService.evaluate") { _ => _ => - for { - synchronizerState <- EitherT.liftF(syncService.getRoutingSynchronizerState) - result <- commandExecutor.execute( - commands = commands, - submissionSeed = submissionSeed, - routingSynchronizerState = synchronizerState, - forExternallySigned = false, - ) - } yield result - } - .semiflatMap(submitTransactionWithDelay) - .valueOrF { error => - metrics.commands.failedCommandInterpretations.mark() - RejectionGenerators.commandExecutorErrorFUS(error) - } - ) - - private def submitTransactionWithDelay( - commandExecutionResult: CommandExecutionResult - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[state.SubmissionResult] = FutureUnlessShutdown.outcomeF { - timeProviderType match { - case TimeProviderType.WallClock => - // Submit transactions such that they arrive at the ledger sequencer exactly when record time equals ledger time. - // If the ledger time of the transaction is far in the future (farther than the expected latency), - // the submission to the SyncService is delayed. - val submitAt = - commandExecutionResult.commandInterpretationResult.transactionMeta.ledgerEffectiveTime.toInstant - val submissionDelay = Duration.between(timeProvider.getCurrentTime, submitAt) - if (submissionDelay.isNegative) - submitTransaction(commandExecutionResult) - else { - logger.info(s"Delaying submission by $submissionDelay") - metrics.commands.delayedSubmissions.mark() - val scalaDelay = scala.concurrent.duration.Duration.fromNanos(submissionDelay.toNanos) - Delayed.Future.by(scalaDelay)(submitTransaction(commandExecutionResult)) - } - case TimeProviderType.Static => - // In static time mode, record time is always equal to ledger time - submitTransaction(commandExecutionResult) - } - } - - private def submitTransaction( - result: CommandExecutionResult - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[state.SubmissionResult] = { - metrics.commands.validSubmissions.mark() - logger.trace("Submitting transaction to ledger.") - syncService - .submitTransaction( - result.commandInterpretationResult.transaction, - result.synchronizerRank, - result.routingSynchronizerState, - result.commandInterpretationResult.submitterInfo, - result.commandInterpretationResult.transactionMeta, - result.commandInterpretationResult.interpretationTimeNanos, - result.commandInterpretationResult.globalKeyMapping, - result.commandInterpretationResult.processedDisclosedContracts, - ) - } - - override def close(): Unit = () -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/CostEstimationHints.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/CostEstimationHints.scala deleted file mode 100644 index b5d92daae4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/CostEstimationHints.scala +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive - -import cats.syntax.traverse.* -import com.daml.ledger.api.v2.interactive.interactive_submission_service.CostEstimationHints as CostEstimationHintsP -import com.digitalasset.canton.crypto.SigningAlgorithmSpec -import com.digitalasset.canton.ledger.api.validation.CryptoValidator -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException - -/** Class holding hints used to provide a better traffic cost estimation of prepared transactions - * @param signingAlgorithmSpec - * the number of signatures and their signing algorithm expected to be used at submission time. - */ -final case class CostEstimationHints( - signingAlgorithmSpec: Seq[SigningAlgorithmSpec] = Seq.empty -) - -object CostEstimationHints { - def fromProto( - estimationP: CostEstimationHintsP - )(implicit - elc: ErrorLoggingContext - ): Either[StatusRuntimeException, Option[CostEstimationHints]] = - Option - .when(!estimationP.disabled) { - estimationP.expectedSignatures - .traverse( - CryptoValidator.validateSigningAlgorithmSpec(_, "estimate_traffic_cost")(elc) - ) - .map(CostEstimationHints(_)) - } - .sequence -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/InteractiveSubmissionServiceImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/InteractiveSubmissionServiceImpl.scala deleted file mode 100644 index a0cbae3311..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/InteractiveSubmissionServiceImpl.scala +++ /dev/null @@ -1,493 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive - -import cats.data.EitherT -import cats.syntax.bifunctor.* -import cats.syntax.either.* -import cats.syntax.traverse.* -import com.daml.ledger.api.v2.interactive.interactive_submission_service as proto -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{ - CostEstimation, - ExecuteSubmissionAndWaitForTransactionResponse, - ExecuteSubmissionAndWaitResponse, -} -import com.daml.ledger.api.v2.transaction_filter.TransactionFormat -import com.daml.ledger.api.v2.update_service.GetUpdateResponse -import com.digitalasset.base.error.ErrorCode.LoggedApiException -import com.digitalasset.base.error.RpcError -import com.digitalasset.canton.LfTimestamp -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.interactive.InteractiveSubmissionEnricher -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService.{ - ExecuteRequest, - PrepareRequest as PrepareRequestInternal, -} -import com.digitalasset.canton.ledger.api.validation.GetPreferredPackagesRequestValidator.PackageVettingRequirements -import com.digitalasset.canton.ledger.api.{Commands as ApiCommands, PackageReference, SubmissionId} -import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors.{ - InteractiveSubmissionExecuteError, - InteractiveSubmissionPreparationError, -} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.SubmissionResult -import com.digitalasset.canton.ledger.participant.state.index.ContractStore -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.* -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.PackagePreferenceBackend -import com.digitalasset.canton.platform.apiserver.SeedService -import com.digitalasset.canton.platform.apiserver.execution.{ - CommandExecutionResult, - CommandExecutor, -} -import com.digitalasset.canton.platform.apiserver.services.command.CommandServiceImpl -import com.digitalasset.canton.platform.apiserver.services.command.CommandServiceImpl.{ - UpdateServices, - validateRequestTimeout, -} -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.ExternalTransactionProcessor -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker.SubmissionKey -import com.digitalasset.canton.platform.apiserver.services.tracking.{ - CompletionResponse, - SubmissionTracker, -} -import com.digitalasset.canton.platform.apiserver.services.{RejectionGenerators, logging} -import com.digitalasset.canton.platform.config.InteractiveSubmissionServiceConfig -import com.digitalasset.canton.protocol.hash.HashTracer -import com.digitalasset.canton.topology.{PhysicalSynchronizerId, SynchronizerId} -import com.digitalasset.canton.tracing.{Spanning, TraceContext} -import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.util.{EitherTUtil, TryUtil} -import com.digitalasset.canton.version.{HashingSchemeVersion, HashingSchemeVersionConverter} -import com.digitalasset.daml.lf.command.ApiCommand -import com.digitalasset.daml.lf.crypto -import io.grpc.Context -import io.opentelemetry.api.trace.Tracer - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -private[apiserver] object InteractiveSubmissionServiceImpl { - - def createApiService( - updateServices: UpdateServices, - submissionSyncService: state.SyncService, - seedService: SeedService, - commandExecutor: CommandExecutor, - metrics: LedgerApiServerMetrics, - checkOverloaded: TraceContext => Option[state.SubmissionResult], - interactiveSubmissionEnricher: InteractiveSubmissionEnricher, - config: InteractiveSubmissionServiceConfig, - contractStore: ContractStore, - packagePreferenceBackend: PackagePreferenceBackend, - transactionSubmissionTracker: SubmissionTracker, - defaultTrackingTimeout: NonNegativeFiniteDuration, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext, - tracer: Tracer, - ): InteractiveSubmissionService & AutoCloseable = new InteractiveSubmissionServiceImpl( - updateServices, - submissionSyncService, - seedService, - commandExecutor, - metrics, - checkOverloaded, - interactiveSubmissionEnricher, - config, - contractStore, - packagePreferenceBackend, - transactionSubmissionTracker, - defaultTrackingTimeout, - loggerFactory, - ) - -} - -private[apiserver] final class InteractiveSubmissionServiceImpl private[services] ( - updateServices: UpdateServices, - syncService: state.SyncService, - seedService: SeedService, - commandExecutor: CommandExecutor, - metrics: LedgerApiServerMetrics, - checkOverloaded: TraceContext => Option[state.SubmissionResult], - interactiveSubmissionEnricher: InteractiveSubmissionEnricher, - config: InteractiveSubmissionServiceConfig, - contractStore: ContractStore, - packagePreferenceService: PackagePreferenceBackend, - transactionSubmissionTracker: SubmissionTracker, - defaultTrackingTimeout: NonNegativeFiniteDuration, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext, tracer: Tracer) - extends InteractiveSubmissionService - with AutoCloseable - with Spanning - with NamedLogging { - - private val externalTransactionProcessor = new ExternalTransactionProcessor( - interactiveSubmissionEnricher, - contractStore, - syncService, - config, - loggerFactory, - ) - - override def prepare( - request: PrepareRequestInternal - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[proto.PrepareSubmissionResponse] = - withEnrichedLoggingContext(logging.commands(request.commands)) { implicit loggingContext => - logger.info( - s"Requesting preparation of daml transaction with command ID ${request.commands.commandId}" - ) - val cmds = request.commands.commands.commands - // TODO(i20726): make sure this does not leak information - logger.debug( - show"Submitted commands for prepare are: ${if (cmds.length > 1) "\n " else ""}${cmds - .map { - case ApiCommand.Create(templateRef, _) => - s"create ${templateRef.qualifiedName}" - case ApiCommand.Exercise(templateRef, _, choiceId, _) => - s"exercise @${templateRef.qualifiedName} $choiceId" - case ApiCommand.ExerciseByKey(templateRef, _, choiceId, _) => - s"exerciseByKey @${templateRef.qualifiedName} $choiceId" - case ApiCommand.CreateAndExercise(templateRef, _, choiceId, _) => - s"createAndExercise ${templateRef.qualifiedName} ... $choiceId ..." - } - .map(_.singleQuoted) - .toSeq - .mkString("\n ")}" - ) - - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext.fromOption( - logger, - loggingContext, - request.commands.submissionId.map(SubmissionId.unwrap), - ) - - if (config.enforceSingleRootNode && cmds.length > 1) { - FutureUnlessShutdown.failed( - InteractiveSubmissionPreparationError - .Reject("Preparing multiple commands is currently not supported") - .asGrpcError - ) - } else { - evaluateAndHash( - seedService.nextSeed(), - request.commands, - request.verboseHashing, - request.maxRecordTime, - request.costEstimationHints, - request.hashingSchemeVersion, - ) - } - } - - private def evaluateAndHash( - submissionSeed: crypto.Hash, - commands: ApiCommands, - verboseHashing: Boolean, - maxRecordTime: Option[LfTimestamp], - costEstimationHints: Option[CostEstimationHints], - hashingSchemeVersion: HashingSchemeVersion, - )(implicit - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): FutureUnlessShutdown[proto.PrepareSubmissionResponse] = { - val result: EitherT[FutureUnlessShutdown, RpcError, proto.PrepareSubmissionResponse] = for { - commandExecutionResult <- withSpan("InteractiveSubmissionService.evaluate") { _ => _ => - for { - synchronizerState <- EitherT.liftF(syncService.getRoutingSynchronizerState) - result <- commandExecutor - .execute( - commands = commands, - submissionSeed = submissionSeed, - routingSynchronizerState = synchronizerState, - forExternallySigned = true, - ) - .leftFlatMap { errCause => - metrics.commands.failedCommandInterpretations.mark() - EitherT.right[RpcError]( - RejectionGenerators.commandExecutorErrorFUS[CommandExecutionResult](errCause) - ) - } - } yield result - } - hashTracer: HashTracer = - if (config.enableVerboseHashing && verboseHashing) - HashTracer.StringHashTracer(traceSubNodes = true) - else - HashTracer.NoOp - prepareResult <- externalTransactionProcessor - .processPrepare( - commandExecutionResult, - commands, - config.contractLookupParallelism, - hashTracer, - maxRecordTime, - hashingSchemeVersion, - ) - .leftWiden[RpcError] - hashingDetails = hashTracer match { - // If we have a NoOp tracer but verboseHashing was requested, it means it's disabled on the participant - // Return a message to explain that - case HashTracer.NoOp if verboseHashing => - Some( - "Verbose hashing is disabled on this participant. Contact the node administrator for more details." - ) - case HashTracer.NoOp => None - case stringTracer: HashTracer.StringHashTracer => Some(stringTracer.result) - } - costEstimation <- costEstimationHints.traverse { costHints => - syncService - .estimateTrafficCost( - synchronizerId = commandExecutionResult.synchronizerRank.synchronizerId.logical, - transaction = commandExecutionResult.commandInterpretationResult.transaction, - transactionMetadata = - commandExecutionResult.commandInterpretationResult.transactionMeta, - submitterInfo = commandExecutionResult.commandInterpretationResult.submitterInfo, - keyResolver = commandExecutionResult.commandInterpretationResult.globalKeyMapping, - disclosedContracts = - commandExecutionResult.commandInterpretationResult.processedDisclosedContracts - .map(contract => contract.contractId -> contract) - .toList - .toMap, - costHints = costHints, - ) - .map { estimation => - CostEstimation( - Some(estimation.estimationTimestamp.toProtoTimestamp), - estimation.confirmationRequestCost.value, - estimation.confirmationResponseCost.value, - estimation.totalCost.value, - ) - } - .leftMap(InteractiveSubmissionPreparationError.Reject(_)) - .leftWiden[RpcError] - } - } yield proto.PrepareSubmissionResponse( - preparedTransaction = Some(prepareResult.transaction), - preparedTransactionHash = prepareResult.hash.unwrap, - hashingSchemeVersion = HashingSchemeVersionConverter.toLAPIProto(prepareResult.hashVersion), - hashingDetails = hashingDetails, - costEstimation = costEstimation, - ) - - result.value.map(_.leftMap(_.asGrpcError).toTry).flatMap(FutureUnlessShutdown.fromTry) - } - - override def close(): Unit = () - - private def submitIfNotOverloaded(executionResult: CommandExecutionResult)(implicit - loggingContext: LoggingContextWithTrace - ): Future[SubmissionResult] = - checkOverloaded(loggingContext.traceContext) match { - case Some(submissionResult) => Future.successful(submissionResult) - case None => submitTransaction(executionResult) - } - - private def submitTransaction(result: CommandExecutionResult)(implicit - loggingContext: LoggingContextWithTrace - ): Future[state.SubmissionResult] = { - metrics.commands.validSubmissions.mark() - logger.trace("Submitting transaction to ledger.") - syncService - .submitTransaction( - result.commandInterpretationResult.transaction, - result.synchronizerRank, - result.routingSynchronizerState, - result.commandInterpretationResult.submitterInfo, - result.commandInterpretationResult.transactionMeta, - result.commandInterpretationResult.interpretationTimeNanos, - result.commandInterpretationResult.globalKeyMapping, - result.commandInterpretationResult.processedDisclosedContracts, - ) - } - - private def handleSubmissionResult(result: Try[state.SubmissionResult])(implicit - loggingContext: LoggingContextWithTrace - ): Try[Unit] = { - import state.SubmissionResult.* - result match { - case Success(Acknowledged) => - logger.debug("Interactive submission acknowledged by sync-service.") - TryUtil.unit - - case Success(result: SynchronousError) => - logger.info(s"Rejected: ${result.description}") - Failure(result.exception) - - // Do not log again on errors that are logging on creation - case Failure(error: LoggedApiException) => Failure(error) - case Failure(error) => - logger.info(s"Rejected: ${error.getMessage}") - Failure(error) - } - } - - override def execute( - executionRequest: ExecuteRequest - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[proto.ExecuteSubmissionResponse] = { - val commandIdLogging = - executionRequest.preparedTransaction.metadata - .flatMap(_.submitterInfo.map(_.commandId)) - .map(logging.commandId) - .toList - - withEnrichedLoggingContext( - logging.submissionId(executionRequest.submissionId), - commandIdLogging *, - ) { implicit loggingContext => - logger.info( - s"Requesting execution of daml transaction with submission ID ${executionRequest.submissionId}" - ) - val result = for { - _ <- EitherTUtil.condUnitET[FutureUnlessShutdown]( - executionRequest.signatures.values - .forall(_.sizeIs <= config.maximumNumberOfSignaturesPerParty.value), - InteractiveSubmissionExecuteError.Reject( - s"One or more parties provided more than the maximum number of signatures allowed (${config.maximumNumberOfSignaturesPerParty.value})" - ), - ) - executionResult <- externalTransactionProcessor.processExecute(executionRequest) - _ <- EitherT - .liftF[Future, InteractiveSubmissionExecuteError.Reject, Unit]( - submitIfNotOverloaded(executionResult) - .transform(handleSubmissionResult) - ) - .mapK(FutureUnlessShutdown.outcomeK) - } yield proto.ExecuteSubmissionResponse() - - result.value.map(_.leftMap(_.asGrpcError).toTry).flatMap(FutureUnlessShutdown.fromTry) - } - } - - override def getPreferredPackages( - packageVettingRequirements: PackageVettingRequirements, - synchronizerId: Option[SynchronizerId], - vettingValidAt: Option[CantonTimestamp], - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[Either[String, (Seq[PackageReference], PhysicalSynchronizerId)]] = - packagePreferenceService - .getPreferredPackages( - packageVettingRequirements = packageVettingRequirements, - packageFilter = PackagePreferenceBackend.AllowAllPackageIds, - synchronizerId = synchronizerId, - vettingValidAt = vettingValidAt, - ) - - private def executeAndWaitInternal(executionRequest: ExecuteRequest)(implicit - loggingContext: LoggingContextWithTrace - ): EitherT[FutureUnlessShutdown, InteractiveSubmissionExecuteError.Reject, CompletionResponse] = { - val commandIdLogging = - executionRequest.preparedTransaction.metadata - .flatMap(_.submitterInfo.map(_.commandId)) - .map(logging.commandId) - .toList - - withEnrichedLoggingContext( - logging.submissionId(executionRequest.submissionId), - commandIdLogging *, - ) { implicit loggingContext => - logger.info( - s"Requesting execution of daml transaction with submission ID ${executionRequest.submissionId}" - ) - // Capture deadline before thread switching in Future for-comprehension - val deadlineO = Option(Context.current().getDeadline) - for { - executionResult <- externalTransactionProcessor.processExecute(executionRequest) - commandId = executionResult.commandInterpretationResult.submitterInfo.commandId - submissionId = executionRequest.submissionId - nonNegativeTimeout <- EitherT - .liftF( - Future.fromTry( - validateRequestTimeout( - deadlineO, - commandId, - submissionId, - defaultTrackingTimeout, - )(errorLoggingContext) - ) - ) - .mapK(FutureUnlessShutdown.outcomeK) - completion <- EitherT - .liftF[Future, InteractiveSubmissionExecuteError.Reject, CompletionResponse]( - transactionSubmissionTracker.track( - submissionKey = SubmissionKey( - commandId = commandId, - submissionId = submissionId, - userId = executionRequest.userId, - parties = executionResult.commandInterpretationResult.submitterInfo.actAs.toSet, - ), - timeout = nonNegativeTimeout, - submit = childContext => { - LoggingContextWithTrace.withNewLoggingContext( - loggingContext.entries.contents.toList* - )(childLoggingContextWithTrace => - FutureUnlessShutdown.outcomeF( - submitIfNotOverloaded(executionResult)(childLoggingContextWithTrace) - .transform(handleSubmissionResult) - ) - )(childContext) - }, - )(errorLoggingContext, loggingContext.traceContext) - ) - .mapK(FutureUnlessShutdown.outcomeK) - } yield completion - } - } - - override def executeAndWait(executionRequest: ExecuteRequest)(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[ExecuteSubmissionAndWaitResponse] = - executeAndWaitInternal(executionRequest) - .map { completion => - proto.ExecuteSubmissionAndWaitResponse( - completion.completion.updateId, - completion.completion.offset, - ) - } - .value - .map(_.leftMap(_.asGrpcError).toTry) - .flatMap(FutureUnlessShutdown.fromTry) - - override def executeAndWaitForTransaction( - executionRequest: ExecuteRequest, - transactionFormat: Option[TransactionFormat], - )(implicit - loggingContext: LoggingContextWithTrace - ): FutureUnlessShutdown[ExecuteSubmissionAndWaitForTransactionResponse] = { - val result = for { - completionResponse <- executeAndWaitInternal(executionRequest) - transaction <- EitherT - .liftF[Future, InteractiveSubmissionExecuteError.Reject, GetUpdateResponse]( - CommandServiceImpl.fetchTransactionFromCompletion( - resp = completionResponse, - transactionFormat = transactionFormat, - updateServices = updateServices, - logger = logger, - ) - ) - .mapK(FutureUnlessShutdown.outcomeK) - } yield proto.ExecuteSubmissionAndWaitForTransactionResponse( - Some(transaction.getTransaction) - ) - - result.value.map(_.leftMap(_.asGrpcError).toTry).flatMap(FutureUnlessShutdown.fromTry) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/EnrichedTransactionData.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/EnrichedTransactionData.scala deleted file mode 100644 index 20305a3800..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/EnrichedTransactionData.scala +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive.codec - -import cats.data.EitherT -import cats.syntax.either.* -import com.digitalasset.canton.crypto.InteractiveSubmission.TransactionMetadataForHashing -import com.digitalasset.canton.crypto.{Hash, InteractiveSubmission} -import com.digitalasset.canton.data.LedgerTimeBoundaries -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.RoutingSynchronizerState -import com.digitalasset.canton.ledger.participant.state.SubmitterInfo.ExternallySignedSubmission -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, TracedLogger} -import com.digitalasset.canton.platform.apiserver.execution.CommandInterpretationResult -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.EnrichedTransactionData.ExternalInputContract -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.canton.protocol.hash.HashTracer -import com.digitalasset.canton.topology.{PhysicalSynchronizerId, Synchronizer, SynchronizerId} -import com.digitalasset.canton.version.{HashingSchemeVersion, ProtocolVersion} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{ImmArray, Time} -import com.digitalasset.daml.lf.engine.Enricher -import com.digitalasset.daml.lf.transaction.{ - FatContractInstance, - GlobalKey, - SubmittedTransaction, - TransactionCoder, -} -import com.digitalasset.daml.lf.value.Value.ContractId -import com.digitalasset.daml.lf.value.{Value, ValueCoder} -import com.google.protobuf.ByteString - -import java.util.UUID -import scala.concurrent.ExecutionContext - -object EnrichedTransactionData { - - /** Class that holds both the enriched FCI and the original FCI. This allows to show an enriched - * version to the external party while maintaining the original contract instance so it stays - * consistent with its Contract Id. - * - * @param enrichedContract - * The enriched contract instance. Use for encoding to the PreparedTransaction proto and - * verifying external hash signatures. - * @param originalContract - * Original event contract. Use within the Canton protocol. - */ - final case class ExternalInputContract( - enrichedContract: FatContractInstance, - originalContract: LfFatContractInst, - ) { - require( - enrichedContract.contractId == originalContract.contractId, - s"Mismatching contractIds between enriched (${enrichedContract.contractId}) and original (${originalContract.contractId})", - ) - - def contractId: ContractId = originalContract.contractId - - /** Return the created event blob for this contract. This does not contain any enrichment. - */ - def toCreateEventBlob: Either[ValueCoder.EncodeError, ByteString] = - TransactionCoder.encodeFatContractInstance(originalContract) - } -} - -/** Interface for an enriched transaction and input contracts. - */ -private[interactive] sealed trait EnrichedTransactionData { - private[codec] def submitterInfo: state.SubmitterInfo - private[codec] def transactionMeta: state.TransactionMeta - private[codec] def transaction: SubmittedTransaction - private[codec] def globalKeyMapping: Map[GlobalKey, Vector[Value.ContractId]] - private[codec] def inputContracts: Map[ContractId, ExternalInputContract] - private[codec] def synchronizer: Synchronizer - private[codec] def mediatorGroup: Int - private[codec] def transactionUUID: UUID - private[codec] def maxRecordTime: Option[Time.Timestamp] - - def computeHash( - hashVersion: HashingSchemeVersion, - protocolVersion: ProtocolVersion, - hashTracer: HashTracer = HashTracer.NoOp, - ): Either[InteractiveSubmission.HashError, Hash] = { - val metadataForHashing = TransactionMetadataForHashing.create( - actAs = submitterInfo.actAs.toSet, - commandId = submitterInfo.commandId, - transactionUUID = transactionUUID, - mediatorGroup = mediatorGroup, - synchronizer = synchronizer, - timeBoundaries = transactionMeta.timeBoundaries, - preparationTime = transactionMeta.preparationTime, - maxRecordTime = maxRecordTime, - // The hash is computed from the enriched contract because that's what the external party signs - disclosedContracts = inputContracts.view.mapValues(_.enrichedContract).toMap, - ) - InteractiveSubmission.computeVersionedHash( - hashVersion, - transaction, - metadataForHashing, - transactionMeta.optNodeSeeds - .map(_.toList.toMap) - .getOrElse(Map.empty), - protocolVersion, - hashTracer, - ) - } -} - -/** Transaction data for an enriched external submission during the prepare phase. This is usually - * passed to the PreparedTransactionEncoder. DO NOT submit this transaction the protocol. - */ -final case class PrepareTransactionData( - private[codec] val submitterInfo: state.SubmitterInfo, - private[codec] val transactionMeta: state.TransactionMeta, - private[codec] val transaction: SubmittedTransaction, - private[codec] val globalKeyMapping: Map[GlobalKey, Vector[Value.ContractId]], - private[codec] val inputContracts: Map[ContractId, ExternalInputContract], - private[codec] val synchronizer: Synchronizer, - private[codec] val mediatorGroup: Int, - private[codec] val transactionUUID: UUID, - private[codec] val maxRecordTime: Option[Timestamp], -) extends EnrichedTransactionData - -/** Transaction data for an enriched external submission during the execute phase. This is usually - * output but the PreparedTransactionDecoder. DO NOT submit this transaction to the protocol, but - * call "impoverish" before. - */ -final case class ExecuteTransactionData( - private[codec] val submitterInfo: state.SubmitterInfo, - private[codec] val transactionMeta: state.TransactionMeta, - private[codec] val transaction: SubmittedTransaction, - private[codec] val globalKeyMapping: Map[GlobalKey, Vector[Value.ContractId]], - private[codec] val inputContracts: Map[ContractId, ExternalInputContract], - private[codec] val synchronizer: Synchronizer, - private val externallySignedSubmission: ExternallySignedSubmission, -) extends EnrichedTransactionData { - override private[codec] val mediatorGroup: Int = externallySignedSubmission.mediatorGroup.value - override private[codec] val transactionUUID: UUID = externallySignedSubmission.transactionUUID - - override def maxRecordTime: Option[Timestamp] = externallySignedSubmission.maxRecordTime - - def impoverish: CommandInterpretationResult = { - val normalizedTransaction = Enricher.impoverish(transaction) - CommandInterpretationResult( - submitterInfo, - transactionMeta, - transaction = SubmittedTransaction(normalizedTransaction), - dependsOnLedgerTime = transactionMeta.timeBoundaries != LedgerTimeBoundaries.unconstrained, - interpretationTimeNanos = 0L, // Irrelevant here as interpretation was done during prepare, - globalKeyMapping = globalKeyMapping, - // Make sure to use the original contract instance here. No need to impoverish it as it hasn't been enriched - processedDisclosedContracts = ImmArray.from(inputContracts.values.map(_.originalContract)), - optSynchronizerId = Some(synchronizer.logical), - ) - } - - def verifySignature( - routingSynchronizerState: RoutingSynchronizerState, - logger: TracedLogger, - )(implicit - loggingContextWithTrace: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): EitherT[FutureUnlessShutdown, String, Unit] = - for { - externallySignedSubmission <- EitherT.fromEither[FutureUnlessShutdown]( - submitterInfo.externallySignedSubmission - .toRight("Missing externally signed submission") - ) - physicalSynchronizerId <- EitherT.fromEither[FutureUnlessShutdown]( - synchronizer match { - case lsid: SynchronizerId => - routingSynchronizerState - .getPhysicalId(lsid) - .toRight( - s"Cannot find a physical synchronizer for $synchronizer. Make sure the participant is connected to the synchronizer." - ) - case psid: PhysicalSynchronizerId => Right(psid) - } - ) - protocolVersion = physicalSynchronizerId.protocolVersion - hash <- EitherT - .fromEither[FutureUnlessShutdown]( - computeHash( - externallySignedSubmission.version, - protocolVersion, - ) - .leftMap(_.message) - ) - topologySnapshot <- EitherT - .fromEither[FutureUnlessShutdown]( - routingSynchronizerState - .getTopologySnapshotFor(physicalSynchronizerId) - .leftMap(_.cause) - ) - cryptoPureApi <- EitherT.fromEither[FutureUnlessShutdown]( - routingSynchronizerState - .getSyncCryptoPureApi(physicalSynchronizerId) - .leftMap(_.cause) - .flatMap( - _.toRight(s"Cannot verify external signature on synchronizer $physicalSynchronizerId") - ) - ) - - // Verify signatures - _ <- InteractiveSubmission - .verifySignatures( - hash, - externallySignedSubmission.signatures, - cryptoPureApi, - topologySnapshot, - submitterInfo.actAs.toSet, - logger, - protocolVersion, - ) - } yield () -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/ExternalTransactionProcessor.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/ExternalTransactionProcessor.scala deleted file mode 100644 index a53f8a3916..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/ExternalTransactionProcessor.scala +++ /dev/null @@ -1,336 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive.codec - -import cats.data.EitherT -import cats.syntax.either.* -import com.daml.ledger.api.v2.interactive.interactive_submission_service.PreparedTransaction -import com.digitalasset.canton.LfTimestamp -import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.crypto.Hash -import com.digitalasset.canton.interactive.InteractiveSubmissionEnricher -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService.ExecuteRequest -import com.digitalasset.canton.ledger.api.{Commands as ApiCommands, DisclosedContract} -import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors.{ - InteractiveSubmissionExecuteError, - InteractiveSubmissionPreparationError, -} -import com.digitalasset.canton.ledger.error.groups.{CommandExecutionErrors, ConsistencyErrors} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.SynchronizerRank -import com.digitalasset.canton.ledger.participant.state.index.{ContractState, ContractStore} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.apiserver.execution.CommandExecutionResult -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.EnrichedTransactionData.ExternalInputContract -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.ExternalTransactionProcessor.PrepareResult -import com.digitalasset.canton.platform.config.InteractiveSubmissionServiceConfig -import com.digitalasset.canton.platform.store.dao.events.InputContractPackages -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.canton.protocol.hash.HashTracer -import com.digitalasset.canton.util.collection.MapsUtil -import com.digitalasset.canton.util.{EitherTUtil, MonadUtil} -import com.digitalasset.canton.version.{HashingSchemeVersion, ProtocolVersion} -import com.digitalasset.daml.lf.transaction.{SubmittedTransaction, Transaction} -import com.digitalasset.daml.lf.value.Value.ContractId - -import java.util.UUID -import scala.concurrent.{ExecutionContext, Future} - -object ExternalTransactionProcessor { - final case class PrepareResult( - transaction: PreparedTransaction, - hash: Hash, - hashVersion: HashingSchemeVersion, - ) -} - -/** This class contains the logic to process, prepare, and execute requests from the interactive - * submission API. The general flow is as follows: - * {{{ - * IC = Input Contract - * - * ┌───────────────┐ ┌──────────────────────┐ ExternalHash = Hash(EnrichedLfTx, EnrichedIC) - * │ LfTx │ │ EnrichedLfTx │ Sign(ExternalHash) - * ┌─────────┐ Interpretation ├───────────────┤ Enrich ├──────────────────────┤ Encode ┌─────────────────────┐ - * Prepare: │ Command ┼────────────────────►│ Original IC │────────►│EnrichedIC, OriginalIC│─────────►│ PreparedTransaction │ - * └─────────┘ └───────────────┘ └──────────────────────┘ └───────────────────┬─┘ - * || || │ - * Equal || Equal || │ - * || || │ - * Submit to Sync ┌───────────────┐ ┌──────────────────────┐ Decode │ - * Execute: ◄──────────────────│ LfTx │ │ EnrichedLfTx │◄─────────────┘ - * ├───────────────┤ ├──────────────────────┤ - * │ Original IC │ │EnrichedIC, OriginalIC│ - * └───────────────┘ └──────────┬───────────┘ - * ▲ │ - * │ │ - * │ VerifySignature(EnrichedTx, EnrichedIC) - * Impoverish(EnrichedLfTx) │ - * │ │ - * │ │ - * └───────────────────────◄───────────────────┘ - * }}} - * Important to note is that input contracts' original data is passed back and forth between - * prepare and execute, whereas the transaction itself is not. That's because it would become - * increasingly difficult to maintain a correct enrich / impoverish logic over arbitrary old input - * contracts with different Lf encodings. The downside is increased payload size for the prepared - * transaction that now contains the input contracts in both enriched and original form. For - * transactions we can tie the enrich / impoverish to the hashing scheme version and ensure that - * the roundtrip is injective within the same version. Prepared transaction also have a much - * shorter lifetime than input contracts in general so re-preparing a transaction with a newer - * hashing version after an upgrade is relatively cheap. - */ -class ExternalTransactionProcessor( - enricher: InteractiveSubmissionEnricher, - contractStore: ContractStore, - syncService: state.SyncService, - config: InteractiveSubmissionServiceConfig, - val loggerFactory: NamedLoggerFactory, -) extends NamedLogging { - private val encoder = new PreparedTransactionEncoder(loggerFactory) - private val decoder = new PreparedTransactionDecoder(loggerFactory) - - private def lookupAndEnrichInputContracts( - transaction: Transaction, - disclosedContracts: Map[ContractId, LfFatContractInst], - contractLookupParallelism: PositiveInt, - )(implicit - loggingContextWithTrace: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): EitherT[FutureUnlessShutdown, String, Map[ContractId, ExternalInputContract]] = { - - def lookupContract(coid: ContractId): FutureUnlessShutdown[LfFatContractInst] = - disclosedContracts.get(coid) match { - case Some(inst) => - FutureUnlessShutdown.pure(inst) - case None => - FutureUnlessShutdown - .outcomeF(contractStore.lookupContractState(coid)) - .flatMap[LfFatContractInst] { - - case active: ContractState.Active => - FutureUnlessShutdown.pure(active.contractInstance) - - // Engine interpretation likely would have failed if that was the case - // However it's possible that the contract was archived or pruned in the meantime - // That's not an issue however because if that was the case the transaction would have failed later - // anyway during conflict detection. - case ContractState.NotFound => - FutureUnlessShutdown - .failed( - ConsistencyErrors.ContractNotFound - .Reject( - s"Contract was not found in the participant contract store. You must either explicitly disclose the contract, or prepare the transaction via a participant that has knowledge of it", - coid, - ) - .asGrpcError - ) - case ContractState.Archived => - FutureUnlessShutdown - .failed( - CommandExecutionErrors.Interpreter.ContractNotActive - .Reject( - "Input contract has seemingly already been archived immediately after interpretation of the transaction", - coid, - None, - ) - .asGrpcError - ) - } - } - - MonadUtil - .parTraverseWithLimit(contractLookupParallelism)( - InputContractPackages.forTransaction(transaction).toList - ) { case (inputCoid, targetPackageIds) => - for { - original <- EitherT.right[String](lookupContract(inputCoid)) - enriched <- enricher.enrichContract(original, targetPackageIds) - } yield { - inputCoid -> ExternalInputContract( - originalContract = original, - enrichedContract = enriched, - ) - } - } - .map(_.toMap) - - } - - private def enrich( - commandExecutionResult: CommandExecutionResult, - disclosedContracts: Seq[DisclosedContract], - contractLookupParallelism: PositiveInt, - maxRecordTime: Option[LfTimestamp], - )(implicit - loggingContextWithTrace: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): EitherT[ - FutureUnlessShutdown, - InteractiveSubmissionPreparationError.Reject, - PrepareTransactionData, - ] = - for { - // First enrich the transaction - enrichedTransaction <- EitherT.liftF( - enricher.enrichVersionedTransaction( - commandExecutionResult.commandInterpretationResult.transaction - ) - ) - disclosedContractMap <- EitherT.fromEither[FutureUnlessShutdown]( - MapsUtil - .toNonConflictingMap( - disclosedContracts.map(_.fatContractInstance).map(c => c.contractId -> c) - ) - .leftMap(err => - CommandExecutionErrors.InteractiveSubmissionPreparationError.Reject( - s"Disclosed contracts contain non-unique contract IDs: $err" - ) - ) - ) - // Compute input contracts by looking them up either from disclosed contracts or the local store - inputContracts <- lookupAndEnrichInputContracts( - enrichedTransaction.transaction, - disclosedContractMap, - contractLookupParallelism, - ) - .leftMap(CommandExecutionErrors.InteractiveSubmissionPreparationError.Reject(_)) - // The participant needs to be connected to this synchronizer ID for the transaction to be submitted successfully - psid = commandExecutionResult.synchronizerRank.synchronizerId - transactionData = PrepareTransactionData( - submitterInfo = commandExecutionResult.commandInterpretationResult.submitterInfo, - transactionMeta = commandExecutionResult.commandInterpretationResult.transactionMeta, - transaction = SubmittedTransaction(enrichedTransaction), - globalKeyMapping = commandExecutionResult.commandInterpretationResult.globalKeyMapping, - inputContracts = inputContracts, - synchronizer = psid.forExternalTransactionHashing, - mediatorGroup = 0, - transactionUUID = UUID.randomUUID(), - maxRecordTime = maxRecordTime, - ) - } yield transactionData - - /** Transform a newly interpreted transaction into a prepared transaction. - */ - private[apiserver] def processPrepare( - commandExecutionResult: CommandExecutionResult, - commands: ApiCommands, - contractLookupParallelism: PositiveInt, - hashTracer: HashTracer, - maxRecordTime: Option[LfTimestamp], - hashingSchemeVersion: HashingSchemeVersion, - )(implicit - loggingContextWithTrace: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): EitherT[ - FutureUnlessShutdown, - InteractiveSubmissionPreparationError.Reject, - PrepareResult, - ] = - for { - // Enrich first - enriched <- enrich( - commandExecutionResult, - commands.disclosedContracts.toList, - contractLookupParallelism, - maxRecordTime, - ) - // Then encode - encoded <- EitherT - .liftF[ - Future, - InteractiveSubmissionPreparationError.Reject, - PreparedTransaction, - ](encoder.encode(enriched)) - .mapK(FutureUnlessShutdown.outcomeK) - // Compute the pre-computed hash for convenience - protocolVersion = commandExecutionResult.synchronizerRank.synchronizerId.protocolVersion - hash <- EitherT - .fromEither[FutureUnlessShutdown]( - enriched - .computeHash(hashingSchemeVersion, protocolVersion, hashTracer) - .leftMap(error => InteractiveSubmissionPreparationError.Reject(error.message)) - ) - } yield { - PrepareResult(encoded, hash, hashingSchemeVersion) - } - - /** Decodes a prepared transaction, verify its signature and convert it to CommandExecutionResult - */ - private[apiserver] def processExecute(executeRequest: ExecuteRequest)(implicit - loggingContextWithTrace: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): EitherT[ - FutureUnlessShutdown, - InteractiveSubmissionExecuteError.Reject, - CommandExecutionResult, - ] = - for { - _ <- EitherTUtil.condUnitET[FutureUnlessShutdown]( - !config.enforceSingleRootNode || executeRequest.preparedTransaction.transaction - .forall(_.roots.sizeIs == 1), - InteractiveSubmissionExecuteError.Reject( - "Transaction with multiple root nodes are not supported" - ), - ) - routingSynchronizerState <- EitherT.liftF(syncService.getRoutingSynchronizerState) - decoded <- EitherT - .liftF[Future, InteractiveSubmissionExecuteError.Reject, ExecuteTransactionData]( - decoder.decode(executeRequest) - ) - .mapK(FutureUnlessShutdown.outcomeK) - // Check upfront if the synchronizerID format is correct for the PV of the target synchronizer - _ <- EitherT.fromEither[FutureUnlessShutdown]( - syncService - .physicalSynchronizerIdForSynchronizerId(decoded.synchronizer.logical) - .filter(_.forExternalTransactionHashing != decoded.synchronizer) - .map { physicalSynchronizerId => - InteractiveSubmissionExecuteError.Reject( - { - val protocolVersionOfSelectedSync = - s"The selected synchronizer $physicalSynchronizerId is on Protocol Version ${physicalSynchronizerId.protocolVersion}." - physicalSynchronizerId.protocolVersion match { - case atOrBefore34 if atOrBefore34 <= ProtocolVersion.v34 => - protocolVersionOfSelectedSync + - s" Please use a Logical Synchronizer ID in the prepared transaction metadata on PVs <= 34" - case _ => - protocolVersionOfSelectedSync + - s" Please use a Physical Synchronizer ID in the prepared transaction metadata on PVs > 34" - } - } - ) - } - .toLeft(()) - ) - _ <- decoded - .verifySignature(routingSynchronizerState, logger) - .leftMap(err => InteractiveSubmissionExecuteError.Reject(err)) - commandInterpretationResult = decoded.impoverish - selectRoutingSynchronizer <- EitherT.liftF( - syncService - .selectRoutingSynchronizer( - submitterInfo = commandInterpretationResult.submitterInfo, - optSynchronizerId = commandInterpretationResult.optSynchronizerId, - transactionMeta = commandInterpretationResult.transactionMeta, - transaction = commandInterpretationResult.transaction, - // We expect to have all input contracts explicitly disclosed here, - // as we do not want the executing participant to use its local contracts when creating the views - disclosedContractIds = - commandInterpretationResult.processedDisclosedContracts.map(_.contractId).toList, - transactionUsedForExternalSigning = true, - routingSynchronizerState = routingSynchronizerState, - ) - .map(FutureUnlessShutdown.pure) - .leftMap(err => FutureUnlessShutdown.failed[SynchronizerRank](err.asGrpcError)) - .merge - .flatten - ) - } yield CommandExecutionResult( - commandInterpretationResult, - selectRoutingSynchronizer, - routingSynchronizerState, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionCodec.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionCodec.scala deleted file mode 100644 index 6330f28c5f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionCodec.scala +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive.codec - -import cats.Applicative -import cats.syntax.either.* -import com.digitalasset.base.error.DamlErrorWithDefiniteAnswer -import com.digitalasset.canton.ledger.error.groups.CommandExecutionErrors -import com.digitalasset.canton.logging.{ErrorLoggingContext, TracedLogger} -import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult -import com.digitalasset.canton.tracing.TraceContext -import io.scalaland.chimney.partial.{Error, Path, Result} - -import scala.concurrent.Future - -object PreparedTransactionCodec { - implicit val chimneyResultApplicative: Applicative[Result] = new Applicative[Result] { - override def pure[A](x: A): Result[A] = Result.fromValue(x) - override def ap[A, B](ff: Result[A => B])(fa: Result[A]): Result[B] = ff.flatMap(fa.map) - } - - // Convenience methods to deal with chimney Result values - implicit private[interactive] class EnhancedChimneyResult[A](val result: Result[A]) - extends AnyVal { - - /** Converts a chimney Result to a Future. In the result is a failure, detailed causes get - * logged at debug level, and a failed Future with a StatusRuntimeException is returned, - * containing only the high level reason of the failure. - */ - def toFutureWithLoggedFailures( - description: String, - logger: TracedLogger, - errorBuilder: String => DamlErrorWithDefiniteAnswer, - )(implicit - traceContext: TraceContext - ): Future[A] = Future.fromTry { - result.asEither - .leftMap { err => - val errorsAsString = err.errors - .map { case Error(err, path) => - s"${err.asString}${Option.when(path != Path.Empty)(s" at path ${path.asString}").getOrElse("")}" - } - .mkString(", ") - logger.info(s"$description: $errorsAsString") - s"$description: $errorsAsString" - } - .leftMap(errorBuilder(_)) - .leftMap(_.asGrpcError) - .toTry - } - - def toFutureWithLoggedFailuresEncode(description: String, logger: TracedLogger)(implicit - errorLoggingContext: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[A] = toFutureWithLoggedFailures( - description, - logger, - err => CommandExecutionErrors.InteractiveSubmissionPreparationError.Reject(err), - ) - - def toFutureWithLoggedFailuresDecode(description: String, logger: TracedLogger)(implicit - errorLoggingContext: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[A] = toFutureWithLoggedFailures( - description, - logger, - err => CommandExecutionErrors.InteractiveSubmissionExecuteError.Reject(err), - ) - } - - implicit private[interactive] class EnhancedEitherString[A](val either: Either[String, A]) - extends AnyVal { - - /** Converts an Either[String, A] to a Result[A] - */ - def toResult: Result[A] = Result.fromEither(either.leftMap(Result.Errors.fromString)) - } - - implicit private[interactive] class EnhancedParsingResult[A](val parsingResult: ParsingResult[A]) - extends AnyVal { - - /** Converts a ParsingResult[A] to a Result[A] - */ - def toResult: Result[A] = parsingResult.leftMap(_.message).toResult - def toFutureWithLoggedFailuresEncode(description: String, logger: TracedLogger)(implicit - errorLoggingContext: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[A] = toResult.toFutureWithLoggedFailuresEncode(description, logger) - - def toFutureWithLoggedFailuresDecode(description: String, logger: TracedLogger)(implicit - errorLoggingContext: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[A] = toResult.toFutureWithLoggedFailuresDecode(description, logger) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionDecoder.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionDecoder.scala deleted file mode 100644 index b23e3387e1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionDecoder.scala +++ /dev/null @@ -1,596 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive.codec - -import cats.syntax.either.* -import cats.syntax.traverse.* -import com.daml.ledger.api.v2.interactive.interactive_submission_service.DamlTransaction.Node.VersionedNode -import com.daml.ledger.api.v2.interactive.interactive_submission_service.Metadata.InputContract.Contract -import com.daml.ledger.api.v2.interactive.transaction.v1.interactive_submission_data as isdv1 -import com.daml.ledger.api.v2.interactive.{ - interactive_submission_common_data as iscd, - interactive_submission_service as iss, -} -import com.daml.ledger.api.v2.value as lapiValue -import com.digitalasset.canton.data.LedgerTimeBoundaries -import com.digitalasset.canton.ledger.api.services.InteractiveSubmissionService.ExecuteRequest -import com.digitalasset.canton.ledger.api.validation.StricterValueValidator -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.SubmitterInfo.ExternallySignedSubmission -import com.digitalasset.canton.ledger.participant.state.{SubmitterInfo, Update} -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.EnrichedTransactionData.ExternalInputContract -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.PreparedTransactionCodec.* -import com.digitalasset.canton.protocol.{LfNode, LfNodeId} -import com.digitalasset.canton.serialization.ProtoConverter -import com.digitalasset.canton.topology.Synchronizer -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf -import com.digitalasset.daml.lf.data.Ref.TypeConId -import com.digitalasset.daml.lf.data.{Bytes, ImmArray, Ref, Time} -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - FatContractInstance, - GlobalKeyWithMaintainers, - NodeId, - SerializationVersion, - SerializationVersion as LfSerializationVersion, - TransactionCoder, -} -import com.digitalasset.daml.lf.value.Value -import com.google.common.annotations.VisibleForTesting -import com.google.protobuf.ByteString -import io.scalaland.chimney.PartialTransformer -import io.scalaland.chimney.dsl.TransformerConfiguration.UpdateFlag -import io.scalaland.chimney.dsl.{TransformedNamesComparison, TransformerConfiguration} -import io.scalaland.chimney.inlined.* -import io.scalaland.chimney.internal.runtime.TransformerFlags -import io.scalaland.chimney.partial.Result -import io.scalaland.chimney.syntax.* - -import scala.concurrent.{ExecutionContext, Future} - -/** Class to decode a PreparedTransaction to an LF Transaction and its metadata. Uses chimney to - * define Transformers and PartialTransformer for all conversions. - */ -final class PreparedTransactionDecoder(override val loggerFactory: NamedLoggerFactory) - extends NamedLogging { - - // General config, applies to all transformers defined in this scope - private implicit val transformerConfig - : UpdateFlag[TransformerFlags.Enable[TransformerFlags.FieldNameComparison[ - TransformedNamesComparison.StrictEquality.type - ], TransformerFlags.Default]] = - TransformerConfiguration.default - // Needed to avoid confusions in the proto generated classes method names with `get` prefix - .enableCustomFieldNameComparison(TransformedNamesComparison.StrictEquality) - - /* - * Decoders from LAPI values to LF values - */ - private implicit def identifierTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[lapiValue.Identifier, lf.data.Ref.Identifier] = - PartialTransformer { src => - StricterValueValidator - .validateIdentifier(src) - .leftMap(_.getMessage) - .toResult - } - - private implicit def valueTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[lapiValue.Value, lf.value.Value] = - PartialTransformer { src => - StricterValueValidator - .validateValue(src) - .leftMap(_.getMessage) - .toResult - } - - /* - * Generic collection decoders - */ - private implicit def immArrayTransformer[A, B](implicit - aToB: PartialTransformer[A, B] - ): PartialTransformer[Seq[A], ImmArray[B]] = - PartialTransformer { src => - src.toList - .traverse(_.transformIntoPartial[B]) - .map(ImmArray.from) - } - - /* - * Straightforward decoders for simple proto values - */ - private implicit val languageVersionTransformer - : PartialTransformer[String, LfSerializationVersion] = - PartialTransformer(LfSerializationVersion.fromString(_).toResult) - - private implicit val contractIdTransformer - : PartialTransformer[String, lf.value.Value.ContractId] = - PartialTransformer { src => - for { - hexString <- lf.data.Ref.IdString.HexString.fromString(src).toResult - bytes = lf.data.Ref.HexString.decode(hexString) - contractId <- lf.value.Value.ContractId.fromBytes(bytes).toResult - } yield contractId - } - private implicit val hashTransformer: PartialTransformer[ByteString, lf.crypto.Hash] = - PartialTransformer { src => - lf.crypto.Hash.fromBytes(Bytes.fromByteString(src)).toResult - } - - private implicit val commandIdTransformer: PartialTransformer[String, lf.data.Ref.CommandId] = - PartialTransformer(src => lf.data.Ref.CommandId.fromString(src).toResult) - - private implicit val nodeSeedTransformer - : PartialTransformer[iss.DamlTransaction.NodeSeed, (lf.transaction.NodeId, lf.crypto.Hash)] = - PartialTransformer { src => - src.seed - .transformIntoPartial[lf.crypto.Hash] - .map(lf.transaction.NodeId(src.nodeId) -> _) - } - - private implicit val timestampTransformer: PartialTransformer[Long, lf.data.Time.Timestamp] = - PartialTransformer(src => lf.data.Time.Timestamp.fromLong(src).toResult) - - private implicit val optionalTimestampTransformer - : PartialTransformer[Option[Long], Option[lf.data.Time.Timestamp]] = - PartialTransformer.derive[Option[Long], Option[lf.data.Time.Timestamp]] - - private implicit val packageNameTransformer: PartialTransformer[String, lf.data.Ref.PackageName] = - PartialTransformer(src => lf.data.Ref.PackageName.fromString(src).toResult) - - private implicit val nameTransformer: PartialTransformer[String, lf.data.Ref.Name] = - PartialTransformer(src => lf.data.Ref.Name.fromString(src).toResult) - - private implicit val partyTransformer: PartialTransformer[String, lf.data.Ref.Party] = - PartialTransformer(src => lf.data.Ref.Party.fromString(src).toResult) - - private implicit val nodeIdTransformer: PartialTransformer[String, lf.transaction.NodeId] = - PartialTransformer { src => - src.toIntOption - .toRight("Node Id is not a valid integer") - .map(lf.transaction.NodeId.apply) - .toResult - } - - /* - * Global key decoders - */ - private implicit def globalKeyTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[iscd.GlobalKey, lf.transaction.GlobalKey] = { - // GlobalKey default constructor is private, so create a constructor function from the companion builder - // and pass that to chimney so it can construct the instance - def globalKeyConstructor( - templateId: TypeConId, - key: Value, - packageName: Ref.PackageName, - hash: lf.crypto.Hash, - ): Result[lf.transaction.GlobalKey] = - lf.transaction.GlobalKey.build(templateId, packageName, key, hash).leftMap(_.msg).toResult - - PartialTransformer - .define[iscd.GlobalKey, lf.transaction.GlobalKey] - .withConstructorPartial(globalKeyConstructor _) - .buildTransformer - } - - private implicit def globalKeyWithMaintainersTransformer(implicit - errorLoggingContext: ErrorLoggingContext, - serializationVersion: LfSerializationVersion, - ): PartialTransformer[iscd.GlobalKeyWithMaintainers, lf.transaction.GlobalKeyWithMaintainers] = - PartialTransformer - .define[iscd.GlobalKeyWithMaintainers, lf.transaction.GlobalKeyWithMaintainers] - .withFieldComputedPartial( - _.globalKey, - globalKeyProto => - serializationVersion match { - case SerializationVersion.V1 => - Result.fromErrorString( - s"Keys are not supported in nodes with LF Serialization version ${serializationVersion.pretty}" - ) - case _ => - globalKeyProto.key - .traverse(_.transformIntoPartial[lf.transaction.GlobalKey]) - .flatMap( - _.map(Result.fromValue) - .getOrElse(Result.fromErrorString("Empty key in GlobalKeyWithMaintainers")) - ) - }, - ) - .buildTransformer - - private def byKeyDecoder[T](byKey: T => Boolean)( - value: T - )(implicit serializationVersion: LfSerializationVersion): Result[Boolean] = - serializationVersion match { - // byKey cannot be true in SerializationVersion.V1 - case SerializationVersion.V1 if byKey(value) => - Result.fromErrorString( - s"byKey is not supported in nodes with LF Serialization version ${SerializationVersion.V1.pretty}" - ) - case _ => Result.fromValue(byKey(value)) - } - - /* - * Node Transformers - * These transformers decode proto nodes to LF nodes. Each proto version can map to several LF version, which is why - * the specific LF version is embedded in each proto node as a field, and set on the LF node during decoding. - */ - - /* - * V1 Transformers - */ - object v1 { - // Make the create transformer visible to the package because some objects require explicitly decoding to a create node - private[interactive] implicit def createNodeTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[isdv1.Create, lf.transaction.Node.Create] = - PartialTransformer[isdv1.Create, lf.transaction.Node.Create] { proto => - proto.lfVersion.transformIntoPartial[LfSerializationVersion].flatMap { implicit version => - proto - .intoPartial[lf.transaction.Node.Create] - .withFieldRenamed(_.contractId, _.coid) - .withFieldComputedPartial( - _.arg, - _.argument - .traverse(_.transformIntoPartial[lf.value.Value]) - .flatMap(_.toRight("Missing argument value").toResult), - ) - .withFieldConst(_.version, version) - .withFieldComputedPartial( - _.keyOpt, - _.key.traverse(_.transformIntoPartial[GlobalKeyWithMaintainers]), - ) - .transform - } - } - - private[interactive] implicit def fetchTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[isdv1.Fetch, lf.transaction.Node.Fetch] = - PartialTransformer[isdv1.Fetch, lf.transaction.Node.Fetch] { proto => - // Extract and decode the LF version first so we can use it in further converters to make - // validations when decoding fields based on it - proto.lfVersion.transformIntoPartial[LfSerializationVersion].flatMap { implicit version => - proto - .intoPartial[lf.transaction.Node.Fetch] - .withFieldRenamed(_.contractId, _.coid) - .withFieldConst(_.version, version) - .withFieldComputedPartial( - _.keyOpt, - _.key.traverse(_.transformIntoPartial[GlobalKeyWithMaintainers]), - ) - .withFieldComputedPartial(_.byKey, byKeyDecoder(_.byKey)) - .transform - } - } - - private[interactive] implicit def queryByKeyTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[isdv1.QueryByKey, lf.transaction.Node.QueryByKey] = - PartialTransformer[isdv1.QueryByKey, lf.transaction.Node.QueryByKey] { proto => - // Extract and decode the LF version first so we can use it in further converters to make - // validations when decoding fields based on it - proto.lfVersion.transformIntoPartial[LfSerializationVersion].flatMap { implicit version => - proto - .intoPartial[lf.transaction.Node.QueryByKey] - .withFieldConst(_.version, version) - .transform - } - } - - private[interactive] implicit def exerciseTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[isdv1.Exercise, lf.transaction.Node.Exercise] = - PartialTransformer[isdv1.Exercise, lf.transaction.Node.Exercise] { proto => - // Extract and decode the LF version first so we can use it in further converters to make - // validations when decoding fields based on it - proto.lfVersion.transformIntoPartial[LfSerializationVersion].flatMap { implicit version => - proto - .intoPartial[lf.transaction.Node.Exercise] - .withFieldRenamed(_.contractId, _.targetCoid) - .withFieldComputedPartial( - _.choiceObservers, - _.choiceObservers.traverse(_.transformIntoPartial[lf.data.Ref.Party]).map(_.toSet), - ) - .withFieldComputedPartial( - _.version, - _.lfVersion.transformIntoPartial[LfSerializationVersion], - ) - .withFieldComputedPartial( - _.keyOpt, - _.key.traverse(_.transformIntoPartial[GlobalKeyWithMaintainers]), - ) - .withFieldComputedPartial(_.byKey, byKeyDecoder(_.byKey)) - // Only supported in LF-dev - .withFieldConst(_.choiceAuthorizers, None) - .transform - } - } - - private implicit val rollbackTransformer - : PartialTransformer[isdv1.Rollback, lf.transaction.Node.Rollback] = - PartialTransformer.derive[isdv1.Rollback, lf.transaction.Node.Rollback] - - private[interactive] def nodeTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[isdv1.Node, lf.transaction.Node] = PartialTransformer { - case isdv1.Node(create: isdv1.Node.NodeType.Create) => - create.value.transformIntoPartial[lf.transaction.Node.Create] - case isdv1.Node(fetch: isdv1.Node.NodeType.Fetch) => - fetch.value.transformIntoPartial[lf.transaction.Node.Fetch] - case isdv1.Node(exercise: isdv1.Node.NodeType.Exercise) => - exercise.value.transformIntoPartial[lf.transaction.Node.Exercise] - case isdv1.Node(rollback: isdv1.Node.NodeType.Rollback) => - rollback.value.transformIntoPartial[lf.transaction.Node.Rollback] - case isdv1.Node(queryByKey: isdv1.Node.NodeType.QueryByKey) => - queryByKey.value.transformIntoPartial[lf.transaction.Node.QueryByKey] - case isdv1.Node(isdv1.Node.NodeType.Empty) => - Result.fromErrorString("Cannot decode empty transaction node") - } - } - - // Version agnostic decoder from proto node to LF node - private implicit def nodeTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[iss.DamlTransaction.Node, (lf.transaction.NodeId, lf.transaction.Node)] = - PartialTransformer { node => - val versionedNode = node.versionedNode - - val decodedNodeResult = versionedNode match { - case VersionedNode.V1(v1Node) => v1.nodeTransformer.transform(v1Node) - case VersionedNode.Empty => Result.fromErrorString("Cannot decode empty versioned node") - } - - for { - nodeId <- node.nodeId.transformIntoPartial[NodeId] - decodedNode <- decodedNodeResult - } yield nodeId -> decodedNode - } - - // Transaction decoder - @VisibleForTesting - private[interactive] implicit def transactionTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[iss.DamlTransaction, lf.transaction.VersionedTransaction] = - PartialTransformer { src => - def lfVersionedConstructor( - version: LfSerializationVersion, - nodes: Map[LfNodeId, LfNode], - roots: ImmArray[LfNodeId], - ): lf.transaction.VersionedTransaction = lf.transaction.VersionedTransaction( - version, - nodes, - roots, - ) - - src - .intoPartial[lf.transaction.VersionedTransaction] - .withFieldComputedPartial( - _.nodes, - _.nodes - .traverse(_.transformIntoPartial[(lf.transaction.NodeId, lf.transaction.Node)]) - .map(_.toMap), - ) - .withConstructor(lfVersionedConstructor _) - .transform - } - - // Input contract decoder - private implicit def inputContractTransformer(implicit - errorLoggingContext: ErrorLoggingContext - ): PartialTransformer[iss.Metadata.InputContract, ExternalInputContract] = - PartialTransformer { src => - val contract = src.contract - val createNodeResult = contract match { - case Contract.V1(value) => v1.createNodeTransformer.transform(value) - case Contract.Empty => - Result.fromErrorString("Cannot decode empty disclosed contract") - } - - for { - createNode <- createNodeResult - createTime <- src.createdAt.transformIntoPartial[Time.Timestamp] - originalContract <- TransactionCoder - .decodeFatContractInstance(src.eventBlob) - .leftMap(_.errorMessage) - .toResult - lfOriginalContract <- Either - .cond( - originalContract.createdAt == CreationTime.CreatedAt(createTime), - originalContract.updateCreateAt(createTime), - "Creation time of the original contract does not match the create time of the input contract", - ) - .toResult - enrichedContract = FatContractInstance.fromCreateNode( - createNode, - CreationTime.CreatedAt(createTime), - originalContract.authenticationData, - ) - } yield ExternalInputContract( - originalContract = lfOriginalContract, - enrichedContract = enrichedContract, - ) - } - - private implicit def timeBoundariesTransformer - : PartialTransformer[iss.Metadata, LedgerTimeBoundaries] = - PartialTransformer { src => - for { - min <- src.minLedgerEffectiveTime.transformIntoPartial[Option[Time.Timestamp]] - max <- src.maxLedgerEffectiveTime.transformIntoPartial[Option[Time.Timestamp]] - } yield LedgerTimeBoundaries.fromConstraints(min, max) - } - - private def requireField[A](optA: Option[A], field: String)(implicit - errorLoggingContext: ErrorLoggingContext - ): Future[A] = - Future.fromTry( - optA.toRight(RequestValidationErrors.MissingField.Reject(field).asGrpcError).toTry - ) - - private def adjustLedgerTimeToBounds( - requested: Time.Timestamp, - timeRange: Time.Range, - )(implicit traceContext: TraceContext): Time.Timestamp = { - val adjusted = if (requested < timeRange.min) { - timeRange.min - } else if (requested <= timeRange.max) { - requested - } else { - timeRange.max - } - - if (adjusted != requested) logger.debug(s"Ledger time adjusted from $requested to $adjusted") - - adjusted - } - - /** Decodes a prepared transaction back into a DeserializationResult that can be submitted. - */ - def decode(executeRequest: ExecuteRequest)(implicit - executionContext: ExecutionContext, - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[ExecuteTransactionData] = { - implicit val traceContext = loggingContext.traceContext - - for { - metadataProto <- requireField(executeRequest.preparedTransaction.metadata, "metadata") - submitterInfoProto <- requireField(metadataProto.submitterInfo, "submitter_info") - transactionUUID <- ProtoConverter.UuidConverter - .fromProtoPrimitive(metadataProto.transactionUuid) - .toFutureWithLoggedFailuresDecode("Failed to deserialize transaction UUID", logger) - externallySignedSubmission <- for { - mediatorGroup <- ProtoConverter - .parseNonNegativeInt("mediator_group", metadataProto.mediatorGroup) - .toFutureWithLoggedFailuresDecode("Failed to deserialize mediator group", logger) - maxLedgerTime <- metadataProto.maxRecordTime - .transformIntoPartial[Option[lf.data.Time.Timestamp]] - .toFutureWithLoggedFailuresDecode("Failed to deserialize max record time", logger) - } yield ExternallySignedSubmission( - executeRequest.serializationVersion, - executeRequest.signatures, - transactionUUID = transactionUUID, - mediatorGroup = mediatorGroup, - maxRecordTime = maxLedgerTime, - ) - submitterInfo <- submitterInfoProto - .intoPartial[SubmitterInfo] - // Read as is unused for the execution as the transaction has already been run through Daml engine at this point - .withFieldConst(_.readAs, List.empty) - .withFieldConst(_.submissionId, Some(executeRequest.submissionId)) - .withFieldConst(_.userId, executeRequest.userId) - .withFieldConstPartial( - _.commandId, - submitterInfoProto.commandId.transformIntoPartial[lf.data.Ref.CommandId], - ) - .withFieldConst(_.deduplicationPeriod, executeRequest.deduplicationPeriod) - .withFieldConst( - _.externallySignedSubmission, - Some(externallySignedSubmission), - ) - .transform - .toFutureWithLoggedFailuresDecode("Failed to deserialize submitter info", logger) - synchronizer <- Future.fromTry( - Synchronizer - .fromLogicalOrPhysicalString(metadataProto.synchronizerId, "synchronizer_id") - .leftMap(_.message) - .leftMap(RequestValidationErrors.InvalidArgument.Reject(_).asGrpcError) - .toTry - ) - transactionProto <- requireField( - executeRequest.preparedTransaction.transaction, - "transaction", - ) - timeBoundaries <- metadataProto - .transformIntoPartial[LedgerTimeBoundaries] - .toFutureWithLoggedFailuresDecode("Failed to deserialize time boundaries", logger) - ledgerEffectiveTime = adjustLedgerTimeToBounds( - executeRequest.tentativeLedgerEffectiveTime, - timeBoundaries.range, - ) - transactionMeta <- - metadataProto - .intoPartial[state.TransactionMeta] - // Unused field - .withFieldConst(_.optUsedPackages, None) - // Submission seed is irrelevant at this point, as we already have the individual node seeds, which are signed - // and shipped to the participants - .withFieldConst(_.submissionSeed, Update.noOpSeed) - .withFieldConstPartial( - _.optNodeSeeds, - transactionProto.nodeSeeds - .transformIntoPartial[ImmArray[(NodeId, lf.crypto.Hash)]] - .map(Some(_)), - ) - .withFieldConst( - _.timeBoundaries, - timeBoundaries, - ) - // Unused field - .withFieldConst(_.optByKeyNodes, None) - // Workflow ID is not supported for interactive submissions - .withFieldConst(_.workflowId, None) - .withFieldConst( - _.ledgerEffectiveTime, - ledgerEffectiveTime, - ) - .withFieldRenamed(_.preparationTime, _.preparationTime) - .transform - .toFutureWithLoggedFailuresDecode("Failed to deserialize transaction meta", logger) - transaction <- transactionProto - .transformIntoPartial[lf.transaction.VersionedTransaction] - .toFutureWithLoggedFailuresDecode("Failed to deserialize transaction", logger) - inputContracts <- metadataProto.inputContracts - .traverse(_.transformIntoPartial[ExternalInputContract]) - .map(_.map(eic => eic.contractId -> eic).toMap) - .toFutureWithLoggedFailuresDecode("Failed to deserialize input contracts", logger) - missingInputContracts = transaction.inputContracts.diff(inputContracts.keySet) - _ <- Either - .cond( - missingInputContracts.isEmpty, - (), - s"Missing input contracts: ${missingInputContracts.mkString(",")}", - ) - .toResult - .toFutureWithLoggedFailuresDecode( - "Provided input contracts do not match the input contracts in the transaction", - logger, - ) - superfluousInputContracts = inputContracts.keySet.diff(transaction.inputContracts) - _ <- Either - .cond( - superfluousInputContracts.isEmpty, - (), - s"Superfluous input contracts: ${superfluousInputContracts.mkString(",")}", - ) - .toResult - .toFutureWithLoggedFailuresDecode( - "Provided input contracts do not match the input contracts in the transaction", - logger, - ) - } yield { - ExecuteTransactionData( - submitterInfo = submitterInfo, - synchronizer = synchronizer, - transactionMeta = transactionMeta, - transaction = lf.transaction.SubmittedTransaction(transaction), - globalKeyMapping = Map.empty, // This field is deprecated - inputContracts = inputContracts, - externallySignedSubmission = externallySignedSubmission, - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionEncoder.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionEncoder.scala deleted file mode 100644 index 0d2a9acbaa..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/codec/PreparedTransactionEncoder.scala +++ /dev/null @@ -1,437 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive.codec - -import cats.syntax.either.* -import cats.syntax.traverse.* -import com.daml.ledger.api.v2.interactive.interactive_submission_service.DamlTransaction.Node.VersionedNode -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{DamlTransaction, Metadata} -import com.daml.ledger.api.v2.interactive.transaction.v1.interactive_submission_data as isdv1 -import com.daml.ledger.api.v2.interactive.{ - interactive_submission_common_data as iscd, - interactive_submission_service as iss, -} -import com.daml.ledger.api.v2.value as lapiValue -import com.digitalasset.canton.ledger.api.util.LfEngineToApi -import com.digitalasset.canton.ledger.participant.state.SubmitterInfo -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.EnrichedTransactionData.ExternalInputContract -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.PreparedTransactionCodec.* -import com.digitalasset.canton.topology.Synchronizer -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.ImmArray -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - GlobalKey, - GlobalKeyWithMaintainers, - Node, - NodeId, - SerializationVersion, -} -import com.google.common.annotations.VisibleForTesting -import com.google.protobuf.ByteString -import io.scalaland.chimney.partial.Result -import io.scalaland.chimney.syntax.* -import io.scalaland.chimney.{PartialTransformer, Transformer} - -import java.util.UUID -import scala.concurrent.{ExecutionContext, Future} - -/** Class to encode an LF Transaction and its metadata to a PreparedTransaction. Uses chimney to - * define Transformers and PartialTransformer for all conversions. - */ -final class PreparedTransactionEncoder( - override val loggerFactory: NamedLoggerFactory -) extends NamedLogging { - - /** Defines the mapping between LF version and Encoding versions. An encoding version can be used - * for several LF Versions. - */ - private val nodeTransformers = Map( - SerializationVersion.V1 -> v1.nodeTransformer(SerializationVersion.V1), - SerializationVersion.V2 -> v1.nodeTransformer(SerializationVersion.V2), - SerializationVersion.VDev -> v1.nodeTransformer(SerializationVersion.VDev), - ) - - private def getEncoderForVersion( - version: SerializationVersion - ): Result[PartialTransformer[lf.transaction.Node, iss.DamlTransaction.Node.VersionedNode]] = - nodeTransformers - .get(version) - .toRight(s"Expected a transformer for version $version") - .toResult - - /* - * Value encoding. For LF Values and identifiers we encode using the LAPI proto. - * The downside is we can't easily version this if LF values change significantly, - * however that should be very rare as LF values need to be stable to guarantee compatibility. - */ - private implicit val identifierTransformer - : Transformer[lf.data.Ref.Identifier, lapiValue.Identifier] = (src: lf.data.Ref.Identifier) => - LfEngineToApi.toApiIdentifier(src) - - private implicit val valueTransformer: PartialTransformer[lf.value.Value, lapiValue.Value] = - PartialTransformer { lfValue => - LfEngineToApi - .lfValueToApiValue(verbose = true, value0 = lfValue) - .toResult - } - - /* - * Generic collections - */ - private implicit def immArrayToSeqTransformer[A, B](implicit - aToB: Transformer[A, B] - ): Transformer[ImmArray[A], Seq[B]] = _.map(_.transformInto[B]).toSeq - - private implicit def optImmArrayToSeqTransformer[A, B](implicit - aToB: Transformer[A, B] - ): Transformer[Option[ImmArray[A]], Seq[B]] = - _.map(_.transformInto[Seq[B]]).getOrElse(Seq.empty) - - /* - * Straightforward encoders for simple LF classes - */ - private implicit val contractIdTransformer: Transformer[lf.value.Value.ContractId, String] = - _.toBytes.toHexString - - private implicit val hashTransformer: Transformer[lf.crypto.Hash, ByteString] = - _.bytes.toByteString - - private implicit val partyVersionTransformer: Transformer[lf.data.Ref.Party, String] = - Transformer.derive - - private implicit val lfSerializationVersionVersionTransformer - : Transformer[lf.transaction.SerializationVersion, String] = - SerializationVersion.toProtoValue(_) - - private implicit val nodeIdTransformer: Transformer[lf.transaction.NodeId, String] = - _.index.toString - - private implicit val timestampTransformer: Transformer[lf.data.Time.Timestamp, Long] = _.micros - - private implicit val synchronizerTransformer: Transformer[Synchronizer, String] = - _.toProtoPrimitive - - private implicit val nodeIdHashTransformer: Transformer[ - (lf.transaction.NodeId, lf.crypto.Hash), - iss.DamlTransaction.NodeSeed, - ] = { case (nodeId, hash) => - iss.DamlTransaction.NodeSeed(nodeId.index, hash.transformInto[ByteString]) - } - - /* - * Node Transformers - * LF Nodes are versioned individually. A proto version serializes one or more several LF versions. - * When a new LF version introduces changes that need either the serialization or the hashing to change, - * we introduce a new Proto version. - */ - - /* - * V1 Transformers - */ - object v1 { - private implicit def createNodeTransformer(implicit - serializationVersion: SerializationVersion - ): PartialTransformer[lf.transaction.Node.Create, isdv1.Create] = Transformer - .definePartial[lf.transaction.Node.Create, isdv1.Create] - .withFieldRenamed(_.coid, _.contractId) - .withFieldRenamed(_.arg, _.argument) - .withFieldComputed(_.signatories, _.signatories.toSeq.sorted) - .withFieldComputed(_.stakeholders, _.stakeholders.toSeq.sorted) - .withFieldConst(_.lfVersion, serializationVersion.transformInto[String]) - .withFieldComputedPartial( - _.key, - _.keyOpt.traverse(_.transformIntoPartial[iscd.GlobalKeyWithMaintainers]), - ) - .buildTransformer - - private[interactive] implicit def exerciseTransformer(implicit - serializationVersion: SerializationVersion - ): PartialTransformer[lf.transaction.Node.Exercise, isdv1.Exercise] = Transformer - .definePartial[lf.transaction.Node.Exercise, isdv1.Exercise] - .withFieldRenamed(_.targetCoid, _.contractId) - .withFieldComputed(_.signatories, _.signatories.toSeq.sorted) - .withFieldComputed(_.stakeholders, _.stakeholders.toSeq.sorted) - .withFieldComputed(_.actingParties, _.actingParties.toSeq.sorted) - .withFieldComputed(_.choiceObservers, _.choiceObservers.toSeq.sorted) - .withFieldConst(_.lfVersion, serializationVersion.transformInto[String]) - .withFieldComputedPartial( - _.key, - _.keyOpt.traverse(_.transformIntoPartial[iscd.GlobalKeyWithMaintainers]), - ) - .withFieldComputed(_.byKey, _.byKey) - .buildTransformer - - private[interactive] implicit def fetchTransformer(implicit - serializationVersion: SerializationVersion - ): PartialTransformer[lf.transaction.Node.Fetch, isdv1.Fetch] = Transformer - .definePartial[lf.transaction.Node.Fetch, isdv1.Fetch] - .withFieldRenamed(_.coid, _.contractId) - .withFieldComputed(_.signatories, _.signatories.toSeq.sorted) - .withFieldComputed(_.stakeholders, _.stakeholders.toSeq.sorted) - .withFieldComputed(_.actingParties, _.actingParties.toSeq.sorted) - .withFieldConst(_.lfVersion, serializationVersion.transformInto[String]) - .withFieldComputedPartial( - _.key, - _.keyOpt.traverse(_.transformIntoPartial[iscd.GlobalKeyWithMaintainers]), - ) - .withFieldComputed(_.byKey, _.byKey) - .buildTransformer - - private[interactive] implicit def queryByKeyTransformer(implicit - serializationVersion: SerializationVersion - ): PartialTransformer[lf.transaction.Node.QueryByKey, isdv1.QueryByKey] = Transformer - .definePartial[lf.transaction.Node.QueryByKey, isdv1.QueryByKey] - .withFieldConst(_.lfVersion, serializationVersion.transformInto[String]) - .buildTransformer - - private implicit val rollbackTransformer - : PartialTransformer[lf.transaction.Node.Rollback, isdv1.Rollback] = Transformer - .definePartial[lf.transaction.Node.Rollback, isdv1.Rollback] - .buildTransformer - - private[interactive] def nodeTransformer(implicit - serializationVersion: SerializationVersion - ): PartialTransformer[lf.transaction.Node, iss.DamlTransaction.Node.VersionedNode] = - PartialTransformer[lf.transaction.Node, iss.DamlTransaction.Node.VersionedNode] { lfNode => - val nodeType = lfNode match { - case create: lf.transaction.Node.Create => - create - .transformIntoPartial[isdv1.Create] - .map(isdv1.Node.NodeType.Create.apply) - case exercise: lf.transaction.Node.Exercise => - exercise - .transformIntoPartial[isdv1.Exercise] - .map(isdv1.Node.NodeType.Exercise.apply) - case fetch: lf.transaction.Node.Fetch => - fetch - .transformIntoPartial[isdv1.Fetch] - .map(isdv1.Node.NodeType.Fetch.apply) - case rollback: lf.transaction.Node.Rollback => - rollback - .transformIntoPartial[isdv1.Rollback] - .map(isdv1.Node.NodeType.Rollback.apply) - case _: lf.transaction.Node.QueryByKey - if serializationVersion == SerializationVersion.V1 => - Result.fromErrorString( - "Query By Key nodes are not supported in LF serialization version 1" - ) - case queryByKey: lf.transaction.Node.QueryByKey => - queryByKey - .transformIntoPartial[isdv1.QueryByKey] - .map(isdv1.Node.NodeType.QueryByKey.apply) - } - - nodeType - .map(isdv1.Node(_)) - .map(iss.DamlTransaction.Node.VersionedNode.V1.apply) - } - } - - // Top level transformer of an lf node to a proto node - // The version is automatically selected based on the LF -> Proto mapping defined at the beginning of this class - private def nodeTransformer( - nodeId: lf.transaction.NodeId - ): PartialTransformer[lf.transaction.Node, iss.DamlTransaction.Node] = PartialTransformer { - lfNode => - val transformerResult = lfNode match { - // Rollback nodes are not versioned so lfNode.optVersion will be empty - // Just pick the transformer for the min version as it doesn't matter here - case _: Node.Rollback => getEncoderForVersion(SerializationVersion.minVersion) - case _ => - lfNode.optVersion - .toRight("Expected a node version but was empty") - .toResult - .flatMap(getEncoderForVersion) - } - - for { - transformer <- transformerResult - versionedNode <- transformer.transform(lfNode) - } yield iss.DamlTransaction.Node( - nodeId = nodeId.transformInto[String], - versionedNode = versionedNode, - ) - } - - // Transformer for a full transaction - private def transactionTransformer( - nodeSeeds: Option[ImmArray[(NodeId, crypto.Hash)]] - ): PartialTransformer[lf.transaction.VersionedTransaction, iss.DamlTransaction] = Transformer - .definePartial[lf.transaction.VersionedTransaction, iss.DamlTransaction] - .withFieldComputed(_.roots, _.roots.map(_.transformInto[String]).toSeq) - .withFieldComputedPartial( - _.nodes, - _.nodes.toList.traverse { case (nodeId, node) => - node.transformIntoPartial[iss.DamlTransaction.Node](nodeTransformer(nodeId)) - }, - ) - .withFieldConst(_.nodeSeeds, nodeSeeds.transformInto[Seq[iss.DamlTransaction.NodeSeed]]) - .buildTransformer - - private implicit val globalKeyTransformer: PartialTransformer[GlobalKey, iscd.GlobalKey] = - PartialTransformer.derive - - private implicit def globalKeyWithMaintainersTransformer(implicit - serializationVersion: SerializationVersion - ): PartialTransformer[GlobalKeyWithMaintainers, iscd.GlobalKeyWithMaintainers] = - if (serializationVersion == SerializationVersion.V1) { - PartialTransformer[GlobalKeyWithMaintainers, iscd.GlobalKeyWithMaintainers](_ => - Result.fromErrorString("Keys are not supported on LF Serialization version 1") - ) - } else { - PartialTransformer - .define[GlobalKeyWithMaintainers, iscd.GlobalKeyWithMaintainers] - .withFieldComputedPartial( - _.key, - lfKey => lfKey.globalKey.transformIntoPartial[iscd.GlobalKey].map(Some(_)), - ) - .withFieldComputed(_.maintainers, _.maintainers.toSeq.sorted) - .buildTransformer - } - - private implicit val inputContractTransformer - : PartialTransformer[ExternalInputContract, Metadata.InputContract] = - Transformer - .definePartial[ExternalInputContract, Metadata.InputContract] - .withFieldComputedPartial( - _.contract, - { contract => - // We show the enriched contract that contains label and type information - val lfCreate = contract.enrichedContract.toCreateNode - - // Encode the disclosed contract with the matching version - getEncoderForVersion(lfCreate.version) - .flatMap(_.transform(lfCreate)) - .flatMap { - // The encoding should have produced a create node, anything else is an error - case VersionedNode.V1(isdv1.Node(create: isdv1.Node.NodeType.Create)) => - Result.fromValue(iss.Metadata.InputContract.Contract.V1(create.value)) - case _ => - Result.fromErrorString("Failed to encode disclosed contract to create contract") - } - }, - ) - .withFieldComputed(_.createdAt, fci => CreationTime.encode(fci.enrichedContract.createdAt)) - .withFieldComputedPartial(_.eventBlob, _.toCreateEventBlob.leftMap(_.errorMessage).toResult) - .buildTransformer - - private implicit val submitterInfoTransformer - : Transformer[SubmitterInfo, iss.Metadata.SubmitterInfo] = - Transformer - .define[SubmitterInfo, iss.Metadata.SubmitterInfo] - // The hashing algorithm expects the actAs field to be sorted, so pre-sort them for the client - .withFieldComputed(_.actAs, _.actAs.sorted.map(_.transformInto[String])) - .buildTransformer - - // Transformer for the transaction metadata - private def resultToMetadataTransformer( - synchronizer: Synchronizer, - transactionUUID: UUID, - mediatorGroup: Int, - inputContracts: Seq[ExternalInputContract], - maxRecordTime: Option[lf.data.Time.Timestamp], - ): PartialTransformer[PrepareTransactionData, iss.Metadata] = - Transformer - .definePartial[PrepareTransactionData, iss.Metadata] - .withFieldComputed(_.preparationTime, _.transactionMeta.preparationTime.transformInto[Long]) - .withFieldConstPartial( - _.inputContracts, - // The hashing algorithm expects disclosed contracts to be sorted by contract ID, so pre-sort them for the client - inputContracts.toList - .sortBy(_.originalContract.contractId.coid) - .traverse( - _.transformIntoPartial[iss.Metadata.InputContract] - ), - ) - .withFieldComputed( - _.submitterInfo, - d => Some(d.submitterInfo.transformInto[iss.Metadata.SubmitterInfo]), - ) - .withFieldConst( - _.globalKeyMapping, - Seq.empty, // This field is deprecated - ) - .withFieldConst(_.synchronizerId, synchronizer.transformInto[String]) - .withFieldConst(_.transactionUuid, transactionUUID.toString) - .withFieldConst(_.mediatorGroup, mediatorGroup) - .withFieldComputed( - _.minLedgerEffectiveTime, - _.transactionMeta.timeBoundaries.minConstraint.map(_.transformInto[Long]), - ) - .withFieldComputed( - _.maxLedgerEffectiveTime, - _.transactionMeta.timeBoundaries.maxConstraint.map(_.transformInto[Long]), - ) - .withFieldConst( - _.maxRecordTime, - maxRecordTime.map(_.transformInto[Long]), - ) - .buildTransformer - - @VisibleForTesting - private[interactive] def serializeTransaction( - transaction: lf.transaction.VersionedTransaction, - nodeSeeds: Option[ImmArray[(NodeId, crypto.Hash)]], - )(implicit - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[DamlTransaction] = { - implicit val traceContext: TraceContext = loggingContext.traceContext - implicit val implicitTransactionTransformer - : PartialTransformer[lf.transaction.VersionedTransaction, iss.DamlTransaction] = - transactionTransformer(nodeSeeds) - - // Convert the LF transaction to the interactive submission proto, this is where all the implicits above - // kick in. - transaction - .transformIntoPartial[iss.DamlTransaction] - .toFutureWithLoggedFailuresEncode("Failed to serialize prepared transaction", logger) - } - - def encode(prepareTransactionData: PrepareTransactionData)(implicit - executionContext: ExecutionContext, - loggingContext: LoggingContextWithTrace, - errorLoggingContext: ErrorLoggingContext, - ): Future[iss.PreparedTransaction] = { - val transactionUUID = prepareTransactionData.transactionUUID - val mediatorGroup = prepareTransactionData.mediatorGroup - implicit val traceContext: TraceContext = loggingContext.traceContext - implicit val metadataTransformer: PartialTransformer[PrepareTransactionData, Metadata] = - resultToMetadataTransformer( - prepareTransactionData.synchronizer, - transactionUUID, - mediatorGroup, - prepareTransactionData.inputContracts.values.toSeq, - prepareTransactionData.maxRecordTime, - ) - val versionedTransaction = lf.transaction.VersionedTransaction( - prepareTransactionData.transaction.version, - prepareTransactionData.transaction.nodes, - prepareTransactionData.transaction.roots, - ) - for { - serializedTransaction <- serializeTransaction( - versionedTransaction, - prepareTransactionData.transactionMeta.optNodeSeeds, - ) - metadata <- prepareTransactionData - .transformIntoPartial[iss.Metadata] - .toFutureWithLoggedFailuresEncode("Failed to serialize metadata", logger) - } yield { - iss.PreparedTransaction( - transaction = Some(serializedTransaction), - metadata = Some(metadata), - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/logging/package.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/logging/package.scala deleted file mode 100644 index 750d8d99bc..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/logging/package.scala +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.logging.entries.ToLoggingKey.* -import com.daml.logging.entries.{LoggingEntries, LoggingEntry, LoggingKey, LoggingValue} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.{ - Commands, - CumulativeFilter, - EventFormat, - TemplateWildcardFilter, - TopologyFormat, - TransactionFormat, - TransactionShape, - UpdateFormat, -} -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.daml.lf.data.Ref.{Identifier, Party} -import com.digitalasset.daml.lf.data.logging.* -import com.digitalasset.daml.lf.value.Value.ContractId - -package object logging { - - private[services] def parties(partyNames: Iterable[Party]): LoggingEntry = - "parties" -> partyNames - - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - private[services] def partyStrings(partyNames: Iterable[String]): LoggingEntry = - parties(partyNames.asInstanceOf[Iterable[Party]]) - - private[services] def party(partyName: Party): LoggingEntry = - "parties" -> Seq(partyName) - - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - private[services] def partyString(partyName: String): LoggingEntry = - party(partyName.asInstanceOf[Party]) - - private[services] def actAs(partyNames: Iterable[Party]): LoggingEntry = - "actAs" -> partyNames - - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - private[services] def actAsStrings(partyNames: Iterable[String]): LoggingEntry = - actAs(partyNames.asInstanceOf[Iterable[Party]]) - - private[services] def readAs(partyNames: Iterable[Party]): LoggingEntry = - "readAs" -> partyNames - - private[services] def submitter(id: String): LoggingEntry = - "submitter" -> id - - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - private[services] def readAsStrings(partyNames: Iterable[String]): LoggingEntry = - readAs(partyNames.asInstanceOf[Iterable[Party]]) - - private[services] def startExclusive(offset: Option[Offset]): LoggingEntry = - "startExclusive" -> offset.fold(0L)(_.unwrap) - - private[services] def startExclusiveOpt(offset: Option[Option[Offset]]): LoggingEntry = - "startExclusiveOpt" -> offset.map(_.fold(0L)(_.unwrap)) - - private[services] def endInclusive( - offset: Option[Offset] - ): LoggingEntry = - "endInclusive" -> offset - - private[services] def descendingOrder( - descendingOrder: Boolean - ): LoggingEntry = - "descendingOrder" -> descendingOrder - - private[services] def offset(offset: Long): LoggingEntry = - "offset" -> offset.toString - - private[services] def activeAtOffset( - offset: Option[Offset] - ): LoggingEntry = - "activeAtOffset" -> offset - - private[services] def commandId(id: String): LoggingEntry = - "commandId" -> id - - private[services] def maxPageSize(size: Int): LoggingEntry = - "maxPageSize" -> size - - private[services] def continueStreamFromIncl( - offset: Option[Offset] - ): LoggingEntry = - "continueStreamFromIncl" -> offset - - private[services] def eventFormat( - eventFormat: EventFormat - ): LoggingEntry = - "filters" -> LoggingValue.Nested( - LoggingEntries.fromMap( - eventFormat.filtersByParty.view.map { case (party, partyFilters) => - party.toLoggingKey -> filtersToLoggingValue(partyFilters) - }.toMap ++ - eventFormat.filtersForAnyParty.fold(Map.empty[LoggingKey, LoggingValue])(filters => - Map("anyParty" -> filtersToLoggingValue(filters)) - ) - ) - ) - - private[services] def transactionShape( - transactionShape: TransactionShape - ): LoggingEntry = - "transactionShape" -> LoggingValue.OfString( - transactionShape match { - case TransactionShape.LedgerEffects => "LedgerEffects" - case TransactionShape.AcsDelta => "AcsDelta" - } - ) - - private[services] def transactionFormat( - transactionFormat: TransactionFormat - ): LoggingEntry = - "transaction format" -> LoggingValue.Nested( - LoggingEntries.fromMap( - Map( - "filters" -> eventFormat(transactionFormat.eventFormat)._2, - transactionShape(transactionFormat.transactionShape), - ) - ) - ) - - private[services] def topologyFormat( - topologyFormat: TopologyFormat - ): LoggingEntry = - "topologyFormat" -> LoggingValue.Nested( - LoggingEntries.fromMap( - Map( - "participantAuthorizationFormat" -> topologyFormat.participantAuthorizationFormat - .map(_.parties match { - case Some(parties) => (if (parties.isEmpty) "all parties" else parties): LoggingValue - case None => LoggingValue.Empty - }) - .getOrElse(LoggingValue.Empty) - ) - ) - ) - - private[services] def updateFormat( - updateFormat: UpdateFormat - ): LoggingEntry = - "updateFormat" -> LoggingValue.Nested( - LoggingEntries.fromMap( - Map( - "transaction format" -> LoggingValue.OfIterable( - updateFormat.includeTransactions - .map(transactionFormat(_)._2) - .toList - ), - "reassignment filters" -> LoggingValue.OfIterable( - updateFormat.includeReassignments - .map(eventFormat(_)._2) - .toList - ), - "topology format" -> LoggingValue.OfIterable( - updateFormat.includeTopologyEvents - .map(topologyFormat(_)._2) - .toList - ), - ) - ) - ) - - private def filtersToLoggingValue(filter: CumulativeFilter): LoggingValue = - LoggingValue.Nested( - LoggingEntries.fromMap( - Map( - "templates" -> LoggingValue.from( - filter.templateFilters.map(_.templateTypeRef) - ), - "interfaces" -> LoggingValue.from( - filter.interfaceFilters.map(_.interfaceTypeRef) - ), - ) - ++ (filter.templateWildcardFilter match { - case Some(TemplateWildcardFilter(includeCreatedEventBlob)) => - Map( - "all-templates, created_event_blob" -> LoggingValue.from( - includeCreatedEventBlob - ) - ) - case None => Map.empty - }) - ) - ) - - private[services] def submissionId(id: String): LoggingEntry = - "submissionId" -> id - - private[services] def updateId(id: String): LoggingEntry = - "updateId" -> id - - private[services] def updateId(id: UpdateId): LoggingEntry = - "updateId" -> id.toHexString - - private[services] def workflowId(id: String): LoggingEntry = - "workflowId" -> id - - private[services] def packageId(id: String): LoggingEntry = - "packageId" -> id - - private[services] def commands(cmds: Commands): LoggingEntry = - "commands" -> cmds - - private[services] def verbose(v: Boolean): LoggingEntry = - "verbose" -> v - - private[services] def contractId(id: ContractId): LoggingEntry = - "contractId" -> id.coid - - private[services] def templateId(id: Identifier): LoggingEntry = - "templateId" -> id.toString -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/package.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/package.scala deleted file mode 100644 index d383ca27b6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/package.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.ledger.api.v2.commands.Commands -import com.daml.ledger.api.v2.reassignment_commands.ReassignmentCommands -import com.daml.tracing.SpanAttribute -import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} - -package object services { - - def getAnnotatedCommandTraceContext(commands: Option[Commands]): TraceContext = { - val traceContext = TraceContextGrpc.fromGrpcContext - commands.foreach { commands => - traceContext.setSpanAttributes( - Seq( - SpanAttribute.UserId -> commands.userId, - SpanAttribute.CommandId -> commands.commandId, - SpanAttribute.Submitter -> commands.actAs.headOption.getOrElse(""), - SpanAttribute.WorkflowId -> commands.workflowId, - ) - ) - } - traceContext - } - - def getAnnotatedReassignmentCommandTraceContext( - commands: Option[ReassignmentCommands] - ): TraceContext = { - val traceContext = TraceContextGrpc.fromGrpcContext - commands.foreach { commands => - traceContext.setSpanAttributes( - Seq( - SpanAttribute.UserId -> commands.userId, - SpanAttribute.CommandId -> commands.commandId, - SpanAttribute.Submitter -> commands.submitter, - SpanAttribute.WorkflowId -> commands.workflowId, - ) - ) - } - traceContext - } - - def getPrepareRequestTraceContext( - userId: String, - commandId: String, - actAs: Seq[String], - ): TraceContext = { - val traceContext = TraceContextGrpc.fromGrpcContext - traceContext.setSpanAttributes( - Seq( - SpanAttribute.UserId -> userId, - SpanAttribute.CommandId -> commandId, - SpanAttribute.Submitter -> actAs.headOption.getOrElse(""), - ) - ) - traceContext - } - - def getExecuteRequestTraceContext( - userId: String, - commandId: Option[String], - actAs: Seq[String], - ): TraceContext = { - val traceContext = TraceContextGrpc.fromGrpcContext - traceContext.setSpanAttributes( - Seq( - SpanAttribute.UserId -> userId, - SpanAttribute.CommandId -> commandId.getOrElse(""), - SpanAttribute.Submitter -> actAs.headOption.getOrElse(""), - ) - ) - traceContext - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CancellableTimeoutSupport.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CancellableTimeoutSupport.scala deleted file mode 100644 index f806dde9f5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CancellableTimeoutSupport.scala +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.tracking - -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.config -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext - -import java.util.{Timer, TimerTask} -import scala.concurrent.Promise -import scala.util.Try -import scala.util.control.NonFatal - -trait CancellableTimeoutSupport { - def scheduleOnce[T]( - duration: config.NonNegativeFiniteDuration, - promise: Promise[T], - onTimeout: => Try[T], - )(implicit traceContext: TraceContext): AutoCloseable -} - -object CancellableTimeoutSupport { - def owner( - timerThreadName: String, - loggerFactory: NamedLoggerFactory, - ): ResourceOwner[CancellableTimeoutSupport] = - ResourceOwner - .forTimer(() => new Timer(timerThreadName, true)) - .map(new CancellableTimeoutSupportImpl(_, loggerFactory)) -} - -private[tracking] class CancellableTimeoutSupportImpl( - timer: Timer, - val loggerFactory: NamedLoggerFactory, -) extends CancellableTimeoutSupport - with NamedLogging { - override def scheduleOnce[T]( - duration: config.NonNegativeFiniteDuration, - promise: Promise[T], - onTimeout: => Try[T], - )(implicit traceContext: TraceContext): AutoCloseable = { - val timerTask = new TimerTask { - override def run(): Unit = - try { - promise.tryComplete(onTimeout).discard - } catch { - case NonFatal(e) => - val exceptionMessage = - "Exception thrown in complete. Resources might have not been appropriately cleaned" - logger.error(exceptionMessage, e) - } - } - timer.schedule(timerTask, duration.underlying.toMillis) - () => timerTask.cancel().discard - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CompletionResponse.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CompletionResponse.scala deleted file mode 100644 index 4f7751947f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CompletionResponse.scala +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.tracking - -import com.daml.ledger.api.v2.completion.Completion as PbCompletion - -final case class CompletionResponse(completion: PbCompletion) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/StreamTracker.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/StreamTracker.scala deleted file mode 100644 index b64d532074..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/StreamTracker.scala +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.tracking - -import com.daml.ledger.resources.ResourceOwner -import com.daml.metrics.api.MetricHandle -import com.digitalasset.canton.config -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors -import com.digitalasset.canton.tracing.{Spanning, TraceContext} -import com.digitalasset.canton.util.Thereafter.syntax.ThereafterAsyncOps -import io.grpc.StatusRuntimeException -import io.opentelemetry.api.trace.Tracer - -import java.util.concurrent.atomic.AtomicInteger -import scala.collection.concurrent.TrieMap -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Success, Try} - -// StreamTracker allows you to manage a workflow where you send a message, -// and wait for a corresponding message to be returned on a separate stream. -// -// The result stream must initially be instrumented to call `onStreamItem` on each item returned. -// Thereafter `track` may be called, passing the initiating action and a key, and returning a Future. -// The Future will return the first subsequent stream item which corresponds to the tracked key. -trait StreamTracker[K, I] extends AutoCloseable { - def track( - key: K, - timeout: NonNegativeFiniteDuration, - )( - start: TraceContext => FutureUnlessShutdown[Any] - )(implicit - ec: ExecutionContext, - errorLogger: ErrorLoggingContext, - traceContext: TraceContext, - tracer: Tracer, - errors: StreamTracker.Errors[K], - ): Future[I] - - def onStreamItem(item: I): Unit -} - -object StreamTracker { - def withTimer[Key, Item]( - timer: java.util.Timer, - itemKey: Item => Option[Key], - inFlightCounter: InFlight, - loggerFactory: NamedLoggerFactory, - ): StreamTracker[Key, Item] = - new StreamTrackerImpl( - new CancellableTimeoutSupportImpl(timer, loggerFactory), - itemKey, - inFlightCounter, - loggerFactory, - ) - - def owner[Key, Item]( - trackerThreadName: String, - itemKey: Item => Option[Key], - inFlightCounter: InFlight, - loggerFactory: NamedLoggerFactory, - ): ResourceOwner[StreamTracker[Key, Item]] = - for { - timeoutSupport <- CancellableTimeoutSupport - .owner(s"$trackerThreadName-timeout-timer", loggerFactory) - streamTracker <- ResourceOwner.forCloseable(() => - new StreamTrackerImpl( - timeoutSupport, - itemKey, - inFlightCounter, - loggerFactory, - ) - ) - } yield streamTracker - - trait Errors[Key] { - def timedOut(key: Key)(implicit errorLogger: ErrorLoggingContext): StatusRuntimeException - def duplicated(key: Key)(implicit - errorLogger: ErrorLoggingContext - ): StatusRuntimeException - } -} - -private[tracking] class StreamTrackerImpl[Key, Item]( - cancellableTimeoutSupport: CancellableTimeoutSupport, - itemKey: Item => Option[Key], - inFlightCounter: InFlight, - val loggerFactory: NamedLoggerFactory, -) extends StreamTracker[Key, Item] - with NamedLogging - with Spanning { - - private[tracking] val pending = - TrieMap.empty[Key, (ErrorLoggingContext, Promise[Item])] - - private val pendingSize = new AtomicInteger(0) - - override def track( - key: Key, - timeout: NonNegativeFiniteDuration, - )( - start: TraceContext => FutureUnlessShutdown[Any] - )(implicit - ec: ExecutionContext, - errorLoggingContext: ErrorLoggingContext, - traceContext: TraceContext, - tracer: Tracer, - errors: StreamTracker.Errors[Key], - ): Future[Item] = - inFlightCounter.check(pendingSize.get()) { - val promise = Promise[Item]() - pending.putIfAbsent(key, (errorLoggingContext, promise)) match { - case Some(_) => promise.failure(errors.duplicated(key)(errorLoggingContext)) - case None => - pendingSize.incrementAndGet().discard - trackWithCancelTimeout(key, timeout, promise, start) - } - promise.future - } - - private def trackWithCancelTimeout( - key: Key, - timeout: config.NonNegativeFiniteDuration, - promise: Promise[Item], - start: TraceContext => FutureUnlessShutdown[Any], - )(implicit - ec: ExecutionContext, - errorLogger: ErrorLoggingContext, - traceContext: TraceContext, - tracer: Tracer, - errors: StreamTracker.Errors[Key], - ): Unit = - Try( - // Start the timeout timer before start to ensure that the timer scheduling - // happens before its cancellation (on start failure OR onStreamItem) - cancellableTimeoutSupport.scheduleOnce( - duration = timeout, - promise = promise, - onTimeout = Failure(errors.timedOut(key)(errorLogger)), - ) - ) match { - case Failure(err) => - logger.error( - "An internal error occurred while trying to register the cancellation timeout. Aborting..", - err, - ) - pending.remove(key).foreach(_ => pendingSize.decrementAndGet().discard) - promise.tryFailure(err).discard - case Success(cancelTimeout) => - withSpan("StreamTracker.track") { childContext => _ => - start(childContext) - .onComplete { - case Success(_) => // succeeded, nothing to do - case Failure(throwable) => - // Start failed, finishing entry with the very same error - promise.tryComplete(Failure(throwable)).discard[Boolean] - } - } - promise.future.onComplete { _ => - // register timeout cancellation and removal from map - withSpan("StreamTracker.complete") { _ => _ => - cancelTimeout.close() - pending.remove(key).foreach(_ => pendingSize.decrementAndGet().discard) - } - } - } - - override def onStreamItem(item: Item): Unit = - itemKey(item).flatMap(pending.get(_)).foreach { case (_traceCtx, promise) => - promise.tryComplete(Success(item)).discard - } - - override def close(): Unit = - pending.values.foreach { case (traceCtx, promise) => - promise.tryFailure(GrpcErrors.AbortedDueToShutdown.Error()(traceCtx).asGrpcError).discard - } -} - -trait InFlight { - def check[T](currCount: Int)( - f: => Future[T] - )(implicit ec: ExecutionContext, errorLogger: ErrorLoggingContext): Future[T] -} - -object InFlight { - final case class Limited(maxCount: Int, metric: MetricHandle.Counter) extends InFlight { - import com.digitalasset.canton.ledger.error.LedgerApiErrors - - def check[T](currCount: Int)( - f: => Future[T] - )(implicit - ec: ExecutionContext, - errorLogger: ErrorLoggingContext, - ): Future[T] = - if (currCount < maxCount) { - metric.inc() - f.thereafter(_ => metric.dec()) - } else { - Future.failed( - LedgerApiErrors.ParticipantBackpressure - .Rejection("Maximum number of in-flight requests reached") - .asGrpcError - ) - } - } - - object Unlimited extends InFlight { - def check[T](currCount: Int)( - f: => Future[T] - )(implicit - ec: ExecutionContext, - errorLogger: ErrorLoggingContext, - ): Future[T] = f - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/SubmissionTracker.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/SubmissionTracker.scala deleted file mode 100644 index 43853b6917..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/tracking/SubmissionTracker.scala +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.tracking - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.ledger.api.v2.completion.Completion -import com.daml.ledger.api.v2.reassignment_commands.ReassignmentCommands -import com.daml.ledger.resources.ResourceOwner -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.ledger.error.CommonErrors -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker.SubmissionKey -import com.digitalasset.canton.tracing.{Spanning, TraceContext} -import io.grpc.StatusRuntimeException -import io.opentelemetry.api.trace.Tracer - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -trait SubmissionTracker extends AutoCloseable { - def track( - submissionKey: SubmissionKey, - timeout: NonNegativeFiniteDuration, - submit: TraceContext => FutureUnlessShutdown[Any], - )(implicit - errorLogger: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[CompletionResponse] - - def onCompletion(completionStreamResponse: CompletionStreamResponse): Unit -} - -object SubmissionTracker { - type Submitters = Set[String] - - implicit object Errors extends StreamTracker.Errors[SubmissionKey] { - import com.digitalasset.canton.ledger.error.groups.ConsistencyErrors - - override def timedOut(k: SubmissionKey)(implicit - errorLogger: ErrorLoggingContext - ): StatusRuntimeException = - CommonErrors.RequestTimeOut - .Reject( - s"Timed out while awaiting for a completion corresponding to a command submission with command-id=${k.commandId} and submission-id=${k.submissionId}.", - definiteAnswer = false, - ) - .asGrpcError - - override def duplicated(k: SubmissionKey)(implicit - errorLogger: ErrorLoggingContext - ): StatusRuntimeException = - ConsistencyErrors.SubmissionAlreadyInFlight - .Reject() - .asGrpcError - } - - def toKey(c: Completion) = Some(SubmissionKey.fromCompletion(c)) - - def owner( - maxCommandsInFlight: Int, - metrics: LedgerApiServerMetrics, - tracer: Tracer, - loggerFactory: NamedLoggerFactory, - ): ResourceOwner[SubmissionTracker] = - for { - streamTracker <- StreamTracker.owner( - trackerThreadName = "submission-tracker", - toKey, - InFlight.Limited(maxCommandsInFlight, metrics.commands.maxInFlightLength), - loggerFactory, - ) - tracker <- ResourceOwner.forCloseable(() => - new SubmissionTrackerImpl( - streamTracker, - maxCommandsInFlight, - metrics, - loggerFactory, - )(tracer) - ) - } yield tracker - - private[tracking] class SubmissionTrackerImpl( - streamTracker: StreamTracker[SubmissionKey, Completion], - maxCommandsInFlight: Int, - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, - )(implicit val tracer: Tracer) - extends SubmissionTracker - with Spanning - with NamedLogging { - - implicit val directEc: ExecutionContext = DirectExecutionContext(noTracingLogger) - - // Set max-in-flight capacity - metrics.commands.maxInFlightCapacity.inc(maxCommandsInFlight.toLong)(MetricsContext.Empty) - - override def track( - submissionKey: SubmissionKey, - timeout: NonNegativeFiniteDuration, - submit: TraceContext => FutureUnlessShutdown[Any], - )(implicit - errorLoggingContext: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[CompletionResponse] = - ensuringSubmissionIdPopulated(submissionKey) { - streamTracker - .track(submissionKey, timeout)(submit) - .flatMap(c => Future.fromTry(Result.fromCompletion(errorLoggingContext, c))) - } - - override def onCompletion(completionStreamResponse: CompletionStreamResponse): Unit = - completionStreamResponse.completionResponse.completion.foreach { completion => - streamTracker.onStreamItem(completion) - } - - override def close(): Unit = - streamTracker.close() - - private def ensuringSubmissionIdPopulated[T](submissionKey: SubmissionKey)(f: => Future[T])( - implicit errorLogger: ErrorLoggingContext - ): Future[T] = - // We need submissionId for tracking submissions - if (submissionKey.submissionId.isEmpty) { - Future.failed( - CommonErrors.ServiceInternalError - .Generic("Missing submission id in submission tracker")(errorLogger) - .asGrpcError - ) - } else { - f - } - } - - final case class SubmissionKey( - commandId: String, - submissionId: String, - userId: String, - parties: Set[String], - ) - - object SubmissionKey { - def fromCompletion(completion: Completion): SubmissionKey = - SubmissionKey( - commandId = completion.commandId, - submissionId = completion.submissionId, - userId = completion.userId, - parties = completion.actAs.toSet, - ) - - def fromReassignmentCommands(commands: ReassignmentCommands): SubmissionKey = - SubmissionKey( - commandId = commands.commandId, - submissionId = commands.submissionId, - userId = commands.userId, - parties = Set(commands.submitter), - ) - } - - object Result { - import com.google.rpc.status - import io.grpc.protobuf.StatusProto - - def fromCompletion( - errorLogger: ErrorLoggingContext, - completion: Completion, - ): Try[CompletionResponse] = - completion.status - .toRight(missingStatusError(errorLogger)) - .toTry - .flatMap { - case status if status.code == 0 => - Success(CompletionResponse(completion)) - case nonZeroStatus => - Failure( - StatusProto.toStatusRuntimeException( - status.Status.toJavaProto(nonZeroStatus) - ) - ) - } - - private def missingStatusError(errorLogger: ErrorLoggingContext): StatusRuntimeException = - CommonErrors.ServiceInternalError - .Generic( - "Missing status in completion response", - throwableO = None, - )(errorLogger) - .asGrpcError - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/FieldNames.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/FieldNames.scala deleted file mode 100644 index b62f564d1c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/FieldNames.scala +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.daml.ledger.api.v2.admin -import scalapb.GeneratedMessageCompanion - -object FieldNames { - object User { - val id: String = resolveFieldName(admin.user_management_service.User)(_.ID_FIELD_NUMBER) - val primaryParty: String = - resolveFieldName(admin.user_management_service.User)(_.PRIMARY_PARTY_FIELD_NUMBER) - val isDeactivated: String = - resolveFieldName(admin.user_management_service.User)(_.IS_DEACTIVATED_FIELD_NUMBER) - val metadata: String = - resolveFieldName(admin.user_management_service.User)(_.METADATA_FIELD_NUMBER) - val identityProviderId: String = - resolveFieldName(admin.user_management_service.User)(_.IDENTITY_PROVIDER_ID_FIELD_NUMBER) - val primaryPartyAuthentication: String = - resolveFieldName(admin.user_management_service.User)( - _.PRIMARY_PARTY_AUTHENTICATION_FIELD_NUMBER - ) - } - object Metadata { - val annotations: String = - resolveFieldName(admin.object_meta.ObjectMeta)(_.ANNOTATIONS_FIELD_NUMBER) - val resourceVersion: String = - resolveFieldName(admin.object_meta.ObjectMeta)(_.RESOURCE_VERSION_FIELD_NUMBER) - } - - object PartyDetails { - val party: String = - resolveFieldName(admin.party_management_service.PartyDetails)(_.PARTY_FIELD_NUMBER) - val localMetadata: String = - resolveFieldName(admin.party_management_service.PartyDetails)(_.LOCAL_METADATA_FIELD_NUMBER) - val isLocal: String = - resolveFieldName(admin.party_management_service.PartyDetails)(_.IS_LOCAL_FIELD_NUMBER) - val identityProviderId: String = - resolveFieldName(admin.party_management_service.PartyDetails)( - _.IDENTITY_PROVIDER_ID_FIELD_NUMBER - ) - } - - object IdentityProviderConfig { - val identityProviderId: String = - resolveFieldName(admin.identity_provider_config_service.IdentityProviderConfig)( - _.IDENTITY_PROVIDER_ID_FIELD_NUMBER - ) - val issuer = - resolveFieldName(admin.identity_provider_config_service.IdentityProviderConfig)( - _.ISSUER_FIELD_NUMBER - ) - val isDeactivated = - resolveFieldName(admin.identity_provider_config_service.IdentityProviderConfig)( - _.IS_DEACTIVATED_FIELD_NUMBER - ) - val jwksUrl = - resolveFieldName(admin.identity_provider_config_service.IdentityProviderConfig)( - _.JWKS_URL_FIELD_NUMBER - ) - val audience = - resolveFieldName(admin.identity_provider_config_service.IdentityProviderConfig)( - _.AUDIENCE_FIELD_NUMBER - ) - } - - private def resolveFieldName[A <: GeneratedMessageCompanion[?]]( - companion: A - )(getFieldNumberFun: A => Int): String = { - val fieldNumber = getFieldNumberFun(companion) - companion.scalaDescriptor - .findFieldByNumber(fieldNumber) - .getOrElse(sys.error(s"Unknown field number $fieldNumber on $companion")) - .name - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/IdentityProviderConfigUpdateMapper.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/IdentityProviderConfigUpdateMapper.scala deleted file mode 100644 index e6e7755090..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/IdentityProviderConfigUpdateMapper.scala +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.localstore.api.IdentityProviderConfigUpdate - -object IdentityProviderConfigUpdateMapper extends UpdateMapperBase { - - import UpdateRequestsPaths.IdentityProviderConfigPaths - - type Resource = IdentityProviderConfigUpdate - type Update = IdentityProviderConfigUpdate - - override val fullResourceTrie: UpdatePathsTrie = IdentityProviderConfigPaths.fullUpdateTrie - - override def makeUpdateObject( - identityProviderConfig: IdentityProviderConfigUpdate, - updateTrie: UpdatePathsTrie, - ): Result[IdentityProviderConfigUpdate] = - for { - isDeactivatedUpdate <- resolveIsDeactivatedUpdate( - updateTrie, - identityProviderConfig.isDeactivatedUpdate, - ) - issuerUpdate <- resolveIssuerUpdate(updateTrie, identityProviderConfig.issuerUpdate) - jwksUrlUpdate <- resolveJwksUrlUpdate(updateTrie, identityProviderConfig.jwksUrlUpdate) - audienceUpdate <- resolveAudienceUpdate( - updateTrie, - identityProviderConfig.audienceUpdate, - ) - } yield { - IdentityProviderConfigUpdate( - identityProviderId = identityProviderConfig.identityProviderId, - isDeactivatedUpdate = isDeactivatedUpdate, - jwksUrlUpdate = jwksUrlUpdate, - issuerUpdate = issuerUpdate, - audienceUpdate = audienceUpdate, - ) - } - - def resolveAudienceUpdate( - updateTrie: UpdatePathsTrie, - newValue: Option[Option[String]], - ): Result[Option[Option[String]]] = - updateTrie - .findMatch(IdentityProviderConfigPaths.audience) - .fold(noUpdate[Option[String]])(updateMatch => - makePrimitiveFieldUpdate[Option[String]]( - updateMatch = updateMatch, - defaultValue = None, - newValue = newValue.flatten, - ) - ) - - def resolveIsDeactivatedUpdate( - updateTrie: UpdatePathsTrie, - newValue: Option[Boolean], - ): Result[Option[Boolean]] = - updateTrie - .findMatch(IdentityProviderConfigPaths.isDeactivated) - .fold(noUpdate[Boolean])(updateMatch => - if (updateMatch.isExact) { - Right(newValue) - } else { - Right(None) - } - ) - - def resolveIssuerUpdate( - updateTrie: UpdatePathsTrie, - newValue: Option[String], - ): Result[Option[String]] = - updateTrie - .findMatch(IdentityProviderConfigPaths.issuer) - .fold(noUpdate[String])(updateMatch => - if (updateMatch.isExact) { - Right(newValue) - } else { - Right(None) - } - ) - - def resolveJwksUrlUpdate( - updateTrie: UpdatePathsTrie, - newValue: Option[JwksUrl], - ): Result[Option[JwksUrl]] = - updateTrie - .findMatch(IdentityProviderConfigPaths.jwksUrl) - .fold(noUpdate[JwksUrl])(updateMatch => - if (updateMatch.isExact) { - Right(newValue) - } else { - Right(None) - } - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/PartyRecordUpdateMapper.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/PartyRecordUpdateMapper.scala deleted file mode 100644 index d315ce4a9f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/PartyRecordUpdateMapper.scala +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.digitalasset.canton.ledger.api.PartyDetails -import com.digitalasset.canton.ledger.localstore.api.{ObjectMetaUpdate, PartyDetailsUpdate} - -object PartyRecordUpdateMapper extends UpdateMapperBase { - - import UpdateRequestsPaths.PartyDetailsPaths - - type Resource = PartyDetails - type Update = PartyDetailsUpdate - - override val fullResourceTrie: UpdatePathsTrie = PartyDetailsPaths.fullUpdateTrie - - override def makeUpdateObject( - partyRecord: PartyDetails, - updateTrie: UpdatePathsTrie, - ): Result[PartyDetailsUpdate] = - for { - annotationsUpdate <- resolveAnnotationsUpdate(updateTrie, partyRecord.metadata.annotations) - isLocalUpdate <- resolveIsLocalUpdate(updateTrie, partyRecord.isLocal) - } yield { - PartyDetailsUpdate( - party = partyRecord.party, - identityProviderId = partyRecord.identityProviderId, - isLocalUpdate = isLocalUpdate, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = partyRecord.metadata.resourceVersionO, - annotationsUpdateO = annotationsUpdate, - ), - ) - } - - def resolveIsLocalUpdate( - updateTrie: UpdatePathsTrie, - newValue: Boolean, - ): Result[Option[Boolean]] = - updateTrie - .findMatch(PartyDetailsPaths.isLocal) - .fold(noUpdate[Boolean])(updateMatch => - makePrimitiveFieldUpdate( - updateMatch = updateMatch, - defaultValue = false, - newValue = newValue, - ) - ) - - def resolveAnnotationsUpdate( - updateTrie: UpdatePathsTrie, - newValue: Map[String, String], - ): Result[Option[Map[String, String]]] = - updateTrie - .findMatch(PartyDetailsPaths.annotations) - .fold(noUpdate[Map[String, String]])(updateMatch => - makeAnnotationsUpdate(newValue = newValue, updateMatch = updateMatch) - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdateMapperBase.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdateMapperBase.scala deleted file mode 100644 index e59bd9493a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdateMapperBase.scala +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import cats.syntax.either.* -import com.digitalasset.canton.platform.apiserver.update.UpdatePathsTrie.MatchResult -import com.google.protobuf.field_mask.FieldMask - -trait UpdateMapperBase { - - type Resource - type Update - - /** A trie containing all update paths. Used for validating the input update mask paths. - */ - def fullResourceTrie: UpdatePathsTrie - - protected[update] def makeUpdateObject( - apiObject: Resource, - updateTrie: UpdatePathsTrie, - ): Result[Update] - - /** Validates its input and produces an update object. NOTE: The return update object might - * represent an empty (no-op) update. - * - * @param apiObject - * represents the new values for the update - * @param updateMask - * indicates which fields should get updated - */ - final def toUpdate( - apiObject: Resource, - updateMask: FieldMask, - ): Result[Update] = - for { - updateTrie <- makeUpdateTrie(updateMask) - updateObject <- makeUpdateObject(apiObject, updateTrie) - } yield { - updateObject - } - - private def makeUpdateTrie(updateMask: FieldMask): Result[UpdatePathsTrie] = - for { - _ <- Either.cond(updateMask.paths.nonEmpty, (), UpdatePathError.EmptyUpdateMask) - parsedPaths <- UpdatePath.parseAll(updateMask.paths) - _ <- validatePathsMatchValidFields(parsedPaths) - updateTrie <- UpdatePathsTrie.fromPaths(parsedPaths) - } yield updateTrie - - protected[update] final def noUpdate[A]: Result[Option[A]] = Right(None) - - protected[update] final def validatePathsMatchValidFields( - paths: Seq[UpdatePath] - ): Result[Unit] = - paths.foldLeft(Either.unit[UpdatePathError]) { (ax, parsedPath) => - for { - _ <- ax - _ <- Either.cond( - fullResourceTrie.containsPrefix(parsedPath.fieldPath), - (), - UpdatePathError.UnknownFieldPath(parsedPath.toRawString), - ) - } yield () - } - - protected[update] final def makeAnnotationsUpdate( - updateMatch: MatchResult, - newValue: Map[String, String], - ): Result[Option[Map[String, String]]] = { - val isDefaultValue = newValue == Map.empty - def some = Right(Some(newValue)) - if (updateMatch.isExact) { - some - } else { - if (isDefaultValue) noUpdate else some - } - } - - protected[update] final def makePrimitiveFieldUpdate[A]( - updateMatch: MatchResult, - defaultValue: A, - newValue: A, - ): Result[Option[A]] = { - val isDefaultValue = newValue == defaultValue - val some = Right(Some(newValue)) - if (updateMatch.isExact) { - some - } else { - if (isDefaultValue) noUpdate else some - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePath.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePath.scala deleted file mode 100644 index 2819d90ce6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePath.scala +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -final case class UpdatePath(fieldPath: List[String]) { - def toRawString: String = fieldPath.mkString(".") -} - -object UpdatePath { - - def parseAll(rawPaths: Seq[String]): Result[Seq[UpdatePath]] = { - val parsedPathsResult: Result[Seq[UpdatePath]] = rawPaths - .map(UpdatePath.parseSingle) - .foldLeft[Result[Seq[UpdatePath]]](Right(Seq.empty)) { (ax, next) => - for { - a <- ax - b <- next - } yield { - a :+ b - } - } - parsedPathsResult - } - - private[update] def parseSingle(rawPath: String): Result[UpdatePath] = - Right(UpdatePath(rawPath.split('.').toList)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathError.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathError.scala deleted file mode 100644 index 1c4399ffb8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathError.scala +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -sealed trait UpdatePathError { - def getReason: String = getClass.getSimpleName -} - -object UpdatePathError { - - private def shorten(s: String): String = - if (s.length > 53) { s.take(50) + "..." } - else s - - final case class UnknownFieldPath(rawUpdatePath: String) extends UpdatePathError { - override def getReason: String = - s"The update path: '${shorten(rawUpdatePath)}' points to an unknown field." - } - - final case class DuplicatedFieldPath(rawUpdatePath: String) extends UpdatePathError { - override def getReason: String = - s"The update path: '${shorten(rawUpdatePath)}' is duplicated." - } - - final case object EmptyUpdateMask extends UpdatePathError { - override def getReason: String = "The update mask contains no entries" - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathsTrie.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathsTrie.scala deleted file mode 100644 index 71a4b1d763..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathsTrie.scala +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.digitalasset.canton.discard.Implicits.DiscardOps - -import scala.annotation.tailrec -import scala.collection.immutable.SortedMap - -import collection.mutable - -object UpdatePathsTrie { - final case class MatchResult( - isExact: Boolean, - matchedPath: UpdatePath, - ) - - def apply( - exists: Boolean, - nodes: (String, UpdatePathsTrie)* - ) = new UpdatePathsTrie( - exists = exists, - nodes = SortedMap.from(nodes), - ) - - def fromPaths( - paths: Seq[List[String]] - )(implicit dummy: DummyImplicit): Result[UpdatePathsTrie] = - fromPaths(paths.map(UpdatePath(_))) - - def fromPaths(paths: Seq[UpdatePath]): Result[UpdatePathsTrie] = { - val builder: Result[Builder] = Right(Builder(exists = false)) - val buildResult = paths.foldLeft(builder)((builderResult, path) => - for { - b <- builderResult - _ <- b.insertUniquePath(path) - } yield b - ) - buildResult.map(build) - } - - private def build(builder: Builder): UpdatePathsTrie = - new UpdatePathsTrie( - exists = builder.exists, - nodes = SortedMap.from(builder.nodes.view.mapValues(build)), - ) - - private object Builder { - def apply( - exists: Boolean, - nodes: (String, Builder)* - ) = new Builder( - exists = exists, - nodes = mutable.SortedMap.from(nodes), - ) - } - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private final case class Builder( - nodes: mutable.SortedMap[String, Builder], - var exists: Boolean = false, - ) { - - /** @param updatePath - * unique path to be inserted - */ - def insertUniquePath(updatePath: UpdatePath): Result[Unit] = - Either.cond( - doInsertUniquePath(updatePath.fieldPath), - (), - UpdatePathError.DuplicatedFieldPath(updatePath.toRawString), - ) - - /** @return - * true if successfully inserted the field path, false if the field path was already present - * in this trie - */ - @tailrec - private def doInsertUniquePath( - fieldPath: List[String] - ): Boolean = - fieldPath match { - case Nil => - if (this.exists) { - false - } else { - this.exists = true - true - } - case key :: subpath => - if (!nodes.contains(key)) { - val empty = new Builder(nodes = mutable.SortedMap.empty, exists = false) - nodes.put(key, empty).discard - } - nodes(key).doInsertUniquePath(subpath) - } - - } -} - -/** Data structure for storing and querying update paths. - * - * Each update path specifies: - * - a field path corresponding to a field of an update request proto message, - * - an update modifier. - * - * See also [[com.google.protobuf.field_mask.FieldMask]]). - */ -private[update] final case class UpdatePathsTrie( - nodes: SortedMap[String, UpdatePathsTrie], - exists: Boolean, -) { - import UpdatePathsTrie.* - - /** @return - * true if 'path' matches some prefix of some field path - */ - @tailrec - final def containsPrefix(path: List[String]): Boolean = - path match { - case Nil => true - case head :: rest if nodes.contains(head) => nodes(head).containsPrefix(rest) - case _ => false - } - - /** There is a match if this trie contains 'path' or if it contains a prefix of 'path'. - * @return - * the match corresponding to the longest matched field path, none otherwise. - */ - def findMatch(path: List[String]): Option[MatchResult] = - if (pathExists(path)) { - Some(MatchResult(isExact = true, matchedPath = UpdatePath(path))) - } else { - val properPrefixesLongestFirst = - path.inits.filter(init => init.sizeCompare(path) != 0).toList.sortBy(-_.length) - properPrefixesLongestFirst.iterator - .find(pathExists) - .map { prefix => - MatchResult(isExact = false, matchedPath = UpdatePath(prefix)) - } - } - - /** @return - * an update modifier of a matching field path, none if there is no matching field path - */ - @tailrec - final private[update] def pathExists(path: List[String]): Boolean = - path match { - case Nil => this.exists - case head :: subpath if nodes.contains(head) => nodes(head).pathExists(subpath) - case _ => false - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdateRequestsPaths.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdateRequestsPaths.scala deleted file mode 100644 index e1825b2dd6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UpdateRequestsPaths.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -protected[update] object UpdateRequestsPaths { - - object UserPaths { - val id: List[String] = List(FieldNames.User.id) - val annotations: List[String] = List(FieldNames.User.metadata, FieldNames.Metadata.annotations) - val resourceVersion: List[String] = - List(FieldNames.User.metadata, FieldNames.Metadata.resourceVersion) - val primaryParty: List[String] = List(FieldNames.User.primaryParty) - val isDeactivated: List[String] = List(FieldNames.User.isDeactivated) - val identityProviderId: List[String] = List(FieldNames.User.identityProviderId) - val primaryPartyAuthentication: List[String] = List(FieldNames.User.primaryPartyAuthentication) - val fullUpdateTrie: UpdatePathsTrie = UpdatePathsTrie - .fromPaths( - Seq( - id, - primaryParty, - isDeactivated, - annotations, - resourceVersion, - identityProviderId, - primaryPartyAuthentication, - ) - ) - .getOrElse(sys.error("Failed to create full update user tree. This should never happen")) - } - - object PartyDetailsPaths { - val party: List[String] = List(FieldNames.PartyDetails.party) - val annotations: List[String] = - List(FieldNames.PartyDetails.localMetadata, FieldNames.Metadata.annotations) - val resourceVersion: List[String] = - List(FieldNames.PartyDetails.localMetadata, FieldNames.Metadata.resourceVersion) - val isLocal: List[String] = List(FieldNames.PartyDetails.isLocal) - val identityProviderId: List[String] = List(FieldNames.PartyDetails.identityProviderId) - val fullUpdateTrie: UpdatePathsTrie = UpdatePathsTrie - .fromPaths( - Seq( - party, - isLocal, - annotations, - resourceVersion, - identityProviderId, - ) - ) - .getOrElse(sys.error("Failed to create full update user tree. This should never happen")) - } - - object IdentityProviderConfigPaths { - val identityProviderId: List[String] = List( - FieldNames.IdentityProviderConfig.identityProviderId - ) - val isDeactivated: List[String] = List(FieldNames.IdentityProviderConfig.isDeactivated) - val jwksUrl: List[String] = List(FieldNames.IdentityProviderConfig.jwksUrl) - val issuer: List[String] = List(FieldNames.IdentityProviderConfig.issuer) - val audience: List[String] = List(FieldNames.IdentityProviderConfig.audience) - val fullUpdateTrie: UpdatePathsTrie = UpdatePathsTrie - .fromPaths( - Seq( - identityProviderId, - isDeactivated, - jwksUrl, - issuer, - audience, - ) - ) - .getOrElse( - sys.error( - "Failed to create full update identity provider config tree. This should never happen" - ) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UserUpdateMapper.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UserUpdateMapper.scala deleted file mode 100644 index 1d2ac0aed0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/UserUpdateMapper.scala +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.digitalasset.canton.ledger.api.User -import com.digitalasset.canton.ledger.localstore.api.{ObjectMetaUpdate, UserUpdate} -import com.digitalasset.daml.lf.data.Ref - -object UserUpdateMapper extends UpdateMapperBase { - - import UpdateRequestsPaths.UserPaths - - type Resource = User - type Update = UserUpdate - - override val fullResourceTrie: UpdatePathsTrie = UserPaths.fullUpdateTrie - - override def makeUpdateObject(user: User, updateTrie: UpdatePathsTrie): Result[UserUpdate] = - for { - annotationsUpdate <- resolveAnnotationsUpdate(updateTrie, user.metadata.annotations) - primaryPartyUpdate <- resolvePrimaryPartyUpdate(updateTrie, user.primaryParty) - isDeactivatedUpdate <- isDeactivatedUpdateResult(updateTrie, user.isDeactivated) - primaryPartyAuthenticationUpdate <- primaryPartyAuthenticationUpdateResult( - updateTrie, - user.primaryPartyAuthentication, - ) - } yield { - UserUpdate( - id = user.id, - identityProviderId = user.identityProviderId, - primaryPartyUpdateO = primaryPartyUpdate, - isDeactivatedUpdateO = isDeactivatedUpdate, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = user.metadata.resourceVersionO, - annotationsUpdateO = annotationsUpdate, - ), - primaryPartyAuthenticationUpdateO = primaryPartyAuthenticationUpdate, - ) - } - - def resolveAnnotationsUpdate( - updateTrie: UpdatePathsTrie, - newValue: Map[String, String], - ): Result[Option[Map[String, String]]] = - updateTrie - .findMatch(UserPaths.annotations) - .fold(noUpdate[Map[String, String]])(updateMatch => - makeAnnotationsUpdate(newValue = newValue, updateMatch = updateMatch) - ) - - def resolvePrimaryPartyUpdate( - updateTrie: UpdatePathsTrie, - newValue: Option[Ref.Party], - ): Result[Option[Option[Ref.Party]]] = - updateTrie - .findMatch(UserPaths.primaryParty) - .fold(noUpdate[Option[Ref.Party]])(updateMatch => - makePrimitiveFieldUpdate( - updateMatch = updateMatch, - defaultValue = None, - newValue = newValue, - ) - ) - - def isDeactivatedUpdateResult( - updateTrie: UpdatePathsTrie, - newValue: Boolean, - ): Result[Option[Boolean]] = - updateTrie - .findMatch(UserPaths.isDeactivated) - .fold(noUpdate[Boolean])(matchResult => - makePrimitiveFieldUpdate( - updateMatch = matchResult, - defaultValue = false, - newValue = newValue, - ) - ) - - def primaryPartyAuthenticationUpdateResult( - updateTrie: UpdatePathsTrie, - newValue: Boolean, - ): Result[Option[Boolean]] = - updateTrie - .findMatch(UserPaths.primaryPartyAuthentication) - .fold(noUpdate[Boolean])(matchResult => - makePrimitiveFieldUpdate( - updateMatch = matchResult, - defaultValue = false, - newValue = newValue, - ) - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/update.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/update.scala deleted file mode 100644 index 48e3b293b2..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/update/update.scala +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -package object update { - type Result[T] = Either[UpdatePathError, T] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/CommandServiceConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/CommandServiceConfig.scala deleted file mode 100644 index 7ce3302d3f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/CommandServiceConfig.scala +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.config.RequireTypes.PositiveInt - -import java.time.Duration - -/** Configuration for the Ledger API Command Service. - * - * @param defaultTrackingTimeout - * The duration that the command service will keep tracking an active command by default. This - * value will be used if a timeout is not specified on a gRPC request. - * @param maxCommandsInFlight - * Maximum number of submitted commands waiting to be completed in parallel. Commands submitted - * after this limit is reached will be rejected. - * @param contractPrefetchingDepth - * Levels of pre-fetching before interpretation. This allows the engine to preload all referenced - * contract ids before interpretation to avoid single contract lookups that have to be done in - * the database. - */ -final case class CommandServiceConfig( - defaultTrackingTimeout: NonNegativeFiniteDuration = - CommandServiceConfig.DefaultDefaultTrackingTimeout, - maxCommandsInFlight: Int = CommandServiceConfig.DefaultMaxCommandsInFlight, - contractPrefetchingDepth: PositiveInt = CommandServiceConfig.DefaultContractPrefetchingDepth, -) - -object CommandServiceConfig { - val DefaultDefaultTrackingTimeout: NonNegativeFiniteDuration = NonNegativeFiniteDuration( - Duration.ofMinutes(5) - ) - val DefaultMaxCommandsInFlight: Int = 256 - lazy val Default: CommandServiceConfig = CommandServiceConfig() - lazy val DefaultContractPrefetchingDepth: PositiveInt = PositiveInt.tryCreate(3) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/IdentityProviderManagementConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/IdentityProviderManagementConfig.scala deleted file mode 100644 index a3bd7856cf..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/IdentityProviderManagementConfig.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.config.NonNegativeFiniteDuration - -final case class IdentityProviderManagementConfig( - cacheExpiryAfterWrite: NonNegativeFiniteDuration = - IdentityProviderManagementConfig.DefaultCacheExpiryAfterWriteInSeconds -) -object IdentityProviderManagementConfig { - val MaxIdentityProviders: Int = 16 - val DefaultCacheExpiryAfterWriteInSeconds: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofMinutes(5) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/IndexServiceConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/IndexServiceConfig.scala deleted file mode 100644 index fcf3ac3150..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/IndexServiceConfig.scala +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.typesafe.scalalogging.Logger - -import scala.concurrent.duration.{Duration, FiniteDuration} - -/** Ledger api index service specific configurations - * - * @param bufferedEventsProcessingParallelism - * parallelism for loading and decoding ledger events for populating ledger api server's internal - * buffers - * @param bufferedStreamsPageSize - * the page size for streams created from ledger api server's in-memory buffers - * @param maxContractStateCacheSize - * maximum caffeine cache size of mutable state cache of contracts - * @param maxContractKeyStateCacheSize - * maximum caffeine cache size of mutable state cache of contract keys - * @param maxTransactionsInMemoryFanOutBufferSize - * maximum number of transactions to hold in the "in-memory fanout" (if enabled) - * @param apiStreamShutdownTimeout - * shutdown timeout for a graceful completion of ledger api server's streams - * @param inMemoryStateUpdaterParallelism - * the processing parallelism of the Ledger API server in-memory state updater - * @param apiQueryServicesThreadPoolSize - * size of the thread-pool backing the Ledger API query-services (fe not the command - * submission/interpretation). If not set, defaults to ((number of thread)/4 + 1) - * @param preparePackageMetadataTimeOutWarning - * timeout for package metadata preparation after which a warning will be logged. deprecated - * @param completionsPageSize - * database / pekko page size for batching of ledger api server index ledger completion queries - * @param activeContractsServiceStreams - * configurations pertaining to the ledger api server's "active contracts service" - * @param updatesStreams - * configurations pertaining to the ledger api server's streams of updates - * @param globalMaxEventIdQueries - * maximum number of concurrent event id queries across all stream types - * @param globalMaxEventPayloadQueries - * maximum number of concurrent event payload queries across all stream types - * @param offsetCheckpointCacheUpdateInterval - * the interval duration for OffsetCheckpoint cache updates - * @param idleStreamOffsetCheckpointTimeout - * the timeout duration for checking if a new OffsetCheckpoint is created - * @param maxLookupLimit - * the maximum limit for contract key lookups. Requests will be capped at this value. - */ -final case class IndexServiceConfig( - bufferedEventsProcessingParallelism: Int = - IndexServiceConfig.DefaultBufferedEventsProcessingParallelism, - bufferedStreamsPageSize: Int = IndexServiceConfig.DefaultBufferedStreamsPageSize, - maxContractStateCacheSize: Long = IndexServiceConfig.DefaultMaxContractStateCacheSize, - maxContractKeyStateCacheSize: Long = IndexServiceConfig.DefaultMaxContractKeyStateCacheSize, - maxTransactionsInMemoryFanOutBufferSize: Int = - IndexServiceConfig.DefaultMaxTransactionsInMemoryFanOutBufferSize, - apiStreamShutdownTimeout: Duration = IndexServiceConfig.DefaultApiStreamShutdownTimeout, - inMemoryStateUpdaterParallelism: Int = - IndexServiceConfig.DefaultInMemoryStateUpdaterParallelism, - apiQueryServicesThreadPoolSize: Option[Int] = None, - preparePackageMetadataTimeOutWarning: NonNegativeFiniteDuration = - IndexServiceConfig.PreparePackageMetadataTimeOutWarning, - completionsPageSize: Int = IndexServiceConfig.DefaultCompletionsPageSize, - activeContractsServiceStreams: ActiveContractsServiceStreamsConfig = - ActiveContractsServiceStreamsConfig.default, - updatesStreams: UpdatesStreamsConfig = UpdatesStreamsConfig.default, - globalMaxEventIdQueries: Int = 20, - globalMaxEventPayloadQueries: Int = 10, - offsetCheckpointCacheUpdateInterval: NonNegativeFiniteDuration = - IndexServiceConfig.OffsetCheckpointCacheUpdateInterval, - idleStreamOffsetCheckpointTimeout: NonNegativeFiniteDuration = - IndexServiceConfig.IdleStreamOffsetCheckpointTimeout, - contractPruningMaxRetries: Int = IndexServiceConfig.DefaultContractPruningMaxRetries, - contractPruningDelayBeforeRetry: NonNegativeFiniteDuration = - IndexServiceConfig.DefaultContractPruningDelayBeforeRetry, - maxLookupLimit: Int = IndexServiceConfig.DefaultMaxLookupLimit, -) - -object IndexServiceConfig { - val DefaultBufferedEventsProcessingParallelism: Int = 8 - val DefaultBufferedStreamsPageSize: Int = 100 - val DefaultMaxContractStateCacheSize: Long = 10000L - val DefaultMaxContractKeyStateCacheSize: Long = 10000L - val DefaultMaxTransactionsInMemoryFanOutBufferSize: Int = 1000 - val DefaultApiStreamShutdownTimeout: Duration = FiniteDuration(5, "seconds") - val DefaultInMemoryStateUpdaterParallelism: Int = 2 - val PreparePackageMetadataTimeOutWarning: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofSeconds(5) - val DefaultCompletionsPageSize = 1000 - val OffsetCheckpointCacheUpdateInterval: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofSeconds(15) - val IdleStreamOffsetCheckpointTimeout: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofMinutes(1) - val DefaultContractPruningMaxRetries: Int = 10 - val DefaultContractPruningDelayBeforeRetry: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofSeconds(2) - val DefaultMaxLookupLimit: Int = 1000 - - def DefaultQueryServicesThreadPoolSize(logger: Logger): Int = { - val numberOfThreads = Threading.detectNumberOfThreads(logger).value - numberOfThreads / 4 + 1 - } -} - -/** Ledger api active contracts service specific configurations - * - * @param maxIdsPerIdPage - * Number of event ids to retrieve in a single query (a page of event ids). - * @param maxPagesPerIdPagesBuffer - * Number of id pages to store in a buffer. There is a buffer for each decomposed filtering - * constraint. - * @param maxWorkingMemoryInBytesForIdPages - * Memory for storing id pages across all id pages buffers. Per single stream. - * @param maxPayloadsPerPayloadsPage - * Number of parallel queries that fetch payloads of create events. Per single stream. - * @param maxParallelActiveIdQueries - * Number of parallel queries that fetch ids of create events. Per single stream. - * @param maxParallelPayloadCreateQueries - * Number of event payloads to retrieve in a single query (a page of event payloads). - * @param contractProcessingParallelism - * The parallelism for contract processing. - * @param maxIncompletePageSize - * The maximum size of an incomplete page. Stream is chunked up this into groups of this size. - */ -final case class ActiveContractsServiceStreamsConfig( - maxIdsPerIdPage: Int = ActiveContractsServiceStreamsConfig.DefaultAcsIdPageSize, - maxPagesPerIdPagesBuffer: Int = ActiveContractsServiceStreamsConfig.DefaultAcsIdPageBufferSize, - maxWorkingMemoryInBytesForIdPages: Int = - ActiveContractsServiceStreamsConfig.DefaultAcsIdPageWorkingMemoryBytes, - maxPayloadsPerPayloadsPage: Int = ActiveContractsServiceStreamsConfig.DefaultEventsPageSize, - maxParallelActiveIdQueries: Int = - ActiveContractsServiceStreamsConfig.DefaultAcsIdFetchingParallelism, - idFilterQueryParallelism: Int = - ActiveContractsServiceStreamsConfig.DefaultAcsIdFilterQueryParallelism, - // Must be a power of 2 - maxParallelPayloadCreateQueries: Int = - ActiveContractsServiceStreamsConfig.DefaultAcsContractFetchingParallelism, - contractProcessingParallelism: Int = - ActiveContractsServiceStreamsConfig.DefaultContractProcessingParallelism, - // Temporary incomplete population parameters - maxIncompletePageSize: Int = 20, -) - -object ActiveContractsServiceStreamsConfig { - val DefaultEventsPageSize: Int = 1000 - val DefaultAcsIdPageSize: Int = 20000 - val DefaultAcsIdPageBufferSize: Int = 1 - val DefaultAcsIdPageWorkingMemoryBytes: Int = 100 * 1024 * 1024 - val DefaultAcsIdFetchingParallelism: Int = 4 - val DefaultAcsIdFilterQueryParallelism: Int = 2 - // Must be a power of 2 - val DefaultAcsContractFetchingParallelism: Int = 2 - val DefaultContractProcessingParallelism: Int = 8 - - val default: ActiveContractsServiceStreamsConfig = ActiveContractsServiceStreamsConfig() - -} - -/** Updates stream configuration. - * - * @param maxIdsPerIdPage - * Number of event ids to retrieve in a single query (a page of event ids). - * @param maxPagesPerIdPagesBuffer - * Number of id pages to store in a buffer. There is a buffer for each decomposed filtering - * constraint. - * @param maxWorkingMemoryInBytesForIdPages - * Memory for storing id pages across all id pages buffers. Per single stream. - * @param maxPayloadsPerPayloadsPage - * Number of event payloads to retrieve in a single query (a page of event payloads). - * @param maxParallelIdActivateQueries - * Number of parallel queries that fetch ids of create events. Per single stream. - * @param maxParallelIdDeactivateQueries - * Number of parallel queries that fetch ids of consuming events. Per single stream. - * @param maxParallelIdTopologyEventsQueries - * Number of parallel queries that fetch payloads of topology events. Per single stream. - * @param maxParallelPayloadActivateQueries - * Number of parallel queries that fetch payloads of create events. Per single stream. - * @param maxParallelPayloadDeactivateQueries - * Number of parallel queries that fetch payloads of consuming events. Per single stream. - * @param maxParallelPayloadTopologyEventsQueries - * Number of parallel queries that fetch ids of topology events. Per single stream. - * @param maxParallelPayloadQueries - * Upper bound on the number of parallel queries that fetch payloads. Per single stream. - * @param transactionsProcessingParallelism - * Number of transactions to process in parallel. Per single stream. - */ - -final case class UpdatesStreamsConfig( - maxIdsPerIdPage: Int = 20000, - maxPagesPerIdPagesBuffer: Int = 1, - maxWorkingMemoryInBytesForIdPages: Int = 100 * 1024 * 1024, - maxPayloadsPerPayloadsPage: Int = 1000, - maxParallelIdActivateQueries: Int = 4, - maxParallelIdDeactivateQueries: Int = 4, - maxParallelIdVariousWitnessedQueries: Int = 4, - maxParallelIdTopologyEventsQueries: Int = 4, - maxParallelPayloadActivateQueries: Int = 2, - maxParallelPayloadDeactivateQueries: Int = 2, - maxParallelPayloadVariousWitnessedQueries: Int = 2, - maxParallelPayloadTopologyEventsQueries: Int = 2, - maxParallelPayloadQueries: Int = 2, - transactionsProcessingParallelism: Int = 8, - idFilterQueryParallelism: Int = 2, -) -object UpdatesStreamsConfig { - val default: UpdatesStreamsConfig = UpdatesStreamsConfig() -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/InteractiveSubmissionServiceConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/InteractiveSubmissionServiceConfig.scala deleted file mode 100644 index 6197bc3d4e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/InteractiveSubmissionServiceConfig.scala +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.config.RequireTypes.PositiveInt - -/** Configuration for the Ledger API Interactive Submission Service. - * - * @param enableVerboseHashing - * if true, the prepare RPC will gather information about the hashing process of the transaction - * and return it as part of the response, if the "verboseHashing" flag was set on the request. If - * false (default), the "verboseHashing" flag on the prepare request has no effect. - * @param contractLookupParallelism - * When the transaction uses input contracts, the preparing participant will attempt to look them - * up from its local store if they are not explicitly disclosed. This limits the parallelism at - * which this lookup will be done to avoid potentially overwhelming the database - * @param enforceSingleRootNode - * Reject early requests with multiple commands (prepare) or multiple root nodes (execute). Such - * transactions are not supported and would be rejected anyway during confirmation. Disabling - * this check won't provide functional support for those transactions, the flag only rejects them - * early and prevent their submission to the synchronizer. - * @param maximumNumberOfSignaturesPerParty - * Maximum number of external signatures that will be allowed per party per submission. - */ -final case class InteractiveSubmissionServiceConfig( - enableVerboseHashing: Boolean = false, - contractLookupParallelism: PositiveInt = PositiveInt.tryCreate(5), - enforceSingleRootNode: Boolean = true, - maximumNumberOfSignaturesPerParty: PositiveInt = PositiveInt.tryCreate(50), -) - -object InteractiveSubmissionServiceConfig { - lazy val Default: InteractiveSubmissionServiceConfig = InteractiveSubmissionServiceConfig() -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/InvalidConfigException.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/InvalidConfigException.scala deleted file mode 100644 index 3213756713..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/InvalidConfigException.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.daml.resources.ProgramResource.StartupException - -class InvalidConfigException(message: String) - extends RuntimeException(message) - with StartupException diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/PackageServiceConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/PackageServiceConfig.scala deleted file mode 100644 index c54e1cf94e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/PackageServiceConfig.scala +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.config.RequireTypes.PositiveInt - -/** Ledger API package service specific configurations - * - * @param maxVettedPackagesPageSize - * maximum number of VettedPackages results returned - */ -final case class PackageServiceConfig( - maxVettedPackagesPageSize: PositiveInt = PackageServiceConfig.DefaultMaxVettedPackagesPageSize -) - -object PackageServiceConfig { - - val DefaultMaxVettedPackagesPageSize: PositiveInt = PositiveInt.tryCreate(100) - - def default: PackageServiceConfig = PackageServiceConfig( - maxVettedPackagesPageSize = DefaultMaxVettedPackagesPageSize - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/PartyManagementServiceConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/PartyManagementServiceConfig.scala deleted file mode 100644 index 174acf79cb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/PartyManagementServiceConfig.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} - -/** Ledger api party management service specific configurations - * - * @param maxPartiesPageSize - * maximum number of parties returned - */ -final case class PartyManagementServiceConfig( - maxPartiesPageSize: PositiveInt = PartyManagementServiceConfig.DefaultMaxPartiesPageSize, - maxSelfAllocatedParties: NonNegativeInt = - PartyManagementServiceConfig.DefaultMaxSelfAllocatedParties, -) - -object PartyManagementServiceConfig { - - val DefaultMaxPartiesPageSize: PositiveInt = PositiveInt.tryCreate(10000) - val DefaultMaxSelfAllocatedParties: NonNegativeInt = NonNegativeInt.tryCreate(0) - - def default: PartyManagementServiceConfig = PartyManagementServiceConfig( - maxPartiesPageSize = DefaultMaxPartiesPageSize, - maxSelfAllocatedParties = DefaultMaxSelfAllocatedParties, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/Readers.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/Readers.scala deleted file mode 100644 index 72855795c7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/Readers.scala +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.daml.tls.TlsVersion -import com.daml.tls.TlsVersion.TlsVersion -import io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth -import scopt.Read - -import java.time.Duration - -object Readers { - - implicit val durationRead: Read[Duration] = new Read[Duration] { - override def arity: Int = 1 - - override val reads: String => Duration = Duration.parse - } - - implicit val clientAuthRead: Read[ClientAuth] = Read.reads { - case "none" => ClientAuth.NONE - case "optional" => ClientAuth.OPTIONAL - case "require" => ClientAuth.REQUIRE - case _ => - throw new InvalidConfigException(s"""Must be one of "none", "optional", or "require".""") - } - - implicit val tlsVersionRead: Read[TlsVersion] = Read.reads { - case "1.2" => TlsVersion.V1_2 - case "1.3" => TlsVersion.V1_3 - case _ => - throw new InvalidConfigException(s"""Must be one of "1.2" or "1.3".""") - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/ServerRole.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/ServerRole.scala deleted file mode 100644 index 0951930373..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/ServerRole.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -/** Used to disambiguate thread pool names. - * - * This is necessary because Hikari connection pools use the pool name when registering metrics. If - * we were to register two connection pools with the same names with a single metrics registry, the - * second would fail with an exception. - */ -sealed trait ServerRole { - val threadPoolSuffix: String -} - -object ServerRole { - - object ApiServer extends ServerRole { - override val threadPoolSuffix: String = "api-server" - } - - object Indexer extends ServerRole { - override val threadPoolSuffix: String = "indexer" - } - - final case class Testing(testClass: Class[?]) extends ServerRole { - override val threadPoolSuffix: String = testClass.getSimpleName.toLowerCase - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/StateServiceConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/StateServiceConfig.scala deleted file mode 100644 index d0a1608753..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/StateServiceConfig.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.platform.config.StateServiceConfig.* - -/** @param maxAcsPageSize - * inclusive limit for max_page_size for GetActiveContractsPage. Requests exceeding this value - * will be rejected. - * @param defaultAcsPageSize - * max_page_size value for GetActiveContractsPage request without max_page_size specified - */ -final case class StateServiceConfig( - maxAcsPageSize: PositiveInt = AcsPageSizeLimit, - defaultAcsPageSize: PositiveInt = DefaultAcsPageSize, -) - -object StateServiceConfig { - private val DefaultAcsPageSize = PositiveInt.tryCreate(500) - private val AcsPageSizeLimit = PositiveInt.tryCreate(10000) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/TopologyAwarePackageSelectionConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/TopologyAwarePackageSelectionConfig.scala deleted file mode 100644 index 937435e6fb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/TopologyAwarePackageSelectionConfig.scala +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.config.RequireTypes.PositiveInt - -/** Ledger API topology-aware package selection specific configurations - * - * @param enabled - * whether to enable topology-aware package selection in command interpretation - */ -final case class TopologyAwarePackageSelectionConfig( - enabled: Boolean = true, - maxPassesDefault: PositiveInt = PositiveInt.three, - maxPassesLimit: PositiveInt = PositiveInt.four, -) - -object TopologyAwarePackageSelectionConfig { - lazy val Default: TopologyAwarePackageSelectionConfig = TopologyAwarePackageSelectionConfig() -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/UpdateServiceConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/UpdateServiceConfig.scala deleted file mode 100644 index ae9aff6bcb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/UpdateServiceConfig.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -import com.digitalasset.canton.config.RequireTypes.{DoubleGreaterEqual1, PositiveInt} -import com.digitalasset.canton.platform.config.UpdateServiceConfig.{ - AscendingFirstPageDynamicBoundOverfetch, - DefaultUpdatesPageSize, - UpdatePageSizeLimit, -} - -/** @param maxUpdatesPageSize - * inclusive limit for max_page_size for getUpdatesPage. Requests exceeding this value will be - * rejected. - * @param defaultUpdatesPageSize - * max_page_size value for getUpdatesPage request without max_page_size specified - * - * @param ascendingFirstPageDynamicBoundOverfetch - * A multiplier by which the page size is multiplied when fetching an ascending updates page with - * dynamic lower bound to avoid interference with pruning offset. The implementation fetches - * (pageSize+1)*ascendingFirstPageDynamicBoundOverfetch elements and if pruning offset in a - * meantime moved in a way that less than pageSize+1 elements are after the pruning offset, the - * page fetch fails, otherwise the first page is successfully returned. Try increasing this value - * if fetching the first page with dynamic lower bound fails ofter. - */ -final case class UpdateServiceConfig( - maxUpdatesPageSize: PositiveInt = UpdatePageSizeLimit, - defaultUpdatesPageSize: PositiveInt = DefaultUpdatesPageSize, - ascendingFirstPageDynamicBoundOverfetch: DoubleGreaterEqual1 = - AscendingFirstPageDynamicBoundOverfetch, -) - -object UpdateServiceConfig { - private val DefaultUpdatesPageSize = - PositiveInt.tryCreate(100) - private val UpdatePageSizeLimit = - PositiveInt.tryCreate(2000) - private val AscendingFirstPageDynamicBoundOverfetch: DoubleGreaterEqual1 = - DoubleGreaterEqual1.tryCreate(4) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/UserManagementServiceConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/UserManagementServiceConfig.scala deleted file mode 100644 index 91814509e6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/UserManagementServiceConfig.scala +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.config - -/** Ledger api user management service specific configurations - * - * @param enabled - * whether to enable participant user management - * @param maxCacheSize - * maximum in-memory cache size for user management state - * @param cacheExpiryAfterWriteInSeconds - * determines the maximum delay for propagating user management state changes - * @param maxUsersPageSize - * maximum number of users returned - * @param maxRightsPerUser - * maximum number of rights per user - * @param additionalAdminUserId - * adds an extra admin - */ -final case class UserManagementServiceConfig( - enabled: Boolean = true, - maxCacheSize: Int = UserManagementServiceConfig.DefaultMaxCacheSize, - cacheExpiryAfterWriteInSeconds: Int = - UserManagementServiceConfig.DefaultCacheExpiryAfterWriteInSeconds, - maxUsersPageSize: Int = UserManagementServiceConfig.DefaultMaxUsersPageSize, - maxRightsPerUser: Int = UserManagementServiceConfig.DefaultMaxRightsPerUser, - additionalAdminUserId: Option[String] = None, -) - -object UserManagementServiceConfig { - - val DefaultMaxCacheSize = 100 - val DefaultCacheExpiryAfterWriteInSeconds = 5 - val DefaultMaxUsersPageSize = 1000 - val DefaultMaxRightsPerUser = 1000 - - def default(enabled: Boolean): UserManagementServiceConfig = UserManagementServiceConfig( - enabled = enabled, - maxCacheSize = DefaultMaxCacheSize, - cacheExpiryAfterWriteInSeconds = DefaultCacheExpiryAfterWriteInSeconds, - maxUsersPageSize = DefaultMaxUsersPageSize, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/ContractStoreBasedMaximumLedgerTimeService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/ContractStoreBasedMaximumLedgerTimeService.scala deleted file mode 100644 index 770e22bf5e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/ContractStoreBasedMaximumLedgerTimeService.scala +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.index - -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.ledger.participant.state.index.{ - ContractState, - ContractStore, - MaximumLedgerTime, - MaximumLedgerTimeService, -} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.ContractId - -import scala.concurrent.Future - -class ContractStoreBasedMaximumLedgerTimeService( - contractStore: ContractStore, - override protected val loggerFactory: NamedLoggerFactory, -) extends MaximumLedgerTimeService - with NamedLogging { - - private val directEc = DirectExecutionContext(noTracingLogger) - - override def lookupMaximumLedgerTimeAfterInterpretation( - ids: Set[Value.ContractId] - )(implicit loggingContext: LoggingContextWithTrace): Future[MaximumLedgerTime] = { - def goAsync( - maximumLedgerTime: Option[Timestamp], - contractIds: List[ContractId], - ): Future[MaximumLedgerTime] = - (maximumLedgerTime, contractIds) match { - case (result, Nil) => - Future.successful(MaximumLedgerTime.from(result)) - - case (resultSoFar, contractId :: otherContractIds) => - contractStore - .lookupContractState(contractId) - .flatMap { - case ContractState.Archived | ContractState.NotFound => - // early termination on the first archived contract in sight - Future.successful(MaximumLedgerTime.Archived(Set(contractId))) - - case active: ContractState.Active => - val createdAt = Some(active.contractInstance.createdAt.time) - val newMaximumLedgerTime = Ordering[Option[Timestamp]].max(resultSoFar, createdAt) - goAsync(newMaximumLedgerTime, otherContractIds) - }(directEc) - } - - goAsync(None, ids.toList) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/InMemoryStateUpdater.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/InMemoryStateUpdater.scala deleted file mode 100644 index 0b45024dbe..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/InMemoryStateUpdater.scala +++ /dev/null @@ -1,650 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.index - -import cats.data.NonEmptyVector -import com.daml.executors.InstrumentedExecutors -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.data.DeduplicationPeriod.{DeduplicationDuration, DeduplicationOffset} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.TopologyEvent.PartyToParticipantAuthorization -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.ledger.participant.state.{CompletionInfo, Update} -import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker -import com.digitalasset.canton.platform.apiserver.services.admin.PartyAllocation -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker -import com.digitalasset.canton.platform.index.InMemoryStateUpdater.{PrepareResult, UpdaterFlow} -import com.digitalasset.canton.platform.indexer.TransactionTraversalUtils.NodeInfo -import com.digitalasset.canton.platform.indexer.parallel.ParallelIndexerSubscription.Batch -import com.digitalasset.canton.platform.store.CompletionFromTransaction -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.cache.OffsetCheckpoint -import com.digitalasset.canton.platform.store.dao.events.ContractStateEvent -import com.digitalasset.canton.platform.store.dao.events.ContractStateEvent.ReassignmentAccepted -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.platform.{InMemoryState, Key} -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.canton.tracing.{SerializableTraceContext, TraceContext} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.transaction.Node.{Create, Exercise} -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.FlowShape -import org.apache.pekko.stream.scaladsl.{Broadcast, Flow, GraphDSL, Merge, Sink, Source} - -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future} - -/** Builder of the in-memory state updater Pekko flow. - * - * This flow is attached at the end of the Indexer pipeline, consumes the - * [[com.digitalasset.canton.ledger.participant.state.Update]]s (that have been ingested by the - * Indexer into the Index database) for populating the Ledger API server in-memory state (see - * [[InMemoryState]]). - */ -private[platform] object InMemoryStateUpdaterFlow { - - private[index] def apply( - prepareUpdatesParallelism: Int, - prepareUpdatesExecutionContext: ExecutionContext, - updateCachesExecutionContext: ExecutionContext, - offsetCheckpointCacheUpdateInterval: FiniteDuration, - metrics: LedgerApiServerMetrics, - )( - inMemoryState: InMemoryState, - prepare: (Vector[(Offset, Update)], LedgerEnd, TraceContext) => PrepareResult, - update: (PrepareResult, Boolean) => Unit, - ): UpdaterFlow = { repairMode => - Flow[Batch[?]] - .filter(_.offsetsUpdates.nonEmpty) - .via(updateOffsetCheckpointCacheFlow(inMemoryState, offsetCheckpointCacheUpdateInterval)) - .mapAsync(prepareUpdatesParallelism) { batch => - Future { - batch -> prepare( - batch.offsetsUpdates, - batch.ledgerEnd, - batch.batchTraceContext, - ) - }(prepareUpdatesExecutionContext) - } - .async - .mapAsync(1) { case (batch, result) => - Future { - update(result, repairMode) - metrics.index.ledgerEndSequentialId.updateValue(result.ledgerEnd.lastEventSeqId) - batch - }(updateCachesExecutionContext) - } - } - - private def updateOffsetCheckpointCacheFlow( - inMemoryState: InMemoryState, - interval: FiniteDuration, - ): Flow[ - Batch[?], - Batch[?], - NotUsed, - ] = { - // tick source so that we update offset checkpoint caches - // tick is denoted by None while the rest elements are encapsulated into a Some - val tick = Source - .tick(interval, interval, None: Option[Nothing]) - .mapMaterializedValue(_ => NotUsed) - - updateOffsetCheckpointCacheFlowWithTickingSource(inMemoryState.offsetCheckpointCache.push, tick) - } - - private[index] def updateOffsetCheckpointCacheFlowWithTickingSource( - updateOffsetCheckpointCache: OffsetCheckpoint => Unit, - tick: Source[Option[Nothing], NotUsed], - ): Flow[ - Batch[?], - Batch[?], - NotUsed, - ] = - Flow.fromGraph(GraphDSL.create() { implicit builder => - import GraphDSL.Implicits.* - // this flow emits the original stream as is while at the same time broadcasts its elements - // through a secondary flow that updates the offset checkpoint cache - // the secondary flow keeps only the Offsets and the Updates of the original stream and merges - // them with a tick source that ticks every interval seconds to signify the update of the cache - - val broadcast = - builder.add(Broadcast[Batch[?]](2)) - - val merge = - builder.add(Merge[Option[(Offset, Update)]](inputPorts = 2, eagerComplete = true)) - - val preprocess: Flow[Batch[?], Option[ - (Offset, Update) - ], NotUsed] = - Flow[Batch[?]] - .map(_.offsetsUpdates) - .mapConcat(identity) - .map(Some(_)) - - val updateCheckpointState: Flow[Option[(Offset, Update)], OffsetCheckpoint, NotUsed] = - Flow[Option[(Offset, Update)]] - .statefulMap[Option[OffsetCheckpoint], Option[OffsetCheckpoint]](create = () => None)( - f = { - // an Offset and Update pair was received - // update the latest checkpoint - case (lastOffsetCheckpointO, Some((off, update))) => - val synchronizerTimeO = update match { - case tx: Update.TransactionAccepted => - Some((tx.synchronizerId, tx.recordTime)) - case reassignment: Update.ReassignmentAccepted => - Some((reassignment.synchronizerId, reassignment.recordTime)) - case commandRejected: Update.CommandRejected => - Some((commandRejected.synchronizerId, commandRejected.recordTime)) - case tt: Update.TopologyTransactionEffective => - Some((tt.synchronizerId, tt.recordTime)) - case sim: Update.SequencerIndexMoved => Some((sim.synchronizerId, sim.recordTime)) - case _: Update.EmptyAcsPublicationRequired => None - case _: Update.LsuTimeReached => None - case _: Update.CommitRepair => None - } - - val lastSynchronizerTimes = - lastOffsetCheckpointO.map(_.synchronizerTimes).getOrElse(Map.empty) - val newSynchronizerTimes = - synchronizerTimeO match { - case Some((synchronizerId, recordTime)) => - lastSynchronizerTimes.updated(synchronizerId, recordTime.toLf) - case None => lastSynchronizerTimes - } - val newOffsetCheckpoint = OffsetCheckpoint(off, newSynchronizerTimes) - (Some(newOffsetCheckpoint), None) - // a tick was received, propagate the OffsetCheckpoint - case (lastOffsetCheckpointO, None) => - (lastOffsetCheckpointO, lastOffsetCheckpointO) - }, - onComplete = _ => None, - ) - .collect { case Some(oc) => oc } - - val pushCheckpoint: Sink[OffsetCheckpoint, NotUsed] = - Sink.foreach(updateOffsetCheckpointCache).mapMaterializedValue(_ => NotUsed) - - (tick ~> merge).discard - broadcast ~> preprocess ~> merge ~> updateCheckpointState ~> pushCheckpoint - - FlowShape(broadcast.in, broadcast.out(1)) - }) - -} - -private[platform] object InMemoryStateUpdater { - final case class PrepareResult( - updates: Vector[TransactionLogUpdate], - ledgerEnd: LedgerEnd, - lastTraceContext: TraceContext, - batchTraceContext: TraceContext, - ) - type UpdaterFlow = - Boolean => Flow[Batch[?], Batch[?], NotUsed] - def owner( - inMemoryState: InMemoryState, - prepareUpdatesParallelism: Int, - offsetCheckpointCacheUpdateInterval: FiniteDuration, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - ): ResourceOwner[UpdaterFlow] = for { - prepareUpdatesExecutor <- ResourceOwner.forExecutorService(() => - InstrumentedExecutors.newWorkStealingExecutor( - metrics.lapi.threadpool.indexBypass.prepareUpdates, - prepareUpdatesParallelism, - ) - ) - updateCachesExecutor <- ResourceOwner.forExecutorService(() => - InstrumentedExecutors.newFixedThreadPool( - metrics.lapi.threadpool.indexBypass.updateInMemoryState, - 1, - ) - ) - logger = loggerFactory.getTracedLogger(getClass) - } yield InMemoryStateUpdaterFlow( - prepareUpdatesParallelism = prepareUpdatesParallelism, - prepareUpdatesExecutionContext = ExecutionContext.fromExecutorService(prepareUpdatesExecutor), - updateCachesExecutionContext = ExecutionContext.fromExecutorService(updateCachesExecutor), - offsetCheckpointCacheUpdateInterval = offsetCheckpointCacheUpdateInterval, - metrics = metrics, - )( - inMemoryState = inMemoryState, - prepare = prepare, - update = update(inMemoryState, logger), - ) - - private[index] def prepare( - batch: Vector[(Offset, Update)], - ledgerEnd: LedgerEnd, - batchTraceContext: TraceContext, - ): PrepareResult = { - val traceContext = batch.lastOption.fold( - throw new NoSuchElementException("empty batch") - )(_._2.traceContext) - PrepareResult( - updates = batch.collect { - case (offset, u: Update.TransactionAccepted) => - convertTransactionAccepted(offset, u) - case (offset, u: Update.CommandRejected) => - convertTransactionRejected(offset, u) - case (offset, u: Update.ReassignmentAccepted) => - convertReassignmentAccepted(offset, u) - case (offset, u: Update.TopologyTransactionEffective) => - convertTopologyTransactionEffective(offset, u) - }, - ledgerEnd = ledgerEnd, - lastTraceContext = traceContext, - batchTraceContext = batchTraceContext, - ) - } - - private[index] def update( - inMemoryState: InMemoryState, - logger: TracedLogger, - )(result: PrepareResult, repairMode: Boolean): Unit = { - updateCaches(inMemoryState, result.updates, result.ledgerEnd, result.batchTraceContext) - // must be the last update: see the comment inside the method for more details - // must be after cache updates: see the comment inside the method for more details - // in case of Repair Mode we will update directly, at the end from the indexer queue - if (!repairMode) { - updateLedgerEnd( - inMemoryState, - result.ledgerEnd, - logger, - )( - result.lastTraceContext - ) - } - // must be after LedgerEnd update because this could trigger API actions relating to this LedgerEnd - // it is expected to be okay to run these in repair mode, as repair operations are not related to tracking - trackTransactionSubmissions(inMemoryState.transactionSubmissionTracker, result.updates) - trackReassignmentSubmissions(inMemoryState.reassignmentSubmissionTracker, result.updates) - // can be done at any point in the pipeline, it is for debugging only - trackCommandProgress(inMemoryState.commandProgressTracker, result.updates) - - trackPartyAllocation( - inMemoryState.partyAllocationTracker, - result.updates, - inMemoryState.participantId, - ) - } - - private def trackTransactionSubmissions( - submissionTracker: SubmissionTracker, - updates: Vector[TransactionLogUpdate], - ): Unit = - updates.view - .collect { - case txAccepted: TransactionLogUpdate.TransactionAccepted => - txAccepted.completionStreamResponseO - - case txRejected: TransactionLogUpdate.TransactionRejected => - Some(txRejected.completionStreamResponse) - } - .flatten - .foreach(submissionTracker.onCompletion) - - private def trackReassignmentSubmissions( - submissionTracker: SubmissionTracker, - updates: Vector[TransactionLogUpdate], - ): Unit = - updates.view - .collect { - case txRejected: TransactionLogUpdate.TransactionRejected => - Some(txRejected.completionStreamResponse) - - case reassignmentAccepted: TransactionLogUpdate.ReassignmentAccepted => - reassignmentAccepted.completionStreamResponseO - } - .flatten - .foreach(submissionTracker.onCompletion) - - private def trackCommandProgress( - commandProgressTracker: CommandProgressTracker, - updates: Vector[TransactionLogUpdate], - ): Unit = - updates.view.foreach(commandProgressTracker.processLedgerUpdate) - - private def trackPartyAllocation( - partyAllocationTracker: PartyAllocation.Tracker, - updates: Vector[TransactionLogUpdate], - participantId: Ref.ParticipantId, - ): Unit = - updates.view - .collect { case TransactionLogUpdate.TopologyTransactionEffective(_, _, _, _, events) => - events.collect { case u: TransactionLogUpdate.PartyToParticipantAuthorization => - PartyAllocation.Completed( - PartyAllocation.TrackerKey(u.party, u.participant, u.authorizationEvent), - IndexerPartyDetails(party = u.party, isLocal = u.participant == participantId), - ) - } - } - .flatten - .foreach(partyAllocationTracker.onStreamItem) - - private[index] def updateCaches( - inMemoryState: InMemoryState, - updates: Vector[TransactionLogUpdate], - ledgerEnd: LedgerEnd, - batchTraceContext: TraceContext, - ): Unit = { - inMemoryState.cachesUpdatedUpto.set(None) // mark caches as being updated - updates.foreach(inMemoryState.inMemoryFanoutBuffer.push) - NonEmptyVector - .fromVector( - updates.iterator - .flatMap(convertToContractStateEvents) - .toVector - ) - .foreach( - inMemoryState.contractStateCaches.push(_, ledgerEnd.lastEventSeqId)(batchTraceContext) - ) - inMemoryState.cachesUpdatedUpto.set(Some(ledgerEnd.lastOffset)) - } - - def updateLedgerEnd( - inMemoryState: InMemoryState, - ledgerEnd: LedgerEnd, - logger: TracedLogger, - )(implicit - traceContext: TraceContext - ): Unit = { - inMemoryState.ledgerEndCache.set(Some(ledgerEnd)) - // the order here is very important: first we need to make data available for point-wise lookups - // and SQL queries, and only then we can make it available on the streams. - // (consider example: completion arrived on a stream, but the transaction cannot be looked up) - inMemoryState.dispatcherState.getDispatcher.signalNewHead(ledgerEnd.lastOffset) - logger.debug(s"Updated ledger end $ledgerEnd") - } - - private[index] def convertLogToStateEvent - : PartialFunction[TransactionLogUpdate.Event, ContractStateEvent] = { - case createdEvent: TransactionLogUpdate.CreatedEvent - // no state updates for participant divulged events and transient events as these events - // cannot lead to successful contract lookup and usage in interpretation anyway - if createdEvent.flatEventWitnesses.nonEmpty => - if (createdEvent.contractKey.isDefined != createdEvent.createKeyHash.isDefined) { - throw new IllegalStateException( - s"Invalid TransactionLogUpdate.CreatedEvent: contractKey and createKeyHash must be both defined or both empty, but was: $createdEvent" - ) - } - ContractStateEvent.Created( - contractId = createdEvent.contractId, - globalKey = createdEvent.contractKey.zip(createdEvent.createKeyHash).map { case (k, kh) => - Key.assertBuild( - createdEvent.templateId, - createdEvent.packageName, - k.unversioned, - kh, - ) - }, - ) - case exercisedEvent: TransactionLogUpdate.ExercisedEvent - // no state updates for participant divulged events and transient events as these events - // cannot lead to successful contract lookup and usage in interpretation anyway - if exercisedEvent.consuming && exercisedEvent.flatEventWitnesses.nonEmpty => - if (exercisedEvent.contractKey.isDefined != exercisedEvent.contractKeyHash.isDefined) { - throw new IllegalStateException( - s"Invalid TransactionLogUpdate.ExercisedEvent: contractKey and contractKeyHash must be both defined or both empty, but was: $exercisedEvent" - ) - } - ContractStateEvent.Archived( - contractId = exercisedEvent.contractId, - globalKey = exercisedEvent.contractKey.flatMap(k => - exercisedEvent.contractKeyHash.map(hash => - Key.assertBuild( - exercisedEvent.templateId, - exercisedEvent.packageName, - k.unversioned, - hash, - ) - ) - ), - ) - } - - private def convertToContractStateEvents( - tx: TransactionLogUpdate - ): Vector[ContractStateEvent] = - tx match { - case tx: TransactionLogUpdate.TransactionAccepted => - tx.events.iterator.collect(convertLogToStateEvent).toVector - case _: TransactionLogUpdate.ReassignmentAccepted => - Vector(ReassignmentAccepted) - case _ => Vector.empty - } - - private def convertTransactionAccepted( - offset: Offset, - txAccepted: Update.TransactionAccepted, - ): TransactionLogUpdate.TransactionAccepted = { - val blinding = txAccepted.transactionInfo.blindingInfo - - val events = txAccepted.transactionInfo.executionOrder.collect { - case NodeInfo(nodeId, create: Create, _) => - val contractId = create.coid - val contractInfo = txAccepted.contractInfos.getOrElse( - contractId, - throw new IllegalStateException( - s"Missing authentication data for contract $contractId" - ), - ) - TransactionLogUpdate.CreatedEvent( - eventOffset = offset, - updateId = txAccepted.updateId.toHexString, - nodeId = nodeId.index, - eventSequentialId = 0L, - contractId = contractId, - ledgerEffectiveTime = txAccepted.transactionMeta.ledgerEffectiveTime, - templateId = create.templateId, - packageName = create.packageName, - packageVersion = None, - commandId = txAccepted.completionInfoO.map(_.commandId).getOrElse(""), - workflowId = txAccepted.transactionMeta.workflowId.getOrElse(""), - contractKey = create.keyOpt.map(k => - com.digitalasset.daml.lf.transaction.Versioned(create.version, k.value) - ), - treeEventWitnesses = blinding.disclosure.getOrElse(nodeId, Set.empty), - flatEventWitnesses = - if (txAccepted.isAcsDelta(contractId)) create.stakeholders else Set.empty, - submitters = txAccepted.completionInfoO - .map(_.actAs.toSet) - .getOrElse(Set.empty), - createArgument = - com.digitalasset.daml.lf.transaction.Versioned(create.version, create.arg), - createSignatories = create.signatories, - createObservers = create.stakeholders.diff(create.signatories), - createKeyHash = create.keyOpt.map(_.globalKey.hash), - createKeyMaintainers = create.keyOpt.map(_.maintainers), - authenticationData = contractInfo.contractAuthenticationData, - representativePackageId = contractInfo.representativePackageId match { - case RepresentativePackageId.SameAsContractPackageId => create.templateId.packageId - case RepresentativePackageId.DedicatedRepresentativePackageId( - representativePackageId - ) => - representativePackageId - }, - ) - case NodeInfo(nodeId, exercise: Exercise, lastDescendantNodeId) => - TransactionLogUpdate.ExercisedEvent( - eventOffset = offset, - updateId = txAccepted.updateId.toHexString, - nodeId = nodeId.index, - eventSequentialId = 0L, - contractId = exercise.targetCoid, - ledgerEffectiveTime = txAccepted.transactionMeta.ledgerEffectiveTime, - templateId = exercise.templateId, - packageName = exercise.packageName, - commandId = txAccepted.completionInfoO.map(_.commandId).getOrElse(""), - workflowId = txAccepted.transactionMeta.workflowId.getOrElse(""), - contractKey = exercise.keyOpt.map(k => - com.digitalasset.daml.lf.transaction.Versioned(exercise.version, k.value) - ), - contractKeyHash = exercise.keyOpt.map(_.globalKey.hash), - treeEventWitnesses = blinding.disclosure.getOrElse(nodeId, Set.empty), - flatEventWitnesses = - if (exercise.consuming && txAccepted.isAcsDelta(exercise.targetCoid)) - exercise.stakeholders - else Set.empty, - submitters = txAccepted.completionInfoO - .map(_.actAs.toSet) - .getOrElse(Set.empty), - choice = exercise.choiceId, - actingParties = exercise.actingParties, - lastDescendantNodeId = lastDescendantNodeId.index, - exerciseArgument = exercise.versionedChosenValue, - exerciseResult = exercise.versionedExerciseResult, - consuming = exercise.consuming, - interfaceId = exercise.interfaceId, - ) - } - - val completionStreamResponse = txAccepted.completionInfoO - .map { completionInfo => - val (deduplicationOffset, deduplicationDurationSeconds, deduplicationDurationNanos) = - deduplicationInfo(completionInfo) - - CompletionFromTransaction.acceptedCompletion( - commonCompletionProperties = CompletionFromTransaction.CommonCompletionProperties - .createFromRecordTimeAndSynchronizerId( - submitters = completionInfo.actAs.toSet, - recordTime = txAccepted.recordTime.toLf, - completionOffset = offset, - commandId = completionInfo.commandId, - userId = completionInfo.userId, - submissionId = completionInfo.submissionId, - deduplicationOffset = deduplicationOffset, - deduplicationDurationSeconds = deduplicationDurationSeconds, - deduplicationDurationNanos = deduplicationDurationNanos, - synchronizerId = txAccepted.synchronizerId.toProtoPrimitive, - traceContext = SerializableTraceContext(txAccepted.traceContext).toDamlProto, - trafficCost = completionInfo.paidTrafficCost.value, - ), - updateId = txAccepted.updateId, - ) - } - - TransactionLogUpdate.TransactionAccepted( - updateId = txAccepted.updateId.toHexString, - commandId = txAccepted.completionInfoO.map(_.commandId).getOrElse(""), - workflowId = txAccepted.transactionMeta.workflowId.getOrElse(""), - effectiveAt = txAccepted.transactionMeta.ledgerEffectiveTime, - offset = offset, - events = events.toVector, - completionStreamResponseO = completionStreamResponse, - synchronizerId = txAccepted.synchronizerId.toProtoPrimitive, - recordTime = txAccepted.recordTime.toLf, - externalTransactionHash = txAccepted.externalTransactionHash, - )(txAccepted.traceContext) - } - - private def convertTransactionRejected( - offset: Offset, - u: Update.CommandRejected, - ): TransactionLogUpdate.TransactionRejected = { - val (deduplicationOffset, deduplicationDurationSeconds, deduplicationDurationNanos) = - deduplicationInfo(u.completionInfo) - - TransactionLogUpdate.TransactionRejected( - offset = offset, - completionStreamResponse = CompletionFromTransaction.rejectedCompletion( - CompletionFromTransaction.CommonCompletionProperties.createFromRecordTimeAndSynchronizerId( - submitters = u.completionInfo.actAs.toSet, - recordTime = u.recordTime.toLf, - completionOffset = offset, - commandId = u.completionInfo.commandId, - userId = u.completionInfo.userId, - submissionId = u.completionInfo.submissionId, - deduplicationOffset = deduplicationOffset, - deduplicationDurationSeconds = deduplicationDurationSeconds, - deduplicationDurationNanos = deduplicationDurationNanos, - synchronizerId = u.synchronizerId.toProtoPrimitive, - traceContext = SerializableTraceContext(u.traceContext).toDamlProto, - trafficCost = u.completionInfo.paidTrafficCost.value, - ), - status = u.reasonTemplate.status, - ), - )(u.traceContext) - } - - private def convertReassignmentAccepted( - offset: Offset, - u: Update.ReassignmentAccepted, - ): TransactionLogUpdate.ReassignmentAccepted = { - val completionStreamResponse = u.optCompletionInfo - .map { completionInfo => - val (deduplicationOffset, deduplicationDurationSeconds, deduplicationDurationNanos) = - deduplicationInfo(completionInfo) - - CompletionFromTransaction.acceptedCompletion( - commonCompletionProperties = CompletionFromTransaction.CommonCompletionProperties - .createFromRecordTimeAndSynchronizerId( - submitters = completionInfo.actAs.toSet, - recordTime = u.recordTime.toLf, - completionOffset = offset, - commandId = completionInfo.commandId, - userId = completionInfo.userId, - submissionId = completionInfo.submissionId, - deduplicationOffset = deduplicationOffset, - deduplicationDurationSeconds = deduplicationDurationSeconds, - deduplicationDurationNanos = deduplicationDurationNanos, - synchronizerId = u.synchronizerId.toProtoPrimitive, - traceContext = SerializableTraceContext(u.traceContext).toDamlProto, - trafficCost = completionInfo.paidTrafficCost.value, - ), - updateId = u.updateId, - ) - } - - TransactionLogUpdate.ReassignmentAccepted( - updateId = u.updateId.toHexString, - commandId = u.optCompletionInfo.map(_.commandId).getOrElse(""), - workflowId = u.workflowId.getOrElse(""), - offset = offset, - recordTime = u.recordTime.toLf, - completionStreamResponseO = completionStreamResponse, - reassignmentInfo = u.reassignmentInfo, - reassignment = u.reassignment, - synchronizerId = u.synchronizerId.toProtoPrimitive, - )(u.traceContext) - } - - private def convertTopologyTransactionEffective( - offset: Offset, - u: Update.TopologyTransactionEffective, - ) = - TransactionLogUpdate.TopologyTransactionEffective( - updateId = u.updateId.toHexString, - offset = offset, - effectiveTime = u.effectiveTime.toLf, - synchronizerId = u.synchronizerId.toProtoPrimitive, - events = u.events - .collect[TransactionLogUpdate.PartyToParticipantAuthorization] { - case event: PartyToParticipantAuthorization => - TransactionLogUpdate.PartyToParticipantAuthorization( - party = event.party, - participant = event.participant, - authorizationEvent = event.authorizationEvent, - ) - } - .toVector, - )(u.traceContext) - - private def deduplicationInfo( - completionInfo: CompletionInfo - ): (Option[Long], Option[Long], Option[Int]) = - completionInfo.optDeduplicationPeriod - .map { - case DeduplicationOffset(offset) => - (Some(offset.fold(0L)(_.unwrap)), None, None) - case DeduplicationDuration(duration) => - (None, Some(duration.getSeconds), Some(duration.getNano)) - } - .getOrElse((None, None, None)) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceImpl.scala deleted file mode 100644 index f64e357905..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceImpl.scala +++ /dev/null @@ -1,1334 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.index - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.ledger.api.v2.event_query_service.GetEventsByContractIdResponse -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.daml.ledger.api.v2.update_service.GetUpdateResponse.Update -import com.daml.ledger.api.v2.update_service.{ - GetUpdateResponse, - GetUpdatesPageResponse, - GetUpdatesResponse, -} -import com.daml.metrics.InstrumentedGraph.* -import com.daml.tracing.{Event, SpanAttribute, Spans} -import com.digitalasset.base.error.DamlErrorWithDefiniteAnswer -import com.digitalasset.base.error.utils.DecodedCantonError -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.config -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.health.HealthStatus -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.ledger.api.messages.update.{GetUpdatesPageRequest, UpdatesPageToken} -import com.digitalasset.canton.ledger.api.{ - CumulativeFilter, - EventFormat, - TraceIdentifiers, - UpdateFormat, -} -import com.digitalasset.canton.ledger.error.LedgerApiErrors.InterfaceViewUpgradeFailureWrapper -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.ledger.error.{CommonErrors, LedgerApiErrors} -import com.digitalasset.canton.ledger.participant.state.index.* -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, - TracedLogger, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.pekkostreams.dispatcher.Dispatcher -import com.digitalasset.canton.pekkostreams.dispatcher.DispatcherImpl.DispatcherIsClosedException -import com.digitalasset.canton.pekkostreams.dispatcher.SubSource.RangeSource -import com.digitalasset.canton.platform.config.UpdateServiceConfig -import com.digitalasset.canton.platform.index.IndexServiceImpl.* -import com.digitalasset.canton.platform.index.IndexServiceOwner.GetPackagePreferenceForViewsUpgrading -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.cache.OffsetCheckpoint -import com.digitalasset.canton.platform.store.dao.{ - EventProjectionProperties, - LedgerDaoCommandCompletionsReader, - LedgerDaoUpdateReader, - LedgerReadDao, -} -import com.digitalasset.canton.platform.{ - InternalEventFormat, - InternalTransactionFormat, - InternalUpdateFormat, - Party, - PruneBuffers, - TemplatePartiesFilter, - *, -} -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.store.packagemeta.PackageMetadata.PackageResolution -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{FullIdentifier, Identifier, NameTypeConRef, PackageId} -import com.digitalasset.daml.lf.transaction.GlobalKey -import com.google.rpc.Status -import io.grpc.StatusRuntimeException -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.{Flow, Sink, Source} - -import scala.collection.concurrent.TrieMap -import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -private[index] class IndexServiceImpl( - participantId: Ref.ParticipantId, - ledgerDao: LedgerReadDao, - updatesReader: LedgerDaoUpdateReader, - commandCompletionsReader: LedgerDaoCommandCompletionsReader, - contractStore: ContractStore, - pruneBuffers: PruneBuffers, - dispatcher: () => Dispatcher[Offset], - fetchOffsetCheckpoint: () => Option[OffsetCheckpoint], - getPackageMetadataSnapshot: ErrorLoggingContext => PackageMetadata, - metrics: LedgerApiServerMetrics, - idleStreamOffsetCheckpointTimeout: config.NonNegativeFiniteDuration, - getPreferredPackages: GetPackagePreferenceForViewsUpgrading, - override protected val loggerFactory: NamedLoggerFactory, - materializer: Materializer, - executionContext: ExecutionContext, - updateServiceConfig: UpdateServiceConfig, -) extends IndexService - with NamedLogging { - - // A Pekko stream buffer is added at the end of all streaming queries, - // allowing to absorb temporary downstream backpressure. - // (e.g. when the client is temporarily slower than upstream delivery throughput) - private val LedgerApiStreamsBufferSize = 128 - - private val maximumLedgerTimeService = new ContractStoreBasedMaximumLedgerTimeService( - contractStore, - loggerFactory, - ) - override def getParticipantId(): Future[Ref.ParticipantId] = - Future.successful(participantId) - - override def currentHealth(): HealthStatus = ledgerDao.currentHealth() - - override def lookupContractKey(readers: Set[Ref.Party], key: GlobalKey)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ContractId]] = - contractStore.lookupContractKey(readers, key) - - override def updates( - startExclusive: Option[Offset], - endInclusive: Option[Offset], - updateFormat: UpdateFormat, - descendingOrder: Boolean, - skipPruningChecks: Boolean, - )(implicit loggingContext: LoggingContextWithTrace): Source[GetUpdatesResponse, NotUsed] = { - val interfaceViewPackageUpgrade = createViewUpgradeMemoized - val contextualizedErrorLogger = ErrorLoggingContext(logger, loggingContext) - val isTailingStream = endInclusive.isEmpty - - withValidatedUpdateFormat( - updateFormat, - getPackageMetadataSnapshot(contextualizedErrorLogger), - ) { - between(startExclusive, endInclusive) { (from, to) => - from.foreach(offset => - Spans.setCurrentSpanAttribute(SpanAttribute.OffsetFrom, offset.toDecimalString) - ) - to.foreach(offset => - Spans.setCurrentSpanAttribute(SpanAttribute.OffsetTo, offset.toDecimalString) - ) - dispatcher() - .startingAt( - startExclusive = from, - subSource = RangeSource { - val memoInternalUpdateFormat = - memoizedInternalUpdateFormat( - getPackageMetadataSnapshot = getPackageMetadataSnapshot, - updateFormat = updateFormat, - interfaceViewPackageUpgrade, - ) - (startInclusive, endInclusive) => - Source(memoInternalUpdateFormat().toList) - .flatMapConcat { internalUpdateFormat => - updatesReader - .getUpdates( - startInclusive = startInclusive, - endInclusive = endInclusive, - internalUpdateFormat = internalUpdateFormat, - descendingOrder = descendingOrder, - skipPruningChecks = skipPruningChecks, - ) - } - .via( - rangeDecorator( - startInclusive, - endInclusive, - ) - ) - }, - endInclusive = to, - ) - // when a tailing stream is requested add checkpoint messages - .via( - checkpointFlow( - startExclusive = startExclusive, - cond = isTailingStream, - fetchOffsetCheckpoint = fetchOffsetCheckpoint, - responseFromCheckpoint = updatesResponse, - ) - ) - .mapError(shutdownError) - .buffered(metrics.index.updatesBufferSize, LedgerApiStreamsBufferSize) - }.wireTap( - _.update match { - case GetUpdatesResponse.Update.Transaction(transaction) => - Spans.addEventToCurrentSpan( - Event(transaction.commandId, TraceIdentifiers.fromTransaction(transaction)) - ) - case _ => () - } - ) - }(contextualizedErrorLogger) - } - - // this flow adds checkpoint messages if the condition is met in the following way: - // a checkpoint message is fetched in the beginning of each batch (RangeBegin decorator) - // and applied exactly after an element that has the same or greater offset - // if the condition is not true the original elements are streamed and the range decorators are ignored - private def checkpointFlow[T]( - startExclusive: Option[Offset], - cond: Boolean, - fetchOffsetCheckpoint: () => Option[OffsetCheckpoint], - responseFromCheckpoint: OffsetCheckpoint => T, - idleStreamOffsetCheckpointTimeout: NonNegativeFiniteDuration = - idleStreamOffsetCheckpointTimeout, - ): Flow[(Offset, Carrier[T]), T, NotUsed] = - if (cond) { - // keepAlive flow so that we create a checkpoint for idle streams - Flow[(Offset, Carrier[T])] - .keepAlive( - idleStreamOffsetCheckpointTimeout.underlying, - () => (Offset.MaxValue, Timeout), // the offset for timeout is ignored - ) - // send the first timeout almost immediately to fetch the initial checkpoint without waiting - .mergePreferred( - Source.single((Offset.MaxValue, Timeout)).delay(500.millis), - preferred = false, - ) - .via( - injectCheckpoints( - fetchOffsetCheckpoint = fetchOffsetCheckpoint, - responseFromCheckpoint = responseFromCheckpoint, - startExclusive = startExclusive, - ) - ) - .map(_._2) - } else - Flow[(Offset, Carrier[T])].collect { case (_offset, Element(elem)) => - elem - } - - override def getCompletions( - startExclusive: Option[Offset], - userId: Ref.UserId, - parties: Set[Ref.Party], - )(implicit loggingContext: LoggingContextWithTrace): Source[CompletionStreamResponse, NotUsed] = - Source - .single(startExclusive) - .flatMapConcat { beginOpt => - dispatcher() - .startingAt( - startExclusive = beginOpt, - subSource = RangeSource((startInclusive, endInclusive) => - commandCompletionsReader - .getCommandCompletions( - startInclusive, - endInclusive, - userId, - parties, - ) - .via( - rangeDecorator( - startInclusive, - endInclusive, - ) - ) - ), - endInclusive = None, - ) - .via( - checkpointFlow( - startExclusive = startExclusive, - cond = true, - fetchOffsetCheckpoint = fetchOffsetCheckpoint, - responseFromCheckpoint = completionsResponse, - ) - ) - .mapError(shutdownError) - } - .buffered(metrics.index.completionsBufferSize, LedgerApiStreamsBufferSize) - - override def getActiveContracts( - eventFormat: EventFormat, - activeAt: Option[Offset], - rangeInfo: AcsRangeInfo, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[GetActiveContractsResponse, NotUsed] = { - val interfaceViewPackageUpgrade = createViewUpgradeMemoized - implicit val errorLoggingContext = ErrorLoggingContext(logger, loggingContext) - foldToSource { - val currentPackageMetadata = getPackageMetadataSnapshot(errorLoggingContext) - for { - _ <- checkUnknownIdentifiers(eventFormat, currentPackageMetadata).left - .map(_.asGrpcError) - endOffset = ledgerEnd() - _ <- validatedAcsActiveAtOffset( - activeAt = activeAt, - ledgerEnd = endOffset, - ) - } yield { - val activeContractsSource = - Source( - eventFormatProjection( - eventFormat, - currentPackageMetadata, - interfaceViewPackageUpgrade, - ).toList - ).flatMapConcat { case InternalEventFormat(templateFilter, eventProjectionProperties) => - ledgerDao.updateReader - .getActiveContracts( - activeAt = activeAt, - filter = templateFilter, - eventProjectionProperties = eventProjectionProperties, - rangeInfo = rangeInfo, - ) - } - activeContractsSource - .buffered(metrics.index.activeContractsBufferSize, LedgerApiStreamsBufferSize) - } - } - } - - override def lookupActiveContract( - forParties: Set[Ref.Party], - contractId: ContractId, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[FatContract]] = - contractStore.lookupActiveContract(forParties, contractId) - - override def getUpdateBy( - lookupKey: LookupKey, - updateFormat: UpdateFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] = { - val interfaceViewPackageUpgrade = createViewUpgradeMemoized - val currentPackageMetadata = getPackageMetadataSnapshot(implicitly) - checkUnknownIdentifiers(updateFormat, currentPackageMetadata).left - .map(_.asGrpcError) - .fold( - Future.failed, - _ => { - // even though memoization is not needed here, we are re-using the function - val internalUpdateFormatO = memoizedInternalUpdateFormat( - getPackageMetadataSnapshot = getPackageMetadataSnapshot, - updateFormat = updateFormat, - interfaceViewPackageUpgrade = interfaceViewPackageUpgrade, - ).apply() - - internalUpdateFormatO match { - case Some(internalUpdateFormat) => - updatesReader.lookupUpdateBy(lookupKey, internalUpdateFormat) - case None => Future.successful(None) - } - }, - ) - } - - override def getEventsByContractId( - contractId: ContractId, - eventFormat: EventFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractIdResponse] = { - val interfaceViewPackageUpgrade = createViewUpgradeMemoized - val currentPackageMetadata = getPackageMetadataSnapshot(implicitly) - checkUnknownIdentifiers(eventFormat, currentPackageMetadata).left - .map(_.asGrpcError) - .fold( - Future.failed, - _ => - ledgerDao.eventsReader.getEventsByContractId( - contractId = contractId, - internalEventFormatO = eventFormatProjection( - eventFormat, - currentPackageMetadata, - interfaceViewPackageUpgrade, - ), - ), - ) - } - - // TODO(i16065): Re-enable getEventsByContractKey tests -// override def getEventsByContractKey( -// contractKey: com.digitalasset.daml.lf.value.Value, -// templateId: Ref.Identifier, -// requestingParties: Set[Ref.Party], -// endExclusiveSeqId: Option[Long], -// )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractKeyResponse] = { -// ledgerDao.eventsReader.getEventsByContractKey( -// contractKey, -// templateId, -// requestingParties, -// endExclusiveSeqId, -// maxIterations = 1000, -// ) -// } - - override def getParties(parties: Seq[Ref.Party])(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] = - ledgerDao.getParties(parties) - - override def listKnownParties( - fromExcl: Option[Party], - filterString: Option[String185], - maxResults: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] = - ledgerDao.listKnownParties(fromExcl, filterString, maxResults) - - override def prune( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusive: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Unit] = { - pruneBuffers(pruneUpToInclusive) - ledgerDao - .prune( - previousPruneUpToInclusive = previousPruneUpToInclusive, - previousIncompleteReassignmentOffsets = previousIncompleteReassignmentOffsets, - pruneUpToInclusive = pruneUpToInclusive, - incompleteReassignmentOffsets = incompleteReassignmentOffsets, - ) - } - - override def indexDbPrunedUpto(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] = - ledgerDao.indexDbPrunedUpTo - - override def isPruningInProgress: Boolean = ledgerDao.isPruningInProgress - - override def currentLedgerEnd(): Future[Option[Offset]] = - Future.successful(ledgerEnd()) - - private def ledgerEnd(): Option[Offset] = dispatcher().getHead() - - private def between[A]( - startExclusive: Option[Offset], - endInclusive: Option[Offset], - )(f: (Option[Offset], Option[Offset]) => Source[A, NotUsed])(implicit - loggingContext: LoggingContextWithTrace - ): Source[A, NotUsed] = - Source.single(startExclusive).flatMapConcat { begin => - endInclusive - .map(off => Source.single(Some(off))) - .getOrElse(Source.single(None)) - .flatMapConcat { - case Some(end) if begin.contains(end) => - Source.empty - case Some(end) if begin > Some(end) => - Source.failed( - RequestValidationErrors.OffsetOutOfRange - .Reject( - s"End offset ${end.unwrap} is before begin offset ${begin.fold(0L)(_.unwrap)}." - )(ErrorLoggingContext(logger, loggingContext)) - .asGrpcError - ) - case endOpt: Option[Offset] => - f(begin, endOpt) - } - } - - private def shutdownError(implicit - loggingContext: LoggingContextWithTrace - ): PartialFunction[scala.Throwable, scala.Throwable] = { case _: DispatcherIsClosedException => - toGrpcError - } - - private def toGrpcError(implicit - loggingContext: LoggingContextWithTrace - ): StatusRuntimeException = - CommonErrors.ServiceNotRunning - .Reject("Index Service")(ErrorLoggingContext(logger, loggingContext)) - .asGrpcError - - override def lookupContractState(contractId: ContractId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[ContractState] = - contractStore.lookupContractState(contractId) - - override def lookupMaximumLedgerTimeAfterInterpretation(ids: Set[ContractId])(implicit - loggingContext: LoggingContextWithTrace - ): Future[MaximumLedgerTime] = - maximumLedgerTimeService.lookupMaximumLedgerTimeAfterInterpretation(ids) - - override def latestPrunedOffset()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] = - ledgerDao.indexDbPrunedUpTo - - private def updatesPageAscendingOrder(getUpdatesPageRequest: GetUpdatesPageRequest)(implicit - loggingContext: LoggingContextWithTrace - ): Future[GetUpdatesPageResponse] = { - val isFirstPageOfAscendingDynamicLowerBound = - getUpdatesPageRequest.startExclusive.isEmpty && getUpdatesPageRequest.continueStreamFromIncl.isEmpty - val limit = - if (isFirstPageOfAscendingDynamicLowerBound) { // special case for the first page of ascending with dynamic bound - ((getUpdatesPageRequest.maxPageSize + 1) * updateServiceConfig.ascendingFirstPageDynamicBoundOverfetch.unwrap).ceil.toInt - } else { - getUpdatesPageRequest.maxPageSize + 1 - } - - implicit val ec: ExecutionContext = executionContext - - val ledgerEndBeforeFetch = ledgerEnd() - for { - calculatedBeginExclusive <- getUpdatesPageRequest.continueStreamFromIncl - .map(_.decrement) - .orElse(getUpdatesPageRequest.startExclusive) match { - case Some(beginExclOffset) => Future.successful(beginExclOffset) - case None => ledgerDao.indexDbPrunedUpTo // Dynamic bound and first page - } - calculatedEndInclusive: Option[Offset] = getUpdatesPageRequest.endInclusive.orElse( - ledgerEndBeforeFetch - ) - transactions <- - fetchTransactionsWithEmptyRangeSupport( - getUpdatesPageRequest = getUpdatesPageRequest, - limit = limit, - calculatedBeginExclusive = calculatedBeginExclusive, - calculatedEndInclusive = calculatedEndInclusive, - skipPruningChecks = isFirstPageOfAscendingDynamicLowerBound, - ) - pruningOffsetAfterFetch <- ledgerDao.indexDbPrunedUpTo - } yield { - processAscendingPageData( - getUpdatesPageRequest = getUpdatesPageRequest, - loggingContext = loggingContext, - isFirstPageOfAscendingDynamicLowerBound = isFirstPageOfAscendingDynamicLowerBound, - limit = limit, - calculatedBeginExclusive = calculatedBeginExclusive, - calculatedEndInclusive = calculatedEndInclusive, - transactions = transactions, - pruningOffsetAfterFetch = pruningOffsetAfterFetch, - logger = logger, - ) - } - } - - private def updatesPageDescendingOrder( - getUpdatesPageRequest: GetUpdatesPageRequest - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[GetUpdatesPageResponse] = { - val limit: Int = getUpdatesPageRequest.maxPageSize + 1 - - implicit val ec: ExecutionContext = executionContext - for { - pruningOffsetBeforeFetch <- ledgerDao.indexDbPrunedUpTo - ledgerEndBeforeFetch = ledgerEnd() - calculatedBeginExclusive: Option[Offset] = getUpdatesPageRequest.startExclusive.getOrElse( - pruningOffsetBeforeFetch - ) - calculatedEndInclusive: Option[Offset] = getUpdatesPageRequest.continueStreamFromIncl.orElse( - getUpdatesPageRequest.endInclusive.orElse(ledgerEndBeforeFetch) - ) - transactions <- fetchTransactionsWithEmptyRangeSupport( - getUpdatesPageRequest = getUpdatesPageRequest, - limit = limit, - calculatedBeginExclusive = calculatedBeginExclusive, - calculatedEndInclusive = calculatedEndInclusive, - skipPruningChecks = - getUpdatesPageRequest.startExclusive.isEmpty, // With strict bound we'll fail inside this function when interfering with pruning - ) - pruningOffsetAfterFetch <- ledgerDao.indexDbPrunedUpTo - } yield { - val trimmedTransactions = - (getUpdatesPageRequest.startExclusive, pruningOffsetAfterFetch) match { - case (None, Some(pruningOffet)) => - transactions.takeWhile(u => - updatesToOffset(u) > pruningOffet - ) // Only if both: dunamic bound and ledger already pruned we need to check if we fetched something before pruning bound - case _ => transactions - } - if (trimmedTransactions.lengthIs == getUpdatesPageRequest.maxPageSize + 1) { // There is still at least one element in the next page - GetUpdatesPageResponse( - updates = trimmedTransactions.take(getUpdatesPageRequest.maxPageSize), - lowestPageOffsetExclusive = - updatesToOffset(trimmedTransactions(getUpdatesPageRequest.maxPageSize)).unwrap, - highestPageOffsetInclusive = calculatedEndInclusive.fold(0L)(_.unwrap), - nextPageToken = Some( - UpdatesPageToken( - lowestPageOffsetExclusive = - Some(updatesToOffset(trimmedTransactions(getUpdatesPageRequest.maxPageSize))), - highestPageOffsetInclusive = calculatedEndInclusive, - participantIdChecksum = getUpdatesPageRequest.participantChecksum, - requestChecksum = getUpdatesPageRequest.requestChecksum, - ).toOpaqueByteString - ), - ) - } else { - GetUpdatesPageResponse( - updates = trimmedTransactions, - lowestPageOffsetExclusive = getUpdatesPageRequest.startExclusive - .getOrElse(pruningOffsetAfterFetch) - .fold(0L)( - _.unwrap - ), // With strict bound we return the bound. With dynamic bound we return pruning offset. Falback to before ledger start if neither is set. - highestPageOffsetInclusive = calculatedEndInclusive.fold(0L)(_.unwrap), - nextPageToken = None, - ) - } - } - } - - private def fetchTransactionsWithEmptyRangeSupport( - getUpdatesPageRequest: GetUpdatesPageRequest, - limit: Int, - calculatedBeginExclusive: Option[Offset], - calculatedEndInclusive: Option[Offset], - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[Seq[GetUpdateResponse]] = - if (calculatedEndInclusive <= calculatedBeginExclusive) { // It can be strictly less in case of pruning offset being used - Future.successful(Vector.empty) - } else { - updates( - startExclusive = calculatedBeginExclusive, - endInclusive = calculatedEndInclusive, - updateFormat = getUpdatesPageRequest.updateFormat, - descendingOrder = getUpdatesPageRequest.descendingOrder, - skipPruningChecks = skipPruningChecks, - ).take(limit.toLong) - .runWith(Sink.seq)(materializer) - .map(_.flatMap(getUpdatesResponseToGetUpdateResponse)) - } - - def updatesPage( - getUpdatesPageRequest: GetUpdatesPageRequest - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[GetUpdatesPageResponse] = - if (getUpdatesPageRequest.descendingOrder) { - updatesPageDescendingOrder(getUpdatesPageRequest) - } else { - updatesPageAscendingOrder(getUpdatesPageRequest) - } - - private def createViewUpgradeMemoized(implicit - loggingContextWithTrace: LoggingContextWithTrace - ): InterfaceViewPackageUpgrade = { - val memoizedSelection = - TrieMap.empty[(Identifier, Ref.PackageName), Future[Either[Status, Ref.PackageId]]] - val contextualizedErrorLogger = ErrorLoggingContext(logger, loggingContextWithTrace) - implicit val directExecutionContext: DirectExecutionContext = DirectExecutionContext( - noTracingLogger - ) - - def handlePreferredPackageVersionError( - computeUpgradeResult: Try[Either[String, Ref.PackageId]], - packageName: Ref.PackageName, - ): Try[Either[Status, PackageId]] = - computeUpgradeResult - .map( - _.left.map(reason => - LedgerApiErrors.NoVettedInterfaceImplementationPackage - .Reject(packageName, reason) - .asGrpcStatus - ) - ) - .recoverWith { case sre: StatusRuntimeException => - DecodedCantonError - .fromStatusRuntimeException(sre) - .fold( - errorCodeDecodeFailure => { - logger.warn(s"Could not decode error: $errorCodeDecodeFailure") - Failure(sre) - }, - decodedError => - // TODO(#25385): Make the NotConnectedToAnySynchronizer error available to this module - // and use its reference code id directly instead of the String representation - if (decodedError.code.id == "NOT_CONNECTED_TO_ANY_SYNCHRONIZER") { - Success(Left(InterfaceViewUpgradeFailureWrapper(decodedError).asGrpcStatus)) - } else Failure(sre), - ) - } - - def computeUpgradeViewPackage( - packageName: Ref.PackageName, - packageIdsWithInterfaceInstance: Set[Ref.PackageId], - )(implicit - loggingContextWithTrace: LoggingContextWithTrace - ): Future[Either[Status, Ref.PackageId]] = - getPreferredPackages( - packageName, - packageIdsWithInterfaceInstance, - "Package-ids with interface instances for the requested interface", - loggingContextWithTrace, - ).asGrpcFuture - .transform(handlePreferredPackageVersionError(_, packageName)) - - // Computes the package-id for up/downgrading the interface instance used for computing an interface view. - // The selection picks the highest-versioned vetted package-id for the package name of the original create event. - // For performance reasons, the result is memoized for the entire lifetime of a stream / query - (interfaceId: Identifier, originalCreateTemplate: Identifier) => { - val packageMetadataSnapshot = getPackageMetadataSnapshot(contextualizedErrorLogger) - val packageIdVersionMap = packageMetadataSnapshot.packageIdVersionMap - - packageIdVersionMap - .get(originalCreateTemplate.packageId) - .map { case (name, _version) => Future.successful(name) } - .getOrElse( - // Expectation is that all the callers are Ledger API-internal - // and have implicitly or explicitly validated that the requested package-id is known - // (i.e. it is in the packageIdVersionMap) - Future.failed( - LedgerApiErrors.InternalError - .Generic( - s"PackageId ${originalCreateTemplate.packageId} not found in the packageIdVersionMap ($packageIdVersionMap)" - ) - .asGrpcError - ) - ) - .flatMap { packageName => - val directImplementationsOfInterface = - packageMetadataSnapshot.interfacesImplementedBy.getOrElse(interfaceId, Set.empty) - memoizedSelection - .getOrElseUpdate( - interfaceId -> packageName, - computeUpgradeViewPackage( - packageName = packageName, - // Used to filter down the candidate package-ids for upgrade that actually implement the interface - // to ensure that the interface view can be computed. - // If no direct implementations are vetted, the view computation fails with NO_VETTED_INTERFACE_IMPLEMENTATION_PACKAGE - packageIdsWithInterfaceInstance = directImplementationsOfInterface.map(_.packageId), - ), - ) - .map(result => - result.map(upgradedViewPackageId => - originalCreateTemplate.copy(pkg = upgradedViewPackageId) - ) - ) - } - } - } - - override def lookupNonUniqueContractKey( - readers: Set[Party], - key: Key, - pageToken: Option[Long], - limit: Int, - )(implicit loggingContext: LoggingContextWithTrace): Future[ContractKeyPage] = - contractStore.lookupNonUniqueContractKey(readers, key, pageToken, limit) -} - -object IndexServiceImpl { - - trait InterfaceViewPackageUpgrade { - - /** Computes an optimal package-id of the ``originalCreateTemplate`` interface instance that's - * used for rendering a view for interface ``interfaceId``. - * - * @param interfaceId - * The interface-id of the view being requested - * @param representativeCreateTemplate - * The template-id of the contract's representative package - * - * @return - * the identifier for the ``originalCreateTemplate`` with the package-id adjusted to the - * selection result - */ - def upgrade( - interfaceId: Identifier, - representativeCreateTemplate: Identifier, - ): Future[Either[Status, Identifier]] - } - - private[index] def checkUnknownIdentifiers( - apiEventFormat: EventFormat, - metadata: PackageMetadata, - )(implicit - contextualizedErrorLogger: ErrorLoggingContext - ): Either[DamlErrorWithDefiniteAnswer, Unit] = { - val unknownPackageNames = Set.newBuilder[Ref.PackageName] - val packageNamesWithNoTemplatesForQualifiedNameBuilder = - Set.newBuilder[(Ref.PackageName, Ref.QualifiedName)] - val packageNamesWithNoInterfacesForQualifiedNameBuilder = - Set.newBuilder[(Ref.PackageName, Ref.QualifiedName)] - - def checkNameTypeConRef( - knownIds: Set[Identifier], - handleUnknownIdForPkgName: ((Ref.PackageName, Ref.QualifiedName)) => Unit, - handleUnknownPkgName: Ref.PackageName => Unit, - )(nameTypeConRef: NameTypeConRef): Unit = { - val packageName = nameTypeConRef.pkg.name - metadata.packageNameMap.get(packageName) match { - case Some(PackageResolution(_, allPackageIdsForName)) - if !allPackageIdsForName.view - .map(Ref.Identifier(_, nameTypeConRef.qualifiedName)) - .exists(knownIds) => - handleUnknownIdForPkgName(packageName -> nameTypeConRef.qualifiedName) - case None => handleUnknownPkgName(packageName) - case _ => () - } - } - - val cumulativeFilters = apiEventFormat.filtersByParty.iterator.map( - _._2 - ) ++ apiEventFormat.filtersForAnyParty.iterator - - cumulativeFilters.foreach { - case CumulativeFilter(templateFilters, interfaceFilters, _wildcardFilters) => - templateFilters.iterator - .map(_.templateTypeRef) - .foreach( - checkNameTypeConRef( - metadata.templates, - packageNamesWithNoTemplatesForQualifiedNameBuilder += _, - unknownPackageNames += _, - ) - ) - interfaceFilters.iterator - .map(_.interfaceTypeRef) - .foreach( - checkNameTypeConRef( - metadata.interfaces, - packageNamesWithNoInterfacesForQualifiedNameBuilder += _, - unknownPackageNames += _, - ) - ) - } - - val packageNames = unknownPackageNames.result() - val packageNamesWithNoTemplatesForQualifiedName = - packageNamesWithNoTemplatesForQualifiedNameBuilder.result() - val packageNamesWithNoInterfacesForQualifiedName = - packageNamesWithNoInterfacesForQualifiedNameBuilder.result() - - for { - _ <- Either.cond( - packageNames.isEmpty, - (), - RequestValidationErrors.NotFound.PackageNamesNotFound.Reject(packageNames), - ) - _ <- Either.cond( - packageNamesWithNoTemplatesForQualifiedName.isEmpty, - (), - RequestValidationErrors.NotFound.NoTemplatesForPackageNameAndQualifiedName.Reject( - packageNamesWithNoTemplatesForQualifiedName - ), - ) - _ <- Either.cond( - packageNamesWithNoInterfacesForQualifiedName.isEmpty, - (), - RequestValidationErrors.NotFound.NoInterfaceForPackageNameAndQualifiedName.Reject( - packageNamesWithNoInterfacesForQualifiedName - ), - ) - } yield () - } - - private[index] def checkUnknownIdentifiers( - apiUpdateFormat: UpdateFormat, - metadata: PackageMetadata, - )(implicit - contextualizedErrorLogger: ErrorLoggingContext - ): Either[DamlErrorWithDefiniteAnswer, Unit] = for { - _ <- apiUpdateFormat.includeTransactions - .map(transactionFormat => checkUnknownIdentifiers(transactionFormat.eventFormat, metadata)) - .getOrElse(Right(())) - _ <- apiUpdateFormat.includeReassignments - .map(checkUnknownIdentifiers(_, metadata)) - .getOrElse(Right(())) - } yield () - - private[index] def foldToSource[A]( - either: Either[StatusRuntimeException, Source[A, NotUsed]] - ): Source[A, NotUsed] = either.fold(Source.failed, identity) - - private[index] def withValidatedUpdateFormat[T]( - apiUpdateFormat: UpdateFormat, - metadata: PackageMetadata, - )( - source: => Source[T, NotUsed] - )(implicit errorLogger: ErrorLoggingContext): Source[T, NotUsed] = - foldToSource( - for { - _ <- checkUnknownIdentifiers(apiUpdateFormat, metadata)(errorLogger).left - .map(_.asGrpcError) - } yield source - ) - - private[index] def validatedAcsActiveAtOffset[T]( - activeAt: Option[Offset], - ledgerEnd: Option[Offset], - )(implicit errorLogger: ErrorLoggingContext): Either[StatusRuntimeException, Unit] = - Either.cond( - activeAt <= ledgerEnd, - (), - RequestValidationErrors.OffsetAfterLedgerEnd - .Reject( - offsetType = "active_at_offset", - requestedOffset = activeAt.fold(0L)(_.unwrap), - ledgerEnd = ledgerEnd.fold(0L)(_.unwrap), - ) - .asGrpcError, - ) - @SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.Var")) - private[index] def memoizedInternalUpdateFormat( - getPackageMetadataSnapshot: ErrorLoggingContext => PackageMetadata, - updateFormat: UpdateFormat, - interfaceViewPackageUpgrade: InterfaceViewPackageUpgrade, - )(implicit - contextualizedErrorLogger: ErrorLoggingContext - ): () => Option[InternalUpdateFormat] = { - @volatile var metadata: PackageMetadata = null - @volatile var internalTransactionFormat: Option[InternalTransactionFormat] = None - @volatile var reassignmentsInternalEventFormat: Option[InternalEventFormat] = None - () => - val currentMetadata = getPackageMetadataSnapshot(contextualizedErrorLogger) - if (metadata ne currentMetadata) { - metadata = currentMetadata - internalTransactionFormat = updateFormat.includeTransactions.flatMap(transactionFormat => - eventFormatProjection( - eventFormat = transactionFormat.eventFormat, - metadata = metadata, - interfaceViewPackageUpgrade = interfaceViewPackageUpgrade, - ).map(internalEventFormat => - InternalTransactionFormat( - internalEventFormat = internalEventFormat, - transactionShape = transactionFormat.transactionShape, - ) - ) - ) - reassignmentsInternalEventFormat = updateFormat.includeReassignments.flatMap(eventFormat => - eventFormatProjection( - eventFormat = eventFormat, - metadata = metadata, - interfaceViewPackageUpgrade = interfaceViewPackageUpgrade, - ) - ) - } - - val topologyEvents = updateFormat.includeTopologyEvents - - if ( - internalTransactionFormat.isEmpty && - reassignmentsInternalEventFormat.isEmpty && - topologyEvents.isEmpty - ) - None - else - Some( - InternalUpdateFormat( - includeTransactions = internalTransactionFormat, - includeReassignments = reassignmentsInternalEventFormat, - includeTopologyEvents = topologyEvents, - ) - ) - } - - private def eventFormatProjection( - eventFormat: EventFormat, - metadata: PackageMetadata, - interfaceViewPackageUpgrade: InterfaceViewPackageUpgrade, - )(implicit contextualizedErrorLogger: ErrorLoggingContext): Option[InternalEventFormat] = { - val templateFilter: Map[NameTypeConRef, Option[Set[Party]]] = - IndexServiceImpl.templateFilter(metadata, eventFormat) - - val templateWildcardFilter: Option[Set[Party]] = - IndexServiceImpl.wildcardFilter(eventFormat) - - if (templateFilter.isEmpty && templateWildcardFilter.fold(false)(_.isEmpty)) { - None - } else { - val eventProjectionProperties = EventProjectionProperties( - eventFormat = eventFormat, - interfaceImplementedBy = - interfaceId => interfacesImplementedByWithUpgrades(metadata, interfaceId), - resolveTypeConRef = metadata.resolveTypeConRef, - interfaceViewPackageUpgrade = interfaceViewPackageUpgrade, - ) - Some( - InternalEventFormat( - templatePartiesFilter = TemplatePartiesFilter( - relation = templateFilter, - templateWildcardParties = templateWildcardFilter, - ), - eventProjectionProperties = eventProjectionProperties, - ) - ) - } - } - - // TODO(#25385): Unit test coverage - private def interfacesImplementedByWithUpgrades( - metadata: PackageMetadata, - interfaceId: FullIdentifier, - )(implicit contextualizedErrorLogger: ErrorLoggingContext): Set[FullIdentifier] = - metadata.interfacesImplementedBy.getOrElse(interfaceId.toIdentifier, Set.empty).flatMap { - originalInterfaceImplementation => - val packageIdVersionMap = metadata.packageIdVersionMap - val (packageName, _packageVersion) = - packageIdVersionMap.getOrElse( - originalInterfaceImplementation.packageId, - // The original implementation template-id is extracted from the package metadata - // hence its package name must be present in the packageIdVersionMap - throw LedgerApiErrors.InternalError - .Generic( - s"Package-name missing for original implementor package-id ${originalInterfaceImplementation.packageId} from packageIdVersionMap: $packageIdVersionMap" - ) - .asGrpcError, - ) - metadata - .resolveTypeConRef( - Ref.NameTypeConRef( - Ref.PackageRef.Name(packageName), - originalInterfaceImplementation.qualifiedName, - ) - ) - } - - private def templateIds( - metadata: PackageMetadata, - cumulativeFilter: CumulativeFilter, - )(implicit contextualizedErrorLogger: ErrorLoggingContext): Set[NameTypeConRef] = { - val fromInterfacesDefs = cumulativeFilter.interfaceFilters.view - .map(_.interfaceTypeRef) - .flatMap(metadata.resolveTypeConRef) - .flatMap(interfacesImplementedByWithUpgrades(metadata, _).view) - .map(_.toNameTypeConRef) - .toSet - - val fromTemplateDefs = cumulativeFilter.templateFilters.view - .map(_.templateTypeRef) - .flatMap(metadata.resolveTypeConRef) - .map(_.toNameTypeConRef) - - fromInterfacesDefs ++ fromTemplateDefs - } - - private[index] def templateFilter( - metadata: PackageMetadata, - eventFormat: EventFormat, - )(implicit - contextualizedErrorLogger: ErrorLoggingContext - ): Map[NameTypeConRef, Option[Set[Party]]] = { - val templatesFilterByParty = - eventFormat.filtersByParty.view.foldLeft(Map.empty[NameTypeConRef, Option[Set[Party]]]) { - case (acc, (party, cumulativeFilter)) => - templateIds(metadata, cumulativeFilter).foldLeft(acc) { case (acc, templateId) => - val updatedPartySet = acc.getOrElse(templateId, Some(Set.empty[Party])).map(_ + party) - acc.updated(templateId, updatedPartySet) - } - } - - // templates filter for all the parties - val templatesFilterForAnyParty: Map[NameTypeConRef, Option[Set[Party]]] = - eventFormat.filtersForAnyParty - .fold(Set.empty[NameTypeConRef])(templateIds(metadata, _)) - .map((_, None)) - .toMap - - // a filter for a specific template that is defined for any party will prevail the filters - // defined for specific parties - templatesFilterByParty ++ templatesFilterForAnyParty - - } - - // template-wildcard for the parties or party-wildcards of the filter given - private[index] def wildcardFilter( - eventFormat: EventFormat - ): Option[Set[Party]] = { - val emptyFiltersMessage = - "Found transaction filter with both template and interface filters being empty, but the" + - "request should have already been rejected in validation" - eventFormat.filtersForAnyParty match { - case Some(CumulativeFilter(_, _, templateWildcardFilter)) - if templateWildcardFilter.isDefined => - None // party-wildcard - case Some( - CumulativeFilter(templateIds, interfaceFilters, templateWildcardFilter) - ) if templateIds.isEmpty && interfaceFilters.isEmpty && templateWildcardFilter.isEmpty => - throw new RuntimeException(emptyFiltersMessage) - case _ => - Some(eventFormat.filtersByParty.view.collect { - case (party, CumulativeFilter(_, _, templateWildcardFilter)) - if templateWildcardFilter.isDefined => - party - case ( - _party, - CumulativeFilter(templateIds, interfaceFilters, templateWildcardFilter), - ) - if templateIds.isEmpty && interfaceFilters.isEmpty && templateWildcardFilter.isEmpty => - throw new RuntimeException(emptyFiltersMessage) - }.toSet) - } - } - - // adds a RangeBegin message exactly before the original elements - // and a RangeEnd message exactly after the original elements - // the decorators are added along with the offset they are referring to - // the decorators' offsets are both inclusive - private[index] def rangeDecorator[T]( - startInclusive: Offset, - endInclusive: Offset, - ): Flow[(Offset, T), (Offset, Carrier[T]), NotUsed] = - Flow[(Offset, T)] - .map[(Offset, Carrier[T])] { case (off, elem) => - (off, Element(elem)) - } - .prepend( - Source.single((startInclusive, RangeBegin)) - ) - .concat(Source.single((endInclusive, RangeEnd))) - - def injectCheckpoints[T]( - fetchOffsetCheckpoint: () => Option[OffsetCheckpoint], - responseFromCheckpoint: OffsetCheckpoint => T, - startExclusive: Option[Offset], - ): Flow[(Offset, Carrier[T]), (Offset, T), NotUsed] = - Flow[(Offset, Carrier[T])] - .statefulMap[ - (Option[OffsetCheckpoint], Option[OffsetCheckpoint], Option[(Offset, Carrier[T])]), - Seq[(Offset, T)], - ](create = () => (None, None, None))( - f = { case ((lastFetchedCheckpointO, lastStreamedCheckpointO, processedElemO), elem) => - elem match { - // range begin received - case (startInclusive, RangeBegin) => - val fetchedCheckpointO = fetchOffsetCheckpoint() - // we allow checkpoints that predate the current range only for RangeBegin to allow checkpoints - // that arrived delayed - val response = - if (fetchedCheckpointO != lastStreamedCheckpointO) - fetchedCheckpointO.collect { - case c: OffsetCheckpoint - if Some(c.offset) == startInclusive.decrement - && Option(c.offset) >= processedElemO.map(_._1) => - (c.offset, responseFromCheckpoint(c)) - }.toList - else Seq.empty - val streamedCheckpointO = - if (response.nonEmpty) fetchedCheckpointO else lastStreamedCheckpointO - val relevantCheckpointO = fetchedCheckpointO.collect { - case c: OffsetCheckpoint if c.offset >= startInclusive => c - } - ((relevantCheckpointO, streamedCheckpointO, Some(elem)), response) - // regular element received - case (currentOffset, Element(currElem)) => - val prepend = lastFetchedCheckpointO.collect { - case c: OffsetCheckpoint if c.offset < currentOffset => - (c.offset, responseFromCheckpoint(c)) - } - val responses = prepend.toList :+ (currentOffset, currElem) - val newCheckpointO = - lastFetchedCheckpointO.collect { case c: OffsetCheckpoint if prepend.isEmpty => c } - val streamedCheckpointO = - if (prepend.nonEmpty) lastFetchedCheckpointO else lastStreamedCheckpointO - ((newCheckpointO, streamedCheckpointO, Some(elem)), responses) - // range end indicator received - case (endInclusive, RangeEnd) => - val responses = lastFetchedCheckpointO.collect { - case c: OffsetCheckpoint if c.offset <= endInclusive => - (c.offset, responseFromCheckpoint(c)) - }.toList - val newCheckpointO = - lastFetchedCheckpointO.collect { - case c: OffsetCheckpoint if responses.isEmpty => c - } - val streamedCheckpointO = - if (responses.nonEmpty) lastFetchedCheckpointO else lastStreamedCheckpointO - ((newCheckpointO, streamedCheckpointO, Some(elem)), responses) - case (_, Timeout) => - val relevantCheckpointO = fetchOffsetCheckpoint().collect { - case c: OffsetCheckpoint - if lastStreamedCheckpointO.fold(true)(_.offset < c.offset) && - // check that we are not in the middle of a range or no elements have been processed (either - // because we are polling elements at ledger end or because Timeout arrived before any other element) - processedElemO - .fold(startExclusive == Option(c.offset))(e => - e._2.isRangeEnd && e._1 == c.offset - ) => - c - } - val response = - relevantCheckpointO - .map(c => (c.offset, responseFromCheckpoint(c))) - .toList - ( - ( - relevantCheckpointO.orElse(lastFetchedCheckpointO), - relevantCheckpointO.orElse(lastStreamedCheckpointO), - processedElemO, - ), - response, - ) - } - }, - onComplete = _ => None, - ) - .mapConcat(identity) - - sealed abstract class Carrier[+T] { - def isRangeEnd: Boolean = false - def isTimeout: Boolean = false - } - - final case object RangeBegin extends Carrier[Nothing] - final case object RangeEnd extends Carrier[Nothing] { - override def isRangeEnd: Boolean = true - } - final case class Element[T](element: T) extends Carrier[T] - final case object Timeout extends Carrier[Nothing] { - override def isTimeout: Boolean = true - } - - private def updatesResponse( - offsetCheckpoint: OffsetCheckpoint - ): GetUpdatesResponse = - GetUpdatesResponse.defaultInstance.withOffsetCheckpoint(offsetCheckpoint.toApi) - - private def completionsResponse( - offsetCheckpoint: OffsetCheckpoint - ): CompletionStreamResponse = - CompletionStreamResponse.defaultInstance.withOffsetCheckpoint(offsetCheckpoint.toApi) - - private def updatesToOffset(getUpdateResponse: GetUpdateResponse): Offset = - getUpdateResponse.update match { - case Update.Empty => - throw new IllegalStateException("Empty update not expected at this point of pipeline") - case Update.Transaction(value) => Offset.tryFromLong(value.offset) - case Update.Reassignment(value) => Offset.tryFromLong(value.offset) - case Update.TopologyTransaction(value) => Offset.tryFromLong(value.offset) - } - - private def getUpdatesResponseToGetUpdateResponse( - getUpdatesResponse: GetUpdatesResponse - ): Option[GetUpdateResponse] = - getUpdatesResponse.update match { - case GetUpdatesResponse.Update.Empty => - Some(GetUpdateResponse(GetUpdateResponse.Update.Empty)) - case GetUpdatesResponse.Update.Transaction(value) => - Some(GetUpdateResponse(GetUpdateResponse.Update.Transaction(value))) - case GetUpdatesResponse.Update.Reassignment(value) => - Some(GetUpdateResponse(GetUpdateResponse.Update.Reassignment(value))) - case GetUpdatesResponse.Update.OffsetCheckpoint(_) => None - case GetUpdatesResponse.Update.TopologyTransaction(value) => - Some(GetUpdateResponse(GetUpdateResponse.Update.TopologyTransaction(value))) - } - - def processAscendingPageData( - getUpdatesPageRequest: GetUpdatesPageRequest, - loggingContext: LoggingContextWithTrace, - isFirstPageOfAscendingDynamicLowerBound: Boolean, - limit: Int, - calculatedBeginExclusive: Option[Offset], - calculatedEndInclusive: Option[Offset], - transactions: Seq[GetUpdateResponse], - pruningOffsetAfterFetch: Option[Offset], - logger: TracedLogger, - ): GetUpdatesPageResponse = - pruningOffsetAfterFetch match { - case Some(pruningOffset) if isFirstPageOfAscendingDynamicLowerBound => - lazy val trimmedTransactions = - transactions.dropWhile(r => pruningOffset >= updatesToOffset(r)) - val fetchedEnoughForFirstPage = - // Trivial exclusion -- no overlap with pruning - calculatedEndInclusive.exists( - _ < pruningOffset - // Fetched up to calculatedEndInclusive -- if page is shorter it's the last page, so OK. - ) || transactions.sizeIs < limit || trimmedTransactions.lengthIs >= getUpdatesPageRequest.maxPageSize + 1 // Non-trivial case where we need to look into transactions - - if (fetchedEnoughForFirstPage) { - buildAscendingPage( - getUpdatesPageRequest = getUpdatesPageRequest, - calculatedBeginExclusive = pruningOffsetAfterFetch, - calculatedEndInclusive = calculatedEndInclusive, - transactions = trimmedTransactions, - ) - } else { - throw RequestValidationErrors.ParticipantPrunedDataAccessed - .Reject( - cause = - "Pruning offset moves faster than participant is able to generate page. You may want to tweak page size or ask the node administrator to increase ascending_first_page_dynamic_bound_overfetch config setting.", - earliestOffset = - pruningOffset.unwrap, // If we are here pruning offset definitely exists - )( - ErrorLoggingContext(logger, loggingContext) - ) - .asGrpcError - } - case _ => - buildAscendingPage( - getUpdatesPageRequest = getUpdatesPageRequest, - calculatedBeginExclusive = calculatedBeginExclusive, - calculatedEndInclusive = calculatedEndInclusive, - transactions = transactions, - ) - } - - private def buildAscendingPage( - getUpdatesPageRequest: GetUpdatesPageRequest, - calculatedBeginExclusive: Option[Offset], - calculatedEndInclusive: Option[Offset], - transactions: Seq[GetUpdateResponse], - ): GetUpdatesPageResponse = - if (transactions.lengthIs >= getUpdatesPageRequest.maxPageSize + 1) { // Full page (note: it may be larger than maxPageSize+1 due to overfetch for the first page) - GetUpdatesPageResponse( - updates = transactions.take(getUpdatesPageRequest.maxPageSize), - lowestPageOffsetExclusive = calculatedBeginExclusive.fold(0L)(_.unwrap), - highestPageOffsetInclusive = - updatesToOffset(transactions(getUpdatesPageRequest.maxPageSize)).unwrap - 1, - nextPageToken = Some( - UpdatesPageToken( - lowestPageOffsetExclusive = calculatedBeginExclusive, - highestPageOffsetInclusive = - updatesToOffset(transactions(getUpdatesPageRequest.maxPageSize)).decrement, - participantIdChecksum = getUpdatesPageRequest.participantChecksum, - requestChecksum = getUpdatesPageRequest.requestChecksum, - ).toOpaqueByteString - ), - ) - } else { // Last page or ledger end with dynamic bound - GetUpdatesPageResponse( - updates = transactions, - lowestPageOffsetExclusive = calculatedBeginExclusive.fold(0L)(_.unwrap), - highestPageOffsetInclusive = calculatedEndInclusive.fold(0L)(_.unwrap), - nextPageToken = getUpdatesPageRequest.endInclusive match { - case Some(_) => None - case None => - Some( - UpdatesPageToken( - lowestPageOffsetExclusive = calculatedBeginExclusive, - highestPageOffsetInclusive = calculatedEndInclusive, - participantIdChecksum = getUpdatesPageRequest.participantChecksum, - requestChecksum = getUpdatesPageRequest.requestChecksum, - ).toOpaqueByteString - ) - }, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceOwner.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceOwner.scala deleted file mode 100644 index 41a8e14023..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceOwner.scala +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.index - -import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner} -import com.daml.resources.ProgramResource.StartupException -import com.daml.timer.RetryStrategy -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.error.IndexErrors.IndexDbException -import com.digitalasset.canton.ledger.participant.state.index.IndexService -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.InMemoryState -import com.digitalasset.canton.platform.apiserver.TimedIndexService -import com.digitalasset.canton.platform.config.{IndexServiceConfig, UpdateServiceConfig} -import com.digitalasset.canton.platform.index.IndexServiceOwner.GetPackagePreferenceForViewsUpgrading -import com.digitalasset.canton.platform.store.backend.common.MismatchException -import com.digitalasset.canton.platform.store.cache.* -import com.digitalasset.canton.platform.store.dao.events.{ - BufferedUpdateReader, - ContractLoader, - LfValueTranslation, -} -import com.digitalasset.canton.platform.store.dao.{ - BufferedCommandCompletionsReader, - JdbcLedgerDao, - LedgerReadDao, -} -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.platform.store.{DbSupport, LedgerApiContractStore} -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.actor.Scheduler -import org.apache.pekko.stream.Materializer - -import scala.concurrent.duration.* -import scala.concurrent.{ExecutionContext, ExecutionContextExecutorService, Future} -import scala.util.control.NoStackTrace - -final class IndexServiceOwner( - config: IndexServiceConfig, - dbSupport: DbSupport, - metrics: LedgerApiServerMetrics, - participantId: Ref.ParticipantId, - inMemoryState: InMemoryState, - tracer: Tracer, - val loggerFactory: NamedLoggerFactory, - incompleteOffsets: ( - Offset, - Option[Set[Ref.Party]], - TraceContext, - ) => FutureUnlessShutdown[Vector[Offset]], - contractLoader: ContractLoader, - getPackageMetadataSnapshot: ErrorLoggingContext => PackageMetadata, - getPackagePreference: GetPackagePreferenceForViewsUpgrading, - lfValueTranslation: LfValueTranslation, - queryExecutionContext: ExecutionContextExecutorService, - commandExecutionContext: ExecutionContextExecutorService, - participantContractStore: LedgerApiContractStore, - materializer: Materializer, - updateServiceConfig: UpdateServiceConfig, - scheduler: Scheduler, -) extends ResourceOwner[IndexService] - with NamedLogging { - private val initializationRetryDelay = 100.millis - private val initializationMaxAttempts = 3000 // give up after 5min - - def acquire()(implicit context: ResourceContext): Resource[IndexService] = { - val ledgerDao = createLedgerReadDao( - ledgerEndCache = inMemoryState.ledgerEndCache, - achsStateCache = inMemoryState.achsStateCache, - stringInterning = inMemoryState.stringInterningView, - contractLoader = contractLoader, - lfValueTranslation = lfValueTranslation, - queryExecutionContext = queryExecutionContext, - commandExecutionContext = commandExecutionContext, - ) - for { - _ <- Resource.fromFuture(verifyParticipantId(ledgerDao)) - _ <- Resource.fromFuture(waitForInMemoryStateInitialization()) - - contractStore = new MutableCacheBackedContractStore( - ledgerDao.contractsReader, - contractStateCaches = inMemoryState.contractStateCaches, - loggerFactory = loggerFactory, - contractStore = participantContractStore, - ledgerEndCache = inMemoryState.ledgerEndCache, - maxLookupLimit = config.maxLookupLimit, - )(commandExecutionContext) - - bufferedTransactionsReader = BufferedUpdateReader( - delegate = ledgerDao.updateReader, - updatesBuffer = inMemoryState.inMemoryFanoutBuffer, - lfValueTranslation = lfValueTranslation, - metrics = metrics, - eventProcessingParallelism = config.bufferedEventsProcessingParallelism, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - bufferedCommandCompletionsReader = BufferedCommandCompletionsReader( - inMemoryFanoutBuffer = inMemoryState.inMemoryFanoutBuffer, - delegate = ledgerDao.completions, - metrics = metrics, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - indexService = new IndexServiceImpl( - participantId = participantId, - ledgerDao = ledgerDao, - updatesReader = bufferedTransactionsReader, - commandCompletionsReader = bufferedCommandCompletionsReader, - contractStore = contractStore, - pruneBuffers = inMemoryState.inMemoryFanoutBuffer.prune, - dispatcher = () => inMemoryState.dispatcherState.getDispatcher, - fetchOffsetCheckpoint = () => inMemoryState.offsetCheckpointCache.getOffsetCheckpoint, - getPackageMetadataSnapshot = getPackageMetadataSnapshot, - metrics = metrics, - loggerFactory = loggerFactory, - idleStreamOffsetCheckpointTimeout = config.idleStreamOffsetCheckpointTimeout, - getPreferredPackages = getPackagePreference, - materializer = materializer, - executionContext = commandExecutionContext, - updateServiceConfig = updateServiceConfig, - ) - } yield new TimedIndexService(indexService, metrics) - } - - private def waitForInMemoryStateInitialization()(implicit - executionContext: ExecutionContext - ): Future[Unit] = - RetryStrategy.constant( - attempts = Some(initializationMaxAttempts), - waitTime = initializationRetryDelay, - ) { case InMemoryStateNotInitialized => true } { (attempt, _) => - if (!inMemoryState.initialized) { - logger.info( - s"Participant in-memory state not initialized on attempt $attempt/$initializationMaxAttempts. Retrying again in $initializationRetryDelay." - )(TraceContext.empty) - Future.failed(InMemoryStateNotInitialized) - } else { - logger.info( - s"Participant in-memory state initialized." - )(TraceContext.empty) - Future.unit - } - } - - private def verifyParticipantId( - ledgerDao: LedgerReadDao - )(implicit - executionContext: ExecutionContext - ): Future[Unit] = { - // If the index database is not yet fully initialized, - // querying for the participant ID will throw different errors, - // depending on the database, and how far the initialization is. - val isRetryable: PartialFunction[Throwable, Boolean] = { - case _: IndexDbException => true - case _: ParticipantIdNotFoundException => true - case _: MismatchException.ParticipantId => false - case _ => false - } - - RetryStrategy.constant( - attempts = Some(initializationMaxAttempts), - waitTime = initializationRetryDelay, - )(isRetryable) { (attempt, _) => - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory)(TraceContext.empty) - ledgerDao - .lookupParticipantId() - .flatMap { - case Some(`participantId`) => - logger.info(s"Found existing participant with ID: $participantId`") - Future.unit - case Some(foundParticipantId) => - Future.failed( - new MismatchException.ParticipantId( - foundParticipantId, - ParticipantId(participantId), - ) with StartupException - ) - case None => - logger.info( - s"Participant ID not found in the index database on attempt $attempt/$initializationMaxAttempts. Retrying again in $initializationRetryDelay." - ) - Future.failed(new ParticipantIdNotFoundException(attempt)) - } - } - } - - private def createLedgerReadDao( - ledgerEndCache: LedgerEndCache, - achsStateCache: AchsStateCache, - stringInterning: StringInterning, - contractLoader: ContractLoader, - lfValueTranslation: LfValueTranslation, - queryExecutionContext: ExecutionContextExecutorService, - commandExecutionContext: ExecutionContextExecutorService, - ): LedgerReadDao = - new JdbcLedgerDao( - dbDispatcher = dbSupport.dbDispatcher, - queryExecutionContext = queryExecutionContext, - commandExecutionContext = commandExecutionContext, - metrics = metrics, - readStorageBackend = dbSupport.storageBackendFactory - .readStorageBackend(ledgerEndCache, stringInterning, loggerFactory), - parameterStorageBackend = - dbSupport.storageBackendFactory.createParameterStorageBackend(stringInterning), - ledgerEndCache = ledgerEndCache, - completionsPageSize = config.completionsPageSize, - activeContractsServiceStreamsConfig = config.activeContractsServiceStreams, - updatesStreamsConfig = config.updatesStreams, - globalMaxEventIdQueries = config.globalMaxEventIdQueries, - globalMaxEventPayloadQueries = config.globalMaxEventPayloadQueries, - tracer = tracer, - loggerFactory = loggerFactory, - incompleteOffsets = incompleteOffsets, - contractLoader = contractLoader, - lfValueTranslation = lfValueTranslation, - contractStore = participantContractStore, - achsStateCache = achsStateCache, - contractPruningMaxRetries = config.contractPruningMaxRetries, - contractPruningDelayBeforeRetry = config.contractPruningDelayBeforeRetry.underlying, - scheduler = scheduler, - )(queryExecutionContext) - - private object InMemoryStateNotInitialized extends NoStackTrace -} - -object IndexServiceOwner { - - trait GetPackagePreferenceForViewsUpgrading { - - /** @param packageName - * the package-name for which the preference is requested - * @param candidatePackageIds - * the candidate package-ids restriction - * @param candidatePackageIdsDescription - * a description of the candidate package-ids restriction - * @param loggingContext - * the logging context for the request - */ - def apply( - packageName: Ref.PackageName, - candidatePackageIds: Set[Ref.PackageId], - candidatePackageIdsDescription: String, - loggingContext: logging.LoggingContextWithTrace, - ): FutureUnlessShutdown[Either[String, Ref.PackageId]] - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/ParticipantIdNotFoundException.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/ParticipantIdNotFoundException.scala deleted file mode 100644 index 6946dff9f0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/ParticipantIdNotFoundException.scala +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.index - -class ParticipantIdNotFoundException(attempts: Int) - extends RuntimeException( - s"""No participant ID found in the index database after $attempts attempts.""" - ) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/IndexerConfig.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/IndexerConfig.scala deleted file mode 100644 index 1dd7a4eaa2..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/IndexerConfig.scala +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer - -import com.digitalasset.canton.config -import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, NonNegativeLong} -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.indexer.IndexerConfig.* -import com.digitalasset.canton.platform.store.DbSupport.{ConnectionPoolConfig, DataSourceProperties} -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig - -import scala.concurrent.duration.{DurationInt, FiniteDuration} - -/** See com.digitalasset.canton.platform.indexer.JdbcIndexer for semantics on these configurations. - * - * - enableCompression: switches on compression for both consuming and non-consuming exercises, - * equivalent to setting both enableCompressionConsumingExercise and - * enableCompressionNonConsumingExercise to true. This is to maintain backward compatibility - * with existing config files. - * - enableCompressionConsumingExercise: switches on compression for consuming exercises - * - enableCompressionNonConsumingExercise: switches on compression for non-consuming exercises - */ -final case class IndexerConfig( - batchingParallelism: NonNegativeInt = NonNegativeInt.tryCreate(DefaultBatchingParallelism), - enableCompression: Boolean = DefaultEnableCompression, - enableCompressionConsumingExercise: Boolean = DefaultEnableCompression, - enableCompressionNonConsumingExercise: Boolean = DefaultEnableCompression, - ingestionParallelism: NonNegativeInt = NonNegativeInt.tryCreate(DefaultIngestionParallelism), - inputMappingParallelism: NonNegativeInt = - NonNegativeInt.tryCreate(DefaultInputMappingParallelism), - dbPrepareParallelism: NonNegativeInt = NonNegativeInt.tryCreate(DefaultDbPrepareParallelism), - maxInputBufferSize: NonNegativeInt = NonNegativeInt.tryCreate(DefaultMaxInputBufferSize), - restartDelay: config.NonNegativeFiniteDuration = - config.NonNegativeFiniteDuration.ofSeconds(DefaultRestartDelay.toSeconds), - useWeightedBatching: Boolean = - DefaultUseWeightedBatching, // feature flag to enable improved batching strategy in ingestion pipeline - submissionBatchSize: Long = DefaultSubmissionBatchSize, - submissionBatchInsertionSize: Long = DefaultSubmissionBatchInsertionSize, - maxOutputBatchedBufferSize: Int = DefaultMaxOutputBatchedBufferSize, - maxTailerBatchSize: Int = DefaultMaxTailerBatchSize, - postProcessingParallelism: Int = DefaultPostProcessingParallelism, - queueMaxBlockedOffer: Int = DefaultQueueMaxBlockedOffer, - queueBufferSize: Int = DefaultQueueBufferSize, - queueUncommittedWarnThreshold: Int = DefaultQueueUncommittedWarnThreshold, - queueRecoveryRetryMinWaitMillis: Int = DefaultQueueRecoveryRetryMinWaitMillis, - queueRecoveryRetryMaxWaitMillis: Int = DefaultQueueRecoveryRetryMaxWaitMillis, - queueRecoveryRetryAttemptWarnThreshold: Int = DefaultQueueRecoveryRetryAttemptWarnThreshold, - queueRecoveryRetryAttemptErrorThreshold: Int = DefaultQueueRecoveryRetryAttemptErrorThreshold, - disableMonotonicityChecks: Boolean = false, - postgresDataSource: PostgresDataSourceConfig = DefaultPostgresDataSourceConfig, - achsConfig: Option[AchsConfig] = DefaultAchsConfig, -) - -object IndexerConfig { - - // Exposed as public method so defaults can be overridden in the downstream code. - def createDataSourcePropertiesForTesting( - indexerConfig: IndexerConfig, - loggerFactory: NamedLoggerFactory, - ): DataSourceProperties = DataSourceProperties( - // PostgresSQL specific configurations - postgres = PostgresDataSourceConfig( - synchronousCommit = Some(PostgresDataSourceConfig.SynchronousCommitValue.Off) - ), - connectionPool = createConnectionPoolConfig(indexerConfig, loggerFactory = loggerFactory), - ) - - def createConnectionPoolConfig( - indexerConfig: IndexerConfig, - loggerFactory: NamedLoggerFactory, - connectionTimeout: FiniteDuration = FiniteDuration( - // 250 millis is the lowest possible value for this Hikari configuration (see HikariConfig JavaDoc) - 250, - "millis", - ), - ): ConnectionPoolConfig = { - // Base pool: ingestion + dbPrepare + 2 (tailing ledger_end + post processing end updates) - val basePoolSize = - indexerConfig.ingestionParallelism.unwrap + - indexerConfig.dbPrepareParallelism.unwrap + - 2 - - // ACHS pool: population + removal + 2 (bump validAt + update last pointers) - val achsPoolSize = indexerConfig.achsConfig.fold(0) { achsConfig => - achsConfig.populationParallelism.unwrap + - achsConfig.removalParallelism.unwrap + - 2 - } - - val connectionPoolSize = basePoolSize + achsPoolSize - - indexerConfig.achsConfig.foreach { achsConfig => - val initParallelism = achsConfig.initParallelism.unwrap * 2 - - if (initParallelism > connectionPoolSize) { - loggerFactory - .getLogger(getClass) - .warn( - s"ACHS initialization parallelism ($initParallelism) exceeds the indexing parallelism ($connectionPoolSize). " + - "Consider decreasing the ACHS initialization parallelism." - ) - } - } - ConnectionPoolConfig( - connectionPoolSize = connectionPoolSize, - connectionTimeout = connectionTimeout, - ) - } - - val DefaultRestartDelay: FiniteDuration = 10.seconds - val DefaultMaxInputBufferSize: Int = 50 - val DefaultInputMappingParallelism: Int = 16 - val DefaultDbPrepareParallelism: Int = 4 - val DefaultBatchingParallelism: Int = 4 - val DefaultIngestionParallelism: Int = 16 - val DefaultUseWeightedBatching: Boolean = false - val DefaultSubmissionBatchSize: Long = 50L - val DefaultSubmissionBatchInsertionSize: Long = 5000L - val DefaultEnableCompression: Boolean = false - val DefaultMaxOutputBatchedBufferSize: Int = 16 - val DefaultMaxTailerBatchSize: Int = 10 - val DefaultPostProcessingParallelism: Int = 8 - val DefaultQueueMaxBlockedOffer: Int = 1000 - val DefaultQueueBufferSize: Int = 50 - val DefaultQueueUncommittedWarnThreshold: Int = 5000 - val DefaultQueueRecoveryRetryMinWaitMillis: Int = 50 - val DefaultQueueRecoveryRetryMaxWaitMillis: Int = 5000 - val DefaultQueueRecoveryRetryAttemptWarnThreshold: Int = 50 - val DefaultQueueRecoveryRetryAttemptErrorThreshold: Int = 100 - val DefaultPostgresDataSourceConfig: PostgresDataSourceConfig = - PostgresDataSourceConfig(networkTimeout = Some(config.NonNegativeFiniteDuration.ofSeconds(20))) - val DefaultAchsConfig: Option[AchsConfig] = None - - /** Configuration for the Active Contracts Head Snapshot (ACHS). See - * [[com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.AchsState]] for more. - * - * @param validAtDistanceTarget - * The target distance (in event sequential ids) between the current ledger end and the valid - * at of the ACHS. - * @param lastPopulatedDistanceTarget - * The target distance (in event sequential ids) between the valid at and the last populated - * offset of the ACHS. - * @param populationParallelism - * Parallelism for ACHS population during normal operation. - * @param removalParallelism - * Parallelism for ACHS removal during normal operation. - * @param aggregationThreshold - * Aggregation threshold for ACHS maintenance batches. - * @param initParallelism - * Parallelism for ACHS population/removal during initialization. - * @param initAggregationThreshold - * Aggregation threshold for ACHS maintenance batches during initialization. - * @param bufferSize - * Size of the ACHS buffer. - */ - final case class AchsConfig( - validAtDistanceTarget: NonNegativeLong, - lastPopulatedDistanceTarget: NonNegativeLong, - populationParallelism: NonNegativeInt = - NonNegativeInt.tryCreate(AchsConfig.DefaultPopulationParallelism), - removalParallelism: NonNegativeInt = - NonNegativeInt.tryCreate(AchsConfig.DefaultRemovalParallelism), - aggregationThreshold: Long = AchsConfig.DefaultAggregationThreshold, - initParallelism: NonNegativeInt = NonNegativeInt.tryCreate(AchsConfig.DefaultInitParallelism), - initAggregationThreshold: Long = AchsConfig.DefaultInitAggregationThreshold, - bufferSize: Int = AchsConfig.DefaultBufferSize, - ) - - object AchsConfig { - val DefaultPopulationParallelism: Int = 4 - val DefaultRemovalParallelism: Int = 4 - val DefaultAggregationThreshold: Long = 10000L - val DefaultInitParallelism: Int = 8 - val DefaultInitAggregationThreshold: Long = 100000L - val DefaultBufferSize: Int = 256 - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/IndexerState.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/IndexerState.scala deleted file mode 100644 index cad22ed57b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/IndexerState.scala +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer - -import cats.arrow.FunctionK -import cats.data.EitherT -import com.daml.timer.RetryStrategy -import com.daml.timer.RetryStrategy.UnhandledFailureException -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.participant.state.Update.CommitRepair -import com.digitalasset.canton.ledger.participant.state.{RepairUpdate, SynchronizerUpdate, Update} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.lifecycle.UnlessShutdown.AbortedDueToShutdown -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.PekkoUtil.{FutureQueue, RecoveringFutureQueue} -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.canton.util.{Mutex, PekkoUtil, TryUtil} -import org.apache.pekko.Done - -import scala.concurrent.duration.Duration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.control.NoStackTrace -import scala.util.{Failure, Success, Try} - -@SuppressWarnings(Array("org.wartremover.warts.Var")) -class IndexerState( - recoveringIndexerFactory: () => RecoveringFutureQueue[Update], - repairIndexerFactory: () => Future[FutureQueue[Update]], - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends NamedLogging { - import IndexerState.* - - private var state: State = Normal(recoveringIndexerFactory(), shutdownInitiated = false) - private val lock = new Mutex() - - private implicit val traceContext: TraceContext = TraceContext.empty - - // Requesting a Repair Indexer turns off normal indexing, therefore it needs to be ensured, before calling: - // - no synchronizers are connected - // - no party/package additions are happening - // The life-cycle of the indexer with regards to Repair mode is as follows: - // 1 - normal indexing is ongoing - // 2 - repair indexing is requested - // 3 - normal indexing waits for an empty indexing queue (no activity) - // 4 - normal indexing stops - // 5 - repair indexing is initiating - // 6 - repair indexing is ready to be used (here provided repairOperation starts executing) - // 7 - repair indexing is used: FutureQueue offer operations - // 8 - repair usage finished (block finished executing) - // 9 - repair is committing: the Ledger End will be persisted - // 10 - repair indexing stops - // 11 - normal indexing resumes operation (starts the recovering initialization loop) - // 12 - normal indexer initialized the first time (important to ensure LAPI in memory state is intact by the time the repair command finishes) - // 13 - the resulting Future finishes - // The client needs to ensure that the provided repair indexer is not used after the resulting Future terminates, also: CommitRepair should not be used directly. - def withRepairIndexer( - repairOperation: FutureQueue[RepairUpdate] => EitherT[Future, String, Unit] - )(implicit traceContext: TraceContext): EitherT[Future, String, Unit] = EitherT( - withStateUnlessShutdown { - case Repair(_, repairDone, _) => - Future.failed(new RepairInProgress(repairDone)) - - case Normal(queue, _) => - logger.info("Switched to Repair Mode, waiting for inactive indexing...") - val repairIndexerF = for { - _ <- waitForEmptyIndexerQueue(queue) - _ = logger.info("Shutting down Indexer...") - _ = queue.shutdown() - _ <- queue.done - _ = logger.info("Initializing Repair Indexer...") - repairIndexer <- withStateUnlessShutdown(_ => repairIndexerFactory()) - _ = logger.info("Repair Indexer ready") - } yield new RepairQueueProxy(repairIndexer, () => onRepairFinished(), loggerFactory) - val result = repairIndexerF.transformWith { - case Failure(t) => - logger.info("Repair Indexer initialization failed, resuming normal indexing...", t) - onRepairFinished().transform(_ => Failure(t)) - - case Success(repairIndexer) => - logger.info("Repair Indexer initialized, executing repair operation...") - executeRepairOperation(repairIndexer, repairOperation) - } - state = Repair( - repairIndexerF, - result.transform(_ => TryUtil.unit), - shutdownInitiated = false, - ) - result - } - ) - - private def waitForEmptyIndexerQueue(queue: RecoveringFutureQueue[Update]): Future[Unit] = - RetryStrategy - .constant(Some(100), Duration.create(100, "millis")) { case t: Throwable => - t.getMessage.contains("Still indexing") - }((_, _) => - withStateUnlessShutdown(_ => - if (queue.uncommittedQueueSnapshot.nonEmpty) - Future.failed(new Exception(s"Still indexing")) - else - Future.unit - ) - ) - .recoverWith { case UnhandledFailureException(_, _, ShutdownInProgress) => - Future.failed(ShutdownInProgress) - } - .recoverWith { err => - // shutting down indexer anyway, as the most probably cause here is that we are shutting down - logger.info("Shutting down Indexer after waiting for empty indexer queue failed...") - queue.shutdown() - queue.done.transform(_ => Failure(err)) - } - - private def onRepairFinished(): Future[Unit] = withStateUnlessShutdown { - case Normal(normalIndexer, _) => - logger.error( - "Illegal state transition: before finished Repair Indexer, normal Indexer operation resumed" - ) - normalIndexer.firstSuccessfulConsumerInitialization - - case Repair(_, _, _) => - logger.info("Switched to Normal Mode") - val normalIndexer = recoveringIndexerFactory() - state = Normal(normalIndexer, shutdownInitiated = false) - normalIndexer.firstSuccessfulConsumerInitialization.thereafter { - case Success(_) => - logger.info("Normal indexing successfully initialized") - - case Failure(t) => - logger.warn("Normal indexing failed to initialize successfully", t) - } - } - - private def executeRepairOperation( - repairIndexer: RepairQueueProxy, - repairOperation: PekkoUtil.FutureQueue[RepairUpdate] => EitherT[Future, String, Unit], - ): Future[Either[String, Unit]] = withStateUnlessShutdown { _ => - def waitForRepairIndexerToTerminateAndThenReturnUnlessShutdown[T](result: Try[T]): Future[T] = - withStateUnlessShutdown(_ => repairIndexer.done.transform(_ => result)) - - def waitForRepairIndexerToTerminateUnlessShutdownAndThenReturn[T](result: Try[T]): Future[T] = - withStateUnlessShutdown(_ => repairIndexer.done).transform(_ => result) - - def commitRepair(): Future[Right[Nothing, Unit]] = withStateUnlessShutdown(_ => - repairIndexer.commit().transformWith { - case Failure(t) => - logger.warn(s"Committing repair changes failed, resuming normal indexing...", t) - repairIndexer.shutdown() - waitForRepairIndexerToTerminateUnlessShutdownAndThenReturn( - Failure(new Exception("Committing repair changes failed", t)) - ) - - case Success(_) => - logger.info(s"Committing repair changes succeeded, resuming normal indexing...") - waitForRepairIndexerToTerminateUnlessShutdownAndThenReturn(Success(Right(()))) - } - ) - - Future.delegate(repairOperation(repairIndexer).value).transformWith { - case Failure(t) => - logger.info("Repair operation failed with exception, resuming normal indexing...", t) - repairIndexer.shutdown() - waitForRepairIndexerToTerminateAndThenReturnUnlessShutdown(Failure(t)) - - case Success(Left(failure)) => - logger.info(s"Repair operation failed with error ($failure), resuming normal indexing...") - repairIndexer.shutdown() - waitForRepairIndexerToTerminateAndThenReturnUnlessShutdown(Success(Left(failure))) - - case Success(Right(_)) => - logger.info(s"Repair operation succeeded, committing changes...") - commitRepair() - } - } - - // Mapping all results to a clean shutdown, to allow further shutdown-steps to complete normally. - private def handleShutdownDoneResult(doneResult: Try[Done]): Success[Unit] = { - doneResult match { - case Success(Done) => - logger.info("IndexerState stopped successfully") - - case Failure(t) => - // Logging at info level since either Repair-Index or Recovering-Indexer should emit warnings in case of shutdown related problems. - logger.info("IndexerState stopped with a failure", t) - } - Success(()) - } - - def shutdown(): Future[Unit] = withState { - case Normal(queue, shutdownInitiated) => - if (!shutdownInitiated) { - queue.shutdown() - state = Normal( - queue, - shutdownInitiated = true, - ) - } - queue.done.transform(handleShutdownDoneResult) - - case Repair(queueF, repairDone, shutdownInitiated) => - if (!shutdownInitiated) { - queueF.onComplete(_.foreach(_.shutdown())) - state = Repair( - queueF, - repairDone, - shutdownInitiated = true, - ) - } - queueF.flatMap(_.done).transform(handleShutdownDoneResult) - } - - def ensureNoProcessingForSynchronizer(synchronizerId: SynchronizerId): Future[Unit] = - withStateUnlessShutdown { - case Normal(recoveringQueue, _) => - RetryStrategy - .constant(None, Duration.create(200, "millis")) { case t: Throwable => - t.getMessage.contains("Still uncommitted") - }((_, _) => - withStateUnlessShutdown(_ => - if ( - recoveringQueue.uncommittedQueueSnapshot.iterator.map(_._2).exists { - case u: SynchronizerUpdate => u.synchronizerId == synchronizerId - case _: Update.CommitRepair => false - } - ) - Future.failed( - new Exception( - s"Still uncommitted activity for synchronizer $synchronizerId, waiting..." - ) - ) - else - Future.unit - ) - ) - .recoverWith { case UnhandledFailureException(_, _, ShutdownInProgress) => - Future.failed(ShutdownInProgress) - } - - case Repair(_, repairDone, _) => - Future.failed(new RepairInProgress(repairDone)) - } - - def waitForFirstSuccessfulIndexerInitialization: Future[Unit] = - withStateUnlessShutdown { - case Normal(recoveringQueue, _) => - logger.info("Waiting for first successful initialization of the indexer") - recoveringQueue.firstSuccessfulConsumerInitialization - .transform( - _ => logger.info("Indexer initialized"), - failure => { - logger.info( - "Waiting for first successful initialization of the indexer failed", - failure, - ) - failure - }, - ) - - case Repair(_, repairDone, _) => - Future.failed(new RepairInProgress(repairDone)) - } - - private def withState[T](f: State => T): T = - (lock.exclusive(f(state))) - - def withStateUnlessShutdown[T](f: State => Future[T]): Future[T] = - withState(s => - if (s.shutdownInitiated) Future.failed(ShutdownInProgress) - else f(s) - ) -} - -object IndexerQueueProxy { - import IndexerState.* - - def apply( - withIndexerState: (IndexerState.State => Future[Unit]) => Future[Unit] - )(implicit executionContext: ExecutionContext): Update => Future[Unit] = elem => - withIndexerState { - case Normal(queue, _) => - elem match { - case commitRepair: CommitRepair => - val failure = new IllegalStateException("CommitRepair should not be used") - commitRepair.persisted.tryFailure(failure).discard - Future.failed(failure) - - case _ => queue.offer(elem).map(_ => ()) - } - - case Repair(_, repairDone, _) => - Future.failed(new RepairInProgress(repairDone)) - } -} - -class RepairQueueProxy( - repairQueue: FutureQueue[Update], - onRepairFinished: () => Future[Unit], - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends FutureQueue[RepairUpdate] - with NamedLogging { - private implicit val traceContext: TraceContext = TraceContext.empty - - override def offer(elem: RepairUpdate): Future[Done] = - repairQueue.offer(elem) - - override def shutdown(): Unit = repairQueue.shutdown() - - override def done: Future[Done] = - repairQueue.done.transformWith { repairDoneResult => - repairDoneResult match { - case Failure(t) => logger.warn("Repair Indexer finished with error", t) - case Success(_) => logger.info("Repair Indexer finished successfully") - } - onRepairFinished().transform(_ => repairDoneResult) - } - - def commit(): Future[Unit] = { - val commitRepair = CommitRepair() - repairQueue - .offer(commitRepair) - .flatMap(_ => commitRepair.persisted.future) - } -} - -object IndexerState { - sealed trait State { - def shutdownInitiated: Boolean - } - - final case class Normal(queue: RecoveringFutureQueue[Update], shutdownInitiated: Boolean) - extends State - - final case class Repair( - queue: Future[FutureQueue[RepairUpdate]], - repairDone: Future[Unit], - shutdownInitiated: Boolean, - ) extends State - - // repairDone should never fail, and only complete if normal indexing is resumed - class RepairInProgress(val repairDone: Future[Unit]) - extends RuntimeException("Repair in progress") - - object ShutdownInProgress extends RuntimeException("Shutdown in progress") with NoStackTrace { - def transformToFUS[T]( - f: Future[T] - )(implicit executionContext: ExecutionContext): FutureUnlessShutdown[T] = - FutureUnlessShutdown - .outcomeF(f) - .recover { case ShutdownInProgress => - AbortedDueToShutdown - } - - def functionK(implicit - executionContext: ExecutionContext - ): FunctionK[Future, FutureUnlessShutdown] = - new FunctionK[Future, FutureUnlessShutdown] { - override def apply[T](f: Future[T]): FutureUnlessShutdown[T] = - transformToFUS(f) - } - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/JdbcIndexer.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/JdbcIndexer.scala deleted file mode 100644 index f8ce46fa22..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/JdbcIndexer.scala +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer - -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.ledger.participant.state.Update -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.InMemoryState -import com.digitalasset.canton.platform.index.InMemoryStateUpdater -import com.digitalasset.canton.platform.indexer.ha.HaConfig -import com.digitalasset.canton.platform.indexer.parallel.AchsMaintenancePipe.AchsWorkRange -import com.digitalasset.canton.platform.indexer.parallel.{ - InitializeParallelIngestion, - ParallelIndexerFactory, - ParallelIndexerSubscription, - PostPublishData, - ReassignmentOffsetPersistence, -} -import com.digitalasset.canton.platform.store.DbSupport.{ - DataSourceProperties, - ParticipantDataSourceConfig, -} -import com.digitalasset.canton.platform.store.backend.StorageBackendFactory -import com.digitalasset.canton.platform.store.backend.h2.H2StorageBackendFactory -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.platform.store.dao.events.{CompressionStrategy, LfValueTranslation} -import com.digitalasset.canton.platform.store.{DbType, LedgerApiContractStore} -import com.digitalasset.canton.time.Clock -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.* -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.{ExecutionContext, Future} - -object JdbcIndexer { - final class Factory( - participantId: Ref.ParticipantId, - participantDataSourceConfig: ParticipantDataSourceConfig, - config: IndexerConfig, - metrics: LedgerApiServerMetrics, - inMemoryState: InMemoryState, - apiUpdaterFlow: InMemoryStateUpdater.UpdaterFlow, - executionContext: ExecutionContext, - tracer: Tracer, - loggerFactory: NamedLoggerFactory, - dataSourceProperties: DataSourceProperties, - highAvailability: HaConfig, - indexServiceDbDispatcher: Option[DbDispatcher], - clock: Clock, - reassignmentOffsetPersistence: ReassignmentOffsetPersistence, - postProcessor: (Vector[PostPublishData], TraceContext) => Future[Unit], - sequentialPostProcessor: Update => Unit, - contractStore: LedgerApiContractStore, - achsInitInterceptor: Source[AchsWorkRange, NotUsed] => Source[AchsWorkRange, NotUsed], - )(implicit materializer: Materializer) { - - def initialized()(implicit - traceContext: TraceContext - ): ResourceOwner[(Indexer, Option[() => Unit])] = { - val factory = StorageBackendFactory.of( - DbType.jdbcType(participantDataSourceConfig.jdbcUrl), - loggerFactory, - ) - val dataSourceStorageBackend = factory.createDataSourceStorageBackend - val ingestionStorageBackend = factory.createIngestionStorageBackend - val parameterStorageBackend = - factory.createParameterStorageBackend(inMemoryState.stringInterningView) - val contractStorageBackend = factory.createContractStorageBackend( - inMemoryState.stringInterningView, - inMemoryState.ledgerEndCache, - ) - val DBLockStorageBackend = factory.createDBLockStorageBackend - val stringInterningStorageBackend = factory.createStringInterningStorageBackend - val completionStorageBackend = - factory.createCompletionStorageBackend(inMemoryState.stringInterningView, loggerFactory) - val eventStorageBackend = factory.createEventStorageBackend( - inMemoryState.ledgerEndCache, - inMemoryState.stringInterningView, - loggerFactory, - ) - val dbConfig = dataSourceProperties - // in case H2 backend, we share a single connection between indexer and index service - // to prevent H2 synchronization bug to materialize - // the ingestion parallelism is also limited to 1 in this case - val (ingestionParallelism, indexerDbDispatcherOverride) = - if (factory == H2StorageBackendFactory) 1 -> indexServiceDbDispatcher - else config.ingestionParallelism.unwrap -> None - val initParallelIngestion = InitializeParallelIngestion( - providedParticipantId = participantId, - parameterStorageBackend = parameterStorageBackend, - ingestionStorageBackend = ingestionStorageBackend, - eventStorageBackend = eventStorageBackend, - completionStorageBackend = completionStorageBackend, - stringInterningStorageBackend = stringInterningStorageBackend, - updatingStringInterningView = inMemoryState.stringInterningView, - postProcessor = postProcessor, - achsStateCache = inMemoryState.achsStateCache, - achsConfig = config.achsConfig, - metrics = metrics, - loggerFactory = loggerFactory, - achsInitInterceptor = achsInitInterceptor, - ) - val achsKillSwitch = initParallelIngestion.achsKillSwitch - ParallelIndexerFactory( - inputMappingParallelism = config.inputMappingParallelism.unwrap, - batchingParallelism = config.batchingParallelism.unwrap, - dbConfig = dbConfig.createDbConfig(participantDataSourceConfig), - haConfig = highAvailability, - metrics = metrics, - dbLockStorageBackend = DBLockStorageBackend, - dataSourceStorageBackend = dataSourceStorageBackend, - initializeParallelIngestion = initParallelIngestion, - parallelIndexerSubscription = ParallelIndexerSubscription( - parameterStorageBackend = parameterStorageBackend, - ingestionStorageBackend = ingestionStorageBackend, - contractStorageBackend = contractStorageBackend, - eventStorageBackend = eventStorageBackend, - participantId = participantId, - translation = new LfValueTranslation( - metrics = metrics, - engineO = None, - loadPackage = (_, _) => Future.successful(None), - loggerFactory = loggerFactory, - ), - compressionStrategy = - if (config.enableCompression) CompressionStrategy.allGZIP(metrics) - else - CompressionStrategy.buildFromConfig(metrics)( - config.enableCompressionConsumingExercise, - config.enableCompressionNonConsumingExercise, - ), - maxInputBufferSize = config.maxInputBufferSize.unwrap, - inputMappingParallelism = config.inputMappingParallelism.unwrap, - dbPrepareParallelism = config.dbPrepareParallelism.unwrap, - batchingParallelism = config.batchingParallelism.unwrap, - ingestionParallelism = ingestionParallelism, - useWeightedBatching = config.useWeightedBatching, - submissionBatchSize = config.submissionBatchSize, - submissionBatchInsertionSize = config.submissionBatchInsertionSize, - maxTailerBatchSize = config.maxTailerBatchSize, - postProcessingParallelism = config.postProcessingParallelism, - achsConfig = config.achsConfig, - maxOutputBatchedBufferSize = config.maxOutputBatchedBufferSize, - metrics = metrics, - inMemoryStateUpdaterFlow = apiUpdaterFlow, - inMemoryState = inMemoryState, - reassignmentOffsetPersistence = reassignmentOffsetPersistence, - postProcessor = postProcessor, - sequentialPostProcessor = sequentialPostProcessor, - contractStore = contractStore, - disableMonotonicityChecks = config.disableMonotonicityChecks, - tracer = tracer, - loggerFactory = loggerFactory, - executionContext = executionContext, - ), - mat = materializer, - executionContext = executionContext, - initializeInMemoryState = inMemoryState.initializeTo, - loggerFactory = loggerFactory, - indexerDbDispatcherOverride = indexerDbDispatcherOverride, - clock = clock, - ).map(indexer => (indexer, achsKillSwitch)) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/TransactionTraversalUtils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/TransactionTraversalUtils.scala deleted file mode 100644 index c874b9e93d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/TransactionTraversalUtils.scala +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer - -import com.digitalasset.daml.lf.transaction.Node.Create -import com.digitalasset.daml.lf.transaction.Transaction.ChildrenRecursion -import com.digitalasset.daml.lf.transaction.{Node, NodeId, Transaction} - -object TransactionTraversalUtils { - - final case class NodeInfo(nodeId: NodeId, node: Node, lastDescendantNodeId: NodeId) - - /** It reorders the node ids of a transaction to follow the execution order (pre-order traversal) - * and finds the node ids of their last descendant, omitting the fetch, the lookup, the rollback - * nodes and the descendants of the rollback nodes. - * - * @param transaction - * the given transaction - * @return - * the node id, node and the node id of the last descendant in the execution order - */ - def executionOrderTraversalForIngestion(transaction: Transaction): Iterator[NodeInfo] = { - // Rearrange node ids to follow the execution order. - // We need to do this to guarantee that the representation of the descendants with the use of the last descendant - // holds. - // It requires that all descendant nodes have a node id which is greater than their ancestor's and - // lower than their ancestor's last descendant node id. - val orderedTx = arrangeNodeIdsInExecutionOrder(transaction) - - val lastDescendantMapping: Map[NodeId, NodeId] = getLastDescendantMapping(orderedTx) - - def lastDescendant(nid: NodeId): NodeId = lastDescendantMapping.getOrElse( - nid, - throw new RuntimeException(s"It should never have been here (nodeId: $nid)!"), - ) - - orderedTx - .foldInExecutionOrder(List.empty[NodeInfo])( - exerciseBegin = (acc, nid, node) => { - ( - NodeInfo(nid, node, lastDescendant(nid)) :: acc, - ChildrenRecursion.DoRecurse, - ) - }, - // Rollback nodes are not indexed - rollbackBegin = (acc, _, _) => (acc, ChildrenRecursion.DoNotRecurse), - // Fetch and Lookup nodes are not indexed - leaf = { - case (acc, nid, node: Create) => NodeInfo(nid, node, lastDescendant(nid)) :: acc - case (acc, _, _) => acc - }, - exerciseEnd = (acc, _, _) => acc, - rollbackEnd = (acc, _, _) => acc, - ) - }.reverseIterator - - private[indexer] def arrangeNodeIdsInExecutionOrder( - transaction: Transaction - ): Transaction = { - val mapping = transaction - .foldInExecutionOrder((List.empty[(NodeId, NodeId)], 0))( - exerciseBegin = { case ((acc, next), nid, _) => - (((nid -> NodeId(next)) :: acc, next + 1), ChildrenRecursion.DoRecurse) - }, - rollbackBegin = { case ((acc, next), nid, _) => - (((nid -> NodeId(next)) :: acc, next + 1), ChildrenRecursion.DoRecurse) - }, - leaf = { case ((acc, next), nid, _) => - ((nid -> NodeId(next)) :: acc, next + 1) - }, - exerciseEnd = (acc, _, _) => acc, - rollbackEnd = (acc, _, _) => acc, - ) - ._1 - .toMap - transaction.mapNodeId(nid => - mapping.getOrElse( - nid, - throw new RuntimeException(s"It should never have been here (nodeId: $nid)!"), - ) - ) - } - - private def getLastDescendantMapping(transaction: Transaction): Map[NodeId, NodeId] = - transaction - .foldInExecutionOrder(List.empty[(NodeId, NodeId)])( - exerciseBegin = (acc, _, _) => (acc, ChildrenRecursion.DoRecurse), - // Rollback nodes are not indexed - rollbackBegin = (acc, _, _) => (acc, ChildrenRecursion.DoNotRecurse), - leaf = (acc, nid, _) => (nid -> nid) :: acc, - exerciseEnd = (acc, nid, _) => { - val lastDescendantNodeId = - // if the exercise node had no children then the last element added to the list will have a node id that is - // less than the exercise node (since we traverse the transaction in execution order) and the node id of - // itself will be stored as its last descendant - NodeId(acc.headOption.map(_._2.index).getOrElse(Int.MinValue).max(nid.index)) - (nid -> lastDescendantNodeId) :: acc - }, - rollbackEnd = (acc, _, _) => acc, - ) - .toMap -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/HaCoordinator.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/HaCoordinator.scala deleted file mode 100644 index 1f0202e02c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/HaCoordinator.scala +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.ha - -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} -import com.digitalasset.canton.platform.store.backend.DBLockStorageBackend -import com.digitalasset.canton.platform.store.backend.DBLockStorageBackend.{Lock, LockId, LockMode} -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.stream.KillSwitch - -import java.sql.Connection -import java.util.Timer -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, ExecutionContext, Future} - -/** A handle of a running program - * @param completed - * will complete right after the program completed - * - if no KillSwitch used, - * - it will complete successfully as program successfully ends - * - it will complete with the same failure that failed the program - * - if KillSwitch aborted, this completes with the same Throwable - * - if KillSwitch shut down, this completes successfully After signalling completion, the - * program finished it's execution and has released all resources it acquired. - * @param killSwitch - * to signal abortion and shutdown - */ -final case class Handle(completed: Future[Unit], killSwitch: KillSwitch) - -/** This functionality initializes a worker Connection, and clears it for further usage. This only - * needs to be done once at the beginning of the Connection life-cycle Initialization errors are - * signaled by throwing an exception. - */ -trait ConnectionInitializer { - def initialize(connection: Connection): Unit -} - -/** To add High Availability related features to a program, which intends to use - * database-connections to do it's work. Features include: - * - Safety: mutual exclusion of these programs ensured by DB locking mechanisms - * - Availability: release of the exclusion is detected by idle programs, which start competing - * for the lock to do their work. - */ -trait HaCoordinator { - - /** Execute in High Availability mode. Wraps around the Handle of the execution. - * - * @param initializeExecution - * HaCoordinator provides a ConnectionInitializer that must to be used for all database - * connections during execution Future[Handle] embodies asynchronous initialization of the - * execution (e.g. not the actual work. That asynchronous execution completes with the - * completed Future of the Handle) - * @return - * the new Handle, which is available immediately to observe and interact with the complete - * program here - */ - def protectedExecution(initializeExecution: ConnectionInitializer => Future[Handle]): Handle -} - -final case class HaConfig( - mainLockAcquireRetryTimeout: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofMillis(500), - mainLockAcquireMaxRetries: NonNegativeLong = NonNegativeLong.tryCreate(10), - workerLockAcquireRetryTimeout: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofMillis(500), - workerLockAcquireMaxRetries: NonNegativeLong = NonNegativeLong.tryCreate(10), - mainLockCheckerPeriod: NonNegativeFiniteDuration = NonNegativeFiniteDuration.ofMillis(1000), - mainLockCheckerJdbcNetworkTimeout: NonNegativeFiniteDuration = - NonNegativeFiniteDuration.ofMillis(10000), - indexerLockId: Int = 0x646d6c0, // note 0x646d6c equals ASCII encoded "dml" - indexerWorkerLockId: Int = 0x646d6c1, -) - -object HaCoordinator { - - /** This implementation of the HaCoordinator - * - provides a database lock based isolation of the protected executions - * - will run the execution at-most once during the entire lifecycle - * - will wait infinitely to acquire the lock needed to start the protected execution - * - provides a ConnectionInitializer function which is mandatory to execute on all worker - * connections during execution - * - will spawn a polling-daemon to observe continuous presence of the main lock - * - * @param mainConnectionFactory - * to spawn the main connection which keeps the Indexer Main Lock - * @param storageBackend - * is the database-independent abstraction of session/connection level database locking - * @param executionContext - * which is use to execute initialisation, will do blocking/IO work, so dedicated execution - * context is recommended - */ - def databaseLockBasedHaCoordinator( - mainConnectionFactory: () => Connection, - storageBackend: DBLockStorageBackend, - executionContext: ExecutionContext, - timer: Timer, - haConfig: HaConfig, - loggerFactory: NamedLoggerFactory, - )(implicit traceContext: TraceContext): HaCoordinator = { - implicit val ec: ExecutionContext = executionContext - - val logger = TracedLogger(loggerFactory.getLogger(getClass)) - val indexerLockId = storageBackend.lock(haConfig.indexerLockId) - val indexerWorkerLockId = storageBackend.lock(haConfig.indexerWorkerLockId) - val preemptableSequence = PreemptableSequence(timer, loggerFactory) - - new HaCoordinator { - override def protectedExecution( - initializeExecution: ConnectionInitializer => Future[Handle] - ): Handle = { - def acquireLock( - connection: Connection, - lockId: LockId, - lockMode: LockMode, - fromHealthCheck: Boolean = false, - ): Lock = { - // Reduce log level if we check the lock acquisition from the health check - if (fromHealthCheck) - logger.trace(s"Acquiring lock $lockId $lockMode") - else - logger.debug(s"Acquiring lock $lockId $lockMode") - storageBackend - .tryAcquire(lockId, lockMode)(connection) - .getOrElse( - throw new CannotAcquireLockException(lockId, lockMode) - ) - } - - def acquireMainLock(connection: Connection, fromHealthCheck: Boolean = false): Unit = - acquireLock(connection, indexerLockId, LockMode.Exclusive, fromHealthCheck).discard - - preemptableSequence.executeSequence { sequenceHelper => - import sequenceHelper.* - logger.info("Starting IndexDB HA Coordinator") - for { - mainConnection <- go[Connection](mainConnectionFactory()) - _ = logger.debug("Step 1: creating main-connection - DONE") - _ = registerRelease { - logger.debug("Releasing main connection...") - mainConnection.close() - logger.debug("Step 8: Released main connection") - logger.info("Stepped down as leader, IndexDB HA Coordinator shut down") - } - _ = logger.info("Waiting to be elected as leader") - _ <- retry( - waitMillisBetweenRetries = haConfig.mainLockAcquireRetryTimeout.duration.toMillis, - maxAmountOfRetries = haConfig.mainLockAcquireMaxRetries.unwrap, - retryable = _.isInstanceOf[CannotAcquireLockException], - )(acquireMainLock(mainConnection)) - _ = logger.info("Elected as leader: starting initialization") - _ = logger.info("Waiting for previous IndexDB HA Coordinator to finish work") - _ = logger.debug( - "Step 2: acquire exclusive Indexer Main Lock on main-connection - DONE" - ) - exclusiveWorkerLock <- retry[Lock]( - waitMillisBetweenRetries = haConfig.workerLockAcquireRetryTimeout.duration.toMillis, - maxAmountOfRetries = haConfig.workerLockAcquireMaxRetries.unwrap, - retryable = _.isInstanceOf[CannotAcquireLockException], - )( - acquireLock( - mainConnection, - indexerWorkerLockId, - LockMode.Exclusive, - ) - ) - _ = logger.info( - "Previous IndexDB HA Coordinator finished work, starting DB connectivity polling" - ) - _ = logger.debug( - "Step 3: acquire exclusive Indexer Worker Lock on main-connection - DONE" - ) - _ <- go(storageBackend.release(exclusiveWorkerLock)(mainConnection)) - _ = logger.debug( - "Step 4: release exclusive Indexer Worker Lock on main-connection - DONE" - ) - mainLockChecker <- go[PollingChecker]( - new PollingChecker( - periodMillis = haConfig.mainLockCheckerPeriod.duration.toMillis, - checkBody = acquireMainLock(mainConnection, fromHealthCheck = true), - killSwitch = - handle.killSwitch, // meaning: this PollingChecker will shut down the main preemptableSequence - loggerFactory = loggerFactory, - ) - ) - _ = logger.debug( - "Step 5: activate periodic checker of the exclusive Indexer Main Lock on the main connection - DONE" - ) - _ = registerRelease { - logger.debug( - "Releasing periodic checker of the exclusive Indexer Main Lock on the main connection..." - ) - logger.info("Stepping down as leader, stopping DB connectivity polling") - mainLockChecker.close() - logger.debug( - "Step 7: Released periodic checker of the exclusive Indexer Main Lock on the main connection" - ) - } - protectedHandle <- goF(initializeExecution { workerConnection => - // this is the checking routine on connection creation - // step 1: acquire shared worker-lock - logger.debug(s"Preparing worker connection. Step 1: acquire lock.") - acquireLock(workerConnection, indexerWorkerLockId, LockMode.Shared).discard - // step 2: check if main connection still holds the lock - logger.debug(s"Preparing worker connection. Step 2: checking main lock.") - mainLockChecker.check() - logger.debug(s"Preparing worker connection DONE.") - }) - _ = logger.debug("Step 6: initialize protected execution - DONE") - _ = logger.info("Elected as leader: initialization complete") - _ <- merge(protectedHandle) - } yield () - } - } - } - } - - class CannotAcquireLockException(lockId: LockId, lockMode: LockMode) extends RuntimeException { - override def getMessage: String = - s"Cannot acquire lock $lockId in lock-mode $lockMode" - } -} - -object NoopHaCoordinator extends HaCoordinator { - override def protectedExecution( - initializeExecution: ConnectionInitializer => Future[Handle] - ): Handle = - Await.result(initializeExecution(_ => ()), Duration.Inf) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/KillSwitchCaptor.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/KillSwitchCaptor.scala deleted file mode 100644 index aca01a6771..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/KillSwitchCaptor.scala +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.ha - -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.stream.KillSwitch - -import java.util.concurrent.atomic.AtomicReference - -/** This KillSwitch captures it's usage in it's internal state, which can be queried. Captured state - * is available with the 'state' method. - * - * Rules of state transitions: - * - Shutdown is always the final state - * - From multiple aborts, the last abort wins - * - * With setDelegate() we can set a delegate KillSwitch, which to usage will be replayed - */ -class KillSwitchCaptor(val loggerFactory: NamedLoggerFactory)(implicit traceContext: TraceContext) - extends KillSwitch - with NamedLogging { - import KillSwitchCaptor.* - import State.* - - private val _state = new AtomicReference[State](Unused) - private val _delegate = new AtomicReference[Option[KillSwitch]](None) - - private def updateState(newState: Used): Unit = { - _state.getAndAccumulate( - newState, - { - case (Shutdown, _) => Shutdown - case (_, used) => used - }, - ) - () - } - - override def shutdown(): Unit = { - logger.info("Shutdown called!") - updateState(Shutdown) - _delegate.get.foreach { ks => - logger.info("Shutdown call delegated!") - ks.shutdown() - } - } - - override def abort(ex: Throwable): Unit = { - logger.info(s"Abort called! (${ex.getMessage})") - updateState(Aborted(ex)) - _delegate.get.foreach { ks => - logger.info(s"Abort call delegated! (${ex.getMessage})") - ks.abort(ex) - } - } - - def state: State = _state.get() - def setDelegate(delegate: Option[KillSwitch]): Unit = _delegate.set(delegate) -} - -object KillSwitchCaptor { - sealed trait State - object State { - case object Unused extends State - sealed trait Used extends State - case object Shutdown extends Used - final case class Aborted(t: Throwable) extends Used - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/PollingChecker.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/PollingChecker.scala deleted file mode 100644 index 1579308952..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/PollingChecker.scala +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.ha - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Mutex -import org.apache.pekko.stream.KillSwitch - -import java.util.{Timer, TimerTask} -import scala.util.{Failure, Success, Try} - -/** A simple host of checking. - * - This will ensure that checkBody is accessed by only one caller at a time - * - Does periodic checking - * - Exposes check() for on-demand checking from the outside - * - If whatever check() fails, it uses killSwitch with an abort - * - It is also an AutoCloseable to release internal resources - * - * @param periodMillis - * period of the checking, between each scheduled checks there will be so much delay - * @param checkBody - * the check function, Exception signals failed check - * @param killSwitch - * to abort if a check fails - */ -class PollingChecker( - periodMillis: Long, - checkBody: => Unit, - killSwitch: KillSwitch, - val loggerFactory: NamedLoggerFactory, -)(implicit traceContext: TraceContext) - extends AutoCloseable - with NamedLogging { - private val timer = new Timer("ha-polling-checker-timer-thread", true) - private val lock = new Mutex() - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var closed: Boolean = false - - timer.schedule( - new TimerTask { - override def run(): Unit = Try(scheduledCheck()).discard - }, - periodMillis, - periodMillis, - ) - - // This is a cruel approach for ensuring single threaded usage of checkBody. - // In theory this could have been made much more efficient: not enqueueing for a check of it's own, - // but collecting requests, and replying in batches. - // Current usage of this class does not necessarily motivate further optimizations: used from HaCoordinator - // to check Indexer Main Lock seems to be sufficiently fast even in peak scenario: the initialization of the - // complete pool. - def check(): Unit = (lock.exclusive { - logger.debug(s"Checking...") - if (closed) { - val errorMsg = - "Internal Error: This check should not be called from outside by the time the PollingChecker is closed." - logger.error(errorMsg) - throw new Exception(errorMsg) - } else { - checkInternal() - } - }) - - private def scheduledCheck(): Unit = (lock.exclusive { - logger.trace(s"Scheduled checking...") - // Timer can fire at most one additional TimerTask after being cancelled. This is to safeguard that corner case. - if (!closed) { - checkInternal() - } - }) - - private def checkInternal(): Unit = (lock.exclusive { - Try(checkBody) match { - case Success(_) => - logger.trace(s"Check successful.") - - case Failure(ex) => - logger.info(s"Check failed (${ex.getMessage}). Calling KillSwitch/abort.") - killSwitch.abort(new Exception("check failed, killSwitch aborted", ex)) - throw ex - } - }) - - def close(): Unit = (lock.exclusive { - closed = true - timer.cancel() - }) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/PreemptableSequence.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/PreemptableSequence.scala deleted file mode 100644 index 637e22358e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/ha/PreemptableSequence.scala +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.ha - -import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Mutex - -import java.util.{Timer, TimerTask} -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Success} - -/** PreemptableSequence is a helper to - * - facilitate the execution of a sequence of Futures, which can be stopped or aborted - * - provide a Handle for the client - * - manage the state to implement the above - */ -trait PreemptableSequence { - - /** Execute the preemptable sequence - * - * @param sequence - * This Future sequence needs to be constructed with the help of the SequenceHelper functions. - * @return - * the Handle, to observe and to interact with the sequence. - * - The completion future will only complete as soon the sequence and all registered release - * functionality finished as well - * - The Handle is available immediately - */ - def executeSequence(sequence: SequenceHelper => Future[?]): Handle -} - -/** A collection of helper functions to compose a preemptable-sequence - */ -trait SequenceHelper { - - /** Register at any point in time a synchronous release function, which will be ensured to run - * before the completion future of the handle completes. - * - * @param release - * the release lambda - */ - def registerRelease(release: => Unit): Unit - - /** Wrap a CBN (lazy) Future, so it is only started if the PreemptableSequence is not yet - * aborted/shut down. - * - * @param f - * The lazy Future - * @return - * the wrapped future - */ - def goF[T](f: => Future[T]): Future[T] - - /** Wrap a CBN (lazy) synchronous function in a Future, which is only started if the - * PreemptableSequence is not yet aborted/shut down. - * - * @param body - * The lazy synchronous body - * @return - * the wrapped future - */ - def go[T](body: => T): Future[T] - - /** Wrap a synchronous call into a Future sequence, which - * - will be preemptable - * - will retry to execute the body if Exception-s thrown, and the exception is retryable - * - * @return - * the preemptable, retrying Future sequence - */ - def retry[T]( - waitMillisBetweenRetries: Long, - maxAmountOfRetries: Long = -1, - retryable: Throwable => Boolean = _ => true, - )(body: => T): Future[T] - - /** Delegate the preemptable-future sequence to another Handle - * - the completion Future future of the PreemptableSequence will only finish after this Handle - * finishes, and previously registered release functions all completed - * - KillSwitch events will be replayed to this handle - * - In case of abort/shutdown the PreemptableSequence's completion result will conform to the - * KillSwitch usage, not to the completion of this handle (although it will wait for it - * naturally) - * - * @param handle - * The handle to delegate to - * @return - * the completion of the Handle - */ - def merge(handle: Handle): Future[Unit] - - /** The handle of the PreemptableSequence. This handle is available for sequence construction as - * well. - * @return - * the Handle - */ - def handle: Handle -} - -object PreemptableSequence { - - /** @param executionContext - * this execution context will be used to: - * - execute future transformations - * - and encapsulate synchronous work in futures (this could be possibly blocking) Because of - * the possible blocking nature a dedicated pool is recommended. - */ - def apply(timer: Timer, loggerFactory: NamedLoggerFactory)(implicit - executionContext: ExecutionContext, - traceContext: TraceContext, - ): PreemptableSequence = { sequence => - val logger = TracedLogger(loggerFactory.getLogger(getClass)) - val resultCompleted = Promise[Unit]() - val killSwitchCaptor = new KillSwitchCaptor(loggerFactory) - val resultHandle = Handle(resultCompleted.future, killSwitchCaptor) - @SuppressWarnings(Array("org.wartremover.warts.Var")) - var releaseStack: List[() => Future[Unit]] = Nil - val lock = new Mutex() - - val helper: SequenceHelper = new SequenceHelper { - private def waitFor(delayMillis: Long): Future[Unit] = { - val p = Promise[Unit]() - timer.schedule( - new TimerTask { - override def run(): Unit = p.success(()) - }, - delayMillis, - ) - goF(p.future) - } - - override def registerRelease(release: => Unit): Unit = (lock.exclusive { - logger.info(s"Registered release function") - releaseStack = (() => Future(release)) :: releaseStack - }) - - override def goF[T](f: => Future[T]): Future[T] = - killSwitchCaptor.state match { - case _: KillSwitchCaptor.State.Used => - // Failing Future here means we interrupt the Future sequencing. - // The failure itself is not important, since the returning Handle-s completion-future-s result is overridden in case KillSwitch was used. - logger.info(s"KillSwitch already used, interrupting sequence!") - Future.failed(new UsedKillSwitch) - - case _ => - f - } - - override def go[T](body: => T): Future[T] = goF[T](Future(body)) - - override def retry[T]( - waitMillisBetweenRetries: Long, - maxAmountOfRetries: Long = -1, - retryable: Throwable => Boolean = _ => true, - )(body: => T): Future[T] = - go(body).transformWith { - case Success(t) => Future.successful(t) - - case Failure(ex) if retryable(ex) => - // since we check countdown to 0, starting from negative means unlimited retries - if (maxAmountOfRetries == 0) { - logger.warn( - s"Maximum amount of retries reached ($maxAmountOfRetries). Failing permanently.", - ex, - ) - Future.failed(ex) - } else { - val retriesLeft = - if (maxAmountOfRetries < 0) "unlimited" - else maxAmountOfRetries - 1 - logger.debug(s"Retrying (retries left: $retriesLeft). Due to: ${ex.getMessage}") - waitFor(waitMillisBetweenRetries).flatMap(_ => - // Note: this recursion is out of stack - retry(waitMillisBetweenRetries, maxAmountOfRetries - 1, retryable)(body) - ) - } - - case Failure(ex: UsedKillSwitch) => - Future.failed(ex) - - case Failure(ex) => - logger.warn(s"Failure not retryable.", ex) - Future.failed(ex) - } - - override def merge(handle: Handle): Future[Unit] = { - logger.debug(s"Delegating KillSwitch upon merge.") - killSwitchCaptor.setDelegate(Some(handle.killSwitch)) - // for safety reasons. if between creation of that killSwitch and delegation there was a usage, we replay that after delegation (worst case multiple calls) - killSwitchCaptor.state match { - case KillSwitchCaptor.State.Shutdown => - logger.debug(s"Replying ShutDown after merge.") - handle.killSwitch.shutdown() - case KillSwitchCaptor.State.Aborted(ex) => - logger.debug(s"Replaying abort (${ex.getMessage}) after merge.") - handle.killSwitch.abort(ex) - case _ => () - } - handle.completed - .transform { r => - // not strictly needed for this use case, but in theory multiple preemptable stages are possible after each other - // this is needed to remove the delegation of the killSwitch after stage is complete - killSwitchCaptor.setDelegate(None) - r - } - } - - override def handle: Handle = resultHandle - } - - def release: Future[Unit] = - (lock.exclusive { - releaseStack match { - case Nil => None - case x :: xs => - releaseStack = xs - Some((x, xs)) - } - }) match { - case None => Future.unit - case Some((x, xs)) => x().transformWith(_ => release) - } - - sequence(helper).transformWith(fResult => release.transform(_ => fResult)).onComplete { - case Success(_) => - killSwitchCaptor.state match { - case KillSwitchCaptor.State.Shutdown => resultCompleted.success(()) - case KillSwitchCaptor.State.Aborted(ex) => resultCompleted.failure(ex) - case _ => resultCompleted.success(()) - } - case Failure(ex) => - killSwitchCaptor.state match { - case KillSwitchCaptor.State.Shutdown => resultCompleted.success(()) - case KillSwitchCaptor.State.Aborted(ex) => resultCompleted.failure(ex) - case _ => resultCompleted.failure(ex) - } - } - - resultHandle - } - - class UsedKillSwitch extends Exception("UsedKillSwitch") -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/package.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/package.scala deleted file mode 100644 index 875387580b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/package.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.digitalasset.canton.ledger.participant.state.Update -import com.digitalasset.canton.util.PekkoUtil.{Commit, FutureQueueConsumer} - -import scala.concurrent.Future - -package object indexer { - - /** Indexer is a factory for indexing. Future[Unit] is the completion Future, as it completes - * indexing is completed with results accordingly (Success/Failure) - */ - type Indexer = Boolean => Commit => Future[FutureQueueConsumer[Update]] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/AchsMaintenancePipe.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/AchsMaintenancePipe.scala deleted file mode 100644 index 93cca8e91b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/AchsMaintenancePipe.scala +++ /dev/null @@ -1,402 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.logging.entries.LoggingEntries -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, TracedLogger} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsAddActivationsParams, - AchsLastPointers, - AchsRemoveDeactivatedParams, - AchsState, -} -import com.digitalasset.canton.platform.store.backend.{EventStorageBackend, ParameterStorageBackend} -import com.digitalasset.canton.platform.store.cache.AchsStateCache -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Flow - -import java.sql.Connection -import scala.concurrent.{ExecutionContext, Future} - -/** A pipe that runs ACHS (Active Contracts Head Snapshot) maintenance. It consists of 5 stages: - * 1. Aggregate: convert incoming batches into AchsWorkDistance, accumulate until the work in - * either dimension breaches the aggregation threshold, then emit AchsWorkRange chunks - * 1. Bump ACHS validAt: update the ACHS validAt based on the max event sequential id seen - * 1. Populate: add activations to the ACHS - * 1. Remove: remove deactivated entries from the ACHS - * 1. Update last pointers: batch and update lastPopulated/lastRemoved in the ACHS state table - */ -object AchsMaintenancePipe { - - def apply[T]( - parameterStorageBackend: ParameterStorageBackend, - eventStorageBackend: EventStorageBackend, - dbDispatcher: DbDispatcher, - achsStateCache: AchsStateCache, - toAchsWorkDistance: T => AchsWorkDistance, - initialWork: AchsWorkDistance, - populationParallelism: Int, - removalParallelism: Int, - aggregationThreshold: Long, - metrics: LedgerApiServerMetrics, - executionContext: ExecutionContext, - logger: TracedLogger, - fullDrain: Boolean, - )(implicit traceContext: TraceContext): Flow[T, AchsWorkRange, NotUsed] = - maintenanceFlow( - toAchsWork = toAchsWorkDistance, - initialWork = initialWork, - bumpAchsValidAt = bumpAchsValidAt( - storeAchsValidAt = storeAchsState( - storeAchsStateFunction = parameterStorageBackend.updateACHSValidAt(_: Long), - dbDispatcher = dbDispatcher, - metrics = metrics, - logger = logger, - ), - achsStateCache = achsStateCache, - executionContext = executionContext, - logger = logger, - metrics = metrics, - ), - populateAchsActivations = populateAchsActivations( - persistActivationsF = persistChangesF( - persistChanges = eventStorageBackend.addActivationsToAchs, - dbDispatcher = dbDispatcher, - metrics = metrics, - ), - logger = logger, - executionContext = executionContext, - ), - removeDeactivatedFromAchs = removeDeactivatedFromAchs( - removeDeactivatedF = persistChangesF( - persistChanges = eventStorageBackend.removeDeactivatedFromAchs, - dbDispatcher = dbDispatcher, - metrics = metrics, - ), - executionContext = executionContext, - logger = logger, - ), - populationParallelism = populationParallelism, - removalParallelism = removalParallelism, - updateAchsLastPointers = storeAchsLastPointersF( - persistAchsLastPointersF = storeAchsState( - storeAchsStateFunction = parameterStorageBackend.updateACHSLastPointers, - dbDispatcher = dbDispatcher, - metrics = metrics, - logger = logger, - ), - achsStateCache = achsStateCache, - executionContext = executionContext, - logger = logger, - metrics = metrics, - ), - aggregationThreshold = aggregationThreshold, - initialAchsState = achsStateCache.get(), - fullDrain = fullDrain, - ) - - /** Computes the initial work distance for the ACHS maintenance pipe. This can be negative, - * representing a debt that must be absorbed by incoming batches before any real work starts. - */ - private[platform] def initialWork( - achsState: AchsState, - lastEventSeqId: Long, - achsConfig: AchsConfig, - ): AchsWorkDistance = { - val validAtDist = achsConfig.validAtDistanceTarget.unwrap - val populateDist = achsConfig.lastPopulatedDistanceTarget.unwrap - // How far the ledger head is from the ACHS pointers, accounting for the configured distances. - // Negative values mean the ledger hasn't advanced far enough for work to begin in that dimension. - AchsWorkDistance( - populate = - (lastEventSeqId - validAtDist - populateDist) - achsState.lastPointers.lastPopulated, - remove = (lastEventSeqId - validAtDist) - achsState.lastPointers.lastRemoved, - ) - } - - /** Represents the accumulated work to be done, measured in event sequential id deltas. */ - final case class AchsWorkDistance(populate: Long, remove: Long) extends PrettyPrinting { - def +(other: AchsWorkDistance): AchsWorkDistance = - AchsWorkDistance(populate = populate + other.populate, remove = remove + other.remove) - - def -(other: AchsWorkDistance): AchsWorkDistance = - AchsWorkDistance(populate = populate - other.populate, remove = remove - other.remove) - - /** For each dimension, returns threshold if value >= threshold, else 0. This ensures we only - * emit work for a dimension that has accumulated enough. - */ - def cap(threshold: Long): AchsWorkDistance = - AchsWorkDistance( - populate = if (populate >= threshold) threshold else 0L, - remove = if (remove >= threshold) threshold else 0L, - ) - - override protected def pretty: Pretty[AchsWorkDistance] = prettyOfClass( - param("populate", _.populate), - param("remove", _.remove), - ) - } - - /** Represents a range of event sequential ids. */ - final case class EventSeqIdRange( - startExclusive: Long, - endInclusive: Long, - ) - - /** Represents a concrete range of event sequential ids for population and removal. */ - final case class AchsWorkRange( - activationsPopulation: EventSeqIdRange, - deactivatedRemoval: EventSeqIdRange, - ) - - private[platform] def maintenanceFlow[T]( - toAchsWork: T => AchsWorkDistance, - bumpAchsValidAt: AchsWorkRange => Future[AchsWorkRange], - populateAchsActivations: AchsWorkRange => Future[AchsWorkRange], - removeDeactivatedFromAchs: AchsWorkRange => Future[AchsWorkRange], - populationParallelism: Int, - removalParallelism: Int, - updateAchsLastPointers: AchsWorkRange => Future[AchsWorkRange], - aggregationThreshold: Long, - initialAchsState: AchsState, - initialWork: AchsWorkDistance, - fullDrain: Boolean, - ): Flow[T, AchsWorkRange, NotUsed] = - Flow[T] - // Stage 0: Convert to work distance, accumulate, and drain threshold-sized AchsWorkRange chunks. - .map(toAchsWork) - .statefulMap(create = () => initialAchsState -> initialWork)( - f = { case ((achsState, accWorkDistance), incomingWorkDistance) => - val totalWork = accWorkDistance + incomingWorkDistance - - // repeatedly drain threshold-sized chunks while the threshold is breached - val (newState, remainingWork, workRanges) = drain( - aggregationThreshold = aggregationThreshold, - state = achsState, - remaining = totalWork, - acc = Vector.empty, - fullDrain = fullDrain, - ) - (newState, remainingWork) -> workRanges - }, - onComplete = _ => None, - ) - .mapConcat(identity) - // Stage 1: Bump the ACHS validAt - .mapAsync(1)(bumpAchsValidAt) - // Stage 2: Populate ACHS with activations - .async - .mapAsync(populationParallelism)(populateAchsActivations) - // Stage 3: Remove deactivated entries from the ACHS - .async - .mapAsync(removalParallelism)(removeDeactivatedFromAchs) - // Stage 4: Batch and update ACHS last pointers (lastPopulated, lastRemoved) - // keep the latest maintenance params - .conflate((_, latest) => latest) - .mapAsync(1)(updateAchsLastPointers) - - private def applyWork( - state: AchsState, - work: AchsWorkDistance, - ): (AchsWorkRange, AchsState) = { - val workRange = AchsWorkRange( - activationsPopulation = EventSeqIdRange( - startExclusive = state.lastPointers.lastPopulated, - endInclusive = state.lastPointers.lastPopulated + work.populate, - ), - deactivatedRemoval = EventSeqIdRange( - startExclusive = state.lastPointers.lastRemoved, - endInclusive = state.lastPointers.lastRemoved + work.remove, - ), - ) - val newState = AchsState( - validAt = state.validAt, - lastPointers = AchsLastPointers( - lastPopulated = state.lastPointers.lastPopulated + work.populate, - lastRemoved = state.lastPointers.lastRemoved + work.remove, - ), - ) - (workRange, newState) - } - - @scala.annotation.tailrec - private[platform] def drain( - aggregationThreshold: Long, - state: AchsState, - remaining: AchsWorkDistance, - acc: Vector[AchsWorkRange], - fullDrain: Boolean, - ): (AchsState, AchsWorkDistance, Vector[AchsWorkRange]) = - if (remaining.populate >= aggregationThreshold || remaining.remove >= aggregationThreshold) { - val chunk = remaining.cap(aggregationThreshold) - val (workRange, newState) = applyWork(state, chunk) - drain( - aggregationThreshold = aggregationThreshold, - state = newState, - remaining = remaining - chunk, - acc = acc :+ workRange, - fullDrain = fullDrain, - ) - } else if (fullDrain && (remaining.populate > 0L || remaining.remove > 0L)) { - // Flush any positive sub-threshold remainder as a final undersized chunk. - // Negative dimensions are clamped to 0 (they represent debt, not real work). - val clamped = AchsWorkDistance( - populate = remaining.populate.max(0L), - remove = remaining.remove.max(0L), - ) - val (workRange, newState) = applyWork(state, clamped) - (newState, remaining - clamped, acc :+ workRange) - } else { - (state, remaining, acc) - } - - /** Bumps the ACHS validAt based on the maximum event sequential id seen (newValidAt = - * deactivatedRemoval.endInclusive). - */ - private[platform] def bumpAchsValidAt( - storeAchsValidAt: Long => Future[Unit], - achsStateCache: AchsStateCache, - executionContext: ExecutionContext, - logger: TracedLogger, - metrics: LedgerApiServerMetrics, - )(workRange: AchsWorkRange)(implicit traceContext: TraceContext): Future[AchsWorkRange] = { - val newValidAt = workRange.deactivatedRemoval.endInclusive.max(0L) - val currentValidAt = achsStateCache.get().validAt - - if (currentValidAt >= newValidAt) { - logger.trace( - s"Not bumping ACHS validAt as the new validAt $newValidAt is not greater than the current validAt $currentValidAt." - ) - Future.successful(workRange) - } else { - logger.debug(s"Bumping ACHS validAt from $currentValidAt to $newValidAt.") - // the in-memory state is used to determine whether the ACHS is valid to fetch from it, so it must be updated before persisting to the database - achsStateCache - .updateValidAt(newValidAt) - metrics.indexer.achsValidAt.updateValue(newValidAt) - storeAchsValidAt(newValidAt) - .map(_ => workRange)(executionContext) - } - } - - /** Adds activations to the ACHS. */ - private[platform] def populateAchsActivations( - persistActivationsF: AchsAddActivationsParams => LoggingContextWithTrace => Future[Unit], - logger: TracedLogger, - executionContext: ExecutionContext, - )(workRange: AchsWorkRange)(implicit traceContext: TraceContext): Future[AchsWorkRange] = { - val loggingContextWithTrace: LoggingContextWithTrace = - new LoggingContextWithTrace(LoggingEntries.empty, traceContext) - - val endInclusive = workRange.activationsPopulation.endInclusive - val startExclusive = workRange.activationsPopulation.startExclusive - // the populations should be active at the latest validAt which is the end of the deactivated removal range - val activeAt = workRange.deactivatedRemoval.endInclusive - - if (endInclusive > startExclusive) { - logger.debug( - s"Adding activations to ACHS in range ($startExclusive, $endInclusive] active at $activeAt." - ) - persistActivationsF( - AchsAddActivationsParams( - startExclusive = startExclusive, - endInclusive = endInclusive, - activeAt = activeAt, - ) - )(loggingContextWithTrace) - } else Future.unit - }.map(_ => workRange)(executionContext) - - /** Removes deactivated entries from the ACHS. It must run after the parallel population stage to - * guarantee that activations from prior batches have been added to the ACHS before potentially - * removing them. - */ - private[platform] def removeDeactivatedFromAchs( - removeDeactivatedF: AchsRemoveDeactivatedParams => LoggingContextWithTrace => Future[Unit], - executionContext: ExecutionContext, - logger: TracedLogger, - )(workRange: AchsWorkRange)(implicit traceContext: TraceContext): Future[AchsWorkRange] = { - val endInclusive = workRange.deactivatedRemoval.endInclusive - val startExclusive = workRange.deactivatedRemoval.startExclusive - val populationEnd = workRange.activationsPopulation.endInclusive - - if (populationEnd <= 0L) { - logger.debug( - s"Skipping ACHS removal as no population has been assigned up to this point (populationEnd=$populationEnd, removalStart=$startExclusive, removalEnd=$endInclusive)." - ) - Future.unit - } else if (endInclusive > startExclusive) { - val loggingContextWithTrace: LoggingContextWithTrace = - new LoggingContextWithTrace(LoggingEntries.empty, traceContext) - logger.debug( - s"Removing deactivated entries from ACHS in range ($startExclusive, $endInclusive]." - ) - removeDeactivatedF( - AchsRemoveDeactivatedParams( - startExclusive = startExclusive, - endInclusive = endInclusive, - ) - )(loggingContextWithTrace) - } else Future.unit - }.map(_ => workRange)(executionContext) - - /** Persists the ACHS lastPopulated and lastRemoved pointers and updates the in-memory cache. */ - private[platform] def storeAchsLastPointersF( - persistAchsLastPointersF: AchsLastPointers => Future[Unit], - achsStateCache: AchsStateCache, - executionContext: ExecutionContext, - logger: TracedLogger, - metrics: LedgerApiServerMetrics, - )(workRange: AchsWorkRange)(implicit traceContext: TraceContext): Future[AchsWorkRange] = { - val lastPopulated = workRange.activationsPopulation.endInclusive - val lastRemoved = workRange.deactivatedRemoval.endInclusive - - if (lastRemoved > 0L) { - val lastPointers = AchsLastPointers(lastRemoved = lastRemoved, lastPopulated = lastPopulated) - // the in-memory state is used to determine whether the ACHS is valid to fetch from it, so it must be updated before persisting to the database - achsStateCache.updateLastPointers(lastPointers) - metrics.indexer.achsLastPopulated.updateValue(lastPopulated) - metrics.indexer.achsLastRemoved.updateValue(lastRemoved) - persistAchsLastPointersF(lastPointers) - .map { _ => - logger.debug( - s"Updated ACHS last pointers: lastRemoved=$lastRemoved, lastPopulated=$lastPopulated." - ) - workRange - }(executionContext) - } else { - logger.debug( - s"Skipping ACHS last pointers update as the new lastRemoved and lastPopulated are not greater than 0." - ) - Future.successful(workRange) - } - } - - private def persistChangesF[T]( - persistChanges: T => Connection => Unit, - dbDispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - )(params: T)(loggingContext: LoggingContextWithTrace): Future[Unit] = - dbDispatcher.executeSql(metrics.indexer.achsProcessing) { connection => - persistChanges(params)(connection) - }(loggingContext) - - private def storeAchsState[T]( - storeAchsStateFunction: T => Connection => Unit, - dbDispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - logger: TracedLogger, - )(changes: T)(implicit traceContext: TraceContext): Future[Unit] = - LoggingContextWithTrace.withNewLoggingContext() { implicit loggingContext => - dbDispatcher.executeSql(metrics.indexer.achsProcessing) { connection => - storeAchsStateFunction(changes)(connection) - logger.debug(s"Changed ACHS state to $changes.")(traceContext) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/AsyncSupport.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/AsyncSupport.scala deleted file mode 100644 index a2cd775276..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/AsyncSupport.scala +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.executors.InstrumentedExecutors -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.tracing.TraceContext -import com.google.common.util.concurrent.ThreadFactoryBuilder - -import scala.concurrent.{ExecutionContext, Future} - -object AsyncSupport { - - trait Executor { - def execute[Fin, Fout](f: Fin => Fout): Fin => Future[Fout] - } - - object Executor { - def forExecutionContext(executionContext: ExecutionContext): Executor = - new Executor { - override def execute[Fin, Fout](f: Fin => Fout): Fin => Future[Fout] = - in => Future(f(in))(executionContext) - } - } - - def asyncPool( - size: Int, - namePrefix: String, - executorName: String, - loggerFactory: NamedLoggerFactory, - )(implicit traceContext: TraceContext): ResourceOwner[Executor] = { - val logger = loggerFactory.getTracedLogger(getClass) - ResourceOwner - .forExecutorService(() => - InstrumentedExecutors.newFixedThreadPoolWithFactory( - executorName, - size, - new ThreadFactoryBuilder() - .setNameFormat(s"$namePrefix-%d") - .build, - throwable => - logger - .error(s"ExecutionContext $namePrefix has failed with an exception", throwable), - ) - ) - .map(Executor.forExecutionContext) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/BatchingParallelIngestionPipe.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/BatchingParallelIngestionPipe.scala deleted file mode 100644 index 6f4f23ad21..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/BatchingParallelIngestionPipe.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Flow - -import scala.concurrent.Future - -object BatchingParallelIngestionPipe { - - def apply[In, InBatch, DbBatch]( - batchingFlow: Flow[In, Iterable[In], NotUsed], - inputMappingParallelism: Int, - contractReInsertion: Iterable[In] => Future[Iterable[In]], - inputMapper: Iterable[In] => Future[InBatch], - seqMapperZero: InBatch, - seqMapper: (InBatch, InBatch) => InBatch, - dbPrepareParallelism: Int, - dbPrepare: InBatch => Future[InBatch], - batchingParallelism: Int, - batcher: InBatch => Future[DbBatch], - ingestingParallelism: Int, - ingester: DbBatch => Future[DbBatch], - maxTailerBatchSize: Int, - ingestTail: Vector[DbBatch] => Future[Vector[DbBatch]], - ): Flow[In, DbBatch, NotUsed] = - // The stream coming from ReadService, involves deserialization and translation to Update-s - Flow[In] - // Batching plus mapping to Database DTOs encapsulates all the CPU intensive computation of the ingestion. Executed in parallel. - .via(batchingFlow) - .mapAsync(inputMappingParallelism)(contractReInsertion) - .mapAsync(inputMappingParallelism)(inputMapper) - // Encapsulates sequential/stateful computation (generation of sequential IDs for events) - .scan(seqMapperZero)(seqMapper) - .drop(1) // remove the zero element from the beginning of the stream - .async - .mapAsync(dbPrepareParallelism)(dbPrepare) - // Mapping to Database specific representation, encapsulates all database specific preparation of the data. Executed in parallel. - .async - .mapAsync(batchingParallelism)(batcher) - // Inserting data into the database. Almost no CPU load here, threads are executing SQL commands over JDBC, and waiting for the result. This defines the parallelism on the SQL database side, same amount of PostgreSQL Backend processes will do the ingestion work. - .async - .mapAsync(ingestingParallelism)(ingester) - // Batching data for throttled ledger-end update in database - .batch(maxTailerBatchSize.toLong, Vector(_))(_ :+ _) - // Updating ledger-end and related data in database (this stage completion demarcates the consistent point-in-time) - .mapAsync(1)(ingestTail) - .mapConcat(identity) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/EventMetricsUpdater.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/EventMetricsUpdater.scala deleted file mode 100644 index f156b8e868..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/EventMetricsUpdater.scala +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.metrics.api.MetricsContext.withExtraMetricLabels -import com.daml.metrics.api.{MetricHandle, MetricsContext} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.participant.state.Update -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted -import com.digitalasset.canton.metrics.IndexerMetrics - -object EventMetricsUpdater { - def apply( - meteredEventsMeter: MetricHandle.Meter - )(implicit - mc: MetricsContext - ): Iterable[(Offset, Update)] => Unit = input => { - - if (input.nonEmpty) { - - (for { - (completionInfo, transactionAccepted) <- input.iterator - .collect { case (_, ta: TransactionAccepted) => ta } - .flatMap(ta => ta.completionInfoO.iterator.map(_ -> ta)) - userId = completionInfo.userId - statistics = transactionAccepted.transactionInfo.statistics - } yield (userId, statistics.committed.actions + statistics.rolledBack.actions)).toList - .groupMapReduce(_._1)(_._2)(_ + _) - .toList - .filter(_._2 != 0) - .sortBy(_._1) - .foreach { case (userId, count) => - withExtraMetricLabels(IndexerMetrics.Labels.userId -> userId) { implicit mc => - meteredEventsMeter.mark(count.toLong) - } - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/InitializeParallelIngestion.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/InitializeParallelIngestion.scala deleted file mode 100644 index 3433ff20f5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/InitializeParallelIngestion.scala +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.discard.Implicits.* -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.indexer.parallel.AchsMaintenancePipe.{ - AchsWorkDistance, - AchsWorkRange, -} -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsLastPointers, - AchsState, - LedgerEnd, -} -import com.digitalasset.canton.platform.store.backend.{ - CompletionStorageBackend, - EventStorageBackend, - IngestionStorageBackend, - ParameterStorageBackend, - StringInterningStorageBackend, -} -import com.digitalasset.canton.platform.store.cache.AchsStateCache -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.platform.store.interning.UpdatingStringInterningView -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import org.apache.pekko.NotUsed -import org.apache.pekko.actor.Cancellable -import org.apache.pekko.stream.scaladsl.{Keep, Sink, Source} -import org.apache.pekko.stream.{KillSwitches, Materializer, UniqueKillSwitch} - -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.duration.* -import scala.concurrent.{ExecutionContext, Future} - -private[platform] final case class InitializeParallelIngestion( - providedParticipantId: Ref.ParticipantId, - ingestionStorageBackend: IngestionStorageBackend[?], - parameterStorageBackend: ParameterStorageBackend, - eventStorageBackend: EventStorageBackend, - completionStorageBackend: CompletionStorageBackend, - stringInterningStorageBackend: StringInterningStorageBackend, - updatingStringInterningView: UpdatingStringInterningView, - postProcessor: (Vector[PostPublishData], TraceContext) => Future[Unit], - achsStateCache: AchsStateCache, - achsConfig: Option[AchsConfig], - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - achsInitInterceptor: Source[AchsWorkRange, NotUsed] => Source[AchsWorkRange, NotUsed], -)(implicit materializer: Materializer) - extends NamedLogging { - - /** Holds a reference to the kill switch for the in-progress ACHS initialization stream. When - * populated, invoking the kill switch will abort the ACHS maintenance pipe. - */ - private val achsKillSwitchRef: AtomicReference[Option[UniqueKillSwitch]] = - new AtomicReference(None) - - def achsKillSwitch(implicit traceContext: TraceContext): Option[() => Unit] = - achsConfig.map { _ => () => - achsKillSwitchRef.get() match { - case Some(ks) => - logger.info("Shutting down ACHS initialization stream via kill switch") - ks.shutdown() - case None => - logger.warn("Could not abort ACHS initialization since kill switch was not set") - } - } - - def apply( - dbDispatcher: DbDispatcher, - initializeInMemoryState: (Option[LedgerEnd], AchsState) => Future[Unit], - ): Future[(Option[LedgerEnd], AchsWorkDistance)] = { - implicit val ec: ExecutionContext = DirectExecutionContext(noTracingLogger) - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace.empty - logger.info(s"Attempting to initialize with participant ID $providedParticipantId") - for { - _ <- dbDispatcher.executeSql(metrics.index.db.initializeLedgerParameters)( - parameterStorageBackend.initializeParameters( - ParameterStorageBackend.IdentityParams( - participantId = ParticipantId(providedParticipantId) - ), - loggerFactory, - ) - ) - ledgerEnd <- dbDispatcher.executeSql(metrics.index.db.getLedgerEnd)( - parameterStorageBackend.ledgerEnd - ) - _ <- dbDispatcher.executeSql(metrics.indexer.initialization) { connection => - // addContractPruningCandidatesAfter must execute before deletePartiallyIngestedData - // addContractPruningCandidatesAfter and deletePartiallyIngestedData must be in one transaction - // so that it cannot be intersected with pruning contract candidate cleansing: that might remove candidates - // as they are referenced after the ledger-end in corner cases. - eventStorageBackend.addContractPruningCandidatesAfter( - eventSeqIdExclusive = ledgerEnd - .map(_.lastEventSeqId) - .getOrElse(-1L) // if no watermark we gather candidates from all events - )( - connection = connection, - traceContext = loggingContext.traceContext, - ) - ingestionStorageBackend.deletePartiallyIngestedData(ledgerEnd)(connection) - } - (postAchsState, postAchsWork) <- initializeAchs( - achsConfig = achsConfig, - lastEventSeqId = ledgerEnd.map(_.lastEventSeqId).getOrElse(0L), - dbDispatcher = dbDispatcher, - ) - _ <- updatingStringInterningView.update(ledgerEnd.map(_.lastStringInterningId)) { - (fromExclusive, toInclusive) => - implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace.empty - dbDispatcher.executeSql(metrics.index.db.loadStringInterningEntries) { - stringInterningStorageBackend.loadStringInterningEntries( - fromExclusive, - toInclusive, - ) - } - } - // post processing recovery should come after initializing string interning when the dependent storage backend operations are running - postProcessingEndOffset <- dbDispatcher.executeSql(metrics.index.db.getPostProcessingEnd)( - parameterStorageBackend.postProcessingEnd - ) - potentiallyNonPostProcessedCompletions <- ledgerEnd.map(_.lastOffset) match { - case Some(lastOffset) => - dbDispatcher.executeSql( - metrics.index.db.getPostProcessingEnd - )( - completionStorageBackend.commandCompletionsForRecovery( - startInclusive = postProcessingEndOffset.fold(Offset.firstOffset)(_.increment), - endInclusive = lastOffset, - ) - ) - case None => Future.successful(Vector.empty) - } - _ <- postProcessor(potentiallyNonPostProcessedCompletions, loggingContext.traceContext) - _ <- dbDispatcher.executeSql(metrics.indexer.postProcessingEndIngestion)( - parameterStorageBackend.updatePostProcessingEnd(ledgerEnd.map(_.lastOffset)) - ) - _ = logger.info(s"Indexer initialized at $ledgerEnd") - _ <- initializeInMemoryState(ledgerEnd, postAchsState) - } yield (ledgerEnd, postAchsWork) - } - - /** Initializes ACHS state and eagerly creates the ACHS snapshot using AchsMaintenancePipe. - * - * If ACHS is enabled, it checks for an existing snapshot of ACHS state in the database: - * - If no snapshot exists, it creates a new snapshot. - * - If a snapshot exists and lags behind, it uses the existing state to build the ACHS. - * - If a snapshot exists and is ahead, it does not alter the existing state and expects the - * ACHS to catch up when new events will be indexed (work distance will be negative, - * representing debt). - * - * If ACHS is disabled, it clears any existing ACHS data from the database. - */ - private def initializeAchs( - achsConfig: Option[AchsConfig], - lastEventSeqId: Long, - dbDispatcher: DbDispatcher, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[(AchsState, AchsWorkDistance)] = - achsConfig match { - case Some(config) => - dbDispatcher - .executeSql(metrics.indexer.initialization) { connection => - val achsState = parameterStorageBackend.fetchACHSState(connection) match { - case None => - logger.info(s"ACHS not found, creating new one.") - val freshState = - ParameterStorageBackend.AchsState( - validAt = 0, - lastPointers = AchsLastPointers(lastRemoved = 0, lastPopulated = 0), - ) - parameterStorageBackend.insertACHSState(freshState)(connection) - freshState - case Some(existingState) => - // snapshot exists (behind or ahead), use existing state. - // initialWork will compute the correct work distance: - // - positive values for catch-up (behind) - // - negative values as debt (ahead, will be absorbed by incoming batches) - logger.info( - s"ACHS resuming from existing state: $existingState" - ) - existingState - } - achsState -> AchsMaintenancePipe.initialWork(achsState, lastEventSeqId, config) - } - .flatMap { case (initialState, initialWork) => - createAchsSnapshot( - initialState = initialState, - initialWork = initialWork, - lastEventSeqId = lastEventSeqId, - config = config, - dbDispatcher = dbDispatcher, - ) - } - case None => - // Clearing ACHS data here is safe because configuration is not changing dynamically. - // Otherwise, clearing could race with ACS retrieval that relies on ACHS data, - // as pointers would be updated after the data is already evicted. - logger.info("ACHS is disabled, clearing existing ACHS data") - dbDispatcher - .executeSql(metrics.indexer.initialization) { connection => - parameterStorageBackend.clearAchsData(connection) - } - .map { _ => - // not used when ACHS is disabled - AchsState( - validAt = 0L, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) -> - AchsWorkDistance(populate = 0L, remove = 0L) - } - } - - /** Eagerly creates the ACHS snapshot by running AchsMaintenancePipe in two phases: - * - * - Phase 1 (Removal): Removes deactivated entries and bumps validAt. No population is done. - * - Phase 2 (Copy): Copies over activations using the updated validAt from Phase 1. - * - * By splitting into two phases, the copy phase only adds entries that are still active after all - * removals and validAt located at its final position. - * - * The pipe runs in fullDrain mode, flushing any sub-threshold remainder when the finite init - * stream completes, so all positive initial work is fully processed. After both phases complete, - * the remaining work distance is recalculated from the updated in-memory ACHS state, which - * correctly accounts for debt (negative work when ACHS is ahead). - */ - private def createAchsSnapshot( - initialState: AchsState, - initialWork: AchsWorkDistance, - lastEventSeqId: Long, - config: AchsConfig, - dbDispatcher: DbDispatcher, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[(AchsState, AchsWorkDistance)] = { - // initialize the in-memory ACHS state here as well to ensure it is available for the snapshot creation step - achsStateCache.set(initialState) - logger.info(s"Initializing ACHS snapshot with initial work distance: $initialWork") - - val lastPointers = new AtomicReference[AchsLastPointers](initialState.lastPointers) - val cancellable = - logProgress[AchsLastPointers]( - action = "ACHS initialization", - targetState = AchsLastPointers( - lastPopulated = initialState.lastPointers.lastPopulated + - initialWork.populate.max(0L), - lastRemoved = initialState.lastPointers.lastRemoved + - initialWork.remove.max(0L), - ), - state = lastPointers, - calcProgress = (from, to) => - (to.lastPopulated - from.lastPopulated) + (to.lastRemoved - from.lastRemoved), - ) - - achsInitInterceptor( - achsMaintenancePipeSource( - initialWork = initialWork, - dbDispatcher = dbDispatcher, - achsConfig = config, - ) - ) - .map { elem => - lastPointers.set( - AchsLastPointers( - lastPopulated = elem.activationsPopulation.endInclusive, - lastRemoved = elem.deactivatedRemoval.endInclusive, - ) - ) - elem - } - .viaMat(KillSwitches.single)(Keep.right) - .toMat(Sink.ignore)(Keep.both) - .run() match { - case (killSwitch, doneFuture) => - achsKillSwitchRef.set(Some(killSwitch)) - doneFuture.map { _ => - achsKillSwitchRef.set(None) - cancellable.cancel().discard - // After both phases, the in-memory ACHS state reflects the actual pointers - // advanced by the pipe (including the flushed sub-threshold remainder via fullDrain). - // Recalculate remaining work from the updated state to correctly account for - // debt (negative work that couldn't be consumed). - val updatedAchsState = achsStateCache.get() - val remainingWork = - AchsMaintenancePipe.initialWork(updatedAchsState, lastEventSeqId, config) - logger.info( - s"ACHS snapshot initialization finished. Initial work: $initialWork, remaining work: $remainingWork (updated ACHS state: $updatedAchsState)" - ) - updatedAchsState -> remainingWork - } - } - } - - private def logProgress[S]( - action: String, - targetState: S, - state: AtomicReference[S], - calcProgress: (S, S) => Long, - interval: FiniteDuration = 10.seconds, - )(implicit loggingContext: LoggingContextWithTrace): Cancellable = { - val startTime = System.currentTimeMillis() - val initialState = state.get() - val totalDist = calcProgress(initialState, targetState) - val prevReportedState = new AtomicReference[S](initialState) - - materializer.system.scheduler.scheduleWithFixedDelay( - initialDelay = interval, - delay = interval, - ) { () => - val currentState = state.get() - val prevState = prevReportedState.getAndSet(currentState) - val delta = calcProgress(prevState, currentState) - val currentDist = calcProgress(initialState, currentState) - val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000 - val intervalSeconds = interval.toSeconds.max(1) - val reportRate = delta / intervalSeconds - val avgRate = if (elapsedSeconds > 0) currentDist / elapsedSeconds else 0L - val percentage = if (totalDist > 0) s"${100 * currentDist / totalDist}%" else "N/A" - val minutesLeft = - if (avgRate > 0 && totalDist > 0) s"${(totalDist - currentDist) / avgRate / 60}" - else "N/A" - logger.info( - s"$action current: $currentState, target: $targetState $currentDist/$totalDist events processed, $percentage, (since last: $delta, $reportRate events/s) (avg: $avgRate events/s, estimated minutes left: $minutesLeft)" - ) - }(materializer.executionContext) - } - - private def achsMaintenancePipeSource( - initialWork: AchsWorkDistance, - dbDispatcher: DbDispatcher, - achsConfig: AchsConfig, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Source[AchsWorkRange, NotUsed] = { - // Split initialWork into two phases: - // Phase 1 (Removal): removes deactivated entries and bumps validAt, no population. - // Phase 2 (Copy): copies over activations using the updated validAt from Phase 1. - val removalOnlyWork = AchsWorkDistance(populate = 0, remove = initialWork.remove) - val copyOnlyWork = AchsWorkDistance(populate = initialWork.populate, remove = 0) - Source(List(removalOnlyWork, copyOnlyWork)) - .via( - AchsMaintenancePipe( - parameterStorageBackend = parameterStorageBackend, - eventStorageBackend = eventStorageBackend, - dbDispatcher = dbDispatcher, - achsStateCache = achsStateCache, - toAchsWorkDistance = identity[AchsWorkDistance], - initialWork = AchsWorkDistance(populate = 0, remove = 0), - populationParallelism = achsConfig.initParallelism.unwrap, - removalParallelism = achsConfig.initParallelism.unwrap, - aggregationThreshold = achsConfig.initAggregationThreshold, - metrics = metrics, - executionContext = ec, - logger = logger, - fullDrain = true, - ) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerFactory.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerFactory.scala deleted file mode 100644 index b5f8986e37..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerFactory.scala +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.executors.InstrumentedExecutors -import com.daml.ledger.resources.{ResourceContext, ResourceOwner} -import com.digitalasset.canton.discard.Implicits.* -import com.digitalasset.canton.ledger.participant.state.Update -import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.ResourceOwnerOps -import com.digitalasset.canton.platform.config.ServerRole -import com.digitalasset.canton.platform.indexer.Indexer -import com.digitalasset.canton.platform.indexer.ha.{ - HaConfig, - HaCoordinator, - Handle, - NoopHaCoordinator, -} -import com.digitalasset.canton.platform.indexer.parallel.AsyncSupport.* -import com.digitalasset.canton.platform.store.DbSupport.DbConfig -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{AchsState, LedgerEnd} -import com.digitalasset.canton.platform.store.backend.{ - DBLockStorageBackend, - DataSourceStorageBackend, -} -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.time.Clock -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.PekkoUtil.{Commit, FutureQueueConsumer} -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.google.common.util.concurrent.ThreadFactoryBuilder -import org.apache.pekko.Done -import org.apache.pekko.stream.{KillSwitch, Materializer} - -import java.util.{Timer, concurrent} -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Success} - -object ParallelIndexerFactory { - - def apply( - inputMappingParallelism: Int, - batchingParallelism: Int, - dbConfig: DbConfig, - haConfig: HaConfig, - metrics: LedgerApiServerMetrics, - dbLockStorageBackend: DBLockStorageBackend, - dataSourceStorageBackend: DataSourceStorageBackend, - initializeParallelIngestion: InitializeParallelIngestion, - parallelIndexerSubscription: ParallelIndexerSubscription[?], - mat: Materializer, - executionContext: ExecutionContext, - initializeInMemoryState: (Option[LedgerEnd], AchsState) => Future[Unit], - loggerFactory: NamedLoggerFactory, - indexerDbDispatcherOverride: Option[DbDispatcher], - clock: Clock, - )(implicit traceContext: TraceContext): ResourceOwner[Indexer] = { - val logger = TracedLogger(loggerFactory.getLogger(getClass)) - for { - inputMapperExecutor <- asyncPool( - inputMappingParallelism, - "input-mapping-pool", - metrics.indexer.inputMapping.executor, - loggerFactory, - ).afterReleased(logger.debug("Input Mapping Threadpool released")) - batcherExecutor <- asyncPool( - batchingParallelism, - "batching-pool", - metrics.indexer.batching.executor, - loggerFactory, - ).afterReleased(logger.debug("Batching Threadpool released")) - haCoordinator <- - if (dbLockStorageBackend.dbLockSupported) { - for { - executionContext <- ResourceOwner - .forExecutorService(() => - ExecutionContext.fromExecutorService( - InstrumentedExecutors.newFixedThreadPoolWithFactory( - "ha-coordinator", - 1, - new ThreadFactoryBuilder().setNameFormat("ha-coordinator-%d").build, - throwable => - logger - .error( - "ExecutionContext has failed with an exception", - throwable, - ), - ) - ) - ) - .afterReleased(logger.debug("HaCoordinator single-threadpool released")) - timer <- ResourceOwner - .forTimer(() => new Timer) - .afterReleased(logger.debug("HaCoordinator Timer released")) - - // this DataSource will be used to spawn the main connection where we keep the Indexer Main Lock - // The life-cycle of such connections matches the life-cycle of a protectedExecution - dataSource = dataSourceStorageBackend.createDataSource( - dbConfig.dataSourceConfig, - loggerFactory, - ) - } yield HaCoordinator.databaseLockBasedHaCoordinator( - mainConnectionFactory = () => { - val connection = dataSource.getConnection - val directExecutor = new concurrent.Executor { - override def execute(command: Runnable): Unit = - // this will execute on the same thread which started the Executor.execute() - command.run() - } - // direct executor is beneficial in context of main connection and network timeout: - // all socket/Connection closure will be happening on the thread which called the JDBC execute, - // instead of happening asynchronously - after error with network timeout the Connection - // needs to be closed anyway. - connection.setNetworkTimeout( - directExecutor, - haConfig.mainLockCheckerJdbcNetworkTimeout.duration.toMillis.toInt, - ) - connection - }, - storageBackend = dbLockStorageBackend, - executionContext = executionContext, - timer = timer, - haConfig = haConfig, - loggerFactory, - ) - } else - ResourceOwner.successful(NoopHaCoordinator) - } yield { (repairMode: Boolean) => (commit: Commit) => - implicit val ec: ExecutionContext = executionContext - implicit val rc: ResourceContext = ResourceContext(ec) - val futureQueueConsumerFactoryPromise = - Promise[Future[Done] => FutureQueueConsumer[Update]]() - val haProtectedExecutionHandle = haCoordinator - .protectedExecution { connectionInitializer => - val indexingHandleF = initializeHandle( - for { - dbDispatcher <- indexerDbDispatcherOverride - .map(ResourceOwner.successful) - .getOrElse( - DbDispatcher - .owner( - // this is the DataSource which will be wrapped by HikariCP, and which will drive the ingestion - // therefore this needs to be configured with the connection-init-hook, what we get from HaCoordinator - dataSource = dataSourceStorageBackend.createDataSource( - dataSourceConfig = dbConfig.dataSourceConfig, - connectionInitHook = Some(connectionInitializer.initialize), - loggerFactory = loggerFactory, - ), - serverRole = ServerRole.Indexer, - connectionPoolSize = dbConfig.connectionPool.connectionPoolSize, - connectionTimeout = dbConfig.connectionPool.connectionTimeout, - metrics = metrics, - loggerFactory = loggerFactory, - ) - .afterReleased(logger.debug("Indexing DbDispatcher released")) - ) - } yield dbDispatcher - ) { dbDispatcher => - for { - (initialLedgerEnd, initialAchsWork) <- initializeParallelIngestion( - dbDispatcher = dbDispatcher, - initializeInMemoryState = initializeInMemoryState, - ) - (handle, futureQueueForCompletion) = parallelIndexerSubscription( - inputMapperExecutor = inputMapperExecutor, - batcherExecutor = batcherExecutor, - dbDispatcher = dbDispatcher, - materializer = mat, - initialLedgerEnd = initialLedgerEnd, - initialAchsWork = initialAchsWork, - commit = commit, - clock = clock, - repairMode = repairMode, - ) - } yield { - futureQueueConsumerFactoryPromise.success(completion => - FutureQueueConsumer( - futureQueue = futureQueueForCompletion(completion), - fromExclusive = initialLedgerEnd.map(_.lastOffset.unwrap).getOrElse(0L), - ) - ) - handle - } - } - indexingHandleF.onComplete { - case Success(indexingHandle) => - logger.info("Indexer initialized, indexing started.") - // in this case futureQueueConsumerPromise is already completed successfully (see above) - indexingHandle.completed.onComplete { - case Success(_) => - logger.info("Indexing finished.") - - case Failure(failure) => - logger.info(s"Indexing finished with failure: ${failure.getMessage}") - } - - case Failure(failure) => - logger.info(s"Indexer initialization failed: ${failure.getMessage}") - // in this case we entered the protected execution, but failed initialization, - // futureQueueConsumerPromise cannot be set from here to failure, since the HA protected surroundings - // need to be torn down first - } - indexingHandleF - } - - haProtectedExecutionHandle.completed - .onComplete { - case Success(_) => - // here the indexing finished successfully and everything torn down successfully too - // here we attempt to complete futureQueueConsumerPromise since if it is not completed yet, - // it would be a programming error - futureQueueConsumerFactoryPromise.tryFailure( - new IllegalStateException( - "Programming error: at this point the futureQueueConsumer should be already completed." - ) - ) - - case Failure(failure) => - // in either case of failures we try to complete the futureQueueConsumerPromise, - // but in the case indexing failed/aborted we already should have it completed, - // so this should succeed if failure arises during HA initialization or indexer initialization. - futureQueueConsumerFactoryPromise.tryFailure(failure) - } - // so that the resulting FutureQueue in the FutureQueueConsumer has a completion future, which completes after not only indexing, but after indexing resources and HA protected execution are both torn down - futureQueueConsumerFactoryPromise.future.map( - _(haProtectedExecutionHandle.completed.map(_ => Done)) - ) - } - } - - /** Helper function to combine a ResourceOwner and an initialization function to initialize a - * Handle. - * - * @param owner - * A ResourceOwner which needs to be used to spawn a resource needed by initHandle - * @param initHandle - * Asynchronous initialization function to create a Handle - * @return - * A Future of a Handle where Future encapsulates initialization (as completed initialization - * completed) - */ - def initializeHandle[T]( - owner: ResourceOwner[T] - )(initHandle: T => Future[Handle])(implicit rc: ResourceContext): Future[Handle] = { - implicit val ec: ExecutionContext = rc.executionContext - val killSwitchPromise = Promise[KillSwitch]() - val completed = owner - .use(resource => - initHandle(resource) - .thereafterP { - // the tricky bit: - // the future in the completion handler will be this one - // but the future for signaling completion of initialization (the Future of the result), needs to complete precisely here - case Success(handle) => killSwitchPromise.success(handle.killSwitch) - } - .flatMap(_.completed) - ) - .thereafterP { - // if error happens: - // - at Resource initialization (inside ResourceOwner.acquire()): result should complete with a Failure - // - at initHandle: result should complete with a Failure - // - at the execution spawned by initHandle (represented by the result Handle's complete): result should be with a success - // In the last case it has already finished the promise with a success, and this tryFailure will not succeed (returning false). - // In the other two cases the promise was not completed, and we complete here successfully with a failure. - case Failure(ex) => killSwitchPromise.tryFailure(ex).discard[Boolean] - } - killSwitchPromise.future - .map(Handle(completed, _)) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerSubscription.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerSubscription.scala deleted file mode 100644 index feb980498c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerSubscription.scala +++ /dev/null @@ -1,1296 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.logging.entries.LoggingEntries -import com.daml.metrics.InstrumentedGraph.* -import com.daml.metrics.Timed -import com.daml.metrics.api.MetricsContext -import com.daml.nonempty.NonEmpty -import com.daml.scalautil.Statement.discard -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.participant.state.Update.{ - CommitRepair, - ContractInfo, - EmptyAcsPublicationRequired, - LsuTimeReached, - ReassignmentAccepted, - SequencedCommandRejected, - SequencerIndexMoved, - TopologyTransactionEffective, - TransactionAccepted, - UnSequencedCommandRejected, -} -import com.digitalasset.canton.ledger.participant.state.{ - Reassignment, - SynchronizerIndex, - SynchronizerUpdate, - Update, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, - TracedLogger, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.platform.InMemoryState -import com.digitalasset.canton.platform.index.InMemoryStateUpdater -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.indexer.ha.Handle -import com.digitalasset.canton.platform.indexer.parallel.AchsMaintenancePipe.AchsWorkDistance -import com.digitalasset.canton.platform.indexer.parallel.AsyncSupport.* -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.backend.* -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.platform.store.dao.events.{CompressionStrategy, LfValueTranslation} -import com.digitalasset.canton.time.Clock -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.{Spanning, TraceContext} -import com.digitalasset.canton.util.PekkoUtil.{Commit, FutureQueue, PekkoSourceQueueToFutureQueue} -import com.digitalasset.canton.util.{BatchN, ErrorUtil} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.value.Value.ContractId -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.stream.scaladsl.{Flow, Keep, Sink, Source} -import org.apache.pekko.stream.{KillSwitches, Materializer, OverflowStrategy} -import org.apache.pekko.{Done, NotUsed} - -import java.sql.Connection -import java.util.concurrent.atomic.AtomicReference -import scala.collection.mutable -import scala.concurrent.{ExecutionContext, Future} -import scala.math.Ordered.orderingToOrdered -import scala.util.chaining.* - -private[platform] final case class ParallelIndexerSubscription[DbBatch]( - ingestionStorageBackend: IngestionStorageBackend[DbBatch], - parameterStorageBackend: ParameterStorageBackend, - contractStorageBackend: ContractStorageBackend, - eventStorageBackend: EventStorageBackend, - participantId: Ref.ParticipantId, - translation: LfValueTranslation, - compressionStrategy: CompressionStrategy, - maxInputBufferSize: Int, - inputMappingParallelism: Int, - dbPrepareParallelism: Int, - batchingParallelism: Int, - ingestionParallelism: Int, - useWeightedBatching: Boolean, - submissionBatchSize: Long, - submissionBatchInsertionSize: Long, - maxOutputBatchedBufferSize: Int, - maxTailerBatchSize: Int, - postProcessingParallelism: Int, - achsConfig: Option[AchsConfig], - metrics: LedgerApiServerMetrics, - inMemoryStateUpdaterFlow: InMemoryStateUpdater.UpdaterFlow, - inMemoryState: InMemoryState, - reassignmentOffsetPersistence: ReassignmentOffsetPersistence, - postProcessor: (Vector[PostPublishData], TraceContext) => Future[Unit], - sequentialPostProcessor: Update => Unit, - contractStore: LedgerApiContractStore, - disableMonotonicityChecks: Boolean, - tracer: Tracer, - executionContext: ExecutionContext, - loggerFactory: NamedLoggerFactory, -) extends NamedLogging - with Spanning { - import ParallelIndexerSubscription.* - - private def mapInSpan( - mapper: Offset => Update => Iterator[DbDto] - )(offset: Offset)(update: Update): Iterator[DbDto] = - withSpan("Indexer.mapInput")(_ => _ => mapper(offset)(update))(update.traceContext, tracer) - - def apply( - inputMapperExecutor: Executor, - batcherExecutor: Executor, - dbDispatcher: DbDispatcher, - materializer: Materializer, - initialLedgerEnd: Option[LedgerEnd], - initialAchsWork: AchsWorkDistance, - commit: Commit, - clock: Clock, - repairMode: Boolean, - )(implicit - traceContext: TraceContext - ): (Handle, Future[Done] => FutureQueue[(Long, Update)]) = { - import MetricsContext.Implicits.empty - // disable ACHS in repair mode, initialization of the parallel indexer should be responsible to maintain the ACHS after repair - val achsConfigEffective = Option.when(!repairMode)(achsConfig).flatten - val aggregatedLedgerEndForRepair - : AtomicReference[Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])]] = - // the LedgerEnd necessarily will be updated as successful repair has at least a CommitRepair Update, which carries the LedgerEnd forward - new AtomicReference(None) - val storeLedgerEndF = storeLedgerEnd( - parameterStorageBackend.updateLedgerEnd, - dbDispatcher, - metrics, - logger, - ) - val storePostProcessingEndF = storePostProcessingEnd( - offset => parameterStorageBackend.updatePostProcessingEnd(Some(offset)), - dbDispatcher, - metrics, - logger, - ) - val loadPreviousSynchronizerIndexF = (synchronizerId: SynchronizerId) => - dbDispatcher.executeSql(metrics.index.db.getCleanSynchronizerIndex)( - parameterStorageBackend.cleanSynchronizerIndex(synchronizerId) - )(LoggingContextWithTrace(loggerFactory)) - val resolveInternalContractIdsF = (tc: TraceContext) => - (contractIds: Iterable[ContractId]) => - contractStore - .lookupBatchedInternalIdsNonReadThrough(contractIds)(tc) - def updateBatchWeightMetrics(w: Long): Unit = metrics.indexer.inputMapping.batchWeight.update(w) - val batchingFlow: Flow[(Offset, Update), Iterable[(Offset, Update)], NotUsed] = - if (useWeightedBatching) { - val submissionBatchWeight = submissionBatchInsertionSize * InsertWeight - metrics.indexer.inputMapping.submissionBatchConfiguredWeight - .updateValue(submissionBatchWeight) - BatchN.weighted(submissionBatchWeight, inputMappingParallelism)( - updateWeightEstimator, - updateBatchWeightMetrics, - ) - } else BatchN(submissionBatchSize.toInt, inputMappingParallelism) - - val ((sourceQueue, uniqueKillSwitch), completionFuture) = Source - .queue[(Long, Update)]( - bufferSize = maxInputBufferSize, - overflowStrategy = OverflowStrategy.backpressure, - maxConcurrentOffers = 1, // This queue is fed by the RecoveringQueue which is sequential - ) - .takeWhile( - // The queue is only consumed until the first CommitRepair: - // - in case Repair Mode, it is invalid to use this queue after committed, - // - in case normal indexing, CommitRepair is invalid. - // The queue and stream processing completes after the CommitRepair. - !_._2.isInstanceOf[CommitRepair], - // the stream also must hold the CommitRepair event itself - inclusive = true, - ) - .map { case (longOffset, update) => Offset.tryFromLong(longOffset) -> update } - .via( - if (disableMonotonicityChecks) - Flow.apply - else - monotonicityValidator( - initialOffset = initialLedgerEnd.map(_.lastOffset), - loadPreviousState = loadPreviousSynchronizerIndexF, - executionContext = executionContext, - )(logger) - ) - .via( - BatchingParallelIngestionPipe( - batchingFlow = batchingFlow, - inputMappingParallelism = inputMappingParallelism, - inputMapper = inputMapperExecutor.execute( - inputMapper( - metrics = metrics, - toDbDto = mapInSpan( - UpdateToDbDto( - participantId = participantId, - translation = translation, - compressionStrategy = compressionStrategy, - metrics = metrics, - ) - ), - eventMetricsUpdater = EventMetricsUpdater(metrics.indexer.meteredEventsMeter), - toDistinctRawStrings = inMemoryState.stringInterningView.distinctNewRawStrings, - logger = logger, - ) - ), - seqMapperZero = seqMapperZero(initialLedgerEnd), - seqMapper = seqMapper( - internize = inMemoryState.stringInterningView.internize, - metrics = metrics, - clock = clock, - logger = logger, - ledgerEndCache = inMemoryState.ledgerEndCache, - activeContracts = mutable.LinkedHashMap.empty, - ), - dbPrepareParallelism = dbPrepareParallelism, - dbPrepare = dbPrepare( - lastActivations = contractStorageBackend.lastActivations, - dbDispatcher = dbDispatcher, - resolveInternalContractIds = resolveInternalContractIdsF, - logger = logger, - metrics = metrics, - executionContext = executionContext, - ), - batchingParallelism = batchingParallelism, - batcher = batcherExecutor.execute( - batcher( - ingestionStorageBackend.batch( - _, - inMemoryState.stringInterningView, - ), - logger, - metrics, - ) - ), - ingestingParallelism = ingestionParallelism, - contractReInsertion = reInsertContracts( - ledgerApiContractStore = contractStore, - executionContext = executionContext, - logger = logger, - ), - ingester = ingester( - ingestFunction = ingestionStorageBackend.insertBatch, - lockUsedContracts = eventStorageBackend.readLockInternalContractIds, - evictContractsFromCache = contractStore.contractsPruned, - reassignmentOffsetPersistence = reassignmentOffsetPersistence, - zeroDbBatch = ingestionStorageBackend.batch( - Vector.empty, - inMemoryState.stringInterningView, - ), - dbDispatcher = dbDispatcher, - logger = logger, - metrics = metrics, - executionContext = executionContext, - ), - maxTailerBatchSize = maxTailerBatchSize, - ingestTail = - if (repairMode) { (batchOfBatches: Vector[Batch[DbBatch]]) => - aggregateLedgerEndForRepair[DbBatch](aggregatedLedgerEndForRepair)(batchOfBatches) - Future.successful(batchOfBatches) - } else - ingestTail[DbBatch]( - storeLedgerEnd = storeLedgerEndF, - executionContext = executionContext, - logger = logger, - ), - ) - ) - .async - .map(sequentialPostProcess(sequentialPostProcessor)) - .mapAsync(postProcessingParallelism)( - postProcess( - postProcessor, - executionContext, - ) - ) - .batch(maxTailerBatchSize.toLong, Vector(_))(_ :+ _) - .via( - if (repairMode) { - // no need to aggregate the postProcessingEnd for repair: this will be done implicitly by aggregating the ledger end - Flow.apply - } else { - Flow.apply.mapAsync(1)( - ingestPostProcessEnd[DbBatch]( - storePostProcessingEndF, - executionContext, - logger, - ) - ) - } - ) - .mapConcat(identity) - .buffered( - counter = metrics.indexer.outputBatchedBufferLength, - size = maxOutputBatchedBufferSize, - ) - .via(inMemoryStateUpdaterFlow(repairMode)) - .via( - if (repairMode) { - Flow.apply.mapAsync(1)( - commitRepair( - storeLedgerEndF, - storePostProcessingEndF, - ledgerEnd => - InMemoryStateUpdater.updateLedgerEnd( - inMemoryState = inMemoryState, - ledgerEnd = ledgerEnd, - logger, - ), - aggregatedLedgerEndForRepair, - executionContext, - logger, - ) - ) - } else { - Flow.apply - } - ) - .map { (batch: Batch[?]) => - batch.offsetsUpdates.lastOption.foreach { case (offset, _) => - commit(offset.unwrap) - } - batch - } - .via( - achsConfigEffective match { - case Some(achsCfg) => - Flow[Batch[?]] - .buffered( - counter = metrics.indexer.achsBufferLength, - size = achsCfg.bufferSize, - ) - .via( - AchsMaintenancePipe( - parameterStorageBackend = parameterStorageBackend, - eventStorageBackend = eventStorageBackend, - dbDispatcher = dbDispatcher, - achsStateCache = inMemoryState.achsStateCache, - toAchsWorkDistance = (batch: Batch[?]) => - AchsWorkDistance(populate = batch.eventCount, remove = batch.eventCount), - initialWork = initialAchsWork, - populationParallelism = achsCfg.populationParallelism.unwrap, - removalParallelism = achsCfg.removalParallelism.unwrap, - aggregationThreshold = achsCfg.aggregationThreshold, - metrics = metrics, - executionContext = executionContext, - logger = logger, - fullDrain = false, - ) - ) - .map(_ => ()) - // ACHS not configured or repair mode - case _ => Flow.apply.map(_ => ()) - } - ) - .viaMat(KillSwitches.single)(Keep.both) - .toMat(Sink.ignore)(Keep.both) - .run()(materializer) - ( - Handle(completionFuture.map(_ => ())(materializer.executionContext), uniqueKillSwitch), - sourceDone => - new PekkoSourceQueueToFutureQueue( - sourceQueue = sourceQueue, - sourceDone = sourceDone, - loggerFactory = loggerFactory, - ), - ) - } -} - -object ParallelIndexerSubscription { - - /** Batch wraps around a T-typed batch, enriching it with processing relevant information. - * - * @param ledgerEnd - * The LedgerEnd for the batch. Needed for tail ingestion. - * @param batch - * The batch of variable type. - * @param batchSize - * Size of the batch measured in number of updates. Needed for metrics population. - * @param offsetsUpdates - * The Updates with Offsets, the source of the batch. - * @param missingDeactivatedActivations - * The set of deactivations need to be looked up at dbPrepare stage. It is optional as this is - * where the lookup-results are stored as well. - * @param batchTraceContext - * The TraceContext constructed for the whole batch. - * @param eventCount - * The number of events (event sequential id delta) in this batch. Used by ACHS maintenance to - * compute work distances. - */ - final case class Batch[+T]( - ledgerEnd: LedgerEnd, - batch: T, - batchSize: Int, - offsetsUpdates: Vector[(Offset, Update)], - missingDeactivatedActivations: Map[SynCon, Option[ActivationRef]], - distinctRawStrings: Iterable[String], - eventCount: Long, - batchTraceContext: TraceContext, - usedInternalContractIds: Set[Long], - ) - - final case class SynCon( - synchronizerId: SynchronizerId, - contractId: ContractId, - ) - - final case class ActivationRef( - eventSeqId: Long, - internalContractId: Long, - ) - - final implicit class RichDeactivate(val dbDto: DbDto.EventDeactivate) extends AnyVal { - def withActivationRef(activationRefO: Option[ActivationRef]): DbDto.EventDeactivate = - dbDto.copy( - deactivated_event_sequential_id = activationRefO.map(_.eventSeqId), - internal_contract_id = activationRefO.map(_.internalContractId), - ) - - def synCon: SynCon = - SynCon( - synchronizerId = dbDto.synchronizer_id, - contractId = dbDto.contract_id, - ) - } - - final implicit class RichActivate(val dbDto: DbDto.EventActivate) extends AnyVal { - def synCon: SynCon = - SynCon( - synchronizerId = dbDto.synchronizer_id, - contractId = dbDto.notPersistedContractId, - ) - - def activationRef: ActivationRef = - ActivationRef( - eventSeqId = dbDto.event_sequential_id, - internalContractId = dbDto.internal_contract_id, - ) - } - - val ZeroLedgerEnd: LedgerEnd = LedgerEnd( - lastOffset = Offset.MaxValue, // it will not be used, will be overridden - lastEventSeqId = - 0L, // this is a property of interest in the zero element, we start the sequential ids at 1L - lastStringInterningId = - 0, // this is a property of interest in the zero element, we start the string interning ids at 1 - lastPublicationTime = - CantonTimestamp.MinValue, // this is a property of interest in the zero element: sets the lower bound for publication time, we start at MinValue - ) - - def monotonicityValidator( - initialOffset: Option[Offset], - loadPreviousState: SynchronizerId => Future[Option[SynchronizerIndex]], - executionContext: ExecutionContext, - )(implicit logger: TracedLogger): Flow[(Offset, Update), (Offset, Update), NotUsed] = { - val stateRef = new AtomicReference[Map[SynchronizerId, SynchronizerIndex]](Map.empty) - val lastOffset = new AtomicReference[Option[Offset]](initialOffset) - - def checkAndUpdateOffset(offset: Offset)(implicit traceContext: TraceContext): Unit = { - assertMonotonicityCondition( - lastOffset.get() < Some(offset), - s"Monotonic Offset violation detected from ${lastOffset.get().getOrElse("participant begin")} to $offset", - ) - lastOffset.set(Some(offset)) - } - - def checkAndUpdateSynchronizerIndex(offset: Offset)( - synchronizerId: SynchronizerId, - synchronizerIndex: SynchronizerIndex, - )(implicit traceContext: TraceContext): Future[Unit] = - stateRef - .get() - .get(synchronizerId) - .map(Some(_)) - .map(Future.successful) - .getOrElse(loadPreviousState(synchronizerId)) - .map { prevSynchronizerIndexO => - checkSynchronizerIndex(prevSynchronizerIndexO, synchronizerIndex, offset, synchronizerId) - stateRef.set( - stateRef - .get() - .updated( - synchronizerId, - prevSynchronizerIndexO.map(_ max synchronizerIndex).getOrElse(synchronizerIndex), - ) - ) - }(executionContext) - - Flow[(Offset, Update)].mapAsync(1) { case (offset, update) => - implicit val traceContext: TraceContext = update.traceContext - checkAndUpdateOffset(offset) - - Option(update) - .collect { case synchronizerUpdate: SynchronizerUpdate => - checkAndUpdateSynchronizerIndex(offset)( - synchronizerId = synchronizerUpdate.synchronizerId, - synchronizerIndex = synchronizerUpdate.synchronizerIndex, - ) - } - .getOrElse(Future.unit) - .map(_ => (offset, update))(executionContext) - } - } - - private def checkSynchronizerIndex( - prevSynchronizerIndex: Option[SynchronizerIndex], - synchronizerIndex: SynchronizerIndex, - offset: Offset, - synchronizerId: SynchronizerId, - )(implicit traceContext: TraceContext, tracedLogger: TracedLogger): Unit = - prevSynchronizerIndex match { - case None => () - - case Some(prevIndex) => - assertMonotonicityCondition( - prevIndex.recordTime <= synchronizerIndex.recordTime, - s"Monotonicity violation detected: record time decreases from ${prevIndex.recordTime} to ${synchronizerIndex.recordTime} at offset $offset and synchronizer $synchronizerId", - ) - prevIndex.sequencerIndex.zip(synchronizerIndex.sequencerIndex).foreach { - case (prevSeqIndex, currSeqIndex) => - assertMonotonicityCondition( - prevSeqIndex < currSeqIndex, - s"Monotonicity violation detected: sequencer timestamp did not increase from $prevSeqIndex to $currSeqIndex at offset $offset and synchronizer $synchronizerId", - ) - } - prevIndex.repairIndex.zip(synchronizerIndex.repairIndex).foreach { - case (prevRepairIndex, currRepairIndex) => - assertMonotonicityCondition( - prevRepairIndex < currRepairIndex, - s"Monotonicity violation detected: repair index did not increase from $prevRepairIndex to $currRepairIndex at offset $offset and synchronizer $synchronizerId", - ) - } - } - - private def assertMonotonicityCondition( - condition: Boolean, - errorMessage: => String, - )(implicit traceContext: TraceContext, tracedLogger: TracedLogger): Unit = - if (!condition) - ErrorUtil.invalidState(errorMessage)(ErrorLoggingContext.fromTracedLogger(tracedLogger)) - - def extractPersistedContracts( - updates: Iterable[(Offset, Update)] - ): Map[ContractId, PersistedContractInstance] = - updates.view.flatMap { - case (_, tx: Update.TransactionAccepted) => - tx.contractInfos.view.map { case (cid, contractInfo) => - cid -> contractInfo.persistedContractInstance - } - - case (_, reassignment: Update.ReassignmentAccepted) => - reassignment.reassignment.reassignments.view.collect { case assign: Reassignment.Assign => - assign.persistedContractInstance.inst.contractId -> assign.persistedContractInstance - } - case (_, _) => Nil - }.toMap - - def reInsertContracts( - ledgerApiContractStore: LedgerApiContractStore, - executionContext: ExecutionContext, - logger: TracedLogger, - )(implicit - traceContext: TraceContext - ): Iterable[(Offset, Update)] => Future[Iterable[(Offset, Update)]] = { updates => - implicit val ec: ExecutionContext = executionContext - def fixContractInfo(newInternalContractId: Long): ContractInfo => ContractInfo = - old => - old.copy( - persistedContractInstance = old.persistedContractInstance.copy( - internalContractId = newInternalContractId - ) - ) - def fixContractInfos( - overrides: Map[ContractId, Long] - ): Map[ContractId, ContractInfo] => Map[ContractId, ContractInfo] = - _.map { case (cid, info) => - overrides.get(cid) match { - case Some(newInternalContractId) => - cid -> fixContractInfo(newInternalContractId)(info) - case None => - cid -> info - } - } - def fixAssign(newInternalContractId: Long): Reassignment.Assign => Reassignment.Assign = - old => - old.copy( - persistedContractInstance = old.persistedContractInstance.copy( - internalContractId = newInternalContractId - ) - ) - def fixReassignments( - overrides: Map[ContractId, Long] - ): Reassignment.Batch => Reassignment.Batch = { old => - val reassignments: NonEmpty[Seq[Reassignment]] = old.reassignments.map { - case assign: Reassignment.Assign => - overrides.get(assign.persistedContractInstance.inst.contractId) match { - case Some(newInternalContractId) => fixAssign(newInternalContractId)(assign) - case None => assign - } - case unassign: Reassignment.Unassign => unassign - } - Reassignment.Batch(reassignments) - } - - def fixUpdate(overrides: Map[ContractId, Long]): Update => Update = { - case u: Update.SequencedTransactionAccepted => - u.copy( - contractInfos = fixContractInfos(overrides)(u.contractInfos) - ) - case u: Update.RepairTransactionAccepted => - u.copy( - contractInfos = fixContractInfos(overrides)(u.contractInfos) - ) - case u: Update.SequencedReassignmentAccepted => - u.copy( - reassignment = fixReassignments(overrides)(u.reassignment) - ) - case u: Update.RepairReassignmentAccepted => - u.copy( - reassignment = fixReassignments(overrides)(u.reassignment) - ) - case u: Update.OnPRReassignmentAccepted => - u.copy( - reassignment = fixReassignments(overrides)(u.reassignment) - ) - - case u: Update.CommandRejected => u - case u: Update.CommitRepair => u - case u: Update.EmptyAcsPublicationRequired => u - case u: Update.LsuTimeReached => u - case u: Update.SequencerIndexMoved => u - case u: Update.TopologyTransactionEffective => u - } - - for { - contracts <- Future(extractPersistedContracts(updates)) - foundInternalContractIds <- ledgerApiContractStore.lookupBatchedInternalIdsNonReadThrough( - contracts.keySet - ) - changedInternalContractIds = contracts.keysIterator.flatMap { cid => - for { - foundInternalId <- foundInternalContractIds.get(cid) - originalInternalContractId <- contracts.get(cid).map(_.internalContractId) - if foundInternalId != originalInternalContractId - } yield cid -> foundInternalId - }.toMap - missingContracts = contracts.filterNot { case (cid, _) => - foundInternalContractIds.contains(cid) - } - storedContracts <- ledgerApiContractStore.storeContracts( - missingContracts.valuesIterator.map(_.asContractInstance).toVector - ) - _ = { - // sanity check - if (storedContracts.keySet != missingContracts.keySet) { - ErrorUtil.invalidState( - s"Programming error: stored and missing contracts are not the same." - )(ErrorLoggingContext.fromTracedLogger(logger)) - } - } - allInternalContractIdOverrides = storedContracts ++ changedInternalContractIds - } yield { - if (allInternalContractIdOverrides.isEmpty) { - updates - } else { - if (storedContracts.nonEmpty) { - logger.info(s"Needed to re-insert ${storedContracts.size} contracts during indexing.") - } - if (changedInternalContractIds.nonEmpty) { - logger.info( - s"There were ${changedInternalContractIds.size} contracts pruned and reinserted during indexing." - ) - } - updates.map { case (offset, update) => - offset -> fixUpdate(allInternalContractIdOverrides)(update) - } - } - } - } - - def inputMapper( - metrics: LedgerApiServerMetrics, - toDbDto: Offset => Update => Iterator[DbDto], - eventMetricsUpdater: Iterable[(Offset, Update)] => Unit, - toDistinctRawStrings: Iterable[DbDto] => Iterable[String], - logger: TracedLogger, - ): Iterable[(Offset, Update)] => Batch[Vector[DbDto]] = { input => - metrics.indexer.inputMapping.batchSize.update(input.size)(MetricsContext.Empty) - - val batch = input.iterator.flatMap { case (offset, update) => - update match { - case _: Update.TransactionAccepted => - logger.info( - s"Phase 7: Storing at offset=${offset.unwrap} $update" - )(update.traceContext) - case _ => - logger.debug( - s"Storing at offset=${offset.unwrap} $update" - )(update.traceContext) - } - toDbDto(offset)(update) - }.toVector - - eventMetricsUpdater(input) - - @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) - val last = input.last - - Batch( - ledgerEnd = ZeroLedgerEnd.copy( - lastOffset = last._1 - // the rest will be filled later in the sequential step - ), - batch = batch, - batchSize = input.size, - offsetsUpdates = input.toVector, - missingDeactivatedActivations = Map.empty, // will be filled later - distinctRawStrings = toDistinctRawStrings(batch), - eventCount = 0L, // will be filled later - batchTraceContext = TraceContext.ofBatch("indexer_update_batch")( - input.iterator.map(_._2) - )(logger), - usedInternalContractIds = Set.empty, // will be filled later - ) - } - - def seqMapperZero( - initialLedgerEndO: Option[LedgerEnd] - ): Batch[Vector[DbDto]] = - Batch( - ledgerEnd = initialLedgerEndO.getOrElse(ZeroLedgerEnd), - batch = Vector.empty, - batchSize = 0, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map.empty, // will be populated later - batchTraceContext = TraceContext.empty, // will be populated later - eventCount = 0L, // will be populated later - distinctRawStrings = Nil, // will be populated later - usedInternalContractIds = Set.empty, // will be populated later - ) - - def seqMapper( - internize: Iterable[String] => Iterable[(Int, String)], - metrics: LedgerApiServerMetrics, - clock: Clock, - logger: TracedLogger, - ledgerEndCache: LedgerEndCache, - activeContracts: mutable.LinkedHashMap[SynCon, ActivationRef], - )( - previous: Batch[Vector[DbDto]], - current: Batch[Vector[DbDto]], - ): Batch[Vector[DbDto]] = - Timed.value( - metrics.indexer.seqMapping.duration, { - val publicationTime = { - val now = clock.monotonicTime() - val next = Ordering[CantonTimestamp].max( - now, - previous.ledgerEnd.lastPublicationTime, - ) - if (now < next) { - logger.info( - s"Local participant clock at $now is before a previous publication time $next. Has the clock been reset, e.g., during participant failover?" - )(current.batchTraceContext) - } - next - } - - val missingDeactivatedActivationsBuilder = - Map.newBuilder[SynCon, Option[ActivationRef]] - def setActivation(dbDto: DbDto.EventActivate): Unit = { - val synCon = dbDto.synCon - val activationRef = dbDto.activationRef - if (activeContracts.contains(synCon)) { - logger.warn( - s"Double activation at eventSeqId: ${activationRef.eventSeqId}. Previous at ${activeContracts.get(synCon).map(_.eventSeqId)} This should not happen" - )(current.batchTraceContext) - activeContracts.remove(synCon).discard // we will add a new now - } - activeContracts.addOne(synCon -> activationRef) - } - def tryToGetDeactivated(dbDto: DbDto.EventDeactivate): Option[ActivationRef] = { - val synCon = dbDto.synCon - activeContracts.get(synCon) match { - case Some(activationRef) => - activeContracts.remove(synCon).discard - Some(activationRef) - case None => - missingDeactivatedActivationsBuilder.addOne(synCon -> None) - None - } - } - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - var eventSeqId = previous.ledgerEnd.lastEventSeqId - @SuppressWarnings(Array("org.wartremover.warts.Var")) - var lastTransactionMetaEventSeqId = eventSeqId - val batchWithSeqIdsAndPublicationTime = current.batch.map { - case dbDto: DbDto.EventActivate => - eventSeqId += 1 - // activation - val result = dbDto.copy(event_sequential_id = eventSeqId) - setActivation(result) - result - - case dbDto: DbDto.EventDeactivate => - eventSeqId += 1 - // deactivation - dbDto - .copy(event_sequential_id = eventSeqId) - .withActivationRef(tryToGetDeactivated(dbDto)) - - case dbDto: DbDto.EventVariousWitnessed => - eventSeqId += 1 - dbDto.copy(event_sequential_id = eventSeqId) - - case dbDto: DbDto.EventPartyToParticipant => - eventSeqId += 1 - dbDto.copy(event_sequential_id = eventSeqId) - - // we do not increase the event_seq_id here, because all the DbDto-s must have the same eventSeqId as the preceding Event - case dbDto: DbDto.IdFilterDbDto => - dbDto.withEventSequentialId(eventSeqId) - - case dbDto: DbDto.TransactionMeta => - dbDto - .copy( - event_sequential_id_first = lastTransactionMetaEventSeqId + 1, - event_sequential_id_last = eventSeqId, - publication_time = publicationTime.underlying.micros, - ) - .tap(_ => lastTransactionMetaEventSeqId = eventSeqId) - - case dbDto: DbDto.CommandCompletion => - dbDto.copy(publication_time = publicationTime.underlying.micros) - - case unChanged: DbDto.PartyEntry => unChanged - case unChanged: DbDto.StringInterningDto => unChanged - case unChanged: DbDto.SequencerIndexMoved => unChanged - } - - val (newLastStringInterningId, dbDtosWithStringInterning) = - internize(current.distinctRawStrings) - .map(DbDto.StringInterningDto.from) - .pipe(newEntries => - newEntries.lastOption.fold( - previous.ledgerEnd.lastStringInterningId -> batchWithSeqIdsAndPublicationTime - )(last => last.internalId -> (batchWithSeqIdsAndPublicationTime ++ newEntries)) - ) - - // prune active contracts so, that only activations remain which are not visible on DB yet - ledgerEndCache().foreach(ledgerEnd => - mutableDropWhile(activeContracts)(_.eventSeqId <= ledgerEnd.lastEventSeqId) - ) - - current.copy( - ledgerEnd = current.ledgerEnd.copy( - lastEventSeqId = eventSeqId, - lastStringInterningId = newLastStringInterningId, - lastPublicationTime = publicationTime, - ), - batch = dbDtosWithStringInterning, - missingDeactivatedActivations = missingDeactivatedActivationsBuilder.result(), - eventCount = eventSeqId - previous.ledgerEnd.lastEventSeqId, - distinctRawStrings = Nil, // not needed anymore - ) - }, - ) - - @SuppressWarnings(Array("org.wartremover.warts.While")) - def mutableDropWhile[T, U]( - linkedHashMap: mutable.LinkedHashMap[T, U] - )(predicate: U => Boolean): Unit = - while ( - linkedHashMap.headOption match { - case Some((key, value)) if predicate(value) => - linkedHashMap.remove(key).discard - true - - case _ => false - } - ) {} - - def dbPrepare( - lastActivations: Iterable[(SynchronizerId, Long)] => Connection => Map[ - (SynchronizerId, Long), - Long, - ], - dbDispatcher: DbDispatcher, - resolveInternalContractIds: TraceContext => Iterable[ContractId] => Future[ - Map[ContractId, Long] - ], - executionContext: ExecutionContext, - metrics: LedgerApiServerMetrics, - logger: TracedLogger, - ): Batch[Vector[DbDto]] => Future[Batch[Vector[DbDto]]] = { batch => - val missingActivations = batch.missingDeactivatedActivations.keys - val missingContracts = missingActivations.map(_.contractId) - if (missingActivations.isEmpty) Future.successful(batch) - else { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - new LoggingContextWithTrace(LoggingEntries.empty, batch.batchTraceContext) - implicit val ec: ExecutionContext = executionContext - for { - resolvedInternalContractIds <- resolveInternalContractIds(batch.batchTraceContext)( - missingContracts - ) - missingActivationsWithInternalContractIds = - missingActivations.view - .flatMap(synCon => - resolvedInternalContractIds.get(synCon.contractId) match { - case Some(internalContractId) => Some(synCon.synchronizerId -> internalContractId) - case None => - logger.warn( - s"internal_contract_id for contract ID:${synCon.contractId} not found, deactivation reference cannot be computed" - ) - None - } - ) - .toVector - lastActivationsWithInternalContractIds <- - dbDispatcher.executeSql(metrics.index.db.lookupLastActivationsDbMetrics)( - lastActivations(missingActivationsWithInternalContractIds) - ) - updatedMissingDeactivatedActivations = - missingActivations.view - .map(synCon => - synCon -> - resolvedInternalContractIds - .get(synCon.contractId) - .flatMap(internalContractId => - lastActivationsWithInternalContractIds - .get(synCon.synchronizerId -> internalContractId) - .map(lastActivationSequentialId => - ActivationRef( - eventSeqId = lastActivationSequentialId, - internalContractId = internalContractId, - ) - ) - ) - ) - .toMap - } yield batch.copy( - missingDeactivatedActivations = updatedMissingDeactivatedActivations - ) - } - } - - def refillMissingDeactivatedActivations( - metrics: LedgerApiServerMetrics, - logger: TracedLogger, - )(batch: Batch[Vector[DbDto]]): Batch[Vector[DbDto]] = { - def fillDeactivationRefFor(dbDto: DbDto.EventDeactivate): DbDto.EventDeactivate = { - val synCon = dbDto.synCon - def marker = s"deactivated event with type:${PersistentEventType.fromInt( - dbDto.event_type - )} offset:${dbDto.event_offset} nodeId:${dbDto.node_id} for synchronizerId:${dbDto.synchronizer_id} contractId:${dbDto.contract_id}" - batch.missingDeactivatedActivations.get(synCon) match { - case None => - ErrorUtil.invalidState( - s"Programming error: deactivation reference is missing for $marker, but lookup was not even initiated." - )(ErrorLoggingContext.fromTracedLogger(logger)(batch.batchTraceContext)) - - case Some(None) => - logger.warn( - s"Activation is missing for a deactivation for $marker." - )(batch.batchTraceContext) - dbDto.withActivationRef(None) - - case Some(Some(deactivationReference)) => - dbDto.withActivationRef(Some(deactivationReference)) - } - } - - val dbDtosWithDeactivationReferences = batch.batch.map { - case deactivate: DbDto.EventDeactivate - if deactivate.deactivated_event_sequential_id.isEmpty => - fillDeactivationRefFor(deactivate) - - case noChange => noChange - } - dbDtosWithDeactivationReferences.foreach { - case deactivate: DbDto.EventDeactivate => - deactivate.deactivated_event_sequential_id.foreach { deactivated_event_sequential_id => - val distance = deactivate.event_sequential_id - deactivated_event_sequential_id - metrics.indexer.deactivationDistances.update(distance) - } - case _ => () - } - batch.copy(batch = dbDtosWithDeactivationReferences) - } - - def batcher[DbBatch]( - batchF: Vector[DbDto] => DbBatch, - logger: TracedLogger, - metrics: LedgerApiServerMetrics, - )(inBatch: Batch[Vector[DbDto]]): Batch[DbBatch] = { - val finalDbDtos = inBatch - .pipe(refillMissingDeactivatedActivations(metrics, logger)) - .batch - val dbBatch = batchF(finalDbDtos) - val usedInternalContractIds = - extractPersistedContracts(inBatch.offsetsUpdates).view.map(_._2.internalContractId).toSet - inBatch.copy( - batch = dbBatch, - usedInternalContractIds = usedInternalContractIds, - ) - } - - def ingester[DbBatch]( - ingestFunction: (Connection, DbBatch) => Unit, - lockUsedContracts: Set[Long] => Connection => Set[Long], - evictContractsFromCache: Iterable[Long] => Unit, - reassignmentOffsetPersistence: ReassignmentOffsetPersistence, - zeroDbBatch: DbBatch, - dbDispatcher: DbDispatcher, - executionContext: ExecutionContext, - metrics: LedgerApiServerMetrics, - logger: TracedLogger, - ): Batch[DbBatch] => Future[Batch[DbBatch]] = { batch => - LoggingContextWithTrace.withNewLoggingContext( - "updateOffsets" -> batch.offsetsUpdates.map(_._1) - ) { implicit loggingContext => - reassignmentOffsetPersistence - .persist( - batch.offsetsUpdates, - logger, - )(batch.batchTraceContext) - .flatMap(_ => - dbDispatcher.executeSql(metrics.indexer.ingestion) { connection => - metrics.indexer.updates.inc(batch.batchSize.toLong)(MetricsContext.Empty) - val missingContracts = Timed.value( - metrics.indexer.ingestionBlockeByPruningDuration, - lockUsedContracts(batch.usedInternalContractIds)(connection), - ) - if (missingContracts.nonEmpty) { - logger.info( - s"Found ${missingContracts.size} missing contracts during indexing. Likely because pruning. Restarting indexer to recover the missing contracts." - ) - metrics.indexer.indexerRestartDueToMissingReferencedContracts.inc() - // As stale cache entries in contract store could cause crash looping, we evict the cached entries here again as a last resort. - // It is OK to evict, as we just had evidence that these do not exist (and never will, as the internal contract IDs increase - // strictly monotonically). - evictContractsFromCache(missingContracts) - throw new ReferencedContractNotFoundException() - } - ingestFunction(connection, batch.batch) - cleanUnusedBatch(zeroDbBatch)(batch) - } - )(executionContext) - }(batch.batchTraceContext) - } - - def ledgerEndSynchronizerIndexFrom( - synchronizerIndexes: Vector[(SynchronizerId, SynchronizerIndex)] - ): Map[SynchronizerId, SynchronizerIndex] = - synchronizerIndexes.groupMapReduce(_._1)(_._2)(_ max _) - - def ingestTail[DbBatch]( - storeLedgerEnd: (LedgerEnd, Map[SynchronizerId, SynchronizerIndex]) => Future[Unit], - executionContext: ExecutionContext, - logger: TracedLogger, - )(implicit - traceContext: TraceContext - ): Vector[Batch[DbBatch]] => Future[Vector[Batch[DbBatch]]] = { batchOfBatches => - batchOfBatches.lastOption match { - case Some(lastBatch) => - storeLedgerEnd( - lastBatch.ledgerEnd, - ledgerEndSynchronizerIndexFrom( - batchOfBatches - .flatMap(_.offsetsUpdates) - .collect { case (_, update: SynchronizerUpdate) => - update.synchronizerId -> update.synchronizerIndex - } - ), - ).map(_ => batchOfBatches)(executionContext) - - case None => - val message = "Unexpectedly encountered a zero-sized batch in ingestTail" - logger.error(message) - Future.failed(new IllegalStateException(message)) - } - } - - private def storeLedgerEnd( - storeTailFunction: (LedgerEnd, Map[SynchronizerId, SynchronizerIndex]) => Connection => Unit, - dbDispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - logger: TracedLogger, - )(implicit - traceContext: TraceContext - ): (LedgerEnd, Map[SynchronizerId, SynchronizerIndex]) => Future[Unit] = - (ledgerEnd, ledgerEndSynchronizerIndexes) => - LoggingContextWithTrace.withNewLoggingContext("updateOffset" -> ledgerEnd.lastOffset) { - implicit loggingContext => - def synchronizerIndexesLog: String = ledgerEndSynchronizerIndexes.toVector - .sortBy(_._1.toProtoPrimitive) - .map { case (synchronizerId, synchronizerIndex) => - s"${synchronizerId.toProtoPrimitive.take(20).mkString} -> $synchronizerIndex" - } - .mkString("synchronizer-indexes: [", ", ", "]") - - dbDispatcher.executeSql(metrics.indexer.tailIngestion) { connection => - storeTailFunction(ledgerEnd, ledgerEndSynchronizerIndexes)(connection) - metrics.indexer.ledgerEndSequentialId - .updateValue(ledgerEnd.lastEventSeqId) - logger.debug( - s"Ledger end updated in IndexDB $synchronizerIndexesLog ${loggingContext - .serializeFiltered("updateOffset")}." - )(loggingContext.traceContext) - } - } - - def aggregateLedgerEndForRepair[DbBatch]( - aggregatedLedgerEnd: AtomicReference[ - Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])] - ] - ): Vector[Batch[DbBatch]] => Unit = - batchOfBatches => - batchOfBatches.lastOption.foreach(lastBatch => - discard(aggregatedLedgerEnd.updateAndGet { aggregated => - val oldSynchronizerIndexes = - aggregated.fold(Vector.empty[(SynchronizerId, SynchronizerIndex)])(_._2.toVector) - // this will also have at the end the offset bump for the CommitRepair Update as well, we accept this for sake of simplicity - val newLedgerEnd = lastBatch.ledgerEnd - val synchronizerIndexesForBatchOfBatches = batchOfBatches - .flatMap(_.offsetsUpdates) - .collect { case (_, update: SynchronizerUpdate) => - update.synchronizerId -> update.synchronizerIndex - } - val newSynchronizerIndexes = - ledgerEndSynchronizerIndexFrom( - oldSynchronizerIndexes ++ synchronizerIndexesForBatchOfBatches - ) - Some((newLedgerEnd, newSynchronizerIndexes)) - }) - ) - - def postProcess[DbBatch]( - processor: (Vector[PostPublishData], TraceContext) => Future[Unit], - executionContext: ExecutionContext, - ): Batch[DbBatch] => Future[Batch[DbBatch]] = { batch => - val postPublishData = batch.offsetsUpdates.flatMap { case (offset, update) => - PostPublishData.from( - update, - offset, - batch.ledgerEnd.lastPublicationTime, - ) - } - processor(postPublishData, batch.batchTraceContext).map(_ => batch)(executionContext) - } - - def sequentialPostProcess[DbBatch]( - sequentialPostProcessor: Update => Unit - ): Batch[DbBatch] => Batch[DbBatch] = { batch => - batch.offsetsUpdates.foreach { case (_, update) => - sequentialPostProcessor(update) - } - batch - } - - def ingestPostProcessEnd[DbBatch]( - storePostProcessingEnd: Offset => Future[Unit], - executionContext: ExecutionContext, - logger: TracedLogger, - )(implicit - traceContext: TraceContext - ): Vector[Batch[DbBatch]] => Future[Vector[Batch[DbBatch]]] = { batchOfBatches => - batchOfBatches.lastOption match { - case Some(lastBatch) => - storePostProcessingEnd(lastBatch.ledgerEnd.lastOffset) - .map(_ => batchOfBatches)(executionContext) - case None => - val message = "Unexpectedly encountered a zero-sized batch in ingestPostProcessEnd" - logger.error(message) - Future.failed(new IllegalStateException(message)) - } - } - - def storePostProcessingEnd( - storePostProcessEndFunction: Offset => Connection => Unit, - dbDispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - logger: TracedLogger, - )(implicit traceContext: TraceContext): Offset => Future[Unit] = offset => - LoggingContextWithTrace.withNewLoggingContext("updateOffset" -> offset) { - implicit loggingContext => - dbDispatcher.executeSql(metrics.indexer.postProcessingEndIngestion) { connection => - storePostProcessEndFunction(offset)(connection) - logger.debug( - s"Post Processing end updated in IndexDB, ${loggingContext.serializeFiltered("updateOffset")}." - )(loggingContext.traceContext) - } - } - - def commitRepair( - storeLedgerEnd: (LedgerEnd, Map[SynchronizerId, SynchronizerIndex]) => Future[Unit], - storePostProcessingEnd: Offset => Future[Unit], - updateInMemoryState: LedgerEnd => Unit, - aggregatedLedgerEnd: AtomicReference[ - Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])] - ], - executionContext: ExecutionContext, - logger: TracedLogger, - )(implicit - traceContext: TraceContext - ): Batch[?] => Future[ - Batch[?] - ] = { batch => - implicit val ec = executionContext - batch.offsetsUpdates.lastOption match { - case Some((_, commitRepair: CommitRepair)) => - aggregatedLedgerEnd.get() match { - case Some((ledgerEnd, synchronizerIndexes)) => - for { - // this order is important to respect crash recovery rules - _ <- storeLedgerEnd(ledgerEnd, synchronizerIndexes) - _ <- storePostProcessingEnd(ledgerEnd.lastOffset) - _ = updateInMemoryState(ledgerEnd) - _ = commitRepair.persisted.trySuccess(()) - } yield { - logger.info("Repair committed, Ledger End stored and updated successfully.") - batch - } - case None => - val message = "Unexpectedly the Repair committed did not update the Ledger End." - logger.error(message) - Future.failed(new IllegalStateException(message)) - } - - case Some(_) => - Future.successful(batch) - - case None => - val message = "Unexpectedly encountered a zero-sized batch in ingestTail" - logger.error(message) - Future.failed(new IllegalStateException(message)) - } - } - - private def cleanUnusedBatch[DbBatch]( - zeroDbBatch: DbBatch - ): Batch[DbBatch] => Batch[DbBatch] = - _.copy( - batch = zeroDbBatch, // not used anymore - batchSize = 0, // not used anymore - missingDeactivatedActivations = Map.empty, // not used anymore - usedInternalContractIds = Set.empty, // not used anymore - ) - - val LightWeight = 1L - val InsertWeight = 100L - - def updateWeightEstimator(input: (Offset, Update)): Long = input match { - case (_, u: CommitRepair) => LightWeight - case (_, u: LsuTimeReached) => LightWeight - case (_, u: SequencerIndexMoved) => LightWeight - case (_, u: EmptyAcsPublicationRequired) => LightWeight - case (_, u: TransactionAccepted) => - (2 + u.transactionInfo.executionOrder.view - .map(_.nodeId) - .flatMap(u.transactionInfo.blindingInfo.disclosure.get) - .map(_.size + 1) - .sum) * InsertWeight - case (_, TopologyTransactionEffective(_, events, _, _)) => (events.size + 1) * InsertWeight - case (_, u: SequencedCommandRejected) => InsertWeight - case (_, u: UnSequencedCommandRejected) => InsertWeight - case (_, u: ReassignmentAccepted) => - (2 + u.reassignment.iterator.map(_.stakeholders.size + 1).sum) * InsertWeight - } - - class ReferencedContractNotFoundException - extends RuntimeException( - "Restarting indexer due to attempt to store events relying on missing internal contract IDs." - ) -} - -trait ReassignmentOffsetPersistence { - def persist( - updates: Seq[(Offset, Update)], - tracedLogger: TracedLogger, - )(implicit traceContext: TraceContext): Future[Unit] -} - -object NoOpReassignmentOffsetPersistence extends ReassignmentOffsetPersistence { - override def persist( - updates: Seq[(Offset, Update)], - tracedLogger: TracedLogger, - )(implicit traceContext: TraceContext): Future[Unit] = Future.unit -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/PostPublishData.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/PostPublishData.scala deleted file mode 100644 index fd446cfb76..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/indexer/parallel/PostPublishData.scala +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.ledger.participant.state.Update.{ - SequencedCommandRejected, - SequencedTransactionAccepted, - UnSequencedCommandRejected, -} -import com.digitalasset.canton.ledger.participant.state.{CompletionInfo, Update} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref - -import java.util.UUID - -final case class PostPublishData( - submissionSynchronizerId: SynchronizerId, - publishSource: PublishSource, - userId: Ref.UserId, - commandId: Ref.CommandId, - actAs: Set[Ref.Party], - offset: Offset, - publicationTime: CantonTimestamp, - submissionId: Option[Ref.SubmissionId], - accepted: Boolean, - traceContext: TraceContext, -) - -object PostPublishData { - def from( - update: Update, - offset: Offset, - publicationTime: CantonTimestamp, - ): Option[PostPublishData] = { - def from( - synchronizerId: SynchronizerId, - publishSource: PublishSource, - completionInfo: CompletionInfo, - accepted: Boolean, - ): PostPublishData = - PostPublishData( - submissionSynchronizerId = synchronizerId, - publishSource = publishSource, - userId = completionInfo.userId, - commandId = completionInfo.commandId, - actAs = completionInfo.actAs.toSet, - offset = offset, - publicationTime = publicationTime, - submissionId = completionInfo.submissionId, - accepted = accepted, - traceContext = update.traceContext, - ) - - update match { - // please note: we pass into deduplication and inflight tracking only the transactions and not the reassignments at acceptance - case u: SequencedTransactionAccepted => - u.completionInfoO.map(completionInfo => - from( - synchronizerId = u.synchronizerId, - publishSource = PublishSource.Sequencer(u.recordTime), - completionInfo = completionInfo, - accepted = true, - ) - ) - - // but: we pass into deduplication and inflight tracking both the transactions and the reassignments upon rejection - case u: SequencedCommandRejected if u.isTransaction => - Some( - from( - synchronizerId = u.synchronizerId, - publishSource = PublishSource.Sequencer(u.recordTime), - completionInfo = u.completionInfo, - accepted = false, - ) - ) - - case u: UnSequencedCommandRejected if u.isTransaction => - Some( - from( - synchronizerId = u.synchronizerId, - publishSource = PublishSource.Local( - messageUuid = u.messageUuid - ), - completionInfo = u.completionInfo, - accepted = false, - ) - ) - - case _ => None - } - } -} - -sealed trait PublishSource extends Product with Serializable -object PublishSource { - final case class Local(messageUuid: UUID) extends PublishSource - final case class Sequencer(sequencerTimestamp: CantonTimestamp) extends PublishSource -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/package.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/package.scala deleted file mode 100644 index 98902fd143..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/package.scala +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton - -import com.daml.ledger.resources.{ResourceContext, ResourceOwner} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.{ExecutionContext, Future} - -/** Type aliases used throughout the package */ -package object platform { - import com.digitalasset.daml.lf.value.Value as lfval - import com.digitalasset.daml.lf.transaction as lfTrans - - private[platform] type ContractId = lfval.ContractId - private[platform] val ContractId = com.digitalasset.daml.lf.value.Value.ContractId - private[platform] type Value = lfval.VersionedValue - private[platform] type FatContract = LfFatContractInst - private[platform] val FatContract: lfTrans.FatContractInstance.type = lfTrans.FatContractInstance - private[platform] type ThinContract = lfval.VersionedThinContractInstance - private[platform] val ThinContract: lfval.VersionedContractInstance.type = - lfval.VersionedContractInstance - - import com.digitalasset.daml.lf.transaction as lftx - private[platform] type NodeId = lftx.NodeId - private[platform] type Node = lftx.Node - private[platform] type Create = lftx.Node.Create - private[platform] type Exercise = lftx.Node.Exercise - private[platform] type Key = lftx.GlobalKey - private[platform] val Key: lftx.GlobalKey.type = lftx.GlobalKey - private[platform] type KeyWithMaintainers = lftx.GlobalKeyWithMaintainers - private[platform] val KeyWithMaintainers: lftx.GlobalKeyWithMaintainers.type = - lftx.GlobalKeyWithMaintainers - - import com.digitalasset.daml.lf.data as lfdata - private[platform] type Party = lfdata.Ref.Party - private[platform] val Party = lfdata.Ref.Party - private[platform] type Identifier = lfdata.Ref.Identifier - private[platform] val Identifier = lfdata.Ref.Identifier - private[platform] type QualifiedName = lfdata.Ref.QualifiedName - private[platform] val QualifiedName = lfdata.Ref.QualifiedName - private[platform] type DottedName = lfdata.Ref.DottedName - private[platform] val DottedName = lfdata.Ref.DottedName - private[platform] type ModuleName = lfdata.Ref.ModuleName - private[platform] val ModuleName = lfdata.Ref.ModuleName - private[platform] type LedgerString = lfdata.Ref.LedgerString - private[platform] val LedgerString = lfdata.Ref.LedgerString - private[platform] type WorkflowId = lfdata.Ref.LedgerString - private[platform] val WorkflowId = lfdata.Ref.LedgerString - private[platform] type SubmissionId = lfdata.Ref.SubmissionId - private[platform] val SubmissionId = lfdata.Ref.SubmissionId - private[platform] type CommandId = lfdata.Ref.CommandId - private[platform] val CommandId = lfdata.Ref.CommandId - private[platform] type ParticipantId = lfdata.Ref.ParticipantId - private[platform] val ParticipantId = lfdata.Ref.ParticipantId - private[platform] type ChoiceName = lfdata.Ref.ChoiceName - private[platform] val ChoiceName = lfdata.Ref.ChoiceName - private[platform] type PackageId = lfdata.Ref.PackageId - private[platform] val PackageId = lfdata.Ref.PackageId - private[platform] type PackageName = lfdata.Ref.PackageName - private[platform] val PackageName = lfdata.Ref.PackageName - private[platform] type Relation[A, B] = lfdata.Relation[A, B] - private[platform] type UserId = lfdata.Ref.UserId - private[platform] val UserId = lfdata.Ref.UserId - - import com.digitalasset.daml.lf.crypto - private[platform] type Hash = crypto.Hash - - private[platform] type PruneBuffers = Offset => Unit - - implicit class ResourceOwnerOps[T](val resourceOwner: ResourceOwner[T]) extends AnyVal { - def afterReleased(body: => Unit): ResourceOwner[T] = - afterReleasedF(Future.successful(body)) - - def afterReleasedF(bodyF: => Future[Unit]): ResourceOwner[T] = - for { - _ <- ResourceOwner.forReleasable(() => ())(_ => bodyF) - t <- resourceOwner - } yield t - } - - implicit class ResourceOwnerFlagCloseableOps[T <: ResourceCloseable]( - val resourceOwner: ResourceOwner[T] - ) extends AnyVal { - def acquireFlagCloseable( - name: String - )(implicit executionContext: ExecutionContext, traceContext: TraceContext): Future[T] = { - val resource = resourceOwner.acquire()(ResourceContext(executionContext)) - resource.asFuture.map( - _.registerResource(resource, name) - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/packages/DeduplicatingPackageLoader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/packages/DeduplicatingPackageLoader.scala deleted file mode 100644 index b29cab7810..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/packages/DeduplicatingPackageLoader.scala +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.packages - -import com.daml.metrics.Timed -import com.daml.metrics.api.MetricHandle.Timer -import com.digitalasset.daml.lf.archive.{DamlLf, Decode} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.PackageId -import com.digitalasset.daml.lf.language.Ast.Package - -import java.util.concurrent.ConcurrentHashMap -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Success, Try} - -/** Deduplicates package load requests, such that concurrent requests only access the data store and - * decode the package once. - */ -class DeduplicatingPackageLoader() { - // Concurrent map of promises to request each package only once. - private[this] val packagePromises: ConcurrentHashMap[Ref.PackageId, Promise[Option[Package]]] = - new ConcurrentHashMap() - - def loadPackage( - packageId: PackageId, - delegate: PackageId => Future[Option[DamlLf.Archive]], - metric: Timer, - )(implicit ec: ExecutionContext): Future[Option[Package]] = { - @SuppressWarnings(Array("org.wartremover.warts.Var")) - var gettingPackage = false - - val promise = packagePromises.computeIfAbsent( - packageId, - _ => { - gettingPackage = true - Promise[Option[Package]]() - }, - ) - - if (gettingPackage) { - val future = - Timed.future( - metric, - delegate(packageId) - .flatMap(archiveO => - Future.fromTry(Try(archiveO.map(archive => Decode.assertDecodeArchive(archive)._2))) - ), - ) - future.onComplete { - case Success(None) | Failure(_) => - // Did not find the package or got an error when looking for it. Remove the promise to allow later retries. - packagePromises.remove(packageId) - - case Success(Some(_)) => - // we don't need to treat a successful package fetch here - } - promise.completeWith(future) - } - - promise.future - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/CompletionFromTransaction.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/CompletionFromTransaction.scala deleted file mode 100644 index 78f76d1003..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/CompletionFromTransaction.scala +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse.CompletionResponse -import com.daml.ledger.api.v2.completion.Completion -import com.daml.ledger.api.v2.completion.Completion.DeduplicationPeriod.Empty -import com.daml.ledger.api.v2.offset_checkpoint.SynchronizerTime -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.util.TimestampConversion.fromInstant -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.google.protobuf.duration.Duration -import com.google.rpc.status.Status as StatusProto -import io.grpc.Status - -// Turn a stream of transactions into a stream of completions for a given user and set of parties -object CompletionFromTransaction { - val OkStatus = StatusProto.of(Status.Code.OK.value(), "", Seq.empty) - private val RejectionUpdateId = "" - - /** Properties defined for both accepted and rejected commands */ - final case class CommonCompletionProperties( - submitters: Set[String], - completionOffset: Long, - synchronizerTime: Option[SynchronizerTime], - commandId: String, - userId: String, - submissionId: Option[String], - traceContext: Option[com.daml.ledger.api.v2.trace_context.TraceContext], - trafficCost: Long, - deduplicationOffset: Option[Long], - deduplicationDurationSeconds: Option[Long], - deduplicationDurationNanos: Option[Int], - ) - - object CommonCompletionProperties { - def createFromRecordTimeAndSynchronizerId( - submitters: Set[String], - recordTime: Timestamp, - completionOffset: Offset, - commandId: String, - userId: String, - submissionId: Option[String], - synchronizerId: String, - traceContext: Option[com.daml.ledger.api.v2.trace_context.TraceContext], - trafficCost: Long, - deduplicationOffset: Option[Long], - deduplicationDurationSeconds: Option[Long], - deduplicationDurationNanos: Option[Int], - ): CommonCompletionProperties = CommonCompletionProperties( - submitters = submitters, - completionOffset = completionOffset.unwrap, - synchronizerTime = Some(toApiSynchronizerTime(synchronizerId, recordTime)), - commandId = commandId, - userId = userId, - submissionId = submissionId, - traceContext = traceContext, - trafficCost = trafficCost, - deduplicationOffset = deduplicationOffset, - deduplicationDurationSeconds = deduplicationDurationSeconds, - deduplicationDurationNanos = deduplicationDurationNanos, - ) - } - - def acceptedCompletion( - commonCompletionProperties: CommonCompletionProperties, - updateId: UpdateId, - ): CompletionStreamResponse = - CompletionStreamResponse.of( - completionResponse = CompletionResponse.Completion( - toApiCompletion( - commonCompletionProperties, - updateId = updateId.toHexString, - optStatus = Some(OkStatus), - ) - ) - ) - - def rejectedCompletion( - commonCompletionProperties: CommonCompletionProperties, - status: StatusProto, - ): CompletionStreamResponse = - CompletionStreamResponse.of( - completionResponse = CompletionResponse.Completion( - toApiCompletion( - commonCompletionProperties = commonCompletionProperties, - updateId = RejectionUpdateId, - optStatus = Some(status), - ) - ) - ) - - private def toApiSynchronizerTime( - synchronizerId: String, - recordTime: Timestamp, - ): SynchronizerTime = - SynchronizerTime.of( - synchronizerId = synchronizerId, - recordTime = Some(fromInstant(recordTime.toInstant)), - ) - - def toApiCompletion( - commonCompletionProperties: CommonCompletionProperties, - updateId: String, - optStatus: Option[StatusProto], - ): Completion = { - val optDeduplicationPeriod = toApiDeduplicationPeriod( - optDeduplicationOffset = commonCompletionProperties.deduplicationOffset, - optDeduplicationDurationSeconds = commonCompletionProperties.deduplicationDurationSeconds, - optDeduplicationDurationNanos = commonCompletionProperties.deduplicationDurationNanos, - ) - - Completion( - commandId = commonCompletionProperties.commandId, - status = optStatus, - updateId = updateId, - userId = commonCompletionProperties.userId, - actAs = commonCompletionProperties.submitters.toSeq, - submissionId = commonCompletionProperties.submissionId.getOrElse(""), - deduplicationPeriod = optDeduplicationPeriod.getOrElse(Empty), - traceContext = commonCompletionProperties.traceContext, - offset = commonCompletionProperties.completionOffset, - synchronizerTime = commonCompletionProperties.synchronizerTime, - paidTrafficCost = commonCompletionProperties.trafficCost, - ) - } - - private def toApiDeduplicationPeriod( - optDeduplicationOffset: Option[Long], - optDeduplicationDurationSeconds: Option[Long], - optDeduplicationDurationNanos: Option[Int], - ): Option[Completion.DeduplicationPeriod] = - // The only invariant that should hold, considering legacy data, is that either - // the deduplication duration seconds and nanos are both populated, or neither is. - ( - optDeduplicationOffset, - (optDeduplicationDurationSeconds, optDeduplicationDurationNanos), - ) match { - case (None, (None, None)) => None - case (Some(offset), _) => - Some( - Completion.DeduplicationPeriod.DeduplicationOffset(offset) - ) - case (_, (Some(deduplicationDurationSeconds), Some(deduplicationDurationNanos))) => - Some( - Completion.DeduplicationPeriod.DeduplicationDuration( - new Duration( - seconds = deduplicationDurationSeconds, - nanos = deduplicationDurationNanos, - ) - ) - ) - case _ => - throw new IllegalArgumentException( - "One of deduplication duration seconds and nanos has been provided " + - "but they must be either both provided or both absent" - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/DbSupport.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/DbSupport.scala deleted file mode 100644 index f5d9a9a074..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/DbSupport.scala +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.daml.ledger.resources.ResourceOwner -import com.digitalasset.canton.health.ReportsHealth -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.config.ServerRole -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig -import com.digitalasset.canton.platform.store.backend.{ - DataSourceStorageBackend, - StorageBackendFactory, - VerifiedDataSource, -} -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.resource.DbStorage -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.ExecutionContext -import scala.concurrent.duration.FiniteDuration - -final case class DbSupport( - dbDispatcher: DbDispatcher with ReportsHealth, - storageBackendFactory: StorageBackendFactory, -) - -object DbSupport { - - final case class ParticipantDataSourceConfig(jdbcUrl: String) - - final case class DataSourceProperties( - connectionPool: ConnectionPoolConfig, - postgres: PostgresDataSourceConfig = PostgresDataSourceConfig(), - ) { - def createDbConfig(config: ParticipantDataSourceConfig): DbConfig = DbConfig( - jdbcUrl = config.jdbcUrl, - connectionPool = connectionPool, - postgres = postgres, - ) - } - - final case class ConnectionPoolConfig( - connectionPoolSize: Int, - connectionTimeout: FiniteDuration, - ) - - final case class DbConfig( - jdbcUrl: String, - connectionPool: ConnectionPoolConfig, - postgres: PostgresDataSourceConfig = PostgresDataSourceConfig(), - ) { - def dataSourceConfig: DataSourceStorageBackend.DataSourceConfig = - DataSourceStorageBackend.DataSourceConfig( - jdbcUrl = jdbcUrl, - postgresConfig = postgres, - ) - } - - def owner( - dbConfig: DbConfig, - serverRole: ServerRole, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - )(implicit - traceContext: TraceContext, - executionContext: ExecutionContext, - ): ResourceOwner[DbSupport] = { - val dbType = DbType.jdbcType(dbConfig.jdbcUrl) - val storageBackendFactory = StorageBackendFactory.of(dbType, loggerFactory) - val dataSourceStorageBackend = storageBackendFactory.createDataSourceStorageBackend - for { - dataSource <- ResourceOwner.forFuture(() => - VerifiedDataSource(dataSourceStorageBackend, dbConfig.dataSourceConfig, loggerFactory) - ) - dbDispatcher <- DbDispatcher - .owner( - dataSource = dataSource, - serverRole = serverRole, - connectionPoolSize = - if (dbType.supportsParallelWrites) dbConfig.connectionPool.connectionPoolSize - else 1, - connectionTimeout = dbConfig.connectionPool.connectionTimeout, - metrics = metrics, - loggerFactory = loggerFactory, - ) - } yield DbSupport( - dbDispatcher = dbDispatcher, - storageBackendFactory = storageBackendFactory, - ) - } - - def forH2DbStorage( - h2DbStorage: DbStorage, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - ): DbSupport = - DbSupport( - dbDispatcher = DbDispatcher.ofDbStorage( - dbStorage = h2DbStorage, - overallWaitTimer = metrics.index.db.waitAll, - overallExecutionTimer = metrics.index.db.execAll, - loggerFactory = loggerFactory, - ), - storageBackendFactory = StorageBackendFactory.of(DbType.H2Database, loggerFactory), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/DbType.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/DbType.scala deleted file mode 100644 index bbcfaecea6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/DbType.scala +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -private[platform] sealed abstract class DbType( - val name: String, - val driver: String, - val supportsParallelWrites: Boolean, - val supportsAsynchronousCommits: Boolean, -) - -object DbType { - object Postgres - extends DbType( - "postgres", - "org.postgresql.Driver", - supportsParallelWrites = true, - supportsAsynchronousCommits = true, - ) - - // H2 does not support concurrent, conditional updates to the ledger_end at read committed isolation - // level: "It is possible that a transaction from one connection overtakes a transaction from a different - // connection. Depending on the operations, this might result in different results, for example when conditionally - // incrementing a value in a row." - from http://www.h2database.com/html/advanced.html - object H2Database - extends DbType( - "h2", - "org.h2.Driver", - supportsParallelWrites = false, - supportsAsynchronousCommits = false, - ) - - def jdbcType(jdbcUrl: String): DbType = jdbcUrl match { - case h2 if h2.startsWith("jdbc:h2:") => H2Database - case pg if pg.startsWith("jdbc:postgresql:") => Postgres - case _ => - sys.error(s"JDBC URL doesn't match any supported databases (h2, pg)") - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/EventSequentialId.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/EventSequentialId.scala deleted file mode 100644 index d40ce36a77..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/EventSequentialId.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -object EventSequentialId { - - /** The sequential id to use if there are no events in the index database. */ - val beforeBegin: Long = 0L -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/FlywayMigrations.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/FlywayMigrations.scala deleted file mode 100644 index 3798a20a5d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/FlywayMigrations.scala +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.store.FlywayMigrations.* -import com.digitalasset.canton.platform.store.backend.VerifiedDataSource -import com.digitalasset.canton.tracing.TraceContext -import org.flywaydb.core.Flyway -import org.flywaydb.core.api.MigrationVersion -import org.flywaydb.core.api.configuration.FluentConfiguration - -import javax.sql.DataSource -import scala.concurrent.{ExecutionContext, Future} - -class FlywayMigrations( - jdbcUrl: String, - val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext, traceContext: TraceContext) - extends NamedLogging { - private val dbType = DbType.jdbcType(jdbcUrl) - - private def runF[T](t: FluentConfiguration => Future[T]): Future[T] = - VerifiedDataSource(jdbcUrl, loggerFactory).flatMap(dataSource => - t(configurationBase(dataSource)) - ) - - private def run[T](t: FluentConfiguration => T): Future[T] = - runF(fc => Future(t(fc))) - - private def configurationBase(dataSource: DataSource): FluentConfiguration = - Flyway - .configure() - .locations((locations(dbType))*) - .dataSource(dataSource) - - private def checkFlywayHistory(flyway: Flyway): Unit = { - val currentVersion = Option(flyway.info().current()) - val requiredVersion = minimumSchemaVersion(dbType) - - currentVersion match { - case None => - () - case Some(current) if current.getVersion.isAtLeast(requiredVersion) => - () - case Some(current) => - throw SchemaVersionIsTooOld( - current = current.getVersion.getVersion, - required = requiredVersion, - ) - } - } - - def migrate(): Future[Unit] = run { configBase => - val flyway = configBase - .baselineOnMigrate(false) - .baselineVersion(MigrationVersion.fromVersion("0")) - .ignoreMigrationPatterns("") // disables the default ignoring "*:future" migrations - .load() - logger.info("Running Flyway migration...") - checkFlywayHistory(flyway) - val migrationResult = flyway.migrate() - logger.info( - s"Flyway schema migration finished successfully, applying ${migrationResult.migrationsExecuted} steps." - ) - } -} - -private[platform] object FlywayMigrations { - private val sqlMigrationClasspathBase = "classpath:db/migration/canton/" - - private[platform] def locations(dbType: DbType) = - List( - sqlMigrationClasspathBase + dbType.name + "/stable" - ) - - private[platform] def minimumSchemaVersion(dbType: DbType) = - dbType match { - case DbType.Postgres => "0" - case DbType.H2Database => "0" - } - - final case class SchemaVersionIsTooOld(current: String, required: String) - extends RuntimeException( - "Database schema version is too old. " + - s"Current schema version is $current, required schema version is $required. " + - "Please read the documentation on data continuity guarantees " + - "and use an older Daml SDK version to migrate to the required schema version." - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/LedgerApiContractStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/LedgerApiContractStore.scala deleted file mode 100644 index c5b6a58afb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/LedgerApiContractStore.scala +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.daml.metrics.Timed -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors.AbortedDueToShutdown -import com.digitalasset.canton.participant.store.{ContractStore, PersistedContractInstance} -import com.digitalasset.canton.protocol.{ContractInstance, LfContractId} -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.{ExecutionContext, Future} - -// this is a wrapper trait around ContractStore to be used from Ledger API layer and handle exceptions and shutdowns uniformly -trait LedgerApiContractStore { - - def lookupPersisted(id: LfContractId)(implicit - traceContext: TraceContext - ): Future[Option[PersistedContractInstance]] - - def lookupBatchedNonReadThrough(internalContractIds: Iterable[Long])(implicit - traceContext: TraceContext - ): Future[Map[Long, PersistedContractInstance]] - - def lookupBatchedInternalIdsNonReadThrough(contractIds: Iterable[LfContractId])(implicit - traceContext: TraceContext - ): Future[Map[LfContractId, Long]] - - def lookupBatchedContractIdsNonReadThrough(internalContractIds: Iterable[Long])(implicit - traceContext: TraceContext - ): Future[Map[Long, LfContractId]] - - def storeContracts(contracts: Seq[ContractInstance])(implicit - traceContext: TraceContext - ): Future[Map[LfContractId, Long]] - - def contractsPruned(internalContractIds: Iterable[Long]): Unit -} - -final case class LedgerApiContractStoreImpl( - participantContractStore: ContractStore, - loggerFactory: NamedLoggerFactory, - metrics: LedgerApiServerMetrics, -)(implicit ec: ExecutionContext) - extends LedgerApiContractStore - with NamedLogging { - - def lookupPersisted(id: LfContractId)(implicit - traceContext: TraceContext - ): Future[Option[PersistedContractInstance]] = - Timed - .future( - metrics.contractStore.lookupPersisted, - failOnShutdown( - participantContractStore - .lookupPersisted(id) - ), - ) - - def lookupBatchedNonReadThrough(internalContractIds: Iterable[Long])(implicit - traceContext: TraceContext - ): Future[Map[Long, PersistedContractInstance]] = - Timed.future( - metrics.contractStore.lookupBatched, - failOnShutdown( - participantContractStore - .lookupBatchedNonReadThrough(internalContractIds) - ), - ) - - def lookupBatchedInternalIdsNonReadThrough(contractIds: Iterable[LfContractId])(implicit - traceContext: TraceContext - ): Future[Map[LfContractId, Long]] = - Timed.future( - metrics.contractStore.lookupBatchedInternalIds, - failOnShutdown( - participantContractStore - .lookupBatchedInternalIdsNonReadThrough(contractIds) - ), - ) - - def lookupBatchedContractIdsNonReadThrough(internalContractIds: Iterable[Long])(implicit - traceContext: TraceContext - ): Future[Map[Long, LfContractId]] = - Timed - .future( - metrics.contractStore.lookupBatchedContractIds, - failOnShutdown( - participantContractStore - .lookupBatchedContractIdsNonReadThrough(internalContractIds) - ), - ) - - def storeContracts(contracts: Seq[ContractInstance])(implicit - traceContext: TraceContext - ): Future[Map[LfContractId, Long]] = - Timed - .future( - metrics.contractStore.reInsertContracts, - failOnShutdown( - participantContractStore - .storeContracts(contracts) - ), - ) - - override def contractsPruned(internalContractIds: Iterable[Long]): Unit = - participantContractStore.contractsPruned(internalContractIds) - - private def failOnShutdown[T](f: FutureUnlessShutdown[T])(implicit - errorLoggingContext: ErrorLoggingContext - ): Future[T] = - f.failOnShutdownTo(AbortedDueToShutdown.Error().asGrpcError) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/PruningOffsetService.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/PruningOffsetService.scala deleted file mode 100644 index 79448d0898..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/PruningOffsetService.scala +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.store.PruningOffsetCache.{Defined, Disabled, Undefined} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Thereafter.syntax.* - -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -private sealed trait PruningOffsetCache -private object PruningOffsetCache { - case object Disabled extends PruningOffsetCache - sealed trait Enabled extends PruningOffsetCache - case object Undefined extends Enabled - final case class Defined(f: Future[Option[Offset]]) extends Enabled -} - -trait PruningOffsetService { - def pruningOffset(implicit traceContext: TraceContext): Future[Option[Offset]] -} - -final class PruningOffsetServiceImpl( - fetchFromDb: TraceContext => Future[Option[Offset]], - val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext) - extends PruningOffsetService - with NamedLogging { - - private val state: AtomicReference[PruningOffsetCache] = new AtomicReference(Undefined) - - override def pruningOffset(implicit traceContext: TraceContext): Future[Option[Offset]] = - state.updateAndGet { - case Undefined => - logger.info("Pruning offset cache updated from Undefined to Defined") - Defined(fetchFromDb(traceContext).thereafter { - case Failure(ex) => - logger.info("Pruning offset fetch failed", ex) - state.updateAndGet { - case Defined(_) => - logger.info( - "Pruning offset cache reset from Defined to Undefined after fetch failure" - ) - Undefined - case other @ (Disabled | Undefined) => - logger.debug( - s"Pruning offset cache is $other during fetch failure, keeping $other" - ) - other - }.discard - case Success(v) => - logger.debug(s"Pruning offset fetched and cached: $v") - }) - case other => other - } match { - case Defined(f) => - logger.debug("Pruning offset served from cache") - f - case Disabled => - logger.debug("Pruning offset cache disabled, returning uncached result") - fetchFromDb(traceContext) - case Undefined => - throw new IllegalStateException( - "Unreachable: updateAndGet should have transitioned Undefined to Defined" - ) - } - - /** Disable caching. While disabled, reads go to the DB without being cached. */ - def disableCache()(implicit traceContext: TraceContext): Unit = { - val previous = state.getAndSet(Disabled) - logger.info( - s"Pruning offset cache disabled (previous state: ${previous.getClass.getSimpleName})" - ) - } - - /** Re-enable caching. The next read will fetch from DB and populate the cache. */ - def reEnableCache()(implicit traceContext: TraceContext): Unit = { - val previous = state.getAndSet(Undefined) - logger.info( - s"Pruning offset cache re-enabled from ${previous.getClass.getSimpleName} to Undefined" - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/ScalaPbStreamingOptimizations.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/ScalaPbStreamingOptimizations.scala deleted file mode 100644 index 20de46a34e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/ScalaPbStreamingOptimizations.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import scalapb.GeneratedMessage - -import scala.util.chaining.* - -object ScalaPbStreamingOptimizations { - implicit class ScalaPbMessageWithPrecomputedSerializedSize[ - ScalaPbMsg <: GeneratedMessage with AnyRef - ](scalaPbMsg: ScalaPbMsg) { - - /** Optimization for gRPC streams throughput. - * - * gRPC internal logic marshalls the protobuf response payloads sequentially before sending - * them over the wire (see io.grpc.ServerCallImpl.sendMessageInternal), imposing as limit the - * maximum marshalling throughput of a payload type. - * - * We've observed empirically that ScalaPB-generated message classes have associated - * marshallers with significant latencies when encoding complex payloads (e.g. - * [[com.daml.ledger.api.v2.update_service.GetUpdatesResponse]]), with the gRPC marshalling - * bottleneck appearing in some performance tests. - * - * To alleviate the problem, we can leverage the fact that ScalaPB message classes have the - * serializedSize value memoized, (see - * [[scalapb.GeneratedMessage.writeTo(output:java\.io\.OutputStream)*]]), whose computation is - * roughly half of the entire marshalling step. - * - * This optimization method takes advantage of the memoized value and forces the message's - * serializedSize computation, roughly doubling the maximum theoretical ScalaPB stream - * throughput over the gRPC server layer. - * - * @return - * A new message [[scalapb.GeneratedMessage]] with precomputed serializedSize. - */ - def withPrecomputedSerializedSize(): ScalaPbMsg = - scalaPbMsg.tap(_.serializedSize) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/Conversions.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/Conversions.scala deleted file mode 100644 index 10f55b1a4a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/Conversions.scala +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import anorm.* -import anorm.Column.nonNull -import com.daml.ledger.api.v2.trace_context.TraceContext as DamlTraceContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent.{ - Added, - ChangedTo, - Onboarding, - Revoked, -} -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel.{ - Confirmation, - Observation, - Submission, -} -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.{ - AuthorizationEvent, - AuthorizationLevel, -} -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.canton.tracing.{SerializableTraceContextConverter, TraceContext} -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.value.Value -import com.google.protobuf.ByteString -import com.typesafe.scalalogging.Logger - -import java.nio.ByteBuffer -import java.sql.PreparedStatement - -object Conversions { - - private def stringColumnToX[X](f: String => Either[String, X]): Column[X] = - Column.nonNull((value: Any, meta) => - Column.columnToString(value, meta).flatMap(x => f(x).left.map(SqlMappingError.apply)) - ) - - private def binaryColumnToX[X](f: Array[Byte] => Either[String, X]): Column[X] = - Column.nonNull((value: Any, meta) => - Column.columnToByteArray(value, meta).flatMap(x => f(x).left.map(SqlMappingError.apply)) - ) - - private final class SubTypeOfStringToStatement[S <: String] extends ToStatement[S] { - override def set(s: PreparedStatement, i: Int, v: S): Unit = - ToStatement.stringToStatement.set(s, i, v) - } - - // Party - - private implicit val columnToParty: Column[Ref.Party] = - stringColumnToX(Ref.Party.fromString) - - def party(columnName: String): RowParser[Ref.Party] = - SqlParser.get[Ref.Party](columnName)(columnToParty) - - implicit val bigDecimalColumnToBoolean: Column[Boolean] = nonNull { (value, meta) => - val MetaDataItem(qualified, _, _) = meta - value match { - case bd: java.math.BigDecimal => Right(bd.equals(new java.math.BigDecimal(1))) - case bool: Boolean => Right(bool) - case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: to Boolean for column $qualified")) - } - } - - def parties(stringInterning: StringInterning)(columName: String): RowParser[Seq[Ref.Party]] = - SqlParser - .byteArray(columName) - .map(IntArrayDBSerialization.decodeFromByteArray) - .map(_.map(stringInterning.party.externalize)) - - // PackageId - - implicit val packageIdToStatement: ToStatement[Ref.PackageId] = - new SubTypeOfStringToStatement[Ref.PackageId] - - // ParticipantId - - private implicit val columnToParticipantId: Column[Ref.ParticipantId] = - stringColumnToX(Ref.ParticipantId.fromString) - - def participantId(columnName: String): RowParser[Ref.ParticipantId] = - SqlParser.get[Ref.ParticipantId](columnName)(columnToParticipantId) - - // ContractId - - private implicit val columnToContractId: Column[Value.ContractId] = - binaryColumnToX(byteArray => Value.ContractId.fromBytes(Bytes.fromByteArray(byteArray))) - - def contractId(columnName: String): RowParser[Value.ContractId] = - SqlParser.get[Value.ContractId](columnName)(columnToContractId) - - // Offset - - implicit object OffsetToStatement extends ToStatement[Offset] { - override def set(s: PreparedStatement, index: Int, v: Offset): Unit = - s.setLong(index, v.unwrap) - } - - def offset(name: String): RowParser[Offset] = - SqlParser - .get[Long](name) - .map(Offset.tryFromLong) - - def offset(position: Int): RowParser[Offset] = - SqlParser - .get[Long](position) - .map(Offset.tryFromLong) - - // Timestamp - - def timestampFromMicros(name: String): RowParser[com.digitalasset.daml.lf.data.Time.Timestamp] = - SqlParser.get[Long](name).map(com.digitalasset.daml.lf.data.Time.Timestamp.assertFromLong) - - // Hash - - implicit object HashToStatement extends ToStatement[Hash] { - override def set(s: PreparedStatement, i: Int, v: Hash): Unit = - s.setString(i, v.bytes.toHexString) - } - - def hashFromHexString(name: String): RowParser[Hash] = - SqlParser.get[String](name).map(Hash.assertFromString) - - private def serializableTraceContextFrom(logger: Logger)(bytes: Array[Byte]) = - SerializableTraceContextConverter - .fromDamlProtoSafeOpt(logger)( - Some(DamlTraceContext.parseFrom(bytes)) - ) - - def traceContextFrom(logger: Logger)(bytes: Array[Byte]): TraceContext = - serializableTraceContextFrom(logger)(bytes).traceContext - - def protoTraceContextFrom(logger: Logger)(bytes: Array[Byte]): Option[DamlTraceContext] = - serializableTraceContextFrom(logger)(bytes).toDamlProto - - // UpdateId - - implicit object UpdateIdToStatement extends ToStatement[UpdateId] { - override def set(s: PreparedStatement, i: Int, v: UpdateId): Unit = - s.setBytes(i, v.getCryptographicEvidence.toByteArray) - } - - private implicit val columnToUpdateId: Column[UpdateId] = - binaryColumnToX(byteArray => - UpdateId.fromProtoPrimitive(ByteString.copyFrom(byteArray)).left.map(_.message) - ) - - def updateId(columnName: String): RowParser[UpdateId] = - SqlParser.get[UpdateId](columnName)(columnToUpdateId) - - // AuthorizationEvent - - private lazy val authorizationLevelToIntMapping: Map[AuthorizationLevel, Int] = Map( - Submission -> 0, - Confirmation -> 1, - Observation -> 2, - ) - - private def authorizationLevel(n: Int): AuthorizationLevel = - authorizationLevelToIntMapping - .map(_.swap) - .getOrElse( - n, - throw new RuntimeException( - s"Integer $n was not expected as an authorization level serialized value." - ), - ) - - def participantPermissionInt(authorizationEvent: AuthorizationEvent): Int = - authorizationEvent match { - case active: AuthorizationEvent.ActiveAuthorization => - authorizationLevelToIntMapping.getOrElse( - active.level, - throw new RuntimeException( - s"Unexpectedly level ${active.level} could not be serialized." - ), - ) - case Revoked => 0 // we do not care about the permission level if the mapping is revoked - } - - def authorizationEventInt(state: AuthorizationEvent): Int = state match { - case Added(_) => 0 - case ChangedTo(_) => 1 - case Revoked => 2 - case Onboarding(_) => 3 - } - - def authorizationEvent(t: Int, l: Int): AuthorizationEvent = t match { - case 0 => Added(authorizationLevel(l)) - case 1 => ChangedTo(authorizationLevel(l)) - case 2 => Revoked - case 3 => Onboarding(authorizationLevel(l)) - case other => - throw new RuntimeException( - s"Integer $other was not expected as an authorization event serialized value." - ) - } - - object IntArrayDBSerialization { - // Ints to Byte Array (with version byte prefix) - def encodeToByteArray(ints: Set[Int]): Array[Byte] = - if (ints.nonEmpty) { - val buffer = ByteBuffer.allocate(1 + ints.size * 4) - buffer.put(1.toByte) // version byte - ints.foreach(buffer.putInt(_).discard) - buffer.array() - } else Array.emptyByteArray - - // Ints from Byte Array (with prefix version byte) - def decodeFromByteArray(bytes: Array[Byte]): Seq[Int] = - if (bytes.sizeIs > 1) { - val buf = ByteBuffer.wrap(bytes) - // first byte = version - val version = buf.get().toInt - if (version != 1) { - throw new IllegalArgumentException( - s"Decoding the bytes to integers failed. Unknown version: $version. The first byte is used as the version byte and should be set to 1." - ) - } - - // remaining are 4-byte ints - val ints = Iterator - .continually(if (buf.remaining() >= 4) Some(buf.getInt()) else None) - .takeWhile(_.isDefined) - .flatten - .toSeq - - ints - } else Seq.empty - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/DbDto.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/DbDto.scala deleted file mode 100644 index 92e3cf6f36..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/DbDto.scala +++ /dev/null @@ -1,749 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.scalautil.NeverEqualsOverride -import com.digitalasset.canton.platform.store.interning.{ - StringInterningBuilder, - StringInterningProvider, -} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Ref.{ - ChoiceName, - Identifier, - NameTypeConRef, - PackageId, - ParticipantId, - Party, - UserId, -} -import com.digitalasset.daml.lf.value.Value.ContractId - -sealed trait DbDto - extends NeverEqualsOverride - with StringInterningProvider - with Product - with Serializable // to aid type inference for case class implementors - -object DbDto { - - final case class EventActivate( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitters: Option[Set[Party]], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - external_transaction_hash: Option[Array[Byte]], - traffic_cost: Option[Long], - - // event related columns - event_type: Int, - event_sequential_id: Long, - node_id: Int, - additional_witnesses: Option[Set[Party]], - source_synchronizer_id: Option[SynchronizerId], - reassignment_counter: Option[Long], - reassignment_id: Option[Array[Byte]], - representative_package_id: PackageId, - - // contract related columns - notPersistedContractId: ContractId, // just needed for processing - internal_contract_id: Long, - create_key_hash: Option[String], - ) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - submitters.foreach(_.foreach(builder.addParty)) - builder.addSynchronizerId(synchronizer_id) - additional_witnesses.foreach(_.foreach(builder.addParty)) - source_synchronizer_id.foreach(builder.addSynchronizerId) - builder.addPackageId(representative_package_id) - } - } - final case class IdFilterActivateStakeholder(idFilter: IdFilter) extends IdFilterDbDto - final case class IdFilterActivateWitness(idFilter: IdFilter) extends IdFilterDbDto - - final case class EventDeactivate( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitters: Option[Set[Party]], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - external_transaction_hash: Option[Array[Byte]], - traffic_cost: Option[Long], - - // event related columns - event_type: Int, - event_sequential_id: Long, - node_id: Int, - deactivated_event_sequential_id: Option[Long], - additional_witnesses: Option[Set[Party]], - exercise_choice: Option[ChoiceName], - exercise_choice_interface_id: Option[Identifier], - exercise_argument: Option[Array[Byte]], - exercise_result: Option[Array[Byte]], - exercise_actors: Option[Set[Party]], - exercise_last_descendant_node_id: Option[Int], - exercise_argument_compression: Option[Int], - exercise_result_compression: Option[Int], - reassignment_id: Option[Array[Byte]], - assignment_exclusivity: Option[Long], - target_synchronizer_id: Option[SynchronizerId], - reassignment_counter: Option[Long], - - // contract related columns - contract_id: ContractId, - internal_contract_id: Option[Long], - template_id: NameTypeConRef, - package_id: PackageId, - stakeholders: Set[Party], - ledger_effective_time: Option[Long], - ) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - submitters.foreach(_.foreach(builder.addParty)) - builder.addSynchronizerId(synchronizer_id) - additional_witnesses.foreach(_.foreach(builder.addParty)) - exercise_choice.foreach(builder.addChoiceName) - exercise_choice_interface_id.foreach(builder.addInterfaceId) - exercise_actors.foreach(_.foreach(builder.addParty)) - target_synchronizer_id.foreach(builder.addSynchronizerId) - builder.addTemplateId(template_id) - builder.addPackageId(package_id) - stakeholders.foreach(builder.addParty) - } - } - final case class IdFilterDeactivateStakeholder(idFilter: IdFilter) extends IdFilterDbDto - final case class IdFilterDeactivateWitness(idFilter: IdFilter) extends IdFilterDbDto - - final case class EventVariousWitnessed( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitters: Option[Set[Party]], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - external_transaction_hash: Option[Array[Byte]], - traffic_cost: Option[Long], - - // event related columns - event_type: Int, - event_sequential_id: Long, - node_id: Int, - additional_witnesses: Set[Party], - consuming: Option[Boolean], - exercise_choice: Option[ChoiceName], - exercise_choice_interface_id: Option[Identifier], - exercise_argument: Option[Array[Byte]], - exercise_result: Option[Array[Byte]], - exercise_actors: Option[Set[Party]], - exercise_last_descendant_node_id: Option[Int], - exercise_argument_compression: Option[Int], - exercise_result_compression: Option[Int], - representative_package_id: Option[PackageId], - - // contract related columns - contract_id: Option[ContractId], - internal_contract_id: Option[Long], - template_id: Option[NameTypeConRef], - package_id: Option[PackageId], - ledger_effective_time: Option[Long], - ) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - submitters.foreach(_.foreach(builder.addParty)) - builder.addSynchronizerId(synchronizer_id) - additional_witnesses.foreach(builder.addParty) - exercise_choice.foreach(builder.addChoiceName) - exercise_choice_interface_id.foreach(builder.addInterfaceId) - exercise_actors.foreach(_.foreach(builder.addParty)) - template_id.foreach(builder.addTemplateId) - representative_package_id.foreach(builder.addPackageId) - package_id.foreach(builder.addPackageId) - } - } - final case class IdFilterVariousWitness(idFilter: IdFilter) extends IdFilterDbDto - - sealed trait IdFilterDbDto extends DbDto { - def idFilter: IdFilter - def withEventSequentialId(id: Long): IdFilterDbDto = { - def idFilterWithEventSequentialId(idFilter: IdFilter): IdFilter = - idFilter.copy(event_sequential_id = id) - this match { - case IdFilterActivateStakeholder(idFilter) => - IdFilterActivateStakeholder(idFilterWithEventSequentialId(idFilter)) - case IdFilterActivateWitness(idFilter) => - IdFilterActivateWitness(idFilterWithEventSequentialId(idFilter)) - case IdFilterDeactivateStakeholder(idFilter) => - IdFilterDeactivateStakeholder(idFilterWithEventSequentialId(idFilter)) - case IdFilterDeactivateWitness(idFilter) => - IdFilterDeactivateWitness(idFilterWithEventSequentialId(idFilter)) - case IdFilterVariousWitness(idFilter) => - IdFilterVariousWitness(idFilterWithEventSequentialId(idFilter)) - } - } - - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - builder.addTemplateId(idFilter.template_id) - builder.addParty(idFilter.party_id) - } - } - final case class IdFilter( - event_sequential_id: Long, - template_id: NameTypeConRef, - party_id: Party, - first_per_sequential_id: Boolean, - ) { - def activateStakeholder: IdFilterActivateStakeholder = IdFilterActivateStakeholder(this) - def activateWitness: IdFilterActivateWitness = IdFilterActivateWitness(this) - def deactivateStakeholder: IdFilterDeactivateStakeholder = IdFilterDeactivateStakeholder(this) - def deactivateWitness: IdFilterDeactivateWitness = IdFilterDeactivateWitness(this) - def variousWitness: IdFilterVariousWitness = IdFilterVariousWitness(this) - } - - final case class EventPartyToParticipant( - event_sequential_id: Long, - event_offset: Long, - update_id: Array[Byte], - party_id: Party, - participant_id: ParticipantId, - participant_permission: Int, - participant_authorization_event: Int, - synchronizer_id: SynchronizerId, - record_time: Long, - trace_context: Array[Byte], - ) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - builder.addParty(party_id) - builder.addParticipantId(participant_id) - builder.addSynchronizerId(synchronizer_id) - } - } - - final case class PartyEntry( - ledger_offset: Long, - recorded_at: Long, - submission_id: Option[String], - party: Option[Party], - typ: String, - rejection_reason: Option[String], - is_local: Option[Boolean], - ) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = - party.foreach(builder.addParty) - } - - final case class CommandCompletion( - completion_offset: Long, - record_time: Long, - publication_time: Long, - user_id: UserId, - submitters: Set[Party], - command_id: String, - update_id: Option[Array[Byte]], - rejection_status_code: Option[Int], - rejection_status_message: Option[String], - rejection_status_details: Option[Array[Byte]], - submission_id: Option[String], - deduplication_offset: Option[Long], - deduplication_duration_seconds: Option[Long], - deduplication_duration_nanos: Option[Int], - synchronizer_id: SynchronizerId, - message_uuid: Option[String], - is_transaction: Boolean, - trace_context: Array[Byte], - traffic_cost: Long, - ) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - builder.addUserId(user_id) - submitters.foreach(builder.addParty) - builder.addSynchronizerId(synchronizer_id) - } - } - - final case class StringInterningDto( - internalId: Int, - externalString: String, - ) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = () - } - - object StringInterningDto { - def from(entry: (Int, String)): StringInterningDto = - StringInterningDto(entry._1, entry._2) - } - - final case class TransactionMeta( - update_id: Array[Byte], - event_offset: Long, - publication_time: Long, - record_time: Long, - synchronizer_id: SynchronizerId, - event_sequential_id_first: Long, - event_sequential_id_last: Long, - ) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = - builder.addSynchronizerId(synchronizer_id) - } - - final case class SequencerIndexMoved(synchronizerId: SynchronizerId) extends DbDto { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = - builder.addSynchronizerId(synchronizerId) - } - - def createDbDtos( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitters: Option[Set[Party]], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - external_transaction_hash: Option[Array[Byte]], - traffic_cost: Option[Long], - - // event related columns - event_sequential_id: Long, - node_id: Int, - additional_witnesses: Set[Party], - representative_package_id: PackageId, - - // contract related columns - notPersistedContractId: ContractId, - internal_contract_id: Long, - create_key_hash: Option[String], - )(stakeholders: Set[Party], template_id: NameTypeConRef): Iterator[DbDto] = - Iterator( - EventActivate( - // update related columns - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - traffic_cost = traffic_cost, - // event related columns - event_type = PersistentEventType.Create.asInt, - event_sequential_id = event_sequential_id, - node_id = node_id, - additional_witnesses = Some(additional_witnesses), - source_synchronizer_id = None, - reassignment_counter = None, - reassignment_id = None, - representative_package_id = representative_package_id, - // contract related columns - notPersistedContractId = notPersistedContractId, - internal_contract_id = internal_contract_id, - create_key_hash = create_key_hash, - ) - ) ++ idFilters( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_ids = stakeholders.iterator, - )(_.activateStakeholder) ++ idFilters( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_ids = additional_witnesses.iterator, - )(_.activateWitness) - - def assignDbDtos( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitter: Option[Party], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - traffic_cost: Option[Long], - - // event related columns - event_sequential_id: Long, - node_id: Int, - source_synchronizer_id: SynchronizerId, - reassignment_counter: Long, - reassignment_id: Array[Byte], - representative_package_id: PackageId, - - // contract related columns - notPersistedContractId: ContractId, - internal_contract_id: Long, - create_key_hash: Option[String], - )(stakeholders: Set[Party], template_id: NameTypeConRef): Iterator[DbDto] = - Iterator( - EventActivate( - // update related columns - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitter.map(Set(_)), - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = None, - event_type = PersistentEventType.Assign.asInt, - traffic_cost = traffic_cost, - // event related columns - event_sequential_id = event_sequential_id, - node_id = node_id, - additional_witnesses = None, - source_synchronizer_id = Some(source_synchronizer_id), - reassignment_counter = Some(reassignment_counter), - reassignment_id = Some(reassignment_id), - representative_package_id = representative_package_id, - // contract related columns - notPersistedContractId = notPersistedContractId, - internal_contract_id = internal_contract_id, - create_key_hash = create_key_hash, - ) - ) ++ idFilters( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_ids = stakeholders.iterator, - )(_.activateStakeholder) - - def consumingExerciseDbDtos( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitters: Option[Set[Party]], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - external_transaction_hash: Option[Array[Byte]], - traffic_cost: Option[Long], - - // event related columns - event_sequential_id: Long, - node_id: Int, - deactivated_event_sequential_id: Option[Long], - additional_witnesses: Set[Party], - exercise_choice: ChoiceName, - exercise_choice_interface_id: Option[Identifier], - exercise_argument: Array[Byte], - exercise_result: Option[Array[Byte]], - exercise_actors: Set[Party], - exercise_last_descendant_node_id: Int, - exercise_argument_compression: Option[Int], - exercise_result_compression: Option[Int], - - // contract related columns - contract_id: ContractId, - internal_contract_id: Option[Long], - template_id: NameTypeConRef, - package_id: PackageId, - stakeholders: Set[Party], - ledger_effective_time: Long, - ): Iterator[DbDto] = - Iterator( - EventDeactivate( - // update related columns - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - traffic_cost = traffic_cost, - // event related columns - event_type = PersistentEventType.ConsumingExercise.asInt, - event_sequential_id = event_sequential_id, - node_id = node_id, - deactivated_event_sequential_id = deactivated_event_sequential_id, - additional_witnesses = Some(additional_witnesses), - exercise_choice = Some(exercise_choice), - exercise_choice_interface_id = exercise_choice_interface_id, - exercise_argument = Some(exercise_argument), - exercise_result = exercise_result, - exercise_actors = Some(exercise_actors), - exercise_last_descendant_node_id = Some(exercise_last_descendant_node_id), - exercise_argument_compression = exercise_argument_compression, - exercise_result_compression = exercise_result_compression, - reassignment_id = None, - assignment_exclusivity = None, - target_synchronizer_id = None, - reassignment_counter = None, - // contract related columns - contract_id = contract_id, - internal_contract_id = internal_contract_id, - template_id = template_id, - package_id = package_id, - stakeholders = stakeholders, - ledger_effective_time = Some(ledger_effective_time), - ) - ) ++ idFilters( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_ids = stakeholders.iterator, - )(_.deactivateStakeholder) ++ idFilters( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_ids = additional_witnesses.iterator, - )(_.deactivateWitness) - - def unassignDbDtos( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitter: Option[Party], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - traffic_cost: Option[Long], - - // event related columns - event_sequential_id: Long, - node_id: Int, - deactivated_event_sequential_id: Option[Long], - reassignment_id: Array[Byte], - assignment_exclusivity: Option[Long], - target_synchronizer_id: SynchronizerId, - reassignment_counter: Long, - - // contract related columns - contract_id: ContractId, - internal_contract_id: Option[Long], - template_id: NameTypeConRef, - package_id: PackageId, - stakeholders: Set[Party], - ): Iterator[DbDto] = - Iterator( - EventDeactivate( - // update related columns - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitter.map(Set(_)), - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = None, - traffic_cost = traffic_cost, - // event related columns - event_type = PersistentEventType.Unassign.asInt, - event_sequential_id = event_sequential_id, - node_id = node_id, - deactivated_event_sequential_id = deactivated_event_sequential_id, - additional_witnesses = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - reassignment_id = Some(reassignment_id), - assignment_exclusivity = assignment_exclusivity, - target_synchronizer_id = Some(target_synchronizer_id), - reassignment_counter = Some(reassignment_counter), - // contract related columns - contract_id = contract_id, - internal_contract_id = internal_contract_id, - template_id = template_id, - package_id = package_id, - stakeholders = stakeholders, - ledger_effective_time = None, - ) - ) ++ idFilters( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_ids = stakeholders.iterator, - )(_.deactivateStakeholder) - - def witnessedCreateDbDtos( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitters: Option[Set[Party]], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - external_transaction_hash: Option[Array[Byte]], - traffic_cost: Option[Long], - - // event related columns - event_sequential_id: Long, - node_id: Int, - additional_witnesses: Set[Party], - representative_package_id: PackageId, - - // contract related columns - internal_contract_id: Long, - )(template_id: NameTypeConRef): Iterator[DbDto] = - Iterator( - EventVariousWitnessed( - // update related columns - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - traffic_cost = traffic_cost, - - // event related columns - event_type = PersistentEventType.WitnessedCreate.asInt, - event_sequential_id = event_sequential_id, - node_id = node_id, - additional_witnesses = additional_witnesses, - consuming = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - representative_package_id = Some(representative_package_id), - - // contract related columns - contract_id = None, - internal_contract_id = Some(internal_contract_id), - template_id = None, - package_id = None, - ledger_effective_time = None, - ) - ) ++ idFilters( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_ids = additional_witnesses.iterator, - )(_.variousWitness) - - def witnessedExercisedDbDtos( - // update related columns - event_offset: Long, - update_id: Array[Byte], - workflow_id: Option[String], - command_id: Option[String], - submitters: Option[Set[Party]], - record_time: Long, - synchronizer_id: SynchronizerId, - trace_context: Array[Byte], - external_transaction_hash: Option[Array[Byte]], - traffic_cost: Option[Long], - - // event related columns - event_sequential_id: Long, - node_id: Int, - additional_witnesses: Set[Party], - consuming: Boolean, - exercise_choice: ChoiceName, - exercise_choice_interface_id: Option[Identifier], - exercise_argument: Array[Byte], - exercise_result: Option[Array[Byte]], - exercise_actors: Set[Party], - exercise_last_descendant_node_id: Int, - exercise_argument_compression: Option[Int], - exercise_result_compression: Option[Int], - - // contract related columns - contract_id: ContractId, - internal_contract_id: Option[Long], - template_id: NameTypeConRef, - package_id: PackageId, - ledger_effective_time: Long, - ): Iterator[DbDto] = - Iterator( - EventVariousWitnessed( - // update related columns - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - traffic_cost = traffic_cost, - - // event related columns - event_type = - if (consuming) PersistentEventType.WitnessedConsumingExercise.asInt - else PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = event_sequential_id, - node_id = node_id, - additional_witnesses = additional_witnesses, - consuming = Some(consuming), - exercise_choice = Some(exercise_choice), - exercise_choice_interface_id = exercise_choice_interface_id, - exercise_argument = Some(exercise_argument), - exercise_result = exercise_result, - exercise_actors = Some(exercise_actors), - exercise_last_descendant_node_id = Some(exercise_last_descendant_node_id), - exercise_argument_compression = exercise_argument_compression, - exercise_result_compression = exercise_result_compression, - representative_package_id = None, - - // contract related columns - contract_id = Some(contract_id), - internal_contract_id = internal_contract_id, - template_id = Some(template_id), - package_id = Some(package_id), - ledger_effective_time = Some(ledger_effective_time), - ) - ) ++ idFilters( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_ids = additional_witnesses.iterator, - )(_.variousWitness) - - def idFilters( - party_ids: Iterator[Party], - template_id: NameTypeConRef, - event_sequential_id: Long, - )(toIdFilterDbDto: IdFilter => IdFilterDbDto): Iterator[IdFilterDbDto] = - party_ids - .take(1) - .map(party_id => - IdFilter( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_id = party_id, - first_per_sequential_id = true, - ) - ) - .++( - party_ids.map(party_id => - IdFilter( - event_sequential_id = event_sequential_id, - template_id = template_id, - party_id = party_id, - first_per_sequential_id = false, - ) - ) - ) - .map(toIdFilterDbDto) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/PersistentEventType.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/PersistentEventType.scala deleted file mode 100644 index 9c14846fe7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/PersistentEventType.scala +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -sealed trait PersistentEventType extends Product with Serializable { - def asInt: Int -} - -// WARNING! The PersistentEventType mappings are stored in DB, changing them only allowed in a backwards compatible way to ensure data continuity! -// Changing these should be reflected in the debug-views (see V2_1__lapi_3.0_views.sql) -object PersistentEventType { - // activations - sealed abstract class ActivationPersistentEventType(override val asInt: Int) - extends PersistentEventType - case object Create extends ActivationPersistentEventType(1) - case object Assign extends ActivationPersistentEventType(2) - // deactivations - sealed abstract class DeactivationPersistentEventType(override val asInt: Int) - extends PersistentEventType - case object ConsumingExercise extends DeactivationPersistentEventType(3) - case object Unassign extends DeactivationPersistentEventType(4) - // various witnessed - sealed abstract class VariousWitnessedPersistentEventType(override val asInt: Int) - extends PersistentEventType - case object NonConsumingExercise extends VariousWitnessedPersistentEventType(5) - case object WitnessedCreate extends VariousWitnessedPersistentEventType(6) - case object WitnessedConsumingExercise extends VariousWitnessedPersistentEventType(7) - // topology transactions - sealed abstract class TopologyTransactionPersistentEventType(override val asInt: Int) - extends PersistentEventType - case object PartyToParticipant extends TopologyTransactionPersistentEventType(8) - - val allEventTypes: Seq[PersistentEventType] = List( - Create, - Assign, - ConsumingExercise, - Unassign, - NonConsumingExercise, - WitnessedCreate, - WitnessedConsumingExercise, - PartyToParticipant, - ) - - private val formIntMap: Map[Int, PersistentEventType] = - allEventTypes - .map(persistentEventType => persistentEventType.asInt -> persistentEventType) - .toMap - - def fromInt(i: Int): PersistentEventType = - formIntMap.getOrElse( - i, - throw new IllegalStateException( - s"Invalid Int $i - no such PersistentEventType can be found." - ), - ) - - assert(allEventTypes.sizeIs == 8) - assert(allEventTypes.toSet.sizeIs == 8) - assert(formIntMap.sizeIs == 8) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/RowDef.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/RowDef.scala deleted file mode 100644 index 7553cabdb6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/RowDef.scala +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import anorm.{Row, RowParser, SimpleSql, ~} -import cats.Applicative -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.* -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* - -import java.sql.Connection - -final case class RowDef[+T]( - columns: Vector[String], - rowParser: RowParser[T], -) { - def queryMultipleRows(sql: CompositeSql => SimpleSql[Row])(implicit - connection: Connection - ): Vector[T] = - sql(columnsCSql).asVectorOf(rowParser)(connection) - - def querySingleOptRow(sql: CompositeSql => SimpleSql[Row])(implicit - connection: Connection - ): Option[T] = - sql(columnsCSql).asSingleOpt(rowParser)(connection) - - private def mapRowParser[U](f: RowParser[T] => RowParser[U]): RowDef[U] = - RowDef(columns = columns, rowParser = f(rowParser)) - - def map[U](f: T => U): RowDef[U] = mapRowParser(_.map(f)) - - def ? : RowDef[Option[T]] = mapRowParser(_.?) - - def branch[U, X >: T](branches: (X, RowDef[U])*): RowDef[U] = { - val branchMap = branches.toMap - RowDef( - columns = branches.flatMap(_._2.columns).++(columns).distinct.toVector, - rowParser = rowParser.flatMap { branchValue => - branchMap.get(branchValue) match { - case Some(rowDef) => rowDef.rowParser - case None => - _ => - throw new IllegalStateException( - s"Cannot find suitable branch for result parsing for extracted branch value $branchValue" - ) - } - }, - ) - } - - private val columnsCSql = cSQL"#${columns.mkString(", ")}" -} - -object RowDef { - def static[T](t: T): RowDef[T] = - RowDef(Vector.empty, _ => anorm.Success(t)) - - def column[T]( - columnName: String, - rowParser: String => RowParser[T], - ): RowDef[T] = - RowDef(Vector(columnName), rowParser(columnName)) - - implicit val applicative: Applicative[RowDef] = - new Applicative[RowDef] { - override def pure[A](x: A): RowDef[A] = static(x) - - override def ap[A, B](ff: RowDef[A => B])(fa: RowDef[A]): RowDef[B] = - RowDef( - columns = Vector(ff, fa).flatMap(_.columns).distinct, - rowParser = (ff.rowParser ~ fa.rowParser).map { case ff ~ fa => ff(fa) }, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/StorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/StorageBackend.scala deleted file mode 100644 index f4a4c9fe0d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/StorageBackend.scala +++ /dev/null @@ -1,886 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.participant.state.SynchronizerIndex -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.indexer.parallel.PostPublishData -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.* -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsAddActivationsParams, - AchsLastPointers, - AchsRemoveDeactivatedParams, - AchsState, - PruneUptoInclusiveAndLedgerEnd, -} -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.CompositeSql -import com.digitalasset.canton.platform.store.backend.common.{ - EventPayloadSourceForUpdatesAcsDelta, - EventPayloadSourceForUpdatesLedgerEffects, - EventReaderQueries, - UpdatePointwiseQueries, - UpdateStreamingQueries, -} -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.IdPageQuery -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.FullIdentifier -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.google.common.annotations.VisibleForTesting - -import java.sql.Connection -import javax.sql.DataSource -import scala.annotation.unused - -/** Encapsulates the interface which hides database technology specific implementations. Naming - * convention for the interface methods, which requiring Connection: - * - read operations are represented as nouns (plural, singular form indicates cardinality) - * - write operations are represented as verbs - */ - -trait ResetStorageBackend { - - /** Truncates ALL storage backend tables. Does not touch other tables, like the Flyway history - * table. The result is a database that looks the same as a freshly created database with Flyway - * migrations applied. - */ - def resetAll(connection: Connection): Unit -} - -trait IngestionStorageBackend[DbBatch] { - - /** The CPU intensive batching operation hides the batching logic, and the mapping to the database - * specific representation of the inserted data. This should be pure CPU logic without IO. - * - * @param dbDtos - * is a collection of DbDto from which the batch is formed - * @param stringInterning - * will be used to switch ingested strings to the internal integers - * @return - * the database-specific batch DTO, which can be inserted via insertBatch - */ - def batch(dbDtos: Vector[DbDto], stringInterning: StringInterning): DbBatch - - /** Using a JDBC connection, a batch will be inserted into the database. No significant CPU load, - * mostly blocking JDBC communication with the database backend. - * - * @param connection - * to be used when inserting the batch - * @param batch - * to be inserted - */ - def insertBatch(connection: Connection, batch: DbBatch): Unit - - /** Deletes all partially ingested data, written during a non-graceful stop of previous indexing. - * No significant CPU load, mostly blocking JDBC communication with the database backend. - * - * @param ledgerEnd - * the current ledger end, or None if no ledger end exists - * @param connection - * to be used when inserting the batch - */ - def deletePartiallyIngestedData(ledgerEnd: Option[ParameterStorageBackend.LedgerEnd])( - connection: Connection - ): Unit -} - -trait ParameterStorageBackend { - - /** This method is used to update the new observable ledger end. No significant CPU load, mostly - * blocking JDBC communication with the database backend. - * - * @param connection - * to be used when updating the parameters table - */ - def updateLedgerEnd( - ledgerEnd: ParameterStorageBackend.LedgerEnd, - lastSynchronizerIndex: Map[SynchronizerId, SynchronizerIndex] = Map.empty, - )(connection: Connection): Unit - - /** Query the current ledger end, read from the parameters table. No significant CPU load, mostly - * blocking JDBC communication with the database backend. - * - * @param connection - * to be used to get the LedgerEnd - * @return - * the current LedgerEnd - */ - def ledgerEnd(connection: Connection): Option[ParameterStorageBackend.LedgerEnd] - - /** The latest SynchronizerIndex for a synchronizerId until all events are processed fully and - * published to the Ledger API DB. The Update which from this SynchronizerIndex originate has - * smaller or equal offset than the current LedgerEnd: LedgerEnd and SynchronizerIndexes are - * persisted consistently in one transaction. - */ - def cleanSynchronizerIndex(synchronizerId: SynchronizerId)( - connection: Connection - ): Option[SynchronizerIndex] - - /** Part of pruning process, this needs to be in the same transaction as the other pruning related - * database operations - */ - def updatePrunedUptoInclusive(prunedUpToInclusive: Offset)(connection: Connection): Unit - - def prunedUpToInclusive(connection: Connection): Option[Offset] - - def prunedUpToInclusiveAndLedgerEnd(connection: Connection): PruneUptoInclusiveAndLedgerEnd - - def updatePostProcessingEnd( - postProcessingEnd: Option[Offset] - )(connection: Connection): Unit - - def postProcessingEnd( - connection: Connection - ): Option[Offset] - - /** Initializes the parameters table and verifies or updates ledger identity parameters. This - * method is idempotent: - * - If no identity parameters are stored, then they are set to the given value. - * - If identity parameters are stored, then they are compared to the given ones. - * - Ledger identity parameters are written at most once, and are never overwritten. No - * significant CPU load, mostly blocking JDBC communication with the database backend. - * - * This method is NOT safe to call concurrently. - */ - def initializeParameters( - params: ParameterStorageBackend.IdentityParams, - loggerFactory: NamedLoggerFactory, - )(connection: Connection): Unit - - /** Returns the ledger identity parameters, or None if the database hasn't been initialized yet. - */ - def ledgerIdentity(connection: Connection): Option[ParameterStorageBackend.IdentityParams] - - /** Fetches the current state of the Active Contracts Head Snapshot (ACHS) from the database, or - * None if it hasn't been initialized yet. - */ - def fetchACHSState(connection: Connection): Option[AchsState] - - /** Inserts the state of the Active Contracts Head Snapshot (ACHS) in the database. Assumes that - * the ACHS state is not yet present. - */ - def insertACHSState(achsState: AchsState)(connection: Connection): Unit - - /** Updates the validAt of the state of the Active Contracts Head Snapshot (ACHS) in the database. - * Throws an IllegalStateException if the update was not successful. - */ - def updateACHSValidAt(validAt: Long)(connection: Connection): Unit - - /** Updates the lastRemoved and the lastPopulated of the state of the Active Contracts Head - * Snapshot (ACHS) in the database. Throws an IllegalStateException if the update was not - * successful. - */ - def updateACHSLastPointers(pointers: AchsLastPointers)(connection: Connection): Unit - - def clearACHSState(connection: Connection): Unit - - /** Clears all ACHS data (both the state row and the filter data table). */ - def clearAchsData(connection: Connection): Unit -} - -object ParameterStorageBackend { - final case class LedgerEnd( - lastOffset: Offset, - lastEventSeqId: Long, - lastStringInterningId: Int, - lastPublicationTime: CantonTimestamp, - ) - - object LedgerEnd { - val beforeBegin: Option[ParameterStorageBackend.LedgerEnd] = None - } - final case class IdentityParams(participantId: ParticipantId) - - final case class PruneUptoInclusiveAndLedgerEnd( - pruneUptoInclusive: Option[Offset], - ledgerEnd: Option[Offset], - ) - - /** Represents the state of the Active Contracts Head Snapshot (ACHS) in the database. The ACHS is - * a snapshot of the active contracts at a specific event sequential ID. However, due to the - * nature of contract activations and deactivations, the ACHS may not be fully populated up to - * that event sequential ID and be populated only partially. The fields in this case class help - * track the state of the ACHS and keep it updated. - * - * @param validAt - * The event sequential ID at which the ACHS is valid. - * @param lastRemoved - * The last event sequential ID for which deactivations were looked up and the corresponding - * activation was removed from the ACHS. At the end of the update of the ACHS this should be - * equal to validAt. - * @param lastPopulated - * The last event sequential ID that was populated into the ACHS. - */ - final case class AchsState( - validAt: Long, - lastPointers: AchsLastPointers, - ) extends PrettyPrinting { - - override def pretty: Pretty[AchsState] = prettyOfClass( - param("validAt", _.validAt), - param("lastRemoved", _.lastPointers.lastRemoved), - param("lastPopulated", _.lastPointers.lastPopulated), - ) - } - - final case class AchsLastPointers( - lastRemoved: Long, - lastPopulated: Long, - ) extends PrettyPrinting { - - override def pretty: Pretty[AchsLastPointers] = prettyOfClass( - param("lastRemoved", _.lastRemoved), - param("lastPopulated", _.lastPopulated), - ) - } - - /** The parameters for removing activations from the Active Contracts Head Snapshot (ACHS) that - * have been deactivated at a specified range of event sequential IDs. - * - * @param startExclusive - * The starting event sequential ID (exclusive). - * @param endInclusive - * The ending event sequential ID (inclusive). - */ - final case class AchsRemoveDeactivatedParams( - startExclusive: Long, - endInclusive: Long, - ) - - /** The parameters for adding activations to the Active Contracts Head Snapshot (ACHS) for a - * specified range of event sequential IDs that are still active at a given event sequential ID. - * - * @param startExclusive - * The starting event sequential ID (exclusive). - * @param endInclusive - * The ending event sequential ID (inclusive). - * @param activeAt - * The event sequential ID at which the contracts are still active. - */ - final case class AchsAddActivationsParams( - startExclusive: Long, - endInclusive: Long, - activeAt: Long, - ) -} - -trait PartyStorageBackend { - def parties(parties: Seq[Party])(connection: Connection): List[IndexerPartyDetails] - def knownParties(fromExcl: Option[Party], filterString: Option[String185], maxResults: Int)( - connection: Connection - ): List[IndexerPartyDetails] -} - -trait CompletionStorageBackend { - def commandCompletions( - startInclusive: Offset, - endInclusive: Offset, - userId: UserId, - parties: Set[Party], - limit: Int, - )(connection: Connection): Vector[CompletionStreamResponse] - - def commandCompletionsForRecovery( - startInclusive: Offset, - endInclusive: Offset, - )(connection: Connection): Vector[PostPublishData] - - /** Part of pruning process, this needs to be in the same transaction as the other pruning related - * database operations - */ - def pruneCompletions( - pruneUpToInclusive: Offset - )(connection: Connection, traceContext: TraceContext): Unit -} - -trait ContractStorageBackend { - - def activeContracts(internalContractIds: Seq[Long], beforeEventSeqId: Long)( - connection: Connection - ): Map[Long, Boolean] - - def lastActivations(synchronizerContracts: Iterable[(SynchronizerId, Long)])( - connection: Connection - ): Map[(SynchronizerId, Long), Long] - - /** Returns true if the batch lookup is implemented */ - def supportsBatchKeyStateLookups: Boolean - - def contractKey(keyPageQuery: ContractStorageBackend.KeysPageQuery)( - connection: Connection - ): ContractStorageBackend.KeysPageResult - - def contractKeysPlain( - keyPageQueries: Seq[ContractStorageBackend.KeysPageQuery], - validAtEventSeqId: Long, - )( - connection: Connection - ): Seq[ContractStorageBackend.KeysPageResult] -} - -object ContractStorageBackend { - final case class KeysPageQuery( - key: Key, - limit: Int, - nextPageToken: Option[Long], - validAtEventSeqId: Long, - ) - - /** @param internalContractIds - * in reverse event sequential ID order starting from nextPageToken (exclusive) or - * validAtEventSeqId (inclusive) from the KeysPageQuery - * @param nextPageToken - * If available, this is the event sequential ID of the last (earliest) contract If not - * available, this is the last page from the page-sequence - */ - final case class KeysPageResult( - internalContractIds: Vector[Long], - nextPageToken: Option[Long], - ) -} - -trait EventStorageBackend { - - def updatePointwiseQueries: UpdatePointwiseQueries - def updateStreamingQueries: UpdateStreamingQueries - def eventReaderQueries: EventReaderQueries - - /** Part of pruning process, this needs to be in the same transaction as the other pruning related - * database operations. The underlying DB operation will populate the - * lapi_pruning_contract_candidate table to prepare for Contract pruning. - */ - def pruneEvents( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusive: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit - connection: Connection, - traceContext: TraceContext, - ): Unit - - /** Attempt to prune contracts prepared in the lapi_pruning_contract_candidate table. This method - * is not guaranteed to succeed as issuing write locks, but guaranteed to be not starved / fail - * fast, if cannot acquire all necessary locks immediately. In case of locking related failure - * PruningContractsBlockedException will be thrown to adhere to DbDispatcher semantics, and the - * DB transaction will be rolled back. In case of success the lapi_pruning_contract_candidate - * table will be emptied. - * - * @return - * The pruned internal_contract_id-s. - */ - def pruneContracts()(implicit - connection: Connection, - traceContext: TraceContext, - ): Iterable[Long] - - /** Removes all contract candidates from lapi_pruning_contract_candidate table which are not - * eligible for pruning. - */ - def cleanPruningCandidates()(implicit - connection: Connection, - traceContext: TraceContext, - ): Unit - - /** Adds contracts to contract pruning candidates lapi_pruning_contract_candidate after the - * defined exclusive bound. This function intended for indexer crash recovery, where during - * initialization before the events getting cleaned after ledger-end watermark, the related new - * contracts are marked for pruning. This function is not pruning the contracts, only adds them - * as candidates, which will be validated and potentially pruned as part of the next pruning - * Index DB pruning. - */ - def addContractPruningCandidatesAfter(eventSeqIdExclusive: Long)(implicit - connection: Connection, - traceContext: TraceContext, - ): Unit - - def activeContractBatch( - eventSequentialIds: Iterable[Long], - allFilterParties: Option[Set[Party]], - )(connection: Connection): Vector[RawThinActiveContract] - - def lookupActivationSequentialIdByOffset( - offsets: Iterable[Long] - )(connection: Connection): Vector[Long] - - def lookupDeactivationSequentialIdByOffset( - offsets: Iterable[Long] - )(connection: Connection): Vector[Long] - - def maxEventSequentialId(untilInclusiveOffset: Option[Offset])( - connection: Connection - ): Long - - def firstSynchronizerOffsetAfterOrAt( - synchronizerId: SynchronizerId, - afterOrAtRecordTimeInclusive: Timestamp, - )(connection: Connection): Option[SynchronizerOffset] - - def lastSynchronizerOffsetBeforeOrAt( - synchronizerIdO: Option[SynchronizerId], - beforeOrAtOffsetInclusive: Offset, - )(connection: Connection): Option[SynchronizerOffset] - - def synchronizerOffset(offset: Offset)(connection: Connection): Option[SynchronizerOffset] - - def firstSynchronizerOffsetAfterOrAtPublicationTime( - afterOrAtPublicationTimeInclusive: Timestamp - )(connection: Connection): Option[SynchronizerOffset] - - def lastSynchronizerOffsetBeforeOrAtPublicationTime( - beforeOrAtPublicationTimeInclusive: Timestamp - )(connection: Connection): Option[SynchronizerOffset] - - // Note: Added for offline party replication as CN is using it. - def lastSynchronizerOffsetBeforeOrAtRecordTime( - synchronizerId: SynchronizerId, - beforeOrAtRecordTimeInclusive: Timestamp, - beforeOrAtLedgerEndOffsetInclusive: Offset, - )(connection: Connection)(implicit traceContext: TraceContext): Option[SynchronizerOffset] - - def lastRecordTimeBeforeOrAtSynchronizerOffset( - synchronizerId: SynchronizerId, - beforeOrAtOffsetInclusive: Offset, - )(connection: Connection): Option[CantonTimestamp] - - def fetchTopologyPartyEventIds(party: Option[Party]): IdPageQuery - - def topologyPartyEventBatch( - eventSequentialIds: SequentialIdBatch - )(connection: Connection): Vector[RawParticipantAuthorization] - - def topologyEventOffsetPublishedOnRecordTime( - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - )(connection: Connection): Option[Offset] - - def fetchEventPayloadsAcsDelta(target: EventPayloadSourceForUpdatesAcsDelta)( - eventSequentialIds: SequentialIdBatch, - requestingPartiesForTx: Option[Set[Party]], - requestingPartiesForReassignment: Option[Set[Party]], - )(connection: Connection): Vector[RawThinAcsDeltaEvent] - - def fetchEventPayloadsLedgerEffects(target: EventPayloadSourceForUpdatesLedgerEffects)( - eventSequentialIds: SequentialIdBatch, - requestingPartiesForTx: Option[Set[Party]], - requestingPartiesForReassignment: Option[Set[Party]], - )(connection: Connection): Vector[RawThinLedgerEffectsEvent] - - /** Adds activations to the Active Contracts Head Snapshot (ACHS) for a specified range of event - * sequential IDs that are still active at a given event sequential ID. - * - * @param params - * The parameters for adding activations to the ACHS. It includes:: - * - startExclusive: The starting event sequential ID (exclusive). - * - endInclusive: The ending event sequential ID (inclusive). - * - activeAt: The event sequential ID at which the contracts are still active. - * @param connection - * The database connection to be used for the operation. - */ - def addActivationsToAchs( - params: AchsAddActivationsParams - )(connection: Connection): Unit - - /** Removes activations from the Active Contracts Head Snapshot (ACHS) looking at a specified - * range of event sequential IDs in the deactivations table. - * - * @param params - * The parameters for removing activations from the ACHS. It includes: - * - startExclusive: The starting event sequential ID (exclusive). - * - endInclusive: The ending event sequential ID (inclusive). - * @param connection - * The database connection to be used for the operation. - */ - def removeDeactivatedFromAchs( - params: AchsRemoveDeactivatedParams - )(connection: Connection): Unit - - def lockExclusivelyPruningProcessingTable(connection: Connection): Unit - - def lockExclusivelyContractPruningProcessingTable(connection: Connection): Unit - - /** @return - * the missing internal contract IDs - */ - def readLockInternalContractIds(internalContractIds: Set[Long])(connection: Connection): Set[Long] - - def writeLockInternalContractIds(whereInternalContractIdExprs: CompositeSql)( - connection: Connection - ): Unit -} - -object EventStorageBackend { - class PruningContractsBlockedException extends RuntimeException - class CannotAcquireAllRowLocksException extends RuntimeException - - sealed trait RawEvent extends Product with Serializable { - def commonEventProperties: CommonEventProperties - final def offset: Long = commonEventProperties.offset - final def nodeId: Int = commonEventProperties.nodeId - final def eventSeqId: Long = commonEventProperties.eventSequentialId - final def workflowId: Option[String] = commonEventProperties.workflowId - final def synchronizerId: String = commonEventProperties.synchronizerId - - def templateId: FullIdentifier - def witnessParties: Set[String] - } - - sealed trait RawAcsDeltaEvent extends RawEvent - sealed trait RawLedgerEffectsEvent extends RawEvent - - sealed trait RawUpdateEvent { - def commonUpdateProperties: CommonUpdateProperties - final def commandId: Option[String] = commonUpdateProperties.commandId - final def updateId: String = commonUpdateProperties.updateId - final def recordTime: Timestamp = commonUpdateProperties.recordTime - final def traceContext: Array[Byte] = commonUpdateProperties.traceContext - final def trafficCost: Option[Long] = commonUpdateProperties.trafficCost - } - - sealed trait RawReassignmentEvent extends RawEvent with RawUpdateEvent { - def reassignmentProperties: ReassignmentProperties - final def reassignmentId: String = reassignmentProperties.reassignmentId - final def submitter: Option[String] = reassignmentProperties.submitter - final def reassignmentCounter: Long = reassignmentProperties.reassignmentCounter - - def sourceSynchronizerId: String - def targetSynchronizerId: String - - final override def commonUpdateProperties: CommonUpdateProperties = - reassignmentProperties.commonUpdateProperties - final override def commonEventProperties: CommonEventProperties = - reassignmentProperties.commonEventProperties - } - - sealed trait RawTransactionEvent extends RawEvent with RawUpdateEvent { - def transactionProperties: TransactionProperties - final def externalTransactionHash: Option[Array[Byte]] = - transactionProperties.externalTransactionHash - - def ledgerEffectiveTime: Timestamp - - final override def commonUpdateProperties: CommonUpdateProperties = - transactionProperties.commonUpdateProperties - final override def commonEventProperties: CommonEventProperties = - transactionProperties.commonEventProperties - } - - sealed trait RawThinEvent extends Product with Serializable { - def offset: Long - def eventSeqId: Long - } - - sealed trait RawThinAcsDeltaEvent extends RawThinEvent - sealed trait RawThinLedgerEffectsEvent extends RawThinEvent - - sealed trait RawThinTransactionEvent extends RawThinEvent - sealed trait RawThinReassignmentEvent extends RawThinEvent - - final case class CommonEventProperties( - eventSequentialId: Long, - offset: Long, - nodeId: Int, - workflowId: Option[String], - synchronizerId: String, - ) - - final case class CommonUpdateProperties( - updateId: String, - commandId: Option[String], - traceContext: Array[Byte], - recordTime: Timestamp, - trafficCost: Option[Long], - ) - - final case class TransactionProperties( - commonEventProperties: CommonEventProperties, - commonUpdateProperties: CommonUpdateProperties, - externalTransactionHash: Option[Array[Byte]], - ) - - final case class ReassignmentProperties( - commonEventProperties: CommonEventProperties, - commonUpdateProperties: CommonUpdateProperties, - reassignmentId: String, - submitter: Option[String], - reassignmentCounter: Long, - ) - - final case class ThinCreatedEventProperties( - representativePackageId: Ref.PackageId, - filteredAdditionalWitnessParties: Set[String], - internalContractId: Long, - requestingParties: Option[Set[String]], - reassignmentCounter: Long, - acsDeltaForParticipant: Boolean, - ) - - sealed trait FatCreatedEvent extends RawEvent { - def fatCreatedEventProperties: FatCreatedEventProperties - final def fatContract: FatContract = fatCreatedEventProperties.fatContract - final def representativePackageId: Ref.PackageId = - fatCreatedEventProperties.thinCreatedEventProperties.representativePackageId - final def internalContractId: Long = - fatCreatedEventProperties.thinCreatedEventProperties.internalContractId - final def acsDeltaForWitnesses: Boolean = - fatCreatedEventProperties.thinCreatedEventProperties.acsDeltaForParticipant && - fatCreatedEventProperties.fatContract.stakeholders.iterator - .map(_.toString) - .exists(witnessParties) - - override final def templateId: FullIdentifier = - fatCreatedEventProperties.fatContract.templateId.toFullIdentifier( - fatCreatedEventProperties.fatContract.packageName - ) - - override final val witnessParties: Set[String] = - fatCreatedEventProperties.thinCreatedEventProperties.filteredAdditionalWitnessParties.iterator - .++(fatCreatedEventProperties.fatContract.stakeholders.iterator.map(_.toString)) - .filter(party => - fatCreatedEventProperties.thinCreatedEventProperties.requestingParties match { - case Some(requestingParties) => requestingParties.contains(party) - case None => true - } - ) - .toSet - } - - final case class FatCreatedEventProperties( - thinCreatedEventProperties: ThinCreatedEventProperties, - fatContract: FatContract, - ) - - final case class RawThinActiveContract( - commonEventProperties: CommonEventProperties, - thinCreatedEventProperties: ThinCreatedEventProperties, - ) extends RawThinEvent { - override def offset: Long = commonEventProperties.offset - override def eventSeqId: Long = commonEventProperties.eventSequentialId - } - - final case class RawFatActiveContract( - commonEventProperties: CommonEventProperties, - fatCreatedEventProperties: FatCreatedEventProperties, - ) extends RawEvent - with FatCreatedEvent - - final case class RawThinCreatedEvent( - transactionProperties: TransactionProperties, - thinCreatedEventProperties: ThinCreatedEventProperties, - ) extends RawThinAcsDeltaEvent - with RawThinLedgerEffectsEvent - with RawThinTransactionEvent { - override def offset: Long = transactionProperties.commonEventProperties.offset - override def eventSeqId: Long = transactionProperties.commonEventProperties.eventSequentialId - } - - final case class RawFatCreatedEvent( - transactionProperties: TransactionProperties, - fatCreatedEventProperties: FatCreatedEventProperties, - ) extends RawAcsDeltaEvent - with RawLedgerEffectsEvent - with RawTransactionEvent - with FatCreatedEvent { - override def ledgerEffectiveTime: Timestamp = - fatCreatedEventProperties.fatContract.createdAt.time - } - - final case class RawThinAssignEvent( - reassignmentProperties: ReassignmentProperties, - thinCreatedEventProperties: ThinCreatedEventProperties, - sourceSynchronizerId: String, - ) extends RawThinAcsDeltaEvent - with RawThinLedgerEffectsEvent - with RawThinReassignmentEvent { - override def offset: Long = reassignmentProperties.commonEventProperties.offset - override def eventSeqId: Long = reassignmentProperties.commonEventProperties.eventSequentialId - } - - final case class RawFatAssignEvent( - reassignmentProperties: ReassignmentProperties, - fatCreatedEventProperties: FatCreatedEventProperties, - sourceSynchronizerId: String, - ) extends RawAcsDeltaEvent - with RawLedgerEffectsEvent - with RawReassignmentEvent - with FatCreatedEvent { - override def targetSynchronizerId: String = synchronizerId - } - - final case class RawArchivedEvent( - transactionProperties: TransactionProperties, - contractId: ContractId, - templateId: FullIdentifier, - filteredStakeholderParties: Set[String], - ledgerEffectiveTime: Timestamp, - deactivatedEventSeqId: Option[Long], - ) extends RawAcsDeltaEvent - with RawTransactionEvent - with RawThinAcsDeltaEvent - with RawThinTransactionEvent { - override def witnessParties: Set[String] = filteredStakeholderParties - } - - final case class RawExercisedEvent( - transactionProperties: TransactionProperties, - contractId: ContractId, - templateId: FullIdentifier, - exerciseConsuming: Boolean, - exerciseChoice: ChoiceName, - exerciseChoiceInterface: Option[Ref.Identifier], - exerciseArgument: Array[Byte], - exerciseArgumentCompression: Option[Int], - exerciseResult: Option[Array[Byte]], - exerciseResultCompression: Option[Int], - exerciseActors: Set[String], - exerciseLastDescendantNodeId: Int, - filteredAdditionalWitnessParties: Set[String], - filteredStakeholderParties: Set[String], - ledgerEffectiveTime: Timestamp, - deactivatedEventSeqId: Option[Long], - acsDeltaForParticipant: Boolean, - ) extends RawLedgerEffectsEvent - with RawTransactionEvent - with RawThinLedgerEffectsEvent - with RawThinTransactionEvent { - override def witnessParties: Set[String] = - filteredStakeholderParties ++ filteredAdditionalWitnessParties - - def acsDeltaForWitnesses: Boolean = - acsDeltaForParticipant && filteredStakeholderParties.nonEmpty - } - - final case class RawUnassignEvent( - reassignmentProperties: ReassignmentProperties, - contractId: ContractId, - templateId: FullIdentifier, - filteredStakeholderParties: Set[String], - assignmentExclusivity: Option[Timestamp], - targetSynchronizerId: String, - deactivatedEventSeqId: Option[Long], - ) extends RawAcsDeltaEvent - with RawLedgerEffectsEvent - with RawReassignmentEvent - with RawThinAcsDeltaEvent - with RawThinLedgerEffectsEvent - with RawThinReassignmentEvent { - override def witnessParties: Set[String] = filteredStakeholderParties - - override def sourceSynchronizerId: String = synchronizerId - } - - final case class SynchronizerOffset( - offset: Offset, - synchronizerId: SynchronizerId, - recordTime: Timestamp, - publicationTime: Timestamp, - ) - - final case class RawParticipantAuthorization( - offset: Offset, - updateId: String, - partyId: String, - participantId: String, - authorizationEvent: AuthorizationEvent, - recordTime: Timestamp, - synchronizerId: String, - traceContext: Array[Byte], - ) - - sealed trait SequentialIdBatch - object SequentialIdBatch { - final case class IdRange(fromInclusive: Long, toInclusive: Long) extends SequentialIdBatch - final case class Ids(ids: Iterable[Long]) extends SequentialIdBatch - } -} - -trait DataSourceStorageBackend { - def createDataSource( - dataSourceConfig: DataSourceStorageBackend.DataSourceConfig, - loggerFactory: NamedLoggerFactory, - connectionInitHook: Option[Connection => Unit] = None, - ): DataSource - - def checkCompatibility(@unused connection: Connection)(implicit - @unused traceContext: TraceContext - ): Unit = () - - def checkDatabaseAvailable(connection: Connection): Unit -} - -object DataSourceStorageBackend { - - /** @param jdbcUrl - * JDBC URL of the database, parameter to establish the connection between the application and - * the database - * @param postgresConfig - * configurations which apply only for the PostgresSQL backend - */ - final case class DataSourceConfig( - jdbcUrl: String, - postgresConfig: PostgresDataSourceConfig = PostgresDataSourceConfig(), - ) -} - -trait DBLockStorageBackend { - def tryAcquire( - lockId: DBLockStorageBackend.LockId, - lockMode: DBLockStorageBackend.LockMode, - )(connection: Connection): Option[DBLockStorageBackend.Lock] - - def release(lock: DBLockStorageBackend.Lock)(connection: Connection): Boolean - - def lock(id: Int): DBLockStorageBackend.LockId - - def dbLockSupported: Boolean -} - -object DBLockStorageBackend { - final case class Lock(lockId: LockId, lockMode: LockMode) - - trait LockId - - sealed trait LockMode - object LockMode { - case object Exclusive extends LockMode - case object Shared extends LockMode - } -} - -trait IntegrityStorageBackend { - - /** Verifies the integrity of the index database, throwing an exception if any issue is found. - * This operation is allowed to take some time to finish. It is not expected that it is used - * during regular index/indexer operation. - */ - @VisibleForTesting - def verifyIntegrity(failForEmptyDB: Boolean = true, inMemoryCantonStore: Boolean = false)( - connection: Connection - ): Unit - - @VisibleForTesting - def numberOfAcceptedTransactionsFor(synchronizerId: SynchronizerId)( - connection: Connection - ): Int - - @VisibleForTesting - def moveLedgerEndBackToScratch()(connection: Connection): Unit -} - -trait StringInterningStorageBackend { - def loadStringInterningEntries(fromIdExclusive: Int, untilIdInclusive: Int)( - connection: Connection - ): Iterable[(Int, String)] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/StorageBackendFactory.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/StorageBackendFactory.scala deleted file mode 100644 index 9a2e778c74..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/StorageBackendFactory.scala +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.DbType -import com.digitalasset.canton.platform.store.backend.h2.H2StorageBackendFactory -import com.digitalasset.canton.platform.store.backend.localstore.{ - IdentityProviderStorageBackend, - PartyRecordStorageBackend, - UserManagementStorageBackend, -} -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresStorageBackendFactory -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.interning.StringInterning - -trait StorageBackendFactory { - def createIngestionStorageBackend: IngestionStorageBackend[?] - def createParameterStorageBackend(stringInterning: StringInterning): ParameterStorageBackend - def createPartyStorageBackend(ledgerEndCache: LedgerEndCache): PartyStorageBackend - def createPartyRecordStorageBackend: PartyRecordStorageBackend - def createCompletionStorageBackend( - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, - ): CompletionStorageBackend - def createContractStorageBackend( - stringInterning: StringInterning, - ledgerEndCache: LedgerEndCache, - ): ContractStorageBackend - def createEventStorageBackend( - ledgerEndCache: LedgerEndCache, - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, - ): EventStorageBackend - def createDataSourceStorageBackend: DataSourceStorageBackend - def createDBLockStorageBackend: DBLockStorageBackend - def createIntegrityStorageBackend: IntegrityStorageBackend - def createResetStorageBackend: ResetStorageBackend - def createStringInterningStorageBackend: StringInterningStorageBackend - def createUserManagementStorageBackend: UserManagementStorageBackend - def createIdentityProviderConfigStorageBackend: IdentityProviderStorageBackend - - final def readStorageBackend( - ledgerEndCache: LedgerEndCache, - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, - ): ReadStorageBackend = - ReadStorageBackend( - partyStorageBackend = createPartyStorageBackend(ledgerEndCache), - completionStorageBackend = createCompletionStorageBackend(stringInterning, loggerFactory), - contractStorageBackend = createContractStorageBackend(stringInterning, ledgerEndCache), - eventStorageBackend = - createEventStorageBackend(ledgerEndCache, stringInterning, loggerFactory), - ) -} - -object StorageBackendFactory { - def of(dbType: DbType, loggerFactory: NamedLoggerFactory): StorageBackendFactory = - dbType match { - case DbType.H2Database => H2StorageBackendFactory - case DbType.Postgres => PostgresStorageBackendFactory(loggerFactory) - } -} - -final case class ReadStorageBackend( - partyStorageBackend: PartyStorageBackend, - completionStorageBackend: CompletionStorageBackend, - contractStorageBackend: ContractStorageBackend, - eventStorageBackend: EventStorageBackend, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/UpdateToDbDto.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/UpdateToDbDto.scala deleted file mode 100644 index 1fe089d36e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/UpdateToDbDto.scala +++ /dev/null @@ -1,654 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.metrics.api.MetricsContext -import com.daml.metrics.api.MetricsContext.{withExtraMetricLabels, withOptionalMetricLabels} -import com.daml.platform.v1.index.StatusDetails -import com.digitalasset.canton.data.DeduplicationPeriod.{DeduplicationDuration, DeduplicationOffset} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.{ - AuthorizationEvent, - TopologyEvent, -} -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId -import com.digitalasset.canton.ledger.participant.state.{CompletionInfo, Reassignment, Update} -import com.digitalasset.canton.metrics.{IndexerMetrics, LedgerApiServerMetrics} -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.indexer.TransactionTraversalUtils.NodeInfo -import com.digitalasset.canton.platform.store.backend.Conversions.{ - authorizationEventInt, - participantPermissionInt, -} -import com.digitalasset.canton.platform.store.dao.JdbcLedgerDao -import com.digitalasset.canton.platform.store.dao.events.* -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.SerializableTraceContext -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.daml.lf.data.Ref.{NameTypeConRef, PackageRef} -import com.digitalasset.daml.lf.data.{Ref, Time} -import com.digitalasset.daml.lf.transaction.Node.Action -import io.grpc.Status - -import java.util.UUID - -object UpdateToDbDto { - import Update.* - - def apply( - participantId: Ref.ParticipantId, - translation: LfValueSerialization, - compressionStrategy: CompressionStrategy, - metrics: LedgerApiServerMetrics, - )(implicit mc: MetricsContext): Offset => Update => Iterator[DbDto] = { offset => tracedUpdate => - val serializedTraceContext = SerializableTraceContext( - tracedUpdate.traceContext - ).toSerializedDamlProto - tracedUpdate match { - case u: CommandRejected => - commandRejectedToDbDto( - metrics = metrics, - offset = offset, - serializedTraceContext = serializedTraceContext, - commandRejected = u, - isTransaction = u.isTransaction, - ) - - case u: TopologyTransactionEffective => - topologyTransactionToDbDto( - metrics = metrics, - participantId = participantId, - offset = offset, - serializedTraceContext = serializedTraceContext, - topologyTransaction = u, - ) - - case u: TransactionAccepted => - transactionAcceptedToDbDto( - translation = translation, - compressionStrategy = compressionStrategy, - metrics = metrics, - offset = offset, - serializedTraceContext = serializedTraceContext, - transactionAccepted = u, - ) - - case u: ReassignmentAccepted => - reassignmentAcceptedToDbDto( - metrics = metrics, - offset = offset, - serializedTraceContext = serializedTraceContext, - reassignmentAccepted = u, - ) - - case u: SequencerIndexMoved => - // nothing to persist, this is only a synthetic DbDto to facilitate updating the StringInterning - Iterator(DbDto.SequencerIndexMoved(u.synchronizerId)) - - case _: EmptyAcsPublicationRequired => Iterator.empty - case _: LsuTimeReached => Iterator.empty - - case _: CommitRepair => - Iterator.empty - } - } - - private def commandRejectedToDbDto( - metrics: LedgerApiServerMetrics, - offset: Offset, - serializedTraceContext: Array[Byte], - commandRejected: CommandRejected, - isTransaction: Boolean, - )(implicit mc: MetricsContext): Iterator[DbDto] = { - withExtraMetricLabels( - IndexerMetrics.Labels.grpcCode -> Status - .fromCodeValue(commandRejected.reasonTemplate.code) - .getCode - .name(), - IndexerMetrics.Labels.userId -> commandRejected.completionInfo.userId, - ) { implicit mc: MetricsContext => - incrementCounterForEvent( - metrics.indexer, - IndexerMetrics.Labels.eventType.transaction, - IndexerMetrics.Labels.status.rejected, - ) - } - val messageUuid = commandRejected match { - case _: SequencedCommandRejected => None - case unSequenced: UnSequencedCommandRejected => Some(unSequenced.messageUuid) - } - Iterator( - commandCompletion( - offset = offset, - recordTime = commandRejected.recordTime.toLf, - updateId = None, - completionInfo = commandRejected.completionInfo, - synchronizerId = commandRejected.synchronizerId, - messageUuid = messageUuid, - serializedTraceContext = serializedTraceContext, - isTransaction = isTransaction, - ).copy( - rejection_status_code = Some(commandRejected.reasonTemplate.code), - rejection_status_message = Some(commandRejected.reasonTemplate.message), - rejection_status_details = - Some(StatusDetails.of(commandRejected.reasonTemplate.status.details).toByteArray), - ) - ) - } - - private def topologyTransactionToDbDto( - metrics: LedgerApiServerMetrics, - participantId: Ref.ParticipantId, - offset: Offset, - serializedTraceContext: Array[Byte], - topologyTransaction: TopologyTransactionEffective, - )(implicit mc: MetricsContext): Iterator[DbDto] = { - incrementCounterForEvent( - metrics.indexer, - IndexerMetrics.Labels.eventType.topologyTransaction, - IndexerMetrics.Labels.status.accepted, - ) - - val transactionMeta = DbDto.TransactionMeta( - update_id = topologyTransaction.updateId.toProtoPrimitive.toByteArray, - event_offset = offset.unwrap, - publication_time = 0, // this is filled later - record_time = topologyTransaction.recordTime.toMicros, - synchronizer_id = topologyTransaction.synchronizerId, - event_sequential_id_first = 0, // this is filled later - event_sequential_id_last = 0, // this is filled later - ) - - val events = topologyTransaction.events.iterator.flatMap { - case TopologyEvent.PartyToParticipantAuthorization(party, participant, authorizationEvent) => - import com.digitalasset.canton.platform.apiserver.services.admin.PartyAllocation - val eventPartyToParticipant = Iterator( - DbDto.EventPartyToParticipant( - event_sequential_id = 0, // this is filled later - event_offset = offset.unwrap, - update_id = topologyTransaction.updateId.toProtoPrimitive.toByteArray, - party_id = party, - participant_id = participant, - participant_permission = participantPermissionInt(authorizationEvent), - participant_authorization_event = authorizationEventInt(authorizationEvent), - synchronizer_id = topologyTransaction.synchronizerId, - record_time = topologyTransaction.recordTime.toMicros, - trace_context = serializedTraceContext, - ) - ) - val partyEntry = Seq(authorizationEvent) - .collect { case active: AuthorizationEvent.ActiveAuthorization => active } - .map(_ => - DbDto.PartyEntry( - ledger_offset = offset.unwrap, - recorded_at = topologyTransaction.recordTime.toMicros, - submission_id = Some( - PartyAllocation.TrackerKey(party, participant, authorizationEvent).submissionId - ), - party = Some(party), - typ = JdbcLedgerDao.acceptType, - rejection_reason = None, - is_local = Some(participant == participantId), - ) - ) - .iterator - eventPartyToParticipant ++ partyEntry - } - - // TransactionMeta DTO must come last in this sequence - // because in a later stage the preceding events - // will be assigned consecutive event sequential ids - // and transaction meta is assigned sequential ids of its first and last event - events ++ Seq(transactionMeta) - } - - private def transactionAcceptedToDbDto( - translation: LfValueSerialization, - compressionStrategy: CompressionStrategy, - metrics: LedgerApiServerMetrics, - offset: Offset, - serializedTraceContext: Array[Byte], - transactionAccepted: TransactionAccepted, - )(implicit mc: MetricsContext): Iterator[DbDto] = { - withOptionalMetricLabels( - IndexerMetrics.Labels.userId -> transactionAccepted.completionInfoO.map( - _.userId - ) - ) { implicit mc: MetricsContext => - incrementCounterForEvent( - metrics.indexer, - IndexerMetrics.Labels.eventType.transaction, - IndexerMetrics.Labels.status.accepted, - ) - } - - val transactionMeta = DbDto.TransactionMeta( - update_id = transactionAccepted.updateId.toProtoPrimitive.toByteArray, - event_offset = offset.unwrap, - publication_time = 0, // this is filled later - record_time = transactionAccepted.recordTime.toMicros, - synchronizer_id = transactionAccepted.synchronizerId, - event_sequential_id_first = 0, // this is filled later - event_sequential_id_last = 0, // this is filled later - ) - - val events: Iterator[DbDto] = transactionAccepted.transactionInfo.executionOrder.iterator - .flatMap { - case NodeInfo(nodeId, create: Create, _) => - createNodeToDbDto( - offset = offset, - serializedTraceContext = serializedTraceContext, - transactionAccepted = transactionAccepted, - nodeId = nodeId, - create = create, - ) - - case NodeInfo(nodeId, exercise: Exercise, lastDescendantNodeId) => - exerciseNodeToDbDto( - compressionStrategy = compressionStrategy, - translation = translation, - offset = offset, - serializedTraceContext = serializedTraceContext, - transactionAccepted = transactionAccepted, - nodeId = nodeId, - exercise = exercise, - lastDescendantNodeId = lastDescendantNodeId, - ) - - case _ => - Iterator.empty // It is okay to collect: blinding info is already there, we are free at hand to filter out the fetch and lookup nodes here already - } - - val completions = - for { - completionInfo <- transactionAccepted.completionInfoO - } yield commandCompletion( - offset = offset, - recordTime = transactionAccepted.recordTime.toLf, - updateId = Some(transactionAccepted.updateId), - completionInfo = completionInfo, - synchronizerId = transactionAccepted.synchronizerId, - messageUuid = None, - serializedTraceContext = serializedTraceContext, - isTransaction = true, - ) - - // TransactionMeta DTO must come last in this sequence - // because in a later stage the preceding events - // will be assigned consecutive event sequential ids - // and transaction meta is assigned sequential ids of its first and last event - events ++ completions ++ Seq(transactionMeta) - } - - def templateIdWithPackageName(node: Action): NameTypeConRef = - node.templateId.copy(pkg = PackageRef.Name(node.packageName)) - - def templateIdWithPackageName(reassignment: Reassignment): NameTypeConRef = - Ref.NameTypeConRef( - PackageRef.Name(reassignment.packageName), - reassignment.templateId.qualifiedName, - ) - - private def createNodeToDbDto( - offset: Offset, - serializedTraceContext: Array[Byte], - transactionAccepted: TransactionAccepted, - nodeId: NodeId, - create: Create, - ): Iterator[DbDto] = { - val templateId = templateIdWithPackageName(create) - val contractInfo = transactionAccepted.contractInfos - .getOrElse( - create.coid, - throw new IllegalStateException( - s"Missing contract info for contract ${create.coid}" - ), - ) - val representativePackageId: Ref.PackageId = contractInfo.representativePackageId match { - case RepresentativePackageId.SameAsContractPackageId => create.templateId.packageId - case RepresentativePackageId.DedicatedRepresentativePackageId( - representativePackageId - ) => - representativePackageId - } - val witnesses = - transactionAccepted.transactionInfo.blindingInfo.disclosure.getOrElse(nodeId, Set.empty) - val internal_contract_id = contractInfo.internalContractId - - if (transactionAccepted.isAcsDelta(create.coid)) { - val stakeholders = create.stakeholders - val additional_witnesses = witnesses.diff(stakeholders) - DbDto.createDbDtos( - event_offset = offset.unwrap, - update_id = transactionAccepted.updateId.toProtoPrimitive.toByteArray, - workflow_id = transactionAccepted.transactionMeta.workflowId, - command_id = transactionAccepted.completionInfoO.map(_.commandId), - submitters = transactionAccepted.completionInfoO.map(_.actAs.toSet), - record_time = transactionAccepted.recordTime.toMicros, - synchronizer_id = transactionAccepted.synchronizerId, - trace_context = serializedTraceContext, - external_transaction_hash = - transactionAccepted.externalTransactionHash.map(_.unwrap.toByteArray), - traffic_cost = transactionAccepted.paidTrafficCost.map(_.value), - event_sequential_id = 0, // this is filled later - node_id = nodeId.index, - additional_witnesses = additional_witnesses, - representative_package_id = representativePackageId, - notPersistedContractId = create.coid, - internal_contract_id = internal_contract_id, - create_key_hash = create.keyOpt.map(_.globalKey.hash.bytes.toHexString), - )( - stakeholders = stakeholders, - template_id = templateId, - ) - } else { - DbDto.witnessedCreateDbDtos( - event_offset = offset.unwrap, - update_id = transactionAccepted.updateId.toProtoPrimitive.toByteArray, - workflow_id = transactionAccepted.transactionMeta.workflowId, - command_id = transactionAccepted.completionInfoO.map(_.commandId), - submitters = transactionAccepted.completionInfoO.map(_.actAs.toSet), - record_time = transactionAccepted.recordTime.toMicros, - synchronizer_id = transactionAccepted.synchronizerId, - trace_context = serializedTraceContext, - external_transaction_hash = - transactionAccepted.externalTransactionHash.map(_.unwrap.toByteArray), - traffic_cost = transactionAccepted.paidTrafficCost.map(_.value), - event_sequential_id = 0, // this is filled later - node_id = nodeId.index, - additional_witnesses = witnesses, - representative_package_id = representativePackageId, - internal_contract_id = internal_contract_id, - )(templateId) - } - } - - private def exerciseNodeToDbDto( - compressionStrategy: CompressionStrategy, - translation: LfValueSerialization, - offset: Offset, - serializedTraceContext: Array[Byte], - transactionAccepted: TransactionAccepted, - nodeId: NodeId, - exercise: Exercise, - lastDescendantNodeId: NodeId, - ): Iterator[DbDto] = { - val (exerciseArgument, exerciseResult, _) = - translation.serialize(exercise) - val templateId = templateIdWithPackageName(exercise) - val witnesses = - transactionAccepted.transactionInfo.blindingInfo.disclosure.getOrElse(nodeId, Set.empty) - if (exercise.consuming && transactionAccepted.isAcsDelta(exercise.targetCoid)) { - val additional_witnesses = witnesses.diff(exercise.stakeholders) - DbDto.consumingExerciseDbDtos( - event_offset = offset.unwrap, - update_id = transactionAccepted.updateId.toProtoPrimitive.toByteArray, - workflow_id = transactionAccepted.transactionMeta.workflowId, - command_id = transactionAccepted.completionInfoO.map(_.commandId), - submitters = transactionAccepted.completionInfoO.map(_.actAs.toSet), - record_time = transactionAccepted.recordTime.toMicros, - synchronizer_id = transactionAccepted.synchronizerId, - trace_context = serializedTraceContext, - external_transaction_hash = - transactionAccepted.externalTransactionHash.map(_.unwrap.toByteArray), - traffic_cost = transactionAccepted.paidTrafficCost.map(_.value), - event_sequential_id = 0, // this is filled later - node_id = nodeId.index, - deactivated_event_sequential_id = None, // this is filled later - additional_witnesses = additional_witnesses, - exercise_choice = exercise.qualifiedChoiceName.choiceName, - exercise_choice_interface_id = exercise.qualifiedChoiceName.interfaceId, - exercise_argument = - compressionStrategy.consumingExerciseArgumentCompression.compress(exerciseArgument), - exercise_result = - exerciseResult.map(compressionStrategy.consumingExerciseResultCompression.compress), - exercise_actors = exercise.actingParties, - exercise_last_descendant_node_id = lastDescendantNodeId.index, - exercise_argument_compression = compressionStrategy.consumingExerciseArgumentCompression.id, - exercise_result_compression = compressionStrategy.consumingExerciseResultCompression.id, - contract_id = exercise.targetCoid, - internal_contract_id = None, // this will be filled later - template_id = templateId, - package_id = exercise.templateId.packageId, - stakeholders = exercise.stakeholders, - ledger_effective_time = transactionAccepted.transactionMeta.ledgerEffectiveTime.micros, - ) - } else { - val internal_contract_id = - if (exercise.consuming) - transactionAccepted.contractInfos.get(exercise.targetCoid).map(_.internalContractId) - else None - val (argumentCompression, resultCompression) = - if (exercise.consuming) - ( - compressionStrategy.consumingExerciseArgumentCompression, - compressionStrategy.consumingExerciseResultCompression, - ) - else - ( - compressionStrategy.nonConsumingExerciseArgumentCompression, - compressionStrategy.nonConsumingExerciseResultCompression, - ) - DbDto.witnessedExercisedDbDtos( - event_offset = offset.unwrap, - update_id = transactionAccepted.updateId.toProtoPrimitive.toByteArray, - workflow_id = transactionAccepted.transactionMeta.workflowId, - command_id = transactionAccepted.completionInfoO.map(_.commandId), - submitters = transactionAccepted.completionInfoO.map(_.actAs.toSet), - record_time = transactionAccepted.recordTime.toMicros, - synchronizer_id = transactionAccepted.synchronizerId, - trace_context = serializedTraceContext, - external_transaction_hash = - transactionAccepted.externalTransactionHash.map(_.unwrap.toByteArray), - traffic_cost = transactionAccepted.paidTrafficCost.map(_.value), - event_sequential_id = 0, // this is filled later - node_id = nodeId.index, - additional_witnesses = witnesses, - consuming = exercise.consuming, - exercise_choice = exercise.qualifiedChoiceName.choiceName, - exercise_choice_interface_id = exercise.qualifiedChoiceName.interfaceId, - exercise_argument = argumentCompression.compress(exerciseArgument), - exercise_result = exerciseResult.map(resultCompression.compress), - exercise_actors = exercise.actingParties, - exercise_last_descendant_node_id = lastDescendantNodeId.index, - exercise_argument_compression = argumentCompression.id, - exercise_result_compression = resultCompression.id, - contract_id = exercise.targetCoid, - internal_contract_id = internal_contract_id, - template_id = templateId, - package_id = exercise.templateId.packageId, - ledger_effective_time = transactionAccepted.transactionMeta.ledgerEffectiveTime.micros, - ) - } - } - - private def reassignmentAcceptedToDbDto( - metrics: LedgerApiServerMetrics, - offset: Offset, - serializedTraceContext: Array[Byte], - reassignmentAccepted: ReassignmentAccepted, - )(implicit mc: MetricsContext): Iterator[DbDto] = { - withOptionalMetricLabels( - IndexerMetrics.Labels.userId -> reassignmentAccepted.optCompletionInfo.map( - _.userId - ) - ) { implicit mc: MetricsContext => - incrementCounterForEvent( - metrics.indexer, - IndexerMetrics.Labels.eventType.reassignment, - IndexerMetrics.Labels.status.accepted, - ) - } - - val events: Iterator[DbDto] = reassignmentAccepted.reassignment.iterator.flatMap { - case unassign: Reassignment.Unassign => - unassignToDbDto( - offset = offset, - serializedTraceContext = serializedTraceContext, - reassignmentAccepted = reassignmentAccepted, - unassign = unassign, - ) - - case assign: Reassignment.Assign => - assignToDbDto( - offset = offset, - serializedTraceContext = serializedTraceContext, - reassignmentAccepted = reassignmentAccepted, - assign = assign, - ) - } - - val completions: Option[DbDto] = - for { - completionInfo <- reassignmentAccepted.optCompletionInfo - } yield commandCompletion( - offset = offset, - recordTime = reassignmentAccepted.recordTime.toLf, - updateId = Some(reassignmentAccepted.updateId), - completionInfo = completionInfo, - synchronizerId = reassignmentAccepted.synchronizerId, - messageUuid = None, - serializedTraceContext = serializedTraceContext, - isTransaction = false, - ) - - val transactionMeta = DbDto.TransactionMeta( - update_id = reassignmentAccepted.updateId.toProtoPrimitive.toByteArray, - event_offset = offset.unwrap, - publication_time = 0, // this is filled later - record_time = reassignmentAccepted.recordTime.toMicros, - synchronizer_id = reassignmentAccepted.synchronizerId, - event_sequential_id_first = 0, // this is filled later - event_sequential_id_last = 0, // this is filled later - ) - - // TransactionMeta DTO must come last in this sequence - // because in a later stage the preceding events - // will be assigned consecutive event sequential ids - // and transaction meta is assigned sequential ids of its first and last event - events ++ completions.iterator ++ Iterator(transactionMeta) - } - - private def unassignToDbDto( - offset: Offset, - serializedTraceContext: Array[Byte], - reassignmentAccepted: ReassignmentAccepted, - unassign: Reassignment.Unassign, - ): Iterator[DbDto] = - DbDto.unassignDbDtos( - event_offset = offset.unwrap, - update_id = reassignmentAccepted.updateId.toProtoPrimitive.toByteArray, - command_id = reassignmentAccepted.optCompletionInfo.map(_.commandId), - workflow_id = reassignmentAccepted.workflowId, - submitter = reassignmentAccepted.reassignmentInfo.submitter, - record_time = reassignmentAccepted.recordTime.toMicros, - synchronizer_id = reassignmentAccepted.reassignmentInfo.sourceSynchronizer.unwrap, - trace_context = serializedTraceContext, - traffic_cost = reassignmentAccepted.paidTrafficCost.map(_.value), - event_sequential_id = 0L, // this is filled later - node_id = unassign.nodeId, - deactivated_event_sequential_id = None, // this is filled later - reassignment_id = reassignmentAccepted.reassignmentInfo.reassignmentId.toBytes.toByteArray, - assignment_exclusivity = unassign.assignmentExclusivity.map(_.micros), - target_synchronizer_id = reassignmentAccepted.reassignmentInfo.targetSynchronizer.unwrap, - reassignment_counter = unassign.reassignmentCounter, - contract_id = unassign.contractId, - internal_contract_id = None, // this is filled later - template_id = templateIdWithPackageName(unassign), - package_id = unassign.templateId.packageId, - stakeholders = unassign.stakeholders, - ) - - private def assignToDbDto( - offset: Offset, - serializedTraceContext: Array[Byte], - reassignmentAccepted: ReassignmentAccepted, - assign: Reassignment.Assign, - ): Iterator[DbDto] = - DbDto.assignDbDtos( - event_offset = offset.unwrap, - update_id = reassignmentAccepted.updateId.toProtoPrimitive.toByteArray, - workflow_id = reassignmentAccepted.workflowId, - command_id = reassignmentAccepted.optCompletionInfo.map(_.commandId), - submitter = reassignmentAccepted.reassignmentInfo.submitter, - record_time = reassignmentAccepted.recordTime.toMicros, - synchronizer_id = reassignmentAccepted.reassignmentInfo.targetSynchronizer.unwrap, - trace_context = serializedTraceContext, - traffic_cost = reassignmentAccepted.paidTrafficCost.map(_.value), - event_sequential_id = 0L, // this is filled later - node_id = assign.nodeId, - source_synchronizer_id = reassignmentAccepted.reassignmentInfo.sourceSynchronizer.unwrap, - reassignment_counter = assign.reassignmentCounter, - reassignment_id = reassignmentAccepted.reassignmentInfo.reassignmentId.toBytes.toByteArray, - representative_package_id = assign.createNode.templateId.packageId, - notPersistedContractId = assign.createNode.coid, - internal_contract_id = assign.internalContractId, - create_key_hash = assign.createNode.keyOpt.map(_.globalKey.hash.bytes.toHexString), - )( - stakeholders = assign.createNode.stakeholders, - template_id = templateIdWithPackageName(assign), - ) - - private def incrementCounterForEvent( - metrics: IndexerMetrics, - eventType: String, - status: String, - )(implicit - mc: MetricsContext - ): Unit = - withExtraMetricLabels( - IndexerMetrics.Labels.eventType.key -> eventType, - IndexerMetrics.Labels.status.key -> status, - ) { implicit mc => - metrics.eventsMeter.mark() - } - - private def commandCompletion( - offset: Offset, - recordTime: Time.Timestamp, - updateId: Option[UpdateId], - completionInfo: CompletionInfo, - synchronizerId: SynchronizerId, - messageUuid: Option[UUID], - isTransaction: Boolean, - serializedTraceContext: Array[Byte], - ): DbDto.CommandCompletion = { - val (deduplicationOffset, deduplicationDurationSeconds, deduplicationDurationNanos) = - completionInfo.optDeduplicationPeriod - .map { - case DeduplicationOffset(offset) => - ( - Some(offset.fold(0L)(_.unwrap)), - None, - None, - ) - case DeduplicationDuration(duration) => - (None, Some(duration.getSeconds), Some(duration.getNano)) - } - .getOrElse((None, None, None)) - - DbDto.CommandCompletion( - completion_offset = offset.unwrap, - record_time = recordTime.micros, - publication_time = 0L, // will be filled later - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = updateId.map(_.toProtoPrimitive.toByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = deduplicationOffset, - deduplication_duration_seconds = deduplicationDurationSeconds, - deduplication_duration_nanos = deduplicationDurationNanos, - synchronizer_id = synchronizerId, - message_uuid = messageUuid.map(_.toString), - is_transaction = isTransaction, - trace_context = serializedTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/VerifiedDataSource.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/VerifiedDataSource.scala deleted file mode 100644 index 7e0ef7cf3e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/VerifiedDataSource.scala +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.timer.RetryStrategy -import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} -import com.digitalasset.canton.platform.store.DbType -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Thereafter.syntax.* - -import javax.sql.DataSource -import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Using} - -/** Returns a DataSource that is guaranteed to be connected to a responsive, compatible database. */ -object VerifiedDataSource { - - private val MaxInitialConnectRetryAttempts: Int = 600 - - def apply(jdbcUrl: String, loggerFactory: NamedLoggerFactory)(implicit - executionContext: ExecutionContext, - traceContext: TraceContext, - ): Future[DataSource] = { - val dataSourceStorageBackend = - StorageBackendFactory - .of(dbType = DbType.jdbcType(jdbcUrl), loggerFactory = loggerFactory) - .createDataSourceStorageBackend - apply( - dataSourceStorageBackend, - DataSourceStorageBackend.DataSourceConfig(jdbcUrl), - loggerFactory, - ) - } - - def apply( - dataSourceStorageBackend: DataSourceStorageBackend, - dataSourceConfig: DataSourceStorageBackend.DataSourceConfig, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext, - traceContext: TraceContext, - ): Future[DataSource] = { - val logger = TracedLogger(loggerFactory.getLogger(getClass)) - for { - dataSource <- RetryStrategy.constant( - attempts = MaxInitialConnectRetryAttempts, - waitTime = 1.second, - ) { (i, _) => - Future { - val createdDatasource = - dataSourceStorageBackend.createDataSource(dataSourceConfig, loggerFactory) - logger.info( - s"Attempting to connect to the database (attempt $i/$MaxInitialConnectRetryAttempts)" - ) - Using.resource(createdDatasource.getConnection)( - dataSourceStorageBackend.checkDatabaseAvailable - ) - createdDatasource - }.thereafterP { case Failure(exception) => - logger.warn(exception.getMessage) - } - } - _ <- Future { - Using.resource(dataSource.getConnection)( - dataSourceStorageBackend.checkCompatibility - ) - } - } yield dataSource - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CommonRowDefs.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CommonRowDefs.scala deleted file mode 100644 index 887f93f11d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CommonRowDefs.scala +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.{byteArray, int, long, str} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.platform.store.backend.Conversions.{parties, timestampFromMicros} -import com.digitalasset.canton.platform.store.backend.RowDef.column -import com.digitalasset.canton.platform.store.backend.{Conversions, RowDef} -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.platform.{CommandId, Party} -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Time.Timestamp - -object CommonRowDefs { - // basic column types - def offset(offsetColumnName: String): RowDef[Offset] = - column(offsetColumnName, Conversions.offset) - def genSynchronizerId(columnName: String)( - stringInterning: StringInterning - ): RowDef[SynchronizerId] = - column(columnName, int(_).map(stringInterning.synchronizerId.externalize)) - def synchronizerId(stringInterning: StringInterning): RowDef[SynchronizerId] = - genSynchronizerId("synchronizer_id")(stringInterning) - - // update related - val updateId: RowDef[UpdateId] = column("update_id", Conversions.updateId) - val recordTime: RowDef[Timestamp] = column("record_time", timestampFromMicros) - val traceContext: RowDef[Array[Byte]] = column("trace_context", byteArray(_)) - val commandId: RowDef[CommandId] = column("command_id", str).map(CommandId.assertFromString) - val trafficCost: RowDef[Long] = column("traffic_cost", long) - def submitters(stringInterning: StringInterning): RowDef[Seq[Party]] = - column("submitters", parties(stringInterning)(_)) - val publicationTime: RowDef[Timestamp] = column("publication_time", timestampFromMicros) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CommonStorageBackendFactory.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CommonStorageBackendFactory.scala deleted file mode 100644 index df2fdd1704..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CommonStorageBackendFactory.scala +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.platform.store.backend.* -import com.digitalasset.canton.platform.store.backend.localstore.{ - IdentityProviderStorageBackend, - IdentityProviderStorageBackendImpl, - PartyRecordStorageBackend, - PartyRecordStorageBackendImpl, - UserManagementStorageBackend, - UserManagementStorageBackendImpl, -} - -trait CommonStorageBackendFactory extends StorageBackendFactory { - - override val createIntegrityStorageBackend: IntegrityStorageBackend = - IntegrityStorageBackendImpl - - override val createStringInterningStorageBackend: StringInterningStorageBackend = - StringInterningStorageBackendImpl - - override val createUserManagementStorageBackend: UserManagementStorageBackend = - UserManagementStorageBackendImpl - - override val createIdentityProviderConfigStorageBackend: IdentityProviderStorageBackend = - IdentityProviderStorageBackendImpl - - override def createPartyRecordStorageBackend: PartyRecordStorageBackend = - PartyRecordStorageBackendImpl - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CompletionStorageBackendTemplate.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CompletionStorageBackendTemplate.scala deleted file mode 100644 index 46be18d0e0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/CompletionStorageBackendTemplate.scala +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.* -import anorm.{Row, SimpleSql} -import cats.syntax.all.* -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.platform.v1.index.StatusDetails -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.indexer.parallel.{PostPublishData, PublishSource} -import com.digitalasset.canton.platform.store.CompletionFromTransaction -import com.digitalasset.canton.platform.store.backend.RowDef.column -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.{ - CompletionStorageBackend, - Conversions, - RowDef, -} -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.platform.{Party, SubmissionId, UserId} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.google.protobuf.any -import com.google.rpc.status.Status as StatusProto - -import java.sql.Connection -import java.util.UUID - -class CompletionStorageBackendTemplate( - stringInterning: StringInterning, - val loggerFactory: NamedLoggerFactory, -) extends CompletionStorageBackend - with NamedLogging { - - object RowDefs { - import CommonRowDefs.* - - def userId(stringInterning: StringInterning): RowDef[UserId] = - column("user_id", int).map(stringInterning.userId.externalize) - val messageUuid: RowDef[UUID] = column("message_uuid", str).map(UUID.fromString) - val submissionId: RowDef[SubmissionId] = - column("submission_id", str).map(SubmissionId.assertFromString) - val completionOffset: RowDef[Offset] = column("completion_offset", Conversions.offset) - - // rejection status - private val rejectionStatusCode: RowDef[Int] = column("rejection_status_code", int) - private val rejectionStatusMessage: RowDef[String] = column("rejection_status_message", str) - private val rejectionStatusDetails: RowDef[Option[Array[Byte]]] = - column("rejection_status_details", byteArray).? - private val rejectionStatus: RowDef[StatusProto] = - (rejectionStatusCode, rejectionStatusMessage, rejectionStatusDetails).mapN(buildStatusProto) - - // deduplication offset - val deduplicationOffset: RowDef[Option[Long]] = column("deduplication_offset", long).? - val deduplicationDurationSeconds: RowDef[Option[Long]] = - column("deduplication_duration_seconds", long).? - val deduplicationDurationNanos: RowDef[Option[Int]] = - column("deduplication_duration_nanos", int).? - - // post publish related - private val isTransaction: RowDef[Boolean] = column("is_transaction", bool) - - private val publishSource: RowDef[PublishSource] = - (messageUuid.?, recordTime).mapN(publishSourceFromColumns) - - private def commandCompletionSharedColumns( - stringInterning: StringInterning, - parties: Set[Party], - ): RowDef[CompletionFromTransaction.CommonCompletionProperties] = ( - submitters(stringInterning).map(_.view.filter(parties).toSet[String]), - recordTime, - completionOffset, - commandId, - userId(stringInterning), - submissionId.?, - synchronizerId(stringInterning).map(_.toProtoPrimitive), - traceContext.map(Conversions.protoTraceContextFrom(noTracingLogger)), - trafficCost.?.map(_.getOrElse(0L)), - deduplicationOffset, - deduplicationDurationSeconds, - deduplicationDurationNanos, - ).mapN( - CompletionFromTransaction.CommonCompletionProperties.createFromRecordTimeAndSynchronizerId - ) - - def commandCompletionParser( - parties: Set[Party] - ): RowDef[CompletionStreamResponse] = updateId.?.map(_.isDefined).branch( - (true, acceptedCommand(parties)), - (false, rejectedCommand(parties)), - ) - - private def acceptedCommand( - parties: Set[Party] - ): RowDef[CompletionStreamResponse] = ( - commandCompletionSharedColumns(stringInterning, parties), - updateId, - ).mapN(CompletionFromTransaction.acceptedCompletion) - - private def rejectedCommand( - parties: Set[Party] - ): RowDef[CompletionStreamResponse] = ( - commandCompletionSharedColumns(stringInterning, parties), - rejectionStatus, - ).mapN(CompletionFromTransaction.rejectedCompletion) - - private def postPublishDataForTransactionParser: RowDef[PostPublishData] = ( - synchronizerId(stringInterning), - publishSource, - userId(stringInterning), - commandId, - submitters(stringInterning).map(_.toSet), - completionOffset, - publicationTime.map(CantonTimestamp.apply), - submissionId.?, - updateId.?.map(_.isDefined), - traceContext.map(Conversions.traceContextFrom(noTracingLogger)), - ).mapN(PostPublishData.apply) - - def postPublishDataParser: RowDef[Option[PostPublishData]] = isTransaction.branch( - (true, postPublishDataForTransactionParser.map(Some(_))), - (false, RowDef.static(None)), - ) - - private def publishSourceFromColumns(messageUuid: Option[UUID], recordTime: Timestamp) = - messageUuid - .map(PublishSource.Local(_): PublishSource) - .getOrElse( - PublishSource.Sequencer( - CantonTimestamp(recordTime) - ) - ) - } - - override def commandCompletions( - startInclusive: Offset, - endInclusive: Offset, - userId: UserId, - parties: Set[Party], - limit: Int, - )(connection: Connection): Vector[CompletionStreamResponse] = { - import ComposableQuery.* - if (parties.isEmpty) { - Vector.empty - } else { - stringInterning.userId.tryInternalize(userId) match { - case Some(internedUserId) => - val query = (columns: CompositeSql) => - SQL""" - SELECT - $columns - FROM - lapi_command_completions - WHERE - ${QueryStrategy.offsetIsBetween( - nonNullableColumn = "completion_offset", - startInclusive = startInclusive, - endInclusive = endInclusive, - )} AND - user_id = $internedUserId - ORDER BY completion_offset ASC - ${QueryStrategy.limitClause(Some(limit))}""" - - RowDefs.commandCompletionParser(parties).queryMultipleRows(query)(connection).collect { - case response if response.getCompletion.actAs.nonEmpty => - response - } - case None => Vector.empty - } - } - } - - private def buildStatusProto( - rejectionStatusCode: Int, - rejectionStatusMessage: String, - rejectionStatusDetails: Option[Array[Byte]], - ): StatusProto = - StatusProto.of( - rejectionStatusCode, - rejectionStatusMessage, - parseRejectionStatusDetails(rejectionStatusDetails), - ) - - private def parseRejectionStatusDetails( - rejectionStatusDetails: Option[Array[Byte]] - ): Seq[any.Any] = - rejectionStatusDetails - .map(StatusDetails.parseFrom) - .map(_.details) - .getOrElse(Seq.empty) - - override def pruneCompletions( - pruneUpToInclusive: Offset - )(connection: Connection, traceContext: TraceContext): Unit = - pruneWithLogging(queryDescription = "Command completions pruning") { - import com.digitalasset.canton.platform.store.backend.Conversions.OffsetToStatement - SQL"delete from lapi_command_completions where completion_offset <= $pruneUpToInclusive" - }(connection, traceContext) - - private def pruneWithLogging(queryDescription: String)(query: SimpleSql[Row])( - connection: Connection, - traceContext: TraceContext, - ): Unit = { - val deletedRows = query.executeUpdate()(connection) - logger.info(s"$queryDescription finished: deleted $deletedRows rows.")( - traceContext - ) - } - - override def commandCompletionsForRecovery( - startInclusive: Offset, - endInclusive: Offset, - )(connection: Connection): Vector[PostPublishData] = { - import ComposableQuery.* - def query(columns: CompositeSql) = SQL""" - SELECT - $columns - FROM - lapi_command_completions - WHERE - ${QueryStrategy.offsetIsBetween( - nonNullableColumn = "completion_offset", - startInclusive = startInclusive, - endInclusive = endInclusive, - )} - ORDER BY completion_offset ASC""" - - RowDefs.postPublishDataParser.queryMultipleRows(query)(connection).flatten - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ComposableQuery.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ComposableQuery.scala deleted file mode 100644 index 9efe64a00c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ComposableQuery.scala +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.{ParameterValue, Row, SimpleSql, ToParameterValue} - -import scala.collection.mutable - -object ComposableQuery { - - sealed trait QueryPart - object QueryPart { - import scala.language.implicitConversions - implicit def from[A](a: A)(implicit c: ToParameterValue[A]): SingleParameter = - SingleParameter(c(a)) - } - - final case class SingleParameter(parameterValue: ParameterValue) extends QueryPart - final case class CompositeSql(stringParts: Seq[String], valueParts: Seq[QueryPart]) - extends QueryPart { - assert( - stringParts.size - 1 == valueParts.size, - s"contract of CompositePart violated: the size of StringParts must be one bigger than the size of valueParts", - ) - } - - implicit class SqlStringInterpolation(val sc: StringContext) extends AnyVal { - def SQL(args: QueryPart*): SimpleSql[Row] = { - val (stringParts, valueParts) = flattenComposite(sc.parts, args) - - anorm - .SqlStringInterpolation(StringContext(stringParts*)) - .SQL(valueParts*) - } - - def cSQL(args: QueryPart*): CompositeSql = CompositeSql(sc.parts, args) - } - - @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) - private[common] def flattenComposite( - stringContextParts: Iterable[String], - values: Iterable[QueryPart], - ): (Seq[String], Seq[ParameterValue]) = { - val stringParts = mutable.ArrayBuffer.empty[String] - val valueParts = mutable.ArrayBuffer.empty[ParameterValue] - // need to maintain StringContext contract: string parts always have size 1 bigger than values - def addStringPart(stringPart: String): Unit = - if (stringParts.sizeIs > valueParts.size) { - stringParts.update(stringParts.size - 1, stringParts.last + stringPart) - } else - stringParts += stringPart - def go( - stringPartsIterator: Iterator[String], - valuePartsIterator: Iterator[QueryPart], - ): Unit = { - stringPartsIterator.zip(valuePartsIterator).foreach { - case (prefix, SingleParameter(parameterValue)) => - addStringPart(prefix) - valueParts += parameterValue - - case (prefix, CompositeSql(strings, values)) => - addStringPart(prefix) - go(strings.iterator, values.iterator) - } - stringPartsIterator.foreach(addStringPart) - } - - go(stringContextParts.iterator, values.iterator) - - (stringParts.toSeq, valueParts.toSeq) - } - - implicit class CompositConcatenationOps(val composits: Iterable[CompositeSql]) extends AnyVal { - def mkComposite(start: String, sep: String, end: String): CompositeSql = { - require(composits.nonEmpty, "composits must be non-empty") - CompositeSql( - stringParts = start :: List.fill(composits.size - 1)(sep) ::: List(end), - valueParts = composits.toSeq, - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ContractStorageBackendTemplate.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ContractStorageBackendTemplate.scala deleted file mode 100644 index 237b99617e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ContractStorageBackendTemplate.scala +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.{bool, long} -import anorm.~ -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.`SimpleSql ops` -import com.digitalasset.canton.platform.store.backend.{ContractStorageBackend, PersistentEventType} -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.topology.SynchronizerId - -import java.sql.Connection - -class ContractStorageBackendTemplate( - queryStrategy: QueryStrategy, - stringInterning: StringInterning, - ledgerEndCache: LedgerEndCache, -) extends ContractStorageBackend { - - override def activeContracts(internalContractIds: Seq[Long], beforeEventSeqId: Long)( - connection: Connection - ): Map[Long, Boolean] = - if (internalContractIds.isEmpty) Map.empty - else { - SQL""" - SELECT - internal_contract_id, - NOT EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract - WHERE - internal_contract_id = lapi_events_activate_contract.internal_contract_id - AND event_sequential_id <= $beforeEventSeqId - AND event_type = ${PersistentEventType.ConsumingExercise.asInt} - LIMIT 1 - ) active - FROM lapi_events_activate_contract - WHERE - internal_contract_id ${queryStrategy.anyOf(internalContractIds)} - AND event_sequential_id <= $beforeEventSeqId""" - .asVectorOf(long("internal_contract_id") ~ bool("active"))(connection) - .view - .map { case internalContractId ~ active => - internalContractId -> active - } - .toMap - } - - override def lastActivations(synchronizerContracts: Iterable[(SynchronizerId, Long)])( - connection: Connection - ): Map[(SynchronizerId, Long), Long] = - ledgerEndCache() - .map { ledgerEnd => - synchronizerContracts.iterator.flatMap { case (synchronizerId, internalContractId) => - val internedSynchronizerId = stringInterning.synchronizerId.internalize(synchronizerId) - SQL""" - SELECT event_sequential_id - FROM lapi_events_activate_contract as activate - WHERE - internal_contract_id = $internalContractId AND - event_sequential_id <= ${ledgerEnd.lastEventSeqId} AND - EXISTS ( -- subquery for triggering (event_sequential_id) INCLUDE (synchronizer_id) index usage - SELECT 1 - FROM lapi_events_activate_contract as activate2 - WHERE - activate2.event_sequential_id = activate.event_sequential_id AND - activate2.synchronizer_id = $internedSynchronizerId - ) - ORDER BY event_sequential_id DESC - LIMIT 1""" - .as(long("event_sequential_id").singleOpt)(connection) - .map((synchronizerId, internalContractId) -> _) - }.toMap - } - .getOrElse(Map.empty) - - override def supportsBatchKeyStateLookups: Boolean = false - - override def contractKey( - keyPageQuery: ContractStorageBackend.KeysPageQuery - )(connection: Connection): ContractStorageBackend.KeysPageResult = { - import com.digitalasset.canton.platform.store.backend.Conversions.HashToStatement - val eventSeqIdUpperBoundInclusive = - keyPageQuery.nextPageToken - .map(_ - 1) // making the exclusive token the inclusive bound - .getOrElse(keyPageQuery.validAtEventSeqId) - val (eventSeqIds, internalContractIds) = SQL""" - SELECT event_sequential_id, internal_contract_id - FROM lapi_events_activate_contract - WHERE - create_key_hash = ${keyPageQuery.key.hash} - AND event_sequential_id <= $eventSeqIdUpperBoundInclusive - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract - WHERE - deactivated_event_sequential_id = lapi_events_activate_contract.event_sequential_id - AND lapi_events_deactivate_contract.event_sequential_id <= ${keyPageQuery.validAtEventSeqId} - ) - ORDER BY event_sequential_id DESC - ${QueryStrategy.limitClause(Some(keyPageQuery.limit + 1))}""" - .asVectorOf( - long("event_sequential_id") ~ long("internal_contract_id") map { - case eventSeqId ~ internalContractId => (eventSeqId -> internalContractId) - } - )(connection) - .unzip - ContractStorageBackend.KeysPageResult( - internalContractIds = - // we asked for limit plus 1 - internalContractIds.take(keyPageQuery.limit), - nextPageToken = Option - // we asked for one more, so there is only make sense to continue if there is limit+1 results - // and then the exclusive token should be one above the identified one - // note: subsequent query still can return empty in case validAtEventSeqId increased and this - // causes all potential activations in the next page to be inactivated. - .when(eventSeqIds.sizeIs == keyPageQuery.limit + 1)( - eventSeqIds.lastOption.map(_ + 1) - ) - .flatten, - ) - } - - override def contractKeysPlain( - keyPageQueries: Seq[ContractStorageBackend.KeysPageQuery], - validAtEventSeqId: Long, - )(connection: Connection): Seq[ContractStorageBackend.KeysPageResult] = - keyPageQueries.map(query => - contractKey(query.copy(validAtEventSeqId = validAtEventSeqId))(connection) - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/DataSourceStorageBackendImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/DataSourceStorageBackendImpl.scala deleted file mode 100644 index 55b92d8467..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/DataSourceStorageBackendImpl.scala +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.get -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - -import java.sql.Connection - -private[backend] object DataSourceStorageBackendImpl { - - def exe(statement: String): Connection => Unit = { implicit connection => - SQL"#$statement".execute().discard - } - - def checkDatabaseAvailable(connection: Connection): Unit = - assert(SQL"SELECT 1".as(get[Int](1).single)(connection) == 1) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/EventReaderQueries.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/EventReaderQueries.scala deleted file mode 100644 index f2c7d0dc90..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/EventReaderQueries.scala +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{ - RawArchivedEvent, - RawThinCreatedEvent, -} -import com.digitalasset.canton.platform.store.backend.PersistentEventType -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.{ - CompositeSql, - SqlStringInterpolation, -} -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.daml.lf.data.Ref.Party - -import java.sql.Connection - -class EventReaderQueries(stringInterning: StringInterning) { - import EventStorageBackendTemplate.* - - type EventSequentialId = Long - - def fetchContractIdEvents( - internalContractId: Long, - requestingParties: Option[Set[Party]], - endEventSequentialId: EventSequentialId, - )( - connection: Connection - ): (Option[RawThinCreatedEvent], Option[RawArchivedEvent]) = { - def queryByInternalContractId( - tableName: String, - eventType: PersistentEventType, - ascending: Boolean, - )(columns: CompositeSql) = - SQL""" - SELECT $columns - FROM #$tableName - WHERE - internal_contract_id = $internalContractId - AND event_sequential_id <= $endEventSequentialId - AND event_type = ${eventType.asInt} - ORDER BY event_sequential_id #${if (ascending) "ASC" else "DESC"} - LIMIT 1 - """ - - def lookupActivateCreated: Option[RawThinCreatedEvent] = - RowDefs - .rawThinCreatedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingParties, - witnessIsAcsDelta = true, - eventIsAcsDeltaForParticipant = true, - ) - .querySingleOptRow( - queryByInternalContractId( - tableName = "lapi_events_activate_contract", - eventType = PersistentEventType.Create, - ascending = true, - ) - )(connection) - - def lookupDeactivateArchived: Option[RawArchivedEvent] = - RowDefs - .rawArchivedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingParties, - acsDeltaForParticipant = true, - ) - .querySingleOptRow( - queryByInternalContractId( - tableName = "lapi_events_deactivate_contract", - eventType = PersistentEventType.ConsumingExercise, - ascending = false, - ) - )(connection) - - def lookupWitnessedCreated: Option[RawThinCreatedEvent] = - RowDefs - .rawThinCreatedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingParties, - witnessIsAcsDelta = true, - eventIsAcsDeltaForParticipant = false, - ) - .querySingleOptRow( - queryByInternalContractId( - tableName = "lapi_events_various_witnessed", - eventType = PersistentEventType.WitnessedCreate, - ascending = true, - ) - )(connection) - - def lookupTransientArchived(createOffset: Long): Option[RawArchivedEvent] = - RowDefs - .rawArchivedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingParties, - acsDeltaForParticipant = false, - ) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_events_various_witnessed - WHERE - internal_contract_id = $internalContractId - AND event_sequential_id <= $endEventSequentialId - AND event_type = ${PersistentEventType.WitnessedConsumingExercise.asInt} - AND event_offset = $createOffset - ORDER BY event_sequential_id - LIMIT 1 - """)(connection) - - lookupActivateCreated - .map(create => Some(create) -> lookupDeactivateArchived) - .orElse( - lookupWitnessedCreated.flatMap(create => - lookupTransientArchived( - create.transactionProperties.commonEventProperties.offset - ).map(transientArchive => Some(create) -> Some(transientArchive)) - ) - ) - .getOrElse(None -> None) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/EventStorageBackendTemplate.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/EventStorageBackendTemplate.scala deleted file mode 100644 index 7b1b32b59a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/EventStorageBackendTemplate.scala +++ /dev/null @@ -1,1492 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.* -import anorm.{Row, RowParser, SimpleSql, ~} -import cats.syntax.all.* -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.store.backend.Conversions.{ - contractId, - parties, - timestampFromMicros, -} -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.* -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsAddActivationsParams, - AchsRemoveDeactivatedParams, -} -import com.digitalasset.canton.platform.store.backend.RowDef.* -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.{ - CompositeSql, - SqlStringInterpolation, -} -import com.digitalasset.canton.platform.store.backend.common.QueryStrategy.withoutNetworkTimeout -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* -import com.digitalasset.canton.platform.store.backend.{ - Conversions, - EventStorageBackend, - PersistentEventType, - RowDef, -} -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.IdPageQuery -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.platform.{ContractId, Party} -import com.digitalasset.canton.protocol.ReassignmentId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{ - ChoiceName, - FullIdentifier, - Identifier, - NameTypeConRefConverter, -} -import com.digitalasset.daml.lf.data.Time.Timestamp - -import java.sql.{Connection, PreparedStatement} -import scala.util.Using - -object EventStorageBackendTemplate { - - private val MaxBatchSizeOfIncompleteReassignmentOffsetTempTablePopulation: Int = 500 - - object RowDefs { - import CommonRowDefs.* - - // update related - val workflowId: RowDef[Option[String]] = - column("workflow_id", str(_).?) - - def sourceSynchronizerId(stringInterning: StringInterning): RowDef[SynchronizerId] = - genSynchronizerId("source_synchronizer_id")(stringInterning) - - def targetSynchronizerId(stringInterning: StringInterning): RowDef[SynchronizerId] = - genSynchronizerId("target_synchronizer_id")(stringInterning) - - val eventOffset: RowDef[Offset] = - offset("event_offset") - val updateIdDef: RowDef[String] = - updateId.map(_.toHexString) - - def commandId( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[Option[String]] = - ( - CommonRowDefs.commandId.?, - submitters(stringInterning).?, - ).mapN(filteredCommandId(_, _, allQueryingPartiesO)) - - val externalTransactionHash: RowDef[Option[Array[Byte]]] = - column("external_transaction_hash", byteArray(_).?) - - def trafficCost( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[Option[Long]] = - ( - CommonRowDefs.trafficCost.?, - submitters(stringInterning).?, - ).mapN(filteredTrafficCost(_, _, allQueryingPartiesO)) - - // event related - val nodeId: RowDef[Int] = - column("node_id", int) - val eventSequentialId: RowDef[Long] = - column("event_sequential_id", long) - - def filteredAdditionalWitnesses( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - )(witnessIsAcsDelta: Boolean): RowDef[Set[String]] = - if (witnessIsAcsDelta) - static(Set.empty) - else - column("additional_witnesses", parties(stringInterning)(_)) - .map(filterWitnesses(allQueryingPartiesO, _)) - - val eventType: RowDef[PersistentEventType] = - column("event_type", int(_).map(PersistentEventType.fromInt)) - val deactivatedEventSeqId: RowDef[Option[Long]] = - column("deactivated_event_sequential_id", long(_).?) - - // contract related - def representativePackageId(stringInterning: StringInterning): RowDef[Ref.PackageId] = - column("representative_package_id", int(_).map(stringInterning.packageId.externalize)) - - val contractIdDef: RowDef[ContractId] = - column("contract_id", contractId) - val internalContractId: RowDef[Long] = - column("internal_contract_id", long) - val reassignmentCounter: RowDef[Long] = - column("reassignment_counter", long(_).?.map(_.getOrElse(0L))) - val ledgerEffectiveTime: RowDef[Timestamp] = - column("ledger_effective_time", timestampFromMicros) - - def templateId(stringInterning: StringInterning): RowDef[FullIdentifier] = - ( - column("template_id", int(_).map(stringInterning.templateId.externalize)), - column("package_id", int(_).map(stringInterning.packageId.externalize)), - ).mapN(_ toFullIdentifier _) - - def filteredStakeholderParties( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[Set[String]] = - // stakeholders are not present in various_witnessed, but exercises and transient/divulged contracts retrieved from there - column("stakeholders", parties(stringInterning)(_).?) - .map(_.getOrElse(Seq.empty)) - .map(filterWitnesses(allQueryingPartiesO, _)) - - // reassignment related - val reassignmentId: RowDef[String] = - column( - "reassignment_id", - byteArray(_).map(ReassignmentId.assertFromBytes(_).toProtoPrimitive), - ) - - def submitter(stringInterning: StringInterning): RowDef[Option[String]] = - column("submitters", parties(stringInterning)(_).?.map(_.getOrElse(Seq.empty).headOption)) - - val assignmentExclusivity: RowDef[Option[Timestamp]] = - column("assignment_exclusivity", timestampFromMicros(_).?) - - // exercise related - val consuming: RowDef[Boolean] = - column("consuming", bool(_)) - - def exerciseChoice(stringInterning: StringInterning): RowDef[ChoiceName] = - column("exercise_choice", int(_).map(stringInterning.choiceName.externalize)) - - def exerciseChoiceInterface(stringInterning: StringInterning): RowDef[Option[Identifier]] = - column( - "exercise_choice_interface", - int(_).?.map(_.map(stringInterning.interfaceId.externalize)), - ) - - val exerciseArgument: RowDef[Array[Byte]] = - column("exercise_argument", byteArray(_)) - val exerciseArgumentCompression: RowDef[Option[Int]] = - column("exercise_argument_compression", int(_).?) - val exerciseResult: RowDef[Option[Array[Byte]]] = - column("exercise_result", byteArray(_).?) - val exerciseResultCompression: RowDef[Option[Int]] = - column("exercise_result_compression", int(_).?) - - def exerciseActors(stringInterning: StringInterning): RowDef[Set[String]] = - column("exercise_actors", parties(stringInterning)(_).map(_.map(_.toString).toSet)) - - val exerciseLastDescendantNodeId: RowDef[Int] = - column("exercise_last_descendant_node_id", int) - - // party related - def partyId(stringInterning: StringInterning) = - column("party_id", int).map(stringInterning.party.externalize) - - // participant related - def participantId(stringInterning: StringInterning) = - column("participant_id", int).map(stringInterning.participantId.externalize) - - // properties - def commonEventPropertiesParser( - stringInterning: StringInterning - ): RowDef[CommonEventProperties] = - ( - eventSequentialId, - eventOffset.map(_.unwrap), - nodeId, - workflowId, - synchronizerId(stringInterning).map(_.toProtoPrimitive), - ).mapN(CommonEventProperties.apply) - - def commonUpdatePropertiesParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[CommonUpdateProperties] = - ( - updateIdDef, - commandId(stringInterning, allQueryingPartiesO), - traceContext, - recordTime, - trafficCost(stringInterning, allQueryingPartiesO), - ).mapN(CommonUpdateProperties.apply) - - def transactionPropertiesParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[TransactionProperties] = - ( - commonEventPropertiesParser(stringInterning), - commonUpdatePropertiesParser(stringInterning, allQueryingPartiesO), - externalTransactionHash, - ).mapN(TransactionProperties.apply) - - def reassignmentPropertiesParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[ReassignmentProperties] = - ( - commonEventPropertiesParser(stringInterning), - commonUpdatePropertiesParser(stringInterning, allQueryingPartiesO), - reassignmentId, - submitter(stringInterning), - reassignmentCounter, - ).mapN(ReassignmentProperties.apply) - - def thinCreatedEventPropertiesParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - witnessIsAcsDelta: Boolean, - eventIsAcsDeltaForParticipant: Boolean, - ): RowDef[ThinCreatedEventProperties] = - ( - representativePackageId(stringInterning), - filteredAdditionalWitnesses(stringInterning, allQueryingPartiesO)(witnessIsAcsDelta), - internalContractId, - static(allQueryingPartiesO.map(_.map(_.toString))), - if (eventIsAcsDeltaForParticipant) reassignmentCounter else static(0L), - static(eventIsAcsDeltaForParticipant), - ).mapN(ThinCreatedEventProperties.apply) - - // raws - def rawThinActiveContractParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[RawThinActiveContract] = - ( - commonEventPropertiesParser(stringInterning), - thinCreatedEventPropertiesParser( - stringInterning = stringInterning, - allQueryingPartiesO = allQueryingPartiesO, - witnessIsAcsDelta = true, - eventIsAcsDeltaForParticipant = true, - ), - ).mapN(RawThinActiveContract.apply) - - def rawThinCreatedEventParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - witnessIsAcsDelta: Boolean, - eventIsAcsDeltaForParticipant: Boolean, - ): RowDef[RawThinCreatedEvent] = - ( - transactionPropertiesParser(stringInterning, allQueryingPartiesO), - thinCreatedEventPropertiesParser( - stringInterning = stringInterning, - allQueryingPartiesO = allQueryingPartiesO, - witnessIsAcsDelta = witnessIsAcsDelta, - eventIsAcsDeltaForParticipant = eventIsAcsDeltaForParticipant, - ), - ).mapN(RawThinCreatedEvent.apply) - - def rawThinAssignEventParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[RawThinAssignEvent] = - ( - reassignmentPropertiesParser(stringInterning, allQueryingPartiesO), - thinCreatedEventPropertiesParser( - stringInterning = stringInterning, - allQueryingPartiesO = allQueryingPartiesO, - witnessIsAcsDelta = true, - eventIsAcsDeltaForParticipant = true, - ), - sourceSynchronizerId(stringInterning).map(_.toProtoPrimitive), - ).mapN(RawThinAssignEvent.apply) - - def rawArchivedEventParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - acsDeltaForParticipant: Boolean, - ): RowDef[RawArchivedEvent] = - ( - transactionPropertiesParser(stringInterning, allQueryingPartiesO), - contractIdDef, - templateId(stringInterning), - if (acsDeltaForParticipant) filteredStakeholderParties(stringInterning, allQueryingPartiesO) - else static(Set.empty[String]), - ledgerEffectiveTime, - if (acsDeltaForParticipant) deactivatedEventSeqId - else static(None), - ).mapN(RawArchivedEvent.apply) - - def rawExercisedEventParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - eventIsAcsDeltaForParticipant: Boolean, - ): RowDef[RawExercisedEvent] = - ( - transactionPropertiesParser(stringInterning, allQueryingPartiesO), - contractIdDef, - templateId(stringInterning), - if (eventIsAcsDeltaForParticipant) static(true) else consuming, - exerciseChoice(stringInterning), - exerciseChoiceInterface(stringInterning), - exerciseArgument, - exerciseArgumentCompression, - exerciseResult, - exerciseResultCompression, - exerciseActors(stringInterning), - exerciseLastDescendantNodeId, - filteredAdditionalWitnesses(stringInterning, allQueryingPartiesO)(witnessIsAcsDelta = - false - ), - if (eventIsAcsDeltaForParticipant) - filteredStakeholderParties(stringInterning, allQueryingPartiesO) - else static(Set.empty[String]), - ledgerEffectiveTime, - if (eventIsAcsDeltaForParticipant) deactivatedEventSeqId - else static(Option.empty[Long]), - static(eventIsAcsDeltaForParticipant), - ).mapN(RawExercisedEvent.apply) - - def rawUnassignEventParser( - stringInterning: StringInterning, - allQueryingPartiesO: Option[Set[Party]], - ): RowDef[RawUnassignEvent] = - ( - reassignmentPropertiesParser(stringInterning, allQueryingPartiesO), - contractIdDef, - templateId(stringInterning), - filteredStakeholderParties(stringInterning, allQueryingPartiesO), - assignmentExclusivity, - targetSynchronizerId(stringInterning).map(_.toProtoPrimitive), - deactivatedEventSeqId, - ).mapN(RawUnassignEvent.apply) - - private def authorizationEventParser( - authorizationLevelColumnName: String, - authorizationEventTypeColumnName: String, - ): RowDef[AuthorizationEvent] = - ( - column(authorizationEventTypeColumnName, int), - column(authorizationLevelColumnName, int), - ).mapN(Conversions.authorizationEvent) - - def partyToParticipantEventParser( - stringInterning: StringInterning - ): RowDef[RawParticipantAuthorization] = - ( - eventOffset, - updateIdDef, - partyId(stringInterning), - participantId(stringInterning), - authorizationEventParser("participant_permission", "participant_authorization_event"), - recordTime, - synchronizerId(stringInterning).map(_.toProtoPrimitive), - traceContext, - ).mapN( - RawParticipantAuthorization.apply - ) - - private def synchronizerOffsetParser( - offsetColumnName: String, - stringInterning: StringInterning, - ): RowDef[SynchronizerOffset] = - ( - offset(offsetColumnName), - synchronizerId(stringInterning), - recordTime, - publicationTime, - ).mapN(SynchronizerOffset.apply) - - def completionSynchronizerOffsetParser( - stringInterning: StringInterning - ): RowDef[SynchronizerOffset] = - synchronizerOffsetParser("completion_offset", stringInterning) - - def metaSynchronizerOffsetParser( - stringInterning: StringInterning - ): RowDef[SynchronizerOffset] = - synchronizerOffsetParser("event_offset", stringInterning) - } - - val EventSequentialIdFirstLast: RowParser[(Long, Long)] = - long("event_sequential_id_first") ~ long("event_sequential_id_last") map { - case event_sequential_id_first ~ event_sequential_id_last => - (event_sequential_id_first, event_sequential_id_last) - } - - private def filterWitnesses( - allQueryingPartiesO: Option[Set[Party]], - witnesses: Seq[Party], - ): Set[String] = - allQueryingPartiesO - .fold(witnesses)(allQueryingParties => - witnesses - .filter(allQueryingParties) - ) - .toSet - - def submittersInQueryingParties( - allQueryingPartiesO: Option[Set[Party]], - submitters: Option[Seq[Party]], - ): Boolean = allQueryingPartiesO match { - case Some(allQueryingParties) => - submitters.getOrElse(Seq.empty).exists(allQueryingParties) - case None => submitters.nonEmpty - } - - private def filteredCommandId( - commandId: Option[String], - submitters: Option[Seq[Party]], - allQueryingPartiesO: Option[Set[Party]], - ): Option[String] = - commandId - .filter(_ != "") - .filter(_ => submittersInQueryingParties(allQueryingPartiesO, submitters)) - - /** Filter the traffic cost value according to the submitting party: If the value is None, the - * cost is unknown, so we stick with that If the value is Some(cost) and the querying party is a - * submitting party, keep the cost Otherwise, set the cost to Some(0L) - */ - private def filteredTrafficCost( - trafficCost: Option[Long], - submitters: Option[Seq[Party]], - allQueryingPartiesO: Option[Set[Party]], - ): Option[Long] = - trafficCost - .filter(_ => submittersInQueryingParties(allQueryingPartiesO, submitters)) -} - -abstract class EventStorageBackendTemplate( - queryStrategy: QueryStrategy, - ledgerEndCache: LedgerEndCache, - stringInterning: StringInterning, - val loggerFactory: NamedLoggerFactory, -) extends EventStorageBackend - with NamedLogging { - import EventStorageBackendTemplate.* - import com.digitalasset.canton.platform.store.backend.Conversions.OffsetToStatement - - override def updatePointwiseQueries: UpdatePointwiseQueries = - new UpdatePointwiseQueries(ledgerEndCache) - - override def updateStreamingQueries: UpdateStreamingQueries = - new UpdateStreamingQueries(stringInterning, queryStrategy) - - override def eventReaderQueries: EventReaderQueries = - new EventReaderQueries(stringInterning) - - override def pruneEvents( - previousPruneUpToInclusiveOffset: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusiveOffset: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit connection: Connection, traceContext: TraceContext): Unit = { - // all of these should execute in a single transaction - assert(!connection.getAutoCommit) - // pruning events could be a long-running operation, so we disable the network timeout - withoutNetworkTimeout { implicit connection => - /* - Incomplete assign: we need to retain the next deactivation if any (otherwise it becomes active) - Incomplete unassign: we need to retain the previous activation (otherwise it cannot be presented) - Deactivation: we need to prune the related activation (otherwise it becomes active) - Pruning always happens in pairs, never ever we prune only activation or deactivation under normal circumstances. - Only exception to this rule is when the deactivation reference was unable to be calculated: then we can prune this orphan deactivation. - Therefore, regarding activations and deactivations: we prune based on the identified deactivations. - We prune all witnessed events (cannot be active, cannot be incomplete). - */ - - def loadOffsets( - offsets: Vector[Offset], - purpose: String, - )(jdbcQueryString: String)(setParams: PreparedStatement => Offset => Unit): Unit = - if (offsets.nonEmpty) { - Using.resource( - connection.prepareStatement(jdbcQueryString) - ) { preparedStatement => - val offsetBatches = offsets - .grouped(MaxBatchSizeOfIncompleteReassignmentOffsetTempTablePopulation) - .toVector - logger.info( - s"Loading ${offsets.size} offsets in ${offsetBatches.size} batches for $purpose" - ) - offsetBatches.iterator.zipWithIndex - .foreach { case (batch, index) => - batch.foreach { offset => - setParams(preparedStatement)(offset) - preparedStatement.addBatch() - } - preparedStatement.executeBatch().discard - logger.debug( - s"Processed offset batch #${index + 1} / ${offsetBatches.size} for $purpose" - ) - } - logger.info( - s"Loaded ${offsets.size} offsets in ${offsetBatches.size} batches for $purpose" - ) - } - } - - val pruningFromExclusiveEventSeqId = - maxEventSequentialId(previousPruneUpToInclusiveOffset)(connection) - val pruningToInclusiveEventSeqId = - maxEventSequentialId(Some(pruneUpToInclusiveOffset))(connection) - - logger.info("Lock pruning processing table for serialized pruning") - lockExclusivelyPruningProcessingTable(connection) - logger.info("Locked pruning processing table for serialized pruning") - - def sizeOfPruningProcessingTable() = - SQL"SELECT count(*) c FROM lapi_pruning_candidate_deactivated".asSingle(long("c")) - if (sizeOfPruningProcessingTable() != 0) { - logger.warn( - "Pruning processing table is not empty. This table must not be used! The contents of the table will be removed" - ) - SQL"""TRUNCATE TABLE lapi_pruning_candidate_deactivated""".execute().discard - } - - logger.info( - s"Start pruning Index DB events. Offsets in range ($previousPruneUpToInclusiveOffset, $pruneUpToInclusiveOffset] event sequential IDs in range ($pruningFromExclusiveEventSeqId, $pruningToInclusiveEventSeqId] with ${previousIncompleteReassignmentOffsets.size} incomplete offsets at the beginning and with ${incompleteReassignmentOffsets.size} at the end of the pruning range." - ) - - val currentIncompleteSet = incompleteReassignmentOffsets.toSet - val completedReassignments = - previousIncompleteReassignmentOffsets.filterNot(currentIncompleteSet) - - // Please note: the big union + join query is deliberately put in one CTE due to some H2 bug. - // (possibly the H2 bug is around multiple CTEs targeting the same table) - loadOffsets(completedReassignments, "populating candidates for completed reassignments")( - """-- first unfolding an offset to all respective event_sequential_id-s - |WITH completed_ids AS ( - | -- completed incomplete unassign: we carry the respective activation as well, both could be pruned now - | SELECT - | d1.event_sequential_id, - | d1.deactivated_event_sequential_id - | FROM lapi_events_deactivate_contract d1 WHERE event_offset = ? - | UNION ALL - | SELECT - | d2.event_sequential_id as event_sequential_id, - | d2.deactivated_event_sequential_id as deactivated_event_sequential_id - | FROM lapi_events_deactivate_contract d2, lapi_events_activate_contract a - | WHERE - | a.event_offset = ? - | -- completed incomplete assign: we only prune if the deactivation is found and is below the fromExclusive - | -- because the deactivations above will anyway added to the candidates - | AND d2.deactivated_event_sequential_id = a.event_sequential_id - | AND d2.event_sequential_id <= ? - |) - |INSERT INTO lapi_pruning_candidate_deactivated(deactivate_event_sequential_id, activate_event_sequential_id) - |SELECT event_sequential_id, deactivated_event_sequential_id - |FROM completed_ids - |WHERE NOT EXISTS ( - | SELECT 1 - | FROM lapi_pruning_candidate_deactivated d3 - | WHERE d3.deactivate_event_sequential_id = deactivated_event_sequential_id - |)""".stripMargin - ) { preparedStatement => offset => - preparedStatement.setLong(1, offset.unwrap) - preparedStatement.setLong(2, offset.unwrap) - preparedStatement.setLong(3, pruningFromExclusiveEventSeqId) - } - - logger.info("Populating candidates for deactivated contracts in the pruned range") - SQL""" - INSERT INTO lapi_pruning_candidate_deactivated(deactivate_event_sequential_id, activate_event_sequential_id) - SELECT event_sequential_id, deactivated_event_sequential_id - FROM lapi_events_deactivate_contract - WHERE - event_sequential_id > $pruningFromExclusiveEventSeqId - AND event_sequential_id <= $pruningToInclusiveEventSeqId - AND NOT EXISTS ( - SELECT 1 - FROM lapi_pruning_candidate_deactivated d3 - WHERE d3.deactivate_event_sequential_id = event_sequential_id - )""".execute().discard - - logger.info("Analyze lapi_pruning_candidate_deactivated table") - SQL"${queryStrategy.analyzeTable("lapi_pruning_candidate_deactivated")}".execute().discard - - loadOffsets( - incompleteReassignmentOffsets, - "removing candidates related to incomplete reassignments", - )( - """-- first unfolding an offset to all respective event_sequential_id-s - |WITH incomplete_ids AS ( - | SELECT event_sequential_id FROM lapi_events_activate_contract WHERE event_offset = ? - | UNION ALL - | SELECT event_sequential_id FROM lapi_events_deactivate_contract WHERE event_offset = ? - |) - |DELETE FROM lapi_pruning_candidate_deactivated - |WHERE EXISTS ( - | SELECT 1 - | FROM incomplete_ids - | WHERE - | -- either respective activation or deactivation is a match, we need to remove both - the whole row - | incomplete_ids.event_sequential_id = lapi_pruning_candidate_deactivated.deactivate_event_sequential_id - | OR incomplete_ids.event_sequential_id = lapi_pruning_candidate_deactivated.activate_event_sequential_id - |)""".stripMargin - ) { preparedStatement => offset => - preparedStatement.setLong(1, offset.unwrap) - preparedStatement.setLong(2, offset.unwrap) - } - - logger.info( - s"Analyze lapi_pruning_candidate_deactivated table, ${sizeOfPruningProcessingTable()} deactivation pairs will be pruned" - ) - SQL"${queryStrategy.analyzeTable("lapi_pruning_candidate_deactivated")}".execute().discard - - // populating all contract pruning candidates from this pruning iteration - logger.info( - "Add contract pruning candidates from lapi_pruning_candidate_deactivated - for deactivate events" - ) - SQL""" - INSERT INTO lapi_pruning_contract_candidate(internal_contract_id) - SELECT DISTINCT lapi_events_deactivate_contract.internal_contract_id - FROM lapi_events_deactivate_contract, lapi_pruning_candidate_deactivated - WHERE - lapi_events_deactivate_contract.event_sequential_id = lapi_pruning_candidate_deactivated.deactivate_event_sequential_id - AND lapi_events_deactivate_contract.internal_contract_id IS NOT NULL - -- only insert new contract IDs - AND NOT EXISTS ( - SELECT 1 - FROM lapi_pruning_contract_candidate c2 - WHERE c2.internal_contract_id = lapi_events_deactivate_contract.internal_contract_id - ) - -- only if no other activation event defines it above pruning - -- this is an approximation only, not prevents to populate candidates related to activations earlier - -- no need to check deactivations as deactivation defined contract IDs are a subset of activation defined contract IDs - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_activate_contract activates - WHERE activates.event_sequential_id > $pruningToInclusiveEventSeqId - AND activates.internal_contract_id = lapi_events_deactivate_contract.internal_contract_id - ) - -- only if no other witnessed event defines it above pruning - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_various_witnessed witnessed - WHERE witnessed.event_sequential_id > $pruningToInclusiveEventSeqId - AND witnessed.internal_contract_id = lapi_events_deactivate_contract.internal_contract_id - ) - """.execute().discard - logger.info("Add contract pruning candidates from lapi_events_various_witnessed") - SQL""" - INSERT INTO lapi_pruning_contract_candidate(internal_contract_id) - SELECT DISTINCT lapi_events_various_witnessed.internal_contract_id - FROM lapi_events_various_witnessed - WHERE - event_sequential_id <= $pruningToInclusiveEventSeqId - AND event_sequential_id > $pruningFromExclusiveEventSeqId - AND lapi_events_various_witnessed.internal_contract_id IS NOT NULL - -- only insert new contract IDs - AND NOT EXISTS ( - SELECT 1 - FROM lapi_pruning_contract_candidate c2 - WHERE c2.internal_contract_id = lapi_events_various_witnessed.internal_contract_id - ) - -- only if no other activation event defines it above pruning - -- this is an approximation only, not prevents to populate candidates related to activations earlier - -- no need to check deactivations as deactivation defined contract IDs are a subset of activation defined contract IDs - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_activate_contract activates - WHERE activates.event_sequential_id > $pruningToInclusiveEventSeqId - AND activates.internal_contract_id = lapi_events_various_witnessed.internal_contract_id - ) - -- only if no other witnessed event defines it above pruning - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_various_witnessed witnessed - WHERE witnessed.event_sequential_id > $pruningToInclusiveEventSeqId - AND witnessed.internal_contract_id = lapi_events_various_witnessed.internal_contract_id - ) - """.execute().discard - logger.info("Analyze lapi_pruning_contract_candidate table") - SQL"${queryStrategy.analyzeTable("lapi_pruning_contract_candidate")}".execute().discard - - // prune activate tables - def pruneActivate(tableName: String): Unit = - pruneWithLogging(s"Pruning $tableName table") { - SQL""" - DELETE from #$tableName - WHERE EXISTS ( - SELECT 1 - FROM lapi_pruning_candidate_deactivated - WHERE #$tableName.event_sequential_id = lapi_pruning_candidate_deactivated.activate_event_sequential_id - )""" - } - pruneActivate("lapi_filter_activate_stakeholder") - pruneActivate("lapi_filter_activate_witness") - pruneActivate("lapi_filter_achs_stakeholder") - pruneActivate("lapi_events_activate_contract") - - // prune deactivate tables - def pruneDeactivate(tableName: String): Unit = - pruneWithLogging(s"Pruning $tableName table") { - SQL""" - DELETE from #$tableName - WHERE EXISTS ( - SELECT 1 - FROM lapi_pruning_candidate_deactivated - WHERE #$tableName.event_sequential_id = lapi_pruning_candidate_deactivated.deactivate_event_sequential_id - )""" - } - pruneDeactivate("lapi_filter_deactivate_stakeholder") - pruneDeactivate("lapi_filter_deactivate_witness") - pruneDeactivate("lapi_events_deactivate_contract") - - // prune witnessed tables - def pruneWitnessed(tableName: String): Unit = - pruneWithLogging(s"Pruning $tableName table") { - SQL""" - DELETE from #$tableName - WHERE - event_sequential_id <= $pruningToInclusiveEventSeqId AND - event_sequential_id > $pruningFromExclusiveEventSeqId""" - } - pruneWitnessed("lapi_filter_various_witness") - pruneWitnessed("lapi_events_various_witnessed") - - // prune meta table - pruneWithLogging("Pruning lapi_update_meta table") { - SQL""" - DELETE FROM lapi_update_meta - WHERE - event_offset <= $pruneUpToInclusiveOffset AND - ${QueryStrategy.offsetIsGreater("event_offset", previousPruneUpToInclusiveOffset)}""" - } - - logger.info("Truncate table for storing pruning candidates") - SQL"""TRUNCATE TABLE lapi_pruning_candidate_deactivated;""".execute().discard - - logger.info("Finished pruning of Index DB events.") - }(connection, noTracingLogger) - } - - override def pruneContracts()(implicit - connection: Connection, - traceContext: TraceContext, - ): Iterable[Long] = { - // all of these should execute in a single transaction - assert(!connection.getAutoCommit) - // pruning events could be a long-running operation, so we disable the network timeout - withoutNetworkTimeout { implicit connection => - logger.info("Lock contract pruning processing table for serialized pruning") - lockExclusivelyContractPruningProcessingTable(connection) - logger.info("Locked contract pruning processing table for serialized pruning") - - logger.info("Lock candidate contract rows for removal") - try { - writeLockInternalContractIds( - cSQL"""IN ( - SELECT internal_contract_id - FROM lapi_pruning_contract_candidate - )""" - )(connection) - } catch { - case _: CannotAcquireAllRowLocksException => - logger.info( - "Unable to acquire write lock for pruning contract candidates - rolling back transaction." - ) - throw new PruningContractsBlockedException - } - - // with holding the write lock, we ensure that Indexer is not inserting concurrently - logger.info(s"Cleaning candidate contracts") - cleanPruningCandidates() - - logger.info(s"Fetching to-be-pruned contract IDs") - val prunedContractIds = - SQL""" - SELECT internal_contract_id - FROM lapi_pruning_contract_candidate""" - .asVectorOf(long("internal_contract_id")) - - pruneWithLogging(s"Pruning ${prunedContractIds.size} contracts from par_contracts table") { - SQL""" - DELETE from par_contracts - WHERE EXISTS ( - SELECT 1 - FROM lapi_pruning_contract_candidate - WHERE par_contracts.internal_contract_id = lapi_pruning_contract_candidate.internal_contract_id - )""" - } - - logger.info("Truncate table for storing candidate contracts") - SQL"""TRUNCATE TABLE lapi_pruning_contract_candidate;""".execute().discard - logger.info(s"Finished pruning of ${prunedContractIds.size} contracts.") - prunedContractIds - }(connection, noTracingLogger) - } - - override def cleanPruningCandidates()(implicit - connection: Connection, - traceContext: TraceContext, - ): Unit = { - logger.info("Lock contract pruning processing table for serialized pruning") - lockExclusivelyContractPruningProcessingTable(connection) - logger.info("Locked contract pruning processing table for serialized pruning") - - // Please note: these queries are deliberately not constraint by the ledger end watermark, as the actively - // inserted events must be also considered for pruning. - logger.info(s"Start removing pruning contract candidates") - val removedActivate = SQL""" - DELETE FROM lapi_pruning_contract_candidate - WHERE - EXISTS ( - SELECT 1 - FROM lapi_events_activate_contract - WHERE - lapi_events_activate_contract.internal_contract_id = lapi_pruning_contract_candidate.internal_contract_id - )""".executeUpdate() - // Please note: deactivations do not need to be considered separately as those are not defining any further contracts - logger.info( - s"Removed $removedActivate contract pruning candidates used by activate/deactivate events" - ) - val removedWitnessed = SQL""" - DELETE FROM lapi_pruning_contract_candidate - WHERE - EXISTS ( - SELECT 1 - FROM lapi_events_various_witnessed - WHERE - lapi_events_various_witnessed.internal_contract_id = lapi_pruning_contract_candidate.internal_contract_id - )""".executeUpdate() - logger.info(s"Removed $removedWitnessed contract pruning candidates used by witnessed events") - logger.info("Analyze lapi_pruning_contract_candidate table") - SQL"${queryStrategy.analyzeTable("lapi_pruning_contract_candidate")}".execute().discard - } - - override def addContractPruningCandidatesAfter(eventSeqIdExclusive: Long)(implicit - connection: Connection, - traceContext: TraceContext, - ): Unit = { - logger.info("Lock contract pruning processing table for serialized pruning") - lockExclusivelyContractPruningProcessingTable(connection) - logger.info("Locked contract pruning processing table for serialized pruning") - - val addedFromActivate = SQL""" - INSERT INTO lapi_pruning_contract_candidate(internal_contract_id) - SELECT DISTINCT lapi_events_activate_contract.internal_contract_id - FROM lapi_events_activate_contract - WHERE - lapi_events_activate_contract.event_sequential_id > $eventSeqIdExclusive - -- only insert new contract IDs - AND NOT EXISTS ( - SELECT 1 - FROM lapi_pruning_contract_candidate c2 - WHERE c2.internal_contract_id = lapi_events_activate_contract.internal_contract_id - ) - -- only if no other activation event defines it before or at eventSeqIdExclusive - -- no need to check deactivations as deactivation defined contract IDs are a subset of activation defined contract IDs - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_activate_contract activates - WHERE activates.event_sequential_id <= $eventSeqIdExclusive - AND activates.internal_contract_id = lapi_events_activate_contract.internal_contract_id - ) - -- only if no other witnessed event defines it above pruning - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_various_witnessed witnessed - WHERE witnessed.event_sequential_id <= $eventSeqIdExclusive - AND witnessed.internal_contract_id = lapi_events_activate_contract.internal_contract_id - ) - """.executeUpdate() - logger.info( - s"Added $addedFromActivate contract pruning candidates from lapi_events_activate_contract - for activate events" - ) - val addedFromWitnessed = SQL""" - INSERT INTO lapi_pruning_contract_candidate(internal_contract_id) - SELECT DISTINCT lapi_events_various_witnessed.internal_contract_id - FROM lapi_events_various_witnessed - WHERE - lapi_events_various_witnessed.event_sequential_id > $eventSeqIdExclusive - AND lapi_events_various_witnessed.internal_contract_id IS NOT NULL - -- only insert new contract IDs - AND NOT EXISTS ( - SELECT 1 - FROM lapi_pruning_contract_candidate c2 - WHERE c2.internal_contract_id = lapi_events_various_witnessed.internal_contract_id - ) - -- only if no other activation event defines it before or at eventSeqIdExclusive - -- no need to check deactivations as deactivation defined contract IDs are a subset of activation defined contract IDs - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_activate_contract activates - WHERE activates.event_sequential_id <= $eventSeqIdExclusive - AND activates.internal_contract_id = lapi_events_various_witnessed.internal_contract_id - ) - -- only if no other witnessed event defines it above pruning - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_various_witnessed witnessed - WHERE witnessed.event_sequential_id <= $eventSeqIdExclusive - AND witnessed.internal_contract_id = lapi_events_various_witnessed.internal_contract_id - ) - """.executeUpdate() - logger.info( - s"Added $addedFromWitnessed contract pruning candidates from lapi_events_various_witnessed - for witnessed events" - ) - SQL"${queryStrategy.analyzeTable("lapi_pruning_contract_candidate")}".execute().discard - logger.info("Analyzed lapi_pruning_contract_candidate table") - } - - private def pruneWithLogging(queryDescription: String)(query: SimpleSql[Row])(implicit - connection: Connection, - traceContext: TraceContext, - ): Unit = { - logger.info(s"$queryDescription") - val deletedRows = query.executeUpdate()(connection) - logger.info(s"$queryDescription finished: deleted $deletedRows rows.") - } - - override def maxEventSequentialId( - untilInclusiveOffset: Option[Offset] - )(connection: Connection): Long = { - val ledgerEnd = ledgerEndCache() - SQL""" - SELECT - event_sequential_id_first - FROM - lapi_update_meta - WHERE - ${QueryStrategy.offsetIsGreater("event_offset", untilInclusiveOffset)} - AND ${QueryStrategy.offsetIsLessOrEqual("event_offset", ledgerEnd.map(_.lastOffset))} - ORDER BY - event_offset - ${QueryStrategy.limitClause(Some(1))} - """.as(get[Long](1).singleOpt)(connection) - .getOrElse( - // after the offset there is no meta, so no tx, - // therefore the next (minimum) event sequential id will be - // the first event sequential id after the ledger end - ledgerEnd.map(_.lastEventSeqId).getOrElse(0L) + 1 - ) - 1 - } - - override def activeContractBatch( - eventSequentialIds: Iterable[Long], - allFilterParties: Option[Set[Party]], - )(connection: Connection): Vector[RawThinActiveContract] = - RowDefs - .rawThinActiveContractParser(stringInterning, allFilterParties) - .queryMultipleRows(columns => - SQL""" - SELECT $columns - FROM lapi_events_activate_contract - WHERE - event_sequential_id ${queryStrategy.anyOf(eventSequentialIds)} - ORDER BY event_sequential_id -- deliver in index order - """ - .withFetchSize(Some(eventSequentialIds.size)) - )(connection) - - override def lookupActivationSequentialIdByOffset( - offsets: Iterable[Long] - )(connection: Connection): Vector[Long] = - SQL""" - SELECT event_sequential_id - FROM lapi_events_activate_contract - WHERE - event_offset ${queryStrategy.anyOf(offsets)} - ORDER BY event_sequential_id -- deliver in index order - """ - .asVectorOf(long("event_sequential_id"))(connection) - - override def lookupDeactivationSequentialIdByOffset( - offsets: Iterable[Long] - )(connection: Connection): Vector[Long] = - SQL""" - SELECT event_sequential_id - FROM lapi_events_deactivate_contract - WHERE - event_offset ${queryStrategy.anyOf(offsets)} - ORDER BY event_sequential_id -- deliver in index order - """ - .asVectorOf(long("event_sequential_id"))(connection) - - def addActivationsToAchs( - params: AchsAddActivationsParams - )(connection: Connection): Unit = - SQL""" - INSERT INTO lapi_filter_achs_stakeholder - SELECT * - FROM lapi_filter_activate_stakeholder filters - WHERE - filters.event_sequential_id > ${params.startExclusive} - AND filters.event_sequential_id <= ${params.endInclusive} - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract deactivate_evs - WHERE - filters.event_sequential_id = deactivate_evs.deactivated_event_sequential_id - AND deactivate_evs.event_sequential_id <= ${params.activeAt} - ) - """.execute()(connection).discard - - def removeDeactivatedFromAchs( - params: AchsRemoveDeactivatedParams - )(connection: Connection): Unit = - SQL""" - DELETE FROM lapi_filter_achs_stakeholder - WHERE EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract deactivate_evs - WHERE - lapi_filter_achs_stakeholder.event_sequential_id = deactivate_evs.deactivated_event_sequential_id - AND deactivate_evs.event_sequential_id <= ${params.endInclusive} - AND deactivate_evs.event_sequential_id > ${params.startExclusive} - ) - """.execute()(connection).discard - - override def firstSynchronizerOffsetAfterOrAt( - synchronizerId: SynchronizerId, - afterOrAtRecordTimeInclusive: Timestamp, - )(connection: Connection): Option[SynchronizerOffset] = - List( - RowDefs - .completionSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_command_completions - WHERE - synchronizer_id = ${stringInterning.synchronizerId.internalize(synchronizerId)} AND - record_time >= ${afterOrAtRecordTimeInclusive.micros} - ORDER BY synchronizer_id ASC, record_time ASC, completion_offset ASC - ${QueryStrategy.limitClause(Some(1))} - """)(connection), - RowDefs - .metaSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_update_meta - WHERE - synchronizer_id = ${stringInterning.synchronizerId.internalize(synchronizerId)} AND - record_time >= ${afterOrAtRecordTimeInclusive.micros} - ORDER BY synchronizer_id ASC, record_time ASC, event_offset ASC - ${QueryStrategy.limitClause(Some(1))} - """)(connection), - ).flatten - .minByOption(_.recordTime) - .filter(synchronizerOffset => - Option(synchronizerOffset.offset) <= ledgerEndCache().map(_.lastOffset) - ) // if the first is after LedgerEnd, then we have none - - override def lastSynchronizerOffsetBeforeOrAt( - synchronizerIdO: Option[SynchronizerId], - beforeOrAtOffsetInclusive: Offset, - )(connection: Connection): Option[SynchronizerOffset] = { - val ledgerEndOffset = ledgerEndCache().map(_.lastOffset) - val safeBeforeOrAtOffset = - if (Option(beforeOrAtOffsetInclusive) > ledgerEndOffset) ledgerEndOffset - else Some(beforeOrAtOffsetInclusive) - val (synchronizerIdFilter, synchronizerIdOrdering) = synchronizerIdO match { - case Some(synchronizerId) => - ( - cSQL"synchronizer_id = ${stringInterning.synchronizerId.internalize(synchronizerId)} AND", - cSQL"synchronizer_id,", - ) - - case None => - ( - cSQL"", - cSQL"", - ) - } - List( - RowDefs - .completionSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_command_completions - WHERE - $synchronizerIdFilter - ${QueryStrategy.offsetIsLessOrEqual("completion_offset", safeBeforeOrAtOffset)} - ORDER BY $synchronizerIdOrdering completion_offset DESC - ${QueryStrategy.limitClause(Some(1))} - """)(connection), - RowDefs - .metaSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_update_meta - WHERE - $synchronizerIdFilter - ${QueryStrategy.offsetIsLessOrEqual("event_offset", safeBeforeOrAtOffset)} - ORDER BY $synchronizerIdOrdering event_offset DESC - ${QueryStrategy.limitClause(Some(1))} - """)(connection), - ).flatten - .sortBy(_.offset) - .reverse - .headOption - } - - // Note: Added for offline party replication as CN is using it. - override def lastSynchronizerOffsetBeforeOrAtRecordTime( - synchronizerId: SynchronizerId, - beforeOrAtRecordTimeInclusive: Timestamp, - beforeOrAtLedgerEndOffsetInclusive: Offset, - )(connection: Connection)(implicit traceContext: TraceContext): Option[SynchronizerOffset] = { - logger.debug( - s"Querying lastSynchronizerOffset: beforeOrAtRecordTime=$beforeOrAtRecordTimeInclusive, ledgerEndOffset=$beforeOrAtLedgerEndOffsetInclusive, synchronizerId=$synchronizerId" - ) - - val completionQueryResult = - RowDefs - .completionSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => - SQL""" - SELECT $columns - FROM lapi_command_completions - WHERE - synchronizer_id = ${stringInterning.synchronizerId.internalize(synchronizerId)} AND - record_time <= ${beforeOrAtRecordTimeInclusive.micros} AND - ${QueryStrategy - .offsetIsLessOrEqual("completion_offset", Some(beforeOrAtLedgerEndOffsetInclusive))} - ORDER BY synchronizer_id DESC, record_time DESC, completion_offset DESC - ${QueryStrategy.limitClause(Some(1))} - """ - )(connection) - - logger.debug(s"lapi_command_completions query result: $completionQueryResult") - - val metaQueryResult = - RowDefs - .metaSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => - SQL""" - SELECT $columns - FROM lapi_update_meta - WHERE - synchronizer_id = ${stringInterning.synchronizerId.internalize(synchronizerId)} AND - record_time <= ${beforeOrAtRecordTimeInclusive.micros} AND - ${QueryStrategy - .offsetIsLessOrEqual("event_offset", Some(beforeOrAtLedgerEndOffsetInclusive))} - ORDER BY synchronizer_id DESC, record_time DESC, event_offset DESC - ${QueryStrategy.limitClause(Some(1))} - """ - )(connection) - - logger.debug(s"lapi_update_meta query result: $metaQueryResult") - - List(completionQueryResult, metaQueryResult).flatten.maxByOption(_.offset) - - } - - def lastRecordTimeBeforeOrAtSynchronizerOffset( - synchronizerId: SynchronizerId, - beforeOrAtOffsetInclusive: Offset, - )(connection: Connection): Option[CantonTimestamp] = { - val ledgerEndOffset = ledgerEndCache().map(_.lastOffset) - val safeBeforeOrAtOffset = - if (Option(beforeOrAtOffsetInclusive) > ledgerEndOffset) ledgerEndOffset - else Some(beforeOrAtOffsetInclusive) - val synchronizerIdFilter = - cSQL"synchronizer_id = ${stringInterning.synchronizerId.internalize(synchronizerId)} AND" - val synchronizerIdOrdering = cSQL"synchronizer_id," - val completionQueryResult = RowDefs - .completionSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_command_completions - WHERE - $synchronizerIdFilter - ${QueryStrategy.offsetIsLessOrEqual("completion_offset", safeBeforeOrAtOffset)} - ORDER BY $synchronizerIdOrdering completion_offset DESC, record_time DESC - ${QueryStrategy.limitClause(Some(1))} - """)(connection) - val metaQueryResult = RowDefs - .metaSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_update_meta - WHERE - $synchronizerIdFilter - ${QueryStrategy.offsetIsLessOrEqual("event_offset", safeBeforeOrAtOffset)} - ORDER BY $synchronizerIdOrdering event_offset DESC, record_time DESC - ${QueryStrategy.limitClause(Some(1))} - """)(connection) - List( - completionQueryResult, - metaQueryResult, - ).flatten - .maxByOption(_.recordTime) - .map(_.recordTime) - .map(CantonTimestamp(_)) - } - - override def synchronizerOffset(offset: Offset)( - connection: Connection - ): Option[SynchronizerOffset] = - List( - RowDefs - .completionSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_command_completions - WHERE - completion_offset = $offset - """)(connection), - RowDefs - .metaSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_update_meta - WHERE - event_offset = $offset - """)(connection), - ).flatten.headOption // if both present they should be the same - .filter(synchronizerOffset => - Option(synchronizerOffset.offset) <= ledgerEndCache().map(_.lastOffset) - ) // only offset allow before or at ledger end - - override def firstSynchronizerOffsetAfterOrAtPublicationTime( - afterOrAtPublicationTimeInclusive: Timestamp - )(connection: Connection): Option[SynchronizerOffset] = - List( - RowDefs - .completionSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_command_completions - WHERE - publication_time >= ${afterOrAtPublicationTimeInclusive.micros} - ORDER BY publication_time ASC, completion_offset ASC - ${QueryStrategy.limitClause(Some(1))} - """)(connection), - RowDefs - .metaSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_update_meta - WHERE - publication_time >= ${afterOrAtPublicationTimeInclusive.micros} - ORDER BY publication_time ASC, event_offset ASC - ${QueryStrategy.limitClause(Some(1))} - """)(connection), - ).flatten - .minByOption(_.offset) - .filter(synchronizerOffset => - Option(synchronizerOffset.offset) <= ledgerEndCache().map(_.lastOffset) - ) // if first offset is beyond the ledger-end then we have no such - - override def lastSynchronizerOffsetBeforeOrAtPublicationTime( - beforeOrAtPublicationTimeInclusive: Timestamp - )(connection: Connection): Option[SynchronizerOffset] = { - val ledgerEndPublicationTime = - ledgerEndCache().map(_.lastPublicationTime).getOrElse(CantonTimestamp.MinValue).underlying - val safePublicationTime = - if (beforeOrAtPublicationTimeInclusive > ledgerEndPublicationTime) - ledgerEndPublicationTime - else - beforeOrAtPublicationTimeInclusive - List( - RowDefs - .completionSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_command_completions - WHERE - publication_time <= ${safePublicationTime.micros} - ORDER BY publication_time DESC, completion_offset DESC - ${QueryStrategy.limitClause(Some(1))} - """)(connection), - RowDefs - .metaSynchronizerOffsetParser(stringInterning) - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_update_meta - WHERE - publication_time <= ${safePublicationTime.micros} - ORDER BY publication_time DESC, event_offset DESC - ${QueryStrategy.limitClause(Some(1))} - """)(connection), - ).flatten - .sortBy(_.offset) - .reverse - .headOption - } - - override def fetchTopologyPartyEventIds( - party: Option[Party] - ): IdPageQuery = - UpdateStreamingQueries.fetchEventIds( - tableName = "lapi_events_party_to_participant", - witnessO = party, - templateIdO = None, - stringInterning = stringInterning, - hasFirstPerSequentialId = false, - ) - - override def topologyPartyEventBatch( - eventSequentialIds: SequentialIdBatch - )(connection: Connection): Vector[EventStorageBackend.RawParticipantAuthorization] = { - val query = (columns: CompositeSql) => - SQL""" - SELECT $columns - FROM lapi_events_party_to_participant e - WHERE ${queryStrategy.inBatch("e.event_sequential_id", eventSequentialIds)} - ORDER BY e.event_sequential_id -- deliver in index order - """ - .withFetchSize(Some(fetchSize(eventSequentialIds))) - RowDefs.partyToParticipantEventParser(stringInterning).queryMultipleRows(query)(connection) - } - - override def topologyEventOffsetPublishedOnRecordTime( - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - )(connection: Connection): Option[Offset] = - stringInterning.synchronizerId - .tryInternalize(synchronizerId) - .flatMap(synchronizerInternedId => - RowDefs.eventOffset - .querySingleOptRow(columns => SQL""" - SELECT $columns - FROM lapi_events_party_to_participant - WHERE record_time = ${recordTime.toMicros} - AND synchronizer_id = $synchronizerInternedId - ORDER BY synchronizer_id ASC, record_time ASC - ${QueryStrategy.limitClause(Some(1))} - """)(connection) - .filter(offset => Option(offset) <= ledgerEndCache().map(_.lastOffset)) - ) - - private def fetchByEventSequentialIds( - tableName: String, - eventSequentialIds: SequentialIdBatch, - )(columns: CompositeSql): SimpleSql[Row] = - SQL""" - SELECT $columns - FROM #$tableName - WHERE ${queryStrategy.inBatch("event_sequential_id", eventSequentialIds)} - ORDER BY event_sequential_id - """.withFetchSize(Some(fetchSize(eventSequentialIds))) - - override def fetchEventPayloadsAcsDelta(target: EventPayloadSourceForUpdatesAcsDelta)( - eventSequentialIds: SequentialIdBatch, - requestingPartiesForTx: Option[Set[Party]], - requestingPartiesForReassignment: Option[Set[Party]], - )(connection: Connection): Vector[RawThinAcsDeltaEvent] = - target match { - case EventPayloadSourceForUpdatesAcsDelta.Activate => - RowDefs.eventType - .branch( - PersistentEventType.Create -> RowDefs.rawThinCreatedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForTx, - witnessIsAcsDelta = true, - eventIsAcsDeltaForParticipant = true, - ), - PersistentEventType.Assign -> RowDefs.rawThinAssignEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForReassignment, - ), - ) - .queryMultipleRows( - fetchByEventSequentialIds( - tableName = "lapi_events_activate_contract", - eventSequentialIds = eventSequentialIds, - ) - )(connection) - case EventPayloadSourceForUpdatesAcsDelta.Deactivate => - RowDefs.eventType - .branch( - PersistentEventType.ConsumingExercise -> RowDefs.rawArchivedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForTx, - acsDeltaForParticipant = true, - ), - PersistentEventType.Unassign -> RowDefs.rawUnassignEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForReassignment, - ), - ) - .queryMultipleRows( - fetchByEventSequentialIds( - tableName = "lapi_events_deactivate_contract", - eventSequentialIds = eventSequentialIds, - ) - )(connection) - } - - override def fetchEventPayloadsLedgerEffects(target: EventPayloadSourceForUpdatesLedgerEffects)( - eventSequentialIds: SequentialIdBatch, - requestingPartiesForTx: Option[Set[Party]], - requestingPartiesForReassignment: Option[Set[Party]], - )(connection: Connection): Vector[RawThinLedgerEffectsEvent] = - target match { - case EventPayloadSourceForUpdatesLedgerEffects.Activate => - RowDefs.eventType - .branch( - PersistentEventType.Create -> RowDefs.rawThinCreatedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForTx, - witnessIsAcsDelta = false, - eventIsAcsDeltaForParticipant = true, - ), - PersistentEventType.Assign -> RowDefs.rawThinAssignEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForReassignment, - ), - ) - .queryMultipleRows( - fetchByEventSequentialIds( - tableName = "lapi_events_activate_contract", - eventSequentialIds = eventSequentialIds, - ) - )(connection) - case EventPayloadSourceForUpdatesLedgerEffects.Deactivate => - RowDefs.eventType - .branch( - PersistentEventType.ConsumingExercise -> RowDefs.rawExercisedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForTx, - eventIsAcsDeltaForParticipant = true, - ), - PersistentEventType.Unassign -> RowDefs.rawUnassignEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForReassignment, - ), - ) - .queryMultipleRows( - fetchByEventSequentialIds( - tableName = "lapi_events_deactivate_contract", - eventSequentialIds = eventSequentialIds, - ) - )(connection) - case EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed => - RowDefs.eventType - .branch( - PersistentEventType.WitnessedCreate -> RowDefs.rawThinCreatedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForTx, - witnessIsAcsDelta = false, - eventIsAcsDeltaForParticipant = false, - ), - PersistentEventType.WitnessedConsumingExercise -> RowDefs.rawExercisedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForTx, - eventIsAcsDeltaForParticipant = false, - ), - PersistentEventType.NonConsumingExercise -> RowDefs.rawExercisedEventParser( - stringInterning = stringInterning, - allQueryingPartiesO = requestingPartiesForTx, - eventIsAcsDeltaForParticipant = false, - ), - ) - .queryMultipleRows( - fetchByEventSequentialIds( - tableName = "lapi_events_various_witnessed", - eventSequentialIds = eventSequentialIds, - ) - )(connection) - } - - private def fetchSize(eventSequentialIds: SequentialIdBatch): Int = - eventSequentialIds match { - case SequentialIdBatch.IdRange(fromInclusive, toInclusive) => - Math.min(toInclusive - fromInclusive + 1, Int.MaxValue).toInt - case SequentialIdBatch.Ids(ids) => ids.size - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Field.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Field.scala deleted file mode 100644 index 9c4fe917eb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Field.scala +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.platform.store.interning.StringInterning - -import java.lang -import java.sql.PreparedStatement -import scala.reflect.ClassTag - -/** @tparam From - * is an arbitrary type from which we can extract the data of interest for the particular column - * @tparam To - * is the intermediary type of the result of the extraction. From => To functionality is intended - * to be injected at Schema definition time. To is not nullable, should express a clean Scala - * type - * @tparam Converted - * is the (possibly primitive) type needed by the JDBC API To => Converted is intended to be - * injected at PGField definition time. Converted might be nullable, primitive, boxed-type, - * whatever the JDBC API requires - */ -private[backend] abstract class Field[From, To, Converted](implicit - classTag: ClassTag[Converted] -) { - def extract: StringInterning => From => To - def convert: To => Converted - def selectFieldExpression(inputFieldName: String): String = inputFieldName - - final def toArray( - input: Vector[From], - stringInterning: StringInterning, - ): Array[Converted] = - input.view - .map(extract(stringInterning) andThen convert) - .toArray(classTag) - - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - final def prepareData(preparedStatement: PreparedStatement, index: Int, value: Any): Unit = - prepareDataTemplate( - preparedStatement, - index, - value.asInstanceOf[Converted], - ) // this cast is safe by design - - def prepareDataTemplate( - preparedStatement: PreparedStatement, - index: Int, - value: Converted, - ): Unit = - preparedStatement.setObject(index, value) -} - -private[backend] abstract class TrivialField[From, To](implicit classTag: ClassTag[To]) - extends Field[From, To, To] { - override def convert: To => To = identity -} - -private[backend] trait TrivialOptionalField[From, To >: Null <: AnyRef] - extends Field[From, Option[To], To] { - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def convert: Option[To] => To = _.orNull -} - -private[backend] final case class StringField[From](extract: StringInterning => From => String) - extends TrivialField[From, String] - -private[backend] final case class StringOptional[From]( - extract: StringInterning => From => Option[String] -) extends TrivialOptionalField[From, String] - -private[backend] final case class Bytea[From](extract: StringInterning => From => Array[Byte]) - extends TrivialField[From, Array[Byte]] - -private[backend] final case class ByteaOptional[From]( - extract: StringInterning => From => Option[Array[Byte]] -) extends TrivialOptionalField[From, Array[Byte]] - -private[backend] final case class Integer[From](extract: StringInterning => From => Int) - extends TrivialField[From, Int] - -private[backend] final case class BooleanField[From](extract: StringInterning => From => Boolean) - extends TrivialField[From, Boolean] - -private[backend] final case class IntOptional[From](extract: StringInterning => From => Option[Int]) - extends Field[From, Option[Int], java.lang.Integer] { - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def convert: Option[Int] => java.lang.Integer = _.map(Int.box).orNull -} - -private[backend] final case class Bigint[From](extract: StringInterning => From => Long) - extends TrivialField[From, Long] - -private[backend] final case class BigintOptional[From]( - extract: StringInterning => From => Option[Long] -) extends Field[From, Option[Long], java.lang.Long] { - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def convert: Option[Long] => java.lang.Long = _.map(Long.box).orNull -} - -private[backend] final case class Smallint[From](extract: StringInterning => From => Int) - extends TrivialField[From, Int] - -private[backend] final case class SmallintOptional[From]( - extract: StringInterning => From => Option[Int] -) extends Field[From, Option[Int], java.lang.Integer] { - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def convert: Option[Int] => java.lang.Integer = _.map(Int.box).orNull -} - -private[backend] final case class BooleanOptional[From]( - extract: StringInterning => From => Option[Boolean] -) extends Field[From, Option[Boolean], java.lang.Boolean] { - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def convert: Option[Boolean] => lang.Boolean = _.map(Boolean.box).orNull -} - -private[backend] final case class BooleanMandatory[From]( - extract: StringInterning => From => Boolean -) extends TrivialField[From, Boolean] - -private[backend] final case class StringArray[From]( - extract: StringInterning => From => Iterable[String] -) extends Field[From, Iterable[String], Array[String]] { - override def convert: Iterable[String] => Array[String] = _.toArray -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/IngestionStorageBackendTemplate.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/IngestionStorageBackendTemplate.scala deleted file mode 100644 index 02e78da261..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/IngestionStorageBackendTemplate.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.{ - DbDto, - IngestionStorageBackend, - ParameterStorageBackend, -} -import com.digitalasset.canton.platform.store.interning.StringInterning - -import java.sql.Connection - -private[backend] class IngestionStorageBackendTemplate( - schema: Schema[DbDto] -) extends IngestionStorageBackend[AppendOnlySchema.Batch] { - - override def deletePartiallyIngestedData( - ledgerEnd: Option[ParameterStorageBackend.LedgerEnd] - )(connection: Connection): Unit = { - val ledgerOffset = ledgerEnd.map(_.lastOffset) - val lastStringInterningIdO = ledgerEnd.map(_.lastStringInterningId) - val lastEventSequentialId = ledgerEnd.map(_.lastEventSeqId) - - List( - SQL"DELETE FROM lapi_command_completions WHERE ${QueryStrategy - .offsetIsGreater("completion_offset", ledgerOffset)}", - SQL"DELETE FROM lapi_events_activate_contract WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - SQL"DELETE FROM lapi_filter_activate_stakeholder WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - SQL"DELETE FROM lapi_filter_activate_witness WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - SQL"DELETE FROM lapi_events_deactivate_contract WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - SQL"DELETE FROM lapi_filter_deactivate_stakeholder WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - SQL"DELETE FROM lapi_filter_deactivate_witness WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - SQL"DELETE FROM lapi_events_various_witnessed WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - SQL"DELETE FROM lapi_filter_various_witness WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - SQL"DELETE FROM lapi_party_entries WHERE ${QueryStrategy.offsetIsGreater("ledger_offset", ledgerOffset)}", - SQL"DELETE FROM lapi_events_party_to_participant WHERE ${QueryStrategy - .eventSeqIdIsGreater("event_sequential_id", lastEventSequentialId)}", - lastStringInterningIdO match { - case None => SQL"DELETE FROM lapi_string_interning" - case Some(lastStringInterningId) => - SQL"DELETE FROM lapi_string_interning WHERE internal_id > $lastStringInterningId" - }, - SQL"DELETE FROM lapi_update_meta WHERE ${QueryStrategy - .offsetIsGreater("event_offset", ledgerOffset)}", - // As reassignment global offsets are persisted before the ledger end, they might change after indexer recovery, so in the cleanup - // phase here we make sure that all the persisted global offsets are revoked which are after the ledger end. - SQL"UPDATE par_reassignments SET unassignment_global_offset = null WHERE ${QueryStrategy - .offsetIsGreater("unassignment_global_offset", ledgerOffset)}", - SQL"UPDATE par_reassignments SET assignment_global_offset = null WHERE ${QueryStrategy - .offsetIsGreater("assignment_global_offset", ledgerOffset)}", - ).map(_.execute()(connection)).discard - } - - override def insertBatch( - connection: Connection, - dbBatch: AppendOnlySchema.Batch, - ): Unit = - schema.executeUpdate(dbBatch, connection) - - override def batch( - dbDtos: Vector[DbDto], - stringInterning: StringInterning, - ): AppendOnlySchema.Batch = - schema.prepareData(dbDtos, stringInterning) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/InitHookDataSourceProxy.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/InitHookDataSourceProxy.scala deleted file mode 100644 index 3e1a57082e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/InitHookDataSourceProxy.scala +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext - -import java.io.PrintWriter -import java.sql.Connection -import java.util.logging.Logger -import javax.sql.DataSource - -private[backend] object InitHookDataSourceProxy { - - def apply( - delegate: DataSource, - initHooks: List[Connection => Unit], - loggerFactory: NamedLoggerFactory, - ): DataSource = - if (initHooks.isEmpty) delegate - else InitHookDataSourceProxy(delegate, c => initHooks.foreach(_(c)), loggerFactory) -} - -private[backend] final case class InitHookDataSourceProxy( - delegate: DataSource, - initHook: Connection => Unit, - loggerFactory: NamedLoggerFactory, -) extends DataSource - with NamedLogging { - - private def getConnection(connectionBody: => Connection): Connection = { - implicit val traceContext: TraceContext = TraceContext.empty - - logger.debug(s"Creating new connection") - val connection = connectionBody - try { - logger.debug(s"Applying connection init hook") - initHook(connection) - } catch { - case t: Throwable => - logger.warn(s"Init hook execution failed", t) - try { - connection.close() // releasing resources in case of initialisation issues - } catch { - case _: Throwable => () // catching all resource-releasing exceptions - } - throw t - } - logger.debug(s"Init hook execution finished successfully, connection ready") - connection - } - - override def getConnection: Connection = getConnection(delegate.getConnection) - - override def getConnection(s: String, s1: String): Connection = getConnection( - delegate.getConnection(s, s1) - ) - - override def getLogWriter: PrintWriter = delegate.getLogWriter - - override def setLogWriter(printWriter: PrintWriter): Unit = delegate.setLogWriter(printWriter) - - override def setLoginTimeout(i: Int): Unit = delegate.setLoginTimeout(i) - - override def getLoginTimeout: Int = delegate.getLoginTimeout - - override def getParentLogger: Logger = delegate.getParentLogger - - override def unwrap[T](aClass: Class[T]): T = delegate.unwrap(aClass) - - override def isWrapperFor(aClass: Class[?]): Boolean = delegate.isWrapperFor(aClass) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/IntegrityStorageBackendImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/IntegrityStorageBackendImpl.scala deleted file mode 100644 index 14947fcb80..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/IntegrityStorageBackendImpl.scala +++ /dev/null @@ -1,642 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.{byteArray, int, long, str} -import anorm.{RowParser, ~} -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.`SimpleSql ops` -import com.digitalasset.canton.platform.store.backend.{IntegrityStorageBackend, PersistentEventType} -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.google.common.annotations.VisibleForTesting - -import java.sql.Connection - -private[backend] object IntegrityStorageBackendImpl extends IntegrityStorageBackend { - import com.digitalasset.canton.platform.store.backend.Conversions.* - - private val allSequentialIds: String = - s""" - |SELECT event_sequential_id FROM lapi_events_activate_contract - |UNION ALL - |SELECT event_sequential_id FROM lapi_events_deactivate_contract - |UNION ALL - |SELECT event_sequential_id FROM lapi_events_various_witnessed - |UNION ALL - |SELECT event_sequential_id FROM lapi_events_party_to_participant - |""".stripMargin - - private val allSequentialIdsAndOffsets: String = - s""" - |SELECT event_sequential_id, event_offset FROM lapi_events_activate_contract - |UNION ALL - |SELECT event_sequential_id, event_offset FROM lapi_events_deactivate_contract - |UNION ALL - |SELECT event_sequential_id, event_offset FROM lapi_events_various_witnessed - |UNION ALL - |SELECT event_sequential_id, event_offset FROM lapi_events_party_to_participant - |""".stripMargin - - private val SqlEventSequentialIdsSummary = SQL""" - WITH sequential_ids AS (#$allSequentialIdsAndOffsets) - SELECT min(event_sequential_id) as min, max(event_sequential_id) as max, count(event_sequential_id) as count - FROM sequential_ids, lapi_parameters - WHERE - lapi_parameters.ledger_end_sequential_id is not null and - event_sequential_id <= lapi_parameters.ledger_end_sequential_id and - ( - lapi_parameters.participant_pruned_up_to_inclusive is null or - event_offset > lapi_parameters.participant_pruned_up_to_inclusive -- this is not backed up by index, but it is okay as this is for testing - ) - """ - - // Don't fetch an unbounded number of rows - private val maxReportedDuplicates = 100 - - private val SqlDuplicateEventSequentialIds = SQL""" - WITH sequential_ids AS (#$allSequentialIds) - SELECT event_sequential_id as id, count(*) as count - FROM sequential_ids, lapi_parameters - WHERE lapi_parameters.ledger_end_sequential_id is not null - AND event_sequential_id <= lapi_parameters.ledger_end_sequential_id - GROUP BY event_sequential_id - HAVING count(*) > 1 - ${QueryStrategy.limitClause(Some(maxReportedDuplicates))} - """ - - private val allEventIds: String = - s""" - |SELECT event_offset, node_id FROM lapi_events_activate_contract - |UNION ALL - |SELECT event_offset, node_id FROM lapi_events_deactivate_contract - |UNION ALL - |SELECT event_offset, node_id FROM lapi_events_various_witnessed - |""".stripMargin - - private val SqlDuplicateOffsets = SQL""" - WITH event_ids AS (#$allEventIds) - SELECT event_offset, node_id, count(*) as count - FROM event_ids, lapi_parameters - WHERE lapi_parameters.ledger_end is not null - AND event_offset <= lapi_parameters.ledger_end - GROUP BY event_offset, node_id - HAVING count(*) > 1 - ${QueryStrategy.limitClause(Some(maxReportedDuplicates))} - """ - - final case class EventSequentialIdsRow(min: Long, max: Long, count: Long) - - private val eventSequantialIdsParser: RowParser[EventSequentialIdsRow] = - long("min").? ~ - long("max").? ~ - long("count") map { case min ~ max ~ count => - EventSequentialIdsRow(min.getOrElse(0L), max.getOrElse(0L), count) - } - - @VisibleForTesting - override def verifyIntegrity( - failForEmptyDB: Boolean, - inMemoryCantonStore: Boolean, - )(connection: Connection): Unit = try { - val duplicateSeqIds = SqlDuplicateEventSequentialIds - .as(long("id").*)(connection) - val duplicateOffsets = SqlDuplicateOffsets - .as(long("event_offset").*)(connection) - val summary = SqlEventSequentialIdsSummary - .as(eventSequantialIdsParser.single)(connection) - - // Verify that there are no duplicate offsets (events with the same offset and node index). - if (duplicateOffsets.nonEmpty) { - throw new RuntimeException( - s"Found ${duplicateOffsets.length} duplicate offsets. Examples: ${duplicateOffsets.mkString(", ")}" - ) - } - - // Verify that there are no duplicate event sequential ids. - if (duplicateSeqIds.nonEmpty) { - throw new RuntimeException( - s"Found ${duplicateSeqIds.length} duplicate event sequential ids. Examples: ${duplicateSeqIds - .mkString(", ")}" - ) - } - - // Verify that all event sequential ids are in fact sequential (i.e., there are no "holes" in the ids). - // Since we already know that there are no duplicates, it is enough to check that the count is consistent with the range. - if (summary.count != 0 && summary.count != summary.max - summary.min + 1) { - throw new RuntimeException( - s"Event sequential ids are not consecutive. Min=${summary.min}, max=${summary.max}, count=${summary.count}." - ) - } - - // Verify monotonic record times per synchronizer - val offsetSynchronizerRecordTime = SQL""" - SELECT event_offset as _offset, record_time, synchronizer_id FROM lapi_events_activate_contract - UNION ALL - SELECT event_offset as _offset, record_time, synchronizer_id FROM lapi_events_deactivate_contract - UNION ALL - SELECT event_offset as _offset, record_time, synchronizer_id FROM lapi_events_various_witnessed - UNION ALL - SELECT completion_offset as _offset, record_time, synchronizer_id FROM lapi_command_completions - UNION ALL - SELECT event_offset as _offset, record_time, synchronizer_id FROM lapi_events_party_to_participant - """.asVectorOf( - offset("_offset") ~ long("record_time") ~ int("synchronizer_id") map { - case offset ~ recordTimeMicros ~ internedSynchronizerId => - (offset.unwrap, internedSynchronizerId, recordTimeMicros) - } - )(connection) - offsetSynchronizerRecordTime.groupBy(_._2).foreach { - case (_, offsetRecordTimePerSynchronizer) => - val inOrderElems = offsetRecordTimePerSynchronizer.sortBy(_._1) - inOrderElems.iterator.zip(inOrderElems.iterator.drop(1)).foreach { - case ((firstOffset, _, firstRecordTime), (secondOffset, _, secondRecordTime)) => - if (firstRecordTime > secondRecordTime) { - throw new RuntimeException( - s"occurrence of decreasing record time found within one synchronizer: offsets ${Offset - .tryFromLong(firstOffset)},${Offset.tryFromLong(secondOffset)} record times: ${CantonTimestamp - .assertFromLong(firstRecordTime)},${CantonTimestamp.assertFromLong(secondRecordTime)}" - ) - } - } - } - - // Verify no duplicate update id - SQL""" - SELECT meta1.update_id as uId, meta1.event_offset as offset1, meta2.event_offset as offset2 - FROM lapi_update_meta as meta1, lapi_update_meta as meta2 - WHERE meta1.update_id = meta2.update_id and - meta1.event_offset != meta2.event_offset - ${QueryStrategy.limitClause(Some(1))} - """ - .asSingleOpt(updateId("uId") ~ offset("offset1") ~ offset("offset2"))(connection) - .foreach { case uId ~ offset1 ~ offset2 => - throw new RuntimeException( - s"occurrence of duplicate update ID [${uId.toHexString}] found for offsets $offset1, $offset2" - ) - } - - // Verify no duplicate completion offset - SQL""" - SELECT completion_offset, count(*) as offset_count - FROM lapi_command_completions - GROUP BY completion_offset - HAVING count(*) > 1 - ${QueryStrategy.limitClause(Some(1))} - """ - .asSingleOpt(offset("completion_offset") ~ int("offset_count"))(connection) - .foreach { case offset ~ count => - throw new RuntimeException( - s"occurrence of duplicate offset found for lapi_command_completions: for offset $offset $count rows found" - ) - } - - // Verify publication time cannot go backwards - val offsetPublicationTimes = - SQL""" - SELECT event_offset as _offset, publication_time FROM lapi_update_meta - UNION ALL - SELECT completion_offset as _offset, publication_time FROM lapi_command_completions - """ - .asVectorOf( - offset("_offset") ~ long("publication_time") map { case offset ~ publicationTime => - (offset.unwrap, publicationTime) - } - )(connection) - .sortBy(_._1) - offsetPublicationTimes.iterator.zip(offsetPublicationTimes.iterator.drop(1)).foreach { - case ((offset, publicationTime), (nextOffset, nextPublicationTime)) => - if (offset == nextOffset && publicationTime != nextPublicationTime) { - throw new RuntimeException( - s"for each offset the publication times should be equal due to indexer batching should respect offset boundaries, but for offset $offset this does not hold" - ) - } - if (offset < nextOffset && publicationTime > nextPublicationTime) { - throw new RuntimeException( - s"publication_time should monotonic in offset time, but from $offset to $nextOffset publication_time decreased" - ) - } - } - - // Verify no duplicate completion entry - val completions = SQL""" - SELECT - completion_offset, - user_id, - submitters, - command_id, - update_id, - submission_id, - message_uuid, - record_time, - synchronizer_id - FROM lapi_command_completions - """ - .asVectorOf( - offset("completion_offset") ~ - int("user_id") ~ - byteArray("submitters") ~ - str("command_id") ~ - updateId("update_id").? ~ - str("submission_id").? ~ - str("message_uuid").? ~ - long("record_time") ~ - long("synchronizer_id") map { - case offset ~ userId ~ submitters ~ commandId ~ updateId ~ submissionId ~ messageUuid ~ recordTimeLong ~ synchronizerId => - CompletionEntry( - userId, - IntArrayDBSerialization.decodeFromByteArray(submitters).toList, - commandId, - updateId, - submissionId, - messageUuid, - recordTimeLong, - synchronizerId, - ) -> offset - } - )(connection) - - // duplicate completions by many fields - completions - .groupMapReduce(_._1)(entry => List(entry._2))(_ ::: _) - .find(_._2.sizeIs > 1) - .map(_._2) - .foreach(offsets => - throw new RuntimeException( - s"duplicate entries found in lapi_command_completions at offsets (first 10 shown) ${offsets.take(10)}" - ) - ) - - // duplicate completions by messageUuid - completions - .map { case (entry, offset) => - (entry.messageUuid, offset) - } - .collect { case (Some(messageUuid), offset) => - (messageUuid, offset) - } - .groupMapReduce(_._1)(entry => List(entry._2))(_ ::: _) - .find(_._2.sizeIs > 1) - .map(_._2) - .foreach(offsets => - throw new RuntimeException( - s"duplicate entries found by messageUuid in lapi_command_completions at offsets (first 10 shown) ${offsets - .take(10)}" - ) - ) - - val filterTableName = "lapi_filter_achs_stakeholder" - - // Verify no duplicate filter table entry - val filterTableEntries: Vector[(Long, Int)] = SQL""" - select - event_sequential_id, count(*) as count - from #$filterTableName - group by event_sequential_id, template_id, party_id - having count(*) > 1 - ${QueryStrategy.limitClause(Some(maxReportedDuplicates))} - """ - .asVectorOf(long("event_sequential_id") ~ int("count") map { case eventSeqId ~ count => - (eventSeqId, count) - })(connection) - // duplicate filter table entries - filterTableEntries - .foreach { case (eventSeqId, count) => - throw new RuntimeException( - s"duplicate entries found ($count in total) in filter table $filterTableName at event sequential id $eventSeqId" - ) - } - - // Verify all fields in lapi_filter_achs_stakeholder are also in lapi_filter_activate_stakeholder - val invalidEntries = SQL""" - select event_sequential_id - from lapi_filter_achs_stakeholder achs - where not exists ( - select 1 - from lapi_filter_activate_stakeholder activate - where achs.event_sequential_id = activate.event_sequential_id - and achs.template_id = activate.template_id - and achs.party_id = activate.party_id - ) - order by event_sequential_id - ${QueryStrategy.limitClause(Some(maxReportedDuplicates))} - """.asVectorOf(long("event_sequential_id"))(connection) - - if (invalidEntries.nonEmpty) { - throw new RuntimeException( - "lapi_filter_achs_stakeholder contains entries not present in lapi_filter_activate_stakeholder at event " + - s"sequential ids (first $maxReportedDuplicates shown): ${invalidEntries.mkString(", ")}" - ) - } - - // verify lapi_achs_state contains at most one row - val lapiAchsStateRowCount = SQL""" - select count(*) as count - from lapi_achs_state - """ - .asSingle(int("count"))(connection) - - if (lapiAchsStateRowCount > 1) { - throw new RuntimeException( - s"lapi_achs_state table contains more than one row: $lapiAchsStateRowCount rows found" - ) - } - - // Verify no duplicate completion entry - val internalContractIds = SQL""" - SELECT - internal_contract_id - FROM par_contracts - """ - .asVectorOf(long("internal_contract_id"))(connection) - val firstTenDuplicatedInternalIds = internalContractIds - .groupMap(identity)(identity) - .iterator - .filter(_._2.sizeIs > 1) - .take(10) - .map(_._1) - .toSeq - if (firstTenDuplicatedInternalIds.nonEmpty) - throw new RuntimeException( - s"duplicate internal_contract_id-s found in table par_contracts (first 10 shown) $firstTenDuplicatedInternalIds" - ) - - if (!inMemoryCantonStore) { - val pruning_started_up_to_inclusive = - SQL"""SELECT started_up_to_inclusive FROM par_pruning_operation - """ - .asSingleOpt(long("started_up_to_inclusive").?)(connection) - .flatten - .getOrElse(-1L) - - val referencedInternalContractIdsWithOffset = SQL""" - SELECT internal_contract_id, event_offset - FROM lapi_events_activate_contract - WHERE event_offset > $pruning_started_up_to_inclusive - UNION ALL - SELECT internal_contract_id, event_offset -- activations which were not deactivated before pruning point - FROM lapi_events_activate_contract - WHERE event_offset <= $pruning_started_up_to_inclusive - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract - WHERE lapi_events_deactivate_contract.deactivated_event_sequential_id = lapi_events_activate_contract.event_sequential_id - AND lapi_events_deactivate_contract.event_offset <= $pruning_started_up_to_inclusive - ) - UNION ALL - SELECT internal_contract_id, event_offset - FROM lapi_events_deactivate_contract - WHERE event_offset > $pruning_started_up_to_inclusive - AND internal_contract_id IS NOT NULL - UNION ALL - SELECT internal_contract_id, event_offset - FROM lapi_events_various_witnessed - WHERE event_offset > $pruning_started_up_to_inclusive - AND internal_contract_id IS NOT NULL - """ - .asVectorOf(long("internal_contract_id") ~ long("event_offset") map { - case internal_contract_id ~ event_offset => (internal_contract_id, event_offset) - })(connection) - val strayInternalContractIdsWithOffset = - referencedInternalContractIdsWithOffset - .filterNot(p => internalContractIds.contains(p._1)) - .distinct - .sorted - if (strayInternalContractIdsWithOffset.nonEmpty) { - throw new RuntimeException( - s"some internal_contract_id-s in events tables are not present in par_contracts (first 10 shown with offsets) ${strayInternalContractIdsWithOffset - .take(10) - .mkString("[", ", ", "]")}" - ) - } - } - - val strayDeactivationsWithOffset = - SQL""" - SELECT deactivated_event_sequential_id, event_offset - FROM lapi_events_deactivate_contract dea, lapi_parameters - WHERE deactivated_event_sequential_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_activate_contract act - WHERE act.event_sequential_id < dea.event_sequential_id - AND act.event_sequential_id = dea.deactivated_event_sequential_id - ) - AND lapi_parameters.ledger_end is not null - AND event_offset <= lapi_parameters.ledger_end - """ - .asVectorOf( - long("lapi_events_deactivate_contract.deactivated_event_sequential_id") ~ long( - "event_offset" - ) map { case deactivated_event_sequential_id ~ event_offset => - (deactivated_event_sequential_id, event_offset) - } - )( - connection - ) - .sorted - if (strayDeactivationsWithOffset.nonEmpty) - throw new RuntimeException( - s"some deactivation events do not have a preceding activation event, deactivated_event_sequential_id-s with offsets (first 10 shown) ${strayDeactivationsWithOffset - .take(10) - .mkString("[", ", ", "]")}" - ) - - val eventSequentialIdsWithMissingMandatories = - SQL""" - SELECT event_sequential_id, event_offset - FROM lapi_events_activate_contract - WHERE (event_type = 2 -- assign - AND (source_synchronizer_id IS NULL OR reassignment_counter IS NULL OR reassignment_id IS NULL)) - - UNION ALL - - SELECT event_sequential_id, event_offset - FROM lapi_events_deactivate_contract - WHERE (event_type = 3 -- consuming exercise - AND ( - additional_witnesses IS NULL OR - exercise_choice IS NULL OR - exercise_argument IS NULL OR - exercise_result IS NULL OR - exercise_actors IS NULL OR - contract_id IS NULL OR - ledger_effective_time IS NULL - ) - ) OR (event_type = 4 -- unassign - AND ( - reassignment_id IS NULL OR - target_synchronizer_id IS NULL OR - reassignment_counter IS NULL OR - contract_id IS NULL - ) - ) - - UNION ALL - - SELECT event_sequential_id, event_offset - FROM lapi_events_various_witnessed - WHERE (event_type = 5 -- non-consuming exercise - AND ( - consuming IS NULL OR - exercise_choice IS NULL OR - exercise_argument IS NULL OR - exercise_result IS NULL OR - exercise_actors IS NULL OR - contract_id IS NULL OR - template_id IS NULL OR - package_id IS NULL OR - ledger_effective_time IS NULL - ) - ) OR (event_type = 6 -- witnessed create - AND (representative_package_id IS NULL OR internal_contract_id IS NULL) - ) OR (event_type = 7 -- witnessed consuming exercise - AND ( - consuming IS NULL OR - exercise_choice IS NULL OR - exercise_argument IS NULL OR - exercise_result IS NULL OR - exercise_actors IS NULL OR - contract_id IS NULL OR - template_id IS NULL OR - package_id IS NULL OR - ledger_effective_time IS NULL - ) - ) - """ - .asVectorOf(long("event_sequential_id") ~ long("event_offset") map { - case event_sequential_id ~ event_offset => (event_sequential_id, event_offset) - })(connection) - if (eventSequentialIdsWithMissingMandatories.nonEmpty) - throw new RuntimeException( - s"some events are missing mandatory fields, event_sequential_ids, offsets (first 10 shown) ${eventSequentialIdsWithMissingMandatories - .take(10) - .mkString("[", ", ", "]")}" - ) - - val lapi_pruning_started_up_to_inclusive = - SQL"""SELECT participant_pruned_up_to_inclusive FROM lapi_parameters""" - .asSingleOpt(long("participant_pruned_up_to_inclusive").?)(connection) - .flatten - .getOrElse(-1L) - - val witnessedShouldHavePruned = - SQL""" - SELECT event_offset - FROM lapi_events_various_witnessed - WHERE event_offset <= $lapi_pruning_started_up_to_inclusive - """ - .asVectorOf(long("event_offset"))(connection) - .sorted - if (witnessedShouldHavePruned.nonEmpty) - throw new RuntimeException( - s"some events in various_witnessed have not been pruned, offsets (first 10 shown) ${witnessedShouldHavePruned - .take(10) - .mkString("[", ", ", "]")}" - ) - - val activateShouldHavePruned = - SQL""" - SELECT event_offset - FROM lapi_events_activate_contract - WHERE event_offset <= $lapi_pruning_started_up_to_inclusive - AND EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract - WHERE lapi_events_deactivate_contract.deactivated_event_sequential_id = lapi_events_activate_contract.event_sequential_id - AND lapi_events_deactivate_contract.event_offset <= $lapi_pruning_started_up_to_inclusive - AND lapi_events_activate_contract.event_type <> ${PersistentEventType.Assign.asInt} - AND lapi_events_deactivate_contract.event_type <> ${PersistentEventType.Unassign.asInt} - ) - """ - .asVectorOf(long("event_offset"))(connection) - .sorted - - if (activateShouldHavePruned.nonEmpty) - throw new RuntimeException( - s"some events in activate have not been pruned, offsets (first 10 shown) ${activateShouldHavePruned - .take(10) - .mkString("[", ", ", "]")}" - ) - - val deactivateShouldHavePruned = - SQL""" - SELECT event_offset - FROM lapi_events_deactivate_contract - WHERE event_offset < $lapi_pruning_started_up_to_inclusive - AND deactivated_event_sequential_id IS NULL - """ - .asVectorOf(long("event_offset"))(connection) - .sorted - - if (deactivateShouldHavePruned.nonEmpty) - throw new RuntimeException( - s"some events in deactivate have not been pruned, offsets (first 10 shown) ${deactivateShouldHavePruned - .take(10) - .mkString("[", ", ", "]")}" - ) - } catch { - case t: Throwable if !failForEmptyDB => - val failure = t.getMessage - val postgresEmptyDBError = failure.contains( - "relation \"lapi_events_activate_contract\" does not exist" - ) - val h2EmptyDBError = failure.contains( - "this database is empty" - ) - if (postgresEmptyDBError || h2EmptyDBError) { - () - } else { - throw t - } - } - - @VisibleForTesting - override def numberOfAcceptedTransactionsFor( - synchronizerId: SynchronizerId - )(connection: Connection): Int = - SQL"""SELECT internal_id - FROM lapi_string_interning - WHERE external_string = ${"d|" + synchronizerId.toProtoPrimitive} - """ - .asSingleOpt(int("internal_id"))(connection) - .map(internedSynchronizerId => SQL""" - SELECT COUNT(*) as count - FROM lapi_update_meta - WHERE synchronizer_id = $internedSynchronizerId - """.asSingle(int("count"))(connection)) - .getOrElse(0) - - /** ONLY FOR TESTING This is causing wiping of all LAPI event data. This should not be used during - * working indexer. - */ - @VisibleForTesting - override def moveLedgerEndBackToScratch()(connection: Connection): Unit = { - SQL"UPDATE lapi_parameters SET ledger_end = 1, ledger_end_sequential_id = 0" - .executeUpdate()(connection) - .discard - SQL"DELETE FROM lapi_post_processing_end".executeUpdate()(connection).discard - SQL"DELETE FROM lapi_ledger_end_synchronizer_index".executeUpdate()(connection).discard - SQL"DELETE FROM par_command_deduplication".executeUpdate()(connection).discard - SQL"DELETE FROM par_in_flight_submission".executeUpdate()(connection).discard - // clean these tables manually, as the ledger_end=1 is an actual ledger-end, and as these tables are cleaned by - // initialization based on offsets, some rubbish can remains (which can cause problems for example for integrity - // checking which motivated this change) - SQL"DELETE FROM lapi_command_completions".executeUpdate()(connection).discard - SQL"DELETE FROM lapi_party_entries".executeUpdate()(connection).discard - SQL"DELETE FROM lapi_update_meta".executeUpdate()(connection).discard - } - - private final case class CompletionEntry( - userId: Int, - submitters: List[Int], - commandId: String, - updateId: Option[UpdateId], - submissionId: Option[String], - messageUuid: Option[String], - recordTimeLong: Long, - synchronizerId: Long, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/MismatchException.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/MismatchException.scala deleted file mode 100644 index de7baa0503..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/MismatchException.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.ledger.api.ParticipantId as ApiParticipantId - -abstract class MismatchException[A]( - description: String, - val existing: A, - val provided: A, -) extends RuntimeException( - s"""The provided $description does not match the existing one. Existing: "$existing", Provided: "$provided".""" - ) - -object MismatchException { - - class ParticipantId( - override val existing: ApiParticipantId, - override val provided: ApiParticipantId, - ) extends MismatchException[ApiParticipantId]("participant id", existing, provided) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ParameterStorageBackendImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ParameterStorageBackendImpl.scala deleted file mode 100644 index 5f4bfa08da..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/ParameterStorageBackendImpl.scala +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.{int, long} -import anorm.{BatchSql, NamedParameter, RowParser, ~} -import com.daml.scalautil.Statement.discard -import com.digitalasset.canton.RepairCounter -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.participant.state.{RepairIndex, SynchronizerIndex} -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.backend.Conversions.offset -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsLastPointers, - AchsState, -} -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.{Conversions, ParameterStorageBackend} -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import scalaz.syntax.tag.* - -import java.sql.Connection - -import SimpleSqlExtensions.* - -private[backend] class ParameterStorageBackendImpl( - queryStrategy: QueryStrategy, - stringInterning: StringInterning, -) extends ParameterStorageBackend { - import Conversions.OffsetToStatement - - override def updateLedgerEnd( - ledgerEnd: ParameterStorageBackend.LedgerEnd, - lastSynchronizerIndex: Map[SynchronizerId, SynchronizerIndex] = Map.empty, - )(connection: Connection): Unit = discard { - queryStrategy.forceSynchronousCommitForCurrentTransactionForPostgreSQL(connection) - discard( - SQL""" - UPDATE - lapi_parameters - SET - ledger_end = ${ledgerEnd.lastOffset}, - ledger_end_sequential_id = ${ledgerEnd.lastEventSeqId}, - ledger_end_string_interning_id = ${ledgerEnd.lastStringInterningId}, - ledger_end_publication_time = ${ledgerEnd.lastPublicationTime.toMicros} - """ - .execute()(connection) - ) - - batchUpsert( - """INSERT INTO - | lapi_ledger_end_synchronizer_index - | (synchronizer_id, sequencer_timestamp, repair_timestamp, repair_counter, record_time) - |VALUES - | ({internalizedSynchronizerId}, {sequencerTimestampMicros}, {repairTimestampMicros}, {repairCounter}, {recordTimeMicros}) - |""".stripMargin, - """UPDATE - | lapi_ledger_end_synchronizer_index - |SET - | sequencer_timestamp = case when {sequencerTimestampMicros} is null then sequencer_timestamp else {sequencerTimestampMicros} end, - | repair_timestamp = case when {repairTimestampMicros} is null then repair_timestamp else {repairTimestampMicros} end, - | repair_counter = case when {repairTimestampMicros} is null then repair_counter else {repairCounter} end, - | record_time = {recordTimeMicros} - |WHERE - | synchronizer_id = {internalizedSynchronizerId} - |""".stripMargin, - lastSynchronizerIndex.toList.map { case (synchronizerId, synchronizerIndex) => - Seq[NamedParameter]( - "internalizedSynchronizerId" -> stringInterning.synchronizerId.internalize( - synchronizerId - ), - "sequencerTimestampMicros" -> synchronizerIndex.sequencerIndex.map( - _.toMicros - ), - "repairTimestampMicros" -> synchronizerIndex.repairIndex.map(_.timestamp.toMicros), - "repairCounter" -> synchronizerIndex.repairIndex.map(_.counter.unwrap), - "recordTimeMicros" -> synchronizerIndex.recordTime.toMicros, - ) - }, - )(connection) - } - - private val SqlGetLedgerEnd = - SQL""" - SELECT - ledger_end, - ledger_end_sequential_id, - ledger_end_string_interning_id, - ledger_end_publication_time - FROM - lapi_parameters - """ - - override def ledgerEnd(connection: Connection): Option[ParameterStorageBackend.LedgerEnd] = - SqlGetLedgerEnd - .as(LedgerEndParser.singleOpt)(connection) - .flatten - - private val TableName: String = "lapi_parameters" - private val ParticipantIdColumnName: String = "participant_id" - private val LedgerEndColumnName: String = "ledger_end" - private val LedgerEndSequentialIdColumnName: String = "ledger_end_sequential_id" - private val LedgerEndStringInterningIdColumnName: String = "ledger_end_string_interning_id" - private val LedgerEndPublicationTimeColumnName: String = "ledger_end_publication_time" - - private val ParticipantIdParser: RowParser[ParticipantId] = - Conversions.participantId(ParticipantIdColumnName).map(ParticipantId(_)) - - private val LedgerEndOffsetParser: RowParser[Option[Offset]] = - offset(LedgerEndColumnName).? - - private val LedgerEndSequentialIdParser: RowParser[Option[Long]] = - long(LedgerEndSequentialIdColumnName).? - - private val LedgerEndStringInterningIdParser: RowParser[Option[Int]] = - int(LedgerEndStringInterningIdColumnName).? - - private val LedgerIdentityParser: RowParser[ParameterStorageBackend.IdentityParams] = - ParticipantIdParser map { case participantId => - ParameterStorageBackend.IdentityParams(participantId) - } - - private val LedgerEndPublicationTimeParser: RowParser[Option[CantonTimestamp]] = - long(LedgerEndPublicationTimeColumnName).map(CantonTimestamp.ofEpochMicro).? - - private val LedgerEndParser: RowParser[Option[ParameterStorageBackend.LedgerEnd]] = - LedgerEndOffsetParser ~ LedgerEndSequentialIdParser ~ LedgerEndStringInterningIdParser ~ LedgerEndPublicationTimeParser map { - case Some(lastOffset) ~ Some(lastEventSequentialId) ~ - Some(lastStringInterningId) ~ Some(lastPublicationTime) => - // the four values are updated the same time, so it is expected that if one is not null, then all of them will not be null - Some( - ParameterStorageBackend.LedgerEnd( - lastOffset, - lastEventSequentialId, - lastStringInterningId, - lastPublicationTime, - ) - ) - case None ~ None ~ None ~ None => None - case _ => - throw new IllegalStateException( - "The offset, eventSequentialId, stringInterningId and publicationTime of the ledger end should have been defined at the same time" - ) - } - - override def initializeParameters( - params: ParameterStorageBackend.IdentityParams, - loggerFactory: NamedLoggerFactory, - )(connection: Connection): Unit = { - val logger = loggerFactory.getTracedLogger(getClass) - implicit val traceContext: TraceContext = TraceContext.empty - // Note: this method is the only one that inserts a row into the parameters table - val previous = ledgerIdentity(connection) - val participantId = params.participantId - previous match { - case None => - logger.info( - s"Initializing new database for participantId '${params.participantId}'" - ) - val lastOffset: Option[Offset] = None - val lastEventSeqId: Option[Long] = None - val lastStringInterningId: Option[Int] = None - val lastPublicationTime: Option[Long] = None - - discard( - SQL"""insert into #$TableName( - #$ParticipantIdColumnName, - #$LedgerEndColumnName, - #$LedgerEndSequentialIdColumnName, - #$LedgerEndStringInterningIdColumnName, - #$LedgerEndPublicationTimeColumnName - ) values( - ${participantId.unwrap: String}, - ${lastOffset.map(_.unwrap)}, - $lastEventSeqId, - $lastStringInterningId, - $lastPublicationTime - )""" - .execute()(connection) - ) - case Some(ParameterStorageBackend.IdentityParams(`participantId`)) => - logger.info( - s"Found existing database for participantId '${params.participantId}'" - ) - case Some(ParameterStorageBackend.IdentityParams(existing)) => - logger.error( - s"Found existing database with mismatching participantId: existing '$existing', provided '${params.participantId}'" - ) - throw new MismatchException.ParticipantId( - existing = existing, - provided = params.participantId, - ) - } - } - - override def ledgerIdentity( - connection: Connection - ): Option[ParameterStorageBackend.IdentityParams] = - SQL"select #$ParticipantIdColumnName from #$TableName" - .as(LedgerIdentityParser.singleOpt)(connection) - - override def updatePrunedUptoInclusive( - prunedUpToInclusive: Offset - )(connection: Connection): Unit = - discard( - SQL""" - update lapi_parameters set participant_pruned_up_to_inclusive=$prunedUpToInclusive - where participant_pruned_up_to_inclusive < $prunedUpToInclusive or participant_pruned_up_to_inclusive is null - """ - .execute()(connection) - ) - - private val SqlSelectMostRecentPruning = - SQL"select participant_pruned_up_to_inclusive from lapi_parameters" - - override def prunedUpToInclusive(connection: Connection): Option[Offset] = - SqlSelectMostRecentPruning - .as(offset("participant_pruned_up_to_inclusive").?.single)(connection) - - private val SqlSelectMostRecentPruningAndLedgerEnd = - SQL"select participant_pruned_up_to_inclusive, #$LedgerEndColumnName from lapi_parameters" - - private val PruneUptoInclusiveAndLedgerEndParser - : RowParser[ParameterStorageBackend.PruneUptoInclusiveAndLedgerEnd] = - offset("participant_pruned_up_to_inclusive").? ~ LedgerEndOffsetParser map { - case pruneUptoInclusive ~ ledgerEndOffset => - ParameterStorageBackend.PruneUptoInclusiveAndLedgerEnd( - pruneUptoInclusive = pruneUptoInclusive, - ledgerEnd = ledgerEndOffset, - ) - } - - override def prunedUpToInclusiveAndLedgerEnd( - connection: Connection - ): ParameterStorageBackend.PruneUptoInclusiveAndLedgerEnd = - SqlSelectMostRecentPruningAndLedgerEnd - .as(PruneUptoInclusiveAndLedgerEndParser.singleOpt)(connection) - .getOrElse( - ParameterStorageBackend.PruneUptoInclusiveAndLedgerEnd( - pruneUptoInclusive = None, - ledgerEnd = None, - ) - ) - - override def cleanSynchronizerIndex(synchronizerId: SynchronizerId)( - connection: Connection - ): Option[SynchronizerIndex] = - stringInterning.synchronizerId - .tryInternalize(synchronizerId) - .orElse( - // allow fallback to stringInterning persistence here to allow broader usage with tricky state inspection integration tests - SQL""" - SELECT internal_id - FROM lapi_string_interning - WHERE external_string = ${"d|" + synchronizerId.toProtoPrimitive} - """ - .asSingleOpt(int("internal_id"))(connection) - ) - .flatMap(internedSynchronizerId => - SQL""" - SELECT - sequencer_timestamp, - repair_timestamp, - repair_counter, - record_time - FROM - lapi_ledger_end_synchronizer_index - WHERE - synchronizer_id = $internedSynchronizerId - """ - .asSingleOpt( - for { - repairTimestampO <- long("repair_timestamp").? - repairCounterO <- long("repair_counter").? - sequencerTimestampO <- long("sequencer_timestamp").? - recordTime <- long("record_time") - } yield { - val repairIndex = (repairTimestampO, repairCounterO) match { - case (Some(repairTimestamp), Some(repairCounter)) => - List( - SynchronizerIndex.forRepairUpdate( - RepairIndex( - timestamp = CantonTimestamp.ofEpochMicro(repairTimestamp), - counter = RepairCounter(repairCounter), - ) - ) - ) - - case (None, None) => - Nil - - case _ => - throw new IllegalStateException( - s"Invalid persisted data in lapi_ledger_end_synchronizer_index table: either both repair_counter and repair_timestamp should be defined or none of them, but an invalid combination found for synchronizer:${synchronizerId.toProtoPrimitive} repair_counter: $repairCounterO, repair_timestamp: $repairTimestampO" - ) - } - val sequencerIndex = sequencerTimestampO - .map(CantonTimestamp.ofEpochMicro) - .map(SynchronizerIndex.forSequencedUpdate) - .toList - val recordTimeSynchronizerIndex = SynchronizerIndex.forFloatingUpdate( - CantonTimestamp.ofEpochMicro(recordTime) - ) - (recordTimeSynchronizerIndex :: repairIndex ::: sequencerIndex) - .reduceOption(_ max _) - .getOrElse( - throw new IllegalStateException( - s"Invalid persisted data in lapi_ledger_end_synchronizer_index table: none of the optional fields are defined for synchronizer ${synchronizerId.toProtoPrimitive}" - ) - ) - } - )(connection) - ) - - override def updatePostProcessingEnd(postProcessingEnd: Option[Offset])( - connection: Connection - ): Unit = - batchUpsert( - "INSERT INTO lapi_post_processing_end VALUES ({postProcessingEnd})", - "UPDATE lapi_post_processing_end SET post_processing_end = {postProcessingEnd}", - List( - Seq[NamedParameter]( - "postProcessingEnd" -> postProcessingEnd.map(_.unwrap) - ) - ), - )(connection) - - override def postProcessingEnd(connection: Connection): Option[Offset] = - SQL"select post_processing_end from lapi_post_processing_end" - .asSingleOpt( - offset("post_processing_end").? - )(connection) - .flatten - - def fetchACHSState(connection: Connection): Option[AchsState] = - SQL"select valid_at, last_removed, last_populated from lapi_achs_state" - .asSingleOpt( - for { - validAt <- long("valid_at") - lastRemoved <- long("last_removed") - lastPopulated <- long("last_populated") - } yield AchsState( - validAt = validAt, - lastPointers = AchsLastPointers( - lastRemoved = lastRemoved, - lastPopulated = lastPopulated, - ), - ) - )(connection) - - def insertACHSState(achsState: AchsState)( - connection: Connection - ): Unit = - discard( - SQL""" - insert into lapi_achs_state (valid_at, last_removed, last_populated) - values ( - ${achsState.validAt}, - ${achsState.lastPointers.lastRemoved}, - ${achsState.lastPointers.lastPopulated} - ) - """.execute()(connection) - ) - - def updateACHSValidAt(validAt: Long)( - connection: Connection - ): Unit = { - val updatedRows = - SQL"update lapi_achs_state set valid_at = $validAt".executeUpdate()(connection) - - if (updatedRows == 0) { - throw new IllegalStateException("Failed to update valid_at in lapi_achs_state table.") - } - } - - def updateACHSLastPointers(pointers: AchsLastPointers)( - connection: Connection - ): Unit = { - val updatedRows = - SQL"update lapi_achs_state set last_removed = ${pointers.lastRemoved}, last_populated = ${pointers.lastPopulated}" - .executeUpdate()(connection) - - if (updatedRows == 0) { - throw new IllegalStateException("Failed to update lapi_achs_state table.") - } - } - def clearACHSState(connection: Connection): Unit = - discard( - SQL"truncate table lapi_achs_state".execute()(connection) - ) - - def clearAchsData(connection: Connection): Unit = { - clearACHSState(connection) - discard(SQL"truncate table lapi_filter_achs_stakeholder".execute()(connection)) - } - - private def batchSql( - sqlWithNamedParams: String, - namedParamsBatch: List[Seq[NamedParameter]], - )(connection: Connection): Array[Int] = - namedParamsBatch match { - case Nil => Array.empty - case head :: tail => - BatchSql(sqlWithNamedParams, head, tail*).execute()(connection) - } - - private def batchUpsert( - insertSql: String, - updateSql: String, - namedParamsBatch: List[Seq[NamedParameter]], - )(connection: Connection): Unit = { - val updateCounts = batchSql(updateSql, namedParamsBatch)(connection) - val insertCounts = batchSql( - insertSql, - updateCounts.toList - .zip(namedParamsBatch) - .filter( - _._1 == 0 - ) // collecting all failed updates, these are the missing entries in the table, which we need to insert - .map(_._2), - )(connection) - assert( - insertCounts.forall(_ == 1), - "batch upserting should succeed for all inserts (maybe batch upserts are running in parallel?)", - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/PartyStorageBackendTemplate.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/PartyStorageBackendTemplate.scala deleted file mode 100644 index 5ddc688789..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/PartyStorageBackendTemplate.scala +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.{bool, str} -import anorm.{RowParser, ~} -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.platform.Party -import com.digitalasset.canton.platform.store.backend.PartyStorageBackend -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* -import com.digitalasset.canton.platform.store.cache.LedgerEndCache - -import java.sql.Connection - -class PartyStorageBackendTemplate(ledgerEndCache: LedgerEndCache) extends PartyStorageBackend { - - private val partyDetailsParser: RowParser[IndexerPartyDetails] = { - import com.digitalasset.canton.platform.store.backend.Conversions.bigDecimalColumnToBoolean - str("party") ~ - bool("is_local") map { case party ~ isLocal => - IndexerPartyDetails( - party = Party.assertFromString(party), - isLocal = isLocal, - ) - } - } - - private def queryParties( - partyFilter: ComposableQuery.CompositeSql, - limitClause: ComposableQuery.CompositeSql, - connection: Connection, - ): Vector[IndexerPartyDetails] = - ledgerEndCache() match { - case None => Vector.empty - case Some(ledgerEnd) => - import com.digitalasset.canton.platform.store.backend.Conversions.OffsetToStatement - SQL""" - SELECT - party, - #${QueryStrategy.booleanOrAggregationFunction}(is_local) is_local - FROM lapi_party_entries - WHERE - ledger_offset <= ${ledgerEnd.lastOffset} AND - $partyFilter - typ = 'accept' - GROUP BY party - ORDER BY party - $limitClause - """.asVectorOf(partyDetailsParser)(connection) - } - - override def parties(parties: Seq[Party])(connection: Connection): List[IndexerPartyDetails] = { - val requestedParties = parties.view.map(_.toString).toSet - val partyFilter = cSQL"lapi_party_entries.party in ($requestedParties) AND" - queryParties(partyFilter, cSQL"", connection).toList - } - - override def knownParties( - fromExcl: Option[Party], - filterString: Option[String185], - maxResults: Int, - )( - connection: Connection - ): List[IndexerPartyDetails] = { - - val offsetPartyFilter = fromExcl match { - case Some(id: String) => cSQL"lapi_party_entries.party > $id AND" - case None => cSQL"" - } - val partyFilter = filterString match { - case Some(filter) => - cSQL"$offsetPartyFilter lapi_party_entries.party LIKE ${filter.str + "%"} AND" - case None => offsetPartyFilter - } - queryParties( - partyFilter = partyFilter, - limitClause = QueryStrategy.limitClause(Some(maxResults)), - connection = connection, - ).toList - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/QueryStrategy.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/QueryStrategy.scala deleted file mode 100644 index cde32710b3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/QueryStrategy.scala +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.{ - CompositeSql, - SqlStringInterpolation, -} -import com.digitalasset.canton.util.ResourceUtil -import com.typesafe.scalalogging.Logger - -import java.sql.{Connection, ResultSet} - -object QueryStrategy { - - /** This populates the following part of the query: SELECT ... WHERE ... ORDER BY ... [THIS PART] - * - * @param limit - * optional limit - * @return - * the composable SQL - */ - def limitClause(limit: Option[Int]): CompositeSql = - limit - .map(to => cSQL"fetch next $to rows only") - .getOrElse(cSQL"") - - /** Would be used in column selectors in GROUP BY situations to see whether a boolean column had - * true Example: getting all groups and see wheter they have someone who had covid: SELECT - * group_name, booleanOrAggregationFunction(has_covid) GROUP BY group_name; - * - * @return - * the function name - */ - def booleanOrAggregationFunction: String = "bool_or" - - /** Select a singleton element from some column based on max value of another column - * - * @param singletonColumn - * column whose value should be returned when the orderingColumn hits max - * @param orderingColumn - * column used for sorting the input rows - * @return - * an sql clause to be composed into the sql query - */ - def lastByProxyAggregateFuction(singletonColumn: String, orderingColumn: String): String = - s"(array_agg($singletonColumn ORDER BY $orderingColumn DESC))[1]" - - /** Constant boolean to be used in a SELECT clause */ - def constBooleanSelect(value: Boolean): String = - if (value) "true" else "false" - - /** Constant boolean to be used in a WHERE clause */ - def constBooleanWhere(value: Boolean): String = - if (value) "true" else "false" - - /** Expression for `(offset > startExclusive)` - * - * The offset column must only contain valid offsets (no NULLs) - */ - def offsetIsGreater( - nonNullableColumn: String, - startExclusive: Option[Offset], - ): CompositeSql = { - import com.digitalasset.canton.platform.store.backend.Conversions.OffsetToStatement - // Note: casing Offset.beforeBegin makes the resulting query simpler: - startExclusive match { - case None => cSQL"#${constBooleanWhere(true)}" - case Some(start) => cSQL"#$nonNullableColumn > $start" - } - } - - /** Expression for `(offset <= endInclusive)` - * - * The offset column must only contain valid offsets (no NULLs) - */ - def offsetIsLessOrEqual( - nonNullableColumn: String, - endInclusiveO: Option[Offset], - ): CompositeSql = { - import com.digitalasset.canton.platform.store.backend.Conversions.OffsetToStatement - endInclusiveO match { - case None => cSQL"#${constBooleanWhere(false)}" - case Some(endInclusive) => - cSQL"#$nonNullableColumn <= $endInclusive" - } - } - - /** Expression for `(eventSeqId > limit)` - * - * The column must only contain valid integers (no NULLs) - */ - def eventSeqIdIsGreater( - nonNullableColumn: String, - limitO: Option[Long], - ): CompositeSql = - limitO match { - case None => cSQL"#${constBooleanWhere(true)}" - case Some(limit) => cSQL"#$nonNullableColumn > $limit" - } - - /** Expression for `(startInclusive <= offset <= endExclusive)` - * - * The offset column must only contain valid offsets (no NULLs) - */ - def offsetIsBetween( - nonNullableColumn: String, - startInclusive: Offset, - endInclusive: Offset, - ): CompositeSql = { - import com.digitalasset.canton.platform.store.backend.Conversions.OffsetToStatement - // Note: special casing Offset.firstOffset makes the resulting query simpler: - if (startInclusive == Offset.firstOffset) { - cSQL"#$nonNullableColumn <= $endInclusive" - } else { - cSQL"(#$nonNullableColumn >= $startInclusive and #$nonNullableColumn <= $endInclusive)" - } - } - - def plainJdbcQuery[T]( - querySqlString: String - )(parser: ResultSet => T)(connection: Connection): Vector[T] = - ResourceUtil.withResource(connection.createStatement())(statement => - ResourceUtil.withResource(statement.executeQuery(querySqlString)) { resultSet => - Iterator - .continually(resultSet.next()) - .takeWhile(identity) - .map(_ => parser(resultSet)) - .toVector - } - ) - - def withoutNetworkTimeout[T]( - f: Connection => T - )(implicit connection: Connection, logger: Logger): T = { - // The postgres jdbc driver ignores the execution context for the network timeout setting and uses its internal - // query executor https://github.com/pgjdbc/pgjdbc/blob/release/42.7.x/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java#L1692 . - // Even if it is not ignored in the future, it is beneficial to use a direct execution context since the thread which - // will call the JDBC execute will close the connection. If an error with setting the network timeout arises, the - // connection needs to be closed anyway and with direct execution context the same thread will be used. - // The H2 driver does not support the network timeout setting altogether. - val directEc = DirectExecutionContext(logger) - val originalNetworkTimeout = connection.getNetworkTimeout - try { - connection.setNetworkTimeout(directEc, 0) // disable network timeout - f(connection) - } finally { - connection.setNetworkTimeout(directEc, originalNetworkTimeout) // restore original timeout - } - } - -} - -trait QueryStrategy { - - /** ANY SQL clause generation for a number of Long values - */ - def anyOf(longs: Iterable[Long]): CompositeSql = { - val longArray: Array[java.lang.Long] = - longs.view.map(Long.box).toArray - cSQL"= ANY($longArray)" - } - - /** ANY SQL clause generation for a number of smallint values - */ - def anyOfSmallInts(ints: Iterable[Int]): CompositeSql = { - val intArray: Array[java.lang.Integer] = - ints.view.map(Int.box).toArray - cSQL"= ANY($intArray)" - } - - /** ANY SQL clause generation for a number of String values - */ - def anyOfStrings(strings: Iterable[String]): CompositeSql = { - val stringArray: Array[String] = - strings.toArray - cSQL"= ANY($stringArray)" - } - - /** ANY SQL clause generation for a number of Binary values - */ - def anyOfBinary(binaries: Iterable[Array[Byte]]): CompositeSql = { - val binaryArray: Array[Array[Byte]] = - binaries.toArray - cSQL"= ANY($binaryArray)" - } - - /** SQL clause to check if an element is in a given batch whether the batch is defined as a range - * or a list of numbers - */ - def inBatch(colName: String, batch: SequentialIdBatch): CompositeSql = batch match { - case SequentialIdBatch.IdRange(fromInclusive, toInclusive) => - cSQL"(#$colName >= $fromInclusive AND #$colName <= $toInclusive)" - case SequentialIdBatch.Ids(ids) => cSQL"#$colName ${anyOf(ids)}" - } - - def analyzeTable(tableName: String): CompositeSql - - def forceSynchronousCommitForCurrentTransactionForPostgreSQL(connection: Connection): Unit = () -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Schema.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Schema.scala deleted file mode 100644 index a113f70dd4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Schema.scala +++ /dev/null @@ -1,441 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.platform.store.backend.Conversions.IntArrayDBSerialization.encodeToByteArray -import com.digitalasset.canton.platform.store.backend.DbDto -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.daml.lf.data.Ref.{ChoiceName, Identifier, NameTypeConRef, Party} - -import java.sql.Connection -import scala.reflect.ClassTag - -private[backend] trait Schema[From] { - def prepareData(in: Vector[From], stringInterning: StringInterning): Array[Array[Array[?]]] - def executeUpdate(data: Array[Array[Array[?]]], connection: Connection): Unit -} - -private[backend] object AppendOnlySchema { - - type Batch = Array[Array[Array[?]]] - - private[backend] trait FieldStrategy { - def string[From](extractor: StringInterning => From => String): Field[From, String, ?] = - StringField(extractor) - - def stringOptional[From]( - extractor: StringInterning => From => Option[String] - ): Field[From, Option[String], ?] = - StringOptional(extractor) - - def stringArray[From]( - extractor: StringInterning => From => Iterable[String] - ): Field[From, Iterable[String], ?] = - StringArray(extractor) - - def bytea[From]( - extractor: StringInterning => From => Array[Byte] - ): Field[From, Array[Byte], ?] = - Bytea(extractor) - - def byteaOptional[From]( - extractor: StringInterning => From => Option[Array[Byte]] - ): Field[From, Option[Array[Byte]], ?] = - ByteaOptional(extractor) - - def party[From](extractor: From => Party): Field[From, Int, ?] = - int[From](stringInterning => from => stringInterning.party.internalize(extractor(from))) - - def partyOptional[From](extractor: From => Option[Party]): Field[From, Option[Int], ?] = - intOptional[From](stringInterning => - from => extractor(from).map(stringInterning.party.internalize) - ) - - def parties[From]( - extractor: From => Set[Party] - ): Field[From, Array[Byte], ?] = - bytea(stringInterning => - from => - encodeToByteArray( - extractor(from).map(stringInterning.party.internalize) - ) - ) - - def partiesOptional[From]( - extractor: From => Option[Set[Party]] - ): Field[From, Option[Array[Byte]], ?] = - byteaOptional(stringInterning => - from => - extractor(from) - .map(_.map(stringInterning.party.internalize)) - .map(encodeToByteArray) - ) - - def template[From](extractor: From => NameTypeConRef): Field[From, Int, ?] = - int[From](intern => from => intern.templateId.internalize(extractor(from))) - - def templateOptional[From]( - extractor: From => Option[NameTypeConRef] - ): Field[From, Option[Int], ?] = - intOptional[From](intern => from => extractor(from).map(intern.templateId.internalize)) - - def interface[From](extractor: From => Identifier): Field[From, Int, ?] = - int[From](intern => from => intern.interfaceId.internalize(extractor(from))) - - def interfaceOptional[From]( - extractor: From => Option[Identifier] - ): Field[From, Option[Int], ?] = - intOptional[From](intern => from => extractor(from).map(intern.interfaceId.internalize)) - - def choice[From](extractor: From => ChoiceName): Field[From, Int, ?] = - int[From](intern => from => intern.choiceName.internalize(extractor(from))) - - def choiceOptional[From](extractor: From => Option[ChoiceName]): Field[From, Option[Int], ?] = - intOptional[From](intern => from => extractor(from).map(intern.choiceName.internalize)) - - def bigint[From](extractor: StringInterning => From => Long): Field[From, Long, ?] = - Bigint(extractor) - - def bigintOptional[From]( - extractor: StringInterning => From => Option[Long] - ): Field[From, Option[Long], ?] = - BigintOptional(extractor) - - def smallintOptional[From]( - extractor: StringInterning => From => Option[Int] - ): Field[From, Option[Int], ?] = - SmallintOptional(extractor) - - def smallint[From]( - extractor: StringInterning => From => Int - ): Field[From, Int, ?] = - Smallint(extractor) - - def int[From](extractor: StringInterning => From => Int): Field[From, Int, ?] = - Integer(extractor) - - def intOptional[From]( - extractor: StringInterning => From => Option[Int] - ): Field[From, Option[Int], ?] = - IntOptional(extractor) - - def booleanOptional[From]( - extractor: StringInterning => From => Option[Boolean] - ): Field[From, Option[Boolean], ?] = - BooleanOptional(extractor) - - def boolean[From]( - extractor: StringInterning => From => Boolean - ): Field[From, Boolean, ?] = - BooleanMandatory(extractor) - - def insert[From](tableName: String)(fields: (String, Field[From, ?, ?])*): Table[From] - } - - def apply(fieldStrategy: FieldStrategy): Schema[DbDto] = { - def idFilter[T <: DbDto.IdFilterDbDto](tableName: String): Table[T] = - fieldStrategy.insert(tableName)( - "event_sequential_id" -> fieldStrategy.bigint(_ => _.idFilter.event_sequential_id), - "template_id" -> fieldStrategy.template(_.idFilter.template_id), - "party_id" -> fieldStrategy.party(_.idFilter.party_id), - "first_per_sequential_id" -> fieldStrategy.booleanOptional(_ => - dto => Option.when(dto.idFilter.first_per_sequential_id)(true) - ), - ) - - val eventActivate: Table[DbDto.EventActivate] = - fieldStrategy.insert("lapi_events_activate_contract")( - // update related columns - "event_offset" -> fieldStrategy.bigint(_ => _.event_offset), - "update_id" -> fieldStrategy.bytea(_ => _.update_id), - "workflow_id" -> fieldStrategy.stringOptional(_ => _.workflow_id), - "command_id" -> fieldStrategy.stringOptional(_ => _.command_id), - "submitters" -> fieldStrategy.partiesOptional(_.submitters), - "record_time" -> fieldStrategy.bigint(_ => _.record_time), - "synchronizer_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.synchronizerId.internalize(dbDto.synchronizer_id) - ), - "trace_context" -> fieldStrategy.bytea(_ => _.trace_context), - "external_transaction_hash" -> fieldStrategy.byteaOptional(_ => - _.external_transaction_hash - ), - "traffic_cost" -> fieldStrategy.bigintOptional(_ => _.traffic_cost), - - // event related columns - "event_type" -> fieldStrategy.smallint(_ => _.event_type), - "event_sequential_id" -> fieldStrategy.bigint(_ => _.event_sequential_id), - "node_id" -> fieldStrategy.int(_ => _.node_id), - "additional_witnesses" -> fieldStrategy.partiesOptional(_.additional_witnesses), - "source_synchronizer_id" -> fieldStrategy.intOptional(stringInterning => - _.source_synchronizer_id.map(stringInterning.synchronizerId.internalize) - ), - "reassignment_counter" -> fieldStrategy.bigintOptional(_ => _.reassignment_counter), - "reassignment_id" -> fieldStrategy.byteaOptional(_ => _.reassignment_id), - "representative_package_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.packageId.unsafe.internalize(dbDto.representative_package_id) - ), - - // contract related columns - "internal_contract_id" -> fieldStrategy.bigint(_ => _.internal_contract_id), - "create_key_hash" -> fieldStrategy.stringOptional(_ => _.create_key_hash), - ) - val idFilterActivateStakeholder: Table[DbDto.IdFilterActivateStakeholder] = - idFilter("lapi_filter_activate_stakeholder") - val idFilterActivateWitness: Table[DbDto.IdFilterActivateWitness] = - idFilter("lapi_filter_activate_witness") - - val eventDeactivate: Table[DbDto.EventDeactivate] = - fieldStrategy.insert("lapi_events_deactivate_contract")( - // update related columns - "event_offset" -> fieldStrategy.bigint(_ => _.event_offset), - "update_id" -> fieldStrategy.bytea(_ => _.update_id), - "workflow_id" -> fieldStrategy.stringOptional(_ => _.workflow_id), - "command_id" -> fieldStrategy.stringOptional(_ => _.command_id), - "submitters" -> fieldStrategy.partiesOptional(_.submitters), - "record_time" -> fieldStrategy.bigint(_ => _.record_time), - "synchronizer_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.synchronizerId.internalize(dbDto.synchronizer_id) - ), - "trace_context" -> fieldStrategy.bytea(_ => _.trace_context), - "external_transaction_hash" -> fieldStrategy.byteaOptional(_ => - _.external_transaction_hash - ), - "traffic_cost" -> fieldStrategy.bigintOptional(_ => _.traffic_cost), - - // event related columns - "event_type" -> fieldStrategy.smallint(_ => _.event_type), - "event_sequential_id" -> fieldStrategy.bigint(_ => _.event_sequential_id), - "node_id" -> fieldStrategy.int(_ => _.node_id), - "deactivated_event_sequential_id" -> fieldStrategy.bigintOptional(_ => - _.deactivated_event_sequential_id - ), - "additional_witnesses" -> fieldStrategy.partiesOptional(_.additional_witnesses), - "exercise_choice" -> fieldStrategy.choiceOptional(_.exercise_choice), - "exercise_choice_interface" -> fieldStrategy.interfaceOptional( - _.exercise_choice_interface_id - ), - "exercise_argument" -> fieldStrategy.byteaOptional(_ => _.exercise_argument), - "exercise_result" -> fieldStrategy.byteaOptional(_ => _.exercise_result), - "exercise_actors" -> fieldStrategy.partiesOptional(_.exercise_actors), - "exercise_last_descendant_node_id" -> fieldStrategy.intOptional(_ => - _.exercise_last_descendant_node_id - ), - "exercise_argument_compression" -> fieldStrategy.smallintOptional(_ => - _.exercise_argument_compression - ), - "exercise_result_compression" -> fieldStrategy.smallintOptional(_ => - _.exercise_result_compression - ), - "reassignment_id" -> fieldStrategy.byteaOptional(_ => _.reassignment_id), - "assignment_exclusivity" -> fieldStrategy.bigintOptional(_ => _.assignment_exclusivity), - "target_synchronizer_id" -> fieldStrategy.intOptional(stringInterning => - _.target_synchronizer_id.map(stringInterning.synchronizerId.internalize) - ), - "reassignment_counter" -> fieldStrategy.bigintOptional(_ => _.reassignment_counter), - - // contract related columns - "contract_id" -> fieldStrategy.bytea(_ => _.contract_id.toBytes.toByteArray), - "internal_contract_id" -> fieldStrategy.bigintOptional(_ => _.internal_contract_id), - "template_id" -> fieldStrategy.template(_.template_id), - "package_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.packageId.unsafe.internalize(dbDto.package_id) - ), - "stakeholders" -> fieldStrategy.parties(_.stakeholders), - "ledger_effective_time" -> fieldStrategy.bigintOptional(_ => _.ledger_effective_time), - ) - val idFilterDeactivateStakeholder: Table[DbDto.IdFilterDeactivateStakeholder] = - idFilter("lapi_filter_deactivate_stakeholder") - val idFilterDeactivateWitness: Table[DbDto.IdFilterDeactivateWitness] = - idFilter("lapi_filter_deactivate_witness") - - val eventVariousWitnessed: Table[DbDto.EventVariousWitnessed] = - fieldStrategy.insert("lapi_events_various_witnessed")( - // update related columns - "event_offset" -> fieldStrategy.bigint(_ => _.event_offset), - "update_id" -> fieldStrategy.bytea(_ => _.update_id), - "workflow_id" -> fieldStrategy.stringOptional(_ => _.workflow_id), - "command_id" -> fieldStrategy.stringOptional(_ => _.command_id), - "submitters" -> fieldStrategy.partiesOptional(_.submitters), - "record_time" -> fieldStrategy.bigint(_ => _.record_time), - "synchronizer_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.synchronizerId.internalize(dbDto.synchronizer_id) - ), - "trace_context" -> fieldStrategy.bytea(_ => _.trace_context), - "external_transaction_hash" -> fieldStrategy.byteaOptional(_ => - _.external_transaction_hash - ), - "traffic_cost" -> fieldStrategy.bigintOptional(_ => _.traffic_cost), - - // event related columns - "event_type" -> fieldStrategy.smallint(_ => _.event_type), - "event_sequential_id" -> fieldStrategy.bigint(_ => _.event_sequential_id), - "node_id" -> fieldStrategy.int(_ => _.node_id), - "additional_witnesses" -> fieldStrategy.parties(_.additional_witnesses), - "consuming" -> fieldStrategy.booleanOptional(_ => _.consuming), - "exercise_choice" -> fieldStrategy.choiceOptional(_.exercise_choice), - "exercise_choice_interface" -> fieldStrategy.interfaceOptional( - _.exercise_choice_interface_id - ), - "exercise_argument" -> fieldStrategy.byteaOptional(_ => _.exercise_argument), - "exercise_result" -> fieldStrategy.byteaOptional(_ => _.exercise_result), - "exercise_actors" -> fieldStrategy.partiesOptional(_.exercise_actors), - "exercise_last_descendant_node_id" -> fieldStrategy.intOptional(_ => - _.exercise_last_descendant_node_id - ), - "exercise_argument_compression" -> fieldStrategy.smallintOptional(_ => - _.exercise_argument_compression - ), - "exercise_result_compression" -> fieldStrategy.smallintOptional(_ => - _.exercise_result_compression - ), - "representative_package_id" -> fieldStrategy.intOptional(stringInterning => - _.representative_package_id.map(stringInterning.packageId.unsafe.internalize) - ), - - // contract related columns - "contract_id" -> fieldStrategy.byteaOptional(_ => _.contract_id.map(_.toBytes.toByteArray)), - "internal_contract_id" -> fieldStrategy.bigintOptional(_ => _.internal_contract_id), - "template_id" -> fieldStrategy.templateOptional(_.template_id), - "package_id" -> fieldStrategy.intOptional(stringInterning => - _.package_id.map(stringInterning.packageId.unsafe.internalize) - ), - "ledger_effective_time" -> fieldStrategy.bigintOptional(_ => _.ledger_effective_time), - ) - val idFilterVariousWitness: Table[DbDto.IdFilterVariousWitness] = - idFilter("lapi_filter_various_witness") - - val partyEntries: Table[DbDto.PartyEntry] = - fieldStrategy.insert("lapi_party_entries")( - "ledger_offset" -> fieldStrategy.bigint(_ => _.ledger_offset), - "recorded_at" -> fieldStrategy.bigint(_ => _.recorded_at), - "submission_id" -> fieldStrategy.stringOptional(_ => _.submission_id), - "party" -> fieldStrategy.stringOptional(_ => _.party), - "typ" -> fieldStrategy.string(_ => _.typ), - "rejection_reason" -> fieldStrategy.stringOptional(_ => _.rejection_reason), - "is_local" -> fieldStrategy.booleanOptional(_ => _.is_local), - "party_id" -> fieldStrategy.partyOptional(_.party), - ) - - val partyToParticipant: Table[DbDto.EventPartyToParticipant] = - fieldStrategy.insert("lapi_events_party_to_participant")( - "event_sequential_id" -> fieldStrategy.bigint(_ => _.event_sequential_id), - "event_offset" -> fieldStrategy.bigint(_ => _.event_offset), - "update_id" -> fieldStrategy.bytea(_ => _.update_id), - "party_id" -> fieldStrategy.party(_.party_id), - "participant_id" -> fieldStrategy.int(stringInterning => - dto => stringInterning.participantId.unsafe.internalize(dto.participant_id) - ), - "participant_permission" -> fieldStrategy.int(_ => _.participant_permission), - "participant_authorization_event" -> fieldStrategy.int(_ => - _.participant_authorization_event - ), - "synchronizer_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.synchronizerId.internalize(dbDto.synchronizer_id) - ), - "record_time" -> fieldStrategy.bigint(_ => _.record_time), - "trace_context" -> fieldStrategy.bytea(_ => _.trace_context), - ) - - val commandCompletions: Table[DbDto.CommandCompletion] = - fieldStrategy.insert("lapi_command_completions")( - "completion_offset" -> fieldStrategy.bigint(_ => _.completion_offset), - "record_time" -> fieldStrategy.bigint(_ => _.record_time), - "publication_time" -> fieldStrategy.bigint(_ => _.publication_time), - "user_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.userId.unsafe.internalize(dbDto.user_id) - ), - "submitters" -> fieldStrategy.parties(_.submitters), - "command_id" -> fieldStrategy.string(_ => _.command_id), - "update_id" -> fieldStrategy.byteaOptional(_ => _.update_id), - "rejection_status_code" -> fieldStrategy.intOptional(_ => _.rejection_status_code), - "rejection_status_message" -> fieldStrategy.stringOptional(_ => _.rejection_status_message), - "rejection_status_details" -> fieldStrategy.byteaOptional(_ => _.rejection_status_details), - "submission_id" -> fieldStrategy.stringOptional(_ => _.submission_id), - "deduplication_offset" -> fieldStrategy.bigintOptional(_ => _.deduplication_offset), - "deduplication_duration_seconds" -> fieldStrategy.bigintOptional(_ => - _.deduplication_duration_seconds - ), - "deduplication_duration_nanos" -> fieldStrategy.intOptional(_ => - _.deduplication_duration_nanos - ), - "synchronizer_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.synchronizerId.internalize(dbDto.synchronizer_id) - ), - "message_uuid" -> fieldStrategy.stringOptional(_ => _.message_uuid), - "is_transaction" -> fieldStrategy.boolean(_ => _.is_transaction), - "trace_context" -> fieldStrategy.bytea(_ => _.trace_context), - "traffic_cost" -> fieldStrategy.bigint(_ => _.traffic_cost), - ) - - val stringInterningTable: Table[DbDto.StringInterningDto] = - fieldStrategy.insert("lapi_string_interning")( - "internal_id" -> fieldStrategy.int(_ => _.internalId), - "external_string" -> fieldStrategy.string(_ => _.externalString), - ) - - val transactionMeta: Table[DbDto.TransactionMeta] = - fieldStrategy.insert("lapi_update_meta")( - "update_id" -> fieldStrategy.bytea(_ => _.update_id), - "event_offset" -> fieldStrategy.bigint(_ => _.event_offset), - "publication_time" -> fieldStrategy.bigint(_ => _.publication_time), - "record_time" -> fieldStrategy.bigint(_ => _.record_time), - "synchronizer_id" -> fieldStrategy.int(stringInterning => - dbDto => stringInterning.synchronizerId.internalize(dbDto.synchronizer_id) - ), - "event_sequential_id_first" -> fieldStrategy.bigint(_ => _.event_sequential_id_first), - "event_sequential_id_last" -> fieldStrategy.bigint(_ => _.event_sequential_id_last), - ) - - val executes: Seq[Array[Array[?]] => Connection => Unit] = List( - eventActivate.executeUpdate, - idFilterActivateStakeholder.executeUpdate, - idFilterActivateWitness.executeUpdate, - eventDeactivate.executeUpdate, - idFilterDeactivateStakeholder.executeUpdate, - idFilterDeactivateWitness.executeUpdate, - eventVariousWitnessed.executeUpdate, - idFilterVariousWitness.executeUpdate, - partyEntries.executeUpdate, - partyToParticipant.executeUpdate, - commandCompletions.executeUpdate, - stringInterningTable.executeUpdate, - transactionMeta.executeUpdate, - ) - - new Schema[DbDto] { - override def prepareData( - in: Vector[DbDto], - stringInterning: StringInterning, - ): Array[Array[Array[?]]] = { - def collectWithFilter[T <: DbDto: ClassTag](filter: T => Boolean): Vector[T] = - in.collect { case dbDto: T if filter(dbDto) => dbDto } - def collect[T <: DbDto: ClassTag]: Vector[T] = collectWithFilter[T](_ => true) - import DbDto.* - Array( - eventActivate.prepareData(collect[EventActivate], stringInterning), - idFilterActivateStakeholder - .prepareData(collect[IdFilterActivateStakeholder], stringInterning), - idFilterActivateWitness.prepareData(collect[IdFilterActivateWitness], stringInterning), - eventDeactivate.prepareData(collect[EventDeactivate], stringInterning), - idFilterDeactivateStakeholder - .prepareData(collect[IdFilterDeactivateStakeholder], stringInterning), - idFilterDeactivateWitness - .prepareData(collect[IdFilterDeactivateWitness], stringInterning), - eventVariousWitnessed.prepareData(collect[EventVariousWitnessed], stringInterning), - idFilterVariousWitness.prepareData(collect[IdFilterVariousWitness], stringInterning), - partyEntries.prepareData(collect[PartyEntry], stringInterning), - partyToParticipant.prepareData(collect[EventPartyToParticipant], stringInterning), - commandCompletions.prepareData(collect[CommandCompletion], stringInterning), - stringInterningTable.prepareData(collect[StringInterningDto], stringInterning), - transactionMeta.prepareData(collect[TransactionMeta], stringInterning), - ) - } - - override def executeUpdate(data: Array[Array[Array[?]]], connection: Connection): Unit = - executes.zip(data).foreach { case (execute, data) => - execute(data)(connection) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/SimpleSqlExtensions.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/SimpleSqlExtensions.scala deleted file mode 100644 index 49de65ab89..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/SimpleSqlExtensions.scala +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.{Cursor, Row, RowParser, SimpleSql} - -import java.sql.Connection -import scala.util.{Failure, Success, Try} - -private[backend] object SimpleSqlExtensions { - - implicit final class `SimpleSql ops`(val sql: SimpleSql[Row]) extends AnyVal { - - /** Returns the result of [[sql]] as a [[Vector]]. - * - * Allows to avoid linear operations in lists when using the default [[anorm.ResultSetParser]]s - * (e.g. when retrieving the result set length in - * [[com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream]] - * - * @param parser - * knows how to turn each row in an [[A]] - * @param conn - * an implicit JDBC connection - * @tparam A - * the type of each item in the result - * @throws Throwable - * if either the query execution or parsing fails - * @return - * the query result as a vector - */ - @throws[Throwable] - def asVectorOf[A](parser: RowParser[A])(implicit conn: Connection): Vector[A] = { - - val resultBuilder = Vector.newBuilder[A] - - @annotation.tailrec - def go(cursor: Option[Cursor]): Try[Vector[A]] = - cursor match { - case Some(cursor) => - cursor.row.as(parser) match { - case Success(value) => - resultBuilder.addOne(value) - go(cursor.next) - - case Failure(f) => Failure(f) - } - case _ => Try(resultBuilder.result()) - } - - sql - .withResult(go) - .fold( - _.headOption.fold(throw new NoSuchElementException("empty list of errors"))(throw _), - _.fold(throw _, identity), - ) - } - - def asSingle[A](parser: RowParser[A])(implicit connection: Connection): A = - sql.as(parser.single) - - def asSingleOpt[A](parser: RowParser[A])(implicit connection: Connection): Option[A] = - sql.as(parser.singleOpt) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/StringInterningStorageBackendImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/StringInterningStorageBackendImpl.scala deleted file mode 100644 index 92763cf916..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/StringInterningStorageBackendImpl.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.{int, str} -import anorm.{RowParser, SqlStringInterpolation, ~} -import com.digitalasset.canton.platform.store.backend.StringInterningStorageBackend -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* - -import java.sql.Connection - -object StringInterningStorageBackendImpl extends StringInterningStorageBackend { - - private val StringInterningEntriesParser: RowParser[(Int, String)] = - int("internal_id") ~ str("external_string") map { case internalId ~ externalString => - (internalId, externalString) - } - - override def loadStringInterningEntries(fromIdExclusive: Int, untilIdInclusive: Int)( - connection: Connection - ): Iterable[(Int, String)] = - SQL""" - SELECT internal_id, external_string - FROM lapi_string_interning - WHERE - internal_id > $fromIdExclusive - AND internal_id <= $untilIdInclusive - """.asVectorOf(StringInterningEntriesParser)(connection) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Table.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Table.scala deleted file mode 100644 index e08c0e3fbd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/Table.scala +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.platform.store.interning.StringInterning - -import java.sql.Connection - -private[backend] trait Table[From] { - def prepareData(in: Vector[From], stringInterning: StringInterning): Array[Array[?]] - def executeUpdate: Array[Array[?]] => Connection => Unit -} - -private[backend] abstract class BaseTable[From]( - fields: Seq[(String, Field[From, ?, ?])], - ordering: Option[Ordering[From]] = None, -) extends Table[From] { - override def prepareData( - in: Vector[From], - stringInterning: StringInterning, - ): Array[Array[?]] = { - val sortedIn = ordering.map(in.sorted(_)).getOrElse(in) - fields.view.map(_._2.toArray(sortedIn, stringInterning)).toArray - } -} - -private[backend] object Table { - def ifNonEmpty(data: Array[Array[?]])(effect: => Any): Unit = - // data(0) accesses the array of data for the first column of the table. This is safe because tables without columns are not supported. Also because of the transposed data-structure here all columns will have data-arrays of the same length. - if (data(0).length > 0) { - effect - () - } - - private def batchedInsertBase[From]( - insertStatement: String - )(fields: Seq[(String, Field[From, ?, ?])]): Table[From] = - new BaseTable[From](fields) { - override def executeUpdate: Array[Array[?]] => Connection => Unit = - data => - connection => - ifNonEmpty(data) { - val preparedStatement = connection.prepareStatement(insertStatement) - data(0).indices.foreach { dataIndex => - fields.indices.foreach { fieldIndex => - fields(fieldIndex)._2.prepareData( - preparedStatement, - fieldIndex + 1, - data(fieldIndex)(dataIndex), - ) - } - preparedStatement.addBatch() - } - preparedStatement.executeBatch() - preparedStatement.close() - } - } - - private def batchedInsertStatement( - tableName: String, - fields: Seq[(String, Field[?, ?, ?])], - ): String = { - def commaSeparatedOf(extractor: ((String, Field[?, ?, ?])) => String): String = - fields.view - .map(extractor) - .mkString(",") - val tableFields = commaSeparatedOf(_._1) - val selectFields = commaSeparatedOf { case (_, field) => - field.selectFieldExpression("?") - } - s""" - |INSERT INTO $tableName - | ($tableFields) - | VALUES - | ($selectFields) - |""".stripMargin - } - - def batchedInsert[From](tableName: String)( - fields: (String, Field[From, ?, ?])* - ): Table[From] = - batchedInsertBase(batchedInsertStatement(tableName, fields))(fields) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/UpdatePointwiseQueries.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/UpdatePointwiseQueries.scala deleted file mode 100644 index fdd88f2cc6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/UpdatePointwiseQueries.scala +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import com.digitalasset.canton.data -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.platform.store.backend.Conversions.* -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.{ - CompositeSql, - SqlStringInterpolation, -} -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.protocol.UpdateId - -import java.sql.Connection - -class UpdatePointwiseQueries( - ledgerEndCache: LedgerEndCache -) { - import EventStorageBackendTemplate.* - - /** Fetches a matching event sequential id range. */ - def fetchIdsFromUpdateMeta( - lookupKey: LookupKey - )(connection: Connection): Option[(Long, Long)] = { - import com.digitalasset.canton.platform.store.backend.Conversions.OffsetToStatement - // 1. Checking whether "event_offset <= ledgerEndOffset" is needed because during indexing - // the events and transaction_meta tables are written to prior to the ledger end being updated. - val ledgerEndOffsetO: Option[Offset] = ledgerEndCache().map(_.lastOffset) - - ledgerEndOffsetO.flatMap { ledgerEndOffset => - val lookupKeyClause: CompositeSql = - lookupKey match { - case LookupKey.ByUpdateId(updateId) => - cSQL"t.update_id = $updateId" - case LookupKey.ByOffset(offset) => - cSQL"t.event_offset = $offset" - } - - SQL""" - SELECT - t.event_sequential_id_first, - t.event_sequential_id_last - FROM - lapi_update_meta t - WHERE - $lookupKeyClause - AND - t.event_offset <= $ledgerEndOffset - """.as(EventSequentialIdFirstLast.singleOpt)(connection) - } - } -} - -object UpdatePointwiseQueries { - sealed trait LookupKey - object LookupKey { - final case class ByUpdateId(updateId: UpdateId) extends LookupKey - final case class ByOffset(offset: data.Offset) extends LookupKey - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/UpdateStreamingQueries.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/UpdateStreamingQueries.scala deleted file mode 100644 index 5e111e2bee..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/common/UpdateStreamingQueries.scala +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.SqlParser.long -import anorm.~ -import com.digitalasset.canton.platform.Party -import com.digitalasset.canton.platform.store.backend.PersistentEventType -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.{ - CompositeSql, - SqlStringInterpolation, -} -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* -import com.digitalasset.canton.platform.store.backend.common.UpdateStreamingQueries.{ - UpdateIdPageQueryBuilder, - eventNotDeactivated, -} -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - IdFilterPageQuery, - IdPage, - IdPageBounds, - IdPageQuery, - PaginationFromTo, - PaginationInput, -} -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.NameTypeConRef - -import java.sql.Connection - -sealed trait EventPayloadSourceForUpdatesAcsDelta -object EventPayloadSourceForUpdatesAcsDelta { - object Activate extends EventPayloadSourceForUpdatesAcsDelta - object Deactivate extends EventPayloadSourceForUpdatesAcsDelta -} -sealed trait EventPayloadSourceForUpdatesLedgerEffects -object EventPayloadSourceForUpdatesLedgerEffects { - object Activate extends EventPayloadSourceForUpdatesLedgerEffects - object Deactivate extends EventPayloadSourceForUpdatesLedgerEffects - object VariousWitnessed extends EventPayloadSourceForUpdatesLedgerEffects -} - -class UpdateStreamingQueries( - stringInterning: StringInterning, - queryStrategy: QueryStrategy, -) { - - def activateStakeholderIds( - witnessO: Option[Party], - templateIdO: Option[NameTypeConRef], - ): UpdateIdPageQueryBuilder = - new UpdateIdPageQueryBuilder( - eventTypeFilter = idFilter("lapi_events_activate_contract"), - idPageQueryBuilder = UpdateStreamingQueries.fetchEventIds( - tableName = "lapi_filter_activate_stakeholder", - witnessO = witnessO, - templateIdO = templateIdO, - stringInterning = stringInterning, - hasFirstPerSequentialId = true, - ), - ) - - def activateWitnessesIds( - witnessO: Option[Party], - templateIdO: Option[NameTypeConRef], - ): UpdateIdPageQueryBuilder = - new UpdateIdPageQueryBuilder( - eventTypeFilter = idFilter("lapi_events_activate_contract"), - idPageQueryBuilder = UpdateStreamingQueries.fetchEventIds( - tableName = "lapi_filter_activate_witness", - witnessO = witnessO, - templateIdO = templateIdO, - stringInterning = stringInterning, - hasFirstPerSequentialId = true, - ), - ) - - def deactivateStakeholderIds( - witnessO: Option[Party], - templateIdO: Option[NameTypeConRef], - ): UpdateIdPageQueryBuilder = - new UpdateIdPageQueryBuilder( - eventTypeFilter = idFilter("lapi_events_deactivate_contract"), - idPageQueryBuilder = UpdateStreamingQueries.fetchEventIds( - tableName = "lapi_filter_deactivate_stakeholder", - witnessO = witnessO, - templateIdO = templateIdO, - stringInterning = stringInterning, - hasFirstPerSequentialId = true, - ), - ) - - def deactivateWitnessesIds( - witnessO: Option[Party], - templateIdO: Option[NameTypeConRef], - ): UpdateIdPageQueryBuilder = - new UpdateIdPageQueryBuilder( - eventTypeFilter = idFilter("lapi_events_deactivate_contract"), - idPageQueryBuilder = UpdateStreamingQueries.fetchEventIds( - tableName = "lapi_filter_deactivate_witness", - witnessO = witnessO, - templateIdO = templateIdO, - stringInterning = stringInterning, - hasFirstPerSequentialId = true, - ), - ) - - def variousWitnessIds( - witnessO: Option[Party], - templateIdO: Option[NameTypeConRef], - ): UpdateIdPageQueryBuilder = - new UpdateIdPageQueryBuilder( - eventTypeFilter = idFilter("lapi_events_various_witnessed"), - idPageQueryBuilder = UpdateStreamingQueries.fetchEventIds( - tableName = "lapi_filter_various_witness", - witnessO = witnessO, - templateIdO = templateIdO, - stringInterning = stringInterning, - hasFirstPerSequentialId = true, - ), - ) - - def fetchActiveIds( - stakeholderO: Option[Ref.Party], - templateIdO: Option[NameTypeConRef], - activeAtEventSeqId: Long, - ): IdFilterPageQuery = - UpdateStreamingQueries - .fetchEventIds( - tableName = "lapi_filter_activate_stakeholder", - witnessO = stakeholderO, - templateIdO = templateIdO, - stringInterning = stringInterning, - hasFirstPerSequentialId = true, - ) - .toFiltered(eventNotDeactivated(activeAtEventSeqId)) - - def fetchAchsIds( - stakeholderO: Option[Ref.Party], - templateIdO: Option[NameTypeConRef], - activeAtEventSeqId: Long, - ): IdFilterPageQuery = - UpdateStreamingQueries - .fetchEventIds( - tableName = "lapi_filter_achs_stakeholder", - witnessO = stakeholderO, - templateIdO = templateIdO, - stringInterning = stringInterning, - hasFirstPerSequentialId = true, - ) - .toFiltered(eventNotDeactivated(activeAtEventSeqId)) - - private def idFilter(tableName: String)(eventTypes: Set[PersistentEventType]): CompositeSql = - cSQL""" - EXISTS ( - SELECT 1 - FROM #$tableName data_table - WHERE - filters.event_sequential_id = data_table.event_sequential_id - AND data_table.event_type ${queryStrategy.anyOfSmallInts(eventTypes.map(_.asInt))} - )""" -} - -object UpdateStreamingQueries { - - // TODO(i22416): Rename the arguments of this function, as witnessO and templateIdO are inadequate for party topology events. - /** @param tableName - * one of the filter tables for create, consuming or non-consuming events - * @param witnessO - * the party for which to fetch the event ids, if None the event ids for all the parties should - * be fetched - * @param templateIdO - * NOTE: this parameter is not applicable for tree tx stream only oriented filters - * @param stringInterning - * the string interning instance to use for internalizing the party and template id - * @param hasFirstPerSequentialId - * true if the table has the first_per_sequential_id column, false otherwise. If true and - * witnessO is None, only a single row per event_sequential_id will be fetched and thus only - * unique event ids will be returned - */ - def fetchEventIds( - tableName: String, - witnessO: Option[Ref.Party], - templateIdO: Option[NameTypeConRef], - stringInterning: StringInterning, - hasFirstPerSequentialId: Boolean, - ): IdPageQueryBuilder = - filterTableClauses( - witnessO = witnessO, - templateIdO = templateIdO, - stringInterning = stringInterning, - hasFirstPerSequentialId = hasFirstPerSequentialId, - ).map(clauses => - new IdPageQueryImpl( - tableName = tableName, - filterTableClauses = clauses, - ) - ).getOrElse(new EmptyIdPageQuery) - - private def filterTableClauses( - witnessO: Option[Ref.Party], - templateIdO: Option[NameTypeConRef], - stringInterning: StringInterning, - hasFirstPerSequentialId: Boolean, - ): Option[FilterTableClauses] = { - val partyIdFilterO = witnessO match { - case Some(witness) => - stringInterning.party - .tryInternalize(witness) match { - case Some(internedPartyFilter) => - // use ordering by party_id even though we are restricting the query to a single party_id - // to ensure that the correct db index is used - Some((cSQL"AND filters.party_id = $internedPartyFilter", cSQL"filters.party_id,")) - case None => None // partyFilter never seen - } - case None => - // do not filter by party, fetch event for all parties - Some((cSQL"", cSQL"")) - } - - val templateIdFilterO = templateIdO.map(stringInterning.templateId.tryInternalize) match { - case Some(None) => None // templateIdFilter never seen - case internedTemplateIdFilterNested => - internedTemplateIdFilterNested.flatten // flatten works for both None, Some(Some(x)) case, Some(None) excluded before - match { - case Some(internedTemplateId) => - // use ordering by template_id even though we are restricting the query to a single template_id - // to ensure that the correct db index is used - Some((cSQL"AND filters.template_id = $internedTemplateId", cSQL"filters.template_id,")) - case None => Some((cSQL"", cSQL"")) - } - } - - // if we do not filter by party and the table has first_per_sequential_id column, we only fetch a single row per event_sequential_id - val firstPerSequentialIdClause = witnessO match { - case None if hasFirstPerSequentialId => - cSQL"AND filters.first_per_sequential_id = true" - case _ => cSQL"" - } - - for { - (partyIdFilterClause, partyIdOrderingClause) <- partyIdFilterO - (templateIdFilterClause, templateIdOrderingClause) <- templateIdFilterO - } yield FilterTableClauses( - partyIdFilterClause = partyIdFilterClause, - partyIdOrderingClause = partyIdOrderingClause, - templateIdFilterClause = templateIdFilterClause, - templateIdOrderingClause = templateIdOrderingClause, - firstPerSequentialIdClause = firstPerSequentialIdClause, - ) - } - - final case class FilterTableClauses( - partyIdFilterClause: CompositeSql, - partyIdOrderingClause: CompositeSql, - templateIdFilterClause: CompositeSql, - templateIdOrderingClause: CompositeSql, - firstPerSequentialIdClause: CompositeSql, - ) - - private def filterTableSelect( - tableName: String, - filterTableClauses: FilterTableClauses, - paginationFromTo: PaginationFromTo, - limit: Option[Int], - idFilter: Option[CompositeSql], - ): CompositeSql = { - val idBoundsSQL = - if (paginationFromTo.descending) - cSQL"${paginationFromTo.toInclusive} <= filters.event_sequential_id AND filters.event_sequential_id < ${paginationFromTo.fromExclusive}" - else - cSQL"${paginationFromTo.fromExclusive} < filters.event_sequential_id AND filters.event_sequential_id <= ${paginationFromTo.toInclusive}" - val idOrderDirectionSQL = if (paginationFromTo.descending) cSQL"DESC" else cSQL"ASC" - cSQL""" - SELECT filters.event_sequential_id event_sequential_id - FROM - #$tableName filters - WHERE - $idBoundsSQL - ${filterTableClauses.partyIdFilterClause} - ${filterTableClauses.templateIdFilterClause} - ${filterTableClauses.firstPerSequentialIdClause} - ${idFilter.map(f => cSQL"AND $f").getOrElse(cSQL"")} - ORDER BY - ${filterTableClauses.partyIdOrderingClause} - ${filterTableClauses.templateIdOrderingClause} - filters.event_sequential_id $idOrderDirectionSQL -- deliver in index order - ${limit.map(l => cSQL"LIMIT $l").getOrElse(cSQL"")}""" - } - - class UpdateIdPageQueryBuilder( - eventTypeFilter: Set[PersistentEventType] => CompositeSql, - idPageQueryBuilder: IdPageQueryBuilder, - ) extends IdPageQuery { - override def fetchPage(connection: Connection)(input: PaginationInput): IdPage = - idPageQueryBuilder.fetchPage(connection)(input) - - def filteredForEventTypes(eventTypes: Set[PersistentEventType]): IdFilterPageQuery = - idPageQueryBuilder.toFiltered(eventTypeFilter(eventTypes)) - } - - trait IdPageQueryBuilder extends IdPageQuery { - def toFiltered(idFilter: CompositeSql): IdFilterPageQuery - } - - class EmptyIdPageQuery extends IdPageQueryBuilder { - override def fetchPage(connection: Connection)(input: PaginationInput): IdPage = - IdPage(Vector.empty, lastPage = true) - - override def toFiltered(idFilter: CompositeSql): IdFilterPageQuery = new EmptyIdFilterPageQuery - } - - class IdPageQueryImpl( - tableName: String, - filterTableClauses: FilterTableClauses, - ) extends IdPageQueryBuilder { - override def fetchPage(connection: Connection)(input: PaginationInput): IdPage = { - val sql = filterTableSelect( - tableName = tableName, - filterTableClauses = filterTableClauses, - paginationFromTo = input.fromTo, - limit = Some(input.limit + 1), - idFilter = - None, // disable regardless - this is the case where we reuse the query for a no-ID-filter population case - ) - val ids = SQL"$sql".asVectorOf(long("event_sequential_id"))(connection) - val lastPage = ids.sizeIs < input.limit + 1 - IdPage( - ids = if (lastPage) ids else ids.dropRight(1), - lastPage = lastPage, - ) - } - - override def toFiltered(idFilter: CompositeSql): IdFilterPageQuery = - new IdFilterPageQueryImpl( - tableName = tableName, - filterTableClauses = filterTableClauses, - idFilter = idFilter, - ) - } - - class EmptyIdFilterPageQuery extends IdFilterPageQuery { - override def fetchPageBounds(connection: Connection)( - input: PaginationInput - ): Option[IdPageBounds] = None - override def fetchPage(connection: Connection)(fromTo: PaginationFromTo): Vector[Long] = - Vector.empty - } - - class IdFilterPageQueryImpl( - tableName: String, - filterTableClauses: FilterTableClauses, - idFilter: CompositeSql, - ) extends IdFilterPageQuery { - override def fetchPageBounds( - connection: Connection - )(input: PaginationInput): Option[IdPageBounds] = { - val filterTableSQL = filterTableSelect( - tableName = tableName, - filterTableClauses = filterTableClauses, - paginationFromTo = input.fromTo, - limit = Some(input.limit + 1), - idFilter = - None, // disable regardless - this is the case where we reuse the query for a ID-filter population: the paginated query - ) - val lastElement = if (input.fromTo.descending) cSQL"min" else cSQL"max" - SQL""" - WITH unfiltered_ids AS ( - $filterTableSQL - ) - SELECT - $lastElement(unfiltered_ids.event_sequential_id) last_event_sequential_id, - count(*) page_size - FROM unfiltered_ids - """ - .asSingle(long("last_event_sequential_id").? ~ long("page_size") map { - case lastIdO ~ pageSize => - lastIdO.map { lastId => - val lastPage = pageSize < input.limit + 1 - IdPageBounds( - fromTo = - if (lastPage) - input.fromTo // the whole input range is returned as this is the last page - else - input.fromTo.copy( - toInclusive = - // as the page queried for limit+1, the last ID represents the toExclusive now - if (input.fromTo.descending) lastId + 1 - else lastId - 1 - ), - lastPage = lastPage, - ) - } - })(connection) - } - - override def fetchPage(connection: Connection)(fromTo: PaginationFromTo): Vector[Long] = { - val sql = filterTableSelect( - tableName = tableName, - filterTableClauses = filterTableClauses, - paginationFromTo = fromTo, - limit = None, - idFilter = Some(idFilter), - ) - SQL"$sql".asVectorOf(long("event_sequential_id"))(connection) - } - } - - /** Checks if an event is not deactivated. It requires that the table with the activations is - * called "filters" in the query that will be called. - */ - def eventNotDeactivated(activeAtEventSeqId: Long): CompositeSql = - cSQL""" - NOT EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract deactivate_evs - WHERE - filters.event_sequential_id = deactivate_evs.deactivated_event_sequential_id - AND deactivate_evs.event_sequential_id <= $activeAtEventSeqId - ) - """ -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2DBLockStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2DBLockStorageBackend.scala deleted file mode 100644 index 2a477dff15..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2DBLockStorageBackend.scala +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import com.digitalasset.canton.platform.store.backend.DBLockStorageBackend - -import java.sql.Connection - -object H2DBLockStorageBackend extends DBLockStorageBackend { - override def tryAcquire( - lockId: DBLockStorageBackend.LockId, - lockMode: DBLockStorageBackend.LockMode, - )(connection: Connection): Option[DBLockStorageBackend.Lock] = - throw new UnsupportedOperationException("db level locks are not supported for H2") - - override def release(lock: DBLockStorageBackend.Lock)(connection: Connection): Boolean = - throw new UnsupportedOperationException("db level locks are not supported for H2") - - override def lock(id: Int): DBLockStorageBackend.LockId = - throw new UnsupportedOperationException("db level locks are not supported for H2") - - override def dbLockSupported: Boolean = false -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2DataSourceStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2DataSourceStorageBackend.scala deleted file mode 100644 index 6ede0dc4b2..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2DataSourceStorageBackend.scala +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.backend.DataSourceStorageBackend -import com.digitalasset.canton.platform.store.backend.common.{ - DataSourceStorageBackendImpl, - InitHookDataSourceProxy, -} - -import java.sql.Connection -import javax.sql.DataSource - -object H2DataSourceStorageBackend extends DataSourceStorageBackend { - override def createDataSource( - dataSourceConfig: DataSourceStorageBackend.DataSourceConfig, - loggerFactory: NamedLoggerFactory, - connectionInitHook: Option[Connection => Unit], - ): DataSource = { - val h2DataSource = new org.h2.jdbcx.JdbcDataSource() - - // H2 (org.h2.jdbcx.JdbcDataSource) does not support setting the user/password within the jdbcUrl, so remove - // those properties from the url if present and set them separately. Note that Postgres supports - // user/password in the URLs, so we don't bother exposing user/password configs separately from the url just for h2 - // which is anyway not supported for production. (This also helps run canton h2 participants that set user and - // password.) - val (urlNoUserNoPassword, user, password) = extractUserPasswordAndRemoveFromUrl( - dataSourceConfig.jdbcUrl - ) - user.foreach(h2DataSource.setUser) - password.foreach(h2DataSource.setPassword) - h2DataSource.setUrl(urlNoUserNoPassword) - - InitHookDataSourceProxy(h2DataSource, connectionInitHook.toList, loggerFactory) - } - - def extractUserPasswordAndRemoveFromUrl( - jdbcUrl: String - ): (String, Option[String], Option[String]) = { - def setKeyValueAndRemoveFromUrl(url: String, key: String): (String, Option[String]) = { - val regex = s".*(;(?i)$key=([^;]*)).*".r - url match { - case regex(keyAndValue, value) => - (url.replace(keyAndValue, ""), Some(value)) - case _ => (url, None) - } - } - - val (urlNoUser, user) = setKeyValueAndRemoveFromUrl(jdbcUrl, "user") - val (urlNoUserNoPassword, password) = setKeyValueAndRemoveFromUrl(urlNoUser, "password") - (urlNoUserNoPassword, user, password) - } - - override def checkDatabaseAvailable(connection: Connection): Unit = - DataSourceStorageBackendImpl.checkDatabaseAvailable(connection) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2EventStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2EventStorageBackend.scala deleted file mode 100644 index b8c269a2db..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2EventStorageBackend.scala +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.backend.common.{ - ComposableQuery, - EventStorageBackendTemplate, -} -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.interning.StringInterning - -import java.sql.Connection - -class H2EventStorageBackend( - ledgerEndCache: LedgerEndCache, - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, -) extends EventStorageBackendTemplate( - queryStrategy = H2QueryStrategy, - ledgerEndCache = ledgerEndCache, - stringInterning = stringInterning, - loggerFactory = loggerFactory, - ) { - - // no need for locking in H2 as we access H2 DB on a single connection - override def lockExclusivelyPruningProcessingTable(connection: Connection): Unit = () - - // no need for locking in H2 as we access H2 DB on a single connection - override def lockExclusivelyContractPruningProcessingTable(connection: Connection): Unit = () - - // no need for locking in H2 as we access H2 DB on a single connection - override def readLockInternalContractIds(internalContractIds: Set[Long])( - connection: Connection - ): Set[Long] = Set.empty - - // no need for locking in H2 as we access H2 DB on a single connection - override def writeLockInternalContractIds( - whereInternalContractIdExprs: ComposableQuery.CompositeSql - )(connection: Connection): Unit = () -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2Field.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2Field.scala deleted file mode 100644 index 7132b442b0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2Field.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import com.digitalasset.canton.platform.store.backend.common.Field -import com.digitalasset.canton.platform.store.interning.StringInterning - -import java.io.{ByteArrayInputStream, InputStream} - -private[h2] final case class H2Bytea[From](extract: StringInterning => From => Array[Byte]) - extends Field[From, Array[Byte], InputStream] { - override def convert: Array[Byte] => InputStream = new ByteArrayInputStream(_) -} - -private[h2] final case class H2ByteaOptional[From]( - extract: StringInterning => From => Option[Array[Byte]] -) extends Field[From, Option[Array[Byte]], InputStream] { - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def convert: Option[Array[Byte]] => InputStream = - _.map(new ByteArrayInputStream(_)).orNull -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2QueryStrategy.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2QueryStrategy.scala deleted file mode 100644 index b3c80df250..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2QueryStrategy.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.common.{ComposableQuery, QueryStrategy} - -object H2QueryStrategy extends QueryStrategy { - - override def analyzeTable(tableName: String): ComposableQuery.CompositeSql = - cSQL"ANALYZE TABLE #$tableName" -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2ResetStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2ResetStorageBackend.scala deleted file mode 100644 index 1ff4a77eb8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2ResetStorageBackend.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.platform.store.backend.ResetStorageBackend -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - -import java.sql.Connection - -object H2ResetStorageBackend extends ResetStorageBackend { - - override def resetAll(connection: Connection): Unit = { - SQL""" - set referential_integrity false; - truncate table lapi_parameters; - truncate table lapi_achs_state; - truncate table lapi_ledger_end_synchronizer_index; - truncate table lapi_command_completions; - truncate table lapi_events_activate_contract; - truncate table lapi_filter_activate_stakeholder; - truncate table lapi_filter_achs_stakeholder; - truncate table lapi_filter_activate_witness; - truncate table lapi_events_deactivate_contract; - truncate table lapi_filter_deactivate_stakeholder; - truncate table lapi_filter_deactivate_witness; - truncate table lapi_events_various_witnessed; - truncate table lapi_filter_various_witness; - truncate table lapi_party_entries; - truncate table lapi_party_records; - truncate table lapi_party_record_annotations; - truncate table lapi_events_party_to_participant; - truncate table lapi_string_interning; - truncate table lapi_update_meta; - truncate table lapi_users; - truncate table lapi_user_rights; - truncate table lapi_user_annotations; - truncate table lapi_identity_provider_config; - truncate table par_pruning_operation; - truncate table par_contracts; - truncate table lapi_pruning_candidate_deactivated; - truncate table lapi_pruning_contract_candidate; - set referential_integrity true; - """ - .execute()(connection) - .discard - () - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2Schema.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2Schema.scala deleted file mode 100644 index 44f1541b98..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2Schema.scala +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import com.digitalasset.canton.platform.store.backend.DbDto -import com.digitalasset.canton.platform.store.backend.common.AppendOnlySchema.FieldStrategy -import com.digitalasset.canton.platform.store.backend.common.{ - AppendOnlySchema, - Field, - Schema, - Table, -} -import com.digitalasset.canton.platform.store.interning.StringInterning - -private[h2] object H2Schema { - private val H2FieldStrategy = new FieldStrategy { - override def bytea[From]( - extractor: StringInterning => From => Array[Byte] - ): Field[From, Array[Byte], ?] = - H2Bytea(extractor) - - override def byteaOptional[From]( - extractor: StringInterning => From => Option[Array[Byte]] - ): Field[From, Option[Array[Byte]], ?] = - H2ByteaOptional(extractor) - - override def insert[From](tableName: String)( - fields: (String, Field[From, ?, ?])* - ): Table[From] = - Table.batchedInsert(tableName)(fields*) - } - - val schema: Schema[DbDto] = AppendOnlySchema(H2FieldStrategy) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2StorageBackendFactory.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2StorageBackendFactory.scala deleted file mode 100644 index 672c526ef6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/h2/H2StorageBackendFactory.scala +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.backend.common.{ - CommonStorageBackendFactory, - CompletionStorageBackendTemplate, - ContractStorageBackendTemplate, - IngestionStorageBackendTemplate, - ParameterStorageBackendImpl, - PartyStorageBackendTemplate, -} -import com.digitalasset.canton.platform.store.backend.localstore.{ - PartyRecordStorageBackend, - PartyRecordStorageBackendImpl, -} -import com.digitalasset.canton.platform.store.backend.{ - CompletionStorageBackend, - ContractStorageBackend, - DBLockStorageBackend, - DataSourceStorageBackend, - EventStorageBackend, - IngestionStorageBackend, - ParameterStorageBackend, - PartyStorageBackend, - ResetStorageBackend, - StorageBackendFactory, -} -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.interning.StringInterning - -object H2StorageBackendFactory extends StorageBackendFactory with CommonStorageBackendFactory { - - override val createIngestionStorageBackend: IngestionStorageBackend[?] = - new IngestionStorageBackendTemplate(H2Schema.schema) - - override def createParameterStorageBackend( - stringInterning: StringInterning - ): ParameterStorageBackend = - new ParameterStorageBackendImpl(H2QueryStrategy, stringInterning) - - override def createPartyStorageBackend(ledgerEndCache: LedgerEndCache): PartyStorageBackend = - new PartyStorageBackendTemplate(ledgerEndCache) - - override def createPartyRecordStorageBackend: PartyRecordStorageBackend = - PartyRecordStorageBackendImpl - - override def createCompletionStorageBackend( - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, - ): CompletionStorageBackend = - new CompletionStorageBackendTemplate(stringInterning, loggerFactory) - - override def createContractStorageBackend( - stringInterning: StringInterning, - ledgerEndCache: LedgerEndCache, - ): ContractStorageBackend = - new ContractStorageBackendTemplate(H2QueryStrategy, stringInterning, ledgerEndCache) - - override def createEventStorageBackend( - ledgerEndCache: LedgerEndCache, - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, - ): EventStorageBackend = - new H2EventStorageBackend( - ledgerEndCache = ledgerEndCache, - stringInterning = stringInterning, - loggerFactory = loggerFactory, - ) - - override val createDataSourceStorageBackend: DataSourceStorageBackend = - H2DataSourceStorageBackend - - override val createDBLockStorageBackend: DBLockStorageBackend = - H2DBLockStorageBackend - - override val createResetStorageBackend: ResetStorageBackend = - H2ResetStorageBackend - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/IdentityProviderStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/IdentityProviderStorageBackend.scala deleted file mode 100644 index 26110e85f6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/IdentityProviderStorageBackend.scala +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.localstore - -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} - -import java.sql.Connection - -trait IdentityProviderStorageBackend { - def createIdentityProviderConfig( - identityProviderConfig: IdentityProviderConfig - )(connection: Connection): Unit - - def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(connection: Connection): Boolean - - def getIdentityProviderConfig(id: IdentityProviderId.Id)( - connection: Connection - ): Option[IdentityProviderConfig] - - def getIdentityProviderConfigByIssuer(issuer: String)( - connection: Connection - ): Option[IdentityProviderConfig] - - def listIdentityProviderConfigs()( - connection: Connection - ): Vector[IdentityProviderConfig] - - def updateIssuer(id: IdentityProviderId.Id, newIssuer: String)( - connection: Connection - ): Boolean - - def updateJwksUrl(id: IdentityProviderId.Id, jwksUrl: JwksUrl)( - connection: Connection - ): Boolean - - def updateAudience(id: IdentityProviderId.Id, audience: Option[String])( - connection: Connection - ): Boolean - - def updateIsDeactivated(id: IdentityProviderId.Id, isDeactivated: Boolean)( - connection: Connection - ): Boolean - - def identityProviderConfigByIssuerExists(ignoreId: IdentityProviderId.Id, issuer: String)( - connection: Connection - ): Boolean - - def countIdentityProviderConfigs()(connection: Connection): Int - - def idpConfigByIdExists(id: IdentityProviderId.Id)(connection: Connection): Boolean - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/IdentityProviderStorageBackendImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/IdentityProviderStorageBackendImpl.scala deleted file mode 100644 index b5e544eca0..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/IdentityProviderStorageBackendImpl.scala +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.localstore - -import anorm.SqlParser.{bool, int, str} -import anorm.{RowParser, SqlParser, ~} -import com.daml.jwt.JwksUrl -import com.daml.scalautil.Statement.discard -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* - -import java.sql.Connection - -object IdentityProviderStorageBackendImpl extends IdentityProviderStorageBackend { - - private val IntParser: RowParser[Int] = - int("dummy") map { i => i } - - private val IdpConfigRecordParser: RowParser[IdentityProviderConfig] = { - import com.digitalasset.canton.platform.store.backend.Conversions.bigDecimalColumnToBoolean - str("identity_provider_id") ~ - bool("is_deactivated") ~ - str("jwks_url") ~ - str("issuer") ~ - str("audience").? map { - case identityProviderId ~ isDeactivated ~ jwksUrl ~ issuer ~ audience => - IdentityProviderConfig( - identityProviderId = IdentityProviderId.Id.assertFromString(identityProviderId), - isDeactivated = isDeactivated, - jwksUrl = JwksUrl.assertFromString(jwksUrl), - issuer = issuer, - audience = audience, - ) - } - } - - override def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig)( - connection: Connection - ): Unit = { - val identityProviderId = identityProviderConfig.identityProviderId.value: String - val isDeactivated = identityProviderConfig.isDeactivated - val jwksUrl = identityProviderConfig.jwksUrl.value - val issuer = identityProviderConfig.issuer - val audience = identityProviderConfig.audience - discard(SQL""" - INSERT INTO lapi_identity_provider_config (identity_provider_id, is_deactivated, jwks_url, issuer, audience) - VALUES ($identityProviderId, $isDeactivated, $jwksUrl, $issuer, $audience) - """.execute()(connection)) - } - - override def deleteIdentityProviderConfig(id: IdentityProviderId.Id)( - connection: Connection - ): Boolean = { - val updatedRowsCount = - SQL""" - DELETE FROM lapi_identity_provider_config WHERE identity_provider_id = ${id.value: String} - """.executeUpdate()(connection) - updatedRowsCount == 1 - } - - override def getIdentityProviderConfig(id: IdentityProviderId.Id)( - connection: Connection - ): Option[IdentityProviderConfig] = - SQL""" - SELECT identity_provider_id, is_deactivated, jwks_url, issuer, audience - FROM lapi_identity_provider_config - WHERE identity_provider_id = ${id.value: String} - """ - .as(IdpConfigRecordParser.singleOpt)(connection) - - override def listIdentityProviderConfigs()( - connection: Connection - ): Vector[IdentityProviderConfig] = - SQL""" - SELECT identity_provider_id, is_deactivated, jwks_url, issuer, audience - FROM lapi_identity_provider_config - ORDER BY identity_provider_id - """ - .asVectorOf(IdpConfigRecordParser)(connection) - - override def identityProviderConfigByIssuerExists( - ignoreId: IdentityProviderId.Id, - issuer: String, - )(connection: Connection): Boolean = { - import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - val res: Seq[?] = - SQL""" - SELECT 1 AS dummy - FROM lapi_identity_provider_config t - WHERE - t.issuer = $issuer AND - identity_provider_id != ${ignoreId.value: String} - """.asVectorOf(IntParser)(connection) - assert(res.sizeIs <= 1) - res.sizeIs == 1 - } - - override def idpConfigByIdExists(id: IdentityProviderId.Id)(connection: Connection): Boolean = { - import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - val res: Seq[?] = - SQL""" - SELECT 1 AS dummy - FROM lapi_identity_provider_config t - WHERE t.identity_provider_id = ${id.value: String} - """.asVectorOf(IntParser)(connection) - assert(res.sizeIs <= 1) - res.sizeIs == 1 - } - - override def updateIssuer(id: IdentityProviderId.Id, newIssuer: String)( - connection: Connection - ): Boolean = { - val rowsUpdated = - SQL""" - UPDATE lapi_identity_provider_config - SET issuer = $newIssuer - WHERE identity_provider_id = ${id.value: String} - """.executeUpdate()(connection) - rowsUpdated == 1 - } - - override def updateJwksUrl(id: IdentityProviderId.Id, jwksUrl: JwksUrl)( - connection: Connection - ): Boolean = { - val rowsUpdated = - SQL""" - UPDATE lapi_identity_provider_config - SET jwks_url = ${jwksUrl.value} - WHERE identity_provider_id = ${id.value: String} - """.executeUpdate()(connection) - rowsUpdated == 1 - } - - override def updateAudience(id: IdentityProviderId.Id, audience: Option[String])( - connection: Connection - ): Boolean = { - val audienceSql = audience match { - case Some(aud) => - cSQL"""SET audience = $aud""" - case None => - cSQL"""SET audience = NULL""" - } - val rowsUpdated = - SQL""" - UPDATE lapi_identity_provider_config - $audienceSql - WHERE identity_provider_id = ${id.value: String} - """.executeUpdate()(connection) - rowsUpdated == 1 - } - - override def updateIsDeactivated(id: IdentityProviderId.Id, isDeactivated: Boolean)( - connection: Connection - ): Boolean = { - val rowsUpdated = - SQL""" - UPDATE lapi_identity_provider_config - SET is_deactivated = $isDeactivated - WHERE identity_provider_id = ${id.value: String} - """.executeUpdate()(connection) - rowsUpdated == 1 - } - - override def countIdentityProviderConfigs()(connection: Connection): Int = - SQL"SELECT count(*) AS identity_provider_configs_count from lapi_identity_provider_config" - .as(SqlParser.int("identity_provider_configs_count").single)(connection) - - override def getIdentityProviderConfigByIssuer( - issuer: String - )(connection: Connection): Option[IdentityProviderConfig] = - SQL""" - SELECT identity_provider_id, is_deactivated, jwks_url, issuer, audience - FROM lapi_identity_provider_config - WHERE issuer = $issuer - """ - .as(IdpConfigRecordParser.singleOpt)(connection) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/ParticipantMetadataBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/ParticipantMetadataBackend.scala deleted file mode 100644 index 6ad8701b01..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/ParticipantMetadataBackend.scala +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.localstore - -import anorm.SqlParser.{long, str} -import anorm.{RowParser, SqlStringInterpolation, ~} -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* - -import java.sql.Connection - -/** Provides primitive backend operations for managing: - * - annotations of a resource, - * - resource versioning and concurrent change control. - */ -object ParticipantMetadataBackend { - - private val AnnotationParser: RowParser[(String, String, Long)] = - str("name") ~ str("val").? ~ long("updated_at") map { case key ~ valueO ~ updateAt => - (key, valueO.getOrElse(""), updateAt) - } - - def addAnnotation( - annotationsTableName: String - )(internalId: Int, key: String, value: String, updatedAt: Long)( - connection: Connection - ): Unit = { - val _ = - SQL""" - INSERT INTO #$annotationsTableName (internal_id, name, val, updated_at) - VALUES ( - $internalId, - $key, - $value, - $updatedAt - ) - """.executeUpdate()(connection) - } - - def deleteAnnotations( - annotationsTableName: String - )(internalId: Int)(connection: Connection): Unit = { - val _ = SQL""" - DELETE FROM #$annotationsTableName - WHERE - internal_id = $internalId - """.executeUpdate()(connection) - } - - def getAnnotations( - annotationsTableName: String - )(internalId: Int)(connection: Connection): Map[String, String] = - try { - SQL""" - SELECT name, val, updated_at - FROM #$annotationsTableName - WHERE - internal_id = $internalId - """ - .asVectorOf(AnnotationParser)(connection) - .iterator - .map { case (key, value, _) => key -> value } - .toMap - } catch { - case e: Exception => - e.printStackTrace() - throw new RuntimeException(e) - } - - /** Invokes a query to increase the version number of a resource if the currently stored version - * matches the expected value. If there are multiple transactions executing this query then the - * first transaction will proceed and all the others will wait until the first transaction - * commits or aborts. This behavior should be obtainable by using Read Committed isolation level. - * - * @return - * True on a successful update. False when no rows were updated (indicating a wrong internal_id - * or a wrong expected version number). - */ - def compareAndIncreaseResourceVersion(tableName: String)( - internalId: Int, - expectedResourceVersion: Long, - )(connection: Connection): Boolean = { - val rowsUpdated = SQL""" - UPDATE #$tableName - SET resource_version = resource_version + 1 - WHERE - internal_id = $internalId - AND - resource_version = $expectedResourceVersion - """.executeUpdate()(connection) - rowsUpdated == 1 - } - - /** Invokes a query to increase the version number of a resource. If there are multiple - * transactions executing this query then the first transaction will proceed and all the others - * will wait until the first transaction commits or aborts. This behavior should be obtainable by - * using Read Committed isolation level. - * - * @return - * True on a successful update. False when no rows were updated (indicating a wrong - * internal_id). - */ - def increaseResourceVersion(tableName: String)(internalId: Int)( - connection: Connection - ): Boolean = { - val rowsUpdated = SQL""" - UPDATE #$tableName - SET resource_version = resource_version + 1 - WHERE - internal_id = $internalId - """.executeUpdate()(connection) - rowsUpdated == 1 - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/PartyRecordStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/PartyRecordStorageBackend.scala deleted file mode 100644 index a5af9f1309..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/PartyRecordStorageBackend.scala +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.localstore - -import com.digitalasset.canton.ledger.api.IdentityProviderId -import com.digitalasset.daml.lf.data.Ref - -import java.sql.Connection - -trait PartyRecordStorageBackend extends ResourceVersionOps { - - def getPartyRecord(party: Ref.Party)( - connection: Connection - ): Option[PartyRecordStorageBackend.DbPartyRecord] - - def createPartyRecord(partyRecord: PartyRecordStorageBackend.DbPartyRecordPayload)( - connection: Connection - ): Int - - def getPartyAnnotations(internalId: Int)(connection: Connection): Map[String, String] - - def addPartyAnnotation(internalId: Int, key: String, value: String, updatedAt: Long)( - connection: Connection - ): Unit - - def deletePartyAnnotations(internalId: Int)(connection: Connection): Unit - - def filterExistingParties( - parties: Set[Ref.Party], - identityProviderId: Option[IdentityProviderId.Id], - )(connection: Connection): Set[Ref.Party] - - def filterExistingParties( - parties: Set[Ref.Party] - )(connection: Connection): Set[Ref.Party] - - def updatePartyRecordIdp(internalId: Int, identityProviderId: Option[IdentityProviderId.Id])( - connection: Connection - ): Boolean - -} - -object PartyRecordStorageBackend { - final case class DbPartyRecordPayload( - party: Ref.Party, - identityProviderId: Option[IdentityProviderId.Id], - resourceVersion: Long, - createdAt: Long, - ) - - final case class DbPartyRecord( - internalId: Int, - payload: DbPartyRecordPayload, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/PartyRecordStorageBackendImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/PartyRecordStorageBackendImpl.scala deleted file mode 100644 index d43e88c2c1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/PartyRecordStorageBackendImpl.scala +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.localstore - -import anorm.SqlParser.{int, long, str} -import anorm.{RowParser, SqlParser, SqlStringInterpolation, ~} -import com.digitalasset.canton.ledger.api.IdentityProviderId -import com.digitalasset.canton.platform.store.backend.Conversions.party -import com.digitalasset.daml.lf.data.Ref - -import java.sql.Connection -import scala.util.Try - -object PartyRecordStorageBackendImpl extends PartyRecordStorageBackend { - - private val PartyRecordParser: RowParser[(Int, Ref.Party, Option[String], Long, Long)] = - int("internal_id") ~ - party("party") ~ - str("identity_provider_id").? ~ - long("resource_version") ~ - long("created_at") map { - case internalId ~ party ~ identityProviderId ~ resourceVersion ~ createdAt => - (internalId, party, identityProviderId, resourceVersion, createdAt) - } - - override def getPartyRecord( - party: Ref.Party - )(connection: Connection): Option[PartyRecordStorageBackend.DbPartyRecord] = - SQL""" - SELECT - internal_id, - party, - identity_provider_id, - resource_version, - created_at - FROM lapi_party_records - WHERE - party = ${party: String} - """ - .as(PartyRecordParser.singleOpt)(connection) - .map { case (internalId, party, identityProviderId, resourceVersion, createdAt) => - PartyRecordStorageBackend.DbPartyRecord( - internalId = internalId, - payload = PartyRecordStorageBackend.DbPartyRecordPayload( - party = party, - identityProviderId = identityProviderId.map(IdentityProviderId.Id.assertFromString), - resourceVersion = resourceVersion, - createdAt = createdAt, - ), - ) - } - - override def createPartyRecord( - partyRecord: PartyRecordStorageBackend.DbPartyRecordPayload - )(connection: Connection): Int = { - val party = partyRecord.party: String - val identityProviderId = partyRecord.identityProviderId.map(_.value): Option[String] - val resourceVersion = partyRecord.resourceVersion - val createdAt = partyRecord.createdAt - val internalId: Try[Int] = SQL""" - INSERT INTO lapi_party_records (party, identity_provider_id, resource_version, created_at) - VALUES ($party, $identityProviderId, $resourceVersion, $createdAt) - """.executeInsert1("internal_id")(SqlParser.scalar[Int].single)(connection) - internalId.fold(throw _, identity) - } - - override def getPartyAnnotations(internalId: Int)(connection: Connection): Map[String, String] = - ParticipantMetadataBackend.getAnnotations("lapi_party_record_annotations")(internalId)( - connection - ) - - override def addPartyAnnotation(internalId: Int, key: String, value: String, updatedAt: Long)( - connection: Connection - ): Unit = - ParticipantMetadataBackend.addAnnotation("lapi_party_record_annotations")( - internalId, - key, - value, - updatedAt, - )(connection) - - override def deletePartyAnnotations(internalId: Int)(connection: Connection): Unit = - ParticipantMetadataBackend.deleteAnnotations("lapi_party_record_annotations")( - internalId - )( - connection - ) - - override def compareAndIncreaseResourceVersion(internalId: Int, expectedResourceVersion: Long)( - connection: Connection - ): Boolean = - ParticipantMetadataBackend.compareAndIncreaseResourceVersion("lapi_party_records")( - internalId, - expectedResourceVersion, - )(connection) - - override def increaseResourceVersion(internalId: Int)(connection: Connection): Boolean = - ParticipantMetadataBackend.increaseResourceVersion("lapi_party_records")(internalId)( - connection - ) - - override def filterExistingParties( - parties: Set[Ref.Party], - identityProviderId: Option[IdentityProviderId.Id], - )(connection: Connection): Set[Ref.Party] = if (parties.nonEmpty) { - import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* - import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - val filteredParties = cSQL"party in (${parties.map(_.toString)})" - - val filteredIdentityProviderId = identityProviderId match { - case Some(id) => cSQL"identity_provider_id = ${id.value: String}" - case None => cSQL"identity_provider_id is NULL" - } - SQL""" - SELECT - party - FROM lapi_party_records - WHERE - $filteredIdentityProviderId AND $filteredParties - """ - .asVectorOf(party("party"))(connection) - .toSet - } else Set.empty - - override def filterExistingParties( - parties: Set[Ref.Party] - )(connection: Connection): Set[Ref.Party] = if (parties.nonEmpty) { - import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* - import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - val filteredParties = cSQL"party in (${parties.map(_.toString)})" - - SQL""" - SELECT - party - FROM lapi_party_records - WHERE - $filteredParties - ORDER BY party - """ - .asVectorOf(party("party"))(connection) - .toSet - } else Set.empty - - override def updatePartyRecordIdp( - internalId: Int, - identityProviderId: Option[IdentityProviderId.Id], - )(connection: Connection): Boolean = { - val idpId = identityProviderId.map(_.value): Option[String] - val rowsUpdated = - SQL""" - UPDATE lapi_party_records - SET identity_provider_id = $idpId - WHERE - internal_id = $internalId - """.executeUpdate()(connection) - rowsUpdated == 1 - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/ResourceVersionOps.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/ResourceVersionOps.scala deleted file mode 100644 index c1dca0a8bf..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/ResourceVersionOps.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.localstore - -import java.sql.Connection - -trait ResourceVersionOps { - def compareAndIncreaseResourceVersion( - internalId: Int, - expectedResourceVersion: Long, - )(connection: Connection): Boolean - - def increaseResourceVersion( - internalId: Int - )(connection: Connection): Boolean -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/UserManagementStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/UserManagementStorageBackend.scala deleted file mode 100644 index 6774f97f8c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/UserManagementStorageBackend.scala +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.localstore - -import com.digitalasset.canton.ledger.api.{IdentityProviderId, UserRight} -import com.digitalasset.canton.platform.UserId -import com.digitalasset.daml.lf.data.Ref - -import java.sql.Connection - -trait UserManagementStorageBackend extends ResourceVersionOps { - - def createUser(user: UserManagementStorageBackend.DbUserPayload)(connection: Connection): Int - - def addUserAnnotation(internalId: Int, key: String, value: String, updatedAt: Long)( - connection: Connection - ): Unit - - def deleteUserAnnotations(internalId: Int)(connection: Connection): Unit - - def getUserAnnotations(internalId: Int)(connection: Connection): Map[String, String] - - def deleteUser(id: UserId)(connection: Connection): Boolean - - def getUser(id: UserId)(connection: Connection): Option[UserManagementStorageBackend.DbUserWithId] - - def getUsersOrderedById( - fromExcl: Option[UserId] = None, - maxResults: Int, - identityProviderId: IdentityProviderId, - )( - connection: Connection - ): Vector[UserManagementStorageBackend.DbUserWithId] - - def addUserRight(internalId: Int, right: UserRight, grantedAt: Long)( - connection: Connection - ): Unit - - /** @return - * true if the right existed and we have just deleted it. - */ - def deleteUserRight(internalId: Int, right: UserRight)(connection: Connection): Boolean - - def userRightExists(internalId: Int, right: UserRight)(connection: Connection): Boolean - - def getUserRights(internalId: Int)( - connection: Connection - ): Set[UserManagementStorageBackend.DbUserRight] - - def countUserRights(internalId: Int)(connection: Connection): Int - - def updateUserPrimaryParty(internalId: Int, primaryPartyO: Option[Ref.Party])( - connection: Connection - ): Boolean - - def updateUserIdp(internalId: Int, identityProviderId: Option[IdentityProviderId.Id])( - connection: Connection - ): Boolean - - def updateUserIsDeactivated( - internalId: Int, - isDeactivated: Boolean, - )(connection: Connection): Boolean - - def updateUserPrimaryPartyAuthentication( - internalId: Int, - primaryPartyAuthentication: Boolean, - )(connection: Connection): Boolean -} - -object UserManagementStorageBackend { - final case class DbUserPayload( - id: Ref.UserId, - primaryPartyO: Option[Ref.Party], - identityProviderId: Option[IdentityProviderId.Id], - isDeactivated: Boolean, - primaryPartyAuthentication: Boolean, - resourceVersion: Long, - createdAt: Long, - ) - - final case class DbUserWithId( - internalId: Int, - payload: DbUserPayload, - ) - final case class DbUserRight(apiRight: UserRight, grantedAt: Long) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/UserManagementStorageBackendImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/UserManagementStorageBackendImpl.scala deleted file mode 100644 index c1e7770c97..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/localstore/UserManagementStorageBackendImpl.scala +++ /dev/null @@ -1,387 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.localstore - -import anorm.SqlParser.{bool, int, long, str} -import anorm.{RowParser, SqlParser, SqlStringInterpolation, ~} -import com.daml.ledger.api.v2.admin.user_management_service.Right -import com.digitalasset.canton.ledger.api.UserRight.{ - CanActAs, - CanExecuteAs, - CanExecuteAsAnyParty, - CanReadAs, - CanReadAsAnyParty, - IdentityProviderAdmin, - ParticipantAdmin, -} -import com.digitalasset.canton.ledger.api.{IdentityProviderId, UserRight} -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* -import com.digitalasset.canton.platform.store.backend.common.{ComposableQuery, QueryStrategy} -import com.digitalasset.canton.platform.{LedgerString, Party, UserId} - -import java.sql.Connection -import scala.util.Try - -object UserManagementStorageBackendImpl extends UserManagementStorageBackend { - - private val ParticipantUserParser - : RowParser[(Int, String, Option[String], Option[String], Boolean, Boolean, Long, Long)] = { - import com.digitalasset.canton.platform.store.backend.Conversions.bigDecimalColumnToBoolean - int("internal_id") ~ - str("user_id") ~ - str("primary_party").? ~ - str("identity_provider_id").? ~ - bool("is_deactivated") ~ - bool("primary_party_authentication") ~ - long("resource_version") ~ - long("created_at") map { - case internalId ~ userId ~ primaryParty ~ identityProviderId ~ isDeactivated ~ primaryPartyAuthentication ~ resourceVersion ~ createdAt => - ( - internalId, - userId, - primaryParty, - identityProviderId, - isDeactivated, - primaryPartyAuthentication, - resourceVersion, - createdAt, - ) - } - } - - private val UserRightParser: RowParser[(Int, Option[String], Long)] = - int("user_right") ~ str("for_party").? ~ long("granted_at") map { - case userRight ~ forParty ~ grantedAt => - (userRight, forParty, grantedAt) - } - - private val IntParser0: RowParser[Int] = - int("dummy") map { i => i } - - override def createUser( - user: UserManagementStorageBackend.DbUserPayload - )(connection: Connection): Int = { - val id = user.id: String - val primaryParty = user.primaryPartyO: Option[String] - val identityProviderId = user.identityProviderId.map(_.value): Option[String] - val isDeactivated = user.isDeactivated - val primaryPartyAuthentication = user.primaryPartyAuthentication - val resourceVersion = user.resourceVersion - val createdAt = user.createdAt - val internalId: Try[Int] = - SQL""" - INSERT INTO lapi_users (user_id, primary_party, identity_provider_id, is_deactivated, primary_party_authentication, resource_version, created_at) - VALUES ($id, $primaryParty, $identityProviderId, $isDeactivated, $primaryPartyAuthentication, $resourceVersion, $createdAt) - """.executeInsert1("internal_id")(SqlParser.scalar[Int].single)(connection) - internalId.fold(throw _, identity) - } - - override def addUserAnnotation(internalId: Int, key: String, value: String, updatedAt: Long)( - connection: Connection - ): Unit = - ParticipantMetadataBackend.addAnnotation("lapi_user_annotations")( - internalId, - key, - value, - updatedAt, - )(connection) - - override def deleteUserAnnotations(internalId: Int)(connection: Connection): Unit = - ParticipantMetadataBackend.deleteAnnotations("lapi_user_annotations")(internalId)( - connection - ) - - override def getUserAnnotations(internalId: Int)(connection: Connection): Map[String, String] = - ParticipantMetadataBackend.getAnnotations("lapi_user_annotations")(internalId)( - connection - ) - - override def compareAndIncreaseResourceVersion( - internalId: Int, - expectedResourceVersion: Long, - )(connection: Connection): Boolean = - ParticipantMetadataBackend.compareAndIncreaseResourceVersion("lapi_users")( - internalId, - expectedResourceVersion, - )(connection) - - override def increaseResourceVersion(internalId: Int)( - connection: Connection - ): Boolean = - ParticipantMetadataBackend.increaseResourceVersion("lapi_users")(internalId)(connection) - - override def getUser( - id: UserId - )(connection: Connection): Option[UserManagementStorageBackend.DbUserWithId] = - SQL""" - SELECT internal_id, user_id, primary_party, is_deactivated, identity_provider_id, primary_party_authentication, resource_version, created_at - FROM lapi_users - WHERE user_id = ${id: String} - """ - .as(ParticipantUserParser.singleOpt)(connection) - .map { - case ( - internalId, - userId, - primaryPartyRaw, - identityProviderId, - isDeactivated, - primaryPartyAuthentication, - resourceVersion, - createdAt, - ) => - UserManagementStorageBackend.DbUserWithId( - internalId = internalId, - payload = UserManagementStorageBackend.DbUserPayload( - id = UserId.assertFromString(userId), - primaryPartyO = dbStringToPartyString(primaryPartyRaw), - identityProviderId = dbStringToIdentityProviderId(identityProviderId), - isDeactivated = isDeactivated, - primaryPartyAuthentication = primaryPartyAuthentication, - resourceVersion = resourceVersion, - createdAt = createdAt, - ), - ) - } - - override def getUsersOrderedById( - fromExcl: Option[UserId], - maxResults: Int, - identityProviderId: IdentityProviderId, - )( - connection: Connection - ): Vector[UserManagementStorageBackend.DbUserWithId] = { - import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - val userIdWhereClause = fromExcl match { - case None => Nil - case Some(id: String) => List(cSQL"user_id > $id") - } - val identityProviderIdWhereClause = identityProviderId match { - case IdentityProviderId.Default => - List(cSQL"identity_provider_id is NULL") - case IdentityProviderId.Id(value) => - List(cSQL"identity_provider_id=${value: String}") - } - val whereClause = { - val clauses = userIdWhereClause ++ identityProviderIdWhereClause - if (clauses.nonEmpty) { - cSQL"WHERE ${clauses.mkComposite("", " AND ", "")}" - } else - cSQL"" - } - SQL"""SELECT internal_id, user_id, primary_party, identity_provider_id, is_deactivated, primary_party_authentication, resource_version, created_at - FROM lapi_users - $whereClause - ORDER BY user_id - ${QueryStrategy.limitClause(Some(maxResults))}""" - .asVectorOf(ParticipantUserParser)(connection) - .map { - case ( - internalId, - userId, - primaryPartyRaw, - identityProviderId, - isDeactivated, - primaryPartyAuthentication, - resourceVersion, - createdAt, - ) => - UserManagementStorageBackend.DbUserWithId( - internalId = internalId, - payload = UserManagementStorageBackend.DbUserPayload( - id = UserId.assertFromString(userId), - primaryPartyO = dbStringToPartyString(primaryPartyRaw), - identityProviderId = dbStringToIdentityProviderId(identityProviderId), - isDeactivated = isDeactivated, - primaryPartyAuthentication = primaryPartyAuthentication, - resourceVersion = resourceVersion, - createdAt = createdAt, - ), - ) - } - } - - override def deleteUser(userId: UserId)(connection: Connection): Boolean = { - val updatedRowsCount = - SQL""" - DELETE FROM lapi_users WHERE user_id = ${userId: String} - """.executeUpdate()(connection) - updatedRowsCount == 1 - } - - override def userRightExists(internalId: Int, right: UserRight)( - connection: Connection - ): Boolean = { - val (userRight: Int, forParty: Option[Party]) = fromUserRight(right) - - import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - val res: Seq[?] = - SQL""" - SELECT 1 AS dummy - FROM lapi_user_rights ur - WHERE ur.user_internal_id = $internalId - AND - ur.user_right = $userRight - AND - ur.for_party ${isForPartyPredicate(forParty)}""".asVectorOf(IntParser0)(connection) - assert(res.sizeIs <= 1) - res.sizeIs == 1 - } - - override def addUserRight(internalId: Int, right: UserRight, grantedAt: Long)( - connection: Connection - ): Unit = { - val (userRight: Int, forParty: Option[Party]) = fromUserRight(right) - val _ = - SQL""" - INSERT INTO lapi_user_rights (user_internal_id, user_right, for_party, granted_at) - VALUES ( - $internalId, - $userRight, - ${forParty: Option[String]}, - $grantedAt - ) - """.executeUpdate()(connection) - } - - override def getUserRights( - internalId: Int - )(connection: Connection): Set[UserManagementStorageBackend.DbUserRight] = { - val rec = - SQL""" - SELECT ur.user_right, ur.for_party, ur.granted_at - FROM lapi_user_rights ur - WHERE ur.user_internal_id = $internalId - """.asVectorOf(UserRightParser)(connection) - rec.map { case (userRight, forPartyRaw, grantedAt) => - UserManagementStorageBackend.DbUserRight( - makeUserRight( - value = userRight, - partyRaw = forPartyRaw, - ), - grantedAt = grantedAt, - ) - }.toSet - } - - override def deleteUserRight(internalId: Int, right: UserRight)( - connection: Connection - ): Boolean = { - val (userRight: Int, forParty: Option[Party]) = fromUserRight(right) - - import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - val updatedRowCount: Int = - SQL""" - DELETE FROM lapi_user_rights ur - WHERE - ur.user_internal_id = $internalId - AND - ur.user_right = $userRight - AND - ur.for_party ${isForPartyPredicate(forParty)} - """.executeUpdate()(connection) - updatedRowCount == 1 - } - - override def countUserRights(internalId: Int)(connection: Connection): Int = - SQL"SELECT count(*) AS user_rights_count from lapi_user_rights WHERE user_internal_id = $internalId" - .as(SqlParser.int("user_rights_count").single)(connection) - - private def makeUserRight(value: Int, partyRaw: Option[String]): UserRight = { - val partyO = dbStringToPartyString(partyRaw) - (value, partyO) match { - case (Right.PARTICIPANT_ADMIN_FIELD_NUMBER, None) => ParticipantAdmin - case (Right.CAN_ACT_AS_FIELD_NUMBER, Some(party)) => CanActAs(party) - case (Right.CAN_READ_AS_FIELD_NUMBER, Some(party)) => CanReadAs(party) - case (Right.CAN_EXECUTE_AS_FIELD_NUMBER, Some(party)) => CanExecuteAs(party) - case (Right.IDENTITY_PROVIDER_ADMIN_FIELD_NUMBER, None) => IdentityProviderAdmin - case (Right.CAN_READ_AS_ANY_PARTY_FIELD_NUMBER, None) => CanReadAsAnyParty - case (Right.CAN_EXECUTE_AS_ANY_PARTY_FIELD_NUMBER, None) => CanExecuteAsAnyParty - case _ => - throw new RuntimeException(s"Could not convert ${(value, partyO)} to a user right.") - } - } - - private def fromUserRight(right: UserRight): (Int, Option[Party]) = - right match { - case ParticipantAdmin => (Right.PARTICIPANT_ADMIN_FIELD_NUMBER, None) - case IdentityProviderAdmin => (Right.IDENTITY_PROVIDER_ADMIN_FIELD_NUMBER, None) - case CanActAs(party) => (Right.CAN_ACT_AS_FIELD_NUMBER, Some(party)) - case CanReadAs(party) => (Right.CAN_READ_AS_FIELD_NUMBER, Some(party)) - case CanReadAsAnyParty => (Right.CAN_READ_AS_ANY_PARTY_FIELD_NUMBER, None) - case CanExecuteAs(party) => (Right.CAN_EXECUTE_AS_FIELD_NUMBER, Some(party)) - case CanExecuteAsAnyParty => (Right.CAN_EXECUTE_AS_ANY_PARTY_FIELD_NUMBER, None) - case _ => - throw new RuntimeException(s"Could not recognize user right: $right.") - } - - private def dbStringToPartyString(raw: Option[String]): Option[Party] = - raw.map(Party.assertFromString) - - private def dbStringToIdentityProviderId( - raw: Option[String] - ): Option[IdentityProviderId.Id] = - raw.map(LedgerString.assertFromString).map(IdentityProviderId.Id.apply) - - private def isForPartyPredicate(forParty: Option[Party]): ComposableQuery.CompositeSql = { - import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - forParty.fold(cSQL"IS NULL") { party => - cSQL"= ${party: String}" - } - } - - override def updateUserPrimaryParty(internalId: Int, primaryPartyO: Option[Party])( - connection: Connection - ): Boolean = { - val rowsUpdated = SQL""" - UPDATE lapi_users - SET primary_party = ${primaryPartyO: Option[String]} - WHERE - internal_id = $internalId - """.executeUpdate()(connection) - rowsUpdated == 1 - } - - override def updateUserIsDeactivated(internalId: Int, isDeactivated: Boolean)( - connection: Connection - ): Boolean = { - val rowsUpdated = SQL""" - UPDATE lapi_users - SET is_deactivated = $isDeactivated - WHERE - internal_id = $internalId - """.executeUpdate()(connection) - rowsUpdated == 1 - } - - override def updateUserIdp(internalId: Int, identityProviderId: Option[IdentityProviderId.Id])( - connection: Connection - ): Boolean = { - val idpId = identityProviderId.map(_.value): Option[String] - val rowsUpdated = - SQL""" - UPDATE lapi_users - SET identity_provider_id = $idpId - WHERE - internal_id = $internalId - """.executeUpdate()(connection) - rowsUpdated == 1 - } - - override def updateUserPrimaryPartyAuthentication( - internalId: Int, - primaryPartyAuthentication: Boolean, - )( - connection: Connection - ): Boolean = { - val rowsUpdated = SQL""" - UPDATE lapi_users - SET primary_party_authentication = $primaryPartyAuthentication - WHERE - internal_id = $internalId - """.executeUpdate()(connection) - rowsUpdated == 1 - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGField.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGField.scala deleted file mode 100644 index 51fb036d97..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGField.scala +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import com.digitalasset.canton.platform.store.backend.common.Field -import com.digitalasset.canton.platform.store.interning.StringInterning - -private[postgresql] trait PGStringArrayBase[From, To] extends Field[From, To, String] { - override def selectFieldExpression(inputFieldName: String): String = - s"string_to_array($inputFieldName, '|')" - - protected def convertBase: Iterable[String] => String = { in => - assert( - in.forall(!_.contains("|")), - s"The following input string(s) contain the character '|', which is not expected: ${in.filter(_.contains("|")).mkString(", ")}", - ) - in.mkString("|") - } -} - -private[postgresql] final case class PGStringArray[From]( - extract: StringInterning => From => Iterable[String] -) extends PGStringArrayBase[From, Iterable[String]] { - override def convert: Iterable[String] => String = convertBase -} - -private[postgresql] final case class PGSmallint[From]( - extract: StringInterning => From => Int -) extends Field[From, Int, java.lang.Integer] { - override def selectFieldExpression(inputFieldName: String): String = - s"$inputFieldName::smallint" - - override def convert: Int => Integer = Int.box -} - -private[postgresql] final case class PGSmallintOptional[From]( - extract: StringInterning => From => Option[Int] -) extends Field[From, Option[Int], java.lang.Integer] { - override def selectFieldExpression(inputFieldName: String): String = - s"$inputFieldName::smallint" - - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def convert: Option[Int] => Integer = _.map(Int.box).orNull -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGSchema.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGSchema.scala deleted file mode 100644 index 5738110983..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGSchema.scala +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import com.digitalasset.canton.platform.store.backend.DbDto -import com.digitalasset.canton.platform.store.backend.common.AppendOnlySchema.FieldStrategy -import com.digitalasset.canton.platform.store.backend.common.{ - AppendOnlySchema, - Field, - Schema, - Table, -} -import com.digitalasset.canton.platform.store.interning.StringInterning - -private[postgresql] object PGSchema { - private val PGFieldStrategy = new FieldStrategy { - override def stringArray[From]( - extractor: StringInterning => From => Iterable[String] - ): Field[From, Iterable[String], ?] = - PGStringArray(extractor) - - override def smallint[From]( - extractor: StringInterning => From => Int - ): Field[From, Int, ?] = - PGSmallint(extractor) - - override def smallintOptional[From]( - extractor: StringInterning => From => Option[Int] - ): Field[From, Option[Int], ?] = - PGSmallintOptional(extractor) - - override def insert[From](tableName: String)( - fields: (String, Field[From, ?, ?])* - ): Table[From] = - PGTable.transposedInsert(tableName)(fields*) - } - - val schema: Schema[DbDto] = AppendOnlySchema(PGFieldStrategy) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGTable.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGTable.scala deleted file mode 100644 index 9fad8a5dfe..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PGTable.scala +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import com.digitalasset.canton.platform.store.backend.common.{BaseTable, Field, Table} - -import java.sql.Connection - -private[postgresql] object PGTable { - - private def transposedInsertBase[From]( - insertStatement: String, - ordering: Option[Ordering[From]] = None, - )(fields: Seq[(String, Field[From, ?, ?])]): Table[From] = - new BaseTable[From](fields, ordering) { - override def executeUpdate: Array[Array[?]] => Connection => Unit = - data => - connection => - Table.ifNonEmpty(data) { - val preparedStatement = connection.prepareStatement(insertStatement) - fields.indices.foreach { i => - preparedStatement.setObject(i + 1, data(i)) - } - preparedStatement.execute() - preparedStatement.close() - } - } - - private def transposedInsertStatement( - tableName: String, - fields: Seq[(String, Field[?, ?, ?])], - statementSuffix: String = "", - ): String = { - def commaSeparatedOf(extractor: ((String, Field[?, ?, ?])) => String): String = - fields.view - .map(extractor) - .mkString(",") - def inputFieldName: String => String = fieldName => s"${fieldName}_in" - val tableFields = commaSeparatedOf(_._1) - val selectFields = commaSeparatedOf { case (fieldName, field) => - field.selectFieldExpression(inputFieldName(fieldName)) - } - val unnestFields = commaSeparatedOf(_ => "?") - val inputFields = commaSeparatedOf(fieldDef => inputFieldName(fieldDef._1)) - s""" - |INSERT INTO $tableName - | ($tableFields) - | SELECT - | $selectFields - | From - | unnest($unnestFields) - | as t($inputFields) - | $statementSuffix - |""".stripMargin - } - - def transposedInsert[From](tableName: String)( - fields: (String, Field[From, ?, ?])* - ): Table[From] = - transposedInsertBase(transposedInsertStatement(tableName, fields))(fields) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresContractStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresContractStorageBackend.scala deleted file mode 100644 index 861e5a78ba..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresContractStorageBackend.scala +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import com.digitalasset.canton.platform.store.backend.ContractStorageBackend -import com.digitalasset.canton.platform.store.backend.common.{ - ContractStorageBackendTemplate, - QueryStrategy, -} -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.interning.StringInterning -import com.digitalasset.canton.topology.SynchronizerId - -import java.sql.Connection - -class PostgresContractStorageBackend( - stringInterning: StringInterning, - ledgerEndCache: LedgerEndCache, -) extends ContractStorageBackendTemplate(PostgresQueryStrategy, stringInterning, ledgerEndCache) { - - private def toArrayLiteral(values: Iterable[Any]): String = - values.mkString("ARRAY[", ", ", "]") - - override def lastActivations(synchronizerContracts: Iterable[(SynchronizerId, Long)])( - connection: Connection - ): Map[(SynchronizerId, Long), Long] = - ledgerEndCache() - .map { ledgerEnd => - val inputWithIndex = synchronizerContracts.zipWithIndex - - val indexArrayLiteral = toArrayLiteral(inputWithIndex.view.map(_._2)) - val synchronizerIdArrayLiteral = toArrayLiteral( - inputWithIndex.view.map(_._1._1).map(stringInterning.synchronizerId.internalize) - ) - val internalContractIdArrayLiteral = toArrayLiteral(inputWithIndex.view.map(_._1._2)) - // Resorting here to non-prepared statement as the combination of prepared statement and unnest and cross lateral join produced very inefficient query plans with PostgreSQL. - // For Future reference: - // * Wrong query plan involved traversing the event_sequential_id index backwards in a index scan and eliminating candidates with filters on table itself (the good plan is the descending index only scan with index condition over the contract ID) - // * Query plans without prepared statement results in an efficient plan in tests - // * Only the prepared statement via JDBC resulted in inefficient plans (creating prepared statements for example via psql tool with PREPARE was not exhibiting the same problem) - val results = QueryStrategy - .plainJdbcQuery(s""" - SELECT input.index as result_index, activate_evs.event_sequential_id as result_event_sequential_id - FROM UNNEST($indexArrayLiteral, $synchronizerIdArrayLiteral, $internalContractIdArrayLiteral) AS input(index, synchronizer_id, internal_contract_id) - CROSS JOIN LATERAL ( - SELECT * - FROM lapi_events_activate_contract activate_evs - WHERE activate_evs.internal_contract_id = input.internal_contract_id - AND activate_evs.event_sequential_id <= ${ledgerEnd.lastEventSeqId} - AND EXISTS ( -- subquery for triggering (event_sequential_id) INCLUDE (synchronizer_id) index usage - SELECT 1 - FROM lapi_events_activate_contract as activate_evs2 - WHERE - activate_evs2.event_sequential_id = activate_evs.event_sequential_id AND - activate_evs2.synchronizer_id = input.synchronizer_id - ) - ORDER BY activate_evs.event_sequential_id DESC - LIMIT 1 - ) activate_evs""")(resultSet => - ( - resultSet.getInt("result_index"), - resultSet.getLong("result_event_sequential_id"), - ) - )(connection) - .toMap - inputWithIndex.iterator.flatMap { case (synCon, index) => - results.get(index).map(synCon -> _) - }.toMap - } - .getOrElse(Map.empty) - - override final def supportsBatchKeyStateLookups: Boolean = true - - override def contractKeysPlain( - keyPageQueries: Seq[ContractStorageBackend.KeysPageQuery], - validAtEventSeqId: Long, - )(connection: Connection): Seq[ContractStorageBackend.KeysPageResult] = - if (keyPageQueries.isEmpty) Seq.empty - else { - val queriesWithIndex = keyPageQueries.zipWithIndex - - def toStringArrayLiteral(values: Iterable[String]): String = - values.map(v => s"'$v'").mkString("ARRAY[", ", ", "]::text[]") - - val indexArrayLiteral = toArrayLiteral(queriesWithIndex.view.map(_._2)) - val keyHashArrayLiteral = toStringArrayLiteral( - queriesWithIndex.view.map(_._1.key.hash.bytes.toHexString) - ) - val eventSeqIdUpperBounds = queriesWithIndex.view.map { case (q, _) => - q.nextPageToken.map(_ - 1).getOrElse(validAtEventSeqId) - } - val upperBoundArrayLiteral = toArrayLiteral(eventSeqIdUpperBounds) - val limitArrayLiteral = toArrayLiteral(queriesWithIndex.view.map(_._1.limit)) - - val results: Vector[(Int, Long, Long)] = QueryStrategy.plainJdbcQuery( - s""" - SELECT input.idx as result_index, - activate.event_sequential_id as result_event_seq_id, - activate.internal_contract_id as result_internal_contract_id - FROM UNNEST($indexArrayLiteral, $keyHashArrayLiteral, $upperBoundArrayLiteral, $limitArrayLiteral) - AS input(idx, key_hash, upper_bound, lim) - CROSS JOIN LATERAL ( - SELECT event_sequential_id, internal_contract_id - FROM lapi_events_activate_contract - WHERE - create_key_hash = input.key_hash - AND event_sequential_id <= input.upper_bound - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract - WHERE - deactivated_event_sequential_id = lapi_events_activate_contract.event_sequential_id - AND lapi_events_deactivate_contract.event_sequential_id <= $validAtEventSeqId - ) - ORDER BY event_sequential_id DESC - LIMIT input.lim + 1 - ) activate - ORDER BY input.idx, activate.event_sequential_id DESC""" - )(resultSet => - ( - resultSet.getInt("result_index"), - resultSet.getLong("result_event_seq_id"), - resultSet.getLong("result_internal_contract_id"), - ) - )(connection) - - val groupedResults: Map[Int, Vector[(Long, Long)]] = - results.groupBy(_._1).view.mapValues(_.map(t => (t._2, t._3))).toMap - - queriesWithIndex.map { case (query, index) => - val rows = groupedResults.getOrElse(index, Vector.empty) - val (eventSeqIds, internalContractIds) = rows.unzip - ContractStorageBackend.KeysPageResult( - internalContractIds = internalContractIds.take(query.limit), - nextPageToken = Option - .when(eventSeqIds.sizeIs == query.limit + 1)( - eventSeqIds.lastOption.map(_ + 1) - ) - .flatten, - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresDBLockStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresDBLockStorageBackend.scala deleted file mode 100644 index 31f373e8cf..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresDBLockStorageBackend.scala +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import anorm.SqlParser.get -import anorm.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.DBLockStorageBackend - -import java.sql.Connection - -object PostgresDBLockStorageBackend extends DBLockStorageBackend { - - override def tryAcquire( - lockId: DBLockStorageBackend.LockId, - lockMode: DBLockStorageBackend.LockMode, - )(connection: Connection): Option[DBLockStorageBackend.Lock] = { - val lockFunction = lockMode match { - case DBLockStorageBackend.LockMode.Exclusive => "pg_try_advisory_lock" - case DBLockStorageBackend.LockMode.Shared => "pg_try_advisory_lock_shared" - } - SQL"SELECT #$lockFunction(${pgBigintLockId(lockId)})" - .as(get[Boolean](1).single)(connection) match { - case true => Some(DBLockStorageBackend.Lock(lockId, lockMode)) - case false => None - } - } - - override def release(lock: DBLockStorageBackend.Lock)(connection: Connection): Boolean = { - val lockFunction = lock.lockMode match { - case DBLockStorageBackend.LockMode.Exclusive => "pg_advisory_unlock" - case DBLockStorageBackend.LockMode.Shared => "pg_advisory_unlock_shared" - } - SQL"SELECT #$lockFunction(${pgBigintLockId(lock.lockId)})" - .as(get[Boolean](1).single)(connection) - } - - final case class PGLockId(id: Long) extends DBLockStorageBackend.LockId - - private def pgBigintLockId(lockId: DBLockStorageBackend.LockId): Long = - lockId match { - case PGLockId(id) => id - case unknown => - throw new Exception( - s"LockId $unknown not supported. Probable cause: LockId was created by a different StorageBackend" - ) - } - - override def lock(id: Int): DBLockStorageBackend.LockId = PGLockId(id.toLong) - - override def dbLockSupported: Boolean = true -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresDataSourceStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresDataSourceStorageBackend.scala deleted file mode 100644 index a254760a14..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresDataSourceStorageBackend.scala +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import anorm.SqlParser.get -import anorm.SqlStringInterpolation -import com.daml.resources.ProgramResource.StartupException -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.config -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.store.backend.DataSourceStorageBackend -import com.digitalasset.canton.platform.store.backend.common.{ - DataSourceStorageBackendImpl, - InitHookDataSourceProxy, -} -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig.SynchronousCommitValue -import com.digitalasset.canton.tracing.TraceContext -import org.postgresql.ds.PGSimpleDataSource - -import java.sql.Connection -import javax.sql.DataSource - -/** Configuration for Postgres data source. - * - * @param synchronousCommit - * Synchronous commit setting for Postgres. - * @param tcpKeepalivesIdle - * TCP keepalive idle time in seconds. Corresponds to `tcp_keepalives_idle`. See - * [[https://www.postgresql.org/docs/14/runtime-config-connection.html#RUNTIME-CONFIG-CONNECTION-SETTINGS]] - * for details. A value of 0 selects the operating system's default. - * @param tcpKeepalivesInterval - * TCP keepalive interval in seconds. Corresponds to `tcp_keepalives_interval`. A value of 0 - * selects the operating system's default. - * @param tcpKeepalivesCount - * TCP keepalive count. Corresponds to `tcp_keepalives_count`. A value of 0 selects the operating - * system's default. - * @param clientConnectionCheckInterval - * Interval for client connection checks. Corresponds to `client_connection_check_interval` - * (Postgres >= 14 only). Millisecond granularity is the lowest supported precision. A value of 0 - * disables connection checks. On macOS with Postgres 14, this must be set to `None` (disabled), - * since in Postgres 14, this setting requires the non-standard `POLLRDHUP` extension to the - * `poll` system call, which is only available on Linux. See - * [[https://www.postgresql.org/docs/14/runtime-config-connection.html]] for details. - * @param networkTimeout - * Network timeout for database operations. Millisecond granularity is the lowest supported - * precision. A value of 0 indicates no timeout. - */ -final case class PostgresDataSourceConfig( - synchronousCommit: Option[SynchronousCommitValue] = None, - tcpKeepalivesIdle: Option[Int] = Some(10), - tcpKeepalivesInterval: Option[Int] = Some(1), - tcpKeepalivesCount: Option[Int] = Some(5), - clientConnectionCheckInterval: Option[config.NonNegativeFiniteDuration] = Some( - config.NonNegativeFiniteDuration.ofSeconds(5) - ), - networkTimeout: Option[config.NonNegativeFiniteDuration] = Some( - config.NonNegativeFiniteDuration.ofSeconds(60) - ), -) - -object PostgresDataSourceConfig { - sealed abstract class SynchronousCommitValue(val pgSqlName: String) - object SynchronousCommitValue { - case object On extends SynchronousCommitValue("on") - case object Off extends SynchronousCommitValue("off") - case object RemoteWrite extends SynchronousCommitValue("remote_write") - case object RemoteApply extends SynchronousCommitValue("remote_apply") - case object Local extends SynchronousCommitValue("local") - val All: Set[SynchronousCommitValue] = Set( - On, - Off, - RemoteWrite, - RemoteApply, - Local, - ) - } -} - -class PostgresDataSourceStorageBackend( - minMajorVersionSupported: Int, - val loggerFactory: NamedLoggerFactory, -) extends DataSourceStorageBackend - with NamedLogging { - - override def createDataSource( - dataSourceConfig: DataSourceStorageBackend.DataSourceConfig, - loggerFactory: NamedLoggerFactory, - connectionInitHook: Option[Connection => Unit], - ): DataSource = { - import DataSourceStorageBackendImpl.exe - val directEc = DirectExecutionContext(noTracingLogger) - - val pgSimpleDataSource = new PGSimpleDataSource() - pgSimpleDataSource.setUrl(dataSourceConfig.jdbcUrl) - - val hookFunctions = List( - dataSourceConfig.postgresConfig.synchronousCommit.toList - .map(synchCommitValue => exe(s"SET synchronous_commit TO ${synchCommitValue.pgSqlName}")), - dataSourceConfig.postgresConfig.tcpKeepalivesIdle.toList - .map(i => exe(s"SET tcp_keepalives_idle TO $i")), - dataSourceConfig.postgresConfig.tcpKeepalivesInterval.toList - .map(i => exe(s"SET tcp_keepalives_interval TO $i")), - dataSourceConfig.postgresConfig.tcpKeepalivesCount.toList - .map(i => exe(s"SET tcp_keepalives_count TO $i")), - dataSourceConfig.postgresConfig.clientConnectionCheckInterval.toList - .map(i => exe(s"SET client_connection_check_interval TO '${i.duration.toMillis} ms'")), - dataSourceConfig.postgresConfig.networkTimeout.toList.map { - networkTimeout => (connection: Connection) => - connection.setNetworkTimeout( - directEc, - // avoid overflow by capping to Int.MaxValue - networkTimeout.duration.toMillis.min(Int.MaxValue).toInt, - ) - }, - connectionInitHook.toList, - ).flatten - InitHookDataSourceProxy(pgSimpleDataSource, hookFunctions, loggerFactory) - } - - override def checkCompatibility( - connection: Connection - )(implicit traceContext: TraceContext): Unit = { - getPostgresVersion(connection) match { - case Some((major, minor)) => - if (major < minMajorVersionSupported) { - val errorMessage = - "Deprecated Postgres version. " + - s"Found Postgres version $major.$minor, minimum required Postgres version is $minMajorVersionSupported. " + - "This application will continue running but is at risk of data loss, as Postgres < 10 does not support crash-fault tolerant hash indices. " + - s"Please upgrade your Postgres database to version $minMajorVersionSupported or later to fix this issue." - logger.error(errorMessage) - throw new PostgresDataSourceStorageBackend.UnsupportedPostgresVersion(errorMessage) - } - case None => - logger.warn( - s"Could not determine the version of the Postgres database. Please verify that this application is compatible with this Postgres version." - ) - } - () - } - - private[backend] def getPostgresVersion( - connection: Connection - )(implicit traceContext: TraceContext): Option[(Int, Int)] = { - val version = SQL"SHOW server_version".as(get[String](1).single)(connection) - logger.debug(s"Found Postgres version $version") - parsePostgresVersion(version) - } - - private[backend] def parsePostgresVersion(version: String): Option[(Int, Int)] = { - val versionPattern = """(\d+)[.](\d+).*""".r - version match { - case versionPattern(major, minor) => Some((major.toInt, minor.toInt)) - case _ => None - } - } - - override def checkDatabaseAvailable(connection: Connection): Unit = - DataSourceStorageBackendImpl.checkDatabaseAvailable(connection) -} - -object PostgresDataSourceStorageBackend { - def apply(loggerFactory: NamedLoggerFactory): PostgresDataSourceStorageBackend = - new PostgresDataSourceStorageBackend(minMajorVersionSupported = 14, loggerFactory) - - final class UnsupportedPostgresVersion(message: String) - extends RuntimeException(message) - with StartupException -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresEventStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresEventStorageBackend.scala deleted file mode 100644 index c42a19d792..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresEventStorageBackend.scala +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import anorm.SqlParser.long -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.CannotAcquireAllRowLocksException -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.{ - CompositeSql, - SqlStringInterpolation, -} -import com.digitalasset.canton.platform.store.backend.common.EventStorageBackendTemplate -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.`SimpleSql ops` -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.interning.StringInterning -import org.postgresql.util.PSQLException - -import java.sql.Connection - -class PostgresEventStorageBackend( - ledgerEndCache: LedgerEndCache, - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, -) extends EventStorageBackendTemplate( - queryStrategy = PostgresQueryStrategy, - ledgerEndCache = ledgerEndCache, - stringInterning = stringInterning, - loggerFactory = loggerFactory, - ) { - - override def lockExclusivelyPruningProcessingTable(connection: Connection): Unit = { - assert(!connection.getAutoCommit) - SQL"""LOCK TABLE lapi_pruning_candidate_deactivated IN ACCESS EXCLUSIVE MODE""" - .execute()(connection) - .discard - } - - override def lockExclusivelyContractPruningProcessingTable(connection: Connection): Unit = { - assert(!connection.getAutoCommit) - SQL"""LOCK TABLE lapi_pruning_contract_candidate IN ACCESS EXCLUSIVE MODE""" - .execute()(connection) - .discard - } - - override def readLockInternalContractIds( - internalContractIds: Set[Long] - )(connection: Connection): Set[Long] = - internalContractIds -- SQL""" - SELECT internal_contract_id - FROM par_contracts - WHERE internal_contract_id ${PostgresQueryStrategy.anyOf(internalContractIds)} - ORDER BY internal_contract_id - FOR KEY SHARE - """ - .withFetchSize(Some(internalContractIds.size)) - .asVectorOf(long("internal_contract_id"))(connection) - .toSet - - override def writeLockInternalContractIds(whereInternalContractIdExprs: CompositeSql)( - connection: Connection - ): Unit = - try { - SQL""" - SELECT internal_contract_id - FROM par_contracts - WHERE internal_contract_id $whereInternalContractIdExprs - ORDER BY internal_contract_id - FOR UPDATE NOWAIT - """.execute()(connection).discard - } catch { - case pSQLException: PSQLException - // 55P03 lock_not_available (reference from https://www.postgresql.org/docs/current/errcodes-appendix.html) - if pSQLException.getServerErrorMessage.getSQLState == "55P03" => - throw new CannotAcquireAllRowLocksException - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresQueryStrategy.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresQueryStrategy.scala deleted file mode 100644 index 9c6524e929..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresQueryStrategy.scala +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import anorm.ToStatement -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.{ - CompositeSql, - SqlStringInterpolation, -} -import com.digitalasset.canton.platform.store.backend.common.QueryStrategy - -import java.sql.{Connection, PreparedStatement} - -object PostgresQueryStrategy extends QueryStrategy { - implicit object ArrayByteaToStatement extends ToStatement[Array[Array[Byte]]] { - override def set(s: PreparedStatement, index: Int, v: Array[Array[Byte]]): Unit = - s.setObject(index, v) - } - - override def anyOf(longs: Iterable[Long]): CompositeSql = { - val longArray: Array[java.lang.Long] = - longs.view.map(Long.box).toArray - cSQL"= ANY($longArray::bigint[])" - } - - override def anyOfSmallInts(ints: Iterable[Int]): CompositeSql = { - val intArray: Array[java.lang.Integer] = - ints.view.map(Int.box).toArray - cSQL"= ANY($intArray::smallint[])" - } - - override def anyOfStrings(strings: Iterable[String]): CompositeSql = { - val stringArray: Array[String] = - strings.toArray - cSQL"= ANY($stringArray::text[])" - } - - /** ANY SQL clause generation for a number of Binary values - */ - override def anyOfBinary(binaries: Iterable[Array[Byte]]): CompositeSql = { - val binaryArray: Array[Array[Byte]] = - binaries.toArray - cSQL"= ANY($binaryArray::bytea[])" - } - - override def analyzeTable(tableName: String): CompositeSql = - cSQL"ANALYZE #$tableName" - - override def forceSynchronousCommitForCurrentTransactionForPostgreSQL( - connection: Connection - ): Unit = SQL"SET LOCAL synchronous_commit TO ON".execute()(connection).discard -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresResetStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresResetStorageBackend.scala deleted file mode 100644 index 90deb8340d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresResetStorageBackend.scala +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.platform.store.backend.ResetStorageBackend -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation - -import java.sql.Connection - -object PostgresResetStorageBackend extends ResetStorageBackend { - - override def resetAll(connection: Connection): Unit = - SQL""" - delete from lapi_parameters cascade; - delete from lapi_achs_state cascade; - delete from lapi_ledger_end_synchronizer_index cascade; - delete from lapi_command_completions cascade; - delete from lapi_events_activate_contract cascade; - delete from lapi_filter_activate_stakeholder cascade; - delete from lapi_filter_achs_stakeholder cascade; - delete from lapi_filter_activate_witness cascade; - delete from lapi_events_deactivate_contract cascade; - delete from lapi_filter_deactivate_stakeholder cascade; - delete from lapi_filter_deactivate_witness cascade; - delete from lapi_events_various_witnessed cascade; - delete from lapi_filter_various_witness cascade; - delete from lapi_party_entries cascade; - delete from lapi_party_records cascade; - delete from lapi_party_record_annotations cascade; - delete from lapi_events_party_to_participant cascade; - delete from lapi_string_interning cascade; - delete from lapi_update_meta cascade; - delete from lapi_users cascade; - delete from lapi_user_annotations cascade; - delete from lapi_user_rights cascade; - delete from lapi_identity_provider_config cascade; - delete from par_pruning_operation; - delete from par_contracts; - delete from lapi_pruning_candidate_deactivated; - delete from lapi_pruning_contract_candidate; - """ - .execute()(connection) - .discard -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresStorageBackendFactory.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresStorageBackendFactory.scala deleted file mode 100644 index d1ba3bcd01..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/backend/postgresql/PostgresStorageBackendFactory.scala +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.postgresql - -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.backend.* -import com.digitalasset.canton.platform.store.backend.common.* -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.interning.StringInterning - -final case class PostgresStorageBackendFactory(loggerFactory: NamedLoggerFactory) - extends StorageBackendFactory - with CommonStorageBackendFactory { - - override val createIngestionStorageBackend: IngestionStorageBackend[?] = - new IngestionStorageBackendTemplate(PGSchema.schema) - - override def createParameterStorageBackend( - stringInterning: StringInterning - ): ParameterStorageBackend = - new ParameterStorageBackendImpl(PostgresQueryStrategy, stringInterning) - - override def createPartyStorageBackend(ledgerEndCache: LedgerEndCache): PartyStorageBackend = - new PartyStorageBackendTemplate(ledgerEndCache) - - override def createCompletionStorageBackend( - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, - ): CompletionStorageBackend = - new CompletionStorageBackendTemplate(stringInterning, loggerFactory) - - override def createContractStorageBackend( - stringInterning: StringInterning, - ledgerEndCache: LedgerEndCache, - ): ContractStorageBackend = - new PostgresContractStorageBackend(stringInterning, ledgerEndCache) - - override def createEventStorageBackend( - ledgerEndCache: LedgerEndCache, - stringInterning: StringInterning, - loggerFactory: NamedLoggerFactory, - ): EventStorageBackend = - new PostgresEventStorageBackend( - ledgerEndCache = ledgerEndCache, - stringInterning = stringInterning, - loggerFactory = loggerFactory, - ) - - override val createDataSourceStorageBackend: DataSourceStorageBackend = - PostgresDataSourceStorageBackend(loggerFactory) - - override val createDBLockStorageBackend: DBLockStorageBackend = - PostgresDBLockStorageBackend - - override val createResetStorageBackend: ResetStorageBackend = - PostgresResetStorageBackend - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/AchsStateCache.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/AchsStateCache.scala deleted file mode 100644 index f60c87b47a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/AchsStateCache.scala +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsLastPointers, - AchsState, -} - -import java.util.concurrent.atomic.AtomicReference - -/** In-memory cache for the ACHS (Active Contracts Head Snapshot) state. */ -class AchsStateCache(val loggerFactory: NamedLoggerFactory) extends NamedLogging { - private val achsState: AtomicReference[AchsState] = - new AtomicReference( - AchsState(validAt = 0, lastPointers = AchsLastPointers(lastRemoved = 0, lastPopulated = 0)) - ) - - def set(state: AchsState): Unit = achsState.set(state) - - def get(): AchsState = achsState.get() - - def updateValidAt(validAt: Long): Unit = - achsState.updateAndGet(_.copy(validAt = validAt)).discard - - def updateLastPointers(lastPointers: AchsLastPointers): Unit = - achsState - .getAndUpdate( - _.copy(lastPointers = lastPointers) - ) - .discard -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractKeyStateCache.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractKeyStateCache.scala deleted file mode 100644 index cf212d3194..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractKeyStateCache.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.digitalasset.canton.caching.SizedCache -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.daml.lf.transaction.GlobalKey - -import scala.concurrent.ExecutionContext - -object ContractKeyStateCache { - def apply( - initialCacheEventSeqIdIndex: Long, - cacheSize: Long, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - )(implicit - ec: ExecutionContext - ): StateCache[GlobalKey, ContractKeyStateValue] = - StateCache( - initialCacheEventSeqIdIndex = initialCacheEventSeqIdIndex, - emptyLedgerState = ContractKeyStateValue.Unassigned, - cache = SizedCache.from[GlobalKey, ContractKeyStateValue]( - SizedCache.Configuration(cacheSize), - metrics.execution.cache.keyState.stateCache, - ), - registerUpdateTimer = metrics.execution.cache.keyState.registerCacheUpdate, - loggerFactory = loggerFactory, - ) -} - -sealed trait ContractKeyStateValue extends Product with Serializable - -object ContractKeyStateValue { - - final case class Assigned(contractId: ContractId) extends ContractKeyStateValue - - final case object Unassigned extends ContractKeyStateValue -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractStateCaches.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractStateCaches.scala deleted file mode 100644 index 959708bccb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractStateCaches.scala +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import cats.data.NonEmptyVector -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.{ - Active, - Archived, - ExistingContractStatus, -} -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.cache.ContractKeyStateValue.{Assigned, Unassigned} -import com.digitalasset.canton.platform.store.dao.events.ContractStateEvent -import com.digitalasset.canton.platform.store.dao.events.ContractStateEvent.ReassignmentAccepted -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.transaction.GlobalKey - -import scala.concurrent.ExecutionContext - -/** Encapsulates the contract and key state caches with operations for mutating them. The caches are - * used for serving contract activeness and key lookups for command interpretation performed during - * command submission. - * - * @param keyState - * The contract key state cache. - * @param contractState - * The contract state cache. - * @param loggerFactory - * The logger factory. - */ -class ContractStateCaches( - private[cache] val keyState: StateCache[GlobalKey, ContractKeyStateValue], - private[cache] val contractState: StateCache[ContractId, ContractStateStatus], - val loggerFactory: NamedLoggerFactory, -) extends NamedLogging { - - /** Update the state caches with a batch of events. - * - * @param eventsBatch - * The contract state update events batch. The updates batch must be non-empty and with - * strictly increasing event sequential ids. - */ - def push( - eventsBatch: NonEmptyVector[ContractStateEvent], - lastEventSeqId: Long, - )(implicit traceContext: TraceContext): Unit = { - val keyMappingsBuilder = Map.newBuilder[Key, ContractKeyStateValue] - val contractMappingsBuilder = Map.newBuilder[ContractId, ExistingContractStatus] - - eventsBatch.toVector.foreach { - case created: ContractStateEvent.Created => - created.globalKey.foreach(key => - keyMappingsBuilder.addOne( - key -> Assigned(created.contractId) - ) - ) - contractMappingsBuilder.addOne(created.contractId -> Active) - - case archived: ContractStateEvent.Archived => - archived.globalKey.foreach { key => - keyMappingsBuilder.addOne(key -> Unassigned) - } - contractMappingsBuilder.addOne(archived.contractId -> Archived) - - case ReassignmentAccepted => () - } - - val keyMappings = keyMappingsBuilder.result() - val contractMappings = contractMappingsBuilder.result() - - val validAt = lastEventSeqId - keyState.putBatch(validAt, keyMappings) - contractState.putBatch(validAt, contractMappings) - } - - /** Reset the contract and key state caches to the specified offset. */ - def reset(lastPersistedLedgerEnd: Option[LedgerEnd]): Unit = { - val index = lastPersistedLedgerEnd.map(_.lastEventSeqId).getOrElse(0L) - keyState.reset(index) - contractState.reset(index) - } -} - -object ContractStateCaches { - def build( - initialCacheEventSeqIdIndex: Long, - maxContractsCacheSize: Long, - maxKeyCacheSize: Long, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext - ): ContractStateCaches = - new ContractStateCaches( - contractState = ContractsStateCache( - initialCacheEventSeqIdIndex, - maxContractsCacheSize, - metrics, - loggerFactory, - ), - keyState = - ContractKeyStateCache(initialCacheEventSeqIdIndex, maxKeyCacheSize, metrics, loggerFactory), - loggerFactory = loggerFactory, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractsStateCache.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractsStateCache.scala deleted file mode 100644 index bdf0bcd2be..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/ContractsStateCache.scala +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.digitalasset.canton.caching.SizedCache -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics - -import scala.concurrent.ExecutionContext - -object ContractsStateCache { - def apply( - initialCacheEventSeqIdIndex: Long, - cacheSize: Long, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - )(implicit - ec: ExecutionContext - ): StateCache[ContractId, ContractStateStatus] = - StateCache( - initialCacheEventSeqIdIndex = initialCacheEventSeqIdIndex, - emptyLedgerState = ContractStateStatus.NotFound, - cache = SizedCache.from[ContractId, ContractStateStatus]( - SizedCache.Configuration(cacheSize), - metrics.execution.cache.contractState.stateCache, - ), - registerUpdateTimer = metrics.execution.cache.contractState.registerCacheUpdate, - loggerFactory = loggerFactory, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/InMemoryFanoutBuffer.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/InMemoryFanoutBuffer.scala deleted file mode 100644 index c3f57be96b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/InMemoryFanoutBuffer.scala +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.daml.metrics.Timed -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer.* -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Mutex - -import scala.collection.Searching.{Found, InsertionPoint, SearchResult} -import scala.collection.View - -/** The in-memory fan-out buffer. - * - * This buffer stores the last ingested `maxBufferSize` accepted and rejected submission updates as - * [[com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate]] and allows bypassing - * IndexDB persistence fetches for recent updates for: - * - update streams - * - command completion streams - * - by-offset and by-update-id update lookups - * - * @param maxBufferSize - * The maximum buffer size. - * @param metrics - * The Daml metrics. - * @param maxBufferedChunkSize - * The maximum size of buffered chunks returned by `slice`. - */ -@SuppressWarnings(Array("org.wartremover.warts.Var")) -class InMemoryFanoutBuffer( - maxBufferSize: Int, - metrics: LedgerApiServerMetrics, - maxBufferedChunkSize: Int, - val loggerFactory: NamedLoggerFactory, -) extends NamedLogging { - @volatile private[cache] var _bufferLog = - Vector.empty[(Offset, TransactionLogUpdate)] - @volatile private[cache] var _lookupMap = - Map.empty[String, TransactionLogUpdate] - - private val bufferMetrics = metrics.services.index.inMemoryFanoutBuffer - private val pushTimer = bufferMetrics.push - private val pruneTimer = bufferMetrics.prune - private val bufferSizeHistogram = bufferMetrics.bufferSize - private val lock = new Mutex() - - /** Appends a new event to the buffer. - * - * Starts evicting from the tail when `maxBufferSize` is reached. - * - * @param entry - * The buffer entry. - */ - def push(entry: TransactionLogUpdate): Unit = - Timed.value( - pushTimer, - (lock.exclusive { - _bufferLog.lastOption.foreach { - // Encountering a non-strictly increasing offset is an error condition. - case (lastOffset, _) if lastOffset >= entry.offset => - throw UnorderedException(lastOffset, entry.offset) - case _ => - } - - if (maxBufferSize <= 0) { - // Do nothing since buffer updates are not atomic and the reads are not synchronized. - // This ensures that reads can never see data in the buffer. - } else { - ensureSize(maxBufferSize - 1)(entry.traceContext) - - _bufferLog = _bufferLog :+ entry.offset -> entry - extractEntryFromMap(entry).foreach { case (key, value) => - _lookupMap = _lookupMap.updated(key, value) - } - } - }), - ) - - /** Returns a slice of events from the buffer. - * - * @param startInclusive - * The start inclusive bound of the requested range. - * @param endInclusive - * The end inclusive bound of the requested range. - * @param filter - * A lambda function that allows pre-filtering the buffered elements before assembling - * `maxBufferedChunkSize`-sized slices. - * @return - * A slice of the series of events as an ordered vector satisfying the input bounds. - */ - def slice[FilterResult]( - startInclusive: Offset, - endInclusive: Offset, - filter: TransactionLogUpdate => Option[FilterResult], - ): BufferSlice[(Offset, FilterResult)] = { - val vectorSnapshot = _bufferLog - - val bufferStartSearchResult = vectorSnapshot.view.map(_._1).search(startInclusive) - val bufferEndSearchResult = vectorSnapshot.view.map(_._1).search(endInclusive) - - val bufferStartInclusiveIdx = indexAt(bufferStartSearchResult) - val bufferEndExclusiveIdx = indexAfter(bufferEndSearchResult) - - val bufferSlice = vectorSnapshot.slice(bufferStartInclusiveIdx, bufferEndExclusiveIdx) - - bufferStartSearchResult match { - case InsertionPoint(0) if bufferSlice.isEmpty => - BufferSlice.LastBufferChunkSuffix( - bufferedStartExclusive = endInclusive, - slice = Vector.empty, - ) - case InsertionPoint(0) => lastFilteredChunk(bufferSlice, filter, maxBufferedChunkSize) - case InsertionPoint(_) | Found(_) => - BufferSlice.Inclusive( - filterAndChunkSlice(bufferSlice.view, filter, maxBufferedChunkSize) - ) - } - } - - /** Returns a slice of events from the buffer in reverse order. - * - * @param startInclusive - * The start inclusive bound of the requested range. - * @param endInclusive - * The end inclusive bound of the requested range. - * @param filter - * A lambda function that allows pre-filtering the buffered elements before assembling - * `maxBufferedChunkSize`-sized slices. - * @return - * A slice of the series of events as a reverse ordered vector satisfying the input bounds. The - * slice is FinalSlice if all the requested data was in IMFO and was returned. and PartialSlice - * if some data might be outside of the IMFO or the chunk was trimmed. In such case another - * call to sliceBackwards should be made, if slice non empty. If slice is empty, then - * persistence fetch must be done. - */ - def sliceBackwards[FilterResult]( - startInclusive: Offset, - endInclusive: Offset, - filter: TransactionLogUpdate => Option[FilterResult], - ): BackwardBufferSlice[(Offset, FilterResult)] = { - val vectorSnapshot = _bufferLog - - val bufferEndSearchResult = vectorSnapshot.view.map(_._1).search(endInclusive) - val bufferEndExclusiveIdx = indexAfter(bufferEndSearchResult) - - val bufferStartSearchResult = vectorSnapshot.view.map(_._1).search(startInclusive) - val bufferStartInclusiveIdx = indexAt(bufferStartSearchResult) - - val filteredBufferSlice = filterAndChunkSlice( - vectorSnapshot.slice(bufferStartInclusiveIdx, bufferEndExclusiveIdx).view.reverse, - filter, - maxBufferedChunkSize, - ) - - noTracingLogger.debug(s"Insertion point: $bufferStartSearchResult") - if ( - filteredBufferSlice.sizeIs < maxBufferedChunkSize && bufferStartSearchResult != InsertionPoint( - 0 - ) - ) { - BackwardBufferSlice.FinalSlice(filteredBufferSlice) - } else { - BackwardBufferSlice.PartialSlice(filteredBufferSlice) - } - } - - /** Lookup the accepted transaction update by transaction id. */ - def lookupTransaction( - updateId: String - ): Option[TransactionLogUpdate.TransactionAccepted] = - _lookupMap.get(updateId).collect { case tx: TransactionLogUpdate.TransactionAccepted => tx } - - /** Lookup the accepted transaction log update by the lookup key. */ - def lookup( - lookupKey: LookupKey - ): Option[TransactionLogUpdate] = lookupKey match { - case LookupKey.ByUpdateId(updateId) => lookup(updateId.toHexString) - case LookupKey.ByOffset(offset) => lookup(offset) - } - - /** Lookup the accepted transaction log update by update id. */ - private def lookup( - updateId: String - ): Option[TransactionLogUpdate] = _lookupMap.get(updateId) - - /** Lookup the accepted transaction log update by update offset. */ - private def lookup( - offset: Offset - ): Option[TransactionLogUpdate] = { - val vectorSnapshot = _bufferLog - - val searchResult = vectorSnapshot.view.map(_._1).search(offset) - - searchResult match { - case Found(idx) => Some(vectorSnapshot(idx)._2) - case _ => None - } - } - - /** Removes entries starting from the buffer head up until `endInclusive`. - * - * @param endInclusive - * The last inclusive (highest) buffer offset to be pruned. - */ - def prune(endInclusive: Offset): Unit = - Timed.value( - pruneTimer, - (lock.exclusive { - val dropCount = _bufferLog.view.map(_._1).search(endInclusive) match { - case Found(foundIndex) => foundIndex + 1 - case InsertionPoint(insertionPoint) => insertionPoint - } - - dropOldest(dropCount) - }), - ) - - /** Remove all buffered entries */ - def flush(): Unit = (lock.exclusive { - _bufferLog = Vector.empty - _lookupMap = Map.empty - }) - - private def ensureSize(targetSize: Int)(implicit traceContext: TraceContext): Unit = ( - lock.exclusive { - val currentBufferLogSize = _bufferLog.size - val currentLookupMapSize = _lookupMap.size - - if (currentLookupMapSize <= currentBufferLogSize) { - bufferSizeHistogram.update(currentBufferLogSize)(MetricsContext.Empty) - - if (currentBufferLogSize > targetSize) { - dropOldest(dropCount = currentBufferLogSize - targetSize) - } - } else { - // This is an error condition. If encountered, clear the in-memory fan-out buffers. - logger - .error( - s"In-memory fan-out lookup map size ($currentLookupMapSize) exceeds the buffer log size ($currentBufferLogSize). Clearing in-memory fan-out.." - ) - - flush() - } - } - ) - - private def dropOldest(dropCount: Int): Unit = (lock.exclusive { - val (evicted, remainingBufferLog) = _bufferLog.splitAt(dropCount) - val lookupKeysToEvict: View[String] = - evicted.view.map(_._2).flatMap(extractEntryFromMap).map(_._1) - - _bufferLog = remainingBufferLog - _lookupMap = _lookupMap -- lookupKeysToEvict - }) - - private def extractEntryFromMap( - transactionLogUpdate: TransactionLogUpdate - ): Option[(String, TransactionLogUpdate)] = - transactionLogUpdate match { - case txAccepted: TransactionLogUpdate.TransactionAccepted => - Some(txAccepted.updateId -> txAccepted) - case reassignment: TransactionLogUpdate.ReassignmentAccepted => - Some(reassignment.updateId -> reassignment) - case topologyTx: TransactionLogUpdate.TopologyTransactionEffective => - Some(topologyTx.updateId -> topologyTx) - case _: TransactionLogUpdate.TransactionRejected => None - } - -} - -private[platform] object InMemoryFanoutBuffer { - - /** Specialized slice representation of a Vector */ - private[platform] sealed trait BufferSlice[+Elem] extends Product with Serializable { - def slice: Vector[Elem] - } - - object BufferSlice { - - /** A slice of a vector that is inclusive (start index of the slice in the source vector is gteq - * to 1) - */ - private[platform] final case class Inclusive[Elem](slice: Vector[Elem]) - extends BufferSlice[Elem] - - /** A slice of a vector that is a suffix of the requested window (i.e. start index of the slice - * in the source vector is 0) - */ - private[platform] final case class LastBufferChunkSuffix[Elem]( - bufferedStartExclusive: Offset, - slice: Vector[Elem], - ) extends BufferSlice[Elem] - } - - private[platform] sealed trait BackwardBufferSlice[+T] { - def slice: Vector[T] - } - - private[platform] object BackwardBufferSlice { - - /** The returned slice does not cover the whole requested range, another request is needed. If - * slice.isEmpty, switch to persistence - */ - final case class PartialSlice[+T](slice: Vector[T]) extends BackwardBufferSlice[T] - - /** The returned slice covers the whole requested range. No further actions needed. */ - final case class FinalSlice[+T](slice: Vector[T]) extends BackwardBufferSlice[T] - } - - private[cache] final case class UnorderedException[O](first: O, second: O) - extends RuntimeException( - s"Elements appended to the buffer should have strictly increasing offsets: $first vs $second" - ) - - private[cache] def indexAt(bufferStartInclusiveSearchResult: SearchResult): Int = - bufferStartInclusiveSearchResult match { - case InsertionPoint(insertionPoint) => insertionPoint - case Found(foundIndex) => foundIndex - } - - private[cache] def indexAfter(bufferEndInclusiveSearchResult: SearchResult): Int = - bufferEndInclusiveSearchResult match { - case InsertionPoint(insertionPoint) => insertionPoint - case Found(foundIndex) => foundIndex + 1 - } - - private[cache] def filterAndChunkSlice[FilterResult]( - sliceView: View[(Offset, TransactionLogUpdate)], - filter: TransactionLogUpdate => Option[FilterResult], - maxChunkSize: Int, - ): Vector[(Offset, FilterResult)] = - sliceView - .flatMap { case (offset, entry) => filter(entry).map(offset -> _) } - .take(maxChunkSize) - .toVector - - @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) - private[cache] def lastFilteredChunk[FilterResult]( - bufferSlice: Vector[(Offset, TransactionLogUpdate)], - filter: TransactionLogUpdate => Option[FilterResult], - maxChunkSize: Int, - ): BufferSlice.LastBufferChunkSuffix[(Offset, FilterResult)] = { - val lastChunk = - filterAndChunkSlice(bufferSlice.view.reverse, filter, maxChunkSize + 1).reverse - - if (lastChunk.isEmpty) - BufferSlice.LastBufferChunkSuffix(bufferSlice.head._1, Vector.empty) - else { - // We waste the first element so we can pass it as the bufferStartExclusive - BufferSlice.LastBufferChunkSuffix(lastChunk.head._1, lastChunk.tail) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/LedgerEndCache.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/LedgerEndCache.scala deleted file mode 100644 index 605cd657aa..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/LedgerEndCache.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd - -import java.util.concurrent.atomic.AtomicReference - -trait LedgerEndCache { - def apply(): Option[LedgerEnd] -} - -trait MutableLedgerEndCache extends LedgerEndCache { - def set(ledgerEnd: Option[LedgerEnd]): Unit -} - -object MutableLedgerEndCache { - def apply(): MutableLedgerEndCache = - new MutableLedgerEndCache { - private val ledgerEnd: AtomicReference[Option[LedgerEnd]] = - new AtomicReference[Option[LedgerEnd]](None) - - override def set(ledgerEnd: Option[LedgerEnd]): Unit = - this.ledgerEnd.set(ledgerEnd) - - override def apply(): Option[LedgerEnd] = - ledgerEnd.get() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStore.scala deleted file mode 100644 index 7c50bf9f91..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStore.scala +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.digitalasset.canton.ledger.participant.state.index -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.ExistingContractStatus -import com.digitalasset.canton.ledger.participant.state.index.{ - ContractKeyPage, - ContractState, - ContractStateStatus, - ContractStore, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.cache.ContractKeyStateValue.* -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader.KeyState -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.transaction.GlobalKey - -import scala.concurrent.{ExecutionContext, Future} - -private[platform] class MutableCacheBackedContractStore( - contractsReader: LedgerDaoContractsReader, - val loggerFactory: NamedLoggerFactory, - private[cache] val contractStateCaches: ContractStateCaches, - contractStore: LedgerApiContractStore, - ledgerEndCache: LedgerEndCache, - maxLookupLimit: Int, -)(implicit executionContext: ExecutionContext) - extends ContractStore - with NamedLogging { - - override def lookupActiveContract(readers: Set[Party], contractId: ContractId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[FatContract]] = - lookupContractState(contractId) - .map(contractStateToFatContract(readers)) - - override def lookupContractState( - contractId: ContractId - )(implicit loggingContext: LoggingContextWithTrace): Future[ContractState] = - contractStateCaches.contractState - .get(contractId) - .map(Future.successful) - .getOrElse(readThroughContractsCache(contractId)) - .flatMap { - case ContractStateStatus.Active => - contractStore - .lookupPersisted(contractId) - .map { - case Some(persistedContract) => ContractState.Active(persistedContract.inst) - case None => - // TODO(i29574): potentially lower log level it this would become a possible cause - logger.error( - s"Contract $contractId marked as active in index (db or cache) but not found in participant's contract store" - ) - ContractState.NotFound - } - case ContractStateStatus.Archived => Future.successful(ContractState.Archived) - case ContractStateStatus.NotFound => Future.successful(ContractState.NotFound) - } - - override def lookupContractKey(readers: Set[Party], key: GlobalKey)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ContractId]] = - contractStateCaches.keyState - .get(key) - .map(Future.successful) - .getOrElse(readThroughKeyCache(key)) - .flatMap(keyStateToResponse(_, readers)) - - override def lookupNonUniqueContractKey( - readers: Set[Ref.Party], - key: GlobalKey, - pageToken: Option[Long], - limit: Int, - )(implicit loggingContext: LoggingContextWithTrace): Future[ContractKeyPage] = { - val cappedLimit = if (limit > maxLookupLimit) { - logger.info( - s"Lookup limit $limit exceeds configured cap of $maxLookupLimit, using cap instead" - ) - maxLookupLimit - } else limit - for { - (contractIds, nextPageToken) <- contractsReader.lookupNonUniqueKey( - key = key, - notEarlierThanEventSeqId = ledgerEndCache().map(_.lastEventSeqId).getOrElse(0L), - nextPageToken = pageToken, - limit = cappedLimit, - ) - contracts <- Future.sequence(contractIds.map(contractStore.lookupPersisted)) - filteredContracts = contracts.view.flatten.map(_.inst).filter(visibleFor(readers)).toVector - } yield ContractKeyPage( - contracts = filteredContracts, - nextPageToken = nextPageToken, - ) - } - - private def readThroughContractsCache(contractId: ContractId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[ContractStateStatus] = - contractStateCaches.contractState - .putAsync( - contractId, - contractsReader.lookupContractState(contractId, _).map(toContractCacheValue), - ) - - private def keyStateToResponse( - value: ContractKeyStateValue, - readers: Set[Party], - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[ContractId]] = value match { - case Assigned(contractId) => - lookupContractState(contractId).map( - contractStateToFatContract(readers)(_).map(_.contractId) - ) - - case _: Assigned | Unassigned => Future.successful(None) - } - - private def contractStateToFatContract(readers: Set[Party])( - value: index.ContractState - ): Option[FatContract] = - value.toContractOption.filter(visibleFor(readers)) - - private def visibleFor(readers: Set[Party])( - contract: FatContract - ): Boolean = contract.stakeholders.view.exists(readers) - - private val toContractCacheValue: Option[ExistingContractStatus] => ContractStateStatus = - _.getOrElse(ContractStateStatus.NotFound) - - private val toKeyCacheValue: KeyState => ContractKeyStateValue = { - case LedgerDaoContractsReader.KeyAssigned(contractId) => - Assigned(contractId) - case LedgerDaoContractsReader.KeyUnassigned => - Unassigned - } - - private def readThroughKeyCache( - key: GlobalKey - )(implicit loggingContext: LoggingContextWithTrace): Future[ContractKeyStateValue] = - // Even if we have a contract id, we do not automatically trigger the loading of the contract here. - // For prefetching at the start of command interpretation, this is done explicitly there. - // For contract key lookups during interpretation, there is no benefit in doing so, - // because contract prefetching blocks in the current architecture. - // So when Daml engine does not need the contract after all, we avoid loading it. - // Conversely, when Daml engine does need the contract, then this will trigger the loading of the contract - // with the same parallelization and batching opportunities. - contractStateCaches.keyState - .putAsync( - key, - contractsReader.lookupKeyState(key, _).map(toKeyCacheValue), - ) -} - -private[platform] object MutableCacheBackedContractStore { - type EventSequentialId = Long -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/OffsetCheckpointCache.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/OffsetCheckpointCache.scala deleted file mode 100644 index 2c2a4854f3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/OffsetCheckpointCache.scala +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.daml.ledger.api.v2.offset_checkpoint as v2 -import com.daml.ledger.api.v2.offset_checkpoint.SynchronizerTime -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.util.TimestampConversion.fromInstant -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Time.Timestamp - -import java.util.concurrent.atomic.AtomicReference - -class OffsetCheckpointCache { - private val offsetCheckpoint: AtomicReference[Option[OffsetCheckpoint]] = - new AtomicReference(None) - - def push(newOffsetCheckpoint: OffsetCheckpoint): Unit = - offsetCheckpoint.set(Some(newOffsetCheckpoint)) - - def getOffsetCheckpoint: Option[OffsetCheckpoint] = offsetCheckpoint.get() - -} - -final case class OffsetCheckpoint( - offset: Offset, - synchronizerTimes: Map[SynchronizerId, Timestamp], -) { - lazy val toApi: v2.OffsetCheckpoint = - v2.OffsetCheckpoint( - offset = offset.unwrap, - synchronizerTimes = synchronizerTimes.map { case (synchronizer, t) => - SynchronizerTime(synchronizer.toProtoPrimitive, Some(fromInstant(t.toInstant))) - }.toSeq, - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/OnlyForTestingTransactionInMemoryStore.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/OnlyForTestingTransactionInMemoryStore.scala deleted file mode 100644 index 7a006eac93..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/OnlyForTestingTransactionInMemoryStore.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.daml.scalautil.Statement.discard -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.protocol.LfVersionedTransaction -import com.digitalasset.canton.util.Mutex - -import scala.collection.mutable - -// WARNING this is only intended to used by testing -class OnlyForTestingTransactionInMemoryStore(override val loggerFactory: NamedLoggerFactory) - extends NamedLogging { - - private val store: mutable.Map[String, LfVersionedTransaction] = mutable.Map() - private val lock = new Mutex() - - def put(updateId: String, lfVersionedTransaction: LfVersionedTransaction): Unit = - ( - lock.exclusive( - // Prevent massive accumulation, and also WARN heavily if potential abuse is detected - if (store.sizeIs > 100) { - noTracingLogger.warn( - "OnlyForTestingTransactionInMemoryStore is being used, and accumulated 100 transactions. Please turn off testing configuration only-for-testing-enable-in-memory-transaction-store." - ) - } else { - discard( - store += updateId -> lfVersionedTransaction - ) - } - ) - ) - - def get(updateId: String): Option[LfVersionedTransaction] = - lock.exclusive( - store.get(updateId) - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/StateCache.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/StateCache.scala deleted file mode 100644 index c7fcc83b05..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/StateCache.scala +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.daml.metrics.Timed -import com.daml.metrics.api.MetricHandle.Timer -import com.digitalasset.canton.caching.Cache -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.store.cache.StateCache.PendingUpdatesState -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.{ErrorUtil, Mutex} - -import scala.collection.mutable -import scala.concurrent.{ExecutionContext, Future} - -/** This class is a wrapper around a Caffeine cache designed to handle correct resolution of - * concurrent updates for the same key. - * - * The [[StateCache]] tracks its own notion of logical time with the `cacheIndex` which evolves - * monotonically based on the index DB's offset (updated by [[putBatch]]). - * - * The cache's logical time (i.e. the `cacheIndex`) is used for establishing precedence of cache - * updates stemming from read-throughs triggered from command interpretation on cache misses. - */ -@SuppressWarnings(Array("org.wartremover.warts.FinalCaseClass")) // This class is mocked in tests -private[platform] case class StateCache[K, V]( - initialCacheEventSeqIdIndex: Long, - emptyLedgerState: V, - cache: Cache[K, V], - registerUpdateTimer: Timer, - loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext) - extends NamedLogging { - private[cache] val pendingUpdates = mutable.Map.empty[K, PendingUpdatesState] - @SuppressWarnings(Array("org.wartremover.warts.Var")) - @volatile private[cache] var cacheEventSeqIdIndex = initialCacheEventSeqIdIndex - private val lock = new Mutex() - - /** Fetch the corresponding value for an input key, if present. - * - * @param key - * the key to query for - * @return - * optionally [[V]] - */ - def get(key: K)(implicit traceContext: TraceContext): Option[V] = - cache.getIfPresent(key) match { - case Some(value) => - logger.debug(s"Cache hit for $key -> ${truncateValueForLogging(value)}") - Some(value) - case None => - logger.debug(s"Cache miss for $key ") - None - } - - /** Synchronous cache updates evolve the cache ahead with the most recent Index DB entries. This - * method increases the `cacheIndex` monotonically. - * - * @param validAtEventSeqId - * ordering discriminator for pending updates for the same key - * @param batch - * the batch of events updating the cache at `validAt` - */ - def putBatch(validAtEventSeqId: Long, batch: Map[K, V])(implicit - traceContext: TraceContext - ): Unit = - Timed.value( - registerUpdateTimer, - (lock.exclusive { - // The mutable contract state cache update stream should generally increase the cacheIndex strictly monotonically. - // However, the most recent updates can be replayed in case of failure of the mutable contract state cache update stream. - // In this case, we must ignore the already seen updates (i.e. that have `validAt` before or at the cacheIndex). - if (validAtEventSeqId > cacheEventSeqIdIndex) { - batch.keySet.foreach { key => - pendingUpdates.updateWith(key)(_.map(_.withValidAt(validAtEventSeqId))).discard - } - cacheEventSeqIdIndex = validAtEventSeqId - cache.putAll(batch) - logger.debug( - s"Updated cache with a batch of ${batch - .map { case (k, v) => s"$k -> ${truncateValueForLogging(v)}" } - .mkString("[", ", ", "]")} at $validAtEventSeqId" - ) - } else - logger.warn( - s"Ignoring incoming synchronous update at an index at event sequential ID($validAtEventSeqId) equal to or before the cache index ($cacheEventSeqIdIndex)" - ) - }), - ) - - /** Update the cache asynchronously. - * - * In face of multiple in-flight updates competing for the `key`, this method registers an async - * update to the cache only if the to-be-inserted tuple is the most recent (i.e. it has `validAt` - * highest amongst the competing updates). - * - * @param key - * the key at which to update the cache - * @param fetchAsync - * fetches asynchronously the value for key `key` at the current cache index - */ - @SuppressWarnings(Array("com.digitalasset.canton.SynchronizedFuture")) - def putAsync(key: K, fetchAsync: Long => Future[V])(implicit - traceContext: TraceContext - ): Future[V] = - Timed.value( - registerUpdateTimer, - (lock.exclusive { - // dereferencing the mutable var to a val here is critical as it will be used asynchronously below - val validAt = cacheEventSeqIdIndex - val eventualValue = Future.delegate(fetchAsync(validAt)) - pendingUpdates.get(key) match { - case Some(freshPendingUpdate) if freshPendingUpdate.latestValidAt == validAt => - eventualValue - - case Some(freshPendingUpdate) if freshPendingUpdate.latestValidAt > validAt => - ErrorUtil.invalidState( - s"Pending update ($freshPendingUpdate) should never be later than the cacheIndex ($validAt)." - ) - - case outdatedOrNew => - pendingUpdates - .put( - key, - PendingUpdatesState( - outdatedOrNew.map(_.pendingCount).getOrElse(0L) + 1L, - validAt, - ), - ) - .discard - registerEventualCacheUpdate(key, eventualValue, validAt) - .flatMap(_ => eventualValue) - } - }), - ) - - /** Resets the cache and cancels are pending asynchronous updates. - * - * @param resetAtEventSeqId - * The cache re-initialization event sequential ID - */ - def reset(resetAtEventSeqId: Long): Unit = - (lock.exclusive { - cacheEventSeqIdIndex = resetAtEventSeqId - pendingUpdates.clear() - cache.invalidateAll() - }) - - private def registerEventualCacheUpdate( - key: K, - eventualUpdate: Future[V], - validAtEventSeqId: Long, - )(implicit traceContext: TraceContext): Future[Unit] = - eventualUpdate - .map { (value: V) => - Timed.value( - registerUpdateTimer, - (lock.exclusive { - pendingUpdates.get(key) match { - case Some(pendingForKey) => - // Only update the cache if the current update is targeting the cacheIndex - // sampled when initially dispatched in `putAsync`. - // Otherwise we can assume that a more recent `putAsync` has an update in-flight - // or that the entry has been updated synchronously with `put` with a recent Index DB entry. - if (pendingForKey.latestValidAt == validAtEventSeqId) { - cache.put(key, value) - logger.debug( - s"Updated cache for $key with ${truncateValueForLogging(value)} at $validAtEventSeqId" - ) - } - removeFromPending(key) - case None => - logger.warn( - s"Pending updates tracker for $key not registered. This could be due to a transient error causing a restart in the index service." - ) - } - }), - ) - } - .recover { case err => - lock.exclusive( - removeFromPending(key) - ) - - logger.info(s"Failure in pending cache update for key $key", err) - } - - private def removeFromPending(key: K)(implicit traceContext: TraceContext): Unit = - pendingUpdates - .updateWith(key) { - case Some(stillPending) if stillPending.pendingCount > 1 => - Some(stillPending.decPendingCount) - - case Some(lastPending) => - None - - case None => - logger.error(s"Expected pending updates tracker for key $key is missing") - None - } - .discard - - private def truncateValueForLogging(value: V) = { - val stringValueRepr = value.toString - val maxValueLength = 250 - if (stringValueRepr.length > maxValueLength) - stringValueRepr.take(maxValueLength) + "..." - else stringValueRepr - } -} - -object StateCache { - - /** Used to track competing updates to the cache for a specific key. - * @param pendingCount - * The number of in-progress updates. - * @param latestValidAt - * Highest version of any pending update. - */ - private[cache] final case class PendingUpdatesState( - pendingCount: Long, - latestValidAt: Long, - ) { - def withValidAt(validAt: Long): PendingUpdatesState = - this.copy(latestValidAt = validAt) - def decPendingCount: PendingUpdatesState = this.copy(pendingCount = pendingCount - 1) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/package.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/package.scala deleted file mode 100644 index 689da1e6de..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/cache/package.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.digitalasset.canton.protocol.LfFatContractInst - -package object cache { - import com.digitalasset.daml.lf.value.Value as lfval - private[cache] type ContractId = lfval.ContractId - private[cache] val ContractId = com.digitalasset.daml.lf.value.Value.ContractId - private[cache] type Value = lfval.VersionedValue - - import com.digitalasset.daml.lf.transaction as lftx - private[cache] type FatContract = LfFatContractInst - private[cache] type Key = lftx.GlobalKey - - import com.digitalasset.daml.lf.data as lfdata - private[cache] type Party = lfdata.Ref.Party - private[cache] val Party = lfdata.Ref.Party - private[cache] type Identifier = lfdata.Ref.Identifier - private[cache] val Identifier = lfdata.Ref.Identifier -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedCommandCompletionsReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedCommandCompletionsReader.scala deleted file mode 100644 index b31a22e4ca..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedCommandCompletionsReader.scala +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer -import com.digitalasset.canton.platform.store.dao.BufferedCommandCompletionsReader.CompletionsFilter -import com.digitalasset.canton.platform.store.dao.BufferedStreamsReader.FetchFromPersistence -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.platform.{Party, UserId} -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.{ExecutionContext, Future} - -class BufferedCommandCompletionsReader( - bufferReader: BufferedStreamsReader[CompletionsFilter, CompletionStreamResponse] -) extends LedgerDaoCommandCompletionsReader { - - override def getCommandCompletions( - startInclusive: Offset, - endInclusive: Offset, - userId: UserId, - parties: Set[Party], - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, CompletionStreamResponse), NotUsed] = - bufferReader - .stream( - startInclusive = startInclusive, - endInclusive = endInclusive, - persistenceFetchArgs = userId -> parties, - bufferFilter = filterCompletions(_, parties, userId), - toApiResponse = (response: CompletionStreamResponse) => Future.successful(response), - descendingOrder = false, - skipPruningChecks = false, - ) - - private def filterCompletions( - transactionLogUpdate: TransactionLogUpdate, - parties: Set[Party], - userId: String, - ): Option[CompletionStreamResponse] = (transactionLogUpdate match { - case accepted: TransactionLogUpdate.TransactionAccepted => accepted.completionStreamResponseO - case rejected: TransactionLogUpdate.TransactionRejected => - Some(rejected.completionStreamResponse) - case u: TransactionLogUpdate.ReassignmentAccepted => u.completionStreamResponseO - case _: TransactionLogUpdate.TopologyTransactionEffective => None - }).flatMap(toApiCompletion(_, parties, userId)) - - private def toApiCompletion( - completionStreamResponse: CompletionStreamResponse, - parties: Set[Party], - userId: String, - ): Option[CompletionStreamResponse] = { - val completion = { - val originalCompletion = completionStreamResponse.completionResponse.completion - .getOrElse(throw new RuntimeException("No completion in completion stream response")) - originalCompletion.withActAs(originalCompletion.actAs.filter(parties.map(_.toString))) - } - - val visibilityPredicate = - completion.userId == userId && - completion.actAs.nonEmpty - - Option.when(visibilityPredicate)( - CompletionStreamResponse.defaultInstance.withCompletion(completion) - ) - } -} - -object BufferedCommandCompletionsReader { - private[dao] type Parties = Set[Party] - private[dao] type CompletionsFilter = (UserId, Parties) - - def apply( - delegate: LedgerDaoCommandCompletionsReader, - inMemoryFanoutBuffer: InMemoryFanoutBuffer, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - )(implicit ec: ExecutionContext): BufferedCommandCompletionsReader = { - val fetchCompletions = new FetchFromPersistence[CompletionsFilter, CompletionStreamResponse] { - override def apply( - startInclusive: Offset, - endInclusive: Offset, - descendingOrder: Boolean, - filter: (UserId, Parties), - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, CompletionStreamResponse), NotUsed] = { - require(!descendingOrder, s"This flow cannot use descending order") - require(!skipPruningChecks, s"This flow cannot use skipping pruning checks") - val (userId, parties) = filter - delegate - .getCommandCompletions( - startInclusive, - endInclusive, - userId, - parties, - ) - } - } - - new BufferedCommandCompletionsReader( - bufferReader = new BufferedStreamsReader[CompletionsFilter, CompletionStreamResponse]( - inMemoryFanoutBuffer = inMemoryFanoutBuffer, - fetchFromPersistence = fetchCompletions, - // Processing for completions is a no-op so it is unnecessary to have configurable parallelism. - bufferedStreamEventsProcessingParallelism = 1, - metrics = metrics, - streamName = "completions", - loggerFactory, - ) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedStreamsReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedStreamsReader.scala deleted file mode 100644 index cf31abef0e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedStreamsReader.scala +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import cats.syntax.option.* -import com.daml.metrics.Timed -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer.{ - BackwardBufferSlice, - BufferSlice, -} -import com.digitalasset.canton.platform.store.dao.BufferedStreamsReader.FetchFromPersistence -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.{ExecutionContext, Future} - -/** Generic class that helps serving Ledger API streams (e.g. transactions, completions) from either - * the in-memory fan-out buffer or from persistence depending on the requested offset range. - * - * @param inMemoryFanoutBuffer - * The in-memory fan-out buffer. - * @param fetchFromPersistence - * Fetch stream events from persistence. - * @param bufferedStreamEventsProcessingParallelism - * The processing parallelism for buffered elements payloads to API responses. - * @param metrics - * Daml metrics. - * @param streamName - * The name of a Ledger API stream. Used as a discriminator in metric registry names - * construction. - * @param executionContext - * The execution context - * @tparam PersistenceFetchArgs - * The Ledger API streams filter type of fetches from persistence. - * @tparam ApiResponse - * The API stream response type. - */ -class BufferedStreamsReader[PersistenceFetchArgs, ApiResponse]( - inMemoryFanoutBuffer: InMemoryFanoutBuffer, - fetchFromPersistence: FetchFromPersistence[PersistenceFetchArgs, ApiResponse], - bufferedStreamEventsProcessingParallelism: Int, - metrics: LedgerApiServerMetrics, - streamName: String, - override protected val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends NamedLogging { - - private val directEc = DirectExecutionContext(noTracingLogger) - - private val bufferReaderMetrics = metrics.services.index.BufferedReader(streamName) - - /** Serves processed and filtered events from the buffer, with fallback to persistence fetches if - * the bounds are not within the buffer range bounds. - * - * @param startInclusive - * The start inclusive offset of the search range. - * @param endInclusive - * The end inclusive offset of the search range. - * @param persistenceFetchArgs - * The filter used for fetching the Ledger API stream responses from persistence. - * @param bufferFilter - * The filter used for filtering when searching within the buffer. - * @param toApiResponse - * To Ledger API stream response converter. - * @param loggingContext - * The logging context. - * @param descendingOrder - * If true then events will be streamed from the most recent ones to the oldest. - * @tparam BufferOut - * The output type of elements retrieved from the buffer. - * @return - * The Ledger API stream source. - */ - def stream[BufferOut]( - startInclusive: Offset, - endInclusive: Offset, - persistenceFetchArgs: PersistenceFetchArgs, - bufferFilter: TransactionLogUpdate => Option[BufferOut], - toApiResponse: BufferOut => Future[ApiResponse], - descendingOrder: Boolean, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, ApiResponse), NotUsed] = { - def toApiResponseStream( - slice: Vector[(Offset, BufferOut)] - ): Source[(Offset, ApiResponse), NotUsed] = - if (slice.isEmpty) Source.empty - else - Source(slice) - .mapAsync(bufferedStreamEventsProcessingParallelism) { case (offset, payload) => - bufferReaderMetrics.fetchedBuffered.inc() - Timed.future( - bufferReaderMetrics.conversion, - Future.delegate { - toApiResponse(payload).map(offset -> _)(directEc) - }, - ) - } - - val source = if (descendingOrder) { - Source - .unfoldAsync(endInclusive.some) { - case Some(end) if startInclusive <= end => - Future { - val bufferSlice = Timed.value( - bufferReaderMetrics.slice, - inMemoryFanoutBuffer.sliceBackwards( - startInclusive = startInclusive, - endInclusive = end, - filter = bufferFilter, - ), - ) - - Some(bufferSlice match { - case BackwardBufferSlice.FinalSlice(slice) => - (None, toApiResponseStream(slice)) - case BackwardBufferSlice.PartialSlice(slice @ _ :+ last) => - (last._1.decrement, toApiResponseStream(slice)) - case BackwardBufferSlice.PartialSlice(_) => // empty vector - ( - None, - fetchFromPersistence( - startInclusive = startInclusive, - endInclusive = end, - filter = persistenceFetchArgs, - descendingOrder = true, - skipPruningChecks = skipPruningChecks, - ), - ) - }) - } - case _ => Future.successful(None) - } - .flatten - } else { - Source - .unfoldAsync(startInclusive) { - case scanFrom if scanFrom <= endInclusive => - Future { - val bufferSlice = Timed.value( - bufferReaderMetrics.slice, - inMemoryFanoutBuffer.slice( - startInclusive = scanFrom, - endInclusive = endInclusive, - filter = bufferFilter, - ), - ) - - bufferReaderMetrics.sliceSize.update(bufferSlice.slice.size)(MetricsContext.Empty) - bufferSlice match { - case BufferSlice.Inclusive(slice) => - val apiResponseSource = toApiResponseStream(slice) - val nextSliceStart = - slice.lastOption.map(_._1).getOrElse(endInclusive).increment - Some(nextSliceStart -> apiResponseSource) - - case BufferSlice.LastBufferChunkSuffix(bufferedStartExclusive, slice) => - val sourceFromBuffer = - fetchFromPersistence( - startInclusive = scanFrom, - endInclusive = bufferedStartExclusive, - filter = persistenceFetchArgs, - descendingOrder = false, - skipPruningChecks = skipPruningChecks, - )(loggingContext) - .concat(toApiResponseStream(slice)) - Some(endInclusive.increment -> sourceFromBuffer) - } - } - case _ => Future.successful(None) - } - .flatMapConcat(identity) - } - - Timed - .source(bufferReaderMetrics.fetchTimer, source) - .map { tx => - bufferReaderMetrics.fetchedTotal.inc() - tx - } - } -} - -private[platform] object BufferedStreamsReader { - trait FetchFromPersistence[FILTER, ApiResponse] { - def apply( - startInclusive: Offset, - endInclusive: Offset, - descendingOrder: Boolean, - filter: FILTER, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, ApiResponse), NotUsed] - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedUpdatePointwiseReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedUpdatePointwiseReader.scala deleted file mode 100644 index 27afb08994..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/BufferedUpdatePointwiseReader.scala +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.store.dao.BufferedUpdatePointwiseReader.{ - FetchUpdatePointwiseFromPersistence, - ToApiResponse, -} -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate - -import scala.concurrent.Future - -/** Generic class that helps serving Ledger API point-wise lookups (UpdateService.{GetUpdateById, - * GetUpdateByOffset}) from either the in-memory fan-out buffer or from persistence. - * - * @param fetchFromPersistence - * Fetch an update by offset or id from persistence. - * @param toApiResponse - * Convert a [[com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate]] to a - * specific API response while also filtering for visibility. - * @tparam QueryParamType - * The query parameter type. - * @tparam ApiResponse - * The Ledger API response type. - */ -class BufferedUpdatePointwiseReader[QueryParamType, ApiResponse]( - fetchFromPersistence: FetchUpdatePointwiseFromPersistence[QueryParamType, ApiResponse], - fetchFromBuffer: QueryParamType => Option[TransactionLogUpdate], - toApiResponse: ToApiResponse[QueryParamType, ApiResponse], -) { - - /** Serves processed and filtered update from the buffer by the query parameter, with fallback to - * a persistence fetch if the update is not anymore in the buffer (i.e. it was evicted) - * - * @param queryParam - * The query parameter. - * @param loggingContext - * The logging context - * @return - * A future wrapping the API response if found. - */ - def fetch(queryParam: QueryParamType)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ApiResponse]] = - fetchFromBuffer(queryParam) match { - case Some(value) => toApiResponse(value, queryParam, loggingContext) - case None => - fetchFromPersistence(queryParam, loggingContext) - } -} - -object BufferedUpdatePointwiseReader { - trait FetchUpdatePointwiseFromPersistence[QueryParamType, ApiResponse] { - def apply( - queryParam: QueryParamType, - loggingContext: LoggingContextWithTrace, - ): Future[Option[ApiResponse]] - } - - trait ToApiResponse[QueryParamType, ApiResponse] { - def apply( - transactionAccepted: TransactionLogUpdate, - queryParam: QueryParamType, - loggingContext: LoggingContextWithTrace, - ): Future[Option[ApiResponse]] - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/CommandCompletionsReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/CommandCompletionsReader.scala deleted file mode 100644 index ef2711f89c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/CommandCompletionsReader.scala +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.ScalaPbStreamingOptimizations.ScalaPbMessageWithPrecomputedSerializedSize -import com.digitalasset.canton.platform.store.backend.CompletionStorageBackend -import com.digitalasset.canton.platform.store.dao.events.QueryValidRange -import com.digitalasset.canton.platform.{Party, UserId} -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -/** @param pageSize - * a single DB fetch query is guaranteed to fetch no more than this many results. - */ -private[dao] final class CommandCompletionsReader( - dispatcher: DbDispatcher, - storageBackend: CompletionStorageBackend, - queryValidRange: QueryValidRange, - metrics: LedgerApiServerMetrics, - pageSize: Int, - override protected val loggerFactory: NamedLoggerFactory, -) extends LedgerDaoCommandCompletionsReader - with NamedLogging { - - private val paginatingAsyncStream = new PaginatingAsyncStream(loggerFactory) - - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) - private def offsetFor(response: CompletionStreamResponse): Offset = - // It would be nice to obtain the offset such that it's obvious that it always exists (rather then relaying on calling .get) - Offset.tryFromLong(response.completionResponse.completion.get.offset) - - override def getCommandCompletions( - startInclusive: Offset, - endInclusive: Offset, - userId: UserId, - parties: Set[Party], - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, CompletionStreamResponse), NotUsed] = { - val pruneSafeQuery = - (range: QueryRange[Offset]) => - queryValidRange.withRangeNotPruned[Vector[CompletionStreamResponse]]( - minOffsetInclusive = startInclusive, - maxOffsetInclusive = endInclusive, - errorPruning = (prunedOffset: Offset) => - s"Command completions request from ${startInclusive.unwrap} to ${endInclusive.unwrap} overlaps with pruned offset ${prunedOffset.unwrap}", - errorLedgerEnd = (ledgerEndOffset: Option[Offset]) => - s"Command completions request from ${startInclusive.unwrap} to ${endInclusive.unwrap} is beyond ledger end offset ${ledgerEndOffset - .fold(0L)(_.unwrap)}", - ) { - dispatcher.executeSql(metrics.index.db.getCompletions)( - storageBackend.commandCompletions( - startInclusive = range.startInclusive, - endInclusive = range.endInclusive, - userId = userId, - parties = parties, - limit = pageSize, - ) - ) - } - - val initialRange = new QueryRange[Offset]( - startInclusive = startInclusive, - endInclusive = endInclusive, - ) - val source: Source[CompletionStreamResponse, NotUsed] = paginatingAsyncStream - .streamFromSeekPagination[QueryRange[Offset], CompletionStreamResponse]( - startFromOffset = initialRange, - getOffset = (previousCompletion: CompletionStreamResponse) => { - val lastOffset = offsetFor(previousCompletion) - initialRange.copy(startInclusive = lastOffset.increment) - }, - ) { (subRange: QueryRange[Offset]) => - pruneSafeQuery(subRange) - } - source.map(response => offsetFor(response) -> response.withPrecomputedSerializedSize()) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/DatabaseSelfServiceError.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/DatabaseSelfServiceError.scala deleted file mode 100644 index cd4d70e2e6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/DatabaseSelfServiceError.scala +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.ledger.error.IndexErrors -import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.StatusRuntimeException -import org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException -import org.postgresql.util.PSQLException - -import java.sql.* - -/** Wraps SQLExceptions into transient and non-transient errors. Transience classification is done - * as follows: - * - * * Problems that are likely to be resolved with retries are transient errors. For example, - * network outages or db access serialization problems. - * - * * Problems that cannot be recovered from are non-transient. For example, an illegal argument - * exception inside a database transaction or a unique constraint violation. - */ -object DatabaseSelfServiceError { - def apply( - exception: Throwable - )(implicit errorLoggingContext: ErrorLoggingContext): Throwable = exception match { - // This frequently occurs when running with H2, because H2 does not properly implement the serializable - // isolation level. This causes unexpected constraint violation exceptions when running with H2 in the presence - // of contention. For now, we retry on these exceptions. - // See https://github.com/h2database/h2database/issues/2167 - case ex: JdbcSQLIntegrityConstraintViolationException => retryable(ex) - case ex: SQLRecoverableException => retryable(ex) - case ex: SQLTransientException => retryable(ex) - case ex: SQLNonTransientException => nonRetryable(ex) - case ex: PSQLException => if (isRetryablePsqlException(ex)) retryable(ex) else nonRetryable(ex) - case ex: BatchUpdateException if ex.getCause != null => DatabaseSelfServiceError(ex.getCause) - case ex: SQLException => nonRetryable(ex) - // Don't handle other exceptions that can be thrown from non-client interactions (e.g. index initialization) - case ex => ex - } - - private def retryable(ex: SQLException)(implicit - errorLoggingContext: ErrorLoggingContext - ): StatusRuntimeException = - IndexErrors.DatabaseErrors.SqlTransientError.Reject(ex).asGrpcError - - private def nonRetryable(ex: SQLException)(implicit - errorLoggingContext: ErrorLoggingContext - ): StatusRuntimeException = - IndexErrors.DatabaseErrors.SqlNonTransientError.Reject(ex).asGrpcError - - // PostgreSQL exceptions are handled based on the SQLState - // https://www.postgresql.org/docs/11/errcodes-appendix.html - private def isRetryablePsqlException(exception: PSQLException): Boolean = - exception.getSQLState match { - // Class 08 — Connection Exception - case state if state.startsWith("08") => true - // Failure to serialize db accesses, happens due to contention - case "40001" => true - // Retry on read only transaction, which can occur on Azure - case "25006" => true - // Retry on operator intervention errors, but not on `query_canceled` and `database_dropped` - case state if state.startsWith("57P") && state != "57014" && state != "57P04" => true - case _ => false - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/DbDispatcher.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/DbDispatcher.scala deleted file mode 100644 index 26a9fe92dd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/DbDispatcher.scala +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.executors.InstrumentedExecutors -import com.daml.executors.executors.NamedExecutionContextExecutorService -import com.daml.ledger.resources.ResourceOwner -import com.daml.logging.entries.LoggingEntry -import com.daml.metrics.api.MetricHandle.Timer -import com.daml.metrics.api.MetricName -import com.daml.metrics.{DatabaseMetrics, Timed} -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.health.{ComponentHealthState, HealthStatus, ReportsHealth} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.{ - implicitExtractTraceContext, - withEnrichedLoggingContext, -} -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, - TracedLogger, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors.AbortedDueToShutdown -import com.digitalasset.canton.platform.ResourceOwnerOps -import com.digitalasset.canton.platform.config.ServerRole -import com.digitalasset.canton.resource.DbStorage -import com.digitalasset.canton.tracing.TraceContext -import com.google.common.util.concurrent.ThreadFactoryBuilder - -import java.sql.Connection -import java.util.concurrent.TimeUnit -import javax.sql.DataSource -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.control.NonFatal - -private[canton] trait DbDispatcher { - - /** consider using executeSqlUS if possible */ - def executeSql[T](databaseMetrics: DatabaseMetrics)(sql: Connection => T)(implicit - loggingContext: LoggingContextWithTrace - ): Future[T] - - def executeSqlUS[T](databaseMetrics: DatabaseMetrics)( - sql: Connection => T - )(implicit loggingContext: LoggingContextWithTrace): FutureUnlessShutdown[T] -} - -private[dao] final class DbDispatcherImpl private[dao] ( - connectionProvider: JdbcConnectionProvider, - executorService: NamedExecutionContextExecutorService, - overallWaitTimer: Timer, - overallExecutionTimer: Timer, - val loggerFactory: NamedLoggerFactory, -) extends DbDispatcher - with ReportsHealth - with NamedLogging { - - private val executionContext = ExecutionContext.fromExecutor( - executorService, - throwable => - logger.error("ExecutionContext has failed with an exception", throwable)(TraceContext.empty), - ) - - override def currentHealth(): HealthStatus = - connectionProvider.currentHealth() - - /** Runs an SQL statement in a dedicated Executor. The whole block will be run in a single - * database transaction. - * - * The isolation level by default is the one defined in the JDBC driver, it can be however - * overridden per query on the Connection. See further details at: - * https://docs.oracle.com/cd/E19830-01/819-4721/beamv/index.html - */ - def executeSql[T](databaseMetrics: DatabaseMetrics)( - sql: Connection => T - )(implicit loggingContext: LoggingContextWithTrace): Future[T] = - DbDispatcher.withEnrichedLoggingContextAndStartWaitNanos(databaseMetrics) { - implicit loggingContext: LoggingContextWithTrace => startWait => - Future { - connectionProvider.runSQL( - DbDispatcher.executeSql( - databaseMetrics = databaseMetrics, - overallWaitTimer = overallWaitTimer, - overallExecutionTimer = overallExecutionTimer, - logger = logger, - startWaitNanos = startWait, - )(sql) - ) - }(executionContext) - .transform(identity, DbDispatcher.handleJdbcError(logger))(executionContext) - } - - def executeSqlUS[T](databaseMetrics: DatabaseMetrics)( - sql: Connection => T - )(implicit loggingContext: LoggingContextWithTrace): FutureUnlessShutdown[T] = - FutureUnlessShutdown.outcomeF(executeSql(databaseMetrics)(sql))(executionContext) -} - -private[dao] final class DbDispatcherOfStorage( - dbStorage: DbStorage, - overallWaitTimer: Timer, - overallExecutionTimer: Timer, - val loggerFactory: NamedLoggerFactory, -) extends DbDispatcher - with ReportsHealth - with NamedLogging { - private implicit val directEc: ExecutionContext = DirectExecutionContext(noTracingLogger) - - /** consider using executeSqlUS if possible */ - override def executeSql[T](databaseMetrics: DatabaseMetrics)( - sql: Connection => T - )(implicit loggingContext: LoggingContextWithTrace): Future[T] = - executeSqlUS(databaseMetrics)(sql) - .failOnShutdownTo(AbortedDueToShutdown.Error().asGrpcError) - - override def executeSqlUS[T](databaseMetrics: DatabaseMetrics)( - sql: Connection => T - )(implicit loggingContext: LoggingContextWithTrace): FutureUnlessShutdown[T] = - DbDispatcher.withEnrichedLoggingContextAndStartWaitNanos(databaseMetrics) { - implicit loggingContext: LoggingContextWithTrace => startWait => - dbStorage - .runJdbcWrite( - loggingContext.traceContext, - DbDispatcher.executeSql( - databaseMetrics = databaseMetrics, - overallWaitTimer = overallWaitTimer, - overallExecutionTimer = overallExecutionTimer, - logger = logger, - startWaitNanos = startWait, - )(sql), - ) - .transform(identity, DbDispatcher.handleJdbcError(logger)) - } - - /** Reports the current health of the object. This should always return immediately. - */ - override def currentHealth(): HealthStatus = dbStorage.initialHealthState match { - case _: ComponentHealthState.Ok => HealthStatus.healthy - case _: ComponentHealthState.HasUnhealthyState => HealthStatus.unhealthy - } -} - -object DbDispatcher { - - def ofDbStorage( - dbStorage: DbStorage, - overallWaitTimer: Timer, - overallExecutionTimer: Timer, - loggerFactory: NamedLoggerFactory, - ): DbDispatcher with ReportsHealth = - new DbDispatcherOfStorage( - dbStorage = dbStorage, - overallWaitTimer = overallWaitTimer, - overallExecutionTimer = overallExecutionTimer, - loggerFactory = loggerFactory, - ) - - def owner( - dataSource: DataSource, - serverRole: ServerRole, - connectionPoolSize: Int, - connectionTimeout: FiniteDuration, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - ): ResourceOwner[DbDispatcher with ReportsHealth] = { - val logger = loggerFactory.getLogger(getClass) - def log(s: String, info: Boolean = false): Unit = { - val logMessage = s"[${serverRole.threadPoolSuffix}] $s" - if (info) logger.info(logMessage) - else logger.debug(logMessage) - } - - for { - hikariDataSource <- HikariDataSourceOwner( - dataSource = dataSource, - serverRole = serverRole, - minimumIdle = connectionPoolSize, - maxPoolSize = connectionPoolSize, - connectionTimeout = connectionTimeout, - ) - .afterReleased(log("HikariDataSource released")) - .afterReleased(log("DbDispatcher released", info = true)) - connectionProvider <- DataSourceConnectionProvider - .owner( - dataSource = hikariDataSource, - logMarker = serverRole.threadPoolSuffix, - loggerFactory = loggerFactory, - ) - .afterReleased(log("DataSourceConnectionProvider released")) - threadPoolName = MetricName( - metrics.index.db.threadpool.connection, - serverRole.threadPoolSuffix, - ) - executor <- ResourceOwner - .forExecutorService( - () => - InstrumentedExecutors.newFixedThreadPoolWithFactory( - threadPoolName, - connectionPoolSize, - new ThreadFactoryBuilder() - .setNameFormat(s"$threadPoolName-%d") - .setUncaughtExceptionHandler((_, e) => - loggerFactory - .getTracedLogger(getClass) - .error("Uncaught exception in the SQL executor.", e)(TraceContext.empty) - ) - .build(), - ), - gracefulAwaitTerminationMillis = - 5000, // waiting 5s for ongoing SQL operations to finish and then forcing them with Thread.interrupt... - forcefulAwaitTerminationMillis = 5000, // ...and then waiting 5s more - ) - .afterReleased(log("ExecutorService released")) - } yield new DbDispatcherImpl( - connectionProvider = connectionProvider, - executorService = executor, - overallWaitTimer = metrics.index.db.waitAll, - overallExecutionTimer = metrics.index.db.execAll, - loggerFactory = loggerFactory, - ) - } - - private[dao] def executeSql[T]( - databaseMetrics: DatabaseMetrics, - overallWaitTimer: Timer, - overallExecutionTimer: Timer, - logger: TracedLogger, - startWaitNanos: Long, - )(sql: Connection => T)(implicit traceContext: TraceContext): Connection => T = { conn => - val waitNanos = System.nanoTime() - startWaitNanos - logger.trace(s"Waited ${(waitNanos / 1e6).toLong} ms to acquire connection.") - databaseMetrics.waitTimer.update(waitNanos, TimeUnit.NANOSECONDS) - overallWaitTimer.update(waitNanos, TimeUnit.NANOSECONDS) - val startExecNanos = System.nanoTime() - conn.setAutoCommit(false) - - def executeSqlInternal(): T = - try { - val res = Timed.value( - databaseMetrics.queryTimer, - sql(conn), - ) - Timed.value( - databaseMetrics.commitTimer, - conn.commit(), - ) - res - } catch { - case NonFatal(t) => - // Log the error in the caller with access to more logging context (such as the sql statement description) - conn.rollback() - throw t - } finally { - conn.close() - } - - def updateMetrics(): Unit = - try { - val execNanos = System.nanoTime() - startExecNanos - logger.trace(s"Executed query in ${(execNanos / 1e6).toLong} ms") - databaseMetrics.executionTimer.update(execNanos, TimeUnit.NANOSECONDS) - overallExecutionTimer.update(execNanos, TimeUnit.NANOSECONDS) - } catch { - case NonFatal(e) => - logger.info("Got an exception while updating timer metrics. Ignoring.", e) - } - - try { - executeSqlInternal() - } finally { - updateMetrics() - } - } - - private[dao] def withEnrichedLoggingContextAndStartWaitNanos[T]( - databaseMetrics: DatabaseMetrics - )( - block: LoggingContextWithTrace => Long => T - )(implicit loggingContextWithTrace: LoggingContextWithTrace): T = - withEnrichedLoggingContext(("metric" -> databaseMetrics.name): LoggingEntry) { loggingContext => - val startWait = System.nanoTime() - block(loggingContext)(startWait) - } - - private[dao] def handleJdbcError(logger: TracedLogger)( - throwable: Throwable - )(implicit loggingContext: LoggingContextWithTrace): Throwable = { - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContext) - throwable match { - case NonFatal(e) => DatabaseSelfServiceError(e) - // fatal errors don't make it for some reason to the setUncaughtExceptionHandler - case t: Throwable => - logger.error("Fatal error!", t) - t - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/EventProjectionProperties.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/EventProjectionProperties.scala deleted file mode 100644 index bb826fdb88..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/EventProjectionProperties.scala +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import cats.syntax.option.* -import com.digitalasset.canton.ledger.api.{CumulativeFilter, EventFormat} -import com.digitalasset.canton.platform.index.IndexServiceImpl.InterfaceViewPackageUpgrade -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties.Projection -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.* -import com.google.common.annotations.VisibleForTesting - -import scala.collection.View -import scala.concurrent.Future - -/** This class encapsulates the logic of how contract arguments and interface views are being - * projected to the consumer based on the filter criteria and the relation between interfaces and - * templates implementing them. - * - * @param verbose - * enriching in verbose mode - * @param witnessTemplateProjections - * per witness party, per template projections - * @param interfaceViewPackageUpgrade - * computes which interface instance version should be used for rendering an interface view for a - * given interface instance - */ -final case class EventProjectionProperties( - verbose: Boolean, - // Map((witness or wildcard) -> Map(template -> projection)), where a None key denotes a party wildcard - witnessTemplateProjections: Map[Option[String], Map[Option[Ref.NameTypeConRef], Projection]] = - Map.empty, -)( - // Note: including this field in a separate argument list to the case class to not affect the deep equality check of the - // regular argument list - val interfaceViewPackageUpgrade: InterfaceViewPackageUpgrade -) { - def render(witnesses: Set[String], templateId: NameTypeConRef): Projection = { - require(witnesses.nonEmpty) - (witnesses.iterator.map(Some(_)) - ++ Iterator(None)) // for the party-wildcard template specific projections) - .flatMap(witnessTemplateProjections.get(_).iterator) - .flatMap(templateMap => Iterator(Some(templateId), None).flatMap(templateMap.get)) - .foldLeft( - Projection() - )(_ append _) - } -} - -object EventProjectionProperties { - - final case class Projection( - interfaces: Set[FullIdentifier] = Set.empty, - createdEventBlob: Boolean = false, - ) { - def append(other: Projection): Projection = - Projection( - interfaces = interfaces ++ other.interfaces, - createdEventBlob = createdEventBlob || other.createdEventBlob, - ) - } - - /** @param eventFormat - * EventFormat as defined by the consumer of the API. - * @param interfaceImplementedBy - * The relation between an interface id and template id. If template has no relation to the - * interface, an empty Set must be returned. - */ - def apply( - eventFormat: EventFormat, - interfaceImplementedBy: FullIdentifier => Set[FullIdentifier], - resolveTypeConRef: NameTypeConRef => Set[FullIdentifier], - interfaceViewPackageUpgrade: InterfaceViewPackageUpgrade, - ): EventProjectionProperties = - EventProjectionProperties( - verbose = eventFormat.verbose, - witnessTemplateProjections = witnessTemplateProjections( - eventFormat, - interfaceImplementedBy, - resolveTypeConRef, - ), - )( - interfaceViewPackageUpgrade = interfaceViewPackageUpgrade - ) - - @VisibleForTesting - val UseOriginalViewPackageId: InterfaceViewPackageUpgrade = - (_: Ref.ValueRef, originalTemplateImplementation: Ref.ValueRef) => - Future.successful(Right(originalTemplateImplementation)) - - @VisibleForTesting - def apply( - eventFormat: EventFormat, - interfaceImplementedBy: FullIdentifier => Set[FullIdentifier], - resolveTypeConRef: NameTypeConRef => Set[FullIdentifier], - ): EventProjectionProperties = - EventProjectionProperties( - eventFormat = eventFormat, - interfaceImplementedBy = interfaceImplementedBy, - resolveTypeConRef = resolveTypeConRef, - interfaceViewPackageUpgrade = (_: Ref.ValueRef, _: Ref.ValueRef) => - Future.failed( - new UnsupportedOperationException("Not expected to be called in unit tests") - ), - ) - - private def witnessTemplateProjections( - apiEventFormat: EventFormat, - interfaceImplementedBy: FullIdentifier => Set[FullIdentifier], - resolveTypeConRef: NameTypeConRef => Set[FullIdentifier], - ): Map[Option[String], Map[Option[NameTypeConRef], Projection]] = { - val partyFilterPairs = - apiEventFormat.filtersByParty.view.map { case (p, f) => - (Some(p), f) - } ++ - apiEventFormat.filtersForAnyParty.toList.view.map((None, _)) - (for { - (partyO, cumulativeFilter) <- partyFilterPairs - } yield { - val interfaceFilterProjections = for { - interfaceFilter <- cumulativeFilter.interfaceFilters.view - interfaceId <- resolveTypeConRef(interfaceFilter.interfaceTypeRef) - implementor <- interfaceImplementedBy(interfaceId).view - } yield implementor.toNameTypeConRef -> Projection( - interfaces = if (interfaceFilter.includeView) Set(interfaceId) else Set.empty, - createdEventBlob = interfaceFilter.includeCreatedEventBlob, - ) - val templateProjections = getTemplateProjections(cumulativeFilter, resolveTypeConRef) - val wildcardTemplateProjectionsForParty = - if (cumulativeFilter.templateWildcardFilter.exists(_.includeCreatedEventBlob)) - Map(None -> Projection(createdEventBlob = true)) - else Map.empty - val projectionsForParty = - (interfaceFilterProjections ++ templateProjections) - .groupMap(t => t._1.some)(_._2) - .view - .mapValues(_.foldLeft(Projection())(_ append _)) - .toMap - - partyO -> (projectionsForParty ++ wildcardTemplateProjectionsForParty) - }).toMap - } - - private def getTemplateProjections( - cumulativeFilter: CumulativeFilter, - resolveTypeConRef: NameTypeConRef => Set[FullIdentifier], - ): View[(NameTypeConRef, Projection)] = - for { - templateFilter <- cumulativeFilter.templateFilters.view - templateId <- resolveTypeConRef(templateFilter.templateTypeRef).view - } yield templateId.toNameTypeConRef -> Projection( - interfaces = Set.empty, - createdEventBlob = templateFilter.includeCreatedEventBlob, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/HikariJdbcConnectionProvider.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/HikariJdbcConnectionProvider.scala deleted file mode 100644 index 633048098c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/HikariJdbcConnectionProvider.scala +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.resources.ResourceOwner -import com.daml.scalautil.Statement.discard -import com.digitalasset.canton.health.{HealthStatus, Healthy, Unhealthy} -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.config.ServerRole -import com.digitalasset.canton.tracing.TraceContext -import com.zaxxer.hikari.{HikariConfig, HikariDataSource} - -import java.sql.{Connection, SQLTransientConnectionException} -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} -import java.util.{Timer, TimerTask} -import javax.sql.DataSource -import scala.concurrent.duration.{DurationInt, FiniteDuration} -import scala.util.control.NonFatal - -private[platform] object HikariDataSourceOwner { - - def apply( - dataSource: DataSource, - serverRole: ServerRole, - minimumIdle: Int, - maxPoolSize: Int, - connectionTimeout: FiniteDuration, - connectionPoolPrefix: String = "daml.index.db.connection", - ): ResourceOwner[DataSource] = - ResourceOwner.forCloseable { () => - val config = new HikariConfig - config.setDataSource(dataSource) - config.setAutoCommit(false) - config.setMaximumPoolSize(maxPoolSize) - config.setMinimumIdle(minimumIdle) - config.setConnectionTimeout(connectionTimeout.toMillis) - config.setPoolName(s"$connectionPoolPrefix.${serverRole.threadPoolSuffix}") - new HikariDataSource(config) - } -} - -object DataSourceConnectionProvider { - private val MaxTransientFailureCount: Int = 5 - private val HealthPollingSchedule: FiniteDuration = 1.second - - def owner( - dataSource: DataSource, - logMarker: String, - loggerFactory: NamedLoggerFactory, - ): ResourceOwner[JdbcConnectionProvider] = - for { - healthPoller <- ResourceOwner.forTimer( - () => new Timer(s"DataSourceConnectionProvider-$logMarker#healthPoller", true), - waitForRunningTasks = false, // do not stop resource release with ongoing healthcheck - ) - transientFailureCount = new AtomicInteger(0) - checkHealth <- ResourceOwner.forCloseable(() => - new HealthCheckTask( - dataSource = dataSource, - transientFailureCount = transientFailureCount, - logMarker = logMarker, - loggerFactory = loggerFactory, - ) - ) - } yield { - healthPoller.schedule(checkHealth, 0, HealthPollingSchedule.toMillis) - - new JdbcConnectionProvider { - override def runSQL[T](block: Connection => T): T = { - val conn = dataSource.getConnection() - try { - block(conn) - } catch { - case e: SQLTransientConnectionException => - transientFailureCount.incrementAndGet() - throw e - } - } - - override def currentHealth(): HealthStatus = - if (transientFailureCount.get() < MaxTransientFailureCount) - Healthy - else - Unhealthy - } - } -} - -class HealthCheckTask( - dataSource: DataSource, - transientFailureCount: AtomicInteger, - logMarker: String, - val loggerFactory: NamedLoggerFactory, -) extends TimerTask - with AutoCloseable - with NamedLogging { - private val closed = new AtomicBoolean(false) - - private implicit val emptyTraceContext: TraceContext = TraceContext.empty - - private def printProblem(problem: String): Unit = { - val count = transientFailureCount.incrementAndGet() - if (count == 1) { - if (closed.get()) { - logger.debug( - s"$logMarker Hikari connection health check failed after health checking stopped with: $problem" - ) - } else { - logger.info(s"$logMarker Hikari connection health check failed with: $problem") - } - } - } - - override def run(): Unit = - try { - dataSource.getConnection.close() - transientFailureCount.set(0) - } catch { - case e: SQLTransientConnectionException => - printProblem(s"transient connection exception: $e") - case NonFatal(e) => - printProblem(s"unexpected exception: $e") - } - - override def close(): Unit = { - discard(this.cancel()) // this prevents further tasks to execute - closed.set(true) // to emit log on debug level instead of info - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/JdbcConnectionProvider.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/JdbcConnectionProvider.scala deleted file mode 100644 index 36d122289f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/JdbcConnectionProvider.scala +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.health.ReportsHealth - -import java.sql.Connection - -/** A helper to run JDBC queries using a pool of managed connections */ -private[platform] trait JdbcConnectionProvider extends ReportsHealth { - - /** Blocks are running in a single transaction as the commit happens when the connection is - * returned to the pool. The block must not recursively call [[runSQL]], as this could result in - * a deadlock waiting for a free connection from the same pool. - */ - def runSQL[T](block: Connection => T): T -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDao.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDao.scala deleted file mode 100644 index bb8056dfc6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDao.scala +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.health.{HealthStatus, ReportsHealth} -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.error.LedgerApiErrors -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.config.{ - ActiveContractsServiceStreamsConfig, - UpdatesStreamsConfig, -} -import com.digitalasset.canton.platform.store.* -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.PruningContractsBlockedException -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.backend.{ParameterStorageBackend, ReadStorageBackend} -import com.digitalasset.canton.platform.store.cache.{AchsStateCache, LedgerEndCache} -import com.digitalasset.canton.platform.store.dao.events.* -import com.digitalasset.canton.platform.store.utils.QueueBasedConcurrencyLimiter -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.daml.lf.data.Ref -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.actor.Scheduler -import org.apache.pekko.pattern - -import java.util.concurrent.atomic.AtomicBoolean -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -private[platform] class JdbcLedgerDao( - dbDispatcher: DbDispatcher & ReportsHealth, - queryExecutionContext: ExecutionContext, - commandExecutionContext: ExecutionContext, - metrics: LedgerApiServerMetrics, - readStorageBackend: ReadStorageBackend, - parameterStorageBackend: ParameterStorageBackend, - ledgerEndCache: LedgerEndCache, - completionsPageSize: Int, - activeContractsServiceStreamsConfig: ActiveContractsServiceStreamsConfig, - updatesStreamsConfig: UpdatesStreamsConfig, - globalMaxEventIdQueries: Int, - globalMaxEventPayloadQueries: Int, - tracer: Tracer, - val loggerFactory: NamedLoggerFactory, - incompleteOffsets: ( - Offset, - Option[Set[Ref.Party]], - TraceContext, - ) => FutureUnlessShutdown[Vector[Offset]], - contractLoader: ContractLoader, - lfValueTranslation: LfValueTranslation, - contractStore: LedgerApiContractStore, - achsStateCache: AchsStateCache, - contractPruningMaxRetries: Int, - contractPruningDelayBeforeRetry: FiniteDuration, - scheduler: Scheduler, -)(implicit ec: ExecutionContext) - extends LedgerReadDao - with NamedLogging { - - private val pruningInProgress: AtomicBoolean = new AtomicBoolean(false) - - override def isPruningInProgress: Boolean = pruningInProgress.get() - - override def currentHealth(): HealthStatus = dbDispatcher.currentHealth() - - override def lookupParticipantId()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ParticipantId]] = - dbDispatcher - .executeSql(metrics.index.db.getParticipantId)( - parameterStorageBackend.ledgerIdentity(_).map(_.participantId) - ) - - /** Defaults to None if ledger_end is unset - */ - override def lookupLedgerEnd()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[LedgerEnd]] = - dbDispatcher - .executeSql(metrics.index.db.getLedgerEnd)( - parameterStorageBackend.ledgerEnd - ) - - override def getParties( - parties: Seq[Party] - )(implicit loggingContext: LoggingContextWithTrace): Future[List[IndexerPartyDetails]] = - if (parties.isEmpty) - Future.successful(List.empty) - else - dbDispatcher - .executeSql(metrics.index.db.loadParties)( - readStorageBackend.partyStorageBackend.parties(parties) - ) - - override def listKnownParties( - fromExcl: Option[Party], - filterParty: Option[String185], - maxResults: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] = - dbDispatcher - .executeSql(metrics.index.db.loadAllParties)( - readStorageBackend.partyStorageBackend.knownParties(fromExcl, filterParty, maxResults) - ) - - private def ensurePruningIsNotInProgress[T]( - f: => Future[T] - )(implicit loggingContext: LoggingContextWithTrace): Future[T] = - if (!pruningInProgress.getAndSet(true)) { - f.thereafter(_ => pruningInProgress.set(false)) - } else { - Future.failed( - RequestValidationErrors.ParticipantPruningInProgress - .Reject()(errorLoggingContext(loggingContext.traceContext)) - .asGrpcError - ) - } - - /** Prunes the events and command completions tables. - * - * @param pruneUpToInclusive - * Offset up to which to prune archived history inclusively. - */ - override def prune( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusive: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit loggingContext: LoggingContextWithTrace): Future[Unit] = - // The pruning offset cache's disable/re-enable cycle requires that pruning requests - // do not run concurrently. - ensurePruningIsNotInProgress { - logger.info(s"Pruning the ledger api server index db up to ${pruneUpToInclusive.unwrap}.") - - implicit val ec: ExecutionContext = commandExecutionContext - - dbDispatcher - .executeSql(metrics.index.db.pruneDbMetrics) { conn => - // verifying pruning is continuous - val prunedUpToInDb = parameterStorageBackend.prunedUpToInclusive(conn) - if (prunedUpToInDb != previousPruneUpToInclusive) { - throw new IllegalStateException( - s"Previous pruned up to ($previousPruneUpToInclusive) is different from the current pruning offset in DB ($prunedUpToInDb)" - ) - } - - readStorageBackend.eventStorageBackend.pruneEvents( - previousPruneUpToInclusive = previousPruneUpToInclusive, - previousIncompleteReassignmentOffsets = previousIncompleteReassignmentOffsets, - pruneUpToInclusive = pruneUpToInclusive, - incompleteReassignmentOffsets = incompleteReassignmentOffsets, - )( - conn, - loggingContext.traceContext, - ) - - readStorageBackend.completionStorageBackend.pruneCompletions(pruneUpToInclusive)( - conn, - loggingContext.traceContext, - ) - parameterStorageBackend.updatePrunedUptoInclusive( - pruneUpToInclusive - )(conn) - // Disable the cache before the transaction is committed to avoid fetching stale values. - pruningOffsetService.disableCache() - } - .thereafter { _ => - pruningOffsetService.reEnableCache() - } - .thereafter { - case Success(_) => - logger.info( - s"Completed pruning of the ledger api server index db" - ) - case Failure(ex) => - logger.warn("Pruning failed", ex) - } - .flatMap(_ => - pattern.retry( - attempt = () => pruneContracts(), - shouldRetry = (_: Unit, t: Throwable) => - t match { - case _: PruningContractsBlockedException => true - case _ => false - }, - attempts = contractPruningMaxRetries + 1, - delayFunction = attempt => { - // tracking each attempt individually - metrics.services.pruning.contractPruningRetried - .mark(attempt.toLong)(MetricsContext.Empty) - // fix delay between retries - Some(contractPruningDelayBeforeRetry) - }, - )(commandExecutionContext, scheduler) - ) - .transform { - case Success(()) => - logger.info(s"Completed pruning of contracts") - Success(()) - case Failure(_: PruningContractsBlockedException) => - metrics.services.pruning.contractPruningBlocked.inc() - Failure( - LedgerApiErrors.ParticipantContractPruningBlocked - .Reject( - retries = contractPruningMaxRetries, - delay = contractPruningDelayBeforeRetry, - ) - .asGrpcError - ) - case Failure(t) => - logger.warn("Pruning of contracts failed", t) - Failure(t) - } - } - - private def pruneContracts()(implicit loggingContext: LoggingContextWithTrace): Future[Unit] = - for { - _ <- dbDispatcher.executeSql( - metrics.index.db.cleanPruningCandidateContractsDbMetrics - )(implicit conn => readStorageBackend.eventStorageBackend.cleanPruningCandidates()) - prunedInternalContractIds <- dbDispatcher.executeSql( - metrics.index.db.pruneContractsDbMetrics - )(implicit conn => readStorageBackend.eventStorageBackend.pruneContracts()) - } yield contractStore.contractsPruned(prunedInternalContractIds) - - override def indexDbPrunedUpTo(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] = - pruningOffsetService.pruningOffset - - private val pruningOffsetService: PruningOffsetServiceImpl = new PruningOffsetServiceImpl( - fetchFromDb = traceContext => { - dbDispatcher.executeSql(metrics.index.db.fetchPruningOffsetsMetrics)( - parameterStorageBackend.prunedUpToInclusive - )(LoggingContextWithTrace(loggerFactory)(traceContext)) - }, - loggerFactory = loggerFactory, - ) - - private val queryValidRange = QueryValidRangeImpl( - ledgerEndCache = ledgerEndCache, - pruningOffsetService = pruningOffsetService, - loggerFactory = loggerFactory, - ) - - private val globalIdQueriesLimiter = new QueueBasedConcurrencyLimiter( - parallelism = globalMaxEventIdQueries, - executionContext = queryExecutionContext, - ) - - private val globalPayloadQueriesLimiter = new QueueBasedConcurrencyLimiter( - parallelism = globalMaxEventPayloadQueries, - executionContext = queryExecutionContext, - ) - - private val acsReader = new ACSReader( - config = activeContractsServiceStreamsConfig, - globalIdQueriesLimiter = globalIdQueriesLimiter, - globalPayloadQueriesLimiter = globalPayloadQueriesLimiter, - dispatcher = dbDispatcher, - queryValidRange = queryValidRange, - eventStorageBackend = readStorageBackend.eventStorageBackend, - lfValueTranslation = lfValueTranslation, - contractStore = contractStore, - achsStateCache = achsStateCache, - incompleteOffsets = incompleteOffsets, - metrics = metrics, - tracer = tracer, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - private val topologyTransactionsStreamReader = new TopologyTransactionsStreamReader( - globalIdQueriesLimiter = globalIdQueriesLimiter, - globalPayloadQueriesLimiter = globalPayloadQueriesLimiter, - dbDispatcher = dbDispatcher, - queryValidRange = queryValidRange, - eventStorageBackend = readStorageBackend.eventStorageBackend, - metrics = metrics, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - private val updatesStreamReader = new UpdatesStreamReader( - config = updatesStreamsConfig, - globalIdQueriesLimiter = globalIdQueriesLimiter, - globalPayloadQueriesLimiter = globalPayloadQueriesLimiter, - dbDispatcher = dbDispatcher, - queryValidRange = queryValidRange, - eventStorageBackend = readStorageBackend.eventStorageBackend, - lfValueTranslation = lfValueTranslation, - contractStore = contractStore, - metrics = metrics, - tracer = tracer, - topologyTransactionsStreamReader = topologyTransactionsStreamReader, - pruningOffsetService = pruningOffsetService, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - private val topologyTransactionPointwiseReader = new TopologyTransactionPointwiseReader( - dbDispatcher = dbDispatcher, - eventStorageBackend = readStorageBackend.eventStorageBackend, - metrics = metrics, - lfValueTranslation = lfValueTranslation, - queryValidRange = queryValidRange, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - private val transactionPointwiseReader = new TransactionOrReassignmentPointwiseReader( - dbDispatcher = dbDispatcher, - eventStorageBackend = readStorageBackend.eventStorageBackend, - metrics = metrics, - lfValueTranslation = lfValueTranslation, - queryValidRange = queryValidRange, - contractStore = contractStore, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - private val updatePointwiseReader = new UpdatePointwiseReader( - dbDispatcher = dbDispatcher, - eventStorageBackend = readStorageBackend.eventStorageBackend, - parameterStorageBackend = parameterStorageBackend, - metrics = metrics, - transactionPointwiseReader = transactionPointwiseReader, - topologyTransactionPointwiseReader = topologyTransactionPointwiseReader, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - override val updateReader: UpdateReader = - new UpdateReader( - dispatcher = dbDispatcher, - queryValidRange = queryValidRange, - eventStorageBackend = readStorageBackend.eventStorageBackend, - metrics = metrics, - updatesStreamReader = updatesStreamReader, - updatePointwiseReader = updatePointwiseReader, - acsReader = acsReader, - )(queryExecutionContext) - - override val contractsReader: ContractsReader = - ContractsReader( - contractLoader, - dbDispatcher, - metrics, - readStorageBackend.contractStorageBackend, - loggerFactory, - )(commandExecutionContext) - - override def eventsReader: LedgerDaoEventsReader = - new EventsReader( - dbDispatcher = dbDispatcher, - eventStorageBackend = readStorageBackend.eventStorageBackend, - parameterStorageBackend = parameterStorageBackend, - metrics = metrics, - lfValueTranslation = lfValueTranslation, - contractStore = contractStore, - ledgerEndCache = ledgerEndCache, - loggerFactory = loggerFactory, - )(queryExecutionContext) - - override val completions: CommandCompletionsReader = - new CommandCompletionsReader( - dbDispatcher, - readStorageBackend.completionStorageBackend, - queryValidRange, - metrics, - pageSize = completionsPageSize, - loggerFactory, - ) - -} - -private[platform] object JdbcLedgerDao { - - val acceptType = "accept" - val rejectType = "reject" -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/LedgerDao.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/LedgerDao.scala deleted file mode 100644 index e9bf9bdb37..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/LedgerDao.scala +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.ledger.api.v2.event_query_service.GetEventsByContractIdResponse -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.daml.ledger.api.v2.update_service.{GetUpdateResponse, GetUpdatesResponse} -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.health.ReportsHealth -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.Future - -private[platform] trait LedgerDaoUpdateReader { - def getUpdates( - startInclusive: Offset, - endInclusive: Offset, - internalUpdateFormat: InternalUpdateFormat, - descendingOrder: Boolean, - skipPruningChecks: Boolean = false, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, GetUpdatesResponse), NotUsed] - - def lookupUpdateBy( - lookupKey: LookupKey, - internalUpdateFormat: InternalUpdateFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] - - def getActiveContracts( - activeAt: Option[Offset], - filter: TemplatePartiesFilter, - eventProjectionProperties: EventProjectionProperties, - rangeInfo: AcsRangeInfo, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[GetActiveContractsResponse, NotUsed] -} - -private[platform] trait LedgerDaoCommandCompletionsReader { - def getCommandCompletions( - startInclusive: Offset, - endInclusive: Offset, - userId: UserId, - parties: Set[Party], - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, CompletionStreamResponse), NotUsed] -} - -private[platform] trait LedgerDaoEventsReader { - - def getEventsByContractId( - contractId: ContractId, - internalEventFormatO: Option[InternalEventFormat], - )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractIdResponse] - - // TODO(i16065): Re-enable getEventsByContractKey tests -// def getEventsByContractKey( -// contractKey: com.digitalasset.daml.lf.value.Value, -// templateId: Ref.Identifier, -// requestingParties: Set[Party], -// endExclusiveSeqId: Option[Long], -// maxIterations: Int, -// )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractKeyResponse] - -} -private[platform] trait LedgerReadDao extends ReportsHealth { - - def lookupParticipantId()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ParticipantId]] - - /** Looks up the current ledger end */ - def lookupLedgerEnd()(implicit loggingContext: LoggingContextWithTrace): Future[Option[LedgerEnd]] - - def updateReader: LedgerDaoUpdateReader - - def contractsReader: LedgerDaoContractsReader - - def eventsReader: LedgerDaoEventsReader - - def completions: LedgerDaoCommandCompletionsReader - - /** Returns a list of party details for the parties specified. */ - def getParties(parties: Seq[Party])(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] - - /** Returns a list of all known parties. */ - def listKnownParties( - fromExcl: Option[Party], - filterParty: Option[String185], - maxResults: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] - - /** Prunes participant events and completions in archived history and remembers largest pruning - * offset processed thus far. - * - * @param pruneUpToInclusive - * offset up to which to prune archived history inclusively - * @return - */ - def prune( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusive: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Unit] - - def indexDbPrunedUpTo(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] - - def isPruningInProgress: Boolean -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/PaginatingAsyncStream.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/PaginatingAsyncStream.scala deleted file mode 100644 index ddab26cde1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/PaginatingAsyncStream.scala +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.store.dao.events.IdPageSizing -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.OverflowStrategy -import org.apache.pekko.stream.scaladsl.Source - -import java.sql.Connection -import scala.concurrent.Future - -private[platform] class PaginatingAsyncStream( - override protected val loggerFactory: NamedLoggerFactory -) extends NamedLogging { - - import PaginatingAsyncStream.* - - private val directEc = DirectExecutionContext(noTracingLogger) - - /** Concatenates the results of multiple asynchronous calls into a single [[Source]], passing the - * last seen event's offset to the next iteration query, so it can continue reading events from - * this point. - * - * This is to implement pagination based on generic offset. The main purpose of the pagination is - * to break down large queries into smaller batches. The reason for this is that we are currently - * using simple blocking JDBC APIs and a long-running stream would end up occupying a thread in - * the DB pool, severely limiting the ability of keeping multiple, concurrent, long-running - * streams while serving lookup calls. - * - * @param startFromOffset - * initial offset - * @param getOffset - * function that returns a position/offset from the element of type [[T]] - * @param query - * a function that fetches results starting from provided offset - * @tparam Off - * the type of the offset - * @tparam T - * the type of the items returned in each call - */ - def streamFromSeekPagination[Off, T](startFromOffset: Off, getOffset: T => Off)( - query: Off => Future[Vector[T]] - ): Source[T, NotUsed] = - Source - .unfoldAsync(Option(startFromOffset)) { - case None => - Future.successful(None) // finished reading the whole thing - case Some(offset) => - query(offset).map { result => - val nextPageOffset: Option[Off] = result.lastOption.map(getOffset) - Some((nextPageOffset, result)) - }(directEc) - } - .flatMapConcat(Source(_)) - - def streamIdsFromSeekPaginationWithoutIdFilter( - idStreamName: String, - idPageSizing: IdPageSizing, - idPageBufferSize: Int, - initialFromIdExclusive: Long, - initialEndInclusive: Long, - descendingOrder: Boolean, - )( - fetchPageDbQuery: IdPageQuery - )( - executeIdQuery: (Connection => IdPage) => Future[IdPage] - )(implicit - traceContext: TraceContext - ): Source[Long, NotUsed] = { - assert(idPageBufferSize > 0) - def fetchPageQuery(paginationInput: PaginationInput): Connection => IdPage = - c => - wrapIdDbQuery( - in = paginationInput, - f = fetchPageDbQuery.fetchPage(c), - )(result => - s"[$idStreamName] for next ID page returned: limit:${paginationInput.limit} from:${paginationInput.fromTo.fromExclusive} to:${paginationInput.fromTo.toInclusive} #IDs:${result.ids.size}" - ) - val initialFromTo = PaginationFromTo.of( - startExclusive = initialFromIdExclusive, - endInclusive = initialEndInclusive, - descending = descendingOrder, - ) - val initialState = IdPaginationState( - fromIdExclusive = initialFromTo.fromExclusive, - pageSize = idPageSizing.minPageSize, - last = false, - ) - Source - .unfoldAsync[IdPaginationState, Vector[Long]](initialState) { state => - executeIdQuery( - fetchPageQuery( - PaginationInput( - fromTo = initialFromTo.copy( - fromExclusive = state.fromIdExclusive - ), - limit = state.pageSize, - ) - ) - ).map(page => - page.ids.lastOption.map(last => - IdPaginationState( - fromIdExclusive = last, - pageSize = Math.min(state.pageSize * 4, idPageSizing.maxPageSize), - last = page.lastPage, - ) -> page.ids - ) - )(directEc) - } - .buffer(idPageBufferSize, OverflowStrategy.backpressure) - .mapConcat(identity) - } - - def streamIdsFromSeekPaginationWithIdFilter( - idStreamName: String, - idPageSizing: IdPageSizing, - idPageBufferSize: Int, - initialFromIdExclusive: Long, - initialEndInclusive: Long, - descendingOrder: Boolean, - )( - fetchPageDbQuery: IdFilterPageQuery - )( - executeFetchBounds: (Connection => Option[IdPageBounds]) => Future[Option[IdPageBounds]], - idFilterQueryParallelism: Int, - executeFetchPage: (Connection => Vector[Long]) => Future[Vector[Long]], - )(implicit - traceContext: TraceContext - ): Source[Long, NotUsed] = { - assert(idPageBufferSize > 0) - streamIdPagesFromSeekPaginationWithIdFilter( - idStreamName = idStreamName, - idPageSizing = idPageSizing, - initialFromIdExclusive = initialFromIdExclusive, - initialEndInclusive = initialEndInclusive, - descendingOrder = descendingOrder, - )(fetchPageDbQuery)( - executeFetchBounds = executeFetchBounds, - idFilterQueryParallelism = idFilterQueryParallelism, - executeFetchPage = executeFetchPage, - ) - .buffer(idPageBufferSize, OverflowStrategy.backpressure) - .mapConcat(_._2) - } - - def streamIdPagesFromSeekPaginationWithIdFilter( - idStreamName: String, - idPageSizing: IdPageSizing, - initialFromIdExclusive: Long, - initialEndInclusive: Long, - descendingOrder: Boolean, - )( - fetchPageDbQuery: IdFilterPageQuery - )( - executeFetchBounds: (Connection => Option[IdPageBounds]) => Future[Option[IdPageBounds]], - idFilterQueryParallelism: Int, - executeFetchPage: (Connection => Vector[Long]) => Future[Vector[Long]], - )(implicit - traceContext: TraceContext - ): Source[(PaginationInput, Vector[Long]), NotUsed] = { - def fetchBoundsQuery( - paginationInput: PaginationInput - ): Connection => Option[IdPageBounds] = - c => - wrapIdDbQuery( - in = paginationInput, - f = fetchPageDbQuery.fetchPageBounds(c), - )(result => - s"[$idStreamName] for next ID page bounds returned: limit:${paginationInput.limit} from:${paginationInput.fromTo.fromExclusive} to:${result - .map(_.fromTo.toInclusive)}" - ) - def fetchPageQuery( - paginationFromTo: PaginationFromTo - ): Connection => Vector[Long] = - c => - wrapIdDbQuery( - in = paginationFromTo, - f = fetchPageDbQuery.fetchPage(c), - )(result => - s"[$idStreamName] for next ID page returned: from:${paginationFromTo.fromExclusive} to:${paginationFromTo.toInclusive} #IDs:${result.size}" - ) - val initialFromTo = PaginationFromTo.of( - startExclusive = initialFromIdExclusive, - endInclusive = initialEndInclusive, - descending = descendingOrder, - ) - val initialState = IdPaginationState( - fromIdExclusive = initialFromTo.fromExclusive, - pageSize = idPageSizing.minPageSize, - last = false, - ) - Source - .unfoldAsync[IdPaginationState, PaginationInput](initialState) { state => - if (state.last) Future.successful(None) - else { - val fromTo = initialFromTo.copy( - fromExclusive = state.fromIdExclusive - ) - executeFetchBounds( - fetchBoundsQuery( - PaginationInput( - fromTo = fromTo, - limit = state.pageSize, - ) - ) - ).map( - _.map(pageBounds => - IdPaginationState( - fromIdExclusive = pageBounds.fromTo.toInclusive, - pageSize = Math.min(state.pageSize * 4, idPageSizing.maxPageSize), - last = pageBounds.lastPage, - ) -> PaginationInput( - fromTo = pageBounds.fromTo, - limit = state.pageSize, - ) - ) - )(directEc) - } - } - .mapAsync(idFilterQueryParallelism)(paginationInput => - executeFetchPage( - fetchPageQuery(paginationInput.fromTo) - ).map(paginationInput -> _)(directEc) - ) - } - - def wrapIdDbQuery[In, Out]( - in: In, - f: In => Out, - )( - log: Out => String - )(implicit traceContext: TraceContext): Out = { - val started = System.nanoTime() - val result = f(in) - def elapsedMillis: Long = (System.nanoTime() - started) / 1000000 - logger.debug( - s"ID query for ${log(result)} DB query took: ${elapsedMillis}ms" - ) - result - } -} - -object PaginatingAsyncStream { - - final case class IdPaginationState(fromIdExclusive: Long, pageSize: Int, last: Boolean) - - /** Describes bounds for generating paginated stream. The stream can be either descending or - * ascending. - * @param fromExclusive - * a starting bound for the stream (a sequential id from which to look for a first element in a - * direction of stream order). In case of ascending stream it's a lower bound in case of the - * descending stream the upper bound of the range. In a descending stream [[fromExclusive]] - * must be greather than or equal to [[toInclusive]], in an ascending one, the inequality sign - * is flipped. - */ - final case class PaginationFromTo( - fromExclusive: Long, - toInclusive: Long, - descending: Boolean, - ) - - object PaginationFromTo { - def ascending( - startExclusive: Long, - endInclusive: Long, - ): PaginationFromTo = { - assert(startExclusive <= endInclusive) - PaginationFromTo( - fromExclusive = startExclusive, - toInclusive = endInclusive, - descending = false, - ) - } - - def descending( - startExclusive: Long, - endInclusive: Long, - ): PaginationFromTo = { - assert(startExclusive <= endInclusive) - PaginationFromTo( - fromExclusive = endInclusive + 1, // Adjust bounds to flip inclusive/exclusive meaning - toInclusive = startExclusive + 1, - descending = true, - ) - } - - def of( - startExclusive: Long, - endInclusive: Long, - descending: Boolean, - ): PaginationFromTo = - if (descending) - PaginationFromTo.descending(startExclusive, endInclusive) - else - ascending(startExclusive, endInclusive) - } - - trait IdFilterPageQuery { - def fetchPageBounds(connection: Connection)(input: PaginationInput): Option[IdPageBounds] - def fetchPage(connection: Connection)(fromTo: PaginationFromTo): Vector[Long] - } - - final case class IdPageBounds( - fromTo: PaginationFromTo, - lastPage: Boolean, - ) - - trait IdPageQuery { - def fetchPage(connection: Connection)(input: PaginationInput): IdPage - } - - final case class IdPage( - ids: Vector[Long], - lastPage: Boolean, - ) - - final case class PaginationInput( - fromTo: PaginationFromTo, - limit: Int, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/PersistenceResponse.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/PersistenceResponse.scala deleted file mode 100644 index e5c7596280..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/PersistenceResponse.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -private[platform] sealed abstract class PersistenceResponse extends Product with Serializable - -private[platform] object PersistenceResponse { - - case object Ok extends PersistenceResponse - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/QueryRange.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/QueryRange.scala deleted file mode 100644 index 3dc2014dba..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/QueryRange.scala +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -final case class QueryRange[A](startInclusive: A, endInclusive: A) { - def map[B](f: A => B): QueryRange[B] = - copy(startInclusive = f(startInclusive), endInclusive = f(endInclusive)) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ACSReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ACSReader.scala deleted file mode 100644 index 4a3da7949e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ACSReader.scala +++ /dev/null @@ -1,872 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.state_service.{ - ActiveContract, - GetActiveContractsResponse, - IncompleteAssigned, - IncompleteUnassigned, -} -import com.daml.metrics.Timed -import com.daml.nameof.NameOf.qualifiedNameOfCurrentFunc -import com.daml.tracing -import com.daml.tracing.{SpanAttribute, Spans} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.api.messages.state.AcsContinuationToken.Checksum -import com.digitalasset.canton.ledger.api.messages.state.{ - AcsContinuationPointerActiveContracts, - AcsContinuationPointerIncompleteReassignments, - AcsContinuationToken, - AcsRangeInfo, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors.AbortedDueToShutdown -import com.digitalasset.canton.platform.config.ActiveContractsServiceStreamsConfig -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.ScalaPbStreamingOptimizations.ScalaPbMessageWithPrecomputedSerializedSize -import com.digitalasset.canton.platform.store.backend.EventStorageBackend -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.Ids -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{ - FatCreatedEventProperties, - RawFatActiveContract, - RawFatAssignEvent, - RawThinAssignEvent, - RawUnassignEvent, -} -import com.digitalasset.canton.platform.store.backend.common.EventPayloadSourceForUpdatesAcsDelta -import com.digitalasset.canton.platform.store.cache.AchsStateCache -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - IdFilterPageQuery, - IdPageBounds, - PaginationFromTo, - PaginationInput, -} -import com.digitalasset.canton.platform.store.dao.events.UpdateReader.endSpanOnTermination -import com.digitalasset.canton.platform.store.dao.{ - DbDispatcher, - EventProjectionProperties, - PaginatingAsyncStream, -} -import com.digitalasset.canton.platform.store.utils.{ - ConcurrencyLimiter, - QueueBasedConcurrencyLimiter, - Telemetry, -} -import com.digitalasset.canton.platform.{FatContract, TemplatePartiesFilter} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.PekkoUtil.syntax.* -import com.digitalasset.canton.util.Thereafter.syntax.ThereafterAsyncOps -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.FullIdentifier -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source -import org.apache.pekko.stream.{Attributes, OverflowStrategy} - -import java.sql.Connection -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.Success -import scala.util.chaining.* - -/** Streams ACS events (active contracts) in a two step process consisting of: - * 1. fetching event sequential ids of the active contracts based on the filtering constraints, - * 1. fetching the active contracts based on the fetched event sequential ids. - * - * Details: An input filtering constraint (consisting of parties and template ids) is converted - * into decomposed filtering constraints (a constraint with exactly one party and at most one - * template id). For each decomposed filter, the matching event sequential ids are fetched in - * parallel and then merged into a strictly increasing sequence. The elements from this sequence - * are then batched and the batch ids serve as the input to the payload fetching step. - */ -class ACSReader( - config: ActiveContractsServiceStreamsConfig, - globalIdQueriesLimiter: ConcurrencyLimiter, - globalPayloadQueriesLimiter: ConcurrencyLimiter, - dispatcher: DbDispatcher, - queryValidRange: QueryValidRange, - eventStorageBackend: EventStorageBackend, - lfValueTranslation: LfValueTranslation, - contractStore: LedgerApiContractStore, - achsStateCache: AchsStateCache, - incompleteOffsets: ( - Offset, - Option[Set[Ref.Party]], - TraceContext, - ) => FutureUnlessShutdown[Vector[Offset]], - metrics: LedgerApiServerMetrics, - tracer: Tracer, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends NamedLogging { - - private val dbMetrics = metrics.index.db - - private val paginatingAsyncStream = new PaginatingAsyncStream(loggerFactory) - - def streamActiveContracts( - filteringConstraints: TemplatePartiesFilter, - activeAt: (Offset, Long), - eventProjectionProperties: EventProjectionProperties, - rangeInfo: AcsRangeInfo, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[GetActiveContractsResponse, NotUsed] = { - val (activeAtOffset, activeAtLong) = activeAt - val span = - Telemetry.Updates.createSpan(tracer, activeAtOffset)(qualifiedNameOfCurrentFunc) - val event = - tracing.Event("contract", Map((SpanAttribute.Offset, activeAtLong.toString))) - Spans.addEventToSpan(event, span) - logger.debug( - s"getActiveContracts($activeAtOffset, $filteringConstraints, $eventProjectionProperties)" - ) - doStreamActiveContracts( - filteringConstraints, - activeAt, - eventProjectionProperties, - rangeInfo, - ) - .watchTermination()(endSpanOnTermination(span)) - } - - private def doStreamActiveContracts( - filter: TemplatePartiesFilter, - activeAt: (Offset, Long), - eventProjectionProperties: EventProjectionProperties, - rangeInfo: AcsRangeInfo, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[GetActiveContractsResponse, NotUsed] = { - val (activeAtOffset, activeAtEventSeqId) = activeAt - def withValidatedActiveAt[T](query: => Future[T]) = - queryValidRange.withOffsetNotBeforePruning( - activeAtOffset, - pruned => - ACSReader.acsBeforePruningErrorReason( - acsOffset = activeAtOffset, - prunedUpToOffset = pruned, - ), - ledgerEnd => - ACSReader.acsAfterLedgerEndErrorReason( - acsOffset = activeAtOffset, - ledgerEndOffset = ledgerEnd, - ), - )(query) - - val allFilterParties = filter.allFilterParties - val decomposedFilters = FilterUtils.decomposeFilters(filter).toVector - val activeIdQueriesLimiter = - new QueueBasedConcurrencyLimiter(config.maxParallelActiveIdQueries, executionContext) - val localPayloadQueriesLimiter = - new QueueBasedConcurrencyLimiter(config.maxParallelPayloadCreateQueries, executionContext) - val idQueryPageSizing = IdPageSizing.calculateFrom( - maxIdPageSize = Math.min( - rangeInfo.limit.map(_.toInt).getOrElse(config.maxIdsPerIdPage), - config.maxIdsPerIdPage, - ), - workingMemoryInBytesForIdPages = config.maxWorkingMemoryInBytesForIdPages, - numOfDecomposedFilters = decomposedFilters.size, - numOfPagesInIdPageBuffer = config.maxPagesPerIdPagesBuffer, - loggerFactory = loggerFactory, - ) - - def achsIsValid: Boolean = activeAtEventSeqId >= achsStateCache.get().validAt - - def fetchAchsIdFilterPageQuery( - filter: DecomposedFilter - ): IdFilterPageQuery = { - val achsQuery: IdFilterPageQuery = - eventStorageBackend.updateStreamingQueries.fetchAchsIds( - stakeholderO = filter.party, - templateIdO = filter.templateId, - activeAtEventSeqId = activeAtEventSeqId, - ) - - new ACSReader.AchsValidatingIdFilterPageQuery( - achsQuery = achsQuery, - achsIsValid = () => achsIsValid, - getLastPopulated = () => achsStateCache.get().lastPointers.lastPopulated, - ) - } - - def fetchActiveIds(initialFromIdExclusive: Long)( - filter: DecomposedFilter - ): Source[Long, NotUsed] = - if (achsStateCache.get().lastPointers.lastPopulated == 0 || !achsIsValid) { - if (!achsIsValid) { - val achsState = achsStateCache.get() - metrics.index.achsSkips.inc() - logger.info( - s"ACHS for $filter skipped since " + - s"validAt (${achsState.validAt}) already surpassed requested activeAt ($activeAtEventSeqId), " + - s"falling back to filter tables" - ) - } - fetchActiveIdsFromFilterTables( - achsLastInput = None, - initialFromIdExclusive = initialFromIdExclusive, - )(filter) - } else { - val achsLastInputPromise = Promise[Option[PaginationInput]]() - paginatingAsyncStream - .streamIdPagesFromSeekPaginationWithIdFilter( - idStreamName = s"ActiveContractIds $filter", - idPageSizing = idQueryPageSizing, - initialFromIdExclusive = initialFromIdExclusive, - initialEndInclusive = activeAtEventSeqId, - descendingOrder = false, - )(fetchAchsIdFilterPageQuery(filter))( - executeFetchBounds = f => - activeIdQueriesLimiter.execute( - globalIdQueriesLimiter.execute( - dispatcher.executeSql(metrics.index.db.getAchsIdRanges)(f) - ) - ), - idFilterQueryParallelism = config.idFilterQueryParallelism, - executeFetchPage = f => - activeIdQueriesLimiter.execute( - globalIdQueriesLimiter.execute( - dispatcher.executeSql( - metrics.index.db.getAchsFilteredIds - )(f) - ) - ), - ) - .takeWhile(_ => achsIsValid) - .statefulMap(() => Option.empty[PaginationInput])( - f = { case (_previousInput, (input, ids)) => - (Some(input), (input, ids)) - }, - onComplete = state => { - // hook to trigger the next, filter table part of the stream - achsLastInputPromise - .trySuccess(state) - .discard - None - }, - ) - .buffer(config.maxPagesPerIdPagesBuffer, OverflowStrategy.backpressure) - .mapConcat(_._2) - .concat( - Source.futureSource( - achsLastInputPromise.future.map { achsLastInput => - val resumeFrom = achsLastInput - .map(_.fromTo.toInclusive) - .getOrElse(0L) - if (!achsIsValid) { - val achsState = achsStateCache.get() - metrics.index.achsMidstreamFallbacks.inc() - logger.info( - s"ACHS stream for $filter fell back to filter tables from $resumeFrom since " + - s"validAt (${achsState.validAt}) surpassed activeAtEventSeqId ($activeAtEventSeqId), " - ) - } else { - logger.debug( - s"ACHS stream for $filter completed, continuing with filter tables from $resumeFrom" - ) - } - fetchActiveIdsFromFilterTables( - achsLastInput = achsLastInput, - initialFromIdExclusive = initialFromIdExclusive, - )(filter) - }(executionContext) - ) - ) - } - - def fetchActiveIdsFromFilterTables( - achsLastInput: Option[PaginationInput], - initialFromIdExclusive: Long, - )( - filter: DecomposedFilter - ): Source[Long, NotUsed] = - paginatingAsyncStream.streamIdsFromSeekPaginationWithIdFilter( - idStreamName = s"ActiveContractIds $filter", - idPageSizing = achsLastInput - .map(lastInput => - idQueryPageSizing.copy( - minPageSize = lastInput.limit - ) - ) - .getOrElse(idQueryPageSizing), - idPageBufferSize = config.maxPagesPerIdPagesBuffer, - initialFromIdExclusive = - achsLastInput.map(_.fromTo.toInclusive).getOrElse(initialFromIdExclusive), - initialEndInclusive = activeAtEventSeqId, - descendingOrder = false, - )( - eventStorageBackend.updateStreamingQueries.fetchActiveIds( - stakeholderO = filter.party, - templateIdO = filter.templateId, - activeAtEventSeqId = activeAtEventSeqId, - ) - )( - executeFetchBounds = f => - activeIdQueriesLimiter.execute( - globalIdQueriesLimiter.execute( - dispatcher.executeSql(metrics.index.db.getActiveContractIdRanges)(f) - ) - ), - idFilterQueryParallelism = config.idFilterQueryParallelism, - executeFetchPage = f => - activeIdQueriesLimiter.execute( - globalIdQueriesLimiter.execute( - dispatcher.executeSql( - metrics.index.db.getFilteredActiveContractIds - )(f) - ) - ), - ) - - def withFatContracts[T]( - internalContractId: T => Long - )(payloads: Vector[T]): Future[Vector[(T, Option[FatContract])]] = - for { - contractsM <- contractStore - .lookupBatchedNonReadThrough( - payloads.map(internalContractId) - ) - } yield payloads.map { payload => - payload -> contractsM - .get(internalContractId(payload)) - .map(_.inst) - } - - def resolveFatInstance[T, U]( - internalContractId: T => Long, - toFatInstance: (T, FatContract) => U, - )(payloads: Vector[(T, Option[FatContract])]): Vector[U] = - payloads.map { - case (payload, None) => - throw new IllegalStateException( - s"Contract for internal contract id ${internalContractId(payload)} was not found in the contract store." - ) - case (payload, Some(fatContract)) => - toFatInstance(payload, fatContract) - } - - def fetchActivePayloads( - ids: Iterable[Long] - ): Future[Vector[RawFatActiveContract]] = - localPayloadQueriesLimiter.execute( - globalPayloadQueriesLimiter.execute( - withValidatedActiveAt( - dispatcher - .executeSql(metrics.index.db.getActiveContractBatch) { - eventStorageBackend.activeContractBatch( - eventSequentialIds = ids, - allFilterParties = allFilterParties, - ) - } - .flatMap( - withFatContracts(_.thinCreatedEventProperties.internalContractId) - ) - ).map( - resolveFatInstance( - internalContractId = _.thinCreatedEventProperties.internalContractId, - toFatInstance = (thin, fatContract) => - RawFatActiveContract( - commonEventProperties = thin.commonEventProperties, - fatCreatedEventProperties = FatCreatedEventProperties( - thinCreatedEventProperties = thin.thinCreatedEventProperties, - fatContract = fatContract, - ), - ), - ) - ).thereafterP { case Success(result) => - logger.debug( - s"getActiveContractBatch returned ${result.size}/${ids.size} ${ids.lastOption - .map(last => s"until $last") - .getOrElse("")}" - ) - } - ) - ) - - def fetchAssignIdsForOffsets( - offsets: Iterable[Offset] - ): Future[Vector[Long]] = - globalIdQueriesLimiter.execute( - dispatcher.executeSql(metrics.index.db.getAssingIdsForOffsets) { connection => - // all activations for an incomplete offset should be assignments - val ids = - eventStorageBackend - .lookupActivationSequentialIdByOffset(offsets.map(_.unwrap))(connection) - logger.debug( - s"Assign Ids for offsets returned #${ids.size} (from ${offsets.size}) ${ids.lastOption - .map(last => s"until $last") - .getOrElse("")}" - ) - ids - } - ) - - def fetchUnassignIdsForOffsets( - offsets: Iterable[Offset] - ): Future[Vector[Long]] = - globalIdQueriesLimiter.execute( - dispatcher.executeSql(metrics.index.db.getUnassingIdsForOffsets) { connection => - // all deactivations for an incomplete offset should be assignments - val ids = - eventStorageBackend - .lookupDeactivationSequentialIdByOffset(offsets.map(_.unwrap))(connection) - logger.debug( - s"Unassign Ids for offsets returned #${ids.size} (from ${offsets.size}) ${ids.lastOption - .map(last => s"until $last") - .getOrElse("")}" - ) - ids - } - ) - - def fetchAssignPayloads( - ids: Iterable[Long] - ): Future[Vector[RawFatAssignEvent]] = - if (ids.isEmpty) Future.successful(Vector.empty) - else - localPayloadQueriesLimiter.execute( - globalPayloadQueriesLimiter.execute( - withValidatedActiveAt( - dispatcher - .executeSql( - metrics.index.db.updatesAcsDeltaStream.fetchEventActivatePayloads - )( - eventStorageBackend.fetchEventPayloadsAcsDelta( - EventPayloadSourceForUpdatesAcsDelta.Activate - )( - eventSequentialIds = Ids(ids), - requestingPartiesForTx = None, - requestingPartiesForReassignment = allFilterParties, - ) - ) - .map(_.collect { case raw: RawThinAssignEvent => - raw - }) - .flatMap(withFatContracts(_.thinCreatedEventProperties.internalContractId)) - ).map( - resolveFatInstance( - internalContractId = _.thinCreatedEventProperties.internalContractId, - toFatInstance = (thin, fatContract) => - RawFatAssignEvent( - reassignmentProperties = thin.reassignmentProperties, - fatCreatedEventProperties = FatCreatedEventProperties( - thinCreatedEventProperties = thin.thinCreatedEventProperties, - fatContract = fatContract, - ), - sourceSynchronizerId = thin.sourceSynchronizerId, - ), - ) - ).thereafterP { case Success(result) => - logger.debug( - s"assignEventBatch returned ${result.size}/${ids.size} ${ids.lastOption - .map(last => s"until $last") - .getOrElse("")}" - ) - } - ) - ) - - def fetchUnassignPayloads( - ids: Iterable[Long] - ): Future[Vector[RawUnassignEvent]] = - localPayloadQueriesLimiter.execute( - globalPayloadQueriesLimiter.execute( - withValidatedActiveAt( - dispatcher - .executeSql( - metrics.index.db.updatesAcsDeltaStream.fetchEventDeactivatePayloads - )( - eventStorageBackend.fetchEventPayloadsAcsDelta( - EventPayloadSourceForUpdatesAcsDelta.Deactivate - )( - eventSequentialIds = Ids(ids), - requestingPartiesForTx = None, - requestingPartiesForReassignment = allFilterParties, - ) - ) - .map(_.collect { case raw: RawUnassignEvent => - raw - }) - ).thereafterP { case Success(result) => - logger.debug( - s"unassignEventBatch returned ${result.size}/${ids.size} ${ids.lastOption - .map(last => s"until $last") - .getOrElse("")}" - ) - } - ) - ) - - def fetchActivationEventsForUnassignedBatch( - batch: Seq[RawUnassignEvent] - ): Future[Seq[(RawUnassignEvent, RawFatActiveContract)]] = { - val (unassignEventWithDeactivationRef, deactivatedEventSeqIds) = batch.view - .flatMap(rawUnassignEvent => - rawUnassignEvent.deactivatedEventSeqId match { - case Some(deactivatedEventSeqId) => Some(rawUnassignEvent -> deactivatedEventSeqId) - case None => - logger.warn( - s"For an IncompleteUnassigned event (offset:${rawUnassignEvent.reassignmentProperties.commonEventProperties.offset} workflow-id:${rawUnassignEvent.reassignmentProperties.commonEventProperties.workflowId} contract-id:${rawUnassignEvent.contractId} template-id:${rawUnassignEvent.templateId} reassignment-counter:${rawUnassignEvent.reassignmentProperties.reassignmentCounter} synchronizer id:${rawUnassignEvent.reassignmentProperties.commonEventProperties.synchronizerId} event-sequential-id:${rawUnassignEvent.reassignmentProperties.commonEventProperties.eventSequentialId}) there is neither CreatedEvent nor AssignedEvent available. This entry will be dropped from the result." - ) - None - } - ) - .toVector - .unzip - fetchActivePayloads(deactivatedEventSeqIds) - .map(unassignEventWithDeactivationRef.zip) - } - - val stringWildcardParties = filter.templateWildcardParties.map(_.map(_.toString)) - val templateFilters = filter.relation.map { case (key, value) => - key -> value - } - def eventMeetsConstraints(templateId: FullIdentifier, witnesses: Set[String]): Boolean = - stringWildcardParties.fold(true)(_.exists(witnesses)) || ( - templateFilters.get(templateId.toNameTypeConRef) match { - case Some(Some(filterParties)) => filterParties.exists(witnesses) - case Some(None) => true // party wildcard - case None => - false // templateId is not in the filter - } - ) - - def unassignMeetsConstraints(rawUnassignEvent: RawUnassignEvent): Boolean = - eventMeetsConstraints( - rawUnassignEvent.templateId, - rawUnassignEvent.witnessParties, - ) - def assignMeetsConstraints(rawAssignEvent: RawFatAssignEvent): Boolean = - eventMeetsConstraints( - rawAssignEvent.templateId, - rawAssignEvent.witnessParties, - ) - - // Pekko requires for this buffer's size to be a power of two. - val inputBufferSize = - Utils.largestSmallerOrEqualPowerOfTwo(config.maxParallelPayloadCreateQueries) - - val activeContractsCountPromise = Promise[Long]() - - def activeContractsStream(startSequentialIdExclusive: Long) = - limitIfNeeded(rangeInfo.limit)( - decomposedFilters - .map(fetchActiveIds(startSequentialIdExclusive)) - .pipe(EventIdsUtils.sortAndDeduplicateIds(descendingOrder = false)) - ).statefulMap(() => 0L)( - f = { case (count, response) => - (count + 1, response) - }, - onComplete = count => { - activeContractsCountPromise.trySuccess(count).discard - None - }, - ).batchN( - maxBatchSize = config.maxPayloadsPerPayloadsPage, - maxBatchCount = config.maxParallelPayloadCreateQueries + 1, - ).addAttributes(Attributes.inputBuffer(initial = inputBufferSize, max = inputBufferSize)) - .mapAsync(config.maxParallelPayloadCreateQueries)(fetchActivePayloads) - .mapConcat(identity) - .mapAsync(config.contractProcessingParallelism)( - toApiResponseActiveContract(eventProjectionProperties, rangeInfo.requestChecksum) - ) - - val activeContracts = rangeInfo.continuationPointer match { - case Some(AcsContinuationPointerIncompleteReassignments(_, _)) => - activeContractsCountPromise.trySuccess(0L).discard - Source.empty - case Some(AcsContinuationPointerActiveContracts(startSequentialIdExclusive)) => - activeContractsStream(startSequentialIdExclusive) - case _ => activeContractsStream(0L) - } - def incompleteReassignments(limit: Option[Long]) = Source.lazyFutureSource(() => - incompleteOffsets( - activeAtOffset, - filter.allFilterParties, - loggingContext.traceContext, - ).map { allOffsets => - val (sequentialIdToContinueFrom, offsetToContinueFrom) = - rangeInfo.continuationPointer match { - case Some(AcsContinuationPointerIncompleteReassignments(sequentialId, offset)) => - (sequentialId, Offset.tryFromLong(offset)) - case _ => (0L, Offset.firstOffset) - } - val offsets = allOffsets.filter(_ >= offsetToContinueFrom) - def incompleteOffsetPages: () => Iterator[Vector[Offset]] = - () => offsets.sliding(config.maxIncompletePageSize, config.maxIncompletePageSize) - - val incompleteAssigned: Source[(Long, GetActiveContractsResponse), NotUsed] = - limitIfNeeded(limit)( - Source - .fromIterator(incompleteOffsetPages) - .mapAsync(config.maxParallelActiveIdQueries)( - fetchAssignIdsForOffsets - ) - .mapConcat(identity) - .dropWhile(_ <= sequentialIdToContinueFrom) - .grouped(config.maxIncompletePageSize) - .mapAsync(config.maxParallelPayloadCreateQueries)( - fetchAssignPayloads - ) - .mapConcat(_.filter(assignMeetsConstraints)) - ) - .mapAsync(config.contractProcessingParallelism)( - toApiResponseIncompleteAssigned(eventProjectionProperties, rangeInfo.requestChecksum) - ) - - val incompleteUnassigned: Source[(Long, GetActiveContractsResponse), NotUsed] = - limitIfNeeded(limit)( - Source - .fromIterator(incompleteOffsetPages) - .mapAsync(config.maxParallelActiveIdQueries)( - fetchUnassignIdsForOffsets - ) - .mapConcat(identity) - .dropWhile(_ <= sequentialIdToContinueFrom) - .grouped(config.maxIncompletePageSize) - .mapAsync(config.maxParallelPayloadCreateQueries)( - fetchUnassignPayloads - ) - .mapConcat(_.filter(unassignMeetsConstraints)) - .grouped(config.maxIncompletePageSize) - .mapAsync(config.maxParallelPayloadCreateQueries)( - fetchActivationEventsForUnassignedBatch - ) - .mapConcat(identity) - ) - .mapAsync(config.contractProcessingParallelism)( - toApiResponseIncompleteUnassigned( - eventProjectionProperties, - rangeInfo.requestChecksum, - ) - ) - - limitIfNeeded(limit)( - incompleteAssigned - .mergeSorted(incompleteUnassigned)(Ordering.by(_._1)) - .map(_._2) - ) - }.onShutdown { - Source.failed( - AbortedDueToShutdown.Error().asGrpcError - ) - } - ) - - val incompleteReassignmentsFutureSource = Source.lazyFutureSource(() => - activeContractsCountPromise.future.map { count => - val rest = rangeInfo.limit.map(l => Math.max(0, l - count)) - if (rest.contains(0L)) Source.empty - else incompleteReassignments(rest) - } - ) - - activeContracts.concatLazy(incompleteReassignmentsFutureSource) - } - - private def limitIfNeeded[A]( - limit: Option[Long] - )(source: Source[A, NotUsed]): Source[A, NotUsed] = - limit match { - case Some(l) => source.take(l) - case None => source - } - - private def toApiResponseActiveContract( - eventProjectionProperties: EventProjectionProperties, - checksum: Checksum, - )( - rawActiveContract: RawFatActiveContract - )(implicit lc: LoggingContextWithTrace): Future[GetActiveContractsResponse] = - Timed.future( - future = Future.delegate( - lfValueTranslation - .toApiCreatedEvent( - eventProjectionProperties = eventProjectionProperties, - fatContractInstance = rawActiveContract.fatCreatedEventProperties.fatContract, - offset = rawActiveContract.commonEventProperties.offset, - nodeId = rawActiveContract.commonEventProperties.nodeId, - representativePackageId = Ref.PackageId.assertFromString( - rawActiveContract.fatCreatedEventProperties.thinCreatedEventProperties.representativePackageId - ), - witnesses = rawActiveContract.witnessParties, - acsDelta = true, - ) - .map(createdEvent => - GetActiveContractsResponse( - workflowId = rawActiveContract.commonEventProperties.workflowId.getOrElse(""), - contractEntry = GetActiveContractsResponse.ContractEntry.ActiveContract( - ActiveContract( - createdEvent = Some(createdEvent), - synchronizerId = rawActiveContract.commonEventProperties.synchronizerId, - reassignmentCounter = - rawActiveContract.fatCreatedEventProperties.thinCreatedEventProperties.reassignmentCounter, - ) - ), - streamContinuationToken = AcsContinuationToken.activeContracts( - sequentialId = rawActiveContract.eventSeqId, - checksum = checksum, - ), - ).withPrecomputedSerializedSize() - ) - ), - timer = dbMetrics.getActiveContracts.translationTimer, - ) - - private def toApiResponseIncompleteAssigned( - eventProjectionProperties: EventProjectionProperties, - checksum: Checksum, - )( - rawFatAssign: RawFatAssignEvent - )(implicit lc: LoggingContextWithTrace): Future[(Long, GetActiveContractsResponse)] = - Timed.future( - future = Future.delegate( - lfValueTranslation - .toApiCreatedEvent( - eventProjectionProperties = eventProjectionProperties, - fatContractInstance = rawFatAssign.fatCreatedEventProperties.fatContract, - offset = rawFatAssign.reassignmentProperties.commonEventProperties.offset, - nodeId = rawFatAssign.reassignmentProperties.commonEventProperties.nodeId, - representativePackageId = Ref.PackageId.assertFromString( - rawFatAssign.fatCreatedEventProperties.thinCreatedEventProperties.representativePackageId - ), - witnesses = rawFatAssign.witnessParties, - acsDelta = true, - ) - .map(createdEvent => - rawFatAssign.reassignmentProperties.commonEventProperties.offset -> GetActiveContractsResponse( - workflowId = rawFatAssign.reassignmentProperties.commonEventProperties.workflowId - .getOrElse(""), - contractEntry = GetActiveContractsResponse.ContractEntry.IncompleteAssigned( - IncompleteAssigned( - Some(UpdateReader.toAssignedEvent(rawFatAssign, createdEvent)) - ) - ), - streamContinuationToken = AcsContinuationToken.incompleteReassignments( - sequentialId = rawFatAssign.eventSeqId, - offset = rawFatAssign.offset, - checksum = checksum, - ), - ).withPrecomputedSerializedSize() - ) - ), - timer = dbMetrics.getActiveContracts.translationTimer, - ) - - private def toApiResponseIncompleteUnassigned( - eventProjectionProperties: EventProjectionProperties, - checksum: Checksum, - )( - rawUnassignEventWithActive: (RawUnassignEvent, RawFatActiveContract) - )(implicit lc: LoggingContextWithTrace): Future[(Long, GetActiveContractsResponse)] = { - val (rawUnassignEvent, rawFatActiveContract) = rawUnassignEventWithActive - Timed.future( - future = lfValueTranslation - .toApiCreatedEvent( - eventProjectionProperties = eventProjectionProperties, - fatContractInstance = rawFatActiveContract.fatCreatedEventProperties.fatContract, - offset = rawFatActiveContract.commonEventProperties.offset, - nodeId = rawFatActiveContract.commonEventProperties.nodeId, - representativePackageId = Ref.PackageId.assertFromString( - rawFatActiveContract.fatCreatedEventProperties.thinCreatedEventProperties.representativePackageId - ), - witnesses = rawFatActiveContract.witnessParties, - acsDelta = true, - ) - .map(createdEvent => - rawUnassignEvent.reassignmentProperties.commonEventProperties.offset -> GetActiveContractsResponse( - workflowId = rawUnassignEvent.reassignmentProperties.commonEventProperties.workflowId - .getOrElse(""), - contractEntry = GetActiveContractsResponse.ContractEntry.IncompleteUnassigned( - IncompleteUnassigned( - createdEvent = Some(createdEvent), - unassignedEvent = Some( - UpdateReader.toUnassignedEvent(rawUnassignEvent) - ), - ) - ), - streamContinuationToken = AcsContinuationToken.incompleteReassignments( - sequentialId = rawUnassignEvent.eventSeqId, - offset = rawUnassignEvent.offset, - checksum = checksum, - ), - ).withPrecomputedSerializedSize() - ), - timer = dbMetrics.getActiveContracts.translationTimer, - ) - } - -} - -object ACSReader { - - /** A wrapper around an ACHS `IdFilterPageQuery` that guards each call with validity checks and - * pins the upper bound to `lastPopulated`. - * - * - `fetchPageBounds` returns `None` when `achsIsValid` is false (completing the stream). When - * valid, it adjusts the input's `toInclusive` to `lastPopulated`, and on the last page, also - * adjusts the returned bounds' `toInclusive` to `lastPopulated` so that the filter table - * continuation starts from the correct position. - * - `fetchPage` returns an empty vector when `achsIsValid` is false, otherwise delegates to - * the underlying query. - * - * @param achsQuery - * the underlying ACHS `IdFilterPageQuery` to delegate to - * @param achsIsValid - * returns whether the ACHS cache is still valid for the requested `activeAtEventSeqId` - * @param getLastPopulated - * returns the current `lastPopulated` event sequential id from the ACHS cache - */ - private[store] class AchsValidatingIdFilterPageQuery( - achsQuery: IdFilterPageQuery, - achsIsValid: () => Boolean, - getLastPopulated: () => Long, - ) extends IdFilterPageQuery { - override def fetchPageBounds(connection: Connection)( - input: PaginationInput - ): Option[IdPageBounds] = - if (!achsIsValid()) None // this completes the stream - else { - val lastPopulated = getLastPopulated() - val adjustedInput = input.copy( - fromTo = input.fromTo.copy( - // pin the upper bound to the moving target of last populated - as soon we were able to catch up, the stream completes - toInclusive = lastPopulated - ) - ) - achsQuery.fetchPageBounds(connection)(adjustedInput).map { bounds => - if (bounds.lastPage) { - // on last page, pin toInclusive to lastPopulated so filter table continuation starts from here - bounds.copy(fromTo = bounds.fromTo.copy(toInclusive = lastPopulated)) - } else bounds - } - } - - override def fetchPage(connection: Connection)( - fromTo: PaginationFromTo - ): Vector[Long] = - // the takeWhile anyway will filter this out later so does not make sense to do the query - if (!achsIsValid()) Vector.empty - else achsQuery.fetchPage(connection)(fromTo) - } - - def acsBeforePruningErrorReason( - acsOffset: Offset, - prunedUpToOffset: Offset, - ): String = - s"Active contracts request at offset ${acsOffset.unwrap} precedes pruned offset ${prunedUpToOffset.unwrap}" - - def acsAfterLedgerEndErrorReason( - acsOffset: Offset, - ledgerEndOffset: Option[Offset], - ): String = - s"Active contracts request at offset ${acsOffset.unwrap} preceded by ledger end offset ${ledgerEndOffset - .fold(0L)(_.unwrap)}" - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/BufferedUpdateReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/BufferedUpdateReader.scala deleted file mode 100644 index 0f234bc11a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/BufferedUpdateReader.scala +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.daml.ledger.api.v2.update_service.{GetUpdateResponse, GetUpdatesResponse} -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer -import com.digitalasset.canton.platform.store.dao.BufferedStreamsReader.FetchFromPersistence -import com.digitalasset.canton.platform.store.dao.events.TransactionLogUpdatesConversions -import com.digitalasset.canton.platform.store.dao.{ - BufferedStreamsReader, - BufferedUpdatePointwiseReader, - EventProjectionProperties, - LedgerDaoUpdateReader, -} -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.platform.{InternalUpdateFormat, TemplatePartiesFilter} -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -import scala.concurrent.{ExecutionContext, Future} - -private[events] class BufferedUpdateReader( - delegate: LedgerDaoUpdateReader, - bufferedUpdatesReader: BufferedStreamsReader[InternalUpdateFormat, GetUpdatesResponse], - bufferedUpdateReader: BufferedUpdatePointwiseReader[ - (LookupKey, InternalUpdateFormat), - GetUpdateResponse, - ], - lfValueTranslation: LfValueTranslation, - directEC: DirectExecutionContext, -)(implicit executionContext: ExecutionContext) - extends LedgerDaoUpdateReader { - - override def getUpdates( - startInclusive: Offset, - endInclusive: Offset, - internalUpdateFormat: InternalUpdateFormat, - descendingOrder: Boolean, - skipPruningChecks: Boolean = false, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, GetUpdatesResponse), NotUsed] = - bufferedUpdatesReader - .stream( - startInclusive = startInclusive, - endInclusive = endInclusive, - persistenceFetchArgs = internalUpdateFormat, - bufferFilter = TransactionLogUpdatesConversions - .filter(internalUpdateFormat), - toApiResponse = TransactionLogUpdatesConversions - .toGetUpdatesResponse(internalUpdateFormat, lfValueTranslation)( - loggingContext, - directEC, - ), - descendingOrder = descendingOrder, - skipPruningChecks = skipPruningChecks, - ) - - def lookupUpdateBy( - lookupKey: LookupKey, - internalUpdateFormat: InternalUpdateFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] = - Future.delegate(bufferedUpdateReader.fetch(lookupKey -> internalUpdateFormat)) - - override def getActiveContracts( - activeAt: Option[Offset], - filter: TemplatePartiesFilter, - eventProjectionProperties: EventProjectionProperties, - rangeInfo: AcsRangeInfo, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[GetActiveContractsResponse, NotUsed] = - delegate.getActiveContracts( - activeAt, - filter, - eventProjectionProperties, - rangeInfo, - ) -} - -private[platform] object BufferedUpdateReader { - def apply( - delegate: LedgerDaoUpdateReader, - updatesBuffer: InMemoryFanoutBuffer, - eventProcessingParallelism: Int, - lfValueTranslation: LfValueTranslation, - metrics: LedgerApiServerMetrics, - loggerFactory: NamedLoggerFactory, - )(implicit - executionContext: ExecutionContext - ): BufferedUpdateReader = { - val directEC = DirectExecutionContext( - loggerFactory.getLogger(BufferedUpdateReader.getClass) - ) - - val updatesStreamReader = - new BufferedStreamsReader[InternalUpdateFormat, GetUpdatesResponse]( - inMemoryFanoutBuffer = updatesBuffer, - fetchFromPersistence = new FetchFromPersistence[InternalUpdateFormat, GetUpdatesResponse] { - override def apply( - startInclusive: Offset, - endInclusive: Offset, - descendingOrder: Boolean, - filter: InternalUpdateFormat, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, GetUpdatesResponse), NotUsed] = - delegate - .getUpdates( - startInclusive = startInclusive, - endInclusive = endInclusive, - internalUpdateFormat = filter, - descendingOrder = descendingOrder, - skipPruningChecks = skipPruningChecks, - ) - }, - bufferedStreamEventsProcessingParallelism = eventProcessingParallelism, - metrics = metrics, - streamName = "transactions", - loggerFactory, - ) - - val updatePointwiseReader = - new BufferedUpdatePointwiseReader[ - (LookupKey, InternalUpdateFormat), - GetUpdateResponse, - ]( - fetchFromPersistence = { - case ( - (lookupKey, internalUpdateFormat), - loggingContext: LoggingContextWithTrace, - ) => - delegate.lookupUpdateBy( - lookupKey = lookupKey, - internalUpdateFormat = internalUpdateFormat, - )(loggingContext) - }, - fetchFromBuffer = queryParam => updatesBuffer.lookup(queryParam._1), - toApiResponse = ( - transactionLogUpdate: TransactionLogUpdate, - queryParam: (LookupKey, InternalUpdateFormat), - loggingContext: LoggingContextWithTrace, - ) => - TransactionLogUpdatesConversions.toGetUpdateResponse( - transactionLogUpdate, - queryParam._2, - lfValueTranslation, - )(loggingContext, directEC), - ) - - new BufferedUpdateReader( - delegate = delegate, - bufferedUpdatesReader = updatesStreamReader, - bufferedUpdateReader = updatePointwiseReader, - lfValueTranslation = lfValueTranslation, - directEC = directEC, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/CompressionMetrics.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/CompressionMetrics.scala deleted file mode 100644 index e8acd3e494..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/CompressionMetrics.scala +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.metrics.api.MetricHandle.Histogram -import com.digitalasset.canton.metrics.LedgerApiServerMetrics - -object CompressionMetrics { - - final class Field(val compressed: Histogram, val uncompressed: Histogram) - - def exerciseArgument(metrics: LedgerApiServerMetrics) = - new Field( - compressed = metrics.index.db.compression.exerciseArgumentCompressed, - uncompressed = metrics.index.db.compression.exerciseArgumentUncompressed, - ) - - def exerciseResult(metrics: LedgerApiServerMetrics) = - new Field( - compressed = metrics.index.db.compression.exerciseResultCompressed, - uncompressed = metrics.index.db.compression.exerciseResultUncompressed, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/CompressionStrategy.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/CompressionStrategy.scala deleted file mode 100644 index dd532947ec..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/CompressionStrategy.scala +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.serialization.Compression - -import java.io.ByteArrayOutputStream - -final case class CompressionStrategy( - consumingExerciseArgumentCompression: FieldCompressionStrategy, - consumingExerciseResultCompression: FieldCompressionStrategy, - nonConsumingExerciseArgumentCompression: FieldCompressionStrategy, - nonConsumingExerciseResultCompression: FieldCompressionStrategy, -) - -object CompressionStrategy { - - def none(metrics: LedgerApiServerMetrics): CompressionStrategy = - buildUniform(Compression.Algorithm.None, metrics) - - def allGZIP(metrics: LedgerApiServerMetrics): CompressionStrategy = - buildUniform(Compression.Algorithm.GZIP, metrics) - - def buildFromConfig( - metrics: LedgerApiServerMetrics - )(consumingExercise: Boolean, nonConsumingExercise: Boolean): CompressionStrategy = { - val consumingAlgorithm: Compression.Algorithm = - if (consumingExercise) Compression.Algorithm.GZIP else Compression.Algorithm.None - val nonConsumingAlgorithm: Compression.Algorithm = - if (nonConsumingExercise) Compression.Algorithm.GZIP else Compression.Algorithm.None - build( - consumingAlgorithm, - consumingAlgorithm, - nonConsumingAlgorithm, - nonConsumingAlgorithm, - metrics, - ) - } - - def buildUniform( - algorithm: Compression.Algorithm, - metrics: LedgerApiServerMetrics, - ): CompressionStrategy = - build(algorithm, algorithm, algorithm, algorithm, metrics) - - def build( - consumingExerciseArgumentAlgorithm: Compression.Algorithm, - consumingExerciseResultAlgorithm: Compression.Algorithm, - nonConsumingExerciseArgumentAlgorithm: Compression.Algorithm, - nonConsumingExerciseResultAlgorithm: Compression.Algorithm, - metrics: LedgerApiServerMetrics, - ): CompressionStrategy = CompressionStrategy( - consumingExerciseArgumentCompression = FieldCompressionStrategy( - consumingExerciseArgumentAlgorithm, - CompressionMetrics.exerciseArgument(metrics), - ), - consumingExerciseResultCompression = FieldCompressionStrategy( - consumingExerciseResultAlgorithm, - CompressionMetrics.exerciseResult(metrics), - ), - nonConsumingExerciseArgumentCompression = FieldCompressionStrategy( - nonConsumingExerciseArgumentAlgorithm, - CompressionMetrics.exerciseArgument(metrics), - ), - nonConsumingExerciseResultCompression = FieldCompressionStrategy( - nonConsumingExerciseResultAlgorithm, - CompressionMetrics.exerciseResult(metrics), - ), - ) -} - -final case class FieldCompressionStrategy(id: Option[Int], compress: Array[Byte] => Array[Byte]) - -object FieldCompressionStrategy { - def apply(a: Compression.Algorithm, metric: CompressionMetrics.Field): FieldCompressionStrategy = - FieldCompressionStrategy( - a.id, - uncompressed => { - val output = new ByteArrayOutputStream(uncompressed.length) - val gzip = a.compress(output) - try { - gzip.write(uncompressed) - } finally { - gzip.close() - } - val compressed = output.toByteArray - output.close() - metric.compressed.update(compressed.length)(MetricsContext.Empty) - metric.uncompressed.update(uncompressed.length)(MetricsContext.Empty) - compressed - }, - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractLoader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractLoader.scala deleted file mode 100644 index 996ec14fdd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractLoader.scala +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.resources.ResourceOwner -import com.daml.metrics.InstrumentedGraph -import com.daml.metrics.api.MetricHandle.Histogram -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.ledger.error.LedgerApiErrors -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.{ - Active, - Archived, - ExistingContractStatus, -} -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.{BatchLoaderMetrics, LedgerApiServerMetrics} -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.backend.ContractStorageBackend -import com.digitalasset.canton.platform.store.backend.ContractStorageBackend.KeysPageQuery -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.util.PekkoUtil.syntax.* -import com.digitalasset.canton.util.TryUtil -import com.digitalasset.daml.lf.value.Value.ContractId -import io.grpc.{Metadata, StatusRuntimeException} -import org.apache.pekko.stream.scaladsl.{Keep, Sink, Source} -import org.apache.pekko.stream.{BoundedSourceQueue, Materializer, QueueOfferResult} - -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Success} - -trait Loader[Key, Value] { - - def load(key: Key)(implicit loggingContext: LoggingContextWithTrace): Future[Option[Value]] - -} - -class PekkoStreamParallelBatchedLoader[Key, Value]( - batchLoad: Seq[(Key, LoggingContextWithTrace)] => Future[Map[Key, Value]], - createQueue: () => Source[ - (Key, LoggingContextWithTrace, Promise[Option[Value]]), - BoundedSourceQueue[ - (Key, LoggingContextWithTrace, Promise[Option[Value]]) - ], - ], - maxBatchSize: Int, - parallelism: Int, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext, materializer: Materializer) - extends Loader[Key, Value] - with NamedLogging { - - private val (queue, done) = createQueue() - .batchN( - maxBatchSize = maxBatchSize, - maxBatchCount = parallelism, - ) - .mapAsyncUnordered(parallelism) { batch => - Future - .delegate( - batchLoad( - batch.view.map { case (key, loggingContext, _) => key -> loggingContext }.toSeq - ) - ) - .transform { - case Success(resultMap) => - batch.view.foreach { case (key, _, promise) => - promise.success(resultMap.get(key)) - () - } - TryUtil.unit - - case Failure(t) => - batch.view.foreach { case (_, _, promise) => - promise.failure( - t match { - case s: StatusRuntimeException => - // creates a new array under the hood, which prevents un-synchronized concurrent changes in the gRPC serving layer - val newMetadata = new Metadata() - newMetadata.merge(s.getTrailers) - new StatusRuntimeException(s.getStatus, newMetadata) - case other => other - } - ) - () - } - TryUtil.unit - } - } - .toMat(Sink.ignore)(Keep.both) - .run() - - override def load( - key: Key - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[Value]] = { - val promise = Promise[Option[Value]]() - queue.offer((key, loggingContext, promise)) match { - case QueueOfferResult.Enqueued => promise.future - - case QueueOfferResult.Dropped => - Future.failed( - LedgerApiErrors.ParticipantBackpressure - .Rejection("Too many pending contract lookups")( - ErrorLoggingContext(logger, loggingContext) - ) - .asGrpcError - ) - - // these should never happen, if this service is closed in the right order - case QueueOfferResult.QueueClosed => Future.failed(new IllegalStateException("Queue closed")) - case QueueOfferResult.Failure(t) => Future.failed(new IllegalStateException(t.getMessage)) - } - } - - def closeAsync(): Future[Unit] = { - queue.complete() - done.map(_ => ()) - } -} - -/** Efficient cross-request batching contract loader - * - * Note that both loaders operate on an identifier -> offset basis. The given offset of a request - * serves as a lower bound for the states. The states can be newer, but not older. We still need to - * have an upper bound of the requests as we don't want to read dirty states (due to parallel - * insertion). - */ -trait ContractLoader { - def contracts: Loader[(ContractId, Long), ExistingContractStatus] - def keys: Loader[KeysPageQuery, (Vector[ContractId], Option[Long])] -} - -object ContractLoader { - - private[events] def maxOffsetAndContextFromBatch[T]( - batch: Seq[((T, Long), LoggingContextWithTrace)], - histogram: Histogram, - ): (Long, LoggingContextWithTrace) = { - val ((_, latestValidAtEventSeqId), usedLoggingContext) = batch - .maxByOption(_._1._2) - .getOrElse( - throw new IllegalStateException("A batch should never be empty") - ) - histogram.update(batch.size)(MetricsContext.Empty) - (latestValidAtEventSeqId, usedLoggingContext) - } - - private[events] def createQueue[K, V](maxQueueSize: Int, metrics: BatchLoaderMetrics)(implicit - materializer: Materializer - ): Source[(K, LoggingContextWithTrace, Promise[Option[V]]), BoundedSourceQueue[ - (K, LoggingContextWithTrace, Promise[Option[V]]) - ]] = - InstrumentedGraph.queue( - bufferSize = maxQueueSize, - capacityCounter = metrics.bufferCapacity, - lengthCounter = metrics.bufferLength, - delayTimer = metrics.bufferDelay, - ) - - private def createContractBatchLoader( - contractStore: LedgerApiContractStore, - contractStorageBackend: ContractStorageBackend, - dbDispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - maxQueueSize: Int, - maxBatchSize: Int, - parallelism: Int, - loggerFactory: NamedLoggerFactory, - )(implicit - materializer: Materializer, - executionContext: ExecutionContext, - ): ResourceOwner[PekkoStreamParallelBatchedLoader[ - (ContractId, Long), - ExistingContractStatus, - ]] = - ResourceOwner - .forReleasable(() => - new PekkoStreamParallelBatchedLoader[ - (ContractId, Long), - ExistingContractStatus, - ]( - batchLoad = { batch => - val (latestValidAtEventSeqId, usedLoggingContext) = maxOffsetAndContextFromBatch( - batch, - metrics.index.db.activeContracts.batchSize, - ) - val contractIds = batch.map(_._1._1) - for { - contractIdToInternalContractId <- contractStore - .lookupBatchedInternalIdsNonReadThrough(contractIds)( - usedLoggingContext.traceContext - ) - internalContractIds = contractIds.flatMap(contractIdToInternalContractId.get) - contractStatuses <- dbDispatcher - .executeSql(metrics.index.db.lookupActiveContractsDbMetrics)( - contractStorageBackend.activeContracts( - internalContractIds = internalContractIds, - beforeEventSeqId = latestValidAtEventSeqId, - ) - )(usedLoggingContext) - } yield batch.view.flatMap { case ((contractId, beforeEventSeqId), _) => - contractIdToInternalContractId - .get(contractId) - .flatMap(contractStatuses.get) - .map { - case true => Active - case false => Archived - } - .map((contractId, beforeEventSeqId) -> _) - }.toMap - }, - createQueue = - () => ContractLoader.createQueue(maxQueueSize, metrics.index.db.activeContracts), - maxBatchSize = maxBatchSize, - parallelism = parallelism, - loggerFactory = loggerFactory, - ) - )(_.closeAsync()) - - private def createContractKeyBatchLoader( - contractStore: LedgerApiContractStore, - contractStorageBackend: ContractStorageBackend, - dbDispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - maxQueueSize: Int, - maxBatchSize: Int, - parallelism: Int, - loggerFactory: NamedLoggerFactory, - )(implicit - materializer: Materializer, - executionContext: ExecutionContext, - ): ResourceOwner[PekkoStreamParallelBatchedLoader[ - KeysPageQuery, - (Vector[ContractId], Option[Long]), - ]] = - ResourceOwner - .forReleasable(() => - new PekkoStreamParallelBatchedLoader[ - KeysPageQuery, - (Vector[ContractId], Option[Long]), - ]( - batchLoad = { batch => - // we can use the latest offset as the API only requires us to not return a state older than the given offset - val (latestValidAtEventSeqId, usedLoggingContext) = - ContractLoader.maxOffsetAndContextFromBatch( - batch.map { case (q, loggingContext) => - (((), q.validAtEventSeqId), loggingContext) - }, - metrics.index.db.activeContractKeys.batchSize, - ) - val queries = batch.map(_._1) - for { - pageResults <- dbDispatcher - .executeSql(metrics.index.db.lookupContractByKeyDbMetrics)( - contractStorageBackend.contractKeysPlain( - queries, - latestValidAtEventSeqId, - ) - )(usedLoggingContext) - allInternalIds = pageResults.flatMap(_.internalContractIds) - internalIdToContractId <- contractStore - .lookupBatchedContractIdsNonReadThrough(allInternalIds)( - usedLoggingContext.traceContext - ) - } yield queries - .zip(pageResults) - .map { case (query, result) => - val contractIds = - result.internalContractIds.flatMap(internalIdToContractId.get) - query -> (contractIds, result.nextPageToken) - } - .toMap - }, - createQueue = - () => ContractLoader.createQueue(maxQueueSize, metrics.index.db.activeContractKeys), - maxBatchSize = maxBatchSize, - parallelism = parallelism, - loggerFactory = loggerFactory, - ) - )(_.closeAsync()) - - private def fetchOneKey( - contractStorageBackend: ContractStorageBackend, - dbDispatcher: DbDispatcher, - contractStore: LedgerApiContractStore, - metrics: LedgerApiServerMetrics, - )( - query: KeysPageQuery - )(implicit - loggingContext: LoggingContextWithTrace, - ec: ExecutionContext, - ): Future[Option[(Vector[ContractId], Option[Long])]] = - for { - pageResult <- dbDispatcher - .executeSql(metrics.index.db.lookupContractByKeyDbMetrics)( - contractStorageBackend.contractKey(query) - ) - contractIdLookup <- contractStore - .lookupBatchedContractIdsNonReadThrough(pageResult.internalContractIds)( - loggingContext.traceContext - ) - } yield { - val contractIds = - pageResult.internalContractIds.flatMap(contractIdLookup.get) - Some((contractIds, pageResult.nextPageToken)) - } - - def create( - participantContractStore: LedgerApiContractStore, - contractStorageBackend: ContractStorageBackend, - dbDispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - maxQueueSize: Int, - maxBatchSize: Int, - parallelism: Int, - loggerFactory: NamedLoggerFactory, - )(implicit - materializer: Materializer, - executionContext: ExecutionContext, - ): ResourceOwner[ContractLoader] = - for { - contractsBatchLoader <- createContractBatchLoader( - participantContractStore, - contractStorageBackend, - dbDispatcher, - metrics, - maxQueueSize, - maxBatchSize, - parallelism, - loggerFactory, - ) - contractKeysBatchLoader <- - if (contractStorageBackend.supportsBatchKeyStateLookups) - createContractKeyBatchLoader( - participantContractStore, - contractStorageBackend, - dbDispatcher, - metrics, - maxQueueSize, - maxBatchSize, - parallelism, - loggerFactory, - ).map(Some(_)) - else ResourceOwner.successful(None) - } yield { - new ContractLoader { - override final val contracts: Loader[(ContractId, Long), ExistingContractStatus] = - new Loader[(ContractId, Long), ExistingContractStatus] { - override def load(key: (ContractId, Long))(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ExistingContractStatus]] = contractsBatchLoader.load(key) - } - override final val keys: Loader[KeysPageQuery, (Vector[ContractId], Option[Long])] = - contractKeysBatchLoader match { - case Some(batchLoader) => - new Loader[KeysPageQuery, (Vector[ContractId], Option[Long])] { - override def load(key: KeysPageQuery)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[(Vector[ContractId], Option[Long])]] = batchLoader.load(key) - } - case None => - new Loader[KeysPageQuery, (Vector[ContractId], Option[Long])] { - override def load(key: KeysPageQuery)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[(Vector[ContractId], Option[Long])]] = - fetchOneKey( - contractStorageBackend, - dbDispatcher, - participantContractStore, - metrics, - )(key) - } - } - } - } - - val dummyLoader = new ContractLoader { - override final val contracts: Loader[(ContractId, Long), ExistingContractStatus] = - new Loader[(ContractId, Long), ExistingContractStatus] { - override def load(key: (ContractId, Long))(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ExistingContractStatus]] = Future.successful(None) - } - override final val keys: Loader[KeysPageQuery, (Vector[ContractId], Option[Long])] = - new Loader[KeysPageQuery, (Vector[ContractId], Option[Long])] { - override def load(key: KeysPageQuery)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[(Vector[ContractId], Option[Long])]] = Future.successful(None) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractStateEvent.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractStateEvent.scala deleted file mode 100644 index ca222ccc79..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractStateEvent.scala +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.platform.* - -sealed trait ContractStateEvent extends Product with Serializable - -object ContractStateEvent { - final case class Created( - contractId: ContractId, - globalKey: Option[Key], - ) extends ContractStateEvent - final case class Archived( - contractId: ContractId, - globalKey: Option[Key], - ) extends ContractStateEvent - // This is merely a placeholder for now, sole purpose is to tick the StateCaches internal index - case object ReassignmentAccepted extends ContractStateEvent -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractsReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractsReader.scala deleted file mode 100644 index e499b8ae43..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/ContractsReader.scala +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.metrics.Timed -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.ExistingContractStatus -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.store.backend.ContractStorageBackend -import com.digitalasset.canton.platform.store.backend.ContractStorageBackend.KeysPageQuery -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader.* - -import scala.concurrent.{ExecutionContext, Future} - -private[dao] sealed class ContractsReader( - contractLoader: ContractLoader, - storageBackend: ContractStorageBackend, - dispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext) - extends LedgerDaoContractsReader - with NamedLogging { - - /** Batch lookup of contract keys directly from the database. - * - * Used to unit test the SQL queries for key lookups. Does not use the Pekko stream batch loader. - */ - override def lookupKeyStatesFromDb(keys: Seq[Key], notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Map[Key, Long]] = - Timed.future( - metrics.index.db.lookupKey, - dispatcher - .executeSql(metrics.index.db.lookupContractByKeyDbMetrics)( - storageBackend.contractKeysPlain( - keys.map(key => - KeysPageQuery( - key = key, - validAtEventSeqId = notEarlierThanEventSeqId, - limit = 1, - nextPageToken = None, - ) - ), - notEarlierThanEventSeqId, - ) - ) - .map { results => - keys - .zip(results) - .flatMap { case (key, result) => - result.internalContractIds.headOption.map(key -> _) - } - .toMap - }, - ) - - /** Lookup a contract key state at a specific ledger offset. - * - * @param key - * the contract key - * @param notEarlierThanEventSeqId - * the lower bound offset of the ledger for which to query for the key state - * @return - * the key state. - */ - override def lookupKeyState(key: Key, notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[KeyState] = - Timed.future( - metrics.index.db.lookupKey, - contractLoader.keys - .load( - KeysPageQuery( - key = key, - validAtEventSeqId = notEarlierThanEventSeqId, - limit = 1, - nextPageToken = None, - ) - ) - .map { - case Some((contractIds, _)) => - contractIds.headOption - .map(KeyAssigned.apply) - .getOrElse(KeyUnassigned) - case None => - logger - .error( - s"Key $key resulted in an invalid empty load at offset $notEarlierThanEventSeqId" - )(loggingContext.traceContext) - KeyUnassigned - }, - ) - - override def lookupContractState(contractId: ContractId, notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ExistingContractStatus]] = - Timed.future( - metrics.index.db.lookupActiveContract, - contractLoader.contracts.load(contractId -> notEarlierThanEventSeqId), - ) - - /** Looks up active contracts for a given key. - * - * Due to batching of several requests, we may return newer information than at the provided - * offset, but never older information. - * - * @param key - * the contract key to query - * @param notEarlierThanEventSeqId - * the offset threshold to resolve the key state (state can be newer, but not older) - * @param nextPageToken - * pagination token for fetching subsequent pages - * @param limit - * maximum number of contract IDs to return - * @return - * a vector of active contract IDs and an optional next page token - */ - override def lookupNonUniqueKey( - key: Key, - notEarlierThanEventSeqId: Long, - nextPageToken: Option[Long], - limit: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[(Vector[ContractId], Option[Long])] = - Timed.future( - metrics.index.db.lookupNonUniqueKey, - contractLoader.keys - .load( - KeysPageQuery( - key = key, - limit = limit, - nextPageToken = nextPageToken, - validAtEventSeqId = notEarlierThanEventSeqId, - ) - ) - .map { - case Some(result) => result - case None => - logger.error( - s"Non-unique key lookup for $key resulted in an invalid empty load" - )(loggingContext.traceContext) - (Vector.empty, None) - }, - ) -} - -private[dao] object ContractsReader { - - private[dao] def apply( - contractLoader: ContractLoader, - dispatcher: DbDispatcher, - metrics: LedgerApiServerMetrics, - storageBackend: ContractStorageBackend, - loggerFactory: NamedLoggerFactory, - )(implicit ec: ExecutionContext): ContractsReader = - new ContractsReader( - contractLoader = contractLoader, - storageBackend = storageBackend, - dispatcher = dispatcher, - metrics = metrics, - loggerFactory = loggerFactory, - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventIdsUtils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventIdsUtils.scala deleted file mode 100644 index d0ab0e8eed..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventIdsUtils.scala +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source - -import scala.annotation.tailrec - -object EventIdsUtils { - - def sortAndDeduplicateIds( - descendingOrder: Boolean - )(ids: Vector[Source[Long, NotUsed]]): Source[Long, NotUsed] = { - implicit val ord: Ordering[Long] = if (descendingOrder) Ordering.Long.reverse else Ordering.Long - mergeSort(ids).statefulMapConcat(statefulDeduplicate) - } - - @tailrec - protected[events] def mergeSort[T: Ordering]( - sources: Vector[Source[T, NotUsed]] - ): Source[T, NotUsed] = - sources match { - case Vector(first, second, _*) => - mergeSort( - sources - .drop(2) - .appended(first.mergeSorted(second)) - ) - case Vector(head) => head - case _ => Source.empty - } - - @SuppressWarnings( - Array( - "org.wartremover.warts.Null", - "org.wartremover.warts.AsInstanceOf", - "org.wartremover.warts.Var", - ) - ) - protected[events] def statefulDeduplicate[T]: () => T => List[T] = - () => { - var last = null.asInstanceOf[T] - elem => - if (elem == last) Nil - else { - last = elem - List(elem) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsRange.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsRange.scala deleted file mode 100644 index c813decb5c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsRange.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.data.Offset - -// [startInclusive, endInclusive] -final case class EventsRange( - startInclusiveOffset: Offset, - startInclusiveEventSeqId: Long, - endInclusiveOffset: Offset, - endInclusiveEventSeqId: Long, -) diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsReader.scala deleted file mode 100644 index 015a53d131..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsReader.scala +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import cats.data.OptionT -import com.daml.ledger.api.v2.event_query_service.{Archived, Created, GetEventsByContractIdResponse} -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.InternalEventFormat -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.ScalaPbStreamingOptimizations.ScalaPbMessageWithPrecomputedSerializedSize -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{ - FatCreatedEventProperties, - RawFatCreatedEvent, -} -import com.digitalasset.canton.platform.store.backend.{EventStorageBackend, ParameterStorageBackend} -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.platform.store.dao.{DbDispatcher, LedgerDaoEventsReader} -import com.digitalasset.canton.util.FutureInstances -import com.digitalasset.daml.lf.value.Value.ContractId - -import scala.concurrent.{ExecutionContext, Future} - -private[dao] sealed class EventsReader( - val dbDispatcher: DbDispatcher, - val eventStorageBackend: EventStorageBackend, - val parameterStorageBackend: ParameterStorageBackend, - val metrics: LedgerApiServerMetrics, - val lfValueTranslation: LfValueTranslation, - val contractStore: LedgerApiContractStore, - val ledgerEndCache: LedgerEndCache, - override val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext) - extends LedgerDaoEventsReader - with NamedLogging { - protected val dbMetrics: metrics.index.db.type = metrics.index.db - - override def getEventsByContractId( - contractId: ContractId, - internalEventFormatO: Option[InternalEventFormat], - )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractIdResponse] = { - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext(logger, loggingContext) - (for { - internalEventFormat <- OptionT.fromOption[Future](internalEventFormatO) - internalContractId <- OptionT( - contractStore - .lookupBatchedInternalIdsNonReadThrough(List(contractId)) - .map(_.values.headOption) - ) - (create, archiveO) <- OptionT( - dbDispatcher - .executeSql(dbMetrics.getEventsByContractId)( - eventStorageBackend.eventReaderQueries.fetchContractIdEvents( - internalContractId, - requestingParties = internalEventFormat.templatePartiesFilter.allFilterParties, - endEventSequentialId = ledgerEndCache().map(_.lastEventSeqId).getOrElse(0L), - ) - ) - .map { - case (None, _) => None - case (Some(create), archive) => Some(create -> archive) - } - ) - // if the fat contract cannot be found we short circuit to none - fatContract <- OptionT( - UpdateReader - .withFatContractIfNeeded(contractStore)(Vector(create)) - .map(_.headOption.flatMap(_._2)) - ) - fatCreatedEvent = RawFatCreatedEvent( - transactionProperties = create.transactionProperties, - fatCreatedEventProperties = FatCreatedEventProperties( - thinCreatedEventProperties = create.thinCreatedEventProperties, - fatContract = fatContract, - ), - ) - // enough to filter the create, they have the same template and witnesses with the archive - _ <- OptionT.when( - UpdateReader - .eventFilter(Some(internalEventFormat.templatePartiesFilter)) - .apply(fatCreatedEvent) - )(())(FutureInstances.parallelApplicativeFuture) - deserializedCreateEvent <- OptionT.liftF( - UpdateReader - .deserializeRawTransactionEvent( - eventProjectionProperties = internalEventFormat.eventProjectionProperties, - lfValueTranslation = lfValueTranslation, - )(fatCreatedEvent) - .map(_.getCreated) - ) - deserializedArchivedEvent = archiveO - .map(_.copy(filteredStakeholderParties = fatCreatedEvent.witnessParties)) - .map( - UpdateReader.archivedEvent( - internalEventFormat.eventProjectionProperties, - lfValueTranslation, - ) - ) - } yield GetEventsByContractIdResponse( - created = Some( - Created( - createdEvent = Some(deserializedCreateEvent), - synchronizerId = fatCreatedEvent.synchronizerId, - ) - ), - archived = deserializedArchivedEvent.map(archivedEvent => - Archived( - archivedEvent = Some(archivedEvent), - synchronizerId = fatCreatedEvent.synchronizerId, - ) - ), - ).withPrecomputedSerializedSize()).value.flatMap { - case Some(result) => Future.successful(result) - case None => - Future.failed( - RequestValidationErrors.NotFound.ContractEvents - .Reject(contractId) - .asGrpcError - ) - } - } - - // TODO(i16065): Re-enable getEventsByContractKey tests -// override def getEventsByContractKey( -// contractKey: Value, -// templateId: Ref.Identifier, -// requestingParties: Set[Party], -// endExclusiveSeqId: Option[EventSequentialId], -// maxIterations: Int, -// )(implicit loggingContext: LoggingContextWithTrace): Future[GetEventsByContractKeyResponse] = { -// val keyHash: String = -// platform.Key.assertBuild(templateId, contractKey).hash.bytes.toHexString -// -// val eventProjectionProperties = EventProjectionProperties( -// // Used by LfEngineToApi -// verbose = true, -// // Needed to get create arguments mapped -// wildcardWitnesses = requestingParties.map(_.toString), -// ) -// -// for { -// -// ( -// rawCreate: Option[FlatEvent.Created], -// rawArchive: Option[FlatEvent.Archived], -// eventSequentialId, -// ) <- dbDispatcher -// .executeSql(dbMetrics.getEventsByContractKey) { conn => -// eventStorageBackend.eventReaderQueries.fetchNextKeyEvents( -// keyHash, -// requestingParties, -// endExclusiveSeqId.getOrElse(ledgerEndCache()._2 + 1), -// maxIterations, -// )(conn) -// } -// -// createEvent <- rawCreate.fold(Future[Option[CreatedEvent]](None)) { e => -// e.deserializeCreateEvent(lfValueTranslation, eventProjectionProperties).map(Some(_)) -// } -// archiveEvent = rawArchive.map(_.deserializedArchivedEvent()) -// -// continuationToken = eventSequentialId -// .map(_.toString) -// .getOrElse(GetEventsByContractKeyResponse.defaultInstance.continuationToken) -// -// } yield { -// GetEventsByContractKeyResponse(createEvent, archiveEvent, continuationToken) -// } -// } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsTable.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsTable.scala deleted file mode 100644 index 39bd67b189..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/EventsTable.scala +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.state_service.ParticipantPermission -import com.daml.ledger.api.v2.state_service.ParticipantPermission.* -import com.daml.ledger.api.v2.topology_transaction.{ - ParticipantAuthorizationAdded, - ParticipantAuthorizationChanged, - ParticipantAuthorizationOnboarding, - ParticipantAuthorizationRevoked, - TopologyEvent, - TopologyTransaction, -} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.util.TimestampConversion -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel.* -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.{ - AuthorizationEvent, - AuthorizationLevel, -} -import com.digitalasset.canton.platform.store.ScalaPbStreamingOptimizations.ScalaPbMessageWithPrecomputedSerializedSize -import com.digitalasset.canton.platform.store.backend.Conversions -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.RawParticipantAuthorization -import com.typesafe.scalalogging.Logger - -object EventsTable { - - object TransactionConversions { - def toParticipantPermission(level: AuthorizationLevel): ParticipantPermission = level match { - case Submission => PARTICIPANT_PERMISSION_SUBMISSION - case Confirmation => PARTICIPANT_PERMISSION_CONFIRMATION - case Observation => PARTICIPANT_PERMISSION_OBSERVATION - } - - def toTopologyEvent( - partyId: String, - participantId: String, - authorizationEvent: AuthorizationEvent, - ): TopologyEvent = - TopologyEvent { - authorizationEvent match { - case AuthorizationEvent.Added(level) => - TopologyEvent.Event.ParticipantAuthorizationAdded( - ParticipantAuthorizationAdded( - partyId = partyId, - participantId = participantId, - participantPermission = toParticipantPermission(level), - ) - ) - case AuthorizationEvent.ChangedTo(level) => - TopologyEvent.Event.ParticipantAuthorizationChanged( - ParticipantAuthorizationChanged( - partyId = partyId, - participantId = participantId, - participantPermission = toParticipantPermission(level), - ) - ) - case AuthorizationEvent.Revoked => - TopologyEvent.Event.ParticipantAuthorizationRevoked( - ParticipantAuthorizationRevoked( - partyId = partyId, - participantId = participantId, - ) - ) - case AuthorizationEvent.Onboarding(level) => - TopologyEvent.Event.ParticipantAuthorizationOnboarding( - ParticipantAuthorizationOnboarding( - partyId = partyId, - participantId = participantId, - participantPermission = toParticipantPermission(level), - ) - ) - } - } - - def toTopologyTransaction(logger: Logger)( - events: Vector[RawParticipantAuthorization] - ): Option[(Offset, TopologyTransaction)] = - events.headOption.map { first => - first.offset -> - TopologyTransaction( - updateId = first.updateId, - events = events - .map(event => - toTopologyEvent( - partyId = event.partyId, - participantId = event.participantId, - authorizationEvent = event.authorizationEvent, - ) - ), - offset = first.offset.unwrap, - synchronizerId = first.synchronizerId, - traceContext = Conversions.protoTraceContextFrom(logger)(first.traceContext), - recordTime = Some(TimestampConversion.fromLf(first.recordTime)), - ).withPrecomputedSerializedSize() - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/FilterUtils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/FilterUtils.scala deleted file mode 100644 index aeec8251b6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/FilterUtils.scala +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.platform.{Party, TemplatePartiesFilter} -import com.digitalasset.daml.lf.data.Ref.NameTypeConRef - -final case class DecomposedFilter(party: Option[Party], templateId: Option[NameTypeConRef]) - -object FilterUtils { - def decomposeFilters(filter: TemplatePartiesFilter): Seq[DecomposedFilter] = { - val wildcardFilters = - filter.templateWildcardParties match { - case Some(parties) => - parties.map(party => DecomposedFilter(Some(party), None)) - case None => Seq(DecomposedFilter(None, None)) - } - val filters = filter.relation.iterator.flatMap { - case (templateId, Some(parties)) => - parties.iterator.map(party => DecomposedFilter(Some(party), Some(templateId))) - case (templateId, None) => - Iterator(DecomposedFilter(None, Some(templateId))) - }.toVector ++ wildcardFilters - filters - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/IdPageSizing.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/IdPageSizing.scala deleted file mode 100644 index 634a917916..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/IdPageSizing.scala +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.tracing.TraceContext - -/** The size of a page is the number of ids in the page. - */ -final case class IdPageSizing( - minPageSize: Int, - maxPageSize: Int, -) { - assert(minPageSize > 0) - assert(maxPageSize >= minPageSize) -} - -object IdPageSizing { - - // Approximation of how many index entries is present in a leaf page of a btree index. - // Fetching fewer ids than this only adds round-trip overhead, without decreasing the number of disk page reads per round-trip. - // Experiments, with default fill ratio for BTree Index, show that: - // - (party_id, template_id) index has 244 tuples per disk page, - // - wildcard party_id index has 254 per disk page. - // We are picking a smaller number to accommodate for pruning, deletions and index bloat effect - // which all result in a smaller ratio of tuples per disk page. - private val NumOfBtreeLeafPageEntriesApprox = 200 - - /** Calculates the ideal page sizes to fetch ids with. - */ - def calculateFrom( - maxIdPageSize: Int, - workingMemoryInBytesForIdPages: Int, - numOfDecomposedFilters: Int, - numOfPagesInIdPageBuffer: Int, - loggerFactory: NamedLoggerFactory, - )(implicit traceContext: TraceContext): IdPageSizing = { - val logger = loggerFactory.getTracedLogger(getClass) - val calculated = calculateMaxNumOfIdsPerPage( - workingMemoryInBytesForIdPages = workingMemoryInBytesForIdPages, - numOfDecomposedFilters = numOfDecomposedFilters, - numOfPagesInIdPageBuffer = numOfPagesInIdPageBuffer, - ) - // maxNumberOfIdsPerIdPage can override this if it is smaller - val minIdPageSize = Math.min(10, maxIdPageSize) - // maxNumberOfIdsPerIdPage can override this if it is smaller - val recommendedIdPageSize = Math.min(NumOfBtreeLeafPageEntriesApprox, maxIdPageSize) - if (calculated < minIdPageSize) { - logger.warn( - s"Calculated maximum ID page size supporting API stream memory limits [$calculated] is too low: $minIdPageSize is used instead. " + - s"Warning: API stream memory limits not respected. Warning: Dangerously low maximum ID page size can cause poor streaming performance. " + - s"Filter size [$numOfDecomposedFilters] too large?" - ) - IdPageSizing(minIdPageSize, minIdPageSize) - } else if (calculated < recommendedIdPageSize) { - logger.warn( - s"Calculated maximum ID page size supporting API stream memory limits [$calculated] is very low. " + - s"Warning: Low maximum ID page size can cause poor streaming performance. Filter size [$numOfDecomposedFilters] too large?" - ) - IdPageSizing(calculated, calculated) - } else if (calculated < maxIdPageSize) { - logger.info( - s"Calculated maximum ID page size supporting API stream memory limits [$calculated] is low. " + - s"Warning: Low maximum ID page size can cause poor streaming performance. Filter size [$numOfDecomposedFilters] too large?" - ) - IdPageSizing(recommendedIdPageSize, calculated) - } else { - logger.debug( - s"Calculated maximum ID page size supporting API stream memory limits [$calculated] is high, using [$maxIdPageSize] instead." - ) - IdPageSizing(recommendedIdPageSize, maxIdPageSize) - } - } - - private def calculateMaxNumOfIdsPerPage( - workingMemoryInBytesForIdPages: Int, - numOfDecomposedFilters: Int, - numOfPagesInIdPageBuffer: Int, - ): Int = { - // An id occupies 8 bytes (it's a 64-bit long) - val numOfIdsInMemory = workingMemoryInBytesForIdPages / 8 - // For each decomposed filter we have: - // 1) one page fetched for merge sorting - // 2) and additional pages residing in the buffer. - val maxNumOfIdPages = (numOfPagesInIdPageBuffer + 1) * numOfDecomposedFilters - val maxNumOfIdsPerPage = numOfIdsInMemory / maxNumOfIdPages - maxNumOfIdsPerPage - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/InputContractPackages.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/InputContractPackages.scala deleted file mode 100644 index c55aff53ad..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/InputContractPackages.scala +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import cats.implicits.toFunctorOps -import com.digitalasset.canton.LfPackageId -import com.digitalasset.canton.protocol.GenContractInstance -import com.digitalasset.daml.lf.data -import com.digitalasset.daml.lf.data.Relation -import com.digitalasset.daml.lf.transaction.{FatContractInstance, Node, Transaction} -import com.digitalasset.daml.lf.value.Value.ContractId - -object InputContractPackages { - - /** Returns a mapping from all contract ids referenced in the transaction to their package ids, - * excluding those that are created within the transaction. - */ - def forTransaction(tx: Transaction): data.Relation[ContractId, LfPackageId] = - tx.fold(data.Relation.empty[ContractId, LfPackageId]) { - case ( - acc, - (_, Node.Exercise(coid, _, templateId, _, _, _, _, _, _, _, _, _, _, _, _, _, _)), - ) if !tx.localContractIds.contains(coid) => - Relation.update(acc, coid, templateId.packageId) - case (acc, (_, Node.Fetch(coid, _, templateId, _, _, _, _, _, _, _))) - if !tx.localContractIds.contains(coid) => - Relation.update(acc, coid, templateId.packageId) - case (acc, (_, Node.QueryByKey(_, templateId, _, _, result, _))) => - Relation.union( - acc, - data.Relation.from( - result.filterNot(tx.localContractIds).map(c => c -> templateId.packageId) - ), - ) - case (acc, _) => acc - } - - /** Merges two maps, returning an error if their key sets differ. */ - private[events] def strictZipByKey[K, V1, V2]( - m1: Map[K, V1], - m2: Map[K, V2], - ): Either[Set[K], Map[K, (V1, V2)]] = { - val keys1 = m1.keySet - val keys2 = m2.keySet - Either.cond( - keys1 == keys2, - keys1.view.map(k => k -> (m1(k), m2(k))).toMap, - (keys1 union keys2) -- (keys1 intersect keys2), - ) - } - - /** Returns a mapping from all contract ids referenced in the transaction to their (contract - * instance, package id), excluding those that are created within the transaction. Fails if the - * set of contract ids in the transaction and in the provided contracts differ. - */ - def forTransactionWithContracts( - tx: Transaction, - contracts: Map[ContractId, GenContractInstance], - ): Either[Set[ContractId], Map[ContractId, (FatContractInstance, Set[LfPackageId])]] = - strictZipByKey(contracts.fmap(_.inst), forTransaction(tx)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/LfEnricher.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/LfEnricher.scala deleted file mode 100644 index 68d3dd9d35..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/LfEnricher.scala +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.PackageId as LfPackageId -import com.digitalasset.canton.platform.packages.DeduplicatingPackageLoader -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref.{ChoiceName, Identifier} -import com.digitalasset.daml.lf.engine as LfEngine -import com.digitalasset.daml.lf.engine.{Engine, Enricher} -import com.digitalasset.daml.lf.value.Value - -import scala.concurrent.{ExecutionContext, Future} - -/** Enricher for LF values. - */ -trait LfEnricher { - - def enrichContractValue(tyCon: Identifier, value: Value)(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] - - def enrichChoiceArgument( - toIdentifier: Identifier, - interfaceId: Option[Identifier], - choiceName: ChoiceName, - unversioned: Value, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] - - def enrichChoiceResult( - toIdentifier: Identifier, - interfaceId: Option[Identifier], - choiceName: ChoiceName, - unversioned: Value, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] - - def enrichContractKey(toIdentifier: Identifier, value: Value)(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] - - def enrichView(interfaceId: Identifier, value: Value)(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] -} - -object LfEnricher { - import LoggingContextWithTrace.implicitExtractTraceContext - - def apply( - engine: Engine, - forbidLocalContractIds: Boolean, - metrics: LedgerApiServerMetrics, - packageLoader: DeduplicatingPackageLoader, - loadPackage: ( - LfPackageId, - TraceContext, - ) => Future[Option[com.digitalasset.daml.lf.archive.DamlLf.Archive]], - ): LfEnricher = - new Impl( - Enricher( - engine = engine, - addTrailingNoneFields = false, - forbidLocalContractIds = forbidLocalContractIds, - ), - metrics, - packageLoader, - loadPackage, - ) - - private class Impl( - delegate: Enricher, - metrics: LedgerApiServerMetrics, - packageLoader: DeduplicatingPackageLoader, - loadPackage: ( - LfPackageId, - TraceContext, - ) => Future[Option[com.digitalasset.daml.lf.archive.DamlLf.Archive]], - ) extends LfEnricher { - - override def enrichContractValue(tyCon: Identifier, value: Value)(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] = - consume(delegate.enrichContract(tyCon, value)) - - override def enrichChoiceArgument( - toIdentifier: Identifier, - interfaceId: Option[Identifier], - choiceName: ChoiceName, - unversioned: Value, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] = - consume(delegate.enrichChoiceArgument(toIdentifier, interfaceId, choiceName, unversioned)) - - override def enrichChoiceResult( - toIdentifier: Identifier, - interfaceId: Option[Identifier], - choiceName: ChoiceName, - unversioned: Value, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] = - consume(delegate.enrichChoiceResult(toIdentifier, interfaceId, choiceName, unversioned)) - - override def enrichContractKey(toIdentifier: Identifier, value: Value)(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] = - consume(delegate.enrichContractKey(toIdentifier, value)) - - override def enrichView(interfaceId: Identifier, value: Value)(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[Value] = - consume(delegate.enrichView(interfaceId, value)) - - private def consume[V]( - result: LfEngine.Result[V] - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[V] = - result match { - case LfEngine.ResultDone(r) => Future.successful(r) - case LfEngine.ResultNeedPackage(packageId, resume) => - packageLoader - .loadPackage( - packageId = packageId, - delegate = packageId => loadPackage(packageId, loggingContext.traceContext), - metric = metrics.index.db.translation.getLfPackage, - ) - .flatMap(pkgO => consume(resume(pkgO))) - case LfEngine.ResultError(e) => Future.failed(new RuntimeException(e.message)) - case result => - Future.failed(new RuntimeException(s"Unexpected ValueEnricher result: $result")) - } - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/LfValueTranslation.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/LfValueTranslation.scala deleted file mode 100644 index a6757f888e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/LfValueTranslation.scala +++ /dev/null @@ -1,565 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.event.{CreatedEvent, ExercisedEvent, InterfaceView} -import com.daml.ledger.api.v2.value -import com.daml.ledger.api.v2.value.{Record as ApiRecord, Value as ApiValue} -import com.daml.metrics.Timed -import com.digitalasset.canton.ledger.api.util.{LfEngineToApi, TimestampConversion} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.services.{ErrorCause, RejectionGenerators} -import com.digitalasset.canton.platform.packages.DeduplicatingPackageLoader -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.RawExercisedEvent -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties -import com.digitalasset.canton.platform.store.dao.events.LfValueTranslation.ApiContractData -import com.digitalasset.canton.platform.store.serialization.{Compression, ValueSerializer} -import com.digitalasset.canton.platform.{ - ContractId, - Create, - Exercise, - Identifier as LfIdentifier, - PackageId as LfPackageId, - Value as LfValue, -} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.MonadUtil -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{FullIdentifier, Identifier} -import com.digitalasset.daml.lf.engine.Engine -import com.digitalasset.daml.lf.transaction.* -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.VersionedValue -import com.digitalasset.daml.lf.{crypto, engine as LfEngine} -import com.google.protobuf.ByteString -import com.google.rpc.Status -import com.google.rpc.status.Status as ProtoStatus -import io.grpc.Status.Code - -import java.io.ByteArrayInputStream -import scala.concurrent.{ExecutionContext, Future} -import scala.util.chaining.* - -/** Serializes and deserializes Daml-Lf values and events. - * - * Deserializing values in verbose mode involves loading packages in order to fill in missing type - * information. That's why these methods return Futures, while the serialization methods are - * synchronous. - */ -trait LfValueSerialization { - def serialize( - contractId: ContractId, - contractArgument: VersionedValue, - ): Array[Byte] - - /** Returns (contract argument, contract key) */ - def serialize(create: Create): (Array[Byte], Option[Array[Byte]]) - - /** Returns (choice argument, exercise result, contract key) */ - def serialize( - exercise: Exercise - ): (Array[Byte], Option[Array[Byte]], Option[Array[Byte]]) -} - -final class LfValueTranslation( - metrics: LedgerApiServerMetrics, - // Note: LfValueTranslation is used by JdbcLedgerDao for both serialization and deserialization. - // Sometimes the JdbcLedgerDao is used in a way that it never needs to deserialize data in verbose mode - // (e.g., the indexer, or some tests). In this case, the engine is not required. - engineO: Option[Engine], - loadPackage: ( - LfPackageId, - TraceContext, - ) => Future[Option[com.digitalasset.daml.lf.archive.DamlLf.Archive]], - val loggerFactory: NamedLoggerFactory, -) extends LfValueSerialization - with NamedLogging { - import LoggingContextWithTrace.implicitExtractTraceContext - - private[this] val packageLoader = new DeduplicatingPackageLoader() - - private val enricherO: Option[LfEnricher] = engineO.map(engine => - LfEnricher( - engine = engine, - forbidLocalContractIds = engine.config.forbidLocalContractIds, - metrics, - packageLoader, - loadPackage, - ) - ) - - private def cantSerialize(attribute: String, forContract: ContractId): String = - s"Cannot serialize $attribute for ${forContract.coid}" - - private def serializeCreateArgOrThrow( - contractId: ContractId, - arg: VersionedValue, - ): Array[Byte] = - ValueSerializer.serializeValue( - value = arg, - errorContext = cantSerialize(attribute = "create argument", forContract = contractId), - ) - - private def serializeCreateArgOrThrow(c: Create): Array[Byte] = - serializeCreateArgOrThrow(c.coid, c.versionedArg) - - private def serializeNullableKeyOrThrow(c: Create): Option[Array[Byte]] = - c.versionedKey.map(k => - ValueSerializer.serializeValue( - value = k.map(_.value), - errorContext = cantSerialize(attribute = "key", forContract = c.coid), - ) - ) - - private def serializeNullableKeyOrThrow(e: Exercise): Option[Array[Byte]] = - e.versionedKey.map(k => - ValueSerializer.serializeValue( - value = k.map(_.value), - errorContext = cantSerialize(attribute = "key", forContract = e.targetCoid), - ) - ) - - private def serializeExerciseArgOrThrow(e: Exercise): Array[Byte] = - ValueSerializer.serializeValue( - value = e.versionedChosenValue, - errorContext = cantSerialize(attribute = "exercise argument", forContract = e.targetCoid), - ) - - private def serializeNullableExerciseResultOrThrow(e: Exercise): Option[Array[Byte]] = - e.versionedExerciseResult.map(exerciseResult => - ValueSerializer.serializeValue( - value = exerciseResult, - errorContext = cantSerialize(attribute = "exercise result", forContract = e.targetCoid), - ) - ) - - override def serialize( - contractId: ContractId, - contractArgument: VersionedValue, - ): Array[Byte] = - serializeCreateArgOrThrow(contractId, contractArgument) - - override def serialize(create: Create): (Array[Byte], Option[Array[Byte]]) = - serializeCreateArgOrThrow(create) -> serializeNullableKeyOrThrow(create) - - override def serialize( - exercise: Exercise - ): (Array[Byte], Option[Array[Byte]], Option[Array[Byte]]) = - ( - serializeExerciseArgOrThrow(exercise), - serializeNullableExerciseResultOrThrow(exercise), - serializeNullableKeyOrThrow(exercise), - ) - - def toApiValue( - value: LfValue, - verbose: Boolean, - attribute: => String, - enrich: LfValue => Future[com.digitalasset.daml.lf.value.Value], - )(implicit - ec: ExecutionContext - ): Future[ApiValue] = for { - enrichedValue <- - if (verbose) - enrich(value) - else - Future.successful(value.unversioned) - } yield { - LfEngineToApi.assertOrRuntimeEx( - failureContext = s"attempting to deserialize persisted $attribute to value", - LfEngineToApi - .lfValueToApiValue( - verbose = verbose, - value0 = enrichedValue, - ), - ) - } - - private def decompressAndDeserialize(algorithm: Compression.Algorithm, value: Array[Byte]) = - ValueSerializer.deserializeValue(algorithm.decompress(new ByteArrayInputStream(value))) - - def enricher: LfEnricher = - enricherO.getOrElse( - sys.error( - "LfValueTranslation used to deserialize values in verbose mode without an Engine" - ) - ) - - def engine: Engine = - engineO.getOrElse( - sys.error( - "LfValueTranslation used to deserialize values in verbose mode without an Engine" - ) - ) - - def deserializeRawExercised( - eventProjectionProperties: EventProjectionProperties, - rawExercisedEvent: RawExercisedEvent, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[ExercisedEvent] = - for { - // Deserialize contract argument and contract key - // This returns the values in Daml-LF format. - exerciseArgument <- Future( - decompressAndDeserialize( - Compression.Algorithm - .assertLookup(rawExercisedEvent.exerciseArgumentCompression), - rawExercisedEvent.exerciseArgument, - ) - ) - exerciseResult <- Future( - rawExercisedEvent.exerciseResult.map( - decompressAndDeserialize( - Compression.Algorithm - .assertLookup(rawExercisedEvent.exerciseResultCompression), - _, - ) - ) - ) - Ref.QualifiedChoiceId(interfaceId, choiceName) = - Ref.QualifiedChoiceId( - rawExercisedEvent.exerciseChoiceInterface, - rawExercisedEvent.exerciseChoice, - ) - // Convert Daml-LF values to ledger API values. - // In verbose mode, this involves loading Daml-LF packages and filling in missing type information. - choiceArgument <- toApiValue( - value = exerciseArgument, - verbose = eventProjectionProperties.verbose, - attribute = "exercise argument", - enrich = value => - enricher.enrichChoiceArgument( - rawExercisedEvent.templateId.toIdentifier, - interfaceId, - choiceName, - value.unversioned, - ), - ) - exerciseResult <- exerciseResult match { - case Some(result) => - toApiValue( - value = result, - verbose = eventProjectionProperties.verbose, - attribute = "exercise result", - enrich = value => - enricher.enrichChoiceResult( - rawExercisedEvent.templateId.toIdentifier, - interfaceId, - choiceName, - value.unversioned, - ), - ).map(Some(_)) - case None => Future.successful(None) - } - } yield ExercisedEvent( - offset = rawExercisedEvent.offset, - nodeId = rawExercisedEvent.nodeId, - contractId = rawExercisedEvent.contractId.coid, - templateId = Some( - LfEngineToApi.toApiIdentifier(rawExercisedEvent.templateId.toIdentifier) - ), - interfaceId = interfaceId.map( - LfEngineToApi.toApiIdentifier - ), - choice = choiceName, - choiceArgument = Some(choiceArgument), - actingParties = rawExercisedEvent.exerciseActors.toSeq, - consuming = rawExercisedEvent.exerciseConsuming, - witnessParties = rawExercisedEvent.witnessParties.toSeq, - lastDescendantNodeId = rawExercisedEvent.exerciseLastDescendantNodeId, - exerciseResult = exerciseResult, - packageName = rawExercisedEvent.templateId.pkgName, - implementedInterfaces = - if (rawExercisedEvent.exerciseConsuming) - implementedInterfaces( - eventProjectionProperties, - rawExercisedEvent.witnessParties, - rawExercisedEvent.templateId, - ) - else Nil, - acsDelta = rawExercisedEvent.acsDeltaForWitnesses, - ) - - def toApiCreatedEvent( - eventProjectionProperties: EventProjectionProperties, - fatContractInstance: FatContractInstance, - offset: Long, - nodeId: Int, - representativePackageId: LfPackageId, - witnesses: Set[String], - acsDelta: Boolean, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[CreatedEvent] = { - val createArgument = fatContractInstance.createArg - val createKey = fatContractInstance.contractKeyWithMaintainers.map(_.globalKey) - - val representativeTemplateId = - fatContractInstance.templateId - .toFullIdentifier(fatContractInstance.packageName) - .copy(pkgId = representativePackageId) - - for { - apiContractData <- toApiContractData( - value = createArgument, - keyO = createKey, - representativeTemplateId = representativeTemplateId, - witnesses = witnesses, - eventProjectionProperties = eventProjectionProperties, - fatContractInstance = fatContractInstance, - ) - } yield CreatedEvent( - offset = offset, - nodeId = nodeId, - contractId = fatContractInstance.contractId.coid, - templateId = Some( - LfEngineToApi.toApiIdentifier(fatContractInstance.templateId) - ), - contractKey = apiContractData.contractKey.map(_._1), - contractKeyHash = apiContractData.contractKey.fold(ByteString.EMPTY)(_._2.bytes.toByteString), - createArguments = Some(apiContractData.createArguments), - createdEventBlob = apiContractData.createdEventBlob.getOrElse(ByteString.EMPTY), - interfaceViews = apiContractData.interfaceViews, - witnessParties = witnesses.toSeq, - signatories = fatContractInstance.signatories.toSeq, - observers = fatContractInstance.stakeholders.diff(fatContractInstance.signatories).toSeq, - createdAt = fatContractInstance.createdAt match { - case CreationTime.CreatedAt(t) => Some(TimestampConversion.fromLf(t)) - case _ => None - }, - packageName = fatContractInstance.packageName, - acsDelta = acsDelta, - representativePackageId = representativePackageId, - ) - } - - private def toApiContractData( - value: Value, - keyO: Option[GlobalKey], - representativeTemplateId: FullIdentifier, - witnesses: Set[String], - eventProjectionProperties: EventProjectionProperties, - fatContractInstance: FatContractInstance, - )(implicit - ec: ExecutionContext, - loggingContext: LoggingContextWithTrace, - ): Future[ApiContractData] = { - val renderResult = - eventProjectionProperties.render(witnesses, representativeTemplateId.toNameTypeConRef) - val verbose = eventProjectionProperties.verbose - def asyncContractArguments = - enrichAsync( - verbose = verbose, - value = value, - enrich = enricher.enrichContractValue(representativeTemplateId.toIdentifier, _), - ) - .map(toContractArgumentApi(verbose)) - def asyncContractKey = keyO match { - case None => Future.successful(None) - case Some(gkey) => - enrichAsync( - verbose = verbose, - value = gkey.key, - enrich = enricher.enrichContractKey(representativeTemplateId.toIdentifier, _), - ) - .map(toContractKeyApi(verbose)) - .map(value => Some((value, gkey.hash))) - } - - def asyncInterfaceViews = - MonadUtil.sequentialTraverse(renderResult.interfaces.toList)(interfaceId => - for { - upgradedInstanceIdentifierResultE <- eventProjectionProperties.interfaceViewPackageUpgrade - .upgrade(interfaceId.toIdentifier, representativeTemplateId.toIdentifier) - viewResult <- upgradedInstanceIdentifierResultE.fold( - failureStatus => Future.successful(Left(failureStatus)), - upgradedInstanceIdentifier => - computeInterfaceView( - templateId = upgradedInstanceIdentifier, - value = value, - interfaceId = interfaceId.toIdentifier, - ), - ) - interfaceView <- toInterfaceView( - verbose = eventProjectionProperties.verbose, - interfaceId = interfaceId.toIdentifier, - result = viewResult, - implementationPackageId = upgradedInstanceIdentifierResultE.toOption, - ) - } yield interfaceView - ) - - def asyncCreatedEventBlob = condFuture(renderResult.createdEventBlob) { - (for { - encoded <- TransactionCoder - .encodeFatContractInstance(fatContractInstance) - .left - .map(_.errorMessage) - } yield encoded).fold( - err => Future.failed(new RuntimeException(s"Cannot serialize createdEventBlob: $err")), - Future.successful, - ) - } - - for { - contractArguments <- asyncContractArguments - createdEventBlob <- asyncCreatedEventBlob - contractKey <- asyncContractKey - interfaceViews <- asyncInterfaceViews - } yield ApiContractData( - createArguments = contractArguments, - createdEventBlob = createdEventBlob, - contractKey = contractKey, - interfaceViews = interfaceViews, - ) - } - - def implementedInterfaces( - eventProjectionProperties: EventProjectionProperties, - witnessParties: Set[String], - templateId: FullIdentifier, - ): Seq[value.Identifier] = eventProjectionProperties - .render(witnessParties, templateId.toNameTypeConRef) - .interfaces - .view - .map(_.toIdentifier) - .map(LfEngineToApi.toApiIdentifier) - .toSeq - - private def toInterfaceView( - verbose: Boolean, - interfaceId: Identifier, - result: Either[Status, Versioned[Value]], - implementationPackageId: Option[Identifier], - )(implicit ec: ExecutionContext, loggingContext: LoggingContextWithTrace): Future[InterfaceView] = - result match { - case Right(versionedValue) => - enrichAsync(verbose, versionedValue.unversioned, enricher.enrichView(interfaceId, _)) - .map(toInterfaceViewApi(verbose, interfaceId, implementationPackageId)) - case Left(errorStatus) => - Future.successful( - InterfaceView( - interfaceId = Some(LfEngineToApi.toApiIdentifier(interfaceId)), - viewStatus = Some(ProtoStatus.fromJavaProto(errorStatus)), - viewValue = None, - implementationPackageId = implementationPackageId.map(_.packageId).getOrElse(""), - ) - ) - } - - private def condFuture[T](cond: Boolean)(f: => Future[T])(implicit - ec: ExecutionContext - ): Future[Option[T]] = - if (cond) f.map(Some(_)) else Future.successful(None) - - private def enrichAsync(verbose: Boolean, value: Value, enrich: Value => Future[Value])(implicit - ec: ExecutionContext - ): Future[Value] = - condFuture(verbose)( - Future.delegate(enrich(value)) - ).map(_.getOrElse(value)) - - private def toApi[T]( - verbose: Boolean, - lfEngineToApiFunction: (Boolean, Value) => Either[String, T], - attribute: String, - )(value: Value): T = - LfEngineToApi.assertOrRuntimeEx( - failureContext = s"attempting to serialize $attribute to API record", - lfEngineToApiFunction(verbose, value), - ) - - private def toContractArgumentApi(verbose: Boolean)(value: Value): ApiRecord = - toApi(verbose, LfEngineToApi.lfValueToApiRecord, "create argument")(value) - - private def toContractKeyApi(verbose: Boolean)(value: Value): ApiValue = - toApi(verbose, LfEngineToApi.lfValueToApiValue, "create key")(value) - - private def toInterfaceViewApi( - verbose: Boolean, - interfaceId: Identifier, - implementationPackageId: Option[Identifier], - )(value: Value)(implicit loggingContextWithTrace: LoggingContextWithTrace) = - InterfaceView( - interfaceId = Some(LfEngineToApi.toApiIdentifier(interfaceId)), - viewStatus = Some(ProtoStatus.of(Code.OK.value(), "", Seq.empty)), - viewValue = Some(toApi(verbose, LfEngineToApi.lfValueToApiRecord, "interface view")(value)), - implementationPackageId = implementationPackageId - .map(_.packageId) - .getOrElse { - logger.error( - s"Unexpected missing implementation package id for interface view of interface $interfaceId" - )(loggingContextWithTrace.traceContext) - "" - }, - ) - - private def computeInterfaceView( - templateId: LfIdentifier, - value: com.digitalasset.daml.lf.value.Value, - interfaceId: LfIdentifier, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[Either[Status, Versioned[Value]]] = Timed.future( - metrics.index.lfValue.computeInterfaceView, { - def goAsync( - res: LfEngine.Result[Versioned[Value]] - ): Future[Either[Status, Versioned[Value]]] = - res match { - case LfEngine.ResultDone(x) => - Future.successful(Right(x)) - - case LfEngine.ResultError(err) => - err - .pipe(ErrorCause.DamlLf.apply) - .pipe(RejectionGenerators.commandExecutorError) - .pipe(_.asGrpcStatus) - .pipe(Left.apply) - .pipe(Future.successful) - - // Note: the compiler should enforce that the computation is a pure function, - // ResultNeedContract and ResultNeedKey should never appear in the result. - case LfEngine.ResultNeedContract(_, _) => - Future.failed(new IllegalStateException("View computation must be a pure function")) - - case LfEngine.ResultNeedKey(_, _, _, _) => - Future.failed(new IllegalStateException("View computation must be a pure function")) - - case LfEngine.ResultNeedPackage(packageId, resume) => - packageLoader - .loadPackage( - packageId = packageId, - delegate = packageId => loadPackage(packageId, loggingContext.traceContext), - metric = metrics.index.db.translation.getLfPackage, - ) - .map(resume) - .flatMap(goAsync) - - case LfEngine.ResultInterruption(continue, _) => - goAsync(continue()) - - case LfEngine.ResultPrefetch(_, _, resume) => - goAsync(resume()) - } - - Future(engine.computeInterfaceView(templateId, value, interfaceId)) - .flatMap(goAsync) - }, - ) -} - -object LfValueTranslation { - - final case class ApiContractData( - createArguments: ApiRecord, - createdEventBlob: Option[ByteString], - contractKey: Option[(ApiValue, crypto.Hash)], - interfaceViews: Seq[InterfaceView], - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/QueryValidRange.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/QueryValidRange.scala deleted file mode 100644 index f49cf4410f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/QueryValidRange.scala +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.platform.store.PruningOffsetService -import com.digitalasset.canton.platform.store.cache.LedgerEndCache -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.Thereafter.syntax.* - -import scala.concurrent.{ExecutionContext, Future} - -trait QueryValidRange { - def withRangeNotPruned[T]( - minOffsetInclusive: Offset, - maxOffsetInclusive: Offset, - errorPruning: Offset => String, - errorLedgerEnd: Option[Offset] => String, - )(query: => Future[T])(implicit - loggingContext: LoggingContextWithTrace - ): Future[T] - - def withOffsetNotBeforePruning[T]( - offset: Offset, - errorPruning: Offset => String, - errorLedgerEnd: Option[Offset] => String, - )(query: => Future[T])(implicit - loggingContext: LoggingContextWithTrace - ): Future[T] - - def filterPrunedEvents[T](offset: T => Offset)( - events: Vector[T] - )(implicit - errorLoggingContext: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[Vector[T]] -} - -final case class QueryValidRangeImpl( - ledgerEndCache: LedgerEndCache, - pruningOffsetService: PruningOffsetService, - loggerFactory: NamedLoggerFactory, -)(implicit - ec: ExecutionContext -) extends QueryValidRange - with NamedLogging { - - /** Runs a query and throws an error if the query accesses an invalid offset range. - * - * @param query - * query to execute - * @param minOffsetInclusive - * minimum, inclusive offset used by the query (i.e. all fetched offsets are larger or equal) - * @param maxOffsetInclusive - * maximum, inclusive offset used by the query (i.e. all fetched offsets are before or equal) - * @param errorPruning - * function that generates a context-specific error parameterized by participant pruning offset - * @param errorLedgerEnd - * function that generates a context-specific error parameterized by ledger end offset - * @tparam T - * type of result passed through - * @return - * either an Error if offset range violates conditions or query result - * - * Note in order to prevent race condition on connections at READ_COMMITTED isolation levels (in - * fact any level below SNAPSHOT isolation level), this check must be performed after fetching - * the corresponding range of data. This way we avoid a race between pruning and the query - * reading the offsets in which offsets are "silently skipped". First fetching the objects and - * only afterwards checking that no pruning operation has interfered, avoids such a race - * condition. - */ - override def withRangeNotPruned[T]( - minOffsetInclusive: Offset, - maxOffsetInclusive: Offset, - errorPruning: Offset => String, - errorLedgerEnd: Option[Offset] => String, - )(query: => Future[T])(implicit - loggingContext: LoggingContextWithTrace - ): Future[T] = { - assert(Option(maxOffsetInclusive) >= minOffsetInclusive.decrement) - val ledgerEnd = ledgerEndCache().map(_.lastOffset) - if (Option(maxOffsetInclusive) > ledgerEnd) { - Future.failed( - RequestValidationErrors.ParticipantDataAccessedAfterLedgerEnd - .Reject( - cause = errorLedgerEnd(ledgerEnd), - latestOffset = ledgerEnd.fold(0L)(_.unwrap), - )( - ErrorLoggingContext(logger, loggingContext) - ) - .asGrpcError - ) - } else - query.thereafterF(_ => - pruningOffsetService.pruningOffset - .map(pruningOffsetO => - pruningOffsetO - .filter(_ >= minOffsetInclusive) - .foreach(pruningOffsetUpToInclusive => - throw RequestValidationErrors.ParticipantPrunedDataAccessed - .Reject( - cause = errorPruning(pruningOffsetUpToInclusive), - earliestOffset = pruningOffsetUpToInclusive.unwrap, - )( - ErrorLoggingContext(logger, loggingContext) - ) - .asGrpcError - ) - ) - ) - } - - override def withOffsetNotBeforePruning[T]( - offset: Offset, - errorPruning: Offset => String, - errorLedgerEnd: Option[Offset] => String, - )(query: => Future[T])(implicit - loggingContext: LoggingContextWithTrace - ): Future[T] = - withRangeNotPruned( - // as the range not pruned forms a condition that the minOffsetInclusive is greater than the pruning offset, - // by setting this to the offset + 1 we ensure that the offset is greater than or equal to the pruning offset. - minOffsetInclusive = offset.increment, - maxOffsetInclusive = offset, - errorPruning = errorPruning, - errorLedgerEnd = errorLedgerEnd, - )(query) - - /** Filters out events that are at or below the participant's pruning offset. - * - * @param offset - * function to extract the offset from an event - * @param events - * the events to filter - * @tparam T - * the type of the events - * @return - * a future of the filtered events - */ - def filterPrunedEvents[T](offset: T => Offset)( - events: Vector[T] - )(implicit - errorLoggingContext: ErrorLoggingContext, - traceContext: TraceContext, - ): Future[Vector[T]] = { - val ledgerEnd = ledgerEndCache().map(_.lastOffset) - val beyondLegerEndO = events.find(event => Option(offset(event)) > ledgerEnd) - beyondLegerEndO match { - case Some(event) => - Future.failed( - RequestValidationErrors.ParticipantDataAccessedAfterLedgerEnd - .Reject( - cause = - s"Offset of event to be filtered ${offset(event)} is beyond ledger end $ledgerEnd", - latestOffset = ledgerEnd.fold(0L)(_.unwrap), - )(errorLoggingContext) - .asGrpcError - ) - case None => - pruningOffsetService.pruningOffset - .map(participantPrunedUpTo => - events.filter(event => Option(offset(event)) > participantPrunedUpTo) - ) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TopologyTransactionPointwiseReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TopologyTransactionPointwiseReader.scala deleted file mode 100644 index 93e3ce3309..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TopologyTransactionPointwiseReader.scala +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.topology_transaction.TopologyTransaction -import com.digitalasset.canton.ledger.api.TopologyFormat -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.Party -import com.digitalasset.canton.platform.store.backend.EventStorageBackend -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.RawParticipantAuthorization -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.IdRange -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.platform.store.dao.events.EventsTable.TransactionConversions -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.{ExecutionContext, Future} - -final class TopologyTransactionPointwiseReader( - val dbDispatcher: DbDispatcher, - val eventStorageBackend: EventStorageBackend, - val metrics: LedgerApiServerMetrics, - val lfValueTranslation: LfValueTranslation, - val queryValidRange: QueryValidRange, - val loggerFactory: NamedLoggerFactory, -)(implicit val ec: ExecutionContext) - extends NamedLogging { - - protected val dbMetrics: metrics.index.db.type = metrics.index.db - - private def fetchRawTopologyEvents( - firstEventSequentialId: Long, - lastEventSequentialId: Long, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Vector[RawParticipantAuthorization]] = - dbDispatcher.executeSql( - dbMetrics.topologyTransactionsPointwise.fetchTopologyPartyEventPayloads - )( - eventStorageBackend.topologyPartyEventBatch( - IdRange(firstEventSequentialId, lastEventSequentialId) - ) - ) - - private def fetchAndFilterEvents( - fetchRawEvents: Future[Vector[RawParticipantAuthorization]], - requestingParties: Option[Set[Party]], // None is a party-wildcard - toResponse: Vector[RawParticipantAuthorization] => Future[Option[TopologyTransaction]], - )(implicit traceContext: TraceContext): Future[Option[TopologyTransaction]] = - // Fetching all events from the event sequential id range - fetchRawEvents - // Filter out events that do not include the parties - .map( - _.filter(event => - requestingParties.fold(true)(parties => parties.map(_.toString).contains(event.partyId)) - ) - ) - // Checking if events are not pruned - .flatMap(queryValidRange.filterPrunedEvents[RawParticipantAuthorization](_.offset)) - // Convert to api response - .flatMap(filteredEventsPruned => toResponse(filteredEventsPruned.toVector)) - - def lookupTopologyTransaction( - eventSeqIdRange: (Long, Long), - topologyFormat: TopologyFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[TopologyTransaction]] = { - // None is a party-wildcard - val requestingParties: Option[Set[Party]] = - topologyFormat.participantAuthorizationFormat - .fold[Option[Set[Party]]](Some(Set.empty))(_.parties) - val (firstEventSeqId, lastEventSeqId) = eventSeqIdRange - - fetchAndFilterEvents( - fetchRawEvents = fetchRawTopologyEvents( - firstEventSequentialId = firstEventSeqId, - lastEventSequentialId = lastEventSeqId, - ), - requestingParties = requestingParties, - toResponse = (topologyEvents: Vector[RawParticipantAuthorization]) => - Future.successful( - TransactionConversions.toTopologyTransaction(noTracingLogger)(topologyEvents).map(_._2) - ), - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TopologyTransactionsStreamReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TopologyTransactionsStreamReader.scala deleted file mode 100644 index 694cb282ae..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TopologyTransactionsStreamReader.scala +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.topology_transaction.TopologyTransaction -import com.daml.metrics.DatabaseMetrics -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.ParticipantAuthorizationFormat -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.Party -import com.digitalasset.canton.platform.store.backend.EventStorageBackend -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.Ids -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{ - RawParticipantAuthorization, - SequentialIdBatch, -} -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.IdPageQuery -import com.digitalasset.canton.platform.store.dao.events.EventsTable.TransactionConversions -import com.digitalasset.canton.platform.store.dao.events.TopologyTransactionsStreamReader.{ - PayloadDbQuery, - TopologyTransactionsStreamQueryParams, -} -import com.digitalasset.canton.platform.store.dao.{DbDispatcher, PaginatingAsyncStream} -import com.digitalasset.canton.platform.store.utils.{ - ConcurrencyLimiter, - QueueBasedConcurrencyLimiter, -} -import com.digitalasset.canton.util.PekkoUtil.syntax.* -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.Attributes -import org.apache.pekko.stream.scaladsl.Source - -import java.sql.Connection -import scala.concurrent.ExecutionContext -import scala.util.chaining.* - -class TopologyTransactionsStreamReader( - globalIdQueriesLimiter: ConcurrencyLimiter, - globalPayloadQueriesLimiter: ConcurrencyLimiter, - dbDispatcher: DbDispatcher, - queryValidRange: QueryValidRange, - eventStorageBackend: EventStorageBackend, - metrics: LedgerApiServerMetrics, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends NamedLogging { - - private val paginatingAsyncStream = new PaginatingAsyncStream(loggerFactory) - - private val dbMetrics = metrics.index.db - - def streamTopologyTransactions( - topologyTransactionsStreamQueryParams: TopologyTransactionsStreamQueryParams - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, TopologyTransaction), NotUsed] = { - import topologyTransactionsStreamQueryParams.* - - val assignedEventIdQueriesLimiter = - new QueueBasedConcurrencyLimiter(maxParallelIdQueries, executionContext) - - def fetchIds( - maxParallelIdQueriesLimiter: QueueBasedConcurrencyLimiter, - maxOutputBatchCount: Int, - metric: DatabaseMetrics, - idPageQuery: Option[Party] => IdPageQuery, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[Iterable[Long], NotUsed] = { - val partiesO: Vector[Option[Party]] = participantAuthorizationFormat.parties match { - case Some(parties) => parties.map(Some(_)).toVector - // fetch ids for all the parties - case None => Vector(None) - } - partiesO - .map { partyO => - paginatingAsyncStream.streamIdsFromSeekPaginationWithoutIdFilter( - idStreamName = s"Update IDs for topology transaction events for partyO:$partyO", - idPageSizing = idPageSizing, - idPageBufferSize = maxPagesPerIdPagesBuffer, - initialFromIdExclusive = queryRange.startInclusiveEventSeqId, - initialEndInclusive = queryRange.endInclusiveEventSeqId, - descendingOrder = descendingOrder, - )(idPageQuery(partyO))( - executeIdQuery = f => - maxParallelIdQueriesLimiter.execute { - globalIdQueriesLimiter.execute { - dbDispatcher.executeSql(metric)(f) - } - } - ) - } - .pipe(EventIdsUtils.sortAndDeduplicateIds(descendingOrder = descendingOrder)) - .batchN( - maxBatchSize = maxPayloadsPerPayloadsPage, - maxBatchCount = maxOutputBatchCount, - ) - } - - def fetchPayloads( - ids: Source[Iterable[Long], NotUsed], - maxParallelPayloadQueries: Int, - dbMetric: DatabaseMetrics, - payloadDbQuery: PayloadDbQuery, - ): Source[RawParticipantAuthorization, NotUsed] = { - // Pekko requires for this buffer's size to be a power of two. - val inputBufferSize = Utils.largestSmallerOrEqualPowerOfTwo(maxParallelPayloadQueries) - ids.async - .addAttributes(Attributes.inputBuffer(initial = inputBufferSize, max = inputBufferSize)) - .mapAsync(maxParallelPayloadQueries)(ids => - payloadQueriesLimiter.execute { - globalPayloadQueriesLimiter.execute { - queryValidRange.withRangeNotPruned( - minOffsetInclusive = queryRange.startInclusiveOffset, - maxOffsetInclusive = queryRange.endInclusiveOffset, - errorPruning = (prunedOffset: Offset) => - s"Topology events request from ${queryRange.startInclusiveOffset.unwrap} to ${queryRange.endInclusiveOffset.unwrap} precedes pruned offset ${prunedOffset.unwrap}", - errorLedgerEnd = (ledgerEndOffset: Option[Offset]) => - s"Topology events request from ${queryRange.startInclusiveOffset.unwrap} to ${queryRange.endInclusiveOffset.unwrap} is beyond ledger end offset ${ledgerEndOffset - .fold(0L)(_.unwrap)}", - ) { - dbDispatcher.executeSql(dbMetric)( - payloadDbQuery.fetchPayloads(eventSequentialIds = Ids(ids)) - ) - } - } - } - ) - .mapConcat(identity) - } - - val ids = - fetchIds( - maxParallelIdQueriesLimiter = assignedEventIdQueriesLimiter, - maxOutputBatchCount = maxParallelPayloadQueries + 1, - metric = dbMetrics.topologyTransactionsStream.fetchTopologyPartyEventIds, - idPageQuery = eventStorageBackend.fetchTopologyPartyEventIds, - ) - val payloads = - fetchPayloads( - ids = ids, - maxParallelPayloadQueries = maxParallelPayloadQueries, - dbMetric = dbMetrics.topologyTransactionsStream.fetchTopologyPartyEventPayloads, - payloadDbQuery = eventStorageBackend.topologyPartyEventBatch, - ) - - UpdateReader - .groupContiguous(payloads)(by = _.updateId) - .mapConcat(TransactionConversions.toTopologyTransaction(noTracingLogger)) - } - -} - -object TopologyTransactionsStreamReader { - final case class TopologyTransactionsStreamQueryParams( - queryRange: EventsRange, - descendingOrder: Boolean, - payloadQueriesLimiter: ConcurrencyLimiter, - idPageSizing: IdPageSizing, - participantAuthorizationFormat: ParticipantAuthorizationFormat, - maxParallelIdQueries: Int, - maxPagesPerIdPagesBuffer: Int, - maxPayloadsPerPayloadsPage: Int, - maxParallelPayloadQueries: Int, - ) - - @FunctionalInterface - trait PayloadDbQuery { - def fetchPayloads( - eventSequentialIds: SequentialIdBatch - ): Connection => Vector[RawParticipantAuthorization] - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TransactionLogUpdatesConversions.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TransactionLogUpdatesConversions.scala deleted file mode 100644 index d2a189741b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TransactionLogUpdatesConversions.scala +++ /dev/null @@ -1,672 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.event as apiEvent -import com.daml.ledger.api.v2.reassignment.ReassignmentEvent.Event.{ - Assigned as ApiAssigned, - Unassigned as ApiUnassigned, -} -import com.daml.ledger.api.v2.reassignment.{ - AssignedEvent as ApiAssignedEvent, - Reassignment as ApiReassignment, - ReassignmentEvent as ApiReassignmentEvent, - UnassignedEvent as ApiUnassignedEvent, -} -import com.daml.ledger.api.v2.topology_transaction.TopologyTransaction -import com.daml.ledger.api.v2.transaction.Transaction as FlatTransaction -import com.daml.ledger.api.v2.update_service.{GetUpdateResponse, GetUpdatesResponse} -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.TransactionShape.{AcsDelta, LedgerEffects} -import com.digitalasset.canton.ledger.api.util.{LfEngineToApi, TimestampConversion} -import com.digitalasset.canton.ledger.api.{ParticipantAuthorizationFormat, TransactionShape} -import com.digitalasset.canton.ledger.participant.state.Reassignment -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.store.ScalaPbStreamingOptimizations.* -import com.digitalasset.canton.platform.store.backend.common.EventStorageBackendTemplate -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties -import com.digitalasset.canton.platform.store.dao.events.EventsTable.TransactionConversions.toTopologyEvent -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate.{ - CreatedEvent, - ExercisedEvent, -} -import com.digitalasset.canton.platform.{ - InternalTransactionFormat, - InternalUpdateFormat, - PackageId as LfPackageId, - TemplatePartiesFilter, - Value, -} -import com.digitalasset.canton.tracing.SerializableTraceContext -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.canton.util.MonadUtil -import com.digitalasset.daml.lf.data.Ref.{IdentifierConverter, NameTypeConRef, Party} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - FatContractInstance, - GlobalKeyWithMaintainers, - Node, -} - -import scala.concurrent.{ExecutionContext, Future} - -private[events] object TransactionLogUpdatesConversions { - - def filter( - internalUpdateFormat: InternalUpdateFormat - ): TransactionLogUpdate => Option[TransactionLogUpdate] = { - case transaction: TransactionLogUpdate.TransactionAccepted => - internalUpdateFormat.includeTransactions.flatMap { transactionFormat => - val transactionEvents = transaction.events.collect { - case createdEvent: TransactionLogUpdate.CreatedEvent => createdEvent - case exercisedEvent: TransactionLogUpdate.ExercisedEvent - if exercisedEvent.consuming || transactionFormat.transactionShape == LedgerEffects => - exercisedEvent - } - val filteredEvents = transactionEvents - .filter(transactionPredicate(transactionFormat)) - - transactionFormat.transactionShape match { - case AcsDelta => - Option.when(filteredEvents.nonEmpty)( - transaction.copy( - events = filteredEvents - )(transaction.traceContext) - ) - - case LedgerEffects => - Option.when(filteredEvents.nonEmpty)( - transaction.copy( - events = filteredEvents - )(transaction.traceContext) - ) - } - } - - case _: TransactionLogUpdate.TransactionRejected => None - - case u: TransactionLogUpdate.ReassignmentAccepted => - internalUpdateFormat.includeReassignments.flatMap { reassignmentFormat => - val filteredReassignments = u.reassignment.iterator.filter { r => - partiesMatchFilter( - reassignmentFormat.templatePartiesFilter, - u.reassignment.iterator - .map(r => r.templateId.toFullIdentifier(r.packageName).toNameTypeConRef) - .toSet, - )(r.stakeholders) - } - NonEmpty - .from(filteredReassignments.toSeq) - .map(rs => u.copy(reassignment = Reassignment.Batch(rs))(u.traceContext)) - } - - case u: TransactionLogUpdate.TopologyTransactionEffective => - internalUpdateFormat.includeTopologyEvents - .flatMap(_.participantAuthorizationFormat) - .flatMap { participantAuthorizationFormat => - val filteredEvents = - u.events.filter(topologyEventPredicate(participantAuthorizationFormat)) - Option.when(filteredEvents.nonEmpty)( - u.copy(events = filteredEvents)(u.traceContext) - ) - } - } - - def toGetUpdatesResponse( - internalUpdateFormat: InternalUpdateFormat, - lfValueTranslation: LfValueTranslation, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): TransactionLogUpdate => Future[GetUpdatesResponse] = { - case transactionAccepted: TransactionLogUpdate.TransactionAccepted => - val internalTransactionFormat = internalUpdateFormat.includeTransactions - .getOrElse( - throw new IllegalStateException( - "Transaction cannot be converted as there is no transaction format specified in update format" - ) - ) - toTransaction( - transactionAccepted, - internalTransactionFormat, - lfValueTranslation, - ) - .map(transaction => - GetUpdatesResponse(GetUpdatesResponse.Update.Transaction(transaction)) - .withPrecomputedSerializedSize() - ) - - case reassignmentAccepted: TransactionLogUpdate.ReassignmentAccepted => - val reassignmentInternalEventFormat = internalUpdateFormat.includeReassignments - .getOrElse( - throw new IllegalStateException( - "Reassignment cannot be converted as there is no reassignment specified in update format" - ) - ) - toReassignment( - reassignmentAccepted, - reassignmentInternalEventFormat.templatePartiesFilter.allFilterParties, - reassignmentInternalEventFormat.eventProjectionProperties, - lfValueTranslation, - ) - .map(reassignment => - GetUpdatesResponse(GetUpdatesResponse.Update.Reassignment(reassignment)) - .withPrecomputedSerializedSize() - ) - - case topologyTransaction: TransactionLogUpdate.TopologyTransactionEffective => - toTopologyTransaction(topologyTransaction).map(transaction => - GetUpdatesResponse(GetUpdatesResponse.Update.TopologyTransaction(transaction)) - .withPrecomputedSerializedSize() - ) - - case illegal => throw new IllegalStateException(s"$illegal is not expected here") - } - - def toGetUpdateResponse( - transactionLogUpdate: TransactionLogUpdate, - internalUpdateFormat: InternalUpdateFormat, - lfValueTranslation: LfValueTranslation, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[Option[GetUpdateResponse]] = - filter(internalUpdateFormat)(transactionLogUpdate) - .collect { - case transactionAccepted: TransactionLogUpdate.TransactionAccepted => - val internalTransactionFormat = internalUpdateFormat.includeTransactions - .getOrElse( - throw new IllegalStateException( - "Transaction cannot be converted as there is no transaction format specified in update format" - ) - ) - toTransaction( - transactionAccepted, - internalTransactionFormat, - lfValueTranslation, - ) - .map(transaction => - GetUpdateResponse(GetUpdateResponse.Update.Transaction(transaction)) - .withPrecomputedSerializedSize() - ) - - case reassignmentAccepted: TransactionLogUpdate.ReassignmentAccepted => - val reassignmentInternalEventFormat = internalUpdateFormat.includeReassignments - .getOrElse( - throw new IllegalStateException( - "Reassignment cannot be converted as there is no reassignment specified in update format" - ) - ) - toReassignment( - reassignmentAccepted, - reassignmentInternalEventFormat.templatePartiesFilter.allFilterParties, - reassignmentInternalEventFormat.eventProjectionProperties, - lfValueTranslation, - ) - .map(reassignment => - GetUpdateResponse(GetUpdateResponse.Update.Reassignment(reassignment)) - .withPrecomputedSerializedSize() - ) - - case topologyTransaction: TransactionLogUpdate.TopologyTransactionEffective => - toTopologyTransaction(topologyTransaction).map(transaction => - GetUpdateResponse(GetUpdateResponse.Update.TopologyTransaction(transaction)) - .withPrecomputedSerializedSize() - ) - } - .map(_.map(Some(_))) - .getOrElse(Future.successful(None)) - - private def toTransaction( - transactionAccepted: TransactionLogUpdate.TransactionAccepted, - internalTransactionFormat: InternalTransactionFormat, - lfValueTranslation: LfValueTranslation, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[FlatTransaction] = { - val requestingParties: Option[Set[Party]] = - internalTransactionFormat.internalEventFormat.templatePartiesFilter.allFilterParties - val commandId = getCommandId(transactionAccepted.events, requestingParties) - Future.delegate { - MonadUtil - .sequentialTraverse(transactionAccepted.events)(event => - toEvent( - event, - internalTransactionFormat, - lfValueTranslation, - ) - ) - .map(events => - FlatTransaction( - updateId = transactionAccepted.updateId, - commandId = commandId, - workflowId = transactionAccepted.workflowId, - effectiveAt = Some(TimestampConversion.fromLf(transactionAccepted.effectiveAt)), - events = events, - offset = transactionAccepted.offset.unwrap, - synchronizerId = transactionAccepted.synchronizerId, - traceContext = SerializableTraceContext(transactionAccepted.traceContext).toDamlProto, - recordTime = Some(TimestampConversion.fromLf(transactionAccepted.recordTime)), - externalTransactionHash = transactionAccepted.externalTransactionHash.map(_.unwrap), - paidTrafficCost = transactionAccepted.paidTrafficCost(requestingParties), - ) - ) - } - } - - private def transactionPredicate( - transactionFormat: InternalTransactionFormat - )(event: TransactionLogUpdate.Event): Boolean = - partiesMatchFilter( - transactionFormat.internalEventFormat.templatePartiesFilter, - Set(event.templateId.toFullIdentifier(event.packageName).toNameTypeConRef), - )(event.witnesses(transactionFormat.transactionShape)) - - private def topologyEventPredicate( - participantAuthorizationFormat: ParticipantAuthorizationFormat - )(event: TransactionLogUpdate.PartyToParticipantAuthorization): Boolean = - matchPartyInSet(event.party)(participantAuthorizationFormat.parties) - - private def toEvent( - event: TransactionLogUpdate.Event, - internalTransactionFormat: InternalTransactionFormat, - lfValueTranslation: LfValueTranslation, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[apiEvent.Event] = { - val requestingParties = - internalTransactionFormat.internalEventFormat.templatePartiesFilter.allFilterParties - - event match { - case createdEvent: TransactionLogUpdate.CreatedEvent => - createdToApiCreatedEvent( - requestingParties, - internalTransactionFormat.internalEventFormat.eventProjectionProperties, - lfValueTranslation, - createdEvent, - _.witnesses(internalTransactionFormat.transactionShape), - ).map(apiCreatedEvent => apiEvent.Event(apiEvent.Event.Event.Created(apiCreatedEvent))) - - case exercisedEvent: TransactionLogUpdate.ExercisedEvent => - exercisedToEvent( - requestingParties, - exercisedEvent, - internalTransactionFormat.transactionShape, - internalTransactionFormat.internalEventFormat.eventProjectionProperties, - lfValueTranslation, - ) - } - } - - private def partiesMatchFilter( - filter: TemplatePartiesFilter, - templateIds: Set[NameTypeConRef], - )(parties: Set[Party]) = { - val matchesByWildcard: Boolean = - filter.templateWildcardParties match { - case Some(include) => parties.exists(p => include(p)) - case None => parties.nonEmpty // the witnesses should not be empty - } - - def matchesByTemplateId(templateId: NameTypeConRef): Boolean = - filter.relation.get(templateId) match { - case Some(Some(include)) => parties.exists(include) - case Some(None) => parties.nonEmpty // party wildcard - case None => false // templateId is not in the filter - } - - matchesByWildcard || templateIds.exists(matchesByTemplateId) - } - - private def exercisedToEvent( - requestingParties: Option[Set[Party]], - exercisedEvent: ExercisedEvent, - transactionShape: TransactionShape, - eventProjectionProperties: EventProjectionProperties, - lfValueTranslation: LfValueTranslation, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[apiEvent.Event] = - transactionShape match { - case AcsDelta if !exercisedEvent.consuming => - Future.failed( - new IllegalStateException( - "Non consuming exercise cannot be rendered for ACS delta shape" - ) - ) - - case AcsDelta => - val witnessParties = requestingParties match { - case Some(parties) => - parties.iterator.filter(exercisedEvent.flatEventWitnesses).toSeq - // party-wildcard - case None => exercisedEvent.flatEventWitnesses.toSeq - } - Future.successful( - apiEvent.Event( - apiEvent.Event.Event.Archived( - apiEvent.ArchivedEvent( - offset = exercisedEvent.eventOffset.unwrap, - nodeId = exercisedEvent.nodeId, - contractId = exercisedEvent.contractId.coid, - templateId = Some(LfEngineToApi.toApiIdentifier(exercisedEvent.templateId)), - packageName = exercisedEvent.packageName, - witnessParties = witnessParties, - implementedInterfaces = lfValueTranslation.implementedInterfaces( - eventProjectionProperties, - witnessParties.toSet, - exercisedEvent.templateId.toFullIdentifier( - exercisedEvent.packageName - ), - ), - ) - ) - ) - ) - - case LedgerEffects => - val choiceArgumentEnricher = (value: Value) => - lfValueTranslation.enricher - .enrichChoiceArgument( - exercisedEvent.templateId, - exercisedEvent.interfaceId, - Ref.Name.assertFromString(exercisedEvent.choice), - value.unversioned, - ) - - val eventualChoiceArgument = lfValueTranslation.toApiValue( - exercisedEvent.exerciseArgument, - eventProjectionProperties.verbose, - "exercise argument", - choiceArgumentEnricher, - ) - - val eventualExerciseResult = exercisedEvent.exerciseResult - .map { exerciseResult => - val choiceResultEnricher = (value: Value) => - lfValueTranslation.enricher.enrichChoiceResult( - exercisedEvent.templateId, - exercisedEvent.interfaceId, - Ref.Name.assertFromString(exercisedEvent.choice), - value.unversioned, - ) - - lfValueTranslation - .toApiValue( - value = exerciseResult, - verbose = eventProjectionProperties.verbose, - attribute = "exercise result", - enrich = choiceResultEnricher, - ) - .map(Some(_)) - } - .getOrElse(Future.successful(None)) - - for { - choiceArgument <- eventualChoiceArgument - maybeExerciseResult <- eventualExerciseResult - witnessParties = requestingParties - .fold(exercisedEvent.treeEventWitnesses)( - _.filter(exercisedEvent.treeEventWitnesses) - ) - .toSeq - flatEventWitnesses = requestingParties - .fold(exercisedEvent.flatEventWitnesses)( - _.filter(exercisedEvent.flatEventWitnesses) - ) - .toSeq - } yield apiEvent.Event( - apiEvent.Event.Event.Exercised( - apiEvent.ExercisedEvent( - offset = exercisedEvent.eventOffset.unwrap, - nodeId = exercisedEvent.nodeId, - contractId = exercisedEvent.contractId.coid, - templateId = Some(LfEngineToApi.toApiIdentifier(exercisedEvent.templateId)), - packageName = exercisedEvent.packageName, - interfaceId = exercisedEvent.interfaceId.map(LfEngineToApi.toApiIdentifier), - choice = exercisedEvent.choice, - choiceArgument = Some(choiceArgument), - actingParties = exercisedEvent.actingParties.toSeq, - consuming = exercisedEvent.consuming, - witnessParties = witnessParties, - lastDescendantNodeId = exercisedEvent.lastDescendantNodeId, - exerciseResult = maybeExerciseResult, - implementedInterfaces = - if (exercisedEvent.consuming) - lfValueTranslation.implementedInterfaces( - eventProjectionProperties, - witnessParties.toSet, - exercisedEvent.templateId.toFullIdentifier( - exercisedEvent.packageName - ), - ) - else Nil, - acsDelta = flatEventWitnesses.nonEmpty, - ) - ) - ) - } - - private def createdToApiCreatedEvent( - requestingPartiesO: Option[Set[Party]], - eventProjectionProperties: EventProjectionProperties, - lfValueTranslation: LfValueTranslation, - createdEvent: CreatedEvent, - createdWitnesses: CreatedEvent => Set[Party], - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[apiEvent.CreatedEvent] = { - val keyOpt = createdEvent.contractKey - .zip(createdEvent.createKeyHash) - .zip(createdEvent.createKeyMaintainers) - .map { case ((keyVersionedValue, keyHash), maintainers) => - GlobalKeyWithMaintainers.assertBuild( - templateId = createdEvent.templateId, - value = keyVersionedValue.unversioned, - valueHash = keyHash, - maintainers = maintainers, - packageName = createdEvent.packageName, - ) - } - val createNode = Node.Create( - coid = createdEvent.contractId, - templateId = createdEvent.templateId, - packageName = createdEvent.packageName, - arg = createdEvent.createArgument.unversioned, - signatories = createdEvent.createSignatories, - stakeholders = createdEvent.createSignatories ++ createdEvent.createObservers, - keyOpt = keyOpt, - version = createdEvent.createArgument.version, - ) - createdToApiCreatedEvent( - requestingPartiesO = requestingPartiesO, - eventProjectionProperties = eventProjectionProperties, - lfValueTranslation = lfValueTranslation, - create = createNode, - ledgerEffectiveTime = createdEvent.ledgerEffectiveTime, - offset = createdEvent.eventOffset, - nodeId = createdEvent.nodeId, - authenticationData = createdEvent.authenticationData, - representativePackageId = createdEvent.representativePackageId, - createdEventWitnesses = createdWitnesses(createdEvent), - flatEventWitnesses = createdEvent.flatEventWitnesses, - ) - } - - private def createdToApiCreatedEvent( - requestingPartiesO: Option[Set[Party]], - eventProjectionProperties: EventProjectionProperties, - lfValueTranslation: LfValueTranslation, - create: Node.Create, - ledgerEffectiveTime: Timestamp, - offset: Offset, - nodeId: Int, - authenticationData: Bytes, - representativePackageId: LfPackageId, - createdEventWitnesses: Set[Party], - flatEventWitnesses: Set[Party], - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[apiEvent.CreatedEvent] = { - - val fatContractInstance: FatContractInstance = - FatContractInstance.fromCreateNode( - create, - CreationTime.CreatedAt(ledgerEffectiveTime), - authenticationData, - ) - - val witnesses = requestingPartiesO - .fold(createdEventWitnesses)(_.view.filter(createdEventWitnesses).toSet) - .map(_.toString) - - val acsDelta = - requestingPartiesO.fold(flatEventWitnesses.view)(_.view.filter(flatEventWitnesses)).nonEmpty - - lfValueTranslation.toApiCreatedEvent( - eventProjectionProperties = eventProjectionProperties, - fatContractInstance = fatContractInstance, - offset = offset.unwrap, - nodeId = nodeId, - representativePackageId = representativePackageId, - witnesses = witnesses, - acsDelta = acsDelta, - ) - } - - private def matchPartyInSet(party: Party)(optSet: Option[Set[Party]]) = - optSet match { - case Some(filterParties) => filterParties.contains(party) - case None => true - } - - private def getCommandId( - flatTransactionEvents: Vector[TransactionLogUpdate.Event], - requestingPartiesO: Option[Set[Party]], - ) = - flatTransactionEvents - .collectFirst { - case event - if EventStorageBackendTemplate.submittersInQueryingParties( - requestingPartiesO, - Some(event.submitters.toSeq), - ) => - event.commandId - } - .getOrElse("") - - private def toReassignment( - reassignmentAccepted: TransactionLogUpdate.ReassignmentAccepted, - requestingParties: Option[Set[Party]], - eventProjectionProperties: EventProjectionProperties, - lfValueTranslation: LfValueTranslation, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - ): Future[ApiReassignment] = { - val stringRequestingParties = requestingParties.map(_.map(_.toString)) - val info = reassignmentAccepted.reassignmentInfo - - (MonadUtil - .sequentialTraverse(reassignmentAccepted.reassignment.toSeq) { - case assigned: Reassignment.Assign => - createdToApiCreatedEvent( - requestingPartiesO = requestingParties, - eventProjectionProperties = eventProjectionProperties, - lfValueTranslation = lfValueTranslation, - create = assigned.createNode, - ledgerEffectiveTime = assigned.ledgerEffectiveTime, - offset = reassignmentAccepted.offset, - nodeId = assigned.nodeId, - authenticationData = assigned.contractAuthenticationData, - // TODO(#28301): Use the assignment representative package ID when available - representativePackageId = assigned.createNode.templateId.packageId, - createdEventWitnesses = assigned.createNode.stakeholders, - flatEventWitnesses = assigned.createNode.stakeholders, - ).map(createdEvent => - ApiReassignmentEvent( - ApiAssigned( - ApiAssignedEvent( - source = info.sourceSynchronizer.unwrap.toProtoPrimitive, - target = info.targetSynchronizer.unwrap.toProtoPrimitive, - reassignmentId = info.reassignmentId.toProtoPrimitive, - submitter = info.submitter.getOrElse(""), - reassignmentCounter = assigned.reassignmentCounter, - createdEvent = Some(createdEvent), - ) - ) - ) - ) - - case unassigned: Reassignment.Unassign => - val stakeholders = unassigned.stakeholders - Future.successful( - ApiReassignmentEvent( - ApiUnassigned( - ApiUnassignedEvent( - offset = reassignmentAccepted.offset.unwrap, - source = info.sourceSynchronizer.unwrap.toProtoPrimitive, - target = info.targetSynchronizer.unwrap.toProtoPrimitive, - reassignmentId = info.reassignmentId.toProtoPrimitive, - submitter = info.submitter.getOrElse(""), - reassignmentCounter = unassigned.reassignmentCounter, - contractId = unassigned.contractId.coid, - templateId = Some(LfEngineToApi.toApiIdentifier(unassigned.templateId)), - packageName = unassigned.packageName, - assignmentExclusivity = - unassigned.assignmentExclusivity.map(TimestampConversion.fromLf), - witnessParties = requestingParties.fold(stakeholders)(stakeholders.filter).toSeq, - nodeId = unassigned.nodeId, - ) - ) - ) - ) - }) - .map(events => - ApiReassignment( - updateId = reassignmentAccepted.updateId, - commandId = reassignmentAccepted.completionStreamResponseO - .flatMap(_.completionResponse.completion) - .filter(completion => stringRequestingParties.fold(true)(completion.actAs.exists)) - .map(_.commandId) - .getOrElse(""), - workflowId = reassignmentAccepted.workflowId, - offset = reassignmentAccepted.offset.unwrap, - events = events, - traceContext = SerializableTraceContext(reassignmentAccepted.traceContext).toDamlProto, - recordTime = Some(TimestampConversion.fromLf(reassignmentAccepted.recordTime)), - synchronizerId = reassignmentAccepted.synchronizerId, - paidTrafficCost = reassignmentAccepted.paidTrafficCost(requestingParties), - ) - ) - } - - private def toTopologyTransaction( - topologyTransaction: TransactionLogUpdate.TopologyTransactionEffective - ): Future[TopologyTransaction] = Future.successful { - TopologyTransaction( - updateId = topologyTransaction.updateId, - offset = topologyTransaction.offset.unwrap, - synchronizerId = topologyTransaction.synchronizerId, - recordTime = Some(TimestampConversion.fromLf(topologyTransaction.effectiveTime)), - events = topologyTransaction.events.map(event => - toTopologyEvent( - partyId = event.party, - participantId = event.participant, - authorizationEvent = event.authorizationEvent, - ) - ), - traceContext = SerializableTraceContext(topologyTransaction.traceContext).toDamlProto, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TransactionOrReassignmentPointwiseReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TransactionOrReassignmentPointwiseReader.scala deleted file mode 100644 index 9d81c1ec5d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/TransactionOrReassignmentPointwiseReader.scala +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.reassignment.Reassignment -import com.daml.ledger.api.v2.transaction.Transaction -import com.daml.ledger.api.v2.update_service.GetUpdateResponse -import com.daml.ledger.api.v2.update_service.GetUpdateResponse.Update -import com.daml.metrics.Timed -import com.daml.metrics.api.MetricHandle -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.TransactionShape -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.InternalUpdateFormat -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.ScalaPbStreamingOptimizations.ScalaPbMessageWithPrecomputedSerializedSize -import com.digitalasset.canton.platform.store.backend.EventStorageBackend -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.IdRange -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{ - RawThinAcsDeltaEvent, - RawThinEvent, - RawThinLedgerEffectsEvent, -} -import com.digitalasset.canton.platform.store.backend.common.{ - EventPayloadSourceForUpdatesAcsDelta, - EventPayloadSourceForUpdatesLedgerEffects, -} -import com.digitalasset.canton.platform.store.dao.DbDispatcher - -import scala.concurrent.{ExecutionContext, Future} - -final class TransactionOrReassignmentPointwiseReader( - val dbDispatcher: DbDispatcher, - val eventStorageBackend: EventStorageBackend, - val metrics: LedgerApiServerMetrics, - val lfValueTranslation: LfValueTranslation, - val queryValidRange: QueryValidRange, - val contractStore: LedgerApiContractStore, - val loggerFactory: NamedLoggerFactory, -)(implicit val ec: ExecutionContext) - extends NamedLogging { - - protected val dbMetrics: metrics.index.db.type = metrics.index.db - - val directEC: DirectExecutionContext = DirectExecutionContext(noTracingLogger) - - private def fetchRawAcsDeltaEvents( - firstEventSequentialId: Long, - lastEventSequentialId: Long, - internalUpdateFormat: InternalUpdateFormat, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Vector[RawThinAcsDeltaEvent]] = for { - activateEvents <- dbDispatcher.executeSql( - dbMetrics.updatesAcsDeltaPointwise.fetchEventActivatePayloads - )( - eventStorageBackend.fetchEventPayloadsAcsDelta(target = - EventPayloadSourceForUpdatesAcsDelta.Activate - )( - eventSequentialIds = IdRange(firstEventSequentialId, lastEventSequentialId), - requestingPartiesForTx = internalUpdateFormat.includeTransactions - .flatMap(_.internalEventFormat.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = internalUpdateFormat.includeReassignments - .flatMap(_.templatePartiesFilter.allFilterParties), - ) - ) - deactivateEvents <- dbDispatcher.executeSql( - dbMetrics.updatesAcsDeltaPointwise.fetchEventDeactivatePayloads - )( - eventStorageBackend.fetchEventPayloadsAcsDelta(target = - EventPayloadSourceForUpdatesAcsDelta.Deactivate - )( - eventSequentialIds = IdRange(firstEventSequentialId, lastEventSequentialId), - requestingPartiesForTx = internalUpdateFormat.includeTransactions - .flatMap(_.internalEventFormat.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = internalUpdateFormat.includeReassignments - .flatMap(_.templatePartiesFilter.allFilterParties), - ) - ) - } yield { - (activateEvents ++ deactivateEvents).sortBy(_.eventSeqId) - } - - private def fetchRawLedgerEffectsEvents( - firstEventSequentialId: Long, - lastEventSequentialId: Long, - internalUpdateFormat: InternalUpdateFormat, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[Vector[RawThinLedgerEffectsEvent]] = for { - activateEvents <- dbDispatcher.executeSql( - dbMetrics.updatesLedgerEffectsPointwise.fetchEventActivatePayloads - )( - eventStorageBackend.fetchEventPayloadsLedgerEffects(target = - EventPayloadSourceForUpdatesLedgerEffects.Activate - )( - eventSequentialIds = IdRange(firstEventSequentialId, lastEventSequentialId), - requestingPartiesForTx = internalUpdateFormat.includeTransactions - .flatMap(_.internalEventFormat.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = internalUpdateFormat.includeReassignments - .flatMap(_.templatePartiesFilter.allFilterParties), - ) - ) - deactivateEvents <- dbDispatcher.executeSql( - dbMetrics.updatesLedgerEffectsPointwise.fetchEventDeactivatePayloads - )( - eventStorageBackend.fetchEventPayloadsLedgerEffects(target = - EventPayloadSourceForUpdatesLedgerEffects.Deactivate - )( - eventSequentialIds = IdRange(firstEventSequentialId, lastEventSequentialId), - requestingPartiesForTx = internalUpdateFormat.includeTransactions - .flatMap(_.internalEventFormat.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = internalUpdateFormat.includeReassignments - .flatMap(_.templatePartiesFilter.allFilterParties), - ) - ) - variousWitnessedEvents <- dbDispatcher.executeSql( - dbMetrics.updatesLedgerEffectsPointwise.fetchEventVariousWitnessedPayloads - )( - eventStorageBackend.fetchEventPayloadsLedgerEffects(target = - EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed - )( - eventSequentialIds = IdRange(firstEventSequentialId, lastEventSequentialId), - requestingPartiesForTx = internalUpdateFormat.includeTransactions - .flatMap(_.internalEventFormat.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = internalUpdateFormat.includeReassignments - .flatMap(_.templatePartiesFilter.allFilterParties), - ) - ) - } yield { - (activateEvents ++ deactivateEvents ++ variousWitnessedEvents).sortBy(_.eventSeqId) - } - - private def fetchAndFilterEvents( - rawEvents: Future[Vector[RawThinEvent]], - internalUpdateFormat: InternalUpdateFormat, - timer: MetricHandle.Timer, - )(implicit lcwt: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] = - // Fetching all events from the event sequential id range - rawEvents - // Looking up fat contracts if needed - .flatMap(UpdateReader.withFatContractIfNeeded(contractStore)) - // Checking if events are not pruned - .flatMap( - queryValidRange.filterPrunedEvents(entry => Offset.tryFromLong(entry._1.offset)) - ) - // Mapping to fat RawEvents - .map(UpdateReader.tryToResolveFatInstance) - // Filtering by template filters - .map(UpdateReader.filterRawEvents(internalUpdateFormat)) - .flatMap(rawEvents => - Timed.future( - timer = timer, - future = UpdateReader.toApiUpdate[GetUpdateResponse]( - reassignmentEventProjectionProperties = - internalUpdateFormat.includeReassignments.map(_.eventProjectionProperties), - transactionEventProjectionProperties = internalUpdateFormat.includeTransactions - .map(_.internalEventFormat.eventProjectionProperties), - lfValueTranslation = lfValueTranslation, - )(rawEvents)( - convertReassignment = (r: Reassignment) => - GetUpdateResponse(Update.Reassignment(r)).withPrecomputedSerializedSize(), - convertTransaction = (t: Transaction) => - GetUpdateResponse(Update.Transaction(t)).withPrecomputedSerializedSize(), - ), - ) - ) - - def lookupUpdateBy( - eventSeqIdRange: (Long, Long), - internalUpdateFormat: InternalUpdateFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] = { - val (firstEventSeqId, lastEventSeqId) = eventSeqIdRange - - internalUpdateFormat.includeTransactions.map(_.transactionShape) match { - case Some(TransactionShape.AcsDelta) => - fetchAndFilterEvents( - rawEvents = fetchRawAcsDeltaEvents( - firstEventSequentialId = firstEventSeqId, - lastEventSequentialId = lastEventSeqId, - internalUpdateFormat = internalUpdateFormat, - ), - internalUpdateFormat = internalUpdateFormat, - timer = dbMetrics.updatesAcsDeltaPointwise.translationTimer, - ) - case Some(TransactionShape.LedgerEffects) => - fetchAndFilterEvents( - rawEvents = fetchRawLedgerEffectsEvents( - firstEventSequentialId = firstEventSeqId, - lastEventSequentialId = lastEventSeqId, - internalUpdateFormat = internalUpdateFormat, - ), - internalUpdateFormat = internalUpdateFormat, - timer = dbMetrics.updatesLedgerEffectsPointwise.translationTimer, - ) - case None if internalUpdateFormat.includeReassignments.isDefined => - fetchAndFilterEvents( - rawEvents = fetchRawAcsDeltaEvents( - firstEventSequentialId = firstEventSeqId, - lastEventSequentialId = lastEventSeqId, - internalUpdateFormat = internalUpdateFormat, - ), - internalUpdateFormat = internalUpdateFormat, - timer = dbMetrics.updatesLedgerEffectsPointwise.translationTimer, - ) - case None => - Future.successful(None) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdatePointwiseReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdatePointwiseReader.scala deleted file mode 100644 index 5fa2469565..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdatePointwiseReader.scala +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.update_service.GetUpdateResponse -import com.daml.ledger.api.v2.update_service.GetUpdateResponse.Update -import com.daml.metrics.DatabaseMetrics -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.InternalUpdateFormat -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.backend.{EventStorageBackend, ParameterStorageBackend} -import com.digitalasset.canton.platform.store.dao.DbDispatcher - -import scala.concurrent.{ExecutionContext, Future} - -final class UpdatePointwiseReader( - val dbDispatcher: DbDispatcher, - val eventStorageBackend: EventStorageBackend, - val parameterStorageBackend: ParameterStorageBackend, - val metrics: LedgerApiServerMetrics, - transactionPointwiseReader: TransactionOrReassignmentPointwiseReader, - topologyTransactionPointwiseReader: TopologyTransactionPointwiseReader, - val loggerFactory: NamedLoggerFactory, -)(implicit val ec: ExecutionContext) - extends NamedLogging { - - protected val dbMetrics: metrics.index.db.type = metrics.index.db - - val dbMetric: DatabaseMetrics = dbMetrics.lookupPointwiseUpdateFetchEventIds - - def lookupUpdateBy( - lookupKey: LookupKey, - internalUpdateFormat: InternalUpdateFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] = - for { - // Fetching event sequential id range corresponding to the requested update id or offset - eventSeqIdRangeO <- dbDispatcher.executeSql(dbMetric)( - eventStorageBackend.updatePointwiseQueries.fetchIdsFromUpdateMeta( - lookupKey = lookupKey - ) - ) - - transactionUpdate: Future[Option[GetUpdateResponse]] = - eventSeqIdRangeO - .flatMap(eventSeqIdRange => - Option.when( - internalUpdateFormat.includeReassignments.isDefined || - internalUpdateFormat.includeTransactions.isDefined - )( - transactionPointwiseReader - .lookupUpdateBy(eventSeqIdRange, internalUpdateFormat) - ) - ) - .getOrElse(Future.successful(None)) - - topologyTransactionUpdate: Future[Option[GetUpdateResponse]] = - eventSeqIdRangeO - .flatMap(eventSeqIdRange => - internalUpdateFormat.includeTopologyEvents - .map( - topologyTransactionPointwiseReader - .lookupTopologyTransaction(eventSeqIdRange, _) - .map( - _.map(topologyUpdate => - GetUpdateResponse( - Update.TopologyTransaction( - topologyUpdate - ) - ) - ) - ) - ) - ) - .getOrElse(Future.successful(None)) - - agg <- Future - .sequence( - Seq( - transactionUpdate, - topologyTransactionUpdate, - ) - ) - .map(_.flatten) - } yield { - // only a single update should exist for a specific offset or update id - agg.headOption - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdateReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdateReader.scala deleted file mode 100644 index 900297049d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdateReader.scala +++ /dev/null @@ -1,589 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.event.{ArchivedEvent, CreatedEvent, Event} -import com.daml.ledger.api.v2.reassignment.{ - AssignedEvent, - Reassignment, - ReassignmentEvent, - UnassignedEvent, -} -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.daml.ledger.api.v2.trace_context.TraceContext as DamlTraceContext -import com.daml.ledger.api.v2.transaction.Transaction -import com.daml.ledger.api.v2.update_service.{GetUpdateResponse, GetUpdatesResponse} -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.ledger.api.util.{LfEngineToApi, TimestampConversion} -import com.digitalasset.canton.logging.{ErrorLoggingContext, LoggingContextWithTrace} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.backend.EventStorageBackend -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{ - FatCreatedEventProperties, - RawArchivedEvent, - RawEvent, - RawExercisedEvent, - RawFatActiveContract, - RawFatAssignEvent, - RawFatCreatedEvent, - RawReassignmentEvent, - RawThinActiveContract, - RawThinAssignEvent, - RawThinCreatedEvent, - RawThinEvent, - RawTransactionEvent, - RawUnassignEvent, -} -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.dao.{ - DbDispatcher, - EventProjectionProperties, - LedgerDaoUpdateReader, -} -import com.digitalasset.canton.platform.{FatContract, InternalUpdateFormat, TemplatePartiesFilter} -import com.digitalasset.canton.util.MonadUtil -import com.google.protobuf.ByteString -import io.opentelemetry.api.trace.Span -import org.apache.pekko.stream.scaladsl.Source -import org.apache.pekko.{Done, NotUsed} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -/** @param updatesStreamReader - * Knows how to stream updates - * @param updatePointwiseReader - * Knows how to fetch a tree transaction by its id or its offset - * @param dispatcher - * Executes the queries prepared by this object - * @param acsReader - * Knows how to streams ACS - * @param executionContext - * Runs transformations on data fetched from the database, including Daml-LF value - * deserialization - */ -private[dao] final class UpdateReader( - updatesStreamReader: UpdatesStreamReader, - updatePointwiseReader: UpdatePointwiseReader, - dispatcher: DbDispatcher, - queryValidRange: QueryValidRange, - eventStorageBackend: EventStorageBackend, - metrics: LedgerApiServerMetrics, - acsReader: ACSReader, -)(implicit executionContext: ExecutionContext) - extends LedgerDaoUpdateReader { - - private val dbMetrics = metrics.index.db - - override def getUpdates( - startInclusive: Offset, - endInclusive: Offset, - internalUpdateFormat: InternalUpdateFormat, - descendingOrder: Boolean, - skipPruningChecks: Boolean = false, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, GetUpdatesResponse), NotUsed] = { - val futureSource = - getEventSeqIdRange(startInclusive, endInclusive) - .map(queryRange => - updatesStreamReader.streamUpdates( - queryRange = queryRange, - internalUpdateFormat = internalUpdateFormat, - descendingOrder = descendingOrder, - skipPruningChecks = skipPruningChecks, - ) - ) - Source - .futureSource(futureSource) - .mapMaterializedValue((_: Future[NotUsed]) => NotUsed) - } - - override def lookupUpdateBy( - lookupKey: LookupKey, - internalUpdateFormat: InternalUpdateFormat, - )(implicit loggingContext: LoggingContextWithTrace): Future[Option[GetUpdateResponse]] = - updatePointwiseReader.lookupUpdateBy( - lookupKey = lookupKey, - internalUpdateFormat = internalUpdateFormat, - ) - - override def getActiveContracts( - activeAt: Option[Offset], - filter: TemplatePartiesFilter, - eventProjectionProperties: EventProjectionProperties, - rangeInfo: AcsRangeInfo, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[GetActiveContractsResponse, NotUsed] = - activeAt match { - case None => Source.empty - case Some(offset) => - val futureSource = getMaxAcsEventSeqId(offset) - .map(maxSeqId => - acsReader.streamActiveContracts( - filteringConstraints = filter, - activeAt = offset -> maxSeqId, - eventProjectionProperties = eventProjectionProperties, - rangeInfo = rangeInfo, - ) - ) - Source - .futureSource(futureSource) - .mapMaterializedValue((_: Future[NotUsed]) => NotUsed) - } - - private def getMaxAcsEventSeqId(activeAt: Offset)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Long] = - queryValidRange.withOffsetNotBeforePruning( - offset = activeAt, - errorPruning = pruned => - ACSReader.acsBeforePruningErrorReason( - acsOffset = activeAt, - prunedUpToOffset = pruned, - ), - errorLedgerEnd = ledgerEnd => - ACSReader.acsAfterLedgerEndErrorReason( - acsOffset = activeAt, - ledgerEndOffset = ledgerEnd, - ), - )( - dispatcher.executeSql(dbMetrics.getAcsEventSeqIdRange)( - eventStorageBackend.maxEventSequentialId(Some(activeAt)) - ) - ) - - private def getEventSeqIdRange( - startInclusive: Offset, - endInclusive: Offset, - )(implicit loggingContext: LoggingContextWithTrace): Future[EventsRange] = - queryValidRange.withRangeNotPruned( - minOffsetInclusive = startInclusive, - maxOffsetInclusive = endInclusive, - errorPruning = (prunedOffset: Offset) => - s"Transactions request from ${startInclusive.unwrap} to ${endInclusive.unwrap} precedes pruned offset ${prunedOffset.unwrap}", - errorLedgerEnd = (ledgerEndOffset: Option[Offset]) => - s"Transactions request from ${startInclusive.unwrap} to ${endInclusive.unwrap} is beyond ledger end offset ${ledgerEndOffset - .fold(0L)(_.unwrap)}", - )(dispatcher.executeSql(dbMetrics.getEventSeqIdRange) { connection => - EventsRange( - startInclusiveOffset = startInclusive, - startInclusiveEventSeqId = - eventStorageBackend.maxEventSequentialId(startInclusive.decrement)(connection), - endInclusiveOffset = endInclusive, - endInclusiveEventSeqId = - eventStorageBackend.maxEventSequentialId(Some(endInclusive))(connection), - ) - }) - -} - -private[dao] object UpdateReader { - - def endSpanOnTermination[Mat]( - span: Span - )(mat: Mat, done: Future[Done])(implicit ec: ExecutionContext): Mat = { - done.onComplete { - case Failure(exception) => - span.recordException(exception) - span.end() - case Success(_) => - span.end() - } - mat - } - - /** Groups together items of type [[A]] that share an attribute [[K]] over a contiguous stretch of - * the input [[Source]]. Well suited to perform group-by operations of streams where [[K]] - * attributes are either sorted or at least show up in blocks. - * - * Implementation detail: this method _must_ use concatSubstreams instead of mergeSubstreams to - * prevent the substreams to be processed in parallel, potentially causing the outputs to be - * delivered in a different order. - * - * Docs: https://doc.akka.io/docs/akka/2.6.10/stream/stream-substream.html#groupby - */ - def groupContiguous[A, K, Mat]( - source: Source[A, Mat] - )(by: A => K): Source[Vector[A], Mat] = - source - .statefulMapConcat { () => - @SuppressWarnings(Array("org.wartremover.warts.Var")) - var previousSegmentKey: Option[K] = None - entry => { - val keyForEntry = by(entry) - val entryWithSplit = entry -> !previousSegmentKey.contains(keyForEntry) - previousSegmentKey = Some(keyForEntry) - List(entryWithSplit) - } - } - .splitWhen(_._2) - .map(_._1) - .fold(Vector.empty[A])(_ :+ _) - .concatSubstreams - - def toUnassignedEvent( - rawUnassignEvent: RawUnassignEvent - ): UnassignedEvent = - UnassignedEvent( - offset = rawUnassignEvent.offset, - reassignmentId = rawUnassignEvent.reassignmentId, - contractId = rawUnassignEvent.contractId.coid, - templateId = Some(LfEngineToApi.toApiIdentifier(rawUnassignEvent.templateId.toIdentifier)), - packageName = rawUnassignEvent.templateId.pkgName, - source = rawUnassignEvent.sourceSynchronizerId, - target = rawUnassignEvent.targetSynchronizerId, - submitter = rawUnassignEvent.submitter.getOrElse(""), - reassignmentCounter = rawUnassignEvent.reassignmentCounter, - assignmentExclusivity = - rawUnassignEvent.assignmentExclusivity.map(TimestampConversion.fromLf), - witnessParties = rawUnassignEvent.witnessParties.toSeq, - nodeId = rawUnassignEvent.nodeId, - ) - - def toAssignedEvent( - rawAssignEvent: RawFatAssignEvent, - createdEvent: CreatedEvent, - ): AssignedEvent = - AssignedEvent( - source = rawAssignEvent.sourceSynchronizerId, - target = rawAssignEvent.targetSynchronizerId, - reassignmentId = rawAssignEvent.reassignmentId, - submitter = rawAssignEvent.submitter.getOrElse(""), - reassignmentCounter = rawAssignEvent.reassignmentCounter, - createdEvent = Some(createdEvent), - ) - - def toApiReassignment( - eventProjectionProperties: EventProjectionProperties, - lfValueTranslation: LfValueTranslation, - )( - rawReassignmentEvents: Seq[RawReassignmentEvent] - )(implicit lc: LoggingContextWithTrace, ec: ExecutionContext): Future[Option[Reassignment]] = - MonadUtil - .sequentialTraverse(rawReassignmentEvents) { - case rawAssignEvent: RawFatAssignEvent => - lfValueTranslation - .toApiCreatedEvent( - eventProjectionProperties = eventProjectionProperties, - fatContractInstance = rawAssignEvent.fatContract, - offset = rawAssignEvent.offset, - nodeId = rawAssignEvent.nodeId, - representativePackageId = rawAssignEvent.representativePackageId, - witnesses = rawAssignEvent.witnessParties, - acsDelta = rawAssignEvent.acsDeltaForWitnesses, - ) - .map(createdEvent => - ReassignmentEvent( - ReassignmentEvent.Event.Assigned( - UpdateReader.toAssignedEvent(rawAssignEvent, createdEvent) - ) - ) - ) - - case rawUnassignEvent: RawUnassignEvent => - Future.successful( - ReassignmentEvent( - ReassignmentEvent.Event.Unassigned( - UnassignedEvent( - offset = rawUnassignEvent.offset, - reassignmentId = rawUnassignEvent.reassignmentId, - contractId = rawUnassignEvent.contractId.coid, - templateId = - Some(LfEngineToApi.toApiIdentifier(rawUnassignEvent.templateId.toIdentifier)), - packageName = rawUnassignEvent.templateId.pkgName, - source = rawUnassignEvent.sourceSynchronizerId, - target = rawUnassignEvent.targetSynchronizerId, - submitter = rawUnassignEvent.submitter.getOrElse(""), - reassignmentCounter = rawUnassignEvent.reassignmentCounter, - assignmentExclusivity = - rawUnassignEvent.assignmentExclusivity.map(TimestampConversion.fromLf), - witnessParties = rawUnassignEvent.witnessParties.toSeq, - nodeId = rawUnassignEvent.nodeId, - ) - ) - ) - ) - } - .map(reassignments => - rawReassignmentEvents.headOption.map { first => - Reassignment( - updateId = first.updateId, - commandId = first.commandId.getOrElse(""), - workflowId = first.workflowId.getOrElse(""), - offset = first.offset, - events = reassignments, - recordTime = Some(TimestampConversion.fromLf(first.recordTime)), - traceContext = Some(DamlTraceContext.parseFrom(first.traceContext)), - synchronizerId = first.synchronizerId, - paidTrafficCost = first.trafficCost, - ) - } - ) - - def archivedEvent( - eventProjectionProperties: EventProjectionProperties, - lfValueTranslation: LfValueTranslation, - )(rawArchived: RawArchivedEvent): ArchivedEvent = - ArchivedEvent( - offset = rawArchived.offset, - nodeId = rawArchived.nodeId, - contractId = rawArchived.contractId.coid, - templateId = Some( - LfEngineToApi.toApiIdentifier(rawArchived.templateId.toIdentifier) - ), - witnessParties = rawArchived.witnessParties.toSeq, - packageName = rawArchived.templateId.pkgName, - implementedInterfaces = lfValueTranslation.implementedInterfaces( - eventProjectionProperties, - rawArchived.witnessParties, - rawArchived.templateId, - ), - ) - - def deserializeRawTransactionEvent( - eventProjectionProperties: EventProjectionProperties, - lfValueTranslation: LfValueTranslation, - )( - rawTransactionEvent: RawTransactionEvent - )(implicit - loggingContext: LoggingContextWithTrace, - ec: ExecutionContext, - ): Future[Event] = rawTransactionEvent match { - case rawCreated: RawFatCreatedEvent => - lfValueTranslation - .toApiCreatedEvent( - eventProjectionProperties = eventProjectionProperties, - fatContractInstance = rawCreated.fatContract, - offset = rawCreated.offset, - nodeId = rawCreated.nodeId, - representativePackageId = rawCreated.representativePackageId, - witnesses = rawCreated.witnessParties, - acsDelta = rawCreated.acsDeltaForWitnesses, - ) - .map(createdEvent => Event(Event.Event.Created(createdEvent))) - - case rawArchived: RawArchivedEvent => - Future.successful( - Event( - Event.Event.Archived( - archivedEvent(eventProjectionProperties, lfValueTranslation)( - rawArchived - ) - ) - ) - ) - - case rawExercisedEvent: RawExercisedEvent => - lfValueTranslation - .deserializeRawExercised( - eventProjectionProperties, - rawExercisedEvent, - ) - .map(exercisedEvent => Event(Event.Event.Exercised(exercisedEvent))) - } - - def toApiTransaction( - eventProjectionProperties: EventProjectionProperties, - lfValueTranslation: LfValueTranslation, - )( - rawTransactionEvents: Seq[RawTransactionEvent] - )(implicit lc: LoggingContextWithTrace, ec: ExecutionContext): Future[Option[Transaction]] = - MonadUtil - .sequentialTraverse(rawTransactionEvents)( - deserializeRawTransactionEvent( - eventProjectionProperties = eventProjectionProperties, - lfValueTranslation = lfValueTranslation, - ) - ) - .map(events => - rawTransactionEvents.headOption.map { first => - Transaction( - updateId = first.updateId, - commandId = first.commandId.getOrElse(""), - effectiveAt = Some(TimestampConversion.fromLf(first.ledgerEffectiveTime)), - workflowId = first.workflowId.getOrElse(""), - offset = first.offset, - events = events, - synchronizerId = first.synchronizerId, - traceContext = Some(DamlTraceContext.parseFrom(first.traceContext)), - recordTime = Some(TimestampConversion.fromLf(first.recordTime)), - externalTransactionHash = first.externalTransactionHash.map(ByteString.copyFrom), - paidTrafficCost = first.trafficCost, - ) - } - ) - - def toApiUpdate[T]( - reassignmentEventProjectionProperties: Option[EventProjectionProperties], - transactionEventProjectionProperties: Option[EventProjectionProperties], - lfValueTranslation: LfValueTranslation, - )(rawEvents: Seq[RawEvent])( - convertReassignment: Reassignment => T, - convertTransaction: Transaction => T, - )(implicit - loggingContext: LoggingContextWithTrace, - ec: ExecutionContext, - ): Future[Option[T]] = { - val reassignmentEvents = rawEvents.collect { case raw: RawReassignmentEvent => - raw - } - val transactionEvents = rawEvents.collect { case raw: RawTransactionEvent => - raw - } - if (transactionEvents.nonEmpty && reassignmentEvents.isEmpty) { - transactionEventProjectionProperties match { - case Some(eventProjectionProperties) => - UpdateReader - .toApiTransaction( - eventProjectionProperties = eventProjectionProperties, - lfValueTranslation = lfValueTranslation, - )(transactionEvents) - .map(_.map(convertTransaction)) - - case None => - throw new IllegalStateException( - s"no internal event format for transactions and transaction event returned from the stream" - ) - } - } else if (reassignmentEvents.nonEmpty && transactionEvents.isEmpty) { - reassignmentEventProjectionProperties match { - case Some(eventProjectionProperties) => - UpdateReader - .toApiReassignment( - eventProjectionProperties = eventProjectionProperties, - lfValueTranslation = lfValueTranslation, - )(reassignmentEvents) - .map(_.map(convertReassignment)) - - case None => - throw new IllegalStateException( - s"no internal event format for transactions and transaction event returned from the stream" - ) - } - } else if (transactionEvents.isEmpty && reassignmentEvents.isEmpty) { - Future.successful(None) - } else { - throw new IllegalStateException(s"reassignment and transaction events mixed for one offset") - } - } - - def eventFilter(templatePartiesFilterO: Option[TemplatePartiesFilter]): RawEvent => Boolean = - templatePartiesFilterO - .map { templatePartiesFilter => - val templateWildcardPartiesO = templatePartiesFilter.templateWildcardParties - val templateSpecifiedPartiesMap = templatePartiesFilter.relation.map { - case (identifier, partiesO) => (identifier, partiesO.map(_.map(_.toString))) - } - templateWildcardPartiesO match { - // the filter allows all parties for all templates (wildcard) - case None => (_: RawEvent) => true - - case Some(templateWildcardParties) => - val templateWildcardPartiesStrings: Set[String] = templateWildcardParties.toSet[String] - (event: RawEvent) => - // at least one of the witnesses exist in the template wildcard filter - event.witnessParties.exists(templateWildcardPartiesStrings) || - (templateSpecifiedPartiesMap.get(event.templateId.toNameTypeConRef) match { - // the event's template id was not found in the filters - case None => false - case Some(partiesO) => partiesO.fold(true)(event.witnessParties.exists) - }) - } - } - .getOrElse(_ => false) - - def filterRawEvents( - internalUpdateFormat: InternalUpdateFormat - )( - rawEvents: Seq[RawEvent] - ): Seq[RawEvent] = { - val reassignmentFilter = eventFilter( - internalUpdateFormat.includeReassignments.map(_.templatePartiesFilter) - ) - val transactionFilter = eventFilter( - internalUpdateFormat.includeTransactions.map(_.internalEventFormat.templatePartiesFilter) - ) - rawEvents.collect { - case tx: RawTransactionEvent if transactionFilter(tx) => tx - case r: RawReassignmentEvent if reassignmentFilter(r) => r - } - } - - val getInternalContractIdO: RawThinEvent => Option[Long] = { - case raw: RawThinAssignEvent => Some(raw.thinCreatedEventProperties.internalContractId) - case raw: RawThinCreatedEvent => Some(raw.thinCreatedEventProperties.internalContractId) - case raw: RawThinActiveContract => Some(raw.thinCreatedEventProperties.internalContractId) - case _: RawArchivedEvent => None - case _: RawUnassignEvent => None - case _: RawExercisedEvent => None - } - - val toFatInstance: (RawThinEvent, Option[FatContract]) => RawEvent = { - case (raw: RawThinAssignEvent, fatContractO) => - RawFatAssignEvent( - reassignmentProperties = raw.reassignmentProperties, - fatCreatedEventProperties = FatCreatedEventProperties( - thinCreatedEventProperties = raw.thinCreatedEventProperties, - fatContract = - tryFatContract(raw.thinCreatedEventProperties.internalContractId, fatContractO), - ), - sourceSynchronizerId = raw.sourceSynchronizerId, - ) - case (raw: RawThinCreatedEvent, fatContractO) => - RawFatCreatedEvent( - transactionProperties = raw.transactionProperties, - fatCreatedEventProperties = FatCreatedEventProperties( - thinCreatedEventProperties = raw.thinCreatedEventProperties, - fatContract = - tryFatContract(raw.thinCreatedEventProperties.internalContractId, fatContractO), - ), - ) - case (raw: RawThinActiveContract, fatContractO) => - RawFatActiveContract( - commonEventProperties = raw.commonEventProperties, - fatCreatedEventProperties = FatCreatedEventProperties( - thinCreatedEventProperties = raw.thinCreatedEventProperties, - fatContract = - tryFatContract(raw.thinCreatedEventProperties.internalContractId, fatContractO), - ), - ) - case (raw: RawArchivedEvent, _) => raw - case (raw: RawUnassignEvent, _) => raw - case (raw: RawExercisedEvent, _) => raw - } - - def tryFatContract( - internalContractId: Long, - fatContractO: Option[FatContract], - ): FatContract = fatContractO.getOrElse( - throw new IllegalStateException( - s"Contract for internal contract id $internalContractId was not found in the contract store." - ) - ) - - def withFatContractIfNeeded(contractStore: LedgerApiContractStore)( - rawEvents: Vector[RawThinEvent] - )(implicit - ec: ExecutionContext, - ecl: ErrorLoggingContext, - ): Future[Vector[(RawThinEvent, Option[FatContract])]] = - contractStore - .lookupBatchedNonReadThrough(rawEvents.flatMap(getInternalContractIdO))(ecl.traceContext) - .map(contracts => - rawEvents.map(event => - event -> getInternalContractIdO(event) - .flatMap(contracts.get) - .map(_.inst) - ) - ) - - def tryToResolveFatInstance( - thinEventsWithFatContract: Vector[(RawThinEvent, Option[FatContract])] - ): Vector[RawEvent] = - thinEventsWithFatContract.map(toFatInstance.tupled) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdatesStreamReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdatesStreamReader.scala deleted file mode 100644 index 029c6a0518..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/UpdatesStreamReader.scala +++ /dev/null @@ -1,1017 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.ledger.api.v2.update_service.GetUpdatesResponse -import com.daml.metrics.DatabaseMetrics -import com.daml.nameof.NameOf.qualifiedNameOfCurrentFunc -import com.daml.tracing -import com.daml.tracing.Spans -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.TransactionShape.{AcsDelta, LedgerEffects} -import com.digitalasset.canton.ledger.api.{TraceIdentifiers, TransactionShape} -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - NamedLogging, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.config.UpdatesStreamsConfig -import com.digitalasset.canton.platform.store.ScalaPbStreamingOptimizations.ScalaPbMessageWithPrecomputedSerializedSize -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.Ids -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{RawEvent, RawThinEvent} -import com.digitalasset.canton.platform.store.backend.common.{ - EventPayloadSourceForUpdatesAcsDelta, - EventPayloadSourceForUpdatesLedgerEffects, -} -import com.digitalasset.canton.platform.store.backend.{EventStorageBackend, PersistentEventType} -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - IdFilterPageQuery, - IdPageQuery, -} -import com.digitalasset.canton.platform.store.dao.events.TopologyTransactionsStreamReader.TopologyTransactionsStreamQueryParams -import com.digitalasset.canton.platform.store.dao.events.UpdatesStreamReader.VectorOps -import com.digitalasset.canton.platform.store.dao.{DbDispatcher, PaginatingAsyncStream} -import com.digitalasset.canton.platform.store.utils.{ - ConcurrencyLimiter, - QueueBasedConcurrencyLimiter, - Telemetry, -} -import com.digitalasset.canton.platform.store.{LedgerApiContractStore, PruningOffsetService} -import com.digitalasset.canton.platform.{ - FatContract, - InternalEventFormat, - InternalTransactionFormat, - InternalUpdateFormat, -} -import com.digitalasset.canton.util.PekkoUtil.syntax.* -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.Attributes -import org.apache.pekko.stream.scaladsl.Source - -import java.sql.Connection -import scala.concurrent.{ExecutionContext, Future} -import scala.util.chaining.* - -class UpdatesStreamReader( - config: UpdatesStreamsConfig, - globalIdQueriesLimiter: ConcurrencyLimiter, - globalPayloadQueriesLimiter: ConcurrencyLimiter, - dbDispatcher: DbDispatcher, - queryValidRange: QueryValidRange, - eventStorageBackend: EventStorageBackend, - lfValueTranslation: LfValueTranslation, - contractStore: LedgerApiContractStore, - metrics: LedgerApiServerMetrics, - tracer: Tracer, - topologyTransactionsStreamReader: TopologyTransactionsStreamReader, - pruningOffsetService: PruningOffsetService, - val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends NamedLogging { - import UpdateReader.* - import config.* - - private val dbMetrics = metrics.index.db - - private val paginatingAsyncStream = new PaginatingAsyncStream(loggerFactory) - - def streamUpdates( - queryRange: EventsRange, - internalUpdateFormat: InternalUpdateFormat, - descendingOrder: Boolean, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, GetUpdatesResponse), NotUsed] = { - val span = - Telemetry.Updates.createSpan( - tracer, - queryRange.startInclusiveOffset, - queryRange.endInclusiveOffset, - )( - qualifiedNameOfCurrentFunc - ) - logger.debug( - s"streamUpdates(${queryRange.startInclusiveOffset}, ${queryRange.endInclusiveOffset}, descending: $descendingOrder, $internalUpdateFormat)" - ) - doStreamUpdates( - queryRange = queryRange, - internalUpdateFormat = internalUpdateFormat, - descendingOrder = descendingOrder, - skipPruningChecks = skipPruningChecks, - ) - .wireTap(_ match { - case (_, getUpdatesResponse) => - getUpdatesResponse.update match { - case GetUpdatesResponse.Update.Transaction(value) => - val event = tracing.Event("update", TraceIdentifiers.fromTransaction(value)) - Spans.addEventToSpan(event, span) - case GetUpdatesResponse.Update.Reassignment(reassignment) => - val event = - tracing.Event("update", TraceIdentifiers.fromReassignment(reassignment)) - Spans.addEventToSpan(event, span) - case GetUpdatesResponse.Update.TopologyTransaction(topologyTransaction) => - val event = tracing - .Event("update", TraceIdentifiers.fromTopologyTransaction(topologyTransaction)) - Spans.addEventToSpan(event, span) - case _ => () - } - }) - .watchTermination()(endSpanOnTermination(span)) - } - - private def doStreamUpdates( - queryRange: EventsRange, - internalUpdateFormat: InternalUpdateFormat, - descendingOrder: Boolean, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, GetUpdatesResponse), NotUsed] = { - val longOrdering: Ordering[Long] = - orderingBasedOnDescending(descendingOrder = descendingOrder) - - val payloadQueriesLimiter = - new QueueBasedConcurrencyLimiter(maxParallelPayloadQueries, executionContext) - val deserializationQueriesLimiter = - new QueueBasedConcurrencyLimiter(transactionsProcessingParallelism, executionContext) - val txFilters: Set[DecomposedFilter] = - internalUpdateFormat.includeTransactions - .map(_.internalEventFormat.templatePartiesFilter) - .toList - .flatMap(txFilteringConstraints => - FilterUtils.decomposeFilters(txFilteringConstraints).toList - ) - .toSet - val reassignmentsFilters: Set[DecomposedFilter] = - internalUpdateFormat.includeReassignments - .map(_.templatePartiesFilter) - .toList - .flatMap(reassignmentsFilteringConstraints => - FilterUtils.decomposeFilters(reassignmentsFilteringConstraints).toList - ) - .toSet - val txAndReassignmentFilters = - txFilters.filter(reassignmentsFilters) - val justTxFilters = - txFilters.filterNot(txAndReassignmentFilters) - val justReassignmentFilters = - reassignmentsFilters.filterNot(txAndReassignmentFilters) - val numTxAndReassignmentFilters = txAndReassignmentFilters.size * - internalUpdateFormat.includeTransactions.fold(0)(_.transactionShape match { - // The ids for ledger effects transactions are retrieved from 5 separate id tables: (activate stakeholder, - // activate witness, deactivate stakeholder, deactivate witness, various witnessed) - case TransactionShape.LedgerEffects => 5 - // The ids for acs delta transactions are retrieved from 2 separate id tables: (activate stakeholder, deactivate stakeholder) - case TransactionShape.AcsDelta => 2 - }) - val numJustTxFilters = justTxFilters.size * - internalUpdateFormat.includeTransactions.fold(0)(_.transactionShape match { - // The ids for ledger effects transactions are retrieved from 5 separate id tables: (activate stakeholder, - // activate witness, deactivate stakeholder, deactivate witness, various witnessed) - case TransactionShape.LedgerEffects => 5 - // The ids for acs delta transactions are retrieved from 2 separate id tables: (activate stakeholder, deactivate stakeholder) - case TransactionShape.AcsDelta => 2 - }) - // The ids for reassignments are retrieved from 2 separate id tables: (assign stakeholder, unassign stakeholder) - val numJustReassignmentsFilters = justReassignmentFilters.size * 2 - // The ids for topology updates are retrieved from 1 id table: (party_to_participant) - val numTopologyDecomposedFilters = internalUpdateFormat.includeTopologyEvents - .flatMap(_.participantAuthorizationFormat) - .fold(0)(_.parties.fold(1)(_.size)) - - val idPageSizing = IdPageSizing.calculateFrom( - maxIdPageSize = maxIdsPerIdPage, - workingMemoryInBytesForIdPages = maxWorkingMemoryInBytesForIdPages, - numOfDecomposedFilters = numTxAndReassignmentFilters + - numJustTxFilters + - numJustReassignmentsFilters + - numTopologyDecomposedFilters, - numOfPagesInIdPageBuffer = maxPagesPerIdPagesBuffer, - loggerFactory = loggerFactory, - ) - - val sourceOfTransactionsAndReassignments = internalUpdateFormat.includeTransactions match { - case Some(InternalTransactionFormat(internalEventFormat, AcsDelta)) => - doStreamAcsDelta( - queryRange = queryRange, - txInternalEventFormat = Some(internalEventFormat), - reassignmentInternalEventFormat = internalUpdateFormat.includeReassignments, - txAndReassignmentFilters = txAndReassignmentFilters, - justTxFilters = justTxFilters, - justReassignmentFilters = justReassignmentFilters, - payloadQueriesLimiter = payloadQueriesLimiter, - idPageSizing = idPageSizing, - descendingOrder = descendingOrder, - skipPruningChecks = skipPruningChecks, - ) - case Some(InternalTransactionFormat(internalEventFormat, LedgerEffects)) => - doStreamLedgerEffects( - queryRange = queryRange, - txInternalEventFormat = Some(internalEventFormat), - reassignmentInternalEventFormat = internalUpdateFormat.includeReassignments, - txAndReassignmentFilters = txAndReassignmentFilters, - justTxFilters = justTxFilters, - justReassignmentFilters = justReassignmentFilters, - payloadQueriesLimiter = payloadQueriesLimiter, - idPageSizing = idPageSizing, - descendingOrder = descendingOrder, - skipPruningChecks = skipPruningChecks, - ) - case None if internalUpdateFormat.includeReassignments.isDefined => - doStreamAcsDelta( - queryRange = queryRange, - txInternalEventFormat = None, - reassignmentInternalEventFormat = internalUpdateFormat.includeReassignments, - txAndReassignmentFilters = txAndReassignmentFilters, - justTxFilters = justTxFilters, - justReassignmentFilters = justReassignmentFilters, - payloadQueriesLimiter = payloadQueriesLimiter, - idPageSizing = idPageSizing, - descendingOrder = descendingOrder, - skipPruningChecks = skipPruningChecks, - ) - - case None => Source.empty - } - - val topologyTransactions = - internalUpdateFormat.includeTopologyEvents.flatMap(_.participantAuthorizationFormat) match { - case Some(participantAuthorizationFormat) => - topologyTransactionsStreamReader - .streamTopologyTransactions( - TopologyTransactionsStreamQueryParams( - queryRange = queryRange, - descendingOrder = descendingOrder, - payloadQueriesLimiter = payloadQueriesLimiter, - idPageSizing = idPageSizing, - participantAuthorizationFormat = participantAuthorizationFormat, - maxParallelIdQueries = maxParallelIdTopologyEventsQueries, - maxPagesPerIdPagesBuffer = maxPayloadsPerPayloadsPage, - maxPayloadsPerPayloadsPage = maxParallelPayloadTopologyEventsQueries, - maxParallelPayloadQueries = transactionsProcessingParallelism, - ) - ) - .map { case (offset, topologyTransaction) => - offset -> GetUpdatesResponse( - GetUpdatesResponse.Update.TopologyTransaction(topologyTransaction) - ).withPrecomputedSerializedSize() - } - case None => Source.empty - } - - UpdateReader - .groupContiguous(sourceOfTransactionsAndReassignments)(_.offset) - .mapAsync(transactionsProcessingParallelism) { rawEvents => - deserializationQueriesLimiter.execute( - UpdateReader.toApiUpdate( - reassignmentEventProjectionProperties = internalUpdateFormat.includeReassignments.map( - _.eventProjectionProperties - ), - transactionEventProjectionProperties = internalUpdateFormat.includeTransactions.map( - _.internalEventFormat.eventProjectionProperties - ), - lfValueTranslation = lfValueTranslation, - )(reverseIfDescendingOrder(descendingOrder, rawEvents))( - convertReassignment = reassignment => - Offset.tryFromLong(reassignment.offset) -> GetUpdatesResponse( - GetUpdatesResponse.Update.Reassignment(reassignment) - ).withPrecomputedSerializedSize(), - convertTransaction = transaction => - Offset.tryFromLong(transaction.offset) -> GetUpdatesResponse( - GetUpdatesResponse.Update.Transaction(transaction) - ).withPrecomputedSerializedSize(), - ) - ) - } - .mapConcat(identity) - .mergeSorted(topologyTransactions)( - Ordering.by[(Offset, GetUpdatesResponse), Long](_._1.unwrap)(longOrdering) - ) - } - - private def reverseIfDescendingOrder( - descendingOrder: Boolean, - rawEvents: Vector[RawEvent], - ): Vector[RawEvent] = - if (descendingOrder) rawEvents.reverse else rawEvents - - private def doStreamAcsDelta( - queryRange: EventsRange, - txInternalEventFormat: Option[InternalEventFormat], - reassignmentInternalEventFormat: Option[InternalEventFormat], - txAndReassignmentFilters: Set[DecomposedFilter], - justTxFilters: Set[DecomposedFilter], - justReassignmentFilters: Set[DecomposedFilter], - payloadQueriesLimiter: QueueBasedConcurrencyLimiter, - idPageSizing: IdPageSizing, - descendingOrder: Boolean, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[RawEvent, NotUsed] = { - val longOrdering: Ordering[Long] = - orderingBasedOnDescending(descendingOrder = descendingOrder) - val activateEventIdQueriesLimiter = - new QueueBasedConcurrencyLimiter(maxParallelIdActivateQueries, executionContext) - val deactivateEventIdQueriesLimiter = - new QueueBasedConcurrencyLimiter(maxParallelIdDeactivateQueries, executionContext) - - val idsActivate = - txAndReassignmentFilters.iterator - .map(filter => - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for ActivateStakeholder $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.activateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = activateEventIdQueriesLimiter, - metric = dbMetrics.updatesAcsDeltaStream.fetchEventActivateIdsStakeholder, - descendingOrder = descendingOrder, - ) - ) - .++( - justTxFilters.iterator.map(filter => - fetchIdsFiltered( - queryRange = queryRange, - idStreamName = - s"Update event seq IDs for ActivateStakeholder $filter for only Create type", - idFilterPageQuery = eventStorageBackend.updateStreamingQueries - .activateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ) - .filteredForEventTypes(Set(PersistentEventType.Create)), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = activateEventIdQueriesLimiter, - metricForLast = - dbMetrics.updatesAcsDeltaStream.fetchEventActivateIdsStakeholderFilteredRange, - metricFiltered = - dbMetrics.updatesAcsDeltaStream.fetchEventActivateIdsStakeholderFilteredIds, - descendingOrder = descendingOrder, - ) - ) - ) - .++( - justReassignmentFilters.iterator.map(filter => - fetchIdsFiltered( - queryRange = queryRange, - idStreamName = - s"Update event seq IDs for ActivateStakeholder $filter for only Assign type", - idFilterPageQuery = eventStorageBackend.updateStreamingQueries - .activateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ) - .filteredForEventTypes(Set(PersistentEventType.Assign)), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = activateEventIdQueriesLimiter, - metricForLast = - dbMetrics.updatesAcsDeltaStream.fetchEventActivateIdsStakeholderFilteredRange, - metricFiltered = - dbMetrics.updatesAcsDeltaStream.fetchEventActivateIdsStakeholderFilteredIds, - descendingOrder = descendingOrder, - ) - ) - ) - .toVector - .pipe( - mergeSortAndBatch( - maxOutputBatchSize = maxPayloadsPerPayloadsPage, - maxOutputBatchCount = maxParallelPayloadActivateQueries + 1, - descendingOrder = descendingOrder, - ) - ) - - val idsDeactivate = - txAndReassignmentFilters.iterator - .map(filter => - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for DeactivateStakeholder $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.deactivateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = deactivateEventIdQueriesLimiter, - metric = dbMetrics.updatesAcsDeltaStream.fetchEventDeactivateIdsStakeholder, - descendingOrder = descendingOrder, - ) - ) - .++( - justTxFilters.iterator.map(filter => - fetchIdsFiltered( - queryRange = queryRange, - idStreamName = - s"Update event seq IDs for DeactivateStakeholder $filter for only ConsumingExercise type", - idFilterPageQuery = eventStorageBackend.updateStreamingQueries - .deactivateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ) - .filteredForEventTypes(Set(PersistentEventType.ConsumingExercise)), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = deactivateEventIdQueriesLimiter, - metricForLast = - dbMetrics.updatesAcsDeltaStream.fetchEventDeactivateIdsStakeholderFilteredRange, - metricFiltered = - dbMetrics.updatesAcsDeltaStream.fetchEventDeactivateIdsStakeholderFilteredIds, - descendingOrder = descendingOrder, - ) - ) - ) - .++( - justReassignmentFilters.iterator.map(filter => - fetchIdsFiltered( - queryRange = queryRange, - idStreamName = - s"Update event seq IDs for DeactivateStakeholder $filter for only Unassign type", - idFilterPageQuery = eventStorageBackend.updateStreamingQueries - .deactivateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ) - .filteredForEventTypes(Set(PersistentEventType.Unassign)), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = deactivateEventIdQueriesLimiter, - metricForLast = - dbMetrics.updatesAcsDeltaStream.fetchEventDeactivateIdsStakeholderFilteredRange, - metricFiltered = - dbMetrics.updatesAcsDeltaStream.fetchEventDeactivateIdsStakeholderFilteredIds, - descendingOrder = descendingOrder, - ) - ) - ) - .toVector - .pipe( - mergeSortAndBatch( - maxOutputBatchSize = maxPayloadsPerPayloadsPage, - maxOutputBatchCount = maxParallelPayloadDeactivateQueries + 1, - descendingOrder = descendingOrder, - ) - ) - - val payloadsActivate = - fetchPayloads( - queryRange = queryRange, - ids = idsActivate, - fetchEvents = (ids, connection) => - eventStorageBackend - .fetchEventPayloadsAcsDelta( - EventPayloadSourceForUpdatesAcsDelta.Activate - )( - eventSequentialIds = Ids(ids), - requestingPartiesForTx = - txInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = - reassignmentInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - )(connection) - .reverseIfDescendingOrder(descendingOrder), - maxParallelPayloadQueries = maxParallelPayloadActivateQueries, - dbMetric = dbMetrics.updatesAcsDeltaStream.fetchEventActivatePayloads, - payloadQueriesLimiter = payloadQueriesLimiter, - contractStore = contractStore, - skipPruningChecks = skipPruningChecks, - ) - val payloadsDeactivate = - fetchPayloads( - queryRange = queryRange, - ids = idsDeactivate, - fetchEvents = (ids, connection) => - eventStorageBackend - .fetchEventPayloadsAcsDelta( - EventPayloadSourceForUpdatesAcsDelta.Deactivate - )( - eventSequentialIds = Ids(ids), - requestingPartiesForTx = - txInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = - reassignmentInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - )(connection) - .reverseIfDescendingOrder(descendingOrder), - maxParallelPayloadQueries = maxParallelPayloadActivateQueries, - dbMetric = dbMetrics.updatesAcsDeltaStream.fetchEventDeactivatePayloads, - payloadQueriesLimiter = payloadQueriesLimiter, - contractStore = contractStore, - skipPruningChecks = skipPruningChecks, - ) - - payloadsActivate - .mergeSorted(payloadsDeactivate)(Ordering.by[RawEvent, Long](_.eventSeqId)(longOrdering)) - } - - private def doStreamLedgerEffects( - queryRange: EventsRange, - txInternalEventFormat: Option[InternalEventFormat], - reassignmentInternalEventFormat: Option[InternalEventFormat], - txAndReassignmentFilters: Set[DecomposedFilter], - justTxFilters: Set[DecomposedFilter], - justReassignmentFilters: Set[DecomposedFilter], - payloadQueriesLimiter: QueueBasedConcurrencyLimiter, - idPageSizing: IdPageSizing, - descendingOrder: Boolean, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[RawEvent, NotUsed] = { - implicit val longOrdering: Ordering[Long] = - orderingBasedOnDescending(descendingOrder = descendingOrder) - - val activateEventIdQueriesLimiter = - new QueueBasedConcurrencyLimiter(maxParallelIdActivateQueries, executionContext) - val deactivateEventIdQueriesLimiter = - new QueueBasedConcurrencyLimiter(maxParallelIdDeactivateQueries, executionContext) - val variousWitnessedEventIdQueriesLimiter = - new QueueBasedConcurrencyLimiter(maxParallelIdVariousWitnessedQueries, executionContext) - - val idsActivate = - txAndReassignmentFilters.iterator - .map(filter => - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for ActivateStakeholder $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.activateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = activateEventIdQueriesLimiter, - metric = dbMetrics.updatesLedgerEffectsStream.fetchEventActivateIdsStakeholder, - descendingOrder = descendingOrder, - ) - ) - .++( - justTxFilters.iterator.map(filter => - fetchIdsFiltered( - queryRange = queryRange, - idStreamName = - s"Update event seq IDs for ActivateStakeholder $filter for only Create type", - idFilterPageQuery = eventStorageBackend.updateStreamingQueries - .activateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ) - .filteredForEventTypes(Set(PersistentEventType.Create)), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = activateEventIdQueriesLimiter, - metricForLast = - dbMetrics.updatesLedgerEffectsStream.fetchEventActivateIdsStakeholderFilteredRange, - metricFiltered = - dbMetrics.updatesLedgerEffectsStream.fetchEventActivateIdsStakeholderFilteredIds, - descendingOrder = descendingOrder, - ) - ) - ) - .++( - justReassignmentFilters.iterator.map(filter => - fetchIdsFiltered( - queryRange = queryRange, - idStreamName = - s"Update event seq IDs for ActivateStakeholder $filter for only Assign type", - idFilterPageQuery = eventStorageBackend.updateStreamingQueries - .activateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ) - .filteredForEventTypes(Set(PersistentEventType.Assign)), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = activateEventIdQueriesLimiter, - metricForLast = - dbMetrics.updatesLedgerEffectsStream.fetchEventActivateIdsStakeholderFilteredRange, - metricFiltered = - dbMetrics.updatesLedgerEffectsStream.fetchEventActivateIdsStakeholderFilteredIds, - descendingOrder = descendingOrder, - ) - ) - ) - .++( - txAndReassignmentFilters.iterator.map(filter => - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for ActivateWitnesses $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.activateWitnessesIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = activateEventIdQueriesLimiter, - metric = dbMetrics.updatesLedgerEffectsStream.fetchEventActivateIdsWitness, - descendingOrder = descendingOrder, - ) - ) - ) - .++( - justTxFilters.iterator.map(filter => - // there are no witnessed only reassignments - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for ActivateWitnesses $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.activateWitnessesIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = activateEventIdQueriesLimiter, - metric = dbMetrics.updatesLedgerEffectsStream.fetchEventActivateIdsWitness, - descendingOrder = descendingOrder, - ) - ) - ) - .toVector - .pipe( - mergeSortAndBatch( - maxOutputBatchSize = maxPayloadsPerPayloadsPage, - maxOutputBatchCount = maxParallelPayloadActivateQueries + 1, - descendingOrder = descendingOrder, - ) - ) - val idsDeactivate = - txAndReassignmentFilters.iterator - .map(filter => - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for DeactivateStakeholder $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.deactivateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = deactivateEventIdQueriesLimiter, - metric = dbMetrics.updatesLedgerEffectsStream.fetchEventDeactivateIdsStakeholder, - descendingOrder = descendingOrder, - ) - ) - .++( - justTxFilters.iterator.map(filter => - fetchIdsFiltered( - queryRange = queryRange, - idStreamName = - s"Update event seq IDs for DeactivateStakeholder $filter for only ConsumingExercise type", - idFilterPageQuery = eventStorageBackend.updateStreamingQueries - .deactivateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ) - .filteredForEventTypes(Set(PersistentEventType.ConsumingExercise)), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = deactivateEventIdQueriesLimiter, - metricForLast = - dbMetrics.updatesLedgerEffectsStream.fetchEventDeactivateIdsStakeholderFilteredRange, - metricFiltered = - dbMetrics.updatesLedgerEffectsStream.fetchEventDeactivateIdsStakeholderFilteredIds, - descendingOrder = descendingOrder, - ) - ) - ) - .++( - justReassignmentFilters.iterator.map(filter => - fetchIdsFiltered( - queryRange = queryRange, - idStreamName = - s"Update event seq IDs for DeactivateStakeholder $filter for only Unassign type", - idFilterPageQuery = eventStorageBackend.updateStreamingQueries - .deactivateStakeholderIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ) - .filteredForEventTypes(Set(PersistentEventType.Unassign)), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = deactivateEventIdQueriesLimiter, - metricForLast = - dbMetrics.updatesLedgerEffectsStream.fetchEventDeactivateIdsStakeholderFilteredRange, - metricFiltered = - dbMetrics.updatesLedgerEffectsStream.fetchEventDeactivateIdsStakeholderFilteredIds, - descendingOrder = descendingOrder, - ) - ) - ) - .++( - txAndReassignmentFilters.iterator.map(filter => - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for DeactivateWitnesses $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.deactivateWitnessesIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = deactivateEventIdQueriesLimiter, - metric = dbMetrics.updatesLedgerEffectsStream.fetchEventDeactivateIdsWitness, - descendingOrder = descendingOrder, - ) - ) - ) - .++( - justTxFilters.iterator.map(filter => - // only tx-es can have only witnesses, so no filtering needed - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for DeactivateWitnesses $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.deactivateWitnessesIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = deactivateEventIdQueriesLimiter, - metric = dbMetrics.updatesLedgerEffectsStream.fetchEventDeactivateIdsWitness, - descendingOrder = descendingOrder, - ) - ) - ) - .toVector - .pipe( - mergeSortAndBatch( - maxOutputBatchSize = maxPayloadsPerPayloadsPage, - maxOutputBatchCount = maxParallelPayloadDeactivateQueries + 1, - descendingOrder = descendingOrder, - ) - ) - val idsVariousWitnessed = - txAndReassignmentFilters.iterator - .map(filter => - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for VariousWitnesses $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.variousWitnessIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = variousWitnessedEventIdQueriesLimiter, - metric = dbMetrics.updatesLedgerEffectsStream.fetchEventVariousIdsWitness, - descendingOrder = descendingOrder, - ) - ) - .++( - justTxFilters.iterator.map(filter => - // only tx-es can be witnessed only - fetchIdsNonFiltered( - queryRange = queryRange, - idStreamName = s"Update event seq IDs for VariousWitnesses $filter", - idPageQuery = eventStorageBackend.updateStreamingQueries.variousWitnessIds( - witnessO = filter.party, - templateIdO = filter.templateId, - ), - idPageSizing = idPageSizing, - maxParallelIdQueriesLimiter = variousWitnessedEventIdQueriesLimiter, - metric = dbMetrics.updatesLedgerEffectsStream.fetchEventVariousIdsWitness, - descendingOrder = descendingOrder, - ) - ) - ) - .toVector - .pipe( - mergeSortAndBatch( - maxOutputBatchSize = maxPayloadsPerPayloadsPage, - maxOutputBatchCount = maxParallelPayloadVariousWitnessedQueries + 1, - descendingOrder = descendingOrder, - ) - ) - - val payloadsActivate = - fetchPayloads( - queryRange = queryRange, - ids = idsActivate, - fetchEvents = (ids, connection) => - eventStorageBackend - .fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Activate - )( - eventSequentialIds = Ids(ids), - requestingPartiesForTx = - txInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = - reassignmentInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - )(connection) - .reverseIfDescendingOrder(descendingOrder), - maxParallelPayloadQueries = maxParallelPayloadActivateQueries, - dbMetric = dbMetrics.updatesLedgerEffectsStream.fetchEventActivatePayloads, - payloadQueriesLimiter = payloadQueriesLimiter, - contractStore = contractStore, - skipPruningChecks = skipPruningChecks, - ) - val payloadsDeactivate = - fetchPayloads( - queryRange = queryRange, - ids = idsDeactivate, - fetchEvents = (ids, connection) => - eventStorageBackend - .fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Deactivate - )( - eventSequentialIds = Ids(ids), - requestingPartiesForTx = - txInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = - reassignmentInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - )(connection) - .reverseIfDescendingOrder(descendingOrder), - maxParallelPayloadQueries = maxParallelPayloadDeactivateQueries, - dbMetric = dbMetrics.updatesLedgerEffectsStream.fetchEventDeactivatePayloads, - payloadQueriesLimiter = payloadQueriesLimiter, - contractStore = contractStore, - skipPruningChecks = skipPruningChecks, - ) - val payloadsVariousWitnessed = - fetchPayloads( - queryRange = queryRange, - ids = idsVariousWitnessed, - fetchEvents = (ids, connection) => - eventStorageBackend - .fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed - )( - eventSequentialIds = Ids(ids), - requestingPartiesForTx = - txInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - requestingPartiesForReassignment = - reassignmentInternalEventFormat.flatMap(_.templatePartiesFilter.allFilterParties), - )(connection) - .reverseIfDescendingOrder(descendingOrder), - maxParallelPayloadQueries = maxParallelPayloadVariousWitnessedQueries, - dbMetric = dbMetrics.updatesLedgerEffectsStream.fetchEventVariousWitnessedPayloads, - payloadQueriesLimiter = payloadQueriesLimiter, - contractStore = contractStore, - skipPruningChecks = skipPruningChecks, - ) - - payloadsActivate - .mergeSorted(payloadsDeactivate)(Ordering.by(_.eventSeqId)) - .mergeSorted(payloadsVariousWitnessed)(Ordering.by(_.eventSeqId)) - } - - private def fetchIdsNonFiltered( - queryRange: EventsRange, - idStreamName: String, - idPageQuery: IdPageQuery, - idPageSizing: IdPageSizing, - maxParallelIdQueriesLimiter: QueueBasedConcurrencyLimiter, - metric: DatabaseMetrics, - descendingOrder: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[Long, NotUsed] = - paginatingAsyncStream.streamIdsFromSeekPaginationWithoutIdFilter( - idStreamName = idStreamName, - idPageSizing = idPageSizing, - idPageBufferSize = maxPagesPerIdPagesBuffer, - initialFromIdExclusive = queryRange.startInclusiveEventSeqId, - initialEndInclusive = queryRange.endInclusiveEventSeqId, - descendingOrder = descendingOrder, - )(idPageQuery)( - executeIdQuery = f => - maxParallelIdQueriesLimiter.execute { - globalIdQueriesLimiter.execute { - dbDispatcher.executeSql(metric)(f) - } - } - ) - - private def fetchIdsFiltered( - queryRange: EventsRange, - idStreamName: String, - idFilterPageQuery: IdFilterPageQuery, - idPageSizing: IdPageSizing, - maxParallelIdQueriesLimiter: QueueBasedConcurrencyLimiter, - metricForLast: DatabaseMetrics, - metricFiltered: DatabaseMetrics, - descendingOrder: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[Long, NotUsed] = - paginatingAsyncStream.streamIdsFromSeekPaginationWithIdFilter( - idStreamName = idStreamName, - idPageSizing = idPageSizing, - idPageBufferSize = maxPagesPerIdPagesBuffer, - initialFromIdExclusive = queryRange.startInclusiveEventSeqId, - initialEndInclusive = queryRange.endInclusiveEventSeqId, - descendingOrder = descendingOrder, - )(idFilterPageQuery)( - executeFetchBounds = f => - maxParallelIdQueriesLimiter.execute { - globalIdQueriesLimiter.execute { - dbDispatcher.executeSql(metricForLast)(f) - } - }, - idFilterQueryParallelism = idFilterQueryParallelism, - executeFetchPage = f => - maxParallelIdQueriesLimiter.execute { - globalIdQueriesLimiter.execute { - dbDispatcher.executeSql(metricFiltered)(f) - } - }, - ) - - private def mergeSortAndBatch( - maxOutputBatchSize: Int, - maxOutputBatchCount: Int, - descendingOrder: Boolean, - )(sourcesOfIds: Vector[Source[Long, NotUsed]]): Source[Iterable[Long], NotUsed] = - EventIdsUtils - .sortAndDeduplicateIds(descendingOrder)(sourcesOfIds) - .batchN( - maxBatchSize = maxOutputBatchSize, - maxBatchCount = maxOutputBatchCount, - ) - - private def fetchPayloads( - queryRange: EventsRange, - ids: Source[Iterable[Long], NotUsed], - fetchEvents: (Iterable[Long], Connection) => Vector[RawThinEvent], - maxParallelPayloadQueries: Int, - dbMetric: DatabaseMetrics, - payloadQueriesLimiter: ConcurrencyLimiter, - contractStore: LedgerApiContractStore, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[RawEvent, NotUsed] = { - // Pekko requires for this buffer's size to be a power of two. - val inputBufferSize = Utils.largestSmallerOrEqualPowerOfTwo(maxParallelPayloadQueries) - ids - .addAttributes(Attributes.inputBuffer(initial = inputBufferSize, max = inputBufferSize)) - .mapAsync(maxParallelPayloadQueries)(ids => - payloadQueriesLimiter.execute { - globalPayloadQueriesLimiter.execute { - UpdatesStreamReader.fetchContractPayloadsInternal( - queryRange = queryRange, - dbMetric = dbMetric, - contractStore = contractStore, - skipPruningChecks = skipPruningChecks, - ids = ids, - fetchEvents = fetchEvents, - pruningOffsetService = pruningOffsetService, - queryValidRange = queryValidRange, - dbDispatcher = dbDispatcher, - ) - } - } - ) - .mapConcat(identity) - } - - private def orderingBasedOnDescending(descendingOrder: Boolean): Ordering[Long] = - if (descendingOrder) - Ordering.Long.reverse - else - Ordering.Long -} - -object UpdatesStreamReader { - final implicit class VectorOps[T](vec: Vector[T]) { - def reverseIfDescendingOrder(descendingOrder: Boolean): Vector[T] = - if (descendingOrder) vec.reverse else vec - } - - def fetchContractPayloadsInternal( - queryRange: EventsRange, - dbMetric: DatabaseMetrics, - contractStore: LedgerApiContractStore, - skipPruningChecks: Boolean, - ids: Iterable[Long], - fetchEvents: (Iterable[Long], Connection) => Vector[RawThinEvent], - pruningOffsetService: PruningOffsetService, - queryValidRange: QueryValidRange, - dbDispatcher: DbDispatcher, - )(implicit - loggingContext: LoggingContextWithTrace, - executionContext: ExecutionContext, - errorLoggingContext: ErrorLoggingContext, - ): Future[Vector[RawEvent]] = { - val pruningCheck: Future[Vector[(RawThinEvent, Option[FatContract])]] => Future[ - Vector[(RawThinEvent, Option[FatContract])] - ] = if (skipPruningChecks) { - _.flatMap(events => - pruningOffsetService.pruningOffset.map(prunedTo => - events.filter(_._1.offset > prunedTo.fold(0L)(_.unwrap)) - ) - ) // Remove all elements after a pruning offset not to break tryToResolveFatInstance - } else { - queryValidRange - .withRangeNotPruned( - minOffsetInclusive = queryRange.startInclusiveOffset, - maxOffsetInclusive = queryRange.endInclusiveOffset, - errorPruning = (prunedOffset: Offset) => - s"Updates request from ${queryRange.startInclusiveOffset.unwrap} to ${queryRange.endInclusiveOffset.unwrap} precedes pruned offset ${prunedOffset.unwrap}", - errorLedgerEnd = (ledgerEndOffset: Option[Offset]) => - s"Updates request from ${queryRange.startInclusiveOffset.unwrap} to ${queryRange.endInclusiveOffset.unwrap} is beyond ledger end offset ${ledgerEndOffset - .fold(0L)(_.unwrap)}", - )(_) - } - pruningCheck { - dbDispatcher - .executeSql(dbMetric)(fetchEvents(ids, _)) - .flatMap(UpdateReader.withFatContractIfNeeded(contractStore)) - } - .map(UpdateReader.tryToResolveFatInstance) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/Utils.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/Utils.scala deleted file mode 100644 index 8b6c5f78fc..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/dao/events/Utils.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -object Utils { - - /** @param n - * needs to be positive - */ - def largestSmallerOrEqualPowerOfTwo(n: Int): Int = - Integer.highestOneBit(n) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interfaces/LedgerDaoContractsReader.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interfaces/LedgerDaoContractsReader.scala deleted file mode 100644 index b2ad636cba..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interfaces/LedgerDaoContractsReader.scala +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interfaces - -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.ExistingContractStatus -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader.* -import com.digitalasset.daml.lf.transaction.GlobalKey -import com.digitalasset.daml.lf.value.Value.ContractId -import com.google.common.annotations.VisibleForTesting - -import scala.concurrent.Future - -private[platform] trait LedgerDaoContractsReader { - - /** Looks up the contract by id - * - * Due to batching of several requests, we may return newer information than at the provided - * offset, but never older information. - * - * @param contractId - * the contract id to query - * @param notEarlierThanEventSeqId - * the offset threshold to resolve the contract state (state can be newer, but not older) - * @return - * the optional boolean flag indicating whether the contract is active (true) or archived - * (false). None if the contract is not found. - */ - def lookupContractState(contractId: ContractId, notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ExistingContractStatus]] - - /** Looks up the state of a contract key - * - * Due to batching of several requests, we may return newer information than at the provided - * offset, but never older information. - * - * @param key - * the contract key to query - * @param notEarlierThanEventSeqId - * the offset threshold to resolve the key state (state can be newer, but not older) - * @return - * the [[KeyState]] - */ - def lookupKeyState(key: GlobalKey, notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[KeyState] - - /** Batch lookup of contract keys - * - * Used to unit test the SQL queries for key lookups. Does not use batching. - */ - @VisibleForTesting - def lookupKeyStatesFromDb(keys: Seq[GlobalKey], notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Map[GlobalKey, Long]] - - /** Looks up active contracts for a given key. - * - * Due to batching of several requests, we may return newer information than at the provided - * offset, but never older information. - * - * @param key - * the contract key to query - * @param notEarlierThanEventSeqId - * the offset threshold to resolve the key state (state can be newer, but not older) - * @param nextPageToken - * pagination token for fetching subsequent pages - * @param limit - * maximum number of contract IDs to return - * @return - * a vector of active contract IDs and an optional next page token - */ - def lookupNonUniqueKey( - key: GlobalKey, - notEarlierThanEventSeqId: Long, - nextPageToken: Option[Long], - limit: Int, - )(implicit loggingContext: LoggingContextWithTrace): Future[(Vector[ContractId], Option[Long])] - -} - -object LedgerDaoContractsReader { - import com.digitalasset.daml.lf.value.Value as lfval - private type ContractId = lfval.ContractId - - sealed trait KeyState extends Product with Serializable - - final case class KeyAssigned(contractId: ContractId) extends KeyState - - final case object KeyUnassigned extends KeyState -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interfaces/TransactionLogUpdate.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interfaces/TransactionLogUpdate.scala deleted file mode 100644 index cbd26d64c6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interfaces/TransactionLogUpdate.scala +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interfaces - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.digitalasset.canton.crypto.Hash as CantonHash -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.TransactionShape -import com.digitalasset.canton.ledger.api.TransactionShape.{AcsDelta, LedgerEffects} -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent -import com.digitalasset.canton.ledger.participant.state.{Reassignment, ReassignmentInfo} -import com.digitalasset.canton.platform.store.cache.MutableCacheBackedContractStore.EventSequentialId -import com.digitalasset.canton.platform.{ContractId, Identifier} -import com.digitalasset.canton.tracing.{HasTraceContext, TraceContext} -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.{PackageName, Party} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.value.Value as LfValue - -/** Generic ledger update event. - * - * Used as data source template for in-memory fan-out buffers for Ledger API streams serving. - */ -sealed trait TransactionLogUpdate extends Product with Serializable with HasTraceContext { - def offset: Offset - def completionStreamResponseO: Option[CompletionStreamResponse] - - /** Traffic cost paid by this node for the ordering of the corresponding confirmation request. - * Only provided if the requesting parties are submitting parties (actAs) - */ - def paidTrafficCost(requestingParties: Option[Set[Party]]): Option[EventSequentialId] = - completionStreamResponseO - .flatMap(_.completionResponse.completion) - .filter { completion => - requestingParties.fold(true)(_.exists(completion.actAs.toSet)) - } - .map(_.paidTrafficCost) -} - -object TransactionLogUpdate { - - /** Complete view of a ledger transaction. - * - * @param updateId - * The transaction id. - * @param workflowId - * The workflow id. - * @param effectiveAt - * The transaction ledger time. - * @param offset - * The transaction's offset in the ledger. - * @param events - * The transaction events, in execution order. - * @param completionStreamResponse - * The successful submission's completion details. - * @param recordTime - * The time at which the transaction was recorded. - * @param externalTransactionHash - * Hash of the transaction (for externall signed transactions only) - */ - final case class TransactionAccepted( - updateId: String, - commandId: String, - workflowId: String, - effectiveAt: Timestamp, - offset: Offset, - events: Vector[Event], - completionStreamResponseO: Option[CompletionStreamResponse], - synchronizerId: String, - recordTime: Timestamp, - externalTransactionHash: Option[CantonHash], - )(implicit override val traceContext: TraceContext) - extends TransactionLogUpdate - - /** A rejected submission. - * - * @param offset - * The offset at which the rejection has been enqueued in the ledger. - * @param completionStreamResponse - * The rejected submission's completion details. - */ - final case class TransactionRejected( - offset: Offset, - completionStreamResponse: CompletionStreamResponse, - )(implicit override val traceContext: TraceContext) - extends TransactionLogUpdate { - override def completionStreamResponseO: Option[CompletionStreamResponse] = Some( - completionStreamResponse - ) - } - - final case class ReassignmentAccepted( - updateId: String, - commandId: String, - workflowId: String, - offset: Offset, - recordTime: Timestamp, - completionStreamResponseO: Option[CompletionStreamResponse], - reassignmentInfo: ReassignmentInfo, - reassignment: Reassignment.Batch, - synchronizerId: String, - )(implicit override val traceContext: TraceContext) - extends TransactionLogUpdate { - def stakeholders: Set[Ref.Party] = reassignment.iterator.flatMap(_.stakeholders).toSet - } - - final case class TopologyTransactionEffective( - updateId: String, - offset: Offset, - effectiveTime: Timestamp, - synchronizerId: String, - events: Vector[PartyToParticipantAuthorization], - )(implicit override val traceContext: TraceContext) - extends TransactionLogUpdate { - override def completionStreamResponseO: Option[CompletionStreamResponse] = None - } - - /* Models all but divulgence events */ - sealed trait Event extends Product with Serializable { - def eventOffset: Offset - def eventSequentialId: EventSequentialId - def updateId: String - def commandId: String - def workflowId: String - def ledgerEffectiveTime: Timestamp - def treeEventWitnesses: Set[Party] - def flatEventWitnesses: Set[Party] - def witnesses(transactionShape: TransactionShape): Set[Party] - def submitters: Set[Party] - def templateId: Identifier - def packageName: PackageName - def contractId: ContractId - } - - final case class CreatedEvent( - eventOffset: Offset, - updateId: String, - nodeId: Int, - eventSequentialId: Long, - contractId: ContractId, - ledgerEffectiveTime: Timestamp, - templateId: Identifier, - representativePackageId: Ref.PackageId, - packageName: PackageName, - packageVersion: Option[Ref.PackageVersion], - commandId: String, - workflowId: String, - contractKey: Option[LfValue.VersionedValue], - treeEventWitnesses: Set[Party], - flatEventWitnesses: Set[Party], - submitters: Set[Party], - createArgument: LfValue.VersionedValue, - createSignatories: Set[Party], - createObservers: Set[Party], - createKeyHash: Option[Hash], - createKeyMaintainers: Option[Set[Party]], - authenticationData: Bytes, - ) extends Event { - def witnesses(transactionShape: TransactionShape): Set[Party] = - transactionShape match { - case AcsDelta => flatEventWitnesses - case LedgerEffects => treeEventWitnesses - } - } - - final case class ExercisedEvent( - eventOffset: Offset, - updateId: String, - nodeId: Int, - eventSequentialId: Long, - contractId: ContractId, - ledgerEffectiveTime: Timestamp, - templateId: Identifier, - packageName: PackageName, - interfaceId: Option[Identifier], - commandId: String, - workflowId: String, - contractKey: Option[LfValue.VersionedValue], - contractKeyHash: Option[Hash], - treeEventWitnesses: Set[Party], - flatEventWitnesses: Set[Party], - submitters: Set[Party], - choice: String, - actingParties: Set[Party], - lastDescendantNodeId: Int, - exerciseArgument: LfValue.VersionedValue, - exerciseResult: Option[LfValue.VersionedValue], - consuming: Boolean, - ) extends Event { - def witnesses(transactionShape: TransactionShape): Set[Party] = - transactionShape match { - case AcsDelta => flatEventWitnesses - case LedgerEffects => treeEventWitnesses - } - } - - final case class PartyToParticipantAuthorization( - party: Party, - participant: Ref.ParticipantId, - authorizationEvent: AuthorizationEvent, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/MockStringInterning.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/MockStringInterning.scala deleted file mode 100644 index 7dd550737d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/MockStringInterning.scala +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interning - -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.Mutex -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.Party -import com.google.common.annotations.VisibleForTesting - -/** This StringInterning implementation is interning in a transparent way everything it sees. This - * is only for test purposes. - */ -@SuppressWarnings(Array("org.wartremover.warts.OptionPartial", "org.wartremover.warts.Var")) -@VisibleForTesting -class MockStringInterning extends StringInterning { - @volatile private var idToString: Map[Int, String] = Map.empty - @volatile private var stringToId: Map[String, Int] = Map.empty - @volatile private var autoIntern: Boolean = true - @volatile private var lastId: Int = 0 - private val lock = new Mutex() - - private val rawStringInterning: StringInterningAccessor[String] = - new StringInterningAccessor[String] { - override def internalize(t: String): Int = tryInternalize(t).get - - override def tryInternalize(t: String): Option[Int] = (lock.exclusive { - stringToId.get(t) match { - case Some(id) => Some(id) - case None if autoIntern => - lastId += 1 - idToString = idToString + (lastId -> t) - stringToId = stringToId + (t -> lastId) - Some(lastId) - case None => None - } - }) - - override def externalize(id: Int): String = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[String] = idToString.get(id) - - } - - override val templateId: StringInterningDomain[Ref.NameTypeConRef] = - new StringInterningDomain[Ref.NameTypeConRef] { - override val unsafe: StringInterningAccessor[String] = rawStringInterning - - override def internalize(t: Ref.NameTypeConRef): Int = tryInternalize(t).get - - override def tryInternalize(t: Ref.NameTypeConRef): Option[Int] = - rawStringInterning.tryInternalize(t.toString) - - override def externalize(id: Int): Ref.NameTypeConRef = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[Ref.NameTypeConRef] = - rawStringInterning.tryExternalize(id).map(Ref.NameTypeConRef.assertFromString) - } - - override val packageId: StringInterningDomain[Ref.PackageId] = - new StringInterningDomain[Ref.PackageId] { - override val unsafe: StringInterningAccessor[String] = rawStringInterning - - override def internalize(t: Ref.PackageId): Int = tryInternalize(t).get - - override def tryInternalize(t: Ref.PackageId): Option[Int] = - rawStringInterning.tryInternalize(t.toString) - - override def externalize(id: Int): Ref.PackageId = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[Ref.PackageId] = - rawStringInterning.tryExternalize(id).map(Ref.PackageId.assertFromString) - } - - override def party: StringInterningDomain[Party] = - new StringInterningDomain[Party] { - override val unsafe: StringInterningAccessor[String] = rawStringInterning - - override def internalize(t: Party): Int = tryInternalize(t).get - - override def tryInternalize(t: Party): Option[Int] = - rawStringInterning.tryInternalize(t.toString) - - override def externalize(id: Int): Party = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[Party] = - rawStringInterning.tryExternalize(id).map(Party.assertFromString) - } - - override val synchronizerId: StringInterningDomain[SynchronizerId] = - new StringInterningDomain[SynchronizerId] { - override val unsafe: StringInterningAccessor[String] = rawStringInterning - - override def internalize(t: SynchronizerId): Int = tryInternalize(t).get - - override def tryInternalize(t: SynchronizerId): Option[Int] = - rawStringInterning.tryInternalize(t.toProtoPrimitive) - - override def externalize(id: Int): SynchronizerId = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[SynchronizerId] = - rawStringInterning.tryExternalize(id).map(SynchronizerId.tryFromString) - } - - override val userId: StringInterningDomain[Ref.UserId] = - new StringInterningDomain[Ref.UserId] { - override val unsafe: StringInterningAccessor[String] = rawStringInterning - - override def internalize(t: Ref.UserId): Int = tryInternalize(t).get - - override def tryInternalize(t: Ref.UserId): Option[Int] = - rawStringInterning.tryInternalize(t.toString) - - override def externalize(id: Int): Ref.UserId = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[Ref.UserId] = - rawStringInterning.tryExternalize(id).map(Ref.UserId.assertFromString) - } - - override val participantId: StringInterningDomain[Ref.ParticipantId] = - new StringInterningDomain[Ref.ParticipantId] { - override val unsafe: StringInterningAccessor[String] = rawStringInterning - - override def internalize(t: Ref.ParticipantId): Int = tryInternalize(t).get - - override def tryInternalize(t: Ref.ParticipantId): Option[Int] = - rawStringInterning.tryInternalize(t.toString) - - override def externalize(id: Int): Ref.ParticipantId = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[Ref.ParticipantId] = - rawStringInterning.tryExternalize(id).map(Ref.ParticipantId.assertFromString) - } - - override val choiceName: StringInterningDomain[Ref.ChoiceName] = - new StringInterningDomain[Ref.ChoiceName] { - override val unsafe: StringInterningAccessor[String] = rawStringInterning - - override def internalize(t: Ref.ChoiceName): Int = tryInternalize(t).get - - override def tryInternalize(t: Ref.ChoiceName): Option[Int] = - rawStringInterning.tryInternalize(t.toString) - - override def externalize(id: Int): Ref.ChoiceName = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[Ref.ChoiceName] = - rawStringInterning.tryExternalize(id).map(Ref.ChoiceName.assertFromString) - } - - override val interfaceId: StringInterningDomain[Ref.Identifier] = - new StringInterningDomain[Ref.Identifier] { - override val unsafe: StringInterningAccessor[String] = rawStringInterning - - override def internalize(t: Ref.Identifier): Int = tryInternalize(t).get - - override def tryInternalize(t: Ref.Identifier): Option[Int] = - rawStringInterning.tryInternalize(t.toString) - - override def externalize(id: Int): Ref.Identifier = tryExternalize(id).get - - override def tryExternalize(id: Int): Option[Ref.Identifier] = - rawStringInterning.tryExternalize(id).map(Ref.Identifier.assertFromString) - } - - private[store] def reset(): Unit = (lock.exclusive { - idToString = Map.empty - stringToId = Map.empty - autoIntern = true - lastId = 0 - }) - - private[store] def setAutoIntern(newAutoIntern: Boolean): Unit = (lock.exclusive { - autoIntern = newAutoIntern - }) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/RawStringInterning.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/RawStringInterning.scala deleted file mode 100644 index dc744e19b9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/RawStringInterning.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interning - -import com.google.common.annotations.VisibleForTesting - -/** @param map - * Maps strings to their internal ID. All IDs are non-negative. - * @param lastId - * The last ID in use. Must be non-negative. - */ -private[interning] final case class RawStringInterning @VisibleForTesting private[interning] ( - map: Map[String, Int], - idMap: Map[Int, String], - lastId: Int, -) - -private[interning] object RawStringInterning { - - def from( - entries: Iterable[(Int, String)], - rawStringInterning: RawStringInterning = RawStringInterning(Map.empty, Map.empty, 0), - ): RawStringInterning = - if (entries.isEmpty) rawStringInterning - else { - val lastId = entries.view.foldLeft(rawStringInterning.lastId) { (lastId, entry) => - val (entryId, entryString) = entry - if (entryId < 0) - throw new IllegalArgumentException( - s"String interning IDs must be non-negative, but $entryString has $entryId." - ) - Math.max(lastId, entryId) - } - RawStringInterning( - map = rawStringInterning.map ++ entries.view.map(_.swap), - idMap = rawStringInterning.idMap ++ entries, - lastId = lastId, - ) - } - - def newEntries( - distinctRawStrings: Iterable[String], - rawStringInterning: RawStringInterning, - ): Vector[(Int, String)] = - distinctRawStrings.view - .filterNot(rawStringInterning.map.contains) - .zipWithIndex - .map { case (string, index) => - val id = index + 1 + rawStringInterning.lastId - if (id < 0) - throw new ArithmeticException( - s"String interning overflow: too many strings interned. Id = $id" - ) - (id, string) - } - .toVector - - def resetTo( - lastPersistedStringInterningId: Int, - rawStringInterning: RawStringInterning, - ): RawStringInterning = - if (lastPersistedStringInterningId < 0) { - throw new IllegalArgumentException( - s"String interning IDs must be non-negative. Got: $lastPersistedStringInterningId" - ) - } else if (lastPersistedStringInterningId < rawStringInterning.lastId) { - val idsToBeRemoved = lastPersistedStringInterningId + 1 to rawStringInterning.lastId - val stringsToBeRemoved = idsToBeRemoved.map(rawStringInterning.idMap) - - RawStringInterning( - map = rawStringInterning.map.removedAll(stringsToBeRemoved), - idMap = rawStringInterning.idMap.removedAll(idsToBeRemoved), - lastId = lastPersistedStringInterningId, - ) - } else rawStringInterning -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/StringInterning.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/StringInterning.scala deleted file mode 100644 index 5638e9258b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/StringInterning.scala +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interning - -import com.digitalasset.canton.platform.Party -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Ref.{ - ChoiceName, - Identifier, - NameTypeConRef, - PackageId, - ParticipantId, - UserId, -} - -trait StringInterningBuilder { - def addTemplateId(templateId: NameTypeConRef): Unit - def addPackageId(packageId: PackageId): Unit - def addParty(party: Party): Unit - def addSynchronizerId(synchronizerId: SynchronizerId): Unit - def addUserId(userId: UserId): Unit - def addParticipantId(participantId: ParticipantId): Unit - def addChoiceName(choiceName: ChoiceName): Unit - def addInterfaceId(interfaceId: Identifier): Unit -} - -trait StringInterningProvider { - def provideInternedStrings(builder: StringInterningBuilder): Unit -} - -/** The facade for all supported string-interning domains - * - * @note - * The accessors defined in this interface are thread-safe and can be used concurrently with - * StringInterningView.internize and [[StringInterningView.update]]. - */ -trait StringInterning { - def templateId: StringInterningDomain[NameTypeConRef] - def packageId: StringInterningDomain[PackageId] - def party: StringInterningDomain[Party] - def synchronizerId: StringInterningDomain[SynchronizerId] - def userId: StringInterningDomain[UserId] - def participantId: StringInterningDomain[ParticipantId] - def choiceName: StringInterningDomain[ChoiceName] - def interfaceId: StringInterningDomain[Identifier] -} - -/** Composes a StringInterningAccessor for the domain-string type and an unsafe - * StringInterningAccessor for raw strings - * - * @tparam T - * is the type of the string-related domain object which is interned - */ -trait StringInterningDomain[T] extends StringInterningAccessor[T] { - def unsafe: StringInterningAccessor[String] -} - -object StringInterningDomain { - private[interning] def prefixing[T]( - prefix: String, - prefixedAccessor: StringInterningAccessor[String], - to: String => T, - from: T => String, - ): StringInterningDomain[T] = - new StringInterningDomain[T] { - override val unsafe: StringInterningAccessor[String] = new StringInterningAccessor[String] { - override def internalize(t: String): Int = prefixedAccessor.internalize(prefix + t) - - override def tryInternalize(t: String): Option[Int] = - prefixedAccessor.tryInternalize(prefix + t) - - override def externalize(id: Int): String = - prefixedAccessor.externalize(id).substring(prefix.length) - - override def tryExternalize(id: Int): Option[String] = - prefixedAccessor.tryExternalize(id).map(_.substring(prefix.length)) - } - - override def internalize(t: T): Int = unsafe.internalize(from(t)) - - override def tryInternalize(t: T): Option[Int] = unsafe.tryInternalize(from(t)) - - override def externalize(id: Int): T = to(unsafe.externalize(id)) - - override def tryExternalize(id: Int): Option[T] = unsafe.tryExternalize(id).map(to) - } -} - -/** The main interface for using string-interning. Client code can use this to map between interned - * id-s and string-domain objects back and forth. All interned ids are non-negative. - * - * @tparam T - * is the type of the string-related domain object which is interned - */ -trait StringInterningAccessor[T] { - - /** Get the interned id - * - * @param t - * the value - * @return - * the integer id, throws exception if id not found - */ - def internalize(t: T): Int - - /** Optionally get the interned id - * @param t - * the value - * @return - * some integer id, or none if not found - */ - def tryInternalize(t: T): Option[Int] - - /** Get the value for an id - * - * @param id - * integer id - * @return - * the value, throws exception if no value found - */ - def externalize(id: Int): T - - /** Optionally get the value for an id - * - * @param id - * integer id - * @return - * some value, or none if not found - */ - def tryExternalize(id: Int): Option[T] -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/StringInterningView.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/StringInterningView.scala deleted file mode 100644 index a9968addeb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/interning/StringInterningView.scala +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interning - -import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.platform.Party -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.Mutex -import com.digitalasset.daml.lf.data.Ref.{ - ChoiceName, - Identifier, - NameTypeConRef, - PackageId, - ParticipantId, - UserId, -} - -import scala.collection.mutable -import scala.concurrent.Future - -trait InternizingStringInterningView { - - private[platform] def distinctNewRawStrings( - interningProviders: Iterable[StringInterningProvider] - ): Iterable[String] - - /** @return - * If some of the entries were not part of the view: they will be added, and these will be - * returned as a interned-id and raw, prefixed string pairs. - * - * @note - * This method is thread-safe. This method should be called from Indexer, which maintains - * consistency between StringInterning view and persistence. - */ - private[platform] def internize( - distinctRawStrings: Iterable[String] - ): Iterable[(Int, String)] -} - -trait UpdatingStringInterningView { - - /** Update the StringInterningView from persistence - * - * @param lastStringInterningId - * this is the "version" of the persistent view, which from the StringInterningView can see if - * it is behind - * @return - * a completion Future: - * - * * if the view is behind, it will load the missing entries from persistence, and update the - * view state. - * - * * if the view is ahead, it will remove all entries with ids greater than the - * `lastStringInterningId` - * - * @note - * This method is NOT thread-safe and should not be called concurrently with itself or - * InternizingStringInterningView.internize. - */ - def update(lastStringInterningId: Option[Int])( - loadPrefixedEntries: LoadStringInterningEntries - ): Future[Unit] -} - -/** Encapsulate the dependency to load a range of string-interning-entries from persistence - */ -trait LoadStringInterningEntries { - def apply( - fromExclusive: Int, - toInclusive: Int, - ): Future[Iterable[(Int, String)]] -} - -/** This uses the prefixed raw representation internally similar to the persistence layer. - * Concurrent view usage is optimized for reading: - * - The single, volatile reference enables non-synchronized access from all threads, accessing - * persistent-immutable datastructure - * - On the writing side it synchronizes (this usage is anyway expected) and maintains the - * immutable internal datastructure - */ -class StringInterningView(override protected val loggerFactory: NamedLoggerFactory) - extends StringInterning - with InternizingStringInterningView - with UpdatingStringInterningView - with NamedLogging { - - private val directEc = DirectExecutionContext(noTracingLogger) - private val lock = new Mutex() - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - @volatile private var raw: RawStringInterning = RawStringInterning.from(Nil) - - private def rawAccessor: StringInterningAccessor[String] = new StringInterningAccessor[String] { - override def internalize(t: String): Int = raw.map(t) - override def tryInternalize(t: String): Option[Int] = raw.map.get(t) - override def externalize(id: Int): String = raw.idMap(id) - override def tryExternalize(id: Int): Option[String] = raw.idMap.get(id) - } - - private val TemplatePrefix = "t|" - private val PartyPrefix = "p|" - private val SynchronizerIdPrefix = "d|" - private val PackageIdPrefix = "i|" - private val UserIdPrefix = "u|" - private val ParticipantIdPrefix = "n|" - private val ChoicePrefix = "c|" - private val InterfacePrefix = "f|" - - override val templateId: StringInterningDomain[NameTypeConRef] = - StringInterningDomain.prefixing( - prefix = TemplatePrefix, - prefixedAccessor = rawAccessor, - to = NameTypeConRef.assertFromString, - from = _.toString, - ) - - override val party: StringInterningDomain[Party] = - StringInterningDomain.prefixing( - prefix = PartyPrefix, - prefixedAccessor = rawAccessor, - to = Party.assertFromString, - from = identity, - ) - - override val synchronizerId: StringInterningDomain[SynchronizerId] = - StringInterningDomain.prefixing( - prefix = SynchronizerIdPrefix, - prefixedAccessor = rawAccessor, - to = SynchronizerId.tryFromString, - from = _.toProtoPrimitive, - ) - - override val packageId: StringInterningDomain[PackageId] = - StringInterningDomain.prefixing( - prefix = PackageIdPrefix, - prefixedAccessor = rawAccessor, - to = PackageId.assertFromString, - from = identity, - ) - - override val userId: StringInterningDomain[UserId] = - StringInterningDomain.prefixing( - prefix = UserIdPrefix, - prefixedAccessor = rawAccessor, - to = UserId.assertFromString, - from = identity, - ) - - override def participantId: StringInterningDomain[ParticipantId] = - StringInterningDomain.prefixing( - prefix = ParticipantIdPrefix, - prefixedAccessor = rawAccessor, - to = ParticipantId.assertFromString, - from = identity, - ) - - override val choiceName: StringInterningDomain[ChoiceName] = - StringInterningDomain.prefixing( - prefix = ChoicePrefix, - prefixedAccessor = rawAccessor, - to = ChoiceName.assertFromString, - from = identity, - ) - - override val interfaceId: StringInterningDomain[Identifier] = - StringInterningDomain.prefixing( - prefix = InterfacePrefix, - prefixedAccessor = rawAccessor, - to = Identifier.assertFromString, - from = _.toString, - ) - - override private[platform] def distinctNewRawStrings( - interningProviders: Iterable[StringInterningProvider] - ): Iterable[String] = { - val rawSnapshot = raw.map - val linkedHashSet = mutable.LinkedHashSet.empty[String] - def add(rawString: String): Unit = - if (!linkedHashSet.contains(rawString) && !rawSnapshot.contains(rawString)) { - linkedHashSet.add(rawString).discard - } - val builder = new StringInterningBuilder { - override def addTemplateId(s: NameTypeConRef): Unit = add(TemplatePrefix + s) - override def addPackageId(s: PackageId): Unit = add(PackageIdPrefix + s) - override def addParty(s: Party): Unit = add(PartyPrefix + s) - override def addSynchronizerId(s: SynchronizerId): Unit = add( - SynchronizerIdPrefix + s.toProtoPrimitive - ) - override def addUserId(s: UserId): Unit = add(UserIdPrefix + s) - override def addParticipantId(s: ParticipantId): Unit = add(ParticipantIdPrefix + s) - override def addChoiceName(s: ChoiceName): Unit = add(ChoicePrefix + s) - override def addInterfaceId(s: Identifier): Unit = add(InterfacePrefix + s) - } - interningProviders.foreach(_.provideInternedStrings(builder)) - linkedHashSet - } - - override private[platform] def internize( - distinctRawStrings: Iterable[String] - ): Iterable[(Int, String)] = lock.exclusive { - val newEntries = RawStringInterning.newEntries( - distinctRawStrings = distinctRawStrings, - rawStringInterning = raw, - ) - updateView(newEntries) - newEntries - } - - override def update(lastStringInterningId: Option[Int])( - loadStringInterningEntries: LoadStringInterningEntries - ): Future[Unit] = - if (lastStringInterningId.getOrElse(0) <= raw.lastId) { - raw = RawStringInterning.resetTo(lastStringInterningId.getOrElse(0), raw) - Future.unit - } else { - loadStringInterningEntries(raw.lastId, lastStringInterningId.getOrElse(0)) - .map(updateView)(directEc) - } - - private def updateView(newEntries: Iterable[(Int, String)]): Unit = (lock.exclusive { - if (newEntries.nonEmpty) { - raw = RawStringInterning.from( - entries = newEntries, - rawStringInterning = raw, - ) - } - }) -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/serialization/Compression.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/serialization/Compression.scala deleted file mode 100644 index a39f496581..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/serialization/Compression.scala +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.serialization - -import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream - -import java.io.{InputStream, OutputStream} -import java.util.zip.GZIPOutputStream - -private[platform] object Compression { - - sealed abstract class Algorithm(val id: Option[Int]) { - def compress(stream: OutputStream): OutputStream - - def decompress(stream: InputStream): InputStream - } - - object Algorithm { - - private val LookupTable = Map[Option[Int], Algorithm]( - None.id -> None, - GZIP.id -> GZIP, - ) - - private def unknownAlgorithm(id: Option[Int]): Nothing = - throw new IllegalArgumentException(s"Unknown compression algorithm identifier: $id") - - @throws[IllegalArgumentException]( - "If the byte does not match a known compression algorithm identifier" - ) - def assertLookup(id: Option[Int]): Algorithm = - LookupTable.getOrElse(id, unknownAlgorithm(id)) - - case object None extends Algorithm(id = Option.empty) { - override def compress(stream: OutputStream): OutputStream = stream - - override def decompress(stream: InputStream): InputStream = stream - } - - case object GZIP extends Algorithm(id = Some(1)) { - override def compress(stream: OutputStream): OutputStream = - new GZIPOutputStream(stream) - - override def decompress(stream: InputStream): InputStream = - // prefer GzipCompressorInputStream over GZIPInputStream, because it doesn't use exceptions for internal - // control flow, as GZIPInputStream does. - new GzipCompressorInputStream(stream, false) - } - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/serialization/ValueSerializer.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/serialization/ValueSerializer.scala deleted file mode 100644 index b9fc940aa5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/serialization/ValueSerializer.scala +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.serialization - -import com.digitalasset.daml.lf.value.Value.VersionedValue -import com.digitalasset.daml.lf.value.{ValueCoder, ValueOuterClass} - -import java.io.InputStream - -private[platform] object ValueSerializer { - - def serializeValue( - value: VersionedValue, - errorContext: => String, - ): Array[Byte] = - ValueCoder - .encodeVersionedValue(versionedValue = value) - .fold(error => sys.error(s"$errorContext (${error.errorMessage})"), _.toByteArray) - - private def deserializeValueHelper( - stream: InputStream, - errorContext: => Option[String], - ): VersionedValue = - ValueCoder - .decodeVersionedValue( - protoValue0 = ValueOuterClass.VersionedValue.parseFrom(stream) - ) - .fold( - error => - sys.error(errorContext.fold(error.errorMessage)(ctx => s"$ctx (${error.errorMessage})")), - identity, - ) - - def deserializeValue( - stream: InputStream - ): VersionedValue = - deserializeValueHelper(stream, None) - - def deserializeValue( - stream: InputStream, - errorContext: => String, - ): VersionedValue = - deserializeValueHelper(stream, Some(errorContext)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/ConcurrencyLimiter.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/ConcurrencyLimiter.scala deleted file mode 100644 index aa2fdd133f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/ConcurrencyLimiter.scala +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.utils - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.util.Mutex -import com.digitalasset.canton.util.Thereafter.syntax.* - -import scala.collection.mutable -import scala.concurrent.{ExecutionContext, Future, Promise} - -trait ConcurrencyLimiter { - def execute[T](task: => Future[T]): Future[T] -} - -@SuppressWarnings(Array("org.wartremover.warts.Var")) -class QueueBasedConcurrencyLimiter( - parallelism: Int, - executionContext: ExecutionContext, -) extends ConcurrencyLimiter { - assert(parallelism > 0) - - type Task = () => Unit - private val waiting = mutable.Queue[Task]() - private var running: Int = 0 - private val lock = new Mutex() - - override def execute[T](task: => Future[T]): Future[T] = { - val promise = Promise[T]() - val waitingTask = () => { - implicit val ec: ExecutionContext = executionContext - task.thereafter { result => - (lock.exclusive { - running = running - 1 - promise.tryComplete(result).discard - startTasks() - }) - }.discard - } - - (lock.exclusive { - waiting.enqueue(waitingTask) - startTasks() - }) - - promise.future - } - - @SuppressWarnings(Array("org.wartremover.warts.While")) - private def startTasks(): Unit = - // No need to put this into a synchronized block because all call sites are inside synchronized blocks - while (running < parallelism && waiting.nonEmpty) { - val head = waiting.dequeue() - running = running + 1 - head() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/EventOps.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/EventOps.scala deleted file mode 100644 index 9cae58c178..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/EventOps.scala +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.utils - -import com.daml.ledger.api.v2.event.Event -import com.daml.ledger.api.v2.event.Event.Event.{Archived, Created, Empty, Exercised} -import com.daml.ledger.api.v2.value.Identifier - -object EventOps { - - implicit class EventOps(val event: Event) extends AnyVal { - - def nodeId: Int = event.event.nodeId - - def witnessParties: Seq[String] = event.event.witnessParties - def updateWitnessParties(set: Seq[String]): Event = - event.copy(event = event.event.updateWitnessParties(set)) - def modifyWitnessParties(f: Seq[String] => Seq[String]): Event = - event.copy(event = event.event.modifyWitnessParties(f)) - - def contractId: String = event.event.contractId - - def templateId: Identifier = event.event.templateId - - def isCreated: Boolean = event.event.isCreated - def isArchived: Boolean = event.event.isArchived - - } - - implicit class EventEventOps(val event: Event.Event) extends AnyVal { - - def nodeId: Int = event match { - case Archived(value) => value.nodeId - case Created(value) => value.nodeId - case Exercised(value) => value.nodeId - case Empty => throw new IllegalArgumentException("Cannot extract Event ID from Empty event.") - } - - def witnessParties: Seq[String] = event match { - case Archived(value) => value.witnessParties - case Created(value) => value.witnessParties - case Exercised(value) => value.witnessParties - case Empty => Seq.empty - } - - def updateWitnessParties(set: Seq[String]): Event.Event = event match { - case Archived(value) => Archived(value.copy(witnessParties = set)) - case Created(value) => Created(value.copy(witnessParties = set)) - case Exercised(value) => Exercised(value.copy(witnessParties = set)) - case Empty => Empty - } - - def modifyWitnessParties(f: Seq[String] => Seq[String]): Event.Event = event match { - case Archived(value) => Archived(value.copy(witnessParties = f(value.witnessParties))) - case Created(value) => Created(value.copy(witnessParties = f(value.witnessParties))) - case Exercised(value) => Exercised(value.copy(witnessParties = f(value.witnessParties))) - case Empty => Empty - } - - private def inspectTemplateId(templateId: Option[Identifier]): Identifier = - templateId.fold( - throw new IllegalArgumentException("Missing template id.") - )(identity) - - def templateId: Identifier = event match { - case Archived(value) => inspectTemplateId(value.templateId) - case Created(value) => inspectTemplateId(value.templateId) - case Exercised(value) => inspectTemplateId(value.templateId) - case Empty => - throw new IllegalArgumentException("Cannot extract Template ID from Empty event.") - } - - def contractId: String = event match { - case Archived(value) => value.contractId - case Created(value) => value.contractId - case Exercised(value) => value.contractId - case Empty => - throw new IllegalArgumentException("Cannot extract contractId from Empty event.") - } - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/Telemetry.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/Telemetry.scala deleted file mode 100644 index cf385cc458..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/store/utils/Telemetry.scala +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.utils - -import com.daml.tracing.SpanAttribute -import com.digitalasset.canton.data.Offset -import io.opentelemetry.api.trace.{Span, Tracer} - -object Telemetry { - - object Updates { - def createSpan(tracer: Tracer, startInclusive: Offset, endInclusive: Offset)( - fullyQualifiedFunctionName: String - ): Span = - tracer - .spanBuilder(fullyQualifiedFunctionName) - .setNoParent() - .setAttribute(SpanAttribute.OffsetFrom.key, startInclusive.toDecimalString) - .setAttribute(SpanAttribute.OffsetTo.key, endInclusive.toDecimalString) - .startSpan() - - def createSpan(tracer: Tracer, activeAt: Offset)( - fullyQualifiedFunctionName: String - ): Span = - tracer - .spanBuilder(fullyQualifiedFunctionName) - .setNoParent() - .setAttribute(SpanAttribute.Offset.key, activeAt.toDecimalString) - .startSpan() - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/tracing/SerializableTraceContextConverter.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/tracing/SerializableTraceContextConverter.scala deleted file mode 100644 index 9fba953e48..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/tracing/SerializableTraceContextConverter.scala +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.tracing - -import com.daml.ledger.api.v2.trace_context.TraceContext as DamlTraceContext -import com.digitalasset.canton.serialization.ProtoConverter -import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult -import com.typesafe.scalalogging.Logger - -object SerializableTraceContextConverter { - implicit class SerializableTraceContextExtension( - val serializableTraceContext: SerializableTraceContext - ) extends AnyVal { - def toDamlProto: Option[DamlTraceContext] = - Option.when(serializableTraceContext.traceContext != TraceContext.empty)( - toDamlTraceContext(serializableTraceContext.traceContext) - ) - - def toSerializedDamlProto: Array[Byte] = - toDamlTraceContext(serializableTraceContext.traceContext).toByteArray - } - - def fromDamlProtoSafeOpt(logger: Logger)( - traceContextP: Option[DamlTraceContext] - ): SerializableTraceContext = - SerializableTraceContext.safely(logger)(fromDamlProtoOpt)(traceContextP) - - def fromDamlProtoOpt( - traceContextP: Option[DamlTraceContext] - ): ParsingResult[SerializableTraceContext] = - for { - tcP <- ProtoConverter.required("traceContext", traceContextP) - tc <- fromDamlProto(tcP) - } yield tc - - def fromDamlProto(tc: DamlTraceContext): ParsingResult[SerializableTraceContext] = - Right(SerializableTraceContext(W3CTraceContext.toTraceContext(tc.traceparent, tc.tracestate))) - - private def toDamlTraceContext(traceContext: TraceContext): DamlTraceContext = { - val w3cTraceContext = traceContext.asW3CTraceContext - DamlTraceContext(w3cTraceContext.map(_.parent), w3cTraceContext.flatMap(_.state)) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/version/HashingSchemeVersionConverter.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/version/HashingSchemeVersionConverter.scala deleted file mode 100644 index 503c8d182b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/version/HashingSchemeVersionConverter.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.version - -import com.daml.ledger.api.v2.interactive.interactive_submission_service as iss - -object HashingSchemeVersionConverter { - def toLAPIProto(hashingSchemeVersion: HashingSchemeVersion): iss.HashingSchemeVersion = - iss.HashingSchemeVersion.fromValue(hashingSchemeVersion.index) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/resources/application.conf b/canton/community/ledger/ledger-api-core/src/test/resources/application.conf deleted file mode 100644 index 4eee3f4ee8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/resources/application.conf +++ /dev/null @@ -1,5 +0,0 @@ -# TODO(#12063) Remove this file and handle logs -pekko { - stdout-loglevel = "OFF" - loglevel = "OFF" -} diff --git a/canton/community/ledger/ledger-api-core/src/test/resources/config/test.conf b/canton/community/ledger/ledger-api-core/src/test/resources/config/test.conf deleted file mode 100644 index d9383c5b97..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/resources/config/test.conf +++ /dev/null @@ -1,6 +0,0 @@ - -test { - value-1 = v1 - value-2 = v2 - value-3 = v3 -} diff --git a/canton/community/ledger/ledger-api-core/src/test/resources/config/test2.conf b/canton/community/ledger/ledger-api-core/src/test/resources/config/test2.conf deleted file mode 100644 index a83ff2a419..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/resources/config/test2.conf +++ /dev/null @@ -1,6 +0,0 @@ - -test { - value-1 = overriden_v1 - value-2 = overriden_v2 - #value-3 = overriden_v3 #not overriden so to check if `value3` -} diff --git a/canton/community/ledger/ledger-api-core/src/test/resources/config/testp.conf b/canton/community/ledger/ledger-api-core/src/test/resources/config/testp.conf deleted file mode 100644 index 8a200dfcbb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/resources/config/testp.conf +++ /dev/null @@ -1,3 +0,0 @@ -value-1 = v1 -value-2 = v2 -value-3 = v3 diff --git a/canton/community/ledger/ledger-api-core/src/test/resources/test-metering-key.json b/canton/community/ledger/ledger-api-core/src/test/resources/test-metering-key.json deleted file mode 100644 index a64cdc7122..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/resources/test-metering-key.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "algorithm": "HmacSHA256", - "encoded": "LwRiwUaPHSpyecdeIrzMTNEZCDIV1XWb5_vsnw90dzw=", - "scheme": "test" -} \ No newline at end of file diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthInterceptorSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthInterceptorSpec.scala deleted file mode 100644 index 385daba6e5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthInterceptorSpec.scala +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.auth - -import com.digitalasset.canton.config.ApiLoggingConfig -import com.digitalasset.canton.logging.SuppressionRule -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import io.grpc.MethodDescriptor.Marshaller -import io.grpc.protobuf.StatusProto -import io.grpc.{Metadata, MethodDescriptor, ServerCall, Status} -import org.mockito.captor.ArgCaptor -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.Assertion -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.slf4j.event.Level - -import scala.concurrent.{Future, Promise} - -class AuthInterceptorSpec - extends AsyncFlatSpec - with MockitoSugar - with Matchers - with ArgumentMatchersSugar - with BaseTest - with HasExecutionContext { - - private val className = classOf[AuthInterceptor].getSimpleName - - behavior of s"$className.interceptCall" - - private val AuthInterceptorSuppressionRule: SuppressionRule = - SuppressionRule.forLogger[AuthInterceptor] && SuppressionRule.Level(Level.ERROR) - - it should "close the ServerCall with a V2 status code on decoding failure" in { - loggerFactory.assertLogs(AuthInterceptorSuppressionRule)( - within = testServerCloseError { case (actualStatus, actualMetadata) => - actualStatus.getCode shouldBe Status.Code.INTERNAL - actualStatus.getDescription shouldBe "An error occurred. Please contact the operator and inquire about the request with tid " - - val actualRpcStatus = StatusProto.fromStatusAndTrailers(actualStatus, actualMetadata) - actualRpcStatus.getDetailsList.size() shouldBe 0 - }, - assertions = _.errorMessage should include( - "INTERNAL_AUTHORIZATION_ERROR(4,0): Failed to get claims from request metadata" - ), - ) - } - - private def testServerCloseError( - assertRpcStatus: (Status, Metadata) => Assertion - ): Future[Assertion] = { - val authService = mock[AuthService] - val serverCall = mock[ServerCall[Nothing, Nothing]] - val marshaller = mock[Marshaller[Nothing]] - val methodDescriptor = MethodDescriptor - .newBuilder[Nothing, Nothing]( - marshaller, - marshaller, - ) - .setFullMethodName("") - .setType(MethodDescriptor.MethodType.UNARY) - .build() - val failedMetadataDecode = - Future.failed[ClaimSet](new RuntimeException("some internal failure")) - - val promise = Promise[Unit]() - // Using a promise to ensure the verify call below happens after the expected call to `serverCall.close` - when(serverCall.getMethodDescriptor).thenReturn(methodDescriptor) - when(serverCall.getAttributes).thenCallRealMethod(); - when(serverCall.close(any[Status], any[Metadata])).thenAnswer { - promise.success(()) - () - } - - val authInterceptor = - new AuthInterceptor( - List(authService), - loggerFactory, - executionContext, - ) - - val statusCaptor = ArgCaptor[Status] - val metadataCaptor = ArgCaptor[Metadata] - - when(authService.decodeToken(any[Option[String]], any[String])(anyTraceContext)) - .thenReturn(failedMetadataDecode) - - new GrpcAuthInterceptor( - authInterceptor, - loggerFactory, - ApiLoggingConfig(), - executionContext, - ).interceptCall[Nothing, Nothing](serverCall, new Metadata(), null) - - promise.future.map { _ => - verify(serverCall).close(statusCaptor.capture, metadataCaptor.capture) - assertRpcStatus(statusCaptor.value, metadataCaptor.value) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthServiceJWTCodecSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthServiceJWTCodecSpec.scala deleted file mode 100644 index e86eaef8a5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthServiceJWTCodecSpec.scala +++ /dev/null @@ -1,586 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.auth - -import com.daml.jwt.{ - AuthServiceJWTCodec, - AuthServiceJWTPayload, - StandardJWTPayload, - StandardJWTTokenFormat, -} -import io.circe.* -import io.circe.syntax.* -import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.TryValues -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import java.time.Instant -import scala.util.{Success, Try} - -@SuppressWarnings(Array("com.digitalasset.canton.TryFailed")) -class AuthServiceJWTCodecSpec - extends AnyWordSpec - with Matchers - with TryValues - with ScalaCheckDrivenPropertyChecks { - - /** Serializes a [[AuthServiceJWTPayload]] to JSON, then parses it back to a AuthServiceJWTPayload - */ - private def serializeAndParse(value: AuthServiceJWTPayload)(implicit - encoder: Encoder[AuthServiceJWTPayload], - decoder: Decoder[AuthServiceJWTPayload], - ): Try[AuthServiceJWTPayload] = - for { - serialized <- Try(value.asJson.noSpaces) - parsed <- parse(serialized) - } yield parsed - - /** Parses a [[AuthServiceJWTPayload]] */ - private def parse(serialized: String)(implicit - decoder: Decoder[AuthServiceJWTPayload] - ): Try[AuthServiceJWTPayload] = - Try(parser.decode(serialized).fold(throw _, identity)) - - private implicit val arbInstant: Arbitrary[Instant] = - Arbitrary { - for { - seconds <- Gen.chooseNum(Instant.MIN.getEpochSecond, Instant.MAX.getEpochSecond) - } yield { - Instant.ofEpochSecond(seconds) - } - } - - private implicit val arbFormat: Arbitrary[StandardJWTTokenFormat] = - Arbitrary( - Gen.oneOf[StandardJWTTokenFormat]( - StandardJWTTokenFormat.Audience, - StandardJWTTokenFormat.Scope, - ) - ) - - // participantId is mandatory for the format `StandardJWTTokenFormat.Audience` - private val StandardJWTPayloadGen = - Gen - .resultOf((StandardJWTPayload.apply _).tupled) - .filterNot { payload => - payload.participantId - .forall(_.isEmpty) && payload.format == StandardJWTTokenFormat.Audience - } - .filterNot { payload => - payload.scope.forall(_.isEmpty) && payload.format == StandardJWTTokenFormat.Scope - } - // we do not fill audiences for Scope or Audience based tokens - .map(payload => payload.copy(audiences = List.empty)) - // we coerce all scopes to contain the official ledger api string, we test the non-conforming ones separately - .map(payload => - payload.copy(scope = payload.scope.map(_ => AuthServiceJWTCodec.scopeLedgerApiFull)) - ) - - "Audience-Based AuthServiceJWTPayload codec" when { - import AuthServiceJWTCodec.AudienceBasedTokenJsonImplicits.* - - val PayloadGen = Gen - .resultOf((StandardJWTPayload.apply _).tupled) - .map(payload => - payload.copy( - participantId = None, - format = StandardJWTTokenFormat.Audience, - scope = payload.scope.map(_.trim()), - ) - ) - - "serializing and parsing a value" should { - "work for arbitrary custom Daml token values" in forAll( - PayloadGen, - minSuccessful(100), - )(value => serializeAndParse(value) shouldBe Success(value)) - } - - "support multiple audiences with a single participant audience" in { - val serialized = - """{ - | "aud": ["https://example.com/non/related/audience", - | "https://daml.com/jwt/aud/participant/someParticipantId"], - | "sub": "someUserId", - | "exp": 100 - |} - """.stripMargin - parse(serialized) shouldBe Success( - StandardJWTPayload( - issuer = None, - participantId = None, - userId = "someUserId", - exp = Some(Instant.ofEpochSecond(100)), - format = StandardJWTTokenFormat.Audience, - audiences = List( - "https://example.com/non/related/audience", - "https://daml.com/jwt/aud/participant/someParticipantId", - ), - scope = None, - ) - ) - } - } - - "Scope-Based AuthServiceJWTPayload codec" when { - import AuthServiceJWTCodec.ScopeBasedTokenJsonImplicits.* - - val PayloadGen = Gen - .resultOf((StandardJWTPayload.apply _).tupled) - .map(payload => - payload.copy( - participantId = None, - format = StandardJWTTokenFormat.Scope, - scope = payload.scope.map(_.trim()), - ) - ) - - "serializing and parsing a value" should { - "work for arbitrary custom Daml token values" in forAll( - PayloadGen, - minSuccessful(100), - )(value => serializeAndParse(value) shouldBe Success(value)) - } - - val expected = StandardJWTPayload( - issuer = Some("issuer"), - participantId = None, - userId = "someUserId", - exp = Some(Instant.ofEpochSecond(100)), - format = StandardJWTTokenFormat.Scope, - audiences = List("someParticipantId"), - scope = Some(AuthServiceJWTCodec.scopeLedgerApiFull), - ) - - "support standard JWT claims with just one scope" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scope": "${AuthServiceJWTCodec.scopeLedgerApiFull}" - |} - """.stripMargin - parse(serialized) shouldBe Success(expected) - } - - "support standard JWT claims with just one scope in scp" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scp": ["${AuthServiceJWTCodec.scopeLedgerApiFull}"] - |} - """.stripMargin - parse(serialized) shouldBe Success(expected) - } - - val extraScopes = s"dummy-scope1 ${AuthServiceJWTCodec.scopeLedgerApiFull} dummy-scope2" - val extraScopesJsArray = extraScopes.split(" ").toSeq.asJson.noSpaces - - "support standard JWT claims with extra scopes" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scope": "$extraScopes" - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(scope = Some(extraScopes))) - } - - "support standard JWT claims with extra scopes in scp" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scp": $extraScopesJsArray - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(scope = Some(extraScopes))) - } - - val extraCompositeScopes = - s"dummy-scope1 ${AuthServiceJWTCodec.scopeLedgerApiFull} dummy-scope2" - val extraCompositeScopesJsArray = extraCompositeScopes.split(" ").toSeq.asJson.noSpaces - - "support standard JWT claims with extra composite scopes" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scope": "$extraCompositeScopes" - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(scope = Some(extraCompositeScopes))) - } - - "support standard JWT claims with extra composite scopes in scp" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scp": $extraCompositeScopesJsArray - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(scope = Some(extraCompositeScopes))) - } - } - - "AuthServiceJWTPayload codec" when { - import AuthServiceJWTCodec.JsonImplicits.* - - "serializing and parsing a value" should { - - "work for arbitrary standard Daml token values" in forAll( - StandardJWTPayloadGen, - minSuccessful(100), - ) { value => - serializeAndParse(value) shouldBe Success(value) - } - - val expected = StandardJWTPayload( - issuer = Some("issuer"), - participantId = Some("someParticipantId"), - userId = "someUserId", - exp = Some(Instant.ofEpochSecond(100)), - format = StandardJWTTokenFormat.Scope, - audiences = List.empty, - scope = Some(AuthServiceJWTCodec.scopeLedgerApiFull), - ) - - "support standard JWT claims with just one scope" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scope": "${AuthServiceJWTCodec.scopeLedgerApiFull}" - |} - """.stripMargin - parse(serialized) shouldBe Success(expected) - } - - "support standard JWT claims with just one scope in scp" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scp": ["${AuthServiceJWTCodec.scopeLedgerApiFull}"] - |} - """.stripMargin - parse(serialized) shouldBe Success(expected) - } - - "reject standard JWT claims with one composite scope" in { - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scope": "resource_server/${AuthServiceJWTCodec.scopeLedgerApiFull}" - |} - """.stripMargin - parse(serialized).failure.exception.getMessage should include( - "Access token with unknown scope" - ) - } - - "support standard JWT claims with extra scopes" in { - val serialized = - s"""{ - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scope": "dummy-scope1 ${AuthServiceJWTCodec.scopeLedgerApiFull} dummy-scope2" - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(issuer = None)) - } - - "support standard JWT claims with extra scopes in scp" in { - val serialized = - s"""{ - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scp": ["dummy-scope1", "${AuthServiceJWTCodec.scopeLedgerApiFull}", "dummy-scope2"] - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(issuer = None)) - } - - "support standard JWT claims with extra composite scopes" in { - val serialized = - s"""{ - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scope": "resource_server/dummy-scope1 ${AuthServiceJWTCodec.scopeLedgerApiFull} resource_server/dummy-scope2" - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(issuer = None)) - } - - "support standard JWT claims with extra composite scopes in scp" in { - val serialized = - s"""{ - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scp": ["resource_server/dummy-scope1", "${AuthServiceJWTCodec.scopeLedgerApiFull}", "resource_server/dummy-scope2"] - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(issuer = None)) - } - - "support standard JWT claims with iss claim as string" in { - val serialized = - s"""{ - | "iss": "issuer1", - | "sub": "someUserId", - | "scope": "${AuthServiceJWTCodec.scopeLedgerApiFull}" - |} - """.stripMargin - val expected = StandardJWTPayload( - issuer = Some("issuer1"), - participantId = None, - userId = "someUserId", - exp = None, - format = StandardJWTTokenFormat.Scope, - audiences = List.empty, - scope = Some(AuthServiceJWTCodec.scopeLedgerApiFull), - ) - parse(serialized) shouldBe Success(expected) - } - - "support standard JWT claims with iss claim as URL" in { - val serialized = - s"""{ - | "iss": "http://daml.com/", - | "sub": "someUserId", - | "scope": "${AuthServiceJWTCodec.scopeLedgerApiFull}" - |}""".stripMargin - val expected = StandardJWTPayload( - issuer = Some("http://daml.com/"), - participantId = None, - userId = "someUserId", - exp = None, - format = StandardJWTTokenFormat.Scope, - audiences = List.empty, - scope = Some(AuthServiceJWTCodec.scopeLedgerApiFull), - ) - parse(serialized) shouldBe Success(expected) - } - - "have stable default values" in { - val serialized = - s"""{ - | "sub": "someUserId", - | "scope": "${AuthServiceJWTCodec.scopeLedgerApiFull}" - |}""".stripMargin - val expected = StandardJWTPayload( - issuer = None, - userId = "someUserId", - participantId = None, - exp = None, - format = StandardJWTTokenFormat.Scope, - audiences = List.empty, - scope = Some(AuthServiceJWTCodec.scopeLedgerApiFull), - ) - parse(serialized) shouldBe Success(expected) - } - - "support additional daml user token with prefixed audience" in { - val serialized = - """{ - | "aud": "https://daml.com/jwt/aud/participant/someParticipantId", - | "sub": "someUserId", - | "exp": 100 - |} - """.stripMargin - val expected = StandardJWTPayload( - issuer = None, - participantId = Some("someParticipantId"), - userId = "someUserId", - exp = Some(Instant.ofEpochSecond(100)), - format = StandardJWTTokenFormat.Audience, - audiences = List.empty, - scope = None, - ) - parse(serialized) shouldBe Success(expected) - } - - "treat a singleton array of audiences equivalent to a string of its first element" in { - val prefixed = - """{ - | "aud": ["https://daml.com/jwt/aud/participant/someParticipantId"], - | "sub": "someUserId", - | "exp": 100 - |} - """.stripMargin - parse(prefixed) shouldBe Success( - StandardJWTPayload( - issuer = None, - participantId = Some("someParticipantId"), - userId = "someUserId", - exp = Some(Instant.ofEpochSecond(100)), - format = StandardJWTTokenFormat.Audience, - audiences = List.empty, - scope = None, - ) - ) - - val standard = - s"""{ - | "aud": ["someParticipantId"], - | "sub": "someUserId", - | "exp": 100, - | "scope": "dummy-scope1 ${AuthServiceJWTCodec.scopeLedgerApiFull} dummy-scope2" - |} - """.stripMargin - parse(standard) shouldBe Success( - StandardJWTPayload( - issuer = None, - participantId = Some("someParticipantId"), - userId = "someUserId", - exp = Some(Instant.ofEpochSecond(100)), - format = StandardJWTTokenFormat.Scope, - audiences = List.empty, - scope = Some(AuthServiceJWTCodec.scopeLedgerApiFull), - ) - ) - } - - "support additional daml user token with prefixed audience and provided scope" in { - val serialized = - s"""{ - | "aud": ["https://daml.com/jwt/aud/participant/someParticipantId"], - | "sub": "someUserId", - | "exp": 100, - | "scope": "${AuthServiceJWTCodec.scopeLedgerApiFull}" - |} - """.stripMargin - val expected = StandardJWTPayload( - issuer = None, - participantId = Some("someParticipantId"), - userId = "someUserId", - exp = Some(Instant.ofEpochSecond(100)), - format = StandardJWTTokenFormat.Audience, - audiences = List.empty, - scope = Some(AuthServiceJWTCodec.scopeLedgerApiFull), - ) - parse(serialized) shouldBe Success(expected) - } - - "support multiple audiences with a single participant audience" in { - val serialized = - """{ - | "aud": ["https://example.com/non/related/audience", - | "https://daml.com/jwt/aud/participant/someParticipantId"], - | "sub": "someUserId", - | "exp": 100 - |} - """.stripMargin - parse(serialized) shouldBe Success( - StandardJWTPayload( - issuer = None, - participantId = Some("someParticipantId"), - userId = "someUserId", - exp = Some(Instant.ofEpochSecond(100)), - format = StandardJWTTokenFormat.Audience, - audiences = List.empty, - scope = None, - ) - ) - } - - "reject the token of ParticipantId format with multiple participant audiences" in { - val serialized = - """{ - | "aud": ["https://daml.com/jwt/aud/participant/someParticipantId", - | "https://daml.com/jwt/aud/participant/someParticipantId2"], - | "sub": "someUserId", - | "exp": 100 - |} - """.stripMargin - parse(serialized).failure.exception.getMessage should include( - "must include a single participantId value prefixed by" - ) - } - - "reject the token of Scope format with multiple audiences" in { - val serialized = - s"""{ - | "aud": ["someParticipantId", - | "someParticipantId2"], - | "sub": "someUserId", - | "exp": 100, - | "scope": "${AuthServiceJWTCodec.scopeLedgerApiFull}" - |} - """.stripMargin - parse(serialized).failure.exception.getMessage should include( - "`aud` must be empty or a single participantId." - ) - } - - "reject the ParticipantId format token with empty participantId" in { - val serialized = - """{ - | "aud": ["https://daml.com/jwt/aud/participant/"], - | "sub": "someUserId", - | "exp": 100 - |} - """.stripMargin - parse(serialized).failure.exception.getMessage - .contains("must include participantId value prefixed by") shouldBe true - } - - "reject token with invalid scope" in { - val serialized = - """{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "someUserId", - | "exp": 100, - | "scope": "resource-server/daml-ledger-api" - |} - """.stripMargin - parse(serialized).failure.exception.getMessage should include( - "Access token with unknown scope" - ) - } - "support standard JWT claims with sub containing all supported characters" in { - val userId = "someUserId@^$.!`-#+'~_|:()" - val serialized = - s"""{ - | "iss": "issuer", - | "aud": "someParticipantId", - | "sub": "$userId", - | "exp": 100, - | "scope": "${AuthServiceJWTCodec.scopeLedgerApiFull}" - |} - """.stripMargin - parse(serialized) shouldBe Success(expected.copy(userId = userId)) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthorizerSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthorizerSpec.scala deleted file mode 100644 index 8f7c010cae..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/AuthorizerSpec.scala +++ /dev/null @@ -1,969 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.auth - -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.{BaseTest, LfLedgerString} -import com.digitalasset.daml.lf.data.Ref -import io.grpc.{Status, StatusRuntimeException} -import org.mockito.MockitoSugar -import org.scalatest.Assertion -import org.scalatest.Assertions.succeed -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import scalapb.lenses.Lens - -import java.time.Instant -import scala.concurrent.Future -import scala.util.{Failure, Success, Try} - -class AuthorizerSpec - extends AsyncFlatSpec - with BaseTest - with Matchers - with MockitoSugar - with PekkoBeforeAndAfterAll { - - import AuthorizerSpec.* - - private val className = classOf[Authorizer].getSimpleName - private val dummyRequest = 1337L - private val expectedSuccessfulResponse = "expectedSuccessfulResponse" - private val dummyReqRes: Long => Future[String] = - Map(dummyRequest -> Future.successful(expectedSuccessfulResponse)) - - private val party = Ref.Party.assertFromString("party") - private val party2 = Ref.Party.assertFromString("party2") - - behavior of s"$className.authorize for RequiredClaim.Public" - - List( - TestDefinition(RequiredClaim.Public(), ClaimSet.Claims.Empty, expectedPermissionDenied), - TestDefinition(RequiredClaim.Public(), ClaimSet.Claims.Wildcard, ExpectedSuccess), - TestDefinition( - RequiredClaim.Public(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimAdmin)), - expectedPermissionDenied, - ), - TestDefinition(RequiredClaim.Public(), ClaimSet.Claims.Admin, ExpectedSuccess), - TestDefinition( - RequiredClaim.Public(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimPublic)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.Public(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimIdentityProviderAdmin)), - expectedPermissionDenied, - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.Admin" - - List( - TestDefinition(RequiredClaim.Admin(), ClaimSet.Claims.Empty, expectedPermissionDenied), - TestDefinition( - RequiredClaim.Admin(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimPublic)), - expectedPermissionDenied, - ), - TestDefinition(RequiredClaim.Admin(), ClaimSet.Claims.Wildcard, ExpectedSuccess), - TestDefinition( - RequiredClaim.Admin(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimIdentityProviderAdmin)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.Admin(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimAdmin)), - ExpectedSuccess, - ), - TestDefinition(RequiredClaim.Admin(), ClaimSet.Claims.Admin, ExpectedSuccess), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.AdminOrIdpAdmin" - - List( - TestDefinition( - RequiredClaim.AdminOrIdpAdmin(), - ClaimSet.Claims.Empty, - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.AdminOrIdpAdmin(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimPublic)), - expectedPermissionDenied, - ), - TestDefinition(RequiredClaim.AdminOrIdpAdmin(), ClaimSet.Claims.Wildcard, ExpectedSuccess), - TestDefinition( - RequiredClaim.AdminOrIdpAdmin(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimIdentityProviderAdmin)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.AdminOrIdpAdmin(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimAdmin)), - ExpectedSuccess, - ), - TestDefinition(RequiredClaim.AdminOrIdpAdmin(), ClaimSet.Claims.Admin, ExpectedSuccess), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.ReadAs" - - List( - TestDefinition(RequiredClaim.ReadAs("party"), ClaimSet.Claims.Empty, expectedPermissionDenied), - TestDefinition(RequiredClaim.ReadAs("party"), ClaimSet.Claims.Wildcard, ExpectedSuccess), - TestDefinition(RequiredClaim.ReadAs("party"), ClaimSet.Claims.Admin, expectedPermissionDenied), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimPublic)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsAnyParty)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsAnyParty)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsAnyParty)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsParty(party))), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsParty(party))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsParty(party))), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsParty(party2))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsParty(party2))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsParty(party2))), - expectedPermissionDenied, - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.ExecuteAs" - - List( - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty, - expectedPermissionDenied, - ), - TestDefinition(RequiredClaim.ExecuteAs("party"), ClaimSet.Claims.Wildcard, ExpectedSuccess), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Admin, - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimPublic)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsAnyParty)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsAnyParty)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsAnyParty)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsParty(party))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsParty(party))), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsParty(party))), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsParty(party2))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsParty(party2))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ExecuteAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsParty(party2))), - expectedPermissionDenied, - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.ReadAsAnyParty" - - List( - TestDefinition(RequiredClaim.ReadAsAnyParty(), ClaimSet.Claims.Empty, expectedPermissionDenied), - TestDefinition(RequiredClaim.ReadAsAnyParty(), ClaimSet.Claims.Wildcard, ExpectedSuccess), - TestDefinition(RequiredClaim.ReadAsAnyParty(), ClaimSet.Claims.Admin, expectedPermissionDenied), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimPublic)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsAnyParty)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsAnyParty)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsAnyParty)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsParty(party))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsParty(party))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsParty(party))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsParty(party2))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsParty(party2))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ReadAsAnyParty(), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsParty(party2))), - expectedPermissionDenied, - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.ActAs" - - List( - TestDefinition(RequiredClaim.ActAs("party"), ClaimSet.Claims.Empty, expectedPermissionDenied), - TestDefinition(RequiredClaim.ActAs("party"), ClaimSet.Claims.Wildcard, ExpectedSuccess), - TestDefinition(RequiredClaim.ActAs("party"), ClaimSet.Claims.Admin, expectedPermissionDenied), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimPublic)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsAnyParty)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsAnyParty)), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsAnyParty)), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsParty(party))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsParty(party))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsParty(party))), - ExpectedSuccess, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimReadAsParty(party2))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimExecuteAsParty(party2))), - expectedPermissionDenied, - ), - TestDefinition( - RequiredClaim.ActAs("party"), - ClaimSet.Claims.Empty.copy(claims = Seq(ClaimActAsParty(party2))), - expectedPermissionDenied, - ), - ).foreach(generateAuthorizationTest) - - val userId1 = "userId1" - val userId2 = "userId2" - - def matchUserIdTestDef( - claims: Seq[Claim], - userId: Option[String], - req: String, - expectedResult: ExpectedResult, - descSuffix: String, - resolvedFromUser: Boolean = true, - ): TestDefinition = - TestDefinition( - RequiredClaim.MatchUserIdForUserManagement(simpleLens), - ClaimSet.Claims.Empty.copy( - claims = claims, - userId = userId, - resolvedFromUser = resolvedFromUser, - ), - expectedResult, - req = req, - descSuffix = descSuffix, - resultAssert = _ shouldBe userId1, - ) - - behavior of s"$className.authorize for RequiredClaim.MatchUserIdForUserManagement without Admin rights" - - List( - matchUserIdTestDef( - Nil, - Some(userId1), - userId1, - ExpectedSuccess, - "when authorized and request user IDs match", - ), - matchUserIdTestDef(Nil, Some(userId1), "", ExpectedSuccess, "when missing request user ID"), - matchUserIdTestDef( - Nil, - Some(userId2), - userId1, - expectedPermissionDenied, - "when authorized and request user IDs don't match", - ), - matchUserIdTestDef(Nil, None, userId1, expectedInternal, "when undefined authorized user ID"), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.MatchUserIdForUserManagement with Admin rights" - List( - matchUserIdTestDef( - Seq(ClaimAdmin), - Some(userId1), - userId1, - ExpectedSuccess, - "when authorized and request user IDs match", - ), - matchUserIdTestDef( - Seq(ClaimAdmin), - Some(userId1), - "", - ExpectedSuccess, - "when missing request user ID", - ), - matchUserIdTestDef( - Seq(ClaimAdmin), - Some(userId2), - userId1, - ExpectedSuccess, - "when authorized and request user IDs don't match", - ), - matchUserIdTestDef( - Seq(ClaimAdmin), - None, - userId1, - expectedInternal, - "when undefined authorized user ID", - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.MatchUserIdForUserManagement with IDP Admin rights" - List( - matchUserIdTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId1), - userId1, - ExpectedSuccess, - "when authorized and request user IDs match", - ), - matchUserIdTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId1), - "", - ExpectedSuccess, - "when missing request user ID", - ), - matchUserIdTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId2), - userId1, - ExpectedSuccess, - "when authorized and request user IDs don't match", - ), - matchUserIdTestDef( - Seq(ClaimIdentityProviderAdmin), - None, - userId1, - expectedInternal, - "when undefined authorized user ID", - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.MatchUserIdForUserManagement not resolvedFromUser" - - List( - matchUserIdTestDef( - Nil, - Some(userId1), - userId1, - expectedPermissionDenied, - "when authorized and request user IDs match", - resolvedFromUser = false, - ), - matchUserIdTestDef( - Nil, - None, - userId1, - expectedPermissionDenied, - "when undefined authenticated user ID", - resolvedFromUser = false, - ), - matchUserIdTestDef( - Nil, - Some(userId2), - userId1, - expectedPermissionDenied, - "when authorized and request user IDs don't match", - resolvedFromUser = false, - ), - matchUserIdTestDef( - Seq(ClaimAdmin), - Some(userId1), - userId1, - ExpectedSuccess, - "when authorized and request user IDs match", - resolvedFromUser = false, - ), - matchUserIdTestDef( - Seq(ClaimAdmin), - None, - userId1, - ExpectedSuccess, - "when undefined authenticated user ID", - resolvedFromUser = false, - ), - matchUserIdTestDef( - Seq(ClaimAdmin), - Some(userId2), - userId1, - ExpectedSuccess, - "when authorized and request user IDs don't match", - resolvedFromUser = false, - ), - ).foreach(generateAuthorizationTest) - - def anyAdminTestDef( - claims: Seq[Claim], - userId: Option[String], - req: String, - expectedResult: ExpectedResult, - descSuffix: String, - resolvedFromUser: Boolean = true, - ): TestDefinition = - TestDefinition( - RequiredClaim.AdminOrIdpAdminOrSelfAdmin(simpleLens), - ClaimSet.Claims.Empty.copy( - claims = claims, - userId = userId, - resolvedFromUser = resolvedFromUser, - ), - expectedResult, - req = req, - descSuffix = descSuffix, - ) - - behavior of s"$className.authorize for AdminOrIdpAdminOrSelfAdmin with Admin claims" - List( - anyAdminTestDef( - Seq(ClaimAdmin), - Some(userId2), - userId2, - ExpectedSuccess, - "when authorized and request user IDs match", - ), - anyAdminTestDef( - Seq(ClaimAdmin), - Some(userId1), - userId2, - ExpectedSuccess, - "when authorized and request user IDs don't match", - ), - anyAdminTestDef( - Seq(ClaimAdmin), - Some(userId1), - "", - ExpectedSuccess, - "when request user ID missing", - ), - anyAdminTestDef( - Seq(ClaimAdmin), - None, - userId2, - ExpectedSuccess, - "when authorized user ID missing", - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for AdminOrIdpAdminOrSelfAdmin with Idp Admin claims" - List( - anyAdminTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId2), - userId2, - ExpectedSuccess, - "when authorized and request user IDs match", - ), - anyAdminTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId1), - userId2, - ExpectedSuccess, - "when authorized and request user IDs don't match", - ), - anyAdminTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId1), - "", - ExpectedSuccess, - "when request user ID missing", - ), - anyAdminTestDef( - Seq(ClaimIdentityProviderAdmin), - None, - userId2, - ExpectedSuccess, - "when authorized user ID missing", - ), - ) - .foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for AdminOrIdpAdminOrSelfAdmin without Admin claims" - List( - anyAdminTestDef( - Nil, - Some(userId2), - userId2, - ExpectedSuccess, - "when authorized and request user IDs match", - ), - anyAdminTestDef( - Nil, - Some(userId1), - userId2, - expectedPermissionDenied, - "when authorized and request user IDs don't match", - ), - anyAdminTestDef( - Nil, - Some(userId1), - "", - expectedPermissionDenied, - "when request user ID missing", - ), - anyAdminTestDef(Nil, None, userId2, expectedInternal, "when authorized user ID missing"), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for AdminOrIdpAdminOrSelfAdmin with Admin claims when not resolvedFromUser" - List( - anyAdminTestDef( - Seq(ClaimAdmin), - Some(userId2), - userId2, - ExpectedSuccess, - "when authorized and request user IDs match", - resolvedFromUser = false, - ), - anyAdminTestDef( - Seq(ClaimAdmin), - Some(userId1), - userId2, - ExpectedSuccess, - "when authorized and request user IDs don't match", - resolvedFromUser = false, - ), - anyAdminTestDef( - Seq(ClaimAdmin), - Some(userId1), - "", - ExpectedSuccess, - "when request user ID missing", - resolvedFromUser = false, - ), - anyAdminTestDef( - Seq(ClaimAdmin), - None, - userId2, - ExpectedSuccess, - "when authorized user ID missing", - resolvedFromUser = false, - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for AdminOrIdpAdminOrSelfAdmin with Idp Admin claims when not resolvedFromUser" - List( - anyAdminTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId2), - userId2, - ExpectedSuccess, - "when authorized and request user IDs match", - resolvedFromUser = false, - ), - anyAdminTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId1), - userId2, - ExpectedSuccess, - "when authorized and request user IDs don't match", - resolvedFromUser = false, - ), - anyAdminTestDef( - Seq(ClaimIdentityProviderAdmin), - Some(userId1), - "", - ExpectedSuccess, - "when request user ID missing", - resolvedFromUser = false, - ), - anyAdminTestDef( - Seq(ClaimIdentityProviderAdmin), - None, - userId2, - ExpectedSuccess, - "when authorized user ID missing", - resolvedFromUser = false, - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for AdminOrIdpAdminOrSelfAdmin without Admin claims when not resolvedFromUser" - List( - anyAdminTestDef( - Nil, - Some(userId2), - userId2, - expectedPermissionDenied, - "when authorized and request user IDs match", - resolvedFromUser = false, - ), - anyAdminTestDef( - Nil, - Some(userId1), - userId2, - expectedPermissionDenied, - "when authorized and request user IDs don't match", - resolvedFromUser = false, - ), - anyAdminTestDef( - Nil, - Some(userId1), - "", - expectedPermissionDenied, - "when request user ID missing", - resolvedFromUser = false, - ), - anyAdminTestDef( - Nil, - None, - userId2, - expectedPermissionDenied, - "when authorized user ID missing", - resolvedFromUser = false, - ), - ).foreach(generateAuthorizationTest) - - behavior of s"$className.authorize for RequiredClaim.MatchUserId" - - it should "authorize for authenticated user ID matching request user ID" in { - val userIdL = Lens[Long, String](_.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - userId = Some(dummyRequest.toString), - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchUserId(userIdL))(dummyRequest) - }.map(_ shouldBe expectedSuccessfulResponse) - } - - it should "authorize for authenticated user ID when request user ID is missing" in { - val userIdL = Lens[Long, String](l => if (l == 0) "" else l.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - userId = Some(dummyRequest.toString), - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchUserId(userIdL))(0) - }.map(_ shouldBe expectedSuccessfulResponse) - } - - it should "return permission denied for authenticated user ID not matching request user ID" in { - val userIdL = Lens[Long, String](_.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - userId = Some("15"), - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchUserId(userIdL))(dummyRequest) - } - .transform( - assertExpectedFailure(Status.PERMISSION_DENIED.getCode) - ) - } - - it should "return permission denied for authenticated user ID not matching request user ID if skipUserIdValidationForAnyPartyReaders" in { - val userIdL = Lens[Long, String](_.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - userId = Some("15"), - ) - ) { - authorizer().rpc(dummyReqRes)( - RequiredClaim.MatchUserId( - userIdL, - skipUserIdValidationForAnyPartyReaders = true, - ) - )(dummyRequest) - } - .transform( - assertExpectedFailure(Status.PERMISSION_DENIED.getCode) - ) - } - - it should "authorize for authenticated user ID not matching request user ID for AnyPartyReaders-s if skipUserIdValidationForAnyPartyReaders" in { - val userIdL = Lens[Long, String](_.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Seq(ClaimReadAsAnyParty), - userId = Some("15"), - ) - ) { - authorizer().rpc(dummyReqRes)( - RequiredClaim.MatchUserId( - userIdL, - skipUserIdValidationForAnyPartyReaders = true, - ) - )(dummyRequest) - }.map(_ shouldBe expectedSuccessfulResponse) - } - - it should "return invalid argument for no authenticated user ID, and no request user ID" in { - val userIdL = Lens[Long, String](l => if (l == 0) "" else l.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - userId = None, - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchUserId(userIdL))(0) - } - .transform( - assertExpectedFailure(Status.INVALID_ARGUMENT.getCode) - ) - } - - behavior of s"$className.authorize for RequiredClaim.MatchIdentityProviderId" - - it should "authorize for identity provider ID in claims matching provided identity provider ID" in { - val idpIdL = Lens[Long, String](_.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - identityProviderId = Some(LfLedgerString.assertFromString(dummyRequest.toString)), - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchIdentityProviderId(idpIdL))(dummyRequest) - }.map(_ shouldBe expectedSuccessfulResponse) - } - - it should "return invalid argument for malformed provided identity provider ID" in { - val idpIdL = Lens[Long, String](_ => "!@#$%^%&^^&*&*)")((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - identityProviderId = Some(LfLedgerString.assertFromString(dummyRequest.toString)), - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchIdentityProviderId(idpIdL))(dummyRequest) - } - .transform( - assertExpectedFailure(Status.INVALID_ARGUMENT.getCode) - ) - } - - it should "authorize for identity provider ID in claims with no identity provider ID provided in the request" in { - val idpIdL = Lens[Long, String](l => if (l == 0) "" else l.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - identityProviderId = Some(LfLedgerString.assertFromString(dummyRequest.toString)), - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchIdentityProviderId(idpIdL))(0) - }.map(_ shouldBe expectedSuccessfulResponse) - } - - it should "return permission denied for identity provider ID in claims with no identity provider ID provided in the request for Admins (for admins no auto-resolution from claims)" in { - val idpIdL = Lens[Long, String](l => if (l == 0) "" else l.toString)((_, s) => s.toLong) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Seq(ClaimAdmin), - identityProviderId = Some(LfLedgerString.assertFromString(dummyRequest.toString)), - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchIdentityProviderId(idpIdL))(0) - } - .transform( - assertExpectedFailure(Status.PERMISSION_DENIED.getCode) - ) - } - - it should "authorize for no identity provider ID in claims with no identity provider ID provided in the request (resolving to the default provider)" in { - val idpIdL = Lens[Long, String](l => if (l == 0) "" else l.toString)((_, s) => - if (s == "") dummyRequest else s.toLong - ) - contextWithClaims( - ClaimSet.Claims.Empty.copy( - claims = Nil, - identityProviderId = None, - ) - ) { - authorizer().rpc(dummyReqRes)(RequiredClaim.MatchIdentityProviderId(idpIdL))(0) - }.map(_ shouldBe expectedSuccessfulResponse) - } - - private def assertExpectedFailure[T]( - expectedStatusCode: Status.Code - ): Try[T] => Try[Assertion] = { - case Failure(ex: StatusRuntimeException) => - ex.getStatus.getCode shouldBe expectedStatusCode - Success(succeed) - case ex => fail(s"Expected a failure with StatusRuntimeException but got $ex") - } - - private def contextWithClaims[R](claims: ClaimSet.Claims)(f: => R): R = - io.grpc.Context.ROOT - .withValue(AuthInterceptor.contextKeyClaimSet, claims) - .call(() => f) - - private def authorizer() = new Authorizer( - () => Instant.ofEpochSecond(1337L), - "participant-id", - loggerFactory = loggerFactory, - ) - - def generateAuthorizationTest(td: TestDefinition): Unit = { - val prettyClaims = s"Claims(${td.suppliedClaim.claims.map(_.toString).mkString(",")})" - val testDescription = td.expectedResult match { - case ExpectedSuccess => - s"authorize for $prettyClaims ${td.descSuffix}" - case ExpectedFailure(code) => - s"return $code for $prettyClaims ${td.descSuffix}" - } - it should testDescription in { - contextWithClaims(td.suppliedClaim) { - authorizer().rpc(simpleReqRes)(td.requiredClaim)(td.req) - }.map(td.resultAssert).transform { - case Success(_) => - td.expectedResult match { - case ExpectedSuccess => - Success(succeed) - case ExpectedFailure(_) => - fail("Unexpected success") - } - case Failure(ex: StatusRuntimeException) => - td.expectedResult match { - case ExpectedSuccess => - fail(s"Unexpected error ${ex.getStatus.getCode}") - case ExpectedFailure(code) if code != ex.getStatus.getCode => - fail(s"Unexpected error ${ex.getStatus.getCode}") - case _ => Success(succeed) - } - case ex => fail(s"Unexpected error $ex") - } - } - } -} - -object AuthorizerSpec { - sealed trait ExpectedResult - final case object ExpectedSuccess extends ExpectedResult - final case class ExpectedFailure(code: Status.Code) extends ExpectedResult - - val expectedPermissionDenied: ExpectedFailure = ExpectedFailure(Status.PERMISSION_DENIED.getCode) - val expectedInternal: ExpectedFailure = ExpectedFailure(Status.INTERNAL.getCode) - - val simpleRequest: String = "simpleRequest" - val simpleReqRes: String => Future[String] = s => Future.successful(s) - val simpleLens: Lens[String, String] = Lens[String, String](s => s)((_, s) => s) - - final case class TestDefinition( - requiredClaim: RequiredClaim[String], - suppliedClaim: ClaimSet.Claims, - expectedResult: ExpectedResult, - req: String = simpleRequest, - descSuffix: String = "", - resultAssert: String => Assertion = _ => succeed, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/JwksSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/JwksSpec.scala deleted file mode 100644 index 5f385a279e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/JwksSpec.scala +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.auth - -import com.auth0.jwt.algorithms.Algorithm -import com.daml.http.test.SimpleHttpServer -import com.daml.jwt.{DecodedJwt, Error, Jwt, JwtSigner, KeyUtils} -import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits.* -import com.daml.test.evidence.tag.Security.SecurityTest.Property.Authenticity -import com.daml.test.evidence.tag.Security.{Attack, SecurityTest} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.security.interfaces.{ECPrivateKey, ECPublicKey, RSAPrivateKey, RSAPublicKey} -import java.security.spec.ECGenParameterSpec -import java.security.{KeyPairGenerator, PrivateKey, PublicKey} -import scala.concurrent.duration.DurationInt - -trait JwksSpec extends AnyFlatSpec with Matchers { self: JwksSpecKeys => - - val securityAsset: SecurityTest = - SecurityTest(property = Authenticity, asset = "JWKS-configured Resource") - - def attack(threat: String): Attack = Attack( - actor = s"JWKS-configured Resource User", - threat = threat, - mitigation = s"Refuse to verify authenticity of the token", - ) - - it should "successfully verify against provided correct key by JWKS server" taggedAs securityAsset - .setHappyCase( - "Successfully verify against provided correct key by JWKS server" - ) in { - val token = generateToken("test-key-1", privateKey1) - .fold(e => fail("Failed to generate signed token: " + e.prettyPrint), x => x) - val result = verifier.verify(token) - - assert( - result.isRight, - s"The correctly signed token should successfully verify, but the result was ${result.left - .map(e => e.prettyPrint)}", - ) - } - - it should "raise an error by verifying a token with an unknown key id" taggedAs securityAsset - .setAttack(attack(threat = "Present an unknown key-id")) in { - val token = generateToken("test-key-unknown", privateKey1) - .fold(e => fail("Failed to generate signed token: " + e.prettyPrint), x => x) - val result = verifier.verify(token) - - assert(result.isLeft, s"The token with an unknown key ID should not successfully verify") - } - - it should "raise an error by verifying a token with wrong public key" taggedAs securityAsset - .setAttack( - attack(threat = "Present a known key-id, but not the one used for the token encryption") - ) in { - val token = generateToken("test-key-1", privateKey2) - .fold(e => fail("Failed to generate signed token: " + e.prettyPrint), x => x) - val result = verifier.verify(token) - - assert( - result.isLeft, - s"The token with a mismatching public key should not successfully verify", - ) - } -} - -trait JwksSpecKeys { - - protected type PublicKeyType <: PublicKey - protected type PrivateKeyType <: PrivateKey - - protected def kpg: KeyPairGenerator - protected def jwks: String - protected def generateToken( - keyId: String, - privateKey: PrivateKeyType, - ): Either[Error, Jwt] - - // Generate some RSA key pairs - private val keyPair1 = kpg.generateKeyPair() - protected val publicKey1: PublicKeyType = keyPair1.getPublic.asInstanceOf[PublicKeyType] - val privateKey1: PrivateKeyType = keyPair1.getPrivate.asInstanceOf[PrivateKeyType] - - private val keyPair2 = kpg.generateKeyPair() - protected val publicKey2: PublicKeyType = keyPair2.getPublic.asInstanceOf[PublicKeyType] - val privateKey2: PrivateKeyType = keyPair2.getPrivate.asInstanceOf[PrivateKeyType] - - private val server = SimpleHttpServer.start(jwks) - private val url = SimpleHttpServer.responseUrl(server) - - protected val verifier: JwksVerifier = JwksVerifier(url, 1000, 10.minutes, 10.seconds, 10.seconds) -} - -class JwksSpecRSA extends JwksSpec with JwksSpecKeys { - - private val keySize = 2048 - - override type PublicKeyType = RSAPublicKey - override type PrivateKeyType = RSAPrivateKey - - override def kpg: KeyPairGenerator = KeyPairGenerator.getInstance("RSA") - kpg.initialize(keySize) - - override def jwks: String = KeyUtils.generateJwks( - Map( - "test-key-1" -> publicKey1, - "test-key-2" -> publicKey2, - ) - ) - - override def generateToken( - keyId: String, - privateKey: PrivateKeyType, - ): Either[Error, Jwt] = { - val jwtPayload = s"""{"test": "JwksSpec"}""" - val jwtHeader = s"""{"alg": "RS256", "typ": "JWT", "kid": "$keyId"}""" - JwtSigner.RSA256.sign(DecodedJwt(jwtHeader, jwtPayload), privateKey) - } -} - -class JwksSpecES256 extends JwksSpec with JwksSpecKeys { - - override type PublicKeyType = ECPublicKey - override type PrivateKeyType = ECPrivateKey - - protected def kpg: KeyPairGenerator = { - val gen = KeyPairGenerator.getInstance("EC") - gen.initialize(new ECGenParameterSpec("secp256r1")) - gen - } - - protected def jwks: String = KeyUtils.generateECJwks( - Map( - "test-key-1" -> publicKey1, - "test-key-2" -> publicKey2, - ) - ) - - protected def generateToken( - keyId: String, - privateKey: PrivateKeyType, - ): Either[Error, Jwt] = { - val jwtPayload = s"""{"test": "JwksSpec"}""" - val jwtHeader = s"""{"alg": "ES256", "typ": "JWT", "kid": "$keyId"}""" - JwtSigner.ECDSA.sign(DecodedJwt(jwtHeader, jwtPayload), privateKey, Algorithm.ECDSA256(null, _)) - } -} - -class JwksSpecES512 extends JwksSpec with JwksSpecKeys { - - override type PublicKeyType = ECPublicKey - override type PrivateKeyType = ECPrivateKey - - protected def kpg: KeyPairGenerator = { - val gen = KeyPairGenerator.getInstance("EC") - gen.initialize(new ECGenParameterSpec("secp521r1")) - gen - } - - protected def jwks: String = KeyUtils.generateECJwks( - Map( - "test-key-1" -> publicKey1, - "test-key-2" -> publicKey2, - ) - ) - - protected def generateToken( - keyId: String, - privateKey: PrivateKeyType, - ): Either[Error, Jwt] = { - val jwtPayload = s"""{"test": "JwksSpec"}""" - val jwtHeader = s"""{"alg": "ES512", "typ": "JWT", "kid": "$keyId"}""" - JwtSigner.ECDSA.sign(DecodedJwt(jwtHeader, jwtPayload), privateKey, Algorithm.ECDSA512(null, _)) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/JwtVerifierLoaderSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/JwtVerifierLoaderSpec.scala deleted file mode 100644 index 0dca5e71e9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/auth/JwtVerifierLoaderSpec.scala +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.auth - -import com.auth0.jwt.algorithms.Algorithm -import com.daml.http.test.SimpleHttpServer -import com.daml.jwt.* -import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits.* -import com.daml.test.evidence.tag.Security.SecurityTest.Property.Authenticity -import com.daml.test.evidence.tag.Security.{Attack, SecurityTest} -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import org.scalatest.Assertion -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.security.interfaces.{ECPrivateKey, ECPublicKey, RSAPrivateKey, RSAPublicKey} -import java.security.spec.ECGenParameterSpec -import java.security.{KeyPairGenerator, PrivateKey, PublicKey} -import scala.concurrent.duration.DurationInt -import scala.util.{Failure, Success, Try} - -trait JwtVerifierLoaderSpec - extends AsyncFlatSpec - with Matchers - with HasExecutionContext - with BaseTest { self: JwtVerifierLoaderSpecKeys => - - protected val verifierLoader: CachedJwtVerifierLoader = new CachedJwtVerifierLoader( - 1000, - 10.minutes, - 10.seconds, - 10.seconds, - loggerFactory = loggerFactory, - ) - - val securityAsset: SecurityTest = - SecurityTest(property = Authenticity, asset = "JWKS-configured Resource") - - def attack(threat: String): Attack = Attack( - actor = s"JWKS-configured Resource User", - threat = threat, - mitigation = s"Refuse to verify authenticity of the token", - ) - - private def assertExpectedFailure[T](msg: String): Try[T] => Try[Assertion] = { - case Failure(t: JwtException) => - t.error.message should include(msg) - Success(succeed) - case ex => fail(s"Expected a failure but got $ex") - } - - it should "successfully verify against provided correct key by JWKS server" taggedAs securityAsset - .setHappyCase( - "Successfully verify against provided correct key by JWKS server" - ) in { - val keyId = "test-key-1" - val token = generateToken(keyId, privateKey1) - .fold(e => fail("Failed to generate signed token: " + e.prettyPrint), x => x) - verifierLoader.loadJwtVerifier(JwksUrl(url), Some(keyId)).map(_.verify(token)).map { result => - assert( - result.isRight, - s"The correctly signed token should successfully verify, but the result was ${result.left - .map(e => e.prettyPrint)}", - ) - } - } - - it should "raise an error by verifying a token with an unknown key id" taggedAs securityAsset - .setAttack(attack(threat = "Present an unknown key-id")) in { - val keyId = "test-key-unknown" - verifierLoader - .loadJwtVerifier(JwksUrl(url), Some(keyId)) - .transform(assertExpectedFailure(s"No key found in $url with kid $keyId")) - } - - it should "raise an error by verifying a token with wrong public key" taggedAs securityAsset - .setAttack( - attack(threat = "Present a known key-id, but not the one used for the token encryption") - ) in { - val keyId = "test-key-1" - val token = generateToken(keyId, privateKey2) - .fold(e => fail("Failed to generate signed token: " + e.prettyPrint), x => x) - verifierLoader.loadJwtVerifier(JwksUrl(url), Some(keyId)).map(_.verify(token)).map { result => - assert( - result.isLeft, - s"The token with a mismatching public key should not successfully verify", - ) - } - } -} - -trait JwtVerifierLoaderSpecKeys { - - protected type PublicKeyType <: PublicKey - protected type PrivateKeyType <: PrivateKey - - protected def kpg: KeyPairGenerator - protected def jwks: String - protected def generateToken( - keyId: String, - privateKey: PrivateKeyType, - ): Either[Error, Jwt] - - // Generate some RSA key pairs - private val keyPair1 = kpg.generateKeyPair() - protected val publicKey1: PublicKeyType = keyPair1.getPublic.asInstanceOf[PublicKeyType] - val privateKey1: PrivateKeyType = keyPair1.getPrivate.asInstanceOf[PrivateKeyType] - - private val keyPair2 = kpg.generateKeyPair() - protected val publicKey2: PublicKeyType = keyPair2.getPublic.asInstanceOf[PublicKeyType] - val privateKey2: PrivateKeyType = keyPair2.getPrivate.asInstanceOf[PrivateKeyType] - - private val server = SimpleHttpServer.start(jwks) - protected val url: String = SimpleHttpServer.responseUrl(server) - - protected val verifier: JwksVerifier = JwksVerifier(url, 1000, 10.minutes, 10.seconds, 10.seconds) -} - -class JwtVerifierLoaderSpecRSA extends JwtVerifierLoaderSpec with JwtVerifierLoaderSpecKeys { - - private val keySize = 2048 - - override type PublicKeyType = RSAPublicKey - override type PrivateKeyType = RSAPrivateKey - - override def kpg: KeyPairGenerator = KeyPairGenerator.getInstance("RSA") - kpg.initialize(keySize) - - override def jwks: String = KeyUtils.generateJwks( - Map( - "test-key-1" -> publicKey1, - "test-key-2" -> publicKey2, - ) - ) - - override def generateToken( - keyId: String, - privateKey: PrivateKeyType, - ): Either[Error, Jwt] = { - val jwtPayload = s"""{"test": "JwksSpec"}""" - val jwtHeader = s"""{"alg": "RS256", "typ": "JWT", "kid": "$keyId"}""" - JwtSigner.RSA256.sign(DecodedJwt(jwtHeader, jwtPayload), privateKey) - } -} - -class JwtVerifierLoaderSpecES256 extends JwtVerifierLoaderSpec with JwtVerifierLoaderSpecKeys { - - override type PublicKeyType = ECPublicKey - override type PrivateKeyType = ECPrivateKey - - protected def kpg: KeyPairGenerator = { - val gen = KeyPairGenerator.getInstance("EC") - gen.initialize(new ECGenParameterSpec("secp256r1")) - gen - } - - protected def jwks: String = KeyUtils.generateECJwks( - Map( - "test-key-1" -> publicKey1, - "test-key-2" -> publicKey2, - ) - ) - - protected def generateToken( - keyId: String, - privateKey: PrivateKeyType, - ): Either[Error, Jwt] = { - val jwtPayload = s"""{"test": "JwksSpec"}""" - val jwtHeader = s"""{"alg": "ES256", "typ": "JWT", "kid": "$keyId"}""" - JwtSigner.ECDSA.sign(DecodedJwt(jwtHeader, jwtPayload), privateKey, Algorithm.ECDSA256(null, _)) - } -} - -class JwtVerifierLoaderSpecES512 extends JwtVerifierLoaderSpec with JwtVerifierLoaderSpecKeys { - - override type PublicKeyType = ECPublicKey - override type PrivateKeyType = ECPrivateKey - - protected def kpg: KeyPairGenerator = { - val gen = KeyPairGenerator.getInstance("EC") - gen.initialize(new ECGenParameterSpec("secp521r1")) - gen - } - - protected def jwks: String = KeyUtils.generateECJwks( - Map( - "test-key-1" -> publicKey1, - "test-key-2" -> publicKey2, - ) - ) - - protected def generateToken( - keyId: String, - privateKey: PrivateKeyType, - ): Either[Error, Jwt] = { - val jwtPayload = s"""{"test": "JwksSpec"}""" - val jwtHeader = s"""{"alg": "ES512", "typ": "JWT", "kid": "$keyId"}""" - JwtSigner.ECDSA.sign(DecodedJwt(jwtHeader, jwtPayload), privateKey, Algorithm.ECDSA512(null, _)) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/error/generator/ErrorCodeDocumentationGeneratorSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/error/generator/ErrorCodeDocumentationGeneratorSpec.scala deleted file mode 100644 index 526a118eae..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/error/generator/ErrorCodeDocumentationGeneratorSpec.scala +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.error.generator - -import com.digitalasset.base.error.ErrorCategory.TransientServerFailure -import com.digitalasset.base.error.{ErrorClass, Explanation, Grouping, Resolution} -import com.digitalasset.canton.error.generator.ErrorCodeDocumentationGenerator.DeprecatedItem -import com.digitalasset.canton.error.testpackage.subpackage.MildErrorsParent -import com.digitalasset.canton.error.testpackage.subpackage.MildErrorsParent.MildErrors -import com.digitalasset.canton.error.testpackage.subpackage.MildErrorsParent.MildErrors.NotSoSeriousError -import com.digitalasset.canton.error.testpackage.{DeprecatedError, SeriousError} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.annotation.nowarn -import scala.reflect.ClassTag - -@nowarn("msg=Specify both message and version") -class ErrorCodeDocumentationGeneratorSpec extends AnyFlatSpec with Matchers { - - it should "return the correct doc items from the error classes" in { - val searchPackages = Array("com.digitalasset.canton.error.testpackage") - val actualGroupDocItems = ErrorCodeDocumentationGenerator.getErrorGroupItems(searchPackages) - val actualErrorDocItems = ErrorCodeDocumentationGenerator.getErrorCodeItems(searchPackages) - - val expectedErrorDocItems = Seq( - ErrorCodeDocItem( - errorCodeClassName = SeriousError.getClass.getTypeName, - category = "SystemInternalAssumptionViolated", - hierarchicalGrouping = ErrorClass(Nil), - conveyance = Some( - "This error is logged with log-level ERROR on the server side. It is exposed on the API with grpc-status INTERNAL without any details for security reasons." - ), - code = "BLUE_SCREEN", - deprecation = None, - explanation = Some(Explanation("Things happen.")), - resolution = Some(Resolution("Turn it off and on again.")), - ), - ErrorCodeDocItem( - errorCodeClassName = DeprecatedError.getClass.getTypeName: @nowarn("cat=deprecation"), - category = "SystemInternalAssumptionViolated", - hierarchicalGrouping = ErrorClass(Nil), - conveyance = Some( - "This error is logged with log-level ERROR on the server side. It is exposed on the API with grpc-status INTERNAL without any details for security reasons." - ), - code = "DEPRECATED_ERROR", - deprecation = - Some(DeprecatedItem(since = Some("since now"), message = "This is deprecated")), - explanation = Some(Explanation("Things happen.")), - resolution = Some(Resolution("Turn it off and on again.")), - ), - ErrorCodeDocItem( - errorCodeClassName = NotSoSeriousError.getClass.getTypeName, - category = "TransientServerFailure", - hierarchicalGrouping = ErrorClass( - List( - Grouping("MildErrorsParent", MildErrorsParent.getClass.getName), - Grouping("MildErrors", MildErrors.getClass.getName), - ) - ), - conveyance = Some( - "This error is logged with log-level INFO on the server side and exposed on the API with grpc-status UNAVAILABLE including a detailed error message." - ), - code = "TEST_ROUTINE_FAILURE_PLEASE_IGNORE", - deprecation = None, - explanation = Some(Explanation("Test: Things like this always happen.")), - resolution = Some(Resolution("Test: Why not ignore?")), - ), - ) - - val expectedGroupDocItems = Seq( - ErrorGroupDocItem( - className = MildErrorsParent.getClass.getName, - explanation = Some(Explanation("Mild error parent explanation")), - errorClass = ErrorClass( - Grouping( - docName = "MildErrorsParent", - fullClassName = MildErrorsParent.getClass.getName, - ) :: Nil - ), - ), - ErrorGroupDocItem( - className = MildErrorsParent.MildErrors.getClass.getName, - explanation = Some(Explanation("Groups mild errors together")), - errorClass = ErrorClass( - Grouping( - docName = "MildErrorsParent", - fullClassName = MildErrorsParent.getClass.getName, - ) :: - Grouping( - docName = "MildErrors", - fullClassName = MildErrorsParent.MildErrors.getClass.getName, - ) :: Nil - ), - ), - ) - - actualErrorDocItems should have length (3) - actualErrorDocItems(0) shouldBe expectedErrorDocItems(0) - actualErrorDocItems(1) shouldBe expectedErrorDocItems(1) - actualErrorDocItems(2) shouldBe expectedErrorDocItems(2) - - actualGroupDocItems should have length (2) - actualGroupDocItems(0) shouldBe expectedGroupDocItems(0) - actualGroupDocItems(1) shouldBe expectedGroupDocItems(1) - } - - it should "exclude errors and groups from specific packages" in { - val searchPackages = Array("com.digitalasset.canton.error.testpackage") - // We want to exclude the 'subpackage' which contains MildErrorsParent - val excludePackages = Array("com.digitalasset.canton.error.testpackage.subpackage") - - val actualErrorDocItems = ErrorCodeDocumentationGenerator.getErrorCodeItems( - searchPackagePrefixes = searchPackages, - excludePackagePrefixes = excludePackages, - ) - - val actualGroupDocItems = ErrorCodeDocumentationGenerator.getErrorGroupItems( - searchPackagePrefixes = searchPackages, - excludePackagePrefixes = excludePackages, - ) - - // Verify Error Items - // We expect 2 items: "SeriousError" and "DeprecatedError" (from the root testpackage) - // We expect "MildErrorsParent" (from subpackage) to be missing - actualErrorDocItems should have length 2 - - // Check presence of retained errors - actualErrorDocItems.map(_.code) should contain allOf ("BLUE_SCREEN", "DEPRECATED_ERROR") - - // Check absence of excluded error - actualErrorDocItems.map(_.code) should not contain "TEST_ROUTINE_FAILURE_PLEASE_IGNORE" - - // Verify Group Items - // MildErrorsParent is defined entirely within the excluded subpackage, so groups should be empty - actualGroupDocItems shouldBe empty - } - - it should "parse annotations of an error category" in { - val actual = ErrorCodeDocumentationGenerator.getErrorCategoryItem(TransientServerFailure) - - actual.resolution should not be (empty) - actual.description should not be (empty) - actual.retryStrategy should not be (empty) - } - - @deprecated(since = "since 123", message = "message 123") object Foo1 - @deprecated(message = "message 123") object Foo2 - @deprecated(since = "since 123") object Foo3 - @deprecated("message 123", "since 123") object Foo4 - - it should "parse annotations" in { - import scala.reflect.runtime.universe as ru - - def getFirstAnnotation[T: ClassTag](obj: T): ru.Annotation = - ru.runtimeMirror(getClass.getClassLoader).reflect(obj).symbol.annotations.head - - ErrorCodeDocumentationGenerator.parseScalaDeprecatedAnnotation( - getFirstAnnotation(Foo1): @nowarn("cat=deprecation") - ) shouldBe DeprecatedItem( - since = Some("since 123"), - message = "message 123", - ) - ErrorCodeDocumentationGenerator.parseScalaDeprecatedAnnotation( - getFirstAnnotation(Foo2): @nowarn("cat=deprecation") - ) shouldBe DeprecatedItem( - since = None, - message = "message 123", - ) - ErrorCodeDocumentationGenerator.parseScalaDeprecatedAnnotation( - getFirstAnnotation(Foo3): @nowarn("cat=deprecation") - ) shouldBe DeprecatedItem( - since = Some("since 123"), - message = "", - ) - ErrorCodeDocumentationGenerator.parseScalaDeprecatedAnnotation( - getFirstAnnotation(Foo4): @nowarn("cat=deprecation") - ) shouldBe DeprecatedItem( - since = Some("since 123"), - message = "message 123", - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/grpc/sampleservice/HelloServiceReferenceImplementation.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/grpc/sampleservice/HelloServiceReferenceImplementation.scala deleted file mode 100644 index 021bc18745..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/grpc/sampleservice/HelloServiceReferenceImplementation.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.grpc.sampleservice - -import com.digitalasset.canton.protobuf -import com.digitalasset.canton.protobuf.HelloServiceGrpc.HelloService -import io.grpc.stub.StreamObserver -import io.grpc.{BindableService, ServerServiceDefinition} - -import scala.concurrent.{ExecutionContext, Future} - -class HelloServiceReferenceImplementation(implicit ec: ExecutionContext) - extends HelloService - with BindableService { - - override def bindService(): ServerServiceDefinition = - protobuf.HelloServiceGrpc.bindService(this, ec) - - override def helloStreamed( - request: protobuf.Hello.Request, - responseObserver: StreamObserver[protobuf.Hello.Response], - ): Unit = { - responseObserver.onNext(protobuf.Hello.Response(request.msg)) - responseObserver.onCompleted() - } - - override def hello(request: protobuf.Hello.Request): Future[protobuf.Hello.Response] = - Future.successful(protobuf.Hello.Response(request.msg * 2)) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/ApiMocks.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/ApiMocks.scala deleted file mode 100644 index 5d8cd5cd43..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/ApiMocks.scala +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - -import com.daml.ledger.api.v2.value.Value -import com.daml.ledger.api.v2.value.Value.Sum -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.IdString -import com.digitalasset.daml.lf.value.Value as Lf -import scalaz.@@ - -object ApiMocks { - - val party: IdString.Party = Ref.Party.assertFromString("party") - - val identifier: Ref.Identifier = Ref.Identifier( - Ref.PackageId.assertFromString("package"), - Ref.QualifiedName.assertFromString("module:entity"), - ) - - val commandId: IdString.LedgerString @@ CommandIdTag = CommandId( - Ref.LedgerString.assertFromString("commandId") - ) - - val submissionId: IdString.LedgerString @@ SubmissionIdTag = SubmissionId( - Ref.LedgerString.assertFromString("submissionId") - ) - - val updateId: IdString.LedgerString @@ UpdateIdTag = UpdateId( - Ref.LedgerString.assertFromString("deadbeef") - ) - - val userId: IdString.UserId = Ref.UserId.assertFromString("userId") - - val workflowId: IdString.LedgerString @@ WorkflowIdTag = WorkflowId( - Ref.LedgerString.assertFromString("workflowId") - ) - - val label: IdString.Name = Ref.Name.assertFromString("label") - - object values { - val int64: Lf.ValueInt64 = Lf.ValueInt64(1) - val constructor: IdString.Name = Ref.Name.assertFromString("constructor") - - private val validPartyString = "party" - val validApiParty: Value = Value(Sum.Party(validPartyString)) - val validLfParty: Lf.ValueParty = Lf.ValueParty(Ref.Party.assertFromString(validPartyString)) - - private val invalidPartyString = "p@rty" - val invalidApiParty: Value = Value(Sum.Party(invalidPartyString)) - val invalidPartyMsg = - """Invalid argument: non expected character 0x40 in Daml-LF Party "p@rty"""" - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/IdentityProviderIdSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/IdentityProviderIdSpec.scala deleted file mode 100644 index e56cb45244..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/IdentityProviderIdSpec.scala +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - - import com.digitalasset.daml.lf.data.Ref.LedgerString -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class IdentityProviderIdSpec extends AnyWordSpec with Matchers { - - "IdentityProviderId.Default" in { - IdentityProviderId.Default.toDb shouldBe None - IdentityProviderId.Default.toRequestString shouldBe "" - } - - "IdentityProviderId.Id" in { - IdentityProviderId.Id(LedgerString.assertFromString("a123")).toDb shouldBe Some( - IdentityProviderId.Id(LedgerString.assertFromString("a123")) - ) - IdentityProviderId.Id(LedgerString.assertFromString("a123")).toRequestString shouldBe "a123" - } - - "IdentityProviderId.Id.fromString" in { - IdentityProviderId.Id.fromString("") shouldBe Left("Daml-LF Ledger String is empty") - - IdentityProviderId.Id.fromString("a" * 256) shouldBe Left( - "Daml-LF Ledger String is too long (max: 255)" - ) - - IdentityProviderId.Id.fromString("a123") shouldBe Right( - IdentityProviderId.Id( - LedgerString.assertFromString("a123") - ) - ) - } - - "IdentityProviderId.Id.assertFromString" in { - assertThrows[IllegalArgumentException] { - IdentityProviderId.Id.assertFromString("") - } - - assertThrows[IllegalArgumentException] { - IdentityProviderId.Id.assertFromString("a" * 256) - } - - IdentityProviderId.Id.assertFromString("a123") shouldBe - IdentityProviderId.Id( - LedgerString.assertFromString("a123") - ) - } - - "IdentityProviderId.apply" in { - IdentityProviderId("") shouldBe IdentityProviderId.Default - IdentityProviderId("a123") shouldBe IdentityProviderId.Id.assertFromString("a123") - } - - "IdentityProviderId.fromString" in { - IdentityProviderId.fromString("") shouldBe Right(IdentityProviderId.Default) - IdentityProviderId.fromString("a123") shouldBe Right( - IdentityProviderId.Id.assertFromString("a123") - ) - IdentityProviderId.fromString("a" * 256) shouldBe Left( - "Daml-LF Ledger String is too long (max: 255)" - ) - } - - "IdentityProviderId.fromDb" in { - IdentityProviderId.fromDb(None) shouldBe IdentityProviderId.Default - IdentityProviderId.fromDb( - Some(IdentityProviderId.Id.assertFromString("a123")) - ) shouldBe IdentityProviderId.Id.assertFromString("a123") - } -} - -object IdentityProviderIdSpec {} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/MockMessages.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/MockMessages.scala deleted file mode 100644 index 802056face..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/MockMessages.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - -import com.daml.ledger.api.v2.command_service.{ - SubmitAndWaitForReassignmentRequest, - SubmitAndWaitForTransactionRequest, - SubmitAndWaitRequest, -} -import com.daml.ledger.api.v2.command_submission_service.SubmitRequest -import com.daml.ledger.api.v2.commands.Commands -import com.daml.ledger.api.v2.commands.Commands.DeduplicationPeriod -import com.daml.ledger.api.v2.reassignment_commands.ReassignmentCommands -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA -import com.daml.ledger.api.v2.transaction_filter.{EventFormat, Filters, TransactionFormat} -import com.google.protobuf.timestamp.Timestamp - -object MockMessages { - - val workflowId = "workflowId" - val userId = "userId" - val commandId = "commandId" - val party = "party" - val party2 = "party2" - val ledgerEffectiveTime: Timestamp = Timestamp(0L, 0) - - val commands: Commands = - Commands( - workflowId = workflowId, - userId = userId, - commandId = commandId, - commands = Nil, - deduplicationPeriod = DeduplicationPeriod.Empty, - minLedgerTimeAbs = None, - minLedgerTimeRel = None, - actAs = Seq(party), - readAs = Nil, - submissionId = "", - disclosedContracts = Nil, - synchronizerId = "", - packageIdSelectionPreference = Nil, - prefetchContractKeys = Nil, - tapsMaxPasses = None, - ) - - val reassignmentCommands: ReassignmentCommands = - ReassignmentCommands( - workflowId = workflowId, - userId = userId, - submitter = party, - commandId = commandId, - submissionId = "", - commands = Nil, - ) - - val submitRequest: SubmitRequest = SubmitRequest(Some(commands)) - - private val eventFormat = - EventFormat( - filtersByParty = Map(party -> Filters(cumulative = Nil)), - filtersForAnyParty = None, - verbose = true, - ) - - private val transactionFormat = TransactionFormat( - eventFormat = Some(eventFormat), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - - val submitAndWaitRequest: SubmitAndWaitRequest = SubmitAndWaitRequest(Some(commands)) - val submitAndWaitForTransactionRequest: SubmitAndWaitForTransactionRequest = - SubmitAndWaitForTransactionRequest(Some(commands), Some(transactionFormat)) - val submitAndWaitForReassignmentRequest: SubmitAndWaitForReassignmentRequest = - SubmitAndWaitForReassignmentRequest(Some(reassignmentCommands), Some(eventFormat)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/TraceIdentifiersTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/TraceIdentifiersTest.scala deleted file mode 100644 index 529154053b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/TraceIdentifiersTest.scala +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - -import com.daml.ledger.api.v2.transaction.Transaction -import com.daml.tracing.SpanAttribute -import org.scalatest.matchers.should.Matchers.* -import org.scalatest.wordspec.AnyWordSpec - -class TraceIdentifiersTest extends AnyWordSpec { - val expected = Map( - (SpanAttribute.TransactionId, "transaction-id"), - (SpanAttribute.CommandId, "command-id"), - (SpanAttribute.WorkflowId, "workflow-id"), - (SpanAttribute.Offset, "12345678"), - ) - - "extract identifiers from Transaction" should { - "set non-empty values" in { - val observed = TraceIdentifiers.fromTransaction( - Transaction( - "transaction-id", - "command-id", - "workflow-id", - None, - Seq(), - 12345678L, - "", - None, - None, - None, - None, - ) - ) - observed shouldEqual expected - } - - "not set empty values" in { - val observed = - TraceIdentifiers.fromTransaction(Transaction.defaultInstance) - observed shouldBe empty - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/ValueConversionRoundTripTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/ValueConversionRoundTripTest.scala deleted file mode 100644 index 40867416db..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/ValueConversionRoundTripTest.scala +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api - -import com.daml.ledger.api.v2.value as api -import com.daml.ledger.api.v2.value.Value.Sum -import com.digitalasset.canton.ledger.api.util.LfEngineToApi -import com.digitalasset.canton.ledger.api.validation.{ValidatorTestUtils, ValueValidator} -import com.digitalasset.canton.logging.NoLogging -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Time -import com.digitalasset.daml.lf.value.Value.ContractId -import com.google.protobuf.empty.Empty -import org.mockito.MockitoSugar -import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor1} -import org.scalatest.wordspec.AnyWordSpec - -class ValueConversionRoundTripTest - extends AnyWordSpec - with ValidatorTestUtils - with TableDrivenPropertyChecks - with MockitoSugar { - - private val recordId = - api.Identifier(packageId, moduleName = "Mod", entityName = "Record") - - private val constructor: String = "constructor" - - private def roundTrip(v: api.Value): Either[String, api.Value] = - for { - lfValue <- ValueValidator.validateValue(v)(NoLogging).left.map(_.getMessage) - apiValue <- LfEngineToApi.lfValueToApiValue(true, lfValue) - } yield apiValue - - "round trip" should { - "be idempotent on value that do not contain non empty text maps, nor signed decimals" in { - - val testCases: TableFor1[Sum] = Table( - "values", - Sum.ContractId(ContractId.V1(Hash.hashPrivateKey("#coid")).coid), - ApiMocks.values.validApiParty.sum, - Sum.Int64(Long.MinValue), - Sum.Int64(0), - Sum.Int64(Long.MaxValue), - Sum.Text("string"), - Sum.Text(""), - Sum.Text("a ¶ ‱ 😂 😃"), - Sum.Timestamp(Time.Timestamp.MinValue.micros), - Sum.Timestamp(0), - Sum.Timestamp(Time.Timestamp.MaxValue.micros), - Sum.Date(Time.Date.MinValue.days), - Sum.Date(0), - Sum.Date(Time.Date.MaxValue.days), - Sum.Bool(true), - Sum.Bool(false), - Sum.Unit(Empty()), - Sum.List(api.List(List.empty)), - Sum.List(api.List((0 to 10).map(i => api.Value(Sum.Int64(i.toLong))))), - Sum.Optional(api.Optional(None)), - Sum.Optional(api.Optional(Some(ApiMocks.values.validApiParty))), - Sum.TextMap(api.TextMap(List.empty)), - Sum.GenMap(api.GenMap(List.empty)), - Sum.GenMap( - api.GenMap( - List( - api.GenMap.Entry(Some(api.Value(Sum.Text("key1"))), Some(api.Value(Sum.Int64(1)))), - api.GenMap.Entry(Some(api.Value(Sum.Text("key3"))), Some(api.Value(Sum.Int64(3)))), - api.GenMap.Entry(Some(api.Value(Sum.Text("key2"))), Some(api.Value(Sum.Int64(2)))), - api.GenMap.Entry(Some(api.Value(Sum.Text("key1"))), Some(api.Value(Sum.Int64(0)))), - ) - ) - ), - Sum.Record( - api.Record( - Some(recordId), - Seq( - api.RecordField("label1", Some(api.Value(Sum.Int64(1)))), - api.RecordField("label2", Some(api.Value(Sum.Int64(2)))), - api.RecordField("label0", Some(api.Value(Sum.Int64(3)))), - ), - ) - ), - Sum.Variant( - api.Variant(Some(recordId), constructor, Some(ApiMocks.values.validApiParty)) - ), - ) - - forEvery(testCases) { testCase => - val input = api.Value(testCase) - roundTrip(input) shouldEqual Right(input) - } - } - - "should sort the entries of a map" in { - val entries = List("‱", "1", "😂", "😃", "a").zipWithIndex.map { case (k, v) => - api.TextMap.Entry(k, Some(api.Value(Sum.Int64(v.toLong)))) - } - val sortedEntries = entries.sortBy(_.key) - - // just to be sure we did not write the entries sorted - assert(entries != sortedEntries) - - val input = api.Value(Sum.TextMap(api.TextMap(entries))) - val expected = api.Value(Sum.TextMap(api.TextMap(sortedEntries))) - - roundTrip(input) shouldNot equal(Right(input)) - roundTrip(input) shouldEqual Right(expected) - } - - "should write the positive decimal in canonical form" in { - - val testCases = Table( - "input/output", - "0" -> "0.", - "0.0" -> "0.", - "3.1415926536" -> "3.1415926536", - ("1" + "0" * 27) -> ("1" + "0" * 27 + "."), - ("1" + "0" * 27 + "." + "0" * 9 + "1") -> ("1" + "0" * 27 + "." + "0" * 9 + "1"), - ("0." + "0" * 9 + "1") -> ("0." + "0" * 9 + "1"), - ("0" * 10 + "42") -> "42.", - ("0" * 10 + "42." + "0" * 10) -> "42.", - ) - - roundTrip(api.Value(Sum.Numeric("0"))) shouldNot equal(api.Value(Sum.Numeric("0"))) - roundTrip(api.Value(Sum.Numeric("+1.0"))) shouldNot equal(api.Value(Sum.Numeric("+1.0"))) - - forEvery(testCases) { case (input, expected) => - roundTrip(api.Value(Sum.Numeric(input))) shouldEqual Right(api.Value(Sum.Numeric(expected))) - roundTrip(api.Value(Sum.Numeric("+" + input))) shouldEqual Right( - api.Value(Sum.Numeric(expected)) - ) - } - } - - "should write the negative decimal in canonical form" in { - - val testCases = Table( - "input/output", - "-0" -> "0.", - "-0.0" -> "0.", - "-3.1415926536" -> "-3.1415926536", - ("-1" + "0" * 27) -> ("-1" + "0" * 27 + "."), - ("-1" + "0" * 27 + "." + "0" * 9 + "1") -> ("-1" + "0" * 27 + "." + "0" * 9 + "1"), - ("-0." + "0" * 9 + "1") -> ("-0." + "0" * 9 + "1"), - ("-" + "0" * 10 + "42") -> "-42.", - ("-" + "0" * 10 + "42." + "0" * 10) -> "-42.", - ) - - roundTrip(api.Value(Sum.Numeric("-0"))) shouldNot equal(api.Value(Sum.Numeric("-0"))) - - forEvery(testCases) { case (input, expected) => - roundTrip(api.Value(Sum.Numeric(input))) shouldEqual Right(api.Value(Sum.Numeric(expected))) - } - } - - "drop trailing Nones in LF -> API conversion" in { - def intValue(valO: Option[Long]): Option[api.Value] = - Some( - api.Value(Sum.Optional(api.Optional(valO.map(v => api.Value(api.Value.Sum.Int64(v)))))) - ) - val testCases = Table[Seq[api.RecordField], Seq[api.RecordField]]( - "input" -> "expected", - // Single None dropped - Seq( - api.RecordField("label1", intValue(None)) - ) -> Seq(), - // Single Some kept - Seq( - api.RecordField("label1", intValue(Some(1L))) - ) -> Seq( - api.RecordField("label1", intValue(Some(1L))) - ), - // None in head position kept - Seq( - api.RecordField("label1", intValue(None)), - api.RecordField("label2", intValue(Some(1L))), - ) -> Seq( - api.RecordField("label1", intValue(None)), - api.RecordField("label2", intValue(Some(1L))), - ), - // None in tail position dropped - Seq( - api.RecordField("label1", intValue(None)), - api.RecordField("label2", intValue(Some(1L))), - api.RecordField("label3", intValue(None)), - ) -> Seq( - api.RecordField("label1", intValue(None)), - api.RecordField("label2", intValue(Some(1L))), - ), - // Two nones in tail position dropped - Seq( - api.RecordField("label1", intValue(Some(1L))), - api.RecordField("label2", intValue(None)), - api.RecordField("label3", intValue(Some(3L))), - api.RecordField("label4", intValue(None)), - api.RecordField("label5", intValue(None)), - ) -> Seq( - api.RecordField("label1", intValue(Some(1L))), - api.RecordField("label2", intValue(None)), - api.RecordField("label3", intValue(Some(3L))), - ), - // Trailing None in nested record dropped - Seq( - api.RecordField( - "label1", - Some( - api.Value( - Sum.Record( - api.Record( - Some(recordId.copy(entityName = "nestedEntity")), - Seq( - api.RecordField("nestedLabel1", intValue(Some(1L))), - api.RecordField("nestedLabel2", intValue(None)), - ), - ) - ) - ) - ), - ) - ) -> Seq( - api.RecordField( - "label1", - Some( - api.Value( - Sum.Record( - api.Record( - Some(recordId.copy(entityName = "nestedEntity")), - Seq( - api.RecordField("nestedLabel1", intValue(Some(1L))) - ), - ) - ) - ) - ), - ) - ), - ) - - forEvery(testCases) { case (inputVals, expectedVals) => - val input = api.Value(Sum.Record(api.Record(Some(recordId), inputVals))) - val expected = api.Value(Sum.Record(api.Record(Some(recordId), expectedVals))) - - roundTrip(input) shouldEqual Right(expected) - roundTrip(expected) shouldEqual Right(expected) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaimsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaimsSpec.scala deleted file mode 100644 index 20546a56ef..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaimsSpec.scala +++ /dev/null @@ -1,535 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA -import com.daml.ledger.api.v2.transaction_filter.{ - EventFormat, - Filters, - ParticipantAuthorizationTopologyFormat, - TopologyFormat, - TransactionFormat, - UpdateFormat, -} -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.auth.RequiredClaim -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import scalapb.lenses.Lens - -class RequiredClaimsSpec extends AsyncFlatSpec with BaseTest with Matchers { - - behavior of "submissionClaims" - - it should "compute the correct claims in the happy path" in { - val userIdL: Lens[String, String] = Lens.unit - RequiredClaims.submissionClaims( - actAs = Set("1", "2", "3"), - readAs = Set("a", "b", "c"), - userIdL = userIdL, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ActAs("1"), - RequiredClaim.ActAs("2"), - RequiredClaim.ActAs("3"), - RequiredClaim.MatchUserId(userIdL), - ) - } - - it should "compute the correct claims if no actAs" in { - val userIdL: Lens[String, String] = Lens.unit - RequiredClaims.submissionClaims( - actAs = Set.empty, - readAs = Set("a", "b", "c"), - userIdL = userIdL, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.MatchUserId(userIdL), - ) - } - - it should "compute the correct claims if no ActAs and no ReadAs" in { - val userIdL: Lens[String, String] = Lens.unit - RequiredClaims.submissionClaims( - actAs = Set.empty, - readAs = Set.empty, - userIdL = userIdL, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.MatchUserId(userIdL) - ) - } - - behavior of "executionClaims" - - it should "compute the correct ExecuteAs claims in the happy path" in { - val userIdL: Lens[String, String] = Lens.unit - RequiredClaims.executionClaims( - executeAs = Set("1", "2", "3"), - readAs = Set("a", "b", "c"), - userIdL = userIdL, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ExecuteAs("1"), - RequiredClaim.ExecuteAs("2"), - RequiredClaim.ExecuteAs("3"), - RequiredClaim.MatchUserId(userIdL), - ) - } - - it should "compute the correct claims if no ExecuteAs" in { - val userIdL: Lens[String, String] = Lens.unit - RequiredClaims.executionClaims( - executeAs = Set.empty, - readAs = Set("a", "b", "c"), - userIdL = userIdL, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.MatchUserId(userIdL), - ) - } - - it should "compute the correct claims if no ExecuteAs and no ReadAs" in { - val userIdL: Lens[String, String] = Lens.unit - RequiredClaims.executionClaims( - executeAs = Set.empty, - readAs = Set.empty, - userIdL = userIdL, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.MatchUserId(userIdL) - ) - } - - behavior of "readAsForAllParties" - - it should "compute the correct claims in the happy path" in { - RequiredClaims.readAsForAllParties[String]( - Seq("a", "b", "c") - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - ) - } - - it should "compute the correct claims if input is empty" in { - RequiredClaims.readAsForAllParties[String]( - Nil - ) shouldBe Nil - } - - behavior of "eventFormatClaims" - - it should "compute the correct claims in the happy path" in { - RequiredClaims.eventFormatClaims[String]( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAsAnyParty(), - ) - } - - it should "compute the correct claims if no any party filters" in { - RequiredClaims.eventFormatClaims[String]( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = false, - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - ) - } - - it should "compute the correct claims if no by party filters" in { - RequiredClaims.eventFormatClaims[String]( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(Filters(Nil)), - verbose = false, - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAsAnyParty() - ) - } - - it should "compute the correct claims if no any party filters and no party filters" in { - RequiredClaims.eventFormatClaims[String]( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = true, - ) - ) shouldBe Nil - } - - behavior of "updateFormatClaims" - - it should "compute the correct claims in the happy path" in { - RequiredClaims.updateFormatClaims[String]( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map( - "1" -> Filters(Nil), - "2" -> Filters(Nil), - "3" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - includeTopologyEvents = Some( - TopologyFormat( - includeParticipantAuthorizationEvents = Some( - ParticipantAuthorizationTopologyFormat( - parties = Seq("A", "B", "C") - ) - ) - ) - ), - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("A"), - RequiredClaim.ReadAs("B"), - RequiredClaim.ReadAs("C"), - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAs("1"), - RequiredClaim.ReadAs("2"), - RequiredClaim.ReadAs("3"), - RequiredClaim.ReadAsAnyParty(), - ) - } - - it should "compute the correct claims if empty parties for ParticipantAuthorizationTopologyFormat" in { - RequiredClaims.updateFormatClaims[String]( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map( - "1" -> Filters(Nil), - "2" -> Filters(Nil), - "3" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - includeTopologyEvents = Some( - TopologyFormat( - includeParticipantAuthorizationEvents = Some( - ParticipantAuthorizationTopologyFormat( - parties = Nil - ) - ) - ) - ), - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAs("1"), - RequiredClaim.ReadAs("2"), - RequiredClaim.ReadAs("3"), - RequiredClaim.ReadAsAnyParty(), - ) - } - - it should "compute the correct claims if empty ParticipantAuthorizationTopologyFormat" in { - RequiredClaims.updateFormatClaims[String]( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map( - "1" -> Filters(Nil), - "2" -> Filters(Nil), - "3" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - includeTopologyEvents = Some( - TopologyFormat( - includeParticipantAuthorizationEvents = None - ) - ), - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAs("1"), - RequiredClaim.ReadAs("2"), - RequiredClaim.ReadAs("3"), - RequiredClaim.ReadAsAnyParty(), - ) - } - - it should "compute the correct claims if empty TopologyFormat" in { - RequiredClaims.updateFormatClaims[String]( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map( - "1" -> Filters(Nil), - "2" -> Filters(Nil), - "3" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - includeTopologyEvents = None, - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAs("1"), - RequiredClaim.ReadAs("2"), - RequiredClaim.ReadAs("3"), - RequiredClaim.ReadAsAnyParty(), - ) - } - - it should "compute the correct claims if no filtersForAnyParty in includeReassignments" in { - RequiredClaims.updateFormatClaims[String]( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map( - "1" -> Filters(Nil), - "2" -> Filters(Nil), - "3" -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = true, - ) - ), - includeTopologyEvents = None, - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAs("1"), - RequiredClaim.ReadAs("2"), - RequiredClaim.ReadAs("3"), - RequiredClaim.ReadAsAnyParty(), - ) - } - - it should "compute the correct claims if no filtersForAnyParty in includeReassignments and in includeTransactions" in { - RequiredClaims.updateFormatClaims[String]( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map( - "1" -> Filters(Nil), - "2" -> Filters(Nil), - "3" -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = true, - ) - ), - includeTopologyEvents = None, - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAs("1"), - RequiredClaim.ReadAs("2"), - RequiredClaim.ReadAs("3"), - ) - } - - it should "compute the correct claims if no includeReassignments" in { - RequiredClaims.updateFormatClaims[String]( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = None, - includeTopologyEvents = None, - ) - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - ) - } - - it should "compute the correct claims if no includeTransactions" in { - RequiredClaims.updateFormatClaims[String]( - UpdateFormat( - includeTransactions = None, - includeReassignments = None, - includeTopologyEvents = None, - ) - ) shouldBe Nil - } - - behavior of "idpAdminClaimsAndMatchingRequestIdpId" - - it should "compute the correct claims in the happy path" in { - val identityProviderIdL: Lens[String, String] = Lens.unit - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId[String]( - identityProviderIdL = identityProviderIdL, - mustBeParticipantAdmin = false, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.MatchIdentityProviderId(identityProviderIdL), - RequiredClaim.AdminOrIdpAdmin(), - ) - } - - it should "compute the correct claims if must be participant admin" in { - val identityProviderIdL: Lens[String, String] = Lens.unit - RequiredClaims.idpAdminClaimsAndMatchingRequestIdpId[String]( - identityProviderIdL = identityProviderIdL, - mustBeParticipantAdmin = true, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.MatchIdentityProviderId(identityProviderIdL), - RequiredClaim.Admin(), - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/StreamAuthorizationComponentSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/StreamAuthorizationComponentSpec.scala deleted file mode 100644 index 8ef8c470bd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/StreamAuthorizationComponentSpec.scala +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.daml.grpc.adapter.client.pekko.ClientAdapter -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA -import com.daml.ledger.api.v2.transaction_filter.{ - EventFormat, - Filters, - TransactionFormat, - UpdateFormat, -} -import com.daml.ledger.api.v2.update_service.* -import com.daml.ledger.api.v2.update_service.UpdateServiceGrpc.{UpdateService, UpdateServiceStub} -import com.daml.ledger.resources.{ResourceContext, ResourceOwner} -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.auth.{ - AuthInterceptor, - Authorizer, - Claim, - ClaimPublic, - ClaimReadAsParty, - ClaimSet, -} -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.config.ServerConfig -import com.digitalasset.canton.ledger.api.UserRight.CanReadAs -import com.digitalasset.canton.ledger.api.auth.services.UpdateServiceAuthorization -import com.digitalasset.canton.ledger.api.grpc.StreamingServiceLifecycleManagement -import com.digitalasset.canton.ledger.api.{IdentityProviderId, User} -import com.digitalasset.canton.ledger.localstore.InMemoryUserManagementStore -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.logging.SuppressionRule.{FullSuppression, LoggerNameContains} -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.{ApiServiceOwner, GrpcServerOwner} -import com.digitalasset.canton.{BaseTest, UniquePortGenerator} -import com.digitalasset.daml.lf.data.Ref -import io.grpc.* -import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder -import io.grpc.stub.StreamObserver -import org.apache.pekko.stream.scaladsl.Source -import org.apache.pekko.{Done, NotUsed} -import org.scalatest.Assertion -import org.scalatest.concurrent.PatienceConfiguration -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.time.Instant -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContextExecutor, Future, Promise} -import scala.util.Try - -class StreamAuthorizationComponentSpec - extends AsyncFlatSpec - with BaseTest - with Matchers - with PekkoBeforeAndAfterAll { - - private val OngoingAuthorizationObserverLoggerName = "UserBasedOngoingAuthorization" - - private implicit val ec: ExecutionContextExecutor = materializer.executionContext - - behavior of s"Stream authorization" - - it should "be successful in the happy path, and client cancellation tears down server side gRPC and pekko-streams too" in test { - fixture => - // this stream takes 10 elements (takes 2 seconds to produce), then it is closed (user side cancellation). - // after one second a scheduled user right check will commence, this check expected to be successful - fixture.clientStream - .take(10) - .map(_ => logger.debug("received")) - .run() - .map(_ => fixture.waitForServerPekkoStream shouldBe None) - } - - it should "not emit STALE_STREAM_AUTHORIZATION after it was cancelled downstream" in test { - fixture => - // this stream takes 10 elements (takes 2 seconds to produce), then it is closed (user side cancellation). - // after one second a scheduled user right check will commence, this check expected to be successful - fixture.clientStream - .take(10) - .zipWithIndex - .map { case (_, index) => - if (index == 9) { - // towards the end we change the user rights, which makes the next scheduled user right check fail - fixture.changeUserRights - } - logger.debug("received") - } - .run() - .map { _ => - // now the stream is cancelled from downstream because of the take(10) above - fixture.waitForServerPekkoStream shouldBe None - val suppressionRules = FullSuppression && - LoggerNameContains(OngoingAuthorizationObserverLoggerName) - loggerFactory.suppress(suppressionRules) { - // waiting 2 seconds for the user right checker schedule task to execute - Threading.sleep(2000) - loggerFactory.fetchRecordedLogEntries shouldBe Nil - } - } - } - - it should "cancel streams if user rights changed" in test { fixture => - val suppressionRules = FullSuppression && - LoggerNameContains(OngoingAuthorizationObserverLoggerName) - loggerFactory.suppress(suppressionRules) { - val result = fixture.clientStream - .take(10) - .zipWithIndex - .map { case (_, index) => - if (index == 1) { - // after 2 received entries (400 millis) the user right change, - // which triggers a STALE_STREAM_AUTHORIZATION - fixture.changeUserRights - } - logger.debug(s"received #$index") - } - .run() - .failed - .map { t => - // the client stream should be cancelled with error - t.getMessage should include("STALE_STREAM_AUTHORIZATION") - // the server stream should be completed - fixture.waitForServerPekkoStream shouldBe None - } - // Please note: asserting on the log message is important because in the previous test - // "not emit STALE_STREAM_AUTHORIZATION after it was cancelled downstream" we are doing - // a negative lookup, and we need to make sure that the negative lookup looks for the - // right log messages. - eventually() { - loggerFactory.fetchRecordedLogEntries should have size (1) - loggerFactory.fetchRecordedLogEntries(0).infoMessage should include( - "STALE_STREAM_AUTHORIZATION" - ) - } - result - } - } - - it should "cancel streams if authorization expired" in test { fixture => - fixture.clientStream - .take(10) - .zipWithIndex - .map { case (_, index) => - if (index == 1) { - // after 2 received entries (400 millis) the user right change, - // which triggers a STALE_STREAM_AUTHORIZATION - fixture.expireUserClaims - } - logger.debug(s"received #$index") - } - .run() - .failed - .map { t => - // the client stream should be cancelled with error - t.getMessage should include("ACCESS_TOKEN_EXPIRED") - // the server stream should be completed - fixture.waitForServerPekkoStream shouldBe None - } - } - - case class Fixture( - clientStream: Source[GetUpdatesResponse, NotUsed], - serverStreamFinished: Future[Done], - userManagementStore: UserManagementStore, - nowRef: AtomicReference[Instant], - ) { - def waitForServerPekkoStream: Option[Throwable] = { - logger.debug("Started waiting for the server stream to finish.") - Try( - serverStreamFinished - .futureValue(timeout = PatienceConfiguration.Timeout(FiniteDuration(5, "seconds"))) - ).toEither.swap.toOption - } - - def changeUserRights = - userManagementStore - .revokeRights( - id = Ref.UserId.assertFromString(userId), - rights = Set(CanReadAs(partyId1)), - identityProviderId = IdentityProviderId.Default, - )(LoggingContextWithTrace.ForTesting) - .futureValue - .isRight shouldBe true - - def expireUserClaims = - nowRef.getAndUpdate(x => x.plusSeconds(20)) - } - - private val userId = "user-id" - val partyId1 = Ref.Party.assertFromString("party1") - - private def test(body: Fixture => Future[Any]): Future[Assertion] = { - val participantId = "participant-id" - val nowRef = new AtomicReference(Instant.now()) - val partyId2 = Ref.Party.assertFromString("party2") - val claimSetFixture = ClaimSet.Claims( - claims = List[Claim](ClaimPublic, ClaimReadAsParty(partyId1), ClaimReadAsParty(partyId2)), - participantId = Some(participantId), - userId = Some(userId), - expiration = Some(nowRef.get().plusSeconds(10)), - identityProviderId = None, - resolvedFromUser = true, - ) - val authorizationClaimSetFixtureInterceptor = new ServerInterceptor { - override def interceptCall[ReqT, RespT]( - call: ServerCall[ReqT, RespT], - headers: Metadata, - next: ServerCallHandler[ReqT, RespT], - ): ServerCall.Listener[ReqT] = { - val nextCtx = - Context.current.withValue(AuthInterceptor.contextKeyClaimSet, claimSetFixture) - Contexts.interceptCall(nextCtx, call, headers, next) - } - } - val userManagementStore = new InMemoryUserManagementStore(loggerFactory = loggerFactory) - userManagementStore - .createUser( - user = User( - id = Ref.UserId.assertFromString(userId), - primaryParty = None, - identityProviderId = IdentityProviderId.Default, - ), - rights = Set( - CanReadAs(partyId1), - CanReadAs(partyId2), - ), - )(LoggingContextWithTrace.ForTesting) - .futureValue - .isRight shouldBe true - val authorizer = new Authorizer( - now = () => nowRef.get(), - participantId = participantId, - ongoingAuthorizationFactory = UserBasedOngoingAuthorization.Factory( - now = () => nowRef.get(), - userManagementStore = userManagementStore, - userRightsCheckIntervalInSeconds = 1, - pekkoScheduler = system.scheduler, - jwtTimestampLeeway = None, - tokenExpiryGracePeriodForStreams = None, - loggerFactory = loggerFactory, - )(ec, traceContext), - jwtTimestampLeeway = None, - loggerFactory = loggerFactory, - ) - val outerLoggerFactory = loggerFactory - val transactionStreamTerminationPromise = Promise[Done]() - val apiTransactionServiceFixture = new UpdateService with StreamingServiceLifecycleManagement { - override def getUpdates( - request: GetUpdatesRequest, - responseObserver: StreamObserver[GetUpdatesResponse], - ): Unit = registerStream(responseObserver) { - Source - .fromIterator(() => Iterator.continually(GetUpdatesResponse.defaultInstance)) - .map { elem => - Threading.sleep(200) - logger.debug("sent") - elem - } - .watchTermination() { case (mat, doneF) => - doneF.onComplete(transactionStreamTerminationPromise.complete) - mat - } - } - - override protected def loggerFactory: NamedLoggerFactory = outerLoggerFactory - - def notSupported = throw new UnsupportedOperationException() - - override def getUpdateByOffset( - request: GetUpdateByOffsetRequest - ): Future[GetUpdateResponse] = notSupported - - override def getUpdateById( - request: GetUpdateByIdRequest - ): Future[GetUpdateResponse] = notSupported - - def getUpdatesPage(request: GetUpdatesPageRequest): Future[GetUpdatesPageResponse] = - notSupported - } - val grpcServerPort = UniquePortGenerator.next - val authorizedTransactionServiceOwner = - ResourceOwner.forCloseable(() => - new UpdateServiceAuthorization(apiTransactionServiceFixture, authorizer) - ) - - def grpcServerOwnerFor(bindableService: BindableService) = GrpcServerOwner( - address = None, - desiredPort = grpcServerPort, - maxInboundMessageSize = ApiServiceOwner.DefaultMaxInboundMessageSize, - maxInboundMetadataSize = ServerConfig.defaultMaxInboundMetadataSize.unwrap, - maxConcurrentCallsPerConnection = ServerConfig.defaultMaxConcurrentCallsPerConnection.unwrap, - sslContext = None, - interceptors = List(authorizationClaimSetFixtureInterceptor), - metrics = LedgerApiServerMetrics.ForTesting, - servicesExecutor = ec, - services = List(bindableService), - loggerFactory = loggerFactory, - keepAlive = None, - ) - - val channelOwner = ResourceOwner.forChannel( - NettyChannelBuilder - .forAddress("localhost", grpcServerPort.unwrap) - .usePlaintext(), - FiniteDuration(10, "seconds"), - ) - - def getTransactions( - channel: Channel, - request: GetUpdatesRequest, - ): Source[GetUpdatesResponse, NotUsed] = - ClientAdapter.serverStreaming( - request, - new UpdateServiceStub(channel).getUpdates, - ) - - val transactionStreamOwner = for { - transactionService <- authorizedTransactionServiceOwner - _ <- grpcServerOwnerFor(transactionService) - grpcChannel <- channelOwner - } yield { - getTransactions( - grpcChannel, - GetUpdatesRequest( - beginExclusive = 0, - endInclusive = None, - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - partyId1 -> Filters(Nil), - partyId2 -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = false, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = None, - includeTopologyEvents = None, - ) - ), - descendingOrder = false, - ), - ) - } - implicit val resourceContext = ResourceContext(ec) - transactionStreamOwner - .use { clientStream => - logger.info("Server and connected client created.") - body( - Fixture( - clientStream, - transactionStreamTerminationPromise.future, - userManagementStore, - nowRef, - ) - ) - .transform { result => - logger.info("Test finished, starting teardown.") - Try(result) - } - } - .map { result => - logger.info("Teardown finished.") - result.success.value // populate error - succeed - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/UserBasedAuthInterceptorSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/UserBasedAuthInterceptorSpec.scala deleted file mode 100644 index f6e81ed42f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/UserBasedAuthInterceptorSpec.scala +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.digitalasset.canton.auth.{AuthInterceptor, AuthService, ClaimSet, GrpcAuthInterceptor} -import com.digitalasset.canton.config.ApiLoggingConfig -import com.digitalasset.canton.ledger.api.auth.interceptor.UserBasedClaimResolver -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.logging.SuppressionRule -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import io.grpc.MethodDescriptor.Marshaller -import io.grpc.protobuf.StatusProto -import io.grpc.{Metadata, MethodDescriptor, ServerCall, Status} -import org.mockito.captor.ArgCaptor -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.Assertion -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.slf4j.event.Level - -import scala.concurrent.{Future, Promise} - -class UserBasedAuthInterceptorSpec - extends AsyncFlatSpec - with MockitoSugar - with Matchers - with ArgumentMatchersSugar - with BaseTest - with HasExecutionContext { - - private val className = classOf[AuthInterceptor].getSimpleName - - behavior of s"$className.interceptCall" - - private val AuthInterceptorSuppressionRule: SuppressionRule = - SuppressionRule.forLogger[AuthInterceptor] && SuppressionRule.Level(Level.ERROR) - - it should "close the ServerCall with a V2 status code on decoding failure" in { - loggerFactory.assertLogs(AuthInterceptorSuppressionRule)( - within = testServerCloseError { case (actualStatus, actualMetadata) => - actualStatus.getCode shouldBe Status.Code.INTERNAL - actualStatus.getDescription shouldBe "An error occurred. Please contact the operator and inquire about the request with tid " - - val actualRpcStatus = StatusProto.fromStatusAndTrailers(actualStatus, actualMetadata) - actualRpcStatus.getDetailsList.size() shouldBe 0 - }, - assertions = _.errorMessage should include( - "INTERNAL_AUTHORIZATION_ERROR(4,0): Failed to get claims from request metadata" - ), - ) - } - - private def testServerCloseError( - assertRpcStatus: (Status, Metadata) => Assertion - ): Future[Assertion] = { - val authService = mock[AuthService] - val identityProviderAwareAuthService = mock[AuthService] - val userManagementService = mock[UserManagementStore] - val serverCall = mock[ServerCall[Nothing, Nothing]] - val marshaller = mock[Marshaller[Nothing]] - val methodDescriptor = MethodDescriptor - .newBuilder[Nothing, Nothing]( - marshaller, - marshaller, - ) - .setFullMethodName("") - .setType(MethodDescriptor.MethodType.UNARY) - .build() - val failedMetadataDecode = - Future.failed[ClaimSet](new RuntimeException("some internal failure")) - - val promise = Promise[Unit]() - // Using a promise to ensure the verify call below happens after the expected call to `serverCall.close` - when(serverCall.getMethodDescriptor).thenReturn(methodDescriptor) - when(serverCall.getAttributes).thenCallRealMethod() - when(serverCall.close(any[Status], any[Metadata])).thenAnswer { - promise.success(()) - () - } - - val authInterceptor = - new AuthInterceptor( - List(authService, identityProviderAwareAuthService), - loggerFactory, - executionContext, - new UserBasedClaimResolver( - Some(userManagementService), - executionContext, - ), - ) - - val statusCaptor = ArgCaptor[Status] - val metadataCaptor = ArgCaptor[Metadata] - - when( - identityProviderAwareAuthService.decodeToken(any[Option[String]], any[String])( - any[TraceContext] - ) - ) - .thenReturn(Future.successful(ClaimSet.Unauthenticated)) - when(authService.decodeToken(any[Option[String]], any[String])(anyTraceContext)) - .thenReturn(failedMetadataDecode) - new GrpcAuthInterceptor( - authInterceptor, - loggerFactory, - ApiLoggingConfig(), - executionContext, - ) - .interceptCall[Nothing, Nothing](serverCall, new Metadata(), null) - - promise.future.map { _ => - verify(serverCall).close(statusCaptor.capture, metadataCaptor.capture) - assertRpcStatus(statusCaptor.value, metadataCaptor.value) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/UserBasedOngoingAuthorizationSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/UserBasedOngoingAuthorizationSpec.scala deleted file mode 100644 index e5b98e53e6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/UserBasedOngoingAuthorizationSpec.scala +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth - -import com.daml.clock.AdjustableClock -import com.daml.jwt.JwtTimestampLeeway -import com.digitalasset.base.error.ErrorsAssertions -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.auth.AuthorizationError.Expired -import com.digitalasset.canton.auth.{AuthorizationChecksErrors, ClaimSet} -import com.digitalasset.canton.config.NonNegativeDuration -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.logging.{ErrorLoggingContext, SuppressionRule} -import io.grpc.StatusRuntimeException -import io.grpc.stub.ServerCallStreamObserver -import org.apache.pekko.actor.{Cancellable, Scheduler} -import org.mockito.{ArgumentCaptor, ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.concurrent.{Eventually, IntegrationPatience} -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.slf4j.event.Level.INFO - -import java.time.{Clock, Duration, Instant, ZoneId} -import scala.concurrent.ExecutionContext -import scala.concurrent.duration.{Duration as SDuration, FiniteDuration} - -class UserBasedOngoingAuthorizationSpec - extends AsyncFlatSpec - with BaseTest - with Matchers - with Eventually - with IntegrationPatience - with MockitoSugar - with ArgumentMatchersSugar - with ErrorsAssertions { - - private implicit val errorLogger: ErrorLoggingContext = ErrorLoggingContext( - loggerFactory.getTracedLogger(getClass), - loggerFactory.properties, - traceContext, - ) - - it should "signal onError aborting the stream when user rights state hasn't been refreshed in a timely manner" in { - - val userRightsCheckIntervalInSeconds = 10 - val (clock, delegate, tested, cancellableMock, _) = - createInfrastructure(None, None, userRightsCheckIntervalInSeconds, None) - - // After 20 seconds pass we expect onError to be called due to lack of user rights state refresh task inactivity - tested.onNext(1) - clock.fastForward(Duration.ofSeconds(2.toLong * userRightsCheckIntervalInSeconds - 1)) - tested.onNext(2) - clock.fastForward(Duration.ofSeconds(2)) - // Next onNext detects the user rights state refresh task inactivity - tested.onNext(3) - - val captor = ArgumentCaptor.forClass(classOf[StatusRuntimeException]) - val order = inOrder(delegate) - order.verify(delegate, times(1)).onNext(1) - order.verify(delegate, times(1)).onNext(2) - order.verify(delegate, times(1)).onError(captor.capture()) - order.verifyNoMoreInteractions() - // Scheduled task is cancelled - verify(cancellableMock, times(1)).cancel() - assertError( - actual = captor.getValue, - expected = AuthorizationChecksErrors.StaleUserManagementBasedStreamClaims - .Reject() - .asGrpcError, - ) - - // onError has already been called by tested implementation so subsequent onNext, onError and onComplete - // must not be forwarded to the delegate observer - tested.onNext(4) - tested.onError(new RuntimeException) - tested.onCompleted() - verifyNoMoreInteractions(delegate) - - succeed - } - - def nonAbortCheck( - parameterName: String, - jwtTimestampLeeway: Option[JwtTimestampLeeway], - tokenExpiryGracePeriodForStreams: Option[Duration], - ): Unit = - it should s"not abort the stream when the token has expired but adding $parameterName overlaps verification time" in { - val (clock, delegate, tested, _, _) = - createInfrastructure(jwtTimestampLeeway, tokenExpiryGracePeriodForStreams) - - tested.onNext(1) - clock.fastForward(Duration.ofSeconds(1)) - tested.onNext(2) - - val order = inOrder(delegate) - order.verify(delegate, times(1)).onNext(1) - order.verify(delegate, times(1)).onNext(2) - order.verifyNoMoreInteractions() - - succeed - } - - nonAbortCheck("leeway time", Some(JwtTimestampLeeway(default = Some(1))), None) - nonAbortCheck("token grace period", None, Some(Duration.ofSeconds(1))) - - it should s"never abort the stream when the token has expired but added an infinite grace period" in { - val clockJump: Int = 1000 * 10000 - val (clock, delegate, tested, _, _) = - createInfrastructure( - None, - Some(NonNegativeDuration(SDuration.Inf).asJavaApproximation), - clockJump, - ) - - tested.onNext(1) - clock.fastForward(Duration.ofSeconds(clockJump.toLong)) - tested.onNext(2) - - val order = inOrder(delegate) - order.verify(delegate, times(1)).onNext(1) - order.verify(delegate, times(1)).onNext(2) - order.verifyNoMoreInteractions() - - succeed - } - - def abortCheck( - parameterName: String, - jwtTimestampLeeway: Option[JwtTimestampLeeway], - tokenExpiryGracePeriodForStreams: Option[Duration], - ): Unit = - it should s"abort the stream when the token has expired with $parameterName" in { - - inside(createInfrastructure(jwtTimestampLeeway, tokenExpiryGracePeriodForStreams)) { - case (clock, delegate, tested, cancellableMock, Some(expiration)) => - // After 2 seconds have passed we expect onError to be called due to invalid expiration claim - tested.onNext(1) - clock.fastForward(Duration.ofSeconds(2)) - // Next onNext detects the invalid expiration claim - tested.onNext(2) - - val captor = ArgumentCaptor.forClass(classOf[StatusRuntimeException]) - val order = inOrder(delegate) - order.verify(delegate, times(1)).onNext(1) - order.verify(delegate, times(1)).onError(captor.capture()) - order.verifyNoMoreInteractions() - - loggerFactory.assertLogs(SuppressionRule.Level(INFO))( - within = { - // Scheduled task is cancelled - verify(cancellableMock, times(1)).cancel() - assertError( - actual = captor.getValue, - expected = AuthorizationChecksErrors.AccessTokenExpired - .Reject(Expired(expiration, clock.instant).reason) - .asGrpcError, - ) - }, - assertions = - _.infoMessage should include("ACCESS_TOKEN_EXPIRED(2,0): Claims were valid until "), - ) - - // onError has already been called by tested implementation so subsequent onNext, onError and onComplete - // must not be forwarded to the delegate observer - tested.onNext(3) - tested.onError(new RuntimeException) - tested.onCompleted() - verifyNoMoreInteractions(delegate) - - succeed - } - } - - abortCheck("leeway time", Some(JwtTimestampLeeway(default = Some(1))), None) - abortCheck("token grace period", None, Some(Duration.ofSeconds(1))) - - def createInfrastructure( - jwtTimestampLeeway: Option[JwtTimestampLeeway], - tokenExpiryGracePeriodForStreams: Option[Duration], - userRightsCheckIntervalInSeconds: Int = 10, - getExpiry: Option[AdjustableClock => Instant] = Some(_.instant.plusSeconds(1)), - ): ( - AdjustableClock, - ServerCallStreamObserver[Int], - ServerCallStreamObserver[Int], - Cancellable, - Option[Instant], - ) = { - val clock = AdjustableClock( - baseClock = Clock.fixed(Instant.now(), ZoneId.systemDefault()), - offset = Duration.ZERO, - ) - val delegate = mock[ServerCallStreamObserver[Int]] - val mockScheduler = mock[Scheduler] - // Set scheduler to do nothing - val cancellableMock = mock[Cancellable] - when( - mockScheduler.scheduleWithFixedDelay(any[FiniteDuration], any[FiniteDuration])( - any[Runnable] - )( - any[ExecutionContext] - ) - ).thenReturn(cancellableMock) - val expiration = getExpiry.map(_.apply(clock)) - val tested = UserBasedOngoingAuthorization( - observer = delegate, - originalClaims = ClaimSet.Claims.Empty.copy( - resolvedFromUser = true, - userId = Some("some_user_id"), - // the expiration claim will be invalid in the next second - expiration = expiration, - ), - nowF = () => clock.instant, - userManagementStore = mock[UserManagementStore], - userRightsCheckIntervalInSeconds = userRightsCheckIntervalInSeconds, - pekkoScheduler = mockScheduler, - jwtTimestampLeeway = jwtTimestampLeeway, - tokenExpiryGracePeriodForStreams = tokenExpiryGracePeriodForStreams, - loggerFactory = loggerFactory, - )(executionContext, traceContext) - (clock, delegate, tested, cancellableMock, expiration) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/services/ApiServicesRequiredClaimSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/services/ApiServicesRequiredClaimSpec.scala deleted file mode 100644 index fc89ffaecb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/auth/services/ApiServicesRequiredClaimSpec.scala +++ /dev/null @@ -1,755 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.auth.services - -import com.daml.ledger.api.v2.admin.party_management_service.{ - PartyDetails, - UpdatePartyDetailsRequest, -} -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamRequest -import com.daml.ledger.api.v2.command_service.SubmitAndWaitForTransactionRequest -import com.daml.ledger.api.v2.commands.Commands -import com.daml.ledger.api.v2.interactive.interactive_submission_service.HashingSchemeVersion.HASHING_SCHEME_VERSION_V2 -import com.daml.ledger.api.v2.interactive.interactive_submission_service.Metadata.SubmitterInfo -import com.daml.ledger.api.v2.interactive.interactive_submission_service.{ - ExecuteSubmissionAndWaitForTransactionRequest, - ExecuteSubmissionRequest, - Metadata, - PrepareSubmissionRequest, - PreparedTransaction, -} -import com.daml.ledger.api.v2.state_service.GetActiveContractsRequest -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA -import com.daml.ledger.api.v2.transaction_filter.{ - EventFormat, - Filters, - ParticipantAuthorizationTopologyFormat, - TopologyFormat, - TransactionFormat, - UpdateFormat, -} -import com.daml.ledger.api.v2.update_service.GetUpdatesRequest -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.auth.RequiredClaim -import com.digitalasset.canton.ledger.api.auth.RequiredClaims -import com.digitalasset.canton.ledger.api.auth.services.ApiServicesRequiredClaimSpec.{ - executeSubmissionAndWaitForTransactionRequest, - executeSubmissionRequest, - prepareSubmissionRequest, - submitAndWaitForTransactionRequest, -} -import com.digitalasset.canton.serialization.ProtoConverter -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import scalapb.lenses.Lens - -import java.util.UUID - -class ApiServicesRequiredClaimSpec extends AsyncFlatSpec with BaseTest with Matchers { - - behavior of "CommandCompletionServiceAuthorization.completionStreamClaims" - - it should "compute the correct claims in the happy path" in { - val result = CommandCompletionServiceAuthorization.completionStreamClaims( - CompletionStreamRequest( - userId = "qwe", - parties = Seq("a", "b", "c"), - beginExclusive = 1234L, - ) - ) - result should have size (4) - result.collect(readAs) should contain theSameElementsAs List( - RequiredClaim.ReadAs[CompletionStreamRequest]("a"), - RequiredClaim.ReadAs[CompletionStreamRequest]("b"), - RequiredClaim.ReadAs[CompletionStreamRequest]("c"), - ) - result - .collectFirst(matchUserId) - .value - .skipUserIdValidationForAnyPartyReaders shouldBe true - } - - it should "compute the correct claims if empty parties" in { - val result = CommandCompletionServiceAuthorization.completionStreamClaims( - CompletionStreamRequest( - userId = "qwe", - parties = Nil, - beginExclusive = 1234L, - ) - ) - result should have size (1) - result.collect(readAs) shouldBe Nil - result - .collectFirst(matchUserId) - .value - .skipUserIdValidationForAnyPartyReaders shouldBe true - } - - behavior of "PartyManagementServiceAuthorization.updatePartyDetailsClaims" - - it should "compute the correct claims in the happy path" in { - val result = PartyManagementServiceAuthorization.updatePartyDetailsClaims( - UpdatePartyDetailsRequest( - partyDetails = Some( - PartyDetails( - party = "abc", - isLocal = true, - localMetadata = None, - identityProviderId = "ABC", - ) - ), - updateMask = None, - ) - ) - result should have size (2) - result.collectFirst(adminOrIdp).isDefined shouldBe true - result.collectFirst(matchIdentityProviderId).isDefined shouldBe true - } - - it should "compute the correct claims if no party details provided" in { - val result = PartyManagementServiceAuthorization.updatePartyDetailsClaims( - UpdatePartyDetailsRequest( - partyDetails = None, - updateMask = None, - ) - ) - result should have size (1) - result.collectFirst(adminOrIdp).isDefined shouldBe true - } - - behavior of "StateServiceAuthorization.getActiveContractsClaims" - - it should "compute the correct claims in the happy path" in { - StateServiceAuthorization.getActiveContractsClaims( - GetActiveContractsRequest( - activeAtOffset = 15, - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - streamContinuationToken = None, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAsAnyParty(), - ) - } - - it should "compute the correct claims if no filtersForAnyParty" in { - StateServiceAuthorization.getActiveContractsClaims( - GetActiveContractsRequest( - activeAtOffset = 15, - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - "c" -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = true, - ) - ), - streamContinuationToken = None, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - ) - } - - it should "compute the correct claims if no filtersByParty" in { - StateServiceAuthorization.getActiveContractsClaims( - GetActiveContractsRequest( - activeAtOffset = 15, - eventFormat = Some( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = true, - ) - ), - streamContinuationToken = None, - ) - ) shouldBe Nil - } - - it should "compute the correct claims if no eventFormat" in { - StateServiceAuthorization.getActiveContractsClaims( - GetActiveContractsRequest( - activeAtOffset = 15, - eventFormat = None, - streamContinuationToken = None, - ) - ) shouldBe Nil - } - - behavior of "InteractiveSubmissionServiceAuthorization.prepareSubmission" - - it should "compute the correct claims in the happy path" in { - InteractiveSubmissionServiceAuthorization.getPreparedSubmissionClaims( - prepareSubmissionRequest - ) should contain theSameElementsAs RequiredClaims[PrepareSubmissionRequest]( - RequiredClaim.ReadAs("1"), - RequiredClaim.ReadAs("2"), - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.MatchUserId( - InteractiveSubmissionServiceAuthorization.userIdForPrepareSubmissionL - ), - ) - } - - behavior of "InteractiveSubmissionServiceAuthorization.executeSubmission" - - it should "compute the correct claims in the happy path" in { - InteractiveSubmissionServiceAuthorization.getExecuteSubmissionClaims( - executeSubmissionRequest, - InteractiveSubmissionServiceAuthorization.preparedTransactionForExecuteSubmissionL, - InteractiveSubmissionServiceAuthorization.userIdForExecuteSubmissionL, - ) should contain theSameElementsAs RequiredClaims[ExecuteSubmissionRequest]( - RequiredClaim.ExecuteAs("1"), - RequiredClaim.ExecuteAs("2"), - RequiredClaim.MatchUserId( - InteractiveSubmissionServiceAuthorization.userIdForExecuteSubmissionL - ), - ) - } - - behavior of "InteractiveSubmissionServiceAuthorization.executeSubmissionAndWaitForTransaction" - - it should "compute the correct claims in the happy path" in { - InteractiveSubmissionServiceAuthorization.getExecuteSubmissionAndWaitForTransactionClaims( - executeSubmissionAndWaitForTransactionRequest - ) should contain theSameElementsAs RequiredClaims[ - ExecuteSubmissionAndWaitForTransactionRequest - ]( - RequiredClaim.ExecuteAs("1"), - RequiredClaim.ExecuteAs("2"), - RequiredClaim.ReadAs("i"), - RequiredClaim.ReadAs("ii"), - RequiredClaim.ReadAsAnyParty(), - RequiredClaim.MatchUserId( - InteractiveSubmissionServiceAuthorization.userIdForExecuteSubmissionAndWaitForTransactionL - ), - ) - } - - behavior of "CommandServiceAuthorization.getSubmitAndWaitForTransactionClaims" - - it should "compute the correct claims in the happy path" in { - CommandServiceAuthorization.getSubmitAndWaitForTransactionClaims( - submitAndWaitForTransactionRequest - ) should contain theSameElementsAs RequiredClaims[SubmitAndWaitForTransactionRequest]( - RequiredClaim.ActAs("1"), - RequiredClaim.ActAs("2"), - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("i"), - RequiredClaim.ReadAs("ii"), - RequiredClaim.ReadAsAnyParty(), - RequiredClaim.MatchUserId(CommandServiceAuthorization.userIdForTransactionL), - ) - } - - it should "compute the correct claims if no filtersForAnyParty" in { - CommandServiceAuthorization.getSubmitAndWaitForTransactionClaims( - submitAndWaitForTransactionRequest.update( - _.transactionFormat.eventFormat.modify(_.clearFiltersForAnyParty) - ) - ) should contain theSameElementsAs RequiredClaims[SubmitAndWaitForTransactionRequest]( - RequiredClaim.ActAs("1"), - RequiredClaim.ActAs("2"), - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("i"), - RequiredClaim.ReadAs("ii"), - RequiredClaim.MatchUserId(CommandServiceAuthorization.userIdForTransactionL), - ) - } - - it should "compute the correct claims if no filtersByParty" in { - CommandServiceAuthorization.getSubmitAndWaitForTransactionClaims( - submitAndWaitForTransactionRequest.update( - _.transactionFormat.eventFormat.modify(_.clearFiltersByParty) - ) - ) should contain theSameElementsAs RequiredClaims[SubmitAndWaitForTransactionRequest]( - RequiredClaim.ActAs("1"), - RequiredClaim.ActAs("2"), - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAsAnyParty(), - RequiredClaim.MatchUserId(CommandServiceAuthorization.userIdForTransactionL), - ) - } - - it should "compute the correct claims if no transactionFormat" in { - CommandServiceAuthorization.getSubmitAndWaitForTransactionClaims( - submitAndWaitForTransactionRequest.clearTransactionFormat - ) should contain theSameElementsAs RequiredClaims[SubmitAndWaitForTransactionRequest]( - RequiredClaim.ActAs("1"), - RequiredClaim.ActAs("2"), - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.MatchUserId(CommandServiceAuthorization.userIdForTransactionL), - ) - } - - it should "compute the correct claims if no actAs" in { - CommandServiceAuthorization.getSubmitAndWaitForTransactionClaims( - submitAndWaitForTransactionRequest.update(_.commands.modify(_.clearActAs)) - ) should contain theSameElementsAs RequiredClaims[SubmitAndWaitForTransactionRequest]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("i"), - RequiredClaim.ReadAs("ii"), - RequiredClaim.ReadAsAnyParty(), - RequiredClaim.MatchUserId(CommandServiceAuthorization.userIdForTransactionL), - ) - } - - it should "compute the correct claims if no readAs" in { - CommandServiceAuthorization.getSubmitAndWaitForTransactionClaims( - submitAndWaitForTransactionRequest.update(_.commands.modify(_.clearReadAs)) - ) should contain theSameElementsAs RequiredClaims[SubmitAndWaitForTransactionRequest]( - RequiredClaim.ActAs("1"), - RequiredClaim.ActAs("2"), - RequiredClaim.ReadAs("i"), - RequiredClaim.ReadAs("ii"), - RequiredClaim.ReadAsAnyParty(), - RequiredClaim.MatchUserId(CommandServiceAuthorization.userIdForTransactionL), - ) - } - - it should "compute the correct claims if no actAs and no readAs" in { - CommandServiceAuthorization.getSubmitAndWaitForTransactionClaims( - submitAndWaitForTransactionRequest.update( - _.commands.modify(_.clearReadAs), - _.commands.modify(_.clearActAs), - ) - ) should contain theSameElementsAs RequiredClaims[SubmitAndWaitForTransactionRequest]( - RequiredClaim.ReadAs("i"), - RequiredClaim.ReadAs("ii"), - RequiredClaim.ReadAsAnyParty(), - RequiredClaim.MatchUserId(CommandServiceAuthorization.userIdForTransactionL), - ) - } - - behavior of "UpdateServiceAuthorization.getUpdatesClaims" - - it should "compute the correct claims in the happy path" in { - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map( - "c" -> Filters(Nil), - "d" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - includeTopologyEvents = Some( - TopologyFormat(Some(ParticipantAuthorizationTopologyFormat(parties = Seq("e", "f")))) - ), - ) - ), - descendingOrder = false, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAs("d"), - RequiredClaim.ReadAs("e"), - RequiredClaim.ReadAs("f"), - RequiredClaim.ReadAsAnyParty(), - ) - } - - it should "compute the correct claims if no party wildcards exist" in { - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "a" -> Filters(Nil), - "b" -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map( - "c" -> Filters(Nil), - "d" -> Filters(Nil), - ), - filtersForAnyParty = None, - verbose = true, - ) - ), - includeTopologyEvents = Some( - TopologyFormat(Some(ParticipantAuthorizationTopologyFormat(parties = Seq("e", "f")))) - ), - ) - ), - descendingOrder = false, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAs("a"), - RequiredClaim.ReadAs("b"), - RequiredClaim.ReadAs("c"), - RequiredClaim.ReadAs("d"), - RequiredClaim.ReadAs("e"), - RequiredClaim.ReadAs("f"), - ) - } - - it should "compute the correct claims if no filtersByParty in transactions and reassignments exists" in { - val eventFormatO = Some( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = true, - ) - ) - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = eventFormatO, - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = eventFormatO, - includeTopologyEvents = Some( - TopologyFormat(Some(ParticipantAuthorizationTopologyFormat(parties = Seq("e", "f")))) - ), - ) - ), - descendingOrder = false, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAs("e"), - RequiredClaim.ReadAs("f"), - ) - } - - it should "compute the correct claims if no filtersByParty in transactions and reassignments exists and topology format is empty" in { - val eventFormatO = Some( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = true, - ) - ) - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = eventFormatO, - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = eventFormatO, - includeTopologyEvents = Some( - TopologyFormat(None) - ), - ) - ), - descendingOrder = false, - ) - ) shouldBe Nil - } - - it should "compute the correct claims if no updateFormat exists" in { - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = None, - descendingOrder = false, - ) - ) shouldBe Nil - } - - it should "compute the correct claims for topology format without wildcard" in { - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = Some( - UpdateFormat( - includeTransactions = None, - includeReassignments = None, - includeTopologyEvents = Some( - TopologyFormat(Some(ParticipantAuthorizationTopologyFormat(parties = Seq("e", "f")))) - ), - ) - ), - descendingOrder = false, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAs("e"), - RequiredClaim.ReadAs("f"), - ) - } - - it should "compute the correct claims for transactions format with wildcard" in { - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(Filters(Nil)), - verbose = false, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = None, - includeTopologyEvents = None, - ) - ), - descendingOrder = false, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAsAnyParty() - ) - } - - it should "compute the correct claims for reassignments with wildcard" in { - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = Some( - UpdateFormat( - includeReassignments = Some( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(Filters(Nil)), - verbose = false, - ) - ), - includeTransactions = None, - includeTopologyEvents = None, - ) - ), - descendingOrder = false, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAsAnyParty() - ) - } - - it should "compute the correct claims for topology format with wildcard" in { - UpdateServiceAuthorization.getUpdatesClaims( - GetUpdatesRequest( - beginExclusive = 10, - endInclusive = Some(15), - updateFormat = Some( - UpdateFormat( - includeTransactions = None, - includeReassignments = None, - includeTopologyEvents = Some( - TopologyFormat(Some(ParticipantAuthorizationTopologyFormat(parties = Seq.empty))) - ), - ) - ), - descendingOrder = false, - ) - ) should contain theSameElementsAs RequiredClaims[GetActiveContractsRequest]( - RequiredClaim.ReadAsAnyParty() - ) - } - - behavior of "UserManagementServiceAuthorization.userReaderClaims" - - it should "compute the correct claims in the happy path" in { - val userIdL = Lens.unit[String] - val identityProviderIdL = Lens.unit[String] - UserManagementServiceAuthorization.userReaderClaims[String]( - userIdL = userIdL, - identityProviderIdL = identityProviderIdL, - ) should contain theSameElementsAs RequiredClaims[String]( - RequiredClaim.MatchUserIdForUserManagement(userIdL), - RequiredClaim.MatchIdentityProviderId(identityProviderIdL), - ) - } - - def readAs[Req]: PartialFunction[RequiredClaim[Req], RequiredClaim.ReadAs[Req]] = { - case readAs: RequiredClaim.ReadAs[Req] => readAs - } - - def admin[Req]: PartialFunction[RequiredClaim[Req], RequiredClaim.Admin[Req]] = { - case admin: RequiredClaim.Admin[Req] => admin - } - - def adminOrIdp[Req]: PartialFunction[RequiredClaim[Req], RequiredClaim.AdminOrIdpAdmin[Req]] = { - case adminOrIdp: RequiredClaim.AdminOrIdpAdmin[Req] => adminOrIdp - } - - def matchUserId[Req]: PartialFunction[RequiredClaim[Req], RequiredClaim.MatchUserId[Req]] = { - case matchUserId: RequiredClaim.MatchUserId[Req] => matchUserId - } - - def matchIdentityProviderId[Req] - : PartialFunction[RequiredClaim[Req], RequiredClaim.MatchIdentityProviderId[Req]] = { - case matchIdentityProviderId: RequiredClaim.MatchIdentityProviderId[Req] => - matchIdentityProviderId - } -} -object ApiServicesRequiredClaimSpec { - val transactionFormat = TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map( - "i" -> Filters(Nil), - "ii" -> Filters(Nil), - ), - filtersForAnyParty = Some(Filters(Nil)), - verbose = true, - ) - ), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - val submitAndWaitForTransactionRequest = - SubmitAndWaitForTransactionRequest( - commands = Some( - Commands.defaultInstance.copy( - actAs = Seq("1", "2"), - readAs = Seq("a", "b"), - userId = "userId", - ) - ), - transactionFormat = Some(transactionFormat), - ) - - val prepareSubmissionRequest = - PrepareSubmissionRequest( - userId = "userId", - commandId = "commandId", - commands = List.empty, - minLedgerTime = None, - actAs = Seq("1", "2"), - readAs = Seq("a", "b"), - disclosedContracts = Seq.empty, - synchronizerId = "", - packageIdSelectionPreference = Seq.empty, - verboseHashing = true, - prefetchContractKeys = Seq.empty, - maxRecordTime = Option.empty, - estimateTrafficCost = None, - tapsMaxPasses = None, - hashingSchemeVersion = None, - ) - - val preparedTransaction = PreparedTransaction( - transaction = None, - metadata = Some( - Metadata( - submitterInfo = Some( - SubmitterInfo( - actAs = Seq("1", "2"), - commandId = "commandId", - ) - ), - synchronizerId = "synchronizerId", - mediatorGroup = 0, - transactionUuid = UUID.randomUUID().toString, - preparationTime = 0, - inputContracts = Seq.empty, - minLedgerEffectiveTime = None, - maxLedgerEffectiveTime = None, - globalKeyMapping = Seq.empty, - maxRecordTime = Option.empty, - ) - ), - ) - - val executeSubmissionRequest = - ExecuteSubmissionRequest( - preparedTransaction = Some(preparedTransaction), - partySignatures = None, - deduplicationPeriod = ExecuteSubmissionRequest.DeduplicationPeriod.DeduplicationDuration( - ProtoConverter.DurationConverter.toProtoPrimitive(java.time.Duration.ofSeconds(1)) - ), - submissionId = "submissionId", - userId = "userId", - hashingSchemeVersion = HASHING_SCHEME_VERSION_V2, - minLedgerTime = None, - ) - - val executeSubmissionAndWaitForTransactionRequest = - ExecuteSubmissionAndWaitForTransactionRequest( - preparedTransaction = Some(preparedTransaction), - partySignatures = None, - deduplicationPeriod = - ExecuteSubmissionAndWaitForTransactionRequest.DeduplicationPeriod.DeduplicationDuration( - ProtoConverter.DurationConverter.toProtoPrimitive(java.time.Duration.ofSeconds(1)) - ), - submissionId = "submissionId", - userId = "userId", - hashingSchemeVersion = HASHING_SCHEME_VERSION_V2, - minLedgerTime = None, - transactionFormat = Some(transactionFormat), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/DropRepeatedSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/DropRepeatedSpec.scala deleted file mode 100644 index 63a56c2520..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/DropRepeatedSpec.scala +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.grpc - -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.pattern.pipe -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.apache.pekko.testkit.{TestKit, TestProbe} -import org.scalatest.BeforeAndAfterAll -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike - -import scala.collection.immutable -import scala.concurrent.ExecutionContext - -final class DropRepeatedSpec - extends TestKit(ActorSystem(classOf[DropRepeatedSpec].getSimpleName)) - with AnyWordSpecLike - with Matchers - with BeforeAndAfterAll { - - private[this] implicit val materializer: Materializer = Materializer(system) - private[this] implicit val executionContext: ExecutionContext = materializer.executionContext - - override def afterAll(): Unit = - TestKit.shutdownActorSystem(system) - - "DropRepeated" should { - "drop repeated elements" in { - val probe = TestProbe() - val input = immutable.Seq(1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5) - - val _ = Source(input) - .via(DropRepeated()) - .runWith(Sink.seq) - .pipeTo(probe.ref) - .failed - .foreach(fail(_)) - - probe.expectMsg(Vector(1, 2, 3, 4, 5)) - } - - "does not drop duplicate elements that are not repeated" in { - val probe = TestProbe() - val input = immutable.Seq(1, 1, 2, 2, 1, 1, 2, 2) - - val _ = Source(input) - .via(DropRepeated()) - .runWith(Sink.seq) - .pipeTo(probe.ref) - .failed - .foreach(fail(_)) - - probe.expectMsg(Vector(1, 2, 1, 2)) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/GrpcClientResource.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/GrpcClientResource.scala deleted file mode 100644 index 343de579ce..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/GrpcClientResource.scala +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.grpc - -import com.daml.ledger.resources.ResourceOwner -import com.daml.ports.Port -import io.grpc.Channel -import io.grpc.netty.shaded.io.grpc.netty.{NegotiationType, NettyChannelBuilder} -import io.grpc.netty.shaded.io.netty.channel.EventLoopGroup -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext -import io.grpc.netty.shaded.io.netty.util.concurrent.DefaultThreadFactory - -import java.net.{InetAddress, InetSocketAddress} -import java.util.UUID -import scala.concurrent.duration.DurationInt - -object GrpcClientResource { - def owner(port: Port, sslContext: Option[SslContext] = None): ResourceOwner[Channel] = { - val threadFactoryName = s"api-client-grpc-event-loop-${UUID.randomUUID()}" - val threadFactory = new DefaultThreadFactory(threadFactoryName, true) - val threadCount = sys.runtime.availableProcessors() - for { - eventLoopGroup <- ResourceOwner.forEventLoopGroup(threadCount, threadFactory) - channelBuilder = makeChannelBuilder(port, eventLoopGroup, sslContext) - channel <- ResourceOwner.forChannel(channelBuilder, shutdownTimeout = 5.seconds) - } yield channel - } - - private def makeChannelBuilder( - port: Port, - eventLoopGroup: EventLoopGroup, - sslContext: Option[SslContext], - ): NettyChannelBuilder = { - val builder = - NettyChannelBuilder - .forAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, port.value)) - .channelType(ResourceOwner.EventLoopGroupChannelType) - .eventLoopGroup(eventLoopGroup) - .directExecutor() - - sslContext - .fold(builder.usePlaintext())( - builder.sslContext(_).negotiationType(NegotiationType.TLS) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/GrpcHealthServiceSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/GrpcHealthServiceSpec.scala deleted file mode 100644 index 08566653fb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/grpc/GrpcHealthServiceSpec.scala +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.grpc - -import com.daml.grpc.GrpcException -import com.daml.grpc.adapter.server.rs.MockServerCallStreamObserver -import com.daml.scalautil.Statement.discard -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.health.{ - HealthChecks, - HealthStatus, - Healthy, - ReportsHealth, - Unhealthy, -} -import com.digitalasset.canton.ledger.api.grpc.GrpcHealthService.* -import com.digitalasset.canton.ledger.api.grpc.GrpcHealthServiceSpec.* -import io.grpc.health.v1.health.{HealthCheckRequest, HealthCheckResponse} -import org.scalatest.concurrent.Eventually -import org.scalatest.time.{Second, Span} -import org.scalatest.wordspec.AsyncWordSpec - -import scala.concurrent.duration.DurationInt - -final class GrpcHealthServiceSpec - extends AsyncWordSpec - with Eventually - with PekkoBeforeAndAfterAll - with BaseTest { - - implicit override val patienceConfig: PatienceConfig = - PatienceConfig(timeout = scaled(Span(1, Second))) - - "HealthService" should { - "report SERVING if there are no health checks" in { - val service = new GrpcHealthService( - new HealthChecks, - loggerFactory = loggerFactory, - ) - - for { - response <- service.check(HealthCheckRequest()) - } yield { - response should be(servingResponse) - } - } - - "report SERVING if there is one healthy check" in { - val service = new GrpcHealthService( - new HealthChecks("component" -> healthyComponent), - loggerFactory = loggerFactory, - ) - - for { - response <- service.check(HealthCheckRequest()) - } yield { - response should be(servingResponse) - } - } - - "report NOT_SERVING if there is one unhealthy check" in { - val service = new GrpcHealthService( - new HealthChecks("component" -> unhealthyComponent), - loggerFactory = loggerFactory, - ) - - for { - response <- service.check(HealthCheckRequest()) - } yield { - response should be(notServingResponse) - } - } - - "report SERVING if all checks are healthy" in { - val service = new GrpcHealthService( - new HealthChecks( - "component A" -> healthyComponent, - "component B" -> healthyComponent, - "component C" -> healthyComponent, - ), - loggerFactory = loggerFactory, - ) - - discard(service.check(HealthCheckRequest())) - for { - response <- service.check(HealthCheckRequest()) - } yield { - response should be(servingResponse) - } - } - - "report NOT_SERVING if a single check is unhealthy" in { - val service = new GrpcHealthService( - new HealthChecks( - "component A" -> healthyComponent, - "component B" -> unhealthyComponent, - "component C" -> healthyComponent, - ), - loggerFactory = loggerFactory, - ) - - for { - response <- service.check(HealthCheckRequest()) - } yield { - response should be(notServingResponse) - } - } - - "report SERVING when querying a single, healthy component" in { - val service = new GrpcHealthService( - new HealthChecks("component" -> healthyComponent), - loggerFactory = loggerFactory, - ) - - for { - response <- service.check(HealthCheckRequest("component")) - } yield { - response should be(servingResponse) - } - } - - "report NOT_SERVING when querying a single, unhealthy component" in { - val service = new GrpcHealthService( - new HealthChecks("component" -> unhealthyComponent), - loggerFactory = loggerFactory, - ) - - for { - response <- service.check(HealthCheckRequest("component")) - } yield { - response should be(notServingResponse) - } - } - - "report SERVING when querying a healthy component alongside other, unhealthy components" in { - val service = new GrpcHealthService( - new HealthChecks( - "component A" -> healthyComponent, - "component B" -> healthyComponent, - "component C" -> unhealthyComponent, - ), - loggerFactory = loggerFactory, - ) - - for { - response <- service.check(HealthCheckRequest("component B")) - } yield { - response should be(servingResponse) - } - } - - "report NOT_SERVING when querying an unhealthy component alongside other, healthy components" in { - val service = new GrpcHealthService( - new HealthChecks( - "component A" -> unhealthyComponent, - "component B" -> healthyComponent, - "component C" -> healthyComponent, - ), - loggerFactory = loggerFactory, - ) - - for { - response <- service.check(HealthCheckRequest("component A")) - } yield { - response should be(notServingResponse) - } - } - - "observe changes in health" in { - val responseObserver = new MockServerCallStreamObserver[HealthCheckResponse] - - var componentAHealth: HealthStatus = Healthy - var componentBHealth: HealthStatus = Healthy - var componentCHealth: HealthStatus = Healthy - val service = new GrpcHealthService( - new HealthChecks( - "component A" -> componentWithHealthBackedBy(() => componentAHealth), - "component B" -> componentWithHealthBackedBy(() => componentBHealth), - "component C" -> componentWithHealthBackedBy(() => componentCHealth), - ), - loggerFactory = loggerFactory, - maximumWatchFrequency = 1.millisecond, - ) - - service.watch(HealthCheckRequest(), responseObserver) - responseObserver.demandResponse(count = 5) - - eventually { - responseObserver.elements should be(Vector(servingResponse)) - } - - componentBHealth = Unhealthy - eventually { - responseObserver.elements should be(Vector(servingResponse, notServingResponse)) - } - - componentBHealth = Healthy - eventually { - responseObserver.elements should be( - Vector(servingResponse, notServingResponse, servingResponse) - ) - } - - componentAHealth = Unhealthy - eventually { - responseObserver.elements should be( - Vector(servingResponse, notServingResponse, servingResponse, notServingResponse) - ) - } - - // this won't emit a new response, because the overall health of the system didn't change. - componentCHealth = Unhealthy - eventually { - responseObserver.elements should be( - Vector(servingResponse, notServingResponse, servingResponse, notServingResponse) - ) - } - - componentCHealth = Healthy - componentAHealth = Healthy - eventually { - responseObserver.elements should be( - Vector( - servingResponse, - notServingResponse, - servingResponse, - notServingResponse, - servingResponse, - ) - ) - } - succeed - } - - "observe changes in a single component's health" in { - val responseObserver = new MockServerCallStreamObserver[HealthCheckResponse] - - var componentAHealth: HealthStatus = Healthy - var componentBHealth: HealthStatus = Healthy - var componentCHealth: HealthStatus = Healthy - val service = new GrpcHealthService( - new HealthChecks( - "component A" -> componentWithHealthBackedBy(() => componentAHealth), - "component B" -> componentWithHealthBackedBy(() => componentBHealth), - "component C" -> componentWithHealthBackedBy(() => componentCHealth), - ), - loggerFactory = loggerFactory, - maximumWatchFrequency = 1.millisecond, - ) - - service.watch(HealthCheckRequest("component C"), responseObserver) - responseObserver.demandResponse(count = 3) - - eventually { - responseObserver.elements should be(Vector(servingResponse)) - } - - // this component won't affect the health of component C - componentBHealth = Unhealthy - eventually { - responseObserver.elements should be(Vector(servingResponse)) - } - - // this component won't affect the health of component C - componentBHealth = Healthy - eventually { - responseObserver.elements should be(Vector(servingResponse)) - } - - // this component won't affect the health of component C - componentAHealth = Unhealthy - eventually { - responseObserver.elements should be(Vector(servingResponse)) - } - - componentCHealth = Unhealthy - eventually { - responseObserver.elements should be(Vector(servingResponse, notServingResponse)) - } - - componentCHealth = Healthy - eventually { - responseObserver.elements should be( - Vector(servingResponse, notServingResponse, servingResponse) - ) - } - - // this component won't affect the health of component C - componentAHealth = Healthy - eventually { - responseObserver.elements should be( - Vector(servingResponse, notServingResponse, servingResponse) - ) - } - succeed - } - } - - "fail gracefully when a non-existent component is checked" in { - val service = new GrpcHealthService( - new HealthChecks("component" -> unhealthyComponent), - loggerFactory = loggerFactory, - ) - - service.check(HealthCheckRequest("another component")).failed.map(assertErrorCode) - } - - "fail gracefully when a non-existent component is watched" in { - val responseObserver = new MockServerCallStreamObserver[HealthCheckResponse] - val service = new GrpcHealthService( - new HealthChecks("component" -> unhealthyComponent), - loggerFactory = loggerFactory, - ) - - service.watch(HealthCheckRequest("another component"), responseObserver) - responseObserver.demandResponse() - - responseObserver.completionFuture.failed.map(assertErrorCode) - } - - private def assertErrorCode(throwable: Throwable) = - throwable match { - case GrpcException.NOT_FOUND() => succeed - case ex => fail(s"Expected a NOT_FOUND error, but got $ex") - } -} - -object GrpcHealthServiceSpec { - private val healthyComponent: ReportsHealth = () => Healthy - - private val unhealthyComponent: ReportsHealth = () => Unhealthy - - private def componentWithHealthBackedBy(fetchCurrentHealth: () => HealthStatus): ReportsHealth = - () => fetchCurrentHealth() -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/messages/state/AcsContinuationTokenTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/messages/state/AcsContinuationTokenTest.scala deleted file mode 100644 index 975e20e2d1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/messages/state/AcsContinuationTokenTest.scala +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.state - -import com.daml.ledger.api.v2.state_service.GetActiveContractsRequest -import com.daml.ledger.api.v2.transaction_filter.{EventFormat, Filters} -import com.digitalasset.canton.ledger.api.messages.state.AcsContinuationToken.Checksum -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import com.google.protobuf.ByteString -import org.scalatest.EitherValues -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.collection.immutable.{HashMap, TreeMap} - -class AcsContinuationTokenTest extends AnyFlatSpec with Matchers with EitherValues { - private val eventFormat = EventFormat( - filtersByParty = Map("party" -> Filters(Nil)), - filtersForAnyParty = None, - verbose = false, - ) - private val originalRequest = GetActiveContractsRequest(125, Some(eventFormat), None) - private val participantId = Ref.ParticipantId.assertFromString("Ledger") - private val originalRequestChecksum = - AcsContinuationToken.calcChecksum(originalRequest, participantId) - private val sequentialId = 42L - private val originalPointer = AcsContinuationPointerActiveContracts(sequentialId) - private val encodedToken = - AcsContinuationToken.activeContracts(sequentialId, originalRequestChecksum) - - private implicit val elc: ErrorLoggingContext = - ErrorLoggingContext.fromTracedLogger(NamedLogging.noopLogger)(TraceContext.empty) - - "AcsContinuationToken" should "successfully decode an untampered token from the request" in { - val checksum = AcsContinuationToken.calcChecksum(originalRequest, participantId) - val decodedToken = - AcsContinuationToken.decodeAndValidate(checksum, encodedToken) - decodedToken.value should equal(originalPointer) - } - - it should "fail if used on a different participant" in { - val differentParticipantId = Ref.ParticipantId.assertFromString("different") - val checksum = AcsContinuationToken.calcChecksum(originalRequest, differentParticipantId) - val decodedToken = - AcsContinuationToken.decodeAndValidate(checksum, encodedToken) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_CONTINUATION_TOKEN(8,0): The submitted command contains an invalid continuation token. Tokens used in ACS " + - "requests must be taken from a valid GetActiveContractsResponse and used with the same EventFormat settings, " + - "with the same Canton participant running the same Canton version." - ) - } - - it should "fail if not the same eventformat is used" in { - val requestWithToken = originalRequest.copy( - eventFormat = originalRequest.eventFormat.map(_.copy(verbose = true)), - streamContinuationToken = Some(encodedToken), - ) - val checksum = AcsContinuationToken.calcChecksum(requestWithToken, participantId) - val decodedToken = - AcsContinuationToken.decodeAndValidate(checksum, encodedToken) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_CONTINUATION_TOKEN(8,0): The submitted command contains an invalid continuation token. Tokens used in " + - "ACS requests must be taken from a valid GetActiveContractsResponse and used with the same EventFormat settings, " + - "with the same Canton participant running the same Canton version." - ) - } - - it should "fail if not the same activeAt is used" in { - val requestWithToken = originalRequest.copy( - activeAtOffset = originalRequest.activeAtOffset + 1, - streamContinuationToken = Some(encodedToken), - ) - val checksum = AcsContinuationToken.calcChecksum(requestWithToken, participantId) - val decodedToken = - AcsContinuationToken.decodeAndValidate(checksum, encodedToken) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_CONTINUATION_TOKEN(8,0): The submitted command contains an invalid continuation token. Tokens used in " + - "ACS requests must be taken from a valid GetActiveContractsResponse and used with the same EventFormat settings, " + - "with the same Canton participant running the same Canton version." - ) - } - - it should "reject an invalid token" in { - val tamperedToken = encodedToken.substring(5) - val checksum = AcsContinuationToken.calcChecksum(originalRequest, participantId) - val decodedToken = - AcsContinuationToken.decodeAndValidate(checksum, tamperedToken) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field stream_continuation_token: " + - "Invalid continuation token for GetActiveContractsRequest" - ) - } - - it should "calculate the checksum correctly for maps" in { - val eventFormatHashMap = EventFormat( - filtersByParty = - HashMap("party1" -> Filters(Nil), "party2" -> Filters(Nil), "party3" -> Filters(Nil)), - filtersForAnyParty = None, - verbose = false, - ) - val eventFormatTreeMap = EventFormat( - filtersByParty = - TreeMap("party1" -> Filters(Nil), "party2" -> Filters(Nil), "party3" -> Filters(Nil)), - filtersForAnyParty = None, - verbose = false, - ) - val originalRequest = GetActiveContractsRequest(125, Some(eventFormatHashMap), None) - val originalRequestChecksum = AcsContinuationToken.calcChecksum(originalRequest, participantId) - val originalPointer = AcsContinuationPointerActiveContracts(sequentialId) - val newToken = AcsContinuationToken.activeContracts(sequentialId, originalRequestChecksum) - val requestWithToken = originalRequest.copy( - eventFormat = Some(eventFormatTreeMap), - streamContinuationToken = Some(newToken), - ) - val checksum = AcsContinuationToken.calcChecksum(requestWithToken, participantId) - val decodedToken = - AcsContinuationToken.decodeAndValidate(checksum, newToken) - decodedToken.value should equal(originalPointer) - } - - it should "calculate the same checksum all the time" in { - originalRequestChecksum should equal( - Checksum(ByteString.copyFrom(Array[Byte](87, -103, -53, 56))) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/messages/state/AcsPageTokenTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/messages/state/AcsPageTokenTest.scala deleted file mode 100644 index 68fc70d949..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/messages/state/AcsPageTokenTest.scala +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.messages.state - -import com.daml.ledger.api.v2.state_service.GetActiveContractsPageRequest -import com.daml.ledger.api.v2.transaction_filter.{EventFormat, Filters} -import com.daml.platform.v1.acs_page_token.AcsPageTokenPayload -import com.digitalasset.canton.ledger.api.messages.state.AcsPageToken.{ - calcParticipantChecksum, - calcRequestChecksum, -} -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLogging} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import com.google.protobuf.ByteString -import org.scalatest.EitherValues -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class AcsPageTokenTest extends AnyFlatSpec with Matchers with EitherValues { - private val eventFormat = EventFormat( - filtersByParty = Map("party" -> Filters(Nil)), - filtersForAnyParty = None, - verbose = false, - ) - private val originalActiveAt = 100L - private val originalRequest = - GetActiveContractsPageRequest(Some(originalActiveAt), Some(eventFormat), Some(5), None) - private val participantId = Ref.ParticipantId.assertFromString("Ledger") - private val originalContinuationToken = - AcsContinuationToken.activeContracts(42, AcsContinuationToken.emptyChecksum) - private val originalToken = AcsPageToken.encode( - originalRequest, - originalContinuationToken, - activeAtOffset = originalActiveAt, - participantId, - ) - private val participantIdChecksum = AcsPageToken.calcParticipantChecksum(participantId) - private val originalRequestChecksum = AcsPageToken.calcRequestChecksum(originalRequest) - - private implicit val elc: ErrorLoggingContext = - ErrorLoggingContext.fromTracedLogger(NamedLogging.noopLogger)(TraceContext.empty) - - "AcsPageToken" should "successfully decode an untampered token from the request" in { - val decoded = - AcsPageToken.decodeAndValidate(originalRequestChecksum, participantIdChecksum, originalToken) - decoded.value._2 should equal(AcsContinuationPointerActiveContracts(42 - 1)) // decreased seq id - decoded.value._1.unwrap should equal(originalActiveAt) - } - - it should "fail if used on a different participant" in { - val differentParticipantId = Ref.ParticipantId.assertFromString("different") - val differentParticipantChecksum = AcsPageToken.calcParticipantChecksum(differentParticipantId) - val decodedToken = - AcsPageToken.decodeAndValidate( - originalRequestChecksum, - differentParticipantChecksum, - originalToken, - ) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_ACS_PAGE_TOKEN(8,0): The submitted command contains an invalid page token. Tokens used in " + - "ACS requests must be taken from a valid GetActiveContractsPageResponse and used with the same " + - "EventFormat settings, with the same Canton participant running the same Canton version. The page " + - "token was prepared by a different participant." - ) - } - - it should "fail if not the same eventformat is used" in { - val requestWithToken = originalRequest.copy( - eventFormat = originalRequest.eventFormat.map(_.copy(verbose = true)), - pageToken = Some(originalToken), - ) - val requestChecksum = AcsPageToken.calcRequestChecksum(requestWithToken) - val decodedToken = - AcsPageToken.decodeAndValidate(requestChecksum, participantIdChecksum, originalToken) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_ACS_PAGE_TOKEN(8,0): The submitted command contains an invalid page token. Tokens used in " + - "ACS requests must be taken from a valid GetActiveContractsPageResponse and used with the same " + - "EventFormat settings, with the same Canton participant running the same Canton version. The page " + - "token was prepared with different event_format or active_at_offset." - ) - } - - it should "fail if not the same activeAt is used" in { - val requestWithToken = originalRequest.copy( - activeAtOffset = originalRequest.activeAtOffset.map(_ + 1), - pageToken = Some(originalToken), - ) - val checksum = AcsPageToken.calcRequestChecksum(requestWithToken) - val decodedToken = - AcsPageToken.decodeAndValidate(checksum, participantIdChecksum, originalToken) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_ACS_PAGE_TOKEN(8,0): The submitted command contains an invalid page token. Tokens used in " + - "ACS requests must be taken from a valid GetActiveContractsPageResponse and used with the same " + - "EventFormat settings, with the same Canton participant running the same Canton version. The page " + - "token was prepared with different event_format or active_at_offset." - ) - } - - it should "reject an invalid token" in { - val tamperedToken = originalToken.substring(5) - val decodedToken = - AcsPageToken.decodeAndValidate(originalRequestChecksum, participantIdChecksum, tamperedToken) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field page_token: " + - "Invalid page token for GetActiveContractsPageRequest" - ) - } - - it should "reject a token with different version" in { - val tokenWithDifferentVersion = AcsPageTokenPayload( - continuationToken = originalContinuationToken, - activeAtOffset = originalActiveAt, - version = 999, - participantIdChecksum = calcParticipantChecksum(participantId), - requestChecksum = calcRequestChecksum(originalRequest), - ).toByteString - val decodedToken = - AcsPageToken.decodeAndValidate( - originalRequestChecksum, - participantIdChecksum, - tokenWithDifferentVersion, - ) - decodedToken.left.value.getStatus.getDescription should equal( - "INVALID_ACS_PAGE_TOKEN(8,0): The submitted command contains an invalid page token. Tokens used in " + - "ACS requests must be taken from a valid GetActiveContractsPageResponse and used with the same " + - "EventFormat settings, with the same Canton participant running the same Canton version. The page " + - "token was prepared with different page API version." - ) - } - - it should "calculate the same checksum all the time" in { - originalRequestChecksum should equal( - ByteString.copyFrom(Array[Byte](-51, 65, -123, 2)) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/util/TimestampConversionSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/util/TimestampConversionSpec.scala deleted file mode 100644 index a2126eba91..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/util/TimestampConversionSpec.scala +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.util - -import com.daml.ledger.api.v2.value.Value.Sum as VSum -import com.digitalasset.canton.ledger.api.util.TimestampConversion.* -import com.digitalasset.daml.lf.data.Time -import org.scalacheck.Prop.exists -import org.scalacheck.{Gen, Prop} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.scalacheck.{Checkers, ScalaCheckDrivenPropertyChecks} - -import java.time.Instant - -class TimestampConversionSpec - extends AnyWordSpec - with Matchers - with Checkers - with ScalaCheckDrivenPropertyChecks { - import TimestampConversionSpec.* - - "instantToMicros" when { - "given any instant without nanos" should { - // documenting a known fact, more than "desired behavior" - "overflow for some values" in { - def prop(i: Instant): Prop = microsToInstant(instantToMicros(i)) != i - check(prop(Instant parse "+959040998-10-20T14:33:31.896722Z") || exists(anyMicroTime)(prop)) - } - } - - "given any instant with nanos in specified range" should { - "throw when unrepresentable with micros" in { - def prop(i: Instant): Prop = - try { - instantToMicros(i) - false - } catch { - case _: IllegalArgumentException => true - } - check(prop(Instant parse "7758-07-09T19:42:21.246906214Z") || exists(anyTimeInRange)(prop)) - } - } - - "given any instant without nanos in specified range" should { - "be retracted by microsToInstant" in forAll(anyMicroInRange) { i => - microsToInstant(instantToMicros(i)) shouldBe i - } - - "treat truncated instants likewise" in forAll(anyTimeInRange) { i => - val it = i truncatedTo java.time.temporal.ChronoUnit.MICROS - microsToInstant(instantToMicros(it)) shouldBe it - } - } - } - - "microsToInstant" when { - "given any long value" should { - "be total" in forAll { (ts: VSum.Timestamp) => - microsToInstant(ts) shouldBe microsToInstant(ts) - } - - "be injective" in forAll { (ts1: VSum.Timestamp, ts2: VSum.Timestamp) => - whenever(ts1 != ts2) { - microsToInstant(ts1) should not be microsToInstant(ts2) - } - } - - // documenting a known fact, more than "desired behavior" - "overflow for some values" in { - def prop(ts: VSum.Timestamp): Prop = instantToMicros(microsToInstant(ts)) != ts - check(prop(VSum.Timestamp(-9223372036854775808L)) || exists(prop)) - } - } - - "given a value in specified range" should { - "be retracted by instantToMicros" in forAll(timestampInRangeGen) { ts => - instantToMicros(microsToInstant(ts)) shouldBe ts - } - } - } - - "fromLf" when { - "given a value in specified range" should { - "be retracted by toLf" in forAll(lfTimestampGen) { ts => - toLf(fromLf(ts), ConversionMode.Exact) shouldBe ts - } - } - } - - "toLf" when { - "given a valid microsecond timestamp" should { - "be retracted by fromLf" in forAll(anyMicroInRange) { ts => - val protoTs = fromInstant(ts) - fromLf(toLf(protoTs, ConversionMode.Exact)) shouldBe protoTs - } - } - - "given a valid nanosecond timestamp" should { - "round half up" in forAll(anyTimeInRange) { ts => - val protoTs = fromInstant(ts) - val halfUp = toLf(protoTs, ConversionMode.HalfUp) - halfUp.toInstant should be > ts.plusNanos(-500) - halfUp.toInstant should be <= ts.plusNanos(500) - } - } - } -} - -object TimestampConversionSpec { - import org.scalacheck.{Arbitrary, Shrink} - import Arbitrary.arbitrary - - val timestampGen: Gen[VSum.Timestamp] = arbitrary[Long] map VSum.Timestamp.apply - implicit val timestampArb: Arbitrary[VSum.Timestamp] = Arbitrary(timestampGen) - implicit val timestampShrink: Shrink[VSum.Timestamp] = - Shrink(ts => Shrink.shrink(ts.value) map VSum.Timestamp.apply) - - val timestampInRangeGen: Gen[VSum.Timestamp] = - Gen.choose(instantToMicros(MIN).value, instantToMicros(MAX).value) map VSum.Timestamp.apply - - def timeGen(min: Instant, max: Instant, microsOnly: Boolean): Gen[Instant] = - Gen - .zip( - Gen.choose(min.getEpochSecond, max.getEpochSecond), - if (microsOnly) Gen.choose(0L, 999999).map(_ * 1000) - else Gen.choose(0L, 999999999), - ) - .map { case (s, n) => Instant.ofEpochSecond(s, n) } - - val anyMicroTime: Gen[Instant] = timeGen(Instant.MIN, Instant.MAX, microsOnly = true) - - val anyTimeInRange: Gen[Instant] = timeGen(MIN, MAX, microsOnly = false) - - val anyMicroInRange: Gen[Instant] = - timeGen(MIN, MAX, microsOnly = true) - - val lfTimestampGen: Gen[Time.Timestamp] = Gen.choose( - Time.Timestamp.MinValue.micros, - Time.Timestamp.MaxValue.micros, - ) map Time.Timestamp.assertFromLong -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/CompletionServiceRequestValidatorTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/CompletionServiceRequestValidatorTest.scala deleted file mode 100644 index 78fd190729..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/CompletionServiceRequestValidatorTest.scala +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamRequest as GrpcCompletionStreamRequest -import com.digitalasset.canton.ledger.api.messages.command.completion.CompletionStreamRequest -import com.digitalasset.canton.logging.{ErrorLoggingContext, NoLogging} -import com.digitalasset.daml.lf.data.Ref -import io.grpc.Status.Code.* -import org.mockito.MockitoSugar -import org.scalatest.wordspec.AnyWordSpec - -import java.time.Duration - -class CompletionServiceRequestValidatorTest - extends AnyWordSpec - with ValidatorTestUtils - with MockitoSugar { - private implicit val noLogging: ErrorLoggingContext = NoLogging - private val grpcCompletionReq = GrpcCompletionStreamRequest( - expectedUserId, - List(party), - offsetLong, - ) - private val completionReq = CompletionStreamRequest( - Ref.UserId.assertFromString(expectedUserId), - List(party).toSet, - offset, - ) - - private val validator = CompletionServiceRequestValidator - - "CompletionRequestValidation" when { - - "validating gRPC completion requests" should { - - "accept plain requests" in { - inside( - validator.validateGrpcCompletionStreamRequest(grpcCompletionReq) - ) { case Right(req) => - req shouldBe completionReq - } - } - - "return the correct error on missing user ID" in { - requestMustFailWith( - request = validator.validateGrpcCompletionStreamRequest( - grpcCompletionReq.withUserId("") - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: user_id", - metadata = Map.empty, - ) - } - - "accept requests with begin exclusive offset zero" in { - inside( - validator.validateGrpcCompletionStreamRequest(grpcCompletionReq.withBeginExclusive(0)) - ) { case Right(req) => - req shouldBe completionReq.copy(offset = None) - } - } - - "return the correct error on negative begin exclusive offset" in { - requestMustFailWith( - request = validator.validateGrpcCompletionStreamRequest( - grpcCompletionReq.withBeginExclusive(-100) - ), - code = INVALID_ARGUMENT, - description = - "NEGATIVE_OFFSET(8,0): Offset -100 in begin_exclusive is a negative integer: the offset in begin_exclusive field has to be a non-negative integer (>=0)", - metadata = Map.empty, - ) - } - - "tolerate all fields filled out" in { - inside( - validator.validateGrpcCompletionStreamRequest(grpcCompletionReq) - ) { case Right(req) => - req shouldBe completionReq - } - } - - "tolerate empty offset (participant begin)" in { - inside( - validator.validateGrpcCompletionStreamRequest( - grpcCompletionReq.withBeginExclusive(0L) - ) - ) { case Right(req) => - req.userId shouldEqual expectedUserId - req.parties shouldEqual Set(party) - req.offset shouldBe empty - } - } - - } - - "validate api completion requests" should { - - "accept simple requests" in { - inside( - validator.validateCompletionStreamRequest(completionReq, ledgerEnd) - ) { case Right(req) => - req shouldBe completionReq - } - - } - - "return the correct error on missing party" in { - requestMustFailWith( - request = validator.validateCompletionStreamRequest( - completionReq.copy(parties = Set.empty), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: parties", - metadata = Map.empty, - ) - } - - "return the correct error when offset is after ledger end" in { - requestMustFailWith( - request = validator.validateCompletionStreamRequest( - completionReq.copy(offset = ledgerEnd.map(_.increment)), - ledgerEnd, - ), - code = OUT_OF_RANGE, - description = - s"OFFSET_AFTER_LEDGER_END(12,0): Begin offset (${ledgerEnd.value.unwrap + 1}) is after ledger end (${ledgerEnd.value.unwrap})", - metadata = Map("definite_answer" -> "false", "category" -> "12"), - retryDelay = Seq(Duration.ofSeconds(1L)), - ) - } - - "tolerate empty offset (participant begin)" in { - inside( - validator.validateCompletionStreamRequest( - completionReq.copy(offset = None), - ledgerEnd, - ) - ) { case Right(req) => - req.userId shouldEqual expectedUserId - req.parties shouldEqual Set(party) - req.offset shouldBe empty - } - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/EventQueryServiceRequestValidatorTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/EventQueryServiceRequestValidatorTest.scala deleted file mode 100644 index 8775269357..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/EventQueryServiceRequestValidatorTest.scala +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.event_query_service -import com.daml.ledger.api.v2.transaction_filter.CumulativeFilter.IdentifierFilter -import com.daml.ledger.api.v2.transaction_filter.{Filters, WildcardFilter} -import com.digitalasset.canton.ledger.api.messages.event -import com.digitalasset.canton.ledger.api.{CumulativeFilter, EventFormat, TemplateWildcardFilter} -import com.digitalasset.canton.logging.{ErrorLoggingContext, NoLogging} -import io.grpc.Status.Code.* -import org.mockito.MockitoSugar -import org.scalatest.wordspec.AnyWordSpec - -class EventQueryServiceRequestValidatorTest - extends AnyWordSpec - with ValidatorTestUtils - with MockitoSugar { - - private implicit val noLogging: ErrorLoggingContext = NoLogging - - "EventQueryServiceRequestValidator" when { - - val someProtoEventFormat = com.daml.ledger.api.v2.transaction_filter.EventFormat( - filtersByParty = Map( - party -> Filters( - Seq( - com.daml.ledger.api.v2.transaction_filter.CumulativeFilter( - IdentifierFilter.WildcardFilter(WildcardFilter(true)) - ) - ) - ) - ), - filtersForAnyParty = None, - verbose = false, - ) - - "validating event by contract id requests" should { - - val expected = event.GetEventsByContractIdRequest( - contractId = contractId, - eventFormat = EventFormat( - filtersByParty = Map( - party -> CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set.empty, - templateWildcardFilter = Some(TemplateWildcardFilter(includeCreatedEventBlob = true)), - ) - ), - filtersForAnyParty = None, - verbose = false, - ), - ) - - val req = event_query_service.GetEventsByContractIdRequest( - contractId.coid, - Some(someProtoEventFormat), - ) - - "pass on valid input" in { - EventQueryServiceRequestValidator.validateEventsByContractId(req) shouldBe Right(expected) - } - - "fail on empty contractId" in { - requestMustFailWith( - request = - EventQueryServiceRequestValidator.validateEventsByContractId(req.withContractId("")), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: contract_id", - metadata = Map.empty, - ) - } - - "fail on empty event format" in { - requestMustFailWith( - request = EventQueryServiceRequestValidator.validateEventsByContractId( - req.clearEventFormat - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: event_format", - metadata = Map.empty, - ) - } - } - - // TODO(i16065): Re-enable getEventsByContractKey tests -// "validating event by contract key requests" should { -// -// val txRequest = event.GetEventsByContractKeyRequest( -// contractKey = Lf.ValueText("contractKey"), -// templateId = refTemplateId, -// requestingParties = Set(party), -// endExclusiveSeqId = None, -// ) -// -// val apiRequest = event_query_service.GetEventsByContractKeyRequest( -// contractKey = Some(api.Value(Value.Sum.Text("contractKey"))), -// templateId = Some( -// com.daml.ledger.api.v2.value -// .Identifier(packageId, moduleName.toString, dottedName.toString) -// ), -// requestingParties = txRequest.requestingParties.toSeq, -// ) -// -// "pass on valid input" in { -// validator.validateEventsByContractKey(apiRequest) shouldBe Right(txRequest) -// } -// -// -// "fail on empty contract_key" in { -// requestMustFailWith( -// request = validator.validateEventsByContractKey(apiRequest.clearContractKey), -// code = INVALID_ARGUMENT, -// description = -// "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: contract_key", -// metadata = Map.empty, -// ) -// } -// -// "fail on empty template_id" in { -// requestMustFailWith( -// request = validator.validateEventsByContractKey(apiRequest.clearTemplateId), -// code = INVALID_ARGUMENT, -// description = -// "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: template_id", -// metadata = Map.empty, -// ) -// } -// -// "fail on empty requesting_parties" in { -// requestMustFailWith( -// request = validator.validateEventsByContractKey(apiRequest.withRequestingParties(Nil)), -// code = INVALID_ARGUMENT, -// description = -// "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: requesting_parties", -// metadata = Map.empty, -// ) -// } -// -// } - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/IdentifierValidatorTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/IdentifierValidatorTest.scala deleted file mode 100644 index 65a75bc09c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/IdentifierValidatorTest.scala +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.value.Identifier -import com.digitalasset.canton.ledger.api.ApiMocks -import com.digitalasset.canton.logging.{ErrorLoggingContext, NoLogging} -import io.grpc.Status.Code.INVALID_ARGUMENT -import org.mockito.MockitoSugar -import org.scalatest.wordspec.AsyncWordSpec - -class IdentifierValidatorTest extends AsyncWordSpec with ValidatorTestUtils with MockitoSugar { - - private implicit val errorLoggingContext: ErrorLoggingContext = NoLogging - - object api { - val identifier = Identifier("package", moduleName = "module", entityName = "entity") - } - - "validating identifiers" should { - "convert a valid identifier" in { - ValueValidator.validateIdentifier(api.identifier) shouldEqual Right(ApiMocks.identifier) - } - - "not allow missing package ids" in { - requestMustFailWith( - ValueValidator.validateIdentifier(api.identifier.withPackageId("")), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: package_id", - metadata = Map.empty, - ) - } - - "not allow missing names" in { - requestMustFailWith( - request = - ValueValidator.validateIdentifier(api.identifier.withModuleName("").withEntityName("")), - code = INVALID_ARGUMENT, - description = - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field module_name: Expected a non-empty string", - metadata = Map.empty, - ) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ResourceAnnotationValidationsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ResourceAnnotationValidationsSpec.scala deleted file mode 100644 index 69c3fc7d29..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ResourceAnnotationValidationsSpec.scala +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import cats.syntax.either.* -import com.digitalasset.canton.ledger.api.validation.ResourceAnnotationValidator.* -import org.scalatest.EitherValues -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class ResourceAnnotationValidationsSpec extends AnyFlatSpec with Matchers with EitherValues { - - private def testedValidationAllowEmptyValues(v: Map[String, String]) = - validateAnnotationsFromApiRequest(v, allowEmptyValues = true) - private def testedValidationDisallowEmptyValues(v: Map[String, String]) = - validateAnnotationsFromApiRequest(v, allowEmptyValues = false) - - it should "validate annotation values" in { - testedValidationDisallowEmptyValues(Map("a" -> "")).left.value shouldBe a[ - EmptyAnnotationsValueError - ] - } - - it should "validate annotations key names" in { - // invalid characters - testedValidationAllowEmptyValues(Map("" -> "a")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map("&" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map("%" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - // valid characters - testedValidationAllowEmptyValues(Map("a" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("Z" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("7" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("aA._-b" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("aA._-b" -> "")).value shouldBe () - testedValidationAllowEmptyValues( - Map("abcdefghijklmnopqrstuvwxyz.-_ABCDEFGHIJKLMNOPQRSTUVWXYZ" -> "") - ) shouldBe Either.unit - testedValidationAllowEmptyValues(Map("some-key.like_this" -> "")).value shouldBe () - // too long - testedValidationAllowEmptyValues(Map("a" * 64 -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - // just right size - testedValidationAllowEmptyValues(Map("a" * 63 -> "")).value shouldBe () - // character in an invalid position - testedValidationAllowEmptyValues(Map(".aaa" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map("aaa_" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map("aaa-" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - } - - it should "validate annotations keys prefixes" in { - testedValidationAllowEmptyValues(Map("aaa/a" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("AAA/a" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("aaa-bbb/a" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("aaa-bbb.ccc-ddd/a" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("00.11.AA-bBb.ccc2/a" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map("aa--aa/a" -> "")).value shouldBe () - testedValidationAllowEmptyValues(Map(".user.management.daml/foo_" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map("aaa./a" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map(".aaa/a" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map("-aaa/a" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map("aaa-/a" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map("aa..aa/a" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map(s"${"a" * 254}/a" -> "")).left.value shouldBe a[ - InvalidAnnotationsKeyError - ] - testedValidationAllowEmptyValues(Map(s"${"a" * 253}/a" -> "")).value shouldBe () - } - - it should "validate annotations' total size - single key, large value" in { - val largeString = "a" * 256 * 1024 - val notSoLargeString = "a" * ((256 * 1024) - 1) - testedValidationAllowEmptyValues( - Map("a" -> largeString) - ).left.value shouldBe AnnotationsSizeExceededError - testedValidationAllowEmptyValues(Map("a" -> notSoLargeString)).value shouldBe () - } - - it should "do not count keys with empty values towards size limit" in { - val notSoLargeString = "a" * ((256 * 1024) - 1) - testedValidationAllowEmptyValues( - Map("a" -> notSoLargeString, "deleteMe" -> "") - ).value shouldBe () - testedValidationAllowEmptyValues( - Map("a" -> notSoLargeString, "deleteM" -> "e") - ).left.value shouldBe AnnotationsSizeExceededError - } - - it should "validate annotations' total size - multiple keys" in { - val sixteenLetters = "abcdefghijklmnop" - val value = "a" * 1022 - val mapWithManyKeys: Map[String, String] = (for { - l1 <- sixteenLetters - l2 <- sixteenLetters - } yield { - val key = s"$l1$l2" - key -> value - }).toMap - val mapWithManyKeys2 = mapWithManyKeys.updated(key = "a", value = "b") - testedValidationAllowEmptyValues(mapWithManyKeys).value shouldBe () - testedValidationAllowEmptyValues( - mapWithManyKeys2 - ).left.value shouldBe AnnotationsSizeExceededError - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/SubmitRequestValidatorTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/SubmitRequestValidatorTest.scala deleted file mode 100644 index 15565d11a8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/SubmitRequestValidatorTest.scala +++ /dev/null @@ -1,1143 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.command_service.SubmitAndWaitForTransactionRequest -import com.daml.ledger.api.v2.commands.Commands.DeduplicationPeriod as DeduplicationPeriodProto -import com.daml.ledger.api.v2.commands.{Command, Commands, CreateCommand, PrefetchContractKey} -import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_ACS_DELTA -import com.daml.ledger.api.v2.transaction_filter.{EventFormat, Filters, TransactionFormat} -import com.daml.ledger.api.v2.value.Value.Sum -import com.daml.ledger.api.v2.value.{ - List as ApiList, - Optional as ApiOptional, - TextMap as ApiTextMap, - *, -} -import com.digitalasset.canton.data.{DeduplicationPeriod, Offset} -import com.digitalasset.canton.ledger.api.ApiMocks.{commandId, submissionId, userId, workflowId} -import com.digitalasset.canton.ledger.api.util.{DurationConversion, TimestampConversion} -import com.digitalasset.canton.ledger.api.{ApiMocks, Commands as ApiCommands, DisclosedContract} -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.{ErrorLoggingContext, NoLogging} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.command.{ - ApiCommand as LfCommand, - ApiCommands as LfCommands, - ApiContractKey, -} -import com.digitalasset.daml.lf.data.* -import com.digitalasset.daml.lf.data.Ref.TypeConRef -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - FatContractInstance, - Node as LfNode, - SerializationVersion as LfSerializationVersion, -} -import com.digitalasset.daml.lf.value.Value as Lf -import com.digitalasset.daml.lf.value.Value.ValueRecord -import com.google.protobuf.duration.Duration -import com.google.protobuf.empty.Empty -import io.grpc.Status.Code.INVALID_ARGUMENT -import io.grpc.StatusRuntimeException -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.wordspec.AnyWordSpec -import scalaz.syntax.tag.* - -import java.time.{Duration as JDuration, Instant} - -class SubmitRequestValidatorTest - extends AnyWordSpec - with ValidatorTestUtils - with TableDrivenPropertyChecks - with MockitoSugar - with ArgumentMatchersSugar { - private implicit val errorLoggingContext: ErrorLoggingContext = NoLogging - - private object api { - private val packageId = "package" - private val moduleName = "module" - private val entityName = "entity" - val identifier = Identifier(packageId, moduleName = moduleName, entityName = entityName) - val int64 = Sum.Int64(1) - val label = "label" - val constructor = "constructor" - val submitter = "party" - val deduplicationDuration = new Duration().withSeconds(10) - val synchronizerId = "x::synchronizerId" - - private def commandDef(createPackageId: String, moduleName: String = moduleName) = - Command.of( - Command.Command.Create( - CreateCommand.of( - Some(Identifier(createPackageId, moduleName = moduleName, entityName = entityName)), - Some( - Record( - Some(Identifier(packageId, moduleName = moduleName, entityName = entityName)), - Seq(RecordField("something", Some(Value(Value.Sum.Bool(true))))), - ) - ), - ) - ) - ) - - val command = commandDef(packageId) - val packageNameEncoded = Ref.PackageRef.Name(packageName).toString - val commandWithPackageNameScoping = commandDef(packageNameEncoded) - val prefetchKey = - PrefetchContractKey(Some(identifier), Some(ApiMocks.values.validApiParty), Some(1)) - val prefetchKeyWithPackageNameScoping = - prefetchKey.copy(templateId = Some(Identifier(packageNameEncoded, moduleName, entityName))) - - val commands = Commands( - workflowId = workflowId.unwrap, - userId = userId, - submissionId = submissionId.unwrap, - commandId = commandId.unwrap, - readAs = Nil, - actAs = Seq(submitter), - commands = Seq(command), - deduplicationPeriod = DeduplicationPeriodProto.DeduplicationDuration(deduplicationDuration), - minLedgerTimeAbs = None, - minLedgerTimeRel = None, - packageIdSelectionPreference = Seq.empty, - synchronizerId = synchronizerId, - prefetchContractKeys = Seq.empty, - disclosedContracts = Nil, - tapsMaxPasses = None, - ) - } - - private object internal { - val ledgerTime = Instant.EPOCH.plusSeconds(10) - val submittedAt = Instant.now - val timeDelta = java.time.Duration.ofSeconds(1) - val maxDeduplicationDuration = java.time.Duration.ofDays(1) - val deduplicationDuration = java.time.Duration.ofSeconds( - api.deduplicationDuration.seconds, - api.deduplicationDuration.nanos.toLong, - ) - - val templateId: Ref.Identifier = Ref.Identifier( - Ref.PackageId.assertFromString(api.identifier.packageId), - Ref.QualifiedName( - Ref.ModuleName.assertFromString("module"), - Ref.DottedName.assertFromString("entity"), - ), - ) - val templateRef: TypeConRef = TypeConRef( - Ref.PackageRef.Id(templateId.packageId), - templateId.qualifiedName, - ) - val templateRefByName: TypeConRef = TypeConRef( - Ref.PackageRef.Name(packageName), - templateId.qualifiedName, - ) - - val disclosedContracts: ImmArray[DisclosedContract] = ImmArray( - DisclosedContract( - fatContractInstance = FatContractInstance.fromCreateNode( - create = LfNode.Create( - coid = Lf.ContractId.V1.assertFromString("00" + "00" * 32), - packageName = Ref.PackageName.assertFromString("package"), - templateId = templateId, - arg = ValueRecord( - Some(templateId), - ImmArray.empty, - ), - signatories = Set(Ref.Party.assertFromString("party")), - stakeholders = Set(Ref.Party.assertFromString("party")), - keyOpt = None, - version = LfSerializationVersion.VDev, - ), - createTime = CreationTime.CreatedAt(Time.Timestamp.now()), - authenticationData = Bytes.Empty, - ), - synchronizerIdO = Some(SynchronizerId.tryFromString(api.synchronizerId)), - ) - ) - - val emptyCommands: ApiCommands = emptyCommandsBuilder(Ref.PackageRef.Id(templateId.packageId)) - def emptyCommandsBuilder( - packageRef: Ref.PackageRef, - packagePreferenceSet: Set[Ref.PackageId] = Set.empty, - packageMap: Map[Ref.PackageId, (Ref.PackageName, Ref.PackageVersion)] = Map.empty, - prefetchKeys: Seq[ApiContractKey] = Seq.empty, - ) = ApiCommands( - workflowId = Some(workflowId), - userId = userId, - commandId = commandId, - submissionId = Some(submissionId), - actAs = Set(ApiMocks.party), - readAs = Set.empty, - submittedAt = Time.Timestamp.assertFromInstant(submittedAt), - deduplicationPeriod = DeduplicationPeriod.DeduplicationDuration(deduplicationDuration), - commands = LfCommands( - ImmArray( - LfCommand.Create( - TypeConRef(packageRef, templateId.qualifiedName), - Lf.ValueRecord( - Option( - templateId - ), - ImmArray((Option(Ref.Name.assertFromString("something")), Lf.ValueTrue)), - ), - ) - ), - Time.Timestamp.assertFromInstant(ledgerTime), - workflowId.unwrap, - ), - disclosedContracts, - packagePreferenceSet = packagePreferenceSet, - synchronizerId = Some(SynchronizerId.tryFromString(api.synchronizerId)), - packageMap = packageMap, - prefetchKeys = prefetchKeys, - tapsMaxPasses = None, - ) - } - - private[this] def withLedgerTime(commands: ApiCommands, let: Instant): ApiCommands = - commands.copy( - commands = commands.commands.copy( - ledgerEffectiveTime = Time.Timestamp.assertFromInstant(let) - ) - ) - - private def unexpectedError = sys.error("unexpected error") - - private val testedCommandValidator = { - val validateDisclosedContractsMock = mock[ValidateDisclosedContracts] - - when(validateDisclosedContractsMock.validateCommands(any[Commands])(any[ErrorLoggingContext])) - .thenReturn(Right(internal.disclosedContracts)) - - new CommandsValidator( - validateUpgradingPackageResolutions = ValidateUpgradingPackageResolutions.Empty, - validateDisclosedContracts = validateDisclosedContractsMock, - ) - } - - private val testedSubmitAndWaitRequestValidator = new SubmitAndWaitRequestValidator( - testedCommandValidator - ) - - private val testedValueValidator = ValueValidator - - "CommandSubmissionRequestValidator" when { - "validating SubmitAndWaitRequestValidator" should { - "validate a request with transaction format" in { - testedSubmitAndWaitRequestValidator.validate( - req = SubmitAndWaitForTransactionRequest( - commands = Some(api.commands), - transactionFormat = Some( - TransactionFormat( - eventFormat = Some(EventFormat(Map.empty, Some(Filters(Nil)), verbose = false)), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - ), - currentLedgerTime = internal.ledgerTime, - currentUtcTime = internal.submittedAt, - maxDeduplicationDuration = internal.maxDeduplicationDuration, - ) shouldEqual Right(()) - } - - "validate a request with empty by party and any party filters" in { - requestMustFailWith( - testedSubmitAndWaitRequestValidator.validate( - req = SubmitAndWaitForTransactionRequest( - commands = Some(api.commands), - transactionFormat = Some( - TransactionFormat( - eventFormat = Some(EventFormat(Map.empty, None, verbose = false)), - transactionShape = TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - ), - currentLedgerTime = internal.ledgerTime, - currentUtcTime = internal.submittedAt, - maxDeduplicationDuration = internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: filtersByParty and filtersForAnyParty " + - "cannot be empty simultaneously", - metadata = Map.empty, - ) - } - - "reject a request without transaction format" in { - requestMustFailWith( - testedSubmitAndWaitRequestValidator.validate( - req = SubmitAndWaitForTransactionRequest( - commands = Some(api.commands), - transactionFormat = None, - ), - currentLedgerTime = internal.ledgerTime, - currentUtcTime = internal.submittedAt, - maxDeduplicationDuration = internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: transaction_format", - metadata = Map.empty, - ) - } - } - - "validating command submission requests" should { - "validate a complete request" in { - testedCommandValidator.validateCommands( - api.commands, - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right(internal.emptyCommands) - } - - "tolerate a missing submissionId" in { - testedCommandValidator.validateCommands( - api.commands.withSubmissionId(""), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right(internal.emptyCommands.copy(submissionId = None)) - } - - "reject requests with empty commands" in { - requestMustFailWith( - request = testedCommandValidator.validateCommands( - api.commands.withCommands(Seq.empty), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: commands", - metadata = Map.empty, - ) - } - - "tolerate a missing workflowId" in { - testedCommandValidator.validateCommands( - api.commands.withWorkflowId(""), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right( - internal.emptyCommands.copy( - workflowId = None, - commands = internal.emptyCommands.commands.copy(commandsReference = ""), - ) - ) - } - - "tolerate a missing synchronizerId" in { - testedCommandValidator.validateCommands( - api.commands.withSynchronizerId(""), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right( - internal.emptyCommands.copy(synchronizerId = None) - ) - } - - "not allow missing userId" in { - requestMustFailWith( - request = testedCommandValidator.validateCommands( - api.commands.withUserId(""), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: user_id", - metadata = Map.empty, - ) - } - - "not allow missing commandId" in { - requestMustFailWith( - request = testedCommandValidator.validateCommands( - api.commands.withCommandId(""), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: command_id", - metadata = Map.empty, - ) - } - - "not allow missing submitter" in { - requestMustFailWith( - request = testedCommandValidator.validateCommands( - api.commands.withActAs(Seq.empty), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: party or act_as", - metadata = Map.empty, - ) - } - - "correctly read and deduplicate multiple submitters" in { - - val result = testedCommandValidator - .validateCommands( - api.commands - .withActAs(Seq("alice")) - .addActAs("bob") - .addReadAs("alice") - .addReadAs("charlie") - .addReadAs("bob"), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) - inside(result) { case Right(cmd) => - // actAs parties are gathered from "party" and "readAs" fields - cmd.actAs shouldEqual Set("alice", "bob") - // readAs should exclude all parties that are already actAs parties - cmd.readAs shouldEqual Set("charlie") - } - } - - "tolerate a single submitter specified in the actAs fields" in { - - testedCommandValidator - .validateCommands( - api.commands.withActAs(Seq.empty).addActAs(api.submitter), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right(internal.emptyCommands) - } - - "tolerate a single submitter specified in party, actAs, and readAs fields" in { - - testedCommandValidator - .validateCommands( - api.commands - .withActAs(Seq(api.submitter)) - .addActAs(api.submitter) - .addReadAs(api.submitter), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right(internal.emptyCommands) - } - - "advance ledger time if minLedgerTimeAbs is set" in { - val minLedgerTimeAbs = internal.ledgerTime.plus(internal.timeDelta) - - testedCommandValidator.validateCommands( - api.commands.copy( - minLedgerTimeAbs = Some(TimestampConversion.fromInstant(minLedgerTimeAbs)) - ), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right(withLedgerTime(internal.emptyCommands, minLedgerTimeAbs)) - } - - "advance ledger time if minLedgerTimeRel is set" in { - val minLedgerTimeAbs = internal.ledgerTime.plus(internal.timeDelta) - - testedCommandValidator.validateCommands( - api.commands.copy( - minLedgerTimeRel = Some(DurationConversion.toProto(internal.timeDelta)) - ), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right(withLedgerTime(internal.emptyCommands, minLedgerTimeAbs)) - } - - "transform valid deduplication into correct internal structure" in { - val deduplicationDuration = Duration.of(10, 0) - forAll( - Table[DeduplicationPeriodProto, DeduplicationPeriod]( - ("input proto deduplication", "valid model deduplication"), - DeduplicationPeriodProto.DeduplicationOffset(12345678L) -> DeduplicationPeriod - .DeduplicationOffset(Some(Offset.tryFromLong(12345678L))), - DeduplicationPeriodProto.DeduplicationDuration( - deduplicationDuration - ) -> DeduplicationPeriod - .DeduplicationDuration(JDuration.ofSeconds(10)), - DeduplicationPeriodProto.Empty -> DeduplicationPeriod.DeduplicationDuration( - internal.maxDeduplicationDuration - ), - ) - ) { - case ( - sentDeduplication: DeduplicationPeriodProto, - expectedDeduplication: DeduplicationPeriod, - ) => - val result = testedCommandValidator.validateCommands( - api.commands.copy(deduplicationPeriod = sentDeduplication), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) - inside(result) { case Right(valid) => - valid.deduplicationPeriod shouldBe expectedDeduplication - } - } - } - - "not allow negative deduplication duration" in { - forAll( - Table( - "deduplication period", - DeduplicationPeriodProto.DeduplicationDuration(Duration.of(-1, 0)), - ) - ) { deduplication => - requestMustFailWith( - testedCommandValidator.validateCommands( - api.commands.copy(deduplicationPeriod = deduplication), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field deduplication_period: Duration must be positive", - metadata = Map.empty, - ) - } - } - - "allow deduplication duration exceeding maximum deduplication duration" in { - val durationSecondsExceedingMax = - internal.maxDeduplicationDuration.plusSeconds(1) - forAll( - Table( - "deduplication period", - DeduplicationPeriodProto.DeduplicationDuration( - Duration.of(durationSecondsExceedingMax.getSeconds, 0) - ), - ) - ) { deduplicationPeriod => - val commandsWithDeduplicationDuration = api.commands - .copy(deduplicationPeriod = deduplicationPeriod) - testedCommandValidator.validateCommands( - commandsWithDeduplicationDuration, - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldBe Right( - internal.emptyCommands.copy( - deduplicationPeriod = - DeduplicationPeriod.DeduplicationDuration(durationSecondsExceedingMax) - ) - ) - } - } - - "default to maximum deduplication duration if deduplication is missing" in { - testedCommandValidator.validateCommands( - api.commands.copy(deduplicationPeriod = DeduplicationPeriodProto.Empty), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right( - internal.emptyCommands.copy( - deduplicationPeriod = - DeduplicationPeriod.DeduplicationDuration(internal.maxDeduplicationDuration) - ) - ) - } - - "fail when disclosed contracts validation fails" in { - val validateDisclosedContractsMock = mock[ValidateDisclosedContracts] - - when( - validateDisclosedContractsMock.validateCommands(any[Commands])(any[ErrorLoggingContext]) - ) - .thenReturn( - Left( - RequestValidationErrors.InvalidField - .Reject("some failed", "some message") - .asGrpcError - ) - ) - - val failingDisclosedContractsValidator = new CommandsValidator( - validateUpgradingPackageResolutions = ValidateUpgradingPackageResolutions.Empty, - validateDisclosedContracts = validateDisclosedContractsMock, - ) - - requestMustFailWith( - request = failingDisclosedContractsValidator - .validateCommands( - api.commands, - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field some failed: some message", - metadata = Map.empty, - ) - } - - "when upgrading" should { - val validateDisclosedContractsMock = mock[ValidateDisclosedContracts] - - when( - validateDisclosedContractsMock.validateCommands(any[Commands])(any[ErrorLoggingContext]) - ) - .thenReturn(Right(internal.disclosedContracts)) - - val packageMap = - Map(packageId -> (packageName, Ref.PackageVersion.assertFromString("1.0.0"))) - val validateUpgradingPackageResolutions = new ValidateUpgradingPackageResolutions { - override def apply(userPackageIdPreferences: Seq[String])(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[ - StatusRuntimeException, - ValidateUpgradingPackageResolutions.ValidatedCommandPackageResolutionsSnapshot, - ] = - Right( - ValidateUpgradingPackageResolutions.ValidatedCommandPackageResolutionsSnapshot( - packagePreferenceSet = - userPackageIdPreferences.toSet.map(Ref.PackageId.assertFromString), - packageMap = packageMap, - ) - ) - } - - val commandsValidatorForUpgrading = new CommandsValidator( - validateUpgradingPackageResolutions = validateUpgradingPackageResolutions, - validateDisclosedContracts = validateDisclosedContractsMock, - ) - - "allow package name reference instead of package id" in { - commandsValidatorForUpgrading - .validateCommands( - api.commands.copy( - commands = Seq(api.commandWithPackageNameScoping), - prefetchContractKeys = Seq(api.prefetchKeyWithPackageNameScoping), - ), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldBe Right( - internal.emptyCommandsBuilder( - Ref.PackageRef.Name(packageName), - packageMap = packageMap, - prefetchKeys = - Seq(ApiContractKey(internal.templateRefByName, ApiMocks.values.validLfParty, 1)), - ) - ) - } - - "allow correctly specifying the package_id_selection_preference" in { - val userPackageIdPreference = Seq("validPackageId", "anotherPackageId") - commandsValidatorForUpgrading - .validateCommands( - api.commands.copy(packageIdSelectionPreference = userPackageIdPreference), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldBe Right( - internal.emptyCommandsBuilder( - Ref.PackageRef.Id(internal.templateId.packageId), - packageMap = packageMap, - packagePreferenceSet = - userPackageIdPreference.map(Ref.PackageId.assertFromString).toSet, - ) - ) - } - } - } - - "validating prefetched contract keys" should { - "allow complete keys" in { - testedCommandValidator.validateCommands( - api.commands.copy(prefetchContractKeys = Seq(api.prefetchKey)), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ) shouldEqual Right( - internal.emptyCommands.copy( - prefetchKeys = - Seq(ApiContractKey(internal.templateRef, ApiMocks.values.validLfParty, 1)) - ) - ) - } - - "reject keys with missing template ID" in { - requestMustFailWith( - request = testedCommandValidator.validateCommands( - api.commands.copy(prefetchContractKeys = Seq(api.prefetchKey.copy(templateId = None))), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: template_id", - metadata = Map.empty, - ) - } - - "reject keys with missing key value" in { - requestMustFailWith( - request = testedCommandValidator.validateCommands( - api.commands.copy(prefetchContractKeys = Seq(api.prefetchKey.copy(contractKey = None))), - internal.ledgerTime, - internal.submittedAt, - internal.maxDeduplicationDuration, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: contract_key", - metadata = Map.empty, - ) - } - } - - "validating contractId values" should { - "succeed" in { - - val coid = Lf.ContractId.V1.assertFromString("00" + "00" * 32) - - val input = Value(Sum.ContractId(coid.coid)) - val expected = Lf.ValueContractId(coid) - - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - } - - "validating party values" should { - "convert valid party" in { - testedValueValidator.validateValue(ApiMocks.values.validApiParty) shouldEqual Right( - ApiMocks.values.validLfParty - ) - } - - "reject non valid party" in { - requestMustFailWith( - request = testedValueValidator.validateValue(ApiMocks.values.invalidApiParty), - code = INVALID_ARGUMENT, - description = - """INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: non expected character 0x40 in Daml-LF Party "p@rty"""", - metadata = Map.empty, - ) - } - } - - "validating decimal values" should { - "convert valid decimals" in { - val signs = Table("signs", "", "+", "-") - val absoluteValues = - Table( - "absolute values" -> "scale", - "0" -> 0, - "0.0" -> 0, - "1.0000" -> 0, - "3.1415926536" -> 10, - "1" + "0" * 27 -> 0, - "1" + "0" * 27 + "." + "0" * 9 + "1" -> 10, - "0." + "0" * 9 + "1" -> 10, - "0." -> 0, - ) - - forEvery(signs) { sign => - forEvery(absoluteValues) { (absoluteValue, expectedScale) => - val s = sign + absoluteValue - val input = Value(Sum.Numeric(s)) - val expected = - Lf.ValueNumeric( - Numeric - .assertFromBigDecimal(Numeric.Scale.assertFromInt(expectedScale), BigDecimal(s)) - ) - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - } - - } - - "reject out-of-bound decimals" in { - val signs = Table("signs", "", "+", "-") - val absoluteValues = - Table( - "absolute values" -> "scale", - "1" + "0" * 38 -> 0, - "1" + "0" * 28 + "." + "0" * 10 + "1" -> 11, - "1" + "0" * 27 + "." + "0" * 11 + "1" -> 12, - ) - - forEvery(signs) { sign => - forEvery(absoluteValues) { (absoluteValue, _) => - val s = sign + absoluteValue - val input = Value(Sum.Numeric(s)) - requestMustFailWith( - request = testedValueValidator.validateValue(input), - code = INVALID_ARGUMENT, - description = - s"""INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: Could not read Numeric string "$s"""", - metadata = Map.empty, - ) - } - } - } - - "reject invalid decimals" in { - val signs = Table("signs", "", "+", "-") - val absoluteValues = - Table( - "invalid absolute values", - "zero", - "1E10", - "+", - "-", - "0x01", - ".", - "", - ".0", - "0." + "0" * 37 + "1", - ) - - forEvery(signs) { sign => - forEvery(absoluteValues) { absoluteValue => - val s = sign + absoluteValue - val input = Value(Sum.Numeric(s)) - requestMustFailWith( - request = testedValueValidator.validateValue(input), - code = INVALID_ARGUMENT, - description = - s"""INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: Could not read Numeric string "$s"""", - metadata = Map.empty, - ) - } - } - } - - } - - "validating text values" should { - "accept any of them" in { - val strings = - Table("string", "", "a¶‱😂", "a¶‱😃") - - forEvery(strings) { s => - val input = Value(Sum.Text(s)) - val expected = Lf.ValueText(s) - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - } - } - - "validating timestamp values" should { - "accept valid timestamp" in { - val testCases = Table( - "long/timestamp", - Time.Timestamp.MinValue.micros -> Time.Timestamp.MinValue, - 0L -> Time.Timestamp.Epoch, - Time.Timestamp.MaxValue.micros -> Time.Timestamp.MaxValue, - ) - - forEvery(testCases) { case (long, timestamp) => - val input = Value(Sum.Timestamp(long)) - val expected = Lf.ValueTimestamp(timestamp) - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - } - - "reject out-of-bound timestamp" in { - val testCases = Table( - "long/timestamp", - Long.MinValue, - Time.Timestamp.MinValue.micros - 1, - Time.Timestamp.MaxValue.micros + 1, - Long.MaxValue, - ) - - forEvery(testCases) { long => - val input = Value(Sum.Timestamp(long)) - requestMustFailWith( - request = testedValueValidator.validateValue(input), - code = INVALID_ARGUMENT, - description = - s"INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: cannot convert long $long into Timestamp:out of bound Timestamp $long", - metadata = Map.empty, - ) - } - } - } - - "validating date values" should { - "accept valid date" in { - val testCases = Table( - "int/date", - Time.Date.MinValue.days -> Time.Date.MinValue, - 0 -> Time.Date.Epoch, - Time.Date.MaxValue.days -> Time.Date.MaxValue, - ) - - forEvery(testCases) { case (int, date) => - val input = Value(Sum.Date(int)) - val expected = Lf.ValueDate(date) - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - } - - "reject out-of-bound date" in { - val testCases = Table( - "int/date", - Int.MinValue, - Time.Date.MinValue.days - 1, - Time.Date.MaxValue.days + 1, - Int.MaxValue, - ) - - forEvery(testCases) { int => - val input = Value(Sum.Date(int)) - requestMustFailWith( - request = testedValueValidator.validateValue(input), - code = INVALID_ARGUMENT, - description = - s"INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: out of bound Date $int", - metadata = Map.empty, - ) - } - } - } - - "validating boolean values" should { - "accept any of them" in { - testedValueValidator.validateValue(Value(Sum.Bool(true))) shouldEqual Right(Lf.ValueTrue) - testedValueValidator.validateValue(Value(Sum.Bool(false))) shouldEqual Right(Lf.ValueFalse) - } - } - - "validating unit values" should { - "succeed" in { - testedValueValidator.validateValue(Value(Sum.Unit(Empty()))) shouldEqual Right(Lf.ValueUnit) - } - } - - "validating record values" should { - "convert valid records" in { - val record = - Value( - Sum.Record( - Record(Some(api.identifier), Seq(RecordField(api.label, Some(Value(api.int64))))) - ) - ) - val expected = - Lf.ValueRecord( - Some(ApiMocks.identifier), - ImmArray(Some(ApiMocks.label) -> ApiMocks.values.int64), - ) - testedValueValidator.validateValue(record) shouldEqual Right(expected) - } - - "tolerate missing identifiers in records" in { - val record = - Value(Sum.Record(Record(None, Seq(RecordField(api.label, Some(Value(api.int64))))))) - val expected = - Lf.ValueRecord(None, ImmArray(Some(ApiMocks.label) -> ApiMocks.values.int64)) - testedValueValidator.validateValue(record) shouldEqual Right(expected) - } - - "tolerate missing labels in record fields" in { - val record = - Value(Sum.Record(Record(None, Seq(RecordField("", Some(Value(api.int64))))))) - val expected = - ValueRecord(None, ImmArray(None -> ApiMocks.values.int64)) - testedValueValidator.validateValue(record) shouldEqual Right(expected) - } - - "not allow missing record values" in { - val record = - Value(Sum.Record(Record(Some(api.identifier), Seq(RecordField(api.label, None))))) - requestMustFailWith( - request = testedValueValidator.validateValue(record), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: value", - metadata = Map.empty, - ) - } - - } - - "validating variant values" should { - - "convert valid variants" in { - val variant = - Value(Sum.Variant(Variant(Some(api.identifier), api.constructor, Some(Value(api.int64))))) - val expected = Lf.ValueVariant( - Some(ApiMocks.identifier), - ApiMocks.values.constructor, - ApiMocks.values.int64, - ) - testedValueValidator.validateValue(variant) shouldEqual Right(expected) - } - - "tolerate missing identifiers" in { - val variant = Value(Sum.Variant(Variant(None, api.constructor, Some(Value(api.int64))))) - val expected = - Lf.ValueVariant(None, ApiMocks.values.constructor, ApiMocks.values.int64) - - testedValueValidator.validateValue(variant) shouldEqual Right(expected) - } - - "not allow missing constructor" in { - val variant = Value(Sum.Variant(Variant(None, "", Some(Value(api.int64))))) - requestMustFailWith( - request = testedValueValidator.validateValue(variant), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: constructor", - metadata = Map.empty, - ) - } - - "not allow missing values" in { - val variant = Value(Sum.Variant(Variant(None, api.constructor, None))) - requestMustFailWith( - request = testedValueValidator.validateValue(variant), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: value", - metadata = Map.empty, - ) - } - - } - - "validating list values" should { - "convert empty lists" in { - val input = Value(Sum.List(ApiList(List.empty))) - val expected = - Lf.ValueNil - - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - - "convert valid lists" in { - val list = Value(Sum.List(ApiList(Seq(Value(api.int64), Value(api.int64))))) - val expected = - Lf.ValueList(FrontStack(ApiMocks.values.int64, ApiMocks.values.int64)) - testedValueValidator.validateValue(list) shouldEqual Right(expected) - } - - "reject lists containing invalid values" in { - val input = Value( - Sum.List( - ApiList(Seq(ApiMocks.values.validApiParty, ApiMocks.values.invalidApiParty)) - ) - ) - requestMustFailWith( - request = testedValueValidator.validateValue(input), - code = INVALID_ARGUMENT, - description = - """INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: non expected character 0x40 in Daml-LF Party "p@rty"""", - metadata = Map.empty, - ) - } - } - - "validating optional values" should { - "convert empty optionals" in { - val input = Value(Sum.Optional(ApiOptional(None))) - val expected = Lf.ValueNone - - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - - "convert valid non-empty optionals" in { - val list = Value(Sum.Optional(ApiOptional(Some(ApiMocks.values.validApiParty)))) - val expected = Lf.ValueOptional(Some(ApiMocks.values.validLfParty)) - testedValueValidator.validateValue(list) shouldEqual Right(expected) - } - - "reject optional containing invalid values" in { - val input = Value(Sum.Optional(ApiOptional(Some(ApiMocks.values.invalidApiParty)))) - requestMustFailWith( - request = testedValueValidator.validateValue(input), - code = INVALID_ARGUMENT, - description = - """INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: non expected character 0x40 in Daml-LF Party "p@rty"""", - metadata = Map.empty, - ) - } - } - - "validating map values" should { - "convert empty maps" in { - val input = Value(Sum.TextMap(ApiTextMap(List.empty))) - val expected = Lf.ValueTextMap(SortedLookupList.Empty) - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - - "convert valid maps" in { - val entries = (1 until 5) - .map { x => - Utf8.sha256(Utf8.getBytes(x.toString)) -> x.toLong - } - .to(ImmArray) - val apiEntries = entries.map { case (k, v) => - ApiTextMap.Entry(k, Some(Value(Sum.Int64(v)))) - } - val input = Value(Sum.TextMap(ApiTextMap(apiEntries.toSeq))) - val lfEntries = entries.map { case (k, v) => k -> Lf.ValueInt64(v) } - val expected = - Lf.ValueTextMap(SortedLookupList.fromImmArray(lfEntries).getOrElse(unexpectedError)) - - testedValueValidator.validateValue(input) shouldEqual Right(expected) - } - - "reject maps with repeated keys" in { - val entries = (1 +: (1 until 5)) - .map { x => - Utf8.sha256(Utf8.getBytes(x.toString)) -> x.toLong - } - .to(ImmArray) - val apiEntries = entries.map { case (k, v) => - ApiTextMap.Entry(k, Some(Value(Sum.Int64(v)))) - } - val input = Value(Sum.TextMap(ApiTextMap(apiEntries.toSeq))) - requestMustFailWith( - request = testedValueValidator.validateValue(input), - code = INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: key 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b duplicated when trying to build map", - metadata = Map.empty, - ) - } - - "reject maps containing invalid value" in { - val apiEntries = - List( - ApiTextMap.Entry("1", Some(ApiMocks.values.validApiParty)), - ApiTextMap.Entry("2", Some(ApiMocks.values.invalidApiParty)), - ) - val input = Value(Sum.TextMap(ApiTextMap(apiEntries))) - requestMustFailWith( - request = testedValueValidator.validateValue(input), - code = INVALID_ARGUMENT, - description = - """INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: non expected character 0x40 in Daml-LF Party "p@rty"""", - metadata = Map.empty, - ) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/UpdateServiceRequestValidatorTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/UpdateServiceRequestValidatorTest.scala deleted file mode 100644 index b87bbd927f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/UpdateServiceRequestValidatorTest.scala +++ /dev/null @@ -1,1000 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.transaction_filter.CumulativeFilter.IdentifierFilter -import com.daml.ledger.api.v2.transaction_filter.{ - CumulativeFilter as ProtoCumulativeFilter, - InterfaceFilter as ProtoInterfaceFilter, - TemplateFilter as ProtoTemplateFilter, - *, -} -import com.daml.ledger.api.v2.update_service.{ - GetUpdateByIdRequest, - GetUpdateByOffsetRequest, - GetUpdatesPageRequest, - GetUpdatesRequest, -} -import com.daml.ledger.api.v2.value.Identifier -import com.daml.platform.v1.page_tokens.UpdatesPageToken -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.{CumulativeFilter, InterfaceFilter, TemplateFilter} -import com.digitalasset.canton.logging.{ErrorLoggingContext, NoLogging} -import com.digitalasset.canton.platform.config.UpdateServiceConfig -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.NameTypeConRef -import com.google.protobuf.ByteString -import io.grpc.Status.Code.* -import org.mockito.MockitoSugar -import org.scalatest.wordspec.AnyWordSpec - -class UpdateServiceRequestValidatorTest - extends AnyWordSpec - with ValidatorTestUtils - with MockitoSugar { - private implicit val noLogging: ErrorLoggingContext = NoLogging - - private val templateId = - Identifier(Ref.PackageRef.Name(packageName).toString, includedModule, includedTemplate) - - private def getFiltersByParty(templateIdsForParty: Seq[Identifier]): Map[String, Filters] = - Map( - party -> - Filters( - templateIdsForParty - .map(tId => - ProtoCumulativeFilter( - IdentifierFilter.TemplateFilter( - ProtoTemplateFilter(Some(tId), includeCreatedEventBlob = false) - ) - ) - ) - ++ - Seq( - ProtoCumulativeFilter( - IdentifierFilter.InterfaceFilter( - ProtoInterfaceFilter( - interfaceId = Some( - Identifier( - packageNameRefEncoded, - moduleName = includedModule, - entityName = includedTemplate, - ) - ), - includeInterfaceView = true, - includeCreatedEventBlob = true, - ) - ) - ) - ) - ) - ) - - private def updatesReqBuilder( - transactionTemplateIdsO: Option[Seq[Identifier]], - reassignmentsTemplateIdsO: Option[Seq[Identifier]] = None, - ) = - GetUpdatesRequest( - beginExclusive = 0L, - endInclusive = Some(offsetLong), - updateFormat = buildUpdateFormat(transactionTemplateIdsO, reassignmentsTemplateIdsO), - descendingOrder = false, - ) - - private def buildUpdateFormat( - transactionTemplateIdsO: Option[Seq[Identifier]], - reassignmentsTemplateIdsO: Option[Seq[Identifier]], - ) = - Some( - UpdateFormat( - includeTransactions = transactionTemplateIdsO - .map(getFiltersByParty) - .map(filtersByParty => - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = filtersByParty, - filtersForAnyParty = None, - verbose = verbose, - ) - ), - transactionShape = TransactionShape.TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = reassignmentsTemplateIdsO - .map(getFiltersByParty) - .map(filtersByParty => - EventFormat( - filtersByParty = filtersByParty, - filtersForAnyParty = None, - verbose = false, - ) - ), - includeTopologyEvents = None, - ) - ) - - private val txReq = updatesReqBuilder(Some(Seq(templateId))) - private val txReqWithId = updatesReqBuilder(Some(Seq(templateId.copy(packageId = packageId)))) - private val reassignmentsReq = updatesReqBuilder( - transactionTemplateIdsO = None, - reassignmentsTemplateIdsO = Some(Seq(templateId)), - ) - - private val txByOffsetReq = - GetUpdateByOffsetRequest( - offset = offsetLong, - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map(party -> Filters(Nil)), - filtersForAnyParty = None, - verbose = false, - ) - ), - transactionShape = TransactionShape.TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = None, - includeTopologyEvents = None, - ) - ), - ) - - private val updateByIdReq = - GetUpdateByIdRequest( - updateId = updateId.toHexString, - updateFormat = Some( - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = Some( - EventFormat( - filtersByParty = Map(party -> Filters(Nil)), - filtersForAnyParty = None, - verbose = false, - ) - ), - transactionShape = TransactionShape.TRANSACTION_SHAPE_ACS_DELTA, - ) - ), - includeReassignments = None, - includeTopologyEvents = None, - ) - ), - ) - - "UpdateRequestValidation" when { - - "validating regular requests" should { - - "accept simple requests" in { - inside(UpdateServiceRequestValidator.validate(txReq, ledgerEnd)) { case Right(req) => - req.startExclusive shouldBe None - req.endInclusive shouldBe offset - val filtersByParty = - req.updateFormat.includeTransactions.map(_.eventFormat.filtersByParty).value - filtersByParty should have size 1 - hasExpectedFilters(req) - req.updateFormat.includeTransactions.value.eventFormat.verbose shouldEqual verbose - } - - } - - "accept simple requests for reassignments" in { - inside(UpdateServiceRequestValidator.validate(reassignmentsReq, ledgerEnd)) { - case Right(req) => - req.startExclusive shouldBe None - req.endInclusive shouldBe offset - val filtersByParty = - req.updateFormat.includeReassignments.value.filtersByParty - filtersByParty should have size 1 - req.updateFormat.includeReassignments.value.verbose shouldEqual verbose - } - - } - - "return the correct error without filters in the event format of transactions" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.update( - _.updateFormat.includeTransactions.eventFormat.filtersByParty := Map.empty - ), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: filtersByParty and filtersForAnyParty cannot be empty simultaneously", - metadata = Map.empty, - ) - } - - "return the correct error without filters in the event format of reassignments" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - reassignmentsReq.update( - _.updateFormat.includeReassignments.filtersByParty := Map.empty - ), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: filtersByParty and filtersForAnyParty cannot be empty simultaneously", - metadata = Map.empty, - ) - } - - "return the correct error on empty interfaceId in interfaceFilter" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.update( - _.updateFormat.includeTransactions.eventFormat.filtersByParty.modify(_.map { - case (p, f) => - p -> f.update( - _.cumulative := Seq( - ProtoCumulativeFilter( - IdentifierFilter.InterfaceFilter( - ProtoInterfaceFilter( - interfaceId = None, - includeInterfaceView = true, - includeCreatedEventBlob = false, - ) - ) - ) - ) - ) - }) - ), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: interfaceId", - metadata = Map.empty, - ) - } - - "return the correct error on empty templateId in templateFilter" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.update( - _.updateFormat.includeTransactions.eventFormat.filtersByParty.modify(_.map { - case (p, f) => - p -> f.update( - _.cumulative := Seq( - ProtoCumulativeFilter( - IdentifierFilter.TemplateFilter( - ProtoTemplateFilter(templateId = None, includeCreatedEventBlob = true) - ) - ) - ) - ) - }) - ), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: templateId", - metadata = Map.empty, - ) - } - - "return the correct error on unspecified transaction shape" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.update( - _.updateFormat.includeTransactions.transactionShape := TransactionShape.TRANSACTION_SHAPE_UNSPECIFIED - ), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: transaction_shape", - metadata = Map.empty, - ) - } - - "return the correct error on unrecognized transaction shape" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.update( - _.updateFormat.includeTransactions.transactionShape := - TransactionShape.Unrecognized(unrecognizedValue = 4) - ), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: transaction_shape is defined with invalid value 4", - metadata = Map.empty, - ) - } - - "return the correct error on invalid party for topology events" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.update( - _.updateFormat.includeTopologyEvents.includeParticipantAuthorizationEvents.parties := Seq( - "notParseableString@" - ) - ), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field parties: non " + - "expected character 0x40 in Daml-LF Party \"notParseableString@\"", - metadata = Map.empty, - ) - } - - "return the correct error when begin offset is after ledger end" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.withBeginExclusive(ledgerEnd.value.unwrap + 10L), - ledgerEnd, - ), - code = OUT_OF_RANGE, - description = - s"OFFSET_AFTER_LEDGER_END(12,0): Begin offset (${ledgerEnd.value.unwrap + 10L}) is after ledger end (${ledgerEnd.value.unwrap})", - metadata = Map.empty, - ) - } - - "return the correct error when end offset is after ledger end" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.withEndInclusive(ledgerEnd.value.unwrap + 10), - ledgerEnd, - ), - code = OUT_OF_RANGE, - description = - s"OFFSET_AFTER_LEDGER_END(12,0): End offset (${ledgerEnd.value.unwrap + 10L}) is after ledger end (${ledgerEnd.value.unwrap})", - metadata = Map.empty, - ) - } - - "return the correct error when begin offset is negative" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.withBeginExclusive(-100L), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "NEGATIVE_OFFSET(8,0): Offset -100 in begin_exclusive is a negative integer: " + - "the offset in begin_exclusive field has to be a non-negative integer (>=0)", - metadata = Map.empty, - ) - } - - "return the correct error when end offset is zero" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.withEndInclusive(0L), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "NON_POSITIVE_OFFSET(8,0): Offset 0 in end_inclusive is not a positive integer: " + - "the offset has to be either a positive integer (>0) or not defined at all", - metadata = Map.empty, - ) - } - - "return the correct error when end offset is negative" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.withEndInclusive(-100L), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "NON_POSITIVE_OFFSET(8,0): Offset -100 in end_inclusive is not a positive integer: " + - "the offset has to be either a positive integer (>0) or not defined at all", - metadata = Map.empty, - ) - } - - "return the correct error when package id is used in filters" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate(txReqWithId, ledgerEnd), - code = INVALID_ARGUMENT, - description = - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field packageId: Received an identifier with package ID packageId, but expected a package name.", - metadata = Map.empty, - ) - } - - "tolerate missing end" in { - inside( - UpdateServiceRequestValidator.validate( - txReq.update(_.optionalEndInclusive := None), - ledgerEnd, - ) - ) { case Right(req) => - req.startExclusive shouldEqual None - req.endInclusive shouldEqual None - req.descendingOrder shouldEqual false - val filtersByParty = - req.updateFormat.includeTransactions.map(_.eventFormat.filtersByParty).value - filtersByParty should have size 1 - hasExpectedFilters(req) - req.updateFormat.includeTransactions.value.eventFormat.verbose shouldEqual verbose - } - } - - "tolerate empty filters_inclusive" in { - inside( - UpdateServiceRequestValidator.validate( - txReq.update( - _.updateFormat.includeTransactions.eventFormat.filtersByParty.modify(_.map { - case (p, f) => - p -> f.update(_.cumulative := Seq(ProtoCumulativeFilter.defaultInstance)) - }) - ), - ledgerEnd, - ) - ) { case Right(req) => - req.startExclusive shouldEqual None - req.endInclusive shouldEqual offset - val filtersByParty = - req.updateFormat.includeTransactions.map(_.eventFormat.filtersByParty).value - filtersByParty should have size 1 - inside(filtersByParty.headOption.value) { case (p, filters) => - p shouldEqual party - filters shouldEqual CumulativeFilter.templateWildcardFilter() - } - req.updateFormat.includeTransactions.value.eventFormat.verbose shouldEqual verbose - } - } - - "tolerate missing filters_inclusive" in { - inside( - UpdateServiceRequestValidator.validate( - txReq.update( - _.updateFormat.includeTransactions.eventFormat.filtersByParty.modify(_.map { - case (p, f) => - p -> f.update(_.cumulative := Seq()) - }) - ), - ledgerEnd, - ) - ) { case Right(req) => - req.startExclusive shouldEqual None - req.endInclusive shouldEqual offset - val filtersByParty = - req.updateFormat.includeTransactions.map(_.eventFormat.filtersByParty).value - filtersByParty should have size 1 - inside(filtersByParty.headOption.value) { case (p, filters) => - p shouldEqual party - filters shouldEqual CumulativeFilter.templateWildcardFilter() - } - req.updateFormat.includeTransactions.value.eventFormat.verbose shouldEqual verbose - } - } - - "tolerate all fields filled out" in { - inside(UpdateServiceRequestValidator.validate(txReq, ledgerEnd)) { case Right(req) => - req.startExclusive shouldEqual None - req.endInclusive shouldEqual offset - hasExpectedFilters(req) - req.updateFormat.includeTransactions.value.eventFormat.verbose shouldEqual verbose - } - } - - "current definition populate the right api request" in { - val result = UpdateServiceRequestValidator.validate( - updatesReqBuilder(Some(Seq.empty)).update( - _.updateFormat.includeTransactions.eventFormat.filtersByParty := - Map( - party -> Filters( - Seq( - ProtoCumulativeFilter( - IdentifierFilter.InterfaceFilter( - ProtoInterfaceFilter( - interfaceId = Some(templateId), - includeInterfaceView = true, - includeCreatedEventBlob = true, - ) - ) - ) - ) - ++ - Seq( - ProtoCumulativeFilter( - IdentifierFilter.TemplateFilter( - ProtoTemplateFilter(Some(templateId), includeCreatedEventBlob = true) - ) - ) - ) - ) - ) - ), - ledgerEnd, - ) - result.map( - _.updateFormat.includeTransactions.value.eventFormat.filtersByParty - ) shouldBe Right( - Map( - party -> - CumulativeFilter( - templateFilters = Set( - TemplateFilter( - NameTypeConRef.assertFromString( - "#somePackageName:includedModule:includedTemplate" - ), - includeCreatedEventBlob = true, - ) - ), - interfaceFilters = Set( - InterfaceFilter( - interfaceTypeRef = Ref.NameTypeConRef.assertFromString( - "#somePackageName:includedModule:includedTemplate" - ), - includeView = true, - includeCreatedEventBlob = true, - ) - ), - templateWildcardFilter = None, - ) - ) - ) - } - - "allow request with missing end_offset when descending_order is false" in { - inside( - UpdateServiceRequestValidator.validate( - txReq.update(_.optionalEndInclusive := None, _.descendingOrder := false), - ledgerEnd, - ) - ) { case Right(req) => - req.startExclusive shouldEqual None - req.endInclusive shouldEqual None - req.descendingOrder shouldEqual false - val filtersByParty = - req.updateFormat.includeTransactions.map(_.eventFormat.filtersByParty).value - filtersByParty should have size 1 - hasExpectedFilters(req) - req.updateFormat.includeTransactions.value.eventFormat.verbose shouldEqual verbose - } - } - - "return correct error when end_offset is zero and descending_order is false" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.withEndInclusive(0L).withDescendingOrder(false), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "NON_POSITIVE_OFFSET(8,0): Offset 0 in end_inclusive is not a positive integer: " + - "the offset has to be either a positive integer (>0) or not defined at all", - metadata = Map.empty, - ) - } - - "allow descending_order true when end_offset is present and positive" in { - inside( - UpdateServiceRequestValidator.validate( - txReq.update(_.descendingOrder := true), - ledgerEnd, - ) - ) { case Right(req) => - req.startExclusive shouldBe None - req.endInclusive shouldBe offset - req.descendingOrder shouldBe true - hasExpectedFilters(req) - } - } - - "return correct error when end_offset is not present and descending_order is true" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq - .update(_.optionalEndInclusive := None) - .update(_.descendingOrder := true), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "DESCENDING_ORDER_MISSING_END(8,0): end_inclusive is not provided when descending_order is true", - metadata = Map.empty, - ) - } - - "return correct error when end_offset is zero and descending_order is true" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validate( - txReq.withEndInclusive(0L).update(_.descendingOrder := true), - ledgerEnd, - ), - code = INVALID_ARGUMENT, - description = - "NON_POSITIVE_OFFSET(8,0): Offset 0 in end_inclusive is not a positive integer: " + - "the offset has to be a positive integer (>0)", - metadata = Map.empty, - ) - } - - } - - "validating transaction by id requests" should { - - "fail on empty updateId" in { - requestMustFailWith( - request = - UpdateServiceRequestValidator.validateUpdateById(updateByIdReq.withUpdateId("")), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: update_id", - metadata = Map.empty, - ) - } - - "fail on empty update format" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validateUpdateById( - updateByIdReq.clearUpdateFormat - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: update_format", - metadata = Map.empty, - ) - } - } - - "validating transaction by offset requests" should { - - "fail on zero offset" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validateUpdateByOffset( - txByOffsetReq.withOffset(0) - ), - code = INVALID_ARGUMENT, - description = - "NON_POSITIVE_OFFSET(8,0): Offset 0 in offset is not a positive integer: the offset has to be a positive integer (>0)", - metadata = Map.empty, - ) - } - - "fail on negative offset" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validateUpdateByOffset( - txByOffsetReq.withOffset(-21) - ), - code = INVALID_ARGUMENT, - description = - "NON_POSITIVE_OFFSET(8,0): Offset -21 in offset is not a positive integer: the offset has to be a positive integer (>0)", - metadata = Map.empty, - ) - } - - "fail on empty update format" in { - requestMustFailWith( - request = UpdateServiceRequestValidator.validateUpdateByOffset( - txByOffsetReq.clearUpdateFormat - ), - code = INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: update_format", - metadata = Map.empty, - ) - } - - } - } - - "UpdateServiceRequestValidator.validateUpdatesPageRequest" should { - val participant = Ref.ParticipantId.assertFromString("participant") - - def requestWithMatchingToken( - beginOffsetExclusive: Option[Long], - endOffsetInclusive: Option[Long], - maxPageSize: Option[Int], - updateFormat: Option[UpdateFormat], - descendingOrder: Boolean, - tokenLowestOffsetExclusive: Long = 5, - tokenHighestOffsetExclusive: Long = 10, - ): (GetUpdatesPageRequest, UpdatesPageToken) = { - val request = GetUpdatesPageRequest( - beginOffsetExclusive = beginOffsetExclusive, - endOffsetInclusive = endOffsetInclusive, - maxPageSize = maxPageSize, - updateFormat = updateFormat, - descendingOrder = descendingOrder, - pageToken = None, - ) - val participantChecksum = com.digitalasset.canton.ledger.api.messages.update.UpdatesPageToken - .participantChecksum(participant) - val requestChecksum = com.digitalasset.canton.ledger.api.messages.update.UpdatesPageToken - .requestChecksum(request) - val token = UpdatesPageToken( - lowestPageOffsetExclusive = tokenLowestOffsetExclusive, - highestPageOffsetInclusive = tokenHighestOffsetExclusive, - version = com.digitalasset.canton.ledger.api.messages.update.UpdatesPageToken.Version, - participantIdChecksum = participantChecksum, - requestChecksum = requestChecksum, - ) - (request, token) - } - - "accept request with matching page token" in { - val (request, token) = requestWithMatchingToken( - beginOffsetExclusive = None, - endOffsetInclusive = None, - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - ) - - val result = UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request.update( - _.pageToken := token.toByteString - ), - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - - result.isRight shouldBe true - } - - "reject request with pager token that cannot be parsed" in { - val (request, _) = requestWithMatchingToken( - beginOffsetExclusive = None, - endOffsetInclusive = None, - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - ) - val result = UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request.update( - _.pageToken := ByteString.copyFrom(Array[Byte](1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) - ), - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - requestMustFailWith( - result, - code = INVALID_ARGUMENT, - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field page_token: Invalid page token for GetUpdatesPageRequest", - Map.empty, - ) - } - - "reject request with wrong version of page token" in { - val (requestInitial, tokenInitial) = requestWithMatchingToken( - beginOffsetExclusive = None, - endOffsetInclusive = None, - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - ) - val request = - requestInitial.update(_.pageToken := tokenInitial.update(_.version := 1024).toByteString) - - val result = UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - requestMustFailWith( - result, - code = INVALID_ARGUMENT, - "INVALID_UPDATES_PAGE_TOKEN(8,0): The submitted command contains an invalid page token. Tokens used in GetUpdatesPage requests must be taken from a valid GetUpdatesPageResponse and used with the same EventFormat settings, the same begin and end with the same Canton participant running the same Canton version. Next page token was generated by a different Canton version", - Map.empty, - ) - } - - "reject request with wrong participant checksum" in { - val participant = Ref.ParticipantId.assertFromString("participant") - val (requestInitial, tokenInitial) = requestWithMatchingToken( - beginOffsetExclusive = None, - endOffsetInclusive = None, - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - ) - val token = tokenInitial.update( - _.participantIdChecksum := com.digitalasset.canton.ledger.api.messages.update.UpdatesPageToken - .participantChecksum(Ref.ParticipantId.assertFromString("otherparticipant")) - ) - val request = requestInitial.update(_.pageToken := token.toByteString) - - val result = UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - requestMustFailWith( - result, - code = INVALID_ARGUMENT, - "INVALID_UPDATES_PAGE_TOKEN(8,0): The submitted command contains an invalid page token. Tokens used in GetUpdatesPage requests must be taken from a valid GetUpdatesPageResponse and used with the same EventFormat settings, the same begin and end with the same Canton participant running the same Canton version. Next page token was obtained from an other participant node", - Map.empty, - ) - } - - "reject request with wrong request checksum" in { - val (requestInitial, _) = requestWithMatchingToken( - beginOffsetExclusive = None, - endOffsetInclusive = None, - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - ) - val (_, token) = requestWithMatchingToken( - beginOffsetExclusive = Some(0), - endOffsetInclusive = Some(200), - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - ) - val request = requestInitial.update(_.pageToken := token.toByteString) - - val result = UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - requestMustFailWith( - result, - code = INVALID_ARGUMENT, - "INVALID_UPDATES_PAGE_TOKEN(8,0): The submitted command contains an invalid page token. Tokens used in GetUpdatesPage requests must be taken from a valid GetUpdatesPageResponse and used with the same EventFormat settings, the same begin and end with the same Canton participant running the same Canton version. Next page token was obtained with different request parameters", - Map.empty, - ) - } - - "reject request with strict upper bound exceeding ledger end" in { - val (requestInitial, token) = requestWithMatchingToken( - beginOffsetExclusive = None, - endOffsetInclusive = Some(200L), - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - ) - val request = requestInitial.update(_.pageToken := token.toByteString) - - val result = UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - - requestMustFailWith( - result, - code = OUT_OF_RANGE, - "OFFSET_AFTER_LEDGER_END(12,0): endOffsetInclusive offset (200) is after ledger end (100)", - Map.empty, - ) - } - - "reject request with begin larger than end" in { - val (requestInitial, token) = requestWithMatchingToken( - beginOffsetExclusive = Some(50L), - endOffsetInclusive = Some(10L), - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - ) - val request = requestInitial.update(_.pageToken := token.toByteString) - - val result = UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - - requestMustFailWith( - result, - code = INVALID_ARGUMENT, - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: beginOffsetExclusive is after endOffsetInclusive", - Map.empty, - ) - } - - "generate validated output without continueStreamFromIncl for request without page token" in { - val request = GetUpdatesPageRequest( - beginOffsetExclusive = Some(0L), - endOffsetInclusive = Some(20L), - maxPageSize = None, - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - pageToken = None, - ) - - inside( - UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - ) { case Right(validated) => - validated.startExclusive shouldBe Some(None) - validated.endInclusive shouldBe Some(Offset.tryFromLong(20L)) - validated.continueStreamFromIncl shouldBe None - validated.maxPageSize shouldBe UpdateServiceConfig().defaultUpdatesPageSize.value - validated.descendingOrder shouldBe false - validated.requestChecksum shouldBe com.digitalasset.canton.ledger.api.messages.update.UpdatesPageToken - .requestChecksum(request) - validated.participantChecksum shouldBe com.digitalasset.canton.ledger.api.messages.update.UpdatesPageToken - .participantChecksum(participant) - validated.updateFormat.includeTransactions.value.eventFormat.filtersByParty should have size 1 - } - } - - "generate validated output for ascending request with page token and max page size" in { - val (requestInitial, token) = requestWithMatchingToken( - beginOffsetExclusive = Some(3L), - endOffsetInclusive = Some(20L), - maxPageSize = Some(17), - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = false, - tokenLowestOffsetExclusive = 5L, - tokenHighestOffsetExclusive = 10L, - ) - val request = requestInitial.update(_.pageToken := token.toByteString) - - inside( - UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - ) { case Right(validated) => - validated.startExclusive shouldBe Some(Some(Offset.tryFromLong(3L))) - validated.endInclusive shouldBe Some(Offset.tryFromLong(20L)) - validated.continueStreamFromIncl shouldBe Some(Offset.tryFromLong(11L)) - validated.maxPageSize shouldBe 17 - validated.descendingOrder shouldBe false - validated.requestChecksum shouldBe token.requestChecksum - validated.participantChecksum shouldBe token.participantIdChecksum - } - } - - "generate validated output for descending request with page token" in { - val (requestInitial, token) = requestWithMatchingToken( - beginOffsetExclusive = Some(3L), - endOffsetInclusive = Some(20L), - maxPageSize = Some(17), - updateFormat = buildUpdateFormat(Some(Seq(templateId)), None), - descendingOrder = true, - tokenLowestOffsetExclusive = 5L, - tokenHighestOffsetExclusive = 10L, - ) - val request = requestInitial.update(_.pageToken := token.toByteString) - - inside( - UpdateServiceRequestValidator.validateUpdatesPageRequest( - req = request, - ledgerEnd = Some(Offset.tryFromLong(100L)), - participantId = participant, - updateServiceConfig = UpdateServiceConfig(), - ) - ) { case Right(validated) => - validated.startExclusive shouldBe Some(Some(Offset.tryFromLong(3L))) - validated.endInclusive shouldBe Some(Offset.tryFromLong(20L)) - validated.continueStreamFromIncl shouldBe Some(Offset.tryFromLong(5L)) - validated.maxPageSize shouldBe 17 - validated.descendingOrder shouldBe true - validated.requestChecksum shouldBe token.requestChecksum - validated.participantChecksum shouldBe token.participantIdChecksum - } - } - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidateDisclosedContractsTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidateDisclosedContractsTest.scala deleted file mode 100644 index 7b0233359d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidateDisclosedContractsTest.scala +++ /dev/null @@ -1,387 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.ledger.api.v2.commands.{ - Commands as ProtoCommands, - DisclosedContract as ProtoDisclosedContract, -} -import com.daml.ledger.api.v2.value.Identifier as ProtoIdentifier -import com.digitalasset.canton.crypto.provider.symbolic.SymbolicPureCrypto -import com.digitalasset.canton.crypto.{Salt, SaltSeed, TestHash} -import com.digitalasset.canton.ledger.api.DisclosedContract -import com.digitalasset.canton.ledger.api.validation.ValidateDisclosedContractsTest.{ - api, - lf, - lfContractId, - underTest, -} -import com.digitalasset.canton.logging.{ErrorLoggingContext, NoLogging} -import com.digitalasset.canton.protocol.* -import com.digitalasset.canton.{DefaultDamlValues, LfValue} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.{Bytes, ImmArray, Ref, Time} -import com.digitalasset.daml.lf.transaction.* -import com.digitalasset.daml.lf.value.Value as Lf -import com.digitalasset.daml.lf.value.Value.{ContractId, ValueRecord} -import com.google.protobuf.ByteString -import io.grpc.Status -import org.scalatest.EitherValues -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class ValidateDisclosedContractsTest - extends AnyFlatSpec - with Matchers - with EitherValues - with ValidatorTestUtils { - private implicit val errorLoggingContext: ErrorLoggingContext = NoLogging - - behavior of classOf[ValidateDisclosedContracts].getSimpleName - - it should "validate the disclosed contracts when enabled" in { - underTest.validateCommands(api.protoCommands) shouldBe Right( - lf.expectedDisclosedContracts - ) - } - - it should "fail validation on missing created event blob" in { - val withMissingBlob = - ProtoCommands.defaultInstance.withDisclosedContracts( - scala.Seq( - api.protoDisclosedContract.copy( - createdEventBlob = ByteString.EMPTY - ) - ) - ) - - requestMustFailWith( - request = underTest.validateCommands(withMissingBlob), - code = Status.Code.INVALID_ARGUMENT, - description = - "MISSING_FIELD(8,0): The submitted command is missing a mandatory field: DisclosedContract.createdEventBlob", - metadata = Map.empty, - ) - } - - it should "support absent contract_id" in { - underTest - .validateCommands( - api.protoCommands.copy( - disclosedContracts = scala.Seq( - api.protoDisclosedContract - .copy( - createdEventBlob = - TransactionCoder.encodeFatContractInstance(lf.fatContractInstance).value - ) - .copy(contractId = "") - ) - ) - ) - .value shouldBe lf.expectedDisclosedContracts - } - - it should "support absent template_id" in { - underTest - .validateCommands( - api.protoCommands.copy( - disclosedContracts = scala.Seq( - api.protoDisclosedContract - .copy(templateId = None) - ) - ) - ) - .value shouldBe lf.expectedDisclosedContracts - } - - it should "fail validation on invalid contract_id" in { - val invalidContractId = "invalidContractId" - requestMustFailWith( - request = underTest.validateCommands( - api.protoCommands.copy( - disclosedContracts = scala.Seq( - api.protoDisclosedContract - .copy( - createdEventBlob = - TransactionCoder.encodeFatContractInstance(lf.fatContractInstance).value - ) - .copy(contractId = invalidContractId) - ) - ) - ), - code = Status.Code.INVALID_ARGUMENT, - description = - s"""INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field DisclosedContract.contract_id: cannot parse ContractId "$invalidContractId"""", - metadata = Map.empty, - ) - } - - it should "fail validation on invalid template_id" in { - val invalidTemplateId = ProtoIdentifier("pkgId", "", "entity") - requestMustFailWith( - request = underTest.validateCommands( - api.protoCommands.copy( - disclosedContracts = scala.Seq( - api.protoDisclosedContract - .copy( - createdEventBlob = - TransactionCoder.encodeFatContractInstance(lf.fatContractInstance).value - ) - .copy(templateId = Some(invalidTemplateId)) - ) - ) - ), - code = Status.Code.INVALID_ARGUMENT, - description = - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field module_name: Expected a non-empty string", - metadata = Map.empty, - ) - } - - it should "fail validation when provided contract_id mismatches the one decoded from the created_event_blob" in { - val otherContractId = "00" + "00" * 31 + "ff" - requestMustFailWith( - request = underTest.validateCommands( - api.protoCommands.copy( - disclosedContracts = scala.Seq( - api.protoDisclosedContract - .copy( - createdEventBlob = - TransactionCoder.encodeFatContractInstance(lf.fatContractInstance).value - ) - .copy(contractId = otherContractId) - ) - ) - ), - code = Status.Code.INVALID_ARGUMENT, - description = - s"INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: Mismatch between DisclosedContract.contract_id ($otherContractId) and contract_id from decoded DisclosedContract.created_event_blob (${lfContractId.coid})", - metadata = Map.empty, - ) - } - - it should "fail validation when provided template_id mismatches the one decoded from the created_event_blob" in { - val otherTemplateId = ProtoIdentifier("otherPkgId", "otherModule", "otherEntity") - requestMustFailWith( - request = underTest.validateCommands( - api.protoCommands.copy( - disclosedContracts = scala.Seq( - api.protoDisclosedContract - .copy( - createdEventBlob = - TransactionCoder.encodeFatContractInstance(lf.fatContractInstance).value - ) - .copy(templateId = Some(otherTemplateId)) - ) - ) - ), - code = Status.Code.INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: Mismatch between DisclosedContract.template_id (otherPkgId:otherModule:otherEntity) and template_id from decoded DisclosedContract.created_event_blob (package:module:entity)", - metadata = Map.empty, - ) - } - - it should "fail validation if decoding the created_event_blob fails" in { - requestMustFailWith( - request = underTest.validateCommands( - api.protoCommands.copy( - disclosedContracts = scala.Seq( - api.protoDisclosedContract - .copy( - createdEventBlob = Bytes.assertFromString("00abcd").toByteString - ) - ) - ) - ), - code = Status.Code.INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: Unable to decode disclosed contract event payload: DecodeError(exception com.google.protobuf.InvalidProtocolBufferException: Protocol message contained an invalid tag (zero). while decoding the versioned object)", - metadata = Map.empty, - ) - } - - it should "fail validation on invalid synchronizer_id" in { - requestMustFailWith( - request = underTest.validateCommands( - ProtoCommands.defaultInstance.copy(disclosedContracts = - scala.Seq(api.protoDisclosedContract.copy(synchronizerId = "cantBe!")) - ) - ), - code = Status.Code.INVALID_ARGUMENT, - description = - "INVALID_FIELD(8,0): The submitted command has a field with invalid value: Invalid field DisclosedContract.synchronizer_id: Invalid unique identifier `cantBe!` with missing namespace.", - metadata = Map.empty, - ) - } - - it should "succeed on duplicate contract ids with same payload" in { - val commandsWithDuplicateDisclosedContracts = - ProtoCommands.defaultInstance.copy(disclosedContracts = - scala.Seq( - api.protoDisclosedContract, - api.protoDisclosedContract, - ) - ) - underTest.validateCommands(commandsWithDuplicateDisclosedContracts) shouldBe Right( - lf.expectedDuplicateDisclosedContracts - ) - } - - it should "fail validation on duplicate contract ids with different payloads" in { - val commandsWithDuplicateDisclosedContracts = - ProtoCommands.defaultInstance.copy(disclosedContracts = - scala.Seq( - api.protoDisclosedContract, - api.protoConflictingDisclosedContractDuplicate, - ) - ) - requestMustFailWith( - request = underTest.validateCommands(commandsWithDuplicateDisclosedContracts), - code = Status.Code.INVALID_ARGUMENT, - description = - s"An error occurred. Please contact the operator and inquire about the request with tid ", - metadata = Map.empty, - ) - } - -} - -object ValidateDisclosedContractsTest { - - private val underTest = ValidateDisclosedContracts - - val lfContractId: ContractId.V1 = CantonContractIdVersion.maxV1.fromDiscriminator( - DefaultDamlValues.lfhash(3), - Unicum(TestHash.digest(4)), - ) - - private object api { - val templateId: ProtoIdentifier = - ProtoIdentifier("package", moduleName = "module", entityName = "entity") - val packageName: Ref.PackageName = Ref.PackageName.assertFromString("pkg-name") - val contractId: String = lfContractId.coid - val alice: Ref.Party = Ref.Party.assertFromString("alice") - private val bob: Ref.Party = Ref.Party.assertFromString("bob") - private val charlie: Ref.Party = Ref.Party.assertFromString("charlie") - val stakeholders: Set[Ref.Party] = Set(alice, bob, charlie) - val signatories: Set[Ref.Party] = Set(alice, bob) - val keyMaintainers: Set[Ref.Party] = Set(bob) - val createdAtSeconds = 1337L - val createdAtSeconds2 = 1338L - val protoDisclosedContract: ProtoDisclosedContract = ProtoDisclosedContract( - templateId = Some(templateId), - contractId = contractId, - createdEventBlob = TransactionCoder - .encodeFatContractInstance(lf.fatContractInstance) - .fold( - err => - throw new RuntimeException(s"Cannot serialize createdEventBlob: ${err.errorMessage}"), - identity, - ), - synchronizerId = "", - ) - val protoConflictingDisclosedContractDuplicate: ProtoDisclosedContract = ProtoDisclosedContract( - templateId = Some(templateId), - contractId = contractId, - createdEventBlob = TransactionCoder - .encodeFatContractInstance(lf.fatContractInstance2) - .fold( - err => - throw new RuntimeException(s"Cannot serialize createdEventBlob: ${err.errorMessage}"), - identity, - ), - synchronizerId = "", - ) - - val dupKeyProtoDisclosedContract: ProtoDisclosedContract = protoDisclosedContract.copy( - contractId = lf.dupKeyFatContractInstance.contractId.coid, - createdEventBlob = TransactionCoder - .encodeFatContractInstance(lf.dupKeyFatContractInstance) - .fold( - err => - throw new RuntimeException(s"Cannot serialize createdEventBlob: ${err.errorMessage}"), - identity, - ), - ) - - val protoCommands: ProtoCommands = - ProtoCommands.defaultInstance.copy(disclosedContracts = scala.Seq(api.protoDisclosedContract)) - } - - private object lf { - private val templateId: Ref.Identifier = Ref.Identifier( - Ref.PackageId.assertFromString(api.templateId.packageId), - Ref.QualifiedName( - Ref.ModuleName.assertFromString(api.templateId.moduleName), - Ref.DottedName.assertFromString(api.templateId.entityName), - ), - ) - private val createArg: ValueRecord = - ValueRecord(tycon = None, fields = ImmArray(None -> Lf.ValueTrue)) - - private val seedSalt: SaltSeed = SaltSeed.generate()(new SymbolicPureCrypto()) - private val salt = Salt.tryDeriveSalt(seedSalt, 0, new SymbolicPureCrypto()) - - private val authenticationDataBytes: Bytes = - ContractAuthenticationDataV1(salt)(CantonContractIdVersion.maxV1).toLfBytes - - val keyWithMaintainers: GlobalKeyWithMaintainers = GlobalKeyWithMaintainers.assertBuild( - lf.templateId, - LfValue.ValueRecord( - None, - ImmArray( - None -> LfValue.ValueParty(api.alice), - None -> LfValue.ValueText("some key"), - ), - ), - crypto.Hash.hashPrivateKey("dummy-key-hash"), - api.keyMaintainers, - api.packageName, - ) - - private val createNode: Node.Create = Node.Create( - coid = lfContractId, - templateId = lf.templateId, - packageName = api.packageName, - arg = lf.createArg, - signatories = api.signatories, - stakeholders = api.stakeholders, - keyOpt = Some(lf.keyWithMaintainers), - version = LfSerializationVersion.StableVersions.max, - ) - - private val dupKeyCreateNode = createNode.copy(ExampleContractFactory.buildContractId()) - - def fatContractInstance: LfFatContractInst = FatContractInstance.fromCreateNode( - create = createNode, - createTime = - CreationTime.CreatedAt(Time.Timestamp.assertFromLong(api.createdAtSeconds * 1000000L)), - authenticationData = lf.authenticationDataBytes, - ) - - def fatContractInstance2: LfFatContractInst = FatContractInstance.fromCreateNode( - create = createNode, - createTime = - CreationTime.CreatedAt(Time.Timestamp.assertFromLong(api.createdAtSeconds2 * 1000000L)), - authenticationData = lf.authenticationDataBytes, - ) - - def dupKeyFatContractInstance: LfFatContractInst = FatContractInstance.fromCreateNode( - create = dupKeyCreateNode, - createTime = fatContractInstance.createdAt, - authenticationData = fatContractInstance.authenticationData, - ) - - val expectedDisclosedContracts: ImmArray[DisclosedContract] = ImmArray( - DisclosedContract(fatContractInstance, None) - ) - - val expectedDuplicateDisclosedContracts: ImmArray[DisclosedContract] = ImmArray( - DisclosedContract(fatContractInstance, None), - DisclosedContract(fatContractInstance, None), - ) - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidateUpgradingPackageResolutionsTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidateUpgradingPackageResolutionsTest.scala deleted file mode 100644 index efaac7ed40..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidateUpgradingPackageResolutionsTest.scala +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.ledger.api.validation.ValidateUpgradingPackageResolutions.ValidatedCommandPackageResolutionsSnapshot -import com.digitalasset.canton.logging.{ErrorLoggingContext, NoLogging} -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.store.packagemeta.PackageMetadata.{ - LocalPackagePreference, - PackageResolution, -} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.PackageVersion -import io.grpc.Status.Code.INVALID_ARGUMENT -import io.grpc.StatusRuntimeException -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.Assertion -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.wordspec.AnyWordSpec - -class ValidateUpgradingPackageResolutionsTest - extends AnyWordSpec - with ValidatorTestUtils - with TableDrivenPropertyChecks - with MockitoSugar - with ArgumentMatchersSugar { - - classOf[ValidateUpgradingPackageResolutionsImpl].getSimpleName should { - "validate and correctly output package resolution structures" in new TestScope() { - testResolutions( - userPreference = Seq(p12, p31), - expectedPreferenceSetResult = Right { - // p21 is back-filled - // p31 is user-preference over the default p32 - Set(p12, p21, p31) - }, - ) - } - - "use defaults resolutions if user preference is empty" in new TestScope() { - testResolutions( - userPreference = Seq.empty, - expectedPreferenceSetResult = Right(preferenceMapSnapshot.values.toSet), - ) - } - - "fail on duplicate user preference for same package name" in new TestScope() { - requestMustFailWith( - request = validator(Seq(p11, p12)), - code = INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: duplicate preference for package-name pkgName1: pkgId11 vs pkgId12", - ) - } - - "fail on invalid packageId format" in new TestScope { - requestMustFailWith( - request = validator(Seq("not%valid^pkgId")), - code = INVALID_ARGUMENT, - description = - """INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: package_id_selection_preference parsing failed with `non expected character 0x25 in Daml-LF Package ID "not%valid^pkgId"`. The package_id_selection_preference field must contain non-empty and valid package ids""", - ) - } - - "fail on user-specified package-id not found" in new TestScope { - requestMustFailWith( - request = validator(Seq("nonExistingPackageId")), - code = INVALID_ARGUMENT, - description = - "INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: user-specified pkg id (nonExistingPackageId) could not be found", - ) - } - } - - class TestScope { - protected val pn1 = Ref.PackageName.assertFromString("pkgName1") - protected val pn2 = Ref.PackageName.assertFromString("pkgName2") - protected val pn3 = Ref.PackageName.assertFromString("pkgName3") - protected val p11 = Ref.PackageId.assertFromString("pkgId11") - protected val p12 = Ref.PackageId.assertFromString("pkgId12") - protected val p21 = Ref.PackageId.assertFromString("pkgId21") - protected val p31 = Ref.PackageId.assertFromString("pkgId31") - protected val p32 = Ref.PackageId.assertFromString("pkgId32") - protected val pv1 = Ref.PackageVersion.assertFromString("1") - protected val pv2 = Ref.PackageVersion.assertFromString("2") - - val preferenceMapSnapshot: Map[Ref.PackageName, Ref.PackageId] = - Map(pn1 -> p12, pn2 -> p21, pn3 -> p32) - val packageMapSnapshot: Map[Ref.PackageId, (Ref.PackageName, Ref.PackageVersion)] = - Map( - p11 -> (pn1 -> pv1), - p12 -> (pn1 -> pv2), - p21 -> (pn2 -> pv1), - p31 -> (pn3 -> pv1), - p32 -> (pn3 -> pv2), - ) - - protected implicit val errorLoggingContext: ErrorLoggingContext = NoLogging - - private val getPackageMetadataSnapshot = (_: ErrorLoggingContext) => - PackageMetadata().copy( - packageIdVersionMap = packageMapSnapshot, - packageNameMap = preferenceMapSnapshot.view.mapValues { preferredPackage => - PackageResolution( - preference = LocalPackagePreference( - version = PackageVersion.assertFromString("0.0.0"), // unused - packageId = preferredPackage, - ), - allPackageIdsForName = NonEmpty.mk(Set, preferredPackage), - ) - }.toMap, - ) - protected val validator = new ValidateUpgradingPackageResolutionsImpl( - getPackageMetadataSnapshot - ) - - def testResolutions( - userPreference: Seq[String], - expectedPreferenceSetResult: Either[StatusRuntimeException, Set[Ref.PackageId]], - ): Assertion = - validator(userPreference) shouldBe expectedPreferenceSetResult.map( - // If validation is successful, packageMapSnapshot is the same as the one retrieved from the metadata snapshot - // so we always assert that it is forwarded - ValidatedCommandPackageResolutionsSnapshot(packageMapSnapshot, _) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidatorTestUtils.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidatorTestUtils.scala deleted file mode 100644 index d73d3ed4f9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/validation/ValidatorTestUtils.scala +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.api.validation - -import com.daml.grpc.GrpcStatus -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.messages.update -import com.digitalasset.canton.ledger.api.{CumulativeFilter, InterfaceFilter, TemplateFilter} -import com.digitalasset.canton.protocol.TestUpdateId -import com.digitalasset.canton.serialization.ProtoConverter -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.value.Value.ContractId -import com.google.rpc.error_details -import com.google.rpc.error_details.RetryInfo -import io.grpc.Status.Code -import io.grpc.StatusRuntimeException -import org.scalatest.* -import org.scalatest.matchers.should.Matchers - -import java.time.Duration - -trait ValidatorTestUtils extends Matchers with Inside with OptionValues with EitherValues { - self: Suite => - - protected val includedModule = "includedModule" - protected val includedTemplate = "includedTemplate" - protected val expectedUserId = "expectedUserId" - protected val packageName = Ref.PackageName.assertFromString("somePackageName") - protected val packageNameRefEncoded = Ref.PackageRef.Name(packageName).toString - protected val templateQualifiedName = - Ref.QualifiedName.assertFromString(s"$includedModule:$includedTemplate") - protected val packageId = Ref.PackageId.assertFromString("packageId") - protected val packageId2 = Ref.PackageId.assertFromString("packageId2") - protected val offsetLong = 42L - protected val offset = Some(Offset.tryFromLong(offsetLong)) - protected val party = Ref.Party.assertFromString("party") - protected val party2 = Ref.Party.assertFromString("party2") - protected val verbose = false - protected val updateId = TestUpdateId("42") - protected val ledgerEnd = Some(Offset.tryFromLong(1000)) - protected val contractId = ContractId.V1.assertFromString("00" * 32 + "0001") - protected val moduleName = Ref.ModuleName.assertFromString(includedModule) - protected val dottedName = Ref.DottedName.assertFromString(includedTemplate) - protected val refTemplateId = Ref.Identifier(packageId, templateQualifiedName) - protected val refTemplateId2 = Ref.Identifier(packageId2, templateQualifiedName) - - private val expectedTemplates = Set( - Ref.NameTypeConRef( - Ref.PackageRef.Name(packageName), - Ref.QualifiedName( - Ref.DottedName.assertFromString(includedModule), - Ref.DottedName.assertFromString(includedTemplate), - ), - ) - ) - - protected def hasExpectedFilters( - req: update.GetUpdatesRequest, - expectedTemplates: Set[Ref.NameTypeConRef] = expectedTemplates, - ): Assertion = { - val filtersByParty = req.updateFormat.includeTransactions.value.eventFormat.filtersByParty - filtersByParty should have size 1 - inside(filtersByParty.headOption.value) { case (p, filters) => - p shouldEqual party - filters shouldEqual - CumulativeFilter( - templateFilters = - expectedTemplates.map(TemplateFilter(_, includeCreatedEventBlob = false)), - interfaceFilters = Set( - InterfaceFilter( - interfaceTypeRef = Ref.NameTypeConRef( - Ref.PackageRef.Name(packageName), - Ref.QualifiedName( - Ref.DottedName.assertFromString(includedModule), - Ref.DottedName.assertFromString(includedTemplate), - ), - ), - includeView = true, - includeCreatedEventBlob = true, - ) - ), - templateWildcardFilter = None, - ) - } - } - - protected def requestMustFailWith( - request: Either[StatusRuntimeException, ?], - code: Code, - description: String, - metadata: Map[String, String] = Map.empty, - retryDelay: Seq[Duration] = Seq.empty, - ): Assertion = - inside(request)(isError(code, description, metadata, retryDelay)) - protected def isError( - expectedCode: Code, - expectedDescription: String, - metadata: Map[String, String], - retryDelay: Seq[Duration], - ): PartialFunction[Either[StatusRuntimeException, ?], Assertion] = { case Left(err) => - err.getStatus should have(Symbol("code")(expectedCode)) - err.getStatus should have(Symbol("description")(expectedDescription)) - val details = GrpcStatus - .toProto(err.getStatus, err.getTrailers) - .details - val errorInfoMetadata: Map[String, String] = details - .collect { - case any if any.is[error_details.ErrorInfo] => - any.unpack[error_details.ErrorInfo].metadata - } - .flatten - .toMap - errorInfoMetadata should contain allElementsOf metadata - val retryInfoMetadata: Seq[Duration] = details.collect { - case any if any.is[error_details.RetryInfo] => - val retryDelay = any - .unpack[RetryInfo] - .getRetryDelay - ProtoConverter.DurationConverter.fromProtoPrimitive(retryDelay).value - } - retryInfoMetadata should contain allElementsOf retryDelay - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/client/ResilientLedgerSubscriptionTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/client/ResilientLedgerSubscriptionTest.scala deleted file mode 100644 index 6fdfdb0930..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/client/ResilientLedgerSubscriptionTest.scala +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.client - -import com.daml.ledger.api.v2.transaction.Transaction -import com.daml.ledger.javaapi.data.Party -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.client.ResilientLedgerSubscriptionTest.SubscriptionState -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.ParticipantPrunedDataAccessed -import com.digitalasset.canton.logging.{LogEntry, NoLogging} -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import io.grpc.{Status, StatusRuntimeException} -import org.apache.pekko.NotUsed -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.{Flow, Source} -import org.reactivestreams.{Publisher, Subscriber, Subscription} -import org.scalatest.Assertion -import org.scalatest.wordspec.AnyWordSpec - -import java.util.concurrent.atomic.AtomicReference - -class ResilientLedgerSubscriptionTest extends AnyWordSpec with BaseTest with HasExecutionContext { - - private implicit lazy val actorSystem: ActorSystem = ActorSystem( - "ResilientLedgerSubscriptionTest" - ) - private implicit lazy val materializer: Materializer = Materializer(actorSystem) - - "restartable subscription" should { - "receive txs successfully" when { - "items are dispatched" in new TestContext { - runTest { sut => - val sub = getSubscriberWhenReady() - sub.subscriber.onNext(tx) - eventually() { - received.get() shouldBe Seq(tx) - } - sut.close() - succeed - }(expectedWarningMessages = Seq.empty) - } - } - - "complete with the last error" when { - "closed after a failed subscription" in new TestContext { - runTest { sut => - val sub = getSubscriberWhenReady() - val exception = new StatusRuntimeException(Status.UNAVAILABLE) - sub.subscriber.onError(exception) - sut.close() - sut.subscriptionF.failed.futureValue shouldBe exception - }( - expectedWarningMessages = Seq( - s"Ledger subscription $subscriptionName failed with an error", - s"wait-for-$subscriptionName-completed finished with an error", - ) - ) - } - } - - "resubscribe from the latest observed offset" when { - "the current subscription fails" in new TestContext { - runTest { sut => - val sub = getSubscriberWhenReady() - sub.subscriber.onNext(tx) - val exception = new StatusRuntimeException(Status.UNAVAILABLE) - sub.subscriber.onError(exception) - eventually() { - val next = getSubscriberWhenReady() - next.offset shouldBe reSubscriptionOffset - } - sut.close() - succeed - }( - expectedWarningMessages = Seq( - s"Ledger subscription $subscriptionName failed with an error" - ) - ) - } - } - - "resubscribe from the latest unpruned offset" when { - s"the current subscription fails with a ${ParticipantPrunedDataAccessed.id} error" in new TestContext { - - private val nextOffsetAfterPruned = 17L - runTest { sut => - val sub = getSubscriberWhenReady() - val reject = - ParticipantPrunedDataAccessed - .Reject("some cause", nextOffsetAfterPruned)(NoLogging) - .asGrpcError - sub.subscriber.onError(reject) - - eventually() { - val next = getSubscriberWhenReady() - next.offset shouldBe nextOffsetAfterPruned - } - - sut.close() - succeed - }( - expectedWarningMessages = Seq( - s"due to pruning. Some commands might timeout or events might become stale.", - s"Ledger subscription $subscriptionName failed with an error", - ) - ) - } - } - } - - private[client] trait TestContext { - val serviceName = "TestServiceForResilientTransactionSubscription" - val subscriptionName = "SubscriptionForTestService" - val sender = new Party("alice") - - val initialOffset: Long = 0L - val reSubscriptionOffset: Long = 7L - val tx = Transaction.defaultInstance.copy(offset = reSubscriptionOffset) - - private[client] val subscriber = new AtomicReference[Option[SubscriptionState]]( - None - ) - - def makeSource(offset: Long): Source[Transaction, NotUsed] = - Source.fromPublisher[Transaction](new Publisher[Transaction] { - override def subscribe(s: Subscriber[? >: Transaction]): Unit = { - subscriber.updateAndGet { cur => - Some( - SubscriptionState( - index = cur.map(x => x.index + 1).getOrElse(1), - offset, - s, - request = -1, - cancel = false, - ) - ) - } - - s.onSubscribe(new Subscription { - override def request(n: Long): Unit = - subscriber.updateAndGet(_.map(_.copy(request = n))) - override def cancel(): Unit = subscriber.updateAndGet(_.map(_.copy(cancel = true))) - }) - } - }) - val received = new AtomicReference[Seq[Transaction]](Seq.empty) - - def getSubscriberWhenReady(): SubscriptionState = - eventually() { - val sub = subscriber.get().valueOrFail("subscriber not set") - assert(sub.request > 0) - sub - } - - def runTest(test: ResilientLedgerSubscription[Transaction, Unit] => Assertion)( - expectedWarningMessages: Seq[String] - ): Assertion = - loggerFactory.assertLoggedWarningsAndErrorsSeq( - { - val sut = new ResilientLedgerSubscription[Transaction, Unit]( - makeSource = makeSource, - consumingFlow = Flow[Transaction].map { tx => - received.updateAndGet(_ :+ tx).discard - }, - subscriptionName = subscriptionName, - startOffset = initialOffset, - extractOffset = tx => Some(tx.offset), - timeouts = timeouts, - loggerFactory = loggerFactory, - resubscribeIfPruned = true, - ) - val result = test(sut) - sut.close() - result - }, - LogEntry.assertLogSeq( - expectedWarningMessages.map(expected => - (_.warningMessage should include(expected), expected) - ) - ), - ) - } -} - -object ResilientLedgerSubscriptionTest { - private[client] final case class SubscriptionState( - index: Int, - offset: Long, - subscriber: Subscriber[? >: Transaction], - request: Long, - cancel: Boolean, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/CachedIdentityProviderConfigStoreSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/CachedIdentityProviderConfigStoreSpec.scala deleted file mode 100644 index 5c9aa53feb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/CachedIdentityProviderConfigStoreSpec.scala +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.ledger.localstore.api.IdentityProviderConfigStore.{ - IdentityProviderConfigByIssuerNotFound, - IdentityProviderConfigNotFound, -} -import com.digitalasset.canton.ledger.localstore.api.{ - IdentityProviderConfigStore, - IdentityProviderConfigUpdate, -} -import com.digitalasset.canton.ledger.localstore.{ - CachedIdentityProviderConfigStore, - InMemoryIdentityProviderConfigStore, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.freespec.AsyncFreeSpec - -import scala.concurrent.duration.* - -class CachedIdentityProviderConfigStoreSpec - extends AsyncFreeSpec - with IdentityProviderConfigStoreTests - with MockitoSugar - with ArgumentMatchersSugar - with BaseTest { - - private implicit val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace.ForTesting - - override def newStore(): IdentityProviderConfigStore = createTested( - new InMemoryIdentityProviderConfigStore(loggerFactory) - ) - - private def createTested( - delegate: IdentityProviderConfigStore - ): CachedIdentityProviderConfigStore = - new CachedIdentityProviderConfigStore( - delegate, - cacheExpiryAfterWrite = 1.second, - maximumCacheSize = 10, - LedgerApiServerMetrics.ForTesting, - loggerFactory, - ) - - "test identity-provider-config cache result gets invalidated after new config creation" in { - val delegate = spy(new InMemoryIdentityProviderConfigStore(loggerFactory)) - val tested = createTested(delegate) - val cfg = config() - for { - getYetNonExistent <- tested.getIdentityProviderConfig(cfg.identityProviderId) - _ <- tested.createIdentityProviderConfig(cfg) - get <- tested.getIdentityProviderConfig(cfg.identityProviderId) - } yield { - getYetNonExistent shouldBe Left(IdentityProviderConfigNotFound(cfg.identityProviderId)) - get.value shouldBe cfg - } - } - - "test cache population" in { - val delegate = spy(new InMemoryIdentityProviderConfigStore(loggerFactory)) - val tested = createTested(delegate) - val cfg = config() - for { - _ <- tested.createIdentityProviderConfig(cfg) - res1 <- tested.getIdentityProviderConfig(cfg.issuer) - res2 <- tested.getIdentityProviderConfig(cfg.issuer) - res3 <- tested.getIdentityProviderConfig(cfg.issuer) - res4 <- tested.listIdentityProviderConfigs() - res5 <- tested.listIdentityProviderConfigs() - } yield { - verify(delegate, times(1)).getIdentityProviderConfig(cfg.issuer) - verify(delegate, times(2)).listIdentityProviderConfigs() - res1.value shouldBe cfg - res2.value shouldBe cfg - res3.value shouldBe cfg - res4.value shouldBe Vector(cfg) - res5.value shouldBe Vector(cfg) - } - } - - "test cache invalidation after every write method" in { - val delegate = spy(new InMemoryIdentityProviderConfigStore(loggerFactory)) - val tested = createTested(delegate) - val cfg = config() - for { - _ <- tested.createIdentityProviderConfig(cfg) - res1 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - res2 <- tested.getIdentityProviderConfig(cfg.issuer) - res3 <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - cfg.identityProviderId, - isDeactivatedUpdate = Some(true), - ) - ) - res4 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - res5 <- tested.getIdentityProviderConfig(cfg.issuer) - res6 <- tested.deleteIdentityProviderConfig(cfg.identityProviderId) - res7 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - res8 <- tested.getIdentityProviderConfig(cfg.issuer) - } yield { - val order = inOrder(delegate) - order.verify(delegate, times(1)).createIdentityProviderConfig(cfg) - order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.identityProviderId) - order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.issuer) - order - .verify(delegate, times(1)) - .updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - cfg.identityProviderId, - isDeactivatedUpdate = Some(true), - ) - ) - order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.identityProviderId) - order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.issuer) - order.verify(delegate, times(1)).deleteIdentityProviderConfig(cfg.identityProviderId) - order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.identityProviderId) - order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.issuer) - order.verifyNoMoreInteractions() - res1.value shouldBe cfg - res2.value shouldBe cfg - res3.value shouldBe cfg.copy(isDeactivated = true) - res4.value shouldBe cfg.copy(isDeactivated = true) - res5.value shouldBe cfg.copy(isDeactivated = true) - res6.value shouldBe () - res7 shouldBe Left(IdentityProviderConfigNotFound(cfg.identityProviderId)) - res8 shouldBe Left(IdentityProviderConfigByIssuerNotFound(cfg.issuer)) - } - } - - "listing all users should not be cached" in { - val delegate = spy(new InMemoryIdentityProviderConfigStore(loggerFactory)) - val tested = createTested(delegate) - val cfg1 = config() - val cfg2 = config() - for { - _ <- tested.createIdentityProviderConfig(cfg1) - _ <- tested.createIdentityProviderConfig(cfg2) - res1 <- tested.listIdentityProviderConfigs() - res2 <- tested.listIdentityProviderConfigs() - res3 <- tested.listIdentityProviderConfigs() - } yield { - verify(delegate, times(3)).listIdentityProviderConfigs() - res1.value should contain theSameElementsAs Vector(cfg1, cfg2) - res2.value should contain theSameElementsAs Vector(cfg1, cfg2) - res3.value should contain theSameElementsAs Vector(cfg1, cfg2) - } - } - - "cache entries expire after a set time" in { - val delegate = spy(new InMemoryIdentityProviderConfigStore(loggerFactory)) - val tested = createTested(delegate) - val cfg = config() - for { - _ <- tested.createIdentityProviderConfig(cfg) - res1 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - res1iss <- tested.getIdentityProviderConfig(cfg.issuer) - res2 <- tested.listIdentityProviderConfigs() - - res3 <- { - Threading.sleep(2000) - tested.getIdentityProviderConfig(cfg.identityProviderId) - } - res3iss <- tested.getIdentityProviderConfig(cfg.issuer) - res4 <- tested.listIdentityProviderConfigs() - } yield { - verify(delegate, times(2)).getIdentityProviderConfig(cfg.identityProviderId) - verify(delegate, times(2)).getIdentityProviderConfig(cfg.issuer) - verify(delegate, times(2)).listIdentityProviderConfigs() - res1.value shouldBe cfg - res1iss.value shouldBe cfg - res2.value shouldBe Vector(cfg) - res3.value shouldBe cfg - res3iss.value shouldBe cfg - res4.value shouldBe Vector(cfg) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/CachedUserManagementStoreSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/CachedUserManagementStoreSpec.scala deleted file mode 100644 index 8661906778..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/CachedUserManagementStoreSpec.scala +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.ledger.api.{ - IdentityProviderConfig, - IdentityProviderId, - ObjectMeta, - User, - UserRight, -} -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore.{ - UserInfo, - UserNotFound, - UsersPage, -} -import com.digitalasset.canton.ledger.localstore.api.{ - ObjectMetaUpdate, - UserManagementStore, - UserUpdate, -} -import com.digitalasset.canton.ledger.localstore.{ - CachedUserManagementStore, - InMemoryUserManagementStore, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.daml.lf.data.Ref -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.freespec.AsyncFreeSpec - -import scala.concurrent.Future - -class CachedUserManagementStoreSpec - extends AsyncFreeSpec - with UserStoreTests - with MockitoSugar - with ArgumentMatchersSugar - with BaseTest { - - override def newStore(): UserManagementStore = - createTested(new InMemoryUserManagementStore(createAdmin = false, loggerFactory)) - - def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig): Future[Unit] = - Future.unit - - private val user = User( - id = Ref.UserId.assertFromString("user_id1"), - primaryParty = Some(Ref.Party.assertFromString("primary_party1")), - false, - ObjectMeta.empty, - ) - private val createdUser1 = User( - id = Ref.UserId.assertFromString("user_id1"), - primaryParty = Some(Ref.Party.assertFromString("primary_party1")), - false, - ObjectMeta( - resourceVersionO = Some(0), - annotations = Map.empty, - ), - ) - - private val right1 = UserRight.CanActAs(Ref.Party.assertFromString("party_id1")) - private val right2 = UserRight.ParticipantAdmin - private val right3 = UserRight.CanActAs(Ref.Party.assertFromString("party_id2")) - private val rights = Set(right1, right2) - private val userInfo = UserInfo(user, rights) - private val createdUserInfo = UserInfo(createdUser1, rights) - private val filter: IdentityProviderId = IdentityProviderId.Default - private val idpId = IdentityProviderId.Default - - "test user-not-found cache result gets invalidated after user creation" in { - val delegate = spy(new InMemoryUserManagementStore(loggerFactory = loggerFactory)) - val tested = createTested(delegate) - for { - getYetNonExistent <- tested.getUserInfo(userInfo.user.id, idpId) - _ <- tested.createUser(userInfo.user, userInfo.rights) - get <- tested.getUserInfo(user.id, idpId) - } yield { - getYetNonExistent shouldBe Left(UserNotFound(createdUserInfo.user.id)) - get shouldBe Right(createdUserInfo) - } - } - - "test cache population" in { - val delegate = spy(new InMemoryUserManagementStore(loggerFactory = loggerFactory)) - val tested = createTested(delegate) - - for { - _ <- tested.createUser(userInfo.user, userInfo.rights) - get1 <- tested.getUserInfo(user.id, idpId) - get2 <- tested.getUserInfo(user.id, idpId) - getUser <- tested.getUser(user.id, idpId) - listRights <- tested.listUserRights(user.id, idpId) - } yield { - verify(delegate, times(1)).createUser(userInfo.user, userInfo.rights) - verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId) - verifyNoMoreInteractions(delegate) - get1 shouldBe Right(createdUserInfo) - get2 shouldBe Right(createdUserInfo) - getUser shouldBe Right(createdUserInfo.user) - listRights shouldBe Right(createdUserInfo.rights) - } - } - - "test cache invalidation after every write method" in { - val delegate = spy(new InMemoryUserManagementStore(loggerFactory = loggerFactory)) - val tested = createTested(delegate) - - val userInfo = UserInfo(user, rights) - - for { - _ <- tested.createUser(userInfo.user, userInfo.rights) - get1 <- tested.getUserInfo(user.id, idpId) - _ <- tested.grantRights(user.id, Set(right1), idpId) - get2 <- tested.getUserInfo(user.id, idpId) - _ <- tested.revokeRights(user.id, Set(right3), idpId) - get3 <- tested.getUserInfo(user.id, idpId) - _ <- tested.updateUser( - UserUpdate( - id = user.id, - identityProviderId = IdentityProviderId.Default, - primaryPartyUpdateO = Some(Some(Ref.Party.assertFromString("newPp"))), - metadataUpdate = ObjectMetaUpdate.empty, - ) - ) - get4 <- tested.getUserInfo(user.id, idpId) - _ <- tested.deleteUser(user.id, idpId) - get5 <- tested.getUserInfo(user.id, idpId) - - } yield { - val order = inOrder(delegate) - order.verify(delegate, times(1)).createUser(user, userInfo.rights) - order.verify(delegate, times(1)).getUserInfo(user.id, idpId) - order - .verify(delegate, times(1)) - .grantRights(eqTo(user.id), any[Set[UserRight]], eqTo(idpId))(any[LoggingContextWithTrace]) - order.verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId) - order - .verify(delegate, times(1)) - .revokeRights(eqTo(user.id), any[Set[UserRight]], eqTo(idpId))(any[LoggingContextWithTrace]) - order.verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId) - order.verify(delegate, times(1)).updateUser(any[UserUpdate])(any[LoggingContextWithTrace]) - order.verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId) - order.verify(delegate, times(1)).deleteUser(userInfo.user.id, idpId) - order.verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId) - order.verifyNoMoreInteractions() - get1 shouldBe Right(createdUserInfo) - get2 shouldBe Right(createdUserInfo) - get3 shouldBe Right(createdUserInfo) - get4.value.user.primaryParty shouldBe Some(Ref.Party.assertFromString("newPp")) - get5 shouldBe Left(UserNotFound(createdUser1.id)) - } - } - - "listing all users should not be cached" in { - val delegate = spy( - new InMemoryUserManagementStore( - createAdmin = false, - loggerFactory = loggerFactory, - ) - ) - val tested = createTested(delegate) - - for { - res0 <- tested.createUser(user, rights) - res1 <- tested.listUsers( - fromExcl = None, - maxResults = 100, - identityProviderId = filter, - ) - res2 <- tested.listUsers( - fromExcl = None, - maxResults = 100, - identityProviderId = filter, - ) - } yield { - val order = inOrder(delegate) - order.verify(delegate, times(1)).createUser(user, rights) - order - .verify(delegate, times(2)) - .listUsers(fromExcl = None, maxResults = 100, identityProviderId = filter) - order.verifyNoMoreInteractions() - res0 shouldBe Right(createdUser1) - res1 shouldBe Right(UsersPage(Seq(createdUser1))) - res2 shouldBe Right(UsersPage(Seq(createdUser1))) - } - } - - "cache entries expire after a set time" in { - val delegate = spy(new InMemoryUserManagementStore(loggerFactory = loggerFactory)) - val tested = createTested(delegate) - - for { - create1 <- tested.createUser(user, rights) - get1 <- tested.getUserInfo(user.id, idpId) - get2 <- tested.getUserInfo(user.id, idpId) - get3 <- { - Threading.sleep(2000); tested.getUserInfo(user.id, idpId) - } - } yield { - val order = inOrder(delegate) - order - .verify(delegate, times(1)) - .createUser(any[User], any[Set[UserRight]])(any[LoggingContextWithTrace]) - order - .verify(delegate, times(2)) - .getUserInfo(any[Ref.UserId], eqTo(idpId))(any[LoggingContextWithTrace]) - order.verifyNoMoreInteractions() - create1 shouldBe Right(createdUser1) - get1 shouldBe Right(createdUserInfo) - get2 shouldBe Right(createdUserInfo) - get3 shouldBe Right(createdUserInfo) - } - } - - private def createTested(delegate: InMemoryUserManagementStore): CachedUserManagementStore = - new CachedUserManagementStore( - delegate, - expiryAfterWriteInSeconds = 1, - maximumCacheSize = 10, - LedgerApiServerMetrics.ForTesting, - loggerFactory, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentChangeControlTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentChangeControlTests.scala deleted file mode 100644 index 6ab877091b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentChangeControlTests.scala +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.metrics.DatabaseMetrics -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.store.backend.StorageBackendProvider -import com.digitalasset.canton.platform.store.backend.localstore.ResourceVersionOps -import org.scalatest.freespec.AsyncFreeSpec -import org.scalatest.matchers.should.Matchers - -import java.sql.Connection -import java.util.concurrent.{ConcurrentLinkedQueue, CountDownLatch, Executors} -import scala.concurrent.{ExecutionContext, Future} - -trait ConcurrentChangeControlTests extends PersistentStoreSpecBase with Matchers { - self: AsyncFreeSpec with StorageBackendProvider => - - private[localstore] def testedResourceVersionBackend: ResourceVersionOps - - private[localstore] type ResourceId - private[localstore] type DbResource - - private[localstore] def createAndGetNewResource(initialResourceVersion: Long)( - connection: Connection - ): DbResource - - private[localstore] def fetchResourceVersion(id: ResourceId)(connection: Connection): Long - - private[localstore] def getResourceVersion(resource: DbResource): Long - - private[localstore] def getId(resource: DbResource): ResourceId - - private[localstore] def getDbInternalId(resource: DbResource): Int - - "concurrent change control primitives" - { - - "comparing and increasing resource version should block slower updater-transaction. only faster transaction does a successful update" in { - for { - updateResults <- whenTwoConcurrentTransactionsUpdateTheSameRowFixture( - updateRowFun = (userInternalId, expectedResourceVersion) => - connection => - testedResourceVersionBackend.compareAndIncreaseResourceVersion( - userInternalId, - expectedResourceVersion, - )(connection) - ) - } yield updateResults shouldBe List(101 -> true, 101 -> false) - - } - - "increasing resource version should block slower updater-transaction. both transactions do a successful update" in { - for { - updateResults <- whenTwoConcurrentTransactionsUpdateTheSameRowFixture( - updateRowFun = (userInternalId, _) => - connection => - testedResourceVersionBackend.increaseResourceVersion( - userInternalId - )(connection) - ) - } yield updateResults shouldBe List(101 -> true, 102 -> true) - } - - } - - private def whenTwoConcurrentTransactionsUpdateTheSameRowFixture( - updateRowFun: (Int, Long) => (Connection) => Boolean - ): Future[List[(Long, Boolean)]] = { - import scala.jdk.CollectionConverters.* - - val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) - - val eventsQueue = new ConcurrentLinkedQueue[String]() - // Latches to coordinate the initial state - val barStarted = new CountDownLatch(1) - val fooIssuedUpdateQuery = new CountDownLatch(1) - val barIsAboutToIssueUpdateQuery = new CountDownLatch(1) - // Latches to coordinate an assertion while both transaction are still in-flight. - val updateStatementInFooDone = new CountDownLatch(1) - val externalCheckDone = new CountDownLatch(1) - - for { - // create a user with a known resource version - resource <- inTransaction { connection => - createAndGetNewResource(initialResourceVersion = 100)(connection) - } - _ = getResourceVersion(resource) shouldBe 100 - txA = Future { - barStarted.await() - }(ec).flatMap(_ => - inTransaction { connection => - eventsQueue.add("Foo0") - val updateSucceeded = updateRowFun(getDbInternalId(resource), 100)(connection) - fooIssuedUpdateQuery.countDown() - val resourceVersion = fetchResourceVersion(id = getId(resource))(connection) - barIsAboutToIssueUpdateQuery.await() - eventsQueue.add("Foo1") - updateStatementInFooDone.countDown() - externalCheckDone.await() - resourceVersion -> updateSucceeded - } - )(ec) - txB = inTransaction { connection => - eventsQueue.add("Bar0") - barStarted.countDown() - // NOTE: This is the only countdown latch Bar is waiting on - fooIssuedUpdateQuery.await() - eventsQueue.add("Bar1") - barIsAboutToIssueUpdateQuery.countDown() - val updateSucceeded = updateRowFun(getDbInternalId(resource), 100)(connection) - eventsQueue.add("Bar2") - val resourceVersion = fetchResourceVersion(id = getId(resource))(connection) - resourceVersion -> updateSucceeded - } - _ = updateStatementInFooDone.await() - // NOTE: Sequence is of events {"Bar0", "Foo0", "Bar1"} represents an initial state guaranteed by the countdown latch setup. - // It means both Foo and Bar transaction are in progress and Foo additionally completed an update query. - // NOTE: The thing we are testing for is that Bar cannot finish before Foo, because - // both wrote to the same row and Foo did it faster. - _ = eventsQueue.iterator().asScala.toList shouldBe List("Bar0", "Foo0", "Bar1", "Foo1") - // NOTE: Here we sleep to to give an extra time for Bar to finish to prevent false positives where Bar was simply very slow, rather than being blocked by the DBMS. - _ = Threading.sleep(100) - _ = eventsQueue.iterator().asScala.toList shouldBe List("Bar0", "Foo0", "Bar1", "Foo1") - _ = externalCheckDone.countDown() - updateResults <- Future.sequence(List(txA, txB)) - _ = eventsQueue.iterator().asScala.toList shouldBe List( - "Bar0", - "Foo0", - "Bar1", - "Foo1", - "Bar2", - ) - } yield { - updateResults - } - } - - private def inTransaction[T](thunk: Connection => T): Future[T] = - dbSupport.dbDispatcher - .executeSql(DatabaseMetrics.ForTesting("concurrent change control"))(thunk)( - LoggingContextWithTrace.ForTesting - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentPersistentPartyRecordStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentPersistentPartyRecordStoreTests.scala deleted file mode 100644 index f3fd0fe3b1..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentPersistentPartyRecordStoreTests.scala +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.platform.store.backend.StorageBackendProvider -import com.digitalasset.canton.platform.store.backend.localstore.{ - PartyRecordStorageBackend, - PartyRecordStorageBackendImpl, - ResourceVersionOps, -} -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.freespec.AsyncFreeSpec - -import java.sql.Connection - -trait ConcurrentPersistentPartyRecordStoreTests extends ConcurrentChangeControlTests { - self: AsyncFreeSpec with StorageBackendProvider => - - override private[localstore] def testedResourceVersionBackend: ResourceVersionOps = - PartyRecordStorageBackendImpl - - private[localstore] type ResourceId = Ref.Party - private[localstore] type DbResource = PartyRecordStorageBackend.DbPartyRecord - - private[localstore] override def createAndGetNewResource( - initialResourceVersion: Long - )(connection: Connection): DbResource = { - val id = Ref.Party.assertFromString("party1") - PartyRecordStorageBackendImpl.createPartyRecord( - PartyRecordStorageBackend.DbPartyRecordPayload( - party = id, - identityProviderId = None, - resourceVersion = initialResourceVersion, - createdAt = 0, - ) - )(connection) - PartyRecordStorageBackendImpl.getPartyRecord(id)(connection).value - } - - private[localstore] override def fetchResourceVersion( - id: ResourceId - )(connection: Connection): Long = - PartyRecordStorageBackendImpl.getPartyRecord(id)(connection).value.payload.resourceVersion - - private[localstore] override def getResourceVersion(resource: DbResource): Long = - resource.payload.resourceVersion - - private[localstore] override def getId(resource: DbResource): ResourceId = resource.payload.party - - private[localstore] override def getDbInternalId(resource: DbResource): Int = resource.internalId - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentPersistentUserStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentPersistentUserStoreTests.scala deleted file mode 100644 index d085abb00a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/ConcurrentPersistentUserStoreTests.scala +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.platform.store.backend.StorageBackendProvider -import com.digitalasset.canton.platform.store.backend.localstore.UserManagementStorageBackend.DbUserPayload -import com.digitalasset.canton.platform.store.backend.localstore.{ - ResourceVersionOps, - UserManagementStorageBackend, - UserManagementStorageBackendImpl, -} -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.freespec.AsyncFreeSpec - -import java.sql.Connection - -trait ConcurrentPersistentUserStoreTests extends ConcurrentChangeControlTests { - self: AsyncFreeSpec with StorageBackendProvider => - - override private[localstore] def testedResourceVersionBackend: ResourceVersionOps = - UserManagementStorageBackendImpl - - private[localstore] type ResourceId = Ref.UserId - private[localstore] type DbResource = UserManagementStorageBackend.DbUserWithId - - private[localstore] override def createAndGetNewResource( - initialResourceVersion: Long - )(connection: Connection): DbResource = { - val id = Ref.UserId.assertFromString("user1") - UserManagementStorageBackendImpl.createUser( - DbUserPayload( - id = id, - primaryPartyO = None, - identityProviderId = None, - isDeactivated = false, - primaryPartyAuthentication = false, - resourceVersion = initialResourceVersion, - createdAt = 0, - ) - )(connection) - UserManagementStorageBackendImpl.getUser(id)(connection).value - } - - private[localstore] override def fetchResourceVersion( - id: ResourceId - )(connection: Connection): Long = - UserManagementStorageBackendImpl.getUser(id)(connection).value.payload.resourceVersion - - private[localstore] override def getResourceVersion(resource: DbResource): Long = - resource.payload.resourceVersion - - private[localstore] override def getId(resource: DbResource): ResourceId = resource.payload.id - - private[localstore] override def getDbInternalId(resource: DbResource): Int = resource.internalId - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/DbDispatcherLeftOpsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/DbDispatcherLeftOpsSpec.scala deleted file mode 100644 index c581750a81..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/DbDispatcherLeftOpsSpec.scala +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.ledger.localstore.Ops -import org.mockito.MockitoSugar -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -import java.sql.Connection - -class DbDispatcherLeftOpsSpec extends AnyFreeSpec with MockitoSugar with Matchers { - - "rollbackOnLeft should rollback on left" in { - val conn = mock[Connection] - Ops - .rollbackOnLeft(_ => Left(""))(conn) shouldBe Left("") - - verify(conn, times(1)).rollback() - verifyNoMoreInteractions(conn) - } - - "rollbackOnLeft should not rollback on right" in { - val conn = mock[Connection] - Ops - .rollbackOnLeft(_ => Right(""))(conn) shouldBe Right("") - - verifyZeroInteractions(conn) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/IdentityProviderConfigStoreSpecBase.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/IdentityProviderConfigStoreSpecBase.scala deleted file mode 100644 index 599ad0a007..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/IdentityProviderConfigStoreSpecBase.scala +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.testing.utils.TestResourceContext -import com.digitalasset.canton.ledger.localstore.api.IdentityProviderConfigStore -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, AsyncTestSuite, EitherValues, OptionValues} - -import scala.concurrent.Future - -trait IdentityProviderConfigStoreSpecBase - extends TestResourceContext - with Matchers - with OptionValues - with EitherValues { self: AsyncTestSuite => - - def newStore(): IdentityProviderConfigStore - - final protected def testIt( - f: IdentityProviderConfigStore => Future[Assertion] - ): Future[Assertion] = f(newStore()) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/IdentityProviderConfigStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/IdentityProviderConfigStoreTests.scala deleted file mode 100644 index c1a9cbd63f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/IdentityProviderConfigStoreTests.scala +++ /dev/null @@ -1,421 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import cats.syntax.either.* -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.ledger.localstore.api.IdentityProviderConfigStore.{ - IdentityProviderConfigByIssuerNotFound, - IdentityProviderConfigExists, - IdentityProviderConfigNotFound, - IdentityProviderConfigWithIssuerExists, - TooManyIdentityProviderConfigs, -} -import com.digitalasset.canton.ledger.localstore.api.IdentityProviderConfigUpdate -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.freespec.AsyncFreeSpec - -import java.util.UUID -import scala.concurrent.Future - -trait IdentityProviderConfigStoreTests extends IdentityProviderConfigStoreSpecBase with BaseTest { - self: AsyncFreeSpec => - implicit val lc: LoggingContextWithTrace = - LoggingContextWithTrace.ForTesting - - val MaxIdentityProviderConfigs = 10 - - def config(): IdentityProviderConfig = - IdentityProviderConfig( - identityProviderId = randomId(), - isDeactivated = false, - jwksUrl = JwksUrl.assertFromString("http://example.com/jwks.json"), - issuer = UUID.randomUUID().toString, - audience = Some(UUID.randomUUID().toString), - ) - - def randomId() = { - val id = UUID.randomUUID().toString - IdentityProviderId.Id(Ref.LedgerString.assertFromString(id)) - } - - "identity provider config store" - { - "allows to create and load unchanged an identity provider config" in { - testIt { tested => - val cfg1 = config() - for { - res1 <- tested.createIdentityProviderConfig(cfg1) - } yield { - res1 shouldBe Right(cfg1) - } - } - } - - "disallow to create identity provider config with non unique id" in { - val id = randomId() - testIt { tested => - val cfg1 = config().copy(identityProviderId = id) - val cfg2 = config().copy(identityProviderId = id) - for { - res1 <- tested.createIdentityProviderConfig(cfg1) - res2 <- tested.createIdentityProviderConfig(cfg2) - } yield { - res1 shouldBe Right(cfg1) - res2 shouldBe Left(IdentityProviderConfigExists(id)) - } - } - } - - "disallow to create identity provider config with non unique issuer" in { - testIt { tested => - val cfg1 = config().copy(issuer = "issuer1") - val cfg2 = config().copy(issuer = "issuer1") - for { - res1 <- tested.createIdentityProviderConfig(cfg1) - res2 <- tested.createIdentityProviderConfig(cfg2) - } yield { - res1 shouldBe Right(cfg1) - res2 shouldBe Left(IdentityProviderConfigWithIssuerExists("issuer1")) - } - } - } - - s"disallow to create more than $MaxIdentityProviderConfigs configs" in { - testIt { tested => - for { - res1 <- Future.sequence( - (1 to MaxIdentityProviderConfigs).map(_ => - tested.createIdentityProviderConfig(config()) - ) - ) - last = config() - res2 <- tested.createIdentityProviderConfig(last) - res3 <- tested.getIdentityProviderConfig(last.identityProviderId) - } yield { - res1.forall(_.isRight) shouldBe true - res2 shouldBe Left(TooManyIdentityProviderConfigs()) - // check res3 has not been created - res3 shouldBe Left(IdentityProviderConfigNotFound(last.identityProviderId)) - } - } - } - - "allows to delete an identity provider config" in { - testIt { tested => - val cfg = config() - for { - res1 <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.deleteIdentityProviderConfig(cfg.identityProviderId) - res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - } yield { - res1 shouldBe Right(cfg) - res2 shouldBe Either.unit - res3 shouldBe Left(IdentityProviderConfigNotFound(cfg.identityProviderId)) - } - } - } - - "allow to get identity provider config by id" in { - testIt { tested => - val cfg = config() - val id = randomId() - for { - res1 <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - res3 <- tested.getIdentityProviderConfig(id) - } yield { - res1 shouldBe Right(cfg) - res2 shouldBe Right(cfg) - res3 shouldBe Left(IdentityProviderConfigNotFound(id)) - } - } - } - - "allow to get identity provider config by issuer" in { - testIt { tested => - val cfg = config() - val nonExistingIssuer = "issuer_which_does_not_exist" - for { - res1 <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.getIdentityProviderConfig(cfg.issuer) - res3 <- tested.getIdentityProviderConfig(nonExistingIssuer) - } yield { - res1 shouldBe Right(cfg) - res2 shouldBe Right(cfg) - res3 shouldBe Left(IdentityProviderConfigByIssuerNotFound(nonExistingIssuer)) - } - } - } - - "allow to get active identity provider config by issuer" in { - testIt { tested => - val cfg = config() - val deactivatedConfig = config().copy(isDeactivated = true) - val nonExistingIssuer = "issuer_which_does_not_exist" - for { - res1 <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.createIdentityProviderConfig(deactivatedConfig) - res3 <- tested.getActiveIdentityProviderByIssuer(cfg.issuer) - res4 <- tested.getActiveIdentityProviderByIssuer(nonExistingIssuer).failed - res5 <- tested.getActiveIdentityProviderByIssuer(deactivatedConfig.issuer).failed - } yield { - res1 shouldBe Right(cfg) - res2 shouldBe Right(deactivatedConfig) - res3 shouldBe cfg - res4 shouldBe an[Exception] - res5 shouldBe an[Exception] - } - } - } - - "allow to check if identity provider config by id exists" in { - testIt { tested => - val cfg = config() - val id = randomId() - for { - res1 <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.identityProviderConfigExists(cfg.identityProviderId) - res3 <- tested.identityProviderConfigExists(id) - } yield { - res1 shouldBe Right(cfg) - res2 shouldBe true - res3 shouldBe false - } - } - } - - "fail to delete non-existing identity provider config" in { - val id = randomId() - testIt { tested => - for { - res <- tested.deleteIdentityProviderConfig(id) - } yield { - res shouldBe Left(IdentityProviderConfigNotFound(id)) - } - } - } - - "allows to update nothing" in { - testIt { tested => - val cfg = config() - for { - _ <- tested.createIdentityProviderConfig(cfg) - res <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId - ) - ) - } yield { - res shouldBe Right(cfg) - } - } - } - - "fail to update non existing config" in { - val id = randomId() - testIt { tested => - for { - res <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = id - ) - ) - } yield { - res shouldBe Left(IdentityProviderConfigNotFound(id)) - } - } - } - - "allows to update existing identity provider config's isDeactivated attribute" in { - testIt { tested => - val cfg = config().copy(isDeactivated = false) - for { - _ <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId, - isDeactivatedUpdate = Some(true), - ) - ) - res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - } yield { - res2 shouldBe Right(cfg.copy(isDeactivated = true)) - res3 shouldBe Right(cfg.copy(isDeactivated = true)) - } - } - } - - "allows to update existing identity provider config's jwksUrl attribute" in { - testIt { tested => - val cfg = config().copy(jwksUrl = JwksUrl.assertFromString("http://daml.com/jwks1.json")) - for { - _ <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId, - jwksUrlUpdate = Some(JwksUrl.assertFromString("http://daml.com/jwks2.json")), - ) - ) - res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - } yield { - val expected = cfg.copy(jwksUrl = JwksUrl.assertFromString("http://daml.com/jwks2.json")) - res2 shouldBe Right(expected) - res3 shouldBe Right(expected) - } - } - } - - "allows to update existing identity provider config's audience attribute" in { - testIt { tested => - val cfg = config().copy(audience = Some("audience1")) - for { - _ <- tested.createIdentityProviderConfig(config()) - _ <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId, - audienceUpdate = Some(Some("audience2")), - ) - ) - res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - // no update - res4 <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId, - audienceUpdate = None, - ) - ) - res5 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - // unset the value - res6 <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId, - audienceUpdate = Some(None), - ) - ) - res7 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - } yield { - res2 shouldBe Right(cfg.copy(audience = Some("audience2"))) - res3 shouldBe Right(cfg.copy(audience = Some("audience2"))) - - res4 shouldBe Right(cfg.copy(audience = Some("audience2"))) - res5 shouldBe Right(cfg.copy(audience = Some("audience2"))) - - res6 shouldBe Right(cfg.copy(audience = None)) - res7 shouldBe Right(cfg.copy(audience = None)) - } - } - } - - "allows to update existing identity provider config's issuer attribute" in { - testIt { tested => - val cfg = config().copy(issuer = "issuer1") - for { - _ <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId, - issuerUpdate = Some("issuer2"), - ) - ) - res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - } yield { - res2 shouldBe Right(cfg.copy(issuer = "issuer2")) - res3 shouldBe Right(cfg.copy(issuer = "issuer2")) - } - } - } - - "allows to update existing identity provider config's issuer attribute to the same value" in { - testIt { tested => - val cfg = config().copy(issuer = "issuer1") - for { - _ <- tested.createIdentityProviderConfig(cfg) - res2 <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId, - issuerUpdate = Some("issuer1"), - ) - ) - res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - } yield { - res2 shouldBe Right(cfg.copy(issuer = "issuer1")) - res3 shouldBe Right(cfg.copy(issuer = "issuer1")) - } - } - } - - "allows to update everything at the same time" in { - testIt { tested => - val id = randomId() - val cfg = IdentityProviderConfig( - identityProviderId = id, - isDeactivated = false, - jwksUrl = JwksUrl.assertFromString("http://example.com/jwks.json"), - issuer = UUID.randomUUID().toString, - audience = Some(UUID.randomUUID().toString), - ) - for { - _ <- tested.createIdentityProviderConfig(cfg) - res <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg.identityProviderId, - issuerUpdate = Some("issuer2"), - jwksUrlUpdate = Some(JwksUrl.assertFromString("http://daml.com/jwks2.json")), - isDeactivatedUpdate = Some(true), - audienceUpdate = Some(Some("aud2")), - ) - ) - res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId) - } yield { - val expected = IdentityProviderConfig( - identityProviderId = id, - isDeactivated = true, - jwksUrl = JwksUrl.assertFromString("http://daml.com/jwks2.json"), - issuer = "issuer2", - audience = Some("aud2"), - ) - res shouldBe Right(expected) - res3 shouldBe Right(expected) - } - } - } - - "disallow updating issuer to non-unique value" in { - testIt { tested => - val cfg1 = config().copy(issuer = "issuer1") - val cfg2 = config().copy(issuer = "issuer2") - for { - _ <- tested.createIdentityProviderConfig(cfg1) - _ <- tested.createIdentityProviderConfig(cfg2) - res <- tested.updateIdentityProviderConfig( - IdentityProviderConfigUpdate( - identityProviderId = cfg1.identityProviderId, - issuerUpdate = Some("issuer2"), - ) - ) - } yield { - res shouldBe Left(IdentityProviderConfigWithIssuerExists("issuer2")) - } - } - } - - "allow listing all configs" in { - testIt { tested => - val cfg1 = config().copy(issuer = "issuer1") - val cfg2 = config().copy(issuer = "issuer2") - for { - _ <- tested.createIdentityProviderConfig(cfg1) - _ <- tested.createIdentityProviderConfig(cfg2) - res <- tested.listIdentityProviderConfigs() - } yield { - res.value should contain theSameElementsAs Vector(cfg1, cfg2) - } - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryIdentityProviderConfigStoreSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryIdentityProviderConfigStoreSpec.scala deleted file mode 100644 index fad06ac989..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryIdentityProviderConfigStoreSpec.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.localstore.InMemoryIdentityProviderConfigStore -import org.scalatest.freespec.AsyncFreeSpec - -class InMemoryIdentityProviderConfigStoreSpec - extends AsyncFreeSpec - with IdentityProviderConfigStoreTests - with BaseTest { - - override def newStore() = - new InMemoryIdentityProviderConfigStore(loggerFactory, MaxIdentityProviderConfigs) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryPartyRecordStoreSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryPartyRecordStoreSpec.scala deleted file mode 100644 index 86fc4cc179..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryPartyRecordStoreSpec.scala +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.api.IdentityProviderConfig -import com.digitalasset.canton.ledger.localstore.InMemoryPartyRecordStore -import com.digitalasset.canton.ledger.localstore.api.PartyRecordStore -import org.scalatest.freespec.AsyncFreeSpec - -import scala.concurrent.Future - -class InMemoryPartyRecordStoreSpec extends AsyncFreeSpec with PartyRecordStoreTests with BaseTest { - - override def newStore(): PartyRecordStore = new InMemoryPartyRecordStore( - executionContext = executionContext, - loggerFactory = loggerFactory, - ) - - override def createIdentityProviderConfig( - identityProviderConfig: IdentityProviderConfig - ): Future[Unit] = Future.unit -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryUserManagementStoreSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryUserManagementStoreSpec.scala deleted file mode 100644 index 358dc1a0f8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/InMemoryUserManagementStoreSpec.scala +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.api.IdentityProviderConfig -import com.digitalasset.canton.ledger.localstore.InMemoryUserManagementStore -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import org.scalatest.freespec.AsyncFreeSpec - -import scala.concurrent.Future - -class InMemoryUserManagementStoreSpec extends AsyncFreeSpec with UserStoreTests with BaseTest { - - override def newStore(): UserManagementStore = - new InMemoryUserManagementStore( - createAdmin = false, - loggerFactory = loggerFactory, - ) - - def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig): Future[Unit] = - Future.unit - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreSpecBase.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreSpecBase.scala deleted file mode 100644 index e704ce6b7e..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreSpecBase.scala +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.testing.utils.TestResourceContext -import com.digitalasset.canton.ledger.api.IdentityProviderConfig -import com.digitalasset.canton.ledger.localstore.api.PartyRecordStore -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, AsyncTestSuite, EitherValues, OptionValues} - -import scala.concurrent.Future - -trait PartyRecordStoreSpecBase - extends TestResourceContext - with Matchers - with OptionValues - with EitherValues { self: AsyncTestSuite => - - def newStore(): PartyRecordStore - - def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig): Future[Unit] - - final protected def testIt( - f: PartyRecordStore => Future[Assertion] - ): Future[Assertion] = f( - newStore() - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreTests.scala deleted file mode 100644 index 0a87a2f5fb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreTests.scala +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId, ObjectMeta} -import com.digitalasset.canton.ledger.localstore.api.PartyRecordStore.{ - PartyNotFound, - PartyRecordExistsFatal, -} -import com.digitalasset.canton.ledger.localstore.api.{ - ObjectMetaUpdate, - PartyRecord, - PartyRecordStore, - PartyRecordUpdate, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import org.scalatest.freespec.AsyncFreeSpec - -import scala.language.implicitConversions - -trait PartyRecordStoreTests extends PartyRecordStoreSpecBase { self: AsyncFreeSpec => - - implicit val lc: LoggingContextWithTrace = LoggingContextWithTrace.ForTesting - - private implicit def toParty(s: String): Party = - Party.assertFromString(s) - - private val party1 = "party1" - private val defaultIdpId = IdentityProviderId.Default - private val idpId1 = IdentityProviderId.Id(LedgerString.assertFromString("idp1")) - private val idpId2 = IdentityProviderId.Id(LedgerString.assertFromString("idp2")) - private val idp1 = IdentityProviderConfig( - identityProviderId = idpId1, - isDeactivated = false, - jwksUrl = JwksUrl("http://identityprovider.com/"), - issuer = "issuer", - audience = Some("audience"), - ) - private val idp2 = IdentityProviderConfig( - identityProviderId = idpId2, - isDeactivated = false, - jwksUrl = JwksUrl("http://identityprovider2.com/"), - issuer = "issuer2", - audience = Some("audience"), - ) - - def newPartyRecord( - name: String = party1, - annotations: Map[String, String] = Map.empty, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - ): PartyRecord = - PartyRecord( - party = name, - metadata = ObjectMeta(None, annotations = annotations), - identityProviderId = identityProviderId, - ) - - def createdPartyRecord( - name: String = party1, - annotations: Map[String, String] = Map.empty, - resourceVersion: Long = 0, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - ): PartyRecord = - PartyRecord( - party = name, - metadata = ObjectMeta( - resourceVersionO = Some(resourceVersion), - annotations = annotations, - ), - identityProviderId = identityProviderId, - ) - - def makePartRecordUpdate( - party: Ref.Party = party1, - annotationsUpdateO: Option[Map[String, String]] = None, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - ): PartyRecordUpdate = PartyRecordUpdate( - party = party, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = annotationsUpdateO, - ), - identityProviderId = identityProviderId, - ) - - def resetResourceVersion( - partyRecord: PartyRecord - ): PartyRecord = - partyRecord.copy(metadata = partyRecord.metadata.copy(resourceVersionO = None)) - - "party record store" - { - - "creating" - { - "allow creating a fresh party record" in { - testIt { tested => - for { - _ <- createIdentityProviderConfig(idp1) - create1 <- tested.createPartyRecord(newPartyRecord("party1")) - create2 <- tested.createPartyRecord(newPartyRecord("party2")) - create3 <- tested.createPartyRecord( - newPartyRecord("party3", identityProviderId = idpId1) - ) - } yield { - create1.value shouldBe createdPartyRecord("party1") - create2.value shouldBe createdPartyRecord("party2") - create3.value shouldBe createdPartyRecord( - "party3", - identityProviderId = idpId1, - ) - } - } - } - - "disallow re-creating an existing party record" in { - testIt { tested => - for { - create1 <- tested.createPartyRecord(newPartyRecord("party1")) - create1b <- tested.createPartyRecord(newPartyRecord("party1")) - } yield { - create1.value shouldBe createdPartyRecord("party1") - create1b.left.value shouldBe PartyRecordExistsFatal(create1.value.party) - } - } - } - } - - "getting" - { - "find a freshly created party record" in { - testIt { tested => - val newPr = newPartyRecord("party1") - for { - create1 <- tested.createPartyRecord(newPr) - get1 <- tested.getPartyRecordO(newPr.party) - } yield { - create1.value shouldBe createdPartyRecord("party1") - get1.value shouldBe Some(createdPartyRecord("party1")) - } - } - } - - "return None for a non-existent party record" in { - testIt { tested => - val party = Ref.Party.assertFromString("party1") - for { - get1 <- tested.getPartyRecordO(party) - } yield { - get1.value shouldBe None - } - } - } - } - - "updating" - { - "update an existing party record" in { - testIt { tested => - val pr1 = newPartyRecord("party1") - for { - create1 <- tested.createPartyRecord(pr1) - _ = create1.value shouldBe createdPartyRecord("party1") - update1 <- tested.updatePartyRecord( - partyRecordUpdate = PartyRecordUpdate( - party = pr1.party, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = create1.value.metadata.resourceVersionO, - annotationsUpdateO = Some(Map("k1" -> "v1")), - ), - identityProviderId = IdentityProviderId.Default, - ), - ledgerPartyIsLocal = true, - ) - _ = resetResourceVersion(update1.value) shouldBe newPartyRecord( - "party1", - annotations = Map("k1" -> "v1"), - ) - } yield succeed - } - } - "should succeed when updating a non-existing party record for which a ledger party exists" in { - testIt { tested => - val party = Ref.Party.assertFromString("party1") - for { - _ <- createIdentityProviderConfig(idp1) - update1 <- tested.updatePartyRecord( - partyRecordUpdate = PartyRecordUpdate( - party = party, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = Some( - Map( - "k1" -> "v1", - "k2" -> "v2", - ) - ), - ), - identityProviderId = idpId1, - ), - ledgerPartyIsLocal = true, - ) - _ = update1.value shouldBe createdPartyRecord( - "party1", - resourceVersion = 0, - annotations = Map("k1" -> "v1", "k2" -> "v2"), - identityProviderId = idpId1, - ) - } yield succeed - } - } - - "should add, update and remove annotations" in { - testIt { tested => - val pr = createdPartyRecord( - "party1", - annotations = Map("k1" -> "v1", "k2" -> "v2", "k3" -> "v3"), - ) - for { - _ <- tested.createPartyRecord( - partyRecord = pr - ) - // first update: with merge annotations semantics - update1 <- tested.updatePartyRecord( - partyRecordUpdate = PartyRecordUpdate( - party = pr.party, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = Some( - Map( - // updating - "k1" -> "v1b", - // deleting - "k3" -> "", - // adding - "k4" -> "v4", - ) - ), - ), - identityProviderId = IdentityProviderId.Default, - ), - ledgerPartyIsLocal = true, - ) - _ = update1.value shouldBe createdPartyRecord( - "party1", - resourceVersion = 1, - annotations = Map("k1" -> "v1b", "k2" -> "v2", "k4" -> "v4"), - ) - } yield { - succeed - } - } - } - - "should raise an error when updating a non-existing party record for which a ledger party doesn't exist" in { - testIt { tested => - val party = Ref.Party.assertFromString("party") - for { - res1 <- tested.updatePartyRecord( - partyRecordUpdate = PartyRecordUpdate( - party = party, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = Some(Map("k1" -> "v1")), - ), - identityProviderId = IdentityProviderId.Default, - ), - ledgerPartyIsLocal = false, - ) - _ = res1.left.value shouldBe PartyRecordStore.PartyNotFound(party) - } yield succeed - } - } - - "should raise an error on resource version mismatch" in { - testIt { tested => - val pr = createdPartyRecord("party1") - for { - _ <- tested.createPartyRecord(pr) - res1 <- tested.updatePartyRecord( - PartyRecordUpdate( - party = pr.party, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = Some(100), - annotationsUpdateO = Some(Map("k1" -> "v1")), - ), - identityProviderId = IdentityProviderId.Default, - ), - ledgerPartyIsLocal = true, - ) - _ = res1.left.value shouldBe PartyRecordStore.ConcurrentPartyUpdate(pr.party) - } yield succeed - } - } - } - - "raise an error when annotations byte size max size exceeded" - { - // This value consumes just a bit over half the allowed max size limit - val bigValue = "big value:" + ("a" * 128 * 1024) - - "when creating a party record" in { - testIt { tested => - val pr = newPartyRecord("party1", annotations = Map("k1" -> bigValue, "k2" -> bigValue)) - for { - res1 <- tested.createPartyRecord(pr) - _ = res1.left.value shouldBe PartyRecordStore.MaxAnnotationsSizeExceeded(pr.party) - } yield succeed - } - } - - "when updating an existing party record" in { - testIt { tested => - val pr = newPartyRecord("party1", annotations = Map("k1" -> bigValue)) - for { - _ <- tested.createPartyRecord(pr) - res1 <- tested.updatePartyRecord( - makePartRecordUpdate(annotationsUpdateO = Some(Map("k2" -> bigValue))), - ledgerPartyIsLocal = true, - ) - _ = res1.left.value shouldBe PartyRecordStore.MaxAnnotationsSizeExceeded(pr.party) - } yield succeed - } - } - - "when updating non-existent party record" in { - testIt { tested => - val party = Ref.Party.assertFromString("party1") - for { - res1 <- tested.updatePartyRecord( - makePartRecordUpdate(annotationsUpdateO = - Some(Map("k1" -> bigValue, "k2" -> bigValue)) - ), - ledgerPartyIsLocal = true, - ) - _ = res1.left.value shouldBe PartyRecordStore.MaxAnnotationsSizeExceeded(party) - } yield succeed - } - } - } - - } - - "reassigning idp" - { - "change party's idp" in { - testIt { tested => - for { - create <- tested.createPartyRecord( - newPartyRecord("p1", identityProviderId = defaultIdpId) - ) - _ = create.value shouldBe createdPartyRecord("p1", identityProviderId = defaultIdpId) - _ <- createIdentityProviderConfig(idp1) - updated <- tested.updatePartyRecordIdp( - sourceIdp = defaultIdpId, - targetIdp = idpId1, - party = create.value.party, - ledgerPartyIsLocal = true, - ) - _ <- updated.value.identityProviderId shouldBe idpId1 - } yield succeed - } - } - - "when using wrong source idp id" in { - testIt { tested => - for { - _ <- createIdentityProviderConfig(idp1) - _ <- createIdentityProviderConfig(idp2) - create <- tested.createPartyRecord(newPartyRecord("p1", identityProviderId = idpId1)) - _ = create.value shouldBe createdPartyRecord("p1", identityProviderId = idpId1) - updateResult <- tested.updatePartyRecordIdp( - sourceIdp = idpId2, - targetIdp = defaultIdpId, - party = create.value.party, - ledgerPartyIsLocal = true, - ) - _ <- updateResult.left.value shouldBe PartyNotFound(create.value.party) - } yield succeed - } - } - - "cannot change idp for non-existent party-record for non-local party" in { - testIt { tested => - val party = Ref.Party.assertFromString("party") - for { - _ <- createIdentityProviderConfig(idp1) - updated <- tested.updatePartyRecordIdp( - sourceIdp = defaultIdpId, - targetIdp = idpId1, - party = party, - ledgerPartyIsLocal = false, - ) - _ <- updated.left.value shouldBe PartyNotFound(party) - } yield succeed - } - } - - "can change idp for non-existent party-record for local party" in { - testIt { tested => - val party = Ref.Party.assertFromString("party") - for { - _ <- createIdentityProviderConfig(idp1) - updated <- tested.updatePartyRecordIdp( - sourceIdp = defaultIdpId, - targetIdp = idpId1, - party = party, - ledgerPartyIsLocal = true, - ) - _ <- updated.value.identityProviderId shouldBe idpId1 - } yield succeed - } - } - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreSpecH2.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreSpecH2.scala deleted file mode 100644 index bb0a039624..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreSpecH2.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.platform.store.backend.StorageBackendProviderH2 -import org.scalatest.freespec.AsyncFreeSpec - -class PersistentIdentityProviderConfigStoreSpecH2 - extends AsyncFreeSpec - with PersistentIdentityProviderConfigStoreTests - with StorageBackendProviderH2 diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreSpecPostgres.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreSpecPostgres.scala deleted file mode 100644 index c8d1eb6d16..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreSpecPostgres.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.platform.store.backend.StorageBackendProviderPostgres -import org.scalatest.freespec.AsyncFreeSpec - -class PersistentIdentityProviderConfigStoreSpecPostgres - extends AsyncFreeSpec - with PersistentIdentityProviderConfigStoreTests - with StorageBackendProviderPostgres diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreTests.scala deleted file mode 100644 index 2703fba24b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentIdentityProviderConfigStoreTests.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.ledger.localstore.PersistentIdentityProviderConfigStore -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.backend.StorageBackendProvider -import org.scalatest.freespec.AsyncFreeSpec - -trait PersistentIdentityProviderConfigStoreTests - extends PersistentStoreSpecBase - with IdentityProviderConfigStoreTests { - self: AsyncFreeSpec with StorageBackendProvider => - - override def newStore() = new PersistentIdentityProviderConfigStore( - dbSupport = dbSupport, - metrics = LedgerApiServerMetrics.ForTesting, - maxIdentityProviders = MaxIdentityProviderConfigs, - loggerFactory = loggerFactory, - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreSpecH2.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreSpecH2.scala deleted file mode 100644 index 9dd4e567f5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreSpecH2.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.platform.store.backend.StorageBackendProviderH2 -import org.scalatest.freespec.AsyncFreeSpec - -class PersistentPartyRecordStoreSpecH2 - extends AsyncFreeSpec - with PersistentPartyRecordStoreTests - with StorageBackendProviderH2 diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreSpecPostgres.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreSpecPostgres.scala deleted file mode 100644 index 099f70bd87..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreSpecPostgres.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.platform.store.backend.StorageBackendProviderPostgres -import org.scalatest.freespec.AsyncFreeSpec - -class PersistentPartyRecordStoreSpecPostgres - extends AsyncFreeSpec - with PersistentPartyRecordStoreTests - with ConcurrentPersistentPartyRecordStoreTests - with StorageBackendProviderPostgres diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreTests.scala deleted file mode 100644 index 5d4a198ff8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentPartyRecordStoreTests.scala +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.ledger.api.IdentityProviderConfig -import com.digitalasset.canton.ledger.api.util.TimeProvider -import com.digitalasset.canton.ledger.localstore.{ - PersistentIdentityProviderConfigStore, - PersistentPartyRecordStore, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.backend.StorageBackendProvider -import org.scalatest.freespec.AsyncFreeSpec - -import scala.concurrent.Future - -trait PersistentPartyRecordStoreTests extends PersistentStoreSpecBase with PartyRecordStoreTests { - self: AsyncFreeSpec with StorageBackendProvider => - - override def newStore(): PersistentPartyRecordStore = - new PersistentPartyRecordStore( - dbSupport = dbSupport, - metrics = LedgerApiServerMetrics.ForTesting, - timeProvider = TimeProvider.UTC, - executionContext = executionContext, - loggerFactory = loggerFactory, - ) - - def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig): Future[Unit] = - new PersistentIdentityProviderConfigStore( - dbSupport, - LedgerApiServerMetrics.ForTesting, - 10, - loggerFactory, - )( - executionContext - ) - .createIdentityProviderConfig(identityProviderConfig)(loggingContext) - .flatMap { - case Left(error) => Future.failed(new Exception(error.toString)) - case Right(_) => Future.unit - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentStoreSpecBase.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentStoreSpecBase.scala deleted file mode 100644 index 9dd93b6964..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentStoreSpecBase.scala +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.ledger.resources.ResourceContext -import com.daml.metrics.DatabaseMetrics -import com.daml.resources.Resource -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{LoggingContextWithTrace, SuppressingLogger} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.config.ServerRole -import com.digitalasset.canton.platform.store.DbSupport.{ConnectionPoolConfig, DbConfig} -import com.digitalasset.canton.platform.store.backend.StorageBackendProvider -import com.digitalasset.canton.platform.store.{DbSupport, FlywayMigrations} -import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite} - -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger -import scala.concurrent.duration.* -import scala.concurrent.{Await, ExecutionContext, Future} - -/** Base class for running persistent store-level tests. - * - * Features: - * - Before all test cases creates a new database and applies the flyway migrations to it. - * - Before each test case resets the contents of the database. - * - Ensures that at most one test case runs at a time. - */ -trait PersistentStoreSpecBase extends BaseTest with BeforeAndAfterEach with BeforeAndAfterAll { - this: Suite & StorageBackendProvider => - - implicit protected val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace.ForTesting - - override val loggerFactory: SuppressingLogger = SuppressingLogger(getClass) - private val runningTests = new AtomicInteger(0) - private val thisSimpleName = getClass.getSimpleName - - protected var dbSupport: DbSupport = _ - protected var dbSupportResource: Resource[ResourceContext, DbSupport] = _ - - // Each test should start with an empty database to allow testing low-level behavior - // However, creating a fresh database for each test would be too expensive. - // Instead, we truncate all tables using the reset() call before each test. - override protected def beforeEach(): Unit = { - super.beforeEach() - val dbMetrics = DatabaseMetrics.ForTesting(getClass.getSimpleName) - val resetDbF = dbSupport.dbDispatcher.executeSql(dbMetrics) { connection => - backend.reset.resetAll(connection) - } - Await.ready(resetDbF, 10.seconds).discard - assert( - runningTests.incrementAndGet() == 1, - s"$thisSimpleName tests must not run in parallel, as they all run against the same database.", - ) - () - } - - override protected def afterEach(): Unit = { - assert( - runningTests.decrementAndGet() == 0, - s"$thisSimpleName tests must not run in parallel, as they all run against the same database.", - ) - super.afterEach() - } - - override protected def afterAll(): Unit = { - Await.ready(dbSupportResource.release(), 15.seconds).discard - super.afterAll() - } - - override protected def beforeAll(): Unit = { - super.beforeAll() - implicit val executionContext: ExecutionContext = ExecutionContext.fromExecutor( - Executors.newFixedThreadPool( - 2 - ) - ) - implicit val resourceContext: ResourceContext = ResourceContext(executionContext) - dbSupportResource = DbSupport - .owner( - dbConfig = DbConfig( - jdbcUrl, - connectionPool = ConnectionPoolConfig( - connectionPoolSize = 2, - connectionTimeout = 250.millis, - ), - ), - serverRole = ServerRole.Testing(getClass), - metrics = LedgerApiServerMetrics.ForTesting, - loggerFactory = loggerFactory, - ) - .acquire() - val initializeDbAndGetDbSupportFuture: Future[DbSupport] = for { - dbSupport <- dbSupportResource.asFuture - _ = logger.info(s"$thisSimpleName About to do Flyway migrations") - _ <- new FlywayMigrations(jdbcUrl, loggerFactory = loggerFactory).migrate() - _ = logger.info(s"$thisSimpleName Completed Flyway migrations") - } yield dbSupport - dbSupport = Await.result(initializeDbAndGetDbSupportFuture, 2.minutes) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreSpecH2.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreSpecH2.scala deleted file mode 100644 index 36c2a54a11..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreSpecH2.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.platform.store.backend.StorageBackendProviderH2 -import org.scalatest.freespec.AsyncFreeSpec - -class PersistentUserStoreSpecH2 - extends AsyncFreeSpec - with PersistentUserStoreTests - with StorageBackendProviderH2 diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreSpecPostgres.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreSpecPostgres.scala deleted file mode 100644 index 90920cb97b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreSpecPostgres.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.platform.store.backend.StorageBackendProviderPostgres -import org.scalatest.freespec.AsyncFreeSpec - -class PersistentUserStoreSpecPostgres - extends AsyncFreeSpec - with PersistentUserStoreTests - with ConcurrentPersistentUserStoreTests - with StorageBackendProviderPostgres diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreTests.scala deleted file mode 100644 index 8109ad00dd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PersistentUserStoreTests.scala +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.digitalasset.canton.ledger.api.IdentityProviderConfig -import com.digitalasset.canton.ledger.api.util.TimeProvider -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import com.digitalasset.canton.ledger.localstore.{ - PersistentIdentityProviderConfigStore, - PersistentUserManagementStore, -} -import com.digitalasset.canton.lifecycle.FlagCloseable -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.backend.StorageBackendProvider -import org.scalatest.freespec.AsyncFreeSpec - -import scala.concurrent.Future - -trait PersistentUserStoreTests extends PersistentStoreSpecBase with UserStoreTests { - self: AsyncFreeSpec with StorageBackendProvider => - - override def newStore(): UserManagementStore = - new PersistentUserManagementStore( - dbSupport = dbSupport, - metrics = LedgerApiServerMetrics.ForTesting, - timeProvider = TimeProvider.UTC, - maxRightsPerUser = 100, - loggerFactory = loggerFactory, - FlagCloseable(logger, timeouts), - ) - - def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig): Future[Unit] = - new PersistentIdentityProviderConfigStore( - dbSupport, - LedgerApiServerMetrics.ForTesting, - 10, - loggerFactory, - ) - .createIdentityProviderConfig(identityProviderConfig)(loggingContext) - .flatMap { - case Left(error) => Future.failed(new Exception(error.toString)) - case Right(_) => Future.unit - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreSpecBase.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreSpecBase.scala deleted file mode 100644 index c16767a35d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreSpecBase.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import com.daml.testing.utils.TestResourceContext -import com.digitalasset.canton.ledger.api.IdentityProviderConfig -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, AsyncTestSuite, EitherValues, OptionValues} - -import scala.concurrent.Future - -trait UserStoreSpecBase - extends TestResourceContext - with Matchers - with OptionValues - with EitherValues { self: AsyncTestSuite => - - def newStore(): UserManagementStore - - def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig): Future[Unit] - - final protected def testIt(f: UserManagementStore => Future[Assertion]): Future[Assertion] = f( - newStore() - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreTests.scala deleted file mode 100644 index 1c2dc28d23..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreTests.scala +++ /dev/null @@ -1,734 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.localstore - -import cats.syntax.either.* -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.api.{ - IdentityProviderConfig, - IdentityProviderId, - ObjectMeta, - User, - UserRight, -} -import com.digitalasset.canton.ledger.localstore.api.UserManagementStore.* -import com.digitalasset.canton.ledger.localstore.api.{ - ObjectMetaUpdate, - UserManagementStore, - UserUpdate, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{LedgerString, Party, UserId} -import org.scalatest.freespec.AsyncFreeSpec - -import scala.concurrent.Future -import scala.language.implicitConversions - -/** Common tests for implementations of [[UserManagementStore]] - */ -trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec => - - implicit val lc: LoggingContextWithTrace = - LoggingContextWithTrace.ForTesting - - private implicit def toParty(s: String): Party = - Party.assertFromString(s) - - private implicit def toUserId(s: String): UserId = - UserId.assertFromString(s) - - private val userId1 = "user1" - - private val idpId1 = IdentityProviderId.Id(LedgerString.assertFromString("idp1")) - private val idpId2 = IdentityProviderId.Id(LedgerString.assertFromString("idp2")) - private val defaultIdpId = IdentityProviderId.Default - private val idp1 = IdentityProviderConfig( - identityProviderId = idpId1, - isDeactivated = false, - jwksUrl = JwksUrl("http://identityprovider.com/"), - issuer = "issuer", - audience = Some("audience"), - ) - private val idp2 = IdentityProviderConfig( - identityProviderId = idpId2, - isDeactivated = false, - jwksUrl = JwksUrl("http://identityprovider2.com/"), - issuer = "issuer2", - audience = Some("audience"), - ) - - def newUser( - name: String = userId1, - primaryParty: Option[Ref.Party] = None, - isDeactivated: Boolean = false, - annotations: Map[String, String] = Map.empty, - identityProviderId: IdentityProviderId = defaultIdpId, - ): User = User( - id = name, - primaryParty = primaryParty, - isDeactivated = isDeactivated, - metadata = ObjectMeta( - resourceVersionO = None, - annotations = annotations, - ), - identityProviderId = identityProviderId, - ) - - def createdUser( - name: String = userId1, - primaryParty: Option[Ref.Party] = None, - isDeactivated: Boolean = false, - resourceVersion: Long = 0, - annotations: Map[String, String] = Map.empty, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - ): User = - User( - id = name, - primaryParty = primaryParty, - isDeactivated = isDeactivated, - metadata = ObjectMeta( - resourceVersionO = Some(resourceVersion), - annotations = annotations, - ), - identityProviderId = identityProviderId, - ) - - def makeUserUpdate( - id: String = userId1, - primaryPartyUpdateO: Option[Option[Ref.Party]] = None, - isDeactivatedUpdateO: Option[Boolean] = None, - annotationsUpdateO: Option[Map[String, String]] = None, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - ): UserUpdate = UserUpdate( - id = id, - primaryPartyUpdateO = primaryPartyUpdateO, - isDeactivatedUpdateO = isDeactivatedUpdateO, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = annotationsUpdateO, - ), - identityProviderId = identityProviderId, - ) - - def resetResourceVersion( - user: User - ): User = - user.copy(metadata = user.metadata.copy(resourceVersionO = None)) - - "user management" - { - - "allow creating a fresh user" in { - testIt { tested => - for { - res1 <- tested.createUser(newUser(s"user1"), Set.empty) - res2 <- tested.createUser(newUser("user2"), Set.empty) - _ <- createIdentityProviderConfig(idp1) - res3 <- tested.createUser( - newUser("user3", identityProviderId = idpId1), - Set.empty, - ) - } yield { - res1 shouldBe Right(createdUser("user1")) - res2 shouldBe Right(createdUser("user2")) - res3 shouldBe Right( - createdUser("user3", identityProviderId = idpId1) - ) - } - } - } - - "disallow re-creating an existing user" in { - testIt { tested => - val user = newUser("user1") - for { - res1 <- tested.createUser(user, Set.empty) - res2 <- tested.createUser(user, Set.empty) - } yield { - res1 shouldBe Right(createdUser("user1")) - res2 shouldBe Left(UserExists(user.id)) - } - } - } - - "disallow re-creating an existing user concurrently" in { - testIt { tested => - val user = newUser("user1") - for { - res <- Future.sequence( - Seq(tested.createUser(user, Set.empty), tested.createUser(user, Set.empty)) - ) - } yield { - res should contain(Left(UserExists(user.id))) - } - } - } - - "disallow re-creating an existing user concurrently" in { - testIt { tested => - val user = newUser("user1") - for { - res <- Future.sequence( - Seq(tested.createUser(user, Set.empty), tested.createUser(user, Set.empty)) - ) - } yield { - res should contain(Left(UserExists(user.id))) - } - } - } - - "deny permission re-creating an existing user within another IDP" in { - testIt { tested => - val user = newUser("user1") - for { - res1 <- tested.createUser(user, Set.empty) - res2 <- tested.createUser( - user.copy(identityProviderId = idpId1), - Set.empty, - ) - } yield { - res1 shouldBe Right(createdUser("user1")) - res2 shouldBe Left(PermissionDenied(user.id)) - } - } - } - - "find a freshly created user" in { - testIt { tested => - val user = newUser("user1") - for { - res1 <- tested.createUser(user, Set.empty) - user1 <- tested.getUser(user.id, defaultIdpId) - } yield { - res1 shouldBe Right(createdUser("user1")) - user1 shouldBe res1 - } - } - } - - "deny to find a freshly created user within another IDP" in { - testIt { tested => - val user = newUser("user1") - for { - res1 <- tested.createUser(user, Set.empty) - user1 <- tested.getUser(user.id, idpId1) - } yield { - res1 shouldBe Right(createdUser("user1")) - user1 shouldBe Left(PermissionDenied(user.id)) - } - } - } - - "not find a non-existent user" in { - testIt { tested => - val userId: Ref.UserId = "user1" - for { - user1 <- tested.getUser(userId, defaultIdpId) - } yield { - user1 shouldBe Left(UserNotFound(userId)) - } - } - } - "not find a deleted user" in { - testIt { tested => - val user = newUser("user1") - for { - res1 <- tested.createUser(user, Set.empty) - user1 <- tested.getUser("user1", defaultIdpId) - res2 <- tested.deleteUser("user1", defaultIdpId) - user2 <- tested.getUser("user1", defaultIdpId) - } yield { - res1 shouldBe Right(createdUser("user1")) - user1 shouldBe res1 - res2 shouldBe Either.unit - user2 shouldBe Left(UserNotFound("user1")) - } - } - } - "deny to delete user within another IDP" in { - testIt { tested => - val user = newUser("user1") - for { - res1 <- tested.createUser(user, Set.empty) - user1 <- tested.getUser("user1", idpId1) - } yield { - res1 shouldBe Right(createdUser("user1")) - user1 shouldBe Left(PermissionDenied(user.id)) - } - } - } - "allow recreating a deleted user" in { - testIt { tested => - val user = newUser("user1") - for { - res1 <- tested.createUser(user, Set.empty) - res2 <- tested.deleteUser(user.id, defaultIdpId) - res3 <- tested.createUser(user, Set.empty) - } yield { - res1 shouldBe Right(createdUser("user1")) - res2 shouldBe Either.unit - res3 shouldBe Right(createdUser("user1")) - } - - } - } - "fail to delete a non-existent user" in { - testIt { tested => - for { - res1 <- tested.deleteUser("user1", defaultIdpId) - } yield { - res1 shouldBe Left(UserNotFound("user1")) - } - } - } - - "list created users" in { - testIt { tested => - for { - _ <- tested.createUser(newUser("user1"), Set.empty) - _ <- tested.createUser(newUser("user2"), Set.empty) - _ <- tested.createUser(newUser("user3"), Set.empty) - _ <- tested.createUser(newUser("user4"), Set.empty) - list1 <- tested.listUsers( - fromExcl = None, - maxResults = 3, - identityProviderId = IdentityProviderId.Default, - ) - _ = list1 shouldBe Right( - UsersPage( - Seq( - createdUser("user1"), - createdUser("user2"), - createdUser("user3"), - ) - ) - ) - list2 <- tested.listUsers( - fromExcl = list1.getOrElse(fail("Expecting a Right()")).lastUserIdOption, - maxResults = 4, - identityProviderId = IdentityProviderId.Default, - ) - _ = list2 shouldBe Right(UsersPage(Seq(createdUser("user4")))) - } yield { - succeed - } - } - } - "not list deleted users" in { - testIt { tested => - for { - res1 <- tested.createUser(newUser("user1"), Set.empty) - res2 <- tested.createUser(newUser("user2"), Set.empty) - users1 <- tested.listUsers( - fromExcl = None, - maxResults = 10000, - identityProviderId = IdentityProviderId.Default, - ) - res3 <- tested.deleteUser("user1", defaultIdpId) - users2 <- tested.listUsers( - fromExcl = None, - maxResults = 10000, - identityProviderId = IdentityProviderId.Default, - ) - } yield { - res1 shouldBe Right(createdUser("user1")) - res2 shouldBe Right(createdUser("user2")) - users1 shouldBe Right( - UsersPage( - Seq( - createdUser("user1"), - createdUser("user2"), - ) - ) - ) - res3 shouldBe Either.unit - users2 shouldBe Right(UsersPage(Seq(createdUser("user2")))) - - } - } - } - "list users within idp" in { - testIt { tested => - for { - _ <- createIdentityProviderConfig(idp1) - _ <- tested.createUser( - newUser("user1", identityProviderId = idpId1), - Set.empty, - ) - _ <- tested.createUser(newUser("user2"), Set.empty) - _ <- tested.createUser(newUser("user3"), Set.empty) - _ <- tested.createUser( - newUser("user4", identityProviderId = idpId1), - Set.empty, - ) - list1 <- tested.listUsers( - fromExcl = None, - maxResults = 3, - identityProviderId = idpId1, - ) - _ = list1 shouldBe Right( - UsersPage( - Seq( - createdUser("user1", identityProviderId = idpId1), - createdUser("user4", identityProviderId = idpId1), - ) - ) - ) - list2 <- tested.listUsers( - fromExcl = None, - maxResults = 4, - identityProviderId = IdentityProviderId.Default, - ) - _ = list2 shouldBe Right( - UsersPage( - Seq( - createdUser("user2"), - createdUser("user3"), - ) - ) - ) - } yield { - succeed - } - } - } - } - - "user rights management" - { - import UserRight.* - "listUserRights should find the rights of a freshly created user" in { - testIt { tested => - for { - res1 <- tested.createUser(newUser("user1"), Set.empty) - rights1 <- tested.listUserRights("user1", defaultIdpId) - user2 <- tested.createUser( - newUser("user2"), - Set(ParticipantAdmin, CanActAs("party1"), CanReadAs("party2")), - ) - rights2 <- tested.listUserRights("user2", defaultIdpId) - } yield { - res1 shouldBe Right(createdUser("user1")) - rights1 shouldBe Right(Set.empty) - user2 shouldBe Right(createdUser("user2")) - rights2 shouldBe Right( - Set(ParticipantAdmin, CanActAs("party1"), CanReadAs("party2")) - ) - } - } - } - "listUserRights should deny for user in another IDP" in { - testIt { tested => - for { - _ <- tested.createUser(newUser("user1"), Set.empty) - rights1 <- tested.listUserRights("user1", idpId1) - } yield { - rights1 shouldBe Left(PermissionDenied("user1")) - } - } - } - "listUserRights should fail on non-existent user" in { - testIt { tested => - for { - rights1 <- tested.listUserRights("user1", defaultIdpId) - } yield { - rights1 shouldBe Left(UserNotFound("user1")) - } - } - } - "grantUserRights should add new rights" in { - testIt { tested => - for { - res1 <- tested.createUser(newUser("user1"), Set.empty) - rights1 <- tested.grantRights("user1", Set(ParticipantAdmin), defaultIdpId) - rights2 <- tested.grantRights("user1", Set(ParticipantAdmin), defaultIdpId) - rights3 <- tested.grantRights( - "user1", - Set(CanActAs("party1"), CanReadAs("party2")), - defaultIdpId, - ) - rights4 <- tested.listUserRights("user1", defaultIdpId) - } yield { - res1 shouldBe Right(createdUser("user1")) - rights1 shouldBe Right(Set(ParticipantAdmin)) - rights2 shouldBe Right(Set.empty) - rights3 shouldBe Right( - Set(CanActAs("party1"), CanReadAs("party2")) - ) - rights4 shouldBe Right( - Set(ParticipantAdmin, CanActAs("party1"), CanReadAs("party2")) - ) - } - } - } - "grantUserRights should not fail when processing the same user concurrently" in { - testIt { tested => - for { - _ <- tested.createUser(newUser("user1"), Set.empty) - allRights = List.tabulate(100)(i => CanActAs(s"party$i"): UserRight) - setsOfRights = allRights.sliding(30, 10).toList - results <- Future.sequence( - setsOfRights.map(rights => tested.grantRights("user1", rights.toSet, defaultIdpId)) - ) - } yield { - results.flatMap(_.value) should contain theSameElementsAs (allRights) - } - } - } - "grantRights should fail on non-existent user" in { - testIt { tested => - for { - rights1 <- tested.grantRights("user1", Set.empty, defaultIdpId) - } yield { - rights1 shouldBe Left(UserNotFound("user1")) - } - - } - } - "grantRights should deny for user in another IDP" in { - testIt { tested => - for { - _ <- tested.createUser(newUser("user1"), Set.empty) - rights1 <- tested.grantRights("user1", Set.empty, idpId1) - } yield { - rights1 shouldBe Left(PermissionDenied("user1")) - } - } - } - "revokeRights should revoke rights" in { - testIt { tested => - for { - res1 <- tested.createUser( - newUser("user1"), - Set(ParticipantAdmin, CanActAs("party1"), CanReadAs("party2")), - ) - rights1 <- tested.listUserRights("user1", defaultIdpId) - rights2 <- tested.revokeRights("user1", Set(ParticipantAdmin), defaultIdpId) - rights3 <- tested.revokeRights("user1", Set(ParticipantAdmin), defaultIdpId) - rights4 <- tested.listUserRights("user1", defaultIdpId) - rights5 <- tested.revokeRights( - "user1", - Set(CanActAs("party1"), CanReadAs("party2")), - defaultIdpId, - ) - rights6 <- tested.listUserRights("user1", defaultIdpId) - } yield { - res1 shouldBe Right(createdUser("user1")) - rights1 shouldBe Right( - Set(ParticipantAdmin, CanActAs("party1"), CanReadAs("party2")) - ) - rights2 shouldBe Right(Set(ParticipantAdmin)) - rights3 shouldBe Right(Set.empty) - rights4 shouldBe Right(Set(CanActAs("party1"), CanReadAs("party2"))) - rights5 shouldBe Right( - Set(CanActAs("party1"), CanReadAs("party2")) - ) - rights6 shouldBe Right(Set.empty) - } - } - } - "revokeRights should fail on non-existent user" in { - testIt { tested => - for { - rights1 <- tested.revokeRights("user1", Set.empty, defaultIdpId) - } yield { - rights1 shouldBe Left(UserNotFound("user1")) - } - } - } - "revokeRights should deny for user in another IDP" in { - testIt { tested => - for { - _ <- tested.createUser(newUser("user1"), Set.empty) - rights1 <- tested.revokeRights("user1", Set.empty, idpId1) - } yield { - rights1 shouldBe Left(PermissionDenied("user1")) - } - } - } - } - - "updating" - { - "update an existing user's annotations" in { - testIt { tested => - val pr1 = newUser("user1", isDeactivated = true, primaryParty = None) - for { - create1 <- tested.createUser(pr1, Set.empty) - _ = create1.value shouldBe createdUser("user1", isDeactivated = true) - update1 <- tested.updateUser( - userUpdate = UserUpdate( - id = pr1.id, - primaryPartyUpdateO = Some(Some(Ref.Party.assertFromString("party123"))), - isDeactivatedUpdateO = Some(false), - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = create1.value.metadata.resourceVersionO, - annotationsUpdateO = Some(Map("k1" -> "v1")), - ), - identityProviderId = pr1.identityProviderId, - ) - ) - _ = resetResourceVersion(update1.value) shouldBe newUser( - "user1", - primaryParty = Some(Ref.Party.assertFromString("party123")), - isDeactivated = false, - annotations = Map("k1" -> "v1"), - ) - } yield succeed - } - } - - "should update metadata annotations" in { - testIt { tested => - val user = newUser("user1", annotations = Map("k1" -> "v1", "k2" -> "v2", "k3" -> "v3")) - for { - _ <- tested.createUser(user, Set.empty) - update1 <- tested.updateUser( - UserUpdate( - id = user.id, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = Some( - Map( - "k1" -> "v1a", - "k3" -> "", - "k4" -> "v4", - ) - ), - ), - identityProviderId = user.identityProviderId, - ) - ) - _ = update1.value shouldBe createdUser( - "user1", - resourceVersion = 1, - annotations = Map("k1" -> "v1a", "k2" -> "v2", "k4" -> "v4"), - ) - } yield { - succeed - } - } - } - - "should raise error when updating a non-existing user" in { - testIt { tested => - val userId = Ref.UserId.assertFromString("user") - for { - res1 <- tested.updateUser( - UserUpdate( - id = userId, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = Some(Map("k1" -> "v1")), - ), - identityProviderId = IdentityProviderId.Default, - ) - ) - _ = res1.left.value shouldBe UserManagementStore.UserNotFound(userId) - } yield succeed - } - } - - "should raise an error on resource version mismatch" in { - testIt { tested => - val user = newUser("user1") - for { - _ <- tested.createUser(user, Set.empty) - res1 <- tested.updateUser( - UserUpdate( - id = user.id, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = Some(100), - annotationsUpdateO = Some(Map("k1" -> "v1")), - ), - identityProviderId = user.identityProviderId, - ) - ) - _ = res1.left.value shouldBe UserManagementStore.ConcurrentUserUpdate(user.id) - } yield succeed - } - } - - "raise an error when annotations byte size max size exceeded" - { - // This value consumes just a bit over half the allowed max size limit - val bigValue = "big value:" + ("a" * 128 * 1024) - - "when creating a user" in { - testIt { tested => - val user = newUser(annotations = Map("k1" -> bigValue, "k2" -> bigValue)) - for { - res1 <- tested.createUser(user, Set.empty) - _ = res1.left.value shouldBe MaxAnnotationsSizeExceeded(user.id) - } yield succeed - } - } - - "when updating an existing user" in { - testIt { tested => - val user = newUser(annotations = Map("k1" -> bigValue)) - for { - _ <- tested.createUser(user, Set.empty) - res1 <- tested.updateUser( - makeUserUpdate(annotationsUpdateO = Some(Map("k2" -> bigValue))) - ) - _ = res1.left.value shouldBe MaxAnnotationsSizeExceeded(user.id) - } yield succeed - } - } - } - - } - - "reassigning idp" - { - "change user's idp" in { - testIt { tested => - for { - create <- tested.createUser( - newUser("user1", identityProviderId = defaultIdpId), - Set.empty, - ) - _ = create.value shouldBe createdUser("user1", identityProviderId = defaultIdpId) - _ <- createIdentityProviderConfig(idp1) - updated <- tested.updateUserIdp( - sourceIdp = defaultIdpId, - targetIdp = idpId1, - id = create.value.id, - ) - _ <- updated.value.identityProviderId shouldBe idpId1 - } yield succeed - } - } - - "when using wrong source idp id" in { - testIt { tested => - for { - _ <- createIdentityProviderConfig(idp1) - _ <- createIdentityProviderConfig(idp2) - user = newUser("user1", identityProviderId = idpId1) - create <- tested.createUser(user, Set.empty) - _ = create.value shouldBe createdUser("user1", identityProviderId = idpId1) - updateResult <- tested.updateUserIdp( - sourceIdp = idpId2, - targetIdp = defaultIdpId, - id = create.value.id, - ) - _ <- updateResult.left.value shouldBe PermissionDenied(user.id) - } yield succeed - } - } - - "cannot change idp for non-existent user" in { - testIt { tested => - val userId: Ref.UserId = "user1" - for { - updated <- tested.updateUserIdp( - sourceIdp = defaultIdpId, - targetIdp = idpId1, - id = userId, - ) - _ <- updated.left.value shouldBe UserNotFound(userId) - } yield succeed - } - } - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommandsBatchTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommandsBatchTest.scala deleted file mode 100644 index fe6aa1078a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/participant/state/ReassignmentCommandsBatchTest.scala +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.participant.state - -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.protocol.{ExampleTransactionFactory, ReassignmentId} -import com.digitalasset.canton.topology.{SynchronizerId, UniqueIdentifier} -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class ReassignmentCommandsBatchTest extends AnyWordSpec with Matchers { - private val cid1 = ExampleTransactionFactory.suffixedId(-1, 0) - private val cid2 = ExampleTransactionFactory.suffixedId(-1, 1) - private val cid3 = ExampleTransactionFactory.suffixedId(-1, 2) - - private def synchronizerId(i: Int) = SynchronizerId( - UniqueIdentifier.tryFromProtoPrimitive(s"synchronizer::source_$i") - ) - - private val unassign = - ReassignmentCommand.Unassign(Source(synchronizerId(1)), Target(synchronizerId(2)), cid1) - private val assign = ReassignmentCommand.Assign( - Source(synchronizerId(1)), - Target(synchronizerId(2)), - ReassignmentId.tryCreate("00"), - ) - - "ReassignmentCommandsBatch.create" when { - "with no commands should fail" in { - ReassignmentCommandsBatch.create(Nil) shouldBe Left(ReassignmentCommandsBatch.NoCommands) - } - - "with one unassign should succeed" in { - ReassignmentCommandsBatch.create(Seq(unassign)) shouldBe Right( - ReassignmentCommandsBatch.Unassignments( - source = unassign.sourceSynchronizer, - target = unassign.targetSynchronizer, - contractIds = NonEmpty.mk(Seq, unassign.contractId), - ) - ) - } - - "with one assign should succeed" in { - ReassignmentCommandsBatch.create(Seq(assign)) shouldBe Right( - ReassignmentCommandsBatch.Assignments( - target = assign.targetSynchronizer, - reassignmentId = assign.reassignmentId, - ) - ) - } - - "with multiple unassigns with same source and target should succeed" in { - ReassignmentCommandsBatch.create( - Seq( - unassign, - unassign.copy(contractId = cid2), - unassign.copy(contractId = cid3), - ) - ) shouldBe Right( - ReassignmentCommandsBatch.Unassignments( - source = unassign.sourceSynchronizer, - target = unassign.targetSynchronizer, - contractIds = NonEmpty.apply(Seq, cid1, cid2, cid3), - ) - ) - } - - "with multiple unassign with different source should fail" in { - ReassignmentCommandsBatch.create( - Seq( - unassign, - unassign.copy(sourceSynchronizer = Source(synchronizerId(42))), - ) - ) shouldBe Left( - ReassignmentCommandsBatch.DifferingSynchronizers - ) - } - - "with multiple unassign with different target should fail" in { - ReassignmentCommandsBatch.create( - Seq( - unassign, - unassign.copy(targetSynchronizer = Target(synchronizerId(42))), - ) - ) shouldBe Left(ReassignmentCommandsBatch.DifferingSynchronizers) - } - - "with multiple assigns should fail" in { - ReassignmentCommandsBatch.create( - Seq( - assign, - assign.copy(reassignmentId = ReassignmentId.tryCreate("0001")), - ) - ) shouldBe Left(ReassignmentCommandsBatch.MixedAssignWithOtherCommands) - } - - "with both assigns and unassign should fail" in { - ReassignmentCommandsBatch.create( - Seq[ReassignmentCommand]( - unassign, - assign, - ) - ) shouldBe Left(ReassignmentCommandsBatch.MixedAssignWithOtherCommands) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/runner/common/ArbitraryConfig.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/runner/common/ArbitraryConfig.scala deleted file mode 100644 index eb201e8968..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/runner/common/ArbitraryConfig.scala +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.runner.common - -import com.daml.jwt.JwtTimestampLeeway -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, NonNegativeLong, Port} -import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig -import com.digitalasset.canton.platform.config.* -import com.digitalasset.canton.platform.indexer.IndexerConfig -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.store.DbSupport -import com.digitalasset.canton.platform.store.DbSupport.DataSourceProperties -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig.SynchronousCommitValue -import com.digitalasset.daml.lf.VersionRange -import com.digitalasset.daml.lf.interpretation.Limits -import com.digitalasset.daml.lf.language.LanguageVersion -import com.digitalasset.daml.lf.transaction.NextGenContractStateMachine as ContractStateMachine -import io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth -import org.scalacheck.Gen - -import java.net.InetSocketAddress -import java.time.Duration -import java.time.temporal.ChronoUnit - -object ArbitraryConfig { - - val nonNegativeIntGen: Gen[NonNegativeInt] = - Gen.chooseNum(0, Int.MaxValue).map(NonNegativeInt.tryCreate) - - val nonNegativeLongGen: Gen[NonNegativeLong] = - Gen.chooseNum(0, Long.MaxValue).map(NonNegativeLong.tryCreate) - - val duration: Gen[Duration] = for { - value <- Gen.chooseNum(0, Int.MaxValue) - unit <- Gen.oneOf( - List( - ChronoUnit.NANOS, - ChronoUnit.MICROS, - ChronoUnit.MILLIS, - ChronoUnit.SECONDS, - ) - ) - } yield Duration.of(value.toLong, unit) - - val nonNegativeFiniteDurationGen: Gen[NonNegativeFiniteDuration] = - duration.map(NonNegativeFiniteDuration.tryFromJavaDuration) - - val versionRange: Gen[VersionRange[LanguageVersion]] = for { - min <- Gen.oneOf(LanguageVersion.allLfVersions) - max <- Gen.oneOf(LanguageVersion.allLfVersions) - if max >= min - } yield VersionRange[LanguageVersion](min, max) - - val limits: Gen[Limits] = for { - contractSignatories <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - contractObservers <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - choiceControllers <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - choiceObservers <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - choiceAuthorizers <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - transactionInputContracts <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - } yield Limits( - contractSignatories, - contractObservers, - choiceControllers, - choiceObservers, - choiceAuthorizers, - transactionInputContracts, - ) - - val contractKeyUniquenessMode: Gen[ContractStateMachine.Mode] = - Gen.oneOf( - ContractStateMachine.Mode.NoKey, - ContractStateMachine.Mode.NUCK, - ) - - val inetSocketAddress = for { - host <- Gen.alphaStr - port <- Gen.chooseNum(1, 65535) - } yield new InetSocketAddress(host, port) - - val clientAuth = Gen.oneOf(ClientAuth.values().toList) - - val port = Gen.choose(0, 65535).map(p => Port.tryCreate(p)) - - val userManagementServiceConfig = for { - enabled <- Gen.oneOf(true, false) - maxCacheSize <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - cacheExpiryAfterWriteInSeconds <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - maxUsersPageSize <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - } yield UserManagementServiceConfig( - enabled = enabled, - maxCacheSize = maxCacheSize, - cacheExpiryAfterWriteInSeconds = cacheExpiryAfterWriteInSeconds, - maxUsersPageSize = maxUsersPageSize, - ) - - val identityProviderManagementConfig = for { - cacheExpiryAfterWrite <- nonNegativeFiniteDurationGen - } yield IdentityProviderManagementConfig( - cacheExpiryAfterWrite = cacheExpiryAfterWrite - ) - - def jwtTimestampLeewayGen: Gen[JwtTimestampLeeway] = - for { - default <- Gen.option(Gen.posNum[Long]) - expiresAt <- Gen.option(Gen.posNum[Long]) - issuedAt <- Gen.option(Gen.posNum[Long]) - notBefore <- Gen.option(Gen.posNum[Long]) - } yield JwtTimestampLeeway( - default = default, - expiresAt = expiresAt, - issuedAt = issuedAt, - notBefore = notBefore, - ) - - val commandServiceConfig = for { - maxCommandsInFlight <- Gen.chooseNum(Int.MinValue, Int.MaxValue) - maxTrackingTimeout <- duration - } yield CommandServiceConfig( - NonNegativeFiniteDuration(maxTrackingTimeout), - maxCommandsInFlight, - ) - - val connectionPoolConfig = for { - connectionPoolSize <- Gen.chooseNum(0, Int.MaxValue) - connectionTimeout <- Gen.finiteDuration - } yield DbSupport.ConnectionPoolConfig( - connectionPoolSize, - connectionTimeout, - ) - - val postgresDataSourceConfig = for { - synchronousCommit <- Gen.option(Gen.oneOf(SynchronousCommitValue.All)) - nonNegativeInt = Gen.chooseNum(0, Int.MaxValue) - tcpKeepalivesIdle <- nonNegativeInt - tcpKeepalivesInterval <- nonNegativeInt - tcpKeepalivesCount <- nonNegativeInt - clientConnectionCheckInterval <- nonNegativeFiniteDurationGen - networkTimeout <- nonNegativeFiniteDurationGen - } yield PostgresDataSourceConfig( - synchronousCommit = synchronousCommit, - tcpKeepalivesIdle = Some(tcpKeepalivesIdle), - tcpKeepalivesInterval = Some(tcpKeepalivesInterval), - tcpKeepalivesCount = Some(tcpKeepalivesCount), - clientConnectionCheckInterval = Some(clientConnectionCheckInterval), - networkTimeout = Some(networkTimeout), - ) - - val achsConfig: Gen[AchsConfig] = for { - validAtDistanceTarget <- nonNegativeLongGen - lastPopulatedDistanceTarget <- nonNegativeLongGen - } yield AchsConfig( - validAtDistanceTarget = validAtDistanceTarget, - lastPopulatedDistanceTarget = lastPopulatedDistanceTarget, - ) - - val dataSourceProperties = for { - connectionPool <- connectionPoolConfig - postgres <- postgresDataSourceConfig - } yield DataSourceProperties(connectionPool = connectionPool, postgres = postgres) - - val rateLimitingConfig = for { - maxApiServicesQueueSize <- Gen.chooseNum(0, Int.MaxValue) - maxApiServicesIndexDbQueueSize <- Gen.chooseNum(0, Int.MaxValue) - maxUsedHeapSpacePercentage <- Gen.chooseNum(0, Int.MaxValue) - minFreeHeapSpaceBytes <- Gen.long - element = RateLimitingConfig( - maxApiServicesQueueSize, - maxApiServicesIndexDbQueueSize, - maxUsedHeapSpacePercentage, - minFreeHeapSpaceBytes, - ) - optElement <- Gen.option(element) - } yield optElement - - val indexerConfig = for { - batchingParallelism <- nonNegativeIntGen - enableCompression <- Gen.oneOf(true, false) - ingestionParallelism <- nonNegativeIntGen - inputMappingParallelism <- nonNegativeIntGen - maxInputBufferSize <- nonNegativeIntGen - restartDelay <- nonNegativeFiniteDurationGen - submissionBatchSize <- Gen.long - achsConfig <- Gen.option(achsConfig) - } yield IndexerConfig( - batchingParallelism = batchingParallelism, - enableCompression = enableCompression, - ingestionParallelism = ingestionParallelism, - inputMappingParallelism = inputMappingParallelism, - maxInputBufferSize = maxInputBufferSize, - restartDelay = restartDelay, - submissionBatchSize = submissionBatchSize, - achsConfig = achsConfig, - ) - - def genActiveContractsServiceStreamConfig: Gen[ActiveContractsServiceStreamsConfig] = - for { - eventsPageSize <- Gen.chooseNum(0, Int.MaxValue) - acsIdPageSize <- Gen.chooseNum(0, Int.MaxValue) - acsIdPageBufferSize <- Gen.chooseNum(0, Int.MaxValue) - acsIdPageWorkingMemoryBytes <- Gen.chooseNum(0, Int.MaxValue) - acsIdFetchingParallelism <- Gen.chooseNum(0, Int.MaxValue) - acsContractFetchingParallelism <- Gen.chooseNum(0, Int.MaxValue) - } yield ActiveContractsServiceStreamsConfig( - maxIdsPerIdPage = acsIdPageSize, - maxPayloadsPerPayloadsPage = eventsPageSize, - maxPagesPerIdPagesBuffer = acsIdPageBufferSize, - maxWorkingMemoryInBytesForIdPages = acsIdPageWorkingMemoryBytes, - maxParallelActiveIdQueries = acsIdFetchingParallelism, - maxParallelPayloadCreateQueries = acsContractFetchingParallelism, - ) - - def genTransactionFlatStreams: Gen[UpdatesStreamsConfig] = - for { - maxIdsPerIdPage <- Gen.chooseNum(0, Int.MaxValue) - maxPayloadsPerPayloadsPage <- Gen.chooseNum(0, Int.MaxValue) - maxPagesPerIdPagesBuffer <- Gen.chooseNum(0, Int.MaxValue) - maxWorkingMemoryInBytesForIdPages <- Gen.chooseNum(0, Int.MaxValue) - maxParallelIdCreateQueries <- Gen.chooseNum(0, Int.MaxValue) - maxParallelPayloadCreateQueries <- Gen.chooseNum(0, Int.MaxValue) - maxParallelIdConsumingQueries <- Gen.chooseNum(0, Int.MaxValue) - maxParallelPayloadConsumingQueries <- Gen.chooseNum(0, Int.MaxValue) - maxParallelPayloadQueries <- Gen.chooseNum(0, Int.MaxValue) - transactionsProcessingParallelism <- Gen.chooseNum(0, Int.MaxValue) - } yield UpdatesStreamsConfig( - maxIdsPerIdPage = maxIdsPerIdPage, - maxPagesPerIdPagesBuffer = maxPayloadsPerPayloadsPage, - maxWorkingMemoryInBytesForIdPages = maxPagesPerIdPagesBuffer, - maxPayloadsPerPayloadsPage = maxWorkingMemoryInBytesForIdPages, - maxParallelIdActivateQueries = maxParallelIdCreateQueries, - maxParallelIdDeactivateQueries = maxParallelPayloadCreateQueries, - maxParallelPayloadActivateQueries = maxParallelIdConsumingQueries, - maxParallelPayloadDeactivateQueries = maxParallelPayloadConsumingQueries, - maxParallelPayloadQueries = maxParallelPayloadQueries, - transactionsProcessingParallelism = transactionsProcessingParallelism, - ) - - val indexServiceConfig: Gen[IndexServiceConfig] = for { - activeContractsServiceStreamsConfig <- genActiveContractsServiceStreamConfig - transactionFlatStreams <- genTransactionFlatStreams - eventsProcessingParallelism <- Gen.chooseNum(0, Int.MaxValue) - bufferedStreamsPageSize <- Gen.chooseNum(0, Int.MaxValue) - maxContractStateCacheSize <- Gen.long - maxContractKeyStateCacheSize <- Gen.long - maxTransactionsInMemoryFanOutBufferSize <- Gen.chooseNum(0, Int.MaxValue) - apiStreamShutdownTimeout <- Gen.finiteDuration - } yield IndexServiceConfig( - eventsProcessingParallelism, - bufferedStreamsPageSize, - maxContractStateCacheSize, - maxContractKeyStateCacheSize, - maxTransactionsInMemoryFanOutBufferSize, - apiStreamShutdownTimeout, - activeContractsServiceStreams = activeContractsServiceStreamsConfig, - updatesStreams = transactionFlatStreams, - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/runner/common/PureConfigReaderWriterSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/runner/common/PureConfigReaderWriterSpec.scala deleted file mode 100644 index e8c6ba01be..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/runner/common/PureConfigReaderWriterSpec.scala +++ /dev/null @@ -1,536 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.ledger.runner.common - -import com.daml.jwt.JwtTimestampLeeway -import com.digitalasset.canton.config -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.ledger.runner.common.OptConfigValue.{ - optReaderEnabled, - optWriterEnabled, -} -import com.digitalasset.canton.platform.apiserver.SeedService.Seeding -import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig -import com.digitalasset.canton.platform.config.{ - CommandServiceConfig, - IndexServiceConfig, - UserManagementServiceConfig, -} -import com.digitalasset.canton.platform.indexer.IndexerConfig -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.indexer.ha.HaConfig -import com.digitalasset.canton.platform.store.DbSupport.ParticipantDataSourceConfig -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig.SynchronousCommitValue -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigValueFactory.fromAnyRef -import org.scalacheck.Gen -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, EitherValues} -import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks -import pureconfig.error.ConfigReaderFailures -import pureconfig.{ConfigConvert, ConfigReader, ConfigSource, ConfigWriter} - -import java.time.Duration -import scala.reflect.{ClassTag, classTag} - -class PureConfigReaderWriterSpec - extends AnyFlatSpec - with Matchers - with ScalaCheckPropertyChecks - with EitherValues { - - def convert[T](converter: ConfigReader[T], str: String): Either[ConfigReaderFailures, T] = { - val value = ConfigFactory.parseString(str) - for { - source <- ConfigSource.fromConfig(value).cursor() - result <- converter.from(source) - } yield result - } - - def testReaderWriterIsomorphism[T: ClassTag: ConfigWriter: ConfigReader]( - secure: Boolean, - generator: Gen[T], - name: Option[String] = None, - ): Unit = { - val secureText = secure match { - case true => "secure " - case false => "" - } - secureText + name.getOrElse(classTag[T].toString) should "be isomorphic" in forAll(generator) { - generatedValue => - val writer = implicitly[ConfigWriter[T]] - val reader = implicitly[ConfigReader[T]] - reader.from(writer.to(generatedValue)).value shouldBe generatedValue - } - } - - def testReaderWriterIsomorphism(secure: Boolean): Unit = { - val readerWriter = new PureConfigReaderWriter(secure) - import readerWriter.* - testReaderWriterIsomorphism(secure, ArbitraryConfig.duration) - testReaderWriterIsomorphism(secure, ArbitraryConfig.port) - testReaderWriterIsomorphism(secure, ArbitraryConfig.userManagementServiceConfig) - testReaderWriterIsomorphism(secure, ArbitraryConfig.identityProviderManagementConfig) - testReaderWriterIsomorphism(secure, ArbitraryConfig.connectionPoolConfig) - testReaderWriterIsomorphism(secure, ArbitraryConfig.postgresDataSourceConfig) - testReaderWriterIsomorphism(secure, ArbitraryConfig.dataSourceProperties) - testReaderWriterIsomorphism(secure, ArbitraryConfig.achsConfig) - testReaderWriterIsomorphism( - secure, - ArbitraryConfig.rateLimitingConfig, - Some("RateLimitingConfig"), - ) - testReaderWriterIsomorphism(secure, ArbitraryConfig.indexerConfig) - testReaderWriterIsomorphism(secure, ArbitraryConfig.commandServiceConfig) - testReaderWriterIsomorphism(secure, ArbitraryConfig.indexServiceConfig) - } - - testReaderWriterIsomorphism(secure = true) - testReaderWriterIsomorphism(secure = false) - - import PureConfigReaderWriter.Secure.* - - behavior of "Duration" - - it should "read/write against predefined values" in { - def compare(duration: Duration, expectedString: String): Assertion = { - javaDurationWriter.to(duration) shouldBe fromAnyRef(expectedString) - javaDurationReader.from(fromAnyRef(expectedString)).value shouldBe duration - } - compare(Duration.ofSeconds(0), "0 days") - compare(Duration.ofSeconds(1), "1 second") - compare(Duration.ofSeconds(30), "30 seconds") - compare(Duration.ofHours(1), "3600 seconds") - } - - behavior of "JwtTimestampLeeway" - - val validJwtTimestampLeewayValue = - """ - | enabled = true - | default = 1 - |""".stripMargin - - it should "read/write against predefined values" in { - def compare(configString: String, expectedValue: Option[JwtTimestampLeeway]) = - convert(jwtTimestampLeewayConfigConvert, configString).value shouldBe expectedValue - - compare( - """ - | enabled = true - | default = 1 - |""".stripMargin, - Some(JwtTimestampLeeway(Some(1), None, None, None)), - ) - compare( - """ - | enabled = true - | expires-at = 2 - |""".stripMargin, - Some(JwtTimestampLeeway(None, Some(2), None, None)), - ) - compare( - """ - | enabled = true - | issued-at = 3 - |""".stripMargin, - Some(JwtTimestampLeeway(None, None, Some(3), None)), - ) - compare( - """ - | enabled = true - | not-before = 4 - |""".stripMargin, - Some(JwtTimestampLeeway(None, None, None, Some(4))), - ) - compare( - """ - | enabled = true - | default = 1 - | expires-at = 2 - | issued-at = 3 - | not-before = 4 - |""".stripMargin, - Some(JwtTimestampLeeway(Some(1), Some(2), Some(3), Some(4))), - ) - compare( - """ - | enabled = false - | default = 1 - | expires-at = 2 - | issued-at = 3 - | not-before = 4 - |""".stripMargin, - None, - ) - } - - it should "not support unknown keys" in { - convert( - jwtTimestampLeewayConfigConvert, - "unknown-key=yes\n" + validJwtTimestampLeewayValue, - ).left.value - .prettyPrint(0) should include("Unknown key") - } - - behavior of "Seeding" - - it should "read/write against predefined values" in { - seedingWriter.to(Seeding.Static) shouldBe fromAnyRef("testing-static") - seedingWriter.to(Seeding.Weak) shouldBe fromAnyRef("testing-weak") - seedingWriter.to(Seeding.Strong) shouldBe fromAnyRef("strong") - seedingReader.from(fromAnyRef("testing-static")).value shouldBe Seeding.Static - seedingReader.from(fromAnyRef("testing-weak")).value shouldBe Seeding.Weak - seedingReader.from(fromAnyRef("strong")).value shouldBe Seeding.Strong - } - - behavior of "userManagementServiceConfig" - - val validUserManagementServiceConfigValue = - """ - | cache-expiry-after-write-in-seconds = 5 - | enabled = true - | max-cache-size = 100 - | max-users-page-size = 1000""".stripMargin - - it should "support current defaults" in { - val value = validUserManagementServiceConfigValue - convert(userManagementServiceConfigConvert, value).value shouldBe UserManagementServiceConfig() - } - - it should "not support invalid keys" in { - val value = "unknown-key=yes\n" + validUserManagementServiceConfigValue - convert(userManagementServiceConfigConvert, value).left.value - .prettyPrint(0) should include("Unknown key") - } - - it should "read/write against predefined values" in { - val value = """ - | cache-expiry-after-write-in-seconds = 1 - | enabled = true - | max-cache-size = 99 - | max-users-page-size = 999""".stripMargin - - convert(userManagementServiceConfigConvert, value).value shouldBe UserManagementServiceConfig( - enabled = true, - cacheExpiryAfterWriteInSeconds = 1, - maxCacheSize = 99, - maxUsersPageSize = 999, - ) - } - - behavior of "PostgresDataSourceConfig" - - val validPostgresDataSourceConfigValue = - """ - | tcp-keepalives-idle = 10 - | tcp-keepalives-interval = 1 - | tcp-keepalives-count = 5""".stripMargin - - it should "support current defaults" in { - val value = validPostgresDataSourceConfigValue - convert( - dbConfigPostgresDataSourceConfigConvert, - value, - ).value shouldBe PostgresDataSourceConfig() - } - - it should "not support invalid keys" in { - val value = "unknown-key=yes\n" + validPostgresDataSourceConfigValue - convert(dbConfigPostgresDataSourceConfigConvert, value).left.value - .prettyPrint(0) should include("Unknown key") - } - - it should "read/write against predefined values" in { - val value = - """ - | synchronous-commit = on - | tcp-keepalives-idle = 9 - | tcp-keepalives-interval = 99 - | tcp-keepalives-count = 999 - | client-connection-check-interval = 111ms - | network-timeout = 222s - """.stripMargin - - convert(dbConfigPostgresDataSourceConfigConvert, value).value shouldBe PostgresDataSourceConfig( - synchronousCommit = Some(SynchronousCommitValue.On), - tcpKeepalivesIdle = Some(9), - tcpKeepalivesInterval = Some(99), - tcpKeepalivesCount = Some(999), - clientConnectionCheckInterval = Some(config.NonNegativeFiniteDuration.ofMillis(111)), - networkTimeout = Some(config.NonNegativeFiniteDuration.ofSeconds(222)), - ) - } - - it should "read/write against some predefined values and some defaults" in { - val value = - """ - | synchronous-commit = on - | tcp-keepalives-idle = 9""".stripMargin - - convert(dbConfigPostgresDataSourceConfigConvert, value).value shouldBe PostgresDataSourceConfig( - synchronousCommit = Some(SynchronousCommitValue.On), - tcpKeepalivesIdle = Some(9), - ) - } - - behavior of "AchsConfig" - - private val validAchsConfigValue = - """ - | valid-at-distance-target = 10 - | last-populated-distance-target = 500000 - | """.stripMargin - - it should "support valid keys" in { - val value = validAchsConfigValue - convert( - achsConfigConvert, - value, - ).value shouldBe AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(10L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(500000L), - ) - } - - it should "not support negative values" in { - val value = - """ - | valid-at-distance-target = -10 - | last-populated-distance-target = 500000 - """.stripMargin - - convert( - achsConfigConvert, - value, - ).left.value.prettyPrint(0) should include("negative") - } - - it should "not support invalid keys" in { - val value = "unknown-key=yes\n" + validAchsConfigValue - convert(achsConfigConvert, value).left.value - .prettyPrint(0) should include("Unknown key") - } - - behavior of "CommandServiceConfig" - - val validCommandConfigurationValue = - """ - | default-tracking-timeout = "300 seconds" - | max-commands-in-flight = 256""".stripMargin - - it should "read/write against predefined values" in { - val value = validCommandConfigurationValue - convert(commandConfigurationConvert, value).value shouldBe CommandServiceConfig() - } - - it should "not support additional unknown keys" in { - val value = "unknown-key=yes\n" + validCommandConfigurationValue - convert(commandConfigurationConvert, value).left.value - .prettyPrint(0) should include("Unknown key") - } - - behavior of "SynchronousCommitValue" - - it should "read/write against predefined values" in { - val conv = dbConfigSynchronousCommitValueConvert - def compare(value: SynchronousCommitValue, str: String): Assertion = { - conv.to(value) shouldBe fromAnyRef(str) - conv.from(fromAnyRef(str)).value shouldBe value - } - compare(SynchronousCommitValue.On, "on") - compare(SynchronousCommitValue.Off, "off") - compare(SynchronousCommitValue.RemoteWrite, "remote-write") - compare(SynchronousCommitValue.RemoteApply, "remote-apply") - compare(SynchronousCommitValue.Local, "local") - } - - behavior of "RateLimitingConfig" - - val validRateLimitingConfig = - """ - | enabled = true - | max-api-services-index-db-queue-size = 1000 - | max-api-services-queue-size = 10000 - | max-used-heap-space-percentage = 85 - | min-free-heap-space-bytes = 300000""".stripMargin - - it should "support current defaults" in { - val value = validRateLimitingConfig - val expected = RateLimitingConfig( - maxApiServicesQueueSize = 10000, - maxApiServicesIndexDbQueueSize = 1000, - maxUsedHeapSpacePercentage = 85, - minFreeHeapSpaceBytes = 300000, - ) - convert(rateLimitingConfigConvert, value).value shouldBe Some(expected) - } - - it should "not support unknown keys" in { - val value = "unknown-key=yes\n" + validRateLimitingConfig - convert(rateLimitingConfigConvert, value).left.value.prettyPrint(0) should include( - "Unknown key" - ) - } - - behavior of "HaConfig" - - val validHaConfigValue = - """ - | indexer-lock-id = 105305792 - | indexer-worker-lock-id = 105305793 - | main-lock-acquire-retry-timeout= 500 milliseconds - | main-lock-checker-period = 1000 milliseconds - | worker-lock-acquire-max-retries = 10 - | worker-lock-acquire-retry-timeout = 500 milliseconds - | main-lock-checker-jdbc-network-timeout = 10000 milliseconds - | """.stripMargin - - it should "support current defaults" in { - val value = validHaConfigValue - convert(haConfigConvert, value).value shouldBe HaConfig() - } - - it should "not support unknown keys" in { - val value = "unknown-key=yes\n" + validHaConfigValue - convert(haConfigConvert, value).left.value.prettyPrint(0) should include("Unknown key") - } - - behavior of "IndexerConfig" - - val validIndexerConfigValue = - """ - | batching-parallelism = 4 - | enable-compression = false - | ingestion-parallelism = 16 - | input-mapping-parallelism = 16 - | max-input-buffer-size = 50 - | restart-delay = "10s" - | submission-batch-size = 50 - | disable-monotonicity-checks = false""".stripMargin - - it should "support current defaults" in { - val value = validIndexerConfigValue - convert(indexerConfigConvert, value).value shouldBe IndexerConfig() - } - - it should "not support unknown keys" in { - val value = "unknown-key=yes\n" + validIndexerConfigValue - convert(indexerConfigConvert, value).left.value.prettyPrint(0) should include( - "Unknown key" - ) - } - - it should "support explicit setting of AchsConfig" in { - val value = - """achs-config { - | valid-at-distance-target = 100 - | last-populated-distance-target = 50 - |}""".stripMargin + validIndexerConfigValue - convert(indexerConfigConvert, value).value shouldBe - IndexerConfig(achsConfig = - Some( - AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(100L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(50L), - ) - ) - ) - } - - behavior of "IndexServiceConfig" - - val validIndexServiceConfigValue = - """| - |active-contracts-service-streams { - | contract-processing-parallelism=8 - | max-ids-per-id-page=20000 - | max-pages-per-id-pages-buffer=1 - | max-parallel-id-create-queries=4 - | id-filter-query-parallelism=2 - | max-parallel-payload-create-queries=2 - | max-payloads-per-payloads-page=1000 - | max-working-memory-in-bytes-for-id-pages=104857600 - |} - |api-stream-shutdown-timeout="5s" - |buffered-streams-page-size=100 - |completions-page-size=1000 - |buffered-events-processing-parallelism=8 - |global-max-event-id-queries=20 - |global-max-event-payload-queries=10 - |in-memory-state-updater-parallelism=2 - |max-contract-key-state-cache-size=10000 - |max-contract-state-cache-size=10000 - |max-transactions-in-memory-fan-out-buffer-size=1000 - |prepare-package-metadata-time-out-warning="5s" - |updates-streams { - | max-ids-per-id-page=20000 - | max-pages-per-id-pages-buffer=1 - | max-parallel-id-consuming-queries=4 - | max-parallel-id-create-queries=4 - | max-parallel-payload-consuming-queries=2 - | max-parallel-payload-create-queries=2 - | max-parallel-payload-queries=2 - | max-payloads-per-payloads-page=1000 - | max-working-memory-in-bytes-for-id-pages=104857600 - | transactions-processing-parallelism=8 - |}""".stripMargin - - it should "support current defaults" in { - val value = validIndexServiceConfigValue - convert(indexServiceConfigConvert, value).value shouldBe IndexServiceConfig() - } - - it should "not support unknown keys" in { - val value = "unknown-key=yes\n" + validIndexServiceConfigValue - convert(indexServiceConfigConvert, value).left.value.prettyPrint(0) should include( - "Unknown key" - ) - } - - behavior of "ParticipantDataSourceConfig" - - it should "read/write against predefined values" in { - val secretUrl = "https://www.daml.com/secrets.json" - participantDataSourceConfigReader - .from(fromAnyRef(secretUrl)) - .value shouldBe ParticipantDataSourceConfig(secretUrl) - participantDataSourceConfigWriter.to( - ParticipantDataSourceConfig(secretUrl) - ) shouldBe fromAnyRef("") - new PureConfigReaderWriter(false).participantDataSourceConfigWriter.to( - ParticipantDataSourceConfig(secretUrl) - ) shouldBe fromAnyRef(secretUrl) - } - - behavior of "optReaderEnabled/optWriterEnabled" - case class Cfg(i: Int) - case class Cfg2(enabled: Boolean, i: Int) - import pureconfig.generic.semiauto.* - val testConvert: ConfigConvert[Cfg] = deriveConvert[Cfg] - val testConvert2: ConfigConvert[Cfg2] = deriveConvert[Cfg2] - - it should "read enabled flag" in { - val reader: ConfigReader[Option[Cfg]] = optReaderEnabled[Cfg](testConvert) - convert(reader, "enabled = true\ni = 1").value shouldBe Some(Cfg(1)) - convert(reader, "enabled = true\ni = 10").value shouldBe Some(Cfg(10)) - convert(reader, "enabled = false\ni = 1").value shouldBe None - convert(reader, "enabled = false").value shouldBe None - } - - it should "write enabled flag" in { - val writer: ConfigWriter[Option[Cfg]] = optWriterEnabled[Cfg](testConvert) - writer.to(Some(Cfg(1))) shouldBe ConfigFactory.parseString("enabled = true\ni = 1").root() - writer.to(Some(Cfg(10))) shouldBe ConfigFactory.parseString("enabled = true\ni = 10").root() - writer.to(None) shouldBe ConfigFactory.parseString("enabled = false").root() - } - - it should "throw if configuration is ambiguous" in { - val writer: ConfigWriter[Option[Cfg2]] = optWriterEnabled[Cfg2](testConvert2) - an[IllegalArgumentException] should be thrownBy writer.to(Some(Cfg2(enabled = false, 1))) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/DispatcherStateSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/DispatcherStateSpec.scala deleted file mode 100644 index ac82e08299..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/DispatcherStateSpec.scala +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.base.error.utils.ErrorDetails -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.error.CommonErrors -import com.digitalasset.canton.pekkostreams.dispatcher.{Dispatcher, SubSource} -import com.digitalasset.canton.util.TryUtil -import com.digitalasset.canton.{BaseTest, HasExecutorService} -import io.grpc.StatusRuntimeException -import org.apache.pekko.stream.scaladsl.Source -import org.mockito.MockitoSugar -import org.scalatest.Assertion -import org.scalatest.flatspec.AsyncFlatSpec - -import java.util.concurrent.ScheduledExecutorService -import scala.concurrent.Future -import scala.concurrent.duration.{Duration, DurationInt} -import scala.util.{Failure, Success} - -class DispatcherStateSpec - extends AsyncFlatSpec - with MockitoSugar - with PekkoBeforeAndAfterAll - with HasExecutorService - with BaseTest { - private val className = classOf[DispatcherState].getSimpleName - - private val initializationOffset = Some(Offset.tryFromLong(12345678L)) - - private val nextOffset = initializationOffset.map(_.increment) - - private val thirdOffset = nextOffset.map(_.increment) - - private implicit val scheduler: ScheduledExecutorService = scheduledExecutor() - - s"$className.{startDispatcher, stopDispatcher}" should "handle correctly the Dispatcher lifecycle" in { - for { - _ <- Future.unit - dispatcherState = new DispatcherState(Duration.Zero, loggerFactory) - // Start the initial Dispatcher - _ = dispatcherState.startDispatcher(initializationOffset) - - // Assert running flag - _ = dispatcherState.isRunning shouldBe true - - initialDispatcher = dispatcherState.getDispatcher - - // Stop the initial Dispatcher - _ <- stopDispatcherAndAssertStreamsFinishedWithFailure(dispatcherState) - - // Assert running flag is false - _ = dispatcherState.isRunning shouldBe false - - // Assert that the initial Dispatcher reference does not accept new subscriptions - _ <- assertDispatcherDoesntAcceptNewSubscriptions(initialDispatcher) - - // Getting the Dispatcher while stopped throws - _ = assertNotRunning(dispatcherState) - - // Start a new Dispatcher - _ = dispatcherState.startDispatcher(nextOffset) - - // Try to start a new Dispatcher - _ = intercept[IllegalStateException] { - dispatcherState.startDispatcher(thirdOffset) - }.getMessage shouldBe "Dispatcher startup triggered while an existing dispatcher is still active." - - anotherDispatcher = dispatcherState.getDispatcher - } yield { - // Assert that the dispatcher instances are different - initialDispatcher should not be anotherDispatcher - } - } - - s"$className.shutdown" should "shutdown the DispatcherState" in { - for { - _ <- Future.unit - dispatcherState = new DispatcherState(1.second, loggerFactory) - // Start the initial Dispatcher - _ = dispatcherState.startDispatcher(initializationOffset) - - // Assert running flag - _ = dispatcherState.isRunning shouldBe true - initialDispatcher = dispatcherState.getDispatcher - - // Shutdown the state - _ <- dispatcherState.shutdown() - - // Assert running flag is false - _ = dispatcherState.isRunning shouldBe false - - // Assert that the initial Dispatcher reference does not accept new subscriptions - _ <- assertDispatcherDoesntAcceptNewSubscriptions(initialDispatcher) - - // Getting the Dispatcher while shutdown - _ = assertNotRunning(dispatcherState) - - // Start a new Dispatcher is not possible in the shutdown state - _ = intercept[IllegalStateException] { - dispatcherState.startDispatcher(nextOffset) - }.getMessage shouldBe "Ledger API offset dispatcher state has already shut down." - } yield succeed - } - - s"$className.shutdown" should "work on not-running Dispatcher state" in { - for { - _ <- Future.unit - // Start a new dispatcher state - dispatcherState = new DispatcherState(Duration.Zero, loggerFactory) - - // Shutting down the state - _ <- dispatcherState.shutdown() - - // Assert shutdown - _ = assertNotRunning(dispatcherState) - - // Stopping the Dispatcher should be a no-op on a shutdown dispatcher - _ <- dispatcherState.stopDispatcher() - } yield succeed - } - - private def assertNotRunning(dispatcherState: DispatcherState) = - ErrorDetails.matches( - e = intercept[StatusRuntimeException](dispatcherState.getDispatcher), - errorCode = CommonErrors.ServiceNotRunning, - ) - - private def assertDispatcherDoesntAcceptNewSubscriptions( - initialDispatcher: Dispatcher[Offset] - ): Future[Assertion] = - initialDispatcher - .startingAt( - startExclusive = None, - subSource = SubSource.RangeSource((_, _) => Source.empty), - ) - .run() - .transform { - case Failure(f) if f.getMessage == "Ledger API offset dispatcher: Dispatcher is closed" => - Success(succeed) - case other => fail(s"Unexpected result: $other") - } - - private def stopDispatcherAndAssertStreamsFinishedWithFailure( - dispatcherState: DispatcherState - ): Future[Unit] = - for { - _ <- Future.unit - // Start a subscription - runF = dispatcherState.getDispatcher - .startingAt( - startExclusive = None, - subSource = SubSource.RangeSource((_, _) => Source.empty), - ) - .run() - // Stop the dispatcher - _ <- dispatcherState.stopDispatcher() - // Assert subscription correctly terminated with failure - _ <- runF.transform { - case Failure(e: StatusRuntimeException) - if ErrorDetails.matches(e, CommonErrors.ServiceNotRunning) => - TryUtil.unit - case Failure(other) => - fail( - s"Expected a self-service error exception of ${CommonErrors.ServiceNotRunning.code} but got $other" - ) - case Success(_) => fail("Expected a failure but got a Success instead") - } - } yield () -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/InMemoryStateSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/InMemoryStateSpec.scala deleted file mode 100644 index 40a8ff3c0f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/InMemoryStateSpec.scala +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform - -import com.digitalasset.canton.TestEssentials -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.pekkostreams.dispatcher.Dispatcher -import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker -import com.digitalasset.canton.platform.apiserver.services.admin.PartyAllocation -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsLastPointers, - LedgerEnd, -} -import com.digitalasset.canton.platform.store.cache.{ - AchsStateCache, - ContractStateCaches, - InMemoryFanoutBuffer, - MutableLedgerEndCache, - OffsetCheckpointCache, -} -import com.digitalasset.canton.platform.store.interning.{ - StringInterningView, - UpdatingStringInterningView, -} -import com.digitalasset.daml.lf.data.Ref -import org.mockito.{InOrder, Mockito, MockitoSugar} -import org.scalatest.Assertion -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.concurrent.Future - -class InMemoryStateSpec extends AsyncFlatSpec with MockitoSugar with Matchers with TestEssentials { - private val className = classOf[InMemoryState].getSimpleName - - s"$className.initialized" should "return false if not initialized" in withTestFixture { - case (inMemoryState, _, _, _, _, _, _, _, _, _) => - inMemoryState.initialized shouldBe false - } - - s"$className.initializeTo" should "initialize the state" in withTestFixture { - case ( - inMemoryState, - mutableLedgerEndCache, - contractStateCaches, - inMemoryFanoutBuffer, - stringInterningView, - dispatcherState, - updateStringInterningView, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - inOrder, - ) => - val initOffset = Offset.tryFromLong(12345678L) - val initEventSequentialId = 1337L - val initStringInterningId = 17 - val initPublicationTime = CantonTimestamp.now() - - val initLedgerEnd = ParameterStorageBackend - .LedgerEnd(initOffset, initEventSequentialId, initStringInterningId, initPublicationTime) - - when(updateStringInterningView(stringInterningView, initLedgerEnd)).thenReturn(Future.unit) - when(dispatcherState.stopDispatcher()).thenReturn(Future.unit) - when(dispatcherState.isRunning).thenReturn(true) - when(mutableLedgerEndCache.apply()).thenReturn(None) - when(dispatcherState.getDispatcher).thenReturn( - Dispatcher( - name = "", - firstIndex = Offset.firstOffset, - headAtInitialization = None, - ) - ) - - for { - // INITIALIZED THE STATE - _ <- inMemoryState.initializeTo( - Some(initLedgerEnd), - ParameterStorageBackend.AchsState( - validAt = 0, - AchsLastPointers(lastRemoved = 0, lastPopulated = 0), - ), - ) - - _ = { - // ASSERT STATE INITIALIZED - - inOrder.verify(dispatcherState).stopDispatcher() - inOrder.verify(contractStateCaches).reset(Some(initLedgerEnd)) - inOrder.verify(inMemoryFanoutBuffer).flush() - inOrder - .verify(mutableLedgerEndCache) - .set(Some(initLedgerEnd)) - inOrder.verify(transactionSubmissionTracker).close() - inOrder.verify(reassignmentSubmissionTracker).close() - inOrder - .verify(dispatcherState) - .startDispatcher(Some(initLedgerEnd.lastOffset)) - - inMemoryState.initialized shouldBe true - } - - reInitOffset = Offset.tryFromLong(12345678L) - reInitEventSequentialId = 9999L - reInitStringInterningId = 50 - reInitPublicationTime = CantonTimestamp.now() - reInitLedgerEnd = ParameterStorageBackend - .LedgerEnd( - reInitOffset, - reInitEventSequentialId, - reInitStringInterningId, - reInitPublicationTime, - ) - - // RESET MOCKS - _ = { - reset( - mutableLedgerEndCache, - contractStateCaches, - inMemoryFanoutBuffer, - updateStringInterningView, - ) - when(updateStringInterningView(stringInterningView, reInitLedgerEnd)).thenReturn( - Future.unit - ) - - when(dispatcherState.stopDispatcher()).thenReturn(Future.unit) - when(mutableLedgerEndCache.apply()).thenReturn(Some(initLedgerEnd)) - when(dispatcherState.getDispatcher).thenReturn( - Dispatcher( - name = "", - firstIndex = Offset.firstOffset, - headAtInitialization = Some(initOffset), - ) - ) - } - - // RE-INITIALIZE THE STATE - _ <- inMemoryState.initializeTo( - Some(reInitLedgerEnd), - ParameterStorageBackend.AchsState( - validAt = 0, - AchsLastPointers(lastRemoved = 0, lastPopulated = 0), - ), - ) - - // ASSERT STATE RE-INITIALIZED - _ = { - inOrder.verify(dispatcherState).stopDispatcher() - - when(dispatcherState.isRunning).thenReturn(false) - inMemoryState.initialized shouldBe false - inOrder.verify(contractStateCaches).reset(Some(reInitLedgerEnd)) - inOrder.verify(inMemoryFanoutBuffer).flush() - inOrder - .verify(mutableLedgerEndCache) - .set(Some(reInitLedgerEnd)) - inOrder.verify(dispatcherState).startDispatcher(Some(reInitOffset)) - - when(dispatcherState.isRunning).thenReturn(true) - inMemoryState.initialized shouldBe true - } - - // RE-INITIALIZE THE SAME STATE - _ <- inMemoryState.initializeTo( - Some(reInitLedgerEnd), - ParameterStorageBackend.AchsState( - validAt = 0, - AchsLastPointers(lastRemoved = 0, lastPopulated = 0), - ), - ) - - // ASSERT STATE RE-INITIALIZED - _ = inMemoryState.initialized shouldBe true - } yield succeed - } - - // since cachesUpdatedUpto can be None to signify invalid caches, we need to ensure that we reset memory state when initializing to None - "InMemoryState.initializeTo(None)" should "should reset the in-memory state" in withTestFixture { - case ( - inMemoryState, - mutableLedgerEndCache, - contractStateCaches, - inMemoryFanoutBuffer, - _, - dispatcherState, - _, - _, - _, - inOrder, - ) => - when(dispatcherState.stopDispatcher()).thenReturn(Future.unit) - when(dispatcherState.isRunning).thenReturn(true) - when(mutableLedgerEndCache.apply()).thenReturn(None) - when(dispatcherState.getDispatcher).thenReturn( - Dispatcher( - name = "", - firstIndex = Offset.firstOffset, - headAtInitialization = None, - ) - ) - - inMemoryState.ledgerEndCache() shouldBe None - dispatcherState.getDispatcher.getHead() shouldBe None - inMemoryState.cachesUpdatedUpto.get() shouldBe None - - for { - _ <- inMemoryState.initializeTo( - None, - ParameterStorageBackend.AchsState( - validAt = 0, - AchsLastPointers(lastRemoved = 0, lastPopulated = 0), - ), - ) - - _ = { - inOrder.verify(contractStateCaches).reset(None) - inOrder.verify(inMemoryFanoutBuffer).flush() - inOrder.verify(mutableLedgerEndCache).set(None) - } - } yield succeed - } - - private def withTestFixture( - test: ( - InMemoryState, - MutableLedgerEndCache, - ContractStateCaches, - InMemoryFanoutBuffer, - StringInterningView, - DispatcherState, - (UpdatingStringInterningView, LedgerEnd) => Future[Unit], - SubmissionTracker, - SubmissionTracker, - InOrder, - ) => Future[Assertion] - ): Future[Assertion] = { - val mutableLedgerEndCache = mock[MutableLedgerEndCache] - val achsStateCache = mock[AchsStateCache] - val contractStateCaches = mock[ContractStateCaches] - val offsetCheckpointCache = mock[OffsetCheckpointCache] - val inMemoryFanoutBuffer = mock[InMemoryFanoutBuffer] - val stringInterningView = mock[StringInterningView] - val dispatcherState = mock[DispatcherState] - val updateStringInterningView = mock[(UpdatingStringInterningView, LedgerEnd) => Future[Unit]] - val transactionSubmissionTracker = mock[SubmissionTracker] - val reassignmentSubmissionTracker = mock[SubmissionTracker] - val partyAllocationTracker = mock[PartyAllocation.Tracker] - val commandProgressTracker = CommandProgressTracker.NoOp - - // Mocks should be called in the asserted order - val inOrderMockCalls = Mockito.inOrder( - mutableLedgerEndCache, - contractStateCaches, - inMemoryFanoutBuffer, - stringInterningView, - dispatcherState, - updateStringInterningView, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - ) - - val inMemoryState = new InMemoryState( - participantId = Ref.ParticipantId.assertFromString("participant1"), - ledgerEndCache = mutableLedgerEndCache, - achsStateCache = achsStateCache, - contractStateCaches = contractStateCaches, - offsetCheckpointCache = offsetCheckpointCache, - inMemoryFanoutBuffer = inMemoryFanoutBuffer, - stringInterningView = stringInterningView, - dispatcherState = dispatcherState, - transactionSubmissionTracker = transactionSubmissionTracker, - reassignmentSubmissionTracker = reassignmentSubmissionTracker, - partyAllocationTracker = partyAllocationTracker, - commandProgressTracker = commandProgressTracker, - loggerFactory = loggerFactory, - ) - - test( - inMemoryState, - mutableLedgerEndCache, - contractStateCaches, - inMemoryFanoutBuffer, - stringInterningView, - dispatcherState, - updateStringInterningView, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - inOrderMockCalls, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/FatContractInstanceHelper.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/FatContractInstanceHelper.scala deleted file mode 100644 index 9696ac1e43..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/FatContractInstanceHelper.scala +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.daml.lf.data.{Bytes, Ref, Time} -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - FatContractInstance, - GlobalKeyWithMaintainers, - Node, - SerializationVersion as LfSerializationVersion, -} -import com.digitalasset.daml.lf.value.Value - -object FatContractInstanceHelper { - - def buildFatContractInstance( - templateId: Ref.Identifier, - packageName: Ref.PackageName, - contractId: Value.ContractId, - argument: Value, - createdAt: Time.Timestamp, - authenticationData: Bytes, - signatories: Set[Ref.Party], - stakeholders: Set[Ref.Party], - keyOpt: Option[GlobalKeyWithMaintainers], - version: LfSerializationVersion, - ): LfFatContractInst = { - val create = Node.Create( - templateId = templateId, - packageName = packageName, - coid = contractId, - arg = argument, - signatories = signatories, - stakeholders = stakeholders, - keyOpt = keyOpt, - version = version, - ) - FatContractInstance.fromCreateNode( - create, - CreationTime.CreatedAt(createdAt), - authenticationData, - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/GrpcServerSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/GrpcServerSpec.scala deleted file mode 100644 index b3483420cc..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/GrpcServerSpec.scala +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import com.daml.ledger.resources.ResourceOwner -import com.daml.metrics.api.testing.{InMemoryMetricsFactory, MetricValues} -import com.daml.metrics.api.{HistogramInventory, MetricName} -import com.daml.testing.utils.TestResourceContext -import com.digitalasset.base.error.{ErrorGenerator, RpcError} -import com.digitalasset.canton.config.RequireTypes.Port -import com.digitalasset.canton.config.ServerConfig -import com.digitalasset.canton.grpc.sampleservice.HelloServiceReferenceImplementation -import com.digitalasset.canton.ledger.client.GrpcChannel -import com.digitalasset.canton.ledger.client.configuration.LedgerClientChannelConfiguration -import com.digitalasset.canton.ledger.error.LedgerApiErrors -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NamedLoggerFactory, - SuppressingLogger, -} -import com.digitalasset.canton.metrics.{LedgerApiServerHistograms, LedgerApiServerMetrics} -import com.digitalasset.canton.networking.grpc.ratelimiting.LimitResult -import com.digitalasset.canton.platform.apiserver.GrpcServerSpec.* -import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig -import com.digitalasset.canton.platform.apiserver.ratelimiting.RateLimitingInterceptorFactory -import com.digitalasset.canton.protobuf.Hello -import com.digitalasset.canton.protobuf.HelloServiceGrpc.HelloService -import com.digitalasset.canton.{BaseTest, HasExecutionContext, protobuf} -import io.grpc.* -import io.grpc.ClientCall.Listener -import io.grpc.ForwardingClientCall.SimpleForwardingClientCall -import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener -import org.scalacheck.Gen -import org.scalatest.Assertion -import org.scalatest.wordspec.AsyncWordSpec - -import java.util.concurrent.Executors -import scala.concurrent.{ExecutionContext, Future} - -final class GrpcServerSpec - extends AsyncWordSpec - with BaseTest - with TestResourceContext - with HasExecutionContext - with MetricValues { - - "a GRPC server" should { - "handle a request to a valid service" in { - resources(loggerFactory).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for { - response <- helloService.hello(protobuf.Hello.Request("foo")) - } yield { - response.msg shouldBe "foofoo" - } - } - } - - "fail with a nice exception" in { - resources(loggerFactory, helloService = new FailingHelloService()(_)).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for { - exception <- helloService - .hello(protobuf.Hello.Request("This is some text.")) - .failed - } yield { - exception.getMessage shouldBe "INVALID_ARGUMENT: INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: This is some text." - } - } - } - - "fail with a nice exception, even when the text is quite long" in { - val errorMessage = "There was an error. " + "x" * 2048 - val returnedMessage = "There was an error. " + "x" * 447 + "..." - resources(loggerFactory, helloService = new FailingHelloService()(_)).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for { - exception <- helloService - .hello(protobuf.Hello.Request(errorMessage)) - .failed - } yield { - exception.getMessage shouldBe s"INVALID_ARGUMENT: INVALID_ARGUMENT(8,0): The submitted request has invalid arguments: $returnedMessage" - } - } - } - - "fail with a nice exception, even when the text is too long for the client to process" in { - val length = 1024 * 1024 - val exceptionMessage = - "There was an error. " + - LazyList.continually("x").take(length).mkString + - " And then some extra text that won't be sent." - - resources(loggerFactory, helloService = new FailingHelloService()(_)).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for { - exception <- helloService - .hello(protobuf.Hello.Request(exceptionMessage)) - .failed - } yield { - // We don't want to test the exact message content, just that it does indeed contain a - // large chunk of the response error message, followed by "...". - exception.getMessage should fullyMatch regex "INVALID_ARGUMENT: INVALID_ARGUMENT\\(8,0\\): The submitted request has invalid arguments: There was an error. x{400,}\\.\\.\\.".r - } - } - } - - "fuzzy ensure non-security sensitive errors are forwarded gracefully" in { - val checkerValue = "Sentinel error" - val nonSecuritySensitiveErrorGen = - ErrorGenerator - .errorGenerator( - redactDetails = Some(false), - // Only generate errors that have a grpc code / meant to be sent over the wire - additionalErrorCategoryFilter = _.grpcCode.isDefined, - ) - .map(err => err.copy(cause = s"$checkerValue - ${err.cause}")) - - fuzzTestErrorCodePropagation( - errorCodeGen = nonSecuritySensitiveErrorGen, - expectedIncludedMessage = checkerValue, - ) - } - - "fuzzy ensure security sensitive errors are forwarded gracefully" in { - val securitySensitiveErrorGen = ErrorGenerator.errorGenerator(redactDetails = Some(true)) - - fuzzTestErrorCodePropagation( - errorCodeGen = securitySensitiveErrorGen, - expectedIncludedMessage = - "An error occurred. Please contact the operator and inquire about the request", - ) - } - - "install rate limit interceptor" in { - val metricsFactory = new InMemoryMetricsFactory - val inventory = new HistogramInventory - val metrics = new LedgerApiServerMetrics( - new LedgerApiServerHistograms(MetricName("test"))(inventory), - metricsFactory, - ) - val overLimitRejection = LedgerApiErrors.ThreadpoolOverloaded.Rejection( - "test", - "test", - 100, - 59, - "test", - ) - val rateLimitingInterceptor = RateLimitingInterceptorFactory.create( - loggerFactory, - rateLimitingConfig, - additionalChecks = List { (_, _) => - LimitResult.OverLimit( - overLimitRejection - ) - }, - ) - resources(loggerFactory, metrics, List(rateLimitingInterceptor)).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - helloService.hello(protobuf.Hello.Request("foo")).failed.map { - case s: StatusRuntimeException => - s.getStatus.getDescription shouldBe overLimitRejection.asGrpcStatus.getMessage - case o => fail(s"Expected StatusRuntimeException, not $o") - } - } - } - - "handle a request with short header" in { - resources( - loggerFactory, - clientInterceptors = List(new HeaderClientInterceptor("ShortHeader")), - ).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for { - response <- helloService.hello(protobuf.Hello.Request("foo")) - } yield { - response.msg shouldBe "foofoo" - } - } - } - - "don't handle a request with long header" in { - resources(loggerFactory, clientInterceptors = List(new HeaderClientInterceptor("A" * 10000))) - .use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - helloService.hello(protobuf.Hello.Request("foo")).failed.map { - case s: StatusRuntimeException => - s.getStatus.getCode shouldBe Status.Code.INTERNAL - s.getStatus.getDescription shouldBe "http2 exception" - s.getCause.getMessage should include("Header size exceeded max allowed size") - case o => fail(s"Expected StatusRuntimeException, not $o") - } - } - } - - "handle a request with long header when large metadata size permitted" in { - resources( - loggerFactory, - clientInterceptors = List(new HeaderClientInterceptor("A" * 10000)), - maxInboundMetadataSize = Some(20000), - ).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for { - response <- helloService.hello(protobuf.Hello.Request("foo")) - } yield { - response.msg shouldBe "foofoo" - } - } - } - - } - - private def fuzzTestErrorCodePropagation( - errorCodeGen: Gen[RpcError], - expectedIncludedMessage: String, - ): Future[Assertion] = { - val numberOfIterations = 100 - - val randomExceptionGeneratingService = new HelloServiceReferenceImplementation { - override def hello(request: Hello.Request): Future[Hello.Response] = - Future.failed(errorCodeGen.sample.value.asGrpcError) - } - - resources(loggerFactory, helloService = _ => randomExceptionGeneratingService).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for (_ <- 1 to numberOfIterations) { - val f = for { - exception <- helloService.hello(Hello.Request("not relevant")).failed - } yield { - exception.getMessage should include(expectedIncludedMessage) - } - f.futureValue - } - succeed - } - } -} - -object GrpcServerSpec { - - private val maxInboundMessageSize = 4 * 1024 * 1024 /* copied from the Sandbox configuration */ - - private val rateLimitingConfig = RateLimitingConfig.Default - - class FailingHelloService(implicit ec: ExecutionContext) - extends HelloServiceReferenceImplementation { - override def hello(request: protobuf.Hello.Request): Future[protobuf.Hello.Response] = { - val loggerFactory = SuppressingLogger(getClass) - val logger = loggerFactory.getTracedLogger(getClass) - val errorLogger = ErrorLoggingContext(logger, LoggingContextWithTrace.ForTesting) - - Future.failed( - RequestValidationErrors.InvalidArgument - .Reject(request.msg)(errorLogger) - .asGrpcError - ) - } - } - - private def resources( - loggerFactory: NamedLoggerFactory, - metrics: LedgerApiServerMetrics = LedgerApiServerMetrics.ForTesting, - serverInterceptors: List[ServerInterceptor] = List.empty, - clientInterceptors: List[ClientInterceptor] = List.empty, - helloService: ExecutionContext => BindableService with HelloService = - new HelloServiceReferenceImplementation()(_), - maxInboundMetadataSize: Option[Int] = None, - )(implicit ec: ExecutionContext): ResourceOwner[Channel] = - for { - executor <- ResourceOwner.forExecutorService(() => Executors.newSingleThreadExecutor()) - server <- GrpcServerOwner( - address = None, - desiredPort = Port.Dynamic, - maxInboundMessageSize = maxInboundMessageSize, - maxInboundMetadataSize = maxInboundMetadataSize.getOrElse(8 * 1024), - maxConcurrentCallsPerConnection = - ServerConfig.defaultMaxConcurrentCallsPerConnection.unwrap, - metrics = metrics, - servicesExecutor = executor, - services = Seq(helloService(ec)), - interceptors = serverInterceptors, - loggerFactory = loggerFactory, - keepAlive = None, - ) - channel <- new GrpcChannel.Owner( - Port.tryCreate(server.getPort).unwrap, - LedgerClientChannelConfiguration.InsecureDefaults, - ) - } yield clientInterceptors.foldLeft[Channel](channel) { case (channel, interceptor) => - ClientInterceptors.intercept(channel, interceptor) - } - - val CUSTOM_HEADER_KEY: Metadata.Key[String] = - Metadata.Key.of("custom_client_header_key", Metadata.ASCII_STRING_MARSHALLER) - - class HeaderClientInterceptor(customHeader: String) extends ClientInterceptor { - override def interceptCall[ReqT, RespT]( - method: MethodDescriptor[ReqT, RespT], - callOptions: CallOptions, - next: Channel, - ): ClientCall[ReqT, RespT] = - new SimpleForwardingClientCall[ReqT, RespT](next.newCall(method, callOptions)) { - override def start(responseListener: Listener[RespT], headers: Metadata): Unit = { - /* put custom header */ - headers.put(CUSTOM_HEADER_KEY, customHeader) - super.start( - new SimpleForwardingClientCallListener[RespT](responseListener) { - override def onHeaders(headers: Metadata): Unit = - super.onHeaders(headers) - }, - headers, - ) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/SeedingSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/SeedingSpec.scala deleted file mode 100644 index db8a509f1d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/SeedingSpec.scala +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class SeedingSpec extends AnyWordSpec with Matchers { - "StaticRandomSeedService" should { - "return the same sequence of random numbers across multiple runs" in { - val gen1 = SeedService.staticRandom("one key") - val gen2 = SeedService.staticRandom("one key") - - val hashes1 = List.fill(100)(gen1.nextSeed()) - val hashes2 = List.fill(100)(gen2.nextSeed()) - hashes1 shouldEqual hashes2 - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/SimpleTimeServiceBackendSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/SimpleTimeServiceBackendSpec.scala deleted file mode 100644 index 3ee8865549..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/SimpleTimeServiceBackendSpec.scala +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver - -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec - -import java.time.{Instant, ZoneOffset, ZonedDateTime} - -class SimpleTimeServiceBackendSpec extends AsyncWordSpec with Matchers with ScalaFutures { - "a simple time service backend" should { - "return the time it started with" in { - val timeService = TimeServiceBackend.simple(instantAt(month = 1)) - timeService.getCurrentTime should be(instantAt(month = 1)) - } - - "update the time to a new time" in { - val timeService = TimeServiceBackend.simple(instantAt(month = 1)) - for { - _ <- timeService.setCurrentTime(instantAt(month = 1), instantAt(month = 2)) - } yield { - timeService.getCurrentTime should be(instantAt(month = 2)) - } - } - - "not allow the time to be updated without a correct expected time" in { - val timeService = TimeServiceBackend.simple(instantAt(month = 1)) - whenReady(timeService.setCurrentTime(instantAt(month = 1), instantAt(month = 2))) { - _ should be(true) - } - whenReady(timeService.setCurrentTime(instantAt(month = 1), instantAt(month = 3))) { - _ should be(false) - } - timeService.getCurrentTime should be(instantAt(month = 2)) - } - } - - // always construct new instants to avoid sharing references, which would allow us to cheat when - // comparing them inside the SimpleTimeServiceBackend - private def instantAt(month: Int): Instant = - ZonedDateTime.of(2020, month, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/error/ErrorInterceptorSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/error/ErrorInterceptorSpec.scala deleted file mode 100644 index 3f35a5e010..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/error/ErrorInterceptorSpec.scala +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.error - -import com.daml.grpc.test.StreamConsumer -import com.daml.ledger.resources.ResourceOwner -import com.daml.testing.utils.{ - PekkoBeforeAndAfterAll, - TestResourceContext, - TestingServerInterceptors, -} -import com.digitalasset.base.error.* -import com.digitalasset.base.error.utils.ErrorDetails -import com.digitalasset.canton.grpc.sampleservice.HelloServiceReferenceImplementation -import com.digitalasset.canton.ledger.api.grpc.StreamingServiceLifecycleManagement -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.{BaseTest, HasExecutionContext, protobuf} -import io.grpc.* -import io.grpc.stub.StreamObserver -import org.apache.pekko.stream.scaladsl.{Flow, Source} -import org.scalatest.* -import org.scalatest.concurrent.{Eventually, IntegrationPatience} -import org.scalatest.freespec.AsyncFreeSpec - -import scala.concurrent.Future - -final class ErrorInterceptorSpec - extends AsyncFreeSpec - with PekkoBeforeAndAfterAll - with OptionValues - with Eventually - with IntegrationPatience - with TestResourceContext - with Checkpoints - with ErrorsAssertions - with BaseTest - with HasExecutionContext { - - import ErrorInterceptorSpec.* - - private val bypassMsg: String = - "(should still intercept the error to bypass default gRPC error handling)" - - classOf[ErrorInterceptor].getSimpleName - { - - assert(FooMissingErrorCode.category.grpcCode.value != Status.Code.INTERNAL) - - "for a server unary future endpoint" - { - "when signalling with a non-self-service error should SANITIZE the server response when arising " - { - "inside a Future" in { - exerciseUnaryFutureEndpoint( - new HelloServiceFailing( - useSelfService = false, - errorInsideFutureOrStream = true, - loggerFactory = loggerFactory, - ) - ).map(assertRedactedError) - } - - s"outside a Future $bypassMsg" in { - exerciseUnaryFutureEndpoint( - new HelloServiceFailing( - useSelfService = false, - errorInsideFutureOrStream = false, - loggerFactory = loggerFactory, - ) - ).map(assertRedactedError) - } - } - - "when signalling with a self-service error should NOT SANITIZE the server response when arising" - { - "inside a Future" in { - exerciseUnaryFutureEndpoint( - new HelloServiceFailing( - useSelfService = true, - errorInsideFutureOrStream = true, - loggerFactory = loggerFactory, - ) - ) - .map { t => - assertFooMissingError( - actual = t, - expectedMsg = "Non-Status.INTERNAL self-service error inside a Future", - ) - } - } - - s"outside a Future $bypassMsg" in { - exerciseUnaryFutureEndpoint( - new HelloServiceFailing( - useSelfService = true, - errorInsideFutureOrStream = false, - loggerFactory = loggerFactory, - ) - ) - .map { t => - assertFooMissingError( - actual = t, - expectedMsg = "Non-Status.INTERNAL self-service error outside a Future", - ) - } - } - } - } - - "for a server streaming endpoint" - { - - "signal server shutting down" in { - val service = - new HelloServiceFailing( - useSelfService = false, - errorInsideFutureOrStream = true, - loggerFactory = loggerFactory, - ) - service.close() - exerciseStreamingEndpoint(service) - .map { t => - assertMatchesErrorCode(t, GrpcErrors.AbortedDueToShutdown) - } - } - - "when signalling with a non-self-service error should SANITIZE the server response when arising" - { - "inside a Stream" in { - loggerFactory.assertLogs( - within = { - exerciseStreamingEndpoint( - new HelloServiceFailing( - useSelfService = false, - errorInsideFutureOrStream = true, - loggerFactory = loggerFactory, - ) - ).map(assertRedactedError) - }, - assertions = _.errorMessage should include( - "SERVICE_INTERNAL_ERROR(4,0): Unexpected or unknown exception occurred." - ), - ) - } - - s"outside a Stream $bypassMsg" in { - exerciseStreamingEndpoint( - new HelloServiceFailing( - useSelfService = false, - errorInsideFutureOrStream = false, - loggerFactory = loggerFactory, - ) - ).map(assertRedactedError) - } - - "outside a Stream by directly calling stream-observer.onError" in { - loggerFactory.assertLogs( - exerciseStreamingEndpoint( - new HelloServiceFailingDirectlyObserverOnError - ).map(assertRedactedError) - // the transformed error is expected to not be logged - // so it is required that no entries will be found - ) - } - } - - "when signalling with a self-service error should NOT SANITIZE the server response when arising" - { - "inside a Stream" in { - exerciseStreamingEndpoint( - new HelloServiceFailing( - useSelfService = true, - errorInsideFutureOrStream = true, - loggerFactory = loggerFactory, - ) - ) - .map { t => - assertFooMissingError( - actual = t, - expectedMsg = "Non-Status.INTERNAL self-service error inside a Stream", - ) - } - } - - s"outside a Stream $bypassMsg" in { - exerciseStreamingEndpoint( - new HelloServiceFailing( - useSelfService = true, - errorInsideFutureOrStream = false, - loggerFactory = loggerFactory, - ) - ) - .map { t => - assertFooMissingError( - actual = t, - expectedMsg = "Non-Status.INTERNAL self-service error outside a Stream", - ) - } - } - } - } - } - - LogOnUnhandledFailureInClose.getClass.getSimpleName - { - "is transparent when no exception is thrown" in { - var idx = 0 - val call = () => { - idx += 1 - idx - } - assert(LogOnUnhandledFailureInClose(logger, call()) === 1) - assert(LogOnUnhandledFailureInClose(logger, call()) === 2) - } - - "logs and re-throws an exception" in { - val failure = new RuntimeException("some failure") - val failingCall = () => throw failure - - loggerFactory - .assertThrowsAndLogs[RuntimeException]( - within = LogOnUnhandledFailureInClose(logger, failingCall()), - assertions = logEntry => { - logEntry.errorMessage shouldBe "LEDGER_API_INTERNAL_ERROR(4,0): Unhandled error in ServerCall.close(). The gRPC client might have not been notified about the call/stream termination. Either notify clients to retry pending unary/streaming calls or restart the participant server." - logEntry.mdc.keys should contain("err-context") - logEntry.mdc - .get("err-context") - .value should fullyMatch regex """\{location=ErrorInterceptor.scala:\d+, throwableO=Some\(java.lang.RuntimeException: some failure\)\}""" - }, - ) - } - } - - private def exerciseUnaryFutureEndpoint( - helloService: BindableService - ): Future[StatusRuntimeException] = { - val response: Future[protobuf.Hello.Response] = server( - tested = new ErrorInterceptor(loggerFactory), - service = helloService, - ).use { channel => - protobuf.HelloServiceGrpc.stub(channel).hello(protobuf.Hello.Request("foo")) - } - recoverToExceptionIf[StatusRuntimeException] { - response - } - } - - private def exerciseStreamingEndpoint( - helloService: BindableService - ): Future[StatusRuntimeException] = { - val response: Future[Vector[protobuf.Hello.Response]] = server( - tested = new ErrorInterceptor(loggerFactory), - service = helloService, - ).use { channel => - val streamConsumer = new StreamConsumer[protobuf.Hello.Response](observer => - protobuf.HelloServiceGrpc - .stub(channel) - .helloStreamed(protobuf.Hello.Request("foo"), observer) - ) - streamConsumer.all() - } - recoverToExceptionIf[StatusRuntimeException] { - response - } - } - - private def assertRedactedError(actual: StatusRuntimeException): Assertion = { - assertError( - actual, - expectedStatusCode = Status.Code.INTERNAL, - expectedMessage = BaseError.RedactedMessage(None), - expectedDetails = Seq(), - verifyEmptyStackTrace = false, - ) - Assertions.succeed - } - - private def assertFooMissingError( - actual: StatusRuntimeException, - expectedMsg: String, - ): Assertion = { - assertError( - actual, - expectedStatusCode = FooMissingErrorCode.category.grpcCode.value, - expectedMessage = s"FOO_MISSING_ERROR_CODE(11,0): Foo is missing: $expectedMsg", - expectedDetails = Seq( - ErrorDetails.ErrorInfoDetail( - "FOO_MISSING_ERROR_CODE", - Map("category" -> "11", "test" -> getClass.getSimpleName), - ) - ), - verifyEmptyStackTrace = false, - ) - Assertions.succeed - } - - /** @param useSelfService - * whether to use self service error codes or "rogue" exceptions - * @param errorInsideFutureOrStream - * whether to signal the exception inside a Future or a Stream, or outside to them - */ - class HelloServiceFailing( - useSelfService: Boolean, - errorInsideFutureOrStream: Boolean, - val loggerFactory: NamedLoggerFactory, - ) extends HelloServiceReferenceImplementation - with StreamingServiceLifecycleManagement - with NamedLogging { - - override def helloStreamed( - request: protobuf.Hello.Request, - responseObserver: StreamObserver[protobuf.Hello.Response], - ): Unit = registerStream(responseObserver) { - implicit val traceContext: TraceContext = TraceContext.empty - val where = if (errorInsideFutureOrStream) "inside" else "outside" - val t: Throwable = if (useSelfService) { - FooMissingErrorCode - .Error(s"Non-Status.INTERNAL self-service error $where a Stream") - .asGrpcError - } else { - new IllegalArgumentException(s"Failure $where a Stream") - } - if (errorInsideFutureOrStream) { - Source - .single(request) - .via(Flow[protobuf.Hello.Request].mapConcat(_ => throw t)) - } else { - throw t - } - } - - override def hello(request: protobuf.Hello.Request): Future[protobuf.Hello.Response] = { - implicit val traceContext: TraceContext = TraceContext.empty - val where = if (errorInsideFutureOrStream) "inside" else "outside" - val t: Throwable = if (useSelfService) { - FooMissingErrorCode - .Error(s"Non-Status.INTERNAL self-service error $where a Future") - .asGrpcError - } else { - new IllegalArgumentException(s"Failure $where a Future") - } - if (errorInsideFutureOrStream) { - Future.failed(t) - } else { - throw t - } - } - } - - class HelloServiceFailingDirectlyObserverOnError extends HelloServiceReferenceImplementation { - - override def helloStreamed( - request: protobuf.Hello.Request, - responseObserver: StreamObserver[protobuf.Hello.Response], - ): Unit = - responseObserver.onError( - new IllegalArgumentException( - s"Failing the stream by passing a non error-code based error directly to observer.onError" - ) - ) - - override def hello(request: protobuf.Hello.Request): Future[protobuf.Hello.Response] = - Assertions.fail("This class is not intended to test unary endpoints") - } -} - -object ErrorInterceptorSpec { - - def server(tested: ErrorInterceptor, service: BindableService): ResourceOwner[Channel] = - TestingServerInterceptors.channelOwner(tested, service) - - object FooMissingErrorCode - extends ErrorCode( - id = "FOO_MISSING_ERROR_CODE", - ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, - )(ErrorClass.root()) { - - final case class Error(msg: String)(implicit - val loggingContext: ErrorLoggingContext - ) extends ContextualizedDamlError( - cause = s"Foo is missing: $msg" - ) - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/LedgerTimeAwareCommandExecutorSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/LedgerTimeAwareCommandExecutorSpec.scala deleted file mode 100644 index 1db72f0442..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/LedgerTimeAwareCommandExecutorSpec.scala +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.data.EitherT -import com.digitalasset.canton.data.DeduplicationPeriod.DeduplicationDuration -import com.digitalasset.canton.data.{DeduplicationPeriod, LedgerTimeBoundaries} -import com.digitalasset.canton.ledger.api.{CommandId, Commands} -import com.digitalasset.canton.ledger.participant.state.index.MaximumLedgerTime -import com.digitalasset.canton.ledger.participant.state.{ - RoutingSynchronizerState, - SubmitterInfo, - SynchronizerRank, - TransactionMeta, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.FatContractInstanceHelper -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.canton.platform.apiserver.services.ErrorCause.LedgerTime -import com.digitalasset.canton.protocol.LfSerializationVersion -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.{BaseTest, FailOnShutdown} -import com.digitalasset.daml.lf.command.ApiCommands as LfCommands -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.{Identifier, PackageName} -import com.digitalasset.daml.lf.data.{Bytes, ImmArray, Ref, Time} -import com.digitalasset.daml.lf.transaction.test.{TestNodeBuilder, TransactionBuilder} -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.ContractId -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.Assertion -import org.scalatest.wordspec.AsyncWordSpec - -import java.time.Duration -import scala.concurrent.ExecutionContext - -class LedgerTimeAwareCommandExecutorSpec - extends AsyncWordSpec - with MockitoSugar - with ArgumentMatchersSugar - with FailOnShutdown - with BaseTest { - - private val loggingContext = - LoggingContextWithTrace.ForTesting - - private val submissionSeed = Hash.hashPrivateKey("a key") - - private val cid = TransactionBuilder.newCid - - private val transaction = TransactionBuilder.justSubmitted( - TestNodeBuilder.fetch( - TestNodeBuilder.create( - id = cid, - templateId = Ref.Identifier( - Ref.PackageId.assertFromString("abc"), - Ref.QualifiedName.assertFromString("Main:Template"), - ), - argument = Value.ValueUnit, - signatories = Set.empty, - observers = Set.empty, - ), - byKey = false, - ) - ) - - private val alice = Ref.Party.assertFromString("alice") - - private val processedDisclosedContracts = ImmArray( - FatContractInstanceHelper.buildFatContractInstance( - templateId = Identifier.assertFromString("some:pkg:identifier"), - packageName = PackageName.assertFromString("pkg-name"), - contractId = cid, - argument = Value.ValueNil, - createdAt = Time.Timestamp.Epoch, - authenticationData = Bytes.Empty, - signatories = Set(alice), - stakeholders = Set(alice), - keyOpt = None, - version = LfSerializationVersion.V1, - ) - ) - private val synchronizerRank = - SynchronizerRank.single(SynchronizerId.tryFromString("some::sync").toPhysical) - private val routingSynchronizerState = mock[RoutingSynchronizerState] - private def runExecutionTest( - dependsOnLedgerTime: Boolean, - resolveMaximumLedgerTimeResults: List[MaximumLedgerTime], - finalExecutionResult: Either[ErrorCause, Time.Timestamp], - usedForExternalSigning: Boolean = false, - ): FutureUnlessShutdown[Assertion] = { - - def commandExecutionResult(let: Time.Timestamp) = CommandExecutionResult( - commandInterpretationResult = CommandInterpretationResult( - SubmitterInfo( - Nil, - Nil, - Ref.UserId.assertFromString("foobar"), - Ref.CommandId.assertFromString("foobar"), - DeduplicationDuration(Duration.ofMinutes(1)), - None, - None, - ), - TransactionMeta( - let, - None, - Time.Timestamp.Epoch, - submissionSeed, - LedgerTimeBoundaries.unconstrained, - None, - None, - None, - ), - transaction, - dependsOnLedgerTime, - 5L, - Map.empty, - processedDisclosedContracts, - None, - ), - synchronizerRank = - SynchronizerRank.single(SynchronizerId.tryFromString("some::sync").toPhysical), - routingSynchronizerState = routingSynchronizerState, - ) - - val mockExecutor = mock[CommandExecutor] - when( - mockExecutor.execute(any[Commands], any[Hash], eqTo(routingSynchronizerState), anyBoolean)( - any[LoggingContextWithTrace] - ) - ) - .thenAnswer((c: Commands) => - EitherT[FutureUnlessShutdown, ErrorCause, CommandExecutionResult]( - FutureUnlessShutdown.pure(Right(commandExecutionResult(c.commands.ledgerEffectiveTime))) - ) - ) - - val mockResolveMaximumLedgerTime = mock[ResolveMaximumLedgerTime] - resolveMaximumLedgerTimeResults.tail.foldLeft( - when( - mockResolveMaximumLedgerTime( - eqTo(processedDisclosedContracts), - any[Set[ContractId]], - )( - any[LoggingContextWithTrace], - any[ExecutionContext], - ) - ) - .thenReturn(FutureUnlessShutdown.pure(resolveMaximumLedgerTimeResults.head)) - ) { case (mock, result) => - mock.andThen(FutureUnlessShutdown.pure(result)) - } - - val commands = Commands( - workflowId = None, - userId = Ref.UserId.assertFromString("userId"), - commandId = CommandId(Ref.CommandId.assertFromString("commandId")), - submissionId = None, - actAs = Set.empty, - readAs = Set.empty, - submittedAt = Time.Timestamp.Epoch, - deduplicationPeriod = DeduplicationPeriod.DeduplicationDuration(Duration.ZERO), - commands = LfCommands( - commands = ImmArray.Empty, - ledgerEffectiveTime = Time.Timestamp.Epoch, - commandsReference = "", - ), - disclosedContracts = ImmArray.empty, - synchronizerId = None, - prefetchKeys = Seq.empty, - tapsMaxPasses = None, - ) - - val instance = new LedgerTimeAwareCommandExecutor( - mockExecutor, - mockResolveMaximumLedgerTime, - 3, - LedgerApiServerMetrics.ForTesting, - loggerFactory, - ) - - instance - .execute( - commands, - submissionSeed, - usedForExternallySigningTransaction = usedForExternalSigning, - routingSynchronizerState = routingSynchronizerState, - )(loggingContext) - .value - .map { actual => - val expectedResult = finalExecutionResult.map(let => - CommandExecutionResult( - CommandInterpretationResult( - SubmitterInfo( - Nil, - Nil, - Ref.UserId.assertFromString("foobar"), - Ref.CommandId.assertFromString("foobar"), - DeduplicationDuration(Duration.ofMinutes(1)), - None, - None, - ), - TransactionMeta( - let, - None, - Time.Timestamp.Epoch, - submissionSeed, - LedgerTimeBoundaries.unconstrained, - None, - None, - None, - ), - transaction, - dependsOnLedgerTime, - 5L, - Map.empty, - processedDisclosedContracts, - None, - ), - synchronizerRank = synchronizerRank, - routingSynchronizerState = routingSynchronizerState, - ) - ) - - verify(mockExecutor, times(resolveMaximumLedgerTimeResults.size)).execute( - any[Commands], - any[Hash], - eqTo(routingSynchronizerState), - eqTo(usedForExternalSigning), - )(any[LoggingContextWithTrace]) - - actual shouldEqual expectedResult - } - } - - private val missingCid: MaximumLedgerTime = MaximumLedgerTime.Archived(Set(cid)) - private val foundEpoch: MaximumLedgerTime = MaximumLedgerTime.Max(Time.Timestamp.Epoch) - private val epochPlus5: Time.Timestamp = Time.Timestamp.Epoch.add(Duration.ofSeconds(5)) - private val foundEpochPlus5: MaximumLedgerTime = MaximumLedgerTime.Max(epochPlus5) - private val noLetFound: MaximumLedgerTime = MaximumLedgerTime.NotAvailable - - "LedgerTimeAwareCommandExecutor" when { - "the model doesn't use getTime" should { - "not retry if ledger effective time is resolved" in { - runExecutionTest( - dependsOnLedgerTime = false, - resolveMaximumLedgerTimeResults = List(foundEpoch), - finalExecutionResult = Right(Time.Timestamp.Epoch), - ) - } - - "not retry if the maximum ledger time is not available" in { - runExecutionTest( - dependsOnLedgerTime = false, - resolveMaximumLedgerTimeResults = List(noLetFound), - finalExecutionResult = Right(Time.Timestamp.Epoch), - ) - } - - "retry if the contract cannot be found in the contract store and fail at max retries" in { - runExecutionTest( - dependsOnLedgerTime = false, - resolveMaximumLedgerTimeResults = List(missingCid, missingCid, missingCid, missingCid), - finalExecutionResult = Left(LedgerTime(3)), - ) - } - - "succeed if the contract can be found on a retry" in { - runExecutionTest( - dependsOnLedgerTime = false, - resolveMaximumLedgerTimeResults = List(missingCid, missingCid, missingCid, foundEpoch), - finalExecutionResult = Right(Time.Timestamp.Epoch), - ) - } - - "advance the output time if the contract's LET is in the future" in { - runExecutionTest( - dependsOnLedgerTime = false, - resolveMaximumLedgerTimeResults = List(foundEpochPlus5), - finalExecutionResult = Right(epochPlus5), - ) - } - } - - "the model uses getTime" should { - "retry if the contract's LET is in the future" in { - runExecutionTest( - dependsOnLedgerTime = true, - resolveMaximumLedgerTimeResults = List( - // the first lookup of +5s will cause the interpretation to be restarted, - // in case the usage of getTime with a different LET would result in a different transaction - foundEpochPlus5, - // The second lookup finds the same ledger time again - foundEpochPlus5, - ), - finalExecutionResult = Right(epochPlus5), - ) - } - - "retry if the contract's LET is in the future and then retry if the contract is missing" in { - runExecutionTest( - dependsOnLedgerTime = true, - resolveMaximumLedgerTimeResults = List( - // the first lookup of +5s will cause the interpretation to be restarted, - // in case the usage of getTime with a different LET would result in a different transaction - foundEpochPlus5, - // during the second interpretation the contract was actually archived - // and could not be found during the maximum ledger time lookup. - // this causes yet another restart of the interpretation. - missingCid, - // The third lookup finds the same ledger time again - foundEpochPlus5, - ), - finalExecutionResult = Right(epochPlus5), - ) - } - } - - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/ResolveMaximumLedgerTimeSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/ResolveMaximumLedgerTimeSpec.scala deleted file mode 100644 index 1d9f25c0c4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/ResolveMaximumLedgerTimeSpec.scala +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import com.digitalasset.canton.ledger.participant.state.index.{ - MaximumLedgerTime, - MaximumLedgerTimeService, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.apiserver.FatContractInstanceHelper -import com.digitalasset.canton.protocol.LfSerializationVersion -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.{Identifier, PackageName} -import com.digitalasset.daml.lf.data.{Bytes, ImmArray, Ref, Time} -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.ContractId -import org.mockito.captor.{ArgCaptor, Captor} -import org.mockito.invocation.InvocationOnMock -import org.mockito.stubbing.Answer -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.concurrent.Future - -class ResolveMaximumLedgerTimeSpec - extends AnyFlatSpec - with Matchers - with MockitoSugar - with ScalaFutures - with ArgumentMatchersSugar - with HasExecutionContext - with BaseTest { - - private implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace.ForTesting - - behavior of classOf[ResolveMaximumLedgerTime].getSimpleName - - it should "resolve maximum ledger time using disclosed contracts with fallback to contract store lookup" in new TestScope { - private val processedDisclosedContracts = ImmArray( - buildProcessedDisclosedContract(cId_1, t1), - buildProcessedDisclosedContract(cId_2, t2), - ) - - resolveMaximumLedgerTime( - processedDisclosedContracts, - Set(cId_2, cId_3, cId_4), - ).futureValueUS shouldBe MaximumLedgerTime.Max(t4) - } - - it should "resolve maximum ledger time when all contracts are provided as explicitly disclosed" in new TestScope { - private val processedDisclosedContracts = ImmArray( - buildProcessedDisclosedContract(cId_1, t1), - buildProcessedDisclosedContract(cId_2, t2), - ) - - resolveMaximumLedgerTime( - processedDisclosedContracts, - Set(cId_1, cId_2), - ).futureValueUS shouldBe MaximumLedgerTime.Max(t2) - } - - it should "resolve maximum ledger time when no disclosed contracts are provided" in new TestScope { - resolveMaximumLedgerTime( - ImmArray.empty, - Set(cId_1, cId_2), - ).futureValueUS shouldBe MaximumLedgerTime.Max(t2) - } - - it should "forward contract store lookup result on archived contracts" in new TestScope { - resolveMaximumLedgerTime( - ImmArray.empty, - archived.contracts + cId_1, - ).futureValueUS shouldBe archived - } - - val alice = Ref.Party.assertFromString("alice") - - private def buildProcessedDisclosedContract(cId: ContractId, createdAt: Time.Timestamp) = - FatContractInstanceHelper.buildFatContractInstance( - templateId = Identifier.assertFromString("some:pkg:identifier"), - packageName = PackageName.assertFromString("pkg-name"), - contractId = cId, - argument = Value.ValueNil, - createdAt = createdAt, - authenticationData = Bytes.Empty, - signatories = Set(alice), - stakeholders = Set(alice), - keyOpt = None, - version = LfSerializationVersion.V1, - ) - - private def contractId(id: Int): ContractId = - ContractId.V1(Hash.hashPrivateKey(id.toString)) - - private trait TestScope { - val t1: Time.Timestamp = Time.Timestamp.assertFromLong(1L) - val t2: Time.Timestamp = Time.Timestamp.assertFromLong(2L) - val t3: Time.Timestamp = Time.Timestamp.assertFromLong(3L) - val t4: Time.Timestamp = Time.Timestamp.assertFromLong(4L) - - val cId_1: ContractId = contractId(1) - val cId_2: ContractId = contractId(2) - val cId_3: ContractId = contractId(3) - val cId_4: ContractId = contractId(4) - val cId_5: ContractId = contractId(5) - - val archived: MaximumLedgerTime.Archived = MaximumLedgerTime.Archived(Set(cId_5)) - - def mapping: Map[ContractId, Time.Timestamp] = Map( - cId_1 -> t1, - cId_2 -> t2, - cId_3 -> t3, - cId_4 -> t4, - ) - - val maximumLedgerTimeServiceMock: MaximumLedgerTimeService = mock[MaximumLedgerTimeService] - val lookedUpCidsCaptor: Captor[Set[ContractId]] = ArgCaptor[Set[ContractId]] - - when( - maximumLedgerTimeServiceMock.lookupMaximumLedgerTimeAfterInterpretation( - lookedUpCidsCaptor.capture - )( - eqTo(loggingContext) - ) - ).delegate.thenAnswer(new Answer[Future[MaximumLedgerTime]]() { - override def answer(invocation: InvocationOnMock): Future[MaximumLedgerTime] = { - val lookedUpCids = lookedUpCidsCaptor.value - - if (lookedUpCids.isEmpty) Future.successful(MaximumLedgerTime.NotAvailable) - else if (archived.contracts.diff(lookedUpCids).isEmpty) Future.successful(archived) - else Future.successful(MaximumLedgerTime.Max(lookedUpCids.map(mapping).max)) - } - }) - - val resolveMaximumLedgerTime = - new ResolveMaximumLedgerTime(maximumLedgerTimeServiceMock, loggerFactory) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreterSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreterSpec.scala deleted file mode 100644 index 970bbb3e0d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreterSpec.scala +++ /dev/null @@ -1,762 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.syntax.either.* -import com.digitalasset.canton.crypto.TestSalt -import com.digitalasset.canton.examples.java.cycle.Cycle -import com.digitalasset.canton.ledger.api.Commands -import com.digitalasset.canton.ledger.api.util.TimeProvider -import com.digitalasset.canton.ledger.participant.state.index.{ - ContractKeyPage, - ContractState, - ContractStore, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.apiserver.execution.StoreBackedCommandInterpreter.StoreNeedKeyContinuationToken -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.canton.platform.apiserver.services.ErrorCause.InterpretationTimeExceeded -import com.digitalasset.canton.platform.config.CommandServiceConfig -import com.digitalasset.canton.protocol.* -import com.digitalasset.canton.time.NonNegativeFiniteDuration -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ContractValidator.ContractAuthenticatorFn -import com.digitalasset.canton.util.TestEngine -import com.digitalasset.canton.{BaseTest, FailOnShutdown, HasExecutionContext, LfPartyId, LfValue} -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.Identifier -import com.digitalasset.daml.lf.data.{Bytes, ImmArray, Ref, Time} -import com.digitalasset.daml.lf.engine.* -import com.digitalasset.daml.lf.transaction.test.TransactionBuilder -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - FatContractInstance, - GlobalKey, - NeedKeyProgression, - NextGenContractStateMachine, - Node as LfNode, -} -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.{crypto, engine} -import com.google.protobuf.ByteString -import monocle.Monocle.toAppliedFocusOps -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.wordspec.AsyncWordSpec - -import java.time.Duration -import java.util.concurrent.atomic.AtomicLong -import scala.concurrent.Future - -class StoreBackedCommandInterpreterSpec - extends AsyncWordSpec - with MockitoSugar - with ArgumentMatchersSugar - with HasExecutionContext - with FailOnShutdown - with BaseTest { - - private val testEngine = - new TestEngine( - packagePaths = Seq(CantonExamplesPath), - iterationsBetweenInterruptions = 10, - loggerFactory = loggerFactory, - ) - private val alice = LfPartyId.assertFromString("Alice") - - private val createCycleApiCommand: Commands = - testEngine.validateCommand(new Cycle("id", alice).create().commands.loneElement, alice) - - private def repeatCycleApiCommand( - cid: ContractId, - disclosedContracts: Seq[FatContractInstance] = Seq.empty, - ): Commands = - testEngine.validateCommand( - new Cycle.ContractId(cid.coid).exerciseRepeat().commands().loneElement, - alice, - disclosedContracts, - ) - - private def createCycleContract(id: String = "id") = { - val (createTx, createMeta) = - testEngine.submitAndConsume(new Cycle(id, alice).create().commands.loneElement, alice) - val createNode = createTx.nodes.values.collect { case c: LfNodeCreate => c }.loneElement - val (_, createSeed) = createMeta.nodeSeeds.toList.loneElement - val contract = ExampleContractFactory.fromCreate(createNode) - (createNode, createSeed, contract) - } - - private val salt: Bytes = ContractAuthenticationDataV1(TestSalt.generateSalt(36))( - CantonContractIdVersion.maxV1 - ).toLfBytes - private val identifier: Identifier = - Ref.Identifier(Ref.PackageId.assertFromString("p"), Ref.QualifiedName.assertFromString("m:n")) - private val packageName: PackageName = PackageName.assertFromString("pkg-name") - private val disclosedContractId: LfContractId = TransactionBuilder.newCid - private def mkCreateNode(contractId: Value.ContractId = disclosedContractId) = - LfNode.Create( - coid = contractId, - packageName = packageName, - templateId = identifier, - arg = Value.ValueTrue, - signatories = Set(Ref.Party.assertFromString("unexpectedSig")), - stakeholders = Set( - Ref.Party.assertFromString("unexpectedSig"), - Ref.Party.assertFromString("unexpectedObs"), - ), - keyOpt = Some( - KeyWithMaintainers.assertBuild( - templateId = identifier, - LfValue.ValueTrue, - crypto.Hash.hashPrivateKey("dummy-key-hash"), - Set(Ref.Party.assertFromString("unexpectedSig")), - packageName, - ) - ), - version = LfSerializationVersion.StableVersions.max, - ) - private val disclosedCreateNode = mkCreateNode() - private val disclosedContractCreateTime = Time.Timestamp.now() - - private val processedDisclosedContracts = ImmArray( - FatContract.fromCreateNode( - create = disclosedCreateNode, - createTime = CreationTime.CreatedAt(disclosedContractCreateTime), - authenticationData = salt, - ) - ) - - private val mode = NextGenContractStateMachine.Mode.default - - private val submissionSeed = Hash.hashPrivateKey("a key") - - private def mkSut( - engine: Engine, - contractStore: ContractStore = mock[ContractStore], - contractAuthenticator: ContractAuthenticatorFn = (_, _) => Left("Not authorized"), - tolerance: NonNegativeFiniteDuration = NonNegativeFiniteDuration.tryOfSeconds(60), - ) = - new StoreBackedCommandInterpreter( - engine = engine, - participant = Ref.ParticipantId.assertFromString("anId"), - packageResolver = testEngine.packageResolver, - contractStore = contractStore, - contractAuthenticator = contractAuthenticator, - metrics = LedgerApiServerMetrics.ForTesting, - prefetchingRecursionLevel = CommandServiceConfig.DefaultContractPrefetchingDepth, - loggerFactory = loggerFactory, - dynParamGetter = new TestDynamicSynchronizerParameterGetter(tolerance), - timeProvider = TimeProvider.UTC, - ) - - "StoreBackedCommandExecutor" should { - "add interpretation time and used disclosed contracts to result" in { - - val sut = mkSut(testEngine.engine, tolerance = NonNegativeFiniteDuration.Zero) - - sut - .interpret(createCycleApiCommand, mode, submissionSeed)( - LoggingContextWithTrace(loggerFactory), - executionContext, - ) - .map { actual => - actual.foreach { actualResult => - actualResult.interpretationTimeNanos should be > 0L - actualResult.processedDisclosedContracts shouldBe processedDisclosedContracts - } - succeed - } - } - - "interpret successfully if time limit is not exceeded" in { - val tolerance = NonNegativeFiniteDuration.tryOfSeconds(60) - val sut = mkSut(testEngine.engine, tolerance = tolerance) - - val commands = - createCycleApiCommand.focus(_.commands.ledgerEffectiveTime).replace(Time.Timestamp.now()) - sut - .interpret(commands, mode, submissionSeed)( - LoggingContextWithTrace(loggerFactory), - executionContext, - ) - .map { - case Right(_) => succeed - case other => fail(s"Did not expect: $other") - } - } - - "abort interpretation when time limit is exceeded" in { - val tolerance = NonNegativeFiniteDuration.tryOfSeconds(10) - val let = Time.Timestamp.now().subtract(Duration.ofSeconds(20)) - val commands = createCycleApiCommand.focus(_.commands.ledgerEffectiveTime).replace(let) - val sut = mkSut(testEngine.engine, tolerance = tolerance) - sut - .interpret(commands, mode, submissionSeed)( - LoggingContextWithTrace(loggerFactory), - executionContext, - ) - .map { - case Left(InterpretationTimeExceeded(`let`, `tolerance`, _)) => succeed - case other => fail(s"Did not expect: $other") - } - } - } - - "Disclosed contract synchronizer id consideration" should { - val synchronizerId1 = SynchronizerId.tryFromString("x::synchronizer1") - val synchronizerId2 = SynchronizerId.tryFromString("x::synchronizer2") - val disclosedContractId1 = TransactionBuilder.newCid - val disclosedContractId2 = TransactionBuilder.newCid - - implicit val traceContext: TraceContext = TraceContext.empty - - "not influence the prescribed synchronizer id if no disclosed contracts are attached" in { - val result = for { - synchronizerId_from_no_prescribed_no_disclosed <- StoreBackedCommandInterpreter - .considerDisclosedContractsSynchronizerId( - prescribedSynchronizerIdO = None, - disclosedContractsUsedInInterpretation = ImmArray.empty, - logger, - ) - synchronizerId_from_prescribed_no_disclosed <- - StoreBackedCommandInterpreter.considerDisclosedContractsSynchronizerId( - prescribedSynchronizerIdO = Some(synchronizerId1), - disclosedContractsUsedInInterpretation = ImmArray.empty, - logger, - ) - } yield { - synchronizerId_from_no_prescribed_no_disclosed shouldBe None - synchronizerId_from_prescribed_no_disclosed shouldBe Some(synchronizerId1) - } - - result.value - } - - "use the disclosed contracts synchronizer id" in { - StoreBackedCommandInterpreter - .considerDisclosedContractsSynchronizerId( - prescribedSynchronizerIdO = None, - disclosedContractsUsedInInterpretation = ImmArray( - disclosedContractId1 -> Some(synchronizerId1), - disclosedContractId2 -> Some(synchronizerId1), - ), - logger, - ) - .map(_ shouldBe Some(synchronizerId1)) - .value - } - - "return an error if synchronizer ids of disclosed contracts mismatch" in { - def test(prescribedSynchronizerIdO: Option[SynchronizerId]) = - inside( - StoreBackedCommandInterpreter - .considerDisclosedContractsSynchronizerId( - prescribedSynchronizerIdO = prescribedSynchronizerIdO, - disclosedContractsUsedInInterpretation = ImmArray( - disclosedContractId1 -> Some(synchronizerId1), - disclosedContractId2 -> Some(synchronizerId2), - ), - logger, - ) - ) { case Left(error: ErrorCause.DisclosedContractsSynchronizerIdsMismatch) => - error.mismatchingDisclosedContractSynchronizerIds shouldBe Map( - disclosedContractId1 -> synchronizerId1, - disclosedContractId2 -> synchronizerId2, - ) - } - - test(prescribedSynchronizerIdO = None) - test(prescribedSynchronizerIdO = Some(SynchronizerId.tryFromString("x::anotherOne"))) - } - - "return an error if the synchronizer id of the disclosed contracts does not match the prescribed synchronizer id" in { - val synchronizerIdOfDisclosedContracts = synchronizerId1 - val prescribedSynchronizerId = synchronizerId2 - - inside( - StoreBackedCommandInterpreter - .considerDisclosedContractsSynchronizerId( - prescribedSynchronizerIdO = Some(prescribedSynchronizerId), - disclosedContractsUsedInInterpretation = ImmArray( - disclosedContractId1 -> Some(synchronizerIdOfDisclosedContracts), - disclosedContractId2 -> Some(synchronizerIdOfDisclosedContracts), - ), - logger, - ) - ) { case Left(error: ErrorCause.PrescribedSynchronizerIdMismatch) => - error.commandsSynchronizerId shouldBe prescribedSynchronizerId - error.synchronizerIdOfDisclosedContracts shouldBe synchronizerIdOfDisclosedContracts - error.disclosedContractIds shouldBe Set(disclosedContractId1, disclosedContractId2) - } - } - } - - "Contract provision" should { - - s"fail if invalid contract id prefix is used" in { - - val contractStore = mock[ContractStore] - - val invalidCid = ExampleContractFactory.buildContractId().mapCid { - case Value.ContractId.V1(d, _) => - Value.ContractId.V1(d, Bytes.fromByteString(ByteString.copyFrom("invalid".getBytes))) - case other => fail(s"Unexpected: $other") - } - - when( - contractStore.lookupContractState( - contractId = any[ContractId] - )(any[LoggingContextWithTrace]) - ).thenReturn(Future.successful(ContractState.NotFound)) // prefetch only - - val commands = repeatCycleApiCommand(invalidCid) - - val sut = mkSut(testEngine.engine, contractStore = contractStore) - sut - .interpret(commands, mode, submissionSeed)( - LoggingContextWithTrace(loggerFactory), - executionContext, - ) - .failed - .map { _ => - succeed - } - - } - - forAll(Seq(true, false)) { disclosed => - val contractType = if (disclosed) "disclosed contract" else "local contract" - s"complete if $contractType authentication passes" in { - - val (_, _, contract) = createCycleContract() - val inst: LfFatContractInst = contract.inst - - val contractStore = mock[ContractStore] - - when( - contractStore.lookupContractState( - contractId = any[ContractId] - )(any[LoggingContextWithTrace]) - ).thenReturn(Future.successful(ContractState.NotFound)) // prefetch only - - // When a disclosed contract should be used the mock will cause a failure if it is tried and so - // verifies that the disclosed key lookup takes precedence. - if (!disclosed) { - when( - contractStore.lookupActiveContract( - readers = any[Set[Ref.Party]], - contractId = eqTo(inst.contractId), - )(any[LoggingContextWithTrace]) - ).thenReturn(Future.successful(Some(inst))) - } - - val commands = - repeatCycleApiCommand(inst.contractId, if (disclosed) Seq(inst) else Seq.empty) - - val sut = mkSut( - testEngine.engine, - contractStore = contractStore, - contractAuthenticator = (_, _) => Either.unit, - ) - - sut - .interpret(commands, mode, submissionSeed)( - LoggingContextWithTrace(loggerFactory), - executionContext, - ) - .map { - case Right(_) => succeed - case other => fail(s"Expected success, got $other") - } - } - - s"error if $contractType authentication fails" in { - - val (_, _, contract) = createCycleContract() - val inst: LfFatContractInst = contract.inst - - val contractStore = mock[ContractStore] - - when( - contractStore.lookupContractState( - contractId = any[ContractId] - )(any[LoggingContextWithTrace]) - ).thenReturn(Future.successful(ContractState.NotFound)) // prefetch only - - // When a disclosed contract should be used the mock will cause a failure if it is tried and so - // verifies that the disclosed lookup takes precedence. - if (!disclosed) { - when( - contractStore.lookupActiveContract( - readers = any[Set[Ref.Party]], - contractId = eqTo(inst.contractId), - )(any[LoggingContextWithTrace]) - ).thenReturn(Future.successful(Some(inst))) - } - - val commands = - repeatCycleApiCommand(inst.contractId, if (disclosed) Seq(inst) else Seq.empty) - - val sut = mkSut( - testEngine.engine, - contractStore = contractStore, - contractAuthenticator = (_, _) => Left("Not authorized"), - ) - - sut - .interpret(commands, mode, submissionSeed)( - LoggingContextWithTrace(loggerFactory), - executionContext, - ) - .map { - case Left(ErrorCause.DamlLf(engine.Error.Interpretation(_, _))) => succeed - case other => fail(s"Did not expect: $other") - } - - } - } - - } - - private val keyHash: crypto.Hash = crypto.Hash.hashPrivateKey("nuck-test-key") - private val globalKey: GlobalKey = - GlobalKey.assertBuild(identifier, packageName, Value.ValueText("key"), keyHash) - - private def mkContract(id: String): LfFatContractInst = { - val (_, _, contract) = createCycleContract(id) - contract.inst - } - - private val testReaders: Set[Ref.Party] = Set(alice) - private val testMetrics: LedgerApiServerMetrics = LedgerApiServerMetrics.ForTesting - private implicit val testLoggingContext: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory) - - private def mkMockContractStore( - pages: Map[Option[Long], ContractKeyPage] - ): ContractStore = { - val store = mock[ContractStore] - when( - store.lookupNonUniqueContractKey( - readers = any[Set[Ref.Party]], - key = any[GlobalKey], - pageToken = any[Option[Long]], - limit = any[Int], - )(any[LoggingContextWithTrace]) - ).thenAnswer[Set[Ref.Party], GlobalKey, Option[Long], Int, LoggingContextWithTrace] { - case (_, _, pageToken, _, _) => - Future.successful( - pages.getOrElse( - pageToken, - fail(s"Unexpected store lookup with pageToken $pageToken"), - ) - ) - } - store - } - - private val emptyContractStore: ContractStore = mkMockContractStore( - Map(None -> ContractKeyPage(contracts = Vector.empty, nextPageToken = None)) - ) - - private val invalidContractStore: ContractStore = { - val store = mock[ContractStore] - when( - store.lookupNonUniqueContractKey( - readers = any[Set[Ref.Party]], - key = any[GlobalKey], - pageToken = any[Option[Long]], - limit = any[Int], - )(any[LoggingContextWithTrace]) - ).thenReturn( - Future.failed(new IllegalStateException("Store lookup should not have been called")) - ) - store - } - - private def lookup( - disclosedContracts: Vector[LfFatContractInst] = Vector.empty, - disclosedContractsById: Map[Value.ContractId, LfFatContractInst] = Map.empty, - limit: Int = 10, - progression: NeedKeyProgression.CanContinue = NeedKeyProgression.Unstarted, - contractStore: ContractStore = emptyContractStore, - ): FutureUnlessShutdown[(Vector[LfFatContractInst], NeedKeyProgression.HasStarted)] = - StoreBackedCommandInterpreter.disclosedOrStoreNKeyLookup( - key = globalKey, - limit = limit, - disclosedContracts = disclosedContracts, - progression = progression, - disclosedContractsById = disclosedContractsById, - contractStore = contractStore, - metrics = testMetrics, - readers = testReaders, - lookupContractKeyTime = new AtomicLong(0L), - lookupContractKeyCount = new AtomicLong(0L), - ) - - private def extractInProgress( - hasStarted: NeedKeyProgression.HasStarted - ): NeedKeyProgression.InProgress = - hasStarted match { - case ip: NeedKeyProgression.InProgress => ip - case _ => fail("Expected InProgress") - } - - "disclosedOrStoreNKeyLookup" should { - - "return empty when no disclosed contracts and store is empty" in { - lookup().map { case (contracts, progression) => - contracts shouldBe empty - progression shouldBe NeedKeyProgression.Finished - } - } - - "return disclosed contracts when they fit within the limit" in { - val c1 = mkContract("1") - val c2 = mkContract("2") - val disclosed = Vector(c1, c2) - - lookup( - disclosedContracts = disclosed, - limit = 5, - contractStore = emptyContractStore, - ).map { case (contracts, _) => - contracts should contain theSameElementsInOrderAs disclosed - } - } - - "return only disclosed contracts when they do not fit within the limit" in { - val disclosed = (1 to 5).map(i => mkContract(i.toString)).toVector - - lookup( - disclosedContracts = disclosed, - limit = 3, - contractStore = invalidContractStore, - ).map { case (contracts, progression) => - contracts should have size 3 - contracts shouldBe disclosed.take(3) - progression shouldBe NeedKeyProgression.InProgress( - StoreNeedKeyContinuationToken.ContinueDisclosed(3) - ) - } - } - - "paginate through disclosed contracts across multiple calls" in { - val disclosed = (1 to 5).map(i => mkContract(i.toString)).toVector - - for { - (page1, token1) <- lookup( - disclosedContracts = disclosed, - limit = 2, - contractStore = invalidContractStore, - ) - _ = token1 shouldBe NeedKeyProgression.InProgress( - StoreNeedKeyContinuationToken.ContinueDisclosed(2) - ) - (page2, token2) <- lookup( - disclosedContracts = disclosed, - limit = 2, - progression = extractInProgress(token1), - contractStore = invalidContractStore, - ) - _ = token2 shouldBe NeedKeyProgression.InProgress( - StoreNeedKeyContinuationToken.ContinueDisclosed(4) - ) - (page3, token3) <- lookup( - disclosedContracts = disclosed, - limit = 2, - progression = extractInProgress(token2), - contractStore = emptyContractStore, - ) - } yield { - page1 shouldBe disclosed.slice(0, 2) - page2 shouldBe disclosed.slice(2, 4) - page3 shouldBe disclosed.slice(4, 5) - token3 shouldBe NeedKeyProgression.Finished - } - } - - "fall back to store when no disclosed contracts" in { - val inStore = mkContract("inStore") - val store = mkMockContractStore( - Map(None -> ContractKeyPage(Vector(inStore), nextPageToken = None)) - ) - - lookup( - disclosedContracts = Vector(), - limit = 3, - contractStore = store, - ).map { case (contracts, progression) => - contracts shouldBe Vector(inStore) - progression shouldBe NeedKeyProgression.Finished - } - } - - "fall back to store when disclosed contracts are exhausted" in { - val c1 = mkContract("disclosed") - val inStore = mkContract("in-store") - val store = mkMockContractStore( - Map(None -> ContractKeyPage(Vector(inStore), nextPageToken = None)) - ) - - lookup( - disclosedContracts = Vector(c1), - limit = 3, - contractStore = store, - ).map { case (contracts, progression) => - contracts shouldBe Vector(c1, inStore) - progression shouldBe NeedKeyProgression.Finished - } - } - - "deduplicate store contracts that are also in disclosed contracts" in { - val sharedContract = mkContract("shared") - val storeOnlyContract = mkContract("store-only") - - val disclosedById = Map(sharedContract.contractId -> sharedContract) - val store = mkMockContractStore( - Map( - None -> ContractKeyPage( - Vector(sharedContract, storeOnlyContract), - nextPageToken = None, - ) - ) - ) - - lookup( - disclosedContracts = Vector(sharedContract), - disclosedContractsById = disclosedById, - limit = 5, - contractStore = store, - ).map { case (contracts, _) => - contracts shouldBe Vector(sharedContract, storeOnlyContract) - } - } - - "correctly pass through store pagination tokens" in { - val storeContract1 = mkContract("store1") - val storeContract2 = mkContract("store2") - - val store = mkMockContractStore( - Map( - None -> ContractKeyPage(Vector(storeContract1), nextPageToken = Some(42L)), - Some(42L) -> ContractKeyPage(Vector(storeContract2), nextPageToken = None), - ) - ) - - for { - (page1, token1) <- lookup( - limit = 1, - contractStore = store, - ) - _ = token1 shouldBe NeedKeyProgression.InProgress( - StoreNeedKeyContinuationToken.ContinueFromStore(Some(42L)) - ) - (page2, token2) <- lookup( - limit = 1, - progression = extractInProgress(token1), - contractStore = store, - ) - } yield { - page1 shouldBe Vector(storeContract1) - page2 shouldBe Vector(storeContract2) - token2 shouldBe NeedKeyProgression.Finished - } - } - - "delegate directly to store when progression has ContinueFromStore token" in { - val storeContract = mkContract("store") - val storeToken = StoreNeedKeyContinuationToken.ContinueFromStore(Some(99L)) - val store = mkMockContractStore( - Map(Some(99L) -> ContractKeyPage(Vector(storeContract), nextPageToken = None)) - ) - - lookup( - disclosedContracts = Vector(mkContract("disclosed")), - limit = 5, - progression = NeedKeyProgression.InProgress(storeToken), - contractStore = store, - ).map { case (contracts, progression) => - contracts shouldBe Vector(storeContract) - progression shouldBe NeedKeyProgression.Finished - } - } - - "throw on invalid continuation token" in { - case object InvalidToken extends NeedKeyProgression.Token - - val result = the[IllegalArgumentException] thrownBy lookup( - progression = NeedKeyProgression.InProgress(InvalidToken) - ) - result.getMessage should include("Invalid token provided") - } - - "filter out all store contracts if they are all in disclosed" in { - val c1 = mkContract("1") - val c2 = mkContract("2") - - val disclosedById = Map( - c1.contractId -> c1, - c2.contractId -> c2, - ) - val store = mkMockContractStore( - Map(None -> ContractKeyPage(Vector(c1, c2), nextPageToken = None)) - ) - - lookup( - disclosedContracts = Vector(c1, c2), - disclosedContractsById = disclosedById, - contractStore = store, - ).map { case (contracts, _) => - contracts shouldBe Vector(c1, c2) - } - } - - "resume from ContinueDisclosed offset correctly" in { - val disclosed = (1 to 6).map(i => mkContract(i.toString)).toVector - val storeContract = mkContract("store") - val store = mkMockContractStore( - Map(None -> ContractKeyPage(Vector(storeContract), nextPageToken = None)) - ) - - lookup( - disclosedContracts = disclosed, - limit = 3, - progression = NeedKeyProgression.InProgress( - StoreNeedKeyContinuationToken.ContinueDisclosed(4) - ), - contractStore = store, - ).map { case (contracts, progression) => - contracts shouldBe disclosed.drop(4) :+ storeContract - progression shouldBe NeedKeyProgression.Finished - } - } - - "deduplicate store contracts against disclosed when resuming from ContinueFromStore" in { - // `shared` was already served from disclosed contracts in a previous page. - val shared = mkContract("shared") - val storeOnly = mkContract("store-only") - - val disclosedById = Map(shared.contractId -> shared) - val storeToken = StoreNeedKeyContinuationToken.ContinueFromStore(Some(42L)) - val store = mkMockContractStore( - Map(Some(42L) -> ContractKeyPage(Vector(shared, storeOnly), nextPageToken = None)) - ) - - lookup( - disclosedContracts = Vector(shared), - disclosedContractsById = disclosedById, - limit = 5, - progression = NeedKeyProgression.InProgress(storeToken), - contractStore = store, - ).map { case (contracts, progression) => - contracts shouldBe Vector(storeOnly) - progression shouldBe NeedKeyProgression.Finished - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/TestDynamicSynchronizerParameterGetter.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/TestDynamicSynchronizerParameterGetter.scala deleted file mode 100644 index 610057398d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/execution/TestDynamicSynchronizerParameterGetter.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.execution - -import cats.data.EitherT -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.time.NonNegativeFiniteDuration -import com.digitalasset.canton.topology.PhysicalSynchronizerId -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.ExecutionContext - -class TestDynamicSynchronizerParameterGetter( - ledgerTimeRecordTimeTolerance: NonNegativeFiniteDuration -)(implicit - ec: ExecutionContext -) extends DynamicSynchronizerParameterGetter { - override def getLedgerTimeRecordTimeTolerance(synchronizerIdO: Option[PhysicalSynchronizerId])( - implicit traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, String, NonNegativeFiniteDuration] = - EitherT.pure[FutureUnlessShutdown, String](ledgerTimeRecordTimeTolerance) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheckSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheckSpec.scala deleted file mode 100644 index 393aa8824b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheckSpec.scala +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.ratelimiting - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.logging.SuppressionRule -import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig -import com.digitalasset.canton.platform.apiserver.ratelimiting.MemoryCheck.* -import org.scalatest.flatspec.AnyFlatSpec -import org.slf4j.event.Level - -import java.lang.management.{MemoryMXBean, MemoryPoolMXBean, MemoryType, MemoryUsage} -import scala.concurrent.duration.DurationInt - -/** Note that most of the check functionality is tested via [[RateLimitingInterceptorChecksSpec]] */ -class MemoryCheckSpec extends AnyFlatSpec with BaseTest { - - private val config = RateLimitingConfig(100, 10, 75, 100 * RateLimitingConfig.Megabyte) - - // For tests that do not involve memory - private def underLimitMemoryPoolMXBean(): MemoryPoolMXBean = { - val memoryPoolBean = mock[MemoryPoolMXBean] - when(memoryPoolBean.getType).thenReturn(MemoryType.HEAP) - when(memoryPoolBean.getName).thenReturn("UnderLimitPool") - when(memoryPoolBean.getCollectionUsage).thenReturn(new MemoryUsage(0, 0, 0, 0)) - when(memoryPoolBean.isCollectionUsageThresholdSupported).thenReturn(true) - when(memoryPoolBean.isCollectionUsageThresholdExceeded).thenReturn(false) - } - - behavior of "MemoryCheck" - - it should "throttle calls to GC" in { - val delegate = mock[MemoryMXBean] - val delayBetweenCalls = 100.milliseconds - val underTest = new GcThrottledMemoryBean(delegate, delayBetweenCalls) - underTest.gc() - underTest.gc() - verify(delegate, times(1)).gc() - Threading.sleep(2 * delayBetweenCalls.toMillis) - underTest.gc() - verify(delegate, times(2)).gc() - succeed - } - - it should "use largest tenured pool as rate limiting pool" in { - val expected = underLimitMemoryPoolMXBean() - when(expected.getCollectionUsage).thenReturn(new MemoryUsage(0, 0, 0, 100)) - loggerFactory.assertLogs(MemoryCheckSpecSuppressionRule)( - within = { - findTenuredMemoryPool(config, Nil, errorLoggingContext) shouldBe None - }, - assertions = _.errorMessage should include("Could not find tenured memory pool"), - ) - findTenuredMemoryPool( - config, - List( - underLimitMemoryPoolMXBean(), - expected, - underLimitMemoryPoolMXBean(), - ), - errorLoggingContext, - ) shouldBe Some(expected) - } - - val MemoryCheckSpecSuppressionRule: SuppressionRule = - SuppressionRule.forLogger[MemoryCheckSpec] && SuppressionRule.Level(Level.ERROR) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/RateLimitingInterceptorChecksSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/RateLimitingInterceptorChecksSpec.scala deleted file mode 100644 index 0839c6f871..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/RateLimitingInterceptorChecksSpec.scala +++ /dev/null @@ -1,687 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.ratelimiting - -import com.daml.ledger.resources.ResourceOwner -import com.daml.metrics.api.{MetricInfo, MetricQualification, MetricsContext} -import com.daml.ports.Port -import com.daml.scalautil.Statement.discard -import com.daml.testing.utils.{PekkoBeforeAndAfterAll, TestResourceContext} -import com.digitalasset.canton.config.RequireTypes.NonNegativeInt -import com.digitalasset.canton.grpc.sampleservice.HelloServiceReferenceImplementation -import com.digitalasset.canton.health.HealthChecks.ComponentName -import com.digitalasset.canton.health.{HealthChecks, ReportsHealth} -import com.digitalasset.canton.ledger.api.grpc.{GrpcClientResource, GrpcHealthService} -import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.ratelimiting.ActiveRequestCounterInterceptor -import com.digitalasset.canton.networking.grpc.ratelimiting.LimitResult.LimitResultCheck -import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig -import com.digitalasset.canton.util.OptionUtils.OptionExtension -import com.digitalasset.canton.{BaseTest, HasExecutionContext, protobuf} -import io.grpc.* -import io.grpc.Status.Code -import io.grpc.health.v1.health.{HealthCheckRequest, HealthCheckResponse, HealthGrpc} -import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder -import io.grpc.protobuf.services.ProtoReflectionServiceV1 -import io.grpc.reflection.v1.{ - ServerReflectionGrpc, - ServerReflectionRequest, - ServerReflectionResponse, -} -import io.grpc.stub.StreamObserver -import org.mockito.MockitoSugar -import org.scalatest.concurrent.Eventually -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.time.{Second, Span} - -import java.io.IOException -import java.lang.management.* -import java.net.{InetAddress, InetSocketAddress} -import java.util.concurrent.{LinkedBlockingQueue, TimeUnit} -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future, Promise} - -final class RateLimitingInterceptorChecksSpec - extends AsyncFlatSpec - with PekkoBeforeAndAfterAll - with Eventually - with TestResourceContext - with MockitoSugar - with HasExecutionContext - with BaseTest { - - import RateLimitingInterceptorChecksSpec.* - - implicit override val patienceConfig: PatienceConfig = - PatienceConfig(timeout = scaled(Span(1, Second))) - - private val config = RateLimitingConfig(100, 10, 75, 100 * RateLimitingConfig.Megabyte) - private val metrics = LedgerApiServerMetrics.ForTesting - - behavior of "RateLimitingInterceptor" - - /** Allowing metadata requests allows grpcurl to be used to debug problems */ - it should "allow metadata requests even when over limit" in { - metrics.openTelemetryMetricsFactory - .meter( - MetricInfo( - metrics.lapi.threadpool.apiQueryServices :+ "submitted", - "", - MetricQualification.Debug, - ) - )(MetricsContext.Empty) - .mark(config.maxApiServicesQueueSize.toLong + 1)(MetricsContext.Empty) // Over limit - - val protoService = ProtoReflectionServiceV1.newInstance() - - withChannel(metrics, protoService, config, loggerFactory).use { channel => - val methodDescriptor: MethodDescriptor[ServerReflectionRequest, ServerReflectionResponse] = - ServerReflectionGrpc.getServerReflectionInfoMethod - val call = channel.newCall(methodDescriptor, CallOptions.DEFAULT) - val promise = Promise[Status]() - val listener = new ClientCall.Listener[ServerReflectionResponse]() { - override def onReady(): Unit = - call.request(1) - override def onClose(status: Status, trailers: Metadata): Unit = - promise.success(status) - } - call.start(listener, new Metadata()) - val request = ServerReflectionRequest - .newBuilder() - .setListServices("services") - .setHost("localhost") - .build() - call.sendMessage(ServerReflectionRequest.newBuilder(request).build()) - call.halfClose() - promise.future.map(status => status shouldBe Status.OK) - } - } - - it should "allow health checks event when over limit" in { - metrics.openTelemetryMetricsFactory - .meter( - MetricInfo( - metrics.lapi.threadpool.apiQueryServices :+ "submitted", - "", - MetricQualification.Debug, - ) - )(MetricsContext.Empty) - .mark(config.maxApiServicesQueueSize.toLong + 1)(MetricsContext.Empty) // Over limit - - val healthService = - new GrpcHealthService(healthChecks, loggerFactory = loggerFactory)( - executionSequencerFactory, - materializer, - executionContext, - ) - - withChannel(metrics, healthService, config, loggerFactory).use { channel => - val healthStub = HealthGrpc.stub(channel) - val promise = Promise[Unit]() - for { - _ <- healthStub.check(HealthCheckRequest()) - _ = healthStub.watch( - HealthCheckRequest(), - new StreamObserver[HealthCheckResponse] { - override def onNext(value: HealthCheckResponse): Unit = - promise.success(()) - override def onError(t: Throwable): Unit = {} - override def onCompleted(): Unit = {} - }, - ) - _ <- promise.future - } yield { - succeed - } - } - } - - it should "limit calls when there is a danger of running out of heap space" in { - val poolName = "Tenured_Gen" - val maxMemory = 100000L - - // Based on a combination of JvmMetricSet and MemoryUsageGaugeSet - val expectedMetric = s"jvm_memory_usage_pools_$poolName" - - val memoryBean = mock[MemoryMXBean] - - val memoryPoolBean = mock[MemoryPoolMXBean] - when(memoryPoolBean.getType).thenReturn(MemoryType.HEAP) - when(memoryPoolBean.getName).thenReturn(poolName) - when(memoryPoolBean.getCollectionUsage).thenReturn(new MemoryUsage(0, 0, 0, maxMemory)) - when(memoryPoolBean.isCollectionUsageThresholdSupported).thenReturn(true) - when(memoryPoolBean.isCollectionUsageThresholdExceeded).thenReturn(false, true, false) - when(memoryPoolBean.getCollectionUsageThreshold).thenReturn( - config.calculateCollectionUsageThreshold(maxMemory) - ) - - val nonCollectableBean = mock[MemoryPoolMXBean] - when(nonCollectableBean.getType).thenReturn(MemoryType.HEAP) - when(nonCollectableBean.isCollectionUsageThresholdSupported).thenReturn(false) - - val nonHeapBean = mock[MemoryPoolMXBean] - when(nonHeapBean.getType).thenReturn(MemoryType.NON_HEAP) - when(nonHeapBean.isCollectionUsageThresholdSupported).thenReturn(true) - - val pool = List(nonCollectableBean, nonHeapBean, memoryPoolBean) - - withChannel( - metrics, - new HelloServiceReferenceImplementation, - config, - loggerFactory, - pool, - memoryBean, - ).use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for { - _ <- helloService.hello(protobuf.Hello.Request("one")) - exception <- helloService.hello(protobuf.Hello.Request("two")).failed - _ <- helloService.hello(protobuf.Hello.Request("three")) - } yield { - verify(memoryPoolBean).setCollectionUsageThreshold( - config.calculateCollectionUsageThreshold(maxMemory) - ) - verify(memoryBean).gc() - exception.getMessage should include(expectedMetric) - } - - } - } - - it should "limit the number of streams" in { - - val waitService = new WaitService() - withChannel( - metrics, - waitService, - config, - loggerFactory, - requestLimits = Map(testStreamName -> 2), - ) - .use { channel => - val (activeGauge, limitGauge) = - metrics.requests.getActiveAndLimitGauge(testApiName, testStreamName) - for { - fStatus1 <- streamHello(channel) // Ok - fStatus2 <- streamHello(channel) // Ok - // Metrics are used by the limiting interceptor, so the following assert causes the test to fail - // rather than being stuck if metrics don't work. - _ = limitGauge.getValue shouldBe 2 - - fStatus3 <- streamHello(channel) // Limited - status3 <- fStatus3 // Closed as part of limiting - _ = waitService.completeStream() - status1 <- fStatus1 - fStatus4 <- streamHello(channel) // Ok - _ = waitService.completeStream() - status2 <- fStatus2 - _ = waitService.completeStream() - status4 <- fStatus4 - } yield { - status1.getCode shouldBe Code.OK - status2.getCode shouldBe Code.OK - status3.getCode shouldBe Code.ABORTED - status4.getCode shouldBe Code.OK - eventually(activeGauge.getValue shouldBe 0) - } - - } - } - - it should "limit the number of unary requests" in { - - val waitService = new WaitService() - val (activeRpcGauge, _) = - metrics.requests.getActiveAndLimitGauge(testApiName, testRpcName) - val (activeStreamGauge, _) = - metrics.requests.getActiveAndLimitGauge(testApiName, testStreamName) - withChannel( - metrics, - waitService, - config, - loggerFactory, - requestLimits = Map(testStreamName -> 0, testRpcName -> 1), - ) - .use { channel => - for { - - fStatus1 <- streamHello(channel) - activeStreams1 = activeStreamGauge.getValue - fHelloStatus1 = singleHello(channel, logger) - _ = eventually() { - activeRpcGauge.getValue shouldBe 1 - } - fStatus2 <- streamHello(channel) - fHelloStatus2 = singleHello(channel, logger) - // complete here (should fail) - helloStatus2 <- fHelloStatus2 - - activeStreams2 = activeStreamGauge.getValue - - _ = waitService.completeSingle() - - status1 <- fStatus1 - helloStatus1 <- fHelloStatus1 - status2 <- fStatus2 - - } yield { - activeStreams1 shouldBe 0 - activeStreams2 shouldBe 0 - status1.getCode shouldBe Code.ABORTED - helloStatus1.getCode shouldBe Code.OK - status2.getCode shouldBe Code.ABORTED - helloStatus2.getCode shouldBe Code.ABORTED - eventually(activeRpcGauge.getValue shouldBe 0) - } - - } - - } - - def testExceptionHandling(waitService: WaitService, channel: Channel): Future[Unit] = - for { - fStatus <- streamHello(channel) - _ = waitService.failStream(new Exception("fail-stream")) - fHelloStatus1 = singleHello(channel, logger) - _ = waitService.failSingle(new Exception("fail-single")) - status <- fStatus - helloStatus1 <- fHelloStatus1 - } yield { - status.getCode shouldBe Code.UNKNOWN - helloStatus1.getCode shouldBe Code.INTERNAL - } - - def testCancelStreamHandling(waitService: WaitService, channel: Channel): Future[Unit] = - for { - (fStatus, cancel) <- streamHelloWithCancel(channel) - _ <- waitService.dropObserver() - _ = cancel() - status <- fStatus - } yield { - status.getCode shouldBe Code.CANCELLED - } - - def testCancelSingleHandling(waitService: WaitService, channel: Channel): Future[Unit] = { - val (fHelloStatus, cancel) = singleHelloWithCancel(channel, logger) - for { - _ <- waitService.dropRequest() - _ = cancel() - status <- fHelloStatus - } yield { - status.getCode shouldBe Code.CANCELLED - } - } - - def testNormalHandling(waitService: WaitService, channel: Channel): Future[Unit] = - for { - fStatus <- streamHello(channel) - fHelloStatus3 = singleHello(channel, logger) - _ = waitService.completeStream() - _ = waitService.completeSingle() - status <- fStatus - helloStatus <- fHelloStatus3 - } yield { - status.getCode shouldBe Code.OK - helloStatus.getCode shouldBe Code.OK - } - - it should "properly account for failed requests" in { - - val waitService = new WaitService() - val (activeRpcGauge, _) = - metrics.requests.getActiveAndLimitGauge(testApiName, testRpcName) - val (activeStreamGauge, _) = - metrics.requests.getActiveAndLimitGauge(testApiName, testStreamName) - withChannel( - metrics, - waitService, - config, - loggerFactory, - requestLimits = Map(testStreamName -> 1, testRpcName -> 1), - ) - .use { channel => - for { - _ <- testExceptionHandling(waitService, channel) - _ <- testCancelSingleHandling(waitService, channel) - _ <- testCancelStreamHandling(waitService, channel) - _ <- testNormalHandling(waitService, channel) - } yield { - eventually(activeRpcGauge.getValue shouldBe 0) - eventually(activeStreamGauge.getValue shouldBe 0) - } - } - } - - it should "exclude non-stream traffic from stream counts" in { - - val waitService = new WaitService() - val (activeGauge, _) = - metrics.requests.getActiveAndLimitGauge(testApiName, testStreamName) - withChannel( - metrics, - waitService, - config, - loggerFactory, - requestLimits = Map(testStreamName -> 2), - ) - .use { channel => - for { - - fStatus1 <- streamHello(channel) - fHelloStatus1 = singleHello(channel, logger) - fStatus2 <- streamHello(channel) - fHelloStatus2 = singleHello(channel, logger) - - activeStreams = activeGauge.getValue - - _ = waitService.completeStream() - _ = waitService.completeSingle() - _ = waitService.completeStream() - _ = waitService.completeSingle() - - status1 <- fStatus1 - helloStatus1 <- fHelloStatus1 - status2 <- fStatus2 - helloStatus2 <- fHelloStatus2 - - } yield { - activeStreams shouldBe 2 - status1.getCode shouldBe Code.OK - helloStatus1.getCode shouldBe Code.OK - status2.getCode shouldBe Code.OK - helloStatus2.getCode shouldBe Code.OK - eventually(activeGauge.getValue shouldBe 0) - } - } - } - - it should "stream rate limiting should not limit non-stream traffic" in { - - val waitService = new WaitService() - withChannel( - metrics, - waitService, - config, - loggerFactory, - requestLimits = Map(testStreamName -> 2), - ).use { channel => - for { - - fStatus1 <- streamHello(channel) - fStatus2 <- streamHello(channel) - fHelloStatus1 = singleHello(channel, logger) - - _ = waitService.completeSingle() - _ = waitService.completeStream() - _ = waitService.completeStream() - - _ <- fStatus1 - _ <- fStatus2 - helloStatus <- fHelloStatus1 - - } yield { - helloStatus.getCode shouldBe Code.OK - } - } - } - - it should "maintain stream count for streams cancellations" in { - - val waitService = new WaitService() - val (activeGauge, _) = - metrics.requests.getActiveAndLimitGauge(testApiName, testStreamName) - withChannel( - metrics, - waitService, - config, - loggerFactory, - requestLimits = Map(testStreamName -> 2), - ).use { channel => - for { - _ <- testCancelStreamHandling(waitService, channel) - } yield { - eventually(activeGauge.getValue shouldBe 0) - } - } - } - - it should "should reset limit if a change of max memory is detected" in { - - val initMemory = 100000L - val increasedMemory = 200000L - - val memoryBean = mock[MemoryMXBean] - - val memoryPoolBean = mock[MemoryPoolMXBean] - when(memoryPoolBean.getType).thenReturn(MemoryType.HEAP) - when(memoryPoolBean.getName).thenReturn("Tenured_Gen") - when(memoryPoolBean.getCollectionUsage).thenReturn( - new MemoryUsage(0, 0, 0, initMemory), - new MemoryUsage(0, 0, 0, increasedMemory), - ) - when(memoryPoolBean.isCollectionUsageThresholdSupported).thenReturn(true) - when(memoryPoolBean.isCollectionUsageThresholdExceeded).thenReturn(true) - - val pool = List(memoryPoolBean) - - loggerFactory.suppressWarnings { - withChannel( - metrics, - new HelloServiceReferenceImplementation, - config, - loggerFactory, - pool, - memoryBean, - ) - .use { channel => - val helloService = protobuf.HelloServiceGrpc.stub(channel) - for { - _ <- helloService.hello(protobuf.Hello.Request("foo")) - } yield { - verify(memoryPoolBean).setCollectionUsageThreshold( - config.calculateCollectionUsageThreshold(initMemory) - ) - verify(memoryPoolBean).setCollectionUsageThreshold( - config.calculateCollectionUsageThreshold(increasedMemory) - ) - succeed - } - } - } - } - - it should "calculate the collection threshold zone size" in { - // The actual threshold used would be max(maxHeapSpacePercentage * maxHeapSize / 100, maxHeapSize - maxOverThresholdZoneSize) - val underTest = - RateLimitingConfig.Default.copy(maxUsedHeapSpacePercentage = 90, minFreeHeapSpaceBytes = 1000) - underTest.calculateCollectionUsageThreshold(3000) shouldBe 2700 // 90% - underTest.calculateCollectionUsageThreshold(101000) shouldBe 100000 // 101000 - 1000 - } - -} - -object RateLimitingInterceptorChecksSpec extends MockitoSugar { - - private val testApiName = "rate-limit-test" - private val testRpcName = "com.digitalasset.canton.protobuf.HelloService/Hello" - private val testStreamName = "com.digitalasset.canton.protobuf.HelloService/HelloStreamed" - - private val healthChecks = new HealthChecks(Map.empty[ComponentName, ReportsHealth]) - - // For tests that do not involve memory - private def underLimitMemoryPoolMXBean(): MemoryPoolMXBean = { - val memoryPoolBean = mock[MemoryPoolMXBean] - when(memoryPoolBean.getType).thenReturn(MemoryType.HEAP) - when(memoryPoolBean.getName).thenReturn("UnderLimitPool") - when(memoryPoolBean.getCollectionUsage).thenReturn(new MemoryUsage(0, 0, 0, 0)) - when(memoryPoolBean.isCollectionUsageThresholdSupported).thenReturn(true) - when(memoryPoolBean.isCollectionUsageThresholdExceeded).thenReturn(false) - } - - def withChannel( - metrics: LedgerApiServerMetrics, - service: BindableService, - config: RateLimitingConfig, - loggerFactory: NamedLoggerFactory, - pool: List[MemoryPoolMXBean] = List(underLimitMemoryPoolMXBean()), - memoryBean: MemoryMXBean = ManagementFactory.getMemoryMXBean, - additionalChecks: List[LimitResultCheck] = List.empty, - requestLimits: Map[String, Int] = Map.empty, - ): ResourceOwner[Channel] = { - val rateLimitingInterceptor = RateLimitingInterceptorFactory.createWithMXBeans( - loggerFactory, - config, - pool, - memoryBean, - additionalChecks, - ) - - val activeStreamMetricsInterceptor = new ActiveRequestCounterInterceptor( - testApiName, - requestLimits.map { case (k, v) => (k, NonNegativeInt.tryCreate(v)) }, - warnOnUnconfiguredLimits = false, - maxLoggingRatePerSecond = NonNegativeInt.tryCreate(10), - metrics = metrics.requests, - loggerFactory = loggerFactory, - ) - for { - server <- ResourceOwner.forServer( - NettyServerBuilder - .forAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 0)) - .directExecutor() - .intercept(activeStreamMetricsInterceptor) - .intercept(rateLimitingInterceptor) - .addService(service), - FiniteDuration(10, "seconds"), - ) - channel <- GrpcClientResource.owner(Port(server.getPort)) - } yield channel - } - - /** By default [[HelloServiceReferenceImplementation]] will return all elements and complete the - * stream on the server side on every request. For stream based rate limiting we want to - * explicitly hold open the stream such that we know for sure how many streams are open. - */ - class WaitService(implicit ec: ExecutionContext) extends HelloServiceReferenceImplementation { - - private val observers = new LinkedBlockingQueue[StreamObserver[protobuf.Hello.Response]]() - private val requests = new LinkedBlockingQueue[Promise[protobuf.Hello.Response]]() - - def completeStream(): Unit = { - val responseObserver = observers.remove() - responseObserver.onNext(protobuf.Hello.Response("last")) - responseObserver.onCompleted() - } - - def dropRequest(): Future[Unit] = - Option(requests.poll(10, TimeUnit.SECONDS)) - .map(_ => ()) - .toFuture(new Exception("Failed to drop request")) - - def dropObserver(): Future[Unit] = - Option(observers.poll(10, TimeUnit.SECONDS)) - .map(_ => ()) - .toFuture(new Exception("Failed to drop observer")) - - def failStream(ex: Exception): Unit = { - val responseObserver = observers.remove() - responseObserver.onError(ex) - } - - def completeSingle(): Unit = - discard( - requests - .poll(10, TimeUnit.SECONDS) - .success(protobuf.Hello.Response("only")) - ) - - def failSingle(ex: Exception): Unit = - discard(requests.poll(10, TimeUnit.SECONDS).failure(ex)) - - override def helloStreamed( - request: protobuf.Hello.Request, - responseObserver: StreamObserver[protobuf.Hello.Response], - ): Unit = { - responseObserver.onNext(protobuf.Hello.Response("first")) - observers.put(responseObserver) - } - - override def hello(request: protobuf.Hello.Request): Future[protobuf.Hello.Response] = { - val promise = Promise[protobuf.Hello.Response]() - requests.put(promise) - promise.future - } - - } - - def singleHello(channel: Channel, logger: TracedLogger): Future[Status] = { - val (f, _) = singleHelloWithCancel(channel, logger) - f - } - - def singleHelloWithCancel( - channel: Channel, - logger: TracedLogger, - ): (Future[Status], () => Unit) = { - - val statusP = Promise[Status]() - - val clientCall = channel.newCall(protobuf.HelloServiceGrpc.METHOD_HELLO, CallOptions.DEFAULT) - - clientCall.start( - new ClientCall.Listener[protobuf.Hello.Response] { - override def onClose(grpcStatus: Status, trailers: Metadata): Unit = { - logger.underlying.debug(s"Single closed with $grpcStatus") - statusP.success(grpcStatus) - } - override def onMessage(message: protobuf.Hello.Response): Unit = - logger.underlying.debug(s"Got single message: $message") - }, - new Metadata(), - ) - - clientCall.sendMessage(protobuf.Hello.Request("foo")) - clientCall.halfClose() - clientCall.request(1) - - (statusP.future, () => clientCall.cancel("Test cancel", new IOException("network down"))) - } - - def streamHello(channel: Channel)(implicit ec: ExecutionContext): Future[Future[Status]] = - streamHelloWithCancel(channel).map { case (f, _) => f } - - def streamHelloWithCancel( - channel: Channel - )(implicit ec: ExecutionContext): Future[(Future[Status], () => Unit)] = { - - val init = Promise[Future[Status]]() - val status = Promise[Status]() - val clientCall = - channel.newCall(protobuf.HelloServiceGrpc.METHOD_HELLO_STREAMED, CallOptions.DEFAULT) - - clientCall.start( - new ClientCall.Listener[protobuf.Hello.Response] { - override def onClose(grpcStatus: Status, trailers: Metadata): Unit = { - if (!init.isCompleted) init.success(status.future) - status.success(grpcStatus) - } - override def onMessage(message: protobuf.Hello.Response): Unit = - if (!init.isCompleted) init.success(status.future) - }, - new Metadata(), - ) - - // When the handshake is single message -> streamHello then onReady is not applicable - clientCall.sendMessage(protobuf.Hello.Request("foo")) - clientCall.halfClose() - clientCall.request(2) // Request both messages - - init.future.map(f => - (f, () => clientCall.cancel("Test cancel", new IOException("network down"))) - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandServiceSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandServiceSpec.scala deleted file mode 100644 index b72c26cc53..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandServiceSpec.scala +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.command_service.{ - SubmitAndWaitForReassignmentRequest, - SubmitAndWaitForReassignmentResponse, - SubmitAndWaitForTransactionRequest, - SubmitAndWaitForTransactionResponse, - SubmitAndWaitRequest, - SubmitAndWaitResponse, -} -import com.daml.ledger.api.v2.commands.{Command, CreateCommand} -import com.daml.ledger.api.v2.value.{Identifier, Record, RecordField, Value} -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.api.MockMessages.* -import com.digitalasset.canton.ledger.api.services.CommandService -import com.digitalasset.canton.ledger.api.validation.{ - CommandsValidator, - ValidateUpgradingPackageResolutions, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.daml.lf.data.Ref -import org.mockito.captor.ArgCaptor -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec - -import java.time.{Duration, Instant} -import java.util.concurrent.atomic.AtomicInteger -import scala.concurrent.Future - -class ApiCommandServiceSpec - extends AsyncWordSpec - with MockitoSugar - with Matchers - with ArgumentMatchersSugar - with BaseTest { - - import ApiCommandServiceSpec.* - - "ApiCommandService" should { - "generate a submission ID if it's empty" in { - val submissionCounter = new AtomicInteger - val mockCommandService = createMockCommandService - val grpcCommandService = new ApiCommandService( - mockCommandService, - commandsValidator = commandsValidator, - currentLedgerTime = () => Instant.EPOCH, - currentUtcTime = () => Instant.EPOCH, - maxDeduplicationDuration = Duration.ZERO, - generateSubmissionId = () => - Ref.SubmissionId.assertFromString( - s"$submissionIdPrefix${submissionCounter.incrementAndGet()}" - ), - loggerFactory = loggerFactory, - ) - - for { - _ <- grpcCommandService.submitAndWait(aSubmitAndWaitRequestWithNoSubmissionId) - _ <- grpcCommandService.submitAndWaitForTransaction( - aSubmitAndWaitForTransactionRequestWithNoSubmissionId - ) - _ <- grpcCommandService.submitAndWaitForReassignment( - aSubmitAndWaitForReassignmentRequestWithNoSubmissionId - ) - } yield { - def expectedSubmitAndWaitRequest(submissionIdSuffix: String): SubmitAndWaitRequest = - aSubmitAndWaitRequestWithNoSubmissionId.update( - _.commands.submissionId := s"$submissionIdPrefix$submissionIdSuffix" - ) - def expectedSubmitAndWaitForTransactionRequest( - submissionIdSuffix: String - ): SubmitAndWaitForTransactionRequest = - aSubmitAndWaitForTransactionRequestWithNoSubmissionId.update( - _.commands.submissionId := s"$submissionIdPrefix$submissionIdSuffix" - ) - def expectedSubmitAndWaitForReassignmentRequest( - submissionIdSuffix: String - ): SubmitAndWaitForReassignmentRequest = - aSubmitAndWaitForReassignmentRequestWithNoSubmissionId.update( - _.reassignmentCommands.submissionId := s"$submissionIdPrefix$submissionIdSuffix" - ) - val requestCaptorSubmitAndWait = ArgCaptor[SubmitAndWaitRequest] - val requestCaptorSubmitAndWaitForTransaction = ArgCaptor[SubmitAndWaitForTransactionRequest] - val requestCaptorSubmitAndWaitForReassignment = - ArgCaptor[SubmitAndWaitForReassignmentRequest] - - verify(mockCommandService).submitAndWait(requestCaptorSubmitAndWait.capture)( - any[LoggingContextWithTrace] - ) - requestCaptorSubmitAndWait.value shouldBe expectedSubmitAndWaitRequest("1") - verify(mockCommandService).submitAndWaitForTransaction( - requestCaptorSubmitAndWaitForTransaction.capture - )( - any[LoggingContextWithTrace] - ) - requestCaptorSubmitAndWaitForTransaction.value shouldBe - expectedSubmitAndWaitForTransactionRequest("2") - verify(mockCommandService).submitAndWaitForReassignment( - requestCaptorSubmitAndWaitForReassignment.capture - )( - any[LoggingContextWithTrace] - ) - requestCaptorSubmitAndWaitForReassignment.value shouldBe - expectedSubmitAndWaitForReassignmentRequest("3") - succeed - } - } - "accept submission with provided disclosed contracts" in { - val mockCommandService = createMockCommandService - - val grpcCommandService = new ApiCommandService( - mockCommandService, - commandsValidator = commandsValidator, - currentLedgerTime = () => Instant.EPOCH, - currentUtcTime = () => Instant.EPOCH, - maxDeduplicationDuration = Duration.ZERO, - generateSubmissionId = () => Ref.SubmissionId.assertFromString(s"submissionId"), - loggerFactory = loggerFactory, - ) - - val submissionWithDisclosedContracts = aSubmitAndWaitRequestWithNoSubmissionId.update( - _.commands.disclosedContracts.set(Seq(DisclosedContractCreator.disclosedContract)) - ) - - val submissionWithDisclosedContractsForTransaction = - aSubmitAndWaitForTransactionRequestWithNoSubmissionId.update( - _.commands.disclosedContracts.set(Seq(DisclosedContractCreator.disclosedContract)) - ) - - for { - _ <- grpcCommandService.submitAndWait(submissionWithDisclosedContracts) - _ <- grpcCommandService.submitAndWaitForTransaction( - submissionWithDisclosedContractsForTransaction - ) - } yield { - succeed - } - } - - } -} - -object ApiCommandServiceSpec { - private val aCommand = Command.of( - Command.Command.Create( - CreateCommand( - Some(Identifier("package", moduleName = "module", entityName = "entity")), - Some( - Record( - Some(Identifier("package", moduleName = "module", entityName = "entity")), - Seq(RecordField("something", Some(Value(Value.Sum.Bool(true))))), - ) - ), - ) - ) - ) - - private val aSubmitAndWaitRequestWithNoSubmissionId = - submitAndWaitRequest.update(_.commands.commands := Seq(aCommand), _.commands.submissionId := "") - private val aSubmitAndWaitForTransactionRequestWithNoSubmissionId = - submitAndWaitForTransactionRequest.update( - _.commands.commands := Seq(aCommand), - _.commands.submissionId := "", - ) - private val aSubmitAndWaitForReassignmentRequestWithNoSubmissionId = - submitAndWaitForReassignmentRequest.update( - _.reassignmentCommands.submissionId := "" - ) - - private val submissionIdPrefix = "submissionId-" - - private val commandsValidator = new CommandsValidator( - validateUpgradingPackageResolutions = ValidateUpgradingPackageResolutions.Empty - ) - - def createMockCommandService: CommandService & AutoCloseable = { - import org.mockito.ArgumentMatchersSugar.* - import org.mockito.MockitoSugar.* - val mockCommandService = mock[CommandService & AutoCloseable] - when( - mockCommandService.submitAndWait(any[SubmitAndWaitRequest])(any[LoggingContextWithTrace]) - ) - .thenReturn(Future.successful(SubmitAndWaitResponse.defaultInstance)) - when( - mockCommandService.submitAndWaitForTransaction(any[SubmitAndWaitForTransactionRequest])( - any[LoggingContextWithTrace] - ) - ) - .thenReturn(Future.successful(SubmitAndWaitForTransactionResponse.defaultInstance)) - when( - mockCommandService.submitAndWaitForReassignment(any[SubmitAndWaitForReassignmentRequest])( - any[LoggingContextWithTrace] - ) - ) - .thenReturn(Future.successful(SubmitAndWaitForReassignmentResponse.defaultInstance)) - mockCommandService - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandSubmissionServiceSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandSubmissionServiceSpec.scala deleted file mode 100644 index 622fe90606..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/ApiCommandSubmissionServiceSpec.scala +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.commands.{Command, CreateCommand} -import com.daml.ledger.api.v2.value.{Identifier, Record, RecordField, Value} -import com.daml.tracing.SpanAttribute -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.api.MockMessages.* -import com.digitalasset.canton.ledger.api.messages.command.submission.SubmitRequest -import com.digitalasset.canton.ledger.api.services.CommandSubmissionService -import com.digitalasset.canton.ledger.api.validation.{ - CommandsValidator, - ValidateUpgradingPackageResolutions, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker -import com.digitalasset.canton.tracing.{Spanning, TestTelemetrySetup, TraceContextGrpc} -import com.digitalasset.daml.lf.data.Ref -import org.mockito.captor.ArgCaptor -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.BeforeAndAfterEach -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec - -import java.time.{Duration, Instant} - -class ApiCommandSubmissionServiceSpec - extends AsyncWordSpec - with MockitoSugar - with Matchers - with ArgumentMatchersSugar - with BaseTest - with BeforeAndAfterEach - with Spanning { - private val generatedSubmissionId = "generated-submission-id" - - var testTelemetrySetup: TestTelemetrySetup = _ - override def beforeEach(): Unit = - testTelemetrySetup = new TestTelemetrySetup() - override def afterEach(): Unit = - testTelemetrySetup.close() - - import ApiCommandSubmissionServiceSpec.* - "ApiCommandSubmissionService" should { - "propagate trace context" in { - // Simulate the creation of a span and attaching it to the gRPC context, as would happen in a real interceptor. - withSpan("grpc-span") { tc => _ => - TraceContextGrpc.withGrpcContext(tc) { - grpcCommandSubmissionService(createMockCommandSubmissionService) - .submit(aSubmitRequest) - } - }(traceContext, testTelemetrySetup.tracer) - .map { _ => - val spanAttributes = testTelemetrySetup.reportedSpanAttributes - spanAttributes should contain(SpanAttribute.UserId -> userId) - spanAttributes should contain(SpanAttribute.CommandId -> commandId) - spanAttributes should contain(SpanAttribute.Submitter -> party) - spanAttributes should contain(SpanAttribute.WorkflowId -> workflowId) - } - } - - "propagate submission id" in { - val expectedSubmissionId = "explicitSubmissionId" - val requestWithSubmissionId = - aSubmitRequest.update(_.commands.submissionId := expectedSubmissionId) - val requestCaptor = - ArgCaptor[SubmitRequest] - val mockCommandSubmissionService = createMockCommandSubmissionService - when( - mockCommandSubmissionService - .submit(any[SubmitRequest])(any[LoggingContextWithTrace]) - ) - .thenReturn(FutureUnlessShutdown.unit) - - grpcCommandSubmissionService(mockCommandSubmissionService) - .submit(requestWithSubmissionId) - .map { _ => - verify(mockCommandSubmissionService) - .submit(requestCaptor.capture)(any[LoggingContextWithTrace]) - requestCaptor.value.commands.submissionId shouldBe Some(expectedSubmissionId) - } - } - - "set submission id if empty" in { - val requestCaptor = - ArgCaptor[SubmitRequest] - - val mockCommandSubmissionService = createMockCommandSubmissionService - when( - mockCommandSubmissionService - .submit(any[SubmitRequest])(any[LoggingContextWithTrace]) - ) - .thenReturn(FutureUnlessShutdown.unit) - - grpcCommandSubmissionService(mockCommandSubmissionService) - .submit(aSubmitRequest) - .map { _ => - verify(mockCommandSubmissionService) - .submit(requestCaptor.capture)(any[LoggingContextWithTrace]) - requestCaptor.value.commands.submissionId shouldBe Some(generatedSubmissionId) - } - } - - "accept submission with provided disclosed contracts" in { - val mockCommandSubmissionService = createMockCommandSubmissionService - - val submissionWithDisclosedContracts = - aSubmitRequest.update( - _.commands.disclosedContracts.set(Seq(DisclosedContractCreator.disclosedContract)) - ) - - grpcCommandSubmissionService(mockCommandSubmissionService) - .submit(submissionWithDisclosedContracts) - .map { _ => - succeed - } - } - } - - private def grpcCommandSubmissionService( - commandSubmissionService: CommandSubmissionService & AutoCloseable - ) = - new ApiCommandSubmissionService( - commandSubmissionService = commandSubmissionService, - commandsValidator = new CommandsValidator( - validateUpgradingPackageResolutions = ValidateUpgradingPackageResolutions.Empty - ), - submissionSyncService = null, - currentLedgerTime = () => Instant.EPOCH, - currentUtcTime = () => Instant.EPOCH, - maxDeduplicationDuration = Duration.ZERO, - submissionIdGenerator = () => Ref.SubmissionId.assertFromString(generatedSubmissionId), - tracker = CommandProgressTracker.NoOp, - metrics = LedgerApiServerMetrics.ForTesting, - loggerFactory = loggerFactory, - ) -} - -object ApiCommandSubmissionServiceSpec { - private val aCommand = Command.of( - Command.Command.Create( - CreateCommand( - Some(Identifier("package", moduleName = "module", entityName = "entity")), - Some( - Record( - Some(Identifier("package", moduleName = "module", entityName = "entity")), - Seq(RecordField("something", Some(Value(Value.Sum.Bool(true))))), - ) - ), - ) - ) - ) - - private val aSubmitRequest = submitRequest.copy( - commands = Some(commands.copy(commands = Seq(aCommand))) - ) - - def createMockCommandSubmissionService: CommandSubmissionService & AutoCloseable = { - import MockitoSugar.* - import org.mockito.ArgumentMatchersSugar.* - val mockCommandSubmissionService = mock[CommandSubmissionService & AutoCloseable] - when( - mockCommandSubmissionService - .submit(any[SubmitRequest])(any[LoggingContextWithTrace]) - ) - .thenReturn(FutureUnlessShutdown.unit) - mockCommandSubmissionService - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/DisclosedContractCreator.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/DisclosedContractCreator.scala deleted file mode 100644 index 66c2a50add..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/DisclosedContractCreator.scala +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services - -import com.daml.ledger.api.v2.commands.DisclosedContract -import com.daml.ledger.api.v2.value.Identifier -import com.digitalasset.canton.LfValue -import com.digitalasset.canton.protocol.ExampleContractFactory -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time} -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - FatContractInstance, - GlobalKeyWithMaintainers, - TransactionCoder, -} -import com.digitalasset.daml.lf.value.Value.{ValueRecord, ValueTrue} - -object DisclosedContractCreator { - - private object api { - val templateId: Identifier = - Identifier("package", moduleName = "module", entityName = "entity") - val packageName: String = "pkg-name" - - val alice: Ref.Party = Ref.Party.assertFromString("alice") - val bob: Ref.Party = Ref.Party.assertFromString("bob") - val charlie: Ref.Party = Ref.Party.assertFromString("charlie") - val stakeholders: Set[Ref.Party] = Set(alice, bob, charlie) - val signatories: Set[Ref.Party] = Set(alice, bob) - val keyMaintainers: Set[Ref.Party] = Set(bob) - val createdAtSeconds = 1337L - } - - private object lf { - private val templateId: Ref.Identifier = Ref.Identifier( - Ref.PackageId.assertFromString(api.templateId.packageId), - Ref.QualifiedName( - Ref.ModuleName.assertFromString(api.templateId.moduleName), - Ref.DottedName.assertFromString(api.templateId.entityName), - ), - ) - private val packageName: Ref.PackageName = Ref.PackageName.assertFromString(api.packageName) - private val createArg: ValueRecord = ValueRecord( - tycon = Some(templateId), - fields = ImmArray(Some(Ref.Name.assertFromString("something")) -> ValueTrue), - ) - - private val keyWithMaintainers: GlobalKeyWithMaintainers = GlobalKeyWithMaintainers.assertBuild( - lf.templateId, - LfValue.ValueRecord( - None, - ImmArray( - None -> LfValue.ValueParty(api.alice), - None -> LfValue.ValueText("some key"), - ), - ), - crypto.Hash.hashPrivateKey("dummy-key-hash"), - api.keyMaintainers, - Ref.PackageName.assertFromString(api.packageName), - ) - - val fatContractInstance: FatContractInstance = ExampleContractFactory - .build( - templateId = templateId, - packageName = packageName, - argument = createArg, - createdAt = - CreationTime.CreatedAt(Time.Timestamp.assertFromLong(api.createdAtSeconds * 1000000L)), - signatories = api.signatories, - stakeholders = api.stakeholders, - keyOpt = Some(lf.keyWithMaintainers), - ) - .inst - } - - val disclosedContract: DisclosedContract = DisclosedContract( - templateId = Some(api.templateId), - contractId = lf.fatContractInstance.contractId.coid, - createdEventBlob = TransactionCoder - .encodeFatContractInstance(lf.fatContractInstance) - .fold( - err => - throw new RuntimeException(s"Cannot serialize createdEventBlob: ${err.errorMessage}"), - identity, - ), - synchronizerId = "", - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPackageManagementServiceSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPackageManagementServiceSpec.scala deleted file mode 100644 index 8c02f7ab58..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPackageManagementServiceSpec.scala +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import cats.data.EitherT -import com.daml.ledger.api.v2.admin.package_management_service.{ - PackageManagementServiceGrpc, - UploadDarFileRequest, - ValidateDarFileRequest, - ValidateDarFileResponse, -} -import com.daml.nonempty.NonEmpty -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.base.error.ErrorsAssertions -import com.digitalasset.canton.crypto.HashOps -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.error.{TransactionError, TransactionRoutingError} -import com.digitalasset.canton.health.HealthStatus -import com.digitalasset.canton.ledger.api.{ - EnrichedVettedPackages, - ListVettedPackagesOpts, - UpdateVettedPackagesOpts, - UploadDarVettingChange, -} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.SyncService.SubmissionCostEstimation -import com.digitalasset.canton.ledger.participant.state.{ - InternalIndexService, - PruningResult, - ReassignmentCommand, - RoutingSynchronizerState, - SubmissionResult, - SubmitterInfo, - SynchronizerRank, - TransactionMeta, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.SuppressionRule -import com.digitalasset.canton.platform.apiserver.services.command.interactive.CostEstimationHints -import com.digitalasset.canton.protocol.{ - LfContractId, - LfFatContractInst, - LfSubmittedTransaction, - LfVersionedTransaction, -} -import com.digitalasset.canton.scheduler.SafeToPruneCommitmentState -import com.digitalasset.canton.topology.{ - DefaultTestIdentities, - ExternalPartyOnboardingDetails, - ParticipantId, - PartyId, - PhysicalSynchronizerId, - SynchronizerId, -} -import com.digitalasset.canton.tracing.{TestTelemetrySetup, TraceContext, TraceContextGrpc} -import com.digitalasset.canton.{BaseTest, LfGlobalKeyMapping, LfPackageId, LfPartyId} -import com.digitalasset.daml.lf.data.Ref.{CommandId, Party, SubmissionId, UserId, WorkflowId} -import com.digitalasset.daml.lf.data.{ImmArray, Ref} -import com.digitalasset.daml.lf.transaction.SubmittedTransaction -import com.google.protobuf.ByteString -import io.opentelemetry.api.trace.Tracer -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.BeforeAndAfterEach -import org.scalatest.concurrent.Eventually -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec -import org.slf4j.event.Level.DEBUG - -import scala.concurrent.Future - -// TODO(#17635) Very thin layer. Revisit utility of testing -class ApiPackageManagementServiceSpec - extends AsyncWordSpec - with MockitoSugar - with Matchers - with ArgumentMatchersSugar - with PekkoBeforeAndAfterAll - with Eventually - with ErrorsAssertions - with BaseTest - with BeforeAndAfterEach { - - import ApiPackageManagementServiceSpec.* - - var testTelemetrySetup: TestTelemetrySetup = _ - - override def beforeEach(): Unit = - testTelemetrySetup = new TestTelemetrySetup() - - override def afterEach(): Unit = - testTelemetrySetup.close() - - "ApiPackageManagementService $suffix" should { - "have a tid" in { - val apiService = createApiService() - val span = testTelemetrySetup.anEmptySpan() - val _ = span.makeCurrent() - TraceContextGrpc.withGrpcContext(TraceContext.createNew("tid-test")) { - loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(DEBUG))( - within = { - apiService - .uploadDarFile( - UploadDarFileRequest( - ByteString.EMPTY, - aSubmissionId, - UploadDarFileRequest.VettingChange.VETTING_CHANGE_VET_ALL_PACKAGES, - synchronizerId = "", - ) - ) - .map(_ => succeed) - }, - { logEntries => - logEntries should not be empty - - val mdcs = logEntries.map(_.mdc) - forEvery(mdcs)(_.getOrElse("trace-id", "") should not be empty) - }, - ) - } - } - - "validate a dar" in { - val apiService = createApiService() - apiService - .validateDarFile(ValidateDarFileRequest(ByteString.EMPTY, aSubmissionId, "")) - .map { case ValidateDarFileResponse() => succeed } - } - } - - private def createApiService(): PackageManagementServiceGrpc.PackageManagementService = - ApiPackageManagementService.createApiService( - TestSyncService(testTelemetrySetup.tracer), - loggerFactory = loggerFactory, - ) -} - -object ApiPackageManagementServiceSpec { - private val aSubmissionId = "aSubmission" - - private final case class TestSyncService(tracer: Tracer) extends state.SyncService { - override def uploadDar( - dar: Seq[ByteString], - submissionId: Ref.SubmissionId, - vettingChange: UploadDarVettingChange, - synchronizerId: Option[SynchronizerId], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] = - Future.successful(state.SubmissionResult.Acknowledged) - - override def validateDar( - dar: ByteString, - darName: String, - synchronizerId: Option[SynchronizerId], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] = - Future.successful(state.SubmissionResult.Acknowledged) - - override def internalIndexService: Option[InternalIndexService] = - throw new UnsupportedOperationException() - - override def registerInternalIndexService(internalIndexService: InternalIndexService): Unit = - throw new UnsupportedOperationException() - - override def unregisterInternalIndexService(): Unit = - throw new UnsupportedOperationException() - - override def currentHealth(): HealthStatus = - throw new UnsupportedOperationException() - - override def hashOps: HashOps = throw new UnsupportedOperationException() - - override def submitTransaction( - transaction: SubmittedTransaction, - synchronizerRank: SynchronizerRank, - routingSynchronizerState: RoutingSynchronizerState, - submitterInfo: SubmitterInfo, - transactionMeta: TransactionMeta, - // Currently, the estimated interpretation cost is not used - _estimatedInterpretationCost: Long, - keyResolver: LfGlobalKeyMapping, - processedDisclosedContracts: ImmArray[LfFatContractInst], - )(implicit - traceContext: TraceContext - ): Future[SubmissionResult] = - throw new UnsupportedOperationException() - - override def submitReassignment( - submitter: Party, - userId: UserId, - commandId: CommandId, - submissionId: Option[SubmissionId], - workflowId: Option[WorkflowId], - reassignmentCommands: Seq[ReassignmentCommand], - )(implicit traceContext: TraceContext): Future[SubmissionResult] = - throw new UnsupportedOperationException() - - override def allocateParty( - partyId: PartyId, - submissionId: SubmissionId, - synchronizerIdO: Option[SynchronizerId], - externalPartyOnboardingDetails: Option[ExternalPartyOnboardingDetails], - )(implicit traceContext: TraceContext): FutureUnlessShutdown[SubmissionResult] = - throw new UnsupportedOperationException() - - override def prune( - pruneUpToInclusive: Offset, - safeToPruneCommitmentState: Option[SafeToPruneCommitmentState], - )(implicit traceContext: TraceContext): Future[PruningResult] = - throw new UnsupportedOperationException() - - override def computePartyVettingMap( - submitters: Set[LfPartyId], - informees: Set[LfPartyId], - vettingValidityTimestamp: CantonTimestamp, - prescribedSynchronizer: Option[SynchronizerId], - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[Map[PhysicalSynchronizerId, Map[LfPartyId, Set[LfPackageId]]]] = - throw new UnsupportedOperationException() - - override def computeHighestRankedSynchronizerFromAdmissible( - submitterInfo: SubmitterInfo, - transaction: LfSubmittedTransaction, - transactionMeta: TransactionMeta, - admissibleSynchronizers: NonEmpty[Set[PhysicalSynchronizerId]], - disclosedContractIds: List[LfContractId], - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, TransactionRoutingError, PhysicalSynchronizerId] = - throw new UnsupportedOperationException() - - override def selectRoutingSynchronizer( - submitterInfo: SubmitterInfo, - transaction: LfSubmittedTransaction, - transactionMeta: TransactionMeta, - disclosedContractIds: List[LfContractId], - optSynchronizerId: Option[SynchronizerId], - transactionUsedForExternalSigning: Boolean, - routingSynchronizerState: RoutingSynchronizerState, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, TransactionError, SynchronizerRank] = - throw new UnsupportedOperationException() - - override def getRoutingSynchronizerState(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[RoutingSynchronizerState] = - throw new UnsupportedOperationException() - - override def estimateTrafficCost( - synchronizerId: SynchronizerId, - transaction: LfVersionedTransaction, - transactionMetadata: TransactionMeta, - submitterInfo: SubmitterInfo, - keyResolver: LfGlobalKeyMapping, - disclosedContracts: Map[LfContractId, LfFatContractInst], - costHints: CostEstimationHints, - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, String, SubmissionCostEstimation] = - throw new UnsupportedOperationException() - - override def listVettedPackages( - opts: ListVettedPackagesOpts - )(implicit - traceContext: TraceContext - ): Future[Seq[EnrichedVettedPackages]] = - throw new UnsupportedOperationException() - - override def updateVettedPackages( - opts: UpdateVettedPackagesOpts - )(implicit - traceContext: TraceContext - ): Future[(Option[EnrichedVettedPackages], Option[EnrichedVettedPackages])] = - throw new UnsupportedOperationException() - - override def physicalSynchronizerIdForSynchronizerId( - synchronizerId: SynchronizerId - ): Option[PhysicalSynchronizerId] = - throw new UnsupportedOperationException() - - override def participantId: ParticipantId = DefaultTestIdentities.participant1 - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPartyManagementServiceSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPartyManagementServiceSpec.scala deleted file mode 100644 index cf54f46640..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiPartyManagementServiceSpec.scala +++ /dev/null @@ -1,1202 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import cats.syntax.traverse.* -import com.daml.ledger.api.v2.admin.party_management_service.AllocateExternalPartyRequest.SignedTransaction -import com.daml.ledger.api.v2.admin.party_management_service.{ - AllocateExternalPartyRequest, - AllocatePartyRequest, - GenerateExternalPartyTopologyRequest, - GenerateExternalPartyTopologyResponse, - PartyDetails as ProtoPartyDetails, -} -import com.daml.ledger.api.v2.crypto.SignatureFormat.SIGNATURE_FORMAT_RAW -import com.daml.ledger.api.v2.{crypto, crypto as lapicrypto} -import com.daml.nonempty.NonEmpty -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.base.error.ErrorsAssertions -import com.digitalasset.base.error.utils.ErrorDetails -import com.digitalasset.base.error.utils.ErrorDetails.RetryInfoDetail -import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} -import com.digitalasset.canton.crypto.v30.SigningKeyScheme.SIGNING_KEY_SCHEME_UNSPECIFIED -import com.digitalasset.canton.crypto.{ - Fingerprint, - HashOps, - SigningKeyUsage, - SigningPublicKey, - TestHash, - v30, -} -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta} -import com.digitalasset.canton.ledger.localstore.api.{ - PartyRecord, - PartyRecordStore, - UserManagementStore, -} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent.Added -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel.Submission -import com.digitalasset.canton.ledger.participant.state.index.{ - IndexPartyManagementService, - IndexerPartyDetails, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{ - LoggingContextWithTrace, - NamedLoggerFactory, - SuppressionRule, -} -import com.digitalasset.canton.platform.apiserver.services.admin.ApiPartyManagementService.{ - CreateSubmissionId, - blindAndConvertToProto, -} -import com.digitalasset.canton.platform.apiserver.services.admin.ApiPartyManagementServiceSpec.* -import com.digitalasset.canton.platform.apiserver.services.tracking.{InFlight, StreamTracker} -import com.digitalasset.canton.topology.transaction.TopologyChangeOp.Replace -import com.digitalasset.canton.topology.transaction.{ - DecentralizedNamespaceDefinition, - DelegationRestriction, - HostingParticipant, - NamespaceDelegation, - ParticipantPermission, - PartyHostingLimits, - PartyToKeyMapping, - PartyToParticipant, - TopologyChangeOp, - TopologyMapping, - TopologyTransaction, -} -import com.digitalasset.canton.topology.{ - DefaultTestIdentities, - ExternalPartyOnboardingDetails, - Namespace, - ParticipantId, - PartyId, - PhysicalSynchronizerId, - SynchronizerId, -} -import com.digitalasset.canton.tracing.{Spanning, TestTelemetrySetup, TraceContext} -import com.digitalasset.canton.{BaseTest, HasExecutorService, LfPartyId} -import com.digitalasset.daml.lf.data.Ref -import com.google.protobuf.ByteString -import io.grpc.Status.Code -import io.grpc.StatusRuntimeException -import io.opentelemetry.api.trace.Tracer -import io.scalaland.chimney.dsl.* -import org.mockito.{ArgumentMatchers, ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.BeforeAndAfterEach -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec -import org.slf4j.event.Level -import scalapb.lenses.{Lens, Mutation} - -import java.security.{KeyPair, KeyPairGenerator, Signature} -import scala.concurrent.duration.{DurationInt, FiniteDuration} -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -class ApiPartyManagementServiceSpec - extends AsyncWordSpec - with MockitoSugar - with Matchers - with ScalaFutures - with ArgumentMatchersSugar - with PekkoBeforeAndAfterAll - with ErrorsAssertions - with BaseTest - with Spanning - with BeforeAndAfterEach - with HasExecutorService { - - var testTelemetrySetup: TestTelemetrySetup = _ - val partiesPageSize = PositiveInt.tryCreate(100) - - val aPartyAllocationTracker = - PartyAllocation.TrackerKey( - DefaultTestIdentities.party1.toLf, - DefaultTestIdentities.participant1.toLf, - Added(Submission), - ) - val createSubmissionId = new CreateSubmissionId { - override def apply( - partyIdHint: LfPartyId, - authorizationLevel: TopologyTransactionEffective.AuthorizationLevel, - ): PartyAllocation.TrackerKey = aPartyAllocationTracker - } - - lazy val ( - mockIdentityProviderExists, - mockIndexPartyManagementService, - mockUserManagementStore, - mockPartyRecordStore, - ) = mockedServices() - val partyAllocationTracker = makePartyAllocationTracker(loggerFactory) - - lazy val apiService = ApiPartyManagementService.createApiService( - mockIndexPartyManagementService, - mockUserManagementStore, - mockIdentityProviderExists, - partiesPageSize, - NonNegativeInt.tryCreate(0), - mockPartyRecordStore, - TestPartySyncService(testTelemetrySetup.tracer), - oneHour, - createSubmissionId, - mock[PartyAllocation.Tracker], - loggerFactory = loggerFactory, - ) - - override def beforeEach(): Unit = - testTelemetrySetup = new TestTelemetrySetup() - - override def afterEach(): Unit = - testTelemetrySetup.close() - - private implicit val ec: ExecutionContext = directExecutionContext - - val ApiPartyManagementServiceSuppressionRule: SuppressionRule = - SuppressionRule.LoggerNameContains("ApiPartyManagementService") && - SuppressionRule.Level(Level.ERROR) - - "ApiPartyManagementService" should { - def blind( - idpId: IdentityProviderId, - partyDetails: IndexerPartyDetails, - partyRecord: Option[PartyRecord], - ): ProtoPartyDetails = - blindAndConvertToProto(idpId)((partyDetails, partyRecord)) - - "translate basic input to the output" in { - blind(IdentityProviderId.Default, partyDetails, Some(partyRecord)) shouldBe protoPartyDetails - } - - "blind identity_provider_id for non default IDP" in { - blind(IdentityProviderId("idp_1"), partyDetails, Some(partyRecord)) shouldBe protoPartyDetails - .copy(isLocal = false) - } - - "blind identity_provider_id if record is for non default IDP" in { - blind( - IdentityProviderId.Default, - partyDetails, - Some(partyRecord.copy(identityProviderId = IdentityProviderId("idp_1"))), - ) shouldBe protoPartyDetails.copy(identityProviderId = "") - } - - "not blind `isLocal` if local record does not exist" in { - blind(IdentityProviderId.Default, partyDetails, None) shouldBe protoPartyDetails - } - - "blind `isLocal` if local record does not exist for non default IDP" in { - blind(IdentityProviderId("idp_1"), partyDetails, None) shouldBe protoPartyDetails - .copy(isLocal = false) - } - - def createSigningKey: (Option[crypto.SigningPublicKey], KeyPair) = { - val keyGen = KeyPairGenerator.getInstance("Ed25519") - val keyPair = keyGen.generateKeyPair() - val protoKey = Some( - lapicrypto.SigningPublicKey( - format = lapicrypto.CryptoKeyFormat.CRYPTO_KEY_FORMAT_DER_X509_SUBJECT_PUBLIC_KEY_INFO, - keyData = ByteString.copyFrom(keyPair.getPublic.getEncoded), - keySpec = lapicrypto.SigningKeySpec.SIGNING_KEY_SPEC_EC_CURVE25519, - ) - ) - (protoKey, keyPair) - } - - def cantonSigningPublicKey(publicKey: crypto.SigningPublicKey) = - SigningPublicKey - .fromProtoV30( - com.digitalasset.canton.crypto.v30.SigningPublicKey( - format = - publicKey.format.transformInto[com.digitalasset.canton.crypto.v30.CryptoKeyFormat], - publicKey = publicKey.keyData, - // Deprecated field - scheme = SIGNING_KEY_SCHEME_UNSPECIFIED, - usage = Seq(SigningKeyUsage.Namespace.toProtoEnum), - keySpec = - publicKey.keySpec.transformInto[com.digitalasset.canton.crypto.v30.SigningKeySpec], - ) - ) - .value - - def sign(keyPair: KeyPair, data: ByteString, signedBy: Fingerprint) = { - val signatureInstance = Signature.getInstance("Ed25519") - signatureInstance.initSign(keyPair.getPrivate) - signatureInstance.update(data.toByteArray) - lapicrypto.Signature( - format = SIGNATURE_FORMAT_RAW, - signature = ByteString.copyFrom(signatureInstance.sign()), - signedBy = signedBy.toProtoPrimitive, - signingAlgorithmSpec = lapicrypto.SigningAlgorithmSpec.SIGNING_ALGORITHM_SPEC_ED25519, - ) - } - - "validate allocateExternalParty request" when { - def testAllocateExternalPartyValidation( - requestTransform: Lens[ - AllocateExternalPartyRequest, - AllocateExternalPartyRequest, - ] => Mutation[AllocateExternalPartyRequest], - expectedFailure: PartyId => Option[String], - ) = - loggerFactory.suppress( - ApiPartyManagementServiceSuppressionRule - ) { - val (publicKey, keyPair) = createSigningKey - val cantonPublicKey = cantonSigningPublicKey(publicKey.value) - val partyId = PartyId.tryCreate("alice", cantonPublicKey.fingerprint) - for { - generatedTransactions <- apiService.generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = DefaultTestIdentities.synchronizerId.toProtoPrimitive, - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = - Seq(DefaultTestIdentities.participant2.uid.toProtoPrimitive), - confirmationThreshold = 1, - observingParticipantUids = - Seq(DefaultTestIdentities.participant3.uid.toProtoPrimitive), - ) - ) - signature = sign(keyPair, generatedTransactions.multiHash, partyId.fingerprint) - request = AllocateExternalPartyRequest( - synchronizer = DefaultTestIdentities.synchronizerId.toProtoPrimitive, - onboardingTransactions = generatedTransactions.topologyTransactions.map(tx => - AllocateExternalPartyRequest.SignedTransaction(tx, Seq.empty) - ), - multiHashSignatures = Seq(signature), - identityProviderId = "", - waitForAllocation = Some(true), - userId = "", - ).update(requestTransform) - result <- apiService - .allocateExternalParty(request) - .transform { - case Failure(e: io.grpc.StatusRuntimeException) => - expectedFailure(partyId) match { - case Some(value) => - e.getStatus.getCode.value() shouldBe io.grpc.Status.INVALID_ARGUMENT.getCode - .value() - e.getStatus.getDescription should include(value) - Success(succeed) - case None => - fail(s"Expected success but allocation failed with $e") - } - case Failure(other) => fail(s"expected a gRPC exception but got $other") - case Success(_) if expectedFailure(partyId).isDefined => - fail("Expected a failure but got a success") - case Success(_) => Success(succeed) - } - } yield result - } - - def mkDecentralizedTx(ownerSize: Int): (SignedTransaction, Namespace) = { - val ownersKeys = Seq.fill(ownerSize)(createSigningKey).map { case (publicKey, keyPair) => - (cantonSigningPublicKey(publicKey.value), keyPair) - } - val namespaceOwners = ownersKeys.map(_._1.fingerprint).toSet.map(Namespace(_)) - val decentralizedNamespace = - DecentralizedNamespaceDefinition.computeNamespace(namespaceOwners) - val decentralizedTx = TopologyTransaction( - Replace, - PositiveInt.one, - DecentralizedNamespaceDefinition.tryCreate( - decentralizedNamespace = decentralizedNamespace, - threshold = PositiveInt.one, - owners = NonEmpty.from(namespaceOwners).value, - ), - testedProtocolVersion, - ) - val signatures = ownersKeys.map { case (publicKey, keyPair) => - sign(keyPair, decentralizedTx.getCryptographicEvidence, publicKey.fingerprint) - } - ( - SignedTransaction( - decentralizedTx.toByteString, - signatures, - ), - decentralizedNamespace, - ) - } - - "fail if missing synchronizerId" in { - testAllocateExternalPartyValidation( - _.synchronizer.modify(_ => ""), - _ => Some("The submitted command is missing a mandatory field: synchronizer"), - ) - } - - "fail if missing a party to participant" in { - val (decentralizedNamespaceTx, _) = mkDecentralizedTx(1) - - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.filterNot(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - .nonEmpty - ) // Add another type of tx, otherwise it fails with "empty transaction field" - .appended(decentralizedNamespaceTx) - ), - _ => Some("One transaction of type PartyToParticipant must be provided, got 0"), - ) - } - - "fail on invalid key usages for party namespace key" in { - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.map(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - .map( - _.toProtoV30.mapping.value.update( - _.partyToParticipant.partySigningKeys.keys.modify( - _.map(_.copy(usage = Seq(v30.SigningKeyUsage.SIGNING_KEY_USAGE_PROTOCOL))) - ) - ) - ) - .map(TopologyMapping.fromProtoV30(_).value) - .map( - TopologyTransaction( - TopologyChangeOp.Replace, - PositiveInt.one, - _, - testedProtocolVersion, - ) - ) - .map { updatedTx => - SignedTransaction(updatedTx.toByteString, tx.signatures) - } - .getOrElse(tx) - ) - ), - _ => Some("Missing Namespace and Protocol usage on the party namespace key"), - ) - } - - "fail on invalid key usages for protocol keys" in { - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.map(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - .map( - _.toProtoV30.mapping.value.update( - _.partyToParticipant.partySigningKeys.keys.modify( - _.map(_.copy(usage = Seq(v30.SigningKeyUsage.SIGNING_KEY_USAGE_NAMESPACE))) - ) - ) - ) - .map(TopologyMapping.fromProtoV30(_).value) - .map( - TopologyTransaction( - TopologyChangeOp.Replace, - PositiveInt.one, - _, - testedProtocolVersion, - ) - ) - .map { updatedTx => - SignedTransaction(updatedTx.toByteString, tx.signatures) - } - .getOrElse(tx) - ) - ), - _ => Some("Missing Protocol usage on signing keys"), - ) - } - - "fail on empty protocol keys" in { - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.map(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - .map( - _.toProtoV30.mapping.value.update( - _.partyToParticipant.optionalPartySigningKeys.set(None) - ) - ) - .map(TopologyMapping.fromProtoV30(_).value) - .map( - TopologyTransaction( - TopologyChangeOp.Replace, - PositiveInt.one, - _, - testedProtocolVersion, - ) - ) - .map { updatedTx => - SignedTransaction(updatedTx.toByteString, tx.signatures) - } - .getOrElse(tx) - ) - ), - _ => - Some( - "Party signing keys must be supplied either in the PartyToParticipant or in a PartyToKeyMapping transaction. Not in both." - ), - ) - } - - "fail on protocol keys in both PTK and PTP" in { - val (publicKey, keyPair) = createSigningKey - val cantonPublicKey = cantonSigningPublicKey(publicKey.value) - - def mkPtkTransaction(partyId: PartyId) = TopologyTransaction( - Replace, - PositiveInt.one, - PartyToKeyMapping.tryCreate( - partyId = partyId, - threshold = PositiveInt.one, - signingKeys = NonEmpty.mk(Seq, cantonPublicKey), - ), - testedProtocolVersion, - ) - def mkSignature( - ptkTransaction: TopologyTransaction[TopologyChangeOp.Replace, PartyToKeyMapping], - partyId: PartyId, - ) = sign( - keyPair, - ptkTransaction.hash.hash.getCryptographicEvidence, - partyId.fingerprint, - ) - - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify { transactions => - val partyId = transactions - .flatMap(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - ) - .loneElement - .mapping - .partyId - - val ptk = mkPtkTransaction(partyId) - val signature = mkSignature(ptk, partyId) - - transactions :+ SignedTransaction(ptk.toByteString, Seq(signature)) - }, - _ => - Some( - "Party signing keys must be supplied either in the PartyToParticipant or in a PartyToKeyMapping transaction. Not in both." - ), - ) - } - - "allow a single P2P" in { - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.filter(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - .nonEmpty - ) - ), - _ => None, - ) - } - - "refuse a P2P with Submission rights" in { - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.map(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - .map { p2p => - TopologyTransaction( - p2p.operation, - p2p.serial, - PartyToParticipant.tryCreate( - p2p.mapping.partyId, - p2p.mapping.threshold, - Seq(HostingParticipant(participantId, ParticipantPermission.Submission)), - ), - testedProtocolVersion, - ) - } - .map { updatedTx => - SignedTransaction(updatedTx.toByteString, tx.signatures) - } - .getOrElse(tx) - ) - ), - _ => - Some( - "The PartyToParticipant transaction must not contain any node with Submission permission. Nodes with submission permission: PAR::participant1::participant1..." - ), - ) - } - - "refuse a non multi-hosted party submitted to another node" in { - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.map(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - .map { p2p => - TopologyTransaction( - p2p.operation, - p2p.serial, - PartyToParticipant.tryCreate( - p2p.mapping.partyId, - p2p.mapping.threshold, - Seq( - HostingParticipant( - DefaultTestIdentities.participant2, - ParticipantPermission.Confirmation, - ) - ), - ), - testedProtocolVersion, - ) - } - .map { updatedTx => - SignedTransaction(updatedTx.toByteString, tx.signatures) - } - .getOrElse(tx) - ) - ), - _ => - Some( - "The party is to be hosted on a single participant (PAR::participant2::participant2...) that is not this participant (PAR::participant1::participant1...). Submit the allocation request on PAR::participant2::participant2... instead." - ), - ) - } - - "refuse a multi-hosted party with no confirming node" in { - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.map(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[PartyToParticipant] - .map { p2p => - TopologyTransaction( - p2p.operation, - p2p.serial, - PartyToParticipant.tryCreate( - p2p.mapping.partyId, - p2p.mapping.threshold, - Seq( - HostingParticipant( - participantId, - ParticipantPermission.Observation, - ), - HostingParticipant( - DefaultTestIdentities.participant2, - ParticipantPermission.Observation, - ), - ), - ), - testedProtocolVersion, - ) - } - .map { updatedTx => - SignedTransaction(updatedTx.toByteString, tx.signatures) - } - .getOrElse(tx) - ) - ), - _ => - Some( - "The PartyToParticipant transaction must contain at least one node with Confirmation permission" - ), - ) - } - - "refuse mismatching ptk namespace and ptp namespace" in { - val (publicKey, keyPair) = createSigningKey - val cantonPublicKey = cantonSigningPublicKey(publicKey.value) - val ptkPartyId = PartyId.tryCreate("alice", cantonPublicKey.fingerprint) - - val ptkTransaction = TopologyTransaction( - Replace, - PositiveInt.one, - PartyToKeyMapping.tryCreate( - partyId = ptkPartyId, - threshold = PositiveInt.one, - signingKeys = NonEmpty.mk(Seq, cantonPublicKey), - ), - testedProtocolVersion, - ) - val signature = sign( - keyPair, - ptkTransaction.hash.hash.getCryptographicEvidence, - ptkPartyId.fingerprint, - ) - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify(transactions => - transactions :+ SignedTransaction( - ptkTransaction.toByteString, - Seq(signature), - ) - ), - partyId => - Some( - s"The PartyToKeyMapping namespace (${ptkPartyId.namespace}) does not match the PartyToParticipant namespace (${partyId.namespace})" - ), - ) - } - - "refuse mismatching party namespace and p2p namespace" in { - val (publicKey, keyPair) = createSigningKey - val cantonPublicKey = cantonSigningPublicKey(publicKey.value) - val nsdPartyId = PartyId.tryCreate("alice", cantonPublicKey.fingerprint) - - val nsdTransaction = TopologyTransaction( - Replace, - PositiveInt.one, - NamespaceDelegation.tryCreate( - namespace = nsdPartyId.namespace, - target = cantonPublicKey, - restriction = DelegationRestriction.CanSignAllMappings, - ), - testedProtocolVersion, - ) - val signature = sign( - keyPair, - nsdTransaction.hash.hash.getCryptographicEvidence, - nsdPartyId.fingerprint, - ) - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify(transactions => - transactions :+ SignedTransaction( - nsdTransaction.toByteString, - Seq(signature), - ) - ), - partyId => - Some( - s"The Party namespace (${nsdPartyId.namespace}) does not match the PartyToParticipant namespace (${partyId.namespace})" - ), - ) - } - - "refuse mismatching decentralized namespace and p2p namespace" in { - val (decentralizedNamespaceTx, namespace) = mkDecentralizedTx(1) - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - // Remove the Namespace delegation generated by default - _.filterNot(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[NamespaceDelegation] - .isDefined - ) - // replace it with a decentralized namespace - .appended(decentralizedNamespaceTx) - ), - partyId => - Some( - s"The Party namespace ($namespace) does not match the PartyToParticipant namespace (${partyId.namespace})" - ), - ) - } - - "refuse decentralized namespace with too many owners" in { - val max = ExternalPartyOnboardingDetails.maxDecentralizedOwnersSize - val (decentralizedNamespaceTx, namespace) = mkDecentralizedTx(max.increment.value) - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - // Remove the Namespace delegation generated by default - _.filterNot(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx.transaction) - .value - .selectMapping[NamespaceDelegation] - .isDefined - ) - // replace it with a decentralized namespace with too many owners - .appended(decentralizedNamespaceTx) - ), - partyId => - Some( - s"The Party namespace ($namespace) does not match the PartyToParticipant namespace (${partyId.namespace})" - ), - ) - } - - "refuse unwanted transactions" in { - testAllocateExternalPartyValidation( - _.onboardingTransactions.modify( - _.appended( - SignedTransaction( - TopologyTransaction( - TopologyChangeOp.Replace, - PositiveInt.one, - PartyHostingLimits.apply( - DefaultTestIdentities.synchronizerId, - DefaultTestIdentities.party1, - ), - testedProtocolVersion, - ).toByteString, - Seq.empty, - ) - ) - ), - _ => - Some( - s"Unsupported transactions found: PartyHostingLimits. Supported transactions are: NamespaceDelegation, DecentralizedNamespaceDefinition, PartyToParticipant, PartyToKeyMapping" - ), - ) - } - } - - "close while allocating party" in { - val ( - mockIdentityProviderExists, - mockIndexPartyManagementService, - mockUserManagementStore, - mockPartyRecordStore, - ) = mockedServices() - val partyAllocationTracker = makePartyAllocationTracker(loggerFactory) - val apiPartyManagementService = ApiPartyManagementService.createApiService( - mockIndexPartyManagementService, - mockUserManagementStore, - mockIdentityProviderExists, - partiesPageSize, - NonNegativeInt.tryCreate(0), - mockPartyRecordStore, - TestPartySyncService(testTelemetrySetup.tracer), - oneHour, - createSubmissionId, - partyAllocationTracker, - loggerFactory = loggerFactory, - ) - - loggerFactory.suppress( - ApiPartyManagementServiceSuppressionRule - ) { - // Kick the interaction off - val future = - apiPartyManagementService.allocateParty(AllocatePartyRequest("aParty", None, "", "", "")) - - // Close the service - apiPartyManagementService.close() - - // Assert that it caused the appropriate failure - future - .transform { - case Success(_) => - fail("Expected a failure, but received success") - case Failure(err: StatusRuntimeException) => - assertError( - actual = err, - expectedStatusCode = Code.UNAVAILABLE, - expectedMessage = "ABORTED_DUE_TO_SHUTDOWN(1,0): request aborted due to shutdown", - expectedDetails = List( - ErrorDetails.ErrorInfoDetail( - "ABORTED_DUE_TO_SHUTDOWN", - Map( - "parties" -> "['aParty']", - "category" -> "1", - "test" -> s"'${getClass.getSimpleName}'", - ), - ), - RetryInfoDetail(10.seconds), - ), - verifyEmptyStackTrace = true, - ) - Success(succeed) - case Failure(other) => - fail("Unexpected error", other) - } - } - } - - "generate-external-topology" when { - def getMappingFromResponse(response: GenerateExternalPartyTopologyResponse) = { - response.topologyTransactions should have length 1 - val txs = response.topologyTransactions.toList - .traverse(tx => - TopologyTransaction - .fromByteString(testedProtocolVersion, tx) - ) - .valueOrFail("unable to parse topology txs") - .map(_.mapping) - txs match { - case (pp: PartyToParticipant) :: Nil => pp - case other => fail("unexpected mappings: " + other) - } - } - "correctly pass through all fields" in { - val (publicKey, _) = createSigningKey - val syncId = DefaultTestIdentities.synchronizerId - - for { - response <- apiService.generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = - Seq(DefaultTestIdentities.participant2.uid.toProtoPrimitive), - confirmationThreshold = 2, - observingParticipantUids = - Seq(DefaultTestIdentities.participant3.uid.toProtoPrimitive), - ) - ) - } yield { - val pp = getMappingFromResponse(response) - pp.participants.toSet shouldBe Set( - HostingParticipant( - DefaultTestIdentities.participant1, - ParticipantPermission.Confirmation, - ), - HostingParticipant( - DefaultTestIdentities.participant2, - ParticipantPermission.Confirmation, - ), - HostingParticipant( - DefaultTestIdentities.participant3, - ParticipantPermission.Observation, - ), - ) - pp.threshold.value shouldBe 2 - - } - } - "correctly interpret local observer" in { - val (publicKey, _) = createSigningKey - val syncId = DefaultTestIdentities.synchronizerId - - for { - response <- apiService.generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = true, - otherConfirmingParticipantUids = - Seq(DefaultTestIdentities.participant2.uid.toProtoPrimitive), - confirmationThreshold = 1, - observingParticipantUids = Seq(), - ) - ) - } yield { - val pp = getMappingFromResponse(response) - pp.participants.toSet shouldBe Set( - HostingParticipant( - DefaultTestIdentities.participant1, - ParticipantPermission.Observation, - ), - HostingParticipant( - DefaultTestIdentities.participant2, - ParticipantPermission.Confirmation, - ), - ) - pp.threshold.value shouldBe 1 - } - } - "correctly reject invalid threshold" in { - val (publicKey, _) = createSigningKey - val syncId = DefaultTestIdentities.synchronizerId - - for { - response <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = - Seq(DefaultTestIdentities.participant2.uid.toProtoPrimitive), - confirmationThreshold = 3, - observingParticipantUids = Seq(), - ) - ) - .failed - } yield { - response.getMessage should include( - "Confirmation threshold exceeds number of confirming participants" - ) - } - } - "fail gracefully on invalid synchronizer-ids" in { - val (publicKey, _) = createSigningKey - for { - response1 <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = "", - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = Seq(), - confirmationThreshold = 1, - observingParticipantUids = Seq(), - ) - ) - .failed - response2 <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = SynchronizerId.tryFromString("not::valid").toProtoPrimitive, - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = Seq(), - confirmationThreshold = 1, - observingParticipantUids = Seq(), - ) - ) - .failed - } yield { - response1.getMessage should include("Empty string is not a valid unique identifier") - response2.getMessage should include("Unknown or not connected synchronizer not::valid") - } - } - "fail gracefully on invalid party hints" in { - val (publicKey, _) = createSigningKey - val syncId = DefaultTestIdentities.synchronizerId - for { - response1 <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = "", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = Seq(), - confirmationThreshold = 1, - observingParticipantUids = Seq(), - ) - ) - .failed - response2 <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = - "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = Seq(), - confirmationThreshold = 1, - observingParticipantUids = Seq(), - ) - ) - .failed - } yield { - response1.getMessage should include("Party hint is empty") - response2.getMessage should include("is too long") - } - } - "fail gracefully on empty keys" in { - val syncId = DefaultTestIdentities.synchronizerId - for { - response1 <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = "alice", - publicKey = None, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = Seq(), - confirmationThreshold = 1, - observingParticipantUids = Seq(), - ) - ) - .failed - } yield { - response1.getMessage should include("Field `public_key` is not set") - } - } - "fail gracefully on invalid duplicate participant ids" in { - val (publicKey, _) = createSigningKey - val syncId = DefaultTestIdentities.synchronizerId - for { - response1 <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = - Seq(DefaultTestIdentities.participant1.uid.toProtoPrimitive), - confirmationThreshold = 1, - observingParticipantUids = Seq(), - ) - ) - .failed - response2 <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = Seq(), - confirmationThreshold = 1, - observingParticipantUids = - Seq(DefaultTestIdentities.participant1.uid.toProtoPrimitive), - ) - ) - .failed - response3 <- apiService - .generateExternalPartyTopology( - GenerateExternalPartyTopologyRequest( - synchronizer = syncId.toProtoPrimitive, - partyHint = "alice", - publicKey = publicKey, - localParticipantObservationOnly = false, - otherConfirmingParticipantUids = - Seq(DefaultTestIdentities.participant2.uid.toProtoPrimitive), - confirmationThreshold = 1, - observingParticipantUids = - Seq(DefaultTestIdentities.participant2.uid.toProtoPrimitive), - ) - ) - .failed - } yield { - response1.getMessage should include( - s"This participant node ($participantId) is also listed in 'otherConfirmingParticipantUids'." + - s" By sending the request to this node, it is de facto a hosting node" + - s" and must not be listed in 'otherConfirmingParticipantUids'." - ) - response2.getMessage should include( - "This participant node (PAR::participant1::participant1...) is also listed in 'observingParticipantUids'." + - " By sending the request to this node, it is de facto a hosting node" + - " and must not be listed in 'observingParticipantUids'." - ) - response3.getMessage should include( - "The following participant IDs are referenced multiple times in the request:" + - " participant2::participant2.... " + - "Please ensure all IDs are referenced only once across" + - " 'otherConfirmingParticipantUids' and 'observingParticipantUids' fields." - ) - } - } - - } - - } - - private def makePartyAllocationTracker( - loggerFactory: NamedLoggerFactory - ): PartyAllocation.Tracker = - StreamTracker.withTimer[PartyAllocation.TrackerKey, PartyAllocation.Completed]( - timer = new java.util.Timer("test-timer"), - itemKey = (_ => Some(aPartyAllocationTracker)), - inFlightCounter = InFlight.Limited(100, mock[com.daml.metrics.api.MetricHandle.Counter]), - loggerFactory, - ) - - private def mockedServices(): ( - IdentityProviderExists, - IndexPartyManagementService, - UserManagementStore, - PartyRecordStore, - ) = { - val mockIdentityProviderExists = mock[IdentityProviderExists] - when( - mockIdentityProviderExists.apply(ArgumentMatchers.eq(IdentityProviderId.Default))( - any[LoggingContextWithTrace] - ) - ) - .thenReturn(Future.successful(true)) - - val mockIndexPartyManagementService = mock[IndexPartyManagementService] - - val mockPartyRecordStore = mock[PartyRecordStore] - when( - mockPartyRecordStore.createPartyRecord(any[PartyRecord])(any[LoggingContextWithTrace]) - ).thenReturn( - Future.successful( - Right(PartyRecord(aParty, ObjectMeta.empty, IdentityProviderId.Default)) - ) - ) - when( - mockPartyRecordStore.getPartyRecordO(any[Ref.Party])(any[LoggingContextWithTrace]) - ).thenReturn(Future.successful(Right(None))) - - val mockUserManagementStore = mock[UserManagementStore] - - ( - mockIdentityProviderExists, - mockIndexPartyManagementService, - mockUserManagementStore, - mockPartyRecordStore, - ) - } -} - -object ApiPartyManagementServiceSpec { - - val participantId = DefaultTestIdentities.participant1 - - val partyDetails: IndexerPartyDetails = IndexerPartyDetails( - party = Ref.Party.assertFromString("Bob"), - isLocal = true, - ) - val partyRecord: PartyRecord = PartyRecord( - party = Ref.Party.assertFromString("Bob"), - ObjectMeta.empty, - IdentityProviderId.Default, - ) - val protoPartyDetails: ProtoPartyDetails = ProtoPartyDetails( - party = "Bob", - localMetadata = Some(new com.daml.ledger.api.v2.admin.object_meta.ObjectMeta("", Map.empty)), - isLocal = true, - identityProviderId = "", - ) - - val aParty = Ref.Party.assertFromString("aParty") - - val oneHour = FiniteDuration(1, java.util.concurrent.TimeUnit.HOURS) - - private final case class TestPartySyncService(tracer: Tracer) - extends state.PartySyncService - with Spanning { - override def allocateParty( - partyId: PartyId, - submissionId: Ref.SubmissionId, - synchronizerIdO: Option[SynchronizerId], - externalPartyOnboardingDetails: Option[ExternalPartyOnboardingDetails], - )(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[state.SubmissionResult] = - FutureUnlessShutdown.pure(state.SubmissionResult.Acknowledged) - - override def physicalSynchronizerIdForSynchronizerId( - synchronizerId: SynchronizerId - ): Option[PhysicalSynchronizerId] = - Option.when(synchronizerId == DefaultTestIdentities.synchronizerId)( - DefaultTestIdentities.physicalSynchronizerId - ) - - override def participantId: ParticipantId = DefaultTestIdentities.participant1 - - override def hashOps: HashOps = TestHash - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiUserManagementServiceSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiUserManagementServiceSpec.scala deleted file mode 100644 index f1b142a04f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/ApiUserManagementServiceSpec.scala +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.daml.platform.v1.page_tokens.ListUsersPageTokenPayload -import com.digitalasset.base.error.ErrorsAssertions -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.EitherValues -import org.scalatest.concurrent.{Eventually, IntegrationPatience} -import org.scalatest.flatspec.AnyFlatSpec - -import java.nio.charset.StandardCharsets -import java.util.Base64 - -class ApiUserManagementServiceSpec - extends AnyFlatSpec - with BaseTest - with Eventually - with IntegrationPatience - with EitherValues - with ErrorsAssertions { - - it should "test users page token encoding and decoding" in { - val id2 = Ref.UserId.assertFromString("user2") - val actualNextPageToken = ApiUserManagementService.encodeNextPageToken(Some(id2)) - actualNextPageToken shouldBe "CgV1c2VyMg==" - ApiUserManagementService.decodeUserIdFromPageToken(actualNextPageToken) shouldBe Right( - Some(id2) - ) - } - - it should "test users empty page token encoding and decoding" in { - val actualNextPageToken = ApiUserManagementService.encodeNextPageToken(None) - actualNextPageToken shouldBe ("") - ApiUserManagementService.decodeUserIdFromPageToken(actualNextPageToken) shouldBe Right(None) - } - - it should "return invalid argument error when token is not a base64" in { - val actualNextPageToken = - ApiUserManagementService.decodeUserIdFromPageToken("not-a-base64-string!!") - val error = actualNextPageToken.left.value - assertError( - actual = error, - expected = RequestValidationErrors.InvalidArgument - .Reject("Invalid page token") - .asGrpcError, - ) - } - - it should "return invalid argument error when token is base64 but not a valid protobuf" in { - val notValidProtoBufBytes = "not a valid proto buf".getBytes() - val badPageToken = new String( - Base64.getEncoder.encode(notValidProtoBufBytes), - StandardCharsets.UTF_8, - ) - - val actualNextPageToken = - ApiUserManagementService.decodeUserIdFromPageToken(badPageToken) - val error = actualNextPageToken.left.value - assertError( - actual = error, - expected = RequestValidationErrors.InvalidArgument - .Reject("Invalid page token") - .asGrpcError, - ) - } - - it should "return invalid argument error when token is valid base64 encoded protobuf but does not contain a valid user id string" in { - val notValidUserId = "not a valid user id" - Ref.UserId.fromString(notValidUserId).isLeft shouldBe true - val payload = ListUsersPageTokenPayload( - userIdLowerBoundExcl = notValidUserId - ) - val badPageToken = new String( - Base64.getEncoder.encode(payload.toByteArray), - StandardCharsets.UTF_8, - ) - - val actualNextPageToken = - ApiUserManagementService.decodeUserIdFromPageToken(badPageToken) - val error = actualNextPageToken.left.value - assertError( - actual = error, - expected = RequestValidationErrors.InvalidArgument - .Reject("Invalid page token") - .asGrpcError, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageTestUtils.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageTestUtils.scala deleted file mode 100644 index e7b476139f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageTestUtils.scala +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.digitalasset.canton.buildinfo.BuildInfo -import com.digitalasset.daml.lf.archive.DamlLf.Archive -import com.digitalasset.daml.lf.archive.testing.Encode -import com.digitalasset.daml.lf.archive.{Dar as LfDar, DarWriter} -import com.digitalasset.daml.lf.data.Ref.PackageName -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.language.{Ast, LanguageVersion} -import com.digitalasset.daml.lf.testing.parser.Implicits.SyntaxHelper -import com.digitalasset.daml.lf.testing.parser.ParserParameters -import com.google.protobuf.ByteString -import org.scalatest.TryValues.convertTryToSuccessOrFailure - -import scala.util.Using - -object PackageTestUtils { - - def astPackageFromLfDef(defn: ParserParameters[?] => Ast.Package)( - lfVersion: LanguageVersion = LanguageVersion.v2_1, - packageId: Ref.PackageId = Ref.PackageId.assertFromString("-self-"), - ): Ast.Package = - defn( - ParserParameters( - defaultPackageId = packageId, - languageVersion = lfVersion, - ) - ) - - def archiveFromLfDef(defn: ParserParameters[?] => Ast.Package)( - lfVersion: LanguageVersion = LanguageVersion.v2_1, - packageId: Ref.PackageId = Ref.PackageId.assertFromString("-self-"), - ): Archive = { - val pkg = astPackageFromLfDef(defn)(lfVersion, packageId) - Encode.encodeArchive(packageId -> pkg, lfVersion) - } - - def sampleAstPackage( - packageName: PackageName, - packageVersion: Ref.PackageVersion, - discriminatorFields: Seq[String] = Seq.empty, - )( - lfVersion: LanguageVersion = LanguageVersion.v2_1, - packageId: Ref.PackageId = Ref.PackageId.assertFromString("-self-"), - ): Ast.Package = - astPackageFromLfDef(implicit parseParameters => - p""" - metadata ( '$packageName' : '${packageVersion.toString}' ) - module Mod { - record @serializable T = { actor: Party ${if (discriminatorFields.isEmpty) "" - else discriminatorFields.mkString(", ", ", ", "")}}; - - template (this: T) = { - precondition True; - signatories Cons @Party [Mod:T {actor} this] (Nil @Party); - observers Nil @Party; - }; - }""" - )(lfVersion, packageId) - - def sampleLfArchive( - packageName: Ref.PackageName, - packageVersion: Ref.PackageVersion, - discriminatorFields: Seq[String] = Seq.empty, - lfVersion: LanguageVersion = LanguageVersion.v2_1, - packageId: Ref.PackageId = Ref.PackageId.assertFromString("-self-"), - ): Archive = { - val pkg = sampleAstPackage( - packageName = packageName, - packageVersion = packageVersion, - discriminatorFields = discriminatorFields, - )( - lfVersion = lfVersion, - packageId = packageId, - ) - Encode.encodeArchive(packageId -> pkg, lfVersion) - } - - implicit class ArchiveOps(archive: Archive) { - def lfArchiveToByteString: ByteString = Using(ByteString.newOutput()) { os => - DarWriter.encode( - BuildInfo.damlLibrariesVersion, - LfDar(("archive.dalf", Bytes.fromByteString(archive.toByteString)), List()), - os, - ) - os.toByteString - }.success.value - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageUpgradeValidatorSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageUpgradeValidatorSpec.scala deleted file mode 100644 index f844b8e2ad..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PackageUpgradeValidatorSpec.scala +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.digitalasset.canton.config.CachingConfigs -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.topology.TopologyManagerError.ParticipantTopologyManagerError.* -import com.digitalasset.canton.{BaseTest, FailOnShutdown, LfPackageName, LfPackageVersion} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.language.{Ast, Util} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec - -class PackageUpgradeValidatorSpec - extends AsyncWordSpec - with FailOnShutdown - with Matchers - with BaseTest { - private val packageUpgradeValidator = - new PackageUpgradeValidator(CachingConfigs.defaultPackageUpgradeCache, loggerFactory) - - protected implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace.ForTesting - - private val v1: (Ref.PackageId, Ast.PackageSignature) = samplePackageSig( - packageId = "test-pkg-v1", - packageName = "TestPkgName", - packageVersion = "1.0.0", - discriminatorFields = Seq.empty, - ) - - private val v11Incompatible: (Ref.PackageId, Ast.PackageSignature) = samplePackageSig( - packageId = "test-pkg-v11", - packageName = "TestPkgName", - packageVersion = "1.1.0", - discriminatorFields = Seq("text : Text"), - ) - - private val v2Compatible: (Ref.PackageId, Ast.PackageSignature) = samplePackageSig( - packageId = "test-pkg-v2", - packageName = "TestPkgName", - packageVersion = "2.0.0", - discriminatorFields = Seq("party : Option Party"), - ) - - private val v3Incompatible: (Ref.PackageId, Ast.PackageSignature) = samplePackageSig( - packageId = "test-pkg-v3-incompat", - packageName = "TestPkgName", - packageVersion = "3.0.0", - discriminatorFields = Seq("party : Option Party", "text : Text"), - ) - - private val v3Compatible: (Ref.PackageId, Ast.PackageSignature) = samplePackageSig( - packageId = "test-pkg-v3-compat", - packageName = "TestPkgName", - packageVersion = "3.0.0", - discriminatorFields = Seq("party : Option Party", "text : Option Text"), - ) - - "validate empty lineage" in { - val res = validateUpgrade(List.empty, List.empty) - res shouldBe Right(()) - } - - "validate compatible lineage" in { - validateUpgrade(List(v1), List.empty) shouldBe Right(()) - validateUpgrade(List(v2Compatible), List(v1)) shouldBe Right(()) - validateUpgrade(List(v1, v2Compatible), List.empty) shouldBe Right(()) - } - - "fail validation of incompatible lineage" in { - inside(validateUpgrade(List(v3Incompatible), List(v1, v2Compatible))) { - case Left(error: Upgradeability.Error) => - error.newPackage shouldBe Util.PkgIdWithNameAndVersion(v3Incompatible) - error.oldPackage shouldBe Util.PkgIdWithNameAndVersion(v2Compatible) - } - - // it does not depend on the vetting order - inside(validateUpgrade(List(v2Compatible), List(v1, v3Incompatible))) { - case Left(error: Upgradeability.Error) => - error.newPackage shouldBe Util.PkgIdWithNameAndVersion(v3Incompatible) - error.oldPackage shouldBe Util.PkgIdWithNameAndVersion(v2Compatible) - } - - inside(validateUpgrade(List(v2Compatible), List(v1, v11Incompatible))) { - case Left(error: Upgradeability.Error) => - error.newPackage shouldBe Util.PkgIdWithNameAndVersion(v11Incompatible) - error.oldPackage shouldBe Util.PkgIdWithNameAndVersion(v1) - } - - inside( - validateUpgrade(List(v11Incompatible), List(v1, v2Compatible, v3Incompatible)) - ) { case Left(error: Upgradeability.Error) => - error.newPackage shouldBe Util.PkgIdWithNameAndVersion(v11Incompatible) - error.oldPackage shouldBe Util.PkgIdWithNameAndVersion(v1) - } - } - - "fail validation because of packages with same name and version" in { - inside(validateUpgrade(List(v3Incompatible), List(v1, v3Compatible))) { - case Left(error: UpgradeVersion.Error) => - Set(error.firstPackage, error.secondPackage) shouldBe Set( - Util.PkgIdWithNameAndVersion(v3Incompatible), - Util.PkgIdWithNameAndVersion(v3Compatible), - ) - } - } - - private def validateUpgrade( - newPackages: List[(Ref.PackageId, Ast.PackageSignature)], - existingPackages: List[(Ref.PackageId, Ast.PackageSignature)], - ) = - packageUpgradeValidator.validateUpgrade( - newPackages.map(_._1).toSet, - (newPackages ++ existingPackages).map(_._1).toSet, - (newPackages ++ existingPackages).toMap, - ) - - private def samplePackageSig( - packageId: String, - packageName: String, - packageVersion: String, - discriminatorFields: Seq[String], - ): (Ref.PackageId, Ast.PackageSignature) = { - val refPackageId = Ref.PackageId.assertFromString(packageId) - val astPackage = PackageTestUtils - .sampleAstPackage( - packageName = LfPackageName.assertFromString(packageName), - packageVersion = LfPackageVersion.assertFromString(packageVersion), - discriminatorFields = discriminatorFields, - )(packageId = refPackageId) - refPackageId -> Util.toSignature(astPackage) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PendingPartyAllocationsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PendingPartyAllocationsSpec.scala deleted file mode 100644 index 08250fb63a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/admin/PendingPartyAllocationsSpec.scala +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.admin - -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.concurrent.Semaphore -import scala.concurrent.Future - -class PendingPartyAllocationsSpec - extends AsyncFlatSpec - with Matchers - with BaseTest - with HasExecutionContext { - - private val className = classOf[PendingPartyAllocations].getSimpleName - - private val ken = Some(Ref.UserId.assertFromString("ken")) - behavior of s"$className.withUser" - - it should "not keep a tally when user not provided" in { - val ppa = new PendingPartyAllocations - for { - outstanding <- ppa.withUser(None)(Future.successful) - } yield { - outstanding shouldBe 0 - } - } - - it should "give 1 as the number of operations running when sequential" in { - val ppa = new PendingPartyAllocations - for { - first <- ppa.withUser(ken)(Future.successful) - second <- ppa.withUser(ken)(Future.successful) - } yield { - first shouldBe 1 - second shouldBe 1 - } - } - - it should "keep tally when one of the operations throws" in { - val ppa = new PendingPartyAllocations - for { - first <- ppa.withUser(ken)(Future.successful) - _ <- ppa - .withUser(ken)(_ => Future.failed(new RuntimeException("deliberate throw"))) - .recover(_ => 1) - second <- ppa.withUser(ken)(Future.successful) - } yield { - first shouldBe 1 - second shouldBe 1 - } - } - - it should "keep tally when concurrent operations" in { - val semaphore = new Semaphore(0) - val elements = 3 - def waitAndReturn(count: Int) = - Future { - if (count < elements) - semaphore.acquire() - else - semaphore.release(3) - count - } - val ppa = new PendingPartyAllocations - val expected = (1 to elements).toList - val futures = expected.map(_ => ppa.withUser(ken)(waitAndReturn)) - for { - result <- Future.sequence(futures) - } yield { - result shouldBe expected - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandServiceImplSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandServiceImplSpec.scala deleted file mode 100644 index da22f42b22..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandServiceImplSpec.scala +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command - -import com.daml.grpc.RpcProtoExtractors -import com.daml.ledger.api.v2.command_service.{CommandServiceGrpc, SubmitAndWaitRequest} -import com.daml.ledger.api.v2.command_submission_service.{ - SubmitReassignmentRequest, - SubmitReassignmentResponse, - SubmitRequest, - SubmitResponse, -} -import com.daml.ledger.api.v2.commands.{Command, Commands, CreateCommand} -import com.daml.ledger.api.v2.completion.Completion -import com.daml.ledger.api.v2.value.{Identifier, Record, RecordField, Value} -import com.daml.ledger.resources.{ResourceContext, ResourceOwner} -import com.digitalasset.canton.ledger.api.validation.{ - CommandsValidator, - ValidateUpgradingPackageResolutions, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.{ErrorLoggingContext, LoggingContextWithTrace} -import com.digitalasset.canton.platform.apiserver.services.command.CommandServiceImplSpec.* -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker -import com.digitalasset.canton.platform.apiserver.services.{ApiCommandService, tracking} -import com.digitalasset.canton.tracing.{TraceContext, Traced} -import com.digitalasset.canton.util.Thereafter.syntax.* -import com.digitalasset.canton.{BaseTest, HasExecutionContext, config} -import com.digitalasset.daml.lf.data.Ref -import com.google.rpc.Code -import com.google.rpc.status.Status as StatusProto -import io.grpc.inprocess.{InProcessChannelBuilder, InProcessServerBuilder} -import io.grpc.{Context, Deadline, Status} -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.Assertion -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec - -import java.time.Instant -import java.util.UUID -import java.util.concurrent.TimeUnit -import scala.concurrent.Future -import scala.concurrent.duration.DurationInt - -@SuppressWarnings(Array("com.digitalasset.canton.TryFailed")) -class CommandServiceImplSpec - extends AsyncWordSpec - with Matchers - with MockitoSugar - with ArgumentMatchersSugar - with BaseTest - with HasExecutionContext { - - private implicit val resourceContext: ResourceContext = ResourceContext(executionContext) - - s"the command service" should { - "submit a request, and wait for a response" in withTestContext { testContext => - import testContext.* - - openChannel( - new CommandServiceImpl( - UnimplementedTransactionServices, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - submit, - submitReassignment, - config.NonNegativeFiniteDuration.ofSeconds(1000L), - loggerFactory, - ) - ).use { stub => - val request = - SubmitAndWaitRequest.of(Some(commands)) - stub.submitAndWait(request).map { response => - verify(transactionSubmissionTracker).track( - eqTo(expectedSubmissionKey), - eqTo(config.NonNegativeFiniteDuration.ofSeconds(1000L)), - any[TraceContext => FutureUnlessShutdown[Any]], - )(any[ErrorLoggingContext], any[TraceContext]) - response.updateId should be("update ID") - response.completionOffset shouldBe offset - } - } - } - - "pass the provided deadline to the tracker as a timeout" in withTestContext { testContext => - import testContext.* - - val now = Instant.parse("2021-09-01T12:00:00Z") - val deadlineTicker = new Deadline.Ticker { - override def nanoTime(): Long = - now.getEpochSecond * TimeUnit.SECONDS.toNanos(1) + now.getNano - } - - openChannel( - new CommandServiceImpl( - UnimplementedTransactionServices, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - submit, - submitReassignment, - config.NonNegativeFiniteDuration.ofSeconds(1L), - loggerFactory, - ), - deadlineTicker, - ).use { stub => - val request = SubmitAndWaitRequest.of(Some(commands)) - stub - .withDeadline(Deadline.after(3600L, TimeUnit.SECONDS, deadlineTicker)) - .submitAndWait(request) - .map { response => - verify(transactionSubmissionTracker).track( - eqTo(expectedSubmissionKey), - eqTo(config.NonNegativeFiniteDuration.ofSeconds(3600L)), - any[TraceContext => FutureUnlessShutdown[Any]], - )(any[ErrorLoggingContext], any[TraceContext]) - response.updateId should be("update ID") - succeed - } - } - } - - "reject and do not submit on deadline exceeded" in withTestContext { testContext => - import testContext.* - - val service = new CommandServiceImpl( - UnimplementedTransactionServices, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - submit, - submitReassignment, - config.NonNegativeFiniteDuration.ofSeconds(1L), - loggerFactory, - ) - - val deadline = Context - .current() - .withDeadline(Deadline.after(0L, TimeUnit.NANOSECONDS), scheduledExecutor()) - - deadline - .call { () => - service - .submitAndWait( - SubmitAndWaitRequest - .of(Some(commands.copy(submissionId = submissionId))) - )( - LoggingContextWithTrace.ForTesting - ) - .transform { response => - verify(transactionSubmissionTracker, never).track( - any[SubmissionTracker.SubmissionKey], - any[config.NonNegativeFiniteDuration], - any[TraceContext => FutureUnlessShutdown[Any]], - )(any[ErrorLoggingContext], any[TraceContext]) - - response.failed.map(inside(_) { case RpcProtoExtractors.Exception(status) => - status.getCode shouldBe Code.DEADLINE_EXCEEDED.getNumber - status.getMessage should fullyMatch regex s"REQUEST_DEADLINE_EXCEEDED\\(3,submissi\\)\\: The gRPC deadline for request with commandId=$commandId and submissionId=$submissionId has expired by .* The request will not be processed further\\." - }) - } - } - .thereafter(_ => deadline.close()) - } - - "time out if the tracker times out" in withTestContext { testContext => - import testContext.* - - when( - transactionSubmissionTracker.track( - eqTo(expectedSubmissionKey), - any[config.NonNegativeFiniteDuration], - any[TraceContext => FutureUnlessShutdown[Any]], - )( - any[ErrorLoggingContext], - any[TraceContext], - ) - ).thenReturn( - Future.failed(SubmissionTracker.Errors.timedOut(expectedSubmissionKey)) - ) - - val service = new CommandServiceImpl( - UnimplementedTransactionServices, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - submit, - submitReassignment, - config.NonNegativeFiniteDuration.ofSeconds(1337L), - loggerFactory, - ) - - openChannel( - service: CommandServiceImpl - ).use { stub => - val request = SubmitAndWaitRequest.of(Some(commands)) - stub.submitAndWait(request).failed.map { - case RpcProtoExtractors.Exception(RpcProtoExtractors.Status(Code.DEADLINE_EXCEEDED)) => - succeed - case unexpected => fail(s"Unexpected exception", unexpected) - } - } - } - - "close the supplied tracker when closed" in withTestContext { testContext => - import testContext.* - - val service = new CommandServiceImpl( - UnimplementedTransactionServices, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - submit, - submitReassignment, - config.NonNegativeFiniteDuration.ofSeconds(1337L), - loggerFactory, - ) - - verifyZeroInteractions(transactionSubmissionTracker) - verifyZeroInteractions(reassignmentSubmissionTracker) - - service.close() - verify(transactionSubmissionTracker).close() - verify(reassignmentSubmissionTracker).close() - succeed - } - } - - private class TestContext { - val trackerCompletionResponse = tracking.CompletionResponse( - completion = completion - ) - val commands = someCommands() - val transactionSubmissionTracker = mock[SubmissionTracker] - val reassignmentSubmissionTracker = mock[SubmissionTracker] - val submit = mock[Traced[SubmitRequest] => FutureUnlessShutdown[SubmitResponse]] - val submitReassignment = - mock[Traced[SubmitReassignmentRequest] => FutureUnlessShutdown[SubmitReassignmentResponse]] - when( - transactionSubmissionTracker.track( - eqTo(expectedSubmissionKey), - any[config.NonNegativeFiniteDuration], - any[TraceContext => FutureUnlessShutdown[Any]], - )( - any[ErrorLoggingContext], - any[TraceContext], - ) - ).thenReturn(Future.successful(trackerCompletionResponse)) - } - - private def withTestContext(test: TestContext => Future[Assertion]): Future[Assertion] = - test(new TestContext) - - private def openChannel( - service: CommandServiceImpl, - deadlineTicker: Deadline.Ticker = Deadline.getSystemTicker, - ): ResourceOwner[CommandServiceGrpc.CommandServiceStub] = { - val commandsValidator = new CommandsValidator( - validateUpgradingPackageResolutions = ValidateUpgradingPackageResolutions.Empty - ) - val apiService = new ApiCommandService( - service = service, - commandsValidator = commandsValidator, - currentLedgerTime = () => Instant.EPOCH, - currentUtcTime = () => Instant.EPOCH, - maxDeduplicationDuration = maxDeduplicationDuration, - generateSubmissionId = () => submissionId, - loggerFactory = loggerFactory, - ) - for { - name <- ResourceOwner.forValue(() => UUID.randomUUID().toString) - _ <- ResourceOwner.forServer( - InProcessServerBuilder - .forName(name) - .deadlineTicker(deadlineTicker) - .addService(() => CommandServiceGrpc.bindService(apiService, parallelExecutionContext)), - shutdownTimeout = 10.seconds, - ) - channel <- ResourceOwner.forChannel( - InProcessChannelBuilder.forName(name), - shutdownTimeout = 10.seconds, - ) - } yield CommandServiceGrpc.stub(channel) - } -} - -object CommandServiceImplSpec { - private val UnimplementedTransactionServices = new CommandServiceImpl.UpdateServices( - getUpdateById = _ => Future.failed(new RuntimeException("This should never be called.")) - ) - - private val OkStatus = StatusProto.of(Status.Code.OK.value, "", Seq.empty) - - val commandId = "command ID" - val userId = "userID" - val submissionId = Ref.SubmissionId.assertFromString("submissionId") - val maxDeduplicationDuration = java.time.Duration.ofDays(1) - val party = "Alice" - - val command = Command.of( - Command.Command.Create( - CreateCommand.of( - Some(Identifier("package", moduleName = "module", entityName = "entity")), - Some( - Record( - Some(Identifier("package", moduleName = "module", entityName = "entity")), - Seq(RecordField("something", Some(Value(Value.Sum.Bool(true))))), - ) - ), - ) - ) - ) - - val offset: Long = 12345678L - - val completion = Completion.defaultInstance.copy( - commandId = "command ID", - status = Some(OkStatus), - updateId = "update ID", - offset = offset, - ) - - val expectedSubmissionKey = SubmissionTracker.SubmissionKey( - commandId = commandId, - submissionId = submissionId, - userId = userId, - parties = Set(party), - ) - - private def someCommands() = Commands.defaultInstance.copy( - commandId = commandId, - userId = userId, - actAs = Seq(party), - commands = Seq(command), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandSubmissionServiceImplSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandSubmissionServiceImplSpec.scala deleted file mode 100644 index 420d504c24..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/CommandSubmissionServiceImplSpec.scala +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command - -import cats.data.EitherT -import com.digitalasset.canton.data.DeduplicationPeriod.DeduplicationDuration -import com.digitalasset.canton.data.{DeduplicationPeriod, LedgerTimeBoundaries} -import com.digitalasset.canton.ledger.api.messages.command.submission.SubmitRequest -import com.digitalasset.canton.ledger.api.util.{TimeProvider, TimeProviderType} -import com.digitalasset.canton.ledger.api.{CommandId, Commands, DisclosedContract} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.{ - RoutingSynchronizerState, - SubmissionResult, - SubmitterInfo, - SynchronizerRank, - TransactionMeta, -} -import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, UnlessShutdown} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.apiserver.execution.{ - CommandExecutionResult, - CommandExecutor, - CommandInterpretationResult, -} -import com.digitalasset.canton.platform.apiserver.services.ErrorCause -import com.digitalasset.canton.platform.apiserver.{FatContractInstanceHelper, SeedService} -import com.digitalasset.canton.protocol.LfSerializationVersion -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import com.digitalasset.daml.lf -import com.digitalasset.daml.lf.command.ApiCommands as LfCommands -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.{Identifier, PackageName} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, ImmArray, Ref, Time} -import com.digitalasset.daml.lf.engine.Error as LfError -import com.digitalasset.daml.lf.interpretation.Error as LfInterpretationError -import com.digitalasset.daml.lf.language.{LookupError, Reference} -import com.digitalasset.daml.lf.transaction.test.TreeTransactionBuilder.* -import com.digitalasset.daml.lf.transaction.test.{ - TestNodeBuilder, - TransactionBuilder, - TreeTransactionBuilder, -} -import com.digitalasset.daml.lf.transaction.{Node as _, *} -import com.digitalasset.daml.lf.value.Value -import com.google.rpc.status.Status as RpcStatus -import io.grpc.{Status, StatusRuntimeException} -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.Inside -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.time.{Duration, Instant} -import scala.concurrent.Future -import scala.util.{Failure, Success, Try} - -class CommandSubmissionServiceImplSpec - extends AnyFlatSpec - with Matchers - with Inside - with MockitoSugar - with ScalaFutures - with ArgumentMatchersSugar - with BaseTest - with HasExecutionContext { - - import TransactionBuilder.Implicits.* - - private implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace.ForTesting - - private val knownParties = (1 to 100).map(idx => s"party-$idx").toArray - private val missingParties = (101 to 200).map(idx => s"party-$idx").toArray - private val allInformeesInTransaction = knownParties ++ missingParties - - private val nodes: Seq[NodeWrapper] = for { - i <- 0 until 100 - } yield { - // Ensure 100 % overlap by having 4 informees per each of the 100 nodes - val informeesOfNode = allInformeesInTransaction.slice(i * 4, (i + 1) * 4) - val (signatories, observers) = informeesOfNode.splitAt(2) - TestNodeBuilder.create( - id = Value.ContractId.V1(Hash.hashPrivateKey(i.toString)).coid, - templateId = "test:test", - argument = Value.ValueNil, - signatories = signatories.toSeq, - observers = observers.toSeq, - ) - } - - private val transaction = SubmittedTransaction( - TreeTransactionBuilder.toVersionedTransaction(nodes*) - ) - - behavior of "submit" - - it should "finish successfully in the happy flow" in new TestContext { - apiSubmissionService() - .submit(SubmitRequest(commands))( - LoggingContextWithTrace(TraceContext.empty) - ) - .futureValueUS - } - - behavior of "submit" - - it should "return proper gRPC status codes for DamlLf errors" in new TestContext { - loggerFactory.assertLogs( - within = { - val tmplId = toIdentifier("M:T") - - val errorsToExpectedStatuses: Seq[(ErrorCause, Status)] = List( - ErrorCause.DamlLf( - LfError.Interpretation( - LfError.Interpretation.DamlException( - LfInterpretationError.ContractNotFound("00" + "00" * 32) - ), - None, - ) - ) -> Status.NOT_FOUND, - ErrorCause.DamlLf( - LfError.Interpretation( - LfError.Interpretation.DamlException( - LfInterpretationError.DuplicateContractKey( - GlobalKey - .assertBuild( - tmplId, - PackageName.assertFromString("pkg-name"), - Value.ValueUnit, - crypto.Hash.hashPrivateKey("dummy-key-hash"), - ) - ) - ), - None, - ) - ) -> Status.ALREADY_EXISTS, - ErrorCause.DamlLf( - LfError.Validation( - LfError.Validation.ReplayMismatch(ReplayMismatch(null, null)) - ) - ) -> Status.INTERNAL, - ErrorCause.DamlLf( - LfError.Preprocessing( - LfError.Preprocessing.Lookup( - LookupError.NotFound( - Reference.Package(defaultPackageId), - Reference.Package(defaultPackageId), - ) - ) - ) - ) -> Status.INVALID_ARGUMENT, - ErrorCause.DamlLf( - LfError.Interpretation( - LfError.Interpretation.DamlException( - LfInterpretationError.FailedAuthorization( - NodeId(1), - lf.ledger.FailedAuthorization.NoSignatories(tmplId, None), - ) - ), - None, - ) - ) -> Status.INVALID_ARGUMENT, - ErrorCause.LedgerTime(0) -> Status.ABORTED, - ) - - // when - val results = errorsToExpectedStatuses - .map { case (error, expectedStatus) => - when( - commandExecutor.execute( - eqTo(commands), - any[Hash], - eqTo(routingSynchronizerState), - anyBoolean, - )(any[LoggingContextWithTrace]) - ).thenReturn( - EitherT[FutureUnlessShutdown, ErrorCause, CommandExecutionResult]( - FutureUnlessShutdown.pure(Left(error)) - ) - ) - - apiSubmissionService() - .submit(SubmitRequest(commands)) - .transform(result => Success(UnlessShutdown.Outcome(expectedStatus -> result))) - .futureValueUS - } - - // then - results.foreach { case (expectedStatus: Status, result: Try[UnlessShutdown[Unit]]) => - inside(result) { case Failure(exception) => - exception.getMessage should startWith(expectedStatus.getCode.toString) - } - } - }, - assertions = _.errorMessage should include( - "LEDGER_API_INTERNAL_ERROR(4,0): Observed un-expected replay mismatch" - ), - _.errorMessage should include("Unhandled internal error"), - ) - } - - it should "rate-limit when configured to do so" in new TestContext { - val grpcError = RpcStatus.of(Status.Code.ABORTED.value(), s"Quota Exceeded", Seq.empty) - - apiSubmissionService(checkOverloaded = _ => Some(SubmissionResult.SynchronousError(grpcError))) - .submit(SubmitRequest(commands)) - .transform { - case Failure(e: StatusRuntimeException) - if e.getStatus.getCode.value == grpcError.code && e.getStatus.getDescription == grpcError.message => - Success(UnlessShutdown.Outcome(succeed)) - case result => - fail(s"Expected submission to be aborted, but got $result") - } - .futureValueUS - } - - private trait TestContext { - val syncService = mock[state.SyncService] - val timeProvider = TimeProvider.Constant(Instant.now) - val timeProviderType = TimeProviderType.Static - val seedService = SeedService.WeakRandom - val commandExecutor = mock[CommandExecutor] - val metrics = LedgerApiServerMetrics.ForTesting - val alice = Ref.Party.assertFromString("alice") - - val synchronizerId: SynchronizerId = SynchronizerId.tryFromString("x::synchronizerId") - - val processedDisclosedContract = - FatContractInstanceHelper.buildFatContractInstance( - templateId = Identifier.assertFromString("some:pkg:identifier"), - packageName = PackageName.assertFromString("pkg-name"), - contractId = TransactionBuilder.newCid, - argument = Value.ValueNil, - createdAt = Timestamp.Epoch, - authenticationData = Bytes.Empty, - signatories = Set(alice), - stakeholders = Set(alice), - keyOpt = None, - version = LfSerializationVersion.V1, - ) - - val disclosedContract = DisclosedContract( - fatContractInstance = processedDisclosedContract, - synchronizerIdO = Some(synchronizerId), - ) - - val commands = Commands( - workflowId = None, - userId = Ref.UserId.assertFromString("app"), - commandId = CommandId(Ref.CommandId.assertFromString("cmd")), - submissionId = None, - actAs = Set.empty, - readAs = Set.empty, - submittedAt = Timestamp.Epoch, - deduplicationPeriod = DeduplicationPeriod.DeduplicationDuration(Duration.ZERO), - commands = LfCommands( - commands = ImmArray.empty, - ledgerEffectiveTime = Timestamp.Epoch, - commandsReference = "", - ), - disclosedContracts = ImmArray(disclosedContract), - synchronizerId = None, - prefetchKeys = Seq.empty, - tapsMaxPasses = None, - ) - - val submitterInfo = SubmitterInfo( - actAs = Nil, - readAs = Nil, - userId = Ref.UserId.assertFromString("foobar"), - commandId = Ref.CommandId.assertFromString("foobar"), - deduplicationPeriod = DeduplicationDuration(Duration.ofMinutes(1)), - submissionId = None, - externallySignedSubmission = None, - ) - val transactionMeta = TransactionMeta( - ledgerEffectiveTime = Timestamp.Epoch, - workflowId = None, - preparationTime = Time.Timestamp.Epoch, - submissionSeed = Hash.hashPrivateKey("SomeHash"), - timeBoundaries = LedgerTimeBoundaries.unconstrained, - optUsedPackages = None, - optNodeSeeds = None, - optByKeyNodes = None, - ) - val estimatedInterpretationCost = 5L - val processedDisclosedContracts = ImmArray(processedDisclosedContract) - val commandInterpretationResult = CommandInterpretationResult( - submitterInfo = submitterInfo, - optSynchronizerId = None, - transactionMeta = transactionMeta, - transaction = transaction, - dependsOnLedgerTime = false, - interpretationTimeNanos = estimatedInterpretationCost, - globalKeyMapping = Map.empty, - processedDisclosedContracts = processedDisclosedContracts, - ) - val synchronizerRank = - SynchronizerRank.single(SynchronizerId.tryFromString("da::test").toPhysical) - val routingSynchronizerState = mock[RoutingSynchronizerState] - val commandExecutionResult = CommandExecutionResult( - commandInterpretationResult = commandInterpretationResult, - synchronizerRank = synchronizerRank, - routingSynchronizerState = routingSynchronizerState, - ) - - when(syncService.getRoutingSynchronizerState(traceContext)).thenReturn( - FutureUnlessShutdown.pure(routingSynchronizerState) - ) - - when( - commandExecutor.execute( - eqTo(commands), - any[Hash], - eqTo(routingSynchronizerState), - anyBoolean, - )( - any[LoggingContextWithTrace] - ) - ) - .thenReturn( - EitherT[FutureUnlessShutdown, ErrorCause, CommandExecutionResult]( - FutureUnlessShutdown.pure(Right(commandExecutionResult)) - ) - ) - when( - syncService.submitTransaction( - eqTo(transaction), - eqTo(synchronizerRank), - eqTo(routingSynchronizerState), - eqTo(submitterInfo), - eqTo(transactionMeta), - eqTo(estimatedInterpretationCost), - eqTo(Map.empty), - eqTo(processedDisclosedContracts), - )(any[TraceContext]) - ).thenReturn(Future(SubmissionResult.Acknowledged)) - - def apiSubmissionService( - checkOverloaded: TraceContext => Option[state.SubmissionResult] = _ => None - ) = new CommandSubmissionServiceImpl( - syncService = syncService, - timeProviderType = timeProviderType, - timeProvider = timeProvider, - seedService = seedService, - commandExecutor = commandExecutor, - checkOverloaded = checkOverloaded, - metrics = metrics, - loggerFactory = loggerFactory, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/GeneratorsInteractiveSubmission.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/GeneratorsInteractiveSubmission.scala deleted file mode 100644 index 318be984fe..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/GeneratorsInteractiveSubmission.scala +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive - -import com.digitalasset.canton.config.GeneratorsConfig.* -import com.digitalasset.canton.config.PositiveFiniteDuration -import com.digitalasset.canton.config.RequireTypes.{PositiveInt, PositiveLong} -import com.digitalasset.canton.data.{DeduplicationPeriod, LedgerTimeBoundaries, Offset} -import com.digitalasset.canton.ledger.participant.state.{SubmitterInfo, TransactionMeta} -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.EnrichedTransactionData.ExternalInputContract -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.PrepareTransactionData -import com.digitalasset.canton.protocol.LfFatContractInst -import com.digitalasset.canton.topology.{GeneratorsTopology, SynchronizerId} -import com.digitalasset.canton.{ - GeneratorsLf, - LedgerUserId, - LfPackageId, - LfPartyId, - LfTimestamp, - LfValue, -} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.{Bytes, ImmArray, Time} -import com.digitalasset.daml.lf.transaction.BackwardsCompatibilityImplicits.* -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - FatContractInstance, - GlobalKey, - Node, - NodeId, - SerializationVersion as LfSerializationVersion, - SubmittedTransaction, - Transaction, - VersionedTransaction, -} -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.test.ValueGenerators -import com.digitalasset.daml.lf.value.test.ValueGenerators.SerializationVersionGen -import magnolify.scalacheck.auto.genArbitrary -import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.{Arbitrary, Gen} - -import scala.jdk.CollectionConverters.* -import scala.util.Random - -final class GeneratorsInteractiveSubmission( - generatorsLf: GeneratorsLf, - generatorsTopology: GeneratorsTopology, - exclusiveMaxSerializationVersion: LfSerializationVersion, -) { - import com.digitalasset.canton.Generators.* - import generatorsLf.* - import generatorsTopology.* - - // The value generator generates record values with trailing nones, which trips up the serialization tests for - // external signing because the trailing nones get stripped when the value gets serialized to a LAPI value, - // and so the deserialized version is not equal to the original generated value. - // The engine now outputs normalized values without trailing Nones anyway so there's no need to test them. - // This may be removed when the Daml repo provides generators that allow generating such transaction natively - def normalizeValue(value: LfValue): LfValue = value match { - case Value.ValueRecord(tycon, fields) => - val fieldsWithoutTrailingNones = fields.reverseIterator - .dropWhile { - case (_, Value.ValueOptional(None)) => true - case _ => false - } - .toList - .map { case (maybeName, value) => - (maybeName, normalizeValue(value)) - } - .reverse - Value.ValueRecord(tycon, ImmArray.from(fieldsWithoutTrailingNones)) - case Value.ValueVariant(tycon, variant, value) => - Value.ValueVariant(tycon, variant, normalizeValue(value)) - case cid: Value.ValueContractId => cid - case Value.ValueList(values) => - Value.ValueList(values.map(normalizeValue)) - case Value.ValueOptional(value) => - Value.ValueOptional(value.map(normalizeValue)) - case Value.ValueTextMap(value) => - Value.ValueTextMap(value.mapValue(normalizeValue)) - case Value.ValueGenMap(entries) => - Value.ValueGenMap(entries.map { case (k, v) => - (normalizeValue(k), normalizeValue(v)) - }) - case atom: Value.ValueCidLessAtom => atom - } - - // Updated nodes that filter out fields not supported in the serializationVersion - def normalizeNode[N <: Node](node: N): N = node match { - case node: Node.Create => - node - .copy( - // signatories should be a subset of stakeholders for the node to be valid - // take a random size subset of stakeholders, but 1 minimum - signatories = node.stakeholders.take(Random.nextInt(10) + 1), - arg = normalizeValue(node.arg), - ) - .asInstanceOf[N] - case node: Node.Exercise => - node - .copy( - chosenValue = normalizeValue(node.chosenValue), - exerciseResult = node.exerciseResult.map(normalizeValue), - ) - .asInstanceOf[N] - case node: Node.Fetch => - node - .copy( - keyOpt = node.keyOpt.map { globalKeyWithMaintainers => - globalKeyWithMaintainers.copy( - globalKey = GlobalKey.assertWithRenormalizedValue( - globalKeyWithMaintainers.globalKey, - normalizeValue(globalKeyWithMaintainers.globalKey.key), - ) - ) - } - ) - .asInstanceOf[N] - case node: Node.QueryByKey => - node - .copy( - key = node.key.copy( - globalKey = GlobalKey.assertWithRenormalizedValue( - node.key.globalKey, - normalizeValue(node.key.globalKey.key), - ) - ) - ) - .asInstanceOf[N] - case node => node - } - - private val nodeIdGen = Arbitrary.arbInt.arbitrary.map(NodeId(_)) - - private def normalizeTxFor(tx: Transaction) = - Transaction(tx.nodes.view.mapValues(normalizeNode).toMap, tx.roots) - - private val versionedTransactionGenerator = for { - version <- SerializationVersionGen(maxVersion = Some(exclusiveMaxSerializationVersion)) - transaction <- ValueGenerators.noDanglingRefGenTransaction(version).map(normalizeTxFor) - } yield VersionedTransaction(version, transaction.nodes, transaction.roots) - - implicit val transactionArb: Arbitrary[VersionedTransaction] = Arbitrary( - versionedTransactionGenerator - ) - - private implicit val genHash: Gen[crypto.Hash] = - Gen - .containerOfN[Array, Byte]( - crypto.Hash.underlyingHashLength, - arbitrary[Byte], - ) - .map(crypto.Hash.assertFromByteArray) - - private implicit val nodeSeed: Gen[(NodeId, Hash)] = for { - nodeId <- nodeIdGen - hash <- genHash - } yield (nodeId, hash) - - private val nodeSeedsGen: Gen[Option[ImmArray[(NodeId, Hash)]]] = for { - seeds <- Gen.listOf(nodeSeed).map(ImmArray.from) - optSeeds <- Gen.option(seeds) - } yield optSeeds - - implicit val nodeSeedsArbitrary: Arbitrary[Option[ImmArray[(NodeId, Hash)]]] = Arbitrary( - nodeSeedsGen - ) - - private implicit val byKeyNodesArbitrary: Arbitrary[Option[ImmArray[NodeId]]] = Arbitrary( - Gen.option(Gen.listOf(nodeIdGen).map(ImmArray.from(_))) - ) - - private implicit val genDeduplicationDuration: Gen[DeduplicationPeriod.DeduplicationDuration] = - Gen - .choose(1, 200L) - .map(PositiveFiniteDuration.ofMinutes) - .map(d => DeduplicationPeriod.DeduplicationDuration(d.asJava)) - private implicit val genDeduplicationOffset: Gen[DeduplicationPeriod.DeduplicationOffset] = - Arbitrary - .arbitrary[PositiveLong] - .map(_.value) - .map(Offset.tryFromLong) - .map(Option.apply) - .map(DeduplicationPeriod.DeduplicationOffset.apply) - private implicit val genDeduplicationPeriodArb: Arbitrary[DeduplicationPeriod] = - Arbitrary(Gen.oneOf(genDeduplicationDuration, genDeduplicationOffset)) - - private implicit val timeBoundariesGen: Gen[LedgerTimeBoundaries] = for { - t1 <- Gen.option(Arbitrary.arbitrary[Time.Timestamp]) - t2 <- Gen.option(Arbitrary.arbitrary[Time.Timestamp]) - } yield { - (t1, t2) match { - case (Some(t1), Some(t2)) if t1 > t2 => LedgerTimeBoundaries(Time.Range(t2, t1)) - case _ => LedgerTimeBoundaries.fromConstraints(t1, t2) - } - } - - private implicit val submitterInfoGen: Gen[SubmitterInfo] = for { - actAs <- Arbitrary.arbitrary[List[LfPartyId]] - readAs <- Arbitrary.arbitrary[List[LfPartyId]] - userId <- Arbitrary.arbitrary[LedgerUserId] - commandId <- lfCommandIdArb.arbitrary - deduplicationPeriod <- genDeduplicationPeriodArb.arbitrary - submissionIdO <- Gen.option(lfSubmissionIdArb.arbitrary) - } yield SubmitterInfo( - actAs, - readAs, - userId, - commandId, - deduplicationPeriod, - submissionIdO, - externallySignedSubmission = None, - ) - - private def transactionMetaGen(transaction: VersionedTransaction): Gen[TransactionMeta] = for { - ledgerEffectiveTime <- Arbitrary.arbitrary[Time.Timestamp] - workflowIdO <- Gen.option(lfWorkflowIdArb.arbitrary) - preparationTime <- Arbitrary.arbitrary[Time.Timestamp] - submissionSeed <- Arbitrary.arbitrary[crypto.Hash] - timeBoundaries <- timeBoundariesGen - usedPackagesO <- Arbitrary.arbitrary[Option[Set[LfPackageId]]] - optNodeSeedsO <- Gen - .listOfN(transaction.nodes.size, Arbitrary.arbitrary[crypto.Hash]) - .map(seeds => transaction.nodes.keySet.zip(seeds)) - optByKeyNodeO <- Arbitrary.arbitrary[Option[ImmArray[NodeId]]] - } yield TransactionMeta( - ledgerEffectiveTime, - workflowIdO, - preparationTime, - submissionSeed, - timeBoundaries, - usedPackagesO, - Some(ImmArray.from(optNodeSeedsO)), - optByKeyNodeO, - ) - - private val globalKeyMappingGen: Gen[Map[GlobalKey, Vector[Value.ContractId]]] = - boundedMapGen[GlobalKey, Option[Value.ContractId]].map(_.transform((_, v) => v.asCidVector)) - - private def inputContractsGen(overrideCid: Value.ContractId): Gen[LfFatContractInst] = for { - version <- ValueGenerators.SerializationVersionGen(maxVersion = - Some(exclusiveMaxSerializationVersion) - ) - create <- ValueGenerators - .malformedCreateNodeGenWithVersion(version) - .map(normalizeNode) - createdAt <- Arbitrary.arbitrary[Time.Timestamp] - authenticationData <- Arbitrary.arbitrary[Array[Byte]].map(Bytes.fromByteArray) - } yield FatContractInstance.fromCreateNode( - create.copy(coid = overrideCid), - CreationTime.CreatedAt(createdAt), - authenticationData, - ) - - private val preparedTransactionDataGen: Gen[PrepareTransactionData] = for { - submitterInfo <- submitterInfoGen - synchronizerId <- Arbitrary.arbitrary[SynchronizerId] - transaction <- versionedTransactionGenerator.map(SubmittedTransaction(_)) - transactionMeta <- transactionMetaGen(transaction) - globalKeyMapping <- globalKeyMappingGen - // Use the contract IDs actually referenced by the transaction (fetch/exercise nodes), - // because the decoder validates that input contracts match the transaction's inputs. - coids = transaction.inputContracts.toList - inputContracts <- Gen.sequence(coids.map(inputContractsGen)) - // The enriched contract must share the same createdAt as the original contract, - // because the decoder checks that created_at (from the enriched contract) matches - // the createdAt encoded in the event_blob (from the original contract). - enrichedInputContracts <- Gen.sequence( - coids.zip(inputContracts.asScala).map { case (cid, originalFci) => - inputContractsGen(cid).map(_.mapCreatedAt(_ => originalFci.createdAt)) - } - ) - mediatorGroup <- Arbitrary.arbitrary[PositiveInt] - transactionUUID <- Gen.uuid - maxRecordTime <- Arbitrary.arbitrary[Option[LfTimestamp]] - } yield PrepareTransactionData( - submitterInfo, - transactionMeta, - transaction, - globalKeyMapping, - inputContracts.asScala - .zip(enrichedInputContracts.asScala) - .map { case (originalFci, enrichedFci) => - originalFci.contractId -> ExternalInputContract(originalFci, enrichedFci) - } - .toMap, - synchronizerId, - mediatorGroup.value, - transactionUUID, - maxRecordTime, - ) - - implicit val preparedTransactionDataArb: Arbitrary[PrepareTransactionData] = Arbitrary( - preparedTransactionDataGen - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/PreparedTransactionCodecV1Spec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/PreparedTransactionCodecV1Spec.scala deleted file mode 100644 index 768afdb062..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/PreparedTransactionCodecV1Spec.scala +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.command.interactive - -import com.daml.ledger.api.v2.interactive.transaction.v1.interactive_submission_data.Node.NodeType -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.PreparedTransactionCodec.* -import com.digitalasset.canton.platform.apiserver.services.command.interactive.codec.{ - PreparedTransactionDecoder, - PreparedTransactionEncoder, -} -import com.digitalasset.canton.topology.GeneratorsTopology -import com.digitalasset.canton.{BaseTest, GeneratorsLf, HasExecutionContext} -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.ImmArray -import com.digitalasset.daml.lf.transaction.{ - Node, - NodeId, - SerializationVersion as LfSerializationVersion, - VersionedTransaction, -} -import com.digitalasset.daml.lf.value.test.ValueGenerators -import org.scalacheck.Arbitrary -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks - -class PreparedTransactionCodecV1Spec - extends AnyWordSpec - with Matchers - with BaseTest - with ScalaCheckPropertyChecks - with HasExecutionContext { - - private implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace.ForTesting - - private val encoder = new PreparedTransactionEncoder(loggerFactory) - private val decoder = new PreparedTransactionDecoder(loggerFactory) - - private lazy val generatorsTopology = new GeneratorsTopology(testedProtocolVersion) - private lazy val generatorsLf = new GeneratorsLf(generatorsTopology) - private lazy val generatorsInteractiveSubmission = - new GeneratorsInteractiveSubmission( - generatorsLf, - generatorsTopology, - exclusiveMaxSerializationVersion = LfSerializationVersion.VDev, - ) - - "Prepared transaction" should { - import generatorsInteractiveSubmission.* - - "round trip encode and decode any LF transaction" in { - forAll(minSuccessful(1000)) { - (transaction: VersionedTransaction, nodeSeeds: Option[ImmArray[(NodeId, Hash)]]) => - val result = for { - encoded <- encoder.serializeTransaction(transaction, nodeSeeds) - decoded <- decoder.transactionTransformer - .transform(encoded) - .toFutureWithLoggedFailuresDecode("Failed to decode transaction", logger) - } yield { - decoded shouldEqual transaction - } - - timeouts.default.await_("Round trip")(result) - } - } - - "support interfaceId on exercise node" in { - implicit val nodeGen: Arbitrary[Node.Exercise] = Arbitrary( - for { - exerciseNode <- ValueGenerators.danglingRefExerciseNodeGenWithVersion( - LfSerializationVersion.V1 - ) - normalized = normalizeNode(exerciseNode).copy( - interfaceId = Some(ValueGenerators.idGen.sample.value) - ) - } yield normalized - ) - - forAll { (node: Node.Exercise) => - val encoded = - encoder.v1.exerciseTransformer(LfSerializationVersion.V1).transform(node).asEither.value - decoder.v1.exerciseTransformer.transform(encoded).asEither.value shouldEqual node - } - } - - "support interfaceId on fetch node" in { - implicit val nodeGen: Arbitrary[Node.Fetch] = Arbitrary( - for { - fetchNode <- ValueGenerators.fetchNodeGenWithVersion(LfSerializationVersion.V1) - normalized = normalizeNode(fetchNode).copy( - interfaceId = Some(ValueGenerators.idGen.sample.value) - ) - } yield normalized - ) - - forAll { (node: Node.Fetch) => - val encoded = - encoder.v1.fetchTransformer(LfSerializationVersion.V1).transform(node).asEither.value - decoder.v1.fetchTransformer.transform(encoded).asEither.value shouldEqual node - } - } - - "support interfaceId on exercise node" in { - implicit val nodeGen: Arbitrary[Node.Exercise] = Arbitrary( - for { - exerciseNode <- ValueGenerators.danglingRefExerciseNodeGen - normalized = normalizeNodeForV1(exerciseNode).copy( - interfaceId = Some(ValueGenerators.idGen.sample.value) - ) - } yield normalized - ) - - forAll { (node: Node.Exercise) => - val encoded = - encoder.v1.exerciseTransformer(LanguageVersion.v2_1).transform(node).asEither.value - decoder.v1.exerciseTransformer.transform(encoded).asEither.value shouldEqual node - } - } - - "support interfaceId on fetch node" in { - implicit val nodeGen: Arbitrary[Node.Fetch] = Arbitrary( - for { - fetchNode <- ValueGenerators.fetchNodeGen - normalized = normalizeNodeForV1(fetchNode).copy( - interfaceId = Some(ValueGenerators.idGen.sample.value) - ) - } yield normalized - ) - - forAll { (node: Node.Fetch) => - val encoded = - encoder.v1.fetchTransformer(LanguageVersion.v2_1).transform(node).asEither.value - decoder.v1.fetchTransformer.transform(encoded).asEither.value shouldEqual node - } - } - - "sort sets of parties" in { - forAll { (transaction: VersionedTransaction, nodeSeeds: Option[ImmArray[(NodeId, Hash)]]) => - val result = for { - encoded <- encoder.serializeTransaction(transaction, nodeSeeds) - } yield { - val partiesLists = encoded.nodes.flatMap { - _.versionedNode.v1.value.nodeType match { - case NodeType.Empty => Seq.empty - case NodeType.Create(value) => Seq(value.signatories, value.stakeholders) - case NodeType.Fetch(value) => - Seq(value.signatories, value.stakeholders, value.actingParties) - case NodeType.Exercise(value) => - Seq( - value.signatories, - value.stakeholders, - value.actingParties, - value.choiceObservers, - ) - case NodeType.Rollback(_) => Seq.empty - case NodeType.QueryByKey(_) => Seq.empty - } - } - partiesLists.foreach { partiesList => - partiesList.sorted should contain theSameElementsInOrderAs (partiesList) - } - } - - timeouts.default.await_("Round trip")(result) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CancellableTimeoutSupportSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CancellableTimeoutSupportSpec.scala deleted file mode 100644 index 81e4225631..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/tracking/CancellableTimeoutSupportSpec.scala +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.tracking - -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.{BaseTest, config} -import org.scalatest.concurrent.{Eventually, ScalaFutures} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.Timer -import scala.concurrent.Promise -import scala.util.Failure - -class CancellableTimeoutSupportSpec - extends AnyFlatSpec - with Matchers - with Eventually - with ScalaFutures - with BaseTest { - - behavior of classOf[CancellableTimeoutSupport].getSimpleName - - it should "schedule a command entry task" in new TestFixture { - override def run(): Unit = { - val timeoutDuration = config.NonNegativeFiniteDuration.ofMillis(10L) - val exception = new RuntimeException("on failure") - val failure = Failure(exception) - val promise = Promise[String]() - - cancellableTimeoutSupport.scheduleOnce( - timeoutDuration, - promise = promise, - onTimeout = failure, - ) - - promise.future.failed.futureValue shouldBe exception - } - } - - it should "cancel a scheduled task on close" in new TestFixture { - override def run(): Unit = { - val timeoutDuration = config.NonNegativeFiniteDuration.ofMillis(100L) - val exception = new RuntimeException("on failure") - val failure = Failure(exception) - val promise = Promise[String]() - - val scheduled = cancellableTimeoutSupport.scheduleOnce( - timeoutDuration, - promise = promise, - onTimeout = failure, - ) - - scheduled.close() - // Check that the task hasn't executed after the timeout duration expired - Threading.sleep(1000L) - promise.isCompleted shouldBe false - } - } - - private trait TestFixture { - def run(): Unit - - private val timer = new Timer("test-timer") - val cancellableTimeoutSupport = new CancellableTimeoutSupportImpl(timer, loggerFactory) - run() - timer.cancel() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/tracking/SubmissionTrackerSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/tracking/SubmissionTrackerSpec.scala deleted file mode 100644 index aa7bbb2e1b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/tracking/SubmissionTrackerSpec.scala +++ /dev/null @@ -1,491 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.services.tracking - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.ledger.api.v2.completion.Completion -import com.digitalasset.base.error.ErrorsAssertions -import com.digitalasset.canton.ledger.error.groups.ConsistencyErrors -import com.digitalasset.canton.ledger.error.{CommonErrors, LedgerApiErrors} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.ErrorLoggingContext -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker.{ - SubmissionKey, - SubmissionTrackerImpl, -} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.{BaseTest, HasExecutionContext, config} -import com.google.rpc.status.Status -import io.grpc.StatusRuntimeException -import org.scalatest.concurrent.{Eventually, IntegrationPatience, ScalaFutures} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.{Assertion, Succeeded} - -import java.util.Timer -import scala.annotation.nowarn -import scala.concurrent.{Future, Promise} -import scala.util.Try - -class SubmissionTrackerSpec - extends AnyFlatSpec - with ScalaFutures - with ErrorsAssertions - with IntegrationPatience - with Eventually - with BaseTest - with HasExecutionContext { - - behavior of classOf[SubmissionTracker].getSimpleName - - it should "track a submission by correct SubmissionKey" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = for { - _ <- Future.unit - // Track new submission - trackedSubmissionF = submissionTracker.track(submissionKey, `1 day timeout`, submitSucceeds) - - // Completion with mismatching submissionId - completionWithMismatchingSubmissionId = completionOk.copy( - submissionId = "wrongSubmissionId", - actAs = submitters.toSeq, - ) - _ = submissionTracker.onCompletion( - CompletionStreamResponse(completionResponse = - CompletionStreamResponse.CompletionResponse.Completion( - completionWithMismatchingSubmissionId - ) - ) - ) - - // Completion with mismatching commandId - completionWithMismatchingCommandId = completionOk.copy( - commandId = "wrongCommandId", - actAs = submitters.toSeq, - ) - _ = submissionTracker.onCompletion( - CompletionStreamResponse(completionResponse = - CompletionStreamResponse.CompletionResponse.Completion(completionWithMismatchingCommandId) - ) - ) - - // Completion with mismatching userId - completionWithMismatchingUserId = completionOk.copy( - userId = "wrongUserId", - actAs = submitters.toSeq, - ) - _ = submissionTracker.onCompletion( - CompletionStreamResponse(completionResponse = - CompletionStreamResponse.CompletionResponse.Completion(completionWithMismatchingUserId) - ) - ) - - // Completion with mismatching actAs - _ = submissionTracker.onCompletion( - CompletionStreamResponse(completionResponse = - CompletionStreamResponse.CompletionResponse.Completion( - completionOk.copy(actAs = submitters.toSeq :+ "another_party") - ) - ) - ) - - // Matching completion - _ = submissionTracker.onCompletion( - CompletionStreamResponse(completionResponse = - CompletionStreamResponse.CompletionResponse.Completion(completionOk) - ) - ) - - trackedSubmission <- trackedSubmissionF - } yield { - trackedSubmission shouldBe CompletionResponse(completionOk) - } - } - - it should "fail on submission failure" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = for { - _ <- Future.unit - // Track new submission - trackedSubmissionF = submissionTracker.track(submissionKey, `1 day timeout`, submitFails) - - failure <- trackedSubmissionF.failed - } yield { - failure shouldBe a[RuntimeException] - failure.getMessage shouldBe failureInSubmit.getMessage - } - } - - it should "fail on completion failure" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = for { - _ <- Future.unit - // Track new submission - trackedSubmissionF = submissionTracker.track(submissionKey, `1 day timeout`, submitSucceeds) - - // Complete the submission with a failed completion - _ = submissionTracker.onCompletion( - CompletionStreamResponse( - completionResponse = CompletionStreamResponse.CompletionResponse.Completion( - completionFailed.copy(actAs = submitters.toSeq) - ) - ) - ) - - failure <- trackedSubmissionF.failed - } yield inside(failure) { case sre: StatusRuntimeException => - assertError( - sre, - completionFailedGrpcCode, - completionFailedMessage, - Seq.empty, - verifyEmptyStackTrace = false, - ) - succeed - } - } - - it should "fail if timeout reached" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = - submissionTracker - .track(submissionKey, zeroTimeout, submitSucceeds) - .failed - .map(inside(_) { case actualStatusRuntimeException: StatusRuntimeException => - assertError( - actual = actualStatusRuntimeException, - expected = CommonErrors.RequestTimeOut - .Reject( - "Timed out while awaiting for a completion corresponding to a command submission with command-id=cId_1 and submission-id=sId_1.", - definiteAnswer = false, - ) - .asGrpcError, - ) - succeed - }) - } - - it should "fail on duplicate submission" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = for { - _ <- Future.unit - - // Track new submission - firstSubmissionF = submissionTracker.track(submissionKey, `1 day timeout`, submitSucceeds) - - // Track the same submission again - actualException <- submissionTracker - .track(submissionKey, `1 day timeout`, submitSucceeds) - .failed - - // Complete the first submission to ensure clean pending map at the end - _ = submissionTracker.onCompletion( - CompletionStreamResponse(completionResponse = - CompletionStreamResponse.CompletionResponse.Completion( - completionOk.copy(actAs = submitters.toSeq) - ) - ) - ) - _ <- firstSubmissionF - } yield inside(actualException) { case actualStatusRuntimeException: StatusRuntimeException => - // Expect duplicate error - assertError( - actual = actualStatusRuntimeException, - expected = ConsistencyErrors.SubmissionAlreadyInFlight - .Reject() - .asGrpcError, - ) - succeed - } - } - - it should "fail on a submission with a command missing the submission id" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = - loggerFactory.assertLogs( - within = { - submissionTracker - .track( - submissionKey = submissionKey.copy(submissionId = ""), - timeout = `1 day timeout`, - submit = submitSucceeds, - ) - .failed - .map(inside(_) { case actualStatusRuntimeException: StatusRuntimeException => - assertError( - actual = actualStatusRuntimeException, - expected = CommonErrors.ServiceInternalError - .Generic("Missing submission id in submission tracker") - .asGrpcError, - ) - succeed - }) - }, - assertions = _.errorMessage should include( - "SERVICE_INTERNAL_ERROR(4,0): Missing submission id in submission tracker" - ), - _.errorMessage should include( - "SERVICE_INTERNAL_ERROR(4,0): Missing submission id in submission tracker" - ), - ) - } - - it should "gracefully handle errors in the cancellable timeout creation" in new SubmissionTrackerFixture { - private lazy val thrownException = new RuntimeException("scheduleOnce throws") - override def timeoutSupport: CancellableTimeoutSupport = new CancellableTimeoutSupport { - override def scheduleOnce[T]( - duration: config.NonNegativeFiniteDuration, - promise: Promise[T], - onTimeout: => Try[T], - )(implicit traceContext: TraceContext): AutoCloseable = - throw thrownException - } - override def run: Future[Assertion] = for { - _ <- Future.unit - // Track new submission - trackedSubmissionF = loggerFactory.suppressErrors( - submissionTracker.track(submissionKey, `1 day timeout`, submitFails) - ) - failure <- trackedSubmissionF.failed - } yield { - failure shouldBe thrownException - } - } - - it should "fail after exceeding the max-commands-in-flight" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = for { - _ <- Future.unit - - _ = submissionTracker.track( - submissionKey.copy(commandId = "c1"), - `1 day timeout`, - submitSucceeds, - ) - _ = submissionTracker.track( - submissionKey.copy(commandId = "c2"), - `1 day timeout`, - submitSucceeds, - ) - _ = submissionTracker.track( - submissionKey.copy(commandId = "c3"), - `1 day timeout`, - submitSucceeds, - ) - // max-commands-in-flight = 3. Expect rejection - submissionOverLimitF = submissionTracker.track( - submissionKey.copy(commandId = "c4"), - `1 day timeout`, - submitSucceeds, - ) - - // Close the tracker to ensure clean pending map at the end - _ = submissionTracker.close() - failure <- submissionOverLimitF.failed - } yield inside(failure) { case actualStatusRuntimeException: StatusRuntimeException => - assertError( - actual = actualStatusRuntimeException, - expected = LedgerApiErrors.ParticipantBackpressure - .Rejection("Maximum number of in-flight requests reached") - .asGrpcError, - ) - succeed - } - } - - it should "fail if a command completion is missing its completion status" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = loggerFactory.suppressErrors { - for { - _ <- Future.unit - // Track new submission - trackedSubmissionF = submissionTracker.track(submissionKey, `1 day timeout`, submitSucceeds) - - // Complete the submission with completion response - _ = submissionTracker.onCompletion( - CompletionStreamResponse( - completionResponse = CompletionStreamResponse.CompletionResponse.Completion( - completionOk.copy( - status = None, - actAs = submitters.toSeq, - ) - ) - ) - ) - - failure <- trackedSubmissionF.failed - } yield inside(failure) { case ex: StatusRuntimeException => - assertError( - actual = ex, - expected = CommonErrors.ServiceInternalError - .Generic("Command completion is missing completion status") - .asGrpcError, - ) - succeed - } - } - } - - it should "cancel all trackers on close" in new SubmissionTrackerFixture { - override def run: Future[Assertion] = for { - _ <- Future.unit - // Track some submissions - submission1 = submissionTracker.track(submissionKey, `1 day timeout`, submitSucceeds) - submission2 = submissionTracker.track(otherSubmissionKey, `1 day timeout`, submitSucceeds) - - // Close the tracker - _ = submissionTracker.close() - - failure1 <- submission1.failed - failure2 <- submission2.failed - } yield { - inside(failure1) { case actualStatusRuntimeException: StatusRuntimeException => - assertError( - actual = actualStatusRuntimeException, - expected = GrpcErrors.AbortedDueToShutdown.Error().asGrpcError, - ) - } - inside(failure2) { case actualStatusRuntimeException: StatusRuntimeException => - assertError( - actual = actualStatusRuntimeException, - expected = GrpcErrors.AbortedDueToShutdown.Error().asGrpcError, - ) - } - succeed - } - } - - it should "gracefully complete the completion promises on races" in new SubmissionTrackerFixture { - private def noConcurrentSubmissions = 100 - private def concurrentSubmissionKeys = - (1 to noConcurrentSubmissions).map(id => submissionKey.copy(commandId = s"cmd-$id")) - - override def maxInFlight = 100 - - /* - Nested inside lead to - Name defaultCase$ is already introduced in an enclosing scope as value defaultCase$ - */ - @nowarn( - "msg=Name defaultCase\\$ is already introduced in an enclosing scope as value defaultCase\\$.*" - ) - override def run: Future[Assertion] = for { - _ <- Future.unit - // Track concurrent submissions - submissions = concurrentSubmissionKeys.map(sk => - sk -> submissionTracker.track(sk, `1 day timeout`, submitSucceeds) - ) - - onCompletions = submissions.map { case (key, _) => - () => - Future { - submissionTracker.onCompletion( - CompletionStreamResponse(completionResponse = - CompletionStreamResponse.CompletionResponse.Completion( - completionOk.copy( - commandId = key.commandId, - actAs = submitters.toSeq, - ) - ) - ) - ) - } - } - - (firstHalfOnComplete, secondHalfOnComplete) = onCompletions.splitAt( - noConcurrentSubmissions / 2 - ) - - f1 = Future.traverse(firstHalfOnComplete)(_.apply()) - s_close = Future(submissionTracker.close()) - f2 = Future.traverse(secondHalfOnComplete)(_.apply()) - - _ <- f2 - _ <- s_close - _ <- f1 - _ <- Future.traverse(submissions)( - _._2 - .map(_ => ()) - .recover(inside(_) { case actualStatusRuntimeException: StatusRuntimeException => - assertError( - actual = actualStatusRuntimeException, - expected = GrpcErrors.AbortedDueToShutdown.Error().asGrpcError, - ) - }) - ) - } yield { - succeed - } - } - - abstract class SubmissionTrackerFixture extends BaseTest with Eventually { - private val timer = new Timer("test-timer") - def timeoutSupport: CancellableTimeoutSupport = - new CancellableTimeoutSupportImpl(timer, loggerFactory) - - def maxInFlight = 3 - - val streamTracker = new StreamTrackerImpl( - timeoutSupport, - SubmissionTracker.toKey, - InFlight.Limited(maxInFlight, LedgerApiServerMetrics.ForTesting.commands.maxInFlightLength), - loggerFactory, - ) - - val submissionTracker = - new SubmissionTrackerImpl( - streamTracker, - maxCommandsInFlight = maxInFlight, - metrics = LedgerApiServerMetrics.ForTesting, - loggerFactory = loggerFactory, - ) - - val zeroTimeout: config.NonNegativeFiniteDuration = config.NonNegativeFiniteDuration.Zero - val `1 day timeout`: config.NonNegativeFiniteDuration = - config.NonNegativeFiniteDuration.ofDays(1L) - - val submissionId = "sId_1" - val commandId = "cId_1" - val userId = "apId_1" - val actAs: Seq[String] = Seq("p1", "p2") - val party = "p3" - val submissionKey: SubmissionKey = SubmissionKey( - submissionId = submissionId, - commandId = commandId, - userId = userId, - parties = Set(party) ++ actAs, - ) - val otherSubmissionKey: SubmissionKey = submissionKey.copy(commandId = "cId_2") - val failureInSubmit = new RuntimeException("failure in submit") - val submitFails: TraceContext => FutureUnlessShutdown[Any] = _ => - FutureUnlessShutdown.failed(failureInSubmit) - val submitSucceeds: TraceContext => FutureUnlessShutdown[Any] = _ => FutureUnlessShutdown.unit - - val submitters: Set[String] = (actAs :+ party).toSet - - val completionOk: Completion = Completion.defaultInstance.copy( - submissionId = submissionId, - commandId = commandId, - status = Some(Status(code = io.grpc.Status.Code.OK.value())), - userId = userId, - actAs = submitters.toSeq, - ) - - val errorLogger: ErrorLoggingContext = - ErrorLoggingContext.withExplicitCorrelationId(logger, Map(), traceContext, submissionId) - - val completionFailedGrpcCode = io.grpc.Status.Code.NOT_FOUND - val completionFailedMessage: String = "ledger rejection" - val completionFailed: Completion = completionOk.copy( - status = Some( - Status(code = completionFailedGrpcCode.value(), message = completionFailedMessage) - ) - ) - - def run: Future[Assertion] - - run.futureValue shouldBe Succeeded - // We want to assert this for each test - // Completion of futures might race with removal of the entries from the map - eventually { - streamTracker.pending shouldBe empty - } - // Stop the timer - timer.purge() - timer.cancel() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/OcspResponderFixture.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/OcspResponderFixture.scala deleted file mode 100644 index 2687a3d19b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/OcspResponderFixture.scala +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.tls - -import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner} -import com.daml.testing.utils.{OwnedResource, PekkoBeforeAndAfterAll} -import com.daml.timer.RetryStrategy -import com.digitalasset.canton.util.ConcurrentBufferedLogger -import org.scalatest.Suite -import org.slf4j.LoggerFactory - -import scala.concurrent.duration.* -import scala.concurrent.{ExecutionContext, Future} -import scala.sys.process.Process - -trait OcspResponderFixture extends PekkoBeforeAndAfterAll { this: Suite => - - private val ec: ExecutionContext = system.dispatcher - - private val ResponderHost: String = "127.0.0.1" - private val ResponderPort: Int = 2560 - - protected def indexPath: String - protected def caCertPath: String - protected def ocspKeyPath: String - protected def ocspCertPath: String - protected def ocspTestCertificate: String - - private val processLogger = new ConcurrentBufferedLogger - - private val logger = LoggerFactory.getLogger(getClass) - - override protected def beforeAll(): Unit = { - super.beforeAll() - try { - responderResource.setup() - } catch { - case e: Throwable => - // at least one of the two ocsp processes failed - logger.error(processLogger.output()) - throw e - } - } - - override protected def afterAll(): Unit = { - responderResource.close() - super.afterAll() - } - - private val opensslExecutable: String = "openssl" - - lazy val responderResource = { - implicit val resourceContext: ResourceContext = ResourceContext(ec) - new OwnedResource[ResourceContext, Process]( - owner = responderResourceOwner, - acquisitionTimeout = 20.seconds, - releaseTimeout = 5.seconds, - ) - } - - private def responderResourceOwner: ResourceOwner[Process] = - new ResourceOwner[Process] { - - override def acquire()(implicit context: ResourceContext): Resource[Process] = { - def start(): Future[Process] = - for { - process <- startResponderProcess() - _ <- verifyResponderReady() - } yield process - - def stop(responderProcess: Process): Future[Unit] = - Future { - responderProcess.destroy() - } - - Resource(start())(stop) - } - } - - private def startResponderProcess()(implicit ec: ExecutionContext): Future[Process] = - Future(Process(ocspServerCommand).run(processLogger)) - - private def verifyResponderReady()(implicit ec: ExecutionContext): Future[String] = - RetryStrategy.constant(attempts = 3, waitTime = 5.seconds) { (_, _) => - Future(Process(testOcspRequestCommand).!!(processLogger)) - } - - private def ocspServerCommand = List( - opensslExecutable, - "ocsp", - "-port", - ResponderPort.toString, - "-text", - "-index", - indexPath, - "-CA", - caCertPath, - "-rkey", - ocspKeyPath, - "-rsigner", - ocspCertPath, - ) - - private def testOcspRequestCommand = List( - opensslExecutable, - "ocsp", - "-CAfile", - caCertPath, - "-url", - s"http://$ResponderHost:$ResponderPort", - "-resp_text", - "-issuer", - caCertPath, - "-cert", - ocspTestCertificate, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsCertificateRevocationCheckingSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsCertificateRevocationCheckingSpec.scala deleted file mode 100644 index 83a9829807..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsCertificateRevocationCheckingSpec.scala +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.tls - -import com.daml.testing.utils.{PekkoBeforeAndAfterAll, TestResourceContext} -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.platform.apiserver.LedgerApiService -import com.digitalasset.canton.util.JarResourceUtils -import org.mockito.MockitoSugar -import org.scalatest.wordspec.AsyncWordSpec - -final class TlsCertificateRevocationCheckingSpec - extends AsyncWordSpec - with MockitoSugar - with PekkoBeforeAndAfterAll - with TestResourceContext - with OcspResponderFixture - with BaseTest { - import TlsCertificateRevocationCheckingSpec.resource - - val serverCrt = resource("server.crt") - val serverKey = resource("server.pem") - val caCrt = resource("ca.crt") - val clientCrt = resource("client.crt") - val clientKey = resource("client.pem") - val clientRevokedCrt = resource("client-revoked.crt") - val clientRevokedKey = resource("client-revoked.pem") - val ocspCrt = resource("ocsp.crt") - val ocspKey = resource("ocsp.key.pem") - val index = resource("index.txt") - - override protected def indexPath: String = index.getAbsolutePath - override protected def caCertPath: String = caCrt.getAbsolutePath - override protected def ocspKeyPath: String = ocspKey.getAbsolutePath - override protected def ocspCertPath: String = ocspCrt.getAbsolutePath - override protected def ocspTestCertificate: String = clientCrt.getAbsolutePath - - classOf[LedgerApiService.type].getSimpleName when { - "certificate revocation checking is enabled" should { - "allow TLS connections with valid certificates" in { - TlsFixture( - loggerFactory, - tlsEnabled = true, - serverCrt, - serverKey, - caCrt, - Some(clientCrt), - Some(clientKey), - certRevocationChecking = true, - ) - .makeARequest() - .map(_ => succeed) - } - - "block TLS connections with revoked certificates" in { - TlsFixture( - loggerFactory, - tlsEnabled = true, - serverCrt, - serverKey, - caCrt, - Some(clientRevokedCrt), - Some(clientRevokedKey), - certRevocationChecking = true, - ) - .makeARequest() - .failed - .collect { - case com.daml.grpc.GrpcException.UNAVAILABLE() => - succeed - case ex => - fail(s"Invalid exception: ${ex.getClass.getCanonicalName}: ${ex.getMessage}") - } - } - } - - "certificate revocation checking is not enabled" should { - "allow TLS connections with valid certificates" in { - TlsFixture( - loggerFactory, - tlsEnabled = false, - serverCrt, - serverKey, - caCrt, - Some(clientCrt), - Some(clientKey), - ) - .makeARequest() - .map(_ => succeed) - } - - "allow TLS connections with revoked certificates" in { - TlsFixture( - loggerFactory, - tlsEnabled = false, - serverCrt, - serverKey, - caCrt, - Some(clientRevokedCrt), - Some(clientRevokedKey), - ) - .makeARequest() - .map(_ => succeed) - } - } - } -} - -object TlsCertificateRevocationCheckingSpec { - - protected def resource(src: String) = - JarResourceUtils.resourceFile("test-certificates/" + src) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsFixture.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsFixture.scala deleted file mode 100644 index 28fc5efb84..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsFixture.scala +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.tls - -import com.daml.ledger.resources.{ResourceContext, ResourceOwner} -import com.daml.tls.{ - ServerAuthRequirementConfig, - TlsClientCertificate, - TlsClientConfig, - TlsServerConfig, -} -import com.digitalasset.canton.config.* -import com.digitalasset.canton.config.RequireTypes.{ExistingFile, Port} -import com.digitalasset.canton.grpc.sampleservice.HelloServiceReferenceImplementation -import com.digitalasset.canton.ledger.client.GrpcChannel -import com.digitalasset.canton.ledger.client.configuration.LedgerClientChannelConfiguration -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.networking.grpc.ClientChannelBuilder -import com.digitalasset.canton.platform.apiserver.{ApiService, ApiServices, LedgerApiService} -import com.digitalasset.canton.protobuf -import com.digitalasset.canton.util.JarResourceUtils -import io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth -import io.grpc.{BindableService, ManagedChannel} - -import java.io.File -import java.util.concurrent.Executors -import scala.collection.immutable -import scala.concurrent.{ExecutionContext, Future} - -final case class TlsFixture( - loggerFactory: NamedLoggerFactory, - tlsEnabled: Boolean, - serverCrt: File, - serverKey: File, - caCrt: File, - clientCrt: Option[File], - clientKey: Option[File], - clientAuth: ClientAuth = ClientAuth.REQUIRE, - certRevocationChecking: Boolean = false, -)(implicit rc: ResourceContext, ec: ExecutionContext) { - - def makeARequest(): Future[protobuf.Hello.Response] = - resources().use { channel => - val testRequest = protobuf.Hello.Request("foo") - protobuf.HelloServiceGrpc - .stub(channel) - .hello(testRequest) - } - - private final class EmptyApiServices extends ApiServices { - override val services: Iterable[BindableService] = List( - new HelloServiceReferenceImplementation - ) - override def withServices(otherServices: immutable.Seq[BindableService]): ApiServices = this - - override def close(): Unit = () - } - - private val serverTlsConfiguration = Option.when(tlsEnabled)( - TlsServerConfig( - certChainFile = PemFile(ExistingFile.tryCreate(serverCrt)), - privateKeyFile = PemFile(ExistingFile.tryCreate(serverKey)), - trustCollectionFile = Some(PemFile(ExistingFile.tryCreate(caCrt))), - clientAuth = clientAuth match { - case ClientAuth.NONE => ServerAuthRequirementConfig.None - case ClientAuth.OPTIONAL => ServerAuthRequirementConfig.Optional - case ClientAuth.REQUIRE => - ServerAuthRequirementConfig.Require( - TlsClientCertificate( - certChainFile = PemFile( - ExistingFile.tryCreate(clientCrt.getOrElse(TlsFixture.resource("index.txt"))) - ), // NB: the file is not used - privateKeyFile = PemFile( - ExistingFile.tryCreate(clientKey.getOrElse(TlsFixture.resource("index.txt"))) - ), // NB: the file is not used - ) - ) - }, - enableCertRevocationChecking = certRevocationChecking, - ) - ) - - private def apiServerOwner(): ResourceOwner[ApiService] = { - val apiServices = new EmptyApiServices - - ResourceOwner - .forExecutorService(() => Executors.newCachedThreadPool()) - .flatMap(servicesExecutor => - LedgerApiService( - apiServices = apiServices, - desiredPort = Port.Dynamic, - maxInboundMessageSize = ServerConfig.defaultMaxInboundMessageSize.unwrap, - maxInboundMetadataSize = ServerConfig.defaultMaxInboundMetadataSize.unwrap, - maxConcurrentCallsPerConnection = - ServerConfig.defaultMaxConcurrentCallsPerConnection.unwrap, - address = None, - tlsConfiguration = serverTlsConfiguration, - servicesExecutor = servicesExecutor, - metrics = LedgerApiServerMetrics.ForTesting, - keepAlive = None, - loggerFactory = loggerFactory, - ) - ) - } - - private val clientTlsConfiguration = - Option.when(tlsEnabled)( - TlsClientConfig( - trustCollectionFile = Some(PemFile(ExistingFile.tryCreate(caCrt))), - clientCert = (clientCrt, clientKey) match { - case (Some(crt), Some(key)) => - Some( - TlsClientCertificate( - certChainFile = PemFile(ExistingFile.tryCreate(crt)), - privateKeyFile = PemFile(ExistingFile.tryCreate(key)), - ) - ) - case _ => None - }, - ) - ) - - private val ledgerClientChannelConfiguration = LedgerClientChannelConfiguration( - sslContext = clientTlsConfiguration.map(ClientChannelBuilder.sslContext(_)) - ) - - private def resources(): ResourceOwner[ManagedChannel] = - for { - apiServer <- apiServerOwner() - channel <- new GrpcChannel.Owner(apiServer.port.unwrap, ledgerClientChannelConfiguration) - } yield channel - -} - -object TlsFixture { - protected def resource(src: String) = - JarResourceUtils.resourceFile("test-certificates/" + src) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsSpec.scala deleted file mode 100644 index 9d5b7328f9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/tls/TlsSpec.scala +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.tls - -import com.daml.testing.utils.TestResourceContext -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.platform.apiserver.LedgerApiService -import com.digitalasset.canton.util.JarResourceUtils -import io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.wordspec.AsyncWordSpec - -import java.io.File - -class TlsSpec - extends AsyncWordSpec - with TableDrivenPropertyChecks - with TestResourceContext - with BaseTest { - import TlsSpec.resource - - val serverCrt = resource("server.crt") - val serverKey = resource("server.pem") - val caCrt = resource("ca.crt") - val clientCrt = resource("client.crt") - val clientKey = resource("client.pem") - val invalidClientCrt = resource("ca_alternative.crt") - val invalidClientKey = resource("ca_alternative.pem") - - classOf[LedgerApiService.type].getSimpleName when { - "client authorization is set to none" should { - "allow TLS connections with valid certificates" in { - assertResponseSuccess(Some(clientCrt), Some(clientKey), ClientAuth.NONE) - } - - "allow TLS connections without certificates" in { - assertResponseSuccess(None, None, ClientAuth.NONE) - } - - "allow TLS connections with invalid certificates" in { - assertResponseSuccess(Some(invalidClientCrt), Some(invalidClientKey), ClientAuth.NONE) - } - } - - "client authorization is set to optional" should { - "allow TLS connections with valid certificates" in { - assertResponseSuccess(Some(clientCrt), Some(clientKey), ClientAuth.OPTIONAL) - } - - "allow TLS connections without certificates" in { - assertResponseSuccess(None, None, ClientAuth.OPTIONAL) - } - - "block TLS connections with invalid certificates" in { - assertResponseUnavailable( - Some(invalidClientCrt), - Some(invalidClientKey), - ClientAuth.OPTIONAL, - ) - } - } - - "client authorization is set to require" should { - "allow TLS connections with valid certificates" in { - assertResponseSuccess(Some(clientCrt), Some(clientKey), ClientAuth.REQUIRE) - } - - "block TLS connections without certificates" in { - assertResponseUnavailable(None, None, ClientAuth.REQUIRE) - } - - "block TLS connections with invalid certificates" in { - assertResponseUnavailable( - Some(invalidClientCrt), - Some(invalidClientKey), - ClientAuth.REQUIRE, - ) - } - } - } - - private def makeARequest( - clientCrt: Option[File], - clientKey: Option[File], - clientAuth: ClientAuth, - ) = - TlsFixture( - loggerFactory, - tlsEnabled = true, - serverCrt, - serverKey, - caCrt, - clientCrt, - clientKey, - clientAuth, - ) - .makeARequest() - - private def assertResponseSuccess( - clientCrt: Option[File], - clientKey: Option[File], - clientAuth: ClientAuth, - ) = - makeARequest(clientCrt, clientKey, clientAuth).map(_ => succeed) - - private def assertResponseUnavailable( - clientCrt: Option[File], - clientKey: Option[File], - clientAuth: ClientAuth, - ) = - makeARequest(clientCrt, clientKey, clientAuth).failed - .collect { - case com.daml.grpc.GrpcException.UNAVAILABLE() => - succeed - case ex => - fail(s"Invalid exception: ${ex.getClass.getCanonicalName}: ${ex.getMessage}") - } - -} - -object TlsSpec { - - protected def resource(src: String) = - JarResourceUtils.resourceFile("test-certificates/" + src) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/IdentityProviderConfigUpdateMapperSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/IdentityProviderConfigUpdateMapperSpec.scala deleted file mode 100644 index aff16db6fd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/IdentityProviderConfigUpdateMapperSpec.scala +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.api.IdentityProviderId -import com.digitalasset.canton.ledger.localstore.api.IdentityProviderConfigUpdate -import com.digitalasset.daml.lf.data.Ref.LedgerString -import com.google.protobuf.field_mask.FieldMask -import org.scalatest.EitherValues -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class IdentityProviderConfigUpdateMapperSpec extends AnyFreeSpec with Matchers with EitherValues { - - private val id1: IdentityProviderId.Id = - IdentityProviderId.Id(LedgerString.assertFromString("idp1")) - - def makeConfigUpdate( - identityProviderId: IdentityProviderId.Id = id1, - isDeactivatedUpdate: Option[Boolean] = None, - jwksUrlUpdate: Option[JwksUrl] = None, - issuerUpdate: Option[String] = None, - audienceUpdate: Option[Option[String]] = None, - ): IdentityProviderConfigUpdate = IdentityProviderConfigUpdate( - identityProviderId = identityProviderId, - isDeactivatedUpdate = isDeactivatedUpdate, - jwksUrlUpdate = jwksUrlUpdate, - issuerUpdate = issuerUpdate, - audienceUpdate = audienceUpdate, - ) - - val emptyConfigUpdate: IdentityProviderConfigUpdate = makeConfigUpdate() - - val config = makeConfigUpdate( - isDeactivatedUpdate = Some(false), - jwksUrlUpdate = Some(JwksUrl("http://url.com/")), - issuerUpdate = Some("issuer"), - audienceUpdate = Some(Some("audience")), - ) - - "map to idp updates" - { - "basic mapping" - { - "with all individual fields to update listed in the update mask" in { - IdentityProviderConfigUpdateMapper - .toUpdate( - config, - FieldMask( - Seq("is_deactivated", "issuer", "jwks_url", "audience") - ), - ) - .value shouldBe makeConfigUpdate( - isDeactivatedUpdate = Some(false), - jwksUrlUpdate = Some(JwksUrl("http://url.com/")), - issuerUpdate = Some("issuer"), - audienceUpdate = Some(Some("audience")), - ) - } - - "with audience" in { - IdentityProviderConfigUpdateMapper - .toUpdate(config, FieldMask(Seq("audience"))) - .value shouldBe makeConfigUpdate(audienceUpdate = Some(Some("audience"))) - - IdentityProviderConfigUpdateMapper - .toUpdate(config.copy(audienceUpdate = None), FieldMask(Seq("audience"))) - .value shouldBe makeConfigUpdate(audienceUpdate = Some(None)) - - IdentityProviderConfigUpdateMapper - .toUpdate(config.copy(audienceUpdate = Some(Some(""))), FieldMask(Seq("audience"))) - .value shouldBe makeConfigUpdate(audienceUpdate = Some(Some(""))) - - IdentityProviderConfigUpdateMapper - .toUpdate(config.copy(audienceUpdate = Some(None)), FieldMask(Seq("audience"))) - .value shouldBe makeConfigUpdate(audienceUpdate = Some(None)) - } - - "with is_deactivated" in { - IdentityProviderConfigUpdateMapper - .toUpdate( - config, - FieldMask( - Seq("is_deactivated") - ), - ) - .value shouldBe makeConfigUpdate( - isDeactivatedUpdate = Some(false) - ) - } - - "with issuer" in { - IdentityProviderConfigUpdateMapper - .toUpdate( - config, - FieldMask( - Seq("issuer") - ), - ) - .value shouldBe makeConfigUpdate( - issuerUpdate = Some("issuer") - ) - } - - "with jwks_url" in { - IdentityProviderConfigUpdateMapper - .toUpdate( - config, - FieldMask( - Seq("jwks_url") - ), - ) - .value shouldBe makeConfigUpdate( - jwksUrlUpdate = Some(JwksUrl("http://url.com/")) - ) - } - } - } - - "produce an error when " - { - "field masks lists unknown field" in { - IdentityProviderConfigUpdateMapper - .toUpdate(config, FieldMask(Seq("some_unknown_field"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field") - - IdentityProviderConfigUpdateMapper - .toUpdate(config, FieldMask(Seq("some_unknown_field", "jwks_url"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field") - - IdentityProviderConfigUpdateMapper - .toUpdate(config, FieldMask(Seq("some_unknown_field", "issuer"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field") - } - "specifying identity_provider_id in the update mask" in { - IdentityProviderConfigUpdateMapper - .toUpdate( - config, - FieldMask( - Seq("identity_provider_id") - ), - ) - .value shouldBe emptyConfigUpdate - } - "empty field mask" in { - IdentityProviderConfigUpdateMapper - .toUpdate( - config, - FieldMask(Seq.empty), - ) - .left - .value shouldBe UpdatePathError.EmptyUpdateMask - } - "update path with invalid field path syntax" in { - IdentityProviderConfigUpdateMapper - .toUpdate(config, FieldMask(Seq(".issuer"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath(".issuer") - IdentityProviderConfigUpdateMapper - .toUpdate(config, FieldMask(Seq(".identity_provider_config.issuer"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath(".identity_provider_config.issuer") - } - "multiple update paths with the same field path" in { - IdentityProviderConfigUpdateMapper - .toUpdate(config, FieldMask(Seq("issuer", "issuer"))) - .left - .value shouldBe UpdatePathError.DuplicatedFieldPath("issuer") - IdentityProviderConfigUpdateMapper - .toUpdate(config, FieldMask(Seq("jwks_url", "jwks_url", "issuer"))) - .left - .value shouldBe UpdatePathError.DuplicatedFieldPath("jwks_url") - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/PartyRecordUpdateMapperSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/PartyRecordUpdateMapperSpec.scala deleted file mode 100644 index 410b373be6..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/PartyRecordUpdateMapperSpec.scala +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta, PartyDetails} -import com.digitalasset.canton.ledger.localstore.api.{ObjectMetaUpdate, PartyDetailsUpdate} -import com.digitalasset.daml.lf.data.Ref -import com.google.protobuf.field_mask.FieldMask -import org.scalatest.EitherValues -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class PartyRecordUpdateMapperSpec extends AnyFreeSpec with Matchers with EitherValues { - - private val party1 = Ref.Party.assertFromString("party") - - def makePartyDetails( - party: Ref.Party = party1, - isLocal: Boolean = false, - annotations: Map[String, String] = Map.empty, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - ): PartyDetails = PartyDetails( - party = party, - isLocal = isLocal, - metadata = ObjectMeta( - resourceVersionO = None, - annotations = annotations, - ), - identityProviderId = identityProviderId, - ) - - def makePartyDetailsUpdate( - party: Ref.Party = party1, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - isLocalUpdate: Option[Boolean] = None, - annotationsUpdateO: Option[Map[String, String]] = None, - ): PartyDetailsUpdate = PartyDetailsUpdate( - party = party, - identityProviderId = identityProviderId, - isLocalUpdate = isLocalUpdate, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = annotationsUpdateO, - ), - ) - - val emptyUpdate: PartyDetailsUpdate = makePartyDetailsUpdate() - - private val testedMapper = PartyRecordUpdateMapper - - "map to party details updates" - { - "for annotations" in { - val newResourceSet = makePartyDetails(annotations = Map("a" -> "b")) - val newResourceUnset = makePartyDetails(annotations = Map.empty) - testedMapper - .toUpdate(newResourceSet, FieldMask(Seq("local_metadata.annotations"))) - .value shouldBe makePartyDetailsUpdate(annotationsUpdateO = Some(Map("a" -> "b"))) - testedMapper - .toUpdate(newResourceSet, FieldMask(Seq("local_metadata"))) - .value shouldBe makePartyDetailsUpdate(annotationsUpdateO = Some(Map("a" -> "b"))) - testedMapper - .toUpdate(newResourceUnset, FieldMask(Seq("local_metadata.annotations"))) - .value shouldBe makePartyDetailsUpdate(annotationsUpdateO = Some(Map.empty)) - testedMapper - .toUpdate(newResourceUnset, FieldMask(Seq("local_metadata"))) - .value shouldBe makePartyDetailsUpdate(annotationsUpdateO = None) - } - "for is_local" in { - val newResourceSet = makePartyDetails(isLocal = true) - val newResourceUnset = makePartyDetails(isLocal = false) - testedMapper - .toUpdate(newResourceSet, FieldMask(Seq("is_local"))) - .value shouldBe makePartyDetailsUpdate(isLocalUpdate = Some(true)) - testedMapper - .toUpdate(newResourceUnset, FieldMask(Seq("is_local"))) - .value shouldBe makePartyDetailsUpdate(isLocalUpdate = Some(false)) - } - "when exact path match on the metadata annotations field" in { - val prWithAnnotations = makePartyDetails(annotations = Map("a" -> "b")) - val prWithoutAnnotations = makePartyDetails() - testedMapper - .toUpdate(prWithAnnotations, FieldMask(Seq("local_metadata.annotations"))) - .value shouldBe makePartyDetailsUpdate(annotationsUpdateO = Some(Map("a" -> "b"))) - testedMapper - .toUpdate( - prWithoutAnnotations, - FieldMask(Seq("local_metadata.annotations")), - ) - .value shouldBe makePartyDetailsUpdate(annotationsUpdateO = Some(Map.empty)) - } - "when inexact path match on metadata annotations field" in { - val prWithAnnotations = makePartyDetails(annotations = Map("a" -> "b")) - testedMapper - .toUpdate(prWithAnnotations, FieldMask(Seq("local_metadata"))) - .value shouldBe makePartyDetailsUpdate( - annotationsUpdateO = Some(Map("a" -> "b")) - ) - } - - "the longest matching path is matched" in { - val pr = makePartyDetails( - annotations = Map("a" -> "b") - ) - testedMapper - .toUpdate( - pr, - FieldMask( - Seq( - "local_metadata", - "local_metadata.annotations", - ) - ), - ) - .value shouldBe makePartyDetailsUpdate( - annotationsUpdateO = Some(Map("a" -> "b")) - ) - testedMapper - .toUpdate( - pr, - FieldMask( - Seq( - "local_metadata", - "local_metadata.annotations", - ) - ), - ) - .value shouldBe makePartyDetailsUpdate( - annotationsUpdateO = Some(Map("a" -> "b")) - ) - testedMapper - .toUpdate( - pr, - FieldMask( - Seq( - "local_metadata", - "local_metadata.annotations", - ) - ), - ) - .value shouldBe makePartyDetailsUpdate( - annotationsUpdateO = Some(Map("a" -> "b")) - ) - } - - } - - "produce an error when " - { - val pd = makePartyDetails(annotations = Map("a" -> "b")) - - "field masks lists unknown field" in { - testedMapper - .toUpdate(pd, FieldMask(Seq("some_unknown_field"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field") - testedMapper - .toUpdate(pd, FieldMask(Seq("local_metadata", "some_unknown_field"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field") - } - "specifying resource version in the update mask" in { - testedMapper - .toUpdate(pd, FieldMask(Seq("local_metadata.resource_version"))) - .value shouldBe emptyUpdate - } - "specifying party in the update mask" in { - testedMapper - .toUpdate(pd, FieldMask(Seq("party"))) - .value shouldBe emptyUpdate - } - "empty field mask" in { - testedMapper - .toUpdate(pd, FieldMask(Seq.empty)) - .left - .value shouldBe UpdatePathError.EmptyUpdateMask - } - "update path with invalid field path syntax" in { - testedMapper - .toUpdate(pd, FieldMask(Seq("..local_metadata"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath( - "..local_metadata" - ) - testedMapper - .toUpdate(pd, FieldMask(Seq(".local_metadata"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath( - ".local_metadata" - ) - } - "multiple update paths with the same field path" in { - testedMapper - .toUpdate( - pd, - FieldMask( - Seq( - "local_metadata.annotations", - "local_metadata.annotations", - ) - ), - ) - .left - .value shouldBe UpdatePathError.DuplicatedFieldPath( - "local_metadata.annotations" - ) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathSpec.scala deleted file mode 100644 index 2099751e14..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathSpec.scala +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import org.scalatest.EitherValues -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class UpdatePathSpec extends AnyFreeSpec with Matchers with EitherValues { - - "parse valid update paths" in { - UpdatePath - .parseAll( - Seq( - "foo.bar", - "foo", - "foo.bar", - "foo!bar", - "..", - ) - ) - .value shouldBe Seq( - UpdatePath(List("foo", "bar")), - UpdatePath(List("foo")), - UpdatePath(List("foo", "bar")), - UpdatePath(List("foo!bar")), - UpdatePath(List()), - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathsTrieSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathsTrieSpec.scala deleted file mode 100644 index c44249b122..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UpdatePathsTrieSpec.scala +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.digitalasset.base.error.ErrorsAssertions -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{EitherValues, OptionValues} - -class UpdatePathsTrieSpec - extends AnyFreeSpec - with Matchers - with EitherValues - with OptionValues - with ErrorsAssertions { - - "finding update paths in" - { - val allPaths = UpdatePath - .parseAll( - Seq( - "a1.a2.c3", - "a1.b2.c3", - "a1.b2.d3.a4", - "a1.b2.d3.b4", - "b1", - ) - ) - .value - val tree = UpdatePathsTrie.fromPaths(allPaths).value - - "proper subtrees" in { - tree.pathExists(List("a1")) shouldBe false - tree.pathExists(List.empty) shouldBe false - tree.pathExists(List("a1", "b2")) shouldBe false - } - "non-existing subtrees" in { - tree.pathExists(List("a1", "b2", "dummy")) shouldBe false - tree.pathExists(List("dummy")) shouldBe false - tree.pathExists(List("")) shouldBe false - } - "existing but empty subtrees" in { - tree.pathExists(List("b1")) shouldBe true - tree.pathExists(List("a1", "b2", "c3")) shouldBe true - tree.pathExists(List("a1", "b2", "d3", "b4")) shouldBe true - } - } - - "constructing a trie" - { - "from one path with one segment" in { - UpdatePathsTrie - .fromPaths( - UpdatePath.parseAll(Seq("foo")).value - ) - .value shouldBe UpdatePathsTrie( - exists = false, - "foo" -> UpdatePathsTrie(exists = true), - ) - } - "from one path with multiple segments" in { - UpdatePathsTrie - .fromPaths( - UpdatePath.parseAll(Seq("foo.bar.baz")).value - ) - .value shouldBe UpdatePathsTrie( - exists = false, - "foo" -> UpdatePathsTrie( - exists = false, - "bar" -> UpdatePathsTrie( - exists = false, - "baz" -> UpdatePathsTrie( - exists = true - ), - ), - ), - ) - } - "from three paths with multiple segments and with update modifiers" in { - val t = UpdatePathsTrie - .fromPaths( - UpdatePath - .parseAll( - Seq( - "foo.bar.baz", - "foo.bar", - "foo.alice", - "bob.eve", - "bob", - ) - ) - .value - ) - .value - t shouldBe UpdatePathsTrie( - exists = false, - "foo" -> UpdatePathsTrie( - exists = false, - "bar" -> UpdatePathsTrie( - exists = true, - "baz" -> UpdatePathsTrie(exists = true), - ), - "alice" -> UpdatePathsTrie(exists = true), - ), - "bob" -> UpdatePathsTrie( - exists = true, - "eve" -> UpdatePathsTrie(exists = true), - ), - ) - } - - } - - "checking for presence of a prefix" in { - val t = UpdatePathsTrie - .fromPaths( - UpdatePath - .parseAll( - Seq( - "foo.bar.baz", - "foo.bax", - ) - ) - .value - ) - .value - t.containsPrefix(List("foo")) shouldBe true - t.containsPrefix(List("foo", "bar")) shouldBe true - t.containsPrefix(List("foo", "bax")) shouldBe true - t.containsPrefix(List("foo", "bar", "baz")) shouldBe true - t.containsPrefix(List("foo", "bar", "bad")) shouldBe false - t.containsPrefix(List("")) shouldBe false - t.containsPrefix(List.empty) shouldBe true - - } - - "fail to build a trie when duplicated field paths" in { - UpdatePathsTrie - .fromPaths( - UpdatePath - .parseAll( - Seq( - "foo.bar", - "foo.bar", - ) - ) - .value - ) - .left - .value shouldBe UpdatePathError.DuplicatedFieldPath("foo.bar") - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UserUpdateMapperSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UserUpdateMapperSpec.scala deleted file mode 100644 index f0d3edf067..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/update/UserUpdateMapperSpec.scala +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.update - -import com.digitalasset.canton.ledger.api.{IdentityProviderId, ObjectMeta, User} -import com.digitalasset.canton.ledger.localstore.api.{ObjectMetaUpdate, UserUpdate} -import com.digitalasset.daml.lf.data.Ref -import com.google.protobuf.field_mask.FieldMask -import org.scalatest.EitherValues -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class UserUpdateMapperSpec extends AnyFreeSpec with Matchers with EitherValues { - - private val userId1: Ref.UserId = Ref.UserId.assertFromString("u1") - private val party1 = Ref.Party.assertFromString("party") - - def makeUser( - id: Ref.UserId = userId1, - primaryParty: Option[Ref.Party] = None, - isDeactivated: Boolean = false, - annotations: Map[String, String] = Map.empty, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - ): User = User( - id = id, - primaryParty = primaryParty, - isDeactivated = isDeactivated, - metadata = ObjectMeta( - resourceVersionO = None, - annotations = annotations, - ), - identityProviderId = identityProviderId, - ) - - def makeUserUpdate( - id: Ref.UserId = userId1, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - primaryPartyUpdateO: Option[Option[Ref.Party]] = None, - isDeactivatedUpdateO: Option[Boolean] = None, - annotationsUpdateO: Option[Map[String, String]] = None, - ): UserUpdate = UserUpdate( - id = id, - primaryPartyUpdateO = primaryPartyUpdateO, - isDeactivatedUpdateO = isDeactivatedUpdateO, - metadataUpdate = ObjectMetaUpdate( - resourceVersionO = None, - annotationsUpdateO = annotationsUpdateO, - ), - identityProviderId = identityProviderId, - ) - - val emptyUserUpdate: UserUpdate = makeUserUpdate() - - "map to user updates" - { - "basic mapping" - { - val user = makeUser( - primaryParty = None, - isDeactivated = false, - annotations = Map("a" -> "b"), - identityProviderId = IdentityProviderId("abc123"), - ) - val expected = makeUserUpdate( - primaryPartyUpdateO = Some(None), - isDeactivatedUpdateO = Some(false), - annotationsUpdateO = Some(Map("a" -> "b")), - identityProviderId = IdentityProviderId("abc123"), - ) - "1) with all individual fields to update listed in the update mask" in { - UserUpdateMapper - .toUpdate( - user, - FieldMask( - Seq("is_deactivated", "primary_party", "metadata.annotations") - ), - ) - .value shouldBe expected - } - "2) with metadata.annotations not listed explicitly" in { - UserUpdateMapper - .toUpdate( - user, - FieldMask(Seq("is_deactivated", "primary_party", "metadata")), - ) - .value shouldBe expected - } - } - - "map api request to update - merge user and reset is_deactivated" - { - val user = makeUser( - // non-default value - primaryParty = Some(party1), - // default value - isDeactivated = false, - // non-default value - annotations = Map("a" -> "b"), - ) - val expected = makeUserUpdate( - primaryPartyUpdateO = Some(Some(party1)), - isDeactivatedUpdateO = Some(false), - annotationsUpdateO = Some(Map("a" -> "b")), - ) - "field mask with non-exact field paths" in { - UserUpdateMapper - .toUpdate(user, FieldMask(Seq("primary_party", "is_deactivated", "metadata"))) - .value shouldBe expected - } - "field mask with exact field paths" in { - UserUpdateMapper - .toUpdate(user, FieldMask(Seq("primary_party", "is_deactivated", "metadata.annotations"))) - .value shouldBe expected - } - } - - "when exact path match on a primitive field" in { - val userWithParty = makeUser(primaryParty = Some(party1)) - val userWithoutParty = makeUser() - UserUpdateMapper - .toUpdate(userWithParty, FieldMask(Seq("primary_party"))) - .value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(Some(party1))) - UserUpdateMapper - .toUpdate(userWithoutParty, FieldMask(Seq("primary_party"))) - .value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(None)) - } - - "when exact path match on the metadata annotations field" in { - val userWithAnnotations = makeUser(annotations = Map("a" -> "b")) - val userWithoutAnnotations = makeUser() - UserUpdateMapper - .toUpdate(userWithAnnotations, FieldMask(Seq("metadata.annotations"))) - .value shouldBe makeUserUpdate(annotationsUpdateO = Some(Map("a" -> "b"))) - UserUpdateMapper - .toUpdate(userWithoutAnnotations, FieldMask(Seq("metadata.annotations"))) - .value shouldBe makeUserUpdate(annotationsUpdateO = Some(Map.empty)) - } - - "when inexact path match on metadata annotations field" in { - val userWithAnnotations = makeUser(annotations = Map("a" -> "b")) - val userWithoutAnnotations = makeUser() - UserUpdateMapper - .toUpdate(userWithAnnotations, FieldMask(Seq("metadata"))) - .value shouldBe makeUserUpdate( - annotationsUpdateO = Some(Map("a" -> "b")) - ) - UserUpdateMapper - .toUpdate(userWithoutAnnotations, FieldMask(Seq("metadata"))) - .value shouldBe emptyUserUpdate - } - - } - - "produce an error when " - { - val user = makeUser(primaryParty = Some(party1)) - - "field masks lists unknown field" in { - UserUpdateMapper - .toUpdate(user, FieldMask(Seq("some_unknown_field"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field") - UserUpdateMapper - .toUpdate(user, FieldMask(Seq("metadata", "some_unknown_field"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field") - UserUpdateMapper - .toUpdate(user, FieldMask(Seq("primary_party", "some_unknown_field"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field") - } - "specifying resource version in the update mask" in { - UserUpdateMapper - .toUpdate(user, FieldMask(Seq("metadata.resource_version"))) - .value shouldBe emptyUserUpdate - } - "specifying id in the update mask" in { - UserUpdateMapper - .toUpdate(user, FieldMask(Seq("id"))) - .value shouldBe emptyUserUpdate - } - "empty field mask" in { - UserUpdateMapper - .toUpdate(user, FieldMask(Seq.empty)) - .left - .value shouldBe UpdatePathError.EmptyUpdateMask - } - "update path with invalid field path syntax" in { - UserUpdateMapper - .toUpdate(user, FieldMask(Seq(".primary_party"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath(".primary_party") - UserUpdateMapper - .toUpdate(user, FieldMask(Seq(".user.primary_party"))) - .left - .value shouldBe UpdatePathError.UnknownFieldPath(".user.primary_party") - } - "multiple update paths with the same field path" in { - UserUpdateMapper - .toUpdate( - user, - FieldMask(Seq("primary_party", "primary_party")), - ) - .left - .value shouldBe UpdatePathError.DuplicatedFieldPath("primary_party") - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/validation/ErrorFactoriesSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/validation/ErrorFactoriesSpec.scala deleted file mode 100644 index 3fac428150..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/validation/ErrorFactoriesSpec.scala +++ /dev/null @@ -1,736 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.apiserver.validation - -import com.digitalasset.base.error.utils.ErrorDetails -import com.digitalasset.base.error.{BaseError, ErrorCode, ErrorsAssertions, RpcError} -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.auth.AuthorizationChecksErrors -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.InvalidDeduplicationPeriodField.ValidMaxDeduplicationFieldKey -import com.digitalasset.canton.ledger.error.groups.{ - AdminServiceErrors, - ConsistencyErrors, - RequestValidationErrors, -} -import com.digitalasset.canton.ledger.error.{CommonErrors, IndexErrors, LedgerApiErrors} -import com.digitalasset.canton.logging.{ErrorLoggingContext, SuppressionRule} -import com.google.rpc.* -import io.grpc.Status.Code -import io.grpc.StatusRuntimeException -import org.scalatest.concurrent.{Eventually, IntegrationPatience} -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.wordspec.AnyWordSpec -import org.slf4j.event.Level -import org.slf4j.event.Level.{ERROR, INFO} - -import java.sql.{SQLNonTransientException, SQLTransientException} -import java.time.Duration -import java.util.concurrent.TimeUnit -import scala.concurrent.duration -import scala.concurrent.duration.* - -class ErrorFactoriesSpec - extends AnyWordSpec - with TableDrivenPropertyChecks - with ErrorsAssertions - with Eventually - with IntegrationPatience - with BaseTest { - - private val originalCorrelationId = "cor-id-12345679" - private val truncatedCorrelationId = "cor-id-1" - - implicit val errorLoggingContext: ErrorLoggingContext = - ErrorLoggingContext.withExplicitCorrelationId( - logger, - loggerFactory.properties, - traceContext, - originalCorrelationId, - ) - - private val expectedCorrelationIdRequestInfo = - ErrorDetails.RequestInfoDetail(originalCorrelationId) - private val expectedLocationRegex = - """\{location=ErrorFactoriesSpec.scala:\d+\}""" - private val expectedInternalErrorMessage = - BaseError.RedactedMessage(Some(originalCorrelationId)) - private val expectedInternalErrorDetails = - Seq[ErrorDetails.ErrorDetail](expectedCorrelationIdRequestInfo) - - "Errors " should { - - "return sqlTransientException" in { - val failureReason = "some db transient failure" - val someSqlTransientException = new SQLTransientException(failureReason) - val msg = - s"INDEX_DB_SQL_TRANSIENT_ERROR(1,$truncatedCorrelationId): Processing the request failed due to a transient database error: $failureReason" - assertError( - IndexErrors.DatabaseErrors.SqlTransientError - .Reject(someSqlTransientException)(errorLoggingContext) - )( - code = Code.UNAVAILABLE, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - expectedCorrelationIdRequestInfo, - ErrorDetails.RetryInfoDetail(1.second), - ErrorDetails.ErrorInfoDetail( - "INDEX_DB_SQL_TRANSIENT_ERROR", - Map( - "category" -> "1", - "definite_answer" -> "false", - "test" -> getClass.getSimpleName, - ), - ), - ), - logLevel = INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return sqlNonTransientException" in { - val failureReason = "some db non-transient failure" - val msg = - s"INDEX_DB_SQL_NON_TRANSIENT_ERROR(4,$truncatedCorrelationId): Processing the request failed due to a non-transient database error: $failureReason" - assertError( - IndexErrors.DatabaseErrors.SqlNonTransientError - .Reject( - new SQLNonTransientException(failureReason) - )(errorLoggingContext) - )( - code = Code.INTERNAL, - message = expectedInternalErrorMessage, - details = expectedInternalErrorDetails, - logLevel = ERROR, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "TrackerErrors" should { - "return failedToEnqueueCommandSubmission" in { - val t = new Exception("message123") - assertStatus( - LedgerApiErrors.InternalError - .Generic("some message", Some(t))( - errorLoggingContext - ) - .asGrpcStatus - )( - code = Code.INTERNAL, - message = expectedInternalErrorMessage, - details = expectedInternalErrorDetails, - logLevel = Level.ERROR, - logMessage = s"LEDGER_API_INTERNAL_ERROR(4,$truncatedCorrelationId): some message", - logErrorContextRegEx = - expectedErrContextRegex("""throwableO=Some\(java.lang.Exception: message123\)"""), - ) - } - } - - "return bufferFul" in { - val msg = - s"PARTICIPANT_BACKPRESSURE(2,$truncatedCorrelationId): The participant is overloaded: Some buffer is full" - assertStatus( - LedgerApiErrors.ParticipantBackpressure - .Rejection("Some buffer is full")(errorLoggingContext) - .asGrpcStatus - )( - code = Code.ABORTED, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "PARTICIPANT_BACKPRESSURE", - Map( - "category" -> "2", - "definite_answer" -> "false", - "reason" -> "Some buffer is full", - "test" -> getClass.getSimpleName, - ), - ), - expectedCorrelationIdRequestInfo, - ErrorDetails.RetryInfoDetail(1.second), - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedErrContextRegex("reason=Some buffer is full"), - ) - } - - "return queueClosed" in { - val msg = - s"SERVICE_NOT_RUNNING(1,$truncatedCorrelationId): Some service is not running." - assertStatus( - CommonErrors.ServiceNotRunning - .Reject("Some service")( - errorLoggingContext - ) - .asGrpcStatus - )( - code = Code.UNAVAILABLE, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "SERVICE_NOT_RUNNING", - Map( - "category" -> "1", - "definite_answer" -> "false", - "service_name" -> "Some service", - "test" -> getClass.getSimpleName, - ), - ), - expectedCorrelationIdRequestInfo, - ErrorDetails.RetryInfoDetail(1.second), - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedErrContextRegex("service_name=Some service"), - ) - } - - "return timeout" in { - val msg = - s"REQUEST_TIME_OUT(3,$truncatedCorrelationId): Timed out while awaiting for a completion corresponding to a command submission." - assertStatus( - CommonErrors.RequestTimeOut - .Reject( - "Timed out while awaiting for a completion corresponding to a command submission.", - definiteAnswer = false, - )( - errorLoggingContext - ) - .asGrpcStatus - )( - code = Code.DEADLINE_EXCEEDED, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "REQUEST_TIME_OUT", - Map("category" -> "3", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ErrorDetails.RetryInfoDetail(1.second), - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return noStatusInResponse" in { - assertStatus( - LedgerApiErrors.InternalError - .Generic( - "Missing status in completion response.", - throwableO = None, - )(errorLoggingContext) - .asGrpcStatus - )( - code = Code.INTERNAL, - message = expectedInternalErrorMessage, - details = expectedInternalErrorDetails, - logLevel = Level.ERROR, - logMessage = - s"LEDGER_API_INTERNAL_ERROR(4,$truncatedCorrelationId): Missing status in completion response.", - logErrorContextRegEx = expectedErrContextRegex("throwableO=None"), - ) - - } - - "return packageNotFound" in { - val msg = s"PACKAGE_NOT_FOUND(11,$truncatedCorrelationId): Could not find package." - assertError( - RequestValidationErrors.NotFound.Package - .Reject("packageId123")(errorLoggingContext) - )( - code = Code.NOT_FOUND, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "PACKAGE_NOT_FOUND", - Map("category" -> "11", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ErrorDetails.ResourceInfoDetail(typ = "PACKAGE", name = "packageId123"), - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return the a versioned service internal error" in { - assertError( - LedgerApiErrors.InternalError.VersionService("message123")(errorLoggingContext) - )( - code = Code.INTERNAL, - message = expectedInternalErrorMessage, - details = expectedInternalErrorDetails, - logLevel = Level.ERROR, - logMessage = s"LEDGER_API_INTERNAL_ERROR(4,$truncatedCorrelationId): message123", - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return the configurationEntryRejected" in { - val msg = s"CONFIGURATION_ENTRY_REJECTED(9,$truncatedCorrelationId): message123" - assertError( - AdminServiceErrors.ConfigurationEntryRejected.Reject("message123")( - errorLoggingContext - ) - )( - code = Code.FAILED_PRECONDITION, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "CONFIGURATION_ENTRY_REJECTED", - Map("category" -> "9", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return an updateNotFound error" in { - val msg = - s"UPDATE_NOT_FOUND(11,$truncatedCorrelationId): Update not found, or not visible." - assertError( - RequestValidationErrors.NotFound.Update - .RejectWithTxId("uId")(errorLoggingContext) - )( - code = Code.NOT_FOUND, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "UPDATE_NOT_FOUND", - Map("category" -> "11", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ErrorDetails.ResourceInfoDetail(typ = "UPDATE_ID", name = "uId"), - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return the DuplicateCommandException" in { - val msg = - s"DUPLICATE_COMMAND(10,$truncatedCorrelationId): A command with the given command id has already been successfully processed" - assertError( - ConsistencyErrors.DuplicateCommand - .Reject(existingCommandSubmissionId = None)(errorLoggingContext) - )( - code = Code.ALREADY_EXISTS, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "DUPLICATE_COMMAND", - Map("category" -> "10", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return a permissionDenied error" in { - assertError( - AuthorizationChecksErrors.PermissionDenied.Reject("some cause")( - errorLoggingContext - ) - )( - code = Code.PERMISSION_DENIED, - message = expectedInternalErrorMessage, - details = expectedInternalErrorDetails, - logLevel = Level.WARN, - logMessage = s"PERMISSION_DENIED(7,$truncatedCorrelationId): some cause", - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return a isTimeoutUnknown_wasAborted error" in { - val msg = s"REQUEST_TIME_OUT(3,$truncatedCorrelationId): message123" - assertError( - CommonErrors.RequestTimeOut - .Reject("message123", definiteAnswer = false)(errorLoggingContext) - )( - code = Code.DEADLINE_EXCEEDED, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "REQUEST_TIME_OUT", - Map("category" -> "3", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ErrorDetails.RetryInfoDetail(1.second), - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return a nonPositiveOffset error" in { - val msg = - s"NON_POSITIVE_OFFSET(8,$truncatedCorrelationId): Offset -123 in fieldName123 is not a positive integer: message123" - assertError( - RequestValidationErrors.NonPositiveOffset - .Error( - fieldName = "fieldName123", - offsetValue = -123L, - message = "message123", - )(errorLoggingContext) - .asGrpcError - )( - code = Code.INVALID_ARGUMENT, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "NON_POSITIVE_OFFSET", - Map("category" -> "8", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return an offsetAfterLedgerEnd error" in { - val expectedMessage = s"Absolute offset (12345678) is after ledger end (42)" - val msg = s"OFFSET_AFTER_LEDGER_END(12,$truncatedCorrelationId): $expectedMessage" - assertError( - RequestValidationErrors.OffsetAfterLedgerEnd - .Reject("Absolute", 12345678L, 42L)(errorLoggingContext) - )( - code = Code.OUT_OF_RANGE, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "OFFSET_AFTER_LEDGER_END", - Map("category" -> "12", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - ErrorDetails.RetryInfoDetail(duration.Duration(1, TimeUnit.SECONDS)), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return a offsetOutOfRange error" in { - val msg = s"OFFSET_OUT_OF_RANGE(9,$truncatedCorrelationId): message123" - assertError( - RequestValidationErrors.OffsetOutOfRange - .Reject("message123")(errorLoggingContext) - )( - code = Code.FAILED_PRECONDITION, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "OFFSET_OUT_OF_RANGE", - Map("category" -> "9", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return an unauthenticatedMissingJwtToken error" in { - assertError( - AuthorizationChecksErrors.Unauthenticated - .MissingJwtToken()(errorLoggingContext) - )( - code = Code.UNAUTHENTICATED, - message = expectedInternalErrorMessage, - details = expectedInternalErrorDetails, - Level.WARN, - s"UNAUTHENTICATED(6,$truncatedCorrelationId): The command is missing a (valid) JWT token", - expectedLocationRegex, - ) - } - - "return an internalAuthenticationError" in { - val someSecuritySafeMessage = "nothing security sensitive in here" - val someThrowable = new RuntimeException("some internal authentication error") - assertError( - AuthorizationChecksErrors.InternalAuthorizationError - .Reject(someSecuritySafeMessage, someThrowable)(errorLoggingContext) - )( - code = Code.INTERNAL, - message = expectedInternalErrorMessage, - details = expectedInternalErrorDetails, - logLevel = Level.ERROR, - logMessage = - s"INTERNAL_AUTHORIZATION_ERROR(4,$truncatedCorrelationId): nothing security sensitive in here", - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return an invalid deduplication period error" in { - val errorDetailMessage = "message" - val maxDeduplicationDuration = Duration.ofSeconds(5) - val msg = - s"INVALID_DEDUPLICATION_PERIOD(9,$truncatedCorrelationId): The submitted command had an invalid deduplication period: $errorDetailMessage" - assertError( - RequestValidationErrors.InvalidDeduplicationPeriodField - .Reject( - reason = errorDetailMessage, - maxDeduplicationDuration = Some(maxDeduplicationDuration), - )(errorLoggingContext) - .asGrpcError - )( - code = Code.FAILED_PRECONDITION, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "INVALID_DEDUPLICATION_PERIOD", - Map( - "category" -> "9", - "definite_answer" -> "false", - ValidMaxDeduplicationFieldKey -> maxDeduplicationDuration.toString, - "test" -> getClass.getSimpleName, - ), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedErrContextRegex("longest_duration=PT5S"), - ) - } - - "return an invalidField error" in { - val fieldName = "my field" - val msg = - s"INVALID_FIELD(8,$truncatedCorrelationId): The submitted command has a field with invalid value: Invalid field $fieldName: my message" - assertError( - RequestValidationErrors.InvalidField - .Reject(fieldName, "my message")(errorLoggingContext) - )( - code = Code.INVALID_ARGUMENT, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "INVALID_FIELD", - Map("category" -> "8", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - "return a participantPrunedDataAccessed error" in { - val msg = s"PARTICIPANT_PRUNED_DATA_ACCESSED(9,$truncatedCorrelationId): my message" - assertError( - RequestValidationErrors.ParticipantPrunedDataAccessed - .Reject( - "my message", - 0L, - )(errorLoggingContext) - )( - code = Code.FAILED_PRECONDITION, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "PARTICIPANT_PRUNED_DATA_ACCESSED", - Map( - "category" -> "9", - "definite_answer" -> "false", - LedgerApiErrors.EarliestOffsetMetadataKey -> "0", - "test" -> getClass.getSimpleName, - ), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = - expectedErrContextRegex(s"${LedgerApiErrors.EarliestOffsetMetadataKey}=0"), - ) - } - - "return a trackerFailure error" in { - assertError(LedgerApiErrors.InternalError.Generic("message123")(errorLoggingContext))( - code = Code.INTERNAL, - message = expectedInternalErrorMessage, - details = expectedInternalErrorDetails, - logLevel = Level.ERROR, - logMessage = s"LEDGER_API_INTERNAL_ERROR(4,$truncatedCorrelationId): message123", - logErrorContextRegEx = expectedErrContextRegex("throwableO=None"), - ) - } - - "return a serviceNotRunning error" in { - val serviceName = "Some API Service" - - val msg = - s"SERVICE_NOT_RUNNING(1,$truncatedCorrelationId): $serviceName is not running." - assertError(CommonErrors.ServiceNotRunning.Reject(serviceName)(errorLoggingContext))( - code = Code.UNAVAILABLE, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "SERVICE_NOT_RUNNING", - Map( - "category" -> "1", - "definite_answer" -> "false", - "service_name" -> serviceName, - "test" -> getClass.getSimpleName, - ), - ), - expectedCorrelationIdRequestInfo, - ErrorDetails.RetryInfoDetail(1.second), - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedErrContextRegex("service_name=Some API Service"), - ) - } - - "return a missingField error" in { - val fieldName = "my field" - - val msg = - s"MISSING_FIELD(8,$truncatedCorrelationId): The submitted command is missing a mandatory field: $fieldName" - assertError( - RequestValidationErrors.MissingField - .Reject(fieldName)(errorLoggingContext) - )( - code = Code.INVALID_ARGUMENT, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "MISSING_FIELD", - Map( - "category" -> "8", - "definite_answer" -> "false", - "field_name" -> fieldName, - "test" -> getClass.getSimpleName, - ), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedErrContextRegex("field_name=my field"), - ) - } - - val msg = - s"INVALID_ARGUMENT(8,$truncatedCorrelationId): The submitted request has invalid arguments: my message" - "return an invalidArgument error" in { - assertError( - RequestValidationErrors.InvalidArgument - .Reject("my message")(errorLoggingContext) - )( - code = Code.INVALID_ARGUMENT, - message = msg, - details = Seq[ErrorDetails.ErrorDetail]( - ErrorDetails.ErrorInfoDetail( - "INVALID_ARGUMENT", - Map("category" -> "8", "definite_answer" -> "false", "test" -> getClass.getSimpleName), - ), - expectedCorrelationIdRequestInfo, - ), - logLevel = Level.INFO, - logMessage = msg, - logErrorContextRegEx = expectedLocationRegex, - ) - } - - } - - private def expectedErrContextRegex(extraInner: String): String = { - val locationRegex = "location=ErrorFactoriesSpec.scala:\\d+" - List(extraInner, locationRegex).sorted.mkString("""\{""", ", ", """\}""") - } - - private def assertStatus(status: => Status)( - code: Code, - message: String, - details: Seq[ErrorDetails.ErrorDetail], - logLevel: Level, - logMessage: String, - logErrorContextRegEx: String, - ): Unit = { - lazy val e = io.grpc.protobuf.StatusProto.toStatusRuntimeException(status) - assertError(new ErrorCode.ApiException(e.getStatus, e.getTrailers))( - code, - message, - details, - logLevel, - logMessage, - logErrorContextRegEx, - ) - } - - private def assertError( - error: => RpcError - )( - code: Code, - message: String, - details: Seq[ErrorDetails.ErrorDetail], - logLevel: Level, - logMessage: String, - logErrorContextRegEx: String, - ): Unit = - loggerFactory.assertLogs(SuppressionRule.Level(logLevel))( - within = assertError( - actual = error.asGrpcError, - expectedStatusCode = code, - expectedMessage = message, - expectedDetails = details, - ), - assertions = logEntry => { - logEntry.level shouldBe logLevel - logEntry.message shouldBe logMessage - logEntry.mdc.keys should contain("err-context") - logEntry.mdc - .get("err-context") - .value should fullyMatch regex logErrorContextRegEx - }, - ) - - private def assertError( - statusRuntimeException: => StatusRuntimeException - )( - code: Code, - message: String, - details: Seq[ErrorDetails.ErrorDetail], - logLevel: Level, - logMessage: String, - logErrorContextRegEx: String, - )(implicit d: DummyImplicit): Unit = - loggerFactory.assertLogs(SuppressionRule.Level(logLevel))( - within = assertError( - actual = statusRuntimeException, - expectedStatusCode = code, - expectedMessage = message, - expectedDetails = details, - ), - assertions = logEntry => { - logEntry.level shouldBe logLevel - logEntry.message shouldBe logMessage - logEntry.mdc.keys should contain("err-context") - logEntry.mdc - .get("err-context") - .value should fullyMatch regex logErrorContextRegEx - }, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/AchsIndexComponentTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/AchsIndexComponentTest.scala deleted file mode 100644 index 538ed32760..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/AchsIndexComponentTest.scala +++ /dev/null @@ -1,932 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import anorm.SqlParser.long -import com.daml.ledger.resources.ResourceContext -import com.daml.metrics.DatabaseMetrics -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.ledger.participant.state.Update -import com.digitalasset.canton.ledger.participant.state.Update.CommitRepair -import com.digitalasset.canton.logging.SuppressionRule -import com.digitalasset.canton.platform.config.{ - ActiveContractsServiceStreamsConfig, - IndexServiceConfig, -} -import com.digitalasset.canton.platform.indexer.IndexerConfig -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.indexer.parallel.AchsMaintenancePipe.AchsWorkRange -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.dao.events.ACSReader -import com.digitalasset.canton.util.PekkoUtil -import com.digitalasset.canton.util.PekkoUtil.{RecoveringFutureQueueImpl, RecoveringQueueMetrics} -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.scalatest.concurrent.PatienceConfiguration -import org.scalatest.flatspec.AnyFlatSpec -import org.slf4j.event.Level - -import scala.concurrent.duration.DurationInt -import scala.concurrent.{Future, Promise} - -class AchsIndexComponentTest - extends AnyFlatSpec - with IndexComponentTest - with PersistenceSqlQueries { - val aggregationThreshold: Long = 5L - - val achsConfig: AchsConfig = AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(60L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(40L), - aggregationThreshold = aggregationThreshold, - initAggregationThreshold = aggregationThreshold, - ) - - override protected val indexerConfig: IndexerConfig = IndexerConfig( - achsConfig = Some(achsConfig) - ) - - private val nextRecordTime = new SingleStepIncreasingRecordTime - - behavior of "ACHS maintenance" - - it should "result in minimal ACHS size with few survivors" in { - val txsCreatedThenArchived = 10 - val txsCreatedNotArchived = 1 - val txSize = 1 - val repetitions = 20 - - val allUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - val achsSizeBefore = getAchsSize - val lastEventSeqIdBefore = getLastEventSeqId - - ingestUpdates(allUpdates*) - - // The only survivors are the not-archived contracts, so ACHS increase should be small - val survivors = txsCreatedNotArchived * repetitions.toLong - eventually() { - val achsSizeAfter = getAchsSize - (achsSizeAfter - achsSizeBefore) should be <= survivors - } - - val expectedLastEventSeqId = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - - verifyAchsConsistency(expectedLastEventSeqId) - } - - it should "populate ACHS correctly for creates only" in { - - val txsCreatedNotArchived = 100 - val txSize = 3 - - val allUpdates = createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = 0, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - - val lastEventSeqIdBefore = getLastEventSeqId - ingestUpdates(allUpdates*) - - val expectedLastEventSeqId = lastEventSeqIdBefore + txSize * txsCreatedNotArchived - - verifyAchsConsistency(expectedLastEventSeqId) - } - - it should "populate ACHS correctly for archives as well" in { - val txsCreatedThenArchived = 100 - val txsCreatedNotArchived = 100 - val txSize = 3 - - val allUpdates = createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - - val lastEventSeqIdBefore = getLastEventSeqId - val targetLastEventSeqId = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) - - ingestUpdates(allUpdates*) - verifyAchsConsistency(targetLastEventSeqId) - } - - behavior of "ACHS reading" - - it should "return active contracts when activeAt is before ACHS' validAt (falls back to activate)" in { - val txsCreatedNotArchived = 200 - val txSize = 3 - - val allUpdates = createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = 0, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - - val start = index.currentLedgerEnd().futureValue.fold(0L)(_.unwrap) - - ingestUpdates(allUpdates*) - - val validAt = getAchsValidAt - val beforeValidAt = offsetForActivateContractEventSeqId(validAt) - 1L - - val contractsBeforeValidAt = activeContractIds(beforeValidAt).filter(_._2 > start) - - contractsBeforeValidAt should not be empty - contractsBeforeValidAt.size shouldBe (beforeValidAt - start) * txSize - } - - it should "return active contracts when activeAt is after ACHS validAt (uses ACHS)" in { - val txsCreatedThenArchived = 10 - val txsCreatedNotArchived = 1 - val txSize = 3 - val repetitions = 50 - - val allUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val start = index.currentLedgerEnd().futureValue.fold(0L)(_.unwrap) - - ingestUpdates(allUpdates*) - - val ledgerEnd = index.currentLedgerEnd().futureValue - - val contractsAtLedgerEnd = activeContractIds(ledgerEnd.value.unwrap).filter(_._2 > start) - - contractsAtLedgerEnd.size shouldBe txsCreatedNotArchived * txSize * repetitions - - // offset at validAt so that ACHS is used - val atValidAtOffset = offsetForActivateContractEventSeqId(getAchsValidAt) - // offset before validAt so that ACHS is not used - val beforeValidAtOffset = atValidAtOffset - 1 - - val achsContracts = activeContractIds(atValidAtOffset).filter(_._2 > start) - val noAchsContracts = activeContractIds(beforeValidAtOffset).filter(_._2 > start) - - achsContracts should not be empty - noAchsContracts should not be empty - achsContracts.size shouldBe noAchsContracts.size + txSize - - noAchsContracts shouldBe achsContracts.filter(_._2 < atValidAtOffset) - } - - it should "return correct active contracts when ACHS validAt overtakes the ACS query point mid-stream" in { - val txsCreatedThenArchived = 0 - val txsCreatedNotArchived = 3 - val txSize = 3 - val initialRepetitions = 40 - - // we need around LedgerApiStreamsBufferSize (128) events in ACHS to ensure we are back-pressuring, and we can pause the pipeline - val initialUpdates = (1 to initialRepetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - val lastEventSeqIdBefore = getLastEventSeqId - val lastEventSeqId = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * initialRepetitions - - ingestUpdates(initialUpdates*) - verifyAchsConsistency(lastEventSeqId) - - // Restart indexer with minimal buffering/parallelism to ensure backpressure propagates - // all the way to the ACHS ID source when the stream is paused. - val indexServiceConfig: IndexServiceConfig = IndexServiceConfig( - activeContractsServiceStreams = ActiveContractsServiceStreamsConfig( - maxIdsPerIdPage = 1, - maxPayloadsPerPayloadsPage = 1, - maxParallelActiveIdQueries = 1, - idFilterQueryParallelism = 1, - maxParallelPayloadCreateQueries = 1, - contractProcessingParallelism = 1, - ) - ) - restartIndexer( - serviceConfig = indexServiceConfig - ) - verifyAchsConsistency(lastEventSeqId) - - // activeAtEventSeqId should be >= validAt, so ACHS is initially used. - val validAtBefore = getAchsValidAt - val queryOffset = offsetForActivateContractEventSeqId(validAtBefore) + 1 - val queryEventSeqId = eventSeqIdForActivateContractOffset(queryOffset) - - // Start ACS retrieval and concurrently ingest more data to make validAt overtake queryEventSeqId. - val concurrentRepetitions = 10 - val concurrentUpdates = (1 to concurrentRepetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val numContracts = 10 - // Stream ACS while ingestion is happening, get the first numContracts from ACHS. - // Then pause until ingestion completes to give ACHS time to advance past the query's activeAt. - // Thus, mid-stream, ACHS' validAt will advance past query's activeAt sequential id. - val fetchStarted = Promise[Unit]() - val ingestionDone = Promise[Unit]() - val acsF = index - .getActiveContracts( - eventFormat = allPartyEventFormat, - activeAt = Some(Offset.tryFromLong(queryOffset)), - rangeInfo = AcsRangeInfo.empty, - ) - .zipWithIndex - .mapAsync(1) { case (elem, idx) => - if (idx == numContracts) { - fetchStarted.trySuccess(()) - ingestionDone.future.map(_ => elem) - } else Future.successful(elem) - } - .runWith(Sink.seq) - - // wait until we have fetched some contracts from ACHS - fetchStarted.future.futureValue - - val fallbackMessage = "fell back to filter tables" - val acs = loggerFactory.assertLogsSeq( - SuppressionRule.LevelAndAbove(Level.DEBUG) && SuppressionRule.forLogger[ACSReader] - )( - // signal that ingestion is done (ACHS' validAt should have advanced past query's activeAt) - { - ingestUpdates(concurrentUpdates*) - // Wait for ACHS validAt to advance past the query point before resuming the ACS stream. - eventually() { - getAchsValidAt should be > queryEventSeqId - } - ingestionDone.trySuccess(()) - acsF.futureValue - }, - logs => - withClue( - s"Expected fallback log '$fallbackMessage' not found. Captured logs:\n${logs.map(_.message).mkString("\n")}" - ) { - logs.exists( - _.message.contains(fallbackMessage) - ) shouldBe true - }, - ) - - // validAt should have advanced past the query's event sequential id, if it fails it means that fallback was not triggered due to the validAt overtaking the query point - val validAtAfter = getAchsValidAt - validAtAfter should be > queryEventSeqId - - val achsContractIds = acs - .flatMap( - _.contractEntry.activeContract - .flatMap(_.createdEvent.map(event => event.contractId -> event.offset)) - ) - - achsContractIds should not be empty - - // The ACHS-enabled result should match the ACHS-disabled reference - restartIndexer( - config = indexerConfig.copy(achsConfig = None) - ) - val referenceContractIds = activeContractIds(queryOffset) - referenceContractIds should not be empty - achsContractIds shouldBe referenceContractIds - - // Restart with original config for the next tests - restartIndexer(config = this.indexerConfig) - } - - it should "return same active contracts with ACHS enabled and disabled" in { - val txsCreatedThenArchived = 5 - val txsCreatedNotArchived = 1 - val txSize = 3 - val repetitions = 50 - - val allUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val lastEventSeqIdBefore = getLastEventSeqId - ingestUpdates(allUpdates*) - val expectedLastEventSeqId = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - - verifyAchsConsistency(expectedLastEventSeqId) - - val ledgerEnd = index.currentLedgerEnd().futureValue.value.unwrap - - // fetch ACS using the ACHS-enabled index service (ACHS should be used) - val achsContracts = activeContractIds(ledgerEnd) - achsContracts should not be empty - - // restart indexer with ACHS disabled and fetch ACS again (ACHS should not be used, but result should be the same) - restartIndexer( - config = indexerConfig.copy( - achsConfig = None - ) - ) - - val noAchsContracts = activeContractIds(ledgerEnd) - noAchsContracts should not be empty - - achsContracts shouldBe noAchsContracts - - // restart with original config for the next tests - restartIndexer(config = indexerConfig) - } - - behavior of "ACHS in repair mode" - - it should "leave ACHS unchanged when ingesting repair transactions" in { - // Ensure we start from the original config - restartIndexer(indexerConfig) - - val txsCreatedThenArchived = 1 - val txsCreatedNotArchived = 1 - val txSize = 3 - val repetitions = 20 - - // Phase 1: ingest normal data with ACHS enabled so that ACHS is populated - val initialUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val lastEventSeqIdInit = getLastEventSeqId - ingestUpdates(initialUpdates*) - - val lastEventSeqIdBeforeRepair = - lastEventSeqIdInit + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - - verifyAchsConsistency(lastEventSeqIdBeforeRepair) - - // Snapshot ACHS state before repair - val achsSizeBefore = getAchsSize - val (validAtBefore, lastPopulatedBefore, lastRemovedBefore) = getAchsState - - achsSizeBefore should be > 0L - getAchsStateRowCount shouldBe 1 - - // restart indexer in repair mode - restartIndexer(config = indexerConfig, repairMode = true) - - // ingest repair transactions - val repairTxSize = 3 - val repairRepetitions = 10 - val repairUpdates = (1 to repairRepetitions) - .map { _ => - repairCreates(recordTime = nextRecordTime, payloadLength = 42)(repairTxSize) - } - .toVector - .appended(CommitRepair() -> Vector.empty) // commit the repair to advance the ledger end - - ingestUpdates(repairUpdates*) - - val lastEventSeqIdAfterRepair = getLastEventSeqId - lastEventSeqIdAfterRepair should be > lastEventSeqIdBeforeRepair - - // verify ACHS remains unchanged - getAchsSize shouldBe achsSizeBefore - getAchsState shouldBe ((validAtBefore, lastPopulatedBefore, lastRemovedBefore)) - - // restart indexer in normal mode and verify ACHS catches up with the new data - restartIndexer(config = indexerConfig) - - verifyAchsConsistency(lastEventSeqIdAfterRepair) - - eventually() { - val achsSizeAfter = getAchsSize - achsSizeAfter should be >= achsSizeBefore - } - } - - behavior of "ACHS pruning" - - it should "prune ACHS entries" in { - val txsCreatedThenArchived = 10 - val txsCreatedNotArchived = 1 - val txSize = 20 - val repetitions = 2 - - // Phase 1: ingest data with ACHS enabled so ACHS is populated - val allUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val lastEventSeqIdBefore = getLastEventSeqId - ingestUpdates(allUpdates*) - - val lastEventSeqId = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - verifyAchsConsistency(lastEventSeqId) - - val achsSizeBefore = getAchsSize - achsSizeBefore should be > 0L - - // prune up to the ledger end (all deactivations are in range, their activations should be pruned) - val ledgerEnd = index.currentLedgerEnd().futureValue.value - - index - .prune( - previousPruneUpToInclusive = None, - previousIncompleteReassignmentOffsets = Vector.empty, - pruneUpToInclusive = ledgerEnd, - incompleteReassignmentOffsets = Vector.empty, - ) - .futureValue - - // verify ACHS is still consistent after pruning. - verifyAchsConsistency(getLastEventSeqId) - - // after pruning, the remaining ACHS entries should be <= what was there before pruning - val achsSizeAfter = getAchsSize - achsSizeAfter should be < achsSizeBefore - - // all ACHS entries should reference existing activation events - getAchsEventSeqIds shouldBe getActivateEventSeqIds - - // restart with ACHS enabled and verify ACHS rebuilds consistently - restartIndexer(config = indexerConfig) - verifyAchsConsistency(getLastEventSeqId) - } - - behavior of "ACHS initialization" - - // The different combinations of positive/negative diffs simulate various scenarios of how the new config's distance - // targets relate to the original config. - - it should "maintain ACHS consistency after restart with positive populate and negative remove work distances" in { - // validAtDistanceDiff +20 so remove work negative, lastPopulatedDistanceDiff -30 so populate work positive - testRestartWithNewConfig(validAtDistanceDiff = +20L, lastPopulatedDistanceDiff = -30L) - } - - it should "maintain ACHS consistency after restart with negative populate and positive remove work distances" in { - // validAtDistanceDiff -20 so remove work positive, lastPopulatedDistanceDiff +30 so populate work negative - testRestartWithNewConfig(validAtDistanceDiff = -20L, lastPopulatedDistanceDiff = +30L) - } - - it should "maintain ACHS consistency after restart with negative populate and negative remove work distances" in { - // validAtDistanceDiff +20 so remove work negative, lastPopulatedDistanceDiff +30 so populate work negative - testRestartWithNewConfig(validAtDistanceDiff = +20L, lastPopulatedDistanceDiff = +30L) - } - - it should "maintain ACHS consistency after restart with positive populate and positive remove work distances" in { - // validAtDistanceDiff -30 so remove work positive, lastPopulatedDistanceDiff -20 so populate work positive - testRestartWithNewConfig(validAtDistanceDiff = -30L, lastPopulatedDistanceDiff = -20L) - } - - it should "maintain ACHS consistency after restart with zero populate and zero remove work distances" in { - // Same distances as original config, initialWork = 0 for both dimensions - testRestartWithNewConfig(validAtDistanceDiff = 0L, lastPopulatedDistanceDiff = 0L) - } - - it should "clear ACHS state and snapshot when disabling ACHS" in { - // ensure we start from the original config - restartIndexer(indexerConfig) - - val txsCreatedThenArchived = 5 - val txsCreatedNotArchived = 1 - val txSize = 3 - val repetitions = 50 - - // Phase 1: ingest data with ACHS enabled and verify it was populated - val allUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val lastEventSeqIdBefore = getLastEventSeqId - ingestUpdates(allUpdates*) - - val lastEventSeqId = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - verifyAchsConsistency(lastEventSeqId) - - val achsSize = getAchsSize - - // Confirm ACHS has data before disabling - achsSize should be > 0L - getAchsStateRowCount shouldBe 1 - - // Phase 2: restart with ACHS disabled (achsConfig = None) - restartIndexer( - config = indexerConfig.copy(achsConfig = None) - ) - - // Both the ACHS state row and filter data should be cleared - getAchsSize shouldBe 0L - getAchsStateRowCount shouldBe 0 - - // Phase 3: restart again with ACHS enabled and verify consistency is rebuilt - restartIndexer(config = indexerConfig) - - verifyAchsConsistency(lastEventSeqId) - - // ACHS should be re-populated and consistent - getAchsSize should be >= achsSize - aggregationThreshold - getAchsSize should be <= achsSize + aggregationThreshold - getAchsStateRowCount shouldBe 1 - } - - it should "skip removal phase when initializing fresh ACHS with large lastPopulatedDistanceTarget" in { - // Start with ACHS disabled so that we get a fresh ACHS (lastPopulated = 0) - restartIndexer(config = indexerConfig.copy(achsConfig = None)) - - val txsCreatedThenArchived = 5 - val txsCreatedNotArchived = 1 - val txSize = 3 - val repetitions = 50 - - // Ingest enough data so that there is significant removal work during initialization - val allUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val lastEventSeqIdBefore = getLastEventSeqId - ingestUpdates(allUpdates*) - - val lastEventSeqId = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - - eventually()(getLastEventSeqId shouldBe lastEventSeqId) - - getAchsSize shouldBe 0L - getAchsStateRowCount shouldBe 0 - - // Restart with a config that has large lastPopulatedDistanceTarget and small validAtDistanceTarget. - // This means during initialization: - // remove work = positive removal work - // populate work = negative - // Since lastPopulated = 0 (fresh ACHS), all removal work ranges will have populationEnd = 0, and removal will be skipped. - val largePopDistConfig = AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(10L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(1000000L), - aggregationThreshold = aggregationThreshold, - initAggregationThreshold = aggregationThreshold, - ) - - val skipMessage = "Skipping ACHS removal as no population has been assigned up to this point" - loggerFactory.assertLogsSeq( - SuppressionRule.LevelAndAbove(Level.DEBUG) - )( - restartIndexer(config = indexerConfig.copy(achsConfig = Some(largePopDistConfig))), - logs => { - val skipLogs = logs.filter(_.message.contains(skipMessage)) - withClue( - s"Expected logs with '$skipMessage' not found. Captured logs:\n${logs.map(_.message).mkString("\n")}" - ) { - skipLogs.size should be > 100 - } - }, - ) - } - - it should "shut down when ACHS initialization is in progress" in { - // Start with ACHS disabled so that restarting with ACHS enabled triggers a large initialization - restartIndexer(config = indexerConfig.copy(achsConfig = None)) - - val txsCreatedThenArchived = 5 - val txsCreatedNotArchived = 1 - val txSize = 3 - val repetitions = 50 - - val allUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val lastEventSeqIdBefore = getLastEventSeqId - ingestUpdates(allUpdates*) - - val lastEventSeqId = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - eventually()(getLastEventSeqId shouldBe lastEventSeqId) - - getAchsSize shouldBe 0L - getAchsStateRowCount shouldBe 0 - - val initConfig = AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(10L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(10L), - ) - - // the initialization stream never completes - val achsInitInterceptor: Source[AchsWorkRange, NotUsed] => Source[AchsWorkRange, NotUsed] = - _.concat(Source.never) - - // Release the current resources first - implicit val resourceContext: ResourceContext = ResourceContext(system.dispatcher) - testServices.indexResource.release().futureValue - - val shutdownMessage = "Shutting down ACHS initialization stream via kill switch" - loggerFactory.assertLogsSeq( - SuppressionRule.LevelAndAbove(Level.INFO) - )( - { - val indexerResource = indexerResourceOwner( - config = indexerConfig.copy(achsConfig = Some(initConfig)), - achsInitInterceptor = achsInitInterceptor, - ).acquire()(resourceContext) - val (indexerF, achsKillSwitch, coreDbSupport) = - indexerResource.asFuture.futureValue(timeout = PatienceConfiguration.Timeout(60.seconds)) - - val coreDbMetrics = DatabaseMetrics.ForTesting("achs-kill-switch-test") - def queryAchsSize(): Long = - coreDbSupport.dbDispatcher - .executeSql(coreDbMetrics) { implicit connection => - SQL"SELECT COUNT(event_sequential_id) AS count FROM lapi_filter_achs_stakeholder" - .as(long("count").single) - } - .futureValue - - val consumerFactory: PekkoUtil.Commit => Future[PekkoUtil.FutureQueueConsumer[Update]] = - commit => Future(indexerF(false)(commit))(system.dispatcher).flatten - - val rq = new RecoveringFutureQueueImpl[Update]( - maxBlockedOffer = indexerConfig.queueMaxBlockedOffer, - bufferSize = indexerConfig.queueBufferSize, - loggerFactory = loggerFactory, - retryStategy = PekkoUtil.exponentialRetryWithCap( - minWait = 100L, - multiplier = 2, - cap = 5000L, - ), - retryAttemptWarnThreshold = 3, - retryAttemptErrorThreshold = 5, - uncommittedWarnTreshold = 1000, - recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = consumerFactory, - initializationKillSwitch = achsKillSwitch, - ) - - // Wait for ACHS initialization to actually start writing data before shutting down. - // The constructor returns immediately now (async consumer factory), so we poll the DB - // to confirm ACHS init is actively running. - eventually(timeUntilSuccess = 30.seconds) { - queryAchsSize() should be > 0L - } - - // shut down while ACHS init is in progress - rq.shutdown() - - rq.done.futureValue(timeout = PatienceConfiguration.Timeout(60.seconds)) - - indexerResource.release().futureValue(timeout = PatienceConfiguration.Timeout(60.seconds)) - }, - logs => { - val killSwitchLogs = logs.filter(_.message.contains(shutdownMessage)) - withClue( - s"Expected kill switch log '$shutdownMessage' not found. Captured logs:\n${logs.map(_.message).mkString("\n")}" - ) { - killSwitchLogs should not be empty - } - }, - ) - - // Restart with original config for the next tests - acquireServices( - config = indexerConfig, - serviceConfig = indexServiceConfig, - repairMode = false, - incompleteOffsets = Seq.empty, - ) - } - - // Ingest data, restart with ACHS config adjusted by the given diffs, verify consistency, ingest more data, verify consistency again. - private def testRestartWithNewConfig( - validAtDistanceDiff: Long, - lastPopulatedDistanceDiff: Long, - ): Unit = { - // Ensure we start from the original config (a previous test may have left a different config active) - restartIndexer(indexerConfig) - - val txsCreatedThenArchived = 5 - val txsCreatedNotArchived = 1 - val txSize = 3 - val repetitions = 50 - - // Phase 1: ingest data with original config and verify ACHS consistency - val initialUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - val lastEventSeqIdBefore = getLastEventSeqId - ingestUpdates(initialUpdates*) - - val lastEventSeqId1 = - lastEventSeqIdBefore + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - - verifyAchsConsistency(lastEventSeqId1) - - // Phase 2: restart with the new ACHS config, the initialization should - // recalculate remaining work and eagerly advance ACHS to match the new config - val newAchsConfig = achsConfig.copy( - validAtDistanceTarget = - NonNegativeLong.tryCreate(achsConfig.validAtDistanceTarget.unwrap + validAtDistanceDiff), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate( - achsConfig.lastPopulatedDistanceTarget.unwrap + lastPopulatedDistanceDiff - ), - ) - - restartIndexer( - config = indexerConfig.copy( - achsConfig = Some(newAchsConfig) - ) - ) - - // After restart, for each dimension use the old config if the work is negative (debt, no-op) - // or the new config if the work is positive (catch-up done eagerly during initialization). - // From AchsMaintenancePipe.initialWork: - // remove work = -validAtDistanceDiff - // populate work = -(validAtDistanceDiff + lastPopulatedDistanceDiff) - val removeWorkIsDebt = validAtDistanceDiff > 0 - val populateWorkIsDebt = validAtDistanceDiff + lastPopulatedDistanceDiff > 0 - val expectedValidAt = - if (removeWorkIsDebt) lastEventSeqId1 - achsConfig.validAtDistanceTarget.unwrap - else lastEventSeqId1 - newAchsConfig.validAtDistanceTarget.unwrap - val expectedLastPopulated = - if (populateWorkIsDebt) - lastEventSeqId1 - achsConfig.validAtDistanceTarget.unwrap - achsConfig.lastPopulatedDistanceTarget.unwrap - else - lastEventSeqId1 - newAchsConfig.validAtDistanceTarget.unwrap - newAchsConfig.lastPopulatedDistanceTarget.unwrap - verifyAchsConsistency( - expectedValidAt = expectedValidAt, - expectedLastPopulated = expectedLastPopulated, - ) - - // Phase 3: ingest more data after restart and verify ACHS consistency is maintained - val moreUpdates = (1 to repetitions).flatMap { _ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedThenArchived, - txsCreatedNotArchived = txsCreatedNotArchived, - createPayloadLength = 42, - archiveArgumentPayloadLengthFromTo = (10, 20), - archiveResultPayloadLengthFromTo = (10, 20), - ) - } - - ingestUpdates(moreUpdates*) - - val lastEventSeqId2 = - lastEventSeqId1 + txSize * (txsCreatedThenArchived * 2 + txsCreatedNotArchived) * repetitions - - verifyAchsConsistency(lastEventSeqId2, newAchsConfig) - - } - - private def verifyAchsConsistency( - targetLastEventSeqId: Long, - config: AchsConfig = achsConfig, - ): Unit = { - eventually()(getLastEventSeqId shouldBe targetLastEventSeqId) - verifyAchsConsistency( - expectedValidAt = targetLastEventSeqId - config.validAtDistanceTarget.unwrap, - expectedLastPopulated = - targetLastEventSeqId - config.validAtDistanceTarget.unwrap - config.lastPopulatedDistanceTarget.unwrap, - ) - } - - private def verifyAchsConsistency( - expectedValidAt: Long, - expectedLastPopulated: Long, - ): Unit = - eventually(1.minute) { - val (validAt, lastPopulated, lastRemoved) = getAchsState - validAt should be > expectedValidAt - aggregationThreshold - validAt should be <= expectedValidAt - - lastPopulated should be > expectedLastPopulated - aggregationThreshold - lastPopulated should be <= expectedLastPopulated - - withClue(s"The lastRemoved pointer was not equal to validAt") { - lastRemoved shouldBe validAt - } - - val (activeIds, achsIds) = - withConnection { implicit connection => - val active = - SQL"""SELECT filters.event_sequential_id - FROM lapi_filter_activate_stakeholder filters - WHERE filters.event_sequential_id <= $lastPopulated - AND NOT EXISTS ( - SELECT 1 - FROM lapi_events_deactivate_contract deactivate_evs - WHERE filters.event_sequential_id = deactivate_evs.deactivated_event_sequential_id - AND deactivate_evs.event_sequential_id <= $lastRemoved - ) - ORDER BY filters.event_sequential_id""" - .as(long("event_sequential_id").*) - .toSet - - val achs = - SQL"""SELECT event_sequential_id - FROM lapi_filter_achs_stakeholder - ORDER BY event_sequential_id""" - .as(long("event_sequential_id").*) - .toSet - - (active, achs) - } - - achsIds should not be empty - achsIds shouldBe activeIds - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/IndexComponentLoadTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/IndexComponentLoadTest.scala deleted file mode 100644 index d9bce91962..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/IndexComponentLoadTest.scala +++ /dev/null @@ -1,743 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import com.daml.ledger.api.v2.update_service.GetUpdateResponse -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.api.TransactionShape.LedgerEffects -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.ledger.api.messages.update.GetUpdatesPageRequest -import com.digitalasset.canton.ledger.api.{ - CumulativeFilter, - EventFormat, - ParticipantAuthorizationFormat, - TopologyFormat, - TransactionFormat, - TransactionShape, - UpdateFormat, -} -import com.digitalasset.canton.ledger.participant.state.{ - Reassignment, - ReassignmentInfo, - TestAcsChangeFactory, - Update, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.platform.indexer.IndexerConfig -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.{ContractId, WorkflowId} -import com.digitalasset.canton.protocol.{ContractInstance, ReassignmentId} -import com.digitalasset.canton.store.db.DbStorageSetup.DbBasicConfig -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ReassignmentTag -import com.digitalasset.daml.lf.data.Time -import com.google.protobuf.ByteString -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.scalatest.concurrent.PatienceConfiguration -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.time.Span -import org.scalatest.{Assertion, Ignore} - -import java.util.concurrent.atomic.{AtomicLong, AtomicReference} -import scala.concurrent.Future -import scala.concurrent.duration.{Duration, FiniteDuration} - -/** Goal of this test is to provide a light-weight approach to ingest synthetic Index DB data for - * load-testing, benchmarking purposes. This test is not supposed to run in CI (this is why it is - * ignored, and logs on WARN log level). - */ -@Ignore -class IndexComponentLoadTest - extends AnyFlatSpec - with IndexComponentTest - with PersistenceSqlQueries { - // How long to wait for a benchmarked data fetch to finish. The test will fail if this is exceeded. - private val benchmarkedTaskPatience = - PatienceConfiguration.Timeout(Span.convertDurationToSpan(Duration(2000, "seconds"))) - - override val dbConfig: com.digitalasset.canton.config.DbConfig = - DbBasicConfig( - username = "postgres", - password = "", - dbName = "load_test", - host = "localhost", - port = 5432, - connectionPoolEnabled = true, - ).toPostgresDbConfig - - override implicit val traceContext: TraceContext = TraceContext.createNew("load-test") - - private val testAcsChangeFactory = TestAcsChangeFactory() - - it should "Index assign/unassign updates" ignore { - val nextRecordTime = nextRecordTimeFactory() - logger.warn(s"start preparing updates...") - val passes = 1 - val batchesPerPasses = 50 // this is doubled: first all assign batches then all unassign batches - val eventsPerBatch = 10 - val allUpdates = - (1 to passes).toVector - .flatMap(_ => - allAssignsThenAllUnassigns( - nextRecordTime = nextRecordTime, - assignPayloadLength = 400, - unassignPayloadLength = 150, - batchSize = eventsPerBatch, - batches = batchesPerPasses, - ) - ) :+ assigns(nextRecordTime(), 400)(2) - val allUpdateSize = allUpdates.size - logger.warn(s"prepared $allUpdateSize updates") - indexUpdates(allUpdates) - } - - // will fail without an explicit flag - it should "Index CN ACS Export NFR fixture updates" ignore cnNFRIngestionFixture() - - it should "10% CN NFR" ignore cnNFRIngestionFixture(passes = 432) - - it should "Cycle Contract Case" ignore cnNFRIngestionFixture( - passes = 43, - txsPerPass = 10000, - activeTxsPerPass = 2, - ) - - it should "10% CN NFR with ACHS enabled (zero survival region)" ignore { - val achsConfig = AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(40000L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(0L), - aggregationThreshold = 10000L, - ) - restartIndexer(config = - IndexerConfig( - achsConfig = Some(achsConfig) - ) - ) - cnNFRIngestionFixture( - passes = 432, - actionName = "ingesting with ACHS maintenance (zero survival region)", - ) - - // Restart to default indexer to not impact other tests - restartIndexer() - } - - it should "10% CN NFR with ACHS enabled (big survival region)" ignore { - val achsConfig = AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(40000L), - // the distance between a contract's creation and archival (if archived) in event sequential ids is (txsCreatedThenArchived + txsCreatedNotArchived) * txSize = 2023 * 5 = 10115 - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(20000L), - aggregationThreshold = 10000L, - ) - restartIndexer(config = - IndexerConfig( - achsConfig = Some(achsConfig) - ) - ) - cnNFRIngestionFixture( - passes = 432, - actionName = "ingesting with ACHS maintenance (big survival region)", - ) - - // Restart to default indexer to not impact other tests - restartIndexer() - } - - it should "Fetch ACS" ignore TraceContext.withNewTraceContext("ACS fetch") { - implicit traceContext => - fetchAcs() - } - - it should "Fetch ACS with ACHS" ignore TraceContext.withNewTraceContext("ACS fetch with ACHS") { - implicit traceContext => - restartIndexer( - config = IndexerConfig( - achsConfig = Some( - AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(40000L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(20000L), - ) - ) - ) - ) - fetchAcs() - - // Restart to default indexer to not impact other tests - restartIndexer() - } - - it should "Fetch updates stream in ascending order" ignore { - fetchUpdatesStream(descendingOrder = false) - } - - it should "Fetch updates pages of size 100 in ascending order" ignore { - fetchUpdatesPaged(100, descendingOrder = false) - } - - it should "Fetch updates pages of size 500 in ascending order" ignore { - fetchUpdatesPaged(500, descendingOrder = false) - } - - it should "Fetch updates stream in descending order" ignore { - fetchUpdatesStream(descendingOrder = true) - } - - it should "Fetch updates pages of size 100 in descending order" ignore { - fetchUpdatesPaged(100, descendingOrder = true) - } - - it should "Fetch updates pages of size 500 in descending order" ignore { - fetchUpdatesPaged(500, descendingOrder = true) - } - - it should "Measure ACHS rendering time during initialization with small survival region" ignore { - measureAchsInitializationTime( - regionName = "small", - achsConfig = AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(200000L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(0L), - ), - ) - } - - it should "Measure ACHS rendering time during initialization with big survival region" ignore { - measureAchsInitializationTime( - regionName = "big", - achsConfig = AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(200000L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(100000L), - ), - ) - } - - private def measureAchsInitializationTime(regionName: String, achsConfig: AchsConfig): Unit = { - // Step 1: Clear any existing ACHS state by restarting without ACHS - restartIndexer(config = IndexerConfig(achsConfig = None)) - - // Step 2: Measure baseline restart time without ACHS (no rendering work) - logger.warn("Measuring baseline restart time without ACHS...") - val baselineStart = System.currentTimeMillis() - restartIndexer(config = IndexerConfig(achsConfig = None)) - val baselineTime = System.currentTimeMillis() - baselineStart - logger.warn(s"Baseline restart (no ACHS) completed in ${seconds(baselineTime)} s") - - val lastEventSeqIdBefore = getLastEventSeqId - logger.warn(s"Last event sequential ID before enabling ACHS: $lastEventSeqIdBefore") - - val achsSizeBefore = getAchsSize - val achsStateRows = getAchsStateRowCount - achsSizeBefore shouldBe 0L - achsStateRows shouldBe 0L - - // Step 3: Enable ACHS and measure rendering time - logger.warn( - s"Enabling ACHS with $regionName survival region (validAtDistance=${achsConfig.validAtDistanceTarget}, " + - s"lastPopulatedDistance=${achsConfig.lastPopulatedDistanceTarget})..." - ) - val renderStart = System.currentTimeMillis() - restartIndexer(config = IndexerConfig(achsConfig = Some(achsConfig))) - val renderTime = System.currentTimeMillis() - renderStart - val renderOverhead = renderTime - baselineTime - logger.warn( - s"ACHS rendering with $regionName survival region completed in ${seconds(renderTime)} s " + - s"(rendering overhead: ${seconds(renderOverhead)} s)" - ) - - // Log and verify ACHS state after rendering - logAchsState() - val achsSize = getAchsSize - val (achsValidAt, achsLastPopulated, achsLastRemoved) = getAchsState - - val initAggThreshold = AchsConfig.DefaultInitAggregationThreshold - - val removeTarget = lastEventSeqIdBefore - achsConfig.validAtDistanceTarget.unwrap - val populateTarget = removeTarget - achsConfig.lastPopulatedDistanceTarget.unwrap - - achsValidAt shouldBe achsLastRemoved - - // Pointers should be within [target - threshold, target] due to aggregation rounding - achsLastRemoved should be <= removeTarget - achsLastRemoved should be >= (removeTarget - initAggThreshold).max(0L) - achsLastPopulated should be <= populateTarget.max(0L) - achsLastPopulated should be >= (populateTarget - initAggThreshold).max(0L) - - achsSize should be > 0L - - // Summary - logger.warn(s"=== ACHS Rendering Time Summary (${regionName.capitalize} Survival) ===") - logger.warn(s"Baseline restart (no ACHS): ${seconds(baselineTime)} s") - logger.warn( - s"${regionName.capitalize} survival region rendering: ${seconds(renderOverhead)} s (total: ${seconds(renderTime)} s, ACHS size: $achsSize)" - ) - } - - private def fetchAcs()(implicit traceContext: TraceContext): Unit = { - val ledgerEndOffset = index.currentLedgerEnd().futureValue - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory) - logger.warn("start fetching acs...") - val startTime = System.currentTimeMillis() - index - .getActiveContracts( - eventFormat = eventFormat(dsoParty.value), - activeAt = ledgerEndOffset, - rangeInfo = AcsRangeInfo.empty, - ) - .zipWithIndex - .runWith(Sink.last) - .map { case (last, lastIndex) => - val totalMillis = System.currentTimeMillis() - startTime - logger.warn( - s"finished fetching acs in ${seconds(totalMillis)} s, ${lastIndex + 1} active contracts returned." - ) - logger.warn(s"last active contract acs: $last") - } - .futureValue( - benchmarkedTaskPatience - ) - } - - private def seconds(milliseconds: Long): String = { - val secs = milliseconds / 1000 - val millis = milliseconds.abs - secs.abs * 1000 - val milliString = (1000 + millis).toString.substring(1) - secs.toString + '.' + milliString - } - - private def nextRecordTimeFactory(): () => CantonTimestamp = { - logger.warn(s"looking up base record time") - val ledgerEnd = index.currentLedgerEnd().futureValue - val baseRecordTime: CantonTimestamp = ledgerEnd match { - case Some(offset) => - // try to get the last one - logger.warn(s"looks like ledger not empty, getting record time from last update") - val lastUpdate: GetUpdateResponse.Update = index - .getUpdateBy( - LookupKey.ByOffset(offset), - UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = allPartyEventFormat, - transactionShape = TransactionShape.LedgerEffects, - ) - ), - includeReassignments = Some(allPartyEventFormat), - includeTopologyEvents = None, - ), - ) - .futureValue - .value - .update - lastUpdate.reassignment - .flatMap(_.recordTime) - .orElse(lastUpdate.transaction.flatMap(_.recordTime)) - .map(CantonTimestamp.fromProtoTimestamp(_).value) - .getOrElse(fail("On LedgerEnd a reassignment or transaction is expected")) - case None => - // empty ledger getting now - logger.warn(s"looks like ledger is empty, using now as a baseline record time") - CantonTimestamp.now() - } - val recordTime = new AtomicReference(baseRecordTime) - () => recordTime.updateAndGet(_.immediateSuccessor) - } - - /** Creates and ingests passes * txsPerPass transactions, each with 5 contracts. After each pass, - * all but activeTxsPerPass of the txsPerPass transactions are archived. - */ - private def cnNFRIngestionFixture( - passes: Int = 4320, - txsPerPass: Int = 2023, - activeTxsPerPass: Int = 23, - yesIReallyWantToRunIt: Boolean = false, - actionName: String = "ingesting", - ): Unit = { - val nextRecordTime: () => CantonTimestamp = nextRecordTimeFactory() - def ingestionIteration(): Unit = { - logger.warn(s"start preparing updates...") - if (!yesIReallyWantToRunIt) - fail( - "WARNING! Please check if you really want to do this! The following parameters result in a fixture probably not fitting in the memory. Please verify parameters. Bigger workloads are possible with doing multiple iterations." - ) - val txSize = 5 - val txsCreatedAndArchivedPerPass = txsPerPass - activeTxsPerPass - val createPayloadLength = 300 - val archiveArgumentPayloadLengthFromTo = (13, 38) - val archiveResultPayloadLengthFromTo = (13, 58) - val allUpdates = (1 to passes).toVector - .flatMap(_ => - createsAndArchives( - nextRecordTime = nextRecordTime, - txSize = txSize, - txsCreatedThenArchived = txsCreatedAndArchivedPerPass, - txsCreatedNotArchived = activeTxsPerPass, - createPayloadLength = createPayloadLength, - archiveArgumentPayloadLengthFromTo = archiveArgumentPayloadLengthFromTo, - archiveResultPayloadLengthFromTo = archiveResultPayloadLengthFromTo, - ) - ) - val allUpdateSize = allUpdates.size - logger.warn(s"prepared $allUpdateSize updates") - indexUpdates(allUpdates, actionName = actionName) - } - - (1 to 1).foreach { i => - logger.warn(s"ingestion iteration: $i started") - ingestionIteration() - logger.warn(s"ingestion iteration: $i finished") - } - } - - private def withReporter[UpdateT, ResultT, Out]( - updates: Vector[UpdateT], - parallelism: Int, - process: UpdateT => Future[ResultT], - action: String, - sink: Sink[ResultT, Future[Out]], - waitingMessage: String = "", - endCheck: () => Assertion = () => succeed, - ): Out = { - val numOfUpdates = updates.size - val startTime = System.currentTimeMillis() - val state = new AtomicLong(0L) - logger.warn(s"start $action $numOfUpdates updates...") - val reportingSeconds = 5 - val reporter = system.scheduler.scheduleAtFixedRate( - initialDelay = FiniteDuration(reportingSeconds, "seconds"), - interval = FiniteDuration(reportingSeconds, "seconds"), - )(new Runnable { - val lastState = new AtomicLong(0L) - override def run(): Unit = { - val current = state.get() - val last = lastState.getAndSet(current) - val reportRate = (current - last) / reportingSeconds - val avgRate = current * 1000 / (System.currentTimeMillis() - startTime) - val minutesLeft = (numOfUpdates - current) / avgRate / 60 - logger.warn( - s"$action $current/$numOfUpdates, ${100 * current / numOfUpdates}% (since last: ${current - last}, $reportRate update/seconds) (avg: $avgRate update/seconds, estimated minutes left: $minutesLeft)..." - ) - } - }) - Source - .fromIterator(() => updates.iterator) - .async - .mapAsync(parallelism)(process) - .async - .map { elem => - state.incrementAndGet() - elem - } - .runWith(sink) - .map { result => - reporter.cancel() - logger.warn( - s"finished $action $numOfUpdates updates to indexer" + waitingMessage - ) - eventually( - timeUntilSuccess = FiniteDuration(1000, "seconds"), - maxPollInterval = FiniteDuration(100, "milliseconds"), - )(endCheck()) - val avgRate = numOfUpdates * 1000 / (System.currentTimeMillis() - startTime) - logger.warn( - s"finished $action $numOfUpdates updates with average rate $avgRate updates/second" - ) - result - } - .futureValue( - PatienceConfiguration.Timeout(Span.convertDurationToSpan(Duration(200000, "seconds"))) - ) - } - - private def indexUpdates( - updates: Vector[(Update, Vector[ContractInstance])], - actionName: String = "ingesting", - ): Unit = { - val startTime = System.currentTimeMillis - val updatesWithIds = fillUpdatesWithInternalContractIds(updates) - val ledgerEndLongBefore = index.currentLedgerEnd().futureValue.map(_.positive).getOrElse(0L) - withReporter( - updates = updatesWithIds, - parallelism = 1, - process = ingestUpdateAsync, - action = actionName, - sink = Sink.ignore, - waitingMessage = ", waiting for all to be indexed...", - endCheck = () => - (index - .currentLedgerEnd() - .futureValue - .map(_.positive) - .getOrElse(0L) - ledgerEndLongBefore) shouldBe updates.size, - ).discard - val timeSpan = seconds(System.currentTimeMillis - startTime) - logger.warn(s"Ingestion cycle completed in $timeSpan seconds") - logAchsState() - } - - private def logAchsState(): Unit = { - val achsStateRows = getAchsStateRowCount - if (achsStateRows > 0) { - val lastEventSeqId = getLastEventSeqId - val achsSize = getAchsSize - val (achsValidAt, achsLastPopulated, achsLastRemoved) = getAchsState - logger.warn( - s"ACHS state: lastEventSequentialId=$lastEventSeqId, size=$achsSize, validAt=$achsValidAt, lastPopulated=$achsLastPopulated, lastRemoved=$achsLastRemoved" - ) - } else { - logger.warn("ACHS state: not initialized (no rows in lapi_achs_state)") - } - } - - private def fillUpdatesWithInternalContractIds( - updates: Vector[(Update, Vector[ContractInstance])] - ): Vector[Update] = - withReporter[(Update, Vector[ContractInstance]), Update, Seq[Update]]( - updates = updates, - parallelism = 100, - process = (storeContracts _).tupled, - action = "storing contracts of updates", - sink = Sink.seq, - ).toVector - - private def allAssignsThenAllUnassigns( - nextRecordTime: () => CantonTimestamp, - assignPayloadLength: Int, - unassignPayloadLength: Int, - batchSize: Int, - batches: Int, - ): Vector[(Update.SequencedReassignmentAccepted, Vector[ContractInstance])] = { - val assigned = - Vector - .fill(batches)(batchSize) - .map(size => assigns(nextRecordTime(), assignPayloadLength)(size)) - - val cidBatches = - assigned.map(_._2.map(_.contractId)) - - assigned ++ cidBatches.map(cids => - unassigns(nextRecordTime(), unassignPayloadLength)(cids) -> Vector.empty - ) - } - - private def assigns(recordTime: CantonTimestamp, payloadLength: Int)( - size: Int - ): (Update.SequencedReassignmentAccepted, Vector[ContractInstance]) = { - val (reassignments, contracts) = - (0 until size) - .map(index => - assign( - nodeId = index, - ledgerEffectiveTime = recordTime.underlying, - argumentPayload = randomString(payloadLength), - ) - ) - .unzip - - reassignment( - sourceSynchronizerId = synchronizer2, - targetSynchronizerId = synchronizer1, - synchronizerId = synchronizer1, - recordTime = recordTime, - workflowId = None, - )(reassignments) -> contracts.toVector - } - - private def unassigns(recordTime: CantonTimestamp, payloadLength: Int)( - coids: Seq[ContractId] - ): Update.SequencedReassignmentAccepted = - reassignment( - sourceSynchronizerId = synchronizer1, - targetSynchronizerId = synchronizer2, - synchronizerId = synchronizer1, - recordTime = recordTime, - workflowId = Some( - WorkflowId.assertFromString(randomString(payloadLength)) - ), // mimick unassign payload with workflowID. This is also stored with all events. - )(coids.zipWithIndex.map { case (coid, index) => - unassign( - coid = coid, - nodeId = index, - ) - }) - - private def assign( - nodeId: Int, - ledgerEffectiveTime: Time.Timestamp, - argumentPayload: String, - ): (Reassignment.Assign, ContractInstance) = { - val contract = genContract( - argumentPayload = argumentPayload, - template = templates(0), - signatories = Set(dsoParty), - ledgerEffectiveTime = ledgerEffectiveTime, - ) - Reassignment.Assign( - reassignmentCounter = 10L, - nodeId = nodeId, - persistedContractInstance = PersistedContractInstance( - internalContractId = -1, // will be filled later - inst = contract.inst, - ), - ) -> contract - } - - private def unassign( - coid: ContractId, - nodeId: Int, - ): Reassignment.Unassign = - Reassignment.Unassign( - contractId = coid, - templateId = templates(0), - packageName = packageName, - stakeholders = Set(dsoParty.value), - assignmentExclusivity = None, - reassignmentCounter = 11L, - nodeId = nodeId, - ) - - private def reassignment( - sourceSynchronizerId: SynchronizerId, - targetSynchronizerId: SynchronizerId, - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - workflowId: Option[WorkflowId], - )(reassignments: Seq[Reassignment]): Update.SequencedReassignmentAccepted = - Update.SequencedReassignmentAccepted( - optCompletionInfo = None, - workflowId = workflowId, - updateId = randomUpdateId, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = ReassignmentTag.Source(sourceSynchronizerId), - targetSynchronizer = ReassignmentTag.Target(targetSynchronizerId), - submitter = Some(dsoParty.value), - reassignmentId = ReassignmentId.tryCreate("000123"), - isReassigningParticipant = false, - ), - reassignment = Reassignment.Batch(reassignments.head, reassignments.tail*), - recordTime = recordTime, - synchronizerId = synchronizerId, - acsChangeFactory = testAcsChangeFactory, - ) - - def updateFormat(transactionShape: TransactionShape) = UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = EventFormat( - filtersByParty = Map(), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter(true)), - verbose = true, - ), - transactionShape = transactionShape, - ) - ), - includeReassignments = Some( - EventFormat( - filtersByParty = Map(), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter(true)), - verbose = true, - ) - ), - includeTopologyEvents = Some( - TopologyFormat( - Some( - ParticipantAuthorizationFormat(None) - ) - ) - ), - ) - - private def fetchUpdatesStream(descendingOrder: Boolean): Unit = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory) - logger.warn("start fetching updates stream...") - val startTime = System.currentTimeMillis() - val fetchAndCountUpdates = for { - ledgerEnd <- index.currentLedgerEnd() - source = index.updates( - begin = None, - endAt = ledgerEnd, - updateFormat = updateFormat(LedgerEffects), - descendingOrder = descendingOrder, - skipPruningChecks = false, - ) - updates <- source.grouped(1000).map(_.size).runWith(Sink.seq)(materializer) - } yield updates.sum - - fetchAndCountUpdates - .map { count => - val totalMillis = System.currentTimeMillis() - startTime - logger.warn( - s"finished fetching updates in ${if (descendingOrder) "descending" - else "ascending"} order in ${seconds(totalMillis)} s, $count transactions returned." - ) - } - .futureValue( - benchmarkedTaskPatience - ) - } - - private def fetchUpdatesPaged(pageSize: Int, descendingOrder: Boolean): Unit = { - implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace(loggerFactory) - - def fetchAllPages( - request: GetUpdatesPageRequest, - fetchedSoFar: Int, - ): Future[Int] = - index - .updatesPage(request) - .flatMap(response => - response.nextPageToken match { - case Some(_) => - fetchAllPages( - request.copy(continueStreamFromIncl = - Some( - if (descendingOrder) - Offset.tryFromLong(response.lowestPageOffsetExclusive) - else - Offset.tryFromLong(response.highestPageOffsetInclusive + 1) - ) - ), - fetchedSoFar + response.updates.size, - ) - case None => Future.successful(fetchedSoFar + response.updates.size) - } - ) - - logger.warn(s"start fetching updates pages($pageSize) in ${if (descendingOrder) "descending" - else "ascending"} order...") - val startTime = System.currentTimeMillis() - val fetchAndCountUpdates = for { - ledgerEnd <- index.currentLedgerEnd() - request = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = ledgerEnd, - continueStreamFromIncl = None, - maxPageSize = pageSize, - updateFormat = updateFormat(LedgerEffects), - descendingOrder = descendingOrder, - requestChecksum = ByteString.empty(), - participantChecksum = ByteString.empty(), - ) - res <- fetchAllPages(request, 0) - } yield res - - fetchAndCountUpdates - .map { count => - val totalMillis = System.currentTimeMillis() - startTime - logger.warn( - s"finished fetching paged($pageSize) updates in ${if (descendingOrder) "descending" - else "ascending"} order in ${seconds(totalMillis)} s, $count transactions returned." - ) - } - .futureValue( - benchmarkedTaskPatience - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/IndexComponentTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/IndexComponentTest.scala deleted file mode 100644 index 6fddf2006d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/IndexComponentTest.scala +++ /dev/null @@ -1,920 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner} -import com.daml.metrics.DatabaseMetrics -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.config.{BatchingConfig, CachingConfigs, ProcessingTimeout} -import com.digitalasset.canton.crypto.HashAlgorithm.Sha256 -import com.digitalasset.canton.crypto.{Hash, HashPurpose} -import com.digitalasset.canton.data.{CantonTimestamp, LedgerTimeBoundaries, Offset} -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.ledger.api.{CumulativeFilter, EventFormat, TemplateWildcardFilter} -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent.Onboarding -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.TopologyEvent.PartyToParticipantAuthorization -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId.SameAsContractPackageId -import com.digitalasset.canton.ledger.participant.state.Update.{ - ContractInfo, - OnPRReassignmentAccepted, - RepairReassignmentAccepted, - RepairTransactionAccepted, - SequencedReassignmentAccepted, - SequencedTransactionAccepted, - TopologyTransactionEffective, -} -import com.digitalasset.canton.ledger.participant.state.index.IndexService -import com.digitalasset.canton.ledger.participant.state.{ - Reassignment, - ReassignmentInfo, - TestAcsChangeFactory, - TransactionMeta, - Update, -} -import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, HasCloseContext} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.{CommonMockMetrics, LedgerApiServerMetrics} -import com.digitalasset.canton.participant.ledger.api.LedgerApiJdbcUrl -import com.digitalasset.canton.participant.store.{ContractStore, PersistedContractInstance} -import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker -import com.digitalasset.canton.platform.config.{IndexServiceConfig, ServerRole, UpdateServiceConfig} -import com.digitalasset.canton.platform.index.IndexServiceOwner -import com.digitalasset.canton.platform.indexer.ha.HaConfig -import com.digitalasset.canton.platform.indexer.parallel.AchsMaintenancePipe.AchsWorkRange -import com.digitalasset.canton.platform.indexer.parallel.NoOpReassignmentOffsetPersistence -import com.digitalasset.canton.platform.indexer.{Indexer, IndexerConfig, JdbcIndexer} -import com.digitalasset.canton.platform.store.DbSupport.{ConnectionPoolConfig, DbConfig} -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig -import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.platform.store.dao.events.{ContractLoader, LfValueTranslation} -import com.digitalasset.canton.platform.store.interning.StringInterningView -import com.digitalasset.canton.platform.store.{ - DbSupport, - FlywayMigrations, - LedgerApiContractStore, - LedgerApiContractStoreImpl, - PruningOffsetService, -} -import com.digitalasset.canton.platform.{ - InMemoryState, - LedgerApiServerInternals, - PackageId, - PackageName, -} -import com.digitalasset.canton.protocol.{ - ContractInstance, - ExampleContractFactory, - LfContractId, - ReassignmentId, - TestUpdateId, - UpdateId, -} -import com.digitalasset.canton.resource.DbStorageSingle -import com.digitalasset.canton.store.db.DbStorageSetup.DbBasicConfig -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.time.{SimClock, WallClock} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.{NoReportingTracerProvider, TraceContext} -import com.digitalasset.canton.util.PekkoUtil.{FutureQueue, IndexingFutureQueue} -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import com.digitalasset.canton.util.{JarResourceUtils, MonadUtil} -import com.digitalasset.canton.{BaseTest, HasExecutorService, RepairCounter, platform} -import com.digitalasset.daml.lf.archive.DarParser -import com.digitalasset.daml.lf.data.{FrontStack, ImmArray, Ref, Time} -import com.digitalasset.daml.lf.engine.{Engine, EngineConfig} -import com.digitalasset.daml.lf.language.LanguageVersion -import com.digitalasset.daml.lf.transaction.test.{NodeIdTransactionBuilder, TestNodeBuilder} -import com.digitalasset.daml.lf.transaction.{CommittedTransaction, CreationTime, Node} -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.ValueParty -import com.google.protobuf.ByteString -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl -import org.apache.pekko.stream.scaladsl.Sink -import org.scalatest.Suite -import org.scalatest.concurrent.PatienceConfiguration - -import java.sql.Connection -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContext, Future} -import scala.util.chaining.scalaUtilChainingOps - -import IndexComponentTest.TestServices - -trait IndexComponentTest - extends PekkoBeforeAndAfterAll - with BaseTest - with HasExecutorService - with HasCloseContext - with FlagCloseable { - self: Suite => - - private val clock = new WallClock(ProcessingTimeout(), loggerFactory) - - implicit val scheduler: ScheduledExecutorService = scheduledExecutor() - implicit val ec: ExecutionContext = system.dispatcher - protected implicit val loggingContextWithTrace: LoggingContextWithTrace = - LoggingContextWithTrace.ForTesting - - private val dbName: String = getClass.getSimpleName.toLowerCase - - protected val dbConfig: com.digitalasset.canton.config.DbConfig = - DbBasicConfig(username = "", password = "", dbName = dbName, host = "", port = 0).toH2DbConfig - - protected def jdbcUrl: String = LedgerApiJdbcUrl.fromDbConfig(dbConfig).value.url - - protected val indexerConfig: IndexerConfig = IndexerConfig() - - protected val indexServiceConfig: IndexServiceConfig = IndexServiceConfig() - - protected val updateServiceConfig: UpdateServiceConfig = UpdateServiceConfig() - - protected val indexReadConnectionPoolSize: Int = 10 - - private val testServicesRef: AtomicReference[TestServices] = new AtomicReference() - - private[this] lazy val dar = "P.dar" - .pipe(JarResourceUtils.extractFileFromJar) - .pipe(DarParser.assertReadArchiveFromFile) - - protected final lazy val packageMap = - dar.all.map(archive => archive.getHash -> archive).toMap - - private lazy val testDarPackageHash = dar.main.getHash - private val choiceNames = Range - .inclusive(1, 300) - .map(id => - Ref.Name.assertFromString(s"Archivingarchivingarchivingarchivingarchivingarchiving$id") - ) - .toVector - - protected def testServices: TestServices = - Option(testServicesRef.get()) - .getOrElse(throw new Exception("TestServices not initialized. Not accessing from a test?")) - - protected def index: IndexService = testServices.index - protected def dbSupport: DbSupport = testServices.dbSupport - protected def dbDispatcher: DbDispatcher = testServices.dbSupport.dbDispatcher - protected def ledgerEndCache: MutableLedgerEndCache = testServices.inMemoryState.ledgerEndCache - protected def contractStore: LedgerApiContractStore = testServices.participantContractStore - - private def ledgerEndOffset = testServices.index.currentLedgerEnd().futureValue - - protected def ingestUpdates(updates: (Update, Vector[ContractInstance])*): Offset = { - val ledgerEndLongBefore = ledgerEndOffset.map(_.positive).getOrElse(0L) - val ingestionTimeout = 60.minutes - // contracts should be stored in participant contract store before ingesting the updates to get the internal contract ids mapping - MonadUtil - .sequentialTraverse_(updates) { case (update, contracts) => - storeContracts(update, contracts).flatMap(testServices.indexer.offer) - } - .futureValue(timeout = PatienceConfiguration.Timeout(ingestionTimeout)) - val expectedOffset = Offset.tryFromLong(updates.size + ledgerEndLongBefore) - eventually(timeUntilSuccess = ingestionTimeout) { - ledgerEndOffset shouldBe Some(expectedOffset) - expectedOffset - } - } - - protected def ingestUpdateAsync(update: Update): Future[Unit] = - testServices.indexer.offer(update).map(_ => ()) - - protected def ingestUpdateSync(update: Update): Offset = { - val ledgerEndBefore = index.currentLedgerEnd().futureValue - ingestUpdateAsync(update).futureValue - - eventually() { - val ledgerEndAfter = index.currentLedgerEnd().futureValue - ledgerEndAfter should be > ledgerEndBefore - ledgerEndAfter.value - } - } - - protected def storeContracts( - update: Update, - contracts: Vector[ContractInstance], - ): Future[Update] = - // this mimics protocol processing that stores contracts and retrieves their internal contract ids afterward - testServices.participantContractStore.participantContractStore - .storeContracts(contracts) - .failOnShutdownToAbortException("storeContracts") - .flatMap(_ => - testServices.participantContractStore - .lookupBatchedInternalIdsNonReadThrough( - contracts.map(_.contractId) - ) - ) - .map { internalContractIds => - update match { - case txAccepted: SequencedTransactionAccepted => - txAccepted.copy(contractInfos = - injectInternalContractIds( - txAccepted.contractInfos, - internalContractIds, - ) - ) - case txAccepted: RepairTransactionAccepted => - txAccepted.copy(contractInfos = - injectInternalContractIds( - txAccepted.contractInfos, - internalContractIds, - ) - ) - case reassignment: SequencedReassignmentAccepted => - reassignment.copy( - reassignment = - injectInternalContractIds(reassignment.reassignment, internalContractIds) - ) - case reassignment: RepairReassignmentAccepted => - reassignment.copy( - reassignment = - injectInternalContractIds(reassignment.reassignment, internalContractIds) - ) - case reassignment: OnPRReassignmentAccepted => - reassignment.copy( - reassignment = - injectInternalContractIds(reassignment.reassignment, internalContractIds) - ) - case other => other - } - } - - private def injectInternalContractIds( - contractInfo: Map[LfContractId, ContractInfo], - internalContractIds: Map[LfContractId, Long], - ): Map[LfContractId, ContractInfo] = - contractInfo.map { case (coid, contractInfo) => - coid -> contractInfo.copy( - persistedContractInstance = contractInfo.persistedContractInstance.copy( - internalContractId = internalContractIds.getOrElse( - coid, - throw new IllegalStateException(s"Internal contract ID is not provided for $coid"), - ) - ) - ) - } - - private def injectInternalContractIds( - reassignmentBatch: Reassignment.Batch, - internalContractIds: Map[LfContractId, Long], - ): Reassignment.Batch = Reassignment.Batch( - reassignmentBatch.reassignments.map { - case assign: Reassignment.Assign => - assign.copy( - persistedContractInstance = assign.persistedContractInstance.copy( - internalContractId = internalContractIds.get(assign.createNode.coid).value - ) - ) - - case unassign: Reassignment.Unassign => unassign: Reassignment - } - ) - - private val wildcardTemplates = CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set.empty, - templateWildcardFilter = Some(TemplateWildcardFilter(includeCreatedEventBlob = false)), - ) - protected def eventFormat(party: Ref.Party) = EventFormat( - filtersByParty = Map( - party -> wildcardTemplates - ), - filtersForAnyParty = None, - verbose = false, - ) - protected val allPartyEventFormat = EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(wildcardTemplates), - verbose = false, - ) - - private val indexComponentTestDbMetrics = DatabaseMetrics.ForTesting("index-component-test") - - protected def withConnection[T](f: Connection => T): T = - dbSupport.dbDispatcher - .executeSql(indexComponentTestDbMetrics)(f) - .futureValue - - protected def sequentialPostProcessor: Update => Unit = _ => () - - protected lazy val stringInterning = new StringInterningView(loggerFactory) - - private lazy val engine = - new Engine(EngineConfig(LanguageVersion.stableLfVersions), loggerFactory) - private lazy val participantId = - Ref.ParticipantId.assertFromString("index-component-test-participant-id") - protected lazy val pruningOffsetService = new PruningOffsetService { - override def pruningOffset(implicit - traceContext: TraceContext - ): Future[Option[Offset]] = index.indexDbPrunedUpto - } - - private def jdbcIndexerResourceOwner( - config: IndexerConfig, - serviceConfig: IndexServiceConfig, - achsInitInterceptor: scaladsl.Source[AchsWorkRange, NotUsed] => scaladsl.Source[ - AchsWorkRange, - NotUsed, - ], - ): ResourceOwner[ - (Indexer, Option[() => Unit], LedgerApiContractStoreImpl, DbSupport, InMemoryState) - ] = - for { - dbStorage <- ResourceOwner - .forCloseable(() => - DbStorageSingle - .tryCreate( - config = dbConfig, - connectionPoolForParticipant = false, - logQueryCost = None, - clock = new SimClock(CantonTimestamp.Epoch, loggerFactory), - scheduler = None, - metrics = CommonMockMetrics.dbStorage, - timeouts = timeouts, - loggerFactory = loggerFactory, - ) - ) - contractStore <- - ResourceOwner - .forCloseable(() => - ContractStore.create( - storage = dbStorage, - processingTimeouts = timeouts, - cachingConfigs = CachingConfigs(), - batchingConfig = BatchingConfig(), - loggerFactory = loggerFactory, - ) - ) - participantContractStore = LedgerApiContractStoreImpl( - contractStore, - loggerFactory, - LedgerApiServerMetrics.ForTesting, - ) - mutableLedgerEndCache = MutableLedgerEndCache() - stringInterningView = stringInterning - (inMemoryState, updaterFlow) <- LedgerApiServerInternals.createInMemoryStateAndUpdater( - participantId = participantId, - commandProgressTracker = CommandProgressTracker.NoOp, - indexServiceConfig = serviceConfig, - maxCommandsInFlight = 1, // not used - metrics = LedgerApiServerMetrics.ForTesting, - executionContext = ec, - tracer = NoReportingTracerProvider.tracer, - loggerFactory = loggerFactory, - )(mutableLedgerEndCache, stringInterningView) - _ <- ResourceOwner.forFuture(() => new FlywayMigrations(jdbcUrl, loggerFactory).migrate()) - dbSupport <- DbSupport - .owner( - serverRole = ServerRole.ApiServer, - metrics = LedgerApiServerMetrics.ForTesting, - dbConfig = DbConfig( - jdbcUrl = jdbcUrl, - connectionPool = ConnectionPoolConfig( - connectionPoolSize = indexReadConnectionPoolSize, - connectionTimeout = 250.millis, - ), - postgres = PostgresDataSourceConfig( - clientConnectionCheckInterval = None - ), - ), - loggerFactory = loggerFactory, - ) - (indexer, killSwitch) <- new JdbcIndexer.Factory( - participantId = participantId, - participantDataSourceConfig = DbSupport.ParticipantDataSourceConfig(jdbcUrl), - config = config, - metrics = LedgerApiServerMetrics.ForTesting, - inMemoryState = inMemoryState, - apiUpdaterFlow = updaterFlow, - executionContext = ec, - tracer = NoReportingTracerProvider.tracer, - loggerFactory = loggerFactory, - dataSourceProperties = - IndexerConfig.createDataSourcePropertiesForTesting(config, loggerFactory), - highAvailability = HaConfig(), - indexServiceDbDispatcher = Some(dbSupport.dbDispatcher), - clock = clock, - reassignmentOffsetPersistence = NoOpReassignmentOffsetPersistence, - postProcessor = (_, _) => Future.unit, - sequentialPostProcessor = sequentialPostProcessor, - contractStore = participantContractStore, - achsInitInterceptor = achsInitInterceptor, - ).initialized() - } yield (indexer, killSwitch, participantContractStore, dbSupport, inMemoryState) - - private def indexResourceOwner( - config: IndexerConfig, - serviceConfig: IndexServiceConfig, - repairMode: Boolean, - incompleteOffsets: Seq[Offset], - ): ResourceOwner[ - (IndexService, FutureQueue[Update], LedgerApiContractStoreImpl, DbSupport, InMemoryState) - ] = - for { - (indexerF, _, participantContractStore, dbSupport, inMemoryState) <- - jdbcIndexerResourceOwner(config, serviceConfig, achsInitInterceptor = identity) - indexerFutureQueueConsumer <- ResourceOwner.forFuture(() => indexerF(repairMode)(_ => ())) - indexer <- ResourceOwner.forReleasable(() => - new IndexingFutureQueue(indexerFutureQueueConsumer) - ) { indexer => - indexer.shutdown() - indexer.done.map(_ => ()) - } - contractLoader <- ContractLoader.create( - participantContractStore = participantContractStore, - contractStorageBackend = dbSupport.storageBackendFactory.createContractStorageBackend( - inMemoryState.stringInterningView, - inMemoryState.ledgerEndCache, - ), - dbDispatcher = dbSupport.dbDispatcher, - metrics = LedgerApiServerMetrics.ForTesting, - maxQueueSize = 10000, - maxBatchSize = 50, - parallelism = 5, - loggerFactory = loggerFactory, - ) - indexService <- new IndexServiceOwner( - dbSupport = dbSupport, - config = serviceConfig, - participantId = Ref.ParticipantId.assertFromString(IndexComponentTest.TestParticipantId), - metrics = LedgerApiServerMetrics.ForTesting, - inMemoryState = inMemoryState, - tracer = NoReportingTracerProvider.tracer, - loggerFactory = loggerFactory, - incompleteOffsets = (_, _, _) => FutureUnlessShutdown.pure(incompleteOffsets.toVector), - contractLoader = contractLoader, - getPackageMetadataSnapshot = _ => PackageMetadata(), - lfValueTranslation = new LfValueTranslation( - metrics = LedgerApiServerMetrics.ForTesting, - engineO = Some(engine), - loadPackage = (packageId, _) => Future.successful(packageMap.get(packageId)), - loggerFactory = loggerFactory, - ), - queryExecutionContext = executorService, - commandExecutionContext = executorService, - getPackagePreference = ( - _: PackageName, - _: Set[PackageId], - _: String, - _: LoggingContextWithTrace, - ) => FutureUnlessShutdown.pure(Left("not used")), - participantContractStore = participantContractStore, - materializer = materializer, - updateServiceConfig = updateServiceConfig, - scheduler = system.scheduler, - ) - } yield (indexService, indexer, participantContractStore, dbSupport, inMemoryState) - - protected def indexerResourceOwner( - config: IndexerConfig, - achsInitInterceptor: scaladsl.Source[AchsWorkRange, NotUsed] => scaladsl.Source[ - AchsWorkRange, - NotUsed, - ], - ): ResourceOwner[(Indexer, Option[() => Unit], DbSupport)] = - jdbcIndexerResourceOwner(config, indexServiceConfig, achsInitInterceptor).map { - case (indexer, killSwitch, _, dbSupport, _) => - (indexer, killSwitch, dbSupport) - } - - protected def acquireServices( - config: IndexerConfig, - serviceConfig: IndexServiceConfig, - repairMode: Boolean, - incompleteOffsets: Seq[Offset], - )(implicit resourceContext: ResourceContext): Unit = { - val indexResource = - indexResourceOwner(config, serviceConfig, repairMode, incompleteOffsets).acquire() - val (index, indexer, participantContractStore, dbSupport, inMemoryState) = - indexResource.asFuture.futureValue(timeout = PatienceConfiguration.Timeout(60.seconds)) - - testServicesRef.set( - TestServices( - indexResource = indexResource, - index = index, - indexer = indexer, - participantContractStore = participantContractStore, - dbSupport = dbSupport, - inMemoryState = inMemoryState, - ) - ) - } - - /** Restarts the indexer and all related services by releasing and re-acquiring all resources. - * Optionally accepts a new IndexerConfig to change the configuration on restart. - */ - protected def restartIndexer( - config: IndexerConfig = indexerConfig, - serviceConfig: IndexServiceConfig = indexServiceConfig, - repairMode: Boolean = false, - incompleteOffsets: Seq[Offset] = Vector.empty, - ): Unit = { - implicit val resourceContext: ResourceContext = ResourceContext(system.dispatcher) - testServices.indexResource.release().futureValue - acquireServices(config, serviceConfig, repairMode, incompleteOffsets) - } - - override protected def beforeAll(): Unit = { - super.beforeAll() - // We use the dispatcher here because the default Scalatest execution context is too slow. - implicit val resourceContext: ResourceContext = ResourceContext(system.dispatcher) - acquireServices( - config = indexerConfig, - serviceConfig = indexServiceConfig, - repairMode = false, - incompleteOffsets = Vector.empty, - ) - } - - override def afterAll(): Unit = { - testServices.indexResource.release().futureValue - super.afterAll() - } - - protected object TxBuilder { - def apply(): NodeIdTransactionBuilder & TestNodeBuilder = new NodeIdTransactionBuilder - with TestNodeBuilder - } - - protected val synchronizer1: SynchronizerId = SynchronizerId.tryFromString("x::synchronizer1") - protected val synchronizer2: SynchronizerId = SynchronizerId.tryFromString("x::synchronizer2") - protected val packageName: Ref.PackageName = Ref.PackageName.assertFromString("-package-name-") - protected val dsoParty: ValueParty = - ValueParty(Ref.Party.assertFromString("dsoParty")) // sees all - private lazy val parties: Seq[ValueParty] = - (1 to 10000).view - .map(index => Ref.Party.assertFromString(s"party$index")) - .map(p => ValueParty(p)) - .toVector - protected lazy val templates: Seq[Ref.FullReference[PackageId]] = - (1 to 300).view - .map(index => Ref.Identifier.assertFromString(s"$testDarPackageHash:M:T$index")) - .toVector - - private val someLFHash = com.digitalasset.daml.lf.crypto.Hash - .assertFromString("01cf85cfeb36d628ca2e6f583fa2331be029b6b28e877e1008fb3f862306c086") - - private val random = new scala.util.Random - protected def randomString(length: Int) = - String.valueOf(random.alphanumeric.take(length).toArray) - - private def randomTemplate = templates(random.nextInt(templates.size)) - private def randomParty = parties(random.nextInt(parties.size)) - private def randomHash: Hash = Hash.digest( - HashPurpose.PreparedSubmission, - ByteString.copyFromUtf8(s"${random.nextLong()}"), - Sha256, - ) - protected def randomUpdateId: UpdateId = TestUpdateId(randomHash.toHexString) - private def randomLength(lengthFromToInclusive: (Int, Int)) = { - val (from, to) = lengthFromToInclusive - val randomDistance = to - from + 1 - assert(randomDistance > 1, s"random range ($from, $to) must have length of at least 1") - from + random.nextInt(randomDistance) - } - private val builder = TxBuilder() - private val testAcsChangeFactory = TestAcsChangeFactory() - - protected def createsAndArchives( - nextRecordTime: () => CantonTimestamp, - txSize: Int, - txsCreatedThenArchived: Int, - txsCreatedNotArchived: Int, - createPayloadLength: Int, - archiveArgumentPayloadLengthFromTo: (Int, Int), - archiveResultPayloadLengthFromTo: (Int, Int), - ): Vector[(Update.SequencedTransactionAccepted, Vector[ContractInstance])] = { - val (createTxs, contracts) = - (1 to txsCreatedThenArchived + txsCreatedNotArchived).iterator - .map(_ => - creates( - recordTime = nextRecordTime, - payloadLength = createPayloadLength, - )(txSize) - ) - .toVector - .unzip - val archivingTxs = contracts.iterator - .take(txsCreatedThenArchived) - .map(_.map(_.inst.toCreateNode)) - .map( - archives( - recordTime = nextRecordTime, - argumentLength = randomLength(archiveArgumentPayloadLengthFromTo), - resultLength = randomLength(archiveResultPayloadLengthFromTo), - ) - ) - .toVector - createTxs.zip(contracts) ++ archivingTxs.map(_ -> Vector.empty) - } - - protected def creates(recordTime: () => CantonTimestamp, payloadLength: Int)( - size: Int - ): (Update.SequencedTransactionAccepted, Vector[ContractInstance]) = { - val recordTimeAndLedgerEffectiveTime = recordTime() - val txBuilder = TxBuilder() - val contracts = - createContracts(payloadLength, size, recordTimeAndLedgerEffectiveTime.underlying) - contracts.map(_.inst.toCreateNode).foreach(txBuilder.add) - val tx = txBuilder.buildCommitted() - transaction( - synchronizerId = synchronizer1, - recordTime = recordTimeAndLedgerEffectiveTime, - )(tx, contracts) -> contracts - } - - private def createContracts( - payloadLength: Int, - size: Int, - ledgerEffectiveTime: Time.Timestamp, - ) = - (1 to size) - .map(_ => - genContract( - argumentPayload = randomString(payloadLength), - template = randomTemplate, - signatories = Set( - dsoParty, - randomParty, - randomParty, - randomParty, - ), - ledgerEffectiveTime = ledgerEffectiveTime, - ) - ) - .toVector - - protected def ingestPartyOnboarding(parties: Set[String], recordTime: CantonTimestamp): Offset = { - val topologyTransaction = TopologyTransactionEffective( - updateId = randomUpdateId, - events = parties.map(party => - PartyToParticipantAuthorization( - party = Ref.Party.assertFromString(party), - participant = Ref.ParticipantId.assertFromString("participant"), - authorizationEvent = Onboarding(AuthorizationLevel.Observation), - ) - ), - synchronizerId = synchronizer1, - effectiveTime = recordTime, - ) - val ledgerEndBeforeTopology = index.currentLedgerEnd().futureValue - ingestUpdateAsync(topologyTransaction).futureValue - eventually() { - val ledgerEndAfterTopology = index.currentLedgerEnd().futureValue - ledgerEndAfterTopology should be > ledgerEndBeforeTopology - ledgerEndAfterTopology.value - } - } - - protected def repairCreates(recordTime: () => CantonTimestamp, payloadLength: Int)( - size: Int - ): (Update.RepairTransactionAccepted, Vector[ContractInstance]) = { - val (sequenced, contracts) = creates(recordTime, payloadLength)(size) - repairTransaction(sequenced) -> contracts - } - - protected def archives( - recordTime: () => CantonTimestamp, - argumentLength: Int, - resultLength: Int, - )( - creates: Seq[Node.Create] - ): Update.SequencedTransactionAccepted = { - val txBuilder = TxBuilder() - val archives = creates.iterator - .map(archiveCreatedContract(argumentLength, resultLength)) - .toVector - archives.foreach(txBuilder.add) - val tx = txBuilder.buildCommitted() - transaction( - synchronizerId = synchronizer1, - recordTime = recordTime(), - )(tx, Nil) - } - - private def archiveCreatedContract(argumentLength: Int, resultLength: Int)( - create: Node.Create - ): Node.Exercise = - archive( - create = create, - actingParties = Set( - randomParty, - randomParty, - randomParty, - ), - argumentPayload = randomString(argumentLength), - resultPayload = randomString(resultLength), - ) - def genContract( - argumentPayload: String, - template: Ref.Identifier, - signatories: Set[ValueParty], - ledgerEffectiveTime: Time.Timestamp, - ): ContractInstance = - ExampleContractFactory - .build( - templateId = template, - argument = Value.ValueRecord( - tycon = None, - fields = ImmArray( - None -> Value.ValueText(argumentPayload), - None -> Value.ValueList(FrontStack.from(signatories)), - ), - ), - signatories = signatories.map(_.value), - stakeholders = signatories.map(_.value), - packageName = packageName, - createdAt = CreationTime.CreatedAt(ledgerEffectiveTime), - ) - - private def archive( - create: Node.Create, - actingParties: Set[ValueParty], - argumentPayload: String, - resultPayload: String, - ): platform.Exercise = { - val id = create.templateId.qualifiedName.name.toString.substring(1).toInt // Skip T in T123 - builder.exercise( - contract = create, - choice = choiceNames(id - 1), - consuming = true, - actingParties = actingParties.map(_.value), - argument = Value.ValueRecord( - tycon = None, - fields = ImmArray(None -> Value.ValueText(argumentPayload)), - ), - byKey = false, - interfaceId = None, - result = Some(Value.ValueText(resultPayload)), - ) - } - - protected def transaction( - synchronizerId: SynchronizerId, - recordTime: CantonTimestamp, - )( - transaction: CommittedTransaction, - contracts: Seq[ContractInstance], - ): Update.SequencedTransactionAccepted = - Update.SequencedTransactionAccepted( - completionInfoO = None, - transactionMeta = TransactionMeta( - ledgerEffectiveTime = recordTime.underlying, - workflowId = None, - preparationTime = recordTime.underlying, - submissionSeed = someLFHash, - timeBoundaries = LedgerTimeBoundaries.unconstrained, - optUsedPackages = None, - optNodeSeeds = None, - optByKeyNodes = None, - ), - transactionInfo = Update.TransactionAccepted.TransactionInfo(transaction), - updateId = randomUpdateId, - synchronizerId = synchronizerId, - recordTime = recordTime, - acsChangeFactory = testAcsChangeFactory, - externalTransactionHash = None, - contractInfos = contracts.view.map { contract => - contract.contractId -> ContractInfo( - representativePackageId = SameAsContractPackageId, - persistedContractInstance = PersistedContractInstance( - inst = contract.inst, - internalContractId = -1L, // will be filled later - ), - ) - }.toMap, - ) - - protected def mkReassignmentAccepted( - party: Ref.Party, - updateIdS: String, - withAcsChange: Boolean, - contracts: Seq[ContractInstance], - ): Update.ReassignmentAccepted = { - val synchronizer1 = SynchronizerId.tryFromString("x::synchronizer1") - val synchronizer2 = SynchronizerId.tryFromString("x::synchronizer2") - val updateId = TestUpdateId(updateIdS) - val recordTime = Time.Timestamp.now() - if (withAcsChange) - Update.OnPRReassignmentAccepted( - workflowId = None, - updateId = updateId, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = Source(synchronizer1), - targetSynchronizer = Target(synchronizer2), - submitter = Option(party), - reassignmentId = ReassignmentId.tryCreate("00"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Assign( - reassignmentCounter = 15L, - nodeId = 0, - persistedContractInstance = PersistedContractInstance( - internalContractId = - -1, // will be filled when contracts are stored in the participant contract store - inst = contracts.head.inst, - ), - ), - contracts.tail.map(contractInstance => - Reassignment.Assign( - reassignmentCounter = 15L, - nodeId = 0, - persistedContractInstance = PersistedContractInstance( - // will be filled when contracts are stored in the participant contract store - internalContractId = -1, - inst = contractInstance.inst, - ), - ) - )* - ), - repairCounter = RepairCounter.Genesis, - recordTime = CantonTimestamp(recordTime), - synchronizerId = synchronizer2, - acsChangeFactory = TestAcsChangeFactory(), - ) - else - Update.RepairReassignmentAccepted( - workflowId = None, - updateId = updateId, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = Source(synchronizer1), - targetSynchronizer = Target(synchronizer2), - submitter = Option(party), - reassignmentId = ReassignmentId.tryCreate("00"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Assign( - reassignmentCounter = 15L, - nodeId = 0, - persistedContractInstance = PersistedContractInstance( - // will be filled when contracts are stored in the participant contract store - internalContractId = -1, - inst = contracts.head.inst, - ), - ), - contracts.tail.map(contractInstance => - Reassignment.Assign( - reassignmentCounter = 15L, - nodeId = 0, - persistedContractInstance = PersistedContractInstance( - // will be filled when contracts are stored in the participant contract store - internalContractId = -1, - inst = contractInstance.inst, - ), - ) - )* - ), - repairCounter = RepairCounter.Genesis, - recordTime = CantonTimestamp(recordTime), - synchronizerId = synchronizer2, - ) - } - - protected def repairTransaction( - sequenced: Update.SequencedTransactionAccepted - ): Update.RepairTransactionAccepted = - Update.RepairTransactionAccepted( - transactionMeta = sequenced.transactionMeta, - transactionInfo = sequenced.transactionInfo, - updateId = sequenced.updateId, - synchronizerId = sequenced.synchronizerId, - repairCounter = RepairCounter.Genesis, - recordTime = sequenced.recordTime, - contractInfos = sequenced.contractInfos, - ) - - protected def activeContractIds(activeAt: Long): Seq[(String, Long)] = - index - .getActiveContracts( - eventFormat = allPartyEventFormat, - activeAt = Some(Offset.tryFromLong(activeAt)), - rangeInfo = AcsRangeInfo.empty, - ) - .runWith(Sink.seq) - .futureValue - .flatMap( - _.contractEntry.activeContract - .flatMap(_.createdEvent.map(event => event.contractId -> event.offset)) - ) -} - -object IndexComponentTest { - - val TestParticipantId = "index-component-test-participant-id" - - final case class TestServices( - indexResource: Resource[Any], - index: IndexService, - indexer: FutureQueue[Update], - participantContractStore: LedgerApiContractStoreImpl, - dbSupport: DbSupport, - inMemoryState: InMemoryState, - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/NonUniqueContractKeyIndexComponentTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/NonUniqueContractKeyIndexComponentTest.scala deleted file mode 100644 index c3fc1a3cab..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/NonUniqueContractKeyIndexComponentTest.scala +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.ledger.participant.state.index.ContractKeyPage -import com.digitalasset.canton.ledger.participant.state.{ - Reassignment, - ReassignmentInfo, - TestAcsChangeFactory, - Update, -} -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.platform.ContractId -import com.digitalasset.canton.protocol.{ - ContractInstance, - ExampleContractFactory, - ReassignmentId, - TestUpdateId, -} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.{ImmArray, Ref} -import com.digitalasset.daml.lf.transaction.{GlobalKey, GlobalKeyWithMaintainers} -import com.digitalasset.daml.lf.value.Value -import org.scalatest.flatspec.AnyFlatSpec - -import scala.annotation.tailrec - -class NonUniqueContractKeyIndexComponentTest extends AnyFlatSpec with IndexComponentTest { - behavior of "Non unique contract lookup" - - it should "successfully look up contract keys" in { - val party = Ref.Party.assertFromString("party1") - - val key1 = GlobalKeyWithMaintainers( - globalKey = GlobalKey.assertBuild( - templateId = ExampleContractFactory.templateId, - key = Value.ValueInt64(10), - packageName = ExampleContractFactory.packageName, - keyHash = crypto.Hash.hashPrivateKey("1"), - ), - maintainers = Set(party), - ) - - val key2 = GlobalKeyWithMaintainers( - globalKey = GlobalKey.assertBuild( - templateId = ExampleContractFactory.templateId, - key = Value.ValueInt64(20), - packageName = ExampleContractFactory.packageName, - keyHash = crypto.Hash.hashPrivateKey("2"), - ), - maintainers = Set(party), - ) - - def contract( - keyWithMaintainers: Option[GlobalKeyWithMaintainers], - index: Long, - ): ContractInstance = - ExampleContractFactory.build( - stakeholders = Set(party), - signatories = Set(party), - templateId = Ref.Identifier.assertFromString("P:M:T"), - argument = Value.ValueRecord( - tycon = None, - fields = ImmArray(None -> Value.ValueInt64(index)), - ), - keyOpt = keyWithMaintainers, - ) - - val synchronizer1 = SynchronizerId.tryFromString("x::synchronizer1") - val synchronizer2 = SynchronizerId.tryFromString("x::synchronizer2") - - def reassignmentAccepted( - updateIdString: String, - recordTime: CantonTimestamp, - synchronizerId: SynchronizerId, - sourceSynchronizerId: SynchronizerId, - targetSynchronizerId: SynchronizerId, - reassignmentBatch: Reassignment.Batch, - ): Update.SequencedReassignmentAccepted = - Update.SequencedReassignmentAccepted( - workflowId = None, - updateId = TestUpdateId(updateIdString), - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = Source(sourceSynchronizerId), - targetSynchronizer = Target(targetSynchronizerId), - submitter = Option(party), - reassignmentId = ReassignmentId.tryCreate("00"), - isReassigningParticipant = true, - ), - reassignment = reassignmentBatch, - recordTime = recordTime, - synchronizerId = synchronizerId, - acsChangeFactory = TestAcsChangeFactory(), - optCompletionInfo = None, - ) - - def assigns( - updateIdString: String, - recordTime: CantonTimestamp, - contracts: Vector[ContractInstance], - ): (Update.SequencedReassignmentAccepted, Vector[ContractInstance]) = - reassignmentAccepted( - updateIdString = updateIdString, - recordTime = recordTime, - synchronizerId = synchronizer2, - sourceSynchronizerId = synchronizer1, - targetSynchronizerId = synchronizer2, - reassignmentBatch = Reassignment.Batch( - NonEmpty - .from(contracts.zipWithIndex.map { case (contract, index) => - Reassignment.Assign( - reassignmentCounter = 15L, - nodeId = index, - persistedContractInstance = PersistedContractInstance( - // will be filled when contracts are stored in the participant contract store - internalContractId = -1, - inst = contract.inst, - ), - ) - }.toSeq) - .value - ), - ) -> contracts - - def unassigns( - updateIdString: String, - recordTime: CantonTimestamp, - contracts: Vector[ContractInstance], - ): (Update.SequencedReassignmentAccepted, Vector[ContractInstance]) = - reassignmentAccepted( - updateIdString = updateIdString, - recordTime = recordTime, - synchronizerId = synchronizer2, - sourceSynchronizerId = synchronizer2, - targetSynchronizerId = synchronizer1, - reassignmentBatch = Reassignment.Batch( - NonEmpty - .from(contracts.zipWithIndex.map { case (contract, index) => - Reassignment.Unassign( - contractId = contract.contractId, - templateId = contract.templateId, - packageName = contract.inst.packageName, - stakeholders = contract.stakeholders, - assignmentExclusivity = None, - reassignmentCounter = 15L, - nodeId = index, - ) - }.toSeq) - .value - ), - ) -> Vector.empty - - val contractNoKey1 = contract(None, 1) - val contractNoKey2 = contract(None, 2) - val contractKey1 = contract(Some(key1), 10) - val contractKey2 = contract(Some(key1), 20) - val contractKey3 = contract(Some(key1), 30) - val contractKey4 = contract(Some(key1), 40) - val contractKey5 = contract(Some(key1), 50) - val contractKey6 = contract(Some(key1), 60) - val contractKey7 = contract(Some(key1), 70) - val contractKey8 = contract(Some(key1), 80) - val contractKey9 = contract(Some(key1), 90) - val contractKey10 = contract(Some(key1), 100) - val contractOtherKey1 = contract(Some(key2), 1000) - val contractOtherKey2 = contract(Some(key2), 2000) - - val baseRecordTime = CantonTimestamp.now() - - val assign1 = assigns( - updateIdString = "assigns1", - recordTime = baseRecordTime, - contracts = Vector( - contractNoKey1, - contractKey1, - contractKey2, - contractKey3, - contractKey4, - contractKey5, - contractOtherKey1, - ), - ) - - val unassign1 = unassigns( - updateIdString = "unassigns1", - recordTime = baseRecordTime.plusSeconds(1), - contracts = Vector( - contractKey1 - ), - ) - - val assign2 = assigns( - updateIdString = "assigns2", - recordTime = baseRecordTime.plusSeconds(2), - contracts = Vector( - contractNoKey2, - contractKey6, - contractKey7, - contractKey8, - contractKey9, - contractKey10, - contractOtherKey2, - ), - ) - - val unassign2 = unassigns( - updateIdString = "unassigns2", - recordTime = baseRecordTime.plusSeconds(3), - contracts = Vector( - contractKey10, - contractOtherKey2, - contractNoKey1, - ), - ) - - ingestUpdates( - assign1, - unassign1, - assign2, - unassign2, - ) - - def lookupNonUniqueKeys( - key: GlobalKeyWithMaintainers, - limit: Int, - token: Option[Long], - ): ContractKeyPage = - index - .lookupNonUniqueContractKey( - readers = key.maintainers, - key = key.globalKey, - pageToken = token, - limit = limit, - ) - .futureValue - - @tailrec - def nuckPages( - key: GlobalKeyWithMaintainers, - limit: Int, - )( - page: ContractKeyPage = lookupNonUniqueKeys(key, limit, None) - )( - acc: Vector[Vector[ContractId]] = Vector.empty - ): Vector[Vector[ContractId]] = - page.nextPageToken match { - case Some(l) => - nuckPages(key, limit)(lookupNonUniqueKeys(key, limit, Some(l)))( - acc appended page.contracts.map(_.contractId) - ) - case None => acc appended page.contracts.map(_.contractId) - } - - def coids(cs: Vector[ContractInstance]*): Vector[Vector[ContractId]] = - cs.map(_.map(_.contractId)).toVector - - nuckPages(key1, 100)()() shouldBe coids( - Vector( - contractKey9, - contractKey8, - contractKey7, - contractKey6, - contractKey5, - contractKey4, - contractKey3, - contractKey2, - ) - ) - nuckPages(key1, 8)()() shouldBe coids( - Vector( - contractKey9, - contractKey8, - contractKey7, - contractKey6, - contractKey5, - contractKey4, - contractKey3, - contractKey2, - ) - ) - nuckPages(key1, 7)()() shouldBe coids( - Vector( - contractKey9, - contractKey8, - contractKey7, - contractKey6, - contractKey5, - contractKey4, - contractKey3, - ), - Vector( - contractKey2 - ), - ) - nuckPages(key1, 4)()() shouldBe coids( - Vector( - contractKey9, - contractKey8, - contractKey7, - contractKey6, - ), - Vector( - contractKey5, - contractKey4, - contractKey3, - contractKey2, - ), - ) - nuckPages(key1, 3)()() shouldBe coids( - Vector( - contractKey9, - contractKey8, - contractKey7, - ), - Vector( - contractKey6, - contractKey5, - contractKey4, - ), - Vector( - contractKey3, - contractKey2, - ), - ) - nuckPages(key1, 2)()() shouldBe coids( - Vector( - contractKey9, - contractKey8, - ), - Vector( - contractKey7, - contractKey6, - ), - Vector( - contractKey5, - contractKey4, - ), - Vector( - contractKey3, - contractKey2, - ), - ) - nuckPages(key1, 1)()() shouldBe coids( - Vector(contractKey9), - Vector(contractKey8), - Vector(contractKey7), - Vector(contractKey6), - Vector(contractKey5), - Vector(contractKey4), - Vector(contractKey3), - Vector(contractKey2), - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/PersistenceSqlQueries.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/PersistenceSqlQueries.scala deleted file mode 100644 index b504370b3f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/PersistenceSqlQueries.scala +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import anorm.SqlParser.long -import anorm.{SqlStringInterpolation, ~} - -trait PersistenceSqlQueries { - self: IndexComponentTest => - - protected def getAchsEventSeqIds: List[Long] = - withConnection { implicit connection => - SQL"""SELECT event_sequential_id - FROM lapi_filter_achs_stakeholder - ORDER BY event_sequential_id""" - .as(long("event_sequential_id").*) - } - - protected def getActivateEventSeqIds: List[Long] = - withConnection { implicit connection => - SQL"""SELECT event_sequential_id - FROM lapi_filter_activate_stakeholder - ORDER BY event_sequential_id""" - .as(long("event_sequential_id").*) - } - - protected def offsetForActivateContractEventSeqId(seqId: Long): Long = - withConnection { implicit connection => - SQL"""SELECT MAX(event_offset) AS max_offset - FROM lapi_events_activate_contract - WHERE event_sequential_id <= $seqId""" - .as(long("max_offset").?.single) - .getOrElse(0L) - } - - protected def eventSeqIdForActivateContractOffset(offset: Long): Long = - withConnection { implicit connection => - SQL"""SELECT MAX(event_sequential_id) AS max_seq_id - FROM lapi_events_activate_contract - WHERE event_offset <= $offset""" - .as(long("max_seq_id").?.single) - .getOrElse(0L) - } - - protected def getLastEventSeqId: Long = - withConnection { implicit connection => - SQL"SELECT ledger_end_sequential_id FROM lapi_parameters" - .as(long("ledger_end_sequential_id").?.single) - .getOrElse(0L) - } - - protected def getAchsState: (Long, Long, Long) = - withConnection { implicit connection => - SQL"SELECT valid_at, last_populated, last_removed FROM lapi_achs_state" - .as((long("valid_at") ~ long("last_populated") ~ long("last_removed")).single)( - connection - ) match { - case v ~ lp ~ lr => (v, lp, lr) - } - } - - protected def getAchsValidAt: Long = getAchsState._1 - - protected def getAchsSize: Long = - withConnection { implicit connection => - SQL"SELECT COUNT(DISTINCT event_sequential_id) AS count FROM lapi_filter_achs_stakeholder" - .as(long("count").single) - } - - protected def getAchsStateRowCount: Long = - withConnection { implicit connection => - SQL"SELECT COUNT(*) AS count FROM lapi_achs_state" - .as(long("count").single) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/SingleStepIncreasingRecordTime.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/SingleStepIncreasingRecordTime.scala deleted file mode 100644 index aa109c1882..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/SingleStepIncreasingRecordTime.scala +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import com.digitalasset.canton.data.CantonTimestamp - -import java.util.concurrent.atomic.AtomicReference - -final class SingleStepIncreasingRecordTime() extends (() => CantonTimestamp) { - private val recordTimeRef = new AtomicReference(CantonTimestamp.now()) - override def apply(): CantonTimestamp = recordTimeRef.updateAndGet(_.immediateSuccessor) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdatePagesComponentTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdatePagesComponentTest.scala deleted file mode 100644 index 7969a46dac..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdatePagesComponentTest.scala +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import com.digitalasset.base.error.ErrorCode.LoggedApiException -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.* -import com.digitalasset.canton.ledger.api.TransactionShape.LedgerEffects -import com.digitalasset.canton.ledger.api.messages.update.GetUpdatesPageRequest -import com.google.protobuf.ByteString -import org.scalatest.wordspec.AnyWordSpec - -class UpdatePagesComponentTest extends AnyWordSpec with IndexComponentTest { - private val nextRecordTime = new SingleStepIncreasingRecordTime - - def updateFormat(transactionShape: TransactionShape) = UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = EventFormat( - filtersByParty = Map(dsoParty.value -> CumulativeFilter.templateWildcardFilter(true)), - filtersForAnyParty = None, - verbose = false, - ), - transactionShape = transactionShape, - ) - ), - includeReassignments = None, - includeTopologyEvents = None, - ) - - "update pages ascending with dynamic bounds" should { - "return an empty pages with an empty ledger" in { - val request = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = None, - continueStreamFromIncl = None, - maxPageSize = 20, - updateFormat = updateFormat(LedgerEffects), - descendingOrder = false, - requestChecksum = ByteString.EMPTY, - participantChecksum = ByteString.EMPTY, - ) - restartIndexer() - val page = index.updatesPage(request).futureValue - - page.updates shouldBe empty - page.nextPageToken shouldNot be(empty) - page.lowestPageOffsetExclusive shouldEqual 0 - page.highestPageOffsetInclusive shouldEqual 0 - } - } - "update pages descending with dynamic bounds" should { - "return an empty pages with an empty ledger" in { - val request = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = None, - continueStreamFromIncl = None, - maxPageSize = 20, - updateFormat = updateFormat(LedgerEffects), - descendingOrder = true, - requestChecksum = ByteString.EMPTY, - participantChecksum = ByteString.EMPTY, - ) - restartIndexer() - val page = index.updatesPage(request).futureValue - - page.updates shouldBe empty - page.nextPageToken shouldBe empty - page.lowestPageOffsetExclusive shouldEqual 0 - page.highestPageOffsetInclusive shouldEqual 0 - } - - "not throw an exception if hitting pruning offset in a middle of a page" in { - val request = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = None, - continueStreamFromIncl = None, - maxPageSize = 2, - updateFormat = updateFormat(LedgerEffects), - descendingOrder = true, - requestChecksum = ByteString.EMPTY, - participantChecksum = ByteString.EMPTY, - ) - val create1 = creates(nextRecordTime, 10)(1) - val create2 = creates(nextRecordTime, 10)(1) - val create3 = creates(nextRecordTime, 10)(1) - val create4 = creates(nextRecordTime, 10)(1) - - val pruneTo = ingestUpdates(create1) - ingestUpdates(create2) - ingestUpdates(create3) - ingestUpdates(create4) - - val firstPage = index.updatesPage(request).futureValue - index - .prune(pruningOffsetService.pruningOffset.futureValue, Vector(), pruneTo, Vector()) - .futureValue - eventually() { - index.indexDbPrunedUpto.futureValue shouldEqual Some(pruneTo) - } - val secondPage = index - .updatesPage( - request.copy(continueStreamFromIncl = - Some(Offset.tryFromLong(firstPage.lowestPageOffsetExclusive)) - ) - ) - .futureValue - - firstPage.updates should have length 2 - secondPage.updates should have length 1 - secondPage.nextPageToken shouldBe empty - } - - "not throw an exception if hitting pruning index after page was fetched" in { - val request = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = None, - continueStreamFromIncl = None, - maxPageSize = 3, - updateFormat = updateFormat(LedgerEffects), - descendingOrder = true, - requestChecksum = ByteString.EMPTY, - participantChecksum = ByteString.EMPTY, - ) - val create1 = creates(nextRecordTime, 10)(1) - val create2 = creates(nextRecordTime, 10)(1) - val create3 = creates(nextRecordTime, 10)(1) - val create4 = creates(nextRecordTime, 10)(1) - - ingestUpdates(create1) - val pruneTo = ingestUpdates(create2) - ingestUpdates(create3) - ingestUpdates(create4) - - val firstPage = index.updatesPage(request).futureValue - index - .prune(pruningOffsetService.pruningOffset.futureValue, Vector(), pruneTo, Vector()) - .futureValue - eventually() { - index.indexDbPrunedUpto.futureValue shouldEqual Some(pruneTo) - } - val secondPage = index - .updatesPage( - request.copy(continueStreamFromIncl = - Some(Offset.tryFromLong(firstPage.lowestPageOffsetExclusive)) - ) - ) - .futureValue - - firstPage.updates should have length 3 - secondPage.updates should have length 0 - secondPage.nextPageToken shouldBe empty - } - } - - "update with ascending pages" should { - "not fail if pruning bound does not catch up with current page generation" in { - val create1 = creates(nextRecordTime, 10)(1) - val create2 = creates(nextRecordTime, 10)(1) - val create3 = creates(nextRecordTime, 10)(1) - val create4 = creates(nextRecordTime, 10)(1) - - val fetchFrom = ingestUpdates(create1) - val pruneTo = ingestUpdates(create2) - ingestUpdates(create3) - val fetchTo = ingestUpdates(create4) - - val request = GetUpdatesPageRequest( - startExclusive = Some(fetchFrom.decrement), - endInclusive = Some(fetchTo), - continueStreamFromIncl = None, - maxPageSize = 2, - updateFormat = updateFormat(LedgerEffects), - descendingOrder = false, - requestChecksum = ByteString.EMPTY, - participantChecksum = ByteString.EMPTY, - ) - - val firstPage = index.updatesPage(request).futureValue - index - .prune(pruningOffsetService.pruningOffset.futureValue, Vector(), pruneTo, Vector()) - .futureValue - eventually() { - index.indexDbPrunedUpto.futureValue shouldEqual Some(pruneTo) - } - val secondPage = index - .updatesPage( - request.copy(continueStreamFromIncl = - Some(Offset.tryFromLong(firstPage.highestPageOffsetInclusive + 1)) - ) - ) - .futureValue - - firstPage.updates should have length 2 - secondPage.updates should have length 2 - secondPage.nextPageToken shouldBe empty - } - - "fail if pruning bound does catches with current page generation" in { - val create1 = creates(nextRecordTime, 10)(1) - val create2 = creates(nextRecordTime, 10)(1) - val create3 = creates(nextRecordTime, 10)(1) - val create4 = creates(nextRecordTime, 10)(1) - - val fetchFrom = ingestUpdates(create1) - ingestUpdates(create2) - val pruneTo = ingestUpdates(create3) - val fetchTo = ingestUpdates(create4) - val request = GetUpdatesPageRequest( - startExclusive = Some(fetchFrom.decrement), - endInclusive = Some(fetchTo), - continueStreamFromIncl = None, - maxPageSize = 2, - updateFormat = updateFormat(LedgerEffects), - descendingOrder = false, - requestChecksum = ByteString.EMPTY, - participantChecksum = ByteString.EMPTY, - ) - - val firstPage = index.updatesPage(request).futureValue - index - .prune(pruningOffsetService.pruningOffset.futureValue, Vector(), pruneTo, Vector()) - .futureValue - val exception = index - .updatesPage( - request.copy(continueStreamFromIncl = - Some(Offset.tryFromLong(firstPage.lowestPageOffsetExclusive)) - ) - ) - .failed - .futureValue - - exception shouldBe a[LoggedApiException] - firstPage.updates should have length 2 - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdateStreamComponentTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdateStreamComponentTest.scala deleted file mode 100644 index 3709984c63..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdateStreamComponentTest.scala +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import com.digitalasset.canton.ledger.api.* -import com.digitalasset.canton.ledger.api.TransactionShape.LedgerEffects -import org.apache.pekko.stream.scaladsl.Sink -import org.scalatest.wordspec.AnyWordSpec - -class UpdateStreamComponentTest extends AnyWordSpec with IndexComponentTest { - def updateFormat(transactionShape: TransactionShape) = UpdateFormat( - includeTransactions = Some( - TransactionFormat( - eventFormat = EventFormat( - filtersByParty = Map(dsoParty.value -> CumulativeFilter.templateWildcardFilter(true)), - filtersForAnyParty = None, - verbose = false, - ), - transactionShape = transactionShape, - ) - ), - includeReassignments = None, - includeTopologyEvents = None, - ) - private val nextRecordTime = new SingleStepIncreasingRecordTime - - "update stream in reverse order" should { - "stream create transactions" in { - val rangeStart = index.currentLedgerEnd().futureValue - val createContracts = - Vector.tabulate(10)(_ => creates(nextRecordTime, 10)(1)) - val rangeEnd = ingestUpdates(createContracts*) - val updatesStream = index.updates( - begin = rangeStart, - endAt = Some(rangeEnd), - updateFormat = updateFormat(LedgerEffects), - descendingOrder = true, - skipPruningChecks = false, - ) - val updates = updatesStream.runWith(Sink.seq).futureValue - updates.flatMap( - _.update.transaction.value.events.map(_.getCreated.contractId) - ) should contain theSameElementsInOrderAs (createContracts.reverse.flatMap( - _._2.map(_.contractId.coid) - )) - } - - "preserve order of events inside a transaction" in { - val rangeStart = index.currentLedgerEnd().futureValue - val createContracts = - Vector.tabulate(10)(_ => creates(nextRecordTime, 10)(5)) - val rangeEnd = ingestUpdates(createContracts*) - val updatesStream = index.updates( - begin = rangeStart, - endAt = Some(rangeEnd), - updateFormat = updateFormat(LedgerEffects), - descendingOrder = true, - skipPruningChecks = false, - ) - val updates = updatesStream.runWith(Sink.seq).futureValue - updates.map( - _.update.transaction.value.events.map(_.getCreated.contractId) - ) should contain theSameElementsInOrderAs (createContracts.reverse.map( - _._2.map(_.contractId.coid) - )) - } - - "properly order topology events interleaved with create events" in { - val rangeStart = index.currentLedgerEnd().futureValue - val createContractsFirst = - Vector.tabulate(3)(_ => creates(nextRecordTime, 10)(1)) - ingestUpdates(createContractsFirst*) - ingestPartyOnboarding(Set("new-party-1"), nextRecordTime()) - ingestPartyOnboarding(Set("new-party-2"), nextRecordTime()) - val createContractsSecond = - Vector.tabulate(2)(_ => creates(nextRecordTime, 10)(1)) - val rangeEnd = ingestUpdates(createContractsSecond*) - - val updatesStream = index.updates( - begin = rangeStart, - endAt = Some(rangeEnd), - updateFormat = updateFormat(LedgerEffects).copy(includeTopologyEvents = - Some( - TopologyFormat( - Some(ParticipantAuthorizationFormat(None)) - ) - ) - ), - descendingOrder = true, - skipPruningChecks = false, - ) - - val updates = updatesStream.runWith(Sink.seq).futureValue - - updates.map(_.update.isTopologyTransaction) should contain theSameElementsInOrderAs (Seq( - false, false, true, true, false, false, false)) - } - - "property order create events interleaved with reassignments" in { - val rangeStart = index.currentLedgerEnd().futureValue - val create1 = creates(nextRecordTime, 10)(1) - val create2 = creates(nextRecordTime, 10)(1) - - ingestUpdates(create1) - ingestUpdates(create2) - - val reassignment1 = mkReassignmentAccepted( - dsoParty.value, - "upd-id-ra-1", - withAcsChange = true, - create1._2, - ) - ingestUpdateSync(reassignment1) - - val reassignment2 = mkReassignmentAccepted( - dsoParty.value, - "upd-id-ra-2", - withAcsChange = true, - create2._2, - ) - ingestUpdateSync(reassignment2) - - val create3 = creates(nextRecordTime, 10)(1) - val rangeEnd = ingestUpdates(create3) - - val updatesStream = index.updates( - begin = rangeStart, - endAt = Some(rangeEnd), - updateFormat = updateFormat(LedgerEffects).copy(includeReassignments = - Some( - EventFormat( - filtersByParty = Map(), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter(false)), - verbose = false, - ) - ) - ), - descendingOrder = true, - skipPruningChecks = false, - ) - - val updates = updatesStream.runWith(Sink.seq).futureValue - - updates should have size 5 - - updates.map(_.update.isReassignment) should contain theSameElementsInOrderAs Seq( - false, true, true, false, false, - ) - - updates( - 1 - ).update.reassignment.value.events.loneElement.event.assigned.value.createdEvent.value.contractId shouldEqual create2._2.loneElement.contractId.coid - - updates( - 2 - ).update.reassignment.value.events.loneElement.event.assigned.value.createdEvent.value.contractId shouldEqual create1._2.loneElement.contractId.coid - - } - - "preserve order of 2 creates, 2 topology events and 2 reassignments interleaved" in { - val rangeStart = index.currentLedgerEnd().futureValue - val create1 = creates(nextRecordTime, 10)(1) - - ingestUpdates(create1) - ingestPartyOnboarding(Set("new-party-1"), nextRecordTime()) - val reassignment1 = mkReassignmentAccepted( - dsoParty.value, - "upd-id-ra-interleave-1", - withAcsChange = true, - create1._2, - ) - ingestUpdateSync(reassignment1) - - val create2 = creates(nextRecordTime, 10)(1) - ingestUpdates(create2) - ingestPartyOnboarding(Set("new-party-2"), nextRecordTime()) - val reassignment2 = mkReassignmentAccepted( - dsoParty.value, - "upd-id-ra-interleave-2", - withAcsChange = true, - create2._2, - ) - ingestUpdateSync(reassignment2) - - val rangeEnd = index.currentLedgerEnd().futureValue.value - - val updatesStream = index.updates( - begin = rangeStart, - endAt = Some(rangeEnd), - updateFormat = updateFormat(LedgerEffects) - .copy( - includeReassignments = Some( - EventFormat( - filtersByParty = Map(), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter(false)), - verbose = false, - ) - ), - includeTopologyEvents = Some( - TopologyFormat(Some(ParticipantAuthorizationFormat(None))) - ), - ), - descendingOrder = true, - skipPruningChecks = false, - ) - - val updates = updatesStream.runWith(Sink.seq).futureValue - - updates should have size 6 - updates.map(u => - (u.update.isReassignment, u.update.isTopologyTransaction, u.update.transaction.isDefined) - ) should contain theSameElementsInOrderAs Seq( - (true, false, false), // reassignment2 - (false, true, false), // topology2 - (false, false, true), // create2 - (true, false, false), // reassignment1 - (false, true, false), // topology1 - (false, false, true), // create1 - ) - - updates( - 0 - ).update.reassignment.value.events.loneElement.event.assigned.value.createdEvent.value.contractId shouldEqual create2._2.loneElement.contractId.coid - updates( - 1 - ).update.topologyTransaction.value.events.loneElement.getParticipantAuthorizationOnboarding.partyId shouldEqual "new-party-2" - updates( - 2 - ).update.transaction.value.events.loneElement.getCreated.contractId shouldEqual create2._2.loneElement.contractId.coid - updates( - 3 - ).update.reassignment.value.events.loneElement.event.assigned.value.createdEvent.value.contractId shouldEqual create1._2.loneElement.contractId.coid - updates( - 4 - ).update.topologyTransaction.value.events.loneElement.getParticipantAuthorizationOnboarding.partyId shouldEqual "new-party-1" - updates( - 5 - ).update.transaction.value.events.loneElement.getCreated.contractId shouldEqual create1._2.loneElement.contractId.coid - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdateStreamReaderPruningComponentTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdateStreamReaderPruningComponentTest.scala deleted file mode 100644 index e337227f31..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/component/UpdateStreamReaderPruningComponentTest.scala +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.component - -import com.daml.metrics.DatabaseMetrics -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.Ids -import com.digitalasset.canton.platform.store.backend.common.EventPayloadSourceForUpdatesLedgerEffects -import com.digitalasset.canton.platform.store.dao.events.{ - EventsRange, - QueryValidRange, - UpdatesStreamReader, -} -import org.scalatest.flatspec.AnyFlatSpec - -class UpdateStreamReaderPruningComponentTest - extends AnyFlatSpec - with IndexComponentTest - with PersistenceSqlQueries { - private val nextRecordTime = new SingleStepIncreasingRecordTime - - private lazy val eventStorageBackend = dbSupport.storageBackendFactory.createEventStorageBackend( - ledgerEndCache = ledgerEndCache, - stringInterning = stringInterning, - loggerFactory = loggerFactory, - ) - - private val create1 = creates(nextRecordTime, 10)(1) - private val create2 = creates(nextRecordTime, 10)(1) - private val create3 = creates(nextRecordTime, 10)(1) - private val create4 = creates(nextRecordTime, 10)(1) - - private var upd1: Offset = _ - private var upd4: Offset = _ - - override def beforeAll(): Unit = { - super.beforeAll() - upd1 = ingestUpdates(create1) - ingestUpdates(create2) - ingestUpdates(create3) - upd4 = ingestUpdates(create4) - } - - private def readUpdates = - UpdatesStreamReader - .fetchContractPayloadsInternal( - queryRange = EventsRange(upd1, 1, upd4, 1000L), - dbMetric = DatabaseMetrics.ForTesting("test"), - contractStore = contractStore, - skipPruningChecks = true, - ids = Vector.range(1L, 1000L), - fetchEvents = (ids, connection) => - eventStorageBackend - .fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Activate - )( - eventSequentialIds = Ids(ids), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - )(connection), - pruningOffsetService = pruningOffsetService, - queryValidRange = mock[QueryValidRange], - dbDispatcher = dbDispatcher, - ) - .futureValue - - behavior of "UpdateStreamReaderComponent" - - it should "fetch all updates from the beginning when nothing was pruned" in { - val updates = readUpdates - updates should have size 4 - } - - it should "fetch all updates from the beginning when first offset was pruned" in { - index.prune(index.latestPrunedOffset().futureValue, Vector(), upd1, Vector()).futureValue - val updates = readUpdates - updates should have size 3 - } - - it should "fetch empty vector from the beginning when all updates were pruned" in { - index.prune(index.latestPrunedOffset().futureValue, Vector(), upd4, Vector()).futureValue - val updates = readUpdates - updates should be(empty) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/ContractStoreBasedMaximumLedgerTimeServiceSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/ContractStoreBasedMaximumLedgerTimeServiceSpec.scala deleted file mode 100644 index 12adcd0c4f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/ContractStoreBasedMaximumLedgerTimeServiceSpec.scala +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.index - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.participant.state.index.{ - ContractKeyPage, - ContractState, - ContractStore, - MaximumLedgerTime, - MaximumLedgerTimeService, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.* -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Bytes -import com.digitalasset.daml.lf.data.Ref.Party -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.transaction.{ - CreationTime, - GlobalKey, - Node, - SerializationVersion as LfSerializationVersion, -} -import com.digitalasset.daml.lf.value.Value -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.time.Instant -import scala.concurrent.Future - -class ContractStoreBasedMaximumLedgerTimeServiceSpec - extends AsyncFlatSpec - with Matchers - with BaseTest { - - import ContractState.* - import com.digitalasset.canton.ledger.participant.state.index.MaximumLedgerTime.* - - private implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace.ForTesting - - private val timestamp1 = timestampFromInstant(Instant.now()) - private val timestamp2 = timestamp1.addMicros(5000) - private val timestamp3 = timestamp2.addMicros(5000) - private val timestamp4 = timestamp3.addMicros(5000) - - private val contractId1 = hashCid("1") - private val contractId2 = hashCid("2") - private val contractId3 = hashCid("3") - private val contractId4 = hashCid("4") - - behavior of "lookupMaximumLedgerTimeAfterInterpretation" - - it should "find the maximum ledger time on the happy path" in { - testeeWithFixture( - contractId1 -> active(timestamp1), - contractId2 -> active(timestamp2), - contractId3 -> active(timestamp3), - contractId4 -> active(timestamp4), - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1, - contractId2, - contractId3, - contractId4, - ) - ).map( - _ shouldBe Max(timestamp4) - ) - } - - it should "find the maximum ledger time if all contracts are active with same ledger time" in { - testeeWithFixture( - contractId1 -> active(timestamp1), - contractId2 -> active(timestamp1), - contractId3 -> active(timestamp1), - contractId4 -> active(timestamp1), - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1, - contractId2, - contractId3, - contractId4, - ) - ).map( - _ shouldBe Max(timestamp1) - ) - } - - it should "find no maximum ledger time if ids is empty" in { - testeeWithFixture( - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - ) - ).map( - _ shouldBe NotAvailable - ) - } - - it should "find the maximum ledger time if there for only one active contract" in { - testeeWithFixture( - contractId1 -> active(timestamp1) - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1 - ) - ).map( - _ shouldBe Max(timestamp1) - ) - } - - it should "find no maximum ledger time if there are some contracts which cannot be found" in { - testeeWithFixture( - contractId1 -> active(timestamp1), - contractId2 -> NotFound, - contractId3 -> active(timestamp3), - contractId4 -> NotFound, - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1, - contractId2, - contractId3, - contractId4, - ) - ).map( - _ shouldBe MaximumLedgerTime.Archived(Set(contractId2)) - ) - } - - it should "find no maximum ledger time if none of the contracts can be found" in { - testeeWithFixture( - contractId1 -> NotFound, - contractId2 -> NotFound, - contractId3 -> NotFound, - contractId4 -> NotFound, - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1, - contractId2, - contractId3, - contractId4, - ) - ).map( - _ shouldBe MaximumLedgerTime.Archived(Set(contractId1)) - ) - } - - it should "find no maximum ledger time if for the one contract cannot be found" in { - testeeWithFixture( - contractId1 -> NotFound - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1 - ) - ).map( - _ shouldBe MaximumLedgerTime.Archived(Set(contractId1)) - ) - } - - it should "return the archived contract, if one of the contracts is archived" in { - testeeWithFixture( - contractId1 -> active(timestamp1), - contractId2 -> active(timestamp2), - contractId3 -> ContractState.Archived, - contractId4 -> active(timestamp4), - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1, - contractId2, - contractId3, - contractId4, - ) - ).map( - _ shouldBe MaximumLedgerTime.Archived(Set(contractId3)) - ) - } - - it should "return one of the archived contracts, if two of the contracts are archived" in { - testeeWithFixture( - contractId1 -> ContractState.Archived, - contractId2 -> active(timestamp2), - contractId3 -> ContractState.Archived, - contractId4 -> active(timestamp4), - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1, - contractId2, - contractId3, - contractId4, - ) - ).map { result => - inside(result) { case MaximumLedgerTime.Archived(archivedResults) => - archivedResults.size shouldBe 1 - val archivedResult = archivedResults.head - Set(contractId1, contractId3) should contain(archivedResult) - } - } - } - - it should "return one of the archived contracts, if all of the contracts are archived" in { - testeeWithFixture( - contractId1 -> ContractState.Archived, - contractId2 -> ContractState.Archived, - contractId3 -> ContractState.Archived, - contractId4 -> ContractState.Archived, - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1, - contractId2, - contractId3, - contractId4, - ) - ).map { result => - inside(result) { case MaximumLedgerTime.Archived(archivedResults) => - archivedResults.size shouldBe 1 - val archivedResult = archivedResults.head - Set(contractId1, contractId2, contractId3, contractId4) should contain(archivedResult) - } - } - } - - it should "return one of the archived contracts, if some of the contracts are archived, and some cannot be found" in { - testeeWithFixture( - contractId1 -> ContractState.Archived, - contractId2 -> active(timestamp2), - contractId3 -> ContractState.Archived, - contractId4 -> NotFound, - ).lookupMaximumLedgerTimeAfterInterpretation( - Set( - contractId1, - contractId2, - contractId3, - contractId4, - ) - ).map { result => - inside(result) { case MaximumLedgerTime.Archived(archivedResults) => - archivedResults.size shouldBe 1 - val archivedResult = archivedResults.head - Set(contractId1, contractId3) should contain(archivedResult) - } - } - } - - private def hashCid(key: String): ContractId = ContractId.V1(Hash.hashPrivateKey(key)) - - private def timestampFromInstant(i: Instant): Timestamp = Timestamp.assertFromInstant(i) - - private val alice = Party.assertFromString("Alice") - - private val dummyCreate = - Node.Create( - coid = hashCid("dummy"), - packageName = PackageName.assertFromString("dummy"), - templateId = Identifier.assertFromString("abcd:dummy:dummy"), - arg = Value.ValueUnit, - signatories = Set(alice), - stakeholders = Set(alice), - keyOpt = None, - version = LfSerializationVersion.V1, - ) - - private def active(ledgerEffectiveTime: Timestamp): ContractState = - Active( - FatContract.fromCreateNode( - dummyCreate, // we do not care about the payload here - CreationTime.CreatedAt(ledgerEffectiveTime), - Bytes.Empty, - ) - ) - - private def testeeWithFixture(fixture: (ContractId, ContractState)*): MaximumLedgerTimeService = { - val fixtureMap = fixture.toMap - new ContractStoreBasedMaximumLedgerTimeService( - new ContractStore { - override def lookupActiveContract(readers: Set[Party], contractId: ContractId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[FatContract]] = - throw new UnsupportedOperationException - - override def lookupContractKey(readers: Set[Party], key: GlobalKey)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ContractId]] = - throw new UnsupportedOperationException - - override def lookupContractState(contractId: ContractId)(implicit - loggingContext: LoggingContextWithTrace - ): Future[ContractState] = - Future.successful(fixtureMap(contractId)) - - override def lookupNonUniqueContractKey( - readers: Set[Party], - key: Key, - pageToken: Option[Long], - limit: Int, - )(implicit loggingContext: LoggingContextWithTrace): Future[ContractKeyPage] = - throw new UnsupportedOperationException - }, - loggerFactory, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/InMemoryStateUpdaterSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/InMemoryStateUpdaterSpec.scala deleted file mode 100644 index b88706a7c7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/InMemoryStateUpdaterSpec.scala +++ /dev/null @@ -1,1431 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.index - -import cats.data.NonEmptyVector -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.ledger.api.v2.completion.Completion -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.crypto.HashAlgorithm.Sha256 -import com.digitalasset.canton.crypto.{Hash, HashPurpose} -import com.digitalasset.canton.data.{CantonTimestamp, LedgerTimeBoundaries, Offset} -import com.digitalasset.canton.ledger.api.ApiMocks.userId -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.Update.CommandRejected.FinalReason -import com.digitalasset.canton.ledger.participant.state.Update.ContractInfo -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent.Added -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.{ - AuthorizationEvent, - AuthorizationLevel, - TopologyEvent, -} -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId.SameAsContractPackageId -import com.digitalasset.canton.ledger.participant.state.{ - CompletionInfo, - Reassignment, - ReassignmentInfo, - TestAcsChangeFactory, - TransactionMeta, - Update, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.pekkostreams.dispatcher.Dispatcher -import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker -import com.digitalasset.canton.platform.apiserver.services.admin.PartyAllocation -import com.digitalasset.canton.platform.apiserver.services.tracking.SubmissionTracker -import com.digitalasset.canton.platform.index.InMemoryStateUpdater.PrepareResult -import com.digitalasset.canton.platform.index.InMemoryStateUpdaterSpec.* -import com.digitalasset.canton.platform.indexer.parallel.ParallelIndexerSubscription.Batch -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.cache.{ - AchsStateCache, - ContractStateCaches, - InMemoryFanoutBuffer, - MutableLedgerEndCache, - OffsetCheckpoint, - OffsetCheckpointCache, -} -import com.digitalasset.canton.platform.store.dao.events.ContractStateEvent -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate.{ - CreatedEvent, - ReassignmentAccepted, - TransactionAccepted, - TransactionRejected, -} -import com.digitalasset.canton.platform.store.interning.StringInterningView -import com.digitalasset.canton.platform.{DispatcherState, InMemoryState} -import com.digitalasset.canton.protocol.{ - ContractInstance, - ExampleContractFactory, - ReassignmentId, - TestUpdateId, - UpdateId, -} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ReassignmentTag -import com.digitalasset.canton.{BaseTest, HasExecutorServiceGeneric, TestEssentials} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.Ref.Identifier -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.transaction.test.{ - NodeIdTransactionBuilder, - TestNodeBuilder, - TransactionBuilder, -} -import com.digitalasset.daml.lf.transaction.{CommittedTransaction, Node, NodeId} -import com.digitalasset.daml.lf.value.Value -import com.google.protobuf.ByteString -import com.google.rpc.status.Status -import org.apache.pekko.Done -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.mockito.matchers.DefaultValueProvider -import org.mockito.{InOrder, MockitoSugar} -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.concurrent.ConcurrentLinkedQueue -import scala.collection.mutable.ArrayBuffer -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future} -import scala.util.chaining.* - -class InMemoryStateUpdaterSpec - extends AnyFlatSpec - with Matchers - with PekkoBeforeAndAfterAll - with MockitoSugar - with BaseTest { - - import TransactionBuilder.Implicits.* - - object TxBuilder { - def apply(): NodeIdTransactionBuilder & TestNodeBuilder = new NodeIdTransactionBuilder - with TestNodeBuilder - } - - "flow" should "correctly process updates in order" in new Scope { - val secondLedgerEnd = someLedgerEnd.copy( - lastOffset = Offset.tryFromLong(12), - lastEventSeqId = 12L, - ) - runFlow( - Seq( - (Vector(update1, metadataChangedUpdate), someLedgerEnd, traceContext), - (Vector(update3, update4), secondLedgerEnd, traceContext), - ) - ) - cacheUpdates should contain theSameElementsInOrderAs Seq( - result(someLedgerEnd), - result(secondLedgerEnd), - ) - } - - "flow" should "not process empty input batches" in new Scope { - val secondLedgerEnd = someLedgerEnd.copy( - lastOffset = Offset.tryFromLong(12), - lastEventSeqId = 12L, - ) - val thirdLedgerEnd = someLedgerEnd.copy( - lastOffset = Offset.tryFromLong(14), - lastEventSeqId = 14L, - lastPublicationTime = CantonTimestamp.assertFromLong(15L), - ) - runFlow( - Seq( - // Empty input batch should have no effect - (Vector.empty, someLedgerEnd, traceContext), - (Vector(update3), secondLedgerEnd, traceContext), - (Vector(anotherMetadataChangedUpdate), thirdLedgerEnd, traceContext), - ) - ) - - cacheUpdates should contain theSameElementsInOrderAs Seq( - result(secondLedgerEnd), - result(thirdLedgerEnd), - // Results in empty batch after processing - ) - } - - "prepare" should "throw exception for an empty vector" in new Scope { - an[NoSuchElementException] should be thrownBy { - InMemoryStateUpdater.prepare( - Vector.empty, - someLedgerEnd, - traceContext, - ) - } - } - - "prepare" should "prepare a batch of a single update" in new Scope { - InMemoryStateUpdater.prepare( - Vector(update1), - someLedgerEnd, - traceContext, - ) shouldBe PrepareResult( - Vector(txLogUpdate1), - someLedgerEnd, - update1._2.traceContext, - traceContext, - ) - } - - "prepare" should "correctly populate external transaction hash" in new Scope { - val externalTransactionHash = - Hash.digest(HashPurpose.PreparedSubmission, ByteString.copyFrom("mock_hash".getBytes), Sha256) - val updateWithTransactionHash = offset(11L) -> transactionAccepted( - t = 0L, - synchronizerId = synchronizerId1, - externalTransactionHash = Some(externalTransactionHash), - ) - - val preparedWithHashResult = InMemoryStateUpdater.prepare( - Vector(updateWithTransactionHash), - someLedgerEnd, - traceContext, - ) - inside(preparedWithHashResult.updates.loneElement) { - case transactionAccepted: TransactionAccepted => - transactionAccepted.externalTransactionHash shouldBe Some(externalTransactionHash) - } - - val preparedWithoutHashResult = InMemoryStateUpdater.prepare( - Vector(update1), - someLedgerEnd, - traceContext, - ) - inside(preparedWithoutHashResult.updates.loneElement) { - case transactionAccepted: TransactionAccepted => - transactionAccepted.externalTransactionHash shouldBe None - } - } - - "prepare" should "correctly populate traffic cost" in new Scope { - val paidTrafficCost = NonNegativeLong.tryCreate(1235) - val completionInfo = Some( - state.CompletionInfo( - actAs = List(party1), - userId = userId, - commandId = commandId, - optDeduplicationPeriod = None, - submissionId = Some(submissionId), - paidTrafficCost = paidTrafficCost, - ) - ) - val accepted = offset(11L) -> transactionAccepted( - t = 0L, - synchronizerId = synchronizerId1, - completionInfoO = completionInfo, - ) - - val preparedWithTrafficCostResult = InMemoryStateUpdater.prepare( - Vector(accepted), - someLedgerEnd, - traceContext, - ) - inside(preparedWithTrafficCostResult.updates.loneElement) { - case transactionAccepted: TransactionAccepted => - // Submitting party filter should return the cost - transactionAccepted.paidTrafficCost(Some(Set(party1))) shouldBe Some(paidTrafficCost.value) - // Wildcard filter (None) should return the cost - transactionAccepted.paidTrafficCost(None) shouldBe Some(paidTrafficCost.value) - // Non submitting party should return no cost - transactionAccepted.paidTrafficCost(Some(Set(party2))) shouldBe None - } - - val rejected = offset(12L) -> commandRejected( - 4, - synchronizerId1, - trafficCost = paidTrafficCost, - actAs = List(party1), - ) - val preparedRejectedWithTrafficCostResult = InMemoryStateUpdater.prepare( - Vector(rejected), - someLedgerEnd, - traceContext, - ) - inside(preparedRejectedWithTrafficCostResult.updates.loneElement) { - case transactionRejected: TransactionRejected => - // Submitting party filter should return the cost - transactionRejected.paidTrafficCost(Some(Set(party1))) shouldBe Some(paidTrafficCost.value) - // Wildcard filter (None) should return the cost - transactionRejected.paidTrafficCost(None) shouldBe Some(paidTrafficCost.value) - // Non submitting party should return no cost - transactionRejected.paidTrafficCost(Some(Set(party2))) shouldBe None - } - - val assign = offset(13L) -> assignmentAccepted( - 2, - source = synchronizerId2, - target = synchronizerId1, - completionInfo = completionInfo, - ) - val prepareAssign = InMemoryStateUpdater.prepare( - Vector(assign), - someLedgerEnd, - traceContext, - ) - inside(prepareAssign.updates.loneElement) { case reassignmentAccepted: ReassignmentAccepted => - // Submitting party filter should return the cost - reassignmentAccepted.paidTrafficCost(Some(Set(party1))) shouldBe Some(paidTrafficCost.value) - // Wildcard filter (None) should return the cost - reassignmentAccepted.paidTrafficCost(None) shouldBe Some(paidTrafficCost.value) - // Non submitting party should return no cost - reassignmentAccepted.paidTrafficCost(Some(Set(party2))) shouldBe None - } - - val unassign = offset(14L) -> unassignmentAccepted( - 3, - source = synchronizerId1, - target = synchronizerId2, - completionInfo = completionInfo, - ) - val prepareUnassign = InMemoryStateUpdater.prepare( - Vector(unassign), - someLedgerEnd, - traceContext, - ) - inside(prepareUnassign.updates.loneElement) { case reassignmentAccepted: ReassignmentAccepted => - // Submitting party filter should return the cost - reassignmentAccepted.paidTrafficCost(Some(Set(party1))) shouldBe Some(paidTrafficCost.value) - // Wildcard filter (None) should return the cost - reassignmentAccepted.paidTrafficCost(None) shouldBe Some(paidTrafficCost.value) - // Non submitting party should return no cost - reassignmentAccepted.paidTrafficCost(Some(Set(party2))) shouldBe None - } - } - - "prepare" should "prepare a batch with reassignments" in new Scope { - InMemoryStateUpdater.prepare( - Vector(update1, update7, update8), - someLedgerEnd, - traceContext, - ) shouldBe PrepareResult( - Vector(txLogUpdate1, assignLogUpdate, unassignLogUpdate), - someLedgerEnd, - update1._2.traceContext, - traceContext, - ) - } - - "prepare" should "prepare a batch with topology transaction" in new Scope { - InMemoryStateUpdater.prepare( - Vector(update1, update9), - someLedgerEnd, - traceContext, - ) shouldBe PrepareResult( - Vector(txLogUpdate1, topologyTransactionLogUpdate), - someLedgerEnd, - update1._2.traceContext, - traceContext, - ) - } - - "prepare" should "set last offset and eventSequentialId to last element" in new Scope { - InMemoryStateUpdater.prepare( - Vector(update1, metadataChangedUpdate), - someLedgerEnd, - traceContext, - ) shouldBe PrepareResult( - Vector(txLogUpdate1), - someLedgerEnd, - metadataChangedUpdate._2.traceContext, - traceContext, - ) - } - - private def checkPrepareFlatEventWitnesses(isAcsDelta: Boolean): Unit = - "prepare" should s"produce ${if (isAcsDelta) "non-empty" - else "empty"} flatEventWitnesses for created and consuming exercised events with isAcsDelta $isAcsDelta" in new Scope { - private val builder = TxBuilder() - private val contract = genContract - private val createNode = contract.inst.toCreateNode - private val exerciseNode = TestNodeBuilder.exercise( - contract = createNode, - choice = "someChoice", - consuming = true, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set("divulgee"), - byKey = false, - ) - builder.add(createNode) - builder.add(exerciseNode) - private val tx = builder.buildCommitted() - - private val update = offset(1L) -> transactionAccepted( - t = 0L, - transaction = tx, - synchronizerId = synchronizerId1, - contracts = Seq(contract), - contractActivenessChanged = isAcsDelta, - ) - - private val events = - InMemoryStateUpdater - .prepare( - Vector(update), - someLedgerEnd, - traceContext, - ) - .updates - .collect { case txAccepted: TransactionLogUpdate.TransactionAccepted => txAccepted } - .flatMap(_.events) - - events.size shouldBe 2 - - private val createdEvent = events.collectFirst { case c: TransactionLogUpdate.CreatedEvent => - c - }.value - private val exercisedEvent = events.collectFirst { - case e: TransactionLogUpdate.ExercisedEvent => e - }.value - - if (isAcsDelta) { - createdEvent.flatEventWitnesses should not be empty - exercisedEvent.flatEventWitnesses should not be empty - } else { - createdEvent.flatEventWitnesses shouldBe empty - exercisedEvent.flatEventWitnesses shouldBe empty - } - } - - checkPrepareFlatEventWitnesses(isAcsDelta = false) - checkPrepareFlatEventWitnesses(isAcsDelta = true) - - "update" should "update the in-memory state" in new Scope { - InMemoryStateUpdater.update(inMemoryState, logger)(prepareResult, repairMode = false) - - inOrder - .verify(inMemoryFanoutBuffer) - .push( - tx_accepted_withCompletionStreamResponse - ) - inOrder - .verify(inMemoryFanoutBuffer) - .push( - tx_accepted_withoutCompletionStreamResponse - ) - inOrder.verify(inMemoryFanoutBuffer).push(tx_rejected) - inOrder - .verify(contractStateCaches) - .push(any[NonEmptyVector[ContractStateEvent]], any[Long])(any[TraceContext]) - - inOrder - .verify(ledgerEndCache) - .set(Some(lastLedgerEnd)) - inOrder.verify(dispatcher).signalNewHead(lastOffset) - inOrder - .verify(transactionSubmissionTracker) - .onCompletion(tx_accepted_completionStreamResponse) - - inOrder - .verify(transactionSubmissionTracker) - .onCompletion(tx_rejected_completionStreamResponse) - - inOrder - .verify(reassignmentSubmissionTracker) - .onCompletion(tx_rejected_completionStreamResponse) - - inOrder.verifyNoMoreInteractions() - } - - "update" should "update the caches even if it only has reassignments" in new Scope { - InMemoryStateUpdater.update(inMemoryState, logger)( - prepareResultOnlyReassignment, - repairMode = false, - ) - - inOrder - .verify(inMemoryFanoutBuffer) - .push( - assignLogUpdate - ) - inOrder - .verify(contractStateCaches) - .push(any[NonEmptyVector[ContractStateEvent]], any[Long])(any[TraceContext]) - inOrder - .verify(ledgerEndCache) - .set(Some(lastLedgerEnd)) - inOrder.verify(dispatcher).signalNewHead(lastOffset) - inOrder.verifyNoMoreInteractions() - } - - "update" should "update the in-memory state, but not the ledger-end and the dispatcher in repair mode" in new Scope { - InMemoryStateUpdater.update(inMemoryState, logger)(prepareResult, repairMode = true) - - inOrder - .verify(inMemoryFanoutBuffer) - .push( - tx_accepted_withCompletionStreamResponse - ) - inOrder - .verify(inMemoryFanoutBuffer) - .push( - tx_accepted_withoutCompletionStreamResponse - ) - inOrder.verify(inMemoryFanoutBuffer).push(tx_rejected) - inOrder - .verify(contractStateCaches) - .push(any[NonEmptyVector[ContractStateEvent]], any[Long])(any[TraceContext]) - - inOrder - .verify(transactionSubmissionTracker) - .onCompletion(tx_accepted_completionStreamResponse) - - inOrder - .verify(transactionSubmissionTracker) - .onCompletion(tx_rejected_completionStreamResponse) - - inOrder - .verify(reassignmentSubmissionTracker) - .onCompletion(tx_rejected_completionStreamResponse) - - inOrder.verifyNoMoreInteractions() - } - - "update" should "only push TransactionAccepted updates with non-empty flatEventWitnesses to inMemoryFanoutBuffer and contractStateCaches" in new Scope { - InMemoryStateUpdater.update(inMemoryState, logger)( - prepareResultWithEmptyFlatEventWitnesses, - repairMode = false, - ) - - inOrder - .verify(inMemoryFanoutBuffer) - .push(tx_accepted_withFlatEventWitnesses) - inOrder - .verify(inMemoryFanoutBuffer) - .push( - tx_accepted_withoutFlatEventWitnesses - ) - inOrder - .verify(contractStateCaches) - .push(any[NonEmptyVector[ContractStateEvent]], any[Long])(any[TraceContext]) - - // the tx_accepted_withoutFlatEventWitnesses should not be pushed as it has empty flatEventWitnesses - verifyNoMoreInteractions(contractStateCaches) - - inOrder - .verify(ledgerEndCache) - .set(Some(lastLedgerEnd.copy(lastOffset = tx_accepted_withoutFlatEventWitnesses.offset))) - inOrder.verify(dispatcher).signalNewHead(tx_accepted_withoutFlatEventWitnesses.offset) - inOrder.verifyNoMoreInteractions() - } - - "updateOffsetCheckpointCacheFlowWithTickingSource" should "not alter the original flow" in new Scope { - implicit val ec: ExecutionContext = executorService - - // here we define the offset, synchronizerId and recordTime for each offset-update pair the same way they arrive to the flow as Some values - // the None values denote the ticks arrived that are used to update the offset checkpoint cache - val offsetsAndTicks = - Seq(Some(1L), Some(2L), None, None, Some(3L), Some(4L), Some(5L), None, Some(6L), None) - val synchronizerIdsAndTicks = - Seq(Some(1L), Some(2L), None, None, Some(2L), Some(1L), Some(3L), None, Some(1L), None) - val recordTimesAndTicks = offsetsAndTicks - - val offsetCheckpointsExpected = - Seq( - // offset -> Map[synchronizer, time] - 2 -> Map( - 1 -> 1, - 2 -> 2, - ), - 2 -> Map( - 1 -> 1, - 2 -> 2, - ), - 5 -> Map( - 1 -> 4, - 2 -> 3, - 3 -> 5, - ), - 6 -> Map( - 1 -> 6, - 2 -> 3, - 3 -> 5, - ), - ).map { case (offset, synchronizerTimesRaw) => - OffsetCheckpoint( - offset = Offset.tryFromLong(offset.toLong), - synchronizerTimes = synchronizerTimesRaw.map { case (d, t) => - SynchronizerId.tryFromString(d.toString + "::default") -> Timestamp(t.toLong) - }, - ) - } - - val input = createInputSeq( - offsetsAndTicks, - synchronizerIdsAndTicks, - recordTimesAndTicks, - ) - - val (expectedOutput, output, checkpoints) = - runUpdateOffsetCheckpointCacheFlow( - input - ).futureValue - - output shouldBe expectedOutput - checkpoints shouldBe findCheckpointOffsets(input) - checkpoints shouldBe offsetCheckpointsExpected - - } - - "updateOffsetCheckpointCacheFlowWithTickingSource" should "update the synchronizer time for all the Update types that contain one" in new Scope { - implicit val ec: ExecutionContext = executorService - - private val updatesSeq: Seq[Update] = Seq( - transactionAccepted(1, synchronizerId1), - assignmentAccepted(2, source = synchronizerId2, target = synchronizerId1), - unassignmentAccepted(3, source = synchronizerId1, target = synchronizerId2), - commandRejected(4, synchronizerId1), - sequencerIndexMoved(5, synchronizerId1), - ) - - private val offsets = (1L to updatesSeq.length.toLong).map(Offset.tryFromLong) - private val updatesWithOffsets = offsets.zip(updatesSeq) - - // tick after each update to have one checkpoint after every update - // the None values denote the ticks arrived that are used to update the offset checkpoint cache - private val input = - updatesWithOffsets.flatMap(elem => Seq(Some(elem), None)) - - private val (expectedOutput, output, checkpoints) = - runUpdateOffsetCheckpointCacheFlow( - input - ).futureValue - - private val offsetCheckpointsExpected = - Seq( - // offset -> Map[synchronizer, time] - 1 -> Map( - synchronizerId1 -> 1 - ), - 2 -> Map( - synchronizerId1 -> 2 - ), - 3 -> Map( - synchronizerId1 -> 3 - ), - 4 -> Map( - synchronizerId1 -> 4 - ), - 5 -> Map( - synchronizerId1 -> 5 - ), - ).map { case (offset, synchronizerTimesRaw) => - OffsetCheckpoint( - offset = Offset.tryFromLong(offset.toLong), - synchronizerTimes = synchronizerTimesRaw.map { case (d, t) => - d -> Timestamp(t.toLong) - }, - ) - } - - output shouldBe expectedOutput - checkpoints shouldBe offsetCheckpointsExpected - - } - - "updateCaches" should "not update cachesUpdatedUpto if contractStateCaches.push throws" in new Scope { - - private val ledgerEnd41 = lastLedgerEnd.copy(lastOffset = Offset.tryFromLong(41L)) - private val ledgerEnd42 = lastLedgerEnd.copy(lastOffset = Offset.tryFromLong(42L)) - - // check initial state - inMemoryState.cachesUpdatedUpto.get() shouldBe None - - doNothing.when(inMemoryFanoutBuffer).push(any[TransactionLogUpdate]) - // updateCaches with transactions up to 41 - InMemoryStateUpdater.updateCaches( - inMemoryState, - updates, - ledgerEnd41, - traceContext, - ) - - inMemoryState.cachesUpdatedUpto.get() shouldBe Some(Offset.tryFromLong(41L)) - - // try updateCaches with transactions with 42 offset, which will throw - doThrow(new RuntimeException("Exception thrown from inMemoryFanoutBuffer.push")) - .when(inMemoryFanoutBuffer) - .push(any[TransactionLogUpdate]) - - an[RuntimeException] should be thrownBy { - InMemoryStateUpdater.updateCaches( - inMemoryState, - updates, - ledgerEnd42, - traceContext, - ) - } - - inMemoryState.cachesUpdatedUpto.get() shouldBe None - - // updateCaches with transactions with 42 offset successfully - doNothing.when(inMemoryFanoutBuffer).push(any[TransactionLogUpdate]) - InMemoryStateUpdater.updateCaches( - inMemoryState, - updates, - ledgerEnd42, - traceContext, - ) - - inMemoryState.cachesUpdatedUpto.get() shouldBe Some(Offset.tryFromLong(42L)) - - } - -} - -object InMemoryStateUpdaterSpec { - - import TraceContext.Implicits.Empty.* - - private val txId1 = TestUpdateId("tx1") - private val txId2 = TestUpdateId("tx2") - private val txId3 = TestUpdateId("tx3") - private val txId4 = TestUpdateId("tx4") - - private val synchronizerId1 = SynchronizerId.tryFromString("x::synchronizerID1") - private val synchronizerId2 = SynchronizerId.tryFromString("x::synchronizerID2") - - private val party1 = Ref.Party.assertFromString("someparty1") - private val party2 = Ref.Party.assertFromString("someparty2") - private val commandId = Ref.CommandId.assertFromString("commandid") - private val submissionId = Ref.SubmissionId.assertFromString("submissionid") - - private val templateId = Identifier.assertFromString("pkgId1:Mod:I") - private val templateId2 = Identifier.assertFromString("pkgId2:Mod:I2") - - private val packageName = Ref.PackageName.assertFromString("pkg-name") - - private val participantId = Ref.ParticipantId.assertFromString("participant1") - private val someContractMetadataBytes = Bytes.assertFromString("00aabb") - private val workflowId: Ref.WorkflowId = Ref.WorkflowId.assertFromString("Workflow") - private val representativePackageId = - Ref.PackageId.assertFromString("some-representative-package-id") - - trait Scope - extends Matchers - with ScalaFutures - with MockitoSugar - with TestEssentials - with HasExecutorServiceGeneric { - - override def handleFailure(message: String) = fail(message) - - val cacheUpdates = ArrayBuffer.empty[PrepareResult] - val cachesUpdateCaptor = - (v: PrepareResult, _: Boolean) => cacheUpdates.addOne(v).pipe(_ => ()) - - val txLogUpdate1 = - TransactionLogUpdate.TransactionAccepted( - updateId = txId1.toHexString, - commandId = "", - workflowId = workflowId, - effectiveAt = Timestamp.Epoch, - offset = offset(11L), - events = Vector(), - completionStreamResponseO = None, - synchronizerId = synchronizerId1.toProtoPrimitive, - recordTime = Timestamp.Epoch, - externalTransactionHash = None, - )(emptyTraceContext) - - val assignLogUpdate = - TransactionLogUpdate.ReassignmentAccepted( - updateId = txId3.toHexString, - commandId = "", - workflowId = workflowId, - offset = offset(17L), - recordTime = Timestamp.Epoch, - completionStreamResponseO = None, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = ReassignmentTag.Source(synchronizerId1), - targetSynchronizer = ReassignmentTag.Target(synchronizerId2), - submitter = Option(party1), - reassignmentId = ReassignmentId.tryCreate("00155555"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Assign( - reassignmentCounter = 15L, - nodeId = 0, - persistedContractInstance = PersistedContractInstance( - internalContractId = 1, - inst = someContract.inst, - ), - ) - ), - synchronizerId = synchronizerId2.toProtoPrimitive, - )(emptyTraceContext) - - val unassignLogUpdate = - TransactionLogUpdate.ReassignmentAccepted( - updateId = txId4.toHexString, - commandId = "", - workflowId = workflowId, - offset = offset(18L), - recordTime = Timestamp.Epoch, - completionStreamResponseO = None, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = ReassignmentTag.Source(synchronizerId2), - targetSynchronizer = ReassignmentTag.Target(synchronizerId1), - submitter = Option(party2), - reassignmentId = ReassignmentId.tryCreate("0001555551"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Unassign( - contractId = someContract.contractId, - templateId = templateId2, - packageName = packageName, - stakeholders = Set(party2), - assignmentExclusivity = Some(Timestamp.assertFromLong(123456L)), - reassignmentCounter = 15L, - nodeId = 0, - ) - ), - synchronizerId = synchronizerId2.toProtoPrimitive, - )(emptyTraceContext) - - val topologyTransactionLogUpdate = - TransactionLogUpdate.TopologyTransactionEffective( - updateId = txId3.toHexString, - synchronizerId = synchronizerId1.toProtoPrimitive, - offset = offset(19L), - effectiveTime = Timestamp.Epoch, - events = Vector( - TransactionLogUpdate.PartyToParticipantAuthorization( - party = party1, - participant = participantId, - authorizationEvent = AuthorizationEvent.Added(AuthorizationLevel.Observation), - ) - ), - )(emptyTraceContext) - - val ledgerEndCache: MutableLedgerEndCache = mock[MutableLedgerEndCache] - val achsStateCache: AchsStateCache = mock[AchsStateCache] - val contractStateCaches: ContractStateCaches = mock[ContractStateCaches] - val offsetCheckpointCache: OffsetCheckpointCache = mock[OffsetCheckpointCache] - val inMemoryFanoutBuffer: InMemoryFanoutBuffer = mock[InMemoryFanoutBuffer] - val stringInterningView: StringInterningView = mock[StringInterningView] - val dispatcherState: DispatcherState = mock[DispatcherState] - val transactionSubmissionTracker: SubmissionTracker = mock[SubmissionTracker] - val reassignmentSubmissionTracker: SubmissionTracker = mock[SubmissionTracker] - val partyAllocationTracker: PartyAllocation.Tracker = mock[PartyAllocation.Tracker] - val dispatcher: Dispatcher[Offset] = mock[Dispatcher[Offset]] - val commandProgressTracker = CommandProgressTracker.NoOp - - val inOrder: InOrder = inOrder( - ledgerEndCache, - contractStateCaches, - inMemoryFanoutBuffer, - stringInterningView, - dispatcherState, - transactionSubmissionTracker, - reassignmentSubmissionTracker, - dispatcher, - ) - - when(dispatcherState.getDispatcher).thenReturn(dispatcher) - - val inMemoryState = new InMemoryState( - participantId = participantId, - ledgerEndCache = ledgerEndCache, - achsStateCache = achsStateCache, - contractStateCaches = contractStateCaches, - offsetCheckpointCache = offsetCheckpointCache, - inMemoryFanoutBuffer = inMemoryFanoutBuffer, - stringInterningView = stringInterningView, - dispatcherState = dispatcherState, - transactionSubmissionTracker = transactionSubmissionTracker, - reassignmentSubmissionTracker = reassignmentSubmissionTracker, - partyAllocationTracker = partyAllocationTracker, - commandProgressTracker = commandProgressTracker, - loggerFactory = loggerFactory, - )(executorService) - - val inMemoryStateUpdater = InMemoryStateUpdaterFlow( - prepareUpdatesParallelism = 2, - prepareUpdatesExecutionContext = executorService, - updateCachesExecutionContext = executorService, - offsetCheckpointCacheUpdateInterval = FiniteDuration(15, "seconds"), - metrics = LedgerApiServerMetrics.ForTesting, - )( - inMemoryState = inMemoryState, - prepare = (_, ledgerEnd, _) => result(ledgerEnd), - update = cachesUpdateCaptor, - ) - - val tx_accepted_commandId = "cAccepted" - val tx_accepted_updateId = "tAccepted" - val tx_accepted_submitters: Set[String] = Set("p1", "p2") - - val tx_rejected_updateId = "tRejected" - val tx_rejected_submitters: Set[String] = Set("p3", "p4") - - val tx_accepted_completion: Completion = Completion.defaultInstance.copy( - commandId = tx_accepted_commandId, - userId = "userId", - updateId = tx_accepted_updateId, - submissionId = "submissionId", - actAs = tx_accepted_submitters.toSeq, - paidTrafficCost = 90L, - ) - val tx_rejected_completion: Completion = - tx_accepted_completion.copy( - updateId = tx_rejected_updateId, - actAs = tx_rejected_submitters.toSeq, - ) - val tx_accepted_completionStreamResponse: CompletionStreamResponse = - CompletionStreamResponse( - CompletionStreamResponse.CompletionResponse.Completion( - tx_accepted_completion - ) - ) - - val tx_rejected_completionStreamResponse = - CompletionStreamResponse( - CompletionStreamResponse.CompletionResponse.Completion( - tx_rejected_completion - ) - ) - - val tx_accepted_withCompletionStreamResponse_offset: Offset = - Offset.tryFromLong(1111L) - - val tx_accepted_withoutCompletionStreamResponse_offset: Offset = - Offset.tryFromLong(2222L) - - val tx_rejected_offset: Offset = Offset.tryFromLong(3333L) - - val tx_accepted_withFlatEventWitnesses_offset: Offset = Offset.tryFromLong(4444L) - val tx_accepted_withoutFlatEventWitnesses_offset: Offset = Offset.tryFromLong(5555L) - - val tx_accepted_withCompletionStreamResponse: TransactionLogUpdate.TransactionAccepted = - TransactionLogUpdate.TransactionAccepted( - updateId = TestUpdateId(tx_accepted_updateId).toHexString, - commandId = tx_accepted_commandId, - workflowId = "wAccepted", - effectiveAt = Timestamp.assertFromLong(1L), - offset = tx_accepted_withCompletionStreamResponse_offset, - events = (1 to 3) - .map(i => - toCreatedEvent( - genContract.inst.toCreateNode, - tx_accepted_withCompletionStreamResponse_offset, - TestUpdateId(tx_accepted_updateId), - NodeId(i), - ) - ) - .toVector, - completionStreamResponseO = Some(tx_accepted_completionStreamResponse), - synchronizerId = synchronizerId1.toProtoPrimitive, - recordTime = Timestamp(1), - externalTransactionHash = None, - )(emptyTraceContext) - - val tx_accepted_withoutCompletionStreamResponse: TransactionLogUpdate.TransactionAccepted = - tx_accepted_withCompletionStreamResponse.copy( - completionStreamResponseO = None, - offset = tx_accepted_withoutCompletionStreamResponse_offset, - )(emptyTraceContext) - - val tx_accepted_withFlatEventWitnesses: TransactionLogUpdate.TransactionAccepted = - tx_accepted_withoutCompletionStreamResponse.copy( - offset = tx_accepted_withFlatEventWitnesses_offset, - events = Vector( - toCreatedEvent( - genContract.inst.toCreateNode, - tx_accepted_withFlatEventWitnesses_offset, - TestUpdateId(tx_accepted_updateId), - NodeId(0), - ) - ), - )(emptyTraceContext) - - val tx_accepted_withoutFlatEventWitnesses: TransactionLogUpdate.TransactionAccepted = - tx_accepted_withFlatEventWitnesses.copy( - offset = tx_accepted_withoutFlatEventWitnesses_offset, - events = Vector( - toCreatedEvent( - genContract.inst.toCreateNode, - tx_accepted_withoutFlatEventWitnesses_offset, - TestUpdateId(tx_accepted_updateId), - NodeId(0), - ).copy( - flatEventWitnesses = Set.empty - ) - ), - )(emptyTraceContext) - - val tx_rejected: TransactionLogUpdate.TransactionRejected = - TransactionLogUpdate.TransactionRejected( - offset = tx_rejected_offset, - completionStreamResponse = tx_rejected_completionStreamResponse, - )(emptyTraceContext) - - val lastOffset: Offset = tx_rejected_offset - val lastEventSeqId = 123L - val lastPublicationTime = CantonTimestamp.MinValue.plusSeconds(1000) - val lastStringInterningId = 234 - val lastLedgerEnd = LedgerEnd( - lastOffset = lastOffset, - lastEventSeqId = lastEventSeqId, - lastStringInterningId = lastStringInterningId, - lastPublicationTime = lastPublicationTime, - ) - val updates: Vector[TransactionLogUpdate] = - Vector( - tx_accepted_withCompletionStreamResponse, - tx_accepted_withoutCompletionStreamResponse, - tx_rejected, - ) - val prepareResult: PrepareResult = PrepareResult( - updates = updates, - ledgerEnd = lastLedgerEnd, - emptyTraceContext, - traceContext, - ) - val prepareResultOnlyReassignment: PrepareResult = PrepareResult( - updates = Vector(assignLogUpdate), - ledgerEnd = lastLedgerEnd, - emptyTraceContext, - traceContext, - ) - val prepareResultWithEmptyFlatEventWitnesses: PrepareResult = PrepareResult( - updates = Vector( - tx_accepted_withFlatEventWitnesses, - tx_accepted_withoutFlatEventWitnesses, - ), - ledgerEnd = lastLedgerEnd.copy(lastOffset = tx_accepted_withoutFlatEventWitnesses.offset), - emptyTraceContext, - traceContext, - ) - - def result(ledgerEnd: LedgerEnd): PrepareResult = - PrepareResult( - Vector.empty, - ledgerEnd, - emptyTraceContext, - traceContext, - ) - - def runFlow( - input: Seq[(Vector[(Offset, Update)], LedgerEnd, TraceContext)] - )(implicit mat: Materializer): Done = - Source(input.map { case (updates, ledgerEnd, tc) => - Batch( - ledgerEnd = ledgerEnd, - batch = (), - batchSize = updates.size, - offsetsUpdates = updates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - batchTraceContext = tc, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ): Batch[?] - }) - .via(inMemoryStateUpdater(false)) - .runWith(Sink.ignore) - .futureValue - } - - private def genContract = - ExampleContractFactory.build( - stakeholders = Set(party1, party2), - signatories = Set(party1), - templateId = templateId, - argument = Value.ValueUnit, - ) - private val someContract = genContract - - private def toCreatedEvent( - createdNode: Node.Create, - txOffset: Offset, - updateId: UpdateId, - nodeId: NodeId, - ) = - CreatedEvent( - eventOffset = txOffset, - updateId = updateId.toHexString, - nodeId = nodeId.index, - eventSequentialId = 0, - contractId = createdNode.coid, - ledgerEffectiveTime = Timestamp.assertFromLong(12222), - templateId = createdNode.templateId, - packageName = createdNode.packageName, - packageVersion = None, - commandId = "", - workflowId = workflowId, - contractKey = None, - treeEventWitnesses = Set.empty, - flatEventWitnesses = createdNode.stakeholders, - submitters = Set.empty, - createArgument = com.digitalasset.daml.lf.transaction - .Versioned(createdNode.version, createdNode.arg), - createSignatories = createdNode.signatories, - createObservers = createdNode.stakeholders.diff(createdNode.signatories), - createKeyHash = createdNode.keyOpt.map(_.globalKey.hash), - createKeyMaintainers = createdNode.keyOpt.map(_.maintainers), - authenticationData = someContractMetadataBytes, - representativePackageId = representativePackageId, - ) - - implicit val defaultValueProviderCreatedEvent - : DefaultValueProvider[NonEmptyVector[ContractStateEvent]] = - new DefaultValueProvider[NonEmptyVector[ContractStateEvent]] { - override def default: NonEmptyVector[ContractStateEvent] = - NonEmptyVector.one( - InMemoryStateUpdater.convertLogToStateEvent( - toCreatedEvent( - genContract.inst.toCreateNode, - Offset.firstOffset, - TestUpdateId("yolo"), - NodeId(0), - ) - ) - ) - } - - private val someTransactionMeta: TransactionMeta = TransactionMeta( - ledgerEffectiveTime = Timestamp.Epoch, - workflowId = Some(workflowId), - preparationTime = Timestamp.Epoch, - submissionSeed = crypto.Hash.hashPrivateKey("SomeTxMeta"), - timeBoundaries = LedgerTimeBoundaries.unconstrained, - optUsedPackages = None, - optNodeSeeds = None, - optByKeyNodes = None, - ) - - private val update1 = offset(11L) -> transactionAccepted(t = 0L, synchronizerId = synchronizerId1) - private def rawMetadataChangedUpdate(offset: Offset, recordTime: Timestamp) = - offset -> - Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp(recordTime), - ) - - private val metadataChangedUpdate = rawMetadataChangedUpdate(offset(12L), Timestamp.Epoch) - private val update3 = offset(13L) -> - Update.SequencedTransactionAccepted( - completionInfoO = None, - transactionMeta = someTransactionMeta, - transactionInfo = - Update.TransactionAccepted.TransactionInfo(CommittedTransaction(TransactionBuilder.Empty)), - updateId = txId2, - synchronizerId = SynchronizerId.tryFromString("da::default"), - recordTime = CantonTimestamp.MinValue, - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map.empty, - ) - - private val update4 = offset(14L) -> - commandRejected(t = 1337L, synchronizerId = SynchronizerId.tryFromString("da::default")) - - private val update7 = offset(17L) -> - assignmentAccepted(t = 0, source = synchronizerId1, target = synchronizerId2) - - private val update8 = offset(18L) -> - unassignmentAccepted(t = 0, source = synchronizerId2, target = synchronizerId1) - - private val update9 = offset(19L) -> - topologyTransactionEffective(t = 0, AuthorizationLevel.Observation) - - private val anotherMetadataChangedUpdate = - rawMetadataChangedUpdate(offset(15L), Timestamp.assertFromLong(1337L)) - - private def offset(idx: Long): Offset = - Offset.tryFromLong(1000000000L + idx) - - // traverse the list from left to right and if a None is found add the exact previous checkpoint in the result - private def findCheckpointOffsets( - input: Seq[Option[(Offset, Update.TransactionAccepted)]] - ): Seq[OffsetCheckpoint] = - input - .foldLeft[(Seq[OffsetCheckpoint], Option[OffsetCheckpoint])]((Seq.empty, None)) { - // new update and offset pair received update offsetCheckpoint - case ((acc, lastCheckpointO), Some((currOffset, update))) => - ( - acc, - Some( - OffsetCheckpoint( - offset = currOffset, - synchronizerTimes = lastCheckpointO - .map(_.synchronizerTimes) - .getOrElse(Map.empty[SynchronizerId, Timestamp]) - .updated(update.synchronizerId, update.recordTime.toLf), - ) - ), - ) - // tick received add checkpoint to the seq, if there is one - case ((acc, Some(lastCheckpoint)), None) => (acc :+ lastCheckpoint, Some(lastCheckpoint)) - case ((acc, None), None) => (acc, None) - } - ._1 - - private def createInputSeq( - offsetsAndTicks: Seq[Option[Long]], - synchronizerIdsAndTicks: Seq[Option[Long]], - recordTimesAndTicks: Seq[Option[Long]], - ): Seq[Option[(Offset, Update.TransactionAccepted)]] = { - val offsets = offsetsAndTicks.map(_.map(Offset.tryFromLong)) - val synchronizerIds = - synchronizerIdsAndTicks.map( - _.map(x => SynchronizerId.tryFromString(x.toString + "::default")) - ) - - val updatesSeq: Seq[Option[Update.TransactionAccepted]] = - recordTimesAndTicks.zip(synchronizerIds).map { - case (Some(t), Some(synchronizer)) => - Some( - transactionAccepted(t, synchronizer) - ) - case _ => None - } - - offsets.zip(updatesSeq).map { - case (Some(offset), Some(tracedUpdate)) => Some((offset, tracedUpdate)) - case _ => None - } - - } - - // this function gets a sequence of offset, update pairs as Some values - // and ticks as Nones - // runs the updateOffsetCheckpointCacheFlowWithTickingSource - // and provides as output: - // - 1. the expected output - // - 2. the actual output - // - 3. the checkpoints updates in the offset checkpoint cache - def runUpdateOffsetCheckpointCacheFlow( - inputSeq: Seq[Option[(Offset, Update)]] - )(implicit materializer: Materializer, ec: ExecutionContext): Future[ - ( - Seq[Vector[(Offset, Update)]], - Seq[Vector[(Offset, Update)]], - Seq[OffsetCheckpoint], - ) - ] = { - val elementsQueue = - new ConcurrentLinkedQueue[Option[(Offset, Update)]] - inputSeq.foreach(elementsQueue.add) - - val flattenedSeq: Seq[Vector[(Offset, Update)]] = - inputSeq.flatten.map(Vector(_)) - - val bufferSize = 100 - val (sourceQueueSomes, sourceSomes) = Source - .queue[Vector[(Offset, Update)]](bufferSize) - .preMaterialize() - val (sourceQueueNones, sourceNones) = Source - .queue[Option[Nothing]](bufferSize) - .preMaterialize() - - def offerNext() = - Option(elementsQueue.poll()) match { - // send element - case Some(Some(pair)) => - sourceQueueSomes.offer(Vector(pair)) - // send tick - case Some(None) => - sourceQueueNones.offer(None) - // queue is empty send finished message - case None => sourceQueueSomes.complete() - } - offerNext() - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - var checkpoints: Seq[OffsetCheckpoint] = Seq.empty - - val output = sourceSomes - .map(updates => - Batch( - ledgerEnd = someLedgerEnd, - batch = (), - batchSize = updates.size, - offsetsUpdates = updates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - batchTraceContext = emptyTraceContext, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - ) - .via( - InMemoryStateUpdaterFlow - .updateOffsetCheckpointCacheFlowWithTickingSource( - updateOffsetCheckpointCache = oc => { - checkpoints = checkpoints :+ oc - offerNext() - }, - tick = sourceNones, - ) - ) - .map(_.offsetsUpdates) - .alsoTo(Sink.foreach(_ => offerNext())) - .runWith(Sink.seq) - - output.map(o => (flattenedSeq, o, checkpoints)) - - } - - private def transactionAccepted( - t: Long, - synchronizerId: SynchronizerId, - externalTransactionHash: Option[Hash] = None, - transaction: CommittedTransaction = CommittedTransaction(TransactionBuilder.Empty), - contracts: Seq[ContractInstance] = Seq.empty, - contractActivenessChanged: Boolean = true, - completionInfoO: Option[CompletionInfo] = None, - ): Update.TransactionAccepted = - Update.SequencedTransactionAccepted( - completionInfoO = completionInfoO, - transactionMeta = someTransactionMeta, - transactionInfo = Update.TransactionAccepted.TransactionInfo(transaction), - updateId = txId1, - synchronizerId = synchronizerId, - recordTime = CantonTimestamp(Timestamp(t)), - externalTransactionHash = externalTransactionHash, - acsChangeFactory = TestAcsChangeFactory(contractActivenessChanged), - contractInfos = contracts.zipWithIndex.map { case (c, idx) => - c.contractId -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = c.inst, - internalContractId = idx.toLong, - ), - representativePackageId = SameAsContractPackageId, - ) - }.toMap, - ) - - private def assignmentAccepted( - t: Long, - source: SynchronizerId, - target: SynchronizerId, - completionInfo: Option[CompletionInfo] = None, - ): Update.ReassignmentAccepted = - Update.SequencedReassignmentAccepted( - optCompletionInfo = completionInfo, - workflowId = Some(workflowId), - updateId = txId3, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = ReassignmentTag.Source(source), - targetSynchronizer = ReassignmentTag.Target(target), - submitter = Option(party1), - reassignmentId = ReassignmentId.tryCreate("00155555"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Assign( - reassignmentCounter = 15L, - nodeId = 0, - persistedContractInstance = PersistedContractInstance( - inst = someContract.inst, - internalContractId = 1, - ), - ) - ), - recordTime = CantonTimestamp(Timestamp(t)), - synchronizerId = target, - acsChangeFactory = TestAcsChangeFactory(), - ) - - private def unassignmentAccepted( - t: Long, - source: SynchronizerId, - target: SynchronizerId, - completionInfo: Option[CompletionInfo] = None, - ): Update.ReassignmentAccepted = - Update.SequencedReassignmentAccepted( - optCompletionInfo = completionInfo, - workflowId = Some(workflowId), - updateId = txId4, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = ReassignmentTag.Source(source), - targetSynchronizer = ReassignmentTag.Target(target), - submitter = Option(party2), - reassignmentId = ReassignmentId.tryCreate("0001555551"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Unassign( - contractId = someContract.contractId, - templateId = templateId2, - packageName = packageName, - stakeholders = Set(party2), - assignmentExclusivity = Some(Timestamp.assertFromLong(123456L)), - reassignmentCounter = 15L, - nodeId = 0, - ) - ), - recordTime = CantonTimestamp(Timestamp(t)), - synchronizerId = source, - acsChangeFactory = TestAcsChangeFactory(), - ) - - private def commandRejected( - t: Long, - synchronizerId: SynchronizerId, - trafficCost: NonNegativeLong = NonNegativeLong.zero, - actAs: List[Ref.Party] = List.empty, - ): Update.CommandRejected = - Update.SequencedCommandRejected( - completionInfo = CompletionInfo( - actAs = actAs, - userId = Ref.UserId.assertFromString("some-app-id"), - commandId = Ref.CommandId.assertFromString("cmdId"), - optDeduplicationPeriod = None, - submissionId = None, - paidTrafficCost = trafficCost, - ), - reasonTemplate = FinalReason(new Status()), - synchronizerId = synchronizerId, - recordTime = CantonTimestamp.assertFromLong(t), - isTransaction = true, - ) - - private def sequencerIndexMoved( - t: Long, - synchronizerId: SynchronizerId, - ): Update.SequencerIndexMoved = - Update.SequencerIndexMoved( - synchronizerId = synchronizerId, - recordTime = CantonTimestamp.assertFromLong(t), - ) - - private def topologyTransactionEffective( - t: Long, - authorizationLevel: AuthorizationLevel, - ): Update.TopologyTransactionEffective = - Update.TopologyTransactionEffective( - updateId = txId3, - synchronizerId = synchronizerId1, - effectiveTime = CantonTimestamp(Timestamp(t)), - events = Set( - TopologyEvent.PartyToParticipantAuthorization( - party = party1, - participant = participantId, - authorizationEvent = Added(authorizationLevel), - ) - ), - ) - - private val someLedgerEnd = LedgerEnd( - lastOffset = Offset.tryFromLong(10L), - lastEventSeqId = 10L, - lastStringInterningId = 10, - lastPublicationTime = CantonTimestamp.assertFromLong(10L), - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/IndexServiceImplSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/IndexServiceImplSpec.scala deleted file mode 100644 index 818f92aeba..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/index/IndexServiceImplSpec.scala +++ /dev/null @@ -1,1848 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.index - -import cats.implicits.catsSyntaxSemigroup -import cats.syntax.either.* -import com.daml.ledger.api.v2.update_service.GetUpdateResponse -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.TransactionShape.AcsDelta -import com.digitalasset.canton.ledger.api.messages.update.GetUpdatesPageRequest -import com.digitalasset.canton.ledger.api.{ - CumulativeFilter, - EventFormat, - InterfaceFilter, - TemplateFilter, - TemplateWildcardFilter, - TransactionFormat, - UpdateFormat, -} -import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors -import com.digitalasset.canton.logging.{ - ErrorLoggingContext, - LoggingContextWithTrace, - NoLogging, - TracedLogger, -} -import com.digitalasset.canton.platform.index.IndexServiceImpl.* -import com.digitalasset.canton.platform.index.IndexServiceImplSpec.Scope -import com.digitalasset.canton.platform.store.cache.OffsetCheckpoint -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties.{ - Projection, - UseOriginalViewPackageId, -} -import com.digitalasset.canton.platform.{ - InternalEventFormat, - InternalTransactionFormat, - InternalUpdateFormat, - TemplatePartiesFilter, -} -import com.digitalasset.canton.store.packagemeta.PackageMetadata -import com.digitalasset.canton.store.packagemeta.PackageMetadata.Implicits.packageMetadataSemigroup -import com.digitalasset.canton.store.packagemeta.PackageMetadata.{ - LocalPackagePreference, - PackageResolution, -} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{ - FullIdentifier, - Identifier, - IdentifierConverter, - NameTypeConRef, - Party, - QualifiedName, -} -import com.google.protobuf.ByteString -import com.google.protobuf.timestamp.Timestamp -import io.grpc.StatusRuntimeException -import org.apache.pekko.NotUsed -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.mockito.MockitoSugar -import org.scalatest.Inspectors.forAll -import org.scalatest.concurrent.PatienceConfiguration -import org.scalatest.concurrent.ScalaFutures.convertScalaFuture -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{EitherValues, OptionValues} -import org.slf4j.helpers.NOPLogger - -import scala.collection.mutable -import scala.concurrent.duration.DurationInt - -class IndexServiceImplSpec - extends AnyFlatSpec - with Matchers - with MockitoSugar - with EitherValues - with OptionValues { - - behavior of "IndexServiceImpl.memoizedInternalUpdateFormat" - - it should "give an empty result if no packages" in new Scope { - currentPackageMetadata = PackageMetadata() - val memoFunc = - memoizedInternalUpdateFormat( - getPackageMetadataSnapshot = getPackageMetadata, - updateFormat = updateFormatForTransactions( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = true, - ) - ), - interfaceViewPackageUpgrade = EventProjectionProperties.UseOriginalViewPackageId, - ) - memoFunc() shouldBe None - } - - it should "change the result in case of new package arrived" in new Scope { - currentPackageMetadata = packageMetadata_iface1 - val eventFormat = EventFormat( - filtersByParty = Map( - party -> CumulativeFilter( - templateFilters = Set(), - interfaceFilters = Set(iface1Filter), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = None, - verbose = true, - ) - // subscribing to iface1 - val memoFuncTransactions = memoizedInternalUpdateFormat( - getPackageMetadataSnapshot = getPackageMetadata, - updateFormat = updateFormatForTransactions(eventFormat), - interfaceViewPackageUpgrade = EventProjectionProperties.UseOriginalViewPackageId, - ) - val memoFuncReassignments = memoizedInternalUpdateFormat( - getPackageMetadataSnapshot = getPackageMetadata, - updateFormat = updateFormatForReassignments(eventFormat), - interfaceViewPackageUpgrade = EventProjectionProperties.UseOriginalViewPackageId, - ) - memoFuncTransactions() shouldBe None // no template implementing iface1 - memoFuncReassignments() shouldBe None // no template implementing iface1 - // template1 implements iface1 - currentPackageMetadata = packageMetadata_iface1_template1 - - val internalEventFormat0 = - InternalEventFormat( - TemplatePartiesFilter(Map(template1 -> Some(Set(party))), Some(Set())), - EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party.toString) -> Map( - Some(template1) -> Projection(Set(iface1Full), false) - ) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ) // filter gets complicated, filters template1 for iface1, projects iface1 - - memoFuncTransactions() shouldBe Some(internalUpdateFormatForTransactions(internalEventFormat0)) - memoFuncReassignments() shouldBe Some( - internalUpdateFormatForReassignments(internalEventFormat0) - ) - - // template2 also implements iface1 as template1 - currentPackageMetadata = packageMetadata_iface1_template1 |+| packageMetadata_iface1_template2 - - val internalEventFormat1 = InternalEventFormat( - templatePartiesFilter = TemplatePartiesFilter( - relation = Map( - template1 -> Some(Set(party)), - template2 -> Some(Set(party)), - ), - templateWildcardParties = Some(Set()), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party.toString) -> Map( - Some(template1) -> Projection(Set(iface1Full), false), - Some(template2) -> Projection(Set(iface1Full), false), - ) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ) // filter gets even more complicated, filters template1 and template2 for iface1, projects iface1 for both templates - - memoFuncTransactions() shouldBe Some(internalUpdateFormatForTransactions(internalEventFormat1)) - memoFuncReassignments() shouldBe Some( - internalUpdateFormatForReassignments(internalEventFormat1) - ) - } - - behavior of "IndexServiceImpl.wildcardFilter" - - it should "give empty result for the empty input" in new Scope { - wildcardFilter( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = false, - ) - ) shouldBe Some(Set.empty) - } - - it should "give empty result for filter without template-wildcards" in new Scope { - wildcardFilter( - EventFormat( - filtersByParty = Map( - party2 -> CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set(), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = None, - verbose = false, - ) - ) shouldBe Some(Set.empty) - - wildcardFilter( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some( - CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set(), - templateWildcardFilter = None, - ) - ), - verbose = false, - ) - ) shouldBe Some(Set.empty) - - } - - it should "provide a party filter for template-wildcard filter" in new Scope { - wildcardFilter( - EventFormat( - filtersByParty = Map(party -> CumulativeFilter.templateWildcardFilter()), - filtersForAnyParty = None, - verbose = false, - ) - ) shouldBe Some(Set(party)) - - wildcardFilter( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set.empty, - templateWildcardFilter = Some(TemplateWildcardFilter(includeCreatedEventBlob = false)), - ) - ), - filtersForAnyParty = None, - verbose = false, - ) - ) shouldBe Some(Set(party)) - - wildcardFilter( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter()), - verbose = false, - ) - ) shouldBe None - - wildcardFilter( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some( - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set.empty, - templateWildcardFilter = Some(TemplateWildcardFilter(includeCreatedEventBlob = false)), - ) - ), - verbose = false, - ) - ) shouldBe None - } - - it should "support multiple template-wildcard filters" in new Scope { - wildcardFilter( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter.templateWildcardFilter(), - party2 -> CumulativeFilter.templateWildcardFilter(), - ), - filtersForAnyParty = None, - verbose = false, - ) - ) shouldBe Some( - Set( - party, - party2, - ) - ) - - wildcardFilter( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter.templateWildcardFilter(), - party2 -> CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set(), - templateWildcardFilter = None, - ), - ), - filtersForAnyParty = None, - verbose = false, - ) - ) shouldBe Some(Set(party)) - } - - it should "support combining party-wildcard with template-wildcard filters" in new Scope { - wildcardFilter( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter.templateWildcardFilter(), - party2 -> CumulativeFilter.templateWildcardFilter(), - ), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter()), - verbose = false, - ) - ) shouldBe None - - wildcardFilter( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter.templateWildcardFilter(), - party2 -> CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set(), - templateWildcardFilter = None, - ), - ), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter()), - verbose = false, - ) - ) shouldBe None - } - - it should "be treated as wildcard filter if templateIds and interfaceIds are empty" in new Scope { - a[RuntimeException] should be thrownBy - wildcardFilter( - EventFormat( - filtersByParty = Map(party -> CumulativeFilter(Set(), Set(), None)), - filtersForAnyParty = None, - verbose = false, - ) - ) - - a[RuntimeException] should be thrownBy wildcardFilter( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(CumulativeFilter(Set(), Set(), None)), - verbose = false, - ) - ) - } - - behavior of "IndexServiceImpl.templateFilter" - - it should "give empty result for the empty input" in new Scope { - templateFilter( - PackageMetadata(), - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map.empty - } - - it should "provide an empty template filter for template-wildcard filters" in new Scope { - templateFilter( - PackageMetadata(), - EventFormat( - filtersByParty = Map(party -> CumulativeFilter.templateWildcardFilter()), - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map.empty - - templateFilter( - PackageMetadata(), - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter()), - verbose = false, - ), - ) shouldBe Map.empty - - templateFilter( - PackageMetadata(), - EventFormat( - filtersByParty = Map(party -> CumulativeFilter.templateWildcardFilter()), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter()), - verbose = false, - ), - ) shouldBe Map.empty - } - - it should "ignore template-wildcard filters and only include template filters" in new Scope { - templateFilter( - packageMetadata_iface1_template1, - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter.templateWildcardFilter(), - party2 -> CumulativeFilter.templateWildcardFilter(), - ), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter()), - verbose = false, - ), - ) shouldBe Map.empty - - templateFilter( - packageMetadata_iface1_template1, - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter.templateWildcardFilter(), - party2 -> CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set(), - templateWildcardFilter = None, - ), - ), - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map( - template1 -> Some(Set(party2)) - ) - - templateFilter( - packageMetadata_iface1_template1, - EventFormat( - filtersByParty = Map( - party2 -> CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set(), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter()), - verbose = false, - ), - ) shouldBe Map( - template1 -> Some(Set(party2)) - ) - } - - it should "ignore template-wildcard filter of the shape where templateIds and interfaceIds are empty" in new Scope { - templateFilter( - PackageMetadata(), - EventFormat( - filtersByParty = Map(party -> CumulativeFilter(Set(), Set(), None)), - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map.empty - - templateFilter( - PackageMetadata(), - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(CumulativeFilter(Set(), Set(), None)), - verbose = false, - ), - ) shouldBe Map.empty - } - - it should "provide a template filter for a simple template filter" in new Scope { - templateFilter( - packageMetadata_iface1_template1, - EventFormat( - filtersByParty = Map(party -> CumulativeFilter(Set(template1Filter), Set(), None)), - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map(template1 -> Some(Set(party))) - - templateFilter( - packageMetadata_iface1_template1, - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(CumulativeFilter(Set(template1Filter), Set(), None)), - verbose = false, - ), - ) shouldBe Map(template1 -> None) - } - - it should "provide an empty template filter if no template implementing this interface" in new Scope { - templateFilter( - packageMetadata_iface1, - EventFormat( - filtersByParty = Map(party -> CumulativeFilter(Set(), Set(iface1Filter), None)), - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map.empty - - templateFilter( - packageMetadata_iface1, - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(CumulativeFilter(Set(), Set(iface1Filter), None)), - verbose = false, - ), - ) shouldBe Map.empty - } - - it should "provide a template filter for related interface filter" in new Scope { - templateFilter( - packageMetadata_iface1_template1, - EventFormat( - filtersByParty = Map(party -> CumulativeFilter(Set(), Set(iface1Filter), None)), - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map(template1 -> Some(Set(party))) - - templateFilter( - packageMetadata_iface1_template1, - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(CumulativeFilter(Set(), Set(iface1Filter), None)), - verbose = false, - ), - ) shouldBe Map(template1 -> None) - } - - it should "merge template filter and interface filter together" in new Scope { - templateFilter( - packageMetadata_iface1_template1_template2, - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter(Set(template1Filter), Set(iface1Filter), None) - ), - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map(template1 -> Some(Set(party)), template2 -> Some(Set(party))) - - templateFilter( - packageMetadata_iface1_template1_template2, - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some( - CumulativeFilter(Set(template1Filter), Set(iface1Filter), None) - ), - verbose = false, - ), - ) shouldBe Map(template1 -> None, template2 -> None) - - } - - it should "merge multiple interface filters" in new Scope { - templateFilter( - packageMetadata_iface1_template1 |+| packageMetadata_iface2_template2 |+| packageMetadata_template3, - EventFormat( - filtersByParty = Map( - party -> - CumulativeFilter( - templateFilters = Set(TemplateFilter(template3, false)), - interfaceFilters = Set( - iface1Filter, - iface2Filter, - ), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = None, - verbose = false, - ), - ) shouldBe Map( - template1 -> Some(Set(party)), - template2 -> Some(Set(party)), - template3 -> Some(Set(party)), - ) - } - - it should "merge interface filters present in both filter by party and filter for any party" in new Scope { - templateFilter( - packageMetadata_iface1_template1 |+| packageMetadata_iface2_template2, - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - iface1Filter - ), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = Some( - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - iface2Filter - ), - templateWildcardFilter = None, - ) - ), - verbose = false, - ), - ) shouldBe Map( - template1 -> Some(Set(party)), - template2 -> None, - ) - } - - it should "merge the same interface filter present in both filter by party and filter for any party" in new Scope { - templateFilter( - packageMetadata_iface1_template1, - EventFormat( - filtersByParty = Map( - party -> - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - iface1Filter - ), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = Some( - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - iface1Filter - ), - templateWildcardFilter = None, - ) - ), - verbose = false, - ), - ) shouldBe Map( - template1 -> None - ) - } - - behavior of "IndexServiceImpl.unknownTemplatesOrInterfaces" - - it should "provide an empty list in case of empty filter and package metadata" in new Scope { - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata(), - ) shouldBe Either.unit - } - - it should "return an unknown template for not known template" in new Scope { - val filters = CumulativeFilter(Set(template1Filter), Set(), None) - - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata(packageNameMap = Map(packageName1 -> packageResolutionForTemplate1)), - ).left.value shouldBe RequestValidationErrors.NotFound.NoTemplatesForPackageNameAndQualifiedName - .Reject( - noKnownReferences = Set((template1.pkg.name, template1.qualifiedName)) - ) - - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = false, - ), - PackageMetadata(packageNameMap = Map(packageName1 -> packageResolutionForTemplate1)), - ).left.value shouldBe RequestValidationErrors.NotFound.NoTemplatesForPackageNameAndQualifiedName - .Reject( - noKnownReferences = Set((template1.pkg.name, template1.qualifiedName)) - ) - } - - it should "return an unknown interface for not known interface" in new Scope { - val filters = CumulativeFilter(Set(), Set(iface1Filter), None) - - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata(packageNameMap = Map(packageName1 -> packageResolutionForTemplate1)), - ).left.value shouldBe RequestValidationErrors.NotFound.NoInterfaceForPackageNameAndQualifiedName - .Reject( - noKnownReferences = Set((iface1Full.pkgName, iface1Full.qualifiedName)) - ) - - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = false, - ), - PackageMetadata(packageNameMap = Map(packageName1 -> packageResolutionForTemplate1)), - ).left.value shouldBe RequestValidationErrors.NotFound.NoInterfaceForPackageNameAndQualifiedName - .Reject( - noKnownReferences = Set((iface1Full.pkgName, iface1Full.qualifiedName)) - ) - } - - it should "return a package name on unknown package name" in new Scope { - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map( - party -> - CumulativeFilter( - templateFilters = Set(template1Filter, packageNameScopedTemplateFilter), - interfaceFilters = Set(iface1Filter), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata(), - ).left.value shouldBe RequestValidationErrors.NotFound.PackageNamesNotFound.Reject( - unknownPackageNames = Set(packageName1) - ) - } - - it should "return an unknown type reference for a package name/template qualified name with no known template-ids" in new Scope { - val unknownTemplateRefFilter = TemplateFilter( - templateTypeRef = NameTypeConRef.assertFromString( - s"${Ref.PackageRef.Name(packageName1).toString}:unknownModule:unknownEntity" - ), - includeCreatedEventBlob = false, - ) - - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter( - templateFilters = Set(template1Filter, unknownTemplateRefFilter), - interfaceFilters = Set(iface1Filter), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata( - interfaces = Set(iface1Id), - templates = Set(template1Id), - packageNameMap = Map(packageName1 -> packageResolutionForTemplate1), - ), - ).left.value shouldBe RequestValidationErrors.NotFound.NoTemplatesForPackageNameAndQualifiedName - .Reject( - noKnownReferences = - Set(packageName1 -> Ref.QualifiedName.assertFromString("unknownModule:unknownEntity")) - ) - } - - it should "return an unknown type reference for a package name/template qualified name with no known interface-ids" in new Scope { - val unknownInterfaceRefFilter = InterfaceFilter( - interfaceTypeRef = NameTypeConRef.assertFromString( - s"${Ref.PackageRef.Name(packageName1).toString}:unknownModule:unknownInterface" - ), - includeView = true, - includeCreatedEventBlob = false, - ) - - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set(iface1Filter, unknownInterfaceRefFilter), - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata( - interfaces = Set(iface1Id), - templates = Set(template1Id), - packageNameMap = Map(packageName1 -> packageResolutionForInterface1), - ), - ).left.value shouldBe RequestValidationErrors.NotFound.NoInterfaceForPackageNameAndQualifiedName - .Reject( - noKnownReferences = - Set(packageName1 -> Ref.QualifiedName.assertFromString("unknownModule:unknownInterface")) - ) - } - - it should "succeed for all query filter identifiers known" in new Scope { - val filters = CumulativeFilter( - templateFilters = Set(template1Filter, packageNameScopedTemplateFilter), - interfaceFilters = Set(iface1Filter, packageNameScopedInterfaceFilter), - templateWildcardFilter = None, - ) - - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata( - interfaces = Set(iface1Id), - templates = Set(template1Id), - packageNameMap = Map(packageName1 -> packageResolutionForTemplate1), - ), - ) shouldBe Either.unit - - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = false, - ), - PackageMetadata( - interfaces = Set(iface1Id), - templates = Set(template1Id), - packageNameMap = Map(packageName1 -> packageResolutionForTemplate1), - ), - ) shouldBe Either.unit - } - - it should "only return unknown templates" in new Scope { - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter(Set(template1Filter), Set(iface1Filter), None), - party2 -> CumulativeFilter(Set(template2Filter, template3Filter), Set(iface2Filter), None), - ), - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata( - templates = Set(template1Id), - interfaces = Set(iface1Id), - packageNameMap = Map(packageName1 -> packageResolutionForTemplate1), - ), - ).left.value shouldBe RequestValidationErrors.NotFound.NoTemplatesForPackageNameAndQualifiedName - .Reject( - noKnownReferences = Set( - (template2.pkg.name, template2.qualifiedName), - (template3.pkg.name, template3.qualifiedName), - ) - ) - } - - it should "only return unknown interfaces" in new Scope { - checkUnknownIdentifiers( - EventFormat( - filtersByParty = Map( - party -> CumulativeFilter(Set(template1Filter), Set(iface1Filter), None), - party2 -> CumulativeFilter(Set(template1Filter, template1Filter), Set(iface2Filter), None), - ), - filtersForAnyParty = None, - verbose = false, - ), - PackageMetadata( - templates = Set(template1Id), - interfaces = Set(iface1Id), - packageNameMap = Map(packageName1 -> packageResolutionForTemplate1), - ), - ).left.value shouldBe RequestValidationErrors.NotFound.NoInterfaceForPackageNameAndQualifiedName - .Reject( - noKnownReferences = Set((iface2.pkg.name, iface2.qualifiedName)) - ) - } - - behavior of "IndexServiceImpl.invalidTemplateOrInterfaceMessage" - - private implicit val errorLoggingContext: ErrorLoggingContext = NoLogging - it should "provide no message if the list of invalid templates or interfaces is empty" in new Scope { - RequestValidationErrors.NotFound.TemplateOrInterfaceIdsNotFound - .Reject(List.empty) - .cause shouldBe "" - } - - it should "combine a message containing invalid interfaces and templates together" in new Scope { - RequestValidationErrors.NotFound.TemplateOrInterfaceIdsNotFound - .Reject(List(Right(iface2Id), Left(template2Id), Left(template3Id))) - .cause shouldBe "Templates do not exist: [PackageId:ModuleName:template2, PackageId:ModuleName:template3]. Interfaces do not exist: [PackageId:ModuleName:iface2]." - } - - it should "provide a message for invalid templates" in new Scope { - RequestValidationErrors.NotFound.TemplateOrInterfaceIdsNotFound - .Reject(List(Left(template2Id), Left(template3Id))) - .cause shouldBe "Templates do not exist: [PackageId:ModuleName:template2, PackageId:ModuleName:template3]." - } - - it should "provide a message for invalid interfaces" in new Scope { - RequestValidationErrors.NotFound.TemplateOrInterfaceIdsNotFound - .Reject( - List(Right(iface1Id), Right(iface2Id)) - ) - .cause shouldBe "Interfaces do not exist: [PackageId:ModuleName:iface1, PackageId:ModuleName:iface2]." - } - - behavior of "IndexServiceImpl.injectCheckpoints" - val end = 10L - def createSource(elements: Seq[Long]): Source[(Offset, Carrier[Unit]), NotUsed] = { - val elementsSource = Source(elements).map(Offset.tryFromLong).map((_, ())) - - elementsSource - .via( - rangeDecorator( - startInclusive = Offset.tryFromLong(elements.head), - endInclusive = Offset.tryFromLong(elements.last), - ) - ) - } - - implicit val system: ActorSystem = ActorSystem("IndexServiceImplSpec") - - def fetchOffsetCheckpoint: Long => () => Option[OffsetCheckpoint] = - off => - () => Some(OffsetCheckpoint(offset = Offset.tryFromLong(off), synchronizerTimes = Map.empty)) - - it should "add a checkpoint at the right position of the stream" in new Scope { - - forAll(Seq(1L to end, Seq(1L, 5L, 10L))) { elements => - forAll(Seq(1L, 4L, 5L, 6L, 10L)) { checkpoint => - val out: Seq[Long] = - createSource(elements) - .via( - injectCheckpoints(fetchOffsetCheckpoint(checkpoint), _ => (), None) - ) - .runWith(Sink.seq) - .futureValue - .map(_._1) - .map(_.unwrap) - out shouldBe elements.appended(checkpoint).sorted - } - } - } - - it should "not add a checkpoint that it is out of range" in new Scope { - val elements = 1L to end - val checkpoint = 11L - - val out: Seq[Long] = - createSource(elements) - .via( - injectCheckpoints(fetchOffsetCheckpoint(checkpoint), _ => (), None) - ) - .runWith(Sink.seq) - .futureValue - .map(_._1) - .map(_.unwrap) - out shouldBe elements - } - - it should "add a checkpoint after the element if they have the same offset" in new Scope { - val elements = 1L to end - val source: Source[(Offset, Carrier[Option[Long]]), NotUsed] = - Source(elements) - .map(x => (Offset.tryFromLong(x), Some(x))) - .via( - rangeDecorator( - startInclusive = Offset.tryFromLong(elements.head), - endInclusive = Offset.tryFromLong(elements.last), - ) - ) - - forAll(Seq(1L, 5L, 10L)) { checkpoint => - val out: Seq[Option[Long]] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoint(checkpoint), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - .map(_._2) - - out shouldBe - (1L to checkpoint).map(Some(_)) ++ Seq(None) ++ (checkpoint + 1 to end).map(Some(_)) - } - } - - it should "add a checkpoint invoked from timeout when its offset is at the last streamed element" in new Scope { - val elements = 1L to end - val checkpoint = end - - val out: Seq[Long] = - createSource(elements) - .concat(Source.single((Offset.MaxValue, Timeout))) - .via( - injectCheckpoints(fetchOffsetCheckpoint(checkpoint), _ => (), None) - ) - .runWith(Sink.seq) - .futureValue - .map(_._1) - .map(_.unwrap) - out shouldBe elements :+ checkpoint - } - - it should "add a checkpoint at the ledgerEnd invoked from timeout when requesting from ledgerEnd" in new Scope { - val checkpoint = end - - val out: Seq[Long] = - Source - .single((Offset.MaxValue, Timeout)) - .via( - injectCheckpoints( - fetchOffsetCheckpoint = fetchOffsetCheckpoint(checkpoint), - responseFromCheckpoint = _ => (), - startExclusive = Some(Offset.tryFromLong(end)), - ) - ) - .runWith(Sink.seq) - .futureValue - .map(_._1) - .map(_.unwrap) - out shouldBe Vector(checkpoint) - } - - it should "not add the same checkpoint at the ledgerEnd invoked from timeout when requesting from ledgerEnd" in new Scope { - val checkpoint = end - - val out: Seq[Long] = - Source(Seq((Offset.MaxValue, Timeout), (Offset.MaxValue, Timeout))) - .via( - injectCheckpoints( - fetchOffsetCheckpoint = fetchOffsetCheckpoint(checkpoint), - responseFromCheckpoint = _ => (), - startExclusive = Some(Offset.tryFromLong(end)), - ) - ) - .runWith(Sink.seq) - .futureValue - .map(_._1) - .map(_.unwrap) - out shouldBe Vector(checkpoint) - } - - it should "not add a checkpoint invoked from timeout when its offset is before ledgerEnd and requesting from ledgerEnd" in new Scope { - val checkpoint = end - 1 - - val out: Seq[Long] = - Source - .single((Offset.MaxValue, Timeout)) - .via( - injectCheckpoints( - fetchOffsetCheckpoint = fetchOffsetCheckpoint(checkpoint), - responseFromCheckpoint = _ => (), - startExclusive = Some(Offset.tryFromLong(end)), - ) - ) - .runWith(Sink.seq) - .futureValue - .map(_._1) - .map(_.unwrap) - out shouldBe empty - } - - it should "not add a checkpoint invoked from timeout when its offset is less or equal to the last streamed checkpoint" in new Scope { - val elements = 1L to end - val checkpoint = 10L - - val out: Seq[Long] = - createSource(elements) - .concat(Source.single((Offset.MaxValue, Timeout))) - .via( - injectCheckpoints(fetchOffsetCheckpoint(checkpoint), _ => (), None) - ) - .runWith(Sink.seq) - .futureValue - .map(_._1) - .map(_.unwrap) - out shouldBe elements :+ checkpoint - } - - it should "not add the same checkpoint invoked from timeout" in new Scope { - val elements = 1L to end - val checkpoint = 10L - - val out: Seq[Long] = - createSource(elements) - .concat(Source(Seq((Offset.MaxValue, Timeout), (Offset.MaxValue, Timeout)))) - .via( - injectCheckpoints(fetchOffsetCheckpoint(checkpoint), _ => (), None) - ) - .runWith(Sink.seq) - .futureValue - .map(_._1) - .map(_.unwrap) - out shouldBe elements :+ checkpoint - } - - // checkpoint for element at offset #xx is denoted by Cxx, - // (xx, RB) is the RangeBegin indicator at offset #xx - // (xx, RE) is the RangeEnd indicator at offset #xx - // TO is the Timeout indicator - // NC means no checkpoint is there - // e.g. (1,RB), C3, 1, 2, (2,RE), (3,RB), C3, 3, (3,RE) -shouldBe> 1, 2, 3, C3 - private val u: Option[Unit] = Some(()) - private val e: Carrier[Option[Unit]] = Element(u) - private val RB: Carrier[Option[Unit]] = RangeBegin - private val RE: Carrier[Option[Unit]] = RangeEnd - private val TO: (Int, Carrier[Option[Unit]]) = (Int.MaxValue, Timeout) - private def fetchOffsetCheckpoints( - checkpoints: mutable.Queue[Option[Int]] - ): () => Option[OffsetCheckpoint] = - () => - checkpoints - .dequeue() - .map(x => - OffsetCheckpoint(offset = Offset.tryFromLong(x.toLong), synchronizerTimes = Map.empty) - ) - - it should "add a checkpoint if checkpoint arrived faster than the elements" in new Scope { - // (1,RB), C3, 1, 2, (2,RE), (3,RB), C3, 3, (3,RE) -shouldBe> 1, 2, 3, C3 - - private val source = Source( - Seq((1, RB), (1, e), (2, e), (2, RE), (3, RB), (3, e), (3, RE)) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(Some(3), Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u), (3, None)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "add a checkpoint if checkpoint arrived exactly after the elements" in new Scope { - // (1,RB), NC, 1, 2, (2,RE), (3,RB), C2, 3, (3,RE) -shouldBe> 1, 2, C2, 3 - - private val source = Source( - Seq((1, RB), (1, e), (2, e), (2, RE), (3, RB), (3, e), (3, RE)) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(None, Some(2)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (2, None), (3, u)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "not add a checkpoint if checkpoint arrived later than the elements" in new Scope { - // (1,RB), NC, 1, 2, (2,RE), (3,RB), C1, 3, (3,RE) -shouldBe> 1, 2, 3 - - private val source = Source( - Seq((1, RB), (1, e), (2, e), (2, RE), (3, RB), (3, e), (3, RE)) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(None, Some(1)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "add multiple checkpoints" in new Scope { - // (1,RB), NC, 1, 2, (2,RE), (3,RB), C2, 3, (3,RE), (4,RB), C3, (4,RE), (5,RB), C4, (5,RE), (6,RB), C4, (9,RE), (10,RB), C9, (10,RE), (11,RB), C12, 11, 13, 14, (15, RE) - // -shouldBe> 1, 2, C2, 3, C3, C4, C9, 11, C12, 13, 14 - - private val source = Source( - Seq( - (1, RB), // no checkpoint - (1, e), - (2, e), - (2, RE), - (3, RB), // C2 - (3, e), - (3, RE), - (4, RB), // C3 - (4, RE), - (5, RB), // C4 - (5, RE), - (6, RB), // C4 - (9, RE), - (10, RB), // C9 - (10, RE), - (11, RB), // C12 - (11, e), - (13, e), - (14, e), - (15, RE), - ) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = - mutable.Queue(None, Some(2), Some(3), Some(4), Some(4), Some(9), Some(12)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - // -shouldBe> 1, 2, C2, 3, C3, C4, C9, 11, C12, 13, 14 - out shouldBe - Seq( - (1, u), - (2, u), - (2, None), - (3, u), - (3, None), - (4, None), - (9, None), - (11, u), - (12, None), - (13, u), - (14, u), - ).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "add checkpoints for dormant streams" in new Scope { - // (1,RB), NC, 1, 2, 3, (3,RE), (TO), C3 -shouldBe> 1, 2, 3, C3 - - private val source = Source( - Seq((1, RB), (1, e), (2, e), (3, e), (3, RE), TO) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(None, Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u), (3, None)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "add checkpoints for dormant streams at ledger end" in new Scope { - // (TO), C3 -shouldBe> C3 - - private val source = Source( - Seq(TO) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints( - fetchOffsetCheckpoint = fetchOffsetCheckpoints(checkpoints), - responseFromCheckpoint = _ => None, - startExclusive = Some(Offset.tryFromLong(3)), - ) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe Seq((Offset.tryFromLong(3), None)) - } - - it should "add checkpoints for dormant streams at ledger end with slowly updated cache" in new Scope { - // (TO), NC, (TO), C2, (TO), C3 -shouldBe> C3 - - private val source = Source( - Seq(TO, TO, TO) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(None, Some(2), Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints( - fetchOffsetCheckpoint = fetchOffsetCheckpoints(checkpoints), - responseFromCheckpoint = _ => None, - startExclusive = Some(Offset.tryFromLong(3)), - ) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe Seq((Offset.tryFromLong(3), None)) - } - - it should "not repeat checkpoints after dormant stream checkpoint" in new Scope { - // (TO), C3, (4, RB), C3, (4, RE) -shouldBe> C3 - - private val source = Source( - Seq(TO, (4, RB), (4, RE)) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(Some(3), Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints( - fetchOffsetCheckpoint = fetchOffsetCheckpoints(checkpoints), - responseFromCheckpoint = _ => None, - startExclusive = Some(Offset.tryFromLong(3)), - ) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe Seq((Offset.tryFromLong(3), None)) - } - - it should "add checkpoints at the right spot when streaming far from history" in new Scope { - // (1,RB), NC, 1, 2, 3, (3,RE), (TO), C5, (4, RB), C5, (4, RE), (5, RB), C5, 5, (5, RE) -shouldBe> 1, 2, 3, 5, C5 - - private val source = Source( - Seq((1, RB), (1, e), (2, e), (3, e), (3, RE), TO, (4, RB), (4, RE), (5, RB), (5, e), (5, RE)) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = - mutable.Queue(None, Some(5), Some(5), Some(5)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u), (5, u), (5, None)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "add checkpoints at the right spot when streaming far from history when element for offset is not output" in new Scope { - // (1,RB), NC, 1, 2, 3, (3,RE), (TO), C5, (4, RB), C5, (4, RE), (5, RB), C5, (5, RE) -shouldBe> 1, 2, 3, C5 - - private val source = Source( - Seq((1, RB), (1, e), (2, e), (3, e), (3, RE), TO, (4, RB), (4, RE), (4, RB), (5, RE)) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = - mutable.Queue(None, Some(5), Some(5), Some(5)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u), (5, None)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "not repeat checkpoints after regular checkpoint" in new Scope { - // e.g. (1,RB), C3, 1, 2, 3, (3,RE), (TO), C3 -shouldBe> 1, 2, 3, C3 - - private val source = Source( - Seq((1, RB), (1, e), (2, e), (3, e), (3, RE), TO) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(Some(3), Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u), (3, None)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "not repeat checkpoints after timeout checkpoint" in new Scope { - // e.g. (1,RB), NC, 1, 2, 3, (3,RE), (TO), C3, (TO), C3 -shouldBe> 1, 2, 3, C3 - - private val source = Source( - Seq((1, RB), (1, e), (2, e), (3, e), (3, RE), TO, TO) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(None, Some(3), Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u), (3, None)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "not emit checkpoints when timeout is the first" in new Scope { - // e.g. NC, (TO), (1,RB), 1, 2, (2,RE), (2, RB), 3, (3, RE) -shouldBe> 1, 2, 3, C3 - - private val source = Source( - Seq(TO, (1, RB), (1, e), (2, e), (2, RE), (2, RB), (3, e), (3, RE)) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(Some(3), Some(3), Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue(timeout = PatienceConfiguration.Timeout(1.second)) - - out shouldBe - Seq((1, u), (2, u), (3, u), (3, None)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "not add checkpoints in the middle of a range" in new Scope { - // (1,RB), NC, 1, (TO), C3, 2, 3, (3,RE) -shouldBe> 1, 2, 3 - - private val source = Source( - Seq((1, RB), (1, e), TO, (2, e), (3, e), (3, RE)) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = mutable.Queue(None, Some(3)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints(fetchOffsetCheckpoints(checkpoints), _ => None, None) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - it should "continue regularly after idleness period" in new Scope { - // e.g. (1,RB), NC, 1, 2, 3, (3,RE), (TO), C3, (TO), C3, (TO), C3, (3, RB), C3, 4, (4,RE), (4,RB), C4, (5,RE) -shouldBe> 1, 2, 3, C3, 4, C4 - - private val source = Source( - Seq( - (1, RB), // no checkpoint - (1, e), - (2, e), - (3, e), - (3, RE), - TO, // C3 - TO, // C3 - TO, // C3 - (4, RB), // C3 - (4, e), - (4, RE), - (5, RB), // C4 - (5, RE), - ) - ).map { case (o, elem) => (Offset.tryFromLong(o.toLong), elem) } - - private val checkpoints: mutable.Queue[Option[Int]] = - mutable.Queue(None, Some(3), Some(3), Some(3), Some(3), Some(4)) - - val out: Seq[(Offset, Option[Unit])] = - source - .via( - injectCheckpoints( - fetchOffsetCheckpoint = fetchOffsetCheckpoints(checkpoints), - responseFromCheckpoint = _ => None, - startExclusive = None, - ) - ) - .runWith(Sink.seq) - .futureValue - - out shouldBe - Seq((1, u), (2, u), (3, u), (3, None), (4, u), (4, None)).map { case (o, elem) => - (Offset.tryFromLong(o.toLong), elem) - } - } - - behavior of "IndexServiceImpl.processAscendingPageData" - - val allUpdatesFormat = updateFormatForTransactions(eventFormat = - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = None, - verbose = false, - ) - ) - - it should "trim updates exceeding page size" in { - val getUpdatesPageRequest = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = None, - continueStreamFromIncl = None, - maxPageSize = 2, - updateFormat = allUpdatesFormat, - descendingOrder = false, - requestChecksum = ByteString.copyFrom(Array[Byte](1)), - participantChecksum = ByteString.copyFrom(Array[Byte](2)), - ) - - val response = IndexServiceImpl.processAscendingPageData( - getUpdatesPageRequest = getUpdatesPageRequest, - loggingContext = LoggingContextWithTrace.ForTesting, - isFirstPageOfAscendingDynamicLowerBound = true, - limit = 8, - calculatedBeginExclusive = Some(Offset.tryFromLong(1)), - calculatedEndInclusive = Some(Offset.tryFromLong(100)), - transactions = Vector( - mockTransaction(1L), - mockTransaction(2L), - mockTransaction(3L), - mockTransaction(5L), - mockTransaction(6L), - ), - pruningOffsetAfterFetch = Some(Offset.tryFromLong(2)), - logger = mock[TracedLogger], - ) - - response.updates should contain theSameElementsInOrderAs Seq( - mockTransaction(3L), - mockTransaction(5L), - ) - response.nextPageToken should not be empty - response.lowestPageOffsetExclusive should equal(2L) - response.highestPageOffsetInclusive should equal(5L) - } - - it should "fail if pruning offset advance does not allow to generate full page" in { - val getUpdatesPageRequest = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = None, - continueStreamFromIncl = None, - maxPageSize = 2, - updateFormat = allUpdatesFormat, - descendingOrder = false, - requestChecksum = ByteString.copyFrom(Array[Byte](1)), - participantChecksum = ByteString.copyFrom(Array[Byte](2)), - ) - - assertThrows[StatusRuntimeException]( - IndexServiceImpl.processAscendingPageData( - getUpdatesPageRequest = getUpdatesPageRequest, - loggingContext = LoggingContextWithTrace.ForTesting, - isFirstPageOfAscendingDynamicLowerBound = true, - limit = 6, - calculatedBeginExclusive = Some(Offset.tryFromLong(1)), - calculatedEndInclusive = Some(Offset.tryFromLong(100)), - transactions = Vector( - mockTransaction(1L), - mockTransaction(2L), - mockTransaction(3L), - mockTransaction(4L), - mockTransaction(5L), - mockTransaction(6L), - ), - pruningOffsetAfterFetch = Some(Offset.tryFromLong(5)), - logger = TracedLogger(NOPLogger.NOP_LOGGER), - ) - ) - } - - it should "succeed if pruning offset advance does not allow to generate full page, but the whole range was fetched" in { - val getUpdatesPageRequest = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = None, - continueStreamFromIncl = None, - maxPageSize = 2, - updateFormat = allUpdatesFormat, - descendingOrder = false, - requestChecksum = ByteString.copyFrom(Array[Byte](1)), - participantChecksum = ByteString.copyFrom(Array[Byte](2)), - ) - - val response = - IndexServiceImpl.processAscendingPageData( - getUpdatesPageRequest = getUpdatesPageRequest, - loggingContext = LoggingContextWithTrace.ForTesting, - isFirstPageOfAscendingDynamicLowerBound = true, - limit = 6, - calculatedBeginExclusive = Some(Offset.tryFromLong(1)), - calculatedEndInclusive = Some(Offset.tryFromLong(100)), - transactions = Vector( - mockTransaction(1L), - mockTransaction(2L), - mockTransaction(50L), // Simulate a lot of filtered offsets - ), - pruningOffsetAfterFetch = Some(Offset.tryFromLong(2)), - logger = mock[TracedLogger], - ) - - response.nextPageToken should not be empty - response.updates should contain theSameElementsInOrderAs Seq(mockTransaction(50L)) - response.lowestPageOffsetExclusive should equal(2L) - response.highestPageOffsetInclusive should equal(100L) - } - - it should "succeed if pruning offset advance does not allow to generate full page, but the whole range was fetched and reached the end" in { - val getUpdatesPageRequest = GetUpdatesPageRequest( - startExclusive = None, - endInclusive = Some(Offset.tryFromLong(100L)), - continueStreamFromIncl = None, - maxPageSize = 2, - updateFormat = allUpdatesFormat, - descendingOrder = false, - requestChecksum = ByteString.copyFrom(Array[Byte](1)), - participantChecksum = ByteString.copyFrom(Array[Byte](2)), - ) - - val response = - IndexServiceImpl.processAscendingPageData( - getUpdatesPageRequest = getUpdatesPageRequest, - loggingContext = LoggingContextWithTrace.ForTesting, - isFirstPageOfAscendingDynamicLowerBound = true, - limit = 6, - calculatedBeginExclusive = Some(Offset.tryFromLong(1)), - calculatedEndInclusive = Some(Offset.tryFromLong(100)), - transactions = Vector( - mockTransaction(1L), - mockTransaction(2L), - mockTransaction(50L), // Simulate a lot of filtered offsets - ), - pruningOffsetAfterFetch = Some(Offset.tryFromLong(2)), - logger = mock[TracedLogger], - ) - - response.nextPageToken should be(empty) - response.updates should contain theSameElementsInOrderAs Seq(mockTransaction(50L)) - response.lowestPageOffsetExclusive should equal(2L) - response.highestPageOffsetInclusive should equal(100L) - } - - def mockTransaction(offset: Long): GetUpdateResponse = - GetUpdateResponse( - GetUpdateResponse.Update.Transaction( - com.daml.ledger.api.v2.transaction.Transaction( - updateId = s"u$offset", - commandId = s"c$offset", - workflowId = s"w$offset", - effectiveAt = Some(Timestamp.defaultInstance), - events = Vector(), - offset = offset, - synchronizerId = "synchronizer", - traceContext = None, - recordTime = Some(Timestamp.defaultInstance), - externalTransactionHash = None, - paidTrafficCost = None, - ) - ) - ) - - def updateFormatForTransactions(eventFormat: EventFormat): UpdateFormat = - UpdateFormat( - includeTransactions = - Some(TransactionFormat(eventFormat = eventFormat, transactionShape = AcsDelta)), - includeReassignments = None, - includeTopologyEvents = None, - ) - - def updateFormatForReassignments(eventFormat: EventFormat): UpdateFormat = - UpdateFormat( - includeTransactions = None, - includeReassignments = Some(eventFormat), - includeTopologyEvents = None, - ) - - def internalUpdateFormatForTransactions( - internalEventFormat: InternalEventFormat - ): InternalUpdateFormat = - InternalUpdateFormat( - includeTransactions = Some( - InternalTransactionFormat( - internalEventFormat = internalEventFormat, - transactionShape = AcsDelta, - ) - ), - includeReassignments = None, - includeTopologyEvents = None, - ) - - def internalUpdateFormatForReassignments( - internalEventFormat: InternalEventFormat - ): InternalUpdateFormat = - InternalUpdateFormat( - includeTransactions = None, - includeReassignments = Some(internalEventFormat), - includeTopologyEvents = None, - ) - -} - -object IndexServiceImplSpec { - trait Scope extends MockitoSugar { - val party: Party = Party.assertFromString("party") - val party2: Party = Party.assertFromString("party2") - val templateQualifiedName1: QualifiedName = - QualifiedName.assertFromString("ModuleName:template1") - - val packageName1: Ref.PackageName = Ref.PackageName.assertFromString("PackageName1") - val packageName1Ref: Ref.PackageRef = Ref.PackageRef.Name(packageName1) - val template1Id: Identifier = Identifier.assertFromString("PackageId:ModuleName:template1") - val template1: NameTypeConRef = template1Id.toFullIdentifier(packageName1).toNameTypeConRef - val template1Filter: TemplateFilter = - TemplateFilter( - templateTypeRef = template1, - includeCreatedEventBlob = false, - ) - - val packageNameScopedTemplateFilter: TemplateFilter = - TemplateFilter( - templateTypeRef = NameTypeConRef.assertFromString(s"$packageName1Ref:ModuleName:template1"), - includeCreatedEventBlob = false, - ) - val template2Id: Identifier = Identifier.assertFromString("PackageId:ModuleName:template2") - val template2: NameTypeConRef = template2Id.toFullIdentifier(packageName1).toNameTypeConRef - val template2Filter: TemplateFilter = - TemplateFilter(templateTypeRef = template2, includeCreatedEventBlob = false) - val template3Id: Identifier = Identifier.assertFromString("PackageId:ModuleName:template3") - val template3: NameTypeConRef = template3Id.toFullIdentifier(packageName1).toNameTypeConRef - val template3Filter: TemplateFilter = - TemplateFilter(templateTypeRef = template3, includeCreatedEventBlob = false) - val iface1Id: Identifier = Identifier.assertFromString("PackageId:ModuleName:iface1") - val iface1Full: FullIdentifier = iface1Id.toFullIdentifier(packageName1) - val iface1Filter: InterfaceFilter = InterfaceFilter( - iface1Full.toNameTypeConRef, - includeView = true, - includeCreatedEventBlob = false, - ) - val packageNameScopedInterfaceFilter = InterfaceFilter( - interfaceTypeRef = NameTypeConRef.assertFromString(s"$packageName1Ref:ModuleName:iface1"), - includeView = true, - includeCreatedEventBlob = false, - ) - val iface2Id: Identifier = Identifier.assertFromString("PackageId:ModuleName:iface2") - val iface2: NameTypeConRef = iface2Id.toFullIdentifier(packageName1).toNameTypeConRef - val iface2Filter: InterfaceFilter = InterfaceFilter( - iface2, - includeView = true, - includeCreatedEventBlob = false, - ) - @volatile var currentPackageMetadata = PackageMetadata() - val getPackageMetadata: ErrorLoggingContext => PackageMetadata = _ => currentPackageMetadata - val packageResolutionForTemplate1: PackageResolution = PackageResolution( - preference = LocalPackagePreference( - Ref.PackageVersion.assertFromString("0.1"), - template1Id.packageId, - ), - allPackageIdsForName = NonEmpty(Set, template1Id.packageId), - ) - val packageResolutionForInterface1 = PackageResolution( - preference = LocalPackagePreference( - Ref.PackageVersion.assertFromString("0.1"), - iface1Id.packageId, - ), - allPackageIdsForName = NonEmpty(Set, iface1Id.packageId), - ) - val packageMetadata_iface1: PackageMetadata = PackageMetadata( - interfaces = Set(iface1Id), - templates = Set.empty, - interfacesImplementedBy = Map.empty, - packageIdVersionMap = Map( - iface1Id.packageId -> (packageName1 -> Ref.PackageVersion.assertFromString("1.0.0")) - ), - packageNameMap = Map( - packageName1 -> PackageResolution( - LocalPackagePreference( - Ref.PackageVersion.assertFromString("1.0.0"), - iface1Id.packageId, - ), - NonEmpty(Set, iface1Id.packageId), - ) - ), - ) - val packageMetadata_iface1_template1: PackageMetadata = packageMetadata_iface1.copy( - templates = Set(template1Id), - interfacesImplementedBy = Map(iface1Id -> Set(template1Id)), - ) - - val packageMetadata_iface1_template2: PackageMetadata = PackageMetadata( - interfaces = Set(iface1Id), - templates = Set(template2Id), - interfacesImplementedBy = Map(iface1Id -> Set(template2Id)), - packageIdVersionMap = Map( - template2Id.packageId -> (packageName1 -> Ref.PackageVersion.assertFromString("1.0.0")) - ), - packageNameMap = Map( - packageName1 -> PackageResolution( - LocalPackagePreference( - Ref.PackageVersion.assertFromString("1.0.0"), - template2Id.packageId, - ), - NonEmpty(Set, template2Id.packageId), - ) - ), - ) - - val packageMetadata_iface1_template1_template2: PackageMetadata = - packageMetadata_iface1_template2.copy(templates = Set(template1Id, template2Id)) - - val packageMetadata_iface2_template2: PackageMetadata = PackageMetadata( - interfaces = Set(iface2Id), - templates = Set(template2Id), - interfacesImplementedBy = Map(iface2Id -> Set(template2Id)), - packageIdVersionMap = Map( - template2Id.packageId -> (packageName1 -> Ref.PackageVersion.assertFromString("1.0.0")) - ), - packageNameMap = Map( - packageName1 -> PackageResolution( - LocalPackagePreference( - Ref.PackageVersion.assertFromString("1.0.0"), - template2Id.packageId, - ), - NonEmpty(Set, template2Id.packageId), - ) - ), - ) - - val packageMetadata_template3: PackageMetadata = PackageMetadata( - templates = Set(template3Id) - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/IndexerStateSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/IndexerStateSpec.scala deleted file mode 100644 index fac050f270..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/IndexerStateSpec.scala +++ /dev/null @@ -1,1607 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer - -import cats.data.EitherT -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.ledger.participant.state.{RepairUpdate, Update} -import com.digitalasset.canton.logging.SuppressionRule -import com.digitalasset.canton.platform.indexer.IndexerState.RepairInProgress -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.PekkoUtil.{FutureQueue, RecoveringFutureQueue} -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import org.apache.pekko.Done -import org.scalatest.Assertion -import org.scalatest.flatspec.AnyFlatSpec - -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.{Future, Promise} - -class IndexerStateSpec extends AnyFlatSpec with BaseTest with HasExecutionContext { - - behavior of "IndexerState" - - it should "successfully initiate indexer and shut down" in { - val initialIndexer = new TestRecoveringIndexer - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory(), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - // shutting down - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // and as the ongoing indexing is shut - initialIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "orchestrate repair operation correctly during the happy path" in { - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { repairQueue => - repairQueue.offer(repairUpdate).futureValue shouldBe Done - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes - repairOperationFinishedPromise.trySuccess(()) - // first the the CommitRepair is expected to be offered to the repairIndexer - repairIndexer.repairReceivedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // then waiting for the message to be persisted - repairIndexer.repairPersistedPromise.trySuccess(()) - // then the repair-indexer should be completing without shutdown signal - repairIndexer.donePromise.trySuccess(Done) - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and also the new indexer is completed it's first successfull initialization - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed - repairOperationF.futureValue - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - afterRepairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // and as the ongoing indexing is shut - afterRepairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "fail repair operation if repair is in progress, but can proceed after repairDone" in { - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // subsequent repair operation should fail immediately, signalling back repairDone - val repairDone = indexerState.withRepairIndexer(_ => fail()).value.failed.futureValue match { - case repairInProgress: RepairInProgress => repairInProgress.repairDone - case unexpected => fail(s"RepairInProgress expected, but was $unexpected") - } - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes - repairOperationFinishedPromise.trySuccess(()) - // first the the CommitRepair is expected to be offered to the repairIndexer - repairIndexer.repairReceivedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // then waiting for the message to be persisted - repairIndexer.repairPersistedPromise.trySuccess(()) - // then the repair-indexer should be completing without shutdown signal - repairIndexer.donePromise.trySuccess(Done) - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - repairDone.isCompleted shouldBe false - // and also the new indexer is completed it's first successfull initialization - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed - repairOperationF.futureValue - // also repairDone should be completed - repairDone.futureValue - // and as the repairDone completes new repair operation should be able to initiate - val repairOperationF2 = indexerState.withRepairIndexer(_ => fail()).value - // and it can be observed the the indexer is going down - afterRepairIndexer.shutdownPromise.future.futureValue - // and as shutting down the IndexerState - val indexerStateTerminatedF = indexerState.shutdown() - // and failing the shutdown of the after-repair-indexer - afterRepairIndexer.donePromise.tryFailure(new RuntimeException("failed")) - // repair operation completes with failure - repairOperationF2.failed.futureValue.getMessage shouldBe "failed" - // and indexer state should be terminated - indexerStateTerminatedF.futureValue - } - - it should "fail repair operation during shutdown" in { - val initialIndexer = new TestRecoveringIndexer - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory(), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // shutting down - val indexerStateTerminated = indexerState.shutdown() - // repair operation should fail immediately - indexerState - .withRepairIndexer(_ => fail()) - .value - .failed - .futureValue - .getMessage shouldBe "Shutdown in progress" - // first the ongoing indexing should be shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // and as the ongoing indexing is shut - initialIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "fail repair operation if the repair indexer failing to come up (also switch back to normal indexing)" in { - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationF = indexerState.withRepairIndexer(_ => fail()).value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - // and as repair indexer is created - repairIndexerCreated.tryFailure(new RuntimeException("repair indexer failed to initialize")) - // then the repair-indexer should be completing without shutdown signal - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - // and also the new indexer is completed it's first successfull initialization - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed with a failure - repairOperationF.failed.futureValue.getMessage shouldBe "repair indexer failed to initialize" - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - afterRepairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // and as the ongoing indexing is shut - afterRepairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - private def testRepairFunctionFailure( - repairFunctionOutcome: Future[Either[String, Unit]], - repairOperationAssertion: Future[Either[String, Unit]] => Assertion, - ): Unit = { - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT(repairOperationFinishedPromise.future.flatMap(_ => repairFunctionOutcome)) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes with failure - repairOperationFinishedPromise.trySuccess(()) - // then the repair-indexer should receive a shutdown (and no RepairCommit) - repairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - repairIndexer.repairReceivedPromise.isCompleted shouldBe false - // and as the repairIndexer is shut - repairIndexer.donePromise.trySuccess(Done) - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - repairIndexer.repairReceivedPromise.isCompleted shouldBe false - // and also the new indexer is completed it's first successfull initialization - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed - repairOperationAssertion(repairOperationF) - repairIndexer.repairReceivedPromise.isCompleted shouldBe false - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - afterRepairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // and as the ongoing indexing is shut - afterRepairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "fail repair operation if the provided repair function fails with an Either.Left (also should tear down repair indexer, and switch back to normal indexing)" in - testRepairFunctionFailure( - repairFunctionOutcome = Future.successful(Left("failed")), - repairOperationAssertion = _.futureValue shouldBe Left("failed"), - ) - - it should "fail repair operation if the provided repair function fails with an Future.failed (also should tear down repair indexer, and switch back to normal indexing)" in - testRepairFunctionFailure( - repairFunctionOutcome = Future.failed(new RuntimeException("failed with exception")), - repairOperationAssertion = _.failed.futureValue.getMessage shouldBe "failed with exception", - ) - - it should "fail repair operation if the provided repair function fails with an exception before Future (also should tear down repair indexer, and switch back to normal indexing)" in { - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - throw new RuntimeException("failed with exception before Future") - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - // then the repair-indexer should receive a shutdown (and no RepairCommit) - repairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - repairIndexer.repairReceivedPromise.isCompleted shouldBe false - // and as the repairIndexer is shut - repairIndexer.donePromise.trySuccess(Done) - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - repairIndexer.repairReceivedPromise.isCompleted shouldBe false - // and also the new indexer is completed it's first successfull initialization - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed - repairOperationF.failed.futureValue.getMessage shouldBe "failed with exception before Future" - repairIndexer.repairReceivedPromise.isCompleted shouldBe false - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - afterRepairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // and as the ongoing indexing is shut - afterRepairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "fail repair operation if the provided repair function succeeds, but commit fails to persist (also should tear down repair indexer, and switch back to normal indexing)" in { - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes - repairOperationFinishedPromise.trySuccess(()) - // first the the CommitRepair is expected to be offered to the repairIndexer - repairIndexer.repairReceivedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - - loggerFactory.assertLogs( - { - // then waiting for the message to be persisted, but that fails - repairIndexer.repairPersistedPromise.tryFailure( - new RuntimeException("commit failed to process") - ) - // then the repair-indexer should receive the shutdown signal - repairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - // and as the repair-indexer shuts down - repairIndexer.donePromise.trySuccess(Done) - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - // and also the new indexer is completed it's first successfull initialization - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed with a failure - repairOperationF.failed.futureValue.getMessage shouldBe "Committing repair changes failed" - repairOperationF.failed.futureValue.getCause.getMessage shouldBe "commit failed to process" - }, - _.warningMessage should include( - "Committing repair changes failed, resuming normal indexing..." - ), - ) - - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - afterRepairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // and as the ongoing indexing is shut - afterRepairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "not fail an otherwise successfull repair operation after successful persisting the RepairCommit message, if the repair indexer shutdown is not successful" in { - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes - repairOperationFinishedPromise.trySuccess(()) - // first the the CommitRepair is expected to be offered to the repairIndexer - repairIndexer.repairReceivedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // then waiting for the message to be persisted - repairIndexer.repairPersistedPromise.trySuccess(()) - - loggerFactory.assertLogs( - { - // then the repair-indexer should be completing without shutdown signal - repairIndexer.donePromise.tryFailure(new RuntimeException("shutdown failure")) - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and also the new indexer is completed it's first successfull initialization - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed - repairOperationF.futureValue - }, - _.warningMessage should include("Repair Indexer finished with error"), - ) - - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - afterRepairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // and as the ongoing indexing is shut - afterRepairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "not even initiate the repair indexer if getting shutdown before initialization started" in { - val initialIndexer = new TestRecoveringIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - fail() - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // inbetween shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // and then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // then repair operation should be terminating too - repairOperationF.failed.futureValue.getMessage shouldBe "Shutdown in progress" - // indexer state should be terminated - indexerStateTerminated.futureValue - // and repair indexer never started to initialize - repairIndexerFactoryCalled.isCompleted shouldBe false - } - - it should "not even start the repair operation if getting shut down during repair indexer initialization" in { - val initialIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - fail() - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - Threading.sleep(20) - // repair operation does not start - repairOperationStartedPromise.isCompleted shouldBe false - // repair operation should be completed with a failure - repairOperationF.failed.futureValue.getMessage shouldBe "Shutdown in progress" - // then repair-indexer should receive a shutdown signal - repairIndexer.shutdownPromise.future.futureValue - // then the repair-indexer should be completing - repairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - // repair operation never started - repairOperationStartedPromise.isCompleted shouldBe false - } - - it should "not attempt to commit repair if getting shut down during repair operation, which then results in an success" in { - val initialIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // then the repair-indexer should receive a shutdown signal - repairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes with success - repairOperationFinishedPromise.trySuccess(()) - // repair operation should be finished with an error already before the repairIndexer is shut - repairOperationF.failed.futureValue.getMessage shouldBe "Shutdown in progress" - Threading.sleep(20) - // and as repairIndexer is shut - repairIndexer.donePromise.trySuccess(Done) - // eventually indexer state should be terminated - indexerStateTerminated.futureValue - Threading.sleep(20) - // and committing the changes should never be triggered - repairIndexer.repairReceivedPromise.isCompleted shouldBe false - } - - it should "not attempt to resume indexing if getting shut down during repair operation, which then results in an error" in { - val initialIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // then the repair-indexer should receive a shutdown signal - repairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes with an error - repairOperationFinishedPromise.tryFailure(new RuntimeException("aborted")) - // repair operation should be finished with an error already before the repairIndexer is shut - repairOperationF.failed.futureValue.getMessage shouldBe "Shutdown in progress" - Threading.sleep(20) - // and as repairIndexer is shut - repairIndexer.donePromise.trySuccess(Done) - // eventually indexer state should be terminated - indexerStateTerminated.futureValue - // and committing the changes should never be triggered - repairIndexer.repairReceivedPromise.isCompleted shouldBe false - } - - it should "not fail an otherwise successfull repair operation after successful persisting the RepairCommit message, if getting shutdown during committing" in { - val initialIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes - repairOperationFinishedPromise.trySuccess(()) - // first the the CommitRepair is expected to be offered to the repairIndexer - repairIndexer.repairReceivedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and then shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // repair indexer should receive the shutdown signal immediately - repairIndexer.shutdownPromise.future.futureValue - // then the message is persisted - repairIndexer.repairPersistedPromise.trySuccess(()) - // then the repair-indexer should be completing - repairIndexer.donePromise.trySuccess(Done) - Threading.sleep(20) - // repair operation should be completed successfully regardless - repairOperationF.futureValue - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "fail a repair operation, which failed at persisting the RepairCommit message, with the RepairCommit failed error, if getting shutdown during committing" in { - val initialIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes - repairOperationFinishedPromise.trySuccess(()) - // first the the CommitRepair is expected to be offered to the repairIndexer - repairIndexer.repairReceivedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and then shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // repair indexer should receive the shutdown signal immediately - repairIndexer.shutdownPromise.future.futureValue - - loggerFactory.assertLogs( - { - // then the commit message fails persisting - repairIndexer.repairPersistedPromise.tryFailure(new RuntimeException("failed to persist")) - // then the repair-indexer should be completing - repairIndexer.donePromise.trySuccess(Done) - // repair operation should be completed with the commit failure - repairOperationF.failed.futureValue.getMessage shouldBe "Committing repair changes failed" - repairOperationF.failed.futureValue.getCause.getMessage shouldBe "failed to persist" - }, - _.warningMessage should include( - "Committing repair changes failed, resuming normal indexing..." - ), - ) - - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "not fail otherwise successful repair operation if shutdown precedes initialization of the next indexer, but report a warning" in { - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { _ => - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes - repairOperationFinishedPromise.trySuccess(()) - // first the the CommitRepair is expected to be offered to the repairIndexer - repairIndexer.repairReceivedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - - loggerFactory.assertEventuallyLogsSeq( - SuppressionRule.forLogger[IndexerState] && SuppressionRule.Level(org.slf4j.event.Level.INFO) - )( - within = { - // then waiting for the message to be persisted - repairIndexer.repairPersistedPromise.trySuccess(()) - // then the repair-indexer should be completing without shutdown signal - repairIndexer.donePromise.trySuccess(Done) - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - }, - // waiting until the next indexer is started, so shutdown comes after the IndexerState transition - assertion = _.find(_.infoMessage.contains("Switched to Normal Mode")).nonEmpty shouldBe true, - ) - - // as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - afterRepairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as the ongoing indexing is shut - afterRepairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - Threading.sleep(20) - repairOperationF.isCompleted shouldBe false - // and also the new indexer signals first successful initialization failure - // (this is the expected behavior from the RecoveringIndexer) - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed - repairOperationF.futureValue - } - - behavior of "ensureNoProcessingForSynchronizer" - - it should "work as expected" in { - val synchronizer1 = SynchronizerId.tryFromString("x::synchronizer1") - val synchronizer2 = SynchronizerId.tryFromString("x::synchronizer2") - val synchronizer3 = SynchronizerId.tryFromString("x::synchronizer3") - - val initialIndexer = new TestRecoveringIndexer - val afterRepairIndexer = new TestRecoveringIndexer - val repairIndexer = new TestRepairIndexer - val repairIndexerFactoryCalled = Promise[Unit]() - val repairIndexerCreated = Promise[FutureQueue[Update]]() - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory( - initialIndexer, - afterRepairIndexer, - ), - repairIndexerFactory = asyncSeqFactory( - repairIndexerFactoryCalled -> repairIndexerCreated.future - ), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - 3L -> update.copy(synchronizerId = synchronizer1), - 4L -> update.copy(synchronizerId = synchronizer1), - 5L -> update.copy(synchronizerId = synchronizer2), - 6L -> update.copy(synchronizerId = synchronizer2), - 7L -> update.copy(synchronizerId = synchronizer3), - 8L -> update.copy(synchronizerId = synchronizer3), - ) - ) - val ensureSynchronizer1 = indexerState.ensureNoProcessingForSynchronizer(synchronizer1) - val ensureSynchronizer2 = indexerState.ensureNoProcessingForSynchronizer(synchronizer2) - val ensureSynchronizer3 = indexerState.ensureNoProcessingForSynchronizer(synchronizer3) - Threading.sleep(20) - ensureSynchronizer1.isCompleted shouldBe false - ensureSynchronizer2.isCompleted shouldBe false - ensureSynchronizer3.isCompleted shouldBe false - // remove the synchronizer1 - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - 6L -> update.copy(synchronizerId = synchronizer2), - 7L -> update.copy(synchronizerId = synchronizer3), - 8L -> update.copy(synchronizerId = synchronizer3), - ) - ) - Threading.sleep(110) - ensureSynchronizer1.futureValue - ensureSynchronizer2.isCompleted shouldBe false - ensureSynchronizer3.isCompleted shouldBe false - // remove the synchronizer2 - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 7L -> update.copy(synchronizerId = synchronizer3), - 8L -> update.copy(synchronizerId = synchronizer3), - ) - ) - Threading.sleep(110) - ensureSynchronizer2.futureValue - ensureSynchronizer3.isCompleted shouldBe false - // remove the synchronizer3 - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update - ) - ) - ensureSynchronizer3.futureValue - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { repairQueue => - repairQueue.offer(repairUpdate).futureValue shouldBe Done - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // as repair operation started the ensureNoProcessingForSynchronizer should fail - indexerState.ensureNoProcessingForSynchronizer(synchronizer1).failed.futureValue match { - case _: RepairInProgress => () - case invalid => fail("Invalid error", invalid) - } - // and as the indexer queue is empty - initialIndexer.uncommittedQueueSnapshotRef.set(Vector()) - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationStartedPromise.isCompleted shouldBe false - repairIndexerFactoryCalled.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // so repair Indexer is getting created - repairIndexerFactoryCalled.future.futureValue - Threading.sleep(20) - repairOperationStartedPromise.isCompleted shouldBe false - // and as repair indexer is created - repairIndexerCreated.trySuccess(repairIndexer) - // repair operation starts - repairOperationStartedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and as repair operation finishes - repairOperationFinishedPromise.trySuccess(()) - // first the the CommitRepair is expected to be offered to the repairIndexer - repairIndexer.repairReceivedPromise.future.futureValue - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // then waiting for the message to be persisted - repairIndexer.repairPersistedPromise.trySuccess(()) - // then the repair-indexer should be completing without shutdown signal - repairIndexer.donePromise.trySuccess(Done) - Threading.sleep(20) - repairIndexer.shutdownPromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // and also the new indexer is completed it's first successfull initialization - afterRepairIndexer.firstSuccessfulConsumerInitializationPromise.trySuccess(()) - // repair operation should be completed - repairOperationF.futureValue - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // first the ongoing indexing should be shut - afterRepairIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - indexerStateTerminated.isCompleted shouldBe false - // as shutdown started the ensureNoProcessingForSynchronizer should fail - indexerState - .ensureNoProcessingForSynchronizer(synchronizer1) - .failed - .futureValue - .getMessage shouldBe "Shutdown in progress" - // and as the ongoing indexing is shut - afterRepairIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - } - - it should "stop waiting for empty indexing queue if getting a shutdown signal during waiting" in { - val initialIndexer = new TestRecoveringIndexer - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory(), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - ) - ) - // starting repair operation - val repairOperationStartedPromise = Promise[Unit]() - val repairOperationFinishedPromise = Promise[Unit]() - val repairOperationF = indexerState.withRepairIndexer { repairQueue => - repairQueue.offer(repairUpdate).futureValue shouldBe Done - repairOperationStartedPromise.trySuccess(()) - EitherT.right[String](repairOperationFinishedPromise.future) - }.value - // first we wait for empty indexer queue - initialIndexer.shutdownPromise.isCompleted shouldBe false - Threading.sleep(220) - initialIndexer.shutdownPromise.isCompleted shouldBe false - // and as shutting down the indexer state - val indexerStateTerminated = indexerState.shutdown() - // the initial Indexer is getting shut - initialIndexer.shutdownPromise.future.futureValue - Threading.sleep(20) - initialIndexer.donePromise.isCompleted shouldBe false - repairOperationF.isCompleted shouldBe false - // then initial Indexer completes - initialIndexer.donePromise.trySuccess(Done) - // indexer state should be terminated - indexerStateTerminated.futureValue - // finally repair operation should be finished - repairOperationF.failed.futureValue shouldBe IndexerState.ShutdownInProgress - } - - it should "stop waiting if shutting down" in { - val synchronizer1 = SynchronizerId.tryFromString("x::synchronizer1") - - val initialIndexer = new TestRecoveringIndexer - - val indexerState = new IndexerState( - recoveringIndexerFactory = seqFactory(initialIndexer), - repairIndexerFactory = asyncSeqFactory(), - loggerFactory = loggerFactory, - ) - - // initial indexer is up and running - Threading.sleep(20) - initialIndexer.shutdownPromise.isCompleted shouldBe false - initialIndexer.donePromise.isCompleted shouldBe false - initialIndexer.uncommittedQueueSnapshotRef.set( - Vector( - 1L -> update, - 2L -> update, - 3L -> update.copy(synchronizerId = synchronizer1), - 4L -> update.copy(synchronizerId = synchronizer1), - ) - ) - val ensureSynchronizer1 = indexerState.ensureNoProcessingForSynchronizer(synchronizer1) - Threading.sleep(300) - ensureSynchronizer1.isCompleted shouldBe false - // and as shutting down the indexer state - val indexerStateTerminatedF = indexerState.shutdown() - initialIndexer.shutdownPromise.future.futureValue - initialIndexer.donePromise.trySuccess(Done) - indexerStateTerminatedF.futureValue - // ensureNoProcessingForSynchronizer should also terminate - ensureSynchronizer1.failed.futureValue shouldBe IndexerState.ShutdownInProgress - } - - behavior of "IndexerQueueProxy" - - it should "allow offer for normal indexing" in { - IndexerQueueProxy(stateF => stateF(IndexerState.Normal(new TestRecoveringIndexer, false)))( - implicitly - )(update).futureValue - } - - it should "deny offer CommitRepair for normal indexing" in { - val commitRepair = Update.CommitRepair() - IndexerQueueProxy(stateF => stateF(IndexerState.Normal(new TestRecoveringIndexer, false)))( - implicitly - )(commitRepair).failed.futureValue.getMessage shouldBe "CommitRepair should not be used" - commitRepair.persisted.future.failed.futureValue.getMessage shouldBe "CommitRepair should not be used" - } - - it should "propagate any exception to offer calls" in { - intercept[IllegalStateException]( - IndexerQueueProxy(_ => throw new IllegalStateException("nah"))(implicitly)(update) - ).getMessage shouldBe "nah" - } - - it should "deny offer during repair indexing" in { - val repairDone = Promise[Unit]() - val repairDoneReturned = IndexerQueueProxy(stateF => - stateF(IndexerState.Repair(Future.never, repairDone.future, false)) - )(implicitly)(update).failed.futureValue match { - case repairInProgress: RepairInProgress => repairInProgress.repairDone - case _ => fail() - } - repairDoneReturned.isCompleted shouldBe false - repairDone.trySuccess(()) - repairDoneReturned.futureValue - } - - class TestRecoveringIndexer extends RecoveringFutureQueue[Update] { - val donePromise = Promise[Done]() - val shutdownPromise = Promise[Unit]() - val firstSuccessfulConsumerInitializationPromise = Promise[Unit]() - val uncommittedQueueSnapshotRef = - new AtomicReference[Vector[(Long, Update)]](Vector.empty) - - override def firstSuccessfulConsumerInitialization: Future[Unit] = - firstSuccessfulConsumerInitializationPromise.future - - override def uncommittedQueueSnapshot: Vector[(Long, Update)] = - uncommittedQueueSnapshotRef.get() - - override def offer(elem: Update): Future[Done] = Future.successful(Done) - - override def shutdown(): Unit = shutdownPromise.trySuccess(()) - - override def done: Future[Done] = donePromise.future - } - - class TestRepairIndexer extends FutureQueue[Update] { - val donePromise = Promise[Done]() - val shutdownPromise = Promise[Unit]() - val repairReceivedPromise = Promise[Unit]() - val repairPersistedPromise = Promise[Unit]() - - override def offer(elem: Update): Future[Done] = elem match { - case commitRepair: Update.CommitRepair => - repairReceivedPromise.trySuccess(()) - repairPersistedPromise.future.onComplete(commitRepair.persisted.tryComplete) - Future.successful(Done) - - case _ => - Future.successful(Done) - } - - override def shutdown(): Unit = shutdownPromise.trySuccess(()) - - override def done: Future[Done] = donePromise.future - } - - def seqFactory[T](ts: T*): () => T = { - val atomicTQueue: AtomicReference[List[T]] = new AtomicReference[List[T]](ts.toList) - () => atomicTQueue.getAndUpdate(_.tail).head - } - - def asyncSeqFactory[T](promises: (Promise[Unit], Future[T])*): () => Future[T] = { - val factory = seqFactory(promises*) - () => { - val (calledPromise, resultFuture) = factory() - calledPromise.trySuccess(()) - resultFuture - } - } - - def update: Update.SequencerIndexMoved = - Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.now(), - ) - - def repairUpdate: RepairUpdate = mock[RepairUpdate] - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/TransactionTraversalUtilsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/TransactionTraversalUtilsSpec.scala deleted file mode 100644 index d6b8ff7016..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/TransactionTraversalUtilsSpec.scala +++ /dev/null @@ -1,430 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.platform.indexer.TransactionTraversalUtils.{ - NodeInfo, - arrangeNodeIdsInExecutionOrder, - executionOrderTraversalForIngestion, -} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.transaction.test.TestNodeBuilder.CreateKey -import com.digitalasset.daml.lf.transaction.test.{ - NodeIdTransactionBuilder, - TestNodeBuilder, - TransactionBuilder, -} -import com.digitalasset.daml.lf.transaction.{Node, NodeId, Transaction} -import com.digitalasset.daml.lf.value.Value -import org.scalatest.Assertion -import org.scalatest.flatspec.AnyFlatSpec - -import scala.util.Random - -class TransactionTraversalUtilsSpec extends AnyFlatSpec with BaseTest { - import TransactionBuilder.Implicits.* - - val shuffles = 100 - - object TxBuilder { - def apply(): NodeIdTransactionBuilder & TestNodeBuilder = new NodeIdTransactionBuilder - with TestNodeBuilder - } - - behavior of "executionOrderTraversalForIngestion" - - it should "handle a single create node" in { - val builder = TxBuilder() - val createNode = create("", builder) - builder.add(createNode) - val transaction = builder.build().transaction - - verifyNodeIdsAreInExecutionOrder(transaction) - - executionOrderTraversalForIngestion(transaction).toSeq shouldBe - Seq( - NodeInfo(NodeId(0), createNode, NodeId(0)) - ) - } - - it should "handle a single exercise node" in { - val builder = TxBuilder() - val exerciseNode = { - val createNode = create("", builder) - exercise("someChoice", createNode, builder) - } - builder.add(exerciseNode) - val transaction = builder.build().transaction - - verifyNodeIdsAreInExecutionOrder(transaction) - - executionOrderTraversalForIngestion(transaction).toSeq shouldBe - Seq( - NodeInfo(NodeId(0), exerciseNode, NodeId(0)) - ) - } - - it should "handle nested exercise nodes" in { - // Previous transaction - // └─ #0 Create - // Transaction - // └─ #0 Exercise (choice A) (last descendant: #2) - // ├─ #1 Exercise (choice B) (last descendant: #1) - // └─ #2 Exercise (choice C) (last descendant: #2) - val builder = TxBuilder() - val createNode0 = create("0", builder) - val exerciseNodeA = exercise("A", createNode0, builder) - val exerciseNodeB = exercise("B", createNode0, builder) - val exerciseNodeC = exercise("C", createNode0, builder) - val exerciseNodeAId = builder.add(exerciseNodeA) - builder.add(exerciseNodeB, exerciseNodeAId) - builder.add(exerciseNodeC, exerciseNodeAId) - val transaction = builder.build().transaction - - val expectedNodeInfos = Seq( - // node id, last descendant - 0 -> 2, - 1 -> 1, - 2 -> 2, - ).map { case (id, lastDescendant) => - NodeInfo(NodeId(id), transaction.nodes.get(NodeId(id)).value, NodeId(lastDescendant)) - } - - verifyRearrangements(transaction, expectedNodeInfos) - } - - it should "handle nested exercise and create nodes (right weighted execution order)" in { - // Previous transactions - // └─ #0 Create - // Transaction - // └─ #0 Exercise (choice A) (last descendant: #3) - // ├─ #1 Create A (last descendant: #1) - // └─ #2 Exercise (choice B) (last descendant: #3) - // └─ #3 Create B (last descendant: #3) - val builder = TxBuilder() - val createNode0 = create("0", builder) - val exerciseNodeA = exercise("A", createNode0, builder) - val createNodeA = create("A", builder) - val exerciseNodeB = exercise("B", createNodeA, builder) - val createNodeB = create("B", builder) - - val exerciseNodeAId = builder.add(exerciseNodeA) - builder.add(createNodeA, exerciseNodeAId) - val exerciseNodeBId = builder.add(exerciseNodeB, exerciseNodeAId) - builder.add(createNodeB, exerciseNodeBId) - val transaction = builder.build().transaction - - val expectedNodeInfos = Seq( - // node id, last descendant - 0 -> 3, - 1 -> 1, - 2 -> 3, - 3 -> 3, - ).map { case (id, lastDescendant) => - NodeInfo(NodeId(id), transaction.nodes.get(NodeId(id)).value, NodeId(lastDescendant)) - } - - verifyRearrangements(transaction, expectedNodeInfos) - } - - it should "handle nested exercise and create nodes (left weighted execution order)" in { - // Previous transactions - // └─ #0 Create - // Transaction - // └─ #0 Exercise (choice A) (last descendant: #3) - // ├─ #1 Exercise (choice B) (last descendant: #2) - // │ └─ #2 Create B (last descendant: #2) - // └─ #3 Create A (last descendant: #3) - val builder = TxBuilder() - val createNode0 = create("0", builder) - val exerciseNodeA = exercise("A", createNode0, builder) - val exerciseNodeB = exercise("B", createNode0, builder) - val createNodeB = create("B", builder) - val createNodeA = create("A", builder) - val exerciseNodeAId = builder.add(exerciseNodeA) - val exerciseNodeBId = builder.add(exerciseNodeB, exerciseNodeAId) - builder.add(createNodeB, exerciseNodeBId) - builder.add(createNodeA, exerciseNodeAId) - val transaction = builder.build().transaction - - val expectedNodeInfos = Seq( - // node id, last descendant - 0 -> 3, - 1 -> 2, - 2 -> 2, - 3 -> 3, - ).map { case (id, lastDescendant) => - NodeInfo(NodeId(id), transaction.nodes.get(NodeId(id)).value, NodeId(lastDescendant)) - } - - verifyRearrangements(transaction, expectedNodeInfos) - } - - it should "handle fetch and lookup nodes (they should not appear in output)" in { - // Previous transaction - // └─ #0 Create - // Transaction - // └─ #0 Exercise (choice A) (last descendant: #5) - // ├─ #1 Exercise (choice B) (last descendant: #2) - // │ └─ #2 Fetch B (should not appear) - // ├─ #3 Lookup A (should not appear) - // ├─ #4 Exercise (choice C) (last descendant: #4) - // └─ #5 Fetch A (should not appear) - val builder = TxBuilder() - val createNode0 = create("0", builder, withKey = true) - val exerciseNodeA = exercise("A", createNode0, builder) - val exerciseNodeB = exercise("B", createNode0, builder) - val fetchNodeB = builder.fetch(createNode0, byKey = true) - val lookupNodeA = builder.lookupByKey(createNode0) - val exerciseNodeC = exercise("C", createNode0, builder) - val fetchNodeA = builder.fetch(createNode0, byKey = false) - - val exerciseNodeAId = builder.add(exerciseNodeA) - val exerciseNodeBId = builder.add(exerciseNodeB, exerciseNodeAId) - builder.add(fetchNodeB, exerciseNodeBId) - builder.add(lookupNodeA, exerciseNodeAId) - builder.add(exerciseNodeC, exerciseNodeAId) - builder.add(fetchNodeA, exerciseNodeAId) - val transaction = builder.build().transaction - - val expectedNodeInfos = Seq( - // node id, last descendant - 0 -> 5, - 1 -> 2, - 4 -> 4, - ).map { case (id, lastDescendant) => - NodeInfo(NodeId(id), transaction.nodes.get(NodeId(id)).value, NodeId(lastDescendant)) - } - - verifyRearrangements(transaction, expectedNodeInfos) - } - - it should "handle rollback nodes (they and their descendants should not appear in output)" in { - // Previous transaction - // └─ #0 Create - // Transaction - // └─ #0 Exercise (choice A) (last descendant: #1) - // ├─ #1 Rollback A (should not appear) - // │ └─ #2 Exercise (choice B) (should not appear) - // │ ├─ #3 Rollback B (should not appear) - // │ │ └─ #4 Exercise (choice C) (should not appear) - // │ └─ #5 Create B (should not appear) - // └─ #6 Exercise (choice D) (last descendant: #6) - val builder = TxBuilder() - val createNode0 = create("0", builder, withKey = true) - val exerciseNodeA = exercise("A", createNode0, builder) - val rollbackNodeA = builder.rollback() - val exerciseNodeB = exercise("B", createNode0, builder) - val rollbackNodeB = builder.rollback() - val exerciseNodeC = exercise("C", createNode0, builder) - val createNodeB = create("B", builder) - val exerciseNodeD = exercise("D", createNode0, builder) - - val exerciseNodeAId = builder.add(exerciseNodeA) - val rollbackNodeAId = builder.add(rollbackNodeA, exerciseNodeAId) - val exerciseNodeBId = builder.add(exerciseNodeB, rollbackNodeAId) - val rollbackNodeBId = builder.add(rollbackNodeB, exerciseNodeBId) - builder.add(exerciseNodeC, rollbackNodeBId) - builder.add(createNodeB, exerciseNodeBId) - builder.add(exerciseNodeD, exerciseNodeAId) - - val transaction = builder.build().transaction - - val expectedNodeInfos = Seq( - // node id, last descendant - 0 -> 6, - 6 -> 6, - ).map { case (id, lastDescendant) => - NodeInfo(NodeId(id), transaction.nodes.get(NodeId(id)).value, NodeId(lastDescendant)) - } - - verifyRearrangements(transaction, expectedNodeInfos) - } - - it should "handle complex combinations of nested exercise and create nodes" in { - // Previous transactions - // └─ #0 Create - // Transaction - // └─ #0 Exercise (choice A) (last descendant: #9) - // ├─ #1 Create A1 (last descendant: #1) - // ├─ #2 Create A2 (last descendant: #2) - // └─ #3 Exercise (choice B) (last descendant: #9) - // ├─ #4 Create B1 (last descendant: #4) - // ├─ #5 Exercise (choice C) (last descendant: #8) - // │ ├─ #6 Create C1 (last descendant: #6) - // │ └─ #7 Exercise (choice D) (last descendant: #8) - // │ └─ #8 Create D1 (last descendant: #8) - // └─ #9 Create B2 (last descendant: #9) - - val builder = TxBuilder() - val createNode0 = create("0", builder) - val exerciseNodeA = exercise("A", createNode0, builder) - val createNodeA1 = create("A1", builder) - val createNodeA2 = create("A2", builder) - val exerciseNodeB = exercise("B", createNode0, builder) - val createNodeB1 = create("B1", builder) - val exerciseNodeC = exercise("C", createNode0, builder) - val createNodeC1 = create("C1", builder) - val exerciseNodeD = exercise("D", createNode0, builder) - val createNodeD1 = create("D1", builder) - val createNodeB2 = create("B2", builder) - - val exerciseNodeAId = builder.add(exerciseNodeA) - - builder.add(createNodeA1, exerciseNodeAId) - builder.add(createNodeA2, exerciseNodeAId) - val exerciseNodeBId = builder.add(exerciseNodeB, exerciseNodeAId) - builder.add(createNodeB1, exerciseNodeBId) - val exerciseNodeCId = builder.add(exerciseNodeC, exerciseNodeBId) - builder.add(createNodeC1, exerciseNodeCId) - val exerciseNodeDId = builder.add(exerciseNodeD, exerciseNodeCId) - builder.add(createNodeD1, exerciseNodeDId) - builder.add(createNodeB2, exerciseNodeBId) - - val transaction = builder.build().transaction - - val expectedNodeInfos = Seq( - // node id, last descendant - 0 -> 9, - 1 -> 1, - 2 -> 2, - 3 -> 9, - 4 -> 4, - 5 -> 8, - 6 -> 6, - 7 -> 8, - 8 -> 8, - 9 -> 9, - ).map { case (id, lastDescendant) => - NodeInfo(NodeId(id), transaction.nodes.get(NodeId(id)).value, NodeId(lastDescendant)) - } - - verifyRearrangements(transaction, expectedNodeInfos) - } - - it should "handle complex combinations with multiple root nodes" in { - // Previous transactions - // └─ #0 Create - // Transaction - // ├─ #0 Create A1 (last descendant: #0) - // ├─ #1 Create A2 (last descendant: #1) - // ├─ #2 Exercise (choice B) (last descendant: #7) - // │ ├─ #3 Exercise (choice C) (last descendant: #6) - // │ │ ├─ #4 Exercise (choice D) (last descendant: #5) - // │ │ │ └─ #5 Create D1 (last descendant: #5) - // │ │ └─ #6 Create C1 (last descendant: #6) - // │ └─ #7 Create B1 (last descendant: #7) - // └─ #8 Create A3 (last descendant: #8) - val builder = TxBuilder() - val createNode0 = create("0", builder) - val createNodeA1 = create("A1", builder) - val createNodeA2 = create("A2", builder) - val exerciseNodeB = exercise("B", createNode0, builder) - val exerciseNodeC = exercise("C", createNode0, builder) - val exerciseNodeD = exercise("D", createNode0, builder) - val createNodeD1 = create("D1", builder) - val createNodeC1 = create("C1", builder) - val createNodeB1 = create("B1", builder) - val createNodeA3 = builder.create( - id = builder.newCid, - templateId = "M:TA3", - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - ) - - builder.add(createNodeA1) - builder.add(createNodeA2) - val exerciseNodeBId = builder.add(exerciseNodeB) - val exerciseNodeCId = builder.add(exerciseNodeC, exerciseNodeBId) - val exerciseNodeDId = builder.add(exerciseNodeD, exerciseNodeCId) - builder.add(createNodeD1, exerciseNodeDId) - builder.add(createNodeC1, exerciseNodeCId) - builder.add(createNodeB1, exerciseNodeBId) - builder.add(createNodeA3) - - val transaction = builder.build().transaction - - val expectedNodeInfos = Seq( - // node id, last descendant - 0 -> 0, - 1 -> 1, - 2 -> 7, - 3 -> 6, - 4 -> 5, - 5 -> 5, - 6 -> 6, - 7 -> 7, - 8 -> 8, - ).map { case (id, lastDescendant) => - NodeInfo(NodeId(id), transaction.nodes.get(NodeId(id)).value, NodeId(lastDescendant)) - } - - verifyRearrangements(transaction, expectedNodeInfos) - } - - def verifyNodeIdsAreInExecutionOrder(transaction: Transaction): Assertion = - transaction shouldBe arrangeNodeIdsInExecutionOrder(transaction) - - def verifyNodeIdsAreNotInExecutionOrder(transaction: Transaction): Assertion = - transaction should not be arrangeNodeIdsInExecutionOrder(transaction) - - def create( - id: String, - builder: NodeIdTransactionBuilder & TestNodeBuilder, - withKey: Boolean = false, - ): Node.Create = - builder.create( - id = builder.newCid, - templateId = "M:T" + id, - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - key = - if (withKey) - CreateKey - .SignatoryMaintainerKey(Value.ValueUnit, crypto.Hash.hashPrivateKey("dummy-key-hash")) - else CreateKey.NoKey, - ) - def exercise( - choice: String, - contract: Node.Create, - builder: NodeIdTransactionBuilder & TestNodeBuilder, - ): Node.Exercise = - builder.exercise( - contract = contract, - choice = choice, - consuming = true, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - - def verifyRearrangements(transaction: Transaction, expectedNodeInfos: Seq[NodeInfo]): Unit = { - verifyNodeIdsAreInExecutionOrder(transaction) - - // change the order of node ids to verify that the executionOrderTraversalForIngestion remaps the node ids to follow - // the execution order - val originalOrder: Seq[Int] = 0 until transaction.nodes.size - - val permutations = (1 to shuffles).map(_ => Random.shuffle(originalOrder)).toSet - - permutations.foreach { permutation => - val tx = - if (permutation == originalOrder) transaction - else { - val mapping = permutation.zipWithIndex.toMap.map { case (x, y) => NodeId(x) -> NodeId(y) } - val changedOrderTx = transaction.mapNodeId(n => mapping.get(n).value) - verifyNodeIdsAreNotInExecutionOrder(changedOrderTx) - changedOrderTx - } - - executionOrderTraversalForIngestion(tx).toSeq shouldBe expectedNodeInfos - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/HaCoordinatorSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/HaCoordinatorSpec.scala deleted file mode 100644 index 7b0927de7a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/HaCoordinatorSpec.scala +++ /dev/null @@ -1,961 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.ha - -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.logging.{SuppressingLogger, TracedLogger} -import com.digitalasset.canton.platform.store.backend.DBLockStorageBackend -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.stream.KillSwitch -import org.apache.pekko.testkit.TestProbe -import org.scalatest.concurrent.Eventually -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.sql.Connection -import java.util.Timer -import java.util.concurrent.atomic.AtomicInteger -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future, Promise, blocking} -import scala.util.{Random, Try} - -class HaCoordinatorSpec - extends AsyncFlatSpec - with Matchers - with PekkoBeforeAndAfterAll - with Eventually { - implicit val ec: ExecutionContext = - system.dispatcher // we need this to not use the default EC which is coming from AsyncTestSuite, and which is serial - private val loggerFactory: SuppressingLogger = SuppressingLogger(getClass) - private implicit val traceContext: TraceContext = TraceContext.empty - private val logger = TracedLogger(loggerFactory.getLogger(getClass)) - private val timer = new Timer(true) - - private val mainLockAcquireRetryTimeout = NonNegativeFiniteDuration.ofMillis(20) - private val mainLockAcquireMaxRetries = 1000000L - private val workerLockAcquireRetryTimeout = NonNegativeFiniteDuration.ofMillis(20) - private val mainLockCheckerPeriod = NonNegativeFiniteDuration.ofMillis(20) - private val timeoutTolerance = NonNegativeFiniteDuration.ofSeconds( - 600 - ) // unfortunately this needs to be a insanely big tolerance, not to render the test flaky. under normal circumstances this should pass with +5 millis - - private val mainLockId = 10 - private val main = TestLockId(mainLockId) - - private val workerLockId = 20 - private val worker = TestLockId(workerLockId) - - implicit class LockPicker(dbLock: Option[DBLockStorageBackend.Lock]) { - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) - def pick: DBLockStorageBackend.Lock = dbLock.get - } - - behavior of "databaseLockBasedHaCoordinator graceful shutdown" - - it should "successfully propagate successful end of execution" in { - val protectedSetup = setup() - import protectedSetup.* - - for { - _ <- connectionInitializerFuture - _ = { - logger.info("As HACoordinator is initialized") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is not completed") - completeExecutionInitialization() - logger.info("As protected execution initialization is finished") - executionCompletedPromise.success(()) - logger.info("As execution is completed") - } - _ <- protectedHandle.completed - } yield { - logger.info("Protected Handle is completed successfully") - 1 shouldBe 1 - } - } - - it should "successfully propagate failed end of execution" in { - val protectedSetup = setup() - import protectedSetup.* - - for { - _ <- connectionInitializerFuture - _ = { - logger.info("As HACoordinator is initialized") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is not completed") - completeExecutionInitialization() - logger.info("As protected execution initialization is finished") - executionCompletedPromise.failure(new Exception("failed execution")) - logger.info("As execution completes with failure") - } - ex <- protectedHandle.completed.failed - } yield { - logger.info("Protected Handle is completed with failure") - ex.getMessage shouldBe "failed execution" - } - } - - it should "propagate graceful shutdown to the protected execution" in { - val protectedSetup = setup() - import protectedSetup.* - - for { - connectionInitializer <- connectionInitializerFuture - _ = { - logger.info("As HACoordinator is initialized") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is not completed") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - completeExecutionInitialization() - logger.info("As protected execution initialization is finished") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - protectedHandle.killSwitch.shutdown() - logger.info("And as graceful shutdown started") - } - _ <- executionShutdownFuture - _ = { - logger.info("Shutdown is observed at execution") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - executionAbortedFuture.isCompleted shouldBe false - logger.info("Abort is not observed at execution") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle not completed") - Threading.sleep(200) - logger.info("As waiting 200 millis") - protectedHandle.completed.isCompleted shouldBe false - logger.info( - "Protected Handle is still not completed (hence it is waiting for execution to finish as the first step of the teardown process)" - ) - executionCompletedPromise.success(()) - logger.info("As execution is completed") - } - _ <- protectedHandle.completed - } yield { - logger.info("Protected Handle is completed successfully") - loggerFactory.assertLogs( - Try(connectionInitializer.initialize(new TestConnection)).isFailure shouldBe true, - _.errorMessage should include( - "Internal Error: This check should not be called from outside by the time the PollingChecker is closed." - ), - ) - logger.info("Connection initializer does not work anymore") - 1 shouldBe 1 - } - } - - it should "propagate graceful shutdown to the protected execution if shutdown initiated before execution initialization is finished" in { - val protectedSetup = setup() - import protectedSetup.* - - for { - connectionInitializer <- connectionInitializerFuture - _ = { - logger.info("As HACoordinator is initialized") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is not completed") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - protectedHandle.killSwitch.shutdown() - logger.info("As graceful shutdown started") - completeExecutionInitialization() - logger.info("And As protected execution initialization is finished") - connectionInitializer.initialize(new TestConnection) - logger.info( - "Connection initializer still works (release process first releases the execution, and only then the main connection and the poller)" - ) - } - _ <- executionShutdownFuture - _ = { - logger.info("Shutdown is observed at execution") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - executionAbortedFuture.isCompleted shouldBe false - logger.info("Abort is not observed at execution") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle not completed") - executionCompletedPromise.success(()) - logger.info("As execution is completed") - } - _ <- protectedHandle.completed - } yield { - logger.info("Protected Handle is completed successfully") - loggerFactory.assertLogs( - Try(connectionInitializer.initialize(new TestConnection)).isFailure shouldBe true, - _.errorMessage should include( - "Internal Error: This check should not be called from outside by the time the PollingChecker is closed." - ), - ) - logger.info("Connection initializer does not work anymore") - 1 shouldBe 1 - } - } - - it should "swallow failures if graceful shutdown is underway" in { - val protectedSetup = setup() - import protectedSetup.* - - for { - connectionInitializer <- connectionInitializerFuture - _ = { - logger.info("As HACoordinator is initialized") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is not completed") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - completeExecutionInitialization() - logger.info("As protected execution initialization is finished") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - protectedHandle.killSwitch.shutdown() - logger.info("And as graceful shutdown started") - } - _ <- executionShutdownFuture - _ = { - logger.info("Shutdown is observed at execution") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - executionAbortedFuture.isCompleted shouldBe false - logger.info("Abort is not observed at execution") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle not completed") - executionCompletedPromise.failure(new Exception("some exception")) - logger.info("As execution is completes with failure") - } - _ <- protectedHandle.completed - } yield { - logger.info("Protected Handle is completed successfully") - loggerFactory.assertLogs( - Try(connectionInitializer.initialize(new TestConnection)).isFailure shouldBe true, - _.errorMessage should include( - "Internal Error: This check should not be called from outside by the time the PollingChecker is closed." - ), - ) - logger.info("Connection initializer does not work anymore") - 1 shouldBe 1 - } - } - - behavior of "databaseLockBasedHaCoordinator initialization" - - it should "fail if getting connection fails" in { - val protectedSetup = - setup(connectionFactory = () => throw new Exception("as getting connection")) - import protectedSetup.* - - for { - result <- protectedHandle.completed.failed - } yield { - logger.info("Protected Handle is completed with failure") - result.getMessage shouldBe "as getting connection" - } - } - - it should "wait if main lock cannot be acquired" in { - val dbLock = new TestDBLockStorageBackend - val blockingConnection = new TestConnection - val blockingLock = - dbLock.tryAcquire(main, DBLockStorageBackend.LockMode.Exclusive)(blockingConnection).pick - logger.info("As acquiring the main lock from the outside") - val protectedSetup = setup(dbLock = dbLock) - import protectedSetup.* - logger.info("As acquiring the main lock from the outside") - Threading.sleep(200) - logger.info("And as waiting for 200 millis") - connectionInitializerFuture.isCompleted shouldBe false - protectedHandle.completed.isCompleted shouldBe false - logger.info("Initialization should be waiting") - dbLock.release(blockingLock)(blockingConnection) shouldBe true - logger.info("As releasing the blocking lock") - - for { - _ <- connectionInitializerFuture - _ = { - logger.info("Initialisation should completed successfully") - completeExecutionInitialization() // cleanup - executionCompletedPromise.success(()) // cleanup - } - _ <- protectedHandle.completed - } yield { - 1 shouldBe 1 - } - } - - it should "wait for main lock can be interrupted by graceful shutdown" in { - val dbLock = new TestDBLockStorageBackend - val blockingConnection = new TestConnection - dbLock.tryAcquire(main, DBLockStorageBackend.LockMode.Exclusive)(blockingConnection).pick - logger.info("As acquiring the main lock from the outside") - val protectedSetup = setup(dbLock = dbLock) - import protectedSetup.* - Threading.sleep(200) - logger.info("And as waiting for 200 millis") - connectionInitializerFuture.isCompleted shouldBe false - protectedHandle.completed.isCompleted shouldBe false - logger.info("Initialization should be waiting") - protectedHandle.killSwitch.shutdown() - logger.info("As graceful shutdown started") - - for { - _ <- protectedHandle.completed - } yield { - logger.info("Protected Handle is completed successfully") - connectionInitializerFuture.isCompleted shouldBe false - } - } - - it should "wait for main lock can be interrupted by exception thrown during tryAcquire" in { - val dbLock = new TestDBLockStorageBackend - val blockingConnection = new TestConnection - dbLock.tryAcquire(main, DBLockStorageBackend.LockMode.Exclusive)(blockingConnection).pick - logger.info("As acquiring the main lock from the outside") - val mainConnection = new TestConnection - val protectedSetup = setup( - dbLock = dbLock, - connectionFactory = () => mainConnection, - ) - import protectedSetup.* - Threading.sleep(200) - logger.info("And as waiting for 200 millis") - connectionInitializerFuture.isCompleted shouldBe false - protectedHandle.completed.isCompleted shouldBe false - logger.info("Initialization should be waiting") - - for { - failure <- loggerFactory.assertLogs( - { - mainConnection.close() - logger.info( - "As main connection is closed (triggers exception as used for acquiring lock)" - ) - protectedHandle.completed.failed - }, - _.warningMessage should include("Failure not retryable"), - ) - } yield { - logger.info("Protected Handle is completed with a failure") - failure.getMessage shouldBe "trying to acquire on a closed connection" - connectionInitializerFuture.isCompleted shouldBe false - } - } - - it should "wait if worker lock cannot be acquired due to exclusive blocking" in { - val dbLock = new TestDBLockStorageBackend - val blockingConnection = new TestConnection - val blockingLock = - dbLock.tryAcquire(worker, DBLockStorageBackend.LockMode.Exclusive)(blockingConnection).pick - logger.info("As acquiring the worker lock from the outside") - val protectedSetup = setup(dbLock = dbLock) - import protectedSetup.* - Threading.sleep(200) - logger.info("And as waiting for 200 millis") - connectionInitializerFuture.isCompleted shouldBe false - protectedHandle.completed.isCompleted shouldBe false - logger.info("Initialization should be waiting") - dbLock.release(blockingLock)(blockingConnection) shouldBe true - logger.info("As releasing the blocking lock") - - for { - _ <- connectionInitializerFuture - _ = { - logger.info("Initialisation should completed successfully") - completeExecutionInitialization() // cleanup - executionCompletedPromise.success(()) // cleanup - } - _ <- protectedHandle.completed - } yield { - 1 shouldBe 1 - } - } - - it should "wait if worker lock cannot be acquired due to shared blocking" in { - val dbLock = new TestDBLockStorageBackend - val blockingConnection = new TestConnection - val blockingLock = - dbLock.tryAcquire(worker, DBLockStorageBackend.LockMode.Shared)(blockingConnection).pick - logger.info("As acquiring the worker lock from the outside") - val protectedSetup = setup(dbLock = dbLock) - import protectedSetup.* - - Threading.sleep(200) - logger.info("And as waiting for 200 millis") - connectionInitializerFuture.isCompleted shouldBe false - protectedHandle.completed.isCompleted shouldBe false - logger.info("Initialization should be waiting") - dbLock.release(blockingLock)(blockingConnection) shouldBe true - logger.info("As releasing the blocking lock") - - for { - _ <- connectionInitializerFuture - _ = { - logger.info("Initialisation should completed successfully") - completeExecutionInitialization() // cleanup - executionCompletedPromise.success(()) // cleanup - } - _ <- protectedHandle.completed - } yield { - 1 shouldBe 1 - } - } - - it should "wait for worker lock can be interrupted by graceful shutdown" in { - val dbLock = new TestDBLockStorageBackend - val blockingConnection = new TestConnection - dbLock.tryAcquire(worker, DBLockStorageBackend.LockMode.Shared)(blockingConnection).pick - logger.info("As acquiring the worker lock from the outside") - val protectedSetup = setup(dbLock = dbLock) - import protectedSetup.* - - Threading.sleep(200) - logger.info("And as waiting for 200 millis") - connectionInitializerFuture.isCompleted shouldBe false - protectedHandle.completed.isCompleted shouldBe false - logger.info("Initialization should be waiting") - protectedHandle.killSwitch.shutdown() - logger.info("As graceful shutdown starts") - - for { - _ <- protectedHandle.completed - } yield { - logger.info("Protected Handle completes successfully") - connectionInitializerFuture.isCompleted shouldBe false - } - } - - it should "wait for worker lock can be interrupted by exception thrown during tryAcquire" in { - val dbLock = new TestDBLockStorageBackend - val blockingConnection = new TestConnection - dbLock.tryAcquire(worker, DBLockStorageBackend.LockMode.Shared)(blockingConnection).pick - logger.info("As acquiring the worker lock from the outside") - val mainConnection = new TestConnection - val protectedSetup = setup( - dbLock = dbLock, - connectionFactory = () => mainConnection, - ) - import protectedSetup.* - - Threading.sleep(200) - logger.info("And as waiting for 200 millis") - connectionInitializerFuture.isCompleted shouldBe false - protectedHandle.completed.isCompleted shouldBe false - logger.info("Initialization should be waiting") - - loggerFactory.assertLogs( - { - mainConnection.close() - logger.info("As main connection is closed (triggers exception as used for acquiring lock)") - for { - failure <- protectedHandle.completed.failed - } yield { - logger.info("Protected Handle completed with a failure") - failure.getMessage shouldBe "trying to acquire on a closed connection" - connectionInitializerFuture.isCompleted shouldBe false - } - }, - _.warningMessage should include("Failure not retryable."), - ) - } - - it should "fail if worker lock cannot be acquired in time due to shared blocking" in { - val dbLock = new TestDBLockStorageBackend - val blockingConnection = new TestConnection - dbLock.tryAcquire(worker, DBLockStorageBackend.LockMode.Shared)(blockingConnection).pick - logger.info("As acquiring the worker lock from the outside") - loggerFactory.assertLogs( - within = { - val protectedSetup = setup( - dbLock = dbLock, - workerLockAcquireMaxRetries = NonNegativeLong.tryCreate(2), - ) - import protectedSetup.* - Threading.sleep(200) - logger.info("And as waiting for 200 millis") - for { - failure <- protectedHandle.completed.failed - } yield { - logger.info("Initialisation should completed with failure") - failure.getMessage shouldBe "Cannot acquire lock TestLockId(20) in lock-mode Exclusive" - } - }, - _.warningMessage should include("Maximum amount of retries reached (0). Failing permanently."), - ) - } - - it should "fail if execution initialization fails" in { - val protectedSetup = setup() - import protectedSetup.* - - for { - _ <- connectionInitializerFuture - _ = { - logger.info("As execution initialization starts") - failDuringExecutionInitialization(new Exception("failed as initializing")) - logger.info("Initialization fails") - } - failure <- protectedHandle.completed.failed - } yield { - logger.info("Protected execution fails with failure") - failure.getMessage shouldBe "failed as initializing" - } - } - - behavior of "databaseLockBasedHaCoordinator main connection polling" - - it should "successfully prevent further worker connection-spawning, and trigger shutdown in execution, if main lock cannot be acquired anymore, triggered by connection-spawning" in { - val protectedSetup = setup() - import protectedSetup.* - - for { - connectionInitializer <- connectionInitializerFuture - _ = { - logger.info("As HACoordinator is initialized") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is not completed") - completeExecutionInitialization() - logger.info("As protected execution initialization is finished") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - Threading.sleep(200) - logger.info("As waiting 200 millis") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is still not completed") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer still works") - executionAbortedFuture.isCompleted shouldBe false - logger.info("Execution is not aborted yet") - dbLock.cutExclusiveLockHoldingConnection(mainLockId) - logger.info("As main connection is severed") - Try(connectionInitializer.initialize(new TestConnection)).isFailure shouldBe true - logger.info("Connection initializer not working anymore") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is still not completed") - } - abortException <- executionAbortedFuture - _ = { - logger.info("Execution is aborted") - abortException.getMessage shouldBe "check failed, killSwitch aborted" - executionCompletedPromise.failure( - new Exception("execution failed due to abort coming from outside") - ) - logger.info("As execution fails") - } - failure <- protectedHandle.completed.failed - } yield { - logger.info("Protected Handle is completed with failure") - failure.getMessage shouldBe "check failed, killSwitch aborted" - logger.info("And completion failure is populated by check-failure") - 1 shouldBe 1 - } - } - - it should "successfully prevent further worker connection-spawning, and trigger shutdown in execution, if main lock cannot be acquired anymore, triggered by timeout" in { - val protectedSetup = setup() - import protectedSetup.* - - for { - connectionInitializer <- connectionInitializerFuture - mainConnectionSeveredAtNanos = { - logger.info("As HACoordinator is initialized") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is not completed") - completeExecutionInitialization() - logger.info("As protected execution initialization is finished") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer works") - Threading.sleep(200) - logger.info("As waiting 200 millis") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is still not completed") - connectionInitializer.initialize(new TestConnection) - logger.info("Connection initializer still works") - protectedSetup.executionAbortedFuture.isCompleted shouldBe false - logger.info("Execution is not aborted yet") - dbLock.cutExclusiveLockHoldingConnection(mainLockId) - logger.info("As main connection is severed") - System.nanoTime() - } - abortException <- executionAbortedFuture - _ = { - logger.info("Execution is aborted") - abortException.getMessage shouldBe "check failed, killSwitch aborted" - (System.nanoTime() - mainConnectionSeveredAtNanos) should be < (( - mainLockAcquireRetryTimeout + - timeoutTolerance - ).duration.toNanos) - logger.info("Within polling time-bounds") - Try(connectionInitializer.initialize(new TestConnection)).isFailure shouldBe true - logger.info("Connection initializer not working anymore") - protectedHandle.completed.isCompleted shouldBe false - logger.info("Protected Handle is still not completed") - executionCompletedPromise.failure( - new Exception("execution failed due to abort coming from outside") - ) - logger.info("As execution fails") - } - failure <- protectedHandle.completed.failed - } yield { - logger.info("Protected Handle is completed with failure") - failure.getMessage shouldBe "check failed, killSwitch aborted" - logger.info("And completion failure is populated by check-failure") - 1 shouldBe 1 - } - } - - behavior of "databaseLockBasedHaCoordinator in multi-node setup" - - it should "successfully protect execution, and respect upper bound threshold as switching over in a 5 node setup, without worker locking" in { - val dbLock = new TestDBLockStorageBackend - - var nodes: Set[ProtectedSetup] = Set.empty - val nodeStartedExecutionProbe = TestProbe() - val nodeStoppedExecutionProbe = TestProbe() - val nodeHaltedProbe = TestProbe() - - def addNode(): Unit = blocking(synchronized { - val node = setup(dbLock = dbLock) - node.connectionInitializerFuture - .foreach { _ => - logger.info(s"execution started") - node.completeExecutionInitialization() - nodeStartedExecutionProbe.send(nodeStartedExecutionProbe.ref, "started") - } - node.executionShutdownFuture - .foreach { _ => - node.executionCompletedPromise.success(()) - nodeStoppedExecutionProbe.send(nodeStoppedExecutionProbe.ref, "stopped") - } - node.executionAbortedFuture - .foreach { t => - node.executionCompletedPromise.failure(t) - nodeStoppedExecutionProbe.send(nodeStoppedExecutionProbe.ref, "stopped") - } - node.protectedHandle.completed - .onComplete { _ => - removeNode(node) - nodeHaltedProbe.send(nodeHaltedProbe.ref, "halted") - } - nodes = nodes + node - logger.info("As a node added") - }) - - def removeNode(node: ProtectedSetup): Unit = blocking(synchronized { - nodes = nodes - node - }) - - def verifyCleanSlate(expectedNumberOfNodes: Int): Unit = blocking(synchronized { - if (expectedNumberOfNodes == 0) { - nodes.size shouldBe 0 - } else { - nodes.size shouldBe expectedNumberOfNodes - nodes.exists(_.protectedHandle.completed.isCompleted) shouldBe false - nodes.count(_.connectionInitializerFuture.isCompleted) shouldBe 1 - nodes.find(_.connectionInitializerFuture.isCompleted).foreach { activeNode => - activeNode.executionAbortedFuture.isCompleted shouldBe false - activeNode.executionShutdownFuture.isCompleted shouldBe false - } - } - nodeStartedExecutionProbe.expectNoMessage(FiniteDuration(0, "seconds")) - nodeStoppedExecutionProbe.expectNoMessage(FiniteDuration(0, "seconds")) - nodeHaltedProbe.expectNoMessage(FiniteDuration(0, "seconds")) - logger.info("Cluster is in expected shape") - }) - - def wait(): Unit = { - val waitMillis: Long = Random.nextInt(100).toLong - Threading.sleep(waitMillis) - logger.info(s"As waiting $waitMillis millis") - } - - addNode() - nodeStartedExecutionProbe.expectMsg("started") - logger.info("The first node started execution") - verifyCleanSlate(1) - addNode() - verifyCleanSlate(2) - addNode() - verifyCleanSlate(3) - addNode() - verifyCleanSlate(4) - addNode() - verifyCleanSlate(5) - logger.info("As adding 5 nodes") - wait() - verifyCleanSlate(5) - logger.info("Cluster stabilized") - - for (_ <- 1 to 30) { - wait() - verifyCleanSlate(5) - dbLock.cutExclusiveLockHoldingConnection(mainLockId) - logger.info(s"main lock force-released") - val mainConnCutNanos = System.nanoTime() - logger.info( - "As active node looses main connection (and main index lock is available for acquisition again)" - ) - nodeStartedExecutionProbe.expectMsg("started") - (System.nanoTime() - mainConnCutNanos) should be < (( - mainLockAcquireRetryTimeout + - timeoutTolerance - ).duration.toNanos) - logger.info("Some other node started execution within time bounds") - nodeStoppedExecutionProbe.expectMsg("stopped") - nodeHaltedProbe.expectMsg("halted") - logger.info("And originally active node stopped") - verifyCleanSlate(4) - addNode() - verifyCleanSlate(5) - } - - def tearDown(): Unit = { - dbLock.cutExclusiveLockHoldingConnection(mainLockId) - nodeStartedExecutionProbe.expectMsg("started") - nodeStoppedExecutionProbe.expectMsg("stopped") - nodeHaltedProbe.expectMsg("halted") - () - } - - tearDown() - verifyCleanSlate(4) - tearDown() - verifyCleanSlate(3) - tearDown() - verifyCleanSlate(2) - tearDown() - verifyCleanSlate(1) - dbLock.cutExclusiveLockHoldingConnection(mainLockId) - nodeStoppedExecutionProbe.expectMsg("stopped") - nodeHaltedProbe.expectMsg("halted") - verifyCleanSlate(0) - - Future.successful(1 shouldBe 1) - } - - it should "successfully protect execution, and respect upper bound threshold as switching over in a 5 node setup, with worker connection locking: loosing only indexer lock" in { - val dbLock = new TestDBLockStorageBackend - - val keepUsingWorkerAfterShutdownMillis = 100L - - var nodes: Set[ProtectedSetup] = Set.empty - val nodeStartedExecutionProbe = TestProbe() - val nodeStoppedExecutionProbe = TestProbe() - val nodeHaltedProbe = TestProbe() - val concurrentWorkers = new AtomicInteger(0) - - def addNode(): Unit = blocking(synchronized { - val node = setup(dbLock = dbLock) - val workerConnection = new TestConnection - node.connectionInitializerFuture - .foreach { connectionInitializer => - logger.info(s"execution started") - node.completeExecutionInitialization() - connectionInitializer.initialize(workerConnection) - concurrentWorkers.incrementAndGet() - nodeStartedExecutionProbe.send(nodeStartedExecutionProbe.ref, "started") - } - node.executionShutdownFuture - .foreach { _ => - Threading.sleep(keepUsingWorkerAfterShutdownMillis) - concurrentWorkers.decrementAndGet() - dbLock.release(DBLockStorageBackend.Lock(worker, DBLockStorageBackend.LockMode.Shared))( - workerConnection - ) - node.executionCompletedPromise.success(()) - nodeStoppedExecutionProbe.send(nodeStoppedExecutionProbe.ref, "stopped") - } - node.executionAbortedFuture - .foreach { t => - Threading.sleep(keepUsingWorkerAfterShutdownMillis) - concurrentWorkers.decrementAndGet() - dbLock.release(DBLockStorageBackend.Lock(worker, DBLockStorageBackend.LockMode.Shared))( - workerConnection - ) - node.executionCompletedPromise.failure(t) - nodeStoppedExecutionProbe.send(nodeStoppedExecutionProbe.ref, "stopped") - } - node.protectedHandle.completed - .onComplete { _ => - removeNode(node) - nodeHaltedProbe.send(nodeHaltedProbe.ref, "halted") - } - nodes = nodes + node - logger.info("As a node added") - }) - - def removeNode(node: ProtectedSetup): Unit = blocking(synchronized { - nodes = nodes - node - }) - - def verifyCleanSlate(expectedNumberOfNodes: Int): Unit = blocking(synchronized { - if (expectedNumberOfNodes == 0) { - nodes.size shouldBe 0 - } else { - concurrentWorkers.get() shouldBe 1 - nodes.size shouldBe expectedNumberOfNodes - nodes.exists(_.protectedHandle.completed.isCompleted) shouldBe false - nodes.count(_.connectionInitializerFuture.isCompleted) shouldBe 1 - nodes.find(_.connectionInitializerFuture.isCompleted).foreach { activeNode => - activeNode.executionAbortedFuture.isCompleted shouldBe false - activeNode.executionShutdownFuture.isCompleted shouldBe false - } - } - nodeStartedExecutionProbe.expectNoMessage(FiniteDuration(0, "seconds")) - nodeStoppedExecutionProbe.expectNoMessage(FiniteDuration(0, "seconds")) - nodeHaltedProbe.expectNoMessage(FiniteDuration(0, "seconds")) - logger.info("Cluster is in expected shape") - }) - - def wait(): Unit = { - val waitMillis: Long = Random.nextInt(100).toLong - Threading.sleep(waitMillis) - logger.info(s"As waiting $waitMillis millis") - } - - addNode() - nodeStartedExecutionProbe.expectMsg("started") - logger.info("The first node started execution") - verifyCleanSlate(1) - addNode() - verifyCleanSlate(2) - addNode() - verifyCleanSlate(3) - addNode() - verifyCleanSlate(4) - addNode() - verifyCleanSlate(5) - logger.info("As adding 5 nodes") - wait() - verifyCleanSlate(5) - logger.info("Cluster stabilized") - - for (_ <- 1 to 30) { - wait() - verifyCleanSlate(5) - dbLock.cutExclusiveLockHoldingConnection(mainLockId) - logger.info(s"main lock force-released") - val mainConnCutNanos = System.nanoTime() - logger.info( - "As active node looses main connection (and with time drops worker connection as well)" - ) - nodeStartedExecutionProbe.expectMsg("started") - (System.nanoTime() - mainConnCutNanos) should be < (( - mainLockCheckerPeriod + // first active node has to realize that it lost the lock - NonNegativeFiniteDuration.ofMillis( - keepUsingWorkerAfterShutdownMillis - ) + // then it is shutting down, but it will take this long to release the worker lock as well - workerLockAcquireRetryTimeout + // by the time of here the new active node already acquired the main lock, and it is polling for the worker lock, so maximum so much time we need to wait - timeoutTolerance - ).duration.toNanos) - logger.info("Some other node started execution within time bounds") - nodeStoppedExecutionProbe.expectMsg("stopped") - nodeHaltedProbe.expectMsg("halted") - logger.info("And originally active node stopped") - verifyCleanSlate(4) - addNode() - verifyCleanSlate(5) - } - - def tearDown(): Unit = { - dbLock.cutExclusiveLockHoldingConnection(mainLockId) - nodeStartedExecutionProbe.expectMsg("started") - nodeStoppedExecutionProbe.expectMsg("stopped") - nodeHaltedProbe.expectMsg("halted") - () - } - - tearDown() - verifyCleanSlate(4) - tearDown() - verifyCleanSlate(3) - tearDown() - verifyCleanSlate(2) - tearDown() - verifyCleanSlate(1) - dbLock.cutExclusiveLockHoldingConnection(mainLockId) - nodeStoppedExecutionProbe.expectMsg("stopped") - nodeHaltedProbe.expectMsg("halted") - verifyCleanSlate(0) - - Future.successful(1 shouldBe 1) - } - - private def setup( - connectionFactory: () => Connection = () => new TestConnection, - workerLockAcquireMaxRetries: NonNegativeLong = NonNegativeLong.tryCreate(100), - dbLock: TestDBLockStorageBackend = new TestDBLockStorageBackend, - ): ProtectedSetup = { - val connectionInitializerPromise = Promise[ConnectionInitializer]() - val executionHandlePromise = Promise[Unit]() - - val shutdownPromise = Promise[Unit]() - val abortPromise = Promise[Throwable]() - val completedPromise = Promise[Unit]() - - val protectedHandle = HaCoordinator - .databaseLockBasedHaCoordinator( - mainConnectionFactory = connectionFactory, - storageBackend = dbLock, - executionContext = system.dispatcher, - timer = timer, - haConfig = HaConfig( - mainLockAcquireRetryTimeout = mainLockAcquireRetryTimeout, - mainLockAcquireMaxRetries = NonNegativeLong.tryCreate(mainLockAcquireMaxRetries), - workerLockAcquireRetryTimeout = workerLockAcquireRetryTimeout, - workerLockAcquireMaxRetries = workerLockAcquireMaxRetries, - mainLockCheckerPeriod = mainLockCheckerPeriod, - indexerLockId = 10, - indexerWorkerLockId = 20, - ), - loggerFactory = loggerFactory, - ) - .protectedExecution { connectionInitializer => - connectionInitializerPromise.success(connectionInitializer) - executionHandlePromise.future.map(_ => - Handle( - killSwitch = new KillSwitch { - override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def abort(ex: Throwable): Unit = abortPromise.trySuccess(ex) - }, - completed = completedPromise.future, - ) - ) - } - - ProtectedSetup( - protectedHandle = protectedHandle, - connectionInitializerFuture = connectionInitializerPromise.future, - executionHandlePromise = executionHandlePromise, - executionShutdownFuture = shutdownPromise.future, - executionAbortedFuture = abortPromise.future, - executionCompletedPromise = completedPromise, - dbLock = dbLock, - ) - } - - case class ProtectedSetup( - protectedHandle: Handle, // the protected Handle to observe and interact with - connectionInitializerFuture: Future[ - ConnectionInitializer - ], // observe ConnectionInitializer, this completes as HA arrives to the stage when execution initialization starts - executionHandlePromise: Promise[Unit], // trigger end of execution initialization - executionShutdownFuture: Future[Unit], // observe shutdown in execution - executionAbortedFuture: Future[Throwable], // observe abort in execution - executionCompletedPromise: Promise[Unit], // trigger completion of execution - dbLock: TestDBLockStorageBackend, // the lock backend - ) { - - /** trigger end of execution initialization */ - def completeExecutionInitialization(): Unit = - executionHandlePromise.success(()) - - /** simulate a failure during execution initialization */ - def failDuringExecutionInitialization(cause: Throwable): Unit = - executionHandlePromise.failure(cause) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/TestDBLockStorageBackend.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/TestDBLockStorageBackend.scala deleted file mode 100644 index 50072088fd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/TestDBLockStorageBackend.scala +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.ha - -import com.digitalasset.canton.platform.store.backend.DBLockStorageBackend -import com.digitalasset.canton.util.Mutex - -import java.sql.{ - Blob, - CallableStatement, - Clob, - Connection, - DatabaseMetaData, - NClob, - PreparedStatement, - SQLWarning, - SQLXML, - Savepoint, - Statement, - Struct, -} -import java.util.Properties -import java.util.concurrent.Executor -import java.{sql, util} -import scala.concurrent.blocking - -class TestDBLockStorageBackend extends DBLockStorageBackend { - - private var locks: Map[DBLockStorageBackend.Lock, Set[Connection]] = Map.empty - - override def tryAcquire( - lockId: DBLockStorageBackend.LockId, - lockMode: DBLockStorageBackend.LockMode, - )(connection: Connection): Option[DBLockStorageBackend.Lock] = blocking(synchronized { - if (connection.isClosed) throw new Exception("trying to acquire on a closed connection") - if (!lockId.isInstanceOf[TestLockId]) throw new Exception("foreign lockId") - removeClosedConnectionRelatedLocks() - val lock = DBLockStorageBackend.Lock(lockId, lockMode) - def doLock(): Option[DBLockStorageBackend.Lock] = { - locks = locks + (lock -> (locks.getOrElse(lock, Set.empty) + connection)) - Some(lock) - } - lockMode match { - case DBLockStorageBackend.LockMode.Exclusive => - ( - locks.get(lock), - locks.get(lock.copy(lockMode = DBLockStorageBackend.LockMode.Shared)), - ) match { - case (None, None) => doLock() - case (Some(connections), None) if connections == Set(connection) => doLock() - case _ => None // if any shared lock held, we cannot lock exclusively - } - case DBLockStorageBackend.LockMode.Shared => - ( - locks.get(lock), - locks.get(lock.copy(lockMode = DBLockStorageBackend.LockMode.Exclusive)), - ) match { - case (_, None) => doLock() - case _ => None // if any exclusive lock held, we cannot lock shared - } - } - }) - - override def release(lock: DBLockStorageBackend.Lock)(connection: Connection): Boolean = - blocking(synchronized { - if (connection.isClosed) throw new Exception("trying to release on a closed connection") - if (!lock.lockId.isInstanceOf[TestLockId]) throw new Exception("foreign lockId") - removeClosedConnectionRelatedLocks() - locks.get(lock) match { - case None => false - case Some(connections) if connections contains connection => - if (connections.sizeIs == 1) locks = locks - lock - else locks = locks + (lock -> (connections - connection)) - true - case _ => false - } - }) - - private def removeClosedConnectionRelatedLocks(): Unit = - locks = locks.collect { - case (dblock, conns) if conns.exists(!_.isClosed) => (dblock, conns.filter(!_.isClosed)) - } - - override def lock(id: Int): DBLockStorageBackend.LockId = - TestLockId(id) - - def cutExclusiveLockHoldingConnection(lockId: Int): Unit = blocking(synchronized { - val mainExclusiveIndexerLock = - DBLockStorageBackend.Lock(TestLockId(lockId), DBLockStorageBackend.LockMode.Exclusive) - locks - .getOrElse(mainExclusiveIndexerLock, Set.empty) - .foreach(_.close()) - locks = locks - mainExclusiveIndexerLock - }) - - override def dbLockSupported: Boolean = true -} - -final case class TestLockId(id: Int) extends DBLockStorageBackend.LockId - -class TestConnection extends Connection { - private val lock = new Mutex() - - override def createStatement(): Statement = throw new UnsupportedOperationException - - override def prepareStatement(s: String): PreparedStatement = - throw new UnsupportedOperationException - - override def prepareCall(s: String): CallableStatement = throw new UnsupportedOperationException - - override def nativeSQL(s: String): String = throw new UnsupportedOperationException - - override def setAutoCommit(b: Boolean): Unit = throw new UnsupportedOperationException - - override def getAutoCommit: Boolean = throw new UnsupportedOperationException - - override def commit(): Unit = throw new UnsupportedOperationException - - override def rollback(): Unit = throw new UnsupportedOperationException - - private var _closed: Boolean = false - - override def close(): Unit = (lock.exclusive { - _closed = true - }) - - override def isClosed: Boolean = (lock.exclusive(_closed)) - - override def getMetaData: DatabaseMetaData = throw new UnsupportedOperationException - - override def setReadOnly(b: Boolean): Unit = throw new UnsupportedOperationException - - override def isReadOnly: Boolean = throw new UnsupportedOperationException - - override def setCatalog(s: String): Unit = throw new UnsupportedOperationException - - override def getCatalog: String = throw new UnsupportedOperationException - - override def setTransactionIsolation(i: Int): Unit = throw new UnsupportedOperationException - - override def getTransactionIsolation: Int = throw new UnsupportedOperationException - - override def getWarnings: SQLWarning = throw new UnsupportedOperationException - - override def clearWarnings(): Unit = throw new UnsupportedOperationException - - override def createStatement(i: Int, i1: Int): Statement = throw new UnsupportedOperationException - - override def prepareStatement(s: String, i: Int, i1: Int): PreparedStatement = - throw new UnsupportedOperationException - - override def prepareCall(s: String, i: Int, i1: Int): CallableStatement = - throw new UnsupportedOperationException - - override def getTypeMap: util.Map[String, Class[?]] = throw new UnsupportedOperationException - - override def setTypeMap(map: util.Map[String, Class[?]]): Unit = - throw new UnsupportedOperationException - - override def setHoldability(i: Int): Unit = throw new UnsupportedOperationException - - override def getHoldability: Int = throw new UnsupportedOperationException - - override def setSavepoint(): Savepoint = throw new UnsupportedOperationException - - override def setSavepoint(s: String): Savepoint = throw new UnsupportedOperationException - - override def rollback(savepoint: Savepoint): Unit = throw new UnsupportedOperationException - - override def releaseSavepoint(savepoint: Savepoint): Unit = - throw new UnsupportedOperationException - - override def createStatement(i: Int, i1: Int, i2: Int): Statement = - throw new UnsupportedOperationException - - override def prepareStatement(s: String, i: Int, i1: Int, i2: Int): PreparedStatement = - throw new UnsupportedOperationException - - override def prepareCall(s: String, i: Int, i1: Int, i2: Int): CallableStatement = - throw new UnsupportedOperationException - - override def prepareStatement(s: String, i: Int): PreparedStatement = - throw new UnsupportedOperationException - - override def prepareStatement(s: String, ints: Array[Int]): PreparedStatement = - throw new UnsupportedOperationException - - override def prepareStatement(s: String, strings: Array[String]): PreparedStatement = - throw new UnsupportedOperationException - - override def createClob(): Clob = throw new UnsupportedOperationException - - override def createBlob(): Blob = throw new UnsupportedOperationException - - override def createNClob(): NClob = throw new UnsupportedOperationException - - override def createSQLXML(): SQLXML = throw new UnsupportedOperationException - - override def isValid(i: Int): Boolean = throw new UnsupportedOperationException - - override def setClientInfo(s: String, s1: String): Unit = throw new UnsupportedOperationException - - override def setClientInfo(properties: Properties): Unit = throw new UnsupportedOperationException - - override def getClientInfo(s: String): String = throw new UnsupportedOperationException - - override def getClientInfo: Properties = throw new UnsupportedOperationException - - override def createArrayOf(s: String, objects: Array[AnyRef]): sql.Array = - throw new UnsupportedOperationException - - override def createStruct(s: String, objects: Array[AnyRef]): Struct = - throw new UnsupportedOperationException - - override def setSchema(s: String): Unit = throw new UnsupportedOperationException - - override def getSchema: String = throw new UnsupportedOperationException - - override def abort(executor: Executor): Unit = throw new UnsupportedOperationException - - override def setNetworkTimeout(executor: Executor, i: Int): Unit = - throw new UnsupportedOperationException - - override def getNetworkTimeout: Int = throw new UnsupportedOperationException - - override def unwrap[T](aClass: Class[T]): T = throw new UnsupportedOperationException - - override def isWrapperFor(aClass: Class[?]): Boolean = throw new UnsupportedOperationException -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/TestDBLockStorageBackendSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/TestDBLockStorageBackendSpec.scala deleted file mode 100644 index e108a3ae92..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/ha/TestDBLockStorageBackendSpec.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.ha - -import com.digitalasset.canton.platform.store.backend.{ - DBLockStorageBackend, - StorageBackendTestsDBLock, -} -import org.scalatest.BeforeAndAfter -import org.scalatest.flatspec.AnyFlatSpec - -import java.sql.Connection - -class TestDBLockStorageBackendSpec - extends AnyFlatSpec - with StorageBackendTestsDBLock - with BeforeAndAfter { - - private var _dbLock: TestDBLockStorageBackend = _ - - before { - _dbLock = new TestDBLockStorageBackend - } - - override def dbLock: DBLockStorageBackend = _dbLock - - override def lockIdSeed: Int = 1000 // Seeding not needed for this test - - override def getConnection: Connection = new TestConnection -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/AchsMaintenancePipeSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/AchsMaintenancePipeSpec.scala deleted file mode 100644 index 2da86c16e8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/AchsMaintenancePipeSpec.scala +++ /dev/null @@ -1,693 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig -import com.digitalasset.canton.platform.indexer.parallel.AchsMaintenancePipe.* -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsAddActivationsParams, - AchsLastPointers, - AchsRemoveDeactivatedParams, - AchsState, -} -import com.digitalasset.canton.platform.store.cache.AchsStateCache -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import org.scalatest.flatspec.AnyFlatSpec - -import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.{ExecutionContext, Future} - -class AchsMaintenancePipeSpec extends AnyFlatSpec with BaseTest with HasExecutionContext { - - private lazy val executionContext = implicitly[ExecutionContext] - - private def workRange( - popStart: Long, - popEnd: Long, - remStart: Long, - remEnd: Long, - ): AchsWorkRange = - AchsWorkRange( - activationsPopulation = EventSeqIdRange(startExclusive = popStart, endInclusive = popEnd), - deactivatedRemoval = EventSeqIdRange(startExclusive = remStart, endInclusive = remEnd), - ) - - behavior of "AchsWorkDistance.+" - - it should "sum two work distances" in { - val a = AchsWorkDistance(populate = 10L, remove = 20L) - val b = AchsWorkDistance(populate = 5L, remove = 22L) - (a + b) shouldBe AchsWorkDistance(populate = 15L, remove = 42L) - } - - behavior of "AchsWorkDistance.-" - - it should "subtract two work distances" in { - val a = AchsWorkDistance(populate = 15L, remove = 23L) - val b = AchsWorkDistance(populate = 5L, remove = 3L) - (a - b) shouldBe AchsWorkDistance(populate = 10L, remove = 20L) - } - - behavior of "AchsWorkDistance.cap" - - it should "return threshold for dimensions at or above it, and 0 for dimensions below (populate)" in { - val work = AchsWorkDistance(populate = 25L, remove = 8L) - val chunk = work.cap(10L) - chunk shouldBe AchsWorkDistance(populate = 10L, remove = 0L) - (work - chunk) shouldBe AchsWorkDistance(populate = 15L, remove = 8L) - } - - it should "return threshold for dimensions at or above it, and 0 for dimensions below (remove)" in { - val work = AchsWorkDistance(populate = 5L, remove = 10L) - val chunk = work.cap(10L) - chunk shouldBe AchsWorkDistance(populate = 0L, remove = 10L) - (work - chunk) shouldBe AchsWorkDistance(populate = 5L, remove = 0L) - } - - it should "return 0 for both dimensions if both are under the threshold" in { - val work = AchsWorkDistance(populate = 5L, remove = 3L) - val chunk = work.cap(10L) - chunk shouldBe AchsWorkDistance(populate = 0L, remove = 0L) - (work - chunk) shouldBe AchsWorkDistance(populate = 5L, remove = 3L) - } - - it should "return threshold for populate and 0 for a negative remove" in { - val work = AchsWorkDistance(populate = 35L, remove = -5000L) - val chunk = work.cap(10L) - chunk shouldBe AchsWorkDistance(populate = 10L, remove = 0L) - (work - chunk) shouldBe AchsWorkDistance(populate = 25L, remove = -5000L) - } - - behavior of "drain" - - it should "produce no work ranges when below threshold" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 80L, lastPopulated = 60L), - ) - val work = AchsWorkDistance(populate = 5L, remove = 5L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = false, - ) - ranges shouldBe empty - newState shouldBe state - remaining shouldBe work - } - - it should "produce one work range when exactly at threshold for both" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 80L, lastPopulated = 60L), - ) - val work = AchsWorkDistance(populate = 10L, remove = 10L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = false, - ) - ranges shouldBe Vector(workRange(popStart = 60L, popEnd = 70L, remStart = 80L, remEnd = 90L)) - newState shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 90L, lastPopulated = 70L), - ) - remaining shouldBe AchsWorkDistance(populate = 0L, remove = 0L) - } - - it should "produce one work range when exactly at threshold for one" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 80L, lastPopulated = 60L), - ) - val work = AchsWorkDistance(populate = 9L, remove = 10L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = false, - ) - ranges shouldBe Vector(workRange(popStart = 60L, popEnd = 60L, remStart = 80L, remEnd = 90L)) - newState shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 90L, lastPopulated = 60L), - ) - remaining shouldBe AchsWorkDistance(populate = 9L, remove = 0L) - } - - it should "produce multiple work ranges when work is greater than a multiple of threshold" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - val work = AchsWorkDistance(populate = 25L, remove = 25L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = false, - ) - ranges shouldBe Vector( - workRange(popStart = 0L, popEnd = 10L, remStart = 0L, remEnd = 10L), - workRange(popStart = 10L, popEnd = 20L, remStart = 10L, remEnd = 20L), - ) - newState shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 20L, lastPopulated = 20L), - ) - remaining shouldBe AchsWorkDistance(populate = 5L, remove = 5L) - } - - it should "produce multiple work ranges with non zero last pointers" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 42L, lastPopulated = 5L), - ) - val work = AchsWorkDistance(populate = 35L, remove = 13L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = false, - ) - ranges shouldBe Vector( - workRange(popStart = 5L, popEnd = 15L, remStart = 42L, remEnd = 52L), - workRange(popStart = 15L, popEnd = 25L, remStart = 52L, remEnd = 52L), - workRange(popStart = 25L, popEnd = 35L, remStart = 52L, remEnd = 52L), - ) - newState shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 52L, lastPopulated = 35L), - ) - remaining shouldBe AchsWorkDistance(populate = 5L, remove = 3L) - } - - it should "produce work ranges with populationEnd = 0 when removal-only starting fresh" in { - val state = AchsState( - validAt = 0L, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - val work = AchsWorkDistance(populate = 0L, remove = 35L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = false, - ) - ranges shouldBe Vector( - workRange(popStart = 0L, popEnd = 0L, remStart = 0L, remEnd = 10L), - workRange(popStart = 0L, popEnd = 0L, remStart = 10L, remEnd = 20L), - workRange(popStart = 0L, popEnd = 0L, remStart = 20L, remEnd = 30L), - ) - // all populationEnd values should be zero - ranges.foreach { wr => - wr.activationsPopulation.endInclusive shouldBe 0L - } - newState shouldBe AchsState( - validAt = 0L, - lastPointers = AchsLastPointers(lastRemoved = 30L, lastPopulated = 0L), - ) - remaining shouldBe AchsWorkDistance(populate = 0L, remove = 5L) - } - - it should "with fullDrain, flush the positive sub-threshold remainder as a final chunk" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - val work = AchsWorkDistance(populate = 25L, remove = 25L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = true, - ) - ranges shouldBe Vector( - workRange(popStart = 0L, popEnd = 10L, remStart = 0L, remEnd = 10L), - workRange(popStart = 10L, popEnd = 20L, remStart = 10L, remEnd = 20L), - workRange(popStart = 20L, popEnd = 25L, remStart = 20L, remEnd = 25L), - ) - newState shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 25L, lastPopulated = 25L), - ) - remaining shouldBe AchsWorkDistance(populate = 0L, remove = 0L) - } - - it should "with fullDrain, clamp negative dimensions to zero in the flushed remainder" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 42L, lastPopulated = 5L), - ) - val work = AchsWorkDistance(populate = 35L, remove = -13L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = true, - ) - ranges shouldBe Vector( - workRange(popStart = 5L, popEnd = 15L, remStart = 42L, remEnd = 42L), - workRange(popStart = 15L, popEnd = 25L, remStart = 42L, remEnd = 42L), - workRange(popStart = 25L, popEnd = 35L, remStart = 42L, remEnd = 42L), - workRange(popStart = 35L, popEnd = 40L, remStart = 42L, remEnd = 42L), - ) - newState shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 42L, lastPopulated = 40L), - ) - remaining shouldBe AchsWorkDistance(populate = 0L, remove = -13L) - } - - it should "with fullDrain, not emit anything when both dimensions are zero" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 80L, lastPopulated = 60L), - ) - val work = AchsWorkDistance(populate = 0L, remove = 0L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = true, - ) - ranges shouldBe empty - newState shouldBe state - remaining shouldBe work - } - - it should "with fullDrain, not emit anything when both dimensions are negative" in { - val state = AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 80L, lastPopulated = 60L), - ) - val work = AchsWorkDistance(populate = -5L, remove = -3L) - val (newState, remaining, ranges) = AchsMaintenancePipe.drain( - aggregationThreshold = 10L, - state = state, - remaining = work, - acc = Vector.empty, - fullDrain = true, - ) - ranges shouldBe empty - newState shouldBe state - remaining shouldBe work - } - - behavior of "bumpAchsValidAt" - - private def storeAchsValidAt(achsStateRef: AtomicReference[AchsState])( - validAt: Long - ): Future[Unit] = - Future.successful { - achsStateRef.updateAndGet(curr => curr.copy(validAt = validAt)) - () - } - - it should "bump the validAt" in { - val state0 = AchsState( - validAt = 30L, - lastPointers = AchsLastPointers(lastRemoved = 30L, lastPopulated = 10L), - ) - - val achsStateRef = new AtomicReference[AchsState](state0) - val achsStateCache = new AchsStateCache(loggerFactory) - achsStateCache.set(state0) - - achsStateRef.get() shouldBe state0 - achsStateCache.get() shouldBe state0 - - val inputWorkRange = workRange( - popStart = 0L, - popEnd = 0L, - remStart = 10L, - remEnd = 60L, - ) - - AchsMaintenancePipe - .bumpAchsValidAt( - storeAchsValidAt = storeAchsValidAt(achsStateRef), - achsStateCache = achsStateCache, - executionContext = executionContext, - logger = loggerFactory.getTracedLogger(this.getClass), - metrics = LedgerApiServerMetrics.ForTesting, - )(inputWorkRange) - .futureValue shouldBe inputWorkRange - - val state1 = AchsState( - validAt = 60L, - lastPointers = AchsLastPointers(lastRemoved = 30L, lastPopulated = 10L), - ) - achsStateRef.get() shouldBe state1 - achsStateCache.get() shouldBe state1 - } - - it should "not bump the validAt if lagging behind" in { - val currState = AchsState( - validAt = 60L, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - - val achsStateRef = new AtomicReference[AchsState](currState) - val achsStateCache = new AchsStateCache(loggerFactory) - achsStateCache.set(currState) - - val inputWorkRange = workRange( - popStart = 0L, - popEnd = 0L, - remStart = 10L, - remEnd = 50L, - ) - - AchsMaintenancePipe - .bumpAchsValidAt( - storeAchsValidAt = storeAchsValidAt(achsStateRef), - achsStateCache = achsStateCache, - executionContext = executionContext, - logger = loggerFactory.getTracedLogger(this.getClass), - metrics = LedgerApiServerMetrics.ForTesting, - )(inputWorkRange) - .futureValue shouldBe inputWorkRange - - achsStateRef.get() shouldBe currState - achsStateCache.get() shouldBe currState - } - - behavior of "storeAchsLastPointersF" - - private val zeroLastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L) - - it should "update lastRemoved and lastPopulated when both are positive" in { - val dbRef = new AtomicReference[AchsLastPointers](zeroLastPointers) - val achsStateCache = new AchsStateCache(loggerFactory) - val validAt = 150L - achsStateCache.set( - AchsState( - validAt = validAt, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - ) - - val inputWorkRange = workRange( - popStart = 65L, - popEnd = 70L, - remStart = 75L, - remEnd = 80L, - ) - - AchsMaintenancePipe - .storeAchsLastPointersF( - persistAchsLastPointersF = lastPointers => Future.successful(dbRef.set(lastPointers)), - achsStateCache = achsStateCache, - executionContext = executionContext, - logger = loggerFactory.getTracedLogger(this.getClass), - metrics = LedgerApiServerMetrics.ForTesting, - )(inputWorkRange) - .futureValue - - dbRef.get() shouldBe AchsLastPointers(lastRemoved = 80L, lastPopulated = 70L) - achsStateCache.get() shouldBe AchsState( - validAt = validAt, - lastPointers = AchsLastPointers( - lastRemoved = 80L, - lastPopulated = 70L, - ), - ) - } - - it should "skip when lastRemoved computes to zero" in { - val dbRef = new AtomicReference[AchsLastPointers](zeroLastPointers) - val achsStateCache = new AchsStateCache(loggerFactory) - val validAt = 100L - achsStateCache.set( - AchsState( - validAt = validAt, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - ) - - val inputWorkRange = workRange( - popStart = 0L, - popEnd = 0L, - remStart = 0L, - remEnd = 0L, - ) - - AchsMaintenancePipe - .storeAchsLastPointersF( - persistAchsLastPointersF = lastPointers => Future.successful(dbRef.set(lastPointers)), - achsStateCache = achsStateCache, - executionContext = executionContext, - logger = loggerFactory.getTracedLogger(this.getClass), - metrics = LedgerApiServerMetrics.ForTesting, - )(inputWorkRange) - .futureValue - - dbRef.get() shouldBe zeroLastPointers - achsStateCache.get() shouldBe AchsState( - validAt = validAt, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - } - - it should "handle zero lastPopulated while lastRemoved is positive" in { - val dbRef = new AtomicReference[AchsLastPointers](zeroLastPointers) - val achsStateCache = new AchsStateCache(loggerFactory) - val validAt = 100L - achsStateCache.set( - AchsState( - validAt = validAt, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - ) - - val inputWorkRange = workRange( - popStart = 0L, - popEnd = 0L, - remStart = 0L, - remEnd = 5L, - ) - - AchsMaintenancePipe - .storeAchsLastPointersF( - persistAchsLastPointersF = lastPointers => Future.successful(dbRef.set(lastPointers)), - achsStateCache = achsStateCache, - executionContext = executionContext, - logger = loggerFactory.getTracedLogger(this.getClass), - metrics = LedgerApiServerMetrics.ForTesting, - )(inputWorkRange) - .futureValue - - dbRef.get() shouldBe AchsLastPointers(lastRemoved = 5L, lastPopulated = 0L) - achsStateCache.get() shouldBe - AchsState( - validAt = validAt, - lastPointers = AchsLastPointers(lastRemoved = 5L, lastPopulated = 0L), - ) - } - - behavior of "populateAchsActivations" - - private val zeroAddParams = - AchsAddActivationsParams(startExclusive = 0L, endInclusive = 0L, activeAt = 0L) - - it should "populate activations in the correct range" in { - val dbRef = new AtomicReference[AchsAddActivationsParams](zeroAddParams) - - val inputWorkRange = workRange( - popStart = 20L, - popEnd = 70L, - remStart = 30L, - remEnd = 80L, - ) - - AchsMaintenancePipe - .populateAchsActivations( - persistActivationsF = params => _ => Future.successful(dbRef.set(params)), - logger = loggerFactory.getTracedLogger(this.getClass), - executionContext = executionContext, - )(inputWorkRange) - .futureValue shouldBe inputWorkRange - - dbRef.get() shouldBe AchsAddActivationsParams( - startExclusive = 20L, - endInclusive = 70L, - activeAt = 80L, - ) - } - - it should "skip population when no sequenced events are in the batch" in { - val dbRef = new AtomicReference[AchsAddActivationsParams](zeroAddParams) - - val inputWorkRange = workRange( - popStart = 70L, - popEnd = 70L, - remStart = 80L, - remEnd = 90L, - ) - - AchsMaintenancePipe - .populateAchsActivations( - persistActivationsF = params => _ => Future.successful(dbRef.set(params)), - logger = loggerFactory.getTracedLogger(this.getClass), - executionContext = executionContext, - )(inputWorkRange) - .futureValue shouldBe inputWorkRange - - dbRef.get() shouldBe zeroAddParams - } - - behavior of "removeDeactivatedFromAchsStage" - - private val zeroRemoveParams = AchsRemoveDeactivatedParams(startExclusive = 0L, endInclusive = 0L) - - it should "remove deactivated entries in the correct range" in { - val dbRef = new AtomicReference[AchsRemoveDeactivatedParams](zeroRemoveParams) - - val inputWorkRange = workRange( - popStart = 20L, - popEnd = 70L, - remStart = 30L, - remEnd = 80L, - ) - - AchsMaintenancePipe - .removeDeactivatedFromAchs( - removeDeactivatedF = - params => (_: LoggingContextWithTrace) => Future.successful(dbRef.set(params)), - executionContext = executionContext, - logger = loggerFactory.getTracedLogger(this.getClass), - )(inputWorkRange) - .futureValue shouldBe inputWorkRange - - dbRef.get() shouldBe AchsRemoveDeactivatedParams( - startExclusive = 30L, - endInclusive = 80L, - ) - } - - it should "skip removal when no sequenced events are in the batch" in { - val dbRef = new AtomicReference[AchsRemoveDeactivatedParams](zeroRemoveParams) - - val inputWorkRange = workRange( - popStart = 50L, - popEnd = 70L, - remStart = 80L, - remEnd = 80L, - ) - - AchsMaintenancePipe - .removeDeactivatedFromAchs( - removeDeactivatedF = - params => (_: LoggingContextWithTrace) => Future.successful(dbRef.set(params)), - executionContext = executionContext, - logger = loggerFactory.getTracedLogger(this.getClass), - )(inputWorkRange) - .futureValue shouldBe inputWorkRange - - dbRef.get() shouldBe zeroRemoveParams - } - - it should "skip removal when nothing has been populated into ACHS (populationEnd <= 0)" in { - val dbRef = new AtomicReference[AchsRemoveDeactivatedParams](zeroRemoveParams) - - val inputWorkRange = workRange( - popStart = 0L, - popEnd = 0L, - remStart = 0L, - remEnd = 10L, - ) - - AchsMaintenancePipe - .removeDeactivatedFromAchs( - removeDeactivatedF = - params => (_: LoggingContextWithTrace) => Future.successful(dbRef.set(params)), - executionContext = executionContext, - logger = loggerFactory.getTracedLogger(this.getClass), - )(inputWorkRange) - .futureValue shouldBe inputWorkRange - - dbRef.get() shouldBe zeroRemoveParams - } - - behavior of "initialWork" - - it should "compute zero work when ACHS state and ledger end match config distances" in { - val achsState = - AchsState(validAt = 95, lastPointers = AchsLastPointers(lastRemoved = 90, lastPopulated = 80)) - val lastEventSeqId = 100L - val config = - AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(10L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(10L), - ) - // populate = lastEventSeqId - validAtDistanceTarget - lastPopulatedDistanceTarget = (100 - 10 - 10) - 80 = 80 - 80 = 0 - // remove = lastEventSeqId - validAtDistanceTarget = (100 - 10) - 90 = 90 - 90 = 0 - initialWork( - achsState = achsState, - lastEventSeqId = lastEventSeqId, - achsConfig = config, - ) shouldBe AchsWorkDistance( - populate = 0L, - remove = 0L, - ) - } - - it should "compute positive work when ledger end is ahead of ACHS state" in { - val achsState = - AchsState(validAt = 55, lastPointers = AchsLastPointers(lastRemoved = 50, lastPopulated = 40)) - val lastEventSeqId = 100L - val config = - AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(10L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(10L), - ) - // populate = (100 - 10 - 10) - 40 = 80 - 40 = 40 - // remove = (100 - 10) - 50 = 90 - 50 = 40 - initialWork( - achsState = achsState, - lastEventSeqId = lastEventSeqId, - achsConfig = config, - ) shouldBe AchsWorkDistance( - populate = 40L, - remove = 40L, - ) - } - - it should "compute negative work when ACHS state is ahead of distance targets" in { - val achsState = - AchsState(validAt = 55, lastPointers = AchsLastPointers(lastRemoved = 50, lastPopulated = 40)) - val lastEventSeqId = 100L - val config = - AchsConfig( - validAtDistanceTarget = NonNegativeLong.tryCreate(70L), - lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(20L), - ) - // populate = (100 - 70 - 20) - 40 = 10 - 40 = -30 - // remove = (100 - 70) - 50 = 30 - 50 = -20 - initialWork( - achsState = achsState, - lastEventSeqId = lastEventSeqId, - achsConfig = config, - ) shouldBe AchsWorkDistance( - populate = -30, - remove = -20, - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/BatchingParallelIngestionPipeSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/BatchingParallelIngestionPipeSpec.scala deleted file mode 100644 index cb6c948257..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/BatchingParallelIngestionPipeSpec.scala +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.annotations.UnstableTest -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.util.BatchN -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source -import org.scalatest.OptionValues -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.concurrent.atomic.AtomicInteger -import scala.collection.mutable.ArrayBuffer -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.{ExecutionContext, Future, Promise, blocking} -import scala.util.chaining.* - -@UnstableTest // TODO(#19208) -class BatchingParallelIngestionPipeSpec - extends AsyncFlatSpec - with Matchers - with OptionValues - with PekkoBeforeAndAfterAll { - - // AsyncFlatSpec is with serial execution context - private implicit val ec: ExecutionContext = system.dispatcher - - private val input = Iterator.continually(util.Random.nextInt()).take(1000).toList - // 1000 items must be separated into 10 iterations of 2 parallel batches, so each batch should hold 50 items - // to hold 50 items with the given weight fn we need a capacity of ~280, rounding it to 300 - private val MaxBatchSize = 300 - private val MaxTailerBatchSize = 4 - - def weightFn(i: Int): Long = i match { - case n if n % 10 == 0 => 20 - case n if n % 3 == 0 => 10 - case _ => 1 - } - - it should "end the stream successfully in a happy path case" in { - runPipe().map { case (ingested, ingestedTail, err) => - err shouldBe empty - ingested.sortBy(_._1) shouldBe input.map(_.toString).zipWithIndex.map { case (s, i) => - (i + 1, s) - } - ingestedTail.last shouldBe 1000 - } - } - - it should "terminate the stream upon error in input mapper" in { - runPipe(inputMapperHook = () => throw new Exception("inputmapper failed")).map { - case (_, _, err) => - err.value.getMessage shouldBe "inputmapper failed" - } - } - - it should "terminate the stream upon error in seqMapper" in { - runPipe(seqMapperHook = () => throw new Exception("seqMapper failed")).map { case (_, _, err) => - err.value.getMessage shouldBe "seqMapper failed" - } - } - - it should "terminate the stream upon error in batcher" in { - runPipe(batcherHook = () => throw new Exception("batcher failed")).map { case (_, _, err) => - err.value.getMessage shouldBe "batcher failed" - } - } - - it should "terminate the stream upon error in ingester" in { - runPipe(ingesterHook = _ => throw new Exception("ingester failed")).map { case (_, _, err) => - err.value.getMessage shouldBe "ingester failed" - } - } - - it should "terminate the stream upon error in ingestTail" in { - runPipe(ingestTailHook = _ => throw new Exception("ingestTail failed")).map { - case (_, _, err) => - err.value.getMessage shouldBe "ingestTail failed" - } - } - - it should "hold the stream if a single ingestion takes too long and timeouts" in { - val ingestedTailAcc = new AtomicInteger(0) - runPipe( - inputMapperHook = () => Threading.sleep(1L), - ingesterHook = batch => { - // due to timing issues it can be that other than full batches are formed, so we check if the batch contains 201 - val max = batch.map(_._1).max - if (max < 201) { - ingestedTailAcc.accumulateAndGet(max, _ max _) - } - if (batch.map(_._1).contains(201)) Threading.sleep(1000) - }, - timeout = FiniteDuration(100, "milliseconds"), - ).map { case (ingested, ingestedTail, err) => - err.value.getMessage shouldBe "timed out" - // worst case: 200 elements will be ingested before 201 - // + 300 elements in the batch that does not contain it and is back-pressured by the batch containing 201 - ingested.size should be <= 500 - ingestedTail.last should be < 201 - ingestedTail.last shouldBe ingestedTailAcc.get() - } - } - - it should "form max-sized batches when back-pressured by downstream" in { - val batchWeights = ArrayBuffer.empty[Long] - runPipe( - // Back-pressure to ensure formation of max batch sizes (of size 5) - inputMapperHook = () => Threading.sleep(1), - ingesterHook = batch => { - blocking(batchWeights.synchronized { - batchWeights.addOne(batch.map(p => weightFn(p._2.toInt)).sum) - }) - () - }, - inputSource = Source(Iterator.continually(util.Random.nextInt()).take(1000).toList), - ).map { case (_, _, err) => - // The first and last batches can be much smaller than `MaxBatchWeight` - // so we drop 2 and assert the average batch weight instead of the weight of individual batches - val measurementBatchWeights = batchWeights.drop(2) - measurementBatchWeights.sum.toDouble / measurementBatchWeights.size should be > (MaxBatchSize.toDouble * 0.7) - err shouldBe empty - } - } - - it should "form small batch sizes under no load" in { - runPipe( - ingesterHook = batch => { - batch.size should be <= 2 - () - }, - inputSource = Source(input).take(10).map(_.tap(_ => Threading.sleep(10L))).async, - ).map { case (ingested, _, err) => - err shouldBe empty - ingested.size shouldBe 10 - } - } - - it should "form big batch sizes of batches before ingestTail under load" in { - val batchSizes = ArrayBuffer.empty[Int] - - runPipe( - ingestTailHook = { batchOfBatches => - // Slow ingest tail - Threading.sleep(20L) - batchSizes.addOne(batchOfBatches.size) - }, - inputSource = Source(input).take(100).async, - ).map { case (_, _, err) => - // The first and last batches can be smaller than `MaxTailerBatchSize` - // so we drop one and assert the average batch size instead of the sizes of individual batches - val measurementBatchSizes = batchSizes.drop(1) - measurementBatchSizes.sum.toDouble / measurementBatchSizes.size should be > (MaxTailerBatchSize.toDouble * 0.7) - err shouldBe empty - } - } - - it should "form small batch sizes of batches before ingestTail under no load" in { - val batchSizes = ArrayBuffer.empty[Int] - - runPipe( - ingestTailHook = { batchOfBatches => batchSizes.addOne(batchOfBatches.size) }, - inputSource = Source(input) - .take(100) - .map( - _.tap(_ => - // Slow down source to ensure ingestTail is faster - Threading.sleep(1L) - ) - ) - .async, - ).map { case (_, _, err) => - batchSizes.sum.toDouble / batchSizes.size should be < (MaxTailerBatchSize.toDouble * 0.3) - err shouldBe empty - } - } - - def runPipe( - inputMapperHook: () => Unit = () => (), - seqMapperHook: () => Unit = () => (), - batcherHook: () => Unit = () => (), - ingesterHook: List[(Int, String)] => Unit = _ => (), - ingestTailHook: Vector[List[(Int, String)]] => Unit = _ => (), - timeout: FiniteDuration = FiniteDuration(10, "seconds"), - inputSource: Source[Int, NotUsed] = Source(input), - ): Future[(Vector[(Int, String)], Vector[Int], Option[Throwable])] = { - val semaphore = new Object - var ingested: Vector[(Int, String)] = Vector.empty - var ingestedTail: Vector[Int] = Vector.empty - val indexingFlow = - BatchingParallelIngestionPipe[Int, List[(Int, Int)], List[(Int, String)]]( - batchingFlow = BatchN.weighted(MaxBatchSize.toLong, 2)(weightFn), - inputMappingParallelism = 2, - contractReInsertion = Future.successful, - inputMapper = ins => - Future { - inputMapperHook() - ins.map((0, _)).toList - }, - seqMapperZero = List((0, 0)), - seqMapper = (prev, current) => { - seqMapperHook() - val lastIndex = prev.last._1 - current.zipWithIndex.map { case ((_, value), index) => - (index + lastIndex + 1, value) - } - }, - dbPrepareParallelism = 2, - dbPrepare = inBatch => Future(inBatch), - batchingParallelism = 2, - batcher = inBatch => - Future { - batcherHook() - inBatch.map { case (index, value) => - (index, value.toString) - } - }, - ingestingParallelism = 2, - ingester = dbBatch => - Future { - ingesterHook(dbBatch) - blocking(semaphore.synchronized { - ingested = ingested ++ dbBatch - }) - dbBatch - }, - maxTailerBatchSize = MaxTailerBatchSize, - ingestTail = dbBatch => - Future { - ingestTailHook(dbBatch) - blocking(semaphore.synchronized { - ingestedTail = ingestedTail :+ dbBatch.last.last._1 - }) - dbBatch - }, - ) - val p = Promise[(Vector[(Int, String)], Vector[Int], Option[Throwable])]() - val timeoutF = org.apache.pekko.pattern.after(timeout, system.scheduler) { - Future.failed(new Exception("timed out")) - } - val indexingF = inputSource.via(indexingFlow).run().map { _ => - blocking(semaphore.synchronized((ingested, ingestedTail, Option.empty[Throwable]))) - } - timeoutF.onComplete(p.tryComplete) - indexingF.onComplete(p.tryComplete) - - p.future.recover { case t => - blocking(semaphore.synchronized((ingested, ingestedTail, Some(t)))) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/EventMetricsUpdaterSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/EventMetricsUpdaterSpec.scala deleted file mode 100644 index 6cce6ee930..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/EventMetricsUpdaterSpec.scala +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.metrics.api.testing.MetricValues -import com.daml.metrics.api.{MetricHandle, MetricsContext} -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.{CantonTimestamp, LedgerTimeBoundaries, Offset} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.TestAcsChangeFactory -import com.digitalasset.canton.ledger.participant.state.Update.ContractInfo -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId.SameAsContractPackageId -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.protocol.{ - ExampleContractFactory, - LfSerializationVersion, - TestUpdateId, -} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time} -import com.digitalasset.daml.lf.transaction.TransactionNodeStatistics.EmptyActions -import com.digitalasset.daml.lf.transaction.test.{TestNodeBuilder, TransactionBuilder} -import com.digitalasset.daml.lf.transaction.{ - CommittedTransaction, - NodeId, - TransactionNodeStatistics, - VersionedTransaction, -} -import com.digitalasset.daml.lf.value.Value -import org.mockito.ArgumentMatchers.any -import org.mockito.MockitoSugar.{mock, verify, verifyZeroInteractions} -import org.mockito.captor.ArgCaptor -import org.scalatest.wordspec.AnyWordSpec - -class EventMetricsUpdaterSpec extends AnyWordSpec with MetricValues { - - import TraceContext.Implicits.Empty.* - - "EventMetricsUpdater" should { - - val userId = Ref.UserId.assertFromString("a0") - - val offset = Offset.tryFromLong(2L) - val statistics = TransactionNodeStatistics( - EmptyActions.copy(creates = 2), - EmptyActions.copy(consumingExercisesByCid = 1), - ) - - val someHash = Hash.hashPrivateKey("p0") - - val someCompletionInfo = state.CompletionInfo( - actAs = Nil, - userId = userId, - commandId = Ref.CommandId.assertFromString("c0"), - optDeduplicationPeriod = None, - submissionId = None, - paidTrafficCost = NonNegativeLong.zero, - ) - val someTransactionMeta = state.TransactionMeta( - ledgerEffectiveTime = Time.Timestamp.assertFromLong(2), - workflowId = None, - preparationTime = Time.Timestamp.assertFromLong(3), - submissionSeed = someHash, - timeBoundaries = LedgerTimeBoundaries.unconstrained, - optUsedPackages = None, - optNodeSeeds = None, - optByKeyNodes = None, - ) - - def someContract = ExampleContractFactory.build( - stakeholders = Set(Ref.Party.assertFromString("party")), - signatories = Set(Ref.Party.assertFromString("party")), - templateId = Ref.Identifier( - Ref.PackageId.assertFromString("abc"), - Ref.QualifiedName.assertFromString("Main:Template"), - ), - argument = Value.ValueUnit, - ) - - val someConsumingExerciseNode = TestNodeBuilder.exercise( - contract = someContract.inst.toCreateNode, - choice = Ref.Name.assertFromString("somechoice"), - consuming = true, - actingParties = Set.empty, - argument = Value.ValueUnit, - byKey = false, - ) - val aContract = someContract - val someTransactionAccepted = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(someCompletionInfo), - transactionMeta = someTransactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo( - TransactionBuilder.justCommitted( - someContract.inst.toCreateNode, - someContract.inst.toCreateNode, - someConsumingExerciseNode, - TestNodeBuilder.rollback( - ImmArray( - NodeId(2) - ) - ), - ) - ), - updateId = TestUpdateId("UpdateId"), - synchronizerId = SynchronizerId.tryFromString("da::default"), - recordTime = CantonTimestamp.now(), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map( - aContract.contractId -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = aContract.inst, - internalContractId = 0L, - ), - representativePackageId = SameAsContractPackageId, - ) - ), - ) - - "extract transaction metering" in { - - val meter: MetricHandle.Meter = mock[MetricHandle.Meter] - val captor = ArgCaptor[Long] - - EventMetricsUpdater(meter)( - MetricsContext.Empty - )( - List((offset, someTransactionAccepted)) - ) - - verify(meter).mark(captor)(any[MetricsContext]) - captor hasCaptured (statistics.committed.actions + statistics.rolledBack.actions).toLong - } - - "aggregate transaction metering across batch" in { - - val meter: MetricHandle.Meter = mock[MetricHandle.Meter] - val captor = ArgCaptor[Long] - - EventMetricsUpdater(meter)( - MetricsContext.Empty - )( - List( - ( - Offset.tryFromLong(1L), - someTransactionAccepted, - ), - ( - Offset.tryFromLong(2L), - someTransactionAccepted, - ), - ) - ) - - verify(meter).mark(captor)(any[MetricsContext]) - captor hasCaptured 2 * (statistics.committed.actions + statistics.rolledBack.actions).toLong - } - - "no metrics if input iterable is empty" in { - val meter: MetricHandle.Meter = mock[MetricHandle.Meter] - EventMetricsUpdater(meter)( - MetricsContext.Empty - )(List.empty) - verifyZeroInteractions(meter) - } - - "no metrics for infrastructure transactions" in { - - val meter: MetricHandle.Meter = mock[MetricHandle.Meter] - val txWithNoActionCount = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(someCompletionInfo), - transactionMeta = someTransactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo( - CommittedTransaction( - VersionedTransaction(LfSerializationVersion.VDev, Map.empty, ImmArray.empty) - ) - ), - updateId = TestUpdateId("UpdateId"), - synchronizerId = SynchronizerId.tryFromString("da::default"), - recordTime = CantonTimestamp.now(), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map( - aContract.contractId -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = aContract.inst, - internalContractId = 0L, - ), - representativePackageId = SameAsContractPackageId, - ) - ), - ) - - EventMetricsUpdater(meter)( - MetricsContext.Empty - )( - List((offset, txWithNoActionCount)) - ) - - verifyZeroInteractions(meter) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerFactorySpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerFactorySpec.scala deleted file mode 100644 index 3f3a79c702..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerFactorySpec.scala +++ /dev/null @@ -1,312 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner} -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.platform.indexer.ha.Handle -import org.apache.pekko.stream.KillSwitch -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.concurrent.{ExecutionContext, Future, Promise} - -class ParallelIndexerFactorySpec extends AsyncFlatSpec with Matchers with PekkoBeforeAndAfterAll { - - // AsyncFlatSpec is with serial execution context - private implicit val ec: ExecutionContext = system.dispatcher - - behavior of "initializeHandle" - - it should "correctly chain initializations and teardown-steps in the happy path" in { - val t = test - import t.* - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - waitALittle() - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - resourceInitPromise.success("happy") - - val completePromise = Promise[Unit]() - - for { - s <- initHandleStarted - _ = { - waitALittle() - s shouldBe "happy" - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleFinished.success(Handle(completePromise.future, SomeKillSwitch)) - } - handle <- initialized - _ = { - waitALittle() - resourceReleasing.isCompleted shouldBe false - handle.completed.isCompleted shouldBe false - handle.killSwitch shouldBe SomeKillSwitch - completePromise.success(()) - } - _ <- resourceReleasing - _ = { - waitALittle() - handle.completed.isCompleted shouldBe false - resourceReleased.success(()) - } - _ <- handle.completed - } yield { - 1 shouldBe 1 - } - } - - it should "propagate error from releasing resource" in { - val t = test - import t.* - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - waitALittle() - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - resourceInitPromise.success("happy") - - val completePromise = Promise[Unit]() - - for { - s <- initHandleStarted - _ = { - waitALittle() - s shouldBe "happy" - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleFinished.success(Handle(completePromise.future, SomeKillSwitch)) - } - handle <- initialized - _ = { - waitALittle() - resourceReleasing.isCompleted shouldBe false - handle.completed.isCompleted shouldBe false - handle.killSwitch shouldBe SomeKillSwitch - completePromise.success(()) - } - _ <- resourceReleasing - _ = { - waitALittle() - handle.completed.isCompleted shouldBe false - resourceReleased.failure(new Exception("releasing resource failed")) - } - failure <- handle.completed.failed - } yield { - failure.getMessage shouldBe "releasing resource failed" - } - } - - it should "propagate failure from resource initialization" in { - val t = test - import t.* - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - waitALittle() - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - resourceInitPromise.failure(new Exception("resource init failed")) - - for { - failure <- initialized.failed - } yield { - waitALittle() - failure.getMessage shouldBe "resource init failed" - initHandleStarted.isCompleted shouldBe false - resourceReleasing.isCompleted shouldBe false - } - } - - it should "propagate failure from handle initialization, complete only after releasing resource" in { - val t = test - import t.* - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - waitALittle() - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - resourceInitPromise.success("happy") - - for { - s <- initHandleStarted - _ = { - waitALittle() - s shouldBe "happy" - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleFinished.failure(new Exception("handle initialization failed")) - } - _ <- resourceReleasing - _ = { - waitALittle() - initialized.isCompleted shouldBe false - resourceReleased.success(()) - } - failure <- initialized.failed - } yield { - failure.getMessage shouldBe "handle initialization failed" - } - } - - it should "propagate failure from handle initialization, complete only after releasing resource, even if releasing failed" in { - val t = test - import t.* - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - waitALittle() - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - resourceInitPromise.success("happy") - - for { - s <- initHandleStarted - _ = { - waitALittle() - s shouldBe "happy" - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleFinished.failure(new Exception("handle initialization failed")) - } - _ <- resourceReleasing - _ = { - waitALittle() - initialized.isCompleted shouldBe false - resourceReleased.failure(new Exception("releasing resource failed")) - } - failure <- initialized.failed - } yield { - failure.getMessage shouldBe "releasing resource failed" - } - } - - it should "propagate failure from completion, but only after releasing resource finished" in { - val t = test - import t.* - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - waitALittle() - - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleStarted.isCompleted shouldBe false - - resourceInitPromise.success("happy") - - val completePromise = Promise[Unit]() - - for { - s <- initHandleStarted - _ = { - waitALittle() - s shouldBe "happy" - resourceReleasing.isCompleted shouldBe false - initialized.isCompleted shouldBe false - initHandleFinished.success(Handle(completePromise.future, SomeKillSwitch)) - } - handle <- initialized - _ = { - waitALittle() - resourceReleasing.isCompleted shouldBe false - handle.completed.isCompleted shouldBe false - handle.killSwitch shouldBe SomeKillSwitch - completePromise.failure(new Exception("completion failed")) - } - _ <- resourceReleasing - _ = { - waitALittle() - handle.completed.isCompleted shouldBe false - resourceReleased.success(()) - } - failure <- handle.completed.failed - } yield { - failure.getMessage shouldBe "completion failed" - } - } - - def test: TestHandle = { - val resourceInitPromise = Promise[String]() - val resourceReleasing = Promise[Unit]() - val resourceReleased = Promise[Unit]() - val initHandleStarted = Promise[String]() - val initHandleFinished = Promise[Handle]() - val result = ParallelIndexerFactory.initializeHandle( - new ResourceOwner[String] { - override def acquire()(implicit context: ResourceContext): Resource[String] = - Resource( - resourceInitPromise.future - ) { _ => - resourceReleasing.success(()) - resourceReleased.future - } - } - ) { s => - initHandleStarted.success(s) - initHandleFinished.future - }(ResourceContext(implicitly)) - - TestHandle( - resourceInitPromise = resourceInitPromise, - initHandleStarted = initHandleStarted.future, - initHandleFinished = initHandleFinished, - initialized = result, - resourceReleasing = resourceReleasing.future, - resourceReleased = resourceReleased, - ) - } - - // Motivation: if we are expecting a stabilized state of the async system, but it would be not stable yet, then let's wait a little bit, so we give a chance to the system to stabilize, so we can observe our expectations fail - private def waitALittle(): Unit = Threading.sleep(10) - - case class TestHandle( - resourceInitPromise: Promise[String], - initHandleStarted: Future[String], - initHandleFinished: Promise[Handle], - initialized: Future[Handle], - resourceReleasing: Future[Unit], - resourceReleased: Promise[Unit], - ) - - object SomeKillSwitch extends KillSwitch { - override def shutdown(): Unit = () - - override def abort(ex: Throwable): Unit = () - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerSubscriptionSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerSubscriptionSpec.scala deleted file mode 100644 index 187adde466..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/ParallelIndexerSubscriptionSpec.scala +++ /dev/null @@ -1,2732 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.daml.metrics.DatabaseMetrics -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.data.{CantonTimestamp, LedgerTimeBoundaries, Offset} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId.SameAsContractPackageId -import com.digitalasset.canton.ledger.participant.state.Update.{ - ContractInfo, - RepairTransactionAccepted, - TopologyTransactionEffective, - TransactionAccepted, -} -import com.digitalasset.canton.ledger.participant.state.{ - Reassignment, - ReassignmentInfo, - RepairIndex, - SynchronizerIndex, - TestAcsChangeFactory, - TransactionMeta, - Update, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.SuppressionRule.LoggerNameContains -import com.digitalasset.canton.logging.{ - LoggingContextWithTrace, - NamedLogging, - SuppressingLogger, - SuppressionRule, - TracedLogger, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.platform.indexer.ha.TestConnection -import com.digitalasset.canton.platform.indexer.parallel.ParallelIndexerSubscription.{ - ActivationRef, - Batch, - SynCon, - ZeroLedgerEnd, -} -import com.digitalasset.canton.platform.store.LedgerApiContractStore -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.backend.{ - DbDto, - ParameterStorageBackend, - ScalatestEqualityHelpers, -} -import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache -import com.digitalasset.canton.platform.store.dao.DbDispatcher -import com.digitalasset.canton.protocol.{ - ContractInstance, - ExampleContractFactory, - LfContractId, - ReassignmentId, - TestUpdateId, -} -import com.digitalasset.canton.time.SimClock -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.canton.tracing.{SerializableTraceContext, TraceContext} -import com.digitalasset.canton.util.ReassignmentTag -import com.digitalasset.canton.{HasExecutionContext, RepairCounter} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.{Ref, Time} -import com.digitalasset.daml.lf.transaction.CommittedTransaction -import com.digitalasset.daml.lf.transaction.test.{ - NodeIdTransactionBuilder, - TestNodeBuilder, - TransactionBuilder, -} -import com.digitalasset.daml.lf.value.Value.ContractId -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.Source -import org.apache.pekko.stream.testkit.scaladsl.TestSink -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.time.SpanSugar.convertIntToGrainOfTime -import org.slf4j.event.Level - -import java.sql.Connection -import java.time.Instant -import java.util.UUID -import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} -import scala.annotation.unused -import scala.collection.mutable -import scala.concurrent.{Await, ExecutionContext, Future, Promise} - -class ParallelIndexerSubscriptionSpec - extends AnyFlatSpec - with ScalaFutures - with Matchers - with HasExecutionContext - with NamedLogging { - - private lazy val executionContext = implicitly[ExecutionContext] - - implicit private val DbDtoEqual: org.scalactic.Equality[DbDto] = ScalatestEqualityHelpers.DbDtoEq - implicit val traceContext: TraceContext = TraceContext.empty - private val serializableTraceContext = - SerializableTraceContext(traceContext).toSerializedDamlProto - override val loggerFactory: SuppressingLogger = SuppressingLogger(getClass) - implicit val actorSystem: ActorSystem = ActorSystem( - classOf[ParallelIndexerSubscriptionSpec].getSimpleName - ) - implicit val materializer: Materializer = Materializer(actorSystem) - val emptyByteArray = new Array[Byte](0) - - private val someParty = DbDto.PartyEntry( - ledger_offset = 1, - recorded_at = 0, - submission_id = null, - party = Some(Ref.Party.assertFromString("party")), - typ = "accept", - rejection_reason = None, - is_local = Some(true), - ) - - private val someSynchronizerId: SynchronizerId = SynchronizerId.tryFromString("x::synchronizerId") - private val someTrafficCost: Option[Long] = Some(31000L) - private val someSynchronizerId2: SynchronizerId = - SynchronizerId.tryFromString("x::synchronizerId2") - private val someSynchronizerId3: SynchronizerId = - SynchronizerId.tryFromString("x::synchronizerId3") - - private val someTime = Instant.now - - private val somePartyAllocation = state.Update.TopologyTransactionEffective( - updateId = TestUpdateId(UUID.randomUUID().toString), - events = Set( - TopologyTransactionEffective.TopologyEvent.PartyToParticipantAuthorization( - party = Ref.Party.assertFromString("party"), - participant = Ref.ParticipantId.assertFromString("participant"), - authorizationEvent = TopologyTransactionEffective.AuthorizationEvent.Added( - TopologyTransactionEffective.AuthorizationLevel.Confirmation - ), - ) - ), - synchronizerId = SynchronizerId.tryFromString("invalid::deadbeef"), - effectiveTime = CantonTimestamp.assertFromInstant(someTime), - ) - - private val updateId = TestUpdateId("mock_hash") - private val updateIdByteArray = updateId.toProtoPrimitive.toByteArray - - private def offset(l: Long): Offset = Offset.tryFromLong(l) - - private val metrics = LedgerApiServerMetrics.ForTesting - - private def hashCid(key: String): ContractId = ContractId.V1(Hash.hashPrivateKey(key)) - - private val someEventActivate = DbDto.EventActivate( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = None, - command_id = None, - submitters = None, - record_time = 1, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = None, - event_type = 1, - event_sequential_id = 15, - node_id = 3, - additional_witnesses = None, - source_synchronizer_id = None, - reassignment_counter = None, - reassignment_id = None, - representative_package_id = Ref.PackageId.assertFromString("p"), - notPersistedContractId = hashCid("1"), - internal_contract_id = 1, - create_key_hash = None, - traffic_cost = someTrafficCost, - ) - - private val someEventDeactivate = DbDto.EventDeactivate( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = None, - command_id = None, - submitters = None, - record_time = 1, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = None, - event_type = 1, - event_sequential_id = 1, - node_id = 1, - deactivated_event_sequential_id = None, - additional_witnesses = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - reassignment_id = None, - assignment_exclusivity = None, - target_synchronizer_id = None, - reassignment_counter = None, - contract_id = hashCid("1"), - internal_contract_id = None, - template_id = Ref.NameTypeConRef.assertFromString("#p:m:t"), - package_id = Ref.PackageId.assertFromString("p"), - stakeholders = Set.empty, - ledger_effective_time = None, - traffic_cost = someTrafficCost, - ) - - private val someEventWitnessed = DbDto.EventVariousWitnessed( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = None, - command_id = None, - submitters = None, - record_time = 1, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = None, - event_type = 1, - event_sequential_id = 1, - node_id = 1, - additional_witnesses = Set.empty, - consuming = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - representative_package_id = None, - contract_id = None, - internal_contract_id = None, - template_id = None, - package_id = None, - ledger_effective_time = None, - traffic_cost = someTrafficCost, - ) - - private val someCompletion = DbDto.CommandCompletion( - completion_offset = 1, - record_time = 0, - publication_time = 0, - user_id = Ref.UserId.assertFromString("user"), - submitters = Set.empty, - command_id = "", - update_id = None, - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = None, - deduplication_offset = None, - deduplication_duration_seconds = None, - deduplication_duration_nanos = None, - synchronizer_id = someSynchronizerId, - message_uuid = None, - is_transaction = true, - trace_context = serializableTraceContext, - traffic_cost = 0L, - ) - - private val offsetsAndUpdates = - Vector(1L, 2L, 3L) - .map(offset) - .zip( - Vector( - somePartyAllocation, - somePartyAllocation.copy(effectiveTime = somePartyAllocation.recordTime.addMicros(1000)), - somePartyAllocation.copy(effectiveTime = somePartyAllocation.recordTime.addMicros(2000)), - ) - ) - - private def mockDbDispatcher(connection: Connection): DbDispatcher = new DbDispatcher { - override def executeSql[T](databaseMetrics: DatabaseMetrics)(sql: Connection => T)(implicit - loggingContext: LoggingContextWithTrace - ): Future[T] = - Future.successful(sql(connection)) - - override def executeSqlUS[T](databaseMetrics: DatabaseMetrics)( - sql: Connection => T - )(implicit loggingContext: LoggingContextWithTrace): FutureUnlessShutdown[T] = - FutureUnlessShutdown.pure(sql(connection)) - } - - behavior of "inputMapper" - - it should "provide required Batch in happy path case" in { - val actual = ParallelIndexerSubscription.inputMapper( - metrics = metrics, - toDbDto = _ => _ => Iterator(someParty, someParty), - eventMetricsUpdater = _ => (), - _ => Vector("1", "2"), - logger, - )( - List( - Offset.tryFromLong(1), - Offset.tryFromLong(2), - Offset.tryFromLong(3), - ).zip(offsetsAndUpdates.map(_._2)) - ) - val expected = Batch[Vector[DbDto]]( - ledgerEnd = LedgerEnd( - lastOffset = offset(3), - lastEventSeqId = 0L, - lastStringInterningId = 0, - lastPublicationTime = CantonTimestamp.MinValue, - ), - batchTraceContext = TraceContext.empty, - batch = Vector( - someParty, - someParty, - someParty, - someParty, - someParty, - someParty, - ), - batchSize = 3, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Vector("1", "2"), - usedInternalContractIds = Set.empty, - ) - actual.copy(batchTraceContext = TraceContext.empty) shouldBe expected - } - - behavior of "seqMapperZero" - - it should "provide required Batch in happy path case" in { - val ledgerEnd = LedgerEnd( - lastOffset = offset(1), - lastEventSeqId = 123, - lastStringInterningId = 234, - lastPublicationTime = CantonTimestamp.now(), - ) - - val result = ParallelIndexerSubscription.seqMapperZero(Some(ledgerEnd)) - result shouldBe Batch( - ledgerEnd = ledgerEnd, - batchTraceContext = TraceContext.empty, - batch = Vector.empty, - batchSize = 0, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - } - - it should "provide required Batch in case starting from scratch" in { - ParallelIndexerSubscription.seqMapperZero(None) shouldBe Batch( - ledgerEnd = ZeroLedgerEnd, - batchTraceContext = TraceContext.empty, - batch = Vector.empty, - batchSize = 0, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - } - - behavior of "mutableDropWhile" - - it should "drop elements correctly happy case" in { - val map = mutable.LinkedHashMap[String, Int]( - "x" -> 1, - "a" -> 1, - "y" -> 2, - "b" -> 3, - "z" -> 3, - "c" -> 4, - "u" -> 4, - "f" -> 5, - ) - ParallelIndexerSubscription.mutableDropWhile(map)(_ <= 3) - - map.toList shouldBe List( - "c" -> 4, - "u" -> 4, - "f" -> 5, - ) - } - - it should "drop elements correctly early" in { - val map = mutable.LinkedHashMap[String, Int]( - "x" -> 1, - "a" -> 1, - "y" -> 2, - "b" -> 3, - "z" -> 3, - "c" -> 4, - "u" -> 4, - "f" -> 5, - ) - ParallelIndexerSubscription.mutableDropWhile(map)(_ <= -3) - - map.toList shouldBe List( - "x" -> 1, - "a" -> 1, - "y" -> 2, - "b" -> 3, - "z" -> 3, - "c" -> 4, - "u" -> 4, - "f" -> 5, - ) - } - - it should "drop elements correctly late" in { - val map = mutable.LinkedHashMap[String, Int]( - "x" -> 1, - "a" -> 1, - "y" -> 2, - "b" -> 3, - "z" -> 3, - "c" -> 4, - "u" -> 4, - "f" -> 5, - ) - ParallelIndexerSubscription.mutableDropWhile(map)(_ <= 150) - - map.toList shouldBe Nil - } - - it should "drop elements correctly empty" in { - val map = mutable.LinkedHashMap[String, Int]() - ParallelIndexerSubscription.mutableDropWhile(map)(_ <= 150) - map.toList shouldBe Nil - } - - behavior of "seqMapper" - - it should "assign sequence ids correctly, and populate string-interning entries correctly in happy path case" in { - val clockStart = CantonTimestamp.now() - val simClock = new SimClock(clockStart, loggerFactory) - - val previousPublicationTime = simClock.monotonicTime() - val currentPublicationTime = simClock.uniqueTime() - previousPublicationTime should not be currentPublicationTime - val previousLedgerEnd = LedgerEnd( - lastOffset = offset(1), - lastEventSeqId = 15, - lastStringInterningId = 26, - lastPublicationTime = previousPublicationTime, - ) - val filter = DbDto.IdFilter( - 0, - Ref.NameTypeConRef.assertFromString("#p:m:t"), - Ref.Party.assertFromString("party"), - first_per_sequential_id = false, - ) - val ledgerEndCache = MutableLedgerEndCache() - val result = ParallelIndexerSubscription.seqMapper( - internize = _.zipWithIndex.map(x => x._2 -> x._2.toString).take(2), - metrics = metrics, - clock = simClock, - logger = logger, - ledgerEndCache = ledgerEndCache, - activeContracts = mutable.LinkedHashMap.empty, - )( - ParallelIndexerSubscription.seqMapperZero(Some(previousLedgerEnd)), - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someParty, - someEventActivate, - filter.activateStakeholder, - filter.activateWitness, - DbDto.TransactionMeta(emptyByteArray, 1, 0L, 0L, someSynchronizerId, 0L, 0L), - someCompletion, - someParty, - someEventDeactivate, - filter.deactivateStakeholder, - filter.deactivateWitness, - someEventDeactivate, - filter.deactivateStakeholder, - filter.deactivateWitness, - DbDto.TransactionMeta(emptyByteArray, 1, 0L, 0L, someSynchronizerId, 0L, 0L), - someParty, - someEventWitnessed, - filter.variousWitness, - DbDto.TransactionMeta(emptyByteArray, 1, 0L, 0L, someSynchronizerId, 0L, 0L), - someParty, - ), - batchSize = 3, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Vector("1", "2"), - usedInternalContractIds = Set.empty, - ), - ) - import scala.util.chaining.* - - result.ledgerEnd.lastEventSeqId shouldBe 19 - result.ledgerEnd.lastStringInterningId shouldBe 1 - result.ledgerEnd.lastPublicationTime shouldBe currentPublicationTime - result.ledgerEnd.lastOffset shouldBe offset(2) - result.eventCount shouldBe 4L - result.distinctRawStrings shouldBe Nil - result.batch(1).asInstanceOf[DbDto.EventActivate].event_sequential_id shouldBe 16 - result - .batch(2) - .asInstanceOf[DbDto.IdFilterActivateStakeholder] - .idFilter - .event_sequential_id shouldBe 16 - result - .batch(3) - .asInstanceOf[DbDto.IdFilterActivateWitness] - .idFilter - .event_sequential_id shouldBe 16 - result.batch(4).asInstanceOf[DbDto.TransactionMeta].tap { transactionMeta => - transactionMeta.event_sequential_id_first shouldBe 16L - transactionMeta.event_sequential_id_last shouldBe 16L - transactionMeta.publication_time shouldBe currentPublicationTime.toMicros - } - result - .batch(5) - .asInstanceOf[DbDto.CommandCompletion] - .publication_time shouldBe currentPublicationTime.toMicros - result.batch(7).asInstanceOf[DbDto.EventDeactivate].event_sequential_id shouldBe 17 - result - .batch(8) - .asInstanceOf[DbDto.IdFilterDeactivateStakeholder] - .idFilter - .event_sequential_id shouldBe 17 - result - .batch(9) - .asInstanceOf[DbDto.IdFilterDeactivateWitness] - .idFilter - .event_sequential_id shouldBe 17 - result.batch(10).asInstanceOf[DbDto.EventDeactivate].event_sequential_id shouldBe 18 - result - .batch(11) - .asInstanceOf[DbDto.IdFilterDeactivateStakeholder] - .idFilter - .event_sequential_id shouldBe 18 - result - .batch(12) - .asInstanceOf[DbDto.IdFilterDeactivateWitness] - .idFilter - .event_sequential_id shouldBe 18 - result.batch(13).asInstanceOf[DbDto.TransactionMeta].tap { transactionMeta => - transactionMeta.event_sequential_id_first shouldBe 17L - transactionMeta.event_sequential_id_last shouldBe 18L - transactionMeta.publication_time shouldBe currentPublicationTime.toMicros - } - result.batch(15).asInstanceOf[DbDto.EventVariousWitnessed].event_sequential_id shouldBe 19 - result - .batch(16) - .asInstanceOf[DbDto.IdFilterVariousWitness] - .idFilter - .event_sequential_id shouldBe 19 - result.batch(17).asInstanceOf[DbDto.TransactionMeta].tap { transactionMeta => - transactionMeta.event_sequential_id_first shouldBe 19L - transactionMeta.event_sequential_id_last shouldBe 19L - transactionMeta.publication_time shouldBe currentPublicationTime.toMicros - } - result.batch(19).asInstanceOf[DbDto.StringInterningDto].internalId shouldBe 0 - result.batch(19).asInstanceOf[DbDto.StringInterningDto].externalString shouldBe "0" - result.batch(20).asInstanceOf[DbDto.StringInterningDto].internalId shouldBe 1 - result.batch(20).asInstanceOf[DbDto.StringInterningDto].externalString shouldBe "1" - } - - it should "preserve sequence id if nothing to assign" in { - val previousLedgerEnd = LedgerEnd( - lastOffset = offset(1), - lastEventSeqId = 15, - lastStringInterningId = 25, - lastPublicationTime = CantonTimestamp.now(), - ) - val simClock = new SimClock(loggerFactory = loggerFactory) - val result = ParallelIndexerSubscription.seqMapper( - internize = _ => Nil, - metrics = metrics, - clock = simClock, - logger = logger, - ledgerEndCache = MutableLedgerEndCache(), - activeContracts = mutable.LinkedHashMap.empty, - )( - ParallelIndexerSubscription.seqMapperZero(Some(previousLedgerEnd)), - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someParty, - someParty, - someParty, - someParty, - ), - batchSize = 3, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - ) - result.ledgerEnd.lastEventSeqId shouldBe 15 - result.ledgerEnd.lastStringInterningId shouldBe 25 - result.ledgerEnd.lastOffset shouldBe offset(2) - result.eventCount shouldBe 0L - } - - private val now = CantonTimestamp.now() - private val previous = now.plusSeconds(10) - private val previousLedgerEnd = LedgerEnd( - lastOffset = offset(1), - lastEventSeqId = 15, - lastStringInterningId = 25, - lastPublicationTime = previous, - ) - private val simClock = new SimClock(now, loggerFactory = loggerFactory) - - it should "take the last publication time, if bigger than the current time, and log" in { - loggerFactory.assertLogs( - LoggerNameContains("ParallelIndexerSubscription") && SuppressionRule.Level(Level.INFO) - )( - ParallelIndexerSubscription - .seqMapper( - internize = _ => Nil, - metrics = metrics, - clock = simClock, - logger = logger, - ledgerEndCache = MutableLedgerEndCache(), - activeContracts = mutable.LinkedHashMap.empty, - )( - ParallelIndexerSubscription.seqMapperZero(Some(previousLedgerEnd)), - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someParty, - someParty, - someParty, - someParty, - ), - batchSize = 3, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - ) - .ledgerEnd - .lastPublicationTime shouldBe previous, - _.infoMessage should include("Has the clock been reset, e.g., during participant failover?"), - ) - } - - it should "activations are added to the ACS" in { - val simClock = new SimClock(now, loggerFactory = loggerFactory) - val zeroBatch = ParallelIndexerSubscription.seqMapperZero(Some(previousLedgerEnd)) - val ledgerEndCache = MutableLedgerEndCache() - val activeContracts = mutable.LinkedHashMap.empty[SynCon, ActivationRef] - val result = ParallelIndexerSubscription - .seqMapper( - internize = _ => Nil, - metrics = metrics, - clock = simClock, - logger = logger, - ledgerEndCache = ledgerEndCache, - activeContracts = activeContracts, - )( - zeroBatch, - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someEventActivate.copy( - synchronizer_id = someSynchronizerId2, - notPersistedContractId = hashCid("C"), - ) - ), - batchSize = 10, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - ) - activeContracts shouldBe Map( - SynCon(someSynchronizerId2, hashCid("C")) -> ActivationRef(16L, 1L) - ) - result.missingDeactivatedActivations shouldBe Map.empty - result.eventCount shouldBe 1L - } - - it should "double activations are reported as warnings" in { - val simClock = new SimClock(now, loggerFactory = loggerFactory) - val zeroBatch = ParallelIndexerSubscription.seqMapperZero(Some(previousLedgerEnd)) - val ledgerEndCache = MutableLedgerEndCache() - val activeContracts = mutable.LinkedHashMap.empty[SynCon, ActivationRef] - activeContracts.addAll( - Seq( - SynCon(someSynchronizerId, hashCid("A")) -> ActivationRef(1L, 1L), - SynCon(someSynchronizerId2, hashCid("B")) -> ActivationRef(2L, 2L), - ) - ) - val result = loggerFactory.assertLogs( - LoggerNameContains("ParallelIndexerSubscription") && SuppressionRule.Level(Level.WARN) - )( - ParallelIndexerSubscription - .seqMapper( - internize = _ => Nil, - metrics = metrics, - clock = simClock, - logger = logger, - ledgerEndCache = ledgerEndCache, - activeContracts = activeContracts, - )( - zeroBatch, - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someEventActivate.copy( - synchronizer_id = someSynchronizerId, - notPersistedContractId = hashCid("A"), - internal_contract_id = 10L, - ), - someEventActivate.copy( - synchronizer_id = someSynchronizerId2, - notPersistedContractId = hashCid("B"), - internal_contract_id = 20L, - ), - ), - batchSize = 10, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - ), - _.warningMessage should include( - "Double activation at eventSeqId: 16. Previous at Some(1) This should not happen" - ), - _.warningMessage should include( - "Double activation at eventSeqId: 17. Previous at Some(2) This should not happen" - ), - ) - activeContracts shouldBe Map( - SynCon(someSynchronizerId, hashCid("A")) -> ActivationRef(16L, 10L), - SynCon(someSynchronizerId2, hashCid("B")) -> ActivationRef(17L, 20L), - ) - result.missingDeactivatedActivations shouldBe Map.empty - } - - it should "deactivation is extending the missing activations if not found (but not for divulged or non-consumed contracts)" in { - val simClock = new SimClock(now, loggerFactory = loggerFactory) - val zeroBatch = ParallelIndexerSubscription.seqMapperZero(Some(previousLedgerEnd)) - val ledgerEndCache = MutableLedgerEndCache() - val activeContracts = mutable.LinkedHashMap.empty[SynCon, ActivationRef] - val result = ParallelIndexerSubscription - .seqMapper( - internize = _ => Nil, - metrics = metrics, - clock = simClock, - logger = logger, - ledgerEndCache = ledgerEndCache, - activeContracts = activeContracts, - )( - zeroBatch, - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("A"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("E"), - ), - someEventWitnessed.copy( - synchronizer_id = someSynchronizerId, - contract_id = Some(hashCid("C")), - ), - someEventWitnessed.copy( - consuming = Some(false), - synchronizer_id = someSynchronizerId, - contract_id = Some(hashCid("D")), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("B"), - ), - ), - batchSize = 10, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - ) - activeContracts shouldBe Map.empty - result.missingDeactivatedActivations shouldBe Map( - SynCon(someSynchronizerId, hashCid("A")) -> None, - SynCon(someSynchronizerId, hashCid("B")) -> None, - SynCon(someSynchronizerId2, hashCid("E")) -> None, - ) - result.batch - .collect { case u: DbDto.EventDeactivate => - u.deactivated_event_sequential_id - } - .shouldBe( - Seq( - None, - None, - None, - ) - ) - } - - it should "deactivation is computed directly from the active contracts if it has it - and also removing activeness thereof" in { - val simClock = new SimClock(now, loggerFactory = loggerFactory) - val zeroBatch = ParallelIndexerSubscription.seqMapperZero(Some(previousLedgerEnd)) - val ledgerEndCache = MutableLedgerEndCache() - val activeContracts = mutable.LinkedHashMap.empty[SynCon, ActivationRef] - activeContracts - .addAll( - Seq( - SynCon(someSynchronizerId, hashCid("A")) -> ActivationRef(1L, 100L), - SynCon(someSynchronizerId2, hashCid("B")) -> ActivationRef(2L, 200L), - SynCon(someSynchronizerId3, hashCid("A")) -> ActivationRef(3L, 300L), - SynCon(someSynchronizerId3, hashCid("B")) -> ActivationRef(4L, 400L), - SynCon(someSynchronizerId3, hashCid("C")) -> ActivationRef(5L, 500L), - SynCon(someSynchronizerId, hashCid("C")) -> ActivationRef(6L, 600L), - ) - ) - val result = ParallelIndexerSubscription - .seqMapper( - internize = _ => Nil, - metrics = metrics, - clock = simClock, - logger = logger, - ledgerEndCache = ledgerEndCache, - activeContracts = activeContracts, - )( - zeroBatch, - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId3, - contract_id = hashCid("C"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("A"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("B"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("C"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("A"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("B"), - ), - ), - batchSize = 10, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - ) - activeContracts shouldBe Map( - SynCon(someSynchronizerId3, hashCid("A")) -> ActivationRef(3L, 300L), - SynCon(someSynchronizerId3, hashCid("B")) -> ActivationRef(4L, 400L), - SynCon(someSynchronizerId, hashCid("C")) -> ActivationRef(6L, 600L), - ) - result.missingDeactivatedActivations shouldBe Map( - SynCon(someSynchronizerId2, hashCid("A")) -> None, - SynCon(someSynchronizerId, hashCid("B")) -> None, - SynCon(someSynchronizerId2, hashCid("C")) -> None, - ) - result.batch - .collect { case u: DbDto.EventDeactivate => - u.deactivated_event_sequential_id -> u.internal_contract_id - } - .shouldBe( - Seq( - Some(5L) -> Some(500L), - None -> None, - None -> None, - None -> None, - Some(1L) -> Some(100L), - Some(2L) -> Some(200L), - ) - ) - } - - it should "activations pruned correctly based on actual ledger-end" in { - val simClock = new SimClock(now, loggerFactory = loggerFactory) - val zeroBatch = ParallelIndexerSubscription.seqMapperZero(Some(previousLedgerEnd)) - val ledgerEndCache = MutableLedgerEndCache() - val activeContracts = mutable.LinkedHashMap.empty[SynCon, ActivationRef] - activeContracts - .addAll( - Seq( - SynCon(someSynchronizerId, hashCid("A")) -> ActivationRef(100L, 1L), - SynCon(someSynchronizerId2, hashCid("B")) -> ActivationRef(110L, 2L), - SynCon(someSynchronizerId3, hashCid("A")) -> ActivationRef(120L, 3L), - SynCon(someSynchronizerId3, hashCid("B")) -> ActivationRef(130L, 4L), - ) - ) - def processSeqMapper() = ParallelIndexerSubscription - .seqMapper( - internize = _ => Nil, - metrics = metrics, - clock = simClock, - logger = logger, - ledgerEndCache = ledgerEndCache, - activeContracts = activeContracts, - )( - zeroBatch, - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someParty - ), - batchSize = 10, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - ) - activeContracts shouldBe Map( - SynCon(someSynchronizerId, hashCid("A")) -> ActivationRef(100L, 1L), - SynCon(someSynchronizerId2, hashCid("B")) -> ActivationRef(110L, 2L), - SynCon(someSynchronizerId3, hashCid("A")) -> ActivationRef(120L, 3L), - SynCon(someSynchronizerId3, hashCid("B")) -> ActivationRef(130L, 4L), - ) - - // ledger end below - ledgerEndCache.set( - Some( - previousLedgerEnd.copy( - lastEventSeqId = 10 - ) - ) - ) - processSeqMapper() - activeContracts shouldBe Map( - SynCon(someSynchronizerId, hashCid("A")) -> ActivationRef(100L, 1L), - SynCon(someSynchronizerId2, hashCid("B")) -> ActivationRef(110L, 2L), - SynCon(someSynchronizerId3, hashCid("A")) -> ActivationRef(120L, 3L), - SynCon(someSynchronizerId3, hashCid("B")) -> ActivationRef(130L, 4L), - ) - - // ledger end on first - ledgerEndCache.set( - Some( - previousLedgerEnd.copy( - lastEventSeqId = 100L - ) - ) - ) - processSeqMapper() - activeContracts shouldBe Map( - SynCon(someSynchronizerId2, hashCid("B")) -> ActivationRef(110L, 2L), - SynCon(someSynchronizerId3, hashCid("A")) -> ActivationRef(120L, 3L), - SynCon(someSynchronizerId3, hashCid("B")) -> ActivationRef(130L, 4L), - ) - - // ledger end after third - ledgerEndCache.set( - Some( - previousLedgerEnd.copy( - lastEventSeqId = 125L - ) - ) - ) - processSeqMapper() - activeContracts shouldBe Map( - SynCon(someSynchronizerId3, hashCid("B")) -> ActivationRef(130L, 4L) - ) - } - - behavior of "refillMissingDeactivatedActivations" - - it should "correctly refill the missing activations" in { - ParallelIndexerSubscription - .refillMissingDeactivatedActivations(LedgerApiServerMetrics.ForTesting, logger)( - Batch( - ledgerEnd = previousLedgerEnd, - batch = Vector( - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("A"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("C"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("B"), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("B"), - deactivated_event_sequential_id = Some(10000), - internal_contract_id = Some(100000), - ), - ), - batchSize = 1, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map( - SynCon(someSynchronizerId2, hashCid("A")) -> Some(ActivationRef(123, 1230)), - SynCon(someSynchronizerId, hashCid("B")) -> Some(ActivationRef(1234, 12340)), - SynCon(someSynchronizerId, hashCid("C")) -> Some(ActivationRef(12345, 123450)), - ), - eventCount = 0L, - batchTraceContext = TraceContext.empty, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - ) - .batch should contain theSameElementsInOrderAs Vector( - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("A"), - deactivated_event_sequential_id = Some(123), - internal_contract_id = Some(1230), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("C"), - deactivated_event_sequential_id = Some(12345), - internal_contract_id = Some(123450), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("B"), - deactivated_event_sequential_id = Some(1234), - internal_contract_id = Some(12340), - ), - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId, - contract_id = hashCid("B"), - deactivated_event_sequential_id = Some(10000), - internal_contract_id = Some(100000), - ), - ) - } - - it should "report warning, but succeed, if activation is missing" in { - loggerFactory.assertLogs( - LoggerNameContains("ParallelIndexerSubscription") && SuppressionRule.Level(Level.WARN) - )( - ParallelIndexerSubscription - .refillMissingDeactivatedActivations(LedgerApiServerMetrics.ForTesting, logger)( - Batch( - ledgerEnd = previousLedgerEnd, - batch = Vector( - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("A"), - deactivated_event_sequential_id = None, - event_type = 3, - ) - ), - batchSize = 1, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map( - SynCon(someSynchronizerId2, hashCid("A")) -> None, - SynCon(someSynchronizerId, hashCid("B")) -> Some(ActivationRef(1234, 12340)), - ), - eventCount = 0L, - batchTraceContext = TraceContext.empty, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - ) - .batch should contain theSameElementsInOrderAs Vector( - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("A"), - deactivated_event_sequential_id = None, - internal_contract_id = None, - event_type = 3, - ) - ), - _.warningMessage should include( - s"Activation is missing for a deactivation for deactivated event with type:ConsumingExercise offset:1 nodeId:1 for synchronizerId:$someSynchronizerId2 contractId:${hashCid("A")}." - ), - ) - } - - it should "report error and fail, if activation was not even requested" in { - loggerFactory.assertInternalError[IllegalStateException]( - ParallelIndexerSubscription.refillMissingDeactivatedActivations( - LedgerApiServerMetrics.ForTesting, - logger, - )( - Batch( - ledgerEnd = previousLedgerEnd, - batch = Vector( - someEventDeactivate.copy( - synchronizer_id = someSynchronizerId2, - contract_id = hashCid("A"), - deactivated_event_sequential_id = None, - event_type = 3, - ) - ), - batchSize = 1, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map( - SynCon(someSynchronizerId, hashCid("B")) -> Some(ActivationRef(1234, 12340)) - ), - eventCount = 0L, - batchTraceContext = TraceContext.empty, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - ), - _.getMessage should include( - s"Programming error: deactivation reference is missing for deactivated event with type:ConsumingExercise offset:1 nodeId:1 for synchronizerId:$someSynchronizerId2 contractId:${hashCid("A")}, but lookup was not even initiated." - ), - ) - } - - behavior of "dbPrepare" - - it should "apply missing deactivated activations" in { - def lastActivations(@unused _synchronizerContracts: Iterable[(SynchronizerId, Long)])( - @unused _connection: Connection - ): Map[(SynchronizerId, Long), Long] = Map( - (someSynchronizerId, 5L) -> 2L, - (someSynchronizerId2, 15L) -> 4L, - ) - - def resolveInternalContractIds(@unused _tc: TraceContext)( - @unused _contractIds: Iterable[ContractId] - ): Future[Map[ContractId, Long]] = Future.successful { - Map( - hashCid("#1") -> 5L, - hashCid("#2") -> 7L, - hashCid("#3") -> 15L, - ) - } - - val dtos = Vector(someEventActivate) - val ledgerEnd = LedgerEnd( - lastOffset = offset(2), - lastEventSeqId = 2000, - lastStringInterningId = 300, - lastPublicationTime = CantonTimestamp.MinValue, - ) - val inBatch = Batch( - ledgerEnd = ledgerEnd, - batchTraceContext = TraceContext.empty, - batch = dtos, - batchSize = dtos.size, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map( - SynCon(someSynchronizerId, hashCid("#1")) -> None, - SynCon(someSynchronizerId, hashCid("#2")) -> None, // not in last activations - SynCon(someSynchronizerId2, hashCid("#3")) -> None, - ), - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - - val outBatchF = ParallelIndexerSubscription.dbPrepare( - lastActivations, - mockDbDispatcher(new TestConnection), - resolveInternalContractIds, - executionContext, - metrics, - logger, - )(inBatch) - - outBatchF.futureValue shouldBe - Batch( - ledgerEnd = ledgerEnd, - batchTraceContext = TraceContext.empty, - batch = dtos, - batchSize = dtos.size, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map( - SynCon(someSynchronizerId, hashCid("#1")) -> Some(ActivationRef(2L, 5L)), - SynCon(someSynchronizerId, hashCid("#2")) -> None, - SynCon(someSynchronizerId2, hashCid("#3")) -> Some(ActivationRef(4L, 15L)), - ), - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - } - - behavior of "batcher" - - it should "batch correctly in happy path case" in { - val result = ParallelIndexerSubscription.batcher( - batchF = _ => "bumm", - logger = logger, - metrics = LedgerApiServerMetrics.ForTesting, - )( - Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = Vector( - someParty, - someParty, - someParty, - someParty, - ), - batchSize = 3, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - ) - result shouldBe Batch( - ledgerEnd = ZeroLedgerEnd.copy(lastOffset = offset(2)), - batchTraceContext = TraceContext.empty, - batch = "bumm", - batchSize = 3, - offsetsUpdates = offsetsAndUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - } - - behavior of "ingester" - - it should "apply ingestFunction and cleanUnusedBatch" in { - val connection = new TestConnection - - val batchPayload = "Some batch payload" - - val ingestFunction: (Connection, String) => Unit = { - case (`connection`, `batchPayload`) => () - case other => fail(s"Unexpected: $other") - } - - val ledgerEnd = LedgerEnd( - lastOffset = offset(2), - lastEventSeqId = 2000, - lastStringInterningId = 300, - lastPublicationTime = CantonTimestamp.MinValue, - ) - val inBatch = Batch( - ledgerEnd = ledgerEnd, - batchTraceContext = TraceContext.empty, - batch = batchPayload, - batchSize = 0, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - - val persistedTransferOffsets = new AtomicBoolean(false) - val zeroDbBatch = "zero" - val outBatchF = - ParallelIndexerSubscription.ingester( - ingestFunction = ingestFunction, - lockUsedContracts = _ => _ => Set.empty, - evictContractsFromCache = _ => (), - reassignmentOffsetPersistence = new ReassignmentOffsetPersistence { - override def persist(updates: Seq[(Offset, Update)], tracedLogger: TracedLogger)(implicit - traceContext: TraceContext - ): Future[Unit] = { - persistedTransferOffsets.set(true) - Future.unit - } - }, - zeroDbBatch = "zero", - dbDispatcher = mockDbDispatcher(connection), - executionContext = executionContext, - metrics = metrics, - logger = logger, - )(inBatch) - - val outBatch = Await.result(outBatchF, 10.seconds) - - outBatch shouldBe - Batch( - ledgerEnd = ledgerEnd, - batchTraceContext = TraceContext.empty, - batch = zeroDbBatch, - batchSize = 0, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - persistedTransferOffsets.get() shouldBe true - } - - behavior of "ingestTail" - - it should "apply ingestTailFunction on the last batch and forward the batch of batches" in { - val ledgerEnd = ParameterStorageBackend.LedgerEnd( - lastOffset = offset(5), - lastEventSeqId = 2000, - lastStringInterningId = 300, - lastPublicationTime = CantonTimestamp.MinValue, - ) - - val secondBatchLedgerEnd = ParameterStorageBackend.LedgerEnd( - lastOffset = offset(6), - lastEventSeqId = 3000, - lastStringInterningId = 400, - lastPublicationTime = CantonTimestamp.MinValue.plusSeconds(10), - ) - - val storeLedgerEndF: (LedgerEnd, Map[SynchronizerId, SynchronizerIndex]) => Future[Unit] = { - case (`secondBatchLedgerEnd`, _) => Future.unit - case otherLedgerEnd => fail(s"Unexpected ledger end: $otherLedgerEnd") - } - - val batch = Batch( - ledgerEnd = ledgerEnd, - batchTraceContext = TraceContext.empty, - batch = "Some batch payload", - batchSize = 0, - offsetsUpdates = Vector.empty, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - - val batchOfBatches = Vector( - batch, - batch.copy(ledgerEnd = secondBatchLedgerEnd), - ) - - val outBatchF = - ParallelIndexerSubscription.ingestTail( - storeLedgerEnd = storeLedgerEndF, - executionContext = executionContext, - logger = logger, - )( - traceContext - )(batchOfBatches) - - val outBatch = Await.result(outBatchF, 10.seconds) - outBatch shouldBe batchOfBatches - } - - behavior of "synchronizerLedgerEndFromBatch" - - private val someSequencerIndex1 = CantonTimestamp.ofEpochMicro(123) - private val someSequencerIndex2 = CantonTimestamp.ofEpochMicro(256) - private val someRepairIndex1 = RepairIndex( - timestamp = CantonTimestamp.ofEpochMicro(153), - counter = RepairCounter(15), - ) - private val someRepairIndex2 = RepairIndex( - timestamp = CantonTimestamp.ofEpochMicro(156), - counter = RepairCounter.Genesis, - ) - private val someRecordTime1 = CantonTimestamp.ofEpochMicro(100) - private val someRecordTime2 = CantonTimestamp.ofEpochMicro(300) - private val someRepairCounter1 = RepairCounter(0) - - it should "populate correct ledger-end from batches for a sequencer counter moved" in { - ParallelIndexerSubscription.ledgerEndSynchronizerIndexFrom( - Vector(someSynchronizerId -> SynchronizerIndex.forSequencedUpdate(someSequencerIndex1)) - ) shouldBe Map( - someSynchronizerId -> SynchronizerIndex.forSequencedUpdate(someSequencerIndex1) - ) - } - - it should "populate correct ledger-end from batches for a repair counter moved" in { - ParallelIndexerSubscription.ledgerEndSynchronizerIndexFrom( - Vector(someSynchronizerId -> SynchronizerIndex.forRepairUpdate(someRepairIndex1)) - ) shouldBe Map( - someSynchronizerId -> SynchronizerIndex.forRepairUpdate(someRepairIndex1) - ) - } - - it should "populate correct ledger-end from batches for a mixed batch" in { - ParallelIndexerSubscription.ledgerEndSynchronizerIndexFrom( - Vector( - someSynchronizerId -> SynchronizerIndex.forSequencedUpdate(someSequencerIndex1), - someSynchronizerId -> SynchronizerIndex.forRepairUpdate( - RepairIndex(someRecordTime1, someRepairCounter1) - ), - someSynchronizerId -> SynchronizerIndex.forSequencedUpdate(someSequencerIndex2), - someSynchronizerId2 -> SynchronizerIndex.forRepairUpdate(someRepairIndex1), - someSynchronizerId2 -> SynchronizerIndex.forRepairUpdate(someRepairIndex2), - someSynchronizerId2 -> SynchronizerIndex.forFloatingUpdate(someRecordTime1), - ) - ) shouldBe Map( - someSynchronizerId -> SynchronizerIndex( - Some(RepairIndex(someRecordTime1, someRepairCounter1)), - Some(someSequencerIndex2), - someSequencerIndex2, - ), - someSynchronizerId2 -> SynchronizerIndex( - Some(someRepairIndex2), - None, - someRepairIndex2.timestamp, - ), - ) - } - - it should "populate correct ledger-end from batches for a mixed batch 2" in { - ParallelIndexerSubscription.ledgerEndSynchronizerIndexFrom( - Vector( - someSynchronizerId -> SynchronizerIndex.forSequencedUpdate(someSequencerIndex1), - someSynchronizerId -> SynchronizerIndex.forRepairUpdate(someRepairIndex1), - someSynchronizerId -> SynchronizerIndex.forFloatingUpdate(someRecordTime2), - someSynchronizerId2 -> SynchronizerIndex.forSequencedUpdate(someSequencerIndex1), - someSynchronizerId2 -> SynchronizerIndex.forRepairUpdate(someRepairIndex2), - someSynchronizerId3 -> SynchronizerIndex.forSequencedUpdate(someSequencerIndex1), - someSynchronizerId3 -> SynchronizerIndex.forRepairUpdate( - RepairIndex(someSequencerIndex1, RepairCounter.Genesis) - ), - ) - ) shouldBe Map( - someSynchronizerId -> SynchronizerIndex( - Some(someRepairIndex1), - Some(someSequencerIndex1), - someRecordTime2, - ), - someSynchronizerId2 -> SynchronizerIndex( - Some(someRepairIndex2), - Some(someSequencerIndex1), - someRepairIndex2.timestamp, - ), - someSynchronizerId3 -> SynchronizerIndex( - Some(RepairIndex(someSequencerIndex1, RepairCounter.Genesis)), - Some(someSequencerIndex1), - someSequencerIndex1, - ), - ) - } - - behavior of "aggregateLedgerEndForRepair" - - private val someAggregatedLedgerEndForRepair - : Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])] = - Some( - ParameterStorageBackend.LedgerEnd( - lastOffset = offset(5), - lastEventSeqId = 2000, - lastStringInterningId = 300, - lastPublicationTime = CantonTimestamp.ofEpochMicro(5), - ) -> Map( - someSynchronizerId -> SynchronizerIndex( - None, - Some(CantonTimestamp.ofEpochMicro(5)), - CantonTimestamp.ofEpochMicro(5), - ), - someSynchronizerId2 -> SynchronizerIndex( - Some(someRepairIndex2), - Some(CantonTimestamp.ofEpochMicro(4)), - CantonTimestamp.ofEpochMicro(4), - ), - ) - ) - - private val someBatchOfBatches: Vector[Batch[Unit]] = Vector( - Batch( - ledgerEnd = LedgerEnd( - lastOffset = offset(10), - lastEventSeqId = 2010, - lastStringInterningId = 310, - lastPublicationTime = CantonTimestamp.ofEpochMicro(15), - ), - batchTraceContext = TraceContext.empty, - batch = (), - batchSize = 0, - offsetsUpdates = Vector( - offset(9) -> - Update.SequencerIndexMoved( - synchronizerId = someSynchronizerId, - recordTime = someSequencerIndex1, - ), - offset(10) -> - Update.SequencerIndexMoved( - synchronizerId = someSynchronizerId2, - recordTime = someSequencerIndex1, - ), - ), - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - Batch( - ledgerEnd = LedgerEnd( - lastOffset = offset(20), - lastEventSeqId = 2020, - lastStringInterningId = 320, - lastPublicationTime = CantonTimestamp.ofEpochMicro(25), - ), - batchTraceContext = TraceContext.empty, - batch = (), - batchSize = 0, - offsetsUpdates = Vector( - offset(19) -> - Update.SequencerIndexMoved( - synchronizerId = someSynchronizerId, - recordTime = someSequencerIndex2, - ), - offset(20) -> - Update.SequencerIndexMoved( - synchronizerId = someSynchronizerId2, - recordTime = someSequencerIndex2, - ), - ), - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ), - ) - - it should "correctly aggregate if batch has no new synchronizer-indexes" in { - val aggregateLedgerEndForRepairRef = - new AtomicReference[Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])]]( - someAggregatedLedgerEndForRepair - ) - ParallelIndexerSubscription - .aggregateLedgerEndForRepair(aggregateLedgerEndForRepairRef) - .apply(Vector.empty) - aggregateLedgerEndForRepairRef.get() shouldBe someAggregatedLedgerEndForRepair - } - - it should "correctly aggregate if old state is empty" in { - val aggregateLedgerEndForRepairRef = - new AtomicReference[Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])]](None) - ParallelIndexerSubscription - .aggregateLedgerEndForRepair(aggregateLedgerEndForRepairRef) - .apply(someBatchOfBatches) - aggregateLedgerEndForRepairRef.get() shouldBe - Some( - ParameterStorageBackend.LedgerEnd( - lastOffset = offset(20), - lastEventSeqId = 2020, - lastStringInterningId = 320, - lastPublicationTime = CantonTimestamp.ofEpochMicro(25), - ) -> Map( - someSynchronizerId -> SynchronizerIndex( - None, - Some(someSequencerIndex2), - someSequencerIndex2, - ), - someSynchronizerId2 -> SynchronizerIndex( - None, - Some(someSequencerIndex2), - someSequencerIndex2, - ), - ) - ) - } - - it should "correctly aggregate old and new ledger-end and synchronizer indexes" in { - val aggregateLedgerEndForRepairRef = - new AtomicReference[Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])]]( - someAggregatedLedgerEndForRepair - ) - ParallelIndexerSubscription - .aggregateLedgerEndForRepair(aggregateLedgerEndForRepairRef) - .apply(someBatchOfBatches) - aggregateLedgerEndForRepairRef.get() shouldBe - Some( - ParameterStorageBackend.LedgerEnd( - lastOffset = offset(20), - lastEventSeqId = 2020, - lastStringInterningId = 320, - lastPublicationTime = CantonTimestamp.ofEpochMicro(25), - ) -> Map( - someSynchronizerId -> SynchronizerIndex( - None, - Some(someSequencerIndex2), - someSequencerIndex2, - ), - someSynchronizerId2 -> SynchronizerIndex( - Some(someRepairIndex2), - Some(someSequencerIndex2), - someSequencerIndex2, - ), - ) - ) - } - - behavior of "commitRepair" - - def toBatch(offsetsUpdates: Vector[(Offset, Update)]) = Batch( - ledgerEnd = ZeroLedgerEnd, - batchTraceContext = TraceContext.empty, - batch = (), - batchSize = 0, - offsetsUpdates = offsetsUpdates, - missingDeactivatedActivations = Map.empty, - eventCount = 0L, - distinctRawStrings = Nil, - usedInternalContractIds = Set.empty, - ) - - it should "trigger storing ledger-end on CommitRepair" in { - val ledgerEndStoredPromise = Promise[Unit]() - val processingEndStoredPromise = Promise[Unit]() - val updateInMemoryStatePromise = Promise[Unit]() - val aggregatedLedgerEnd = - new AtomicReference[Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])]]( - Some( - LedgerEnd( - lastOffset = offset(1), - lastEventSeqId = 1, - lastStringInterningId = 1, - lastPublicationTime = CantonTimestamp.MinValue, - ) - -> Map.empty - ) - ) - val input = toBatch( - Vector( - offset(13) -> update, - offset(14) -> update, - offset(15) -> Update.CommitRepair(), - ) - ) - ParallelIndexerSubscription - .commitRepair( - storeLedgerEnd = (_, _) => { - ledgerEndStoredPromise.success(()) - Future.unit - }, - storePostProcessingEnd = _ => { - processingEndStoredPromise.success(()) - Future.unit - }, - updateInMemoryState = _ => updateInMemoryStatePromise.success(()), - aggregatedLedgerEnd = aggregatedLedgerEnd, - logger = loggerFactory.getTracedLogger(this.getClass), - executionContext = executionContext, - )(implicitly)(input) - .futureValue shouldBe input - ledgerEndStoredPromise.future.isCompleted shouldBe true - processingEndStoredPromise.future.isCompleted shouldBe true - updateInMemoryStatePromise.future.isCompleted shouldBe true - } - - it should "not trigger storing ledger-end on non CommitRepair Updates" in { - val ledgerEndStoredPromise = Promise[Unit]() - val processingEndStoredPromise = Promise[Unit]() - val updateInMemoryStatePromise = Promise[Unit]() - val aggregatedLedgerEnd = - new AtomicReference[Option[(LedgerEnd, Map[SynchronizerId, SynchronizerIndex])]]( - Some( - LedgerEnd( - lastOffset = offset(1), - lastEventSeqId = 1, - lastStringInterningId = 1, - lastPublicationTime = CantonTimestamp.MinValue, - ) -> Map.empty - ) - ) - val input = toBatch( - Vector( - offset(13) -> update, - offset(14) -> update, - ) - ) - ParallelIndexerSubscription - .commitRepair( - storeLedgerEnd = (_, _) => { - ledgerEndStoredPromise.success(()) - Future.unit - }, - storePostProcessingEnd = _ => { - processingEndStoredPromise.success(()) - Future.unit - }, - updateInMemoryState = _ => updateInMemoryStatePromise.success(()), - aggregatedLedgerEnd = aggregatedLedgerEnd, - logger = loggerFactory.getTracedLogger(this.getClass), - executionContext = executionContext, - )(implicitly)(input) - .futureValue shouldBe input - ledgerEndStoredPromise.future.isCompleted shouldBe false - processingEndStoredPromise.future.isCompleted shouldBe false - updateInMemoryStatePromise.future.isCompleted shouldBe false - } - - behavior of "monotonicOffsetValidator" - - it should "throw if offsets are not in a strictly increasing order" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.Epoch, - ), - offset(3L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.ofEpochSecond(1), - ), - offset(2L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.ofEpochSecond(2), - ), - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => Future.successful(None), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe) - - testSink.request(3) - testSink.expectNextN(offsetsUpdates.take(2)) - - throw testSink.expectError() - }, - _.getMessage shouldBe "Monotonic Offset violation detected from Offset(3) to Offset(2)", - ) - - it should "throw if offsets are not in a strictly increasing compared to the initial offset" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.Epoch, - ) - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = Some(Offset.tryFromLong(2)), - loadPreviousState = _ => Future.successful(None), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe) - - testSink.request(1) - throw testSink.expectError() - }, - _.getMessage shouldBe "Monotonic Offset violation detected from Offset(2) to Offset(1)", - ) - - it should "throw if sequenced timestamps decrease" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.ofEpochSecond(1), - ), - offset(2L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.Epoch, - ), - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => Future.successful(None), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(2) - testSink.expectNextN(offsetsUpdates.take(1)) - - throw testSink.expectError() - }, - _.getMessage should include regex raw"Monotonicity violation detected: record time decreases from .* to .* at offset Offset\(2\)", - ) - - it should "throw if sequenced timestamps decrease compared to clean synchronizer index" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.ofEpochSecond(1), - ) - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => - Future.successful( - Some( - SynchronizerIndex.forSequencedUpdate( - CantonTimestamp.ofEpochSecond(10) - ) - ) - ), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(1) - throw testSink.expectError() - }, - _.getMessage should include regex raw"Monotonicity violation detected: record time decreases from .* to .* at offset Offset\(1\)", - ) - - it should "throw if sequenced timestamps not increasing" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.ofEpochSecond(1), - ), - offset(2L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.ofEpochSecond(1), - ), - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => Future.successful(None), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(2) - testSink.expectNextN(offsetsUpdates.take(1)) - - throw testSink.expectError() - }, - _.getMessage should include regex raw"Monotonicity violation detected: sequencer timestamp did not increase from .* to .* at offset Offset\(2\)", - ) - - it should "throw if sequenced timestamps not increasing compared to clean synchronizer index" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.ofEpochSecond(1), - ) - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => - Future.successful( - Some( - SynchronizerIndex.forSequencedUpdate( - CantonTimestamp.ofEpochSecond(1) - ) - ) - ), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(1) - throw testSink.expectError() - }, - _.getMessage should include regex raw"Monotonicity violation detected: sequencer timestamp did not increase from .* to .* at offset Offset\(1\)", - ) - - it should "throw if repair counters decrease" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> repairUpdate(CantonTimestamp.Epoch, RepairCounter(15L)), - offset(2L) -> repairUpdate(CantonTimestamp.Epoch, RepairCounter(13L)), - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => Future.successful(None), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(2) - testSink.expectNextN(offsetsUpdates.take(1)) - - throw testSink.expectError() - }, - _.getMessage should include regex - raw"Monotonicity violation detected: repair index did not increase from .* to .* at offset Offset\(2\)", - ) - - it should "throw if repair counters are the same" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> repairUpdate(CantonTimestamp.Epoch, RepairCounter(15L)), - offset(2L) -> repairUpdate(CantonTimestamp.Epoch, RepairCounter(15L)), - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => Future.successful(None), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(2) - testSink.expectNextN(offsetsUpdates.take(1)) - - throw testSink.expectError() - }, - _.getMessage should include regex - raw"Monotonicity violation detected: repair index did not increase from .* to .* at offset Offset\(2\)", - ) - - it should "throw if repair counters decrease compared to clean synchronizer index" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> repairUpdate(CantonTimestamp.ofEpochSecond(10), RepairCounter(15L)) - ) - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => - Future.successful( - Some( - SynchronizerIndex.forRepairUpdate( - RepairIndex( - counter = RepairCounter(20L), - timestamp = CantonTimestamp.ofEpochSecond(10), - ) - ) - ) - ), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(1) - throw testSink.expectError() - }, - _.getMessage should include regex - raw"Monotonicity violation detected: repair index did not increase from .* to .* at offset Offset\(1\)", - ) - - it should "throw if repair counters are the same compared to clean synchronizer index" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> repairUpdate(CantonTimestamp.ofEpochSecond(10), RepairCounter(15L)) - ) - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => - Future.successful( - Some( - SynchronizerIndex.forRepairUpdate( - RepairIndex( - counter = RepairCounter(15L), - timestamp = CantonTimestamp.ofEpochSecond(10), - ) - ) - ) - ), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(1) - throw testSink.expectError() - }, - _.getMessage should include regex - raw"Monotonicity violation detected: repair index did not increase from .* to .* at offset Offset\(1\)", - ) - - it should "throw if record time decreases for floating events" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> floatingUpdate(CantonTimestamp.ofEpochSecond(10)), - offset(2L) -> floatingUpdate(CantonTimestamp.ofEpochSecond(10)), - offset(3L) -> floatingUpdate(CantonTimestamp.Epoch), - ) - - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => Future.successful(None), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(3) - testSink.expectNextN(offsetsUpdates.take(2)) - - throw testSink.expectError() - }, - _.getMessage should include regex - raw"Monotonicity violation detected: record time decreases from .* to .* at offset Offset\(3\)", - ) - - it should "throw if record time decreases for floating events compared to clean synchronizer index" in - loggerFactory.assertInternalError[IllegalStateException]( - { - val offsetsUpdates: Vector[(Offset, Update)] = Vector( - offset(1L) -> floatingUpdate(CantonTimestamp.ofEpochSecond(10)) - ) - val testSink = Source(offsetsUpdates) - .via( - ParallelIndexerSubscription.monotonicityValidator( - initialOffset = None, - loadPreviousState = _ => - Future.successful( - Some( - SynchronizerIndex.forFloatingUpdate( - CantonTimestamp.ofEpochSecond(11) - ) - ) - ), - executionContext = executionContext, - )(logger) - ) - .runWith(TestSink.probe[(Offset, Update)]) - - testSink.request(1) - throw testSink.expectError() - }, - _.getMessage should include regex - raw"Monotonicity violation detected: record time decreases from .* to .* at offset Offset\(1\)", - ) - - def update: Update = - Update.SequencerIndexMoved( - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - recordTime = CantonTimestamp.now(), - ) - - def repairUpdate(recordTime: CantonTimestamp, repairCounter: RepairCounter): Update = - RepairTransactionAccepted( - transactionMeta = TransactionMeta( - ledgerEffectiveTime = Time.Timestamp.assertFromLong(2), - workflowId = None, - preparationTime = Time.Timestamp.assertFromLong(3), - submissionSeed = crypto.Hash.assertFromString( - "01cf85cfeb36d628ca2e6f583fa2331be029b6b28e877e1008fb3f862306c086" - ), - timeBoundaries = LedgerTimeBoundaries.unconstrained, - optUsedPackages = None, - optNodeSeeds = None, - optByKeyNodes = None, - ), - transactionInfo = - TransactionAccepted.TransactionInfo(CommittedTransaction(TransactionBuilder.Empty)), - updateId = TestUpdateId("15000"), - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - repairCounter = repairCounter, - recordTime = recordTime, - contractInfos = Map.empty, - )(TraceContext.empty) - - def floatingUpdate(recordTime: CantonTimestamp): Update = - TopologyTransactionEffective( - updateId = TestUpdateId("16000"), - events = Set.empty, - synchronizerId = SynchronizerId.tryFromString("x::synchronizer"), - effectiveTime = recordTime, - )(TraceContext.empty) - - behavior of "reInsertContracts" - - it should "return empty for empty input" in { - ParallelIndexerSubscription - .reInsertContracts( - ledgerApiContractStore = MockLedgerApiContractStore( - expectedLookup = Set.empty, - lookupResult = Map.empty, - expectedStore = Set.empty, - storeResult = Map.empty, - ), - executionContext = parallelExecutionContext, - logger = logger, - )(traceContext)(List()) - .futureValue shouldBe List() - } - - it should "return the same if all contracts found with the same ID" in { - val c1 = contract - val c2 = contract - val c3 = contract - val c4 = contract - val c5 = contract - val updatesFixture = - List( - offset(1) -> sequencedTransaction( - Map( - 1L -> c1, - 2L -> c2, - ) - ), - offset(2) -> repairTransaction( - Map( - 2L -> c2, - 3L -> c3, - ) - ), - offset(3) -> sequencedReassignment( - Map( - 3L -> c3, - 4L -> c4, - ) - ), - offset(4) -> repairReassignment( - Map( - 4L -> c4, - 5L -> c5, - ) - ), - offset(5) -> onPrReassignment( - Map( - 5L -> c5, - 1L -> c1, - ) - ), - ) - ParallelIndexerSubscription - .reInsertContracts( - ledgerApiContractStore = MockLedgerApiContractStore( - expectedLookup = Set( - c1.contractId, - c2.contractId, - c3.contractId, - c4.contractId, - c5.contractId, - ), - lookupResult = Map( - c1.contractId -> 1L, - c2.contractId -> 2L, - c3.contractId -> 3L, - c4.contractId -> 4L, - c5.contractId -> 5L, - ), - expectedStore = Set.empty, - storeResult = Map.empty, - ), - executionContext = parallelExecutionContext, - logger = logger, - )(traceContext)(updatesFixture) - .futureValue shouldBe updatesFixture - } - - it should "replace internal contract ID if changed" in { - val c1 = contract - val c2 = contract - val c3 = contract - val c4 = contract - val c5 = contract - val c6 = contract - ParallelIndexerSubscription - .reInsertContracts( - ledgerApiContractStore = MockLedgerApiContractStore( - expectedLookup = Set( - c1.contractId, - c2.contractId, - c3.contractId, - c4.contractId, - c5.contractId, - c6.contractId, - ), - lookupResult = Map( - c1.contractId -> 1L, - c2.contractId -> 2L, - c3.contractId -> 3L, - c4.contractId -> 4L, - c5.contractId -> 5L, - c6.contractId -> 16L, - ), - expectedStore = Set.empty, - storeResult = Map.empty, - ), - executionContext = parallelExecutionContext, - logger = logger, - )(traceContext)( - List( - offset(1) -> sequencedTransaction( - Map( - 1L -> c1, - 2L -> c2, - 6L -> c6, - ) - ), - offset(2) -> repairTransaction( - Map( - 2L -> c2, - 3L -> c3, - 6L -> c6, - ) - ), - offset(3) -> sequencedReassignment( - Map( - 3L -> c3, - 4L -> c4, - 6L -> c6, - ) - ), - offset(4) -> repairReassignment( - Map( - 4L -> c4, - 5L -> c5, - 6L -> c6, - ) - ), - offset(5) -> onPrReassignment( - Map( - 5L -> c5, - 1L -> c1, - 6L -> c6, - ) - ), - ) - ) - .futureValue shouldBe List( - offset(1) -> sequencedTransaction( - Map( - 1L -> c1, - 2L -> c2, - 16L -> c6, - ) - ), - offset(2) -> repairTransaction( - Map( - 2L -> c2, - 3L -> c3, - 16L -> c6, - ) - ), - offset(3) -> sequencedReassignment( - Map( - 3L -> c3, - 4L -> c4, - 16L -> c6, - ) - ), - offset(4) -> repairReassignment( - Map( - 4L -> c4, - 5L -> c5, - 16L -> c6, - ) - ), - offset(5) -> onPrReassignment( - Map( - 5L -> c5, - 1L -> c1, - 16L -> c6, - ) - ), - ) - } - - it should "store missing contract and replace internal contract ID" in { - val c1 = contract - val c2 = contract - val c3 = contract - val c4 = contract - val c5 = contract - val c6 = contract - ParallelIndexerSubscription - .reInsertContracts( - ledgerApiContractStore = MockLedgerApiContractStore( - expectedLookup = Set( - c1.contractId, - c2.contractId, - c3.contractId, - c4.contractId, - c5.contractId, - c6.contractId, - ), - lookupResult = Map( - c1.contractId -> 1L, - c2.contractId -> 2L, - c3.contractId -> 3L, - c4.contractId -> 4L, - c5.contractId -> 5L, - ), - expectedStore = Set(c6.contractId), - storeResult = Map(c6.contractId -> 10L), - ), - executionContext = parallelExecutionContext, - logger = logger, - )(traceContext)( - List( - offset(1) -> sequencedTransaction( - Map( - 1L -> c1, - 2L -> c2, - 6L -> c6, - ) - ), - offset(2) -> repairTransaction( - Map( - 2L -> c2, - 3L -> c3, - 6L -> c6, - ) - ), - offset(3) -> sequencedReassignment( - Map( - 3L -> c3, - 4L -> c4, - 6L -> c6, - ) - ), - offset(4) -> repairReassignment( - Map( - 4L -> c4, - 5L -> c5, - 6L -> c6, - ) - ), - offset(5) -> onPrReassignment( - Map( - 5L -> c5, - 1L -> c1, - 6L -> c6, - ) - ), - ) - ) - .futureValue shouldBe List( - offset(1) -> sequencedTransaction( - Map( - 1L -> c1, - 2L -> c2, - 10L -> c6, - ) - ), - offset(2) -> repairTransaction( - Map( - 2L -> c2, - 3L -> c3, - 10L -> c6, - ) - ), - offset(3) -> sequencedReassignment( - Map( - 3L -> c3, - 4L -> c4, - 10L -> c6, - ) - ), - offset(4) -> repairReassignment( - Map( - 4L -> c4, - 5L -> c5, - 10L -> c6, - ) - ), - offset(5) -> onPrReassignment( - Map( - 5L -> c5, - 1L -> c1, - 10L -> c6, - ) - ), - ) - } - - it should "work properly for a combined case with multiple replace / store-replace" in { - val c1 = contract - val c2 = contract - val c3 = contract - val c4 = contract - val c5 = contract - val c6 = contract - val c7 = contract - val c8 = contract - val c9 = contract - val c10 = contract - ParallelIndexerSubscription - .reInsertContracts( - ledgerApiContractStore = MockLedgerApiContractStore( - expectedLookup = Set( - c1.contractId, - c2.contractId, - c3.contractId, - c4.contractId, - c5.contractId, - c6.contractId, - c7.contractId, - c8.contractId, - c9.contractId, - c10.contractId, - ), - lookupResult = Map( - c1.contractId -> 1L, - c2.contractId -> 2L, - c3.contractId -> 3L, - c4.contractId -> 4L, - c5.contractId -> 5L, - c7.contractId -> 17L, - c8.contractId -> 18L, - ), - expectedStore = Set( - c6.contractId, - c9.contractId, - c10.contractId, - ), - storeResult = Map( - c6.contractId -> 11L, - c9.contractId -> 19L, - c10.contractId -> 20L, - ), - ), - executionContext = parallelExecutionContext, - logger = logger, - )(traceContext)( - List( - offset(1) -> sequencedTransaction( - Map( - 1L -> c1, - 2L -> c2, - 6L -> c6, - 7L -> c7, - 8L -> c8, - ) - ), - offset(2) -> repairTransaction( - Map( - 2L -> c2, - 3L -> c3, - 6L -> c6, - 9L -> c9, - ) - ), - offset(3) -> sequencedReassignment( - Map( - 3L -> c3, - 4L -> c4, - 6L -> c6, - ) - ), - offset(4) -> repairReassignment( - Map( - 4L -> c4, - 5L -> c5, - 6L -> c6, - 7L -> c7, - 9L -> c9, - ) - ), - offset(5) -> onPrReassignment( - Map( - 5L -> c5, - 1L -> c1, - 6L -> c6, - 8L -> c8, - 10L -> c10, - ) - ), - ) - ) - .futureValue shouldBe List( - offset(1) -> sequencedTransaction( - Map( - 1L -> c1, - 2L -> c2, - 11L -> c6, - 17L -> c7, - 18L -> c8, - ) - ), - offset(2) -> repairTransaction( - Map( - 2L -> c2, - 3L -> c3, - 11L -> c6, - 19L -> c9, - ) - ), - offset(3) -> sequencedReassignment( - Map( - 3L -> c3, - 4L -> c4, - 11L -> c6, - ) - ), - offset(4) -> repairReassignment( - Map( - 4L -> c4, - 5L -> c5, - 11L -> c6, - 17L -> c7, - 19L -> c9, - ) - ), - offset(5) -> onPrReassignment( - Map( - 5L -> c5, - 1L -> c1, - 11L -> c6, - 18L -> c8, - 20L -> c10, - ) - ), - ) - } - - def contract: ContractInstance = ExampleContractFactory.build() - - val someTransactionMeta: TransactionMeta = state.TransactionMeta( - ledgerEffectiveTime = Time.Timestamp.assertFromLong(2), - workflowId = None, - preparationTime = Time.Timestamp.assertFromLong(3), - submissionSeed = crypto.Hash.assertFromString( - "01cf85cfeb36d628ca2e6f583fa2331be029b6b28e877e1008fb3f862306c086" - ), - timeBoundaries = LedgerTimeBoundaries.unconstrained, - optUsedPackages = None, - optNodeSeeds = None, - optByKeyNodes = None, - ) - - def contractInfos(contracts: Map[Long, ContractInstance]): Map[ContractId, ContractInfo] = - contracts.map { case (internalContractId, contract) => - contract.contractId -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - internalContractId = internalContractId, - inst = contract.inst, - ), - representativePackageId = SameAsContractPackageId, - ) - } - - val transactionInfo: Update.TransactionAccepted.TransactionInfo = { - val builder = new NodeIdTransactionBuilder with TestNodeBuilder - val createNode = contract.inst.toCreateNode - builder.add(createNode) - val transaction = builder.buildCommitted() - Update.TransactionAccepted.TransactionInfo(transaction) - } - - def sequencedTransaction(contracts: Map[Long, ContractInstance]): Update.TransactionAccepted = - state.Update.SequencedTransactionAccepted( - completionInfoO = None, - transactionMeta = someTransactionMeta, - transactionInfo = transactionInfo, - updateId = updateId, - synchronizerId = someSynchronizerId, - recordTime = someRecordTime1, - externalTransactionHash = None, - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = contractInfos(contracts), - ) - - def repairTransaction(contracts: Map[Long, ContractInstance]): Update.TransactionAccepted = - state.Update.RepairTransactionAccepted( - transactionMeta = someTransactionMeta, - transactionInfo = transactionInfo, - updateId = updateId, - synchronizerId = someSynchronizerId, - recordTime = someRecordTime1, - contractInfos = contractInfos(contracts), - repairCounter = RepairCounter.Genesis, - ) - - def reassignmentBatch(contracts: Map[Long, ContractInstance]): Reassignment.Batch = - Reassignment.Batch( - NonEmpty - .from[Seq[Reassignment]]( - contracts.toSeq.sortBy(_._2.contractId.coid).map { case (internalContractId, contract) => - Reassignment.Assign( - reassignmentCounter = 1500L, - nodeId = 0, - persistedContractInstance = PersistedContractInstance( - internalContractId = internalContractId, - inst = contract.inst, - ), - ) - } - ) - .getOrElse(fail("should be non empty")) - ) - - def reassignmentInfo: ReassignmentInfo = ReassignmentInfo( - sourceSynchronizer = ReassignmentTag.Source(someSynchronizerId), - targetSynchronizer = ReassignmentTag.Target(someSynchronizerId2), - submitter = None, - reassignmentId = ReassignmentId.tryCreate("001000000000"), - isReassigningParticipant = true, - ) - - def sequencedReassignment(contracts: Map[Long, ContractInstance]): Update.ReassignmentAccepted = - Update.SequencedReassignmentAccepted( - optCompletionInfo = None, - workflowId = None, - updateId = updateId, - reassignmentInfo = reassignmentInfo, - reassignment = reassignmentBatch(contracts), - recordTime = someRecordTime1, - synchronizerId = someSynchronizerId, - acsChangeFactory = TestAcsChangeFactory(), - ) - - def repairReassignment(contracts: Map[Long, ContractInstance]): Update.ReassignmentAccepted = - Update.RepairReassignmentAccepted( - workflowId = None, - updateId = updateId, - reassignmentInfo = reassignmentInfo, - reassignment = reassignmentBatch(contracts), - recordTime = someRecordTime1, - synchronizerId = someSynchronizerId, - repairCounter = RepairCounter.Genesis, - ) - - def onPrReassignment(contracts: Map[Long, ContractInstance]): Update.ReassignmentAccepted = - Update.OnPRReassignmentAccepted( - workflowId = None, - updateId = updateId, - reassignmentInfo = reassignmentInfo, - reassignment = reassignmentBatch(contracts), - recordTime = someRecordTime1, - synchronizerId = someSynchronizerId, - repairCounter = RepairCounter.Genesis, - acsChangeFactory = TestAcsChangeFactory(), - ) - - case class MockLedgerApiContractStore( - expectedLookup: Set[LfContractId], - lookupResult: Map[LfContractId, Long], - expectedStore: Set[LfContractId], - storeResult: Map[LfContractId, Long], - ) extends LedgerApiContractStore { - override def lookupPersisted(id: LfContractId)(implicit - traceContext: TraceContext - ): Future[Option[PersistedContractInstance]] = - fail("should not be used") - - override def lookupBatchedNonReadThrough(internalContractIds: Iterable[Long])(implicit - traceContext: TraceContext - ): Future[Map[Long, PersistedContractInstance]] = - fail("should not be used") - - override def lookupBatchedInternalIdsNonReadThrough( - contractIds: Iterable[LfContractId] - )(implicit traceContext: TraceContext): Future[Map[LfContractId, Long]] = - Future { - contractIds.toSet shouldBe expectedLookup - lookupResult - } - - override def lookupBatchedContractIdsNonReadThrough(internalContractIds: Iterable[Long])( - implicit traceContext: TraceContext - ): Future[Map[Long, LfContractId]] = - fail("should not be used") - - override def storeContracts( - contracts: Seq[ContractInstance] - )(implicit traceContext: TraceContext): Future[Map[LfContractId, Long]] = - Future { - contracts.map(_.contractId).toSet shouldBe expectedStore - storeResult - } - - override def contractsPruned(internalContractIds: Iterable[Long]): Unit = - fail("should not be used") - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/PostPublishDataSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/PostPublishDataSpec.scala deleted file mode 100644 index 90354331cb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/indexer/parallel/PostPublishDataSpec.scala +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.indexer.parallel - -import com.digitalasset.canton.RepairCounter -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.{CantonTimestamp, LedgerTimeBoundaries, Offset} -import com.digitalasset.canton.ledger.participant.state.Update.CommandRejected.FinalReason -import com.digitalasset.canton.ledger.participant.state.Update.{ - RepairTransactionAccepted, - SequencedCommandRejected, - SequencedTransactionAccepted, - TransactionAccepted, - UnSequencedCommandRejected, -} -import com.digitalasset.canton.ledger.participant.state.{ - CompletionInfo, - TestAcsChangeFactory, - TransactionMeta, -} -import com.digitalasset.canton.logging.{NamedLogging, SuppressingLogger} -import com.digitalasset.canton.protocol.TestUpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.{Ref, Time} -import com.digitalasset.daml.lf.transaction.CommittedTransaction -import com.digitalasset.daml.lf.transaction.test.TransactionBuilder -import io.grpc.Status -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.UUID - -class PostPublishDataSpec extends AnyFlatSpec with Matchers with NamedLogging { - override val loggerFactory: SuppressingLogger = SuppressingLogger(getClass) - - private val synchronizerId = SynchronizerId.tryFromString("x::synchronizer1") - private val party = Ref.Party.assertFromString("party") - private val userId = Ref.UserId.assertFromString("userid1") - private val cantonTime1 = CantonTimestamp.now() - private val cantonTime2 = CantonTimestamp.now() - private val commandId = Ref.CommandId.assertFromString(UUID.randomUUID().toString) - private val paidTrafficCost = NonNegativeLong.tryCreate(465) - private val offset = Offset.tryFromLong(15) - private val submissionId = Some(Ref.SubmissionId.assertFromString(UUID.randomUUID().toString)) - private val updateId = TestUpdateId("15000") - private val someHash = - crypto.Hash.assertFromString("01cf85cfeb36d628ca2e6f583fa2331be029b6b28e877e1008fb3f862306c086") - private val transactionMeta = TransactionMeta( - ledgerEffectiveTime = Time.Timestamp.assertFromLong(2), - workflowId = None, - preparationTime = Time.Timestamp.assertFromLong(3), - submissionSeed = someHash, - timeBoundaries = LedgerTimeBoundaries.unconstrained, - optUsedPackages = None, - optNodeSeeds = None, - optByKeyNodes = None, - ) - private val status = - com.google.rpc.status.Status.of(Status.Code.ABORTED.value(), "test reason", Seq.empty) - private val messageUuid = UUID.randomUUID() - - behavior of "from" - - it should "populate post PostPublishData correctly for sequenced TransactionAccepted" in { - val update = SequencedTransactionAccepted( - completionInfoO = Some( - CompletionInfo( - actAs = List(party), - userId = userId, - commandId = commandId, - optDeduplicationPeriod = None, - submissionId = submissionId, - paidTrafficCost = paidTrafficCost, - ) - ), - transactionMeta = transactionMeta, - transactionInfo = - TransactionAccepted.TransactionInfo(CommittedTransaction(TransactionBuilder.Empty)), - updateId = updateId, - synchronizerId = synchronizerId, - recordTime = cantonTime2, - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map.empty, - )(TraceContext.empty) - - PostPublishData.from( - update = update, - offset = offset, - publicationTime = cantonTime1, - ) shouldBe Some( - PostPublishData( - submissionSynchronizerId = synchronizerId, - publishSource = PublishSource.Sequencer( - sequencerTimestamp = cantonTime2 - ), - userId = userId, - commandId = commandId, - actAs = Set(party), - offset = offset, - publicationTime = cantonTime1, - submissionId = submissionId, - accepted = true, - traceContext = TraceContext.empty, - ) - ) - - PostPublishData.from( - update = update.copy(completionInfoO = None)(TraceContext.empty), - offset = offset, - publicationTime = cantonTime1, - ) shouldBe None - } - - it should "populate no post PostPublishData for repair transactions" in { - PostPublishData.from( - update = RepairTransactionAccepted( - transactionMeta = transactionMeta, - transactionInfo = - TransactionAccepted.TransactionInfo(CommittedTransaction(TransactionBuilder.Empty)), - updateId = updateId, - synchronizerId = synchronizerId, - repairCounter = RepairCounter(65), - recordTime = cantonTime2, - contractInfos = Map.empty, - )(TraceContext.empty), - offset = offset, - publicationTime = cantonTime1, - ) shouldBe None - } - - it should "populate post PostPublishData correctly for SequencedCommandRejected" in { - val update = SequencedCommandRejected( - completionInfo = CompletionInfo( - actAs = List(party), - userId = userId, - commandId = commandId, - optDeduplicationPeriod = None, - submissionId = submissionId, - paidTrafficCost = NonNegativeLong.zero, - ), - reasonTemplate = FinalReason(status), - synchronizerId = synchronizerId, - recordTime = cantonTime2, - isTransaction = true, - )(TraceContext.empty) - - PostPublishData.from( - update = update, - offset = offset, - publicationTime = cantonTime1, - ) shouldBe Some( - PostPublishData( - submissionSynchronizerId = synchronizerId, - publishSource = PublishSource.Sequencer( - sequencerTimestamp = cantonTime2 - ), - userId = userId, - commandId = commandId, - actAs = Set(party), - offset = offset, - publicationTime = cantonTime1, - submissionId = submissionId, - accepted = false, - traceContext = TraceContext.empty, - ) - ) - - PostPublishData.from( - update = update.copy(isTransaction = false)(TraceContext.empty), - offset = offset, - publicationTime = cantonTime1, - ) shouldBe None - } - - it should "populate post PostPublishData correctly for UnSequencedCommandRejected" in { - val update = UnSequencedCommandRejected( - completionInfo = CompletionInfo( - actAs = List(party), - userId = userId, - commandId = commandId, - optDeduplicationPeriod = None, - submissionId = submissionId, - paidTrafficCost = paidTrafficCost, - ), - reasonTemplate = FinalReason(status), - synchronizerId = synchronizerId, - recordTime = cantonTime2, - messageUuid = messageUuid, - isTransaction = true, - )(TraceContext.empty) - - PostPublishData.from( - update = update, - offset = offset, - publicationTime = cantonTime1, - ) shouldBe Some( - PostPublishData( - submissionSynchronizerId = synchronizerId, - publishSource = PublishSource.Local(messageUuid), - userId = userId, - commandId = commandId, - actAs = Set(party), - offset = offset, - publicationTime = cantonTime1, - submissionId = submissionId, - accepted = false, - traceContext = TraceContext.empty, - ) - ) - PostPublishData.from( - update = update.copy(isTransaction = false)(TraceContext.empty), - offset = offset, - publicationTime = cantonTime1, - ) shouldBe None - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/multisynchronizer/MultiSynchronizerIndexComponentTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/multisynchronizer/MultiSynchronizerIndexComponentTest.scala deleted file mode 100644 index 35ebfe0c8b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/multisynchronizer/MultiSynchronizerIndexComponentTest.scala +++ /dev/null @@ -1,412 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.multisynchronizer - -import cats.syntax.traverse.* -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.digitalasset.canton.RepairCounter -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.ledger.api.messages.state.{AcsContinuationToken, AcsRangeInfo} -import com.digitalasset.canton.ledger.api.{CumulativeFilter, EventFormat} -import com.digitalasset.canton.ledger.participant.state.{ - Reassignment, - ReassignmentInfo, - TestAcsChangeFactory, - Update, -} -import com.digitalasset.canton.logging.LogEntry -import com.digitalasset.canton.platform.Party -import com.digitalasset.canton.platform.component.IndexComponentTest -import com.digitalasset.canton.protocol.{ - ContractInstance, - ExampleContractFactory, - ReassignmentId, - TestUpdateId, -} -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import com.digitalasset.daml.lf.data.{ImmArray, Ref, Time} -import com.digitalasset.daml.lf.value.Value -import com.google.protobuf.ByteString -import org.apache.pekko.stream.scaladsl.Sink -import org.scalatest.Assertion -import org.scalatest.flatspec.AnyFlatSpec - -import scala.collection.mutable - -class MultiSynchronizerIndexComponentTest extends AnyFlatSpec with IndexComponentTest { - behavior of "MultiSynchronizer contract lookup" - - private val sequentiallyPostProcessedUpdates = mutable.Buffer[Update]() - - override protected def sequentialPostProcessor: Update => Unit = - sequentiallyPostProcessedUpdates.append - - val templateId = Ref.Identifier.assertFromString("P:M:T") - - it should "successfully look up contract, even if only the assigned event is visible" in { - val party = Ref.Party.assertFromString("party1") - - val c1 = createContract(party) - val c2 = createContract(party) - val cn1 = c1 - val reassignmentAccepted1 = - mkReassignmentAccepted( - party, - "UpdateId1", - contracts = Seq(cn1), - withAcsChange = false, - ) - val cn2 = c2 - val reassignmentAccepted2 = - mkReassignmentAccepted( - party, - "UpdateId2", - contracts = Seq(cn2), - withAcsChange = true, - ) - ingestUpdates(reassignmentAccepted1 -> Vector(c1), reassignmentAccepted2 -> Vector(c2)) - - (for { - activeContractO1 <- index.lookupActiveContract(Set(party), cn1.contractId) - activeContractO2 <- index.lookupActiveContract(Set(party), cn2.contractId) - } yield { - Seq(cn1 -> activeContractO1, cn2 -> activeContractO2).foreach { case (cn, activeContractO) => - activeContractO.map(_.createArg) shouldBe Some(cn.inst.createArg) - activeContractO.map(_.templateId) shouldBe Some(cn.templateId) - } - }).futureValue - - // Verify that the AcsChanges have been propagated to the sequential post-processor. - sequentiallyPostProcessedUpdates.count { - case _: Update.OnPRReassignmentAccepted => true - case _ => false - } shouldBe 1 - sequentiallyPostProcessedUpdates.count { - case _: Update.RepairReassignmentAccepted => true - case _ => false - } shouldBe 1 - } - - def setupEvents(party: Party) = { - val (createC1, contracts1) = mkTransaction(createContract(party)) - val (createC2, contracts2) = mkTransaction(createContract(party)) - val incompleteUnassignC1 = - mkReassignmentWithUnassign(party, "incompleteUnassignC1", contracts1.map(_.contractId)) - val (_, contracts3) = mkTransaction(createContract(party)) - val incompleteAssignC3 = - mkReassignmentAccepted(party, "incompleteAssignC3", true, contracts3) - val (createC4, contracts4) = mkTransaction(createContract(party)) - val (createC5, contracts5) = mkTransaction(createContract(party)) - val incompleteUnassignC4 = - mkReassignmentWithUnassign(party, "incompleteUnassignC4", contracts4.map(_.contractId)) - val (_, contracts6) = mkTransaction(createContract(party)) - val incompleteAssignC6 = - mkReassignmentAccepted(party, "incompleteAssignC3", true, contracts6) - // last round with multiple events for each offset - val (createC7, contracts7) = - mkTransaction(createContract(party), createContract(party), createContract(party)) - val (createC8, contracts8) = - mkTransaction(createContract(party), createContract(party), createContract(party)) - val incompleteUnassignC7 = - mkReassignmentWithUnassign(party, "incompleteUnassignC7", contracts7.map(_.contractId)) - val (_, contracts9) = - mkTransaction(createContract(party), createContract(party), createContract(party)) - val incompleteAssignC9 = - mkReassignmentAccepted(party, "incompleteAssignC9", true, contracts9) - - setupLedgerWithIncompleteOffsets( - createC1 -> contracts1, // temporary activation for unassign - incompleteUnassignC1 -> contracts1, // incomplete unassign - createC2 -> contracts2, // std activation - incompleteAssignC3 -> contracts3, // incomplete assign - createC4 -> contracts4, // temporary activation for unassign - incompleteUnassignC4 -> contracts4, // incomplete unassign - createC5 -> contracts5, // std activation - incompleteAssignC6 -> contracts6, // incomplete assign - createC7 -> contracts7, // temporary activation for unassign - incompleteUnassignC7 -> contracts7, // incomplete unassign - createC8 -> contracts8, // std activation - incompleteAssignC9 -> contracts9, // incomplete assign - ) - } - - it should "support continuation of ACS stream with incomplete reassignments" in { - val party = Ref.Party.assertFromString("party2") - setupEvents(party) - - val eventFormat = EventFormat( - filtersByParty = Map(party -> CumulativeFilter.templateWildcardFilter()), - filtersForAnyParty = None, - verbose = false, - ) - - val allContracts = getAcsF(eventFormat, None).futureValue - allContracts should have length (4 + 4 + 3 * 4) - - val continuationPointers = allContracts.map(_.streamContinuationToken) - for { - i <- continuationPointers.indices - } yield { - val continuation = getAcsF(eventFormat, Some(continuationPointers(i))).futureValue - (allContracts.take(i + 1) ++ continuation) should equal(allContracts) - } - } - - it should "support ACS pagination with incomplete reassignments" in { - val party = Ref.Party.assertFromString("party3") - setupEvents(party) - - val eventFormat = EventFormat( - filtersByParty = Map(party -> CumulativeFilter.templateWildcardFilter()), - filtersForAnyParty = None, - verbose = false, - ) - - val allContracts = getAcsF(eventFormat, None).futureValue - allContracts should have length (4 + 4 + 3 * 4) - - def getAllPages(pageSize: Int) = Vector.unfold(None: Option[ByteString]) { continuationToken => - getAcsF(eventFormat, continuationToken, Some(pageSize)) - .map(page => - if (page.isEmpty) None - else Some(page -> page.lastOption.map(_.streamContinuationToken)) - ) - .futureValue - } - - val pagesWithSize1 = getAllPages(1) - pagesWithSize1 should contain theSameElementsInOrderAs createSlices(1, allContracts) - val pagesWithSize3 = getAllPages(3) - pagesWithSize3 should contain theSameElementsInOrderAs createSlices(3, allContracts) - val pagesWithSize100 = getAllPages(100) - pagesWithSize100 should contain theSameElementsInOrderAs createSlices(100, allContracts) - } - - it should "successfully re-insert contracts if not found" in { - val party = Ref.Party.assertFromString("party9") - val (tx, _) = mkTransaction(createContract(party)) - ingestUpdateSync(tx) - getAcsF( - eventFormat = EventFormat( - filtersByParty = Map(party -> CumulativeFilter.templateWildcardFilter()), - filtersForAnyParty = None, - verbose = false, - ), - continuationToken = None, - limit = None, - ).futureValue.size shouldBe 1 - } - - it should "return full pages if some reassignment events are filtered out" in { - val party4 = Ref.Party.assertFromString("party4") - val party5 = Ref.Party.assertFromString("party5") - val party6 = Ref.Party.assertFromString("party6") - - val updates = (0 until 4).flatMap { _ => - val (createC1, contracts1) = mkTransaction(createContract(party4)) - val incompleteUnassignC1 = - mkReassignmentWithUnassign(party4, "incompleteUnassignC1", contracts1.map(_.contractId)) - val (_, contracts2) = mkTransaction(createContract(party4)) - val incompleteAssignC2 = - mkReassignmentAccepted(party4, "incompleteAssignC2", true, contracts2) - val (createC3, contracts3) = mkTransaction(createContract(party5)) - val incompleteUnassignC3 = - mkReassignmentWithUnassign(party5, "incompleteUnassignC3", contracts3.map(_.contractId)) - val (_, contracts4) = mkTransaction(createContract(party5)) - val incompleteAssignC4 = - mkReassignmentAccepted(party5, "incompleteAssignC4", true, contracts4) - val (createC5, contracts5) = mkTransaction(createContract(party6)) - val incompleteUnassignC5 = - mkReassignmentWithUnassign(party6, "incompleteUnassignC5", contracts5.map(_.contractId)) - val (_, contracts6) = mkTransaction(createContract(party6)) - val incompleteAssignC6 = - mkReassignmentAccepted(party6, "incompleteAssignC6", true, contracts6) - Seq( - createC1 -> contracts1, - incompleteUnassignC1 -> contracts1, - incompleteAssignC2 -> contracts2, - createC3 -> contracts3, - incompleteUnassignC3 -> contracts3, - incompleteAssignC4 -> contracts4, - createC5 -> contracts5, - incompleteUnassignC5 -> contracts5, - incompleteAssignC6 -> contracts6, - ) - } - setupLedgerWithIncompleteOffsets(updates*) - - val eventFormat = EventFormat( - filtersByParty = Map(party4 -> CumulativeFilter.templateWildcardFilter()), - filtersForAnyParty = None, - verbose = false, - ) - - val allContracts = getAcsF(eventFormat, None).futureValue - val pageSize = 12 - val page1 = getAcsF(eventFormat, None, Some(pageSize)).futureValue - page1 should have length pageSize.toLong - page1 should contain theSameElementsInOrderAs allContracts.take(pageSize) - } - - it should "return full pages even if unassignments without CreateEvent found" in suppressLogWarnings { - val party = Ref.Party.assertFromString("party7") - val (_c, missingCreateContracts) = mkTransaction(createContract(party)) - val missingCreateUnassign = mkReassignmentWithUnassign( - party, - "missingCreateUnassign", - missingCreateContracts.map(_.contractId), - ) - val (createC1, contracts1) = mkTransaction(createContract(party)) - val (createC2, contracts2) = mkTransaction(createContract(party)) - val incompleteUnassignC2 = // ez itten lecsokkenti az ACSt. Miert? Nem incomplete? - mkReassignmentWithUnassign(party, "incompleteUnassignC2", contracts2.map(_.contractId)) - setupLedgerWithIncompleteOffsets( - createC1 -> contracts1, - missingCreateUnassign -> missingCreateContracts, - createC2 -> contracts2, - incompleteUnassignC2 -> contracts2, - ) - val eventFormat = EventFormat( - filtersByParty = Map(party -> CumulativeFilter.templateWildcardFilter()), - filtersForAnyParty = None, - verbose = false, - ) - - val allContracts = getAcsF(eventFormat, None).futureValue - val pageSize = 2 - val page1 = getAcsF(eventFormat, None, Some(pageSize)).futureValue - page1 should have length pageSize.toLong - page1 should contain theSameElementsInOrderAs allContracts.take(pageSize) - } - - private def setupLedgerWithIncompleteOffsets(updates: (Update, Vector[ContractInstance])*) = { - val ledgerEndOpt = index.currentLedgerEnd().futureValue - val ledgerEnd = ledgerEndOpt.map(_.increment).getOrElse(Offset.firstOffset) - val incompleteOffsetAcc = updates.map(_._1).foldLeft(ledgerEnd -> Seq[Offset]()) { - case ((currentOffset, acc), _u: Update.ReassignmentAccepted) => - currentOffset.increment -> (acc :+ currentOffset) - case ((currentOffset, acc), _) => (currentOffset.increment, acc) - } - restartIndexer(incompleteOffsets = incompleteOffsetAcc._2) - ingestUpdates(updates*) - } - - private def createSlices(pageSize: Int, list: Vector[GetActiveContractsResponse]) = - Vector.tabulate((list.size + pageSize - 1) / pageSize) { i => - list.slice(i * pageSize, (i + 1) * pageSize) - } - - private def createContract(party: Ref.Party) = ExampleContractFactory.build( - stakeholders = Set(party), - signatories = Set(party), - templateId = templateId, - argument = Value.ValueRecord( - tycon = None, - fields = ImmArray(None -> Value.ValueText("42")), - ), - ) - - private def mkTransaction(contracts: ContractInstance*) = { - val txBuilder = TxBuilder() - contracts.foreach(c => txBuilder.add(c.inst.toCreateNode)) - val txn = - transaction(synchronizer1, recordTime())(txBuilder.buildCommitted(), contracts) - (txn, contracts.toVector) - } - - private def getAcsF( - eventFormat: EventFormat, - continuationToken: Option[ByteString], - limit: Option[Int] = None, - ) = - continuationToken.traverse(token => - AcsContinuationToken.decodeAndValidate(AcsContinuationToken.emptyChecksum, token) - ) match { - case Left(error) => - fail(s"Failed to decode continuation token: ${error.getStatus.getDescription}") - case Right(continuationPointer) => - for { - ledgerEnd <- index.currentLedgerEnd() - responses <- index - .getActiveContracts( - eventFormat, - ledgerEnd, - AcsRangeInfo( - continuationPointer = continuationPointer, - requestChecksum = AcsContinuationToken.emptyChecksum, - limit = limit.map(_.toLong), - ), - ) - .runWith(Sink.collection) - } yield responses.toVector - } - - private def recordTime() = CantonTimestamp(Time.Timestamp.now()) - - private def mkReassignmentWithUnassign( - party: Ref.Party, - updateIdS: String, - contracIds: Seq[Value.ContractId], - ) = { - val updateId = TestUpdateId(updateIdS) - Update.OnPRReassignmentAccepted( - workflowId = None, - updateId = updateId, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = Source(synchronizer1), - targetSynchronizer = Target(synchronizer2), - submitter = Option(party), - reassignmentId = ReassignmentId.tryCreate("00"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Unassign( - contractId = contracIds.head, - templateId = templateId, - packageName = Ref.PackageName.fromInt(5), - stakeholders = Set(party), - assignmentExclusivity = None, - reassignmentCounter = 15L, - nodeId = 0, - ), - contracIds.tail.map(contractId => - Reassignment.Unassign( - contractId = contractId, - templateId = templateId, - packageName = Ref.PackageName.fromInt(5), - stakeholders = Set(party), - assignmentExclusivity = None, - reassignmentCounter = 15L, - nodeId = 0, - ) - )* - ), - repairCounter = RepairCounter.Genesis, - recordTime = recordTime(), - synchronizerId = synchronizer2, - acsChangeFactory = TestAcsChangeFactory(), - ) - } - - private def suppressLogWarnings(testCode: => Assertion): Assertion = - loggerFactory.assertLoggedWarningsAndErrorsSeq( - testCode, - LogEntry.assertLogSeq( - Seq( - ( - _.warningMessage should include( - "Activation is missing for a deactivation for deactivated event with type" - ), - "warning that unassigns were found without corresponding active events", - ), - ( - _.warningMessage should include( - "there is neither CreatedEvent nor AssignedEvent available. This entry will be dropped from the result." - ), - "warning that unassigns were found without corresponding active events", - ), - ) - ), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/packages/DeduplicatingPackageLoaderSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/packages/DeduplicatingPackageLoaderSpec.scala deleted file mode 100644 index adb4d075fd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/packages/DeduplicatingPackageLoaderSpec.scala +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.packages - -import com.daml.ledger.api.testtool.TestDars -import com.daml.metrics.api.noop.NoOpMetricsFactory -import com.daml.metrics.api.{MetricInfo, MetricName, MetricQualification} -import com.daml.testing.utils.TestResourceContext -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.daml.lf.archive.{DamlLf, DarParser} -import com.digitalasset.daml.lf.data.Ref.PackageId -import org.apache.pekko.actor.{ActorSystem, Scheduler} -import org.scalatest.BeforeAndAfterEach -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec - -import java.util.UUID -import java.util.concurrent.atomic.AtomicLong -import scala.concurrent.duration.{DurationInt, FiniteDuration} -import scala.concurrent.{Await, Future} - -class DeduplicatingPackageLoaderSpec - extends AsyncWordSpec - with Matchers - with TestResourceContext - with BeforeAndAfterEach { - - private[this] var actorSystem: ActorSystem = _ - private[this] val loadCount = new AtomicLong() - private[this] val metric = - NoOpMetricsFactory.timer(MetricInfo(MetricName("test-metric"), "", MetricQualification.Debug)) - - private[this] val dar = DarParser.assertReadArchiveFromFile(TestDars.v2_2.SemanticTestDar.file) - - private[this] def delayedLoad(duration: FiniteDuration): Future[Option[DamlLf.Archive]] = { - implicit val scheduler: Scheduler = actorSystem.scheduler - loadCount.incrementAndGet() - org.apache.pekko.pattern.after(duration, scheduler) { - Future.successful(Some(dar.main)) - } - } - - private[this] def delayedFail(duration: FiniteDuration): Future[Option[DamlLf.Archive]] = { - implicit val scheduler: Scheduler = actorSystem.scheduler - loadCount.incrementAndGet() - org.apache.pekko.pattern.after(duration, scheduler) { - Future.failed(new RuntimeException("Simulated package load failure")) - } - } - - private[this] def delayedNotFound(duration: FiniteDuration): Future[Option[DamlLf.Archive]] = { - implicit val scheduler: Scheduler = actorSystem.scheduler - loadCount.incrementAndGet() - org.apache.pekko.pattern.after(duration, scheduler) { - Future.successful(None) - } - } - - override def beforeEach(): Unit = { - super.beforeEach() - actorSystem = ActorSystem(getClass.getSimpleName) - loadCount.set(0) - } - - override def afterEach(): Unit = { - Await.result(actorSystem.terminate(), 10.seconds) - super.afterEach() - } - - "DeduplicatingPackageLoaderSpec" should { - "correctly load two different packages" in { - val loader = new DeduplicatingPackageLoader() - val packageId1 = PackageId.assertFromString(UUID.randomUUID().toString) - val packageId2 = PackageId.assertFromString(UUID.randomUUID().toString) - val future1 = loader.loadPackage(packageId1, _ => delayedLoad(200.millis), metric) - val future2 = loader.loadPackage(packageId2, _ => delayedLoad(200.millis), metric) - for { - result1 <- future1 - result2 <- future2 - } yield { - result1.isDefined shouldBe true - result2.isDefined shouldBe true - - // 2 successful - loadCount.get() shouldBe 2 - } - } - - "deduplicate concurrent package load requests" in { - val loader = new DeduplicatingPackageLoader() - val packageId = PackageId.assertFromString(UUID.randomUUID().toString) - val future1 = loader.loadPackage(packageId, _ => delayedLoad(200.millis), metric) - val future2 = loader.loadPackage(packageId, _ => delayedLoad(200.millis), metric) - for { - result1 <- future1 - result2 <- future2 - } yield { - result1.isDefined shouldBe true - result2.isDefined shouldBe true - - // 1 successful, 1 deduplicated - loadCount.get() shouldBe 1 - } - } - - "deduplicate sequential package load requests" in { - val loader = new DeduplicatingPackageLoader() - val packageId = PackageId.assertFromString(UUID.randomUUID().toString) - for { - result1 <- loader.loadPackage(packageId, _ => delayedLoad(10.millis), metric) - result2 <- loader.loadPackage(packageId, _ => delayedLoad(10.millis), metric) - } yield { - result1.isDefined shouldBe true - result2.isDefined shouldBe true - - // 1 successful, 1 deduplicated - loadCount.get() shouldBe 1 - } - } - - "retry after a failed package load requests" in { - val loader = new DeduplicatingPackageLoader() - val packageId = PackageId.assertFromString(UUID.randomUUID().toString) - for { - _ <- loader.loadPackage(packageId, _ => delayedFail(10.millis), metric).failed - // Wait for a short time so that the package loader can remove the failed load from the cache. - // Without the wait, the second call might get the failed result from above - _ = Threading.sleep(100, 0) - result2 <- loader.loadPackage(packageId, _ => delayedLoad(10.millis), metric) - } yield { - result2.isDefined shouldBe true - - // 1 failed, 1 successful - loadCount.get() shouldBe 2 - } - } - - "retry after a package was not found" in { - val loader = new DeduplicatingPackageLoader() - val packageId = PackageId.assertFromString(UUID.randomUUID().toString) - for { - result1 <- loader.loadPackage(packageId, _ => delayedNotFound(10.millis), metric) - _ = Threading.sleep(100, 0) - result2 <- loader.loadPackage(packageId, _ => delayedLoad(10.millis), metric) - } yield { - result1 shouldBe None - result2.isDefined shouldBe true - - // 1 package not found, 1 successful - loadCount.get() shouldBe 2 - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/CompletionFromTransactionSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/CompletionFromTransactionSpec.scala deleted file mode 100644 index c7fedb485b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/CompletionFromTransactionSpec.scala +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.daml.ledger.api.v2.completion.Completion.DeduplicationPeriod -import com.digitalasset.canton.TestEssentials -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.platform.store.CompletionFromTransaction.CommonCompletionProperties -import com.digitalasset.canton.protocol.TestUpdateId -import com.digitalasset.canton.tracing.SerializableTraceContext -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.daml.lf.data.Time -import com.google.protobuf.duration.Duration -import com.google.protobuf.timestamp.Timestamp -import com.google.rpc.status.Status -import org.scalatest.OptionValues -import org.scalatest.matchers.should.Matchers -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.wordspec.AnyWordSpec - -import java.time.Instant - -class CompletionFromTransactionSpec - extends AnyWordSpec - with Matchers - with OptionValues - with TestEssentials - with TableDrivenPropertyChecks { - - "CompletionFromTransaction" should { - "create an accepted completion" in { - val testCases = Table( - ( - "submissionId", - "deduplicationOffset", - "deduplicationDurationSeconds", - "deduplicationDurationNanos", - "expectedSubmissionId", - "expectedDeduplicationPeriod", - ), - (Some("submissionId"), None, None, None, "submissionId", DeduplicationPeriod.Empty), - (None, None, None, None, "", DeduplicationPeriod.Empty), - ( - None, - Some(12345678L), - None, - None, - "", - DeduplicationPeriod.DeduplicationOffset(12345678L), - ), - ( - None, - None, - Some(1L), - Some(2), - "", - DeduplicationPeriod - .DeduplicationDuration(new Duration(1, 2)) - .asInstanceOf[ // otherwise the compilation fails due to an inference warning - DeduplicationPeriod - ], - ), - ) - - forEvery(testCases) { - ( - submissionId, - deduplicationOffset, - deduplicationDurationSeconds, - deduplicationDurationNanos, - expectedSubmissionId, - expectedDeduplicationPeriod, - ) => - val completionStream = CompletionFromTransaction.acceptedCompletion( - CompletionFromTransaction.CommonCompletionProperties - .createFromRecordTimeAndSynchronizerId( - submitters = Set("party1", "party2"), - recordTime = Time.Timestamp.Epoch, - completionOffset = Offset.firstOffset, - commandId = "commandId", - userId = "userId", - submissionId = submissionId, - synchronizerId = "synchronizer id", - traceContext = SerializableTraceContext(traceContext).toDamlProto, - deduplicationOffset = deduplicationOffset, - deduplicationDurationSeconds = deduplicationDurationSeconds, - deduplicationDurationNanos = deduplicationDurationNanos, - trafficCost = 4324L, - ), - TestUpdateId("updateId"), - ) - - val completion = completionStream.completionResponse.completion.value - completion.synchronizerTime.value.recordTime shouldBe Some(Timestamp(Instant.EPOCH)) - completion.offset shouldBe 1L - - completion.commandId shouldBe "commandId" - completion.updateId shouldBe TestUpdateId("updateId").toHexString - completion.userId shouldBe "userId" - completion.submissionId shouldBe expectedSubmissionId - completion.deduplicationPeriod shouldBe expectedDeduplicationPeriod - completion.actAs.toSet shouldBe Set("party1", "party2") - completion.paidTrafficCost shouldBe 4324L - } - } - - "fail on an invalid deduplication duration" in { - val testCases = Table( - ("deduplicationDurationSeconds", "deduplicationDurationNanos"), - (Some(1L), None), - (None, Some(1)), - ) - - forEvery(testCases) { (deduplicationDurationSeconds, deduplicationDurationNanos) => - an[IllegalArgumentException] shouldBe thrownBy( - CompletionFromTransaction.acceptedCompletion( - CommonCompletionProperties.createFromRecordTimeAndSynchronizerId( - submitters = Set.empty, - recordTime = Time.Timestamp.Epoch, - completionOffset = Offset.firstOffset, - commandId = "commandId", - userId = "userId", - submissionId = Some("submissionId"), - synchronizerId = "synchronizer id", - traceContext = SerializableTraceContext(traceContext).toDamlProto, - trafficCost = 4234L, - deduplicationOffset = None, - deduplicationDurationSeconds = deduplicationDurationSeconds, - deduplicationDurationNanos = deduplicationDurationNanos, - ), - updateId = TestUpdateId("updateId"), - ) - ) - } - } - - "create a rejected completion" in { - val status = Status.of(io.grpc.Status.Code.INTERNAL.value(), "message", Seq.empty) - val completionStream = CompletionFromTransaction.rejectedCompletion( - commonCompletionProperties = CompletionFromTransaction.CommonCompletionProperties - .createFromRecordTimeAndSynchronizerId( - submitters = Set("party"), - recordTime = Time.Timestamp.Epoch, - completionOffset = Offset.tryFromLong(2L), - commandId = "commandId", - userId = "userId", - submissionId = Some("submissionId"), - synchronizerId = "synchronizer id", - traceContext = SerializableTraceContext(traceContext).toDamlProto, - trafficCost = 4324L, - deduplicationOffset = None, - deduplicationDurationSeconds = None, - deduplicationDurationNanos = None, - ), - status = status, - ) - - val completion = completionStream.completionResponse.completion.value - completion.synchronizerTime.value.recordTime shouldBe Some(Timestamp(Instant.EPOCH)) - completion.offset shouldBe 2L - - completion.commandId shouldBe "commandId" - completion.userId shouldBe "userId" - completion.submissionId shouldBe "submissionId" - completion.status shouldBe Some(status) - completion.actAs shouldBe Seq("party") - completion.paidTrafficCost shouldBe 4324 - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/FlywayMigrationsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/FlywayMigrationsSpec.scala deleted file mode 100644 index a39d5a10c5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/FlywayMigrationsSpec.scala +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.daml.crypto.MessageDigestPrototype -import com.digitalasset.canton.platform.store.FlywayMigrationsSpec.* -import org.apache.commons.io.IOUtils -import org.flywaydb.core.Flyway -import org.flywaydb.core.api.configuration.FluentConfiguration -import org.flywaydb.core.api.migration.JavaMigration -import org.flywaydb.core.api.resource.LoadableResource -import org.flywaydb.core.internal.scanner.{LocationScannerCache, ResourceNameCache, Scanner} -import org.scalatest.matchers.should.Matchers.* -import org.scalatest.wordspec.AnyWordSpec - -import java.math.BigInteger -import java.nio.charset.Charset -import scala.jdk.CollectionConverters.* - -// SQL MIGRATION AND THEIR DIGEST FILES SHOULD BE CREATED ONLY ONCE AND NEVER CHANGED AGAIN, -// OTHERWISE MIGRATIONS BREAK ON EXISTING DEPLOYMENTS! -final class FlywayMigrationsSpec extends AnyWordSpec { - - "Postgres flyway migration files" should { - "always have a valid SHA-256 digest file accompanied" in { - assertFlywayMigrationFileHashes(DbType.Postgres, 1) - } - } - - "H2 database flyway migration files" should { - "always have a valid SHA-256 digest file accompanied" in { - assertFlywayMigrationFileHashes(DbType.H2Database, 1) - } - } -} - -object FlywayMigrationsSpec { - - private val digester = MessageDigestPrototype.Sha256.newDigest - - private def assertFlywayMigrationFileHashes( - dbType: DbType, - minMigrationCount: Int, - ): Unit = { - val config = Flyway - .configure() - .locations(FlywayMigrations.locations(dbType)*) - val resourceScanner = scanner(config) - val resources = resourceScanner.getResources("", ".sql").asScala.toSeq - resources.size should be >= minMigrationCount - - // TODO(#16458) Remove these exceptions - def skipCheck(filename: String): Boolean = { - val skip = Seq("V1_1__initial", "V1_2__initial_views") - skip.exists(filename.contains) - } - - resources.collect { - case res if !skipCheck(res.getFilename) => - val fileName = res.getFilename - val expectedDigest = - getExpectedDigest(fileName, fileName.dropRight(4) + ".sha256", resourceScanner) - val currentDigest = getCurrentDigest(res, config.getEncoding) - - assert( - currentDigest == expectedDigest, - s"Digest of migration file $fileName has changed! It is NOT allowed to change neither existing sql migrations files nor their digests!", - ) - } - } - - private def scanner(config: FluentConfiguration) = - new Scanner( - classOf[JavaMigration], - false, - new ResourceNameCache, - new LocationScannerCache, - config, - ) - - private def getExpectedDigest( - sourceFile: String, - digestFile: String, - resourceScanner: Scanner[?], - ): String = - IOUtils.toString( - Option(resourceScanner.getResource(digestFile)) - .getOrElse(sys.error(s"""Missing sha-256 file $digestFile! - |Are you introducing a new Flyway migration step? - |You need to create a sha-256 digest file by either running: - | - shasum -a 256 $sourceFile | awk '{print $$1}' > $digestFile (under the db/migration folder) - | - or community/common/src/main/resources/db/migration/canton/recompute-sha256sums.sh - |""".stripMargin)) - .read() - ) - - private def getCurrentDigest(res: LoadableResource, encoding: Charset) = { - val digest = digester.digest(IOUtils.toByteArray(res.read(), encoding)) - String.format(s"%0${digest.length * 2}x\n", new BigInteger(1, digest)) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/PruningOffsetServiceSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/PruningOffsetServiceSpec.scala deleted file mode 100644 index f4a18d177f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/PruningOffsetServiceSpec.scala +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.SuppressingLogger -import com.digitalasset.canton.tracing.TraceContext -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} -import scala.concurrent.{Future, Promise} - -final class PruningOffsetServiceSpec extends AsyncFlatSpec with Matchers { - - private implicit val tc: TraceContext = TraceContext.empty - private val loggerFactory = SuppressingLogger(getClass) - - private def offset(n: Long): Option[Offset] = Some(Offset.tryFromLong(n)) - - behavior of "PruningOffsetServiceImpl" - - it should "fetch from DB on first call and cache the result" in { - val callCount = new AtomicInteger(0) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - callCount.incrementAndGet() - Future.successful(offset(10)) - }, - loggerFactory = loggerFactory, - ) - - for { - r1 <- service.pruningOffset - r2 <- service.pruningOffset - r3 <- service.pruningOffset - } yield { - r1 shouldBe offset(10) - r2 shouldBe offset(10) - r3 shouldBe offset(10) - callCount.get() shouldBe 1 - } - } - - it should "return None when DB returns None" in { - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => Future.successful(None), - loggerFactory = loggerFactory, - ) - - for { - result <- service.pruningOffset - } yield result shouldBe None - } - - it should "fetch from DB every time when disabled" in { - val callCount = new AtomicInteger(0) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - callCount.incrementAndGet() - Future.successful(offset(10)) - }, - loggerFactory = loggerFactory, - ) - - service.disableCache() - - for { - r1 <- service.pruningOffset - r2 <- service.pruningOffset - } yield { - r1 shouldBe offset(10) - r2 shouldBe offset(10) - callCount.get() shouldBe 2 - } - } - - it should "re-fetch from DB after reEnableCache and then cache again" in { - val callCount = new AtomicInteger(0) - val currentValue = new AtomicInteger(10) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - callCount.incrementAndGet() - Future.successful(offset(currentValue.get().toLong)) - }, - loggerFactory = loggerFactory, - ) - - for { - r1 <- service.pruningOffset - _ = callCount.get() shouldBe 1 - _ = currentValue.set(20) - _ = service.reEnableCache() - r2 <- service.pruningOffset - r3 <- service.pruningOffset - } yield { - r1 shouldBe offset(10) - r2 shouldBe offset(20) - r3 shouldBe offset(20) - callCount.get() shouldBe 2 - } - } - - it should "not cache when disabled, then cache after re-enable" in { - val callCount = new AtomicInteger(0) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - callCount.incrementAndGet() - Future.successful(offset(10)) - }, - loggerFactory = loggerFactory, - ) - - service.disableCache() - - for { - r1 <- service.pruningOffset - _ = callCount.get() shouldBe 1 - r2 <- service.pruningOffset - _ = callCount.get() shouldBe 2 - _ = service.reEnableCache() - r3 <- service.pruningOffset - r4 <- service.pruningOffset - } yield { - r1 shouldBe offset(10) - r2 shouldBe offset(10) - r3 shouldBe offset(10) - r4 shouldBe offset(10) - callCount.get() shouldBe 3 - } - } - - it should "reset cache to Undefined on fetch failure so next call retries" in { - val callCount = new AtomicInteger(0) - val shouldFail = new AtomicBoolean(true) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - callCount.incrementAndGet() - if (shouldFail.get()) Future.failed(new RuntimeException("DB error")) - else Future.successful(offset(10)) - }, - loggerFactory = loggerFactory, - ) - callCount.get() shouldBe 0 - - for { - r1 <- service.pruningOffset.failed - _ = r1 shouldBe a[RuntimeException] - _ = callCount.get() shouldBe 1 - _ = shouldFail.set(false) - r2 <- service.pruningOffset - // after the failure, cache should have reset to Undefined and another fetch from db is triggered - _ = r2 shouldBe offset(10) - _ = callCount.get() shouldBe 2 - r3 <- service.pruningOffset - _ = r3 shouldBe offset(10) - _ = callCount.get() shouldBe 2 // cache is working again, no new db call - } yield succeed - } - - it should "not overwrite Disabled with Undefined on fetch failure" in { - val fetchPromise = Promise[Option[Offset]]() - val callCount = new AtomicInteger(0) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - val count = callCount.incrementAndGet() - if (count == 1) fetchPromise.future - else Future.successful(offset(99)) - }, - loggerFactory = loggerFactory, - ) - - // Start a fetch (transitions Undefined -> Defined) - val f1 = service.pruningOffset - - // Disable before the fetch completes - service.disableCache() - - // Fail the fetch - fetchPromise.failure(new RuntimeException("DB error")) - - for { - // The failure handler should NOT reset to Undefined because - // disableCache() already changed the state to Disabled. - ex <- f1.failed - _ = ex shouldBe a[RuntimeException] - // Subsequent reads should still go to Disabled - // returning fresh results each time (no caching) - r2 <- service.pruningOffset - r3 <- service.pruningOffset - } yield { - r2 shouldBe offset(99) - r3 shouldBe offset(99) - callCount.get() shouldBe 3 - } - } - - it should "not overwrite Disabled with Defined on fetch success" in { - val fetchPromise = Promise[Option[Offset]]() - val callCount = new AtomicInteger(0) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - val count = callCount.incrementAndGet() - if (count == 1) fetchPromise.future - else Future.successful(offset(99)) - }, - loggerFactory = loggerFactory, - ) - - // Start a long-running fetch (transitions Undefined -> Defined) - val f1 = service.pruningOffset - - // Disable before the fetch completes - service.disableCache() - - // Complete the fetch successfully - fetchPromise.success(offset(10)) - - for { - // The success should NOT reset state, Disabled should be preserved. - r1 <- f1 - _ = r1 shouldBe offset(10) - // Each call fetches from DB since caching is disabled - r2 <- service.pruningOffset - r3 <- service.pruningOffset - } yield { - r2 shouldBe offset(99) - r3 shouldBe offset(99) - callCount.get() shouldBe 3 - } - } - - it should "handle disable and reEnable during in-flight fetch" in { - val fetchPromise = Promise[Option[Offset]]() - val callCount = new AtomicInteger(0) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - val count = callCount.incrementAndGet() - if (count == 1) fetchPromise.future - else Future.successful(offset(99)) - }, - loggerFactory = loggerFactory, - ) - - // Start a long-running fetch (Undefined -> Defined) - val f1 = service.pruningOffset - - // Disable, then re-enable (Defined -> Disabled -> Undefined) - service.disableCache() - service.reEnableCache() - - // A new fetch should now be possible - val f2 = service.pruningOffset - - // Complete original fetch - fetchPromise.success(offset(1)) - - for { - r1 <- f1 - r2 <- f2 - _ = r1 shouldBe offset(1) - _ = r2 shouldBe offset(99) - _ = callCount.get() shouldBe 2 - r3 <- service.pruningOffset - _ = callCount.get() shouldBe 2 - _ = r3 shouldBe offset(99) - } yield succeed - } - - it should "simulate the pruning transaction lifecycle correctly" in { - val callCount = new AtomicInteger(0) - val currentValue = new AtomicInteger(1) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - callCount.incrementAndGet() - Future.successful(offset(currentValue.get().toLong)) - }, - loggerFactory = loggerFactory, - ) - - for { - // Initial state: no pruning yet - r1 <- service.pruningOffset - _ = r1 shouldBe offset(1) - _ = callCount.get() shouldBe 1 - - // Simulate pruning transaction: disable cache before commit - _ = service.disableCache() - - // During transaction: reads go to DB (still sees old value) - r2 <- service.pruningOffset - _ = r2 shouldBe offset(1) - _ = callCount.get() shouldBe 2 - - // Transaction commits, update the DB value - _ = currentValue.set(100) - - // Value is updated in the DB, but cache is disabled so reads go directly to DB - r3 <- service.pruningOffset - _ = r3 shouldBe offset(100) - _ = callCount.get() shouldBe 3 - // Another read while disabled, still goes to DB (not cached) - r4 <- service.pruningOffset - _ = r4 shouldBe offset(100) - _ = callCount.get() shouldBe 4 - - // Re-enable cache after commit - _ = service.reEnableCache() - - // First read after re-enable: fetches new value and caches - r5 <- service.pruningOffset - _ = r5 shouldBe offset(100) - _ = callCount.get() shouldBe 5 - - // Subsequent reads: served from cache - r6 <- service.pruningOffset - } yield { - r6 shouldBe offset(100) - callCount.get() shouldBe 5 - } - } - - it should "handle multiple disable/reEnable cycles" in { - val callCount = new AtomicInteger(0) - val currentValue = new AtomicInteger(10) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - callCount.incrementAndGet() - Future.successful(offset(currentValue.get().toLong)) - }, - loggerFactory = loggerFactory, - ) - - for { - r1 <- service.pruningOffset - _ = { - r1 shouldBe offset(10) - service.disableCache() - currentValue.set(20) - service.reEnableCache() - } - r2 <- service.pruningOffset - _ = { - r2 shouldBe offset(20) - service.disableCache() - currentValue.set(30) - service.reEnableCache() - } - r3 <- service.pruningOffset - } yield { - r3 shouldBe offset(30) - callCount.get() shouldBe 3 - } - } - - it should "disableCache from initial Undefined state" in { - val callCount = new AtomicInteger(0) - val service = new PruningOffsetServiceImpl( - fetchFromDb = _ => { - callCount.incrementAndGet() - Future.successful(offset(5)) - }, - loggerFactory = loggerFactory, - ) - - service.disableCache() - - for { - r1 <- service.pruningOffset - r2 <- service.pruningOffset - } yield { - r1 shouldBe offset(5) - r2 shouldBe offset(5) - callCount.get() shouldBe 2 - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/DbDtoSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/DbDtoSpec.scala deleted file mode 100644 index 56810558a8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/DbDtoSpec.scala +++ /dev/null @@ -1,822 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.platform.Party -import com.digitalasset.canton.platform.store.backend.DbDto.IdFilter -import com.digitalasset.canton.platform.store.interning.StringInterningBuilder -import com.digitalasset.canton.protocol.TestUpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{ - ChoiceName, - Identifier, - NameTypeConRef, - PackageId, - ParticipantId, - UserId, -} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import scala.collection.mutable - -class DbDtoSpec extends AnyWordSpec with Matchers { - - import StorageBackendTestValues.* - - implicit private val DbDtoEqual: org.scalactic.Equality[DbDto] = ScalatestEqualityHelpers.DbDtoEq - - val updateId = TestUpdateId("mock_hash") - val updateIdByteArray = updateId.toProtoPrimitive.toByteArray - - "DbDto.createDbDtos" should { - "populate correct DbDtos" in { - DbDto - .createDbDtos( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - traffic_cost = Some(1513L), - event_sequential_id = 3, - node_id = 4, - additional_witnesses = Set(someParty2), - representative_package_id = someRepresentativePackageId, - notPersistedContractId = hashCid("1"), - internal_contract_id = 3, - create_key_hash = Some("hash"), - )( - stakeholders = Set(someParty3, someParty4), - template_id = someTemplateId, - ) - .toList should contain theSameElementsInOrderAs List( - DbDto.EventActivate( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - traffic_cost = Some(1513L), - event_type = PersistentEventType.Create.asInt, - event_sequential_id = 3, - node_id = 4, - additional_witnesses = Some(Set(someParty2)), - source_synchronizer_id = None, - reassignment_counter = None, - reassignment_id = None, - representative_package_id = someRepresentativePackageId, - notPersistedContractId = hashCid("1"), - internal_contract_id = 3, - create_key_hash = Some("hash"), - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty3, - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty4, - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateWitness( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty2, - first_per_sequential_id = true, - ) - ), - ) - } - } - - "DbDto.assignDbDtos" should { - "populate correct DbDtos" in { - DbDto - .assignDbDtos( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitter = Some(someParty), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - traffic_cost = Some(1513L), - event_sequential_id = 3, - node_id = 4, - source_synchronizer_id = someSynchronizerId2, - reassignment_counter = 19, - reassignment_id = Array(1, 2), - representative_package_id = someRepresentativePackageId, - notPersistedContractId = hashCid("1"), - internal_contract_id = 3, - create_key_hash = Some("abc"), - )( - stakeholders = someParties("party3", "party4"), - template_id = someTemplateId, - ) - .toList should contain theSameElementsInOrderAs List( - DbDto.EventActivate( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = None, - traffic_cost = Some(1513L), - event_type = PersistentEventType.Assign.asInt, - event_sequential_id = 3, - node_id = 4, - additional_witnesses = None, - source_synchronizer_id = Some(someSynchronizerId2), - reassignment_counter = Some(19), - reassignment_id = Some(Array(1, 2)), - representative_package_id = someRepresentativePackageId, - notPersistedContractId = hashCid("1"), - internal_contract_id = 3, - create_key_hash = Some("abc"), - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty3, - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty4, - first_per_sequential_id = false, - ) - ), - ) - } - } - - "DbDto.consumingExerciseDbDtos" should { - "populate correct DbDtos" in { - DbDto - .consumingExerciseDbDtos( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - traffic_cost = Some(1513L), - event_sequential_id = 3, - node_id = 4, - deactivated_event_sequential_id = Some(10), - additional_witnesses = Set(someParty2), - exercise_choice = someChoice, - exercise_choice_interface_id = Some(someInterfaceId), - exercise_argument = Array(1, 2, 3), - exercise_result = Some(Array(1, 2, 3, 4)), - exercise_actors = Set(someParty5), - exercise_last_descendant_node_id = 10, - exercise_argument_compression = Some(1), - exercise_result_compression = Some(2), - contract_id = hashCid("23"), - internal_contract_id = Some(3), - template_id = someTemplateId, - package_id = somePackageId, - stakeholders = someParties("1", "2", "3"), - ledger_effective_time = 13, - ) - .toList should contain theSameElementsInOrderAs List( - DbDto.EventDeactivate( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - traffic_cost = Some(1513L), - event_type = PersistentEventType.ConsumingExercise.asInt, - event_sequential_id = 3, - node_id = 4, - deactivated_event_sequential_id = Some(10), - additional_witnesses = Some(Set(someParty2)), - exercise_choice = Some(someChoice), - exercise_choice_interface_id = Some(someInterfaceId), - exercise_argument = Some(Array(1, 2, 3)), - exercise_result = Some(Array(1, 2, 3, 4)), - exercise_actors = Some(Set(someParty5)), - exercise_last_descendant_node_id = Some(10), - exercise_argument_compression = Some(1), - exercise_result_compression = Some(2), - reassignment_id = None, - assignment_exclusivity = None, - target_synchronizer_id = None, - reassignment_counter = None, - contract_id = hashCid("23"), - internal_contract_id = Some(3), - template_id = someTemplateId, - package_id = somePackageId, - stakeholders = someParties("1", "2", "3"), - ledger_effective_time = Some(13), - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = Ref.Party.assertFromString("1"), - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = Ref.Party.assertFromString("2"), - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = Ref.Party.assertFromString("3"), - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterDeactivateWitness( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty2, - first_per_sequential_id = true, - ) - ), - ) - } - } - - "DbDto.unassignDbDtos" should { - "populate correct DbDtos" in { - DbDto - .unassignDbDtos( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitter = Some(someParty), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - traffic_cost = Some(1513L), - event_sequential_id = 3, - node_id = 4, - deactivated_event_sequential_id = Some(10), - reassignment_id = Array(2, 3, 4), - assignment_exclusivity = Some(10), - target_synchronizer_id = someSynchronizerId2, - reassignment_counter = 234, - contract_id = hashCid("23"), - internal_contract_id = Some(3), - template_id = someTemplateId, - package_id = somePackageId, - stakeholders = someParties("1", "2", "3"), - ) - .toList should contain theSameElementsInOrderAs List( - DbDto.EventDeactivate( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = None, - traffic_cost = Some(1513L), - event_type = PersistentEventType.Unassign.asInt, - event_sequential_id = 3, - node_id = 4, - deactivated_event_sequential_id = Some(10), - additional_witnesses = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - reassignment_id = Some(Array(2, 3, 4)), - assignment_exclusivity = Some(10), - target_synchronizer_id = Some(someSynchronizerId2), - reassignment_counter = Some(234), - contract_id = hashCid("23"), - internal_contract_id = Some(3), - template_id = someTemplateId, - package_id = somePackageId, - stakeholders = someParties("1", "2", "3"), - ledger_effective_time = None, - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = Ref.Party.assertFromString("1"), - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = Ref.Party.assertFromString("2"), - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = Ref.Party.assertFromString("3"), - first_per_sequential_id = false, - ) - ), - ) - } - } - - "DbDto.witnessedExercisedDbDtos" should { - "populate correct DbDtos for witnessed consuming exercise" in { - DbDto - .witnessedExercisedDbDtos( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - event_sequential_id = 3, - node_id = 4, - additional_witnesses = Set(someParty2), - consuming = true, - exercise_choice = someChoice, - exercise_choice_interface_id = Some(someInterfaceId), - exercise_argument = Array(1, 2, 3), - exercise_result = Some(Array(1, 2, 3, 4)), - exercise_actors = Set(someParty5), - exercise_last_descendant_node_id = 10, - exercise_argument_compression = Some(1), - exercise_result_compression = Some(2), - contract_id = hashCid("23"), - internal_contract_id = Some(3), - template_id = someTemplateId, - package_id = somePackageId, - ledger_effective_time = 13, - traffic_cost = Some(186L), - ) - .toList should contain theSameElementsInOrderAs List( - DbDto.EventVariousWitnessed( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - event_type = PersistentEventType.WitnessedConsumingExercise.asInt, - event_sequential_id = 3, - node_id = 4, - additional_witnesses = Set(someParty2), - consuming = Some(true), - exercise_choice = Some(someChoice), - exercise_choice_interface_id = Some(someInterfaceId), - exercise_argument = Some(Array(1, 2, 3)), - exercise_result = Some(Array(1, 2, 3, 4)), - exercise_actors = Some(Set(someParty5)), - exercise_last_descendant_node_id = Some(10), - exercise_argument_compression = Some(1), - exercise_result_compression = Some(2), - representative_package_id = None, - contract_id = Some(hashCid("23")), - internal_contract_id = Some(3), - template_id = Some(someTemplateId), - package_id = Some(somePackageId), - ledger_effective_time = Some(13), - traffic_cost = Some(186L), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty2, - first_per_sequential_id = true, - ) - ), - ) - } - } - - "DbDto.witnessedExercisedDbDtos" should { - "populate correct DbDtos for witnessed non consuming exercise" in { - DbDto - .witnessedExercisedDbDtos( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - event_sequential_id = 3, - node_id = 4, - additional_witnesses = Set(someParty2), - consuming = false, - exercise_choice = someChoice, - exercise_choice_interface_id = Some(someInterfaceId), - exercise_argument = Array(1, 2, 3), - exercise_result = Some(Array(1, 2, 3, 4)), - exercise_actors = Set(someParty5), - exercise_last_descendant_node_id = 10, - exercise_argument_compression = Some(1), - exercise_result_compression = Some(2), - contract_id = hashCid("23"), - internal_contract_id = Some(3), - template_id = someTemplateId, - package_id = somePackageId, - ledger_effective_time = 13, - traffic_cost = Some(186L), - ) - .toList should contain theSameElementsInOrderAs List( - DbDto.EventVariousWitnessed( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 3, - node_id = 4, - additional_witnesses = Set(someParty2), - consuming = Some(false), - exercise_choice = Some(someChoice), - exercise_choice_interface_id = Some(someInterfaceId), - exercise_argument = Some(Array(1, 2, 3)), - exercise_result = Some(Array(1, 2, 3, 4)), - exercise_actors = Some(Set(someParty5)), - exercise_last_descendant_node_id = Some(10), - exercise_argument_compression = Some(1), - exercise_result_compression = Some(2), - representative_package_id = None, - contract_id = Some(hashCid("23")), - internal_contract_id = Some(3), - template_id = Some(someTemplateId), - package_id = Some(somePackageId), - ledger_effective_time = Some(13), - traffic_cost = Some(186L), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty2, - first_per_sequential_id = true, - ) - ), - ) - } - } - - "DbDto.witnessedCreateDbDtos" should { - "populate correct DbDtos" in { - DbDto - .witnessedCreateDbDtos( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - event_sequential_id = 3, - node_id = 4, - additional_witnesses = Set(someParty2), - representative_package_id = someRepresentativePackageId, - internal_contract_id = 3, - traffic_cost = Some(186L), - )(template_id = someTemplateId) - .toList should contain theSameElementsInOrderAs List( - DbDto.EventVariousWitnessed( - event_offset = 1, - update_id = updateIdByteArray, - workflow_id = Some("w"), - command_id = Some("c"), - submitters = Some(Set(someParty)), - record_time = 2, - synchronizer_id = someSynchronizerId, - trace_context = serializableTraceContext, - external_transaction_hash = Some(someExternalTransactionHashBinary), - event_type = PersistentEventType.WitnessedCreate.asInt, - event_sequential_id = 3, - node_id = 4, - additional_witnesses = Set(someParty2), - consuming = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - representative_package_id = Some(someRepresentativePackageId), - contract_id = None, - internal_contract_id = Some(3), - template_id = None, - package_id = None, - ledger_effective_time = None, - traffic_cost = Some(186L), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 3, - template_id = someTemplateId, - party_id = someParty2, - first_per_sequential_id = true, - ) - ), - ) - } - } - - "DbDto.provideInternedStrings" should { - import StorageBackendTestValues.* - - class TestBuilder extends StringInterningBuilder { - val templates: mutable.Builder[String, List[String]] = List.newBuilder[String] - override def addTemplateId(templateId: NameTypeConRef): Unit = - templates.addOne(templateId.toString) - - val packages: mutable.Builder[String, List[String]] = List.newBuilder[String] - override def addPackageId(packageId: PackageId): Unit = packages.addOne(packageId) - - val parties: mutable.Builder[String, List[String]] = List.newBuilder[String] - override def addParty(party: Party): Unit = parties.addOne(party) - - val syncs: mutable.Builder[String, List[String]] = List.newBuilder[String] - override def addSynchronizerId(synchronizerId: SynchronizerId): Unit = - syncs.addOne(synchronizerId.toProtoPrimitive) - - val users: mutable.Builder[String, List[String]] = List.newBuilder[String] - override def addUserId(userId: UserId): Unit = users.addOne(userId) - - val ps: mutable.Builder[String, List[String]] = List.newBuilder[String] - override def addParticipantId(participantId: ParticipantId): Unit = ps.addOne(participantId) - - val choices: mutable.Builder[String, List[String]] = List.newBuilder[String] - override def addChoiceName(choiceName: ChoiceName): Unit = choices.addOne(choiceName) - - val interfaces: mutable.Builder[String, List[String]] = List.newBuilder[String] - override def addInterfaceId(interfaceId: Identifier): Unit = - interfaces.addOne(interfaceId.toString) - } - - "provide correct strings for interning for create" in { - val testBuilder = new TestBuilder - dtosCreate()().headOption.value.provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set( - "submitter1", - "submitter2", - "witness1", - "witness2", - ) - testBuilder.templates.result().toSet shouldBe Set() - testBuilder.packages.result().toSet shouldBe Set("representativepackage") - testBuilder.syncs.result().toSet shouldBe Set("x::sourcesynchronizer") - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for assign" in { - val testBuilder = new TestBuilder - dtosAssign()().headOption.value.provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set("submitter1") - testBuilder.templates.result().toSet shouldBe Set() - testBuilder.packages.result().toSet shouldBe Set("representativepackage") - testBuilder.syncs.result().toSet shouldBe Set( - "x::sourcesynchronizer", - "x::targetsynchronizer", - ) - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for consuming exercise" in { - val testBuilder = new TestBuilder - dtosConsumingExercise().headOption.value.provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set( - "submitter1", - "submitter2", - "witness1", - "witness2", - "actor1", - "actor2", - "stakeholder1", - "stakeholder2", - ) - testBuilder.templates.result().toSet shouldBe Set("#tem:pl:ate") - testBuilder.packages.result().toSet shouldBe Set("package") - testBuilder.syncs.result().toSet shouldBe Set("x::sourcesynchronizer") - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set("choice") - testBuilder.interfaces.result().toSet shouldBe Set("in:ter:face") - } - - "provide correct strings for interning for unassign" in { - val testBuilder = new TestBuilder - dtosUnassign().headOption.value.provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set("submitter1", "stakeholder1", "stakeholder2") - testBuilder.templates.result().toSet shouldBe Set("#tem:pl:ate") - testBuilder.packages.result().toSet shouldBe Set("package") - testBuilder.syncs.result().toSet shouldBe Set( - "x::sourcesynchronizer", - "x::targetsynchronizer", - ) - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for witnessed create" in { - val testBuilder = new TestBuilder - dtosWitnessedCreate()().headOption.value.provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set( - "submitter1", - "submitter2", - "witness1", - "witness2", - ) - testBuilder.templates.result().toSet shouldBe Set() - testBuilder.packages.result().toSet shouldBe Set("representativepackage") - testBuilder.syncs.result().toSet shouldBe Set("x::sourcesynchronizer") - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for witnessed exercised" in { - val testBuilder = new TestBuilder - dtosWitnessedExercised().headOption.value.provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set( - "submitter1", - "submitter2", - "witness1", - "witness2", - "actor1", - "actor2", - ) - testBuilder.templates.result().toSet shouldBe Set("#tem:pl:ate") - testBuilder.packages.result().toSet shouldBe Set("package") - testBuilder.syncs.result().toSet shouldBe Set("x::sourcesynchronizer") - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set("choice") - testBuilder.interfaces.result().toSet shouldBe Set("in:ter:face") - } - - "provide correct strings for interning for PTP" in { - val testBuilder = new TestBuilder - dtoPartyToParticipant(Offset.tryFromLong(1L), 10).provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set("party") - testBuilder.templates.result().toSet shouldBe Set() - testBuilder.packages.result().toSet shouldBe Set() - testBuilder.syncs.result().toSet shouldBe Set("x::sourcesynchronizer") - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set("participant") - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for completion" in { - val testBuilder = new TestBuilder - dtoCompletion(Offset.tryFromLong(1L)).provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set("signatory") - testBuilder.templates.result().toSet shouldBe Set() - testBuilder.packages.result().toSet shouldBe Set() - testBuilder.syncs.result().toSet shouldBe Set("x::sourcesynchronizer") - testBuilder.users.result().toSet shouldBe Set("user_id") - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for transaction meta" in { - val testBuilder = new TestBuilder - dtoTransactionMeta(Offset.tryFromLong(1L), 1, 1).provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set() - testBuilder.templates.result().toSet shouldBe Set() - testBuilder.packages.result().toSet shouldBe Set() - testBuilder.syncs.result().toSet shouldBe Set("x::sourcesynchronizer") - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for party entry" in { - val testBuilder = new TestBuilder - dtoPartyEntry(Offset.tryFromLong(1L)).provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set("party") - testBuilder.templates.result().toSet shouldBe Set() - testBuilder.packages.result().toSet shouldBe Set() - testBuilder.syncs.result().toSet shouldBe Set() - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for sequencer index moved" in { - val testBuilder = new TestBuilder - DbDto.SequencerIndexMoved(someSynchronizerId).provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set() - testBuilder.templates.result().toSet shouldBe Set() - testBuilder.packages.result().toSet shouldBe Set() - testBuilder.syncs.result().toSet shouldBe Set("x::sourcesynchronizer") - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - - "provide correct strings for interning for sequencer IdFilter" in { - val testBuilder = new TestBuilder - DbDto - .IdFilterVariousWitness( - IdFilter( - 1L, - someTemplateId, - someParty, - first_per_sequential_id = false, - ) - ) - .provideInternedStrings(testBuilder) - testBuilder.parties.result().toSet shouldBe Set("party") - testBuilder.templates.result().toSet shouldBe Set("#pkg-name:Mod:Template") - testBuilder.packages.result().toSet shouldBe Set() - testBuilder.syncs.result().toSet shouldBe Set() - testBuilder.users.result().toSet shouldBe Set() - testBuilder.ps.result().toSet shouldBe Set() - testBuilder.choices.result().toSet shouldBe Set() - testBuilder.interfaces.result().toSet shouldBe Set() - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/PruningDtoQueries.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/PruningDtoQueries.scala deleted file mode 100644 index 8de6dd6b26..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/PruningDtoQueries.scala +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import anorm.RowParser -import anorm.SqlParser.long -import com.digitalasset.canton.platform.store.backend.Conversions.offset -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.* - -import java.sql.Connection - -/** Contains dto classes each holding a minimal set of data sufficient to uniquely identify a row in - * the corresponding table. - */ -object PruningDto { - - final case class TxMeta(offset: Long) - final case class Completion(offset: Long) - -} -class PruningDtoQueries { - import PruningDto.* - private def offsetParser[T](f: Long => T): RowParser[T] = - offset("ledger_offset").map(_.unwrap) map f - - def eventActivate(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_events_activate_contract ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - def eventDeactivate(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_events_deactivate_contract ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - def eventVariousWitnessed(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_events_various_witnessed ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - - def filterActivateStakeholder(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_filter_activate_stakeholder ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - def filterActivateWitness(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_filter_activate_witness ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - def filterAchsStakeholder(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_filter_achs_stakeholder ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - def filterDeactivateStakeholder(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_filter_deactivate_stakeholder ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - def filterDeactivateWitness(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_filter_deactivate_witness ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - def filterVariousWitness(implicit c: Connection): Seq[Long] = - SQL"SELECT event_sequential_id FROM lapi_filter_various_witness ORDER BY event_sequential_id" - .asVectorOf(long("event_sequential_id"))(c) - - def updateMeta(implicit c: Connection): Seq[TxMeta] = - SQL"SELECT event_offset AS ledger_offset FROM lapi_update_meta ORDER BY event_offset" - .asVectorOf(offsetParser(TxMeta.apply))(c) - def completions(implicit c: Connection): Seq[Completion] = - SQL"SELECT completion_offset AS ledger_offset FROM lapi_command_completions ORDER BY completion_offset" - .asVectorOf(offsetParser(Completion.apply))(c) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/ScalatestEqualityHelpers.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/ScalatestEqualityHelpers.scala deleted file mode 100644 index 99d236c2ac..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/ScalatestEqualityHelpers.scala +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import org.scalactic.Equality -import org.scalatest.matchers.should.Matchers - -import scala.annotation.nowarn - -// DbDto case classes contain serialized values in Arrays (sometimes wrapped in Options), -// because this representation can efficiently be passed to Jdbc. -// Using Arrays means DbDto instances are not comparable, so we have to define a custom equality operator. -object ScalatestEqualityHelpers extends Matchers { - - @nowarn("cat=lint-infer-any") - val DbDtoEq: org.scalactic.Equality[DbDto] = { - case (a: DbDto, b: DbDto) => - (a.productPrefix === b.productPrefix) && - (a.productArity == b.productArity) && - (a.productIterator zip b.productIterator).forall { - case (x: Array[?], y: Array[?]) => x sameElements y - case (Some(x: Array[?]), Some(y: Array[?])) => x sameElements y - case (x, y) => x === y - } - case (_, _) => false - } - - @nowarn("cat=lint-infer-any") - @SuppressWarnings(Array("org.wartremover.warts.Product")) - def caseClassArrayEq[T <: Product]: org.scalactic.Equality[T] = { - case (a: Product, b: Product) => - (a.productPrefix === b.productPrefix) && - (a.productArity == b.productArity) && - (a.productIterator zip b.productIterator).forall { - case (x: Array[?], y: Array[?]) => x sameElements y - case (Some(x: Array[?]), Some(y: Array[?])) => x sameElements y - case (p1: Product, p2: Product) => caseClassArrayEq.areEquivalent(p1, p2) - case (x, y) => x === y - } - case (_, _) => false - } - - val DbDtoSeqEq: org.scalactic.Equality[Seq[DbDto]] = { - case (a: Seq[?], b: Seq[?]) => - a.sizeCompare(b) == 0 && a.zip(b).forall { case (x, y) => DbDtoEq.areEqual(x, y) } - case (_, _) => false - } - - implicit val eqOptArray: Equality[Option[Array[Byte]]] = (first: Option[Array[Byte]], b: Any) => { - val second = Option(b).getOrElse(Some[Array[Byte]]).asInstanceOf[Option[Array[Byte]]] - (first, second) match { - case (None, None) => true - case (None, Some(s)) => s.isEmpty - case (Some(f), None) => f.isEmpty - case (Some(f), Some(s)) => f === s - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/ScalatestEqualityHelpersSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/ScalatestEqualityHelpersSpec.scala deleted file mode 100644 index 31769fe6d8..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/ScalatestEqualityHelpersSpec.scala +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class ScalatestEqualityHelpersSpec extends AnyWordSpec with Matchers { - - import ScalatestEqualityHelpers.* - - "DbDtoEq" should { - - "compare DbDto when used with `decided` keyword" in { - - val dto0 = DbDto.StringInterningDto( - internalId = 1337, - externalString = "leet", - ) - - val dto1 = dto0.copy() - val dto2 = dto0.copy() - - dto0 should equal(dto0) // Works due to object equality shortcut - dto1 shouldNot equal(dto2) // As equality is overridden to be false with DbDto - dto1 should equal(dto2)(decided by DbDtoEq) - List(dto1) should equal(List(dto2))(decided by DbDtoSeqEq) - - } - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendProvider.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendProvider.scala deleted file mode 100644 index b6e041f830..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendProvider.scala +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.PruningOffsetService -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.backend.h2.H2StorageBackendFactory -import com.digitalasset.canton.platform.store.backend.localstore.{ - IdentityProviderStorageBackend, - PartyRecordStorageBackend, - UserManagementStorageBackend, -} -import com.digitalasset.canton.platform.store.backend.postgresql.PostgresStorageBackendFactory -import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache -import com.digitalasset.canton.platform.store.interning.MockStringInterning -import com.digitalasset.canton.platform.store.testing.postgresql.PostgresAroundAll -import org.mockito.MockitoSugar.mock -import org.scalatest.Suite - -import java.sql.Connection - -/** Creates a database and a [[TestBackend]]. Used by [[StorageBackendSpec]] to run all - * StorageBackend tests on different databases. - */ -trait StorageBackendProvider { - protected def jdbcUrl: String - protected def lockIdSeed: Int - protected def backend: TestBackend - - protected final def ingest(dbDtos: Vector[DbDto], connection: Connection): Unit = { - def typeBoundIngest[T](ingestionStorageBackend: IngestionStorageBackend[T]): Unit = - ingestionStorageBackend.insertBatch( - connection, - ingestionStorageBackend.batch(dbDtos, backend.stringInterningSupport), - ) - typeBoundIngest(backend.ingestion) - } - - protected final def updateLedgerEnd( - ledgerEndOffset: Offset, - ledgerEndSequentialId: Long, - ledgerEndPublicationTime: CantonTimestamp = CantonTimestamp.now(), - )(connection: Connection): Unit = { - backend.parameter.updateLedgerEnd( - LedgerEnd( - ledgerEndOffset, - ledgerEndSequentialId, - 0, - ledgerEndPublicationTime, - ) - )( - connection - ) // we do not care about the stringInterningId here - updateLedgerEndCache(connection) - } - - protected final def updateLedgerEnd(ledgerEnd: LedgerEnd)(connection: Connection): Unit = { - backend.parameter.updateLedgerEnd(ledgerEnd)(connection) - updateLedgerEndCache(connection) - } - - protected final def updateLedgerEndCache(connection: Connection): Unit = - backend.ledgerEndCache.set(backend.parameter.ledgerEnd(connection)) -} - -trait StorageBackendProviderPostgres - extends StorageBackendProvider - with PostgresAroundAll - with BaseTest { - this: Suite => - override protected def jdbcUrl: String = postgresDatabase.url - override protected val backend: TestBackend = TestBackend( - PostgresStorageBackendFactory(loggerFactory), - loggerFactory, - ) -} - -trait StorageBackendProviderH2 extends StorageBackendProvider with BaseTest { this: Suite => - override protected def jdbcUrl: String = "jdbc:h2:mem:storage_backend_provider;db_close_delay=-1" - override protected def lockIdSeed: Int = - throw new UnsupportedOperationException // DB Locking is not supported for H2 - override protected val backend: TestBackend = TestBackend(H2StorageBackendFactory, loggerFactory) -} - -final case class TestBackend( - ingestion: IngestionStorageBackend[?], - parameter: ParameterStorageBackend, - pruningOffsetService: PruningOffsetService, - party: PartyStorageBackend, - completion: CompletionStorageBackend, - contract: ContractStorageBackend, - event: EventStorageBackend, - dataSource: DataSourceStorageBackend, - dbLock: DBLockStorageBackend, - integrity: IntegrityStorageBackend, - reset: ResetStorageBackend, - stringInterning: StringInterningStorageBackend, - ledgerEndCache: MutableLedgerEndCache, - stringInterningSupport: MockStringInterning, - userManagement: UserManagementStorageBackend, - participantPartyStorageBackend: PartyRecordStorageBackend, - identityProviderStorageBackend: IdentityProviderStorageBackend, - pruningDtoQueries: PruningDtoQueries = new PruningDtoQueries, -) - -object TestBackend { - def apply( - storageBackendFactory: StorageBackendFactory, - loggerFactory: NamedLoggerFactory, - ): TestBackend = { - val ledgerEndCache = MutableLedgerEndCache() - val stringInterning = new MockStringInterning - - TestBackend( - ingestion = storageBackendFactory.createIngestionStorageBackend, - parameter = storageBackendFactory.createParameterStorageBackend(stringInterning), - pruningOffsetService = mock[PruningOffsetService], - party = storageBackendFactory.createPartyStorageBackend(ledgerEndCache), - completion = - storageBackendFactory.createCompletionStorageBackend(stringInterning, loggerFactory), - contract = - storageBackendFactory.createContractStorageBackend(stringInterning, ledgerEndCache), - event = storageBackendFactory - .createEventStorageBackend(ledgerEndCache, stringInterning, loggerFactory), - dataSource = storageBackendFactory.createDataSourceStorageBackend, - dbLock = storageBackendFactory.createDBLockStorageBackend, - integrity = storageBackendFactory.createIntegrityStorageBackend, - reset = storageBackendFactory.createResetStorageBackend, - stringInterning = storageBackendFactory.createStringInterningStorageBackend, - ledgerEndCache = ledgerEndCache, - stringInterningSupport = stringInterning, - userManagement = storageBackendFactory.createUserManagementStorageBackend, - participantPartyStorageBackend = storageBackendFactory.createPartyRecordStorageBackend, - identityProviderStorageBackend = - storageBackendFactory.createIdentityProviderConfigStorageBackend, - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpec.scala deleted file mode 100644 index e645b99135..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpec.scala +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.platform.store.FlywayMigrations -import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, TestSuite} - -import java.sql.Connection -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger -import javax.sql.DataSource -import scala.concurrent.duration.DurationInt -import scala.concurrent.{Await, ExecutionContext, Future} -import scala.util.{Try, Using} - -trait StorageBackendSpec - extends BaseTest - with StorageBackendProvider - with BeforeAndAfterEach - with BeforeAndAfterAll { - this: TestSuite => - - // Data source (initialized once) - private var dataSource: DataSource = _ - - // Default connection for database operations (initialized for each test, since connections are stateful) - private var defaultConnection: Connection = _ - - // Execution context with a fixed number of threads, used for running parallel queries (where each query is - // executed in its own thread). - private val connectionPoolSize = 16 - private val connectionPoolExecutionContext = ExecutionContext.fromExecutor( - Executors.newFixedThreadPool( - connectionPoolSize - ) - ) - - /** Runs the given database operations in parallel. Each operation will run in a separate thread - * and will use a separate database connection. - */ - protected def executeParallelSql[T](fs: Vector[Connection => T]): Vector[T] = { - require(fs.sizeIs <= connectionPoolSize) - - val connections = Vector.fill(fs.size)(dataSource.getConnection()) - - implicit val ec: ExecutionContext = connectionPoolExecutionContext - val result = Try( - Await.result( - Future.sequence( - Vector.tabulate(fs.size)(i => Future(fs(i)(connections(i)))) - ), - 60.seconds, - ) - ) - - connections.foreach(_.close()) - result.success.value - } - - /** Runs the given database operation */ - protected def executeSql[T](f: Connection => T): T = f(defaultConnection) - - protected def withConnections[T](n: Int)(f: List[Connection] => T): T = - Using - .Manager { manager => - val connections = List.fill(n)(manager(dataSource.getConnection)) - f(connections) - } - .success - .value - - override protected def beforeAll(): Unit = { - super.beforeAll() - - // Note: reusing the connection pool EC for initialization - implicit val ec: ExecutionContext = connectionPoolExecutionContext - - val dataSourceFuture = for { - _ <- new FlywayMigrations(jdbcUrl, loggerFactory = loggerFactory).migrate() - dataSource <- VerifiedDataSource(jdbcUrl, loggerFactory = loggerFactory) - } yield dataSource - - dataSource = Await.result(dataSourceFuture, 60.seconds) - - logger.info( - s"Finished setting up database $jdbcUrl for tests. You can now connect to this database to debug failed tests. Note that tables are truncated between each test." - ) - } - - override protected def afterAll(): Unit = - super.afterAll() - - private val runningTests = new AtomicInteger(0) - - // Each test should start with an empty database to allow testing low-level behavior - // However, creating a fresh database for each test would be too expensive. - // Instead, we truncate all tables using the reset() call before each test. - override protected def beforeEach(): Unit = { - super.beforeEach() - - defaultConnection = dataSource.getConnection() - - assert( - runningTests.incrementAndGet() == 1, - "StorageBackendSpec tests must not run in parallel, as they all run against the same database.", - ) - - // Reset the content of the index database - backend.reset.resetAll(defaultConnection) - updateLedgerEndCache(defaultConnection) - - // Note: here we reset the MockStringInterning object to make sure each test starts with empty interning state. - // This is not strictly necessary, as tryInternalize() always succeeds in MockStringInterning - we don't have - // a problem where the interning would be affected by data left over by previous tests. - // To write tests that are sensitive to interning unknown data, we would have to use a custom storage backend - // implementation. - backend.stringInterningSupport.reset() - } - - override protected def afterEach(): Unit = { - assert( - runningTests.decrementAndGet() == 0, - "StorageBackendSpec tests must not run in parallel, as they all run against the same database.", - ) - - defaultConnection.close() - - super.afterEach() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpecH2.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpecH2.scala deleted file mode 100644 index 1a9d7ba351..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpecH2.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import org.scalatest.flatspec.AnyFlatSpec - -final class StorageBackendSpecH2 - extends AnyFlatSpec - with StorageBackendProviderH2 - with StorageBackendSuite diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpecPostgres.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpecPostgres.scala deleted file mode 100644 index d382ebaec4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSpecPostgres.scala +++ /dev/null @@ -1,662 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import anorm.SqlParser.{byteArray, long, scalar} -import anorm.{SqlParser, ~} -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.config -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.CannotAcquireAllRowLocksException -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.{ - CompositeSql, - SqlStringInterpolation, -} -import com.digitalasset.canton.platform.store.backend.common.QueryStrategy.withoutNetworkTimeout -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.`SimpleSql ops` -import com.digitalasset.canton.platform.store.backend.postgresql.{ - PostgresDataSourceConfig, - PostgresDataSourceStorageBackend, - PostgresQueryStrategy, -} -import org.scalatest.Inside -import org.scalatest.exceptions.TestFailedException -import org.scalatest.flatspec.AnyFlatSpec - -import java.sql.{Connection, SQLException} -import java.util.concurrent.TimeUnit -import scala.concurrent.Future - -final class StorageBackendSpecPostgres - extends AnyFlatSpec - with StorageBackendProviderPostgres - with StorageBackendSuite - with Inside { - - behavior of "StorageBackend (Postgres)" - - it should "find the Postgres version" in { - val version = executeSql(PostgresDataSourceStorageBackend(loggerFactory).getPostgresVersion) - - inside(version) { case Some(versionNumbers) => - // Minimum Postgres version used in tests - versionNumbers._1 should be >= 9 - versionNumbers._2 should be >= 0 - } - } - - it should "correctly parse a Postgres version" in { - val backend = PostgresDataSourceStorageBackend(loggerFactory) - backend.parsePostgresVersion("1.2") shouldBe Some((1, 2)) - backend.parsePostgresVersion("1.2.3") shouldBe Some((1, 2)) - backend.parsePostgresVersion("1.2.3-alpha.4.5") shouldBe Some((1, 2)) - backend.parsePostgresVersion("10.11") shouldBe Some((10, 11)) - } - - it should "fail the compatibility check for Postgres versions lower than minimum" in { - val version = executeSql(PostgresDataSourceStorageBackend(loggerFactory).getPostgresVersion) - val currentlyUsedMajorVersion = inside(version) { case Some((majorVersion, _)) => - majorVersion - } - val backend = - new PostgresDataSourceStorageBackend( - minMajorVersionSupported = currentlyUsedMajorVersion + 1, - loggerFactory = loggerFactory, - ) - - loggerFactory.assertThrowsAndLogs[PostgresDataSourceStorageBackend.UnsupportedPostgresVersion]( - within = executeSql( - backend.checkCompatibility - ), - assertions = _.errorMessage should include( - "Deprecated Postgres version." - ), - ) - } - - it should "throw an exception if network timeout has been violated" in { - import anorm.SqlStringInterpolation - - val backend = - new PostgresDataSourceStorageBackend( - minMajorVersionSupported = 14, - loggerFactory = loggerFactory, - ) - - val dataSource = backend.createDataSource( - DataSourceStorageBackend - .DataSourceConfig( - jdbcUrl = jdbcUrl, - postgresConfig = PostgresDataSourceConfig( - networkTimeout = Some(config.NonNegativeFiniteDuration.ofSeconds(1)) - ), - ), - loggerFactory, - ) - - val connection = dataSource.getConnection - val startTime = System.nanoTime() - val thrown = intercept[SQLException] { - // sleep for 5 seconds to simulate a long-running query - SQL"SELECT pg_sleep(5);".execute()(connection) - } - thrown.getMessage should include("An I/O error occurred while sending to the backend.") - thrown.getCause.getMessage should include("Read timed out") - val endTime = System.nanoTime() - val timedOutAfterMillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime) - - // the network timeout should have occurred after approximately 1 second - timedOutAfterMillis should be < 1500L - timedOutAfterMillis should be >= 1000L - - connection.close() - - // when the network timeout is disabled, the long-running query should complete successfully - val connection2 = dataSource.getConnection - withoutNetworkTimeout { connection => - SQL"SELECT pg_sleep(5);".execute()(connection) - }(connection2, noTracingLogger) - - // and when re-enabling the network timeout, it should again throw after the specified time - val thrown2 = intercept[SQLException] { - SQL"SELECT pg_sleep(5);".execute()(connection2) - } - thrown2.getMessage should include("An I/O error occurred while sending to the backend.") - thrown2.getCause.getMessage should include("Read timed out") - - connection2.close() - } - - it should "wait for a long running query when clientConnectionCheckInterval is not set" in { - import anorm.SqlStringInterpolation - - val backend = - new PostgresDataSourceStorageBackend( - minMajorVersionSupported = 14, - loggerFactory = loggerFactory, - ) - - val dataSource = backend.createDataSource( - DataSourceStorageBackend - .DataSourceConfig( - jdbcUrl = jdbcUrl, - postgresConfig = PostgresDataSourceConfig( - clientConnectionCheckInterval = None, - networkTimeout = None, - ), - ), - loggerFactory, - ) - - val connection1 = dataSource.getConnection - val connection2 = dataSource.getConnection - - // acquire advisory lock - val acquired1 = SQL"SELECT pg_try_advisory_lock(123456);".as( - SqlParser.bool("pg_try_advisory_lock").single - )(connection1) - acquired1 shouldBe true - - // blocking acquire advisory lock in another connection - val acquired2 = Future( - SQL"SELECT pg_advisory_lock(123456);".execute()(connection2) - )(parallelExecutionContext) - - // long-running query - val longRunning = Future( - SQL"SELECT pg_sleep(5);".execute()(connection1) - )(parallelExecutionContext) - - Threading.sleep(500) // let some time for db to start the long-running query - acquired2.isCompleted shouldBe false - // close the connection while the long-running query is still running simulating a lost client connection - // since there is no clientConnectionCheckInterval the server will not interrupt the long-running query and will - // only release the lock after it - val thrown = intercept[TestFailedException] { - connection1.close() - longRunning.futureValue - } - thrown.getCause shouldBe a[org.postgresql.util.PSQLException] - - val startTime = System.nanoTime() - acquired2.futureValue - val endTime = System.nanoTime() - val getLockDurationMillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime) - // should take almost 5 seconds to acquire the lock after client is disconnected since there is no clientConnectionCheckInterval - // and the long-running query continues to run until completion and then releases the lock - getLockDurationMillis should be > 4000L - - connection2.close() - } - - it should "abort a long running query when clientConnectionCheckInterval is set" in { - import anorm.SqlStringInterpolation - - val backend = - new PostgresDataSourceStorageBackend( - minMajorVersionSupported = 14, - loggerFactory = loggerFactory, - ) - - val dataSource = backend.createDataSource( - DataSourceStorageBackend - .DataSourceConfig( - jdbcUrl = jdbcUrl, - postgresConfig = PostgresDataSourceConfig( - clientConnectionCheckInterval = Some(config.NonNegativeFiniteDuration.ofSeconds(2)), - networkTimeout = None, - ), - ), - loggerFactory, - ) - - val connection1 = dataSource.getConnection - val connection2 = dataSource.getConnection - // acquire advisory lock - val acquired1 = SQL"SELECT pg_try_advisory_lock(123456);" - .as(SqlParser.bool("pg_try_advisory_lock").single)(connection1) - acquired1 shouldBe true - - // blocking acquire advisory lock in another connection - val acquired2 = Future( - SQL"SELECT pg_advisory_lock(123456);".execute()(connection2) - )(parallelExecutionContext) - - // long-running query - val longRunning = Future( - SQL"SELECT pg_sleep(5);".execute()(connection1) - )(parallelExecutionContext) - - Threading.sleep(500) // let some time for db to start the long-running query - acquired2.isCompleted shouldBe false - // close the connection while the long-running query is still running simulating a lost client connection - // when clientConnectionCheckInterval kicks in the server should interrupt the long-running query and release the lock - val thrown = intercept[TestFailedException] { - connection1.close() - longRunning.futureValue - } - thrown.getCause shouldBe a[org.postgresql.util.PSQLException] - - val startTime = System.nanoTime() - acquired2.futureValue - val endTime = System.nanoTime() - val getLockDurationMillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime) - - // should take at most 2 seconds for the server to check that the client is disconnected, abort the long-running query - // and release the lock so that the second connection can acquire it - getLockDurationMillis should be < 2000L - - connection2.close() - } - - it should "throw an exception when a long-running query violates network timeout" in { - import anorm.SqlStringInterpolation - - val backend = - new PostgresDataSourceStorageBackend( - minMajorVersionSupported = 14, - loggerFactory = loggerFactory, - ) - - val clientConnectionCheckInterval = config.NonNegativeFiniteDuration.ofSeconds(1) - val networkTimeout = config.NonNegativeFiniteDuration.ofSeconds(2) - - val postgresConfig = PostgresDataSourceConfig( - clientConnectionCheckInterval = Some(clientConnectionCheckInterval), - networkTimeout = Some(networkTimeout), - ) - - val dataSource = backend.createDataSource( - DataSourceStorageBackend - .DataSourceConfig( - jdbcUrl = jdbcUrl, - postgresConfig = postgresConfig, - ), - loggerFactory, - ) - - val connection1 = dataSource.getConnection - val connection2 = dataSource.getConnection() - // setting a high timeout for the second connection - connection2.setNetworkTimeout( - parallelExecutionContext, - 60000, // 60 seconds - ) - // acquire advisory lock - val acquired1 = SQL"SELECT pg_try_advisory_lock(123456);" - .as(SqlParser.bool("pg_try_advisory_lock").single)(connection1) - acquired1 shouldBe true - - // blocking acquire advisory lock in another connection - val acquired2 = Future( - SQL"SELECT pg_advisory_lock(123456);".execute()(connection2) - )(parallelExecutionContext) - - // long-running query that will violate the network timeout - val longRunning = Future( - SQL"SELECT pg_sleep(60);".execute()(connection1) - )(parallelExecutionContext) - - Threading.sleep(500L) // let some time for db to start the long-running query - acquired2.isCompleted shouldBe false - val startTime = System.nanoTime() - - // long-running query should throw after network timeout, however the server continues to execute it - val thrown = intercept[TestFailedException] { - longRunning.futureValue - } - thrown.getCause shouldBe a[org.postgresql.util.PSQLException] - thrown.getCause.getCause.getMessage should include("Read timed out") - - val networkTimeoutTime = System.nanoTime() - val timeUntilNetworkTimeout = TimeUnit.NANOSECONDS.toMillis(networkTimeoutTime - startTime) - // should take at most networkTimeout for the client to check that the network timeout has been violated - timeUntilNetworkTimeout should be < networkTimeout.duration.toMillis - - acquired2.futureValue - val endTime = System.nanoTime() - - val leewayForSchedulingDelaysMillis = 500L - // should take at most clientConnectionCheckInterval more for the server to check that the client is disconnected, abort the long-running query - // and release the lock so that the second connection can acquire it - val timeToAbortAfterTimeout = TimeUnit.NANOSECONDS.toMillis(endTime - networkTimeoutTime) - timeToAbortAfterTimeout should be < clientConnectionCheckInterval.duration.toMillis + leewayForSchedulingDelaysMillis - - val getLockDurationMillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime) - getLockDurationMillis should be < (postgresConfig.networkTimeout.value.duration.toMillis + postgresConfig.clientConnectionCheckInterval.value.duration.toMillis + leewayForSchedulingDelaysMillis) - - connection1.close() - connection2.close() - } - - behavior of "exclusive table locking" - - it should "block other exclusive lock in lockExclusivelyPruningProcessingTable" in withConnections( - 2 - ) { - case List(c1, c2) => - c1.setAutoCommit(false) - c2.setAutoCommit(false) - backend.event.lockExclusivelyPruningProcessingTable(c1) - val blockedCall = - Future(backend.event.lockExclusivelyPruningProcessingTable(c2))(parallelExecutionContext) - Threading.sleep(1000) - blockedCall.value shouldBe None - c1.commit() - c1.close() - blockedCall.futureValue - c2.close() - - case unexpected => fail(s"Incorrect amount of connections: ${unexpected.size}") - } - - it should "block other exclusive locks in lockExclusivelyContractPruningProcessingTable" in withConnections( - 2 - ) { - case List(c1, c2) => - c1.setAutoCommit(false) - c2.setAutoCommit(false) - backend.event.lockExclusivelyContractPruningProcessingTable(c1) - val blockedCall = - Future(backend.event.lockExclusivelyContractPruningProcessingTable(c2))( - parallelExecutionContext - ) - Threading.sleep(1000) - blockedCall.value shouldBe None - c1.commit() - c1.close() - blockedCall.futureValue - c2.close() - - case unexpected => fail(s"Incorrect amount of connections: ${unexpected.size}") - } - - it should "block other operations accessing contract candidate table (locked by lockExclusivelyContractPruningProcessingTable)" in withConnections( - 2 - ) { - case List(c1, c2) => - c1.setAutoCommit(false) - c2.setAutoCommit(false) - backend.event.lockExclusivelyContractPruningProcessingTable(c1) - val blockedCall = - Future(backend.event.cleanPruningCandidates()(c2, implicitly))(parallelExecutionContext) - Threading.sleep(1000) - blockedCall.value shouldBe None - c1.commit() - c1.close() - blockedCall.futureValue - c2.close() - - case unexpected => fail(s"Incorrect amount of connections: ${unexpected.size}") - } - - it should "block other operations inserting into contract candidate table (locked by lockExclusivelyContractPruningProcessingTable)" in withConnections( - 2 - ) { - case List(c1, c2) => - c1.setAutoCommit(false) - c2.setAutoCommit(false) - backend.event.lockExclusivelyContractPruningProcessingTable(c1) - val blockedCall = Future( - SQL"INSERT INTO lapi_pruning_contract_candidate(internal_contract_id) VALUES (11)" - .execute()(c2) - )(parallelExecutionContext) - Threading.sleep(1000) - blockedCall.value shouldBe None - c1.commit() - c1.close() - blockedCall.futureValue - c2.close() - - case unexpected => fail(s"Incorrect amount of connections: ${unexpected.size}") - } - - it should "be block by other operations accessing contract candidate table (attempt to lock by lockExclusivelyContractPruningProcessingTable)" in withConnections( - 2 - ) { - case List(c1, c2) => - c1.setAutoCommit(false) - c2.setAutoCommit(false) - backend.event.cleanPruningCandidates()(c1, implicitly) - val blockedCall = - Future(backend.event.lockExclusivelyContractPruningProcessingTable(c2))( - parallelExecutionContext - ) - Threading.sleep(1000) - blockedCall.value shouldBe None - c1.commit() - c1.close() - blockedCall.futureValue - c2.close() - - case unexpected => fail(s"Incorrect amount of connections: ${unexpected.size}") - } - - it should "be block by other operations inserting into contract candidate table (attempt to lock by lockExclusivelyContractPruningProcessingTable)" in withConnections( - 2 - ) { - case List(c1, c2) => - c1.setAutoCommit(false) - c2.setAutoCommit(false) - SQL"INSERT INTO lapi_pruning_contract_candidate(internal_contract_id) VALUES (11)" - .execute()(c1) - val blockedCall = - Future(backend.event.lockExclusivelyContractPruningProcessingTable(c2))( - parallelExecutionContext - ) - Threading.sleep(1000) - blockedCall.value shouldBe None - c1.commit() - c1.close() - blockedCall.futureValue - c2.close() - - case unexpected => fail(s"Incorrect amount of connections: ${unexpected.size}") - } - - behavior of "read/write row locking of internal contract IDs" - - it should "row lock blocking leads to exception" in testRowLocking { env => - import env.* - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c1) - assertThrows[CannotAcquireAllRowLocksException] { - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid2, cid3)))(c2) - } - } - - it should "disjoint locking is not blocking" in testRowLocking { env => - import env.* - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c1) - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid3, cid4)))(c2) - } - - it should "write should block read" in testRowLocking { env => - import env.* - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c1) - val f = Future( - backend.event.readLockInternalContractIds(Set(cid2, cid3, -15))(c2) shouldBe Set(-15) - )(parallelExecutionContext) - Threading.sleep(1000) - f.value shouldBe None - c1.commit() - f.futureValue - } - - it should "read should block write" in testRowLocking { env => - import env.* - backend.event.readLockInternalContractIds(Set(cid2, cid3, -15))(c1) shouldBe Set(-15) - assertThrows[CannotAcquireAllRowLocksException] { - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c2) - } - c2.rollback() - c1.commit() - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c2) - } - - it should "read should not block read" in testRowLocking { env => - import env.* - backend.event.readLockInternalContractIds(Set(cid2, cid3, -15))(c1) shouldBe Set(-15) - backend.event.readLockInternalContractIds(Set(cid2, cid3, -15))(c2) shouldBe Set(-15) - } - - it should "write should block write" in testRowLocking { env => - import env.* - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid2, -15)))(c1) - assertThrows[CannotAcquireAllRowLocksException] { - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c2) - } - c2.rollback() - c1.commit() - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c2) - } - - it should "read lock should block write lock, write lock also might be starved by further read locks, and also the blocked writer progressively locking all entries as soon as possible" in testRowLocking { - env => - import env.* - backend.event.readLockInternalContractIds(Set(cid2, cid3, -15))(c1) shouldBe Set(-15) - // write is blocked by the read for cid2, but cid1 is write locked - val writeF = Future( - blockingWriteLockInternalContractIds( - PostgresQueryStrategy.anyOf(List(cid1, cid2)) - )(c2) - )(parallelExecutionContext) - Threading.sleep(500) - writeF.value shouldBe None - // another read is not blocked by the current read on cid2, so it executes immediately - backend.event.readLockInternalContractIds(Set(cid2, cid4, -18))(c3) shouldBe Set(-18) - Threading.sleep(500) - writeF.value shouldBe None - c1.commit() - Threading.sleep(500) - // although c1 already released it's cid2 read lock, c3 still has an open tx with cid2 read lock - writeF.value shouldBe None - // another tx incoming for cid2, c3 still hold the read lock, but this one can execute immediately - backend.event.readLockInternalContractIds(Set(cid2, cid3, -19))(c1) shouldBe Set(-19) - c3.commit() - Threading.sleep(500) - // although c3 released its cid2 read lock, c1 still holds it o writer is still blocked (starving) - writeF.value shouldBe None - // c3 now blocked as cid1 held by c2 with a write lock - val readF = Future( - backend.event.readLockInternalContractIds(Set(cid1, cid3, -19))(c3) shouldBe Set(-19) - )(parallelExecutionContext) - Threading.sleep(500) - // c1 still holds the read lock for cid2 - writeF.value shouldBe None - readF.value shouldBe None - c1.commit() - // finally c2 can get the write lock for cid2 as c1 released the read lock - writeF.futureValue - Threading.sleep(500) - readF.value shouldBe None - c2.commit() - readF.futureValue - } - - it should "read lock should not block contract upsert" in testRowLocking { env => - import env.* - backend.event.readLockInternalContractIds(Set(cid2, cid3, -15))(c1) shouldBe Set(-15) - upsertParContracts(c2) - } - - it should "contract upsert should not block read lock" in testRowLocking { env => - import env.* - upsertParContracts(c1) - backend.event.readLockInternalContractIds(Set(cid2, cid3, -15))(c2) shouldBe Set(-15) - } - - it should "write lock should not block contract upsert" in testRowLocking { env => - import env.* - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c1) - upsertParContracts(c2) - } - - it should "contract upsert should not block write lock" in testRowLocking { env => - import env.* - upsertParContracts(c1) - backend.event.writeLockInternalContractIds(PostgresQueryStrategy.anyOf(List(cid1, cid2)))(c2) - } - - case class TestRowLockingEnv( - c1: Connection, - c2: Connection, - c3: Connection, - cid1: Long, - cid2: Long, - cid3: Long, - cid4: Long, - ) - - private def testRowLocking(test: TestRowLockingEnv => Unit): Unit = - withConnections(3) { - case cs @ List(c1, c2, c3) => - cs.foreach { c => - c.setAutoCommit(false) // operations running in a transaction - c.setNetworkTimeout(null, 5000) // in 5 second all JDBC calls should finish - } - test( - TestRowLockingEnv( - c1, - c2, - c3, - insertParContract("first"), - insertParContract("second"), - insertParContract("third"), - insertParContract("fourth"), - ) - ) - - case unexpected => fail(s"Incorrect amount of connections: ${unexpected.size}") - } - - private def insertParContract(contractId: String): Long = { - val contractIdBytes = contractId.getBytes - executeSql( - SQL""" - INSERT INTO par_contracts (contract_id, instance, package_id, template_id) - VALUES ($contractIdBytes, $contractIdBytes, 'pid', 'tid') - RETURNING internal_contract_id""" - .executeInsert(scalar[Long].single)(_) - ) - } - - // Upserting "first" "second" from the fixture and "new" which does exist yet with on commit do nothing - // also querying the contracts which have been found to simulate contract store DB behavior - private def upsertParContracts(connection: Connection): Unit = { - val contractIdBytes1 = "second".getBytes - val contractIdBytes2 = "first".getBytes - val contractIdBytes3 = "new".getBytes - SQL""" - INSERT INTO par_contracts (contract_id, instance, package_id, template_id) - VALUES - ($contractIdBytes1, $contractIdBytes1, 'pid', 'tid'), - ($contractIdBytes2, $contractIdBytes2, 'pid', 'tid'), - ($contractIdBytes3, $contractIdBytes3, 'pid', 'tid') - ON CONFLICT(contract_id) DO NOTHING - RETURNING internal_contract_id, contract_id""" - .asVectorOf(long("internal_contract_id") ~ byteArray("contract_id") map { - case _ ~ contractId => contractId.toList - })(connection) shouldBe List("new".getBytes.toList) - SQL""" - SELECT contract_id - FROM par_contracts - WHERE contract_id ${PostgresQueryStrategy.anyOfBinary( - List("second", "first").map(_.getBytes) - )}""" - .asVectorOf(byteArray("contract_id"))(connection) - .map(_.toList) - .toSet shouldBe List("second", "first").map(_.getBytes.toList).toSet - } - - private def blockingWriteLockInternalContractIds(whereInternalContractIdExprs: CompositeSql)( - connection: Connection - ): Unit = - SQL""" - SELECT internal_contract_id - FROM par_contracts - WHERE internal_contract_id $whereInternalContractIdExprs - ORDER BY internal_contract_id - FOR UPDATE - """.execute()(connection).discard -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSuite.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSuite.scala deleted file mode 100644 index a25a862589..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendSuite.scala +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import org.scalatest.flatspec.AnyFlatSpec - -trait StorageBackendSuite - extends StorageBackendTestsInitialization - with StorageBackendTestsInitializeIngestion - with StorageBackendTestsConversions - with StorageBackendTestsParties - with StorageBackendTestsEvents - with StorageBackendTestsCompletions - with StorageBackendTestsContracts - with StorageBackendTestsReset - with StorageBackendTestsPruning - with StorageBackendTestsDBLockForSuite - with StorageBackendTestsIntegrity - with StorageBackendTestsTimestamps - with StorageBackendTestsStringInterning - with StorageBackendTestsUserManagement - with StorageBackendTestsIDPConfig - with StorageBackendTestsPartyRecord - with StorageBackendTestsPartyToParticipant - with StorageBackendTestsQueryValidRange - with StorageBackendTestsParameters { - this: AnyFlatSpec => -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestValues.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestValues.scala deleted file mode 100644 index b0527b522b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestValues.scala +++ /dev/null @@ -1,663 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.crypto.HashAlgorithm.Sha256 -import com.digitalasset.canton.crypto.{Hash as CantonHash, HashPurpose} -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent.Added -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.{ - AuthorizationEvent, - AuthorizationLevel, -} -import com.digitalasset.canton.platform.store.backend.Conversions.{ - authorizationEventInt, - participantPermissionInt, -} -import com.digitalasset.canton.platform.store.dao.JdbcLedgerDao -import com.digitalasset.canton.protocol.{ReassignmentId, TestUpdateId, UpdateId} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.canton.tracing.{SerializableTraceContext, TraceContext} -import com.digitalasset.daml.lf.archive.DamlLf -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.{ - ChoiceName, - Identifier, - NameTypeConRef, - NameTypeConRefConverter, - PackageId, - Party, - UserId, -} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.value.Value.ContractId -import com.google.protobuf.ByteString -import org.scalatest.OptionValues -import scalaz.Tag - -import java.time.Instant -import java.util.UUID - -/** Except where specified, values should be treated as opaque - */ -@SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) -private[store] object StorageBackendTestValues extends OptionValues { - - def hashCid(key: String): ContractId = ContractId.V1(Hash.hashPrivateKey(key)) - - /** Produces offsets that are ordered the same as the input value */ - def offset(x: Long): Offset = Offset.tryFromLong(x) - def ledgerEnd(o: Long, e: Long): ParameterStorageBackend.LedgerEnd = - ParameterStorageBackend.LedgerEnd(offset(o), e, 0, CantonTimestamp.now()) - def updateIdFromOffset(x: Offset): UpdateId = TestUpdateId(x.toDecimalString) - def updateIdArrayFromOffset(x: Offset): Array[Byte] = updateIdFromOffset( - x - ).toProtoPrimitive.toByteArray - - def timestampFromInstant(i: Instant): Timestamp = Timestamp.assertFromInstant(i) - val someTime: Timestamp = timestampFromInstant(Instant.now()) - - val someParticipantId: ParticipantId = ParticipantId( - Ref.ParticipantId.assertFromString("participant") - ) - val somePackageId: Ref.PackageId = Ref.PackageId.assertFromString("pkg") - val someTemplateId: NameTypeConRef = NameTypeConRef.assertFromString("#pkg-name:Mod:Template") - val someInterfaceId: Identifier = Identifier.assertFromString("0abc:Mod:Template") - val someTemplateIdFull: Ref.FullIdentifier = someTemplateId.toFullIdentifier(somePackageId) - val someRepresentativePackageId: Ref.PackageId = - Ref.PackageId.assertFromString("representative-pkg") - val someTemplateId2: NameTypeConRef = NameTypeConRef.assertFromString("#pkg-name:Mod:Template2") - val someIdentityParams: ParameterStorageBackend.IdentityParams = - ParameterStorageBackend.IdentityParams(someParticipantId) - val someChoice: Ref.ChoiceName = Ref.ChoiceName.assertFromString("choice") - val someParty: Ref.Party = Ref.Party.assertFromString("party") - val someParty2: Ref.Party = Ref.Party.assertFromString("party2") - val someParty3: Ref.Party = Ref.Party.assertFromString("party3") - val someParty4: Ref.Party = Ref.Party.assertFromString("party4") - val someParty5: Ref.Party = Ref.Party.assertFromString("party5") - def someParties(names: String*): Set[Ref.Party] = Set(names.map(Ref.Party.assertFromString)*) - val someParticipant: Ref.ParticipantId = Ref.ParticipantId.assertFromString("participant1") - val someUserId: Ref.UserId = Ref.UserId.assertFromString("user_id") - val someSubmissionId: Ref.SubmissionId = Ref.SubmissionId.assertFromString("submission_id") - val someAuthenticationData: Bytes = Bytes.assertFromString("00abcd") - val someAuthenticationDataBytes: Array[Byte] = someAuthenticationData.toByteArray - - val someArchive: DamlLf.Archive = DamlLf.Archive.newBuilder - .setHash("00001") - .setHashFunction(DamlLf.HashFunction.SHA256) - .setPayload(ByteString.copyFromUtf8("payload 1")) - .build - - val someSerializedDamlLfValue: Array[Byte] = Array.empty[Byte] - val someSynchronizerId: SynchronizerId = SynchronizerId.tryFromString("x::sourcesynchronizer") - val someSynchronizerId2: SynchronizerId = SynchronizerId.tryFromString("x::targetsynchronizer") - - val testTraceContext = TraceContext.withNewTraceContext("test trace context")(identity) - val serializableTraceContext: Array[Byte] = - SerializableTraceContext(testTraceContext).toSerializedDamlProto - val someExternalTransactionHash: CantonHash = - CantonHash - .digest(HashPurpose.PreparedSubmission, ByteString.copyFromUtf8("mock_hash"), Sha256) - val someExternalTransactionHashBinary: Array[Byte] = - someExternalTransactionHash.getCryptographicEvidence.toByteArray - val reassignmentId: Array[Byte] = - ReassignmentId.create("0012345678").toOption.get.toBytes.toByteArray - - def dtoPartyEntry( - offset: Offset, - party: Party = someParty, - isLocal: Boolean = true, - reject: Boolean = false, - ): DbDto.PartyEntry = - DbDto.PartyEntry( - ledger_offset = offset.unwrap, - recorded_at = someTime.micros, - submission_id = Some("submission_id"), - party = Some(party), - typ = if (reject) JdbcLedgerDao.rejectType else JdbcLedgerDao.acceptType, - rejection_reason = Option.when(reject)("some rejection reason"), - is_local = Some(isLocal), - ) - - def dtosCreate( - // update related columns - event_offset: Long = 10L, - update_id: Array[Byte] = TestUpdateId("update").toProtoPrimitive.toByteArray, - workflow_id: Option[String] = Some("workflow-id"), - command_id: Option[String] = Some("command-id"), - submitters: Option[Set[Party]] = Some( - Set("submitter1", "submitter2").map(Party.assertFromString) - ), - record_time: Long = 100L, - synchronizer_id: SynchronizerId = someSynchronizerId, - trace_context: Array[Byte] = serializableTraceContext, - external_transaction_hash: Option[Array[Byte]] = Some(someExternalTransactionHashBinary), - traffic_cost: Option[Long] = Some(8465L), - - // event related columns - event_sequential_id: Long = 500L, - node_id: Int = 15, - additional_witnesses: Set[Party] = Set("witness1", "witness2").map(Party.assertFromString), - representative_package_id: PackageId = PackageId.assertFromString("representativepackage"), - - // contract related columns - notPersistedContractId: ContractId = hashCid("c1"), - internal_contract_id: Long = 10, - create_key_hash: Option[String] = Some("keyhash"), - )( - stakeholders: Set[Party] = Set("stakeholder1", "stakeholder2").map(Party.assertFromString), - template_id: NameTypeConRef = NameTypeConRef.assertFromString("#tem:pl:ate"), - ): Seq[DbDto] = DbDto - .createDbDtos( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - traffic_cost = traffic_cost, - event_sequential_id = event_sequential_id, - node_id = node_id, - additional_witnesses = additional_witnesses, - representative_package_id = representative_package_id, - notPersistedContractId = notPersistedContractId, - internal_contract_id = internal_contract_id, - create_key_hash = create_key_hash, - )( - stakeholders = stakeholders, - template_id = template_id, - ) - .toSeq - - def dtosAssign( - // update related columns - event_offset: Long = 10L, - update_id: Array[Byte] = TestUpdateId("update").toProtoPrimitive.toByteArray, - workflow_id: Option[String] = Some("workflow-id"), - command_id: Option[String] = Some("command-id"), - submitter: Option[Party] = Some(Party.assertFromString("submitter1")), - record_time: Long = 100L, - synchronizer_id: SynchronizerId = someSynchronizerId, - trace_context: Array[Byte] = serializableTraceContext, - traffic_cost: Option[Long] = Some(8465L), - - // event related columns - event_sequential_id: Long = 500L, - node_id: Int = 15, - source_synchronizer_id: SynchronizerId = someSynchronizerId2, - reassignment_counter: Long = 345, - reassignment_id: Array[Byte] = reassignmentId, - representative_package_id: PackageId = PackageId.assertFromString("representativepackage"), - - // contract related columns - notPersistedContractId: ContractId = hashCid("c1"), - internal_contract_id: Long = 10, - create_key_hash: Option[String] = Some("keyhash"), - )( - stakeholders: Set[Party] = Set("stakeholder1", "stakeholder2").map(Party.assertFromString), - template_id: NameTypeConRef = NameTypeConRef.assertFromString("#tem:pl:ate"), - ): Seq[DbDto] = DbDto - .assignDbDtos( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitter = submitter, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - traffic_cost = traffic_cost, - event_sequential_id = event_sequential_id, - node_id = node_id, - source_synchronizer_id = source_synchronizer_id, - reassignment_counter = reassignment_counter, - reassignment_id = reassignment_id, - representative_package_id = representative_package_id, - notPersistedContractId = notPersistedContractId, - internal_contract_id = internal_contract_id, - create_key_hash = create_key_hash, - )( - stakeholders = stakeholders, - template_id = template_id, - ) - .toSeq - - def dtosConsumingExercise( - // update related columns - event_offset: Long = 10L, - update_id: Array[Byte] = TestUpdateId("update").toProtoPrimitive.toByteArray, - workflow_id: Option[String] = Some("workflow-id"), - command_id: Option[String] = Some("command-id"), - submitters: Option[Set[Party]] = Some( - Set("submitter1", "submitter2").map(Party.assertFromString) - ), - record_time: Long = 100L, - synchronizer_id: SynchronizerId = someSynchronizerId, - trace_context: Array[Byte] = serializableTraceContext, - external_transaction_hash: Option[Array[Byte]] = Some(someExternalTransactionHashBinary), - traffic_cost: Option[Long] = Some(8465L), - - // event related columns - event_sequential_id: Long = 500L, - node_id: Int = 15, - deactivated_event_sequential_id: Option[Long] = Some(2L), - additional_witnesses: Set[Party] = Set("witness1", "witness2").map(Party.assertFromString), - exercise_choice: ChoiceName = ChoiceName.assertFromString("choice"), - exercise_choice_interface_id: Option[Identifier] = Some( - Identifier.assertFromString("in:ter:face") - ), - exercise_argument: Array[Byte] = Array(1, 2, 3), - exercise_result: Option[Array[Byte]] = Some(Array(2, 3, 4)), - exercise_actors: Set[Party] = Set("actor1", "actor2").map(Party.assertFromString), - exercise_last_descendant_node_id: Int = 3, - exercise_argument_compression: Option[Int] = Some(1), - exercise_result_compression: Option[Int] = Some(2), - - // contract related columns - contract_id: ContractId = hashCid("c1"), - internal_contract_id: Option[Long] = Some(10), - template_id: NameTypeConRef = NameTypeConRef.assertFromString("#tem:pl:ate"), - package_id: PackageId = PackageId.assertFromString("package"), - stakeholders: Set[Party] = Set("stakeholder1", "stakeholder2").map(Party.assertFromString), - ledger_effective_time: Long = 123456, - ): Seq[DbDto] = DbDto - .consumingExerciseDbDtos( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - traffic_cost = traffic_cost, - event_sequential_id = event_sequential_id, - node_id = node_id, - deactivated_event_sequential_id = deactivated_event_sequential_id, - additional_witnesses = additional_witnesses, - exercise_choice = exercise_choice, - exercise_choice_interface_id = exercise_choice_interface_id, - exercise_argument = exercise_argument, - exercise_result = exercise_result, - exercise_actors = exercise_actors, - exercise_last_descendant_node_id = exercise_last_descendant_node_id, - exercise_argument_compression = exercise_argument_compression, - exercise_result_compression = exercise_result_compression, - contract_id = contract_id, - internal_contract_id = internal_contract_id, - template_id = template_id, - package_id = package_id, - stakeholders = stakeholders, - ledger_effective_time = ledger_effective_time, - ) - .toSeq - - def dtosUnassign( - // update related columns - event_offset: Long = 10L, - update_id: Array[Byte] = TestUpdateId("update").toProtoPrimitive.toByteArray, - workflow_id: Option[String] = Some("workflow-id"), - command_id: Option[String] = Some("command-id"), - submitter: Option[Party] = Some(Party.assertFromString("submitter1")), - record_time: Long = 100L, - synchronizer_id: SynchronizerId = someSynchronizerId, - trace_context: Array[Byte] = serializableTraceContext, - traffic_cost: Option[Long] = Some(8465L), - - // event related columns - event_sequential_id: Long = 500L, - node_id: Int = 15, - deactivated_event_sequential_id: Option[Long] = Some(67), - reassignment_id: Array[Byte] = reassignmentId, - assignment_exclusivity: Option[Long] = Some(111333), - target_synchronizer_id: SynchronizerId = someSynchronizerId2, - reassignment_counter: Long = 345, - - // contract related columns - contract_id: ContractId = hashCid("c1"), - internal_contract_id: Option[Long] = Some(10), - template_id: NameTypeConRef = NameTypeConRef.assertFromString("#tem:pl:ate"), - package_id: PackageId = PackageId.assertFromString("package"), - stakeholders: Set[Party] = Set("stakeholder1", "stakeholder2").map(Party.assertFromString), - ): Seq[DbDto] = DbDto - .unassignDbDtos( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitter = submitter, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - traffic_cost = traffic_cost, - event_sequential_id = event_sequential_id, - node_id = node_id, - deactivated_event_sequential_id = deactivated_event_sequential_id, - reassignment_id = reassignment_id, - assignment_exclusivity = assignment_exclusivity, - target_synchronizer_id = target_synchronizer_id, - reassignment_counter = reassignment_counter, - contract_id = contract_id, - internal_contract_id = internal_contract_id, - template_id = template_id, - package_id = package_id, - stakeholders = stakeholders, - ) - .toSeq - - def dtosWitnessedCreate( - // update related columns - event_offset: Long = 10L, - update_id: Array[Byte] = TestUpdateId("update").toProtoPrimitive.toByteArray, - workflow_id: Option[String] = Some("workflow-id"), - command_id: Option[String] = Some("command-id"), - submitters: Option[Set[Party]] = Some( - Set("submitter1", "submitter2").map(Party.assertFromString) - ), - record_time: Long = 100L, - synchronizer_id: SynchronizerId = someSynchronizerId, - trace_context: Array[Byte] = serializableTraceContext, - external_transaction_hash: Option[Array[Byte]] = Some(someExternalTransactionHashBinary), - traffic_cost: Option[Long] = Some(8465L), - - // event related columns - event_sequential_id: Long = 500L, - node_id: Int = 15, - additional_witnesses: Set[Party] = Set("witness1", "witness2").map(Party.assertFromString), - representative_package_id: PackageId = PackageId.assertFromString("representativepackage"), - - // contract related columns - internal_contract_id: Long = 10, - )(template_id: NameTypeConRef = NameTypeConRef.assertFromString("#tem:pl:ate")): Seq[DbDto] = - DbDto - .witnessedCreateDbDtos( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - event_sequential_id = event_sequential_id, - node_id = node_id, - additional_witnesses = additional_witnesses, - representative_package_id = representative_package_id, - internal_contract_id = internal_contract_id, - traffic_cost = traffic_cost, - )( - template_id = template_id - ) - .toSeq - - def dtosWitnessedExercised( - // update related columns - event_offset: Long = 10L, - update_id: Array[Byte] = TestUpdateId("update").toProtoPrimitive.toByteArray, - workflow_id: Option[String] = Some("workflow-id"), - command_id: Option[String] = Some("command-id"), - submitters: Option[Set[Party]] = Some( - Set("submitter1", "submitter2").map(Party.assertFromString) - ), - record_time: Long = 100L, - synchronizer_id: SynchronizerId = someSynchronizerId, - trace_context: Array[Byte] = serializableTraceContext, - external_transaction_hash: Option[Array[Byte]] = Some(someExternalTransactionHashBinary), - traffic_cost: Option[Long] = Some(8465L), - - // event related columns - event_sequential_id: Long = 500L, - node_id: Int = 15, - additional_witnesses: Set[Party] = Set("witness1", "witness2").map(Party.assertFromString), - consuming: Boolean = true, - exercise_choice: ChoiceName = someChoice, - exercise_choice_interface_id: Option[Identifier] = Some( - Identifier.assertFromString("in:ter:face") - ), - exercise_argument: Array[Byte] = Array(1, 2, 3), - exercise_result: Option[Array[Byte]] = Some(Array(2, 3, 4)), - exercise_actors: Set[Party] = Set("actor1", "actor2").map(Party.assertFromString), - exercise_last_descendant_node_id: Int = 3, - exercise_argument_compression: Option[Int] = Some(1), - exercise_result_compression: Option[Int] = Some(2), - - // contract related columns - contract_id: ContractId = hashCid("c1"), - internal_contract_id: Option[Long] = Some(10), - template_id: NameTypeConRef = NameTypeConRef.assertFromString("#tem:pl:ate"), - package_id: PackageId = PackageId.assertFromString("package"), - ledger_effective_time: Long = 123456, - ): Seq[DbDto] = DbDto - .witnessedExercisedDbDtos( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - event_sequential_id = event_sequential_id, - node_id = node_id, - additional_witnesses = additional_witnesses, - consuming = consuming, - exercise_choice = exercise_choice, - exercise_choice_interface_id = exercise_choice_interface_id, - exercise_argument = exercise_argument, - exercise_result = exercise_result, - exercise_actors = exercise_actors, - exercise_last_descendant_node_id = exercise_last_descendant_node_id, - exercise_argument_compression = exercise_argument_compression, - exercise_result_compression = exercise_result_compression, - contract_id = contract_id, - internal_contract_id = internal_contract_id, - template_id = template_id, - package_id = package_id, - ledger_effective_time = ledger_effective_time, - traffic_cost = traffic_cost, - ) - .toSeq - - def dtoPartyToParticipant( - offset: Offset, - eventSequentialId: Long, - party: Party = someParty, - participant: ParticipantId = someParticipantId, - authorizationEvent: AuthorizationEvent = Added(AuthorizationLevel.Submission), - synchronizerId: SynchronizerId = someSynchronizerId, - recordTime: Timestamp = someTime, - traceContext: Array[Byte] = serializableTraceContext, - ): DbDto.EventPartyToParticipant = { - val updateId = updateIdArrayFromOffset(offset) - DbDto.EventPartyToParticipant( - event_sequential_id = eventSequentialId, - event_offset = offset.unwrap, - update_id = updateId, - party_id = party, - participant_id = Tag.unwrap(participant), - participant_permission = participantPermissionInt(authorizationEvent), - participant_authorization_event = authorizationEventInt(authorizationEvent), - synchronizer_id = synchronizerId, - record_time = recordTime.micros, - trace_context = traceContext, - ) - } - - def dtoCompletion( - offset: Offset, - submitters: Set[Party] = Set(Party.assertFromString("signatory")), - commandId: String = UUID.randomUUID().toString, - userId: UserId = someUserId, - submissionId: Option[String] = Some(UUID.randomUUID().toString), - deduplicationOffset: Option[Long] = None, - deduplicationDurationSeconds: Option[Long] = None, - deduplicationDurationNanos: Option[Int] = None, - synchronizerId: SynchronizerId = someSynchronizerId, - traceContext: Array[Byte] = serializableTraceContext, - recordTime: Timestamp = someTime, - messageUuid: Option[String] = None, - updateId: Option[Array[Byte]] = Some(new Array[Byte](0)), - publicationTime: Timestamp = someTime, - isTransaction: Boolean = true, - trafficCost: Long = 0L, - ): DbDto.CommandCompletion = - DbDto.CommandCompletion( - completion_offset = offset.unwrap, - record_time = recordTime.micros, - publication_time = publicationTime.micros, - user_id = userId, - submitters = submitters, - command_id = commandId, - update_id = updateId.filter(_.isEmpty).map(_ => updateIdArrayFromOffset(offset)), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = submissionId, - deduplication_offset = deduplicationOffset, - deduplication_duration_seconds = deduplicationDurationSeconds, - deduplication_duration_nanos = deduplicationDurationNanos, - synchronizer_id = synchronizerId, - message_uuid = messageUuid, - is_transaction = isTransaction, - trace_context = traceContext, - traffic_cost = trafficCost, - ) - - def dtoTransactionMeta( - offset: Offset, - event_sequential_id_first: Long, - event_sequential_id_last: Long, - recordTime: Timestamp = someTime, - udpateId: Option[Array[Byte]] = None, - synchronizerId: SynchronizerId = someSynchronizerId, - publicationTime: Timestamp = someTime, - ): DbDto.TransactionMeta = DbDto.TransactionMeta( - update_id = udpateId.getOrElse(updateIdArrayFromOffset(offset)), - event_offset = offset.unwrap, - publication_time = publicationTime.micros, - record_time = recordTime.micros, - synchronizer_id = synchronizerId, - event_sequential_id_first = event_sequential_id_first, - event_sequential_id_last = event_sequential_id_last, - ) - - def dtoInterning( - internal: Int, - external: String, - ): DbDto.StringInterningDto = DbDto.StringInterningDto( - internalId = internal, - externalString = external, - ) - - def dtoUpdateId(dto: DbDto): UpdateId = - dto match { - case _ => sys.error(s"$dto does not have a transaction id") - } - - def dtoEventSeqId(dto: DbDto): Long = - dto match { - case e: DbDto.EventActivate => e.event_sequential_id - case e: DbDto.EventDeactivate => e.event_sequential_id - case e: DbDto.EventVariousWitnessed => e.event_sequential_id - case e: DbDto.IdFilterDbDto => e.idFilter.event_sequential_id - case _ => sys.error(s"$dto does not have a event sequential id") - } - - def dtoOffset(dto: DbDto): Long = - dto match { - case _ => sys.error(s"$dto does not have a offset id") - } - - def dtoUserId(dto: DbDto): Ref.UserId = - dto match { - case e: DbDto.CommandCompletion => Ref.UserId.assertFromString(e.user_id) - case _ => sys.error(s"$dto does not have an user id") - } - - def metaFromSingle(dbDto: DbDto): DbDto.TransactionMeta = DbDto.TransactionMeta( - update_id = dtoUpdateId(dbDto).toProtoPrimitive.toByteArray, - event_offset = dtoOffset(dbDto), - publication_time = someTime.micros, - record_time = someTime.micros, - synchronizer_id = someSynchronizerId, - event_sequential_id_first = dtoEventSeqId(dbDto), - event_sequential_id_last = dtoEventSeqId(dbDto), - ) - - def meta( - // update related columns - event_offset: Long = 10L, - update_id: Array[Byte] = TestUpdateId("update").toProtoPrimitive.toByteArray, - workflow_id: Option[String] = Some("workflow-id"), - command_id: Option[String] = Some("command-id"), - submitters: Option[Set[Party]] = Some(Set(Party.assertFromString("submitter1"))), - record_time: Long = 100L, - synchronizer_id: SynchronizerId = someSynchronizerId, - trace_context: Array[Byte] = serializableTraceContext, - external_transaction_hash: Option[Array[Byte]] = Some(someExternalTransactionHashBinary), - // meta related columns - publication_time: Long = 1000, - )(dbDtosInOrder: Seq[DbDto]): Vector[DbDto] = - dbDtosInOrder - .map { - case dto: DbDto.EventActivate => - dto.copy( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - ) - case dto: DbDto.EventDeactivate => - dto.copy( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - ) - case dto: DbDto.EventVariousWitnessed => - dto.copy( - event_offset = event_offset, - update_id = update_id, - workflow_id = workflow_id, - command_id = command_id, - submitters = submitters, - record_time = record_time, - synchronizer_id = synchronizer_id, - trace_context = trace_context, - external_transaction_hash = external_transaction_hash, - ) - case x => x - } - .toVector - .appended( - DbDto.TransactionMeta( - event_offset = event_offset, - update_id = update_id, - record_time = record_time, - synchronizer_id = synchronizer_id, - publication_time = publication_time, - event_sequential_id_first = dtoEventSeqId(dbDtosInOrder.headOption.value), - event_sequential_id_last = dtoEventSeqId(dbDtosInOrder.lastOption.value), - ) - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsCompletions.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsCompletions.scala deleted file mode 100644 index 0c75fa2534..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsCompletions.scala +++ /dev/null @@ -1,434 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.platform.indexer.parallel.{PostPublishData, PublishSource} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.canton.tracing.{SerializableTraceContext, TraceContext} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.google.protobuf.duration.Duration -import org.scalatest.Inside -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.UUID - -private[backend] trait StorageBackendTestsCompletions - extends Matchers - with Inside - with StorageBackendSpec { - this: AnyFlatSpec => - - behavior of "StorageBackend (completions)" - - import StorageBackendTestValues.* - - it should "correctly find completions by offset range" in { - TraceContext.withNewTraceContext("test") { aTraceContext => - val party = someParty - val userId = someUserId - val emptyTraceContext = SerializableTraceContext(TraceContext.empty).toSerializedDamlProto - val serializableTraceContext = SerializableTraceContext(aTraceContext).toSerializedDamlProto - - val dtos = Vector( - dtoCompletion(offset(1), submitters = Set(party)), - dtoCompletion(offset(2), submitters = Set(party), traceContext = emptyTraceContext), - dtoCompletion( - offset(3), - submitters = Set(party), - traceContext = serializableTraceContext, - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(3), 3L)) - val completions0to2 = executeSql( - backend.completion - .commandCompletions( - Offset.firstOffset, - offset(2), - userId, - Set(party), - limit = 10, - ) - ) - val completions1to2 = executeSql( - backend.completion - .commandCompletions( - offset(2), - offset(2), - userId, - Set(party), - limit = 10, - ) - ) - val completions0to9 = executeSql( - backend.completion - .commandCompletions( - Offset.firstOffset, - offset(9), - userId, - Set(party), - limit = 10, - ) - ) - - completions0to2 should have length 2 - completions1to2 should have length 1 - completions0to9 should have length 3 - - completions0to9.head.completionResponse.completion.map(_.traceContext) shouldBe Some( - SerializableTraceContext(testTraceContext).toDamlProto - ) - completions0to9(1).completionResponse.completion.map(_.traceContext) shouldBe Some(None) - completions0to9(2).completionResponse.completion.map(_.traceContext) shouldBe Some( - SerializableTraceContext(aTraceContext).toDamlProto - ) - } - } - - it should "correctly persist and retrieve user IDs" in { - val party = someParty - val userId = someUserId - - val dtos = Vector( - dtoCompletion(offset(1), submitters = Set(party)) - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(1), 1L)) - - val completions = executeSql( - backend.completion - .commandCompletions( - Offset.firstOffset, - offset(1), - userId, - Set(party), - limit = 10, - ) - ) - - completions should not be empty - completions.head.completionResponse.completion should not be empty - completions.head.completionResponse.completion.toList.head.userId should be( - userId - ) - } - - it should "correctly persist and retrieve submission IDs" in { - val party = someParty - val submissionId = Some(someSubmissionId) - - val dtos = Vector( - dtoCompletion(offset(1), submitters = Set(party), submissionId = submissionId), - dtoCompletion(offset(2), submitters = Set(party), submissionId = None), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - val completions = executeSql( - backend.completion - .commandCompletions( - Offset.firstOffset, - offset(2), - someUserId, - Set(party), - limit = 10, - ) - ).toList - - completions should have length 2 - inside(completions) { case List(completionWithSubmissionId, completionWithoutSubmissionId) => - completionWithSubmissionId.completionResponse.completion should not be empty - completionWithSubmissionId.completionResponse.completion.toList.head.submissionId should be( - someSubmissionId - ) - completionWithoutSubmissionId.completionResponse.completion should not be empty - completionWithoutSubmissionId.completionResponse.completion.toList.head.submissionId should be( - "" - ) - } - } - - it should "correctly persist and retrieve command deduplication offsets" in { - val party = someParty - val anOffset = 1L - - val dtos = Vector( - dtoCompletion( - offset(1), - submitters = Set(party), - deduplicationOffset = Some(anOffset), - ), - dtoCompletion(offset(2), submitters = Set(party), deduplicationOffset = None), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - - executeSql(updateLedgerEnd(offset(2), 2L)) - val completions = executeSql( - backend.completion - .commandCompletions( - Offset.firstOffset, - offset(2), - someUserId, - Set(party), - limit = 10, - ) - ).toList - - completions should have length 2 - inside(completions) { - case List(completionWithDeduplicationOffset, completionWithoutDeduplicationOffset) => - completionWithDeduplicationOffset.completionResponse.completion should not be empty - completionWithDeduplicationOffset.completionResponse.completion.toList.head.deduplicationPeriod.deduplicationOffset should be( - Some(anOffset) - ) - completionWithoutDeduplicationOffset.completionResponse.completion should not be empty - completionWithoutDeduplicationOffset.completionResponse.completion.toList.head.deduplicationPeriod.deduplicationOffset should not be defined - } - } - - it should "correctly persist and retrieve command deduplication durations" in { - val party = someParty - val seconds = 100L - val nanos = 10 - val expectedDuration = Duration.of(seconds, nanos) - - val dtos = Vector( - dtoCompletion( - offset(1), - submitters = Set(party), - deduplicationDurationSeconds = Some(seconds), - deduplicationDurationNanos = Some(nanos), - ), - dtoCompletion( - offset(2), - submitters = Set(party), - deduplicationDurationSeconds = None, - deduplicationDurationNanos = None, - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - - executeSql(updateLedgerEnd(offset(2), 2L)) - val completions = executeSql( - backend.completion - .commandCompletions( - Offset.firstOffset, - offset(2), - someUserId, - Set(party), - limit = 10, - ) - ).toList - - completions should have length 2 - inside(completions) { - case List(completionWithDeduplicationOffset, completionWithoutDeduplicationOffset) => - completionWithDeduplicationOffset.completionResponse.completion should not be empty - completionWithDeduplicationOffset.completionResponse.completion.toList.head.deduplicationPeriod.deduplicationDuration should be( - Some(expectedDuration) - ) - completionWithoutDeduplicationOffset.completionResponse.completion should not be empty - completionWithoutDeduplicationOffset.completionResponse.completion.toList.head.deduplicationPeriod.deduplicationDuration should not be defined - } - } - - it should "correctly persist and retrieve submitters/act_as" in { - val party = someParty - val party2 = someParty2 - val party3 = someParty3 - - val dtos = Vector( - dtoCompletion( - offset(1), - submitters = Set(party, party2, party3), - ), - dtoCompletion( - offset(2), - submitters = Set(party), - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - - executeSql(updateLedgerEnd(offset(2), 2L)) - val completions = executeSql( - backend.completion - .commandCompletions( - Offset.firstOffset, - offset(2), - someUserId, - Set(party, party2), - limit = 10, - ) - ).toList - - completions should have length 2 - inside(completions) { case List(completion1, completion2) => - completion1.completionResponse.completion should not be empty - completion1.completionResponse.completion.toList.head.actAs.toSet should be( - Set(party, party2) - ) - completion2.completionResponse.completion should not be empty - completion2.completionResponse.completion.toList.head.actAs.toSet should be( - Set(party) - ) - } - } - - it should "fail on broken command deduplication durations in DB" in { - val party = someParty - val seconds = 100L - val nanos = 10 - - val expectedErrorMessage = - "One of deduplication duration seconds and nanos has been provided " + - "but they must be either both provided or both absent" - - val dtos1 = Vector( - dtoCompletion( - offset(1), - submitters = Set(party), - deduplicationDurationSeconds = Some(seconds), - deduplicationDurationNanos = None, - ) - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos1, _)) - executeSql(updateLedgerEnd(offset(1), 1L)) - val caught = intercept[IllegalArgumentException]( - executeSql( - backend.completion.commandCompletions( - Offset.firstOffset, - offset(1), - someUserId, - Set(party), - limit = 10, - ) - ) - ) - - caught.getMessage should be(expectedErrorMessage) - - val dtos2 = Vector( - dtoCompletion( - offset(2), - submitters = Set(party), - deduplicationDurationSeconds = None, - deduplicationDurationNanos = Some(nanos), - ) - ) - - executeSql(ingest(dtos2, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - val caught2 = intercept[IllegalArgumentException]( - executeSql( - backend.completion.commandCompletions( - offset(2), - offset(2), - someUserId, - Set(party), - limit = 10, - ) - ) - ) - caught2.getMessage should be(expectedErrorMessage) - } - - it should "correctly retrieve completions for post processing recovery" in { - val messageUuid = UUID.randomUUID() - val commandId = UUID.randomUUID().toString - val publicationTime = Timestamp.now() - val recordTime = Timestamp.now().addMicros(15) - val submissionId = UUID.randomUUID().toString - val synchronizerId = SynchronizerId.tryFromString("x::synchronizer1") - val dtos = Vector( - dtoCompletion( - offset(1) - ), - dtoCompletion( - offset = offset(2), - submitters = Set(someParty), - commandId = commandId, - userId = Ref.UserId.assertFromString("userid1"), - submissionId = Some(submissionId), - synchronizerId = synchronizerId, - messageUuid = Some(messageUuid.toString), - publicationTime = publicationTime, - isTransaction = true, - ), - dtoCompletion( - offset = offset(5), - submitters = Set(someParty), - commandId = commandId, - userId = Ref.UserId.assertFromString("userid1"), - submissionId = Some(submissionId), - synchronizerId = synchronizerId, - messageUuid = Some(messageUuid.toString), - publicationTime = publicationTime, - isTransaction = false, - ), - dtoCompletion( - offset = offset(9), - submitters = Set(someParty), - commandId = commandId, - userId = Ref.UserId.assertFromString("userid1"), - submissionId = Some(submissionId), - synchronizerId = synchronizerId, - recordTime = recordTime, - messageUuid = None, - updateId = None, - publicationTime = publicationTime, - isTransaction = true, - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - backend.completion.commandCompletionsForRecovery(offset(2), offset(10)) - ) shouldBe Vector( - PostPublishData( - submissionSynchronizerId = SynchronizerId.tryFromString("x::synchronizer1"), - publishSource = PublishSource.Local(messageUuid), - userId = Ref.UserId.assertFromString("userid1"), - commandId = Ref.CommandId.assertFromString(commandId), - actAs = Set(someParty), - offset = offset(2), - publicationTime = CantonTimestamp(publicationTime), - submissionId = Some(Ref.SubmissionId.assertFromString(submissionId)), - accepted = true, - traceContext = testTraceContext, - ), - PostPublishData( - submissionSynchronizerId = SynchronizerId.tryFromString("x::synchronizer1"), - publishSource = PublishSource.Sequencer( - sequencerTimestamp = CantonTimestamp(recordTime) - ), - userId = Ref.UserId.assertFromString("userid1"), - commandId = Ref.CommandId.assertFromString(commandId), - actAs = Set(someParty), - offset = offset(9), - publicationTime = CantonTimestamp(publicationTime), - submissionId = Some(Ref.SubmissionId.assertFromString(submissionId)), - accepted = false, - traceContext = testTraceContext, - ), - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsContracts.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsContracts.scala deleted file mode 100644 index c06cacec8d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsContracts.scala +++ /dev/null @@ -1,959 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.platform.store.backend.ContractStorageBackend.{ - KeysPageQuery, - KeysPageResult, -} -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.Identifier -import com.digitalasset.daml.lf.transaction.GlobalKey -import com.digitalasset.daml.lf.value.Value.{ValueText, ValueUnit} -import org.scalatest.Inside -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -private[backend] trait StorageBackendTestsContracts - extends Matchers - with Inside - with StorageBackendSpec { - this: AnyFlatSpec => - - import StorageBackendTestValues.* - - behavior of "StorageBackend (contracts)" - - it should "correctly find key states using non-unique key lookup with limit 1" in { - val key1 = GlobalKey.assertBuild( - Identifier.assertFromString("A:B:C"), - someTemplateId.pkg.name, - ValueUnit, - crypto.Hash.hashPrivateKey("dummy-key-hash-1"), - ) - val key2 = GlobalKey.assertBuild( - Identifier.assertFromString("A:B:C"), - someTemplateId.pkg.name, - ValueText("value"), - crypto.Hash.hashPrivateKey("dummy-key-hash-2"), - ) - val internalContractId = 123L - val internalContractId2 = 223L - val internalContractId3 = 323L - val internalContractId4 = 423L - val signatory = Ref.Party.assertFromString("signatory") - - val dtos: Vector[DbDto] = Vector( - dtosCreate( - event_offset = 1L, - event_sequential_id = 1L, - internal_contract_id = internalContractId4, - create_key_hash = Some(key2.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosCreate( - event_offset = 2L, - event_sequential_id = 2L, - internal_contract_id = internalContractId, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosCreate( - event_offset = 3L, - event_sequential_id = 3L, - internal_contract_id = internalContractId2, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosConsumingExercise( - event_offset = 4L, - event_sequential_id = 4L, - internal_contract_id = Some(internalContractId2), - deactivated_event_sequential_id = Some(3L), - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosCreate( - event_offset = 5L, - event_sequential_id = 5L, - internal_contract_id = internalContractId3, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosConsumingExercise( - event_offset = 6L, - event_sequential_id = 6L, - internal_contract_id = Some(internalContractId4), - deactivated_event_sequential_id = Some(1L), - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - updateLedgerEnd(offset(6), 6L) - ) - - def lookupKeyState(key: GlobalKey, validAt: Long): Option[Long] = - executeSql( - backend.contract.contractKey( - KeysPageQuery(key = key, validAtEventSeqId = validAt, limit = 1, nextPageToken = None) - ) - ).internalContractIds.headOption - - def lookupKeyStates(keys: List[GlobalKey], validAt: Long): Map[GlobalKey, Long] = { - val queries = keys.map(key => - KeysPageQuery(key = key, validAtEventSeqId = validAt, limit = 1, nextPageToken = None) - ) - val results = executeSql( - backend.contract.contractKeysPlain(queries, validAt) - ) - keys - .zip(results) - .flatMap { case (key, result) => - result.internalContractIds.headOption.map(key -> _) - } - .toMap - } - - val keyStates2 = lookupKeyStates(List(key1, key2), 2L) - val keyStateKey1_2 = lookupKeyState(key1, 2L) - val keyStateKey2_2 = lookupKeyState(key2, 2L) - val keyStates3 = lookupKeyStates(List(key1, key2), 3L) - val keyStateKey1_3 = lookupKeyState(key1, 3L) - val keyStateKey2_3 = lookupKeyState(key2, 3L) - val keyStates4 = lookupKeyStates(List(key1, key2), 4L) - val keyStateKey1_4 = lookupKeyState(key1, 4L) - val keyStateKey2_4 = lookupKeyState(key2, 4L) - val keyStates5 = lookupKeyStates(List(key1, key2), 5L) - val keyStateKey1_5 = lookupKeyState(key1, 5L) - val keyStateKey2_5 = lookupKeyState(key2, 5L) - val keyStates6 = lookupKeyStates(List(key1, key2), 6L) - val keyStateKey1_6 = lookupKeyState(key1, 6L) - val keyStateKey2_6 = lookupKeyState(key2, 6L) - - keyStates2 shouldBe Map( - key1 -> internalContractId, - key2 -> internalContractId4, - ) - keyStateKey1_2 shouldBe Some(internalContractId) - keyStateKey2_2 shouldBe Some(internalContractId4) - keyStates3 shouldBe Map( - key1 -> internalContractId2, - key2 -> internalContractId4, - ) - keyStateKey1_3 shouldBe Some(internalContractId2) - keyStateKey2_3 shouldBe Some(internalContractId4) - keyStates4 shouldBe Map( - key1 -> internalContractId, - key2 -> internalContractId4, - ) - keyStateKey1_4 shouldBe Some(internalContractId) - keyStateKey2_4 shouldBe Some(internalContractId4) - keyStates5 shouldBe Map( - key1 -> internalContractId3, - key2 -> internalContractId4, - ) - keyStateKey1_5 shouldBe Some(internalContractId3) - keyStateKey2_5 shouldBe Some(internalContractId4) - keyStates6 shouldBe Map( - key1 -> internalContractId3 - ) - keyStateKey1_6 shouldBe Some(internalContractId3) - keyStateKey2_6 shouldBe None - } - - it should "correctly find non unique contract key contracts" in { - val key1 = GlobalKey.assertBuild( - Identifier.assertFromString("A:B:C"), - someTemplateId.pkg.name, - ValueUnit, - crypto.Hash.hashPrivateKey("1"), - ) - val key2 = GlobalKey.assertBuild( - Identifier.assertFromString("A:B:C"), - someTemplateId.pkg.name, - ValueText("value"), - keyHash = crypto.Hash.hashPrivateKey("2"), - ) - val internalContractId = 123L - val internalContractId2 = 223L - val internalContractId3 = 323L - val internalContractId4 = 423L - val signatory = Ref.Party.assertFromString("signatory") - - val dtos: Vector[DbDto] = Vector( - dtosCreate( - event_offset = 1L, - event_sequential_id = 1L, - internal_contract_id = internalContractId4, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosCreate( - event_offset = 2L, - event_sequential_id = 2L, - internal_contract_id = internalContractId, - create_key_hash = Some(key2.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosCreate( - event_offset = 3L, - event_sequential_id = 3L, - internal_contract_id = internalContractId2, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosConsumingExercise( - event_offset = 4L, - event_sequential_id = 4L, - internal_contract_id = Some(internalContractId2), - stakeholders = Set(signatory), - template_id = someTemplateId, - deactivated_event_sequential_id = Some(3L), - ), - dtosCreate( - event_offset = 5L, - event_sequential_id = 5L, - internal_contract_id = internalContractId3, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - updateLedgerEnd(offset(5), 5L) - ) - - executeSql( - backend.contract.contractKey( - KeysPageQuery( - key = key1, - limit = 1, - nextPageToken = None, - validAtEventSeqId = 5, - ) - ) - ) shouldBe KeysPageResult( - internalContractIds = Vector(internalContractId3), - nextPageToken = Some(2L), - ) - - executeSql( - backend.contract.contractKey( - KeysPageQuery( - key = key1, - limit = 1, - nextPageToken = Some(5L), - validAtEventSeqId = 5, - ) - ) - ) shouldBe KeysPageResult( - internalContractIds = Vector(internalContractId4), - nextPageToken = None, - ) - - executeSql( - backend.contract.contractKey( - KeysPageQuery( - key = key1, - limit = 10, - nextPageToken = None, - validAtEventSeqId = 5, - ) - ) - ) shouldBe KeysPageResult( - internalContractIds = Vector(internalContractId3, internalContractId4), - nextPageToken = None, - ) - - executeSql( - backend.contract.contractKey( - KeysPageQuery( - key = key1, - limit = 3, - nextPageToken = None, - validAtEventSeqId = 5, - ) - ) - ) shouldBe KeysPageResult( - internalContractIds = Vector(internalContractId3, internalContractId4), - nextPageToken = None, - ) - - executeSql( - backend.contract.contractKey( - KeysPageQuery( - key = key1, - limit = 2, - nextPageToken = None, - validAtEventSeqId = 5, - ) - ) - ) shouldBe KeysPageResult( - internalContractIds = Vector(internalContractId3, internalContractId4), - nextPageToken = None, - ) - - executeSql( - backend.contract.contractKey( - KeysPageQuery( - key = key1, - limit = 3, - nextPageToken = None, - validAtEventSeqId = 4, - ) - ) - ) shouldBe KeysPageResult( - internalContractIds = Vector(internalContractId4), - nextPageToken = None, - ) - - executeSql( - backend.contract.contractKey( - KeysPageQuery( - key = key1, - limit = 3, - nextPageToken = None, - validAtEventSeqId = 3, - ) - ) - ) shouldBe KeysPageResult( - internalContractIds = Vector(internalContractId2, internalContractId4), - nextPageToken = None, - ) - - executeSql( - backend.contract.contractKey( - KeysPageQuery( - key = key2, - limit = 3, - nextPageToken = None, - validAtEventSeqId = 5, - ) - ) - ) shouldBe KeysPageResult( - internalContractIds = Vector(internalContractId), - nextPageToken = None, - ) - } - - it should "correctly find non unique contract key contracts in batch via nonUniqueContractKeysPlain" in { - val key1 = GlobalKey.assertBuild( - Identifier.assertFromString("A:B:C"), - someTemplateId.pkg.name, - ValueUnit, - crypto.Hash.hashPrivateKey("batch-1"), - ) - val key2 = GlobalKey.assertBuild( - Identifier.assertFromString("A:B:C"), - someTemplateId.pkg.name, - ValueText("value"), - keyHash = crypto.Hash.hashPrivateKey("batch-2"), - ) - val iid1 = 101L - val iid2 = 102L - val iid3 = 103L - val iid4 = 104L - val signatory = Ref.Party.assertFromString("signatory") - - val dtos: Vector[DbDto] = Vector( - dtosCreate( - event_offset = 1L, - event_sequential_id = 1L, - internal_contract_id = iid1, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosCreate( - event_offset = 2L, - event_sequential_id = 2L, - internal_contract_id = iid2, - create_key_hash = Some(key2.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosCreate( - event_offset = 3L, - event_sequential_id = 3L, - internal_contract_id = iid3, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosConsumingExercise( - event_offset = 4L, - event_sequential_id = 4L, - internal_contract_id = Some(iid3), - stakeholders = Set(signatory), - template_id = someTemplateId, - deactivated_event_sequential_id = Some(3L), - ), - dtosCreate( - event_offset = 5L, - event_sequential_id = 5L, - internal_contract_id = iid4, - create_key_hash = Some(key1.hash.toHexString), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - updateLedgerEnd(offset(5), 5L) - ) - - // Batch query: multiple keys in a single call - val batchResults = executeSql( - backend.contract.contractKeysPlain( - Seq( - KeysPageQuery(key = key1, limit = 10, nextPageToken = None, validAtEventSeqId = 5), - KeysPageQuery(key = key2, limit = 10, nextPageToken = None, validAtEventSeqId = 5), - ), - validAtEventSeqId = 5L, - ) - ) - batchResults should have size 2 - batchResults(0) shouldBe KeysPageResult( - internalContractIds = Vector(iid4, iid1), - nextPageToken = None, - ) - batchResults(1) shouldBe KeysPageResult( - internalContractIds = Vector(iid2), - nextPageToken = None, - ) - - val pagedResult = executeSql( - backend.contract.contractKeysPlain( - Seq( - KeysPageQuery(key = key1, limit = 1, nextPageToken = None, validAtEventSeqId = 5) - ), - validAtEventSeqId = 5L, - ) - ) - pagedResult.loneElement shouldBe KeysPageResult( - internalContractIds = Vector(iid4), - nextPageToken = Some(2L), - ) - - // Batch query with nextPageToken - val nextPageResult = executeSql( - backend.contract.contractKeysPlain( - Seq( - KeysPageQuery(key = key1, limit = 10, nextPageToken = Some(5L), validAtEventSeqId = 5) - ), - validAtEventSeqId = 5L, - ) - ) - nextPageResult.loneElement shouldBe KeysPageResult( - internalContractIds = Vector(iid1), - nextPageToken = None, - ) - - // Batch query respects deactivation visibility (validAtEventSeqId = 3, before the archive) - val beforeArchive = executeSql( - backend.contract.contractKeysPlain( - Seq( - KeysPageQuery(key = key1, limit = 10, nextPageToken = None, validAtEventSeqId = 3) - ), - validAtEventSeqId = 3L, - ) - ) - beforeArchive.loneElement shouldBe KeysPageResult( - internalContractIds = Vector(iid3, iid1), - nextPageToken = None, - ) - - // Batch query with mixed limits: limit=1 for key1, limit=2 for key2 - val mixedLimits = executeSql( - backend.contract.contractKeysPlain( - Seq( - KeysPageQuery(key = key1, limit = 1, nextPageToken = None, validAtEventSeqId = 5), - KeysPageQuery(key = key2, limit = 2, nextPageToken = None, validAtEventSeqId = 5), - ), - validAtEventSeqId = 5L, - ) - ) - mixedLimits should have size 2 - // key1 has 2 active contracts (iid4, iid1), limit=1 returns first and a nextPageToken - mixedLimits(0) shouldBe KeysPageResult( - internalContractIds = Vector(iid4), - nextPageToken = Some(2L), - ) - // key2 has 1 active contract (iid2), limit=2 returns it with no next page - mixedLimits(1) shouldBe KeysPageResult( - internalContractIds = Vector(iid2), - nextPageToken = None, - ) - - // Empty batch - val emptyResult = executeSql( - backend.contract.contractKeysPlain( - Seq.empty, - validAtEventSeqId = 5L, - ) - ) - emptyResult shouldBe Seq.empty - - // Batch query with identical KeysPageQuery entries: order is preserved and results are duplicated - val key1PageQuery = - KeysPageQuery(key = key1, limit = 10, nextPageToken = None, validAtEventSeqId = 5) - val key2PageQuery = - KeysPageQuery(key = key2, limit = 10, nextPageToken = None, validAtEventSeqId = 5) - - val identicalQueries = executeSql( - backend.contract.contractKeysPlain( - Seq( - key1PageQuery, - key2PageQuery, - key1PageQuery, - ), - validAtEventSeqId = 5L, - ) - ) - identicalQueries should have size 3 - identicalQueries(0) shouldBe KeysPageResult( - internalContractIds = Vector(iid4, iid1), - nextPageToken = None, - ) - identicalQueries(1) shouldBe KeysPageResult( - internalContractIds = Vector(iid2), - nextPageToken = None, - ) - identicalQueries(2) shouldBe identicalQueries(0) - } - - it should "correctly handle various create, assign, unassign, archive event sequences in batch" in { - val keyHash = crypto.Hash.hashPrivateKey("mixed-events") - val key = GlobalKey.assertBuild( - Identifier.assertFromString("A:B:C"), - someTemplateId.pkg.name, - ValueUnit, - keyHash, - ) - val signatory = Ref.Party.assertFromString("signatory") - val keyHashHex = Some(key.hash.toHexString) - - def c(iid: Long, seqId: Long): Seq[DbDto] = - dtosCreate( - event_offset = seqId, - event_sequential_id = seqId, - internal_contract_id = iid, - create_key_hash = keyHashHex, - )(stakeholders = Set(signatory), template_id = someTemplateId) - - def a(iid: Long, seqId: Long): Seq[DbDto] = - dtosAssign( - event_offset = seqId, - event_sequential_id = seqId, - internal_contract_id = iid, - create_key_hash = keyHashHex, - )(stakeholders = Set(signatory), template_id = someTemplateId) - - def u(iid: Long, seqId: Long, deactivates: Long): Seq[DbDto] = - dtosUnassign( - event_offset = seqId, - event_sequential_id = seqId, - internal_contract_id = Some(iid), - deactivated_event_sequential_id = Some(deactivates), - stakeholders = Set(signatory), - template_id = someTemplateId, - ) - - def d(iid: Long, seqId: Long, deactivates: Long): Seq[DbDto] = - dtosConsumingExercise( - event_offset = seqId, - event_sequential_id = seqId, - internal_contract_id = Some(iid), - deactivated_event_sequential_id = Some(deactivates), - stakeholders = Set(signatory), - template_id = someTemplateId, - ) - - def queryAt(validAt: Long): KeysPageResult = - executeSql( - backend.contract.contractKeysPlain( - Seq( - KeysPageQuery(key = key, limit = 100, nextPageToken = None, validAtEventSeqId = validAt) - ), - validAtEventSeqId = validAt, - ) - ).loneElement - - // Two contracts per sequence pattern, all sharing the same key. - val iid00 = 1000L - val iid01 = 1001L - val iid10 = 1010L - val iid11 = 1011L - val iid20 = 1020L - val iid21 = 1021L - val iid30 = 1030L - val iid31 = 1031L - val iid40 = 1040L - val iid41 = 1041L - val iid50 = 1050L - val iid51 = 1051L - - val dtos: Vector[DbDto] = Vector( - // create(iid00,1), unassign(iid00,2), assign(iid01,3), archive(iid01,4) - c(iid00, 1), - u(iid00, 2, deactivates = 1), - a(iid01, 3), - d(iid01, 4, deactivates = 3), - // create(iid10,5), assign(iid11,6), unassign(iid11,7), archive(iid10,8) - c(iid10, 5), - a(iid11, 6), - u(iid11, 7, deactivates = 6), - d(iid10, 8, deactivates = 5), - // create(iid20,9), assign(iid21,10), archive(iid20,11), unassign(iid21,12) - c(iid20, 9), - a(iid21, 10), - d(iid20, 11, deactivates = 9), - u(iid21, 12, deactivates = 10), - // assign(iid30,13), archive(iid30,14), create(iid31,15), unassign(iid31,16) - a(iid30, 13), - d(iid30, 14, deactivates = 13), - c(iid31, 15), - u(iid31, 16, deactivates = 15), - // assign(iid40,17), create(iid41,18), archive(iid41,19), unassign(iid40,20) - a(iid40, 17), - c(iid41, 18), - d(iid41, 19, deactivates = 18), - u(iid40, 20, deactivates = 17), - // assign(iid50,21), create(iid51,22), unassign(iid50,23), archive(iid51,24) - a(iid50, 21), - c(iid51, 22), - u(iid50, 23, deactivates = 21), - d(iid51, 24, deactivates = 22), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(24L), 24L)) - - // create(1), unassign(2), assign(3), archive(4) - queryAt(1) shouldBe KeysPageResult(internalContractIds = Vector(iid00), nextPageToken = None) - queryAt(2) shouldBe KeysPageResult(internalContractIds = Vector.empty, nextPageToken = None) - queryAt(3) shouldBe KeysPageResult(internalContractIds = Vector(iid01), nextPageToken = None) - queryAt(4) shouldBe KeysPageResult(internalContractIds = Vector.empty, nextPageToken = None) - - // create(5), assign(6), unassign(7) the second contract, archive(8) - queryAt(5) shouldBe KeysPageResult(internalContractIds = Vector(iid10), nextPageToken = None) - queryAt(6) shouldBe KeysPageResult( - internalContractIds = Vector(iid11, iid10), - nextPageToken = None, - ) - queryAt(7) shouldBe KeysPageResult(internalContractIds = Vector(iid10), nextPageToken = None) - queryAt(8) shouldBe KeysPageResult(internalContractIds = Vector.empty, nextPageToken = None) - - // create(9), assign(10), archive(11), unassign(12) - queryAt(9) shouldBe KeysPageResult(internalContractIds = Vector(iid20), nextPageToken = None) - queryAt(10) shouldBe KeysPageResult( - internalContractIds = Vector(iid21, iid20), - nextPageToken = None, - ) - queryAt(11) shouldBe KeysPageResult(internalContractIds = Vector(iid21), nextPageToken = None) - queryAt(12) shouldBe KeysPageResult(internalContractIds = Vector.empty, nextPageToken = None) - - // assign(13), archive(14), create(15), unassign(16) - queryAt(13) shouldBe KeysPageResult(internalContractIds = Vector(iid30), nextPageToken = None) - queryAt(14) shouldBe KeysPageResult(internalContractIds = Vector.empty, nextPageToken = None) - queryAt(15) shouldBe KeysPageResult(internalContractIds = Vector(iid31), nextPageToken = None) - queryAt(16) shouldBe KeysPageResult(internalContractIds = Vector.empty, nextPageToken = None) - - // assign(17), create(18), archive(19), unassign(20) - queryAt(17) shouldBe KeysPageResult(internalContractIds = Vector(iid40), nextPageToken = None) - queryAt(18) shouldBe KeysPageResult( - internalContractIds = Vector(iid41, iid40), - nextPageToken = None, - ) - queryAt(19) shouldBe KeysPageResult(internalContractIds = Vector(iid40), nextPageToken = None) - queryAt(20) shouldBe KeysPageResult(internalContractIds = Vector.empty, nextPageToken = None) - - // assign(21), create(22), unassign(23) , archive(24) - queryAt(21) shouldBe KeysPageResult(internalContractIds = Vector(iid50), nextPageToken = None) - queryAt(22) shouldBe KeysPageResult( - internalContractIds = Vector(iid51, iid50), - nextPageToken = None, - ) - queryAt(23) shouldBe KeysPageResult(internalContractIds = Vector(iid51), nextPageToken = None) - queryAt(24) shouldBe KeysPageResult(internalContractIds = Vector.empty, nextPageToken = None) - } - - it should "correctly find active contracts" in { - val internalContractId = 123L - val internalContractId2 = 223L - val internalContractId3 = 323L - val signatory = Ref.Party.assertFromString("signatory") - - val dtos: Vector[DbDto] = Vector( - dtosCreate( - event_offset = 1L, - event_sequential_id = 1L, - internal_contract_id = internalContractId, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosAssign( - event_offset = 2L, - event_sequential_id = 2L, - internal_contract_id = internalContractId2, - synchronizer_id = someSynchronizerId2, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosAssign( - event_offset = 3L, - event_sequential_id = 3L, - internal_contract_id = internalContractId3, - synchronizer_id = someSynchronizerId2, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosAssign( - event_offset = 4L, - event_sequential_id = 4L, - internal_contract_id = internalContractId3, - synchronizer_id = someSynchronizerId, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - updateLedgerEnd(offset(3), 3L) - ) - val activeContracts2 = executeSql( - backend.contract.activeContracts( - List( - internalContractId, - internalContractId2, - internalContractId3, - ), - 2L, - ) - ) - val activeContracts3 = executeSql( - backend.contract.activeContracts( - List( - internalContractId, - internalContractId2, - internalContractId3, - ), - 3L, - ) - ) - val activeIds = executeSql( - backend.event.updateStreamingQueries - .fetchActiveIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 1000, - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ) - ) - ) - val lastActivations = executeSql( - backend.contract.lastActivations( - List( - someSynchronizerId -> internalContractId, - someSynchronizerId -> internalContractId3, - someSynchronizerId2 -> internalContractId2, - someSynchronizerId2 -> internalContractId3, - ) - ) - ) - - activeContracts2 shouldBe Map( - internalContractId -> true, - internalContractId2 -> true, - ) - activeContracts3 shouldBe Map( - internalContractId -> true, - internalContractId2 -> true, - internalContractId3 -> true, - ) - activeIds shouldBe Vector(1L, 2L, 3L, 4L) - lastActivations shouldBe Map( - (someSynchronizerId, internalContractId) -> 1L, - (someSynchronizerId2, internalContractId2) -> 2L, - (someSynchronizerId2, internalContractId3) -> 3L, - ) - } - - it should "correctly find deactivated contracts" in { - val internalContractId = 123L - val internalContractId2 = 223L - val internalContractId3 = 323L - val signatory = Ref.Party.assertFromString("signatory") - - val dtos: Vector[DbDto] = Vector( - dtosCreate( - event_offset = 1L, - event_sequential_id = 1L, - internal_contract_id = internalContractId, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosAssign( - event_offset = 2L, - event_sequential_id = 2L, - internal_contract_id = internalContractId2, - synchronizer_id = someSynchronizerId2, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosUnassign( - event_offset = 3L, - event_sequential_id = 3L, - internal_contract_id = Some(internalContractId), - deactivated_event_sequential_id = Some(1L), - synchronizer_id = someSynchronizerId, - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosConsumingExercise( - event_offset = 4L, - event_sequential_id = 4L, - internal_contract_id = Some(internalContractId2), - deactivated_event_sequential_id = Some(2L), - synchronizer_id = someSynchronizerId2, - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - updateLedgerEnd(offset(4), 4L) - ) - val activeContracts2 = executeSql( - backend.contract.activeContracts( - List( - internalContractId, - internalContractId2, - ), - 2L, - ) - ) - val activeContracts4 = executeSql( - backend.contract.activeContracts( - List( - internalContractId, - internalContractId2, - internalContractId3, - ), - 4L, - ) - ) - val activeIds2 = executeSql( - backend.event.updateStreamingQueries - .fetchActiveIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 2L, - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 2L, - ) - ) - ) - val activeIds4 = executeSql( - backend.event.updateStreamingQueries - .fetchActiveIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 4, - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 4L, - ) - ) - ) - val lastActivations = executeSql( - backend.contract.lastActivations( - List( - someSynchronizerId -> internalContractId, - someSynchronizerId2 -> internalContractId2, - ) - ) - ) - - activeContracts2 shouldBe Map( - internalContractId -> true, - internalContractId2 -> true, - ) - activeContracts4 shouldBe Map( - internalContractId -> true, // although deactivated, this logic only cares about archivals - internalContractId2 -> false, - ) - activeIds2 shouldBe Vector(1L, 2L) - activeIds4 shouldBe Vector.empty - // lastActivation does not care about deactivations - lastActivations shouldBe Map( - (someSynchronizerId, internalContractId) -> 1L, - (someSynchronizerId2, internalContractId2) -> 2L, - ) - } - - it should "be able to query with 1000 contract ids" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql( - updateLedgerEnd(offset(3), 6L) - ) - val activeContracts = executeSql( - backend.contract.activeContracts( - 1.to(1000).map(_.toLong), - 2, - ) - ) - activeContracts shouldBe empty - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsConversions.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsConversions.scala deleted file mode 100644 index 70cf51a735..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsConversions.scala +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.platform.store.backend.Conversions.IntArrayDBSerialization.{ - decodeFromByteArray, - encodeToByteArray, -} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -private[backend] trait StorageBackendTestsConversions - extends Matchers - with ScalaCheckDrivenPropertyChecks { this: AnyFlatSpec => - - behavior of "StorageBackend (conversions)" - - it should "serialize and deserialize sets of ints to bytes correctly" in { - import org.scalacheck.Gen - - val setGen: Gen[Set[Int]] = - Gen.containerOf[Set, Int](Gen.choose(Int.MinValue, Int.MaxValue)) - - forAll(setGen) { set => - val encoded = encodeToByteArray(set) - - decodeFromByteArray(encoded) should contain theSameElementsAs set - if (set.isEmpty) { - encoded shouldBe empty - } else { - encoded(0) shouldBe 1 - encoded.length shouldBe set.size * 4 + 1 // version byte + 4 bytes per int - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsDBLock.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsDBLock.scala deleted file mode 100644 index 29c5b1d8c4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsDBLock.scala +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.platform.store.backend.DBLockStorageBackend.{Lock, LockId, LockMode} -import org.scalatest.concurrent.Eventually -import org.scalatest.concurrent.PatienceConfiguration.Timeout -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.time.{Seconds, Span} -import org.scalatest.{Assertion, OptionValues, TryValues} - -import java.sql.Connection -import scala.util.Try - -private[platform] trait StorageBackendTestsDBLock - extends Matchers - with Eventually - with OptionValues - with TryValues { - this: AnyFlatSpec => - - protected def dbLock: DBLockStorageBackend - protected def getConnection: Connection - protected def lockIdSeed: Int - - behavior of "DBLockStorageBackend" - - it should "allow to acquire the same shared lock many times" in dbLockTestCase(1) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)) should not be empty - } - - it should "allow to acquire the same exclusive lock many times" in dbLockTestCase(1) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)) should not be empty - } - - it should "allow shared locking" in dbLockTestCase(2) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(2)) should not be empty - } - - it should "allow shared locking many times" in dbLockTestCase(5) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(2)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(3)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(4)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(5)) should not be empty - } - - it should "not allow exclusive when locked by shared" in dbLockTestCase(2) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) shouldBe empty - } - - it should "not allow exclusive when locked by exclusive" in dbLockTestCase(2) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) shouldBe empty - } - - it should "not allow shared when locked by exclusive" in dbLockTestCase(2) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(2)) shouldBe empty - } - - it should "unlock successfully a shared lock" in dbLockTestCase(2) { c => - val lock = dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)).value - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) shouldBe empty - dbLock.release(lock)(c(1)) shouldBe true - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) should not be empty - } - - it should "release successfully a shared lock if connection closed" in dbLockTestCase(2) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)).value - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) shouldBe empty - c(1).close() - eventually(timeout)( - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) should not be empty - ) - } - - it should "unlock successfully an exclusive lock" in dbLockTestCase(2) { c => - val lock = dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)).value - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) shouldBe empty - dbLock.release(lock)(c(1)) shouldBe true - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) should not be empty - } - - it should "release successfully an exclusive lock if connection closed" in dbLockTestCase(2) { - c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)).value - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) shouldBe empty - c(1).close() - eventually(timeout)( - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) should not be empty - ) - } - - it should "be able to lock exclusive, if all shared locks are released" in dbLockTestCase(4) { - c => - val shared1 = dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)).value - val shared2 = dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(2)).value - val shared3 = dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(3)).value - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(4)) shouldBe empty - - dbLock.release(shared1)(c(1)) - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(4)) shouldBe empty - dbLock.release(shared2)(c(2)) - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(4)) shouldBe empty - dbLock.release(shared3)(c(3)) - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(4)) should not be empty - } - - it should "lock immediately, or fail immediately" in dbLockTestCase(2) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)) should not be empty - val start = System.currentTimeMillis() - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(2)) shouldBe empty - (System.currentTimeMillis() - start) should be < 500L - } - - it should "fail to unlock lock held by others" in dbLockTestCase(2) { c => - val lock = dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Shared)(c(1)).value - dbLock.release(lock)(c(2)) shouldBe false - } - - it should "fail to unlock lock which is not held by anyone" in dbLockTestCase(1) { c => - dbLock.release(Lock(dbLock.lock(lockIdSeed), LockMode.Shared))(c(1)) shouldBe false - } - - it should "fail if attempt to use backend-foreign lock-id for locking" in dbLockTestCase(1) { c => - Try(dbLock.tryAcquire(new LockId {}, LockMode.Shared)(c(1))).isFailure shouldBe true - } - - it should "fail if attempt to use backend-foreign lock-id for un-locking" in dbLockTestCase(1) { - c => - Try(dbLock.release(Lock(new LockId {}, LockMode.Shared))(c(1))).isFailure shouldBe true - } - - it should "lock successfully exclusive different locks" in dbLockTestCase(1) { c => - dbLock.tryAcquire(dbLock.lock(lockIdSeed), LockMode.Exclusive)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed + 1), LockMode.Exclusive)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed + 2), LockMode.Exclusive)(c(1)) should not be empty - dbLock.tryAcquire(dbLock.lock(lockIdSeed + 3), LockMode.Exclusive)(c(1)) should not be empty - } - - private val timeout = Timeout(Span(10, Seconds)) - - private def dbLockTestCase( - numOfConnectionsNeeded: Int - )(test: List[Connection] => Assertion): Assertion = - if (dbLock.dbLockSupported) { - // prepending with null so we can refer to connections 1 based in tests - val connections = null :: List.fill(numOfConnectionsNeeded)(getConnection) - val result = Try(test(connections)) - connections.foreach(c => Try(c.close())) - result.success.value - } else { - info( - s"This test makes sense only for StorageBackend which supports DB-Locks. For ${dbLock.getClass.getName} StorageBackend this test is disabled." - ) - succeed - } -} - -trait StorageBackendTestsDBLockForSuite - extends StorageBackendTestsDBLock - with StorageBackendProvider - with BaseTest { - this: AnyFlatSpec => - - override val dbLock: DBLockStorageBackend = backend.dbLock - - override def getConnection: Connection = - backend.dataSource - .createDataSource(DataSourceStorageBackend.DataSourceConfig(jdbcUrl), loggerFactory) - .getConnection -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsEvents.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsEvents.scala deleted file mode 100644 index f405b392fb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsEvents.scala +++ /dev/null @@ -1,3371 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{ - CommonEventProperties, - CommonUpdateProperties, - FatCreatedEventProperties, - RawArchivedEvent, - RawExercisedEvent, - RawFatCreatedEvent, - RawThinAcsDeltaEvent, - RawThinActiveContract, - RawThinAssignEvent, - RawThinCreatedEvent, - RawThinLedgerEffectsEvent, - RawUnassignEvent, - ReassignmentProperties, - SequentialIdBatch, - SynchronizerOffset, - ThinCreatedEventProperties, - TransactionProperties, -} -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsAddActivationsParams, - AchsRemoveDeactivatedParams, -} -import com.digitalasset.canton.platform.store.backend.StorageBackendTestsEvents.PaginationFromToOps -import com.digitalasset.canton.platform.store.backend.common.{ - EventPayloadSourceForUpdatesAcsDelta, - EventPayloadSourceForUpdatesLedgerEffects, -} -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - IdPage, - IdPageBounds, - PaginationFromTo, - PaginationInput, -} -import com.digitalasset.canton.platform.store.dao.events.ACSReader -import com.digitalasset.canton.protocol.TestUpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Ref.{ - ChoiceName, - Identifier, - NameTypeConRef, - PackageName, - Party, -} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.transaction.test.TestNodeBuilder -import com.digitalasset.daml.lf.transaction.{CreationTime, FatContractInstance} -import com.digitalasset.daml.lf.value.Value -import org.scalactic.Equality -import org.scalatest.Inside -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.sql.Connection -import java.util.concurrent.atomic.AtomicReference -import scala.util.chaining.scalaUtilChainingOps - -private[backend] trait StorageBackendTestsEvents - extends Matchers - with Inside - with StorageBackendSpec { - this: AnyFlatSpec => - - private def testBidirectionalFetchPage(caseClue: String)( - input: PaginationInput, - query: Connection => PaginationInput => IdPage, - ascendingExpected: IdPage, - descendingExpected: IdPage, - ): Unit = { - require(!input.fromTo.descending) - executeSql(query(_)(input)) shouldBe ascendingExpected - executeSql(query(_)(input.copy(fromTo = input.fromTo.reverse))) shouldBe descendingExpected - }.withClue(caseClue) - - private def testBidirectionalFetchPageFiltered(caseClue: String)( - input: PaginationFromTo, - query: Connection => PaginationFromTo => Vector[Long], - ascendingExpected: Vector[Long], - descendingExpected: Vector[Long], - ): Unit = { - require(!input.descending) - executeSql(query(_)(input)) shouldBe ascendingExpected - executeSql(query(_)(input.reverse)) shouldBe descendingExpected - }.withClue(caseClue) - - private def testBidirectionalFetchBounds(caseClue: String)( - input: PaginationInput, - query: Connection => PaginationInput => Option[IdPageBounds], - ascendingExpected: Option[IdPageBounds], - descendingExpected: Option[IdPageBounds], - ): Unit = { - require(!input.fromTo.descending) - executeSql(query(_)(input)) shouldBe ascendingExpected - executeSql(query(_)(input.copy(fromTo = input.fromTo.reverse))) shouldBe descendingExpected - }.withClue(caseClue) - - behavior of "StorageBackend (events)" - - import StorageBackendTestValues.* - import ScalatestEqualityHelpers.* - - it should "find contracts by party" in { - val partySignatory = Ref.Party.assertFromString("signatory") - val partyObserver1 = Ref.Party.assertFromString("observer1") - val partyObserver2 = Ref.Party.assertFromString("observer2") - - val dtos = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - notPersistedContractId = hashCid("#1"), - )( - stakeholders = Set(partySignatory, partyObserver1) - ), - dtosAssign( - event_offset = 2, - event_sequential_id = 2L, - notPersistedContractId = hashCid("#2"), - )( - stakeholders = Set(partySignatory, partyObserver2) - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - testBidirectionalFetchPage("signatory")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L, 2L), lastPage = true), - descendingExpected = IdPage(Vector(2L, 1L), lastPage = true), - ) - testBidirectionalFetchPage("observer1")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partyObserver1), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L), lastPage = true), - descendingExpected = IdPage(Vector(1L), lastPage = true), - ) - testBidirectionalFetchPage("observer2")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partyObserver2), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(2L), lastPage = true), - descendingExpected = IdPage(Vector(2L), lastPage = true), - ) - testBidirectionalFetchPage("super reader")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = None, - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L, 2L), lastPage = true), - descendingExpected = IdPage(Vector(2L, 1L), lastPage = true), - ) - } - - it should "find contracts by party and by event_type" in { - val partySignatory = Ref.Party.assertFromString("signatory") - val partyObserver1 = Ref.Party.assertFromString("observer1") - val partyObserver2 = Ref.Party.assertFromString("observer2") - - val dtos = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - notPersistedContractId = hashCid("#1"), - )( - stakeholders = Set(partySignatory, partyObserver1) - ), - dtosAssign( - event_offset = 5, - event_sequential_id = 5L, - notPersistedContractId = hashCid("#2"), - )( - stakeholders = Set(partySignatory, partyObserver2) - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(6), 6L)) - testBidirectionalFetchPageFiltered("signatory Create")( - input = PaginationFromTo.ascending(0L, 10L), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Create)) - .fetchPage, - ascendingExpected = Vector(1L), - descendingExpected = Vector(1L), - ) - testBidirectionalFetchPageFiltered("signatory Assign")( - input = PaginationFromTo.ascending(0L, 10L), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Assign)) - .fetchPage, - ascendingExpected = Vector(5L), - descendingExpected = Vector(5L), - ) - testBidirectionalFetchPageFiltered("signatory Create and Assign")( - input = PaginationFromTo.ascending(0L, 10L), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Assign, PersistentEventType.Create)) - .fetchPage, - ascendingExpected = Vector(1L, 5L), - descendingExpected = Vector(5L, 1L), - ) - testBidirectionalFetchPageFiltered("signatory WitnessedCreate")( - input = PaginationFromTo.ascending(0L, 10L), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.WitnessedCreate)) - .fetchPage, - ascendingExpected = Vector.empty, - descendingExpected = Vector.empty, - ) - testBidirectionalFetchPage("foreign PaginationInput")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 100), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L, 5L), lastPage = true), - descendingExpected = IdPage(Vector(5L, 1L), lastPage = true), - ) - - testBidirectionalFetchBounds("bounds assign and create")( - input = PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 10L, - ), - limit = 100, - ), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Assign, PersistentEventType.Create)) - .fetchPageBounds, - ascendingExpected = Some(IdPageBounds(PaginationFromTo.ascending(0L, 10L), lastPage = true)), - descendingExpected = Some(IdPageBounds(PaginationFromTo.descending(0L, 10L), lastPage = true)), - ) - testBidirectionalFetchBounds("bounds only create")( - input = PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 10L, - ), - limit = 100, - ), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Create)) - .fetchPageBounds, - ascendingExpected = Some(IdPageBounds(PaginationFromTo.ascending(0L, 10L), lastPage = true)), - descendingExpected = Some(IdPageBounds(PaginationFromTo.descending(0L, 10L), lastPage = true)), - ) - testBidirectionalFetchBounds( - "bounds only create, bounds pushed forward to right before the next element" - )( - input = PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 10L, - ), - limit = 1, - ), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Create)) - .fetchPageBounds, - ascendingExpected = Some(IdPageBounds(PaginationFromTo.ascending(0L, 4L), lastPage = false)), - descendingExpected = - Some(IdPageBounds(PaginationFromTo.descending(1L, 10L), lastPage = false)), - ) - testBidirectionalFetchBounds("bounds only create, last page detected with right on the limit")( - input = PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 10L, - ), - limit = 2, - ), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Create)) - .fetchPageBounds, - ascendingExpected = Some(IdPageBounds(PaginationFromTo.ascending(0L, 10L), lastPage = true)), - descendingExpected = Some(IdPageBounds(PaginationFromTo.descending(0L, 10L), lastPage = true)), - ) - } - - it should "find contracts by party and template" in { - val partySignatory = Ref.Party.assertFromString("signatory") - val partyObserver1 = Ref.Party.assertFromString("observer1") - val partyObserver2 = Ref.Party.assertFromString("observer2") - - val dtos = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - notPersistedContractId = hashCid("#1"), - )( - stakeholders = Set(partySignatory, partyObserver1), - template_id = someTemplateId, - ), - dtosAssign( - event_offset = 2, - event_sequential_id = 2L, - notPersistedContractId = hashCid("#2"), - )( - stakeholders = Set(partySignatory, partyObserver2), - template_id = someTemplateId, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - testBidirectionalFetchPage("signatory with template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = Some(someTemplateId), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L, 2L), lastPage = true), - descendingExpected = IdPage(Vector(2L, 1L), lastPage = true), - ) - testBidirectionalFetchPage("observer1 with template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partyObserver1), - templateIdO = Some(someTemplateId), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L), lastPage = true), - descendingExpected = IdPage(Vector(1L), lastPage = true), - ) - testBidirectionalFetchPage("observer2 with template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partyObserver2), - templateIdO = Some(someTemplateId), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(2L), lastPage = true), - descendingExpected = IdPage(Vector(2L), lastPage = true), - ) - testBidirectionalFetchPage("super reader with template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = None, - templateIdO = Some(someTemplateId), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L, 2L), lastPage = true), - descendingExpected = IdPage(Vector(2L, 1L), lastPage = true), - ) - } - - it should "not find contracts when the template doesn't match" in { - val partySignatory = Ref.Party.assertFromString("signatory") - val partyObserver1 = Ref.Party.assertFromString("observer1") - val partyObserver2 = Ref.Party.assertFromString("observer2") - val otherTemplate = NameTypeConRef.assertFromString("#pkg-name:Mod:Template2") - - val dtos = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - notPersistedContractId = hashCid("#1"), - )( - stakeholders = Set(partySignatory, partyObserver1) - ), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - notPersistedContractId = hashCid("#2"), - )( - stakeholders = Set(partySignatory, partyObserver2) - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - testBidirectionalFetchPage("signatory other template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = Some(otherTemplate), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(), lastPage = true), - descendingExpected = IdPage(Vector(), lastPage = true), - ) - testBidirectionalFetchPage("observer1 other template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partyObserver1), - templateIdO = Some(otherTemplate), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(), lastPage = true), - descendingExpected = IdPage(Vector(), lastPage = true), - ) - testBidirectionalFetchPage("observer2 other template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partyObserver2), - templateIdO = Some(otherTemplate), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(), lastPage = true), - descendingExpected = IdPage(Vector(), lastPage = true), - ) - testBidirectionalFetchPage("super reader other template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = None, - templateIdO = Some(otherTemplate), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(), lastPage = true), - descendingExpected = IdPage(Vector(), lastPage = true), - ) - } - - it should "not find contracts when unknown names are used" in { - val partySignatory = Ref.Party.assertFromString("signatory") - val partyObserver = Ref.Party.assertFromString("observer") - val partyUnknown = Ref.Party.assertFromString("unknown") - val unknownTemplate = NameTypeConRef.assertFromString("#unknown:unknown:unknown") - - val dtos = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - notPersistedContractId = hashCid("#1"), - )( - stakeholders = Set(partySignatory, partyObserver) - ) - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(1), 1L)) - testBidirectionalFetchPage("unknown party")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partyUnknown), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(), lastPage = true), - descendingExpected = IdPage(Vector(), lastPage = true), - ) - testBidirectionalFetchPage("unknown template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = Some(unknownTemplate), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(), lastPage = true), - descendingExpected = IdPage(Vector(), lastPage = true), - ) - testBidirectionalFetchPage("unknown party and template")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partyUnknown), - templateIdO = Some(unknownTemplate), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(), lastPage = true), - descendingExpected = IdPage(Vector(), lastPage = true), - ) - testBidirectionalFetchPage("unknown template super reader")( - input = PaginationInput(PaginationFromTo.ascending(0L, 10L), 10), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = None, - templateIdO = Some(unknownTemplate), - ) - .fetchPage, - ascendingExpected = IdPage(Vector(), lastPage = true), - descendingExpected = IdPage(Vector(), lastPage = true), - ) - } - - it should "respect bounds and limits" in { - val partySignatory = Ref.Party.assertFromString("signatory") - val partyObserver1 = Ref.Party.assertFromString("observer1") - val partyObserver2 = Ref.Party.assertFromString("observer2") - - val dtos = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - notPersistedContractId = hashCid("#1"), - )( - stakeholders = Set(partySignatory, partyObserver1) - ), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - notPersistedContractId = hashCid("#2"), - )( - stakeholders = Set(partySignatory, partyObserver2) - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - testBidirectionalFetchPage("range [0,1] limit 2")( - input = PaginationInput(PaginationFromTo.ascending(0L, 1L), 2), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L), lastPage = true), - descendingExpected = IdPage(Vector(1L), lastPage = true), - ) - testBidirectionalFetchPage("range [1,2] limit 2")( - input = PaginationInput(PaginationFromTo.ascending(1L, 2L), 2), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(2L), lastPage = true), - descendingExpected = IdPage(Vector(2L), lastPage = true), - ) - testBidirectionalFetchPage("range [0,2] limit 1")( - input = PaginationInput(PaginationFromTo.ascending(0L, 2L), 1), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L), lastPage = false), - descendingExpected = IdPage(Vector(2L), lastPage = false), - ) - testBidirectionalFetchPage("range [0,2] limit 2")( - input = PaginationInput(PaginationFromTo.ascending(0L, 2L), 2), - query = backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(partySignatory), - templateIdO = None, - ) - .fetchPage, - ascendingExpected = IdPage(Vector(1L, 2L), lastPage = true), - descendingExpected = IdPage(Vector(2L, 1L), lastPage = true), - ) - } - - it should "populate correct maxEventSequentialId based on transaction_meta entries" in { - val dtos = Vector( - dtoTransactionMeta(offset(10), 1000, 1099), - dtoTransactionMeta(offset(15), 1100, 1100), - dtoTransactionMeta(offset(20), 1101, 1110), - dtoTransactionMeta(offset(21), 1111, 1115), - dtoTransactionMeta(offset(1000), 1119, 1120), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(offset(25), 1115)) - val maxEventSequentialId: Long => Long = - longOffset => - executeSql( - backend.event.maxEventSequentialId(Some(offset(longOffset))) - ) - - executeSql(backend.event.maxEventSequentialId(None)) shouldBe 999 - maxEventSequentialId(1) shouldBe 999 - maxEventSequentialId(2) shouldBe 999 - maxEventSequentialId(9) shouldBe 999 - maxEventSequentialId(10) shouldBe 1099 - maxEventSequentialId(11) shouldBe 1099 - maxEventSequentialId(14) shouldBe 1099 - maxEventSequentialId(15) shouldBe 1100 - maxEventSequentialId(16) shouldBe 1100 - maxEventSequentialId(19) shouldBe 1100 - maxEventSequentialId(20) shouldBe 1110 - maxEventSequentialId(21) shouldBe 1115 - maxEventSequentialId(22) shouldBe 1115 - maxEventSequentialId(24) shouldBe 1115 - maxEventSequentialId(25) shouldBe 1115 - maxEventSequentialId(26) shouldBe 1115 - - executeSql(updateLedgerEnd(offset(20), 1110)) - maxEventSequentialId(20) shouldBe 1110 - maxEventSequentialId(21) shouldBe 1110 - } - - it should "work properly for SynchronizerOffset queries" in { - val startRecordTimeSynchronizer = Timestamp.now() - val startRecordTimeSynchronizer2 = Timestamp.now().addMicros(10000) - val startPublicationTime = Timestamp.now().addMicros(100000) - val dbDtos = Vector( - dtoCompletion( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ), - dtoTransactionMeta( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - dtoTransactionMeta( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - dtoCompletion( - offset = offset(7), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ), - dtoCompletion( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ), - dtoTransactionMeta( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - dtoCompletion( - offset = offset(13), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(3000), - publicationTime = startPublicationTime.addMicros(2000), - ), - dtoTransactionMeta( - offset = offset(15), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(3000), - publicationTime = startPublicationTime.addMicros(2000), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql( - updateLedgerEnd(offset(12), 2L, CantonTimestamp(startPublicationTime.addMicros(1000))) - ) - - Vector( - someSynchronizerId -> startRecordTimeSynchronizer -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - someSynchronizerId -> startRecordTimeSynchronizer.addMicros(500) -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - someSynchronizerId -> startRecordTimeSynchronizer.addMicros(501) -> Some( - SynchronizerOffset( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - someSynchronizerId -> startRecordTimeSynchronizer.addMicros(1000) -> Some( - SynchronizerOffset( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - someSynchronizerId -> startRecordTimeSynchronizer.addMicros(1500) -> Some( - SynchronizerOffset( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - someSynchronizerId -> startRecordTimeSynchronizer.addMicros(2000) -> Some( - SynchronizerOffset( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - someSynchronizerId -> startRecordTimeSynchronizer.addMicros(2001) -> None, - someSynchronizerId2 -> startRecordTimeSynchronizer2 -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - someSynchronizerId2 -> startRecordTimeSynchronizer2.addMicros(500) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - someSynchronizerId2 -> startRecordTimeSynchronizer2.addMicros(700) -> Some( - SynchronizerOffset( - offset = offset(7), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - someSynchronizerId2 -> startRecordTimeSynchronizer2.addMicros(1000) -> Some( - SynchronizerOffset( - offset = offset(7), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - someSynchronizerId2 -> startRecordTimeSynchronizer2.addMicros(1001) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - someSynchronizerId2 -> startRecordTimeSynchronizer2.addMicros(2000) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - someSynchronizerId2 -> startRecordTimeSynchronizer2.addMicros(2001) -> None, - ).zipWithIndex.foreach { - case (((synchronizerId, afterOrAtRecordTimeInclusive), expectation), index) => - withClue( - s"test $index firstSynchronizerOffsetAfterOrAt($synchronizerId,$afterOrAtRecordTimeInclusive)" - ) { - executeSql( - backend.event.firstSynchronizerOffsetAfterOrAt( - synchronizerId = synchronizerId, - afterOrAtRecordTimeInclusive = afterOrAtRecordTimeInclusive, - ) - ) shouldBe expectation - } - } - - Vector( - Some(someSynchronizerId) -> offset(1) -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - Some(someSynchronizerId) -> offset(2) -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - Some(someSynchronizerId) -> offset(4) -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - Some(someSynchronizerId) -> offset(5) -> Some( - SynchronizerOffset( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId) -> offset(7) -> Some( - SynchronizerOffset( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId) -> offset(9) -> Some( - SynchronizerOffset( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId) -> offset(10) -> Some( - SynchronizerOffset( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId) -> offset(12) -> Some( - SynchronizerOffset( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId) -> offset(20) -> Some( - SynchronizerOffset( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId2) -> offset(3) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - Some(someSynchronizerId2) -> offset(6) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - Some(someSynchronizerId2) -> offset(7) -> Some( - SynchronizerOffset( - offset = offset(7), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId2) -> offset(9) -> Some( - SynchronizerOffset( - offset = offset(7), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId2) -> offset(11) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId2) -> offset(12) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - Some(someSynchronizerId2) -> offset(20) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - None -> offset(1) -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - None -> offset(2) -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - None -> offset(3) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - None -> offset(4) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - None -> offset(5) -> Some( - SynchronizerOffset( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - None -> offset(12) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - None -> offset(20) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - ).zipWithIndex.foreach { - case (((synchronizerIdO, beforeOrAtOffsetInclusive), expectation), index) => - withClue( - s"test $index lastSynchronizerOffsetBeforeOrAt($synchronizerIdO,$beforeOrAtOffsetInclusive)" - ) { - executeSql( - backend.event.lastSynchronizerOffsetBeforeOrAt( - synchronizerIdO = synchronizerIdO, - beforeOrAtOffsetInclusive = beforeOrAtOffsetInclusive, - ) - ) shouldBe expectation - } - } - - Vector( - offset(1) -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - offset(2) -> None, - offset(3) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - offset(5) -> Some( - SynchronizerOffset( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - offset(13) -> None, - offset(15) -> None, - ).zipWithIndex.foreach { case ((offset, expectation), index) => - withClue(s"test $index synchronizer Offset($offset)") { - executeSql( - backend.event.synchronizerOffset( - offset = offset - ) - ) shouldBe expectation - } - } - - Vector( - startPublicationTime -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - startPublicationTime.addMicros(500) -> Some( - SynchronizerOffset( - offset = offset(1), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - startPublicationTime.addMicros(501) -> Some( - SynchronizerOffset( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - startPublicationTime.addMicros(1000) -> Some( - SynchronizerOffset( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(1000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - startPublicationTime.addMicros(1001) -> None, - ).zipWithIndex.foreach { case ((afterOrAtPublicationTimeInclusive, expectation), index) => - withClue( - s"test $index firstSynchronizerOffsetAfterOrAtPublicationTime($afterOrAtPublicationTimeInclusive)" - ) { - executeSql( - backend.event.firstSynchronizerOffsetAfterOrAtPublicationTime( - afterOrAtPublicationTimeInclusive = afterOrAtPublicationTimeInclusive - ) - ) shouldBe expectation - } - } - - Vector( - startPublicationTime -> None, - startPublicationTime.addMicros(499) -> None, - startPublicationTime.addMicros(500) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - startPublicationTime.addMicros(501) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - startPublicationTime.addMicros(1000) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - startPublicationTime.addMicros(1001) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - startPublicationTime.addMicros(2000) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - startPublicationTime.addMicros(4000) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - ).zipWithIndex.foreach { case ((beforeOrAtPublicationTimeInclusive, expectation), index) => - withClue( - s"test $index lastSynchronizerOffsetBeforeOrAtPublicationTime($beforeOrAtPublicationTimeInclusive)" - ) { - executeSql( - backend.event.lastSynchronizerOffsetBeforeOrAtPublicationTime( - beforeOrAtPublicationTimeInclusive = beforeOrAtPublicationTimeInclusive - ) - ) shouldBe expectation - } - } - Vector( - startRecordTimeSynchronizer2 -> None, - startRecordTimeSynchronizer2.addMicros(499) -> None, - startRecordTimeSynchronizer2.addMicros(500) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - startRecordTimeSynchronizer2.addMicros(501) -> Some( - SynchronizerOffset( - offset = offset(3), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ) - ), - startRecordTimeSynchronizer2.addMicros(2000) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - startRecordTimeSynchronizer2.addMicros(2001) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - startRecordTimeSynchronizer2.addMicros(2000) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - // never return a synchronizer offset with an offset greater than the ledger end - startRecordTimeSynchronizer2.addMicros(4000) -> Some( - SynchronizerOffset( - offset = offset(11), - synchronizerId = someSynchronizerId2, - recordTime = startRecordTimeSynchronizer2.addMicros(2000), - publicationTime = startPublicationTime.addMicros(1000), - ) - ), - ).zipWithIndex.foreach { case ((beforeOrAtRecordTime, expectation), index) => - withClue( - s"test $index lastSynchronizerOffsetBeforeOrAtRecordTime($beforeOrAtRecordTime)" - ) { - executeSql( - backend.event.lastSynchronizerOffsetBeforeOrAtRecordTime( - synchronizerId = someSynchronizerId2, - beforeOrAtRecordTimeInclusive = beforeOrAtRecordTime, - beforeOrAtLedgerEndOffsetInclusive = offset(12), - ) - ) shouldBe expectation - } - } - } - - it should "work with multiple transaction_metadata entries sharing the same record_time - firstSynchronizerOffsetAfterOrAt" in { - val startRecordTimeSynchronizer = Timestamp.now().addMicros(10000) - val startPublicationTime = Timestamp.now().addMicros(100000) - val dbDtos = Vector( - dtoTransactionMeta( - offset = offset(3), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - dtoTransactionMeta( - offset = offset(7), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(550), - publicationTime = startPublicationTime.addMicros(700), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - dtoTransactionMeta( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(550), - publicationTime = startPublicationTime.addMicros(800), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - // insertion is out of order for this entry, for testing result is not reliant on insertion order, but rather on index order (regression for bug #26434) - dtoTransactionMeta( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(550), - publicationTime = startPublicationTime.addMicros(600), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - dtoTransactionMeta( - offset = offset(11), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(600), - publicationTime = startPublicationTime.addMicros(900), - event_sequential_id_first = 1, - event_sequential_id_last = 1, - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql( - updateLedgerEnd(offset(12), 2L, CantonTimestamp(startPublicationTime.addMicros(1000))) - ) - - executeSql( - backend.event.firstSynchronizerOffsetAfterOrAt( - synchronizerId = someSynchronizerId, - afterOrAtRecordTimeInclusive = startRecordTimeSynchronizer.addMicros(540), - ) - ).value.offset shouldBe offset(5) - executeSql( - backend.event.firstSynchronizerOffsetAfterOrAt( - synchronizerId = someSynchronizerId, - afterOrAtRecordTimeInclusive = startRecordTimeSynchronizer.addMicros(550), - ) - ).value.offset shouldBe offset(5) - } - - it should "work with multiple completion entries sharing the same record_time - firstSynchronizerOffsetAfterOrAt" in { - val startRecordTimeSynchronizer = Timestamp.now().addMicros(10000) - val startPublicationTime = Timestamp.now().addMicros(100000) - val dbDtos = Vector( - dtoCompletion( - offset = offset(3), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(500), - publicationTime = startPublicationTime.addMicros(500), - ), - dtoCompletion( - offset = offset(7), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(550), - publicationTime = startPublicationTime.addMicros(700), - ), - dtoCompletion( - offset = offset(9), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(550), - publicationTime = startPublicationTime.addMicros(800), - ), - // insertion is out of order for this entry, for testing result is not reliant on insertion order, but rather on index order (regression for bug #26434) - dtoCompletion( - offset = offset(5), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(550), - publicationTime = startPublicationTime.addMicros(600), - ), - dtoCompletion( - offset = offset(11), - synchronizerId = someSynchronizerId, - recordTime = startRecordTimeSynchronizer.addMicros(600), - publicationTime = startPublicationTime.addMicros(900), - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql( - updateLedgerEnd(offset(12), 2L, CantonTimestamp(startPublicationTime.addMicros(1000))) - ) - - executeSql( - backend.event.firstSynchronizerOffsetAfterOrAt( - synchronizerId = someSynchronizerId, - afterOrAtRecordTimeInclusive = startRecordTimeSynchronizer.addMicros(540), - ) - ).value.offset shouldBe offset(5) - executeSql( - backend.event.firstSynchronizerOffsetAfterOrAt( - synchronizerId = someSynchronizerId, - afterOrAtRecordTimeInclusive = startRecordTimeSynchronizer.addMicros(550), - ) - ).value.offset shouldBe offset(5) - } - - it should "fetch correctly AcsDelta and LedgerEffects Raw events" in { - implicit val eq: Equality[RawThinAcsDeltaEvent] = caseClassArrayEq - implicit val eq2: Equality[RawThinLedgerEffectsEvent] = caseClassArrayEq - - val dbDtos = Vector( - dtosCreate(event_sequential_id = 1L)(), - dtosAssign(event_sequential_id = 2L)(), - dtosConsumingExercise(event_sequential_id = 3L), - dtosUnassign(event_sequential_id = 4L), - dtosWitnessedCreate(event_sequential_id = 5L)(), - dtosWitnessedExercised(event_sequential_id = 6L), - dtosWitnessedExercised(event_sequential_id = 7L, consuming = false), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql( - updateLedgerEnd(offset(10000), 10000L) - ) - - executeSql( - backend.event.fetchEventPayloadsAcsDelta(EventPayloadSourceForUpdatesAcsDelta.Activate)( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - requestingPartiesForReassignment = - Some(Set("witness2", "stakeholder2", "submitter1", "actor2").map(Party.assertFromString)), - ) - ).toList should contain theSameElementsInOrderAs List( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 1L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ), - RawThinAssignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 2L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - reassignmentId = "0012345678", - submitter = Some("submitter1"), - reassignmentCounter = 345, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = Some(Set("witness2", "stakeholder2", "submitter1", "actor2")), - reassignmentCounter = 345, - acsDeltaForParticipant = true, - ), - sourceSynchronizerId = someSynchronizerId2.toProtoPrimitive, - ), - ) - executeSql( - backend.event.fetchEventPayloadsAcsDelta(EventPayloadSourceForUpdatesAcsDelta.Deactivate)( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - requestingPartiesForReassignment = - Some(Set("witness2", "stakeholder2", "submitter1", "actor2").map(Party.assertFromString)), - ) - ).toList should contain theSameElementsInOrderAs List( - RawArchivedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 3L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set("stakeholder1"), - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - deactivatedEventSeqId = Some(2), - ), - RawUnassignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 4L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - reassignmentId = "0012345678", - submitter = Some("submitter1"), - reassignmentCounter = 345, - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set("stakeholder2"), - assignmentExclusivity = Some(Timestamp.assertFromLong(111333)), - targetSynchronizerId = someSynchronizerId2.toProtoPrimitive, - deactivatedEventSeqId = Some(67), - ), - ) - - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Activate - )( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - requestingPartiesForReassignment = - Some(Set("witness2", "stakeholder2", "submitter1", "actor2").map(Party.assertFromString)), - ) - ).toList should contain theSameElementsInOrderAs List( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 1L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set("witness1"), - internalContractId = 10L, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ), - RawThinAssignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 2L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - reassignmentId = "0012345678", - submitter = Some("submitter1"), - reassignmentCounter = 345, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = Some(Set("witness2", "stakeholder2", "submitter1", "actor2")), - reassignmentCounter = 345, - acsDeltaForParticipant = true, - ), - sourceSynchronizerId = someSynchronizerId2.toProtoPrimitive, - ), - ) - val testThinCreatedEvent = RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 1L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set("witness1"), - internalContractId = 10L, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ) - RawFatCreatedEvent( - transactionProperties = testThinCreatedEvent.transactionProperties, - fatCreatedEventProperties = FatCreatedEventProperties( - thinCreatedEventProperties = testThinCreatedEvent.thinCreatedEventProperties, - fatContract = FatContractInstance.fromCreateNode( - TestNodeBuilder.create( - id = hashCid("a"), - templateId = someInterfaceId, - argument = Value.ValueUnit, - signatories = List( - "stakeholder1", // intersection with querying parties - "someparty", - ).map(Ref.Party.assertFromString).toSet, - ), - CreationTime.CreatedAt(Timestamp.now()), - Bytes.Empty, - ), - ), - ).acsDeltaForWitnesses shouldBe true - RawFatCreatedEvent( - transactionProperties = testThinCreatedEvent.transactionProperties, - fatCreatedEventProperties = FatCreatedEventProperties( - thinCreatedEventProperties = testThinCreatedEvent.thinCreatedEventProperties, - fatContract = FatContractInstance.fromCreateNode( - TestNodeBuilder.create( - id = hashCid("a"), - templateId = someInterfaceId, - argument = Value.ValueUnit, - signatories = List( - "someparty" // no intersection with querying parties - ).map(Ref.Party.assertFromString).toSet, - ), - CreationTime.CreatedAt(Timestamp.now()), - Bytes.Empty, - ), - ), - ).acsDeltaForWitnesses shouldBe false - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Deactivate - )( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - requestingPartiesForReassignment = - Some(Set("witness2", "stakeholder2", "submitter1", "actor2").map(Party.assertFromString)), - ) - ).toList should contain theSameElementsInOrderAs List( - RawExercisedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 3L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - exerciseConsuming = true, - exerciseChoice = ChoiceName.assertFromString("choice"), - exerciseChoiceInterface = Option(Identifier.assertFromString("in:ter:face")), - exerciseArgument = Array(1, 2, 3), - exerciseArgumentCompression = Some(1), - exerciseResult = Some(Array(2, 3, 4)), - exerciseResultCompression = Some(2), - exerciseActors = Set("actor1", "actor2"), - exerciseLastDescendantNodeId = 3, - filteredAdditionalWitnessParties = Set("witness1"), - filteredStakeholderParties = Set("stakeholder1"), - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - acsDeltaForParticipant = true, - deactivatedEventSeqId = Some(2), - ).tap(_.acsDeltaForWitnesses shouldBe true), - RawUnassignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 4L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - reassignmentId = "0012345678", - submitter = Some("submitter1"), - reassignmentCounter = 345, - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set("stakeholder2"), - assignmentExclusivity = Some(Timestamp.assertFromLong(111333)), - targetSynchronizerId = someSynchronizerId2.toProtoPrimitive, - deactivatedEventSeqId = Some(67), - ), - ) - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Deactivate - )( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = - Some(Set("witness1", "submitter1", "actor1").map(Party.assertFromString)), - requestingPartiesForReassignment = - Some(Set("witness2", "stakeholder2", "submitter1", "actor2").map(Party.assertFromString)), - ) - ).toList should contain theSameElementsInOrderAs List( - RawExercisedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 3L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - exerciseConsuming = true, - exerciseChoice = ChoiceName.assertFromString("choice"), - exerciseChoiceInterface = Option(Identifier.assertFromString("in:ter:face")), - exerciseArgument = Array(1, 2, 3), - exerciseArgumentCompression = Some(1), - exerciseResult = Some(Array(2, 3, 4)), - exerciseResultCompression = Some(2), - exerciseActors = Set("actor1", "actor2"), - exerciseLastDescendantNodeId = 3, - filteredAdditionalWitnessParties = Set("witness1"), - filteredStakeholderParties = Set(), - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - acsDeltaForParticipant = true, - deactivatedEventSeqId = Some(2), - ).tap(_.acsDeltaForWitnesses shouldBe false), - RawUnassignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 4L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - reassignmentId = "0012345678", - submitter = Some("submitter1"), - reassignmentCounter = 345, - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set("stakeholder2"), - assignmentExclusivity = Some(Timestamp.assertFromLong(111333)), - targetSynchronizerId = someSynchronizerId2.toProtoPrimitive, - deactivatedEventSeqId = Some(67), - ), - ) - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed - )( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - requestingPartiesForReassignment = - Some(Set("witness2", "stakeholder2", "submitter2", "actor2").map(Party.assertFromString)), - ) - ).toList should contain theSameElementsInOrderAs List( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 5L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set("witness1"), - internalContractId = 10L, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 0L, - acsDeltaForParticipant = false, - ), - ), - RawExercisedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 6L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - exerciseConsuming = true, - exerciseChoice = ChoiceName.assertFromString("choice"), - exerciseChoiceInterface = Option(Identifier.assertFromString("in:ter:face")), - exerciseArgument = Array(1, 2, 3), - exerciseArgumentCompression = Some(1), - exerciseResult = Some(Array(2, 3, 4)), - exerciseResultCompression = Some(2), - exerciseActors = Set("actor1", "actor2"), - exerciseLastDescendantNodeId = 3, - filteredAdditionalWitnessParties = Set("witness1"), - filteredStakeholderParties = Set.empty, - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - acsDeltaForParticipant = false, - deactivatedEventSeqId = None, - ), - RawExercisedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 7L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - exerciseConsuming = false, - exerciseChoice = ChoiceName.assertFromString("choice"), - exerciseChoiceInterface = Option(Identifier.assertFromString("in:ter:face")), - exerciseArgument = Array(1, 2, 3), - exerciseArgumentCompression = Some(1), - exerciseResult = Some(Array(2, 3, 4)), - exerciseResultCompression = Some(2), - exerciseActors = Set("actor1", "actor2"), - exerciseLastDescendantNodeId = 3, - filteredAdditionalWitnessParties = Set("witness1"), - filteredStakeholderParties = Set.empty, - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - acsDeltaForParticipant = false, - deactivatedEventSeqId = None, - ), - ) - } - - it should "fetch correctly EventQueryService results" in { - implicit val eq: Equality[RawThinCreatedEvent] = caseClassArrayEq - implicit val eq2: Equality[RawArchivedEvent] = caseClassArrayEq - - val createOnlyInternalContractId = 10L - val createAndArchiveInternalContractId = 11L - val transientInternalContractId = 12L - val divulgedInternalContractId = 13L - - val dbDtos = Vector( - dtosCreate( - event_sequential_id = 1L, - internal_contract_id = createOnlyInternalContractId, - event_offset = 116, - )(), - dtosCreate( - event_sequential_id = 2L, - internal_contract_id = createAndArchiveInternalContractId, - event_offset = 136, - )(), - dtosConsumingExercise( - event_sequential_id = 3L, - internal_contract_id = Some(createAndArchiveInternalContractId), - event_offset = 146, - ), - dtosWitnessedCreate( - event_sequential_id = 4L, - internal_contract_id = transientInternalContractId, - event_offset = 156, - )(), - dtosWitnessedExercised( - event_sequential_id = 5L, - internal_contract_id = Some(transientInternalContractId), - event_offset = 156, - ), - dtosWitnessedCreate( - event_sequential_id = 6L, - internal_contract_id = divulgedInternalContractId, - event_offset = 160, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql( - updateLedgerEnd(offset(10000), 10000L) - ) - - val createOnly = executeSql( - backend.event.eventReaderQueries.fetchContractIdEvents( - internalContractId = createOnlyInternalContractId, - requestingParties = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - endEventSequentialId = 10000L, - ) - ) - createOnly._1.value should equal( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 1L, - offset = 116L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = createOnlyInternalContractId, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ) - ) - createOnly._2 shouldBe None - - val createAndArchiveOnly = executeSql( - backend.event.eventReaderQueries.fetchContractIdEvents( - internalContractId = createAndArchiveInternalContractId, - requestingParties = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - endEventSequentialId = 10000L, - ) - ) - createAndArchiveOnly._1.value should equal( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 2L, - offset = 136L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = createAndArchiveInternalContractId, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ) - ) - createAndArchiveOnly._2.value should equal( - RawArchivedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 3L, - offset = 146L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set("stakeholder1"), - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - deactivatedEventSeqId = Some(2), - ) - ) - - val transient = executeSql( - backend.event.eventReaderQueries.fetchContractIdEvents( - internalContractId = transientInternalContractId, - requestingParties = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - endEventSequentialId = 10000L, - ) - ) - transient._1.value should equal( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 4L, - offset = 156L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = transientInternalContractId, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 0L, - acsDeltaForParticipant = false, - ), - ) - ) - transient._2.value should equal( - RawArchivedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 5L, - offset = 156L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = Some("command-id"), - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = Some(8465L), - ), - externalTransactionHash = Some(someExternalTransactionHashBinary), - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set.empty, - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - deactivatedEventSeqId = None, - ) - ) - - val divulged = executeSql( - backend.event.eventReaderQueries.fetchContractIdEvents( - internalContractId = divulgedInternalContractId, - requestingParties = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - endEventSequentialId = 10000L, - ) - ) - divulged shouldBe (None -> None) - } - - it should "fetch correctly the stream contents for acs" in { - implicit val eq: Equality[RawThinActiveContract] = caseClassArrayEq - - val dbDtos = Vector( - dtosCreate(event_sequential_id = 1L)(), - dtosAssign(event_sequential_id = 2L)(), - dtosConsumingExercise(event_sequential_id = 3L), - dtosUnassign(event_sequential_id = 4L), - dtosWitnessedCreate(event_sequential_id = 5L)(), - dtosWitnessedExercised(event_sequential_id = 6L), - dtosWitnessedExercised( - event_sequential_id = 7L, - consuming = false, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql( - updateLedgerEnd(offset(10000), 10000L) - ) - - val acs = executeSql( - backend.event.activeContractBatch( - eventSequentialIds = 0L to 100L, - allFilterParties = - Some(Set("witness1", "stakeholder1", "submitter1", "actor1").map(Party.assertFromString)), - ) - ).toList - - acs should contain theSameElementsInOrderAs List( - RawThinActiveContract( - commonEventProperties = CommonEventProperties( - eventSequentialId = 1L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ), - RawThinActiveContract( - commonEventProperties = CommonEventProperties( - eventSequentialId = 2L, - offset = 10L, - nodeId = 15, - workflowId = Some("workflow-id"), - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = Some(Set("witness1", "stakeholder1", "submitter1", "actor1")), - reassignmentCounter = 345, - acsDeltaForParticipant = true, - ), - ), - ) - - acs.size shouldBe - executeSql( - backend.event.fetchEventPayloadsAcsDelta(EventPayloadSourceForUpdatesAcsDelta.Activate)( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).size - } - - it should "fetch empty values for optional fields when not defined" in { - implicit val eq: Equality[RawThinAcsDeltaEvent] = caseClassArrayEq - implicit val eq2: Equality[RawThinLedgerEffectsEvent] = caseClassArrayEq - - val dbDtos = Vector( - dtosCreate( - event_sequential_id = 1L, - workflow_id = None, - command_id = None, - submitters = None, - external_transaction_hash = None, - create_key_hash = None, - traffic_cost = None, - )(), - dtosAssign( - event_sequential_id = 2L, - workflow_id = None, - command_id = None, - submitter = None, - traffic_cost = None, - )(), - dtosConsumingExercise( - event_sequential_id = 3L, - workflow_id = None, - command_id = None, - submitters = None, - external_transaction_hash = None, - deactivated_event_sequential_id = None, - exercise_choice_interface_id = None, - exercise_result = None, - exercise_argument_compression = None, - exercise_result_compression = None, - internal_contract_id = None, - traffic_cost = None, - ), - dtosUnassign( - event_sequential_id = 4L, - workflow_id = None, - command_id = None, - submitter = None, - deactivated_event_sequential_id = None, - assignment_exclusivity = None, - internal_contract_id = None, - traffic_cost = None, - ), - dtosWitnessedCreate( - event_sequential_id = 5L, - workflow_id = None, - command_id = None, - submitters = None, - external_transaction_hash = None, - traffic_cost = None, - )(), - dtosWitnessedExercised( - event_sequential_id = 6L, - workflow_id = None, - command_id = None, - submitters = None, - external_transaction_hash = None, - exercise_choice_interface_id = None, - exercise_result = None, - exercise_argument_compression = None, - exercise_result_compression = None, - internal_contract_id = None, - traffic_cost = None, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql( - updateLedgerEnd(offset(10000), 10000L) - ) - - // acs delta with optional fields undefined - executeSql( - backend.event.fetchEventPayloadsAcsDelta(EventPayloadSourceForUpdatesAcsDelta.Activate)( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList should contain theSameElementsInOrderAs List( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 1L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - externalTransactionHash = None, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = None, - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ), - RawThinAssignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 2L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - reassignmentId = "0012345678", - submitter = None, - reassignmentCounter = 345, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = None, - reassignmentCounter = 345, - acsDeltaForParticipant = true, - ), - sourceSynchronizerId = someSynchronizerId2.toProtoPrimitive, - ), - ) - - executeSql( - backend.event.fetchEventPayloadsAcsDelta(EventPayloadSourceForUpdatesAcsDelta.Deactivate)( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList should contain theSameElementsInOrderAs List( - RawArchivedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 3L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - externalTransactionHash = None, - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set("stakeholder1", "stakeholder2"), - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - deactivatedEventSeqId = None, - ), - RawUnassignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 4L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - reassignmentId = "0012345678", - submitter = None, - reassignmentCounter = 345, - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set("stakeholder1", "stakeholder2"), - assignmentExclusivity = None, - targetSynchronizerId = someSynchronizerId2.toProtoPrimitive, - deactivatedEventSeqId = None, - ), - ) - - // ledger effects events with optional fields undefined - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Activate - )( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList should contain theSameElementsInOrderAs List( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 1L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - externalTransactionHash = None, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set("witness1", "witness2"), - internalContractId = 10L, - requestingParties = None, - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ), - RawThinAssignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 2L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - reassignmentId = "0012345678", - submitter = None, - reassignmentCounter = 345, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = None, - reassignmentCounter = 345, - acsDeltaForParticipant = true, - ), - sourceSynchronizerId = someSynchronizerId2.toProtoPrimitive, - ), - ) - - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Deactivate - )( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList should contain theSameElementsInOrderAs List( - RawExercisedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 3L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - externalTransactionHash = None, - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - exerciseConsuming = true, - exerciseChoice = ChoiceName.assertFromString("choice"), - exerciseChoiceInterface = None, - exerciseArgument = Array(1, 2, 3), - exerciseArgumentCompression = None, - exerciseResult = None, - exerciseResultCompression = None, - exerciseActors = Set("actor1", "actor2"), - exerciseLastDescendantNodeId = 3, - filteredAdditionalWitnessParties = Set("witness1", "witness2"), - filteredStakeholderParties = Set("stakeholder1", "stakeholder2"), - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - acsDeltaForParticipant = true, - deactivatedEventSeqId = None, - ), - RawUnassignEvent( - reassignmentProperties = ReassignmentProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 4L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - reassignmentId = "0012345678", - submitter = None, - reassignmentCounter = 345, - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - filteredStakeholderParties = Set("stakeholder1", "stakeholder2"), - assignmentExclusivity = None, - targetSynchronizerId = someSynchronizerId2.toProtoPrimitive, - deactivatedEventSeqId = None, - ), - ) - - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed - )( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList should contain theSameElementsInOrderAs List( - RawThinCreatedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 5L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - externalTransactionHash = None, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set("witness1", "witness2"), - internalContractId = 10L, - requestingParties = None, - reassignmentCounter = 0L, - acsDeltaForParticipant = false, - ), - ), - RawExercisedEvent( - transactionProperties = TransactionProperties( - commonEventProperties = CommonEventProperties( - eventSequentialId = 6L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - commonUpdateProperties = CommonUpdateProperties( - updateId = TestUpdateId("update").toHexString, - commandId = None, - traceContext = serializableTraceContext, - recordTime = Timestamp.assertFromLong(100L), - trafficCost = None, - ), - externalTransactionHash = None, - ), - contractId = hashCid("c1"), - templateId = Identifier - .assertFromString("package:pl:ate") - .toFullIdentifier(PackageName.assertFromString("tem")), - exerciseConsuming = true, - exerciseChoice = ChoiceName.assertFromString("choice"), - exerciseChoiceInterface = None, - exerciseArgument = Array(1, 2, 3), - exerciseArgumentCompression = None, - exerciseResult = None, - exerciseResultCompression = None, - exerciseActors = Set("actor1", "actor2"), - exerciseLastDescendantNodeId = 3, - filteredAdditionalWitnessParties = Set("witness1", "witness2"), - filteredStakeholderParties = Set.empty, - ledgerEffectiveTime = Timestamp.assertFromLong(123456), - acsDeltaForParticipant = false, - deactivatedEventSeqId = None, - ), - ) - - // active contracts with optional fields undefined - executeSql( - backend.event.activeContractBatch( - eventSequentialIds = 0L to 100L, - allFilterParties = None, - ) - ).toList should contain theSameElementsInOrderAs List( - RawThinActiveContract( - commonEventProperties = CommonEventProperties( - eventSequentialId = 1L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = None, - reassignmentCounter = 0L, - acsDeltaForParticipant = true, - ), - ), - RawThinActiveContract( - commonEventProperties = CommonEventProperties( - eventSequentialId = 2L, - offset = 10L, - nodeId = 15, - workflowId = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ), - thinCreatedEventProperties = ThinCreatedEventProperties( - representativePackageId = Ref.PackageId.assertFromString("representativepackage"), - filteredAdditionalWitnessParties = Set.empty, - internalContractId = 10L, - requestingParties = None, - reassignmentCounter = 345, - acsDeltaForParticipant = true, - ), - ), - ) - } - - it should "fetch the same events when queried by list of sequential ids" in { - implicit val eq: Equality[RawThinAcsDeltaEvent] = caseClassArrayEq - implicit val eq2: Equality[RawThinLedgerEffectsEvent] = caseClassArrayEq - - val dbDtos = Vector( - dtosCreate(event_sequential_id = 1L)(), - dtosAssign(event_sequential_id = 2L)(), - dtosConsumingExercise(event_sequential_id = 3L), - dtosUnassign(event_sequential_id = 4L), - dtosWitnessedCreate(event_sequential_id = 5L)(), - dtosWitnessedExercised(event_sequential_id = 6L), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql( - updateLedgerEnd(offset(10000), 10000L) - ) - - for { - target <- Seq( - EventPayloadSourceForUpdatesAcsDelta.Activate, - EventPayloadSourceForUpdatesAcsDelta.Deactivate, - ) - } yield withClue(s"Failed for target: $target") { - - val range = executeSql( - backend.event.fetchEventPayloadsAcsDelta(target)( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList - - val list = executeSql( - backend.event.fetchEventPayloadsAcsDelta(target)( - eventSequentialIds = SequentialIdBatch.Ids((0L to 100L).toList), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList - - range should contain theSameElementsInOrderAs list - range.size shouldBe list.size - } - - for { - target <- Seq( - EventPayloadSourceForUpdatesLedgerEffects.Activate, - EventPayloadSourceForUpdatesLedgerEffects.Deactivate, - EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed, - ) - } yield withClue(s"Failed for target: $target") { - - val range = executeSql( - backend.event.fetchEventPayloadsLedgerEffects(target)( - eventSequentialIds = SequentialIdBatch.IdRange(0, 100), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList - - val list = executeSql( - backend.event.fetchEventPayloadsLedgerEffects(target)( - eventSequentialIds = SequentialIdBatch.Ids((0L to 100L).toList), - requestingPartiesForTx = None, - requestingPartiesForReassignment = None, - ) - ).toList - - range should contain theSameElementsInOrderAs list - range.size shouldBe list.size - } - } - - behavior of "incomplete lookup related event_sequential_id lookup queries" - - it should "return the correct sequence of event sequential IDs" in { - val synchronizerId1 = SynchronizerId.tryFromString("x::synchronizer1") - val synchronizerId2 = SynchronizerId.tryFromString("x::synchronizer2") - val synchronizerId3 = SynchronizerId.tryFromString("x::synchronizer3") - val synchronizerId4 = SynchronizerId.tryFromString("x::synchronizer4") - - val dbDtos = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - internal_contract_id = 1, - synchronizer_id = synchronizerId1, - )(), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - internal_contract_id = 2, - synchronizer_id = synchronizerId1, - )(), - dtosConsumingExercise( - event_offset = 3, - event_sequential_id = 3L, - internal_contract_id = Some(2L), - synchronizer_id = synchronizerId2, - ), - dtosUnassign( - event_offset = 4, - event_sequential_id = 4L, - internal_contract_id = Some(2L), - synchronizer_id = synchronizerId2, - target_synchronizer_id = synchronizerId1, - ), - dtosAssign( - event_offset = 5, - event_sequential_id = 5L, - internal_contract_id = 2L, - source_synchronizer_id = synchronizerId2, - synchronizer_id = synchronizerId1, - )(), - dtosAssign( - event_offset = 6, - event_sequential_id = 6L, - internal_contract_id = 2L, - source_synchronizer_id = synchronizerId3, - synchronizer_id = synchronizerId4, - )(), - dtosConsumingExercise( - event_offset = 10, - event_sequential_id = 10L, - internal_contract_id = Some(2L), - synchronizer_id = synchronizerId1, - ), - dtosUnassign( - event_offset = 11, - event_sequential_id = 11L, - internal_contract_id = Some(1L), - synchronizer_id = synchronizerId1, - ), - dtosUnassign( - event_offset = 12, - event_sequential_id = 12L, - internal_contract_id = Some(1L), - target_synchronizer_id = synchronizerId2, - ), - dtosAssign( - event_offset = 13, - event_sequential_id = 13L, - internal_contract_id = 2L, - synchronizer_id = synchronizerId2, - )(), - dtosUnassign( - event_offset = 14, - event_sequential_id = 14L, - internal_contract_id = Some(2L), - synchronizer_id = synchronizerId2, - ), - dtosAssign( - event_offset = 15, - event_sequential_id = 15L, - internal_contract_id = 2L, - synchronizer_id = synchronizerId2, - )(), - dtosCreate( - event_offset = 16, - event_sequential_id = 16L, - internal_contract_id = 3, - synchronizer_id = synchronizerId4, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dbDtos, _)) - executeSql(updateLedgerEnd(offset(16), 16L)) - - executeSql( - backend.event.lookupActivationSequentialIdByOffset( - List( - 1L, - 5L, - 6L, - 7L, - ) - ) - ) shouldBe Vector( - 1L, // this is actually a Create, but it is fine as the payload query is filtered to assign and it is invalid for create and assign to mix on one offset - 5L, - 6L, - ) - executeSql( - backend.event.lookupDeactivationSequentialIdByOffset( - List( - 1L, 4L, 6L, 7L, 11L, - ) - ) - ) shouldBe Vector(4L, 11L) - } - - behavior of "addActivationsToAchs" - private val signatory = Ref.Party.assertFromString("signatory") - - it should "add activations to ACHS respecting the limits" in { - val dtos: Vector[DbDto] = (1L to 5L).zipWithIndex - .map { case (id, index) => - dtosCreate( - event_offset = index + 1L, - event_sequential_id = index + 1L, - internal_contract_id = id, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ) - } - .toVector - .flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 1L, endInclusive = 3L, activeAt = 1000L) - ) - ) - - val achs = executeSql( - backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 1000L, - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ) - ) - ) - - achs shouldBe Vector(2L, 3L) - } - - private val dtos: Vector[DbDto] = Vector( - dtosCreate( - event_offset = 1L, - event_sequential_id = 1L, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosAssign( - event_offset = 2L, - event_sequential_id = 2L, - synchronizer_id = someSynchronizerId2, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosUnassign( - event_offset = 3L, - event_sequential_id = 3L, - deactivated_event_sequential_id = Some(1L), - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - dtosConsumingExercise( - event_offset = 4L, - event_sequential_id = 4L, - deactivated_event_sequential_id = Some(2L), - stakeholders = Set(signatory), - template_id = someTemplateId, - ), - ).flatten - - it should "correctly handle deactivated contracts (when activeAt is at ledger end)" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0L, endInclusive = 2L, activeAt = 2L) - ) - ) - executeSql( - updateLedgerEnd(offset(4), 4L) - ) - - val achsActiveAt2 = executeSql( - backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, // disable inactive filtration to see the complete ACHS - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ) - ) - ) - - achsActiveAt2 shouldBe Vector(1L, 2L) - - } - - it should "correctly handle deactivated contracts (when activeAt contains a deactivation)" in { - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0L, endInclusive = 3L, activeAt = 3L) - ) - ) - - val achsActiveAt3 = executeSql( - backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, // disable inactive filtration to see the complete ACHS - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ) - ) - ) - - achsActiveAt3 shouldBe Vector(2L) - } - - it should "correctly handle deactivated contracts (when activeAt contains a deactivation for all activations)" in { - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0L, endInclusive = 3L, activeAt = 4L) - ) - ) - executeSql( - updateLedgerEnd(offset(4), 4L) - ) - - val achsActiveAt4 = executeSql( - backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, // disable inactive filtration to see the complete ACHS - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ) - ) - ) - - achsActiveAt4 shouldBe empty - } - - behavior of "removeActivationsFromACHS" - - it should "correctly remove deactivated contracts" in { - val signatory = Ref.Party.assertFromString("signatory") - - val activations = Vector(1L, 2L, 4L, 6L, 7L, 8L, 10L).map { i => - dtosCreate( - event_offset = i, - event_sequential_id = i, - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ) - } - - // deactivations: at 3 deactivates 2, at 5 deactivates 1, at 9 deactivates 4 - val deactivations = Vector(3L -> 2L, 5L -> 1L, 9L -> 4L).map { case (i, deactivatedId) => - dtosConsumingExercise( - event_offset = i, - event_sequential_id = i, - deactivated_event_sequential_id = Some(deactivatedId), - ) - } - - val dtos: Vector[DbDto] = (activations ++ deactivations).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0L, endInclusive = 4L, activeAt = 4L) - ) - ) - executeSql( - updateLedgerEnd(offset(4), 4L) - ) - - val achs = executeSql( - backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, // disable inactive filtration to see the complete ACHS - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ) - ) - ) - - achs shouldBe Vector(1L, 4L) - - executeSql( - backend.event - .removeDeactivatedFromAchs( - AchsRemoveDeactivatedParams(startExclusive = 4L, endInclusive = 8L) - ) - ) - val achsAfter = executeSql( - backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, // disable inactive filtration to see the complete ACHS - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ) - ) - ) - - achsAfter shouldBe Vector(4L) - } - - behavior of "fetchAchsIds" - - it should "correctly filter deactivated contracts from ACHS with varying activeAtEventSeqId" in { - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0L, endInclusive = 2L, activeAt = 2L) - ) - ) - executeSql( - updateLedgerEnd(offset(4), 4L) - ) - - def fetchACHS(activeAtEventSeqId: Long): Vector[Long] = - executeSql( - backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = activeAtEventSeqId, - ) - .fetchPage(_)( - PaginatingAsyncStream.PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ) - ) - ) - - fetchACHS(0L) shouldBe Vector(1L, 2L) - fetchACHS(2L) shouldBe Vector(1L, 2L) - fetchACHS(3L) shouldBe Vector(2L) - fetchACHS(4L) shouldBe empty - } - - behavior of "AchsValidatingIdFilterPageQuery (fetchAchsIdFilterPageQuery)" - - private def withAchsData(test: => Unit): Unit = { - val achsDtos: Vector[DbDto] = (1L to 5L) - .map { i => - dtosCreate( - event_offset = i, - event_sequential_id = i, - internal_contract_id = i, - notPersistedContractId = hashCid(s"#achs-$i"), - )( - stakeholders = Set(signatory), - template_id = someTemplateId, - ) - } - .toVector - .flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(achsDtos, _)) - executeSql(updateLedgerEnd(offset(5), 5L)) - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0L, endInclusive = 5L, activeAt = 1000L) - ) - ) - test - } - - private def toggle(flag: AtomicReference[Boolean]): Boolean = - flag.updateAndGet(x => !x) - - it should "fetchPage: return empty when achsIsValid is false from the beginning" in withAchsData { - val underlying = backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, - ) - val wrapped = new ACSReader.AchsValidatingIdFilterPageQuery( - achsQuery = underlying, - achsIsValid = () => false, - getLastPopulated = () => 500L, - ) - executeSql( - wrapped.fetchPage(_)( - PaginationFromTo.ascending(startExclusive = 0L, endInclusive = 1000L) - ) - ) shouldBe empty - } - - it should "fetchPage: delegate to underlying when achsIsValid is true" in withAchsData { - val underlying = backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, - ) - val wrapped = new ACSReader.AchsValidatingIdFilterPageQuery( - achsQuery = underlying, - achsIsValid = () => true, - getLastPopulated = () => 500L, - ) - executeSql( - wrapped.fetchPage(_)( - PaginationFromTo.ascending(startExclusive = 0L, endInclusive = 1000L) - ) - ) shouldBe Vector(1L, 2L, 3L, 4L, 5L) - } - - it should "fetchPageBounds: return None when achsIsValid is false from the beginning" in withAchsData { - val underlying = backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, - ) - val wrapped = new ACSReader.AchsValidatingIdFilterPageQuery( - achsQuery = underlying, - achsIsValid = () => false, - getLastPopulated = () => 500L, - ) - val input = PaginationInput(PaginationFromTo.ascending(0L, 1000L), limit = 100) - executeSql(wrapped.fetchPageBounds(_)(input)) shouldBe None - } - - it should "fetchPageBounds: on last page, pin result toInclusive to lastPopulated" in withAchsData { - val lastPopulated = 10L - val underlying = backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, - ) - val wrapped = new ACSReader.AchsValidatingIdFilterPageQuery( - achsQuery = underlying, - achsIsValid = () => true, - getLastPopulated = () => lastPopulated, - ) - // limit=100 means all 5 entries fit in one page => last page - val input = PaginationInput(PaginationFromTo.ascending(0L, 1000L), limit = 100) - val result = executeSql(wrapped.fetchPageBounds(_)(input)) - - result shouldBe defined - result.value.lastPage shouldBe true - // Last page pins toInclusive to lastPopulated - result.value.fromTo.toInclusive shouldBe lastPopulated - } - - it should "fetchPageBounds: on non-last page, do not pin result toInclusive to lastPopulated" in withAchsData { - val lastPopulated = 10L - val underlying = backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, - ) - val wrapped = new ACSReader.AchsValidatingIdFilterPageQuery( - achsQuery = underlying, - achsIsValid = () => true, - getLastPopulated = () => lastPopulated, - ) - val input = PaginationInput(PaginationFromTo.ascending(0L, 1000L), limit = 1) - val result = executeSql(wrapped.fetchPageBounds(_)(input)) - - result shouldBe defined - result.value.lastPage shouldBe false - // Non-last page should NOT have toInclusive pinned to lastPopulated - result.value.fromTo.toInclusive should not be lastPopulated - } - - it should "fetchPageBounds: become invalid mid-flight (valid on first call, invalid on second)" in withAchsData { - val valid = new AtomicReference[Boolean](true) - val underlying = backend.event.updateStreamingQueries - .fetchAchsIds( - stakeholderO = Some(signatory), - templateIdO = None, - activeAtEventSeqId = 0L, - ) - val wrapped = new ACSReader.AchsValidatingIdFilterPageQuery( - achsQuery = underlying, - achsIsValid = () => valid.get(), - getLastPopulated = () => 500L, - ) - val input = PaginationInput(PaginationFromTo.ascending(0L, 1000L), limit = 100) - - // First call: valid - executeSql(wrapped.fetchPageBounds(_)(input)) shouldBe defined - - // Invalidate mid-flight - toggle(valid) shouldBe false - - // Second call: should return None - executeSql(wrapped.fetchPageBounds(_)(input)) shouldBe None - } -} - -object StorageBackendTestsEvents { - implicit class PaginationFromToOps(paginationFromTo: PaginationFromTo) { - def reverse: PaginationFromTo = if (paginationFromTo.descending) - PaginationFromTo( - fromExclusive = paginationFromTo.toInclusive - 1, - toInclusive = paginationFromTo.fromExclusive - 1, - descending = false, - ) - else - PaginationFromTo( - fromExclusive = paginationFromTo.toInclusive + 1, - toInclusive = paginationFromTo.fromExclusive + 1, - descending = true, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsIDPConfig.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsIDPConfig.scala deleted file mode 100644 index 17ff33f2e3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsIDPConfig.scala +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, OptionValues} - -import java.sql.SQLException -import java.util.UUID - -private[backend] trait StorageBackendTestsIDPConfig - extends Matchers - with Inside - with StorageBackendSpec - with OptionValues { - this: AnyFlatSpec => - - behavior of "StorageBackend (Identity Provider Config)" - - private def tested = backend.identityProviderStorageBackend - - it should "create and load unchanged an identity provider config" in { - val cfg = config() - executeSql(tested.createIdentityProviderConfig(cfg)) - executeSql(tested.getIdentityProviderConfig(cfg.identityProviderId)) shouldBe Some(cfg) - } - - it should "delete an identity provider config" in { - val cfg = config() - executeSql(tested.createIdentityProviderConfig(cfg)) - executeSql(tested.getIdentityProviderConfig(cfg.identityProviderId)) shouldBe Some(cfg) - executeSql(tested.deleteIdentityProviderConfig(cfg.identityProviderId)) - executeSql(tested.getIdentityProviderConfig(cfg.identityProviderId)) shouldBe None - } - - it should "update existing identity provider config's isDeactivated attribute" in { - val cfg = config().copy(isDeactivated = false) - executeSql(tested.createIdentityProviderConfig(cfg)) - // deactivate - executeSql(tested.updateIsDeactivated(cfg.identityProviderId, true)) shouldBe true - executeSql( - tested.getIdentityProviderConfig(cfg.identityProviderId) - ).value.isDeactivated shouldBe true - // activate again - executeSql(tested.updateIsDeactivated(cfg.identityProviderId, false)) shouldBe true - executeSql( - tested.getIdentityProviderConfig(cfg.identityProviderId) - ).value.isDeactivated shouldBe false - } - - it should "update existing identity provider config's jwksUrl attribute" in { - val cfg = config() - executeSql(tested.createIdentityProviderConfig(cfg)) - val newJwksUrl = JwksUrl("http://example.com/jwks2.json") - executeSql(tested.updateJwksUrl(cfg.identityProviderId, newJwksUrl)) shouldBe true - executeSql( - tested.getIdentityProviderConfig(cfg.identityProviderId) - ).value.jwksUrl shouldBe newJwksUrl - } - - it should "update existing identity provider config's issuer attribute" in { - val cfg = config() - executeSql(tested.createIdentityProviderConfig(cfg)) - val newIssuer = UUID.randomUUID().toString - executeSql(tested.updateIssuer(cfg.identityProviderId, newIssuer)) shouldBe true - executeSql( - tested.getIdentityProviderConfig(cfg.identityProviderId) - ).value.issuer shouldBe newIssuer - } - - it should "check if identity provider config's issuer exists" in { - val cfg = config() - executeSql( - tested.identityProviderConfigByIssuerExists(cfg.identityProviderId, cfg.issuer) - ) shouldBe false - executeSql(tested.createIdentityProviderConfig(cfg)) - executeSql(tested.identityProviderConfigByIssuerExists(randomId(), cfg.issuer)) shouldBe true - executeSql( - tested.identityProviderConfigByIssuerExists(cfg.identityProviderId, cfg.issuer) - ) shouldBe false - } - - it should "check if identity provider config by id exists" in { - val cfg = config() - executeSql(tested.idpConfigByIdExists(cfg.identityProviderId)) shouldBe false - executeSql(tested.createIdentityProviderConfig(cfg)) - executeSql(tested.idpConfigByIdExists(cfg.identityProviderId)) shouldBe true - } - - it should "return success for no-op issuer update" in { - val cfg = config() - executeSql(tested.createIdentityProviderConfig(cfg)) - executeSql(tested.updateIssuer(cfg.identityProviderId, cfg.issuer)) shouldBe true - } - - it should "fail to update issuer for non existing identity provider config" in { - executeSql(tested.updateIssuer(randomId(), "whatever")) shouldBe false - executeSql(tested.updateIssuer(randomId(), "")) shouldBe false - } - - it should "fail to update isDeactivated for non existing identity provider config" in { - executeSql(tested.updateIsDeactivated(randomId(), true)) shouldBe false - executeSql(tested.updateIsDeactivated(randomId(), false)) shouldBe false - } - - it should "fail to update JwksUrl for non existing identity provider config" in { - executeSql( - tested.updateJwksUrl(randomId(), JwksUrl("http://example.com/jwks.json")) - ) shouldBe false - executeSql( - tested.updateJwksUrl(randomId(), JwksUrl("http://example2.com/jwks.json")) - ) shouldBe false - } - - it should "return success for no-op JwksUrl update" in { - val cfg = config() - executeSql(tested.createIdentityProviderConfig(cfg)) - executeSql(tested.updateJwksUrl(cfg.identityProviderId, cfg.jwksUrl)) shouldBe true - } - - it should "return success for no-op isDeactivated update" in { - val cfg = config() - executeSql(tested.createIdentityProviderConfig(cfg)) - executeSql(tested.updateIsDeactivated(cfg.identityProviderId, cfg.isDeactivated)) shouldBe true - } - - it should "fail to update identity provider config issuer attribute to non-unique issuer" in { - val cfg1 = config() - val cfg2 = config() - executeSql(tested.createIdentityProviderConfig(cfg1)) - executeSql(tested.createIdentityProviderConfig(cfg2)) - assertThrows[SQLException] { - executeSql(tested.updateIssuer(cfg1.identityProviderId, cfg2.issuer)) - } - } - - it should "fail to create identity provider config with non-unique issuer" in { - val cfg1 = config() - val cfg2 = config() - executeSql(tested.createIdentityProviderConfig(cfg1)) - assertThrows[SQLException] { - executeSql(tested.createIdentityProviderConfig(cfg2.copy(issuer = cfg1.issuer))) - } - } - - it should "fail to create identity provider config with non-unique id" in { - val cfg1 = config() - val cfg2 = config() - executeSql(tested.createIdentityProviderConfig(cfg1)) - assertThrows[SQLException] { - executeSql( - tested.createIdentityProviderConfig(cfg2.copy(identityProviderId = cfg1.identityProviderId)) - ) - } - } - - it should "get all identity provider configs ordered by id" in { - val cfg1 = config().copy(identityProviderId = id("a")) - val cfg2 = config().copy(identityProviderId = id("b")) - val cfg3 = config().copy(identityProviderId = id("c")) - executeSql(tested.createIdentityProviderConfig(cfg1)) - executeSql(tested.createIdentityProviderConfig(cfg2)) - executeSql(tested.createIdentityProviderConfig(cfg3)) - - executeSql( - tested.listIdentityProviderConfigs() - ) shouldBe Vector(cfg1, cfg2, cfg3) - } - - private def config() = - IdentityProviderConfig( - identityProviderId = randomId(), - isDeactivated = false, - jwksUrl = JwksUrl.assertFromString("http://example.com/jwks.json"), - issuer = UUID.randomUUID().toString, - audience = Some(UUID.randomUUID().toString), - ) - - private def randomId() = - id(UUID.randomUUID().toString) - - private def id(str: String) = IdentityProviderId.Id(Ref.LedgerString.assertFromString(str)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsInitialization.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsInitialization.scala deleted file mode 100644 index f5bdb80571..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsInitialization.scala +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.platform.store.backend.common.MismatchException -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -private[backend] trait StorageBackendTestsInitialization extends Matchers with StorageBackendSpec { - this: AnyFlatSpec => - - behavior of "StorageBackend (initialization)" - - it should "correctly handle repeated initialization" in { - val participantId = ParticipantId(Ref.ParticipantId.assertFromString("participant")) - val otherParticipantId = ParticipantId(Ref.ParticipantId.assertFromString("otherParticipant")) - - loggerFactory.assertLogs( - within = { - executeSql( - backend.parameter.initializeParameters( - ParameterStorageBackend.IdentityParams( - participantId = participantId - ), - loggerFactory, - ) - ) - val error = intercept[RuntimeException]( - executeSql( - backend.parameter.initializeParameters( - ParameterStorageBackend.IdentityParams( - participantId = otherParticipantId - ), - loggerFactory, - ) - ) - ) - executeSql( - backend.parameter.initializeParameters( - ParameterStorageBackend.IdentityParams( - participantId = participantId - ), - loggerFactory, - ) - ) - - error.asInstanceOf[MismatchException.ParticipantId].existing shouldBe participantId - error.asInstanceOf[MismatchException.ParticipantId].provided shouldBe otherParticipantId - }, - assertions = _.errorMessage should include( - "Found existing database with mismatching participantId: existing 'participant', provided 'otherParticipant'" - ), - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsInitializeIngestion.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsInitializeIngestion.scala deleted file mode 100644 index 4c1aed62b4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsInitializeIngestion.scala +++ /dev/null @@ -1,713 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import anorm.SqlParser.long -import anorm.SqlStringInterpolation -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api -import com.digitalasset.canton.logging.SuppressingLogger -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.IdRange -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.`SimpleSql ops` -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.backend.common.{ - EventPayloadSourceForUpdatesAcsDelta, - EventPayloadSourceForUpdatesLedgerEffects, -} -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - PaginationFromTo, - PaginationInput, -} -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, Inside} - -private[backend] trait StorageBackendTestsInitializeIngestion - extends Matchers - with Inside - with StorageBackendSpec { - this: AnyFlatSpec => - - behavior of "StorageBackend (initializeIngestion)" - import StorageBackendTestValues.* - - private val signatory = Ref.Party.assertFromString("signatory") - private val participant = api.ParticipantId(Ref.ParticipantId.assertFromString("someParticipant")) - val dtos = Vector( - // 1: party allocation - dtoPartyEntry(offset(1), someParty) - ) - it should "delete overspill entries - parties" in { - fixture( - dtos1 = dtos, - lastOffset1 = 2L, - lastEventSeqId1 = 0L, - dtos2 = Vector( - // 3: party allocation - dtoPartyEntry(offset(3), someParty2) - ), - lastOffset2 = 3L, - lastEventSeqId2 = 0L, - checkContentsBefore = () => { - val parties = executeSql(backend.party.knownParties(None, None, 10)) - parties should have length 1 - }, - checkContentsAfter = () => { - val parties = executeSql(backend.party.knownParties(None, None, 10)) - parties should have length 1 - }, - ) - } - - it should "delete overspill entries written before first ledger end update - parties" in { - fixtureOverspillEntriesPriorToFirstLedgerEndUpdate( - dtos = dtos, - lastOffset = 3, - lastEventSeqId = 0L, - checkContentsAfter = () => { - val parties2 = executeSql(backend.party.knownParties(None, None, 10)) - parties2 shouldBe empty - }, - ) - } - - val dtos1 = Vector( - // 1: transaction with a create node - dtosCreate( - 1L, - event_sequential_id = 1, - internal_contract_id = 101, - additional_witnesses = Set(someParty), - )(stakeholders = Set(signatory, someParty)), - Seq( - dtoTransactionMeta( - offset(1), - event_sequential_id_first = 1L, - event_sequential_id_last = 1L, - ), - dtoCompletion(offset(41)), - ), - // 2: transaction with exercise node - dtosWitnessedExercised( - 2L, - event_sequential_id = 2, - consuming = false, - internal_contract_id = Some(101), - additional_witnesses = Set(someParty), - ), - dtosConsumingExercise( - 2L, - event_sequential_id = 3, - internal_contract_id = Some(102), - stakeholders = Set(someParty), - additional_witnesses = Set(someParty), - ), - Seq( - dtoTransactionMeta( - offset(2), - event_sequential_id_first = 2L, - event_sequential_id_last = 4L, - ), - dtoCompletion(offset(2)), - ), - // 3: assign - dtosAssign( - 3L, - event_sequential_id = 4, - internal_contract_id = 103, - )(stakeholders = Set(someParty)), - // 4: unassign - dtosUnassign( - 4L, - event_sequential_id = 5, - internal_contract_id = Some(103), - stakeholders = Set(someParty), - ), - // 5: topology transactions - Seq( - dtoPartyToParticipant( - offset(5), - eventSequentialId = 6, - party = someParty, - participant = participant, - ), - dtoPartyToParticipant( - offset(5), - eventSequentialId = 7, - party = someParty2, - participant = participant, - ), - ), - ).flatten - - it should "delete overspill entries - events, transaction meta, completions" in { - val dtos2 = Vector( - // 6: transaction with create node - dtosCreate( - 6L, - event_sequential_id = 8L, - internal_contract_id = 201, - additional_witnesses = Set(someParty), - )(stakeholders = Set(signatory, someParty)), - Seq( - dtoTransactionMeta( - offset(6), - event_sequential_id_first = 8L, - event_sequential_id_last = 8L, - ), - dtoCompletion(offset(6)), - ), - // 7: transaction with exercise node - dtosWitnessedExercised( - 7L, - event_sequential_id = 9L, - consuming = false, - internal_contract_id = Some(201), - additional_witnesses = Set(someParty), - ), - dtosConsumingExercise( - 7L, - event_sequential_id = 10L, - internal_contract_id = Some(202), - stakeholders = Set(someParty), - additional_witnesses = Set(someParty), - ), - Seq( - dtoTransactionMeta( - offset(7), - event_sequential_id_first = 9L, - event_sequential_id_last = 10L, - ), - dtoCompletion(offset(7)), - ), - // 8: assign - dtosAssign(8L, event_sequential_id = 11, internal_contract_id = 203)(stakeholders = - Set(someParty) - ), - // 9: unassign - dtosUnassign( - 9L, - event_sequential_id = 12, - internal_contract_id = Some(203), - stakeholders = Set(someParty), - ), - // 10: topology transactions - Seq( - dtoPartyToParticipant( - offset(10), - eventSequentialId = 13, - party = someParty, - participant = participant, - ), - dtoPartyToParticipant( - offset(10), - eventSequentialId = 14, - party = someParty3, - participant = participant, - ), - ), - ).flatten - val allDtos = dtos1 ++ dtos2 - fixture( - dtos1 = dtos1, - lastOffset1 = 5L, - lastEventSeqId1 = 7L, - dtos2 = dtos2, - lastOffset2 = 12L, - lastEventSeqId2 = 15L, - checkContentsBefore = () => { - val activateEventSeqIds = - executeSql( - backend.event.fetchEventPayloadsAcsDelta( - EventPayloadSourceForUpdatesAcsDelta.Activate - )(IdRange(1L, 100L), Some(Set.empty), None) - ).map(_.eventSeqId) - val deactivateEventSeqIds = executeSql( - backend.event.fetchEventPayloadsAcsDelta( - EventPayloadSourceForUpdatesAcsDelta.Deactivate - )(IdRange(1L, 100L), Some(Set.empty), None) - ).map(_.eventSeqId) - val witnessEventSeqIds = executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed - )(IdRange(1L, 100L), Some(Set.empty), None) - ).map(_.eventSeqId) - val topologyPartyEvents = - executeSql( - backend.event.topologyPartyEventBatch(IdRange(1L, 100L)) - ).map(_.partyId) - activateEventSeqIds shouldBe List(1, 4, 8, 11) - deactivateEventSeqIds shouldBe List(3, 5, 10, 12) - witnessEventSeqIds shouldBe List(2, 9) - topologyPartyEvents shouldBe List( - someParty, - someParty2, - someParty, - someParty3, - ) // not constrained by ledger end - fetchIdsFromTransactionMetaUpdateIds(allDtos.collect { case meta: DbDto.TransactionMeta => - meta.update_id - }) shouldBe Set((1, 1), (2, 4)) - fetchIdsFromTransactionMetaUpdateIds(allDtos.collect { case meta: DbDto.TransactionMeta => - meta.update_id - }) shouldBe fetchIdsFromTransactionMetaOffsets(allDtos.collect { - case meta: DbDto.TransactionMeta => - meta.event_offset - }) - fetchIdsCreateStakeholder() shouldBe List( - 1L, - 8L, - ) // since ledger-end does not limit the range query - fetchIdsCreateNonStakeholder() shouldBe List(1L, 8L) - fetchIdsConsumingStakeholder() shouldBe List(3L, 10L) - fetchIdsConsumingNonStakeholder() shouldBe List(3L, 10L) - fetchIdsNonConsuming() shouldBe List(2L, 9L) - fetchIdsAssignStakeholder() shouldBe List(4L, 11L) - fetchTopologyParty() shouldBe List(6, 13) - }, - checkContentsAfter = () => { - val activateEventSeqIds = - executeSql( - backend.event.fetchEventPayloadsAcsDelta( - EventPayloadSourceForUpdatesAcsDelta.Activate - )(IdRange(1L, 100L), Some(Set.empty), None) - ).map(_.eventSeqId) - val deactivateEventSeqIds = executeSql( - backend.event.fetchEventPayloadsAcsDelta( - EventPayloadSourceForUpdatesAcsDelta.Deactivate - )(IdRange(1L, 100L), Some(Set.empty), None) - ).map(_.eventSeqId) - val witnessEventSeqIds = executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed - )(IdRange(1L, 100L), Some(Set.empty), None) - ).map(_.eventSeqId) - val topologyPartyEvents = - executeSql( - backend.event.topologyPartyEventBatch(IdRange(1L, 100L)) - ).map(_.partyId) - activateEventSeqIds shouldBe List(1, 4) - deactivateEventSeqIds shouldBe List(3, 5) - witnessEventSeqIds shouldBe List(2) - topologyPartyEvents shouldBe List( - someParty, - someParty2, - ) // not constrained by ledger end - fetchIdsFromTransactionMetaUpdateIds(allDtos.collect { case meta: DbDto.TransactionMeta => - meta.update_id - }) shouldBe Set((1, 1), (2, 4)) - fetchIdsFromTransactionMetaUpdateIds(allDtos.collect { case meta: DbDto.TransactionMeta => - meta.update_id - }) shouldBe fetchIdsFromTransactionMetaOffsets(allDtos.collect { - case meta: DbDto.TransactionMeta => - meta.event_offset - }) - fetchIdsCreateStakeholder() shouldBe List(1L) - fetchIdsCreateNonStakeholder() shouldBe List(1L) - fetchIdsConsumingStakeholder() shouldBe List(3L) - fetchIdsConsumingNonStakeholder() shouldBe List(3L) - fetchIdsNonConsuming() shouldBe List(2L) - fetchIdsAssignStakeholder() shouldBe List(4L) - fetchTopologyParty() shouldBe List(6) - }, - ) - } - - it should "delete overspill entries written before first ledger end update - events, transaction meta, completions" in { - fixtureOverspillEntriesPriorToFirstLedgerEndUpdate( - dtos = dtos1, - lastOffset = 5, - lastEventSeqId = 7L, - checkContentsAfter = () => { - val contractsCreated = - executeSql( - backend.contract - .activeContracts(List(101, 201), 1000) - ) - val contractsAssigned = - executeSql( - backend.contract - .activeContracts(List(103, 203), 1000) - ) - val topologyPartyEvents = - executeSql( - backend.event.topologyPartyEventBatch(IdRange(1L, 100L)) - ).map(_.partyId) - contractsCreated should not contain hashCid("#101") - contractsAssigned should not contain hashCid("#103") - contractsAssigned should not contain hashCid("#203") - topologyPartyEvents shouldBe empty - fetchIdsFromTransactionMetaUpdateIds(dtos1.collect { case meta: DbDto.TransactionMeta => - meta.update_id - }) shouldBe empty - fetchIdsFromTransactionMetaOffsets(dtos1.collect { case meta: DbDto.TransactionMeta => - meta.event_offset - }) shouldBe empty - fetchIdsCreateStakeholder() shouldBe empty - fetchIdsCreateNonStakeholder() shouldBe empty - fetchIdsConsumingStakeholder() shouldBe empty - fetchIdsConsumingNonStakeholder() shouldBe empty - fetchIdsNonConsuming() shouldBe empty - fetchIdsAssignStakeholder() shouldBe empty - fetchTopologyParty() shouldBe empty - }, - ) - } - - private def fetchIdsNonConsuming(): Vector[Long] = - executeSql( - backend.event.updateStreamingQueries - .variousWitnessIds( - witnessO = Some(someParty), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.NonConsumingExercise)) - .fetchPage(_)( - PaginationFromTo.ascending( - startExclusive = 0, - endInclusive = 1000, - ) - ) - ) - - private def fetchIdsConsumingNonStakeholder(): Vector[Long] = - executeSql( - backend.event.updateStreamingQueries - .deactivateWitnessesIds( - witnessO = Some(someParty), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.ConsumingExercise)) - .fetchPage(_)( - PaginationFromTo.ascending( - startExclusive = 0, - endInclusive = 1000, - ) - ) - ) - - private def fetchIdsConsumingStakeholder(): Vector[Long] = - executeSql( - backend.event.updateStreamingQueries - .deactivateStakeholderIds( - witnessO = Some(someParty), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.ConsumingExercise)) - .fetchPage(_)( - PaginationFromTo.ascending( - startExclusive = 0, - endInclusive = 1000, - ) - ) - ) - - private def fetchIdsCreateNonStakeholder(): Vector[Long] = - executeSql( - backend.event.updateStreamingQueries - .activateWitnessesIds( - witnessO = Some(someParty), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Create)) - .fetchPage(_)( - PaginationFromTo.ascending( - startExclusive = 0, - endInclusive = 1000, - ) - ) - ) - - private def fetchIdsCreateStakeholder(): Vector[Long] = - executeSql( - backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(someParty), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Create)) - .fetchPage(_)( - PaginationFromTo.ascending( - startExclusive = 0, - endInclusive = 1000, - ) - ) - ) - - private def fetchIdsAssignStakeholder(): Vector[Long] = - executeSql( - backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = Some(someParty), - templateIdO = None, - ) - .filteredForEventTypes(Set(PersistentEventType.Assign)) - .fetchPage(_)( - PaginationFromTo.ascending( - startExclusive = 0, - endInclusive = 1000, - ) - ) - ) - - private def fetchTopologyParty(): Vector[Long] = - executeSql( - backend.event - .fetchTopologyPartyEventIds( - party = Some(someParty) - ) - .fetchPage(_)( - PaginationInput( - fromTo = PaginationFromTo.ascending( - startExclusive = 0, - endInclusive = 1000, - ), - limit = 1000, - ) - ) - .ids - ) - - private def fetchIdsFromTransactionMetaUpdateIds( - updateIds: Seq[Array[Byte]] - ): Set[(Long, Long)] = { - val txPointwiseQueries = backend.event.updatePointwiseQueries - updateIds - .map(UpdateId.tryFromByteArray) - .map { updateId => - executeSql( - txPointwiseQueries.fetchIdsFromUpdateMeta( - lookupKey = LookupKey.ByUpdateId(updateId) - ) - ) - } - .flatMap(_.toList) - .toSet - } - - private def fetchIdsFromTransactionMetaOffsets(offsets: Seq[Long]): Set[(Long, Long)] = { - val txPointwiseQueries = backend.event.updatePointwiseQueries - offsets - .map(Offset.tryFromLong) - .map { offset => - executeSql( - txPointwiseQueries.fetchIdsFromUpdateMeta( - lookupKey = LookupKey.ByOffset(offset) - ) - ) - } - .flatMap(_.toList) - .toSet - } - - private def fixture( - dtos1: Vector[DbDto], - lastOffset1: Long, - lastEventSeqId1: Long, - dtos2: Vector[DbDto], - lastOffset2: Long, - lastEventSeqId2: Long, - checkContentsBefore: () => Assertion, - checkContentsAfter: () => Assertion, - ): Assertion = { - val loggerFactory = SuppressingLogger(getClass) - // Initialize - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - // Start the indexer (a no-op in this case) - val end1 = executeSql(backend.parameter.ledgerEnd) - executeSql(backend.ingestion.deletePartiallyIngestedData(end1)) - // Fully insert first batch of updates - executeSql(ingest(dtos1, _)) - executeSql(updateLedgerEnd(ledgerEnd(lastOffset1, lastEventSeqId1))) - // Partially insert second batch of updates (indexer crashes before updating ledger end) - executeSql(ingest(dtos2, _)) - // Check the contents - checkContentsBefore() - // Restart the indexer - should delete data from the partial insert above - val end2 = executeSql(backend.parameter.ledgerEnd) - executeSql(backend.ingestion.deletePartiallyIngestedData(end2)) - // Move the ledger end so that any non-deleted data would become visible - executeSql(updateLedgerEnd(ledgerEnd(lastOffset2 + 1, lastEventSeqId2 + 1))) - // Check the contents - checkContentsAfter() - } - - private def fixtureOverspillEntriesPriorToFirstLedgerEndUpdate( - dtos: Vector[DbDto], - lastOffset: Long, - lastEventSeqId: Long, - checkContentsAfter: () => Assertion, - ): Assertion = { - val loggerFactory = SuppressingLogger(getClass) - // Initialize - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - // Start the indexer (a no-op in this case) - val end1 = executeSql(backend.parameter.ledgerEnd) - executeSql(backend.ingestion.deletePartiallyIngestedData(end1)) - // Insert first batch of updates, but crash before writing the first ledger end - executeSql(ingest(dtos, _)) - // Restart the indexer - should delete data from the partial insert above - val end2 = executeSql(backend.parameter.ledgerEnd) - executeSql(backend.ingestion.deletePartiallyIngestedData(end2)) - // Move the ledger end so that any non-deleted data would become visible - executeSql(updateLedgerEnd(ledgerEnd(lastOffset + 1, lastEventSeqId + 1))) - checkContentsAfter() - } - - behavior of "addContractPruningCandidatesAfter" - - it should "populate candidates correctly during initialization" in { - // baseline: there should be no candidates - contractCandidates() shouldBe Vector.empty - - val ledgerEnd = 1000L - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql( - ingest( - Vector( - // before ledgerEnd1 - dtosCreate( - event_offset = 100L, - event_sequential_id = 100L, - internal_contract_id = 50, - )(), - dtosWitnessedCreate( - event_offset = 1000L, - event_sequential_id = 1000L, - internal_contract_id = 51, - )(), - // between ledgerEnd1 and ledgerEnd2 - - // check activated - // will be added + lower bound check - dtosCreate( - event_offset = 1001L, - event_sequential_id = 1001L, - internal_contract_id = 100, - )(), - // will be added second - dtosCreate( - event_offset = 1002L, - event_sequential_id = 1002L, - internal_contract_id = 101, - )(), - // won't be added: has before activate - dtosCreate( - event_offset = 1003L, - event_sequential_id = 1003L, - internal_contract_id = 50, - )(), - // won't be added: has before witnessed - dtosCreate( - event_offset = 1004L, - event_sequential_id = 1004L, - internal_contract_id = 51, - )(), - // won't be added: already there - dtosCreate( - event_offset = 1005L, - event_sequential_id = 1005L, - internal_contract_id = 1, - )(), - // won't be added: duplicate activate - dtosCreate( - event_offset = 1006L, - event_sequential_id = 1006L, - internal_contract_id = 101, - )(), - // won't be added: duplicate witnessed - dtosCreate( - event_offset = 1007L, - event_sequential_id = 1007L, - internal_contract_id = 104, - )(), - - // check witnessed - // will be added - dtosWitnessedCreate( - event_offset = 1008L, - event_sequential_id = 1008L, - internal_contract_id = 103, - )(), - // will be added second - dtosWitnessedCreate( - event_offset = 1009L, - event_sequential_id = 1009L, - internal_contract_id = 104, - )(), - // won't be added: has before activate - dtosWitnessedCreate( - event_offset = 1010L, - event_sequential_id = 1010L, - internal_contract_id = 50, - )(), - // won't be added: has before witnessed - dtosWitnessedCreate( - event_offset = 1011L, - event_sequential_id = 1011L, - internal_contract_id = 51, - )(), - // won't be added: already there - dtosWitnessedCreate( - event_offset = 1012L, - event_sequential_id = 1012L, - internal_contract_id = 2, - )(), - // won't be added: duplicate activate - dtosWitnessedCreate( - event_offset = 1013L, - event_sequential_id = 1013L, - internal_contract_id = 101, - )(), - // won't be added: duplicate witnessed - dtosWitnessedCreate( - event_offset = 1014L, - event_sequential_id = 1014L, - internal_contract_id = 104, - )(), - - // won't be there, no deactivation is selected - dtosConsumingExercise( - event_offset = 1015L, - event_sequential_id = 1015L, - internal_contract_id = Some(3), - ), - ).flatten, - _, - ) - ) - - manuallyAddContractCandidates(Vector(1, 2)) - contractCandidates() shouldBe Vector(1, 2) - - executeSql { connection => - // non-auto commit is enforced by the PG locking mechanism used inside - connection.setAutoCommit(false) - backend.event.addContractPruningCandidatesAfter(ledgerEnd)(connection, implicitly) - connection.commit() - } - contractCandidates() shouldBe Vector(1, 2, 100, 101, 103, 104) - } - - private def contractCandidates(): Vector[Long] = - executeSql( - SQL""" - select internal_contract_id - from lapi_pruning_contract_candidate - order by internal_contract_id - """.asVectorOf(long("internal_contract_id"))(_) - ) - - private def manuallyAddContractCandidates(internalContractIds: Vector[Long]): Unit = - executeSql( - SQL""" - insert into lapi_pruning_contract_candidate(internal_contract_id) - values #${internalContractIds.map(id => s"($id)").mkString(", ")} - """.executeUpdate()(_) - ) shouldBe internalContractIds.size - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsIntegrity.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsIntegrity.scala deleted file mode 100644 index 129d2f0d90..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsIntegrity.scala +++ /dev/null @@ -1,1119 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import anorm.SqlParser.scalar -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent.{ - Added, - ChangedTo, - Revoked, -} -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel -import com.digitalasset.canton.platform.store.backend.DbDto.{ - EventActivate, - EventDeactivate, - EventVariousWitnessed, -} -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.AchsAddActivationsParams -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Time.Timestamp -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.UUID - -private[backend] trait StorageBackendTestsIntegrity extends Matchers with StorageBackendSpec { - this: AnyFlatSpec => - - import StorageBackendTestValues.* - - private val time1 = Timestamp.now() - private val time2 = time1.addMicros(10) - private val time3 = time2.addMicros(10) - private val time4 = time3.addMicros(10) - private val time5 = time4.addMicros(10) - private val time6 = time5.addMicros(10) - private val time7 = time6.addMicros(10) - - behavior of "IntegrityStorageBackend" - - it should "find duplicate offsets" in { - val updates = Vector( - dtosCreate(event_offset = 7, event_sequential_id = 6L)(), - dtosCreate(event_offset = 7, event_sequential_id = 7L)(), // duplicate offset - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(7), 7L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - - // Error message should contain the duplicate event sequential id - failure.getMessage should include regex "duplicate offsets.* 7" - } - - it should "find duplicate event ids" in { - val updates = Vector( - dtosCreate(event_offset = 6, event_sequential_id = 7L)(), - dtosCreate(event_offset = 7, event_sequential_id = 7L)(), // duplicate id - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(7), 7L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - - // Error message should contain the duplicate event sequential id - failure.getMessage should include regex "duplicate event sequential ids.* 7" - } - - it should "find duplicate entries for lapi_filter_achs_stakeholder filter table" in { - val updates = Vector( - dtosCreate(event_offset = 7, event_sequential_id = 7L)() - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0, endInclusive = 10, activeAt = 10) - ) - ) - executeSql( - backend.event - .addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0, endInclusive = 10, activeAt = 10) - ) - ) - executeSql(updateLedgerEnd(offset(10), 10L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - - failure.getMessage should include regex "duplicate entries found .* in filter table lapi_filter_achs_stakeholder.* at event sequential id 7" - } - - it should "find lapi_filter_achs_stakeholder's entries not present in lapi_filter_activate_stakeholder" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - - executeSql { connection => - SQL""" - INSERT INTO lapi_filter_achs_stakeholder (event_sequential_id, template_id, party_id) VALUES (42, 2, 3), (999, 4, 5) - """.execute()(connection) - } - - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - - failure.getMessage should include regex - "lapi_filter_achs_stakeholder contains entries not present in lapi_filter_activate_stakeholder at event sequential ids.*: 42, 999" - } - - it should "verify lapi_achs_state contains at most one row" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - - // Insert multiple rows into lapi_achs_state to simulate the error - executeSql { connection => - SQL""" - INSERT INTO lapi_achs_state (valid_at, last_removed, last_populated) VALUES (1, 2, 3) - """.execute()(connection) - } - executeSql { connection => - SQL""" - INSERT INTO lapi_achs_state (valid_at, last_removed, last_populated) VALUES (4, 5, 6) - """.execute()(connection) - } - - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - - failure.getMessage should include("lapi_achs_state table contains more than one row") - } - - it should "find non-consecutive event ids" in { - val updates = Vector( - dtosCreate(event_offset = 1, event_sequential_id = 1L)(), - dtosCreate(event_offset = 3, event_sequential_id = 3L)(), // non-consecutive id - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(3), 3L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - - failure.getMessage should include("consecutive") - - } - - it should "not find non-consecutive event ids if those gaps are before the pruning offset" in { - val internalContractId = insertParContracts() - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - internal_contract_id = internalContractId, - )(), - dtosCreate( - event_offset = 3, - event_sequential_id = 3L, - internal_contract_id = internalContractId, - )(), // non-consecutive id but after pruning offset - dtosCreate( - event_offset = 4, - event_sequential_id = 4L, - internal_contract_id = internalContractId, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(backend.parameter.updatePrunedUptoInclusive(offset(2))) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(4), 4L)) - executeSql(backend.integrity.verifyIntegrity()) - } - - it should "detect monotonicity violation of record times for one synchronizer in activate table" in { - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - synchronizer_id = someSynchronizerId, - record_time = time5.micros, - )(), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - synchronizer_id = someSynchronizerId2, - record_time = time1.micros, - )(), - dtosCreate( - event_offset = 3, - event_sequential_id = 3L, - synchronizer_id = someSynchronizerId, - record_time = time7.micros, - )(), - dtosCreate( - event_offset = 4, - event_sequential_id = 4L, - synchronizer_id = someSynchronizerId2, - record_time = time3.micros, - )(), - dtosCreate( - event_offset = 5, - event_sequential_id = 5L, - synchronizer_id = someSynchronizerId, - record_time = time6.micros, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 5L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "occurrence of decreasing record time found within one synchronizer: offsets Offset(3),Offset(5)" - ) - } - - it should "detect monotonicity violation of record times for one synchronizer in deactivate table" in { - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - synchronizer_id = someSynchronizerId, - record_time = time5.micros, - )(), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - synchronizer_id = someSynchronizerId2, - record_time = time1.micros, - )(), - dtosConsumingExercise( - event_offset = 3, - event_sequential_id = 3L, - synchronizer_id = someSynchronizerId, - record_time = time7.micros, - ), - dtosCreate( - event_offset = 4, - event_sequential_id = 4L, - synchronizer_id = someSynchronizerId2, - record_time = time3.micros, - )(), - dtosCreate( - event_offset = 5, - event_sequential_id = 5L, - synchronizer_id = someSynchronizerId, - record_time = time6.micros, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 5L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "occurrence of decreasing record time found within one synchronizer: offsets Offset(3),Offset(5)" - ) - } - - it should "detect monotonicity violation of record times for one synchronizer in various witnessed table" in { - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - synchronizer_id = someSynchronizerId, - record_time = time5.micros, - )(), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - synchronizer_id = someSynchronizerId2, - record_time = time1.micros, - )(), - dtosWitnessedExercised( - event_offset = 3, - event_sequential_id = 3L, - consuming = false, - synchronizer_id = someSynchronizerId, - record_time = time7.micros, - ), - dtosCreate( - event_offset = 4, - event_sequential_id = 4L, - synchronizer_id = someSynchronizerId2, - record_time = time3.micros, - )(), - dtosCreate( - event_offset = 5, - event_sequential_id = 5L, - synchronizer_id = someSynchronizerId, - record_time = time6.micros, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 5L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "occurrence of decreasing record time found within one synchronizer: offsets Offset(3),Offset(5)" - ) - } - - it should "detect monotonicity violation of record times for one synchronizer in completions table" in { - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - synchronizer_id = someSynchronizerId, - record_time = time5.micros, - )(), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - synchronizer_id = someSynchronizerId2, - record_time = time1.micros, - )(), - Seq( - dtoCompletion( - offset(3), - synchronizerId = someSynchronizerId, - recordTime = time7, - ) - ), - dtosCreate( - event_offset = 4, - event_sequential_id = 3L, - synchronizer_id = someSynchronizerId2, - record_time = time3.micros, - )(), - dtosCreate( - event_offset = 5, - event_sequential_id = 4L, - synchronizer_id = someSynchronizerId, - record_time = time6.micros, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 4L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "occurrence of decreasing record time found within one synchronizer: offsets Offset(3),Offset(5)" - ) - } - - it should "detect monotonicity violation of record times for one synchronizer in completions table, if it is a timely-reject going backwards" in { - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - synchronizer_id = someSynchronizerId, - record_time = time5.micros, - )(), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - synchronizer_id = someSynchronizerId2, - record_time = time1.micros, - )(), - Seq( - dtoCompletion( - offset(3), - synchronizerId = someSynchronizerId, - recordTime = time7, - messageUuid = Some("message uuid"), - ) - ), - dtosCreate( - event_offset = 4, - event_sequential_id = 3L, - synchronizer_id = someSynchronizerId2, - record_time = time3.micros, - )(), - dtosCreate( - event_offset = 5, - event_sequential_id = 4L, - synchronizer_id = someSynchronizerId, - record_time = time6.micros, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 4L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "occurrence of decreasing record time found within one synchronizer: offsets Offset(3),Offset(5)" - ) - } - - it should "detect monotonicity violation of record times for one synchronizer in party to participant table" in { - val updates = Vector( - dtoPartyToParticipant( - offset(1), - 1L, - someParty, - someParticipantId, - Added(AuthorizationLevel.Submission), - synchronizerId = someSynchronizerId, - recordTime = time5, - ), - dtoPartyToParticipant( - offset(2), - 2L, - someParty, - someParticipantId, - ChangedTo(AuthorizationLevel.Confirmation), - synchronizerId = someSynchronizerId2, - recordTime = time1, - ), - dtoPartyToParticipant( - offset(3), - 3L, - someParty, - someParticipantId, - ChangedTo(AuthorizationLevel.Observation), - synchronizerId = someSynchronizerId, - recordTime = time7, - ), - dtoPartyToParticipant( - offset(4), - 4L, - someParty, - someParticipantId, - Revoked, - synchronizerId = someSynchronizerId2, - recordTime = time3, - ), - dtoPartyToParticipant( - offset(5), - 5L, - someParty, - someParticipantId, - Added(AuthorizationLevel.Submission), - synchronizerId = someSynchronizerId, - recordTime = time6, - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 5L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "occurrence of decreasing record time found within one synchronizer: offsets Offset(3),Offset(5)" - ) - } - - it should "detect duplicated update ids" in { - val updates = Vector( - dtoTransactionMeta( - offset(1), - 1L, - 4L, - udpateId = Some(updateIdArrayFromOffset(offset(1))), - ), - dtoTransactionMeta( - offset(2), - 1L, - 4L, - udpateId = Some(updateIdArrayFromOffset(offset(2))), - ), - dtoTransactionMeta( - offset(3), - 1L, - 4L, - udpateId = Some(updateIdArrayFromOffset(offset(2))), - ), - dtoTransactionMeta( - offset(4), - 1L, - 4L, - udpateId = Some(updateIdArrayFromOffset(offset(4))), - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 4L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - val hashForOffset2 = updateIdFromOffset(offset(2)).toHexString - failure.getMessage should include( - s"occurrence of duplicate update ID [$hashForOffset2] found for offsets Offset(2), Offset(3)" - ) - } - - it should "detect duplicated completion offsets" in { - val updates = Vector( - dtoCompletion( - offset(1) - ), - dtoCompletion( - offset(2) - ), - dtoCompletion( - offset(2) - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 4L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "occurrence of duplicate offset found for lapi_command_completions: for offset Offset(2) 2 rows found" - ) - } - - it should "detect same completion entries for different offsets" in { - val updates = Vector( - dtoCompletion( - offset(1) - ), - dtoCompletion( - offset(2), - commandId = "commandid", - submissionId = Some("submissionid"), - updateId = Some(updateIdArrayFromOffset(offset(2))), - ), - dtoCompletion( - offset(3), - commandId = "commandid", - submissionId = Some("submissionid"), - updateId = Some(updateIdArrayFromOffset(offset(2))), - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 4L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "duplicate entries found in lapi_command_completions at offsets (first 10 shown) List(Offset(2), Offset(3))" - ) - } - - it should "detect completion entries with the same messageUuid for different offsets" in { - val messageUuid = Some(UUID.randomUUID().toString) - val updates = Vector( - dtoCompletion( - offset(1) - ), - dtoCompletion( - offset(2), - commandId = "commandid1", - submissionId = Some("submissionid1"), - updateId = Some(updateIdArrayFromOffset(offset(2))), - messageUuid = messageUuid, - ), - dtoCompletion( - offset(3), - commandId = "commandid", - submissionId = Some("submissionid"), - updateId = Some(updateIdArrayFromOffset(offset(3))), - messageUuid = messageUuid, - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 4L)) - val failure = - intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "duplicate entries found by messageUuid in lapi_command_completions at offsets (first 10 shown) List(Offset(2), Offset(3))" - ) - } - - it should "not detect same completion entries for different offsets, if synchronizer id differs" in { - val updates = Vector( - dtoCompletion( - offset(1) - ), - dtoCompletion( - offset(2), - commandId = "commandid", - submissionId = Some("submissionid"), - updateId = Some(updateIdArrayFromOffset(offset(2))), - ), - dtoCompletion( - offset(3), - commandId = "commandid", - submissionId = Some("submissionid"), - updateId = Some(updateIdArrayFromOffset(offset(2))), - synchronizerId = SynchronizerId.tryFromString("x::othersynchronizerid"), - ), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 4L)) - executeSql(backend.integrity.verifyIntegrity()) - } - - it should "not find errors beyond the ledger end" in { - val internalContractId = insertParContracts() - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - internal_contract_id = internalContractId, - )(), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - internal_contract_id = internalContractId, - )(), - dtosCreate( - event_offset = 7, - event_sequential_id = 7L, - internal_contract_id = internalContractId, - )(), // beyond the ledger end - dtosCreate( - event_offset = 7, - event_sequential_id = 7L, - internal_contract_id = internalContractId, - )(), // duplicate id (beyond ledger end) - dtosCreate( - event_offset = 9, - event_sequential_id = 9L, - internal_contract_id = internalContractId, - )(), // non-consecutive id (beyond ledger end) - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - executeSql(backend.integrity.verifyIntegrity()) - - // Succeeds if verifyIntegrity() doesn't throw - succeed - } - - private def prepareMissingReferencedParContracts(): Unit = { - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - notPersistedContractId = hashCid("#1"), - internal_contract_id = 1L, - )( - stakeholders = Set(someParty) - ), - dtosConsumingExercise( - event_offset = 2, - event_sequential_id = 2L, - deactivated_event_sequential_id = Some(1L), - internal_contract_id = Some(2L), - ), - dtosWitnessedExercised( - event_offset = 3, - event_sequential_id = 3L, - internal_contract_id = Some(3L), - consuming = false, - ), - // events above are under par_pruning_operation.started_up_to_inclusive, no not checked - dtosCreate( - event_offset = 4, - event_sequential_id = 4L, - notPersistedContractId = hashCid("#1"), - internal_contract_id = 4L, - )( - stakeholders = Set(someParty) - ), - // event above still under par_pruning_operation.started_up_to_inclusive, but has a deactivation event above - dtosCreate( - event_offset = 11, - event_sequential_id = 5L, - notPersistedContractId = hashCid("#1"), - internal_contract_id = 5L, - )( - stakeholders = Set(someParty) - ), - dtosConsumingExercise( - event_offset = 12, - event_sequential_id = 6L, - deactivated_event_sequential_id = Some(1L), - internal_contract_id = Some(6L), - ), - dtosWitnessedExercised( - event_offset = 13, - event_sequential_id = 7L, - internal_contract_id = Some(7L), - consuming = false, - ), - dtosConsumingExercise( - event_offset = 14, - event_sequential_id = 8L, - deactivated_event_sequential_id = Some(4L), - internal_contract_id = Some(4L), - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(14), 8L)) - } - - it should "find missing referenced par_contracts" in { - prepareMissingReferencedParContracts() - insertPruningOperationUpTo(10L) - val failure = intercept[RuntimeException](executeSql(backend.integrity.verifyIntegrity())) - failure.getMessage should include( - "some internal_contract_id-s in events tables are not present in par_contracts (first 10 shown with offsets) [(4,4), (4,14), (5,11), (6,12), (7,13)]" - ) - } - - it should "not report error for missing referenced par_contracts when inMemory" in { - prepareMissingReferencedParContracts() - insertPruningOperationUpTo(0L) - executeSql(backend.integrity.verifyIntegrity(inMemoryCantonStore = true)) - succeed - } - - it should "find stray deactivations" in { - val updates = Vector( - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - notPersistedContractId = hashCid("#2"), - internal_contract_id = 1L, - )( - stakeholders = Set(someParty) - ), - dtosConsumingExercise( // correct deactivation of #2 - event_offset = 3, - event_sequential_id = 3L, - deactivated_event_sequential_id = Some(2L), - internal_contract_id = Some(1L), - ), - dtosConsumingExercise( // unknown deactivated_event_sequential_id - event_offset = 4, - event_sequential_id = 4L, - deactivated_event_sequential_id = Some(1L), - internal_contract_id = Some(1L), - ), - dtosConsumingExercise( // deactivated_event_sequential_id is greater than event_sequential_id - event_offset = 5, - event_sequential_id = 5L, - deactivated_event_sequential_id = Some(6L), - internal_contract_id = Some(1L), - ), - dtosConsumingExercise( // deactivated_event_sequential_id is NULL - not reported - event_offset = 6, - event_sequential_id = 6L, - deactivated_event_sequential_id = None, - ), - dtosCreate( - event_offset = 7, - event_sequential_id = 7L, - notPersistedContractId = hashCid("#7"), - internal_contract_id = 2L, - )( - stakeholders = Set(someParty) - ), - dtosConsumingExercise( // a deactivation after ledger end, should be ignored - event_offset = 100, - event_sequential_id = 100L, - deactivated_event_sequential_id = Some(8L), - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(7), 7L)) - - // using inMemoryCantonStore = true to skip the par_contracts check - val failure = - intercept[RuntimeException]( - executeSql(backend.integrity.verifyIntegrity(inMemoryCantonStore = true)) - ) - failure.getMessage should include( - "some deactivation events do not have a preceding activation event, deactivated_event_sequential_id-s with offsets (first 10 shown) [(1,4), (6,5)]" - ) - } - - it should "report leftover witnessed events after pruning" in { - val updates = Vector( - dtosWitnessedExercised( - event_offset = 2, - event_sequential_id = 2L, - consuming = false, - internal_contract_id = None, - ), - dtosWitnessedExercised( - event_offset = 5, - event_sequential_id = 3L, - consuming = false, - internal_contract_id = None, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(backend.parameter.updatePrunedUptoInclusive(offset(3))) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(5), 3L)) - - // using inMemoryCantonStore = true to skip the par_contracts check - val failure = intercept[RuntimeException]( - executeSql(backend.integrity.verifyIntegrity(inMemoryCantonStore = true)) - ) - - failure.getMessage should include( - "some events in various_witnessed have not been pruned, offsets (first 10 shown) [2]" - ) - } - - it should "report leftover activate events after pruning" in { - val updates = Vector( - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, // deactivated by 3L - )(), - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, // not deactivated - )(), - dtosConsumingExercise( - event_offset = 3, - event_sequential_id = 3L, - deactivated_event_sequential_id = Some(1L), - ), - // incomplete reassignment 1 - dtosCreate( - event_offset = 4, - event_sequential_id = 4L, // deactivated by 5L - )(), - dtosUnassign( - event_offset = 5, - event_sequential_id = 5L, - deactivated_event_sequential_id = Some(4L), - ), - // incomplete reassignment 2 - dtosAssign( - event_offset = 6, - event_sequential_id = 6L, // deactivated by 7L - )(), - dtosConsumingExercise( - event_offset = 7, - event_sequential_id = 7L, - deactivated_event_sequential_id = Some(6L), - ), - // incomplete reassignment 3 - dtosAssign( - event_offset = 8, - event_sequential_id = 8L, // deactivated by 9L - )(), - dtosUnassign( - event_offset = 9, - event_sequential_id = 9L, - deactivated_event_sequential_id = Some(8L), - ), - // after pruning offset - dtosCreate( - event_offset = 10, - event_sequential_id = 10L, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(backend.parameter.updatePrunedUptoInclusive(offset(9))) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(10), 10L)) - - // using inMemoryCantonStore = true to skip the par_contracts check - val failure = intercept[RuntimeException]( - executeSql(backend.integrity.verifyIntegrity(inMemoryCantonStore = true)) - ) - - failure.getMessage should include( - "some events in activate have not been pruned, offsets (first 10 shown) [1]" - ) - } - - it should "report leftover deactivate events after pruning" in { - val updates = Vector( - dtosConsumingExercise( - event_offset = 2, - event_sequential_id = 2L, - deactivated_event_sequential_id = None, - ), - dtosConsumingExercise( - event_offset = 5, - event_sequential_id = 3L, - deactivated_event_sequential_id = None, - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(backend.parameter.updatePrunedUptoInclusive(offset(3))) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - - // using inMemoryCantonStore = true to skip the par_contracts check - val failure = intercept[RuntimeException]( - executeSql(backend.integrity.verifyIntegrity(inMemoryCantonStore = true)) - ) - - failure.getMessage should include( - "some events in deactivate have not been pruned, offsets (first 10 shown) [2]" - ) - } - - it should "report leftover ACHS filter entries after pruning" in { - val updates = Vector( - // activation that will be deactivated within the pruning range - dtosCreate( - event_offset = 1, - event_sequential_id = 1L, - )(), - // activation that will NOT be deactivated (should survive in ACHS) - dtosCreate( - event_offset = 2, - event_sequential_id = 2L, - )(), - // deactivation of seq id 1, within the pruning range - dtosConsumingExercise( - event_offset = 3, - event_sequential_id = 3L, - deactivated_event_sequential_id = Some(1L), - ), - // after pruning range - dtosCreate( - event_offset = 10, - event_sequential_id = 4L, - )(), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(10), 4L)) - // Populate ACHS: both seq ids 1 and 2 are added (activeAt=2 means deactivation at seq id 3 is not yet visible) - executeSql( - backend.event.addActivationsToAchs( - AchsAddActivationsParams(startExclusive = 0, endInclusive = 2, activeAt = 2) - ) - ) - // Simulate pruning: remove the activation and deactivation events that would have been pruned, - // but deliberately leave the ACHS entry for seq id 1 behind (simulating a bug where ACHS pruning was skipped). - executeSql { connection => - SQL"DELETE FROM lapi_events_activate_contract WHERE event_sequential_id = 1".execute()( - connection - ) - SQL"DELETE FROM lapi_filter_activate_stakeholder WHERE event_sequential_id = 1".execute()( - connection - ) - SQL"DELETE FROM lapi_events_deactivate_contract WHERE event_sequential_id = 3".execute()( - connection - ) - } - // Set pruning offset past the deactivation - executeSql(backend.parameter.updatePrunedUptoInclusive(offset(3))) - - // The integrity check should detect that ACHS still contains seq id 1, which references a pruned activation - val failure = intercept[RuntimeException]( - executeSql(backend.integrity.verifyIntegrity(inMemoryCantonStore = true)) - ) - - failure.getMessage should include( - "lapi_filter_achs_stakeholder contains entries not present in lapi_filter_activate_stakeholder" - ) - } - - private def insertParContracts(): Long = - executeSql( - SQL"INSERT INTO par_contracts (contract_id, instance, package_id, template_id) VALUES (${"c".getBytes}, ${"d".getBytes}, 'pid', 'tid')" - .executeInsert(scalar[Long].single)(_) - ) - - def insertPruningOperationUpTo[A](offset: Long): Unit = - executeSql( - SQL"INSERT INTO par_pruning_operation (name, started_up_to_inclusive) VALUES ('n', $offset)" - .execute()(_) - ) - - private def performMissingMandatoryFieldCheck(updates: Seq[DbDto]) = { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates.toVector, _)) - executeSql(updateLedgerEnd(offset(2), 2L)) - - // using inMemoryCantonStore = true to skip the par_contracts check - val failure = - intercept[RuntimeException]( - executeSql(backend.integrity.verifyIntegrity(inMemoryCantonStore = true)) - ) - failure.getMessage should include( - "some events are missing mandatory fields, event_sequential_ids, offsets (first 10 shown) [(3,3)]" - ) - } - - private def checkMissingAssignField(f: String)(c: EventActivate => EventActivate) = - it should s"find missing mandatory Assign fields: $f" in { - performMissingMandatoryFieldCheck( - dtosAssign(event_offset = 3, event_sequential_id = 3L)( - stakeholders = Set(someParty) - ).map { - case t: EventActivate => c(t) - case o => o - } - ) - } - - private def checkMissingConsumingExerciseField(f: String)(c: EventDeactivate => EventDeactivate) = - it should s"find missing mandatory Consuming Exercise fields: $f" in { - performMissingMandatoryFieldCheck( - dtosCreate(event_sequential_id = 2L)(Set(someParty)) ++ - dtosConsumingExercise( - event_offset = 3, - event_sequential_id = 3L, - deactivated_event_sequential_id = Some(2L), - ).map { - case t: EventDeactivate => c(t) - case o => o - } - ) - } - - private def checkUnassign(f: String)(c: EventDeactivate => EventDeactivate) = - it should s"find missing mandatory Unassign fields: $f" in { - performMissingMandatoryFieldCheck( - dtosCreate(event_sequential_id = 2L)(Set(someParty)) ++ - dtosUnassign( - event_offset = 3, - event_sequential_id = 3L, - deactivated_event_sequential_id = Some(2L), - ).map { - case t: EventDeactivate => c(t) - case o => o - } - ) - } - - private def checkNonConsumingExercise( - f: String - )(c: EventVariousWitnessed => EventVariousWitnessed) = - it should s"find missing mandatory NonConsuming Exercise fields: $f" in { - performMissingMandatoryFieldCheck( - dtosWitnessedExercised(event_offset = 3, event_sequential_id = 3L, consuming = false).map { - case t: EventVariousWitnessed => c(t) - case o => o - } - ) - } - - private def checkMissingWitnessedCreateField( - f: String - )(c: EventVariousWitnessed => EventVariousWitnessed) = - it should s"find missing mandatory Witnessed Create fields: $f" in { - performMissingMandatoryFieldCheck( - dtosWitnessedCreate(event_offset = 3, event_sequential_id = 3L)().map { - case t: EventVariousWitnessed => c(t) - case o => o - } - ) - } - - private def checkMissingWitnessedConsumingExerciseField( - f: String - )(c: EventVariousWitnessed => EventVariousWitnessed) = - it should s"find missing mandatory Witnessed Consuming Exercise fields: $f" in { - performMissingMandatoryFieldCheck( - dtosWitnessedExercised(event_offset = 3, event_sequential_id = 3L, consuming = true).map { - case t: EventVariousWitnessed => c(t) - case o => o - } - ) - } - - checkMissingAssignField("source_synchronizer_id")(_.copy(source_synchronizer_id = None)) - checkMissingAssignField("reassignment_counter")(_.copy(reassignment_counter = None)) - checkMissingAssignField("reassignment_id")(_.copy(reassignment_id = None)) - checkMissingConsumingExerciseField("additional_witnesses")(_.copy(additional_witnesses = None)) - checkMissingConsumingExerciseField("exercise_choice")(_.copy(exercise_choice = None)) - checkMissingConsumingExerciseField("exercise_argument")(_.copy(exercise_argument = None)) - checkMissingConsumingExerciseField("exercise_result")(_.copy(exercise_result = None)) - checkMissingConsumingExerciseField("exercise_actors")(_.copy(exercise_actors = None)) - checkMissingConsumingExerciseField("ledger_effective_time")(_.copy(ledger_effective_time = None)) - checkUnassign("reassignment_id")(_.copy(reassignment_id = None)) - checkUnassign("target_synchronizer_id")(_.copy(target_synchronizer_id = None)) - checkUnassign("reassignment_counter")(_.copy(reassignment_counter = None)) - checkNonConsumingExercise("consuming")(_.copy(consuming = None)) - checkNonConsumingExercise("exercise_choice")(_.copy(exercise_choice = None)) - checkNonConsumingExercise("exercise_argument")(_.copy(exercise_argument = None)) - checkNonConsumingExercise("exercise_result")(_.copy(exercise_result = None)) - checkNonConsumingExercise("exercise_actors")(_.copy(exercise_actors = None)) - checkNonConsumingExercise("contract_id")(_.copy(contract_id = None)) - checkNonConsumingExercise("template_id")(_.copy(template_id = None)) - checkNonConsumingExercise("package_id")(_.copy(package_id = None)) - checkMissingWitnessedCreateField("representative_package_id")( - _.copy(representative_package_id = None) - ) - checkMissingWitnessedCreateField("internal_contract_id")(_.copy(internal_contract_id = None)) - checkMissingWitnessedConsumingExerciseField("consuming")(_.copy(consuming = None)) - checkMissingWitnessedConsumingExerciseField("exercise_choice")(_.copy(exercise_choice = None)) - checkMissingWitnessedConsumingExerciseField("exercise_argument")(_.copy(exercise_argument = None)) - checkMissingWitnessedConsumingExerciseField("exercise_result")(_.copy(exercise_result = None)) - checkMissingWitnessedConsumingExerciseField("exercise_actors")(_.copy(exercise_actors = None)) - checkMissingWitnessedConsumingExerciseField("contract_id")(_.copy(contract_id = None)) - checkMissingWitnessedConsumingExerciseField("template_id")(_.copy(template_id = None)) - checkMissingWitnessedConsumingExerciseField("package_id")(_.copy(package_id = None)) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParameters.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParameters.scala deleted file mode 100644 index c214858b00..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParameters.scala +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import anorm.SqlStringInterpolation -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.ledger.participant.state.{RepairIndex, SynchronizerIndex} -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsLastPointers, - AchsState, - LedgerEnd, -} -import com.digitalasset.canton.{HasExecutionContext, RepairCounter} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, OptionValues} - -private[backend] trait StorageBackendTestsParameters - extends Matchers - with Inside - with OptionValues - with StorageBackendSpec - with HasExecutionContext { this: AnyFlatSpec => - - behavior of "StorageBackend Parameters" - - import StorageBackendTestValues.* - - it should "store and retrieve ledger end and synchronizer indexes correctly" in { - val someOffset = offset(1) - val someSequencerTime = CantonTimestamp.now().plusSeconds(10) - val someSynchronizerIndex = SynchronizerIndex.forRepairUpdate( - RepairIndex( - timestamp = someSequencerTime, - counter = RepairCounter(20), - ) - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(backend.parameter.ledgerEnd) shouldBe LedgerEnd.beforeBegin - executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId) - ) shouldBe None - val someSynchronizerIdInterned = - backend.stringInterningSupport.synchronizerId.internalize( - StorageBackendTestValues.someSynchronizerId - ) - executeSql(connection => - ingest( - Vector( - DbDto.StringInterningDto( - someSynchronizerIdInterned, - "d|" + StorageBackendTestValues.someSynchronizerId.toProtoPrimitive, - ) - ), - connection, - ) - ) - executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId2) - ) shouldBe None - val someSynchronizerIdInterned2 = - backend.stringInterningSupport.synchronizerId.internalize( - StorageBackendTestValues.someSynchronizerId2 - ) - executeSql(connection => - ingest( - Vector( - DbDto.StringInterningDto( - someSynchronizerIdInterned2, - "d|" + StorageBackendTestValues.someSynchronizerId2.toProtoPrimitive, - ) - ), - connection, - ) - ) - - // updating ledger end and inserting one synchronizer index - executeSql( - backend.parameter.updateLedgerEnd( - ledgerEnd = LedgerEnd( - lastOffset = someOffset, - lastEventSeqId = 1, - lastStringInterningId = 1, - lastPublicationTime = CantonTimestamp.MinValue.plusSeconds(10), - ), - lastSynchronizerIndex = Map( - StorageBackendTestValues.someSynchronizerId -> someSynchronizerIndex - ), - ) - ) - executeSql(backend.parameter.ledgerEnd) shouldBe Some( - LedgerEnd( - lastOffset = someOffset, - lastEventSeqId = 1, - lastStringInterningId = 1, - lastPublicationTime = CantonTimestamp.MinValue.plusSeconds(10), - ) - ) - val resultSynchronizerIndex = executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId) - ) - resultSynchronizerIndex.value.repairIndex shouldBe someSynchronizerIndex.repairIndex - resultSynchronizerIndex.value.sequencerIndex shouldBe someSynchronizerIndex.sequencerIndex - resultSynchronizerIndex.value.recordTime shouldBe someSynchronizerIndex.recordTime - - // updating ledger end and inserting two synchronizer index (one is updating just the request index part, the other is inserting just a sequencer index) - val someSynchronizerIndexSecond = SynchronizerIndex.forRepairUpdate( - RepairIndex( - timestamp = someSequencerTime.plusSeconds(10), - counter = RepairCounter.Genesis, - ) - ) - val someSynchronizerIndex2 = SynchronizerIndex.forSequencedUpdate( - sequencerTimestamp = someSequencerTime.plusSeconds(5) - ) - executeSql( - backend.parameter.updateLedgerEnd( - ledgerEnd = LedgerEnd( - lastOffset = offset(100), - lastEventSeqId = 100, - lastStringInterningId = 100, - lastPublicationTime = CantonTimestamp.MinValue.plusSeconds(100), - ), - lastSynchronizerIndex = Map( - StorageBackendTestValues.someSynchronizerId -> someSynchronizerIndexSecond, - StorageBackendTestValues.someSynchronizerId2 -> someSynchronizerIndex2, - ), - ) - ) - executeSql(backend.parameter.ledgerEnd) shouldBe Some( - LedgerEnd( - lastOffset = offset(100), - lastEventSeqId = 100, - lastStringInterningId = 100, - lastPublicationTime = CantonTimestamp.MinValue.plusSeconds(100), - ) - ) - val resultSynchronizerIndexSecond = executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId) - ) - resultSynchronizerIndexSecond.value.repairIndex shouldBe someSynchronizerIndexSecond.repairIndex - resultSynchronizerIndexSecond.value.sequencerIndex shouldBe someSynchronizerIndex.sequencerIndex - resultSynchronizerIndexSecond.value.recordTime shouldBe someSynchronizerIndexSecond.recordTime - val resultSynchronizerIndexSecond2 = executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId2) - ) - resultSynchronizerIndexSecond2.value.repairIndex shouldBe None - resultSynchronizerIndexSecond2.value.sequencerIndex shouldBe someSynchronizerIndex2.sequencerIndex - resultSynchronizerIndexSecond2.value.recordTime shouldBe someSynchronizerIndex2.recordTime - - // updating ledger end and inserting one synchronizer index only overriding the record time - val someSynchronizerIndexThird = - SynchronizerIndex.forFloatingUpdate(someSequencerTime.plusSeconds(20)) - executeSql( - backend.parameter.updateLedgerEnd( - ledgerEnd = LedgerEnd( - lastOffset = offset(200), - lastEventSeqId = 200, - lastStringInterningId = 200, - lastPublicationTime = CantonTimestamp.MinValue.plusSeconds(200), - ), - lastSynchronizerIndex = Map( - StorageBackendTestValues.someSynchronizerId -> someSynchronizerIndexThird - ), - ) - ) - executeSql(backend.parameter.ledgerEnd) shouldBe Some( - LedgerEnd( - lastOffset = offset(200), - lastEventSeqId = 200, - lastStringInterningId = 200, - lastPublicationTime = CantonTimestamp.MinValue.plusSeconds(200), - ) - ) - val resultSynchronizerIndexThird = executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId) - ) - resultSynchronizerIndexThird.value.repairIndex shouldBe someSynchronizerIndexSecond.repairIndex - resultSynchronizerIndexThird.value.sequencerIndex shouldBe someSynchronizerIndex.sequencerIndex - resultSynchronizerIndexThird.value.recordTime shouldBe someSequencerTime.plusSeconds(20) - - // resetting and disabling interning - backend.stringInterningSupport.reset() - backend.stringInterningSupport.setAutoIntern(false) - - // ensuring that auto-interning indeed does not work - backend.stringInterningSupport.synchronizerId.tryInternalize( - StorageBackendTestValues.someSynchronizerId - ) shouldBe None - backend.stringInterningSupport.synchronizerId.tryInternalize( - StorageBackendTestValues.someSynchronizerId2 - ) shouldBe None - - // ensuring the same results - executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId) - ) shouldBe resultSynchronizerIndexThird - executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId2) - ) shouldBe resultSynchronizerIndexSecond2 - - // and if string interning table is wiped - executeSql { c => - SQL"delete from lapi_string_interning".executeUpdate()(c) shouldBe 2 - } - - // cleanSynchronizerIndex returns empty - executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId) - ) shouldBe None - executeSql( - backend.parameter.cleanSynchronizerIndex(StorageBackendTestValues.someSynchronizerId2) - ) shouldBe None - } - - it should "store and retrieve post processing end correctly" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(backend.parameter.postProcessingEnd) shouldBe None - executeSql(backend.parameter.updatePostProcessingEnd(Some(offset(10)))) - executeSql(backend.parameter.postProcessingEnd) shouldBe Some(offset(10)) - executeSql(backend.parameter.updatePostProcessingEnd(Some(offset(20)))) - executeSql(backend.parameter.postProcessingEnd) shouldBe Some(offset(20)) - executeSql(backend.parameter.updatePostProcessingEnd(None)) - executeSql(backend.parameter.postProcessingEnd) shouldBe None - } - - it should "fetch and update AchsState correctly" in { - executeSql(backend.parameter.fetchACHSState) shouldBe None - - val achsState0 = AchsState( - validAt = 1000L, - lastPointers = AchsLastPointers( - lastRemoved = 123L, - lastPopulated = 10L, - ), - ) - // check insertion to empty state - executeSql(backend.parameter.insertACHSState(achsState0)) - executeSql(backend.parameter.fetchACHSState) shouldBe Some(achsState0) - - // check updates of validAt - executeSql(backend.parameter.updateACHSValidAt(validAt = 2000L)) - val achsState1 = achsState0.copy(validAt = 2000L) - executeSql(backend.parameter.fetchACHSState) shouldBe Some(achsState1) - - // check updates of lastRemoved and lastPopulated - executeSql( - backend.parameter.updateACHSLastPointers( - AchsLastPointers(lastRemoved = 200L, lastPopulated = 20L) - ) - ) - val achsState2 = - achsState1.copy(lastPointers = AchsLastPointers(lastRemoved = 200L, lastPopulated = 20L)) - executeSql(backend.parameter.fetchACHSState) shouldBe Some(achsState2) - - // clear the state - executeSql(backend.parameter.clearACHSState) - executeSql(backend.parameter.fetchACHSState) shouldBe None - - // updating a non-existing state with validAt fails - an[IllegalStateException] should be thrownBy executeSql( - backend.parameter.updateACHSValidAt(validAt = 3000L) - ) - - // updating a non-existing state with lastRemoved and lastPopulated fails - an[IllegalStateException] should be thrownBy executeSql( - backend.parameter.updateACHSLastPointers( - AchsLastPointers(lastRemoved = 300L, lastPopulated = 30L) - ) - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParticipantMetadata.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParticipantMetadata.scala deleted file mode 100644 index c3b28bf288..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParticipantMetadata.scala +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.platform.store.backend.localstore.ParticipantMetadataBackend -import org.scalatest.OptionValues -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.nio.charset.StandardCharsets -import java.sql.SQLException - -trait TestedResource { - def createResourceAndReturnInternalId(): Int - def fetchResourceVersion(): Long -} - -private[backend] trait ParticipantResourceMetadataTests - extends Matchers - with StorageBackendSpec - with OptionValues { - this: AnyFlatSpec => - - private def tested = ParticipantMetadataBackend - - def resourceVersionTableName: String - def resourceAnnotationsTableName: String - def newResource(): TestedResource - - it should "compare and swap resource version" in { - val resource = newResource() - val internalId = resource.createResourceAndReturnInternalId() - resource.fetchResourceVersion() shouldBe 0 - executeSql( - tested.compareAndIncreaseResourceVersion(resourceVersionTableName)( - internalId = internalId, - expectedResourceVersion = 0, - ) - ) shouldBe true - executeSql( - tested.compareAndIncreaseResourceVersion(resourceVersionTableName)( - internalId = internalId, - expectedResourceVersion = 404, - ) - ) shouldBe false - executeSql( - tested.compareAndIncreaseResourceVersion(resourceVersionTableName)( - internalId = internalId, - expectedResourceVersion = 1, - ) - ) shouldBe true - resource.fetchResourceVersion() shouldBe 2 - } - - it should "get, add and delete user's annotations" in { - val resource = newResource() - val internalId = resource.createResourceAndReturnInternalId() - executeSql(tested.getAnnotations(resourceAnnotationsTableName)(internalId)) shouldBe Map.empty - // Add key1 - executeSql( - tested.addAnnotation(resourceAnnotationsTableName)( - internalId, - key = "key1", - value = "value1", - updatedAt = 123, - ) - ) - executeSql(tested.getAnnotations(resourceAnnotationsTableName)(internalId)) shouldBe Map( - "key1" -> "value1" - ) - // Add key2 - executeSql( - tested.addAnnotation(resourceAnnotationsTableName)( - internalId, - key = "key2", - value = "value2", - updatedAt = 123, - ) - ) - executeSql(tested.getAnnotations(resourceAnnotationsTableName)(internalId)) shouldBe Map( - "key1" -> "value1", - "key2" -> "value2", - ) - // Duplicated key2 - assertThrows[SQLException]( - executeSql( - tested - .addAnnotation(resourceAnnotationsTableName)( - internalId, - key = "key2", - value = "value2b", - updatedAt = 123, - ) - ) - ) - // Delete - executeSql(tested.deleteAnnotations(resourceAnnotationsTableName)(internalId)) - executeSql(tested.getAnnotations(resourceAnnotationsTableName)(internalId)) shouldBe Map.empty - } - - it should "store and retrieve an empty annotation value" in { - val resource = newResource() - val internalId = resource.createResourceAndReturnInternalId() - executeSql( - tested.addAnnotation(resourceAnnotationsTableName)( - internalId, - key = "key", - value = "", - updatedAt = 0, - ) - ) - executeSql(tested.getAnnotations(resourceAnnotationsTableName)(internalId)) shouldBe Map( - "key" -> "" - ) - } - - it should "allow to store 256kb of annotations (counted in utf-8 bytes)" in { - val resource = newResource() - val internalId = resource.createResourceAndReturnInternalId() - val key = "key" - val value = "a" * (256 * 1024 - 3) - (key.getBytes(StandardCharsets.UTF_8).length + value - .getBytes(StandardCharsets.UTF_8) - .length) shouldBe 256 * 1024 - executeSql( - tested.addAnnotation(resourceAnnotationsTableName)( - internalId, - key = key, - value = value, - updatedAt = 0, - ) - ) - } - - it should "allow to store key of length 317 " in { - val resource = newResource() - val internalId = resource.createResourceAndReturnInternalId() - val longestKeyPrefix = "a" * 253 - val longestKeyName = "b" * 63 - val longestKey = s"$longestKeyPrefix/$longestKeyName" - longestKey should have length (317) - executeSql( - tested.addAnnotation(resourceAnnotationsTableName)( - internalId, - key = longestKey, - value = "longest key", - updatedAt = 0, - ) - ) - } - - it should "store and retrieve trailing spaces in annotation keys" in { - val resource = newResource() - val internalId = resource.createResourceAndReturnInternalId() - val key = "key" - val value = "a" + " " * 10 - executeSql( - tested.addAnnotation(resourceAnnotationsTableName)( - internalId, - key = key, - value = value, - updatedAt = 0, - ) - ) - executeSql(tested.getAnnotations(resourceAnnotationsTableName)(internalId)) shouldBe - Map(key -> value) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParties.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParties.scala deleted file mode 100644 index 5c42878adf..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsParties.scala +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.{HasExecutionContext, LfPartyId} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, OptionValues} - -private[backend] trait StorageBackendTestsParties - extends Matchers - with Inside - with OptionValues - with StorageBackendSpec - with HasExecutionContext { this: AnyFlatSpec => - - behavior of "StorageBackend (parties)" - - import StorageBackendTestValues.* - import com.digitalasset.daml.lf.data.Ref.Party.assertFromString as party - - it should "ingest a single party update" in { - val someOffset = offset(1) - val dtos = Vector( - dtoPartyEntry(someOffset) - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - val partiesBeforeLedgerEndUpdate = executeSql(backend.party.knownParties(None, None, 10)) - executeSql( - updateLedgerEnd(someOffset, ledgerEndSequentialId = 0) - ) - val partiesAfterLedgerEndUpdate = executeSql(backend.party.knownParties(None, None, 10)) - - // The first query is executed before the ledger end is updated. - // It should not see the already ingested party allocation. - partiesBeforeLedgerEndUpdate shouldBe empty - - // The second query should now see the party. - partiesAfterLedgerEndUpdate should not be empty - } - - it should "accumulate multiple party records into one response" in { - val dtos = Vector( - // singular non-local - dtoPartyEntry(offset(1), party("aaf"), isLocal = false), - // singular local - dtoPartyEntry(offset(2), party("bbt"), isLocal = true), - // desired values in last record - dtoPartyEntry(offset(3), party("cct"), isLocal = false), - dtoPartyEntry(offset(4), party("cct"), isLocal = false), - dtoPartyEntry(offset(5), party("cct"), isLocal = true), - // desired values in last record except of is-local - dtoPartyEntry(offset(6), party("ddt"), isLocal = false), - dtoPartyEntry(offset(7), party("ddt"), isLocal = true), - dtoPartyEntry(offset(8), party("ddt"), isLocal = false), - // desired values in last record, reject coming in the middle - dtoPartyEntry(offset(9), party("eef"), isLocal = false), - dtoPartyEntry(offset(10), party("eef"), isLocal = true, reject = true), - dtoPartyEntry(offset(11), party("eef"), isLocal = false), - // desired values in middle record, reject coming last - dtoPartyEntry(offset(12), party("fff"), isLocal = false), - dtoPartyEntry(offset(13), party("fff"), isLocal = false), - dtoPartyEntry(offset(14), party("fff"), isLocal = true, reject = true), - // desired values before ledger end, undesired accept after ledger end - dtoPartyEntry(offset(15), party("ggf"), isLocal = false), - dtoPartyEntry(offset(17), party("ggf"), isLocal = true), - // desired values before ledger end, undesired reject after ledger end - dtoPartyEntry(offset(16), party("hhf"), isLocal = false), - dtoPartyEntry(offset(18), party("hhf"), isLocal = true, reject = true), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - // ledger end deliberately omitting the last test entries - executeSql( - updateLedgerEnd(offset(16), ledgerEndSequentialId = 0) - ) - - def validateEntries(entry: IndexerPartyDetails): Unit = - entry.isLocal shouldBe entry.party.lastOption.contains('t') - - val allKnownParties = executeSql(backend.party.knownParties(None, None, 10)) - allKnownParties.length shouldBe 8 - allKnownParties.foreach(validateEntries) - - val pageOne = executeSql(backend.party.knownParties(None, None, 4)) - pageOne.length shouldBe 4 - pageOne.foreach(validateEntries) - pageOne.exists(_.party == "aaf") shouldBe true - pageOne.exists(_.party == "bbt") shouldBe true - pageOne.exists(_.party == "cct") shouldBe true - pageOne.exists(_.party == "ddt") shouldBe true - - val pageTwo = - executeSql(backend.party.knownParties(Some(LfPartyId.assertFromString("ddt")), None, 10)) - pageTwo.length shouldBe 4 - pageTwo.foreach(validateEntries) - pageTwo.exists(_.party == "eef") shouldBe true - pageTwo.exists(_.party == "fff") shouldBe true - pageTwo.exists(_.party == "ggf") shouldBe true - pageTwo.exists(_.party == "hhf") shouldBe true - } - - it should "get all parties ordered by id using binary collation" in { - val dtos = Vector( - dtoPartyEntry(offset(1), party("a"), isLocal = false), - dtoPartyEntry(offset(2), party("a-"), isLocal = false), - dtoPartyEntry(offset(3), party("b"), isLocal = false), - dtoPartyEntry(offset(4), party("a_"), isLocal = false), - dtoPartyEntry(offset(5), party("-a"), isLocal = false), - dtoPartyEntry(offset(6), party("_a"), isLocal = false), - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - // ledger end deliberately omitting the last test entries - executeSql( - updateLedgerEnd(offset(6), ledgerEndSequentialId = 0) - ) - - val allKnownParties = executeSql(backend.party.knownParties(None, None, 10)) - allKnownParties.length shouldBe 6 - - val filteredParties = - executeSql(backend.party.knownParties(None, Some(String185.tryCreate("a-")), 10)) - filteredParties.length shouldBe 1 - - allKnownParties - .map(_.party) shouldBe Seq("-a", "_a", "a", "a-", "a_", "b") - - val pageOne = executeSql(backend.party.knownParties(None, None, 3)) - pageOne.length shouldBe 3 - pageOne - .map(_.party) shouldBe Seq("-a", "_a", "a") - - val pageTwo = - executeSql(backend.party.knownParties(Some(LfPartyId.assertFromString("a")), None, 10)) - pageTwo.length shouldBe 3 - pageTwo - .map(_.party) shouldBe Seq("a-", "a_", "b") - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPartyRecord.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPartyRecord.scala deleted file mode 100644 index 89744c6a26..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPartyRecord.scala +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId} -import com.digitalasset.canton.platform.store.backend.localstore.PartyRecordStorageBackend -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.LedgerString -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, OptionValues} - -import java.sql.SQLException -import java.util.UUID - -private[backend] trait StorageBackendTestsPartyRecord - extends Matchers - with Inside - with StorageBackendSpec - with OptionValues - with ParticipantResourceMetadataTests { - this: AnyFlatSpec => - - behavior of "StorageBackend (party record)" - - private val zeroMicros: Long = 0 - - private val idpId = IdentityProviderId.Id(Ref.LedgerString.assertFromString("idp1")) - private val idpConfig = IdentityProviderConfig( - identityProviderId = idpId, - isDeactivated = false, - jwksUrl = JwksUrl("http//identityprovider.org/"), - issuer = "issuer", - audience = Some("audience"), - ) - - private def tested = backend.participantPartyStorageBackend - - override def newResource(): TestedResource = new TestedResource { - private val partyRecord = newDbPartyRecord() - - override def createResourceAndReturnInternalId(): Int = { - val internalId = executeSql(tested.createPartyRecord(partyRecord)) - internalId - } - - override def fetchResourceVersion(): Long = - executeSql(tested.getPartyRecord(partyRecord.party)).value.payload.resourceVersion - } - - override def resourceVersionTableName: String = "lapi_party_records" - - override def resourceAnnotationsTableName: String = "lapi_party_record_annotations" - - it should "handle created_at attribute correctly" in { - val partyRecord = newDbPartyRecord(createdAt = 123) - val _ = executeSql(tested.createPartyRecord(partyRecord)) - executeSql(tested.getPartyRecord(partyRecord.party)).map(_.payload.createdAt) shouldBe Some(123) - } - - it should "create party record (createPartyRecord)" in { - val partyRecord1 = newDbPartyRecord() - val partyRecord2 = newDbPartyRecord() - val internalId1 = executeSql(tested.createPartyRecord(partyRecord1)) - // Attempting to add a duplicate user - assertThrows[SQLException](executeSql(tested.createPartyRecord(partyRecord1))) - val internalId2 = executeSql(tested.createPartyRecord(partyRecord2)) - val _ = - executeSql(tested.createPartyRecord(newDbPartyRecord())) - internalId1 should not equal internalId2 - } - - it should "handle party record ops (getPartyRecord)" in { - val partyRecord1 = newDbPartyRecord() - val partyRecord2 = newDbPartyRecord() - val _ = executeSql(tested.createPartyRecord(partyRecord1)) - val getExisting = executeSql(tested.getPartyRecord(partyRecord1.party)) - val getNonexistent = executeSql(tested.getPartyRecord(partyRecord2.party)) - getExisting.value.payload shouldBe partyRecord1 - getNonexistent shouldBe None - } - - it should "filter parties within the same idp" in { - val idpId = IdentityProviderId.Id(LedgerString.assertFromString("abc")) - val _ = executeSql( - backend.identityProviderStorageBackend.createIdentityProviderConfig( - IdentityProviderConfig( - identityProviderId = idpId, - issuer = "issuer", - jwksUrl = JwksUrl("http://daml.com/jwks.json"), - audience = None, - ) - ) - ) - val party1 = Ref.Party.assertFromString("party1") - val party2 = Ref.Party.assertFromString("party2") - val partyRecord1 = newDbPartyRecord(partyId = "party1") - val partyRecord2 = newDbPartyRecord( - partyId = "party2", - identityProviderId = Some(idpId), - ) - val _ = executeSql(tested.createPartyRecord(partyRecord1)) - val _ = executeSql(tested.createPartyRecord(partyRecord2)) - executeSql( - tested.filterExistingParties( - Set(), - Some(IdentityProviderId.Id(LedgerString.assertFromString("cde"))), - ) - ) shouldBe Set.empty - - executeSql( - tested.filterExistingParties( - Set(), - None, - ) - ) shouldBe Set.empty - - executeSql( - tested.filterExistingParties( - Set(party1, party2), - None, - ) - ) shouldBe Set(party1) - - executeSql( - tested.filterExistingParties( - Set(party1, party2), - Some(idpId), - ) - ) shouldBe Set(party2) - } - - it should "update party's identityProviderId" in { - executeSql( - backend.identityProviderStorageBackend.createIdentityProviderConfig( - idpConfig - ) - ) - // create with the default idp - val pr = newDbPartyRecord( - createdAt = 123, - partyId = "party", - identityProviderId = IdentityProviderId.Default.toDb, - ) - val internalId = executeSql(tested.createPartyRecord(pr)) - executeSql( - (tested.getPartyRecord(pr.party)) - ).value.payload.identityProviderId shouldBe IdentityProviderId.Default.toDb - // update to idp1 - executeSql( - tested.updatePartyRecordIdp(internalId, identityProviderId = idpId.toDb) - ) shouldBe true - executeSql( - (tested.getPartyRecord(pr.party)) - ).value.payload.identityProviderId shouldBe idpId.toDb - // update to idp1 again - executeSql( - tested.updatePartyRecordIdp(internalId, identityProviderId = idpId.toDb) - ) shouldBe true - executeSql( - (tested.getPartyRecord(pr.party)) - ).value.payload.identityProviderId shouldBe idpId.toDb - // update to the default idp - executeSql( - tested.updatePartyRecordIdp(internalId, identityProviderId = IdentityProviderId.Default.toDb) - ) shouldBe true - executeSql( - (tested.getPartyRecord(pr.party)) - ).value.payload.identityProviderId shouldBe IdentityProviderId.Default.toDb - // update on non-existent user - executeSql( - tested.updatePartyRecordIdp(100000, identityProviderId = IdentityProviderId.Default.toDb) - ) shouldBe false - } - - private def newDbPartyRecord( - partyId: String = "", - resourceVersion: Long = 0, - createdAt: Long = zeroMicros, - identityProviderId: Option[IdentityProviderId.Id] = None, - ): PartyRecordStorageBackend.DbPartyRecordPayload = { - val uuid = UUID.randomUUID.toString - val party = if (partyId != "") partyId else s"party_id_$uuid" - PartyRecordStorageBackend.DbPartyRecordPayload( - party = Ref.Party.assertFromString(party), - identityProviderId = identityProviderId, - resourceVersion = resourceVersion, - createdAt = createdAt, - ) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPartyToParticipant.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPartyToParticipant.scala deleted file mode 100644 index 432683b6af..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPartyToParticipant.scala +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.HasExecutionContext -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent.{ - Added, - ChangedTo, - Revoked, -} -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel.{ - Confirmation, - Observation, - Submission, -} -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.RawParticipantAuthorization -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.{ - IdRange, - Ids, -} -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - PaginationFromTo, - PaginationInput, -} -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Time.Timestamp -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, OptionValues} - -private[backend] trait StorageBackendTestsPartyToParticipant - extends Matchers - with Inside - with OptionValues - with StorageBackendSpec - with HasExecutionContext { this: AnyFlatSpec => - - behavior of "StorageBackend (party to participant)" - - import StorageBackendTestValues.* - - val otherParticipantId: ParticipantId = ParticipantId( - Ref.ParticipantId.assertFromString("participant") - ) - - private val singleDto = Vector( - dtoPartyToParticipant(offset(1), 1L) - ) - - private val multipleDtos = Vector( - dtoPartyToParticipant(offset(1), 1L), - dtoPartyToParticipant(offset(2), 2L, someParty2), - dtoPartyToParticipant(offset(3), 3L, someParty, otherParticipantId), - dtoPartyToParticipant(offset(4), 4L, someParty, someParticipantId, Revoked), - ) - - private val authorizationEvents: Vector[AuthorizationEvent] = Vector( - Added(Confirmation): AuthorizationEvent, - Added(Observation), - Added(Submission), - ChangedTo(Confirmation), - ChangedTo(Observation), - ChangedTo(Submission), - Revoked, - ) - - private val authorizationEventDtos: Vector[DbDto.EventPartyToParticipant] = - authorizationEvents.zipWithIndex.map { case (event, i) => - dtoPartyToParticipant( - offset = offset(i.toLong + 1), // cannot be zero - eventSequentialId = i.toLong, - authorizationEvent = event, - ) - } - - def toRaw(dbDto: DbDto.EventPartyToParticipant): RawParticipantAuthorization = - RawParticipantAuthorization( - offset = Offset.tryFromLong(dbDto.event_offset), - updateId = UpdateId.tryFromByteArray(dbDto.update_id).toHexString, - partyId = dbDto.party_id, - participantId = dbDto.participant_id, - authorizationEvent = Conversions - .authorizationEvent(dbDto.participant_authorization_event, dbDto.participant_permission), - recordTime = Timestamp.assertFromLong(dbDto.record_time), - synchronizerId = dbDto.synchronizer_id.toProtoPrimitive, - traceContext = dbDto.trace_context, - ) - - private def sanitize: RawParticipantAuthorization => RawParticipantAuthorization = - _.copy(traceContext = Array.emptyByteArray) - - it should "return correct index for a single party to participant mapping" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(singleDto, _)) - val eventsForAll = executeSql( - backend.event - .fetchTopologyPartyEventIds( - party = None - ) - .fetchPage(_)( - PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 10L, - ), - limit = 10, - ) - ) - ).ids - executeSql( - updateLedgerEnd(offset(1), ledgerEndSequentialId = 1L) - ) - val eventsForSomeParty = executeSql( - backend.event - .fetchTopologyPartyEventIds( - party = Some(someParty) - ) - .fetchPage(_)( - PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 10L, - ), - limit = 10, - ) - ) - ).ids - - eventsForAll should not be empty - eventsForSomeParty should not be empty - } - - it should "return correct indices for multiple party to participant mappings" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(multipleDtos, _)) - val eventsForAll = executeSql( - backend.event - .fetchTopologyPartyEventIds( - party = None - ) - .fetchPage(_)( - PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 10L, - ), - limit = 10, - ) - ) - ).ids - executeSql( - updateLedgerEnd(offset(4), ledgerEndSequentialId = 4L) - ) - val eventsForSomeParty = executeSql( - backend.event - .fetchTopologyPartyEventIds( - party = Some(someParty) - ) - .fetchPage(_)( - PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 10L, - ), - limit = 10, - ) - ) - ).ids - - eventsForAll should contain theSameElementsAs Vector(1L, 2L, 3L, 4L) - eventsForSomeParty should contain theSameElementsAs Vector(1L, 3L, 4L) - } - - it should "respond with payloads for a single party to participant mapping" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(singleDto, _)) - val payloadsForAll = executeSql( - backend.event.topologyPartyEventBatch(Ids(Vector(1L))) - ) - - payloadsForAll should not be empty - payloadsForAll.map(sanitize) should contain theSameElementsAs singleDto.map(toRaw).map(sanitize) - - val payloadsForAllRange = executeSql( - backend.event.topologyPartyEventBatch(IdRange(1L, 1L)) - ) - payloadsForAllRange.map(sanitize) shouldBe payloadsForAll.map(sanitize) - } - - it should "respond with payloads for a multiple party to participant mappings" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(multipleDtos, _)) - val payloadsForAll = executeSql( - backend.event.topologyPartyEventBatch(Ids(Vector(1L, 2L, 3L, 4L))) - ) - - payloadsForAll should not be empty - payloadsForAll - .map(sanitize) should contain theSameElementsAs multipleDtos.map(toRaw).map(sanitize) - - val payloadsForAllRange = executeSql( - backend.event.topologyPartyEventBatch(IdRange(1L, 4L)) - ) - payloadsForAllRange.map(sanitize) shouldBe payloadsForAll.map(sanitize) - } - - it should "handle the different authorization events" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(authorizationEventDtos, _)) - - authorizationEventDtos.foreach { dto => - val payloads = executeSql( - backend.event.topologyPartyEventBatch(Ids(Vector(dto.event_sequential_id))) - ) - - payloads should have size 1 - payloads.headOption.value.authorizationEvent shouldBe - authorizationEvents(dto.event_sequential_id.toInt) - } - } - - behavior of "topologyEventOffsetPublishedOnRecordTime" - - private val synchronizerId1 = SynchronizerId.tryFromString("x::synchronizer1") - private val synchronizerId2 = SynchronizerId.tryFromString("x::synchronizer2") - - it should "be the offset if there is one" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql( - ingest( - Vector( - dtoPartyToParticipant( - offset(1), - 1L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1504), - ), - dtoPartyToParticipant( - offset(2), - 2L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1505), - ), - dtoPartyToParticipant( - offset(3), - 3L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1506), - ), - ), - _, - ) - ) - executeSql( - updateLedgerEnd(offset(3), 3L) - ) - backend.stringInterningSupport.synchronizerId.internalize(synchronizerId1) - backend.stringInterningSupport.synchronizerId.internalize(synchronizerId2) - executeSql( - backend.event - .topologyEventOffsetPublishedOnRecordTime( - synchronizerId1, - CantonTimestamp.ofEpochMicro(1505), - ) - ) shouldBe Some(offset(2)) - } - - it should "be no offset (None) if there is none" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql( - ingest( - Vector( - dtoPartyToParticipant( - offset(1), - 1L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1504), - ), - dtoPartyToParticipant( - offset(3), - 3L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1506), - ), - ), - _, - ) - ) - executeSql( - updateLedgerEnd(offset(3), 3L) - ) - executeSql( - backend.event - .topologyEventOffsetPublishedOnRecordTime( - synchronizerId1, - CantonTimestamp.ofEpochMilli(1505), - ) - ) shouldBe None - } - - it should "be no offset (None) if it is on a different synchronizer" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql( - ingest( - Vector( - dtoPartyToParticipant( - offset(1), - 1L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1504), - ), - dtoPartyToParticipant( - offset(2), - 2L, - synchronizerId = synchronizerId2, - recordTime = Timestamp.assertFromLong(1505), - ), - dtoPartyToParticipant( - offset(3), - 3L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1506), - ), - ), - _, - ) - ) - executeSql( - updateLedgerEnd(offset(3), 3L) - ) - executeSql( - backend.event - .topologyEventOffsetPublishedOnRecordTime( - synchronizerId1, - CantonTimestamp.ofEpochMilli(1505), - ) - ) shouldBe None - } - - it should "be no offset (None) if it is after the ledger end" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql( - ingest( - Vector( - dtoPartyToParticipant( - offset(1), - 1L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1504), - ), - dtoPartyToParticipant( - offset(2), - 2L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1505), - ), - dtoPartyToParticipant( - offset(3), - 3L, - synchronizerId = synchronizerId1, - recordTime = Timestamp.assertFromLong(1506), - ), - ), - _, - ) - ) - executeSql( - updateLedgerEnd(offset(1), 1L) - ) - executeSql( - backend.event - .topologyEventOffsetPublishedOnRecordTime( - synchronizerId1, - CantonTimestamp.ofEpochMilli(1505), - ) - ) shouldBe None - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPruning.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPruning.scala deleted file mode 100644 index 996e040acc..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsPruning.scala +++ /dev/null @@ -1,1065 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import anorm.SqlParser.{long, scalar} -import com.daml.scalautil.Statement -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.AchsAddActivationsParams -import com.digitalasset.canton.platform.store.backend.PruningDto.* -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.common.SimpleSqlExtensions.`SimpleSql ops` -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, Checkpoints, OptionValues} - -import java.sql.Connection - -private[backend] trait StorageBackendTestsPruning - extends Matchers - with OptionValues - with Checkpoints - with StorageBackendSpec { - this: AnyFlatSpec => - - behavior of "StorageBackend (pruning)" - - import StorageBackendTestValues.* - - def executeSqlInTx[T](sql: Connection => T): T = - executeSql { conn => - conn.setAutoCommit(false) - try { - val result = sql(conn) - conn.commit() - result - } catch { - case t: Throwable => - conn.rollback() - throw t - } finally { - conn.setAutoCommit(true) - } - } - - def pruneEventsSql( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusive: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit - traceContext: TraceContext - ): Unit = - executeSqlInTx { conn => - backend.event.pruneEvents( - previousPruneUpToInclusive = previousPruneUpToInclusive, - previousIncompleteReassignmentOffsets = previousIncompleteReassignmentOffsets, - pruneUpToInclusive = pruneUpToInclusive, - incompleteReassignmentOffsets = incompleteReassignmentOffsets, - )( - conn, - traceContext, - ) - } - - def populateAchsFromActivateStakeholder(endInclusive: Long, activeAt: Long): Unit = - executeSql( - backend.event.addActivationsToAchs( - AchsAddActivationsParams( - startExclusive = 0L, - endInclusive = endInclusive, - activeAt = activeAt, - ) - ) - ) - - def contractCandidates: Vector[Long] = - executeSql( - SQL""" - SELECT internal_contract_id - FROM lapi_pruning_contract_candidate - ORDER BY internal_contract_id""" - .asVectorOf(long("internal_contract_id"))(_) - ) - - it should "correctly update the pruning offset" in { - val offset_1 = offset(3) - val offset_2 = offset(2) - val offset_3 = offset(4) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - val initialPruningOffset = executeSql(backend.parameter.prunedUpToInclusive) - - executeSql(backend.parameter.updatePrunedUptoInclusive(offset_1)) - val updatedPruningOffset_1 = executeSql(backend.parameter.prunedUpToInclusive) - - executeSql(backend.parameter.updatePrunedUptoInclusive(offset_2)) - val updatedPruningOffset_2 = executeSql(backend.parameter.prunedUpToInclusive) - - executeSql(backend.parameter.updatePrunedUptoInclusive(offset_3)) - val updatedPruningOffset_3 = executeSql(backend.parameter.prunedUpToInclusive) - - initialPruningOffset shouldBe empty - updatedPruningOffset_1 shouldBe Some(offset_1) - // The pruning offset is not updated if lower than the existing offset - updatedPruningOffset_2 shouldBe Some(offset_1) - updatedPruningOffset_3 shouldBe Some(offset_3) - } - - it should "prune completions" in { - val someParty = Ref.Party.assertFromString("party") - val completion = dtoCompletion( - offset = offset(1), - submitters = Set(someParty), - ) - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - // Ingest a completion - executeSql(ingest(Vector(completion), _)) - assertIndexDbDataSql(completion = Seq(PruningDto.Completion(1))) - // Prune - executeSql(backend.completion.pruneCompletions(offset(1))(_, TraceContext.empty)) - assertIndexDbDataSql(completion = Seq.empty) - } - - it should "prune various witnessed events" in { - val updates = Vector( - // before pruning start - meta(event_offset = 1)( - dtosWitnessedCreate( - event_sequential_id = 100, - internal_contract_id = 100, - )() - ), - meta(event_offset = 2)( - dtosWitnessedExercised( - event_sequential_id = 200, - consuming = true, - internal_contract_id = Some(200), - ) - ), - meta(event_offset = 3)( - dtosWitnessedExercised( - event_sequential_id = 300, - consuming = false, - internal_contract_id = Some(300), - ) - ), - // in pruning range - meta(event_offset = 4)( - dtosWitnessedCreate( - event_sequential_id = 400, - internal_contract_id = 400, - )() - ), - meta(event_offset = 5)( - dtosWitnessedExercised( - event_sequential_id = 500, - consuming = true, - internal_contract_id = Some(400), - ) - ), - meta(event_offset = 6)( - dtosWitnessedExercised( - event_sequential_id = 600, - consuming = false, - internal_contract_id = Some(300), - ) ++ dtosWitnessedExercised( - event_sequential_id = 601, - consuming = false, - internal_contract_id = Some(800), - ) ++ dtosWitnessedExercised( - event_sequential_id = 602, - consuming = false, - internal_contract_id = Some(901), - ) - ), - // after pruning range - meta(event_offset = 7)( - dtosWitnessedCreate( - event_sequential_id = 700 - )() - ), - meta(event_offset = 8)( - dtosWitnessedExercised( - event_sequential_id = 800, - consuming = true, - internal_contract_id = Some(800), - ) - ), - meta(event_offset = 9)( - dtosWitnessedExercised( - event_sequential_id = 900, - consuming = false, - internal_contract_id = Some(900), - ) ++ dtosCreate( - event_sequential_id = 901, - internal_contract_id = 901, - additional_witnesses = Set.empty, - )(stakeholders = Set.empty) - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(9), 900L)) - assertIndexDbDataSql( - activate = List(901), - variousWitnessed = List( - 100, 200, 300, 400, 500, 600, 601, 602, 700, 800, 900, - ), - variousFilterWitness = List( - 100, 100, 200, 200, 300, 300, 400, 400, 500, 500, 600, 600, 601, 601, 602, 602, 700, 700, - 800, 800, 900, 900, - ), - txMeta = List( - TxMeta(1), - TxMeta(2), - TxMeta(3), - TxMeta(4), - TxMeta(5), - TxMeta(6), - TxMeta(7), - TxMeta(8), - TxMeta(9), - ), - ) - contractCandidates shouldBe Vector.empty - // Prune - pruneEventsSql( - previousPruneUpToInclusive = Some(offset(3)), - previousIncompleteReassignmentOffsets = Vector.empty, - pruneUpToInclusive = offset(6), - incompleteReassignmentOffsets = Vector.empty, - )(TraceContext.empty) - - assertIndexDbDataSql( - activate = List(901), - variousWitnessed = List( - 100, 200, 300, 700, 800, 900, - ), - variousFilterWitness = List( - 100, 100, 200, 200, 300, 300, 700, 700, 800, 800, 900, 900, - ), - txMeta = List( - TxMeta(1), - TxMeta(2), - TxMeta(3), - TxMeta(7), - TxMeta(8), - TxMeta(9), - ), - ) - contractCandidates shouldBe Vector(300L, 400L) - } - - it should "prune activate and deactivate events" in { - val updates = Vector( - // before pruning start will be pruned later - meta(event_offset = 1)( - dtosCreate( - event_sequential_id = 100, - internal_contract_id = 10, - )() - ), - meta(event_offset = 2)( - dtosAssign( - event_sequential_id = 200, - internal_contract_id = 20, - )() ++ dtosWitnessedExercised( - event_sequential_id = 201, - consuming = true, - internal_contract_id = Some(60), - additional_witnesses = Set.empty, - ) - ), - // before pruning start won't be pruned later - meta(event_offset = 3)( - dtosCreate( - event_sequential_id = 300, - internal_contract_id = 10, - )() - ), - meta(event_offset = 4)( - dtosAssign( - event_sequential_id = 400, - internal_contract_id = 40, - )() - ), - // in pruning range will be pruned later - meta(event_offset = 5)( - dtosCreate( - event_sequential_id = 500, - internal_contract_id = 50, - )() - ), - meta(event_offset = 6)( - dtosAssign( - event_sequential_id = 600, - internal_contract_id = 60, - )() - ), - // in pruning range will be not pruned - no deactivation - meta(event_offset = 7)( - dtosCreate( - event_sequential_id = 700, - internal_contract_id = 70, - )() - ), - meta(event_offset = 8)( - dtosAssign( - event_sequential_id = 800, - internal_contract_id = 20, - )() - ), - // in pruning range will be not pruned - deactivation outside the pruning range - meta(event_offset = 9)( - dtosCreate( - event_sequential_id = 900, - internal_contract_id = 90, - )() - ), - meta(event_offset = 10)( - dtosAssign( - event_sequential_id = 1000, - internal_contract_id = 100, - )() ++ dtosWitnessedExercised( - event_sequential_id = 1001, - consuming = true, - internal_contract_id = Some(60), - additional_witnesses = Set.empty, - ) - ), - // deactivations in pruning range - meta(event_offset = 11)( - dtosConsumingExercise( - event_sequential_id = 1100, - deactivated_event_sequential_id = Some(100), - internal_contract_id = Some(10), - ) - ), - meta(event_offset = 12)( - dtosUnassign( - event_sequential_id = 1200, - deactivated_event_sequential_id = Some(200), - internal_contract_id = Some(20), - ) - ), - meta(event_offset = 13)( - dtosUnassign( - event_sequential_id = 1300, - deactivated_event_sequential_id = Some(500), - internal_contract_id = Some(50), - ) - ), - meta(event_offset = 14)( - dtosConsumingExercise( - event_sequential_id = 1400, - deactivated_event_sequential_id = Some(600), - internal_contract_id = Some(60), - ) - ), - meta(event_offset = 15)( - dtosConsumingExercise( - event_sequential_id = 1500, - deactivated_event_sequential_id = None, - internal_contract_id = None, - ) - ), - // outside of pruning range some activations deactivated later - meta(event_offset = 16)( - dtosCreate( - event_sequential_id = 1600, - internal_contract_id = 50, - )() ++ dtosWitnessedExercised( - event_sequential_id = 1601, - consuming = true, - internal_contract_id = Some(10), - additional_witnesses = Set.empty, - ) - ), - meta(event_offset = 17)( - dtosAssign( - event_sequential_id = 1700, - internal_contract_id = 170, - )() - ), - // outside of pruning range some activations never deactivated - meta(event_offset = 18)( - dtosCreate( - event_sequential_id = 1800, - internal_contract_id = 180, - )() - ), - meta(event_offset = 19)( - dtosAssign( - event_sequential_id = 1900, - internal_contract_id = 190, - )() - ), - // outside of pruning range some deactivations - meta(event_offset = 20)( - dtosUnassign( - event_sequential_id = 2000, - deactivated_event_sequential_id = Some(1700), - internal_contract_id = Some(170), - ) - ), - meta(event_offset = 21)( - dtosConsumingExercise( - event_sequential_id = 2100, - deactivated_event_sequential_id = Some(1600), - internal_contract_id = Some(160), - ) - ), - meta(event_offset = 22)( - dtosConsumingExercise( - event_sequential_id = 2200, - deactivated_event_sequential_id = None, - internal_contract_id = None, - ) - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(22), 2200L)) - populateAchsFromActivateStakeholder(endInclusive = 1000L, activeAt = 1000L) - assertIndexDbDataSql( - activate = List( - 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1600, 1700, 1800, 1900, - ), - activateFilterStakeholder = List( - 100, 100, 200, 200, 300, 300, 400, 400, 500, 500, 600, 600, 700, 700, 800, 800, 900, 900, - 1000, 1000, 1600, 1600, 1700, 1700, 1800, 1800, 1900, 1900, - ), - activateFilterWitness = List( - 100, 100, 300, 300, 500, 500, 700, 700, 900, 900, 1600, 1600, 1800, 1800, - ), - achsFilterStakeholder = List( - 100, 100, 200, 200, 300, 300, 400, 400, 500, 500, 600, 600, 700, 700, 800, 800, 900, 900, - 1000, 1000, - ), - deactivate = List( - 1100, 1200, 1300, 1400, 1500, 2000, 2100, 2200, - ), - deactivateFilterStakeholder = List( - 1100, 1100, 1200, 1200, 1300, 1300, 1400, 1400, 1500, 1500, 2000, 2000, 2100, 2100, 2200, - 2200, - ), - deactivateFilterWitness = List( - 1100, 1100, 1400, 1400, 1500, 1500, 2100, 2100, 2200, 2200, - ), - variousWitnessed = List(201, 1001, 1601), - txMeta = List( - TxMeta(1), - TxMeta(2), - TxMeta(3), - TxMeta(4), - TxMeta(5), - TxMeta(6), - TxMeta(7), - TxMeta(8), - TxMeta(9), - TxMeta(10), - TxMeta(11), - TxMeta(12), - TxMeta(13), - TxMeta(14), - TxMeta(15), - TxMeta(16), - TxMeta(17), - TxMeta(18), - TxMeta(19), - TxMeta(20), - TxMeta(21), - TxMeta(22), - ), - ) - contractCandidates shouldBe Vector.empty - // Prune - pruneEventsSql( - previousPruneUpToInclusive = Some(offset(4)), - previousIncompleteReassignmentOffsets = Vector.empty, - pruneUpToInclusive = offset(15), - incompleteReassignmentOffsets = Vector.empty, - )(TraceContext.empty) - - assertIndexDbDataSql( - activate = List( - 300, 400, 700, 800, 900, 1000, 1600, 1700, 1800, 1900, - ), - activateFilterStakeholder = List( - 300, 300, 400, 400, 700, 700, 800, 800, 900, 900, 1000, 1000, 1600, 1600, 1700, 1700, 1800, - 1800, 1900, 1900, - ), - activateFilterWitness = List( - 300, 300, 700, 700, 900, 900, 1600, 1600, 1800, 1800, - ), - achsFilterStakeholder = List( - 300, 300, 400, 400, 700, 700, 800, 800, 900, 900, 1000, 1000, - ), - deactivate = List( - 2000, - 2100, - 2200, - ), - deactivateFilterStakeholder = List( - 2000, 2000, 2100, 2100, 2200, 2200, - ), - deactivateFilterWitness = List( - 2100, - 2100, - 2200, - 2200, - ), - variousWitnessed = List(201, 1601), - txMeta = List( - TxMeta(1), - TxMeta(2), - TxMeta(3), - TxMeta(4), - TxMeta(16), - TxMeta(17), - TxMeta(18), - TxMeta(19), - TxMeta(20), - TxMeta(21), - TxMeta(22), - ), - ) - contractCandidates shouldBe Vector(20, 60) - } - - it should "not prune incomplete events and related other events, but prune completed, older incomplete events and related other events" in { - val updates = Vector( - // before pruning start, incomplete assignments, won't be pruned - meta(event_offset = 2)( - dtosAssign(event_sequential_id = 200)() ++ - dtosAssign(event_sequential_id = 201)() ++ - dtosAssign(event_sequential_id = 202)() - ), - // before pruning start, relates to incomplete, so still retained - meta(event_offset = 3)( - dtosConsumingExercise( - event_sequential_id = 300, - deactivated_event_sequential_id = Some(202), - ) - ), - // in pruning range will be pruned later - meta(event_offset = 5)( - dtosCreate(event_sequential_id = 500)() - ), - meta(event_offset = 6)( - dtosCreate(event_sequential_id = 600)() - ), - // deactivations in pruning range - meta(event_offset = 11)( - dtosConsumingExercise( - event_sequential_id = 1100, - deactivated_event_sequential_id = Some(500), - ) ++ - // related to incomplete assignment, won't be pruned - dtosConsumingExercise( - event_sequential_id = 1101, - deactivated_event_sequential_id = Some(201), - ) - ), - // incomplete unassignments, won't be pruned - meta(event_offset = 12)( - // this also relates to an incomplete unassignment - dtosUnassign(event_sequential_id = 1200, deactivated_event_sequential_id = Some(200)) ++ - dtosUnassign(event_sequential_id = 1201, deactivated_event_sequential_id = Some(600)) - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - populateAchsFromActivateStakeholder(endInclusive = 1000L, activeAt = 1000L) - executeSql(updateLedgerEnd(offset(22), 2200L)) - assertIndexDbDataSql( - activate = List( - 200, 201, 202, 500, 600, - ), - activateFilterStakeholder = List( - 200, 200, 201, 201, 202, 202, 500, 500, 600, 600, - ), - activateFilterWitness = List( - 500, - 500, - 600, - 600, - ), - achsFilterStakeholder = List( - 200, 200, 201, 201, 500, 500, 600, 600, - ), - deactivate = List( - 300, 1100, 1101, 1200, 1201, - ), - deactivateFilterStakeholder = List( - 300, 300, 1100, 1100, 1101, 1101, 1200, 1200, 1201, 1201, - ), - deactivateFilterWitness = List( - 300, 300, 1100, 1100, 1101, 1101, - ), - txMeta = List( - TxMeta(2), - TxMeta(3), - TxMeta(5), - TxMeta(6), - TxMeta(11), - TxMeta(12), - ), - ) - // Prune - pruneEventsSql( - previousPruneUpToInclusive = Some(offset(4)), - previousIncompleteReassignmentOffsets = Vector(offset(2)), - pruneUpToInclusive = offset(15), - incompleteReassignmentOffsets = Vector(offset(2), offset(12)), - )(TraceContext.empty) - - assertIndexDbDataSql( - activate = List( - 200, - 201, - 202, - 600, - ), - activateFilterStakeholder = List( - 200, 200, 201, 201, 202, 202, 600, 600, - ), - activateFilterWitness = List( - 600, - 600, - ), - achsFilterStakeholder = List( - 200, 200, 201, 201, 600, 600, - ), - deactivate = List( - 300, - 1101, - 1200, - 1201, - ), - deactivateFilterStakeholder = List( - 300, 300, 1101, 1101, 1200, 1200, 1201, 1201, - ), - deactivateFilterWitness = List( - 300, - 300, - 1101, - 1101, - ), - txMeta = List( - TxMeta(2), - TxMeta(3), - ), - ) - - // Prune again - pruneEventsSql( - previousPruneUpToInclusive = Some(offset(15)), - previousIncompleteReassignmentOffsets = Vector(offset(2), offset(12)), - pruneUpToInclusive = offset(17), - incompleteReassignmentOffsets = Vector(offset(2)), - )(TraceContext.empty) - - assertIndexDbDataSql( - activate = List( - 200, - 201, - 202, - ), - activateFilterStakeholder = List( - 200, 200, 201, 201, 202, 202, - ), - activateFilterWitness = List(), - achsFilterStakeholder = List( - 200, - 200, - 201, - 201, - ), - deactivate = List( - 300, - 1101, - 1200, - ), - deactivateFilterStakeholder = List( - 300, 300, 1101, 1101, 1200, 1200, - ), - deactivateFilterWitness = List( - 300, - 300, - 1101, - 1101, - ), - txMeta = List( - TxMeta(2), - TxMeta(3), - ), - ) - - // Prune again - pruneEventsSql( - previousPruneUpToInclusive = Some(offset(17)), - previousIncompleteReassignmentOffsets = Vector(offset(2)), - pruneUpToInclusive = offset(21), - incompleteReassignmentOffsets = Vector(), - )(TraceContext.empty) - - assertIndexDbDataSql( - activate = List(), - activateFilterStakeholder = List(), - activateFilterWitness = List(), - achsFilterStakeholder = List(), - deactivate = List(), - deactivateFilterStakeholder = List(), - deactivateFilterWitness = List(), - txMeta = List( - TxMeta(2), - TxMeta(3), - ), - ) - } - - it should "not prune incomplete events and related other events, but prune completed, older incomplete events and related other events - combined case having new incomplete and complete as well" in { - val updates = Vector( - // before pruning start, older incomplete assignments, will become completed, and prunable - meta(event_offset = 2)( - dtosAssign(event_sequential_id = 200)() ++ - dtosAssign(event_sequential_id = 201)() ++ - dtosAssign(event_sequential_id = 202)() - ), - // before pruning start, relates to incomplete, so still retained previously, but with that becoming completed, prunable - meta(event_offset = 3)( - dtosConsumingExercise( - event_sequential_id = 300, - deactivated_event_sequential_id = Some(202), - ) - ), - // in pruning range will be pruned later - meta(event_offset = 5)( - dtosCreate(event_sequential_id = 500)() - ), - meta(event_offset = 6)( - dtosCreate(event_sequential_id = 600)() - ), - // deactivations in pruning range - meta(event_offset = 11)( - dtosConsumingExercise( - event_sequential_id = 1100, - deactivated_event_sequential_id = Some(500), - ) ++ - // related to completed assignment, will be pruned - dtosConsumingExercise( - event_sequential_id = 1101, - deactivated_event_sequential_id = Some(201), - ) - ), - // incomplete unassignments, won't be pruned - meta(event_offset = 12)( - // this also relates to a previously incomplete unassignment - dtosUnassign(event_sequential_id = 1200, deactivated_event_sequential_id = Some(200)) ++ - dtosUnassign(event_sequential_id = 1201, deactivated_event_sequential_id = Some(600)) - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(22), 2200L)) - populateAchsFromActivateStakeholder(endInclusive = 1000L, activeAt = 1000L) - assertIndexDbDataSql( - activate = List( - 200, 201, 202, 500, 600, - ), - activateFilterStakeholder = List( - 200, 200, 201, 201, 202, 202, 500, 500, 600, 600, - ), - activateFilterWitness = List( - 500, - 500, - 600, - 600, - ), - achsFilterStakeholder = List( - 200, 200, 201, 201, 500, 500, 600, 600, - ), - deactivate = List( - 300, 1100, 1101, 1200, 1201, - ), - deactivateFilterStakeholder = List( - 300, 300, 1100, 1100, 1101, 1101, 1200, 1200, 1201, 1201, - ), - deactivateFilterWitness = List( - 300, 300, 1100, 1100, 1101, 1101, - ), - txMeta = List( - TxMeta(2), - TxMeta(3), - TxMeta(5), - TxMeta(6), - TxMeta(11), - TxMeta(12), - ), - ) - // Prune - pruneEventsSql( - previousPruneUpToInclusive = Some(offset(4)), - previousIncompleteReassignmentOffsets = Vector(offset(2)), - pruneUpToInclusive = offset(15), - incompleteReassignmentOffsets = Vector(offset(12)), - )(TraceContext.empty) - - assertIndexDbDataSql( - activate = List( - 200, - 600, - ), - activateFilterStakeholder = List( - 200, - 200, - 600, - 600, - ), - activateFilterWitness = List( - 600, - 600, - ), - achsFilterStakeholder = List( - 200, - 200, - 600, - 600, - ), - deactivate = List( - 1200, - 1201, - ), - deactivateFilterStakeholder = List( - 1200, - 1200, - 1201, - 1201, - ), - deactivateFilterWitness = List(), - txMeta = List( - TxMeta(2), - TxMeta(3), - ), - ) - - // Prune again - pruneEventsSql( - previousPruneUpToInclusive = Some(offset(15)), - previousIncompleteReassignmentOffsets = Vector(offset(12)), - pruneUpToInclusive = offset(17), - incompleteReassignmentOffsets = Vector(), - )(TraceContext.empty) - - assertIndexDbDataSql( - activate = List(), - activateFilterStakeholder = List(), - activateFilterWitness = List(), - achsFilterStakeholder = List(), - deactivate = List(), - deactivateFilterStakeholder = List(), - deactivateFilterWitness = List(), - txMeta = List( - TxMeta(2), - TxMeta(3), - ), - ) - } - - behavior of "pruning of contracts" - - private def insertParContract(contractId: String): Long = { - val contractIdBytes = contractId.getBytes - executeSql( - SQL""" - INSERT INTO par_contracts (contract_id, instance, package_id, template_id) - VALUES ($contractIdBytes, $contractIdBytes, 'pid', 'tid')""" - .executeInsert(scalar[Long].single)(_) - ) - executeSql( - SQL""" - SELECT internal_contract_id - FROM par_contracts - WHERE contract_id=$contractIdBytes""" - .asSingle(long("internal_contract_id"))(_) - ) - } - - private def contracts: Vector[Long] = - executeSql( - SQL""" - SELECT internal_contract_id - FROM par_contracts - ORDER BY internal_contract_id""" - .asVectorOf(long("internal_contract_id"))(_) - ) - - private def insertPruningCandidate(internalContractId: Long): Unit = - executeSql( - SQL""" - INSERT INTO lapi_pruning_contract_candidate(internal_contract_id) - VALUES ($internalContractId)""".executeUpdate()(_) - ) shouldBe 1 - - private def pruningFixture(): Vector[Long] = { - val contractIds: Vector[Long] = (1 to 12).map { i => - insertParContract(i.toString) - }.toVector - - 0 to 9 foreach (i => insertPruningCandidate(contractIds(i))) - - val updates = Vector( - // before Ledger End - meta(event_offset = 1)( - dtosWitnessedCreate( - event_sequential_id = 100, - internal_contract_id = contractIds(0), - )() - ), - meta(event_offset = 2)( - dtosWitnessedExercised( - event_sequential_id = 200, - consuming = true, - internal_contract_id = Some(contractIds(1)), - ) - ), - meta(event_offset = 3)( - dtosUnassign( - event_sequential_id = 300, - internal_contract_id = Some(contractIds(2)), - ) - ), - meta(event_offset = 4)( - dtosAssign( - event_sequential_id = 400, - internal_contract_id = contractIds(3), - )() - ), - // after ledger end - meta(event_offset = 5)( - dtosWitnessedCreate( - event_sequential_id = 500, - internal_contract_id = contractIds(4), - )() - ), - meta(event_offset = 6)( - dtosWitnessedExercised( - event_sequential_id = 600, - consuming = true, - internal_contract_id = Some(contractIds(5)), - ) - ), - meta(event_offset = 7)( - dtosUnassign( - event_sequential_id = 700, - internal_contract_id = Some(contractIds(6)), - ) - ), - meta(event_offset = 8)( - dtosAssign( - event_sequential_id = 800, - internal_contract_id = contractIds(7), - )() - ), - ).flatten - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(updates, _)) - executeSql(updateLedgerEnd(offset(4), 400L)) - - contractCandidates shouldBe (0 to 9).map(contractIds).toVector - contracts shouldBe contractIds - contractIds - } - - it should "remove contract candidates correctly" in { - val contractIds = pruningFixture() - executeSqlInTx(backend.event.cleanPruningCandidates()(_, implicitly)) - contractCandidates shouldBe List(2, 6, 8, 9).map(contractIds) - contracts shouldBe contractIds - } - - it should "prune contract correctly after cleaning candidates" in { - val contractIds = pruningFixture() - executeSqlInTx(backend.event.pruneContracts()(_, implicitly)) - contractCandidates shouldBe Vector.empty - contracts shouldBe (0 to 11).filterNot(Set(2, 6, 8, 9)).map(contractIds) - } - - // TODO(i21351) Implement pruning tests for topology events - - /** Asserts the content of the tables subject to pruning. By default, asserts the tables are - * empty. - */ - def assertIndexDbDataSql( - activate: Seq[Long] = Seq.empty, - activateFilterStakeholder: Seq[Long] = Seq.empty, - activateFilterWitness: Seq[Long] = Seq.empty, - achsFilterStakeholder: Seq[Long] = Seq.empty, - deactivate: Seq[Long] = Seq.empty, - deactivateFilterStakeholder: Seq[Long] = Seq.empty, - deactivateFilterWitness: Seq[Long] = Seq.empty, - variousWitnessed: Seq[Long] = Seq.empty, - variousFilterWitness: Seq[Long] = Seq.empty, - txMeta: Seq[TxMeta] = Seq.empty, - completion: Seq[Completion] = Seq.empty, - ): Assertion = executeSql { implicit c => - val queries = backend.pruningDtoQueries - val cp = new Checkpoint - // activate - cp(clue("activate")(Statement.discard(queries.eventActivate shouldBe activate))) - cp( - clue("activate filter stakeholder")( - Statement.discard(queries.filterActivateStakeholder shouldBe activateFilterStakeholder) - ) - ) - cp( - clue("activate filter witness")( - Statement.discard(queries.filterActivateWitness shouldBe activateFilterWitness) - ) - ) - // achs - cp( - clue("achs filter stakeholder")( - Statement.discard(queries.filterAchsStakeholder shouldBe achsFilterStakeholder) - ) - ) - // deactivate - cp(clue("deactivate")(Statement.discard(queries.eventDeactivate shouldBe deactivate))) - cp( - clue("deactivate filter stakeholder")( - Statement.discard(queries.filterDeactivateStakeholder shouldBe deactivateFilterStakeholder) - ) - ) - cp( - clue("deactivate filter witness")( - Statement.discard(queries.filterDeactivateWitness shouldBe deactivateFilterWitness) - ) - ) - // witnessed - cp( - clue("various witnessed")( - Statement.discard(queries.eventVariousWitnessed shouldBe variousWitnessed) - ) - ) - cp( - clue("various witnessed filter")( - Statement.discard(queries.filterVariousWitness shouldBe variousFilterWitness) - ) - ) - // other - cp(clue("meta")(Statement.discard(queries.updateMeta shouldBe txMeta))) - cp(clue("completion")(Statement.discard(queries.completions shouldBe completion))) - cp.reportAll() - succeed - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsQueryValidRange.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsQueryValidRange.scala deleted file mode 100644 index 4cc6378352..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsQueryValidRange.scala +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.logging.entries.LoggingEntries -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.{LoggingContextWithTrace, SuppressionRule} -import com.digitalasset.canton.platform.store.backend.StorageBackendTestValues.{ - offset, - someIdentityParams, -} -import com.digitalasset.canton.platform.store.dao.events.QueryValidRangeImpl -import com.digitalasset.canton.tracing.TraceContext -import io.grpc.StatusRuntimeException -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.slf4j.event.Level - -import scala.concurrent.{ExecutionContext, Future} - -private[backend] trait StorageBackendTestsQueryValidRange extends Matchers with StorageBackendSpec { - this: AnyFlatSpec => - - implicit val loggingContextWithTrace: LoggingContextWithTrace = - new LoggingContextWithTrace(LoggingEntries.empty, TraceContext.empty) - - implicit val ec: ExecutionContext = directExecutionContext - - behavior of "QueryValidRange.withRangeNotPruned" - - it should "allow valid range if no pruning and before ledger end" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(None)) - executeSql(updateLedgerEnd(offset(10), 10L)) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withRangeNotPruned( - minOffsetInclusive = offset(3), - maxOffsetInclusive = offset(8), - errorPruning = _ => "", - errorLedgerEnd = _ => "", - )(Future.unit) - .futureValue - } - - it should "allow valid range if no pruning and before ledger end and start from ledger begin" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(None)) - executeSql(updateLedgerEnd(offset(10), 10L)) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withRangeNotPruned( - minOffsetInclusive = Offset.firstOffset, - maxOffsetInclusive = offset(8), - errorPruning = _ => "", - errorLedgerEnd = _ => "", - )(Future.unit) - .futureValue - } - - it should "allow valid range after pruning and before ledger end" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withRangeNotPruned( - minOffsetInclusive = offset(6), - maxOffsetInclusive = offset(8), - errorPruning = _ => "", - errorLedgerEnd = _ => "", - )(Future.unit) - .futureValue - } - - it should "allow valid range boundary case" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withRangeNotPruned( - minOffsetInclusive = offset(4), - maxOffsetInclusive = offset(10), - errorPruning = _ => "", - errorLedgerEnd = _ => "", - )(Future.unit) - .futureValue - } - - it should "deny in-valid range: earlier than pruning" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - loggerFactory - .assertThrowsAndLogsSuppressingAsync[StatusRuntimeException]( - SuppressionRule.Level(Level.INFO) - )( - within = QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withRangeNotPruned( - minOffsetInclusive = offset(3), - maxOffsetInclusive = offset(10), - errorPruning = pruningOffset => s"pruning issue: ${pruningOffset.unwrap}", - errorLedgerEnd = _ => "", - )(Future.unit), - assertions = _.infoMessage should include( - "PARTICIPANT_PRUNED_DATA_ACCESSED(9,0): pruning issue: 3" - ), - ) - .futureValue - } - - it should "deny in-valid range: later than ledger end when ledger is not empty" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - loggerFactory - .assertThrowsAndLogsSuppressingAsync[StatusRuntimeException]( - SuppressionRule.Level(Level.INFO) - )( - within = QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withRangeNotPruned( - minOffsetInclusive = offset(4), - maxOffsetInclusive = offset(11), - errorPruning = _ => "", - errorLedgerEnd = - ledgerEndOffset => s"ledger-end issue: ${ledgerEndOffset.fold(0L)(_.unwrap)}", - )(Future.unit), - assertions = _.infoMessage should include( - "PARTICIPANT_DATA_ACCESSED_AFTER_LEDGER_END(9,0): ledger-end issue: 10" - ), - ) - } - - it should "deny in-valid range: later than ledger end when ledger end is none" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - loggerFactory - .assertThrowsAndLogsSuppressingAsync[StatusRuntimeException]( - SuppressionRule.Level(Level.INFO) - )( - within = QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withRangeNotPruned( - minOffsetInclusive = offset(1), - maxOffsetInclusive = offset(1), - errorPruning = _ => "", - errorLedgerEnd = - ledgerEndOffset => s"ledger-end issue: ${ledgerEndOffset.fold(0L)(_.unwrap)}", - )(Future.unit), - assertions = _.infoMessage should include( - "PARTICIPANT_DATA_ACCESSED_AFTER_LEDGER_END(9,0): ledger-end issue: 0" - ), - ) - .futureValue - } - - behavior of "QueryValidRange.withOffsetNotBeforePruning" - - it should "allow offset in the valid range" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withOffsetNotBeforePruning( - offset = offset(5), - errorPruning = _ => "", - errorLedgerEnd = _ => "", - )(Future.unit) - .futureValue - } - - it should "allow offset in the valid range if no pruning before" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(None)) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withOffsetNotBeforePruning( - offset = offset(5), - errorPruning = _ => "", - errorLedgerEnd = _ => "", - )(Future.unit) - .futureValue - } - - it should "allow offset in the valid range lower boundary" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withOffsetNotBeforePruning( - offset = offset(3), - errorPruning = _ => "", - errorLedgerEnd = _ => "", - )(Future.unit) - .futureValue - } - - it should "allow offset in the valid range higher boundary" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withOffsetNotBeforePruning( - offset = offset(10), - errorPruning = _ => "", - errorLedgerEnd = _ => "", - )(Future.unit) - .futureValue - } - - it should "deny in-valid range: earlier than pruning" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - loggerFactory - .assertThrowsAndLogsSuppressingAsync[StatusRuntimeException]( - SuppressionRule.Level(Level.INFO) - )( - within = QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withOffsetNotBeforePruning( - offset = offset(2), - errorPruning = pruningOffset => s"pruning issue: ${pruningOffset.unwrap}", - errorLedgerEnd = _ => "", - )(Future.unit), - assertions = _.infoMessage should include( - "PARTICIPANT_PRUNED_DATA_ACCESSED(9,0): pruning issue: 3" - ), - ) - .futureValue - } - - it should "deny in-valid range: later than ledger end" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - loggerFactory - .assertThrowsAndLogsSuppressingAsync[StatusRuntimeException]( - SuppressionRule.Level(Level.INFO) - )( - within = QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).withOffsetNotBeforePruning( - offset = offset(11), - errorPruning = _ => "", - errorLedgerEnd = - ledgerEndOffset => s"ledger-end issue: ${ledgerEndOffset.fold(0L)(_.unwrap)}", - )(Future.unit), - assertions = _.infoMessage should include( - "PARTICIPANT_DATA_ACCESSED_AFTER_LEDGER_END(9,0): ledger-end issue: 10" - ), - ) - .futureValue - } - - behavior of "QueryValidRange.filterPrunedEvents" - - it should "return all events if no pruning" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - val events = (1L to 5L).map(offset) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(None)) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).filterPrunedEvents[Offset](identity)(events.toVector).futureValue shouldBe events - } - - it should "filter out events at or below the pruning offset" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - val events = (1L to 5L).map(offset) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).filterPrunedEvents[Offset](identity)(events.toVector).futureValue shouldBe (4L to 5L).map( - offset - ) - } - - it should "return empty if all events are pruned" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - val events = (1L to 3L).map(offset) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).filterPrunedEvents[Offset](identity)(events.toVector).futureValue shouldBe empty - } - - it should "return all events if pruning offset is before all events" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(10), 10L)) - val events = (5L to 7L).map(offset) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(Some(offset(3)))) - QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).filterPrunedEvents[Offset](identity)(events.toVector).futureValue shouldBe events - } - - it should "fail if any event offset is beyond ledger end" in { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(offset(2), 2L)) - val events = (1L to 5L).map(offset) - when(backend.pruningOffsetService.pruningOffset(any[TraceContext])) - .thenReturn(Future.successful(None)) - loggerFactory - .assertThrowsAndLogsSuppressingAsync[StatusRuntimeException]( - SuppressionRule.Level(Level.INFO) - )( - within = QueryValidRangeImpl( - ledgerEndCache = backend.ledgerEndCache, - pruningOffsetService = backend.pruningOffsetService, - loggerFactory = this.loggerFactory, - ).filterPrunedEvents[Offset](identity)(events.toVector), - assertions = _.infoMessage should include( - "PARTICIPANT_DATA_ACCESSED_AFTER_LEDGER_END(9,0): Offset of event to be filtered Offset(3) is beyond ledger end" - ), - ) - .futureValue - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsReset.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsReset.scala deleted file mode 100644 index 81ed603fce..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsReset.scala +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.IdRange -import com.digitalasset.canton.platform.store.backend.common.EventPayloadSourceForUpdatesLedgerEffects -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - PaginationFromTo, - PaginationInput, -} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -private[backend] trait StorageBackendTestsReset extends Matchers with StorageBackendSpec { - this: AnyFlatSpec => - - behavior of "StorageBackend (reset)" - - import StorageBackendTestValues.* - - it should "start with an empty index" in { - val identity = executeSql(backend.parameter.ledgerIdentity) - val end = executeSql(backend.parameter.ledgerEnd) - val parties = executeSql(backend.party.knownParties(None, None, 10)) - val stringInterningEntries = executeSql( - backend.stringInterning.loadStringInterningEntries(0, 1000) - ) - - identity shouldBe None - end shouldBe ParameterStorageBackend.LedgerEnd.beforeBegin - parties shouldBe empty - stringInterningEntries shouldBe empty - } - - it should "not see any data after advancing the ledger end" in { - advanceLedgerEndToMakeOldDataVisible() - val parties = executeSql(backend.party.knownParties(None, None, 10)) - - parties shouldBe empty - } - - it should "reset everything when using resetAll" in { - val dtos: Vector[DbDto] = Vector( - // 1: party allocation - Seq(dtoPartyEntry(offset(1))), - // 2: transaction with create node - dtosCreate( - event_offset = 2L, - event_sequential_id = 1L, - notPersistedContractId = hashCid("#3"), - )(), - Seq(dtoCompletion(offset(2))), - // 3: transaction with exercise node and retroactive divulgence - dtosConsumingExercise( - event_offset = 3L, - event_sequential_id = 2L, - ), - Seq(dtoCompletion(offset(3))), - // 4: assign event - dtosAssign( - event_offset = 4L, - event_sequential_id = 3L, - notPersistedContractId = hashCid("#4"), - )(), - // 5: unassign event - dtosUnassign( - event_offset = 5L, - event_sequential_id = 4L, - ), - // 6: topology transaction - Seq(dtoPartyToParticipant(offset = offset(6), eventSequentialId = 5L)), - // 7: witnessed create - dtosWitnessedCreate(event_offset = 7L, event_sequential_id = 6L)(), - // 8: witnessed consuming exercise - dtosWitnessedExercised(event_offset = 8L, event_sequential_id = 7L), - // 9: witnessed non-consuming exercise - dtosWitnessedExercised(event_offset = 9L, event_sequential_id = 8L, consuming = false), - // String interning - Seq(DbDto.StringInterningDto(internalId = 10, externalString = "d|x:abc")), - ).flatten - - // Initialize and insert some data - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(ingest(dtos, _)) - executeSql(updateLedgerEnd(ledgerEnd(10, 10L))) - - // queries - def identity = executeSql(backend.parameter.ledgerIdentity) - - def end = executeSql(backend.parameter.ledgerEnd) - - def events = - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Activate - )( - eventSequentialIds = IdRange(1L, 10L), - requestingPartiesForTx = Some(Set.empty), - requestingPartiesForReassignment = Some(Set.empty), - ) - ) ++ - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Deactivate - )( - eventSequentialIds = IdRange(1L, 10L), - requestingPartiesForTx = Some(Set.empty), - requestingPartiesForReassignment = Some(Set.empty), - ) - ) ++ - executeSql( - backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.VariousWitnessed - )( - eventSequentialIds = IdRange(1L, 10L), - requestingPartiesForTx = Some(Set.empty), - requestingPartiesForReassignment = Some(Set.empty), - ) - ) - - def parties = executeSql(backend.party.knownParties(None, None, 10)) - - def stringInterningEntries = executeSql( - backend.stringInterning.loadStringInterningEntries(0, 1000) - ) - - val paginationInput = PaginationInput( - PaginationFromTo.ascending( - startExclusive = 0L, - endInclusive = 1000L, - ), - limit = 1000, - ) - - def activateStakeholderIds = executeSql( - backend.event.updateStreamingQueries - .activateStakeholderIds( - witnessO = None, - templateIdO = None, - ) - .fetchPage(_)(paginationInput) - .ids - ) - - def activateWitnessesIds = executeSql( - backend.event.updateStreamingQueries - .activateWitnessesIds( - witnessO = None, - templateIdO = None, - ) - .fetchPage(_)(paginationInput) - .ids - ) - - def deactivateStakeholderIds = executeSql( - backend.event.updateStreamingQueries - .deactivateStakeholderIds( - witnessO = None, - templateIdO = None, - ) - .fetchPage(_)(paginationInput) - .ids - ) - - def deactivateWitnessesIds = executeSql( - backend.event.updateStreamingQueries - .deactivateWitnessesIds( - witnessO = None, - templateIdO = None, - ) - .fetchPage(_)(paginationInput) - .ids - ) - - def variousWitnessesIds = executeSql( - backend.event.updateStreamingQueries - .variousWitnessIds( - witnessO = None, - templateIdO = None, - ) - .fetchPage(_)(paginationInput) - .ids - ) - - // verify queries indeed return something - identity should not be None - end should not be ParameterStorageBackend.LedgerEnd.beforeBegin - events.size shouldBe 7 - parties should not be empty - stringInterningEntries should not be empty - activateStakeholderIds should not be empty - activateWitnessesIds should not be empty - deactivateStakeholderIds should not be empty - deactivateWitnessesIds should not be empty - variousWitnessesIds should not be empty - - // Reset - executeSql(backend.reset.resetAll) - - // Check the contents (queries that do not depend on ledger end) - identity shouldBe None - end shouldBe ParameterStorageBackend.LedgerEnd.beforeBegin - events shouldBe empty - - // Check the contents (queries that don't read beyond ledger end) - advanceLedgerEndToMakeOldDataVisible() - - parties shouldBe empty - stringInterningEntries shouldBe empty - activateStakeholderIds shouldBe empty - activateWitnessesIds shouldBe empty - deactivateStakeholderIds shouldBe empty - deactivateWitnessesIds shouldBe empty - variousWitnessesIds shouldBe empty - } - - // Some queries are protected to never return data beyond the current ledger end. - // By advancing the ledger end to a large value, we can check whether these - // queries now find any left-over data not cleaned by reset. - private def advanceLedgerEndToMakeOldDataVisible(): Unit = { - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - executeSql(updateLedgerEnd(ledgerEnd(10000, 10000))) - () - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsStringInterning.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsStringInterning.scala deleted file mode 100644 index 7bb420670f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsStringInterning.scala +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import org.scalatest.Inside -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -private[backend] trait StorageBackendTestsStringInterning - extends Matchers - with Inside - with StorageBackendSpec { - this: AnyFlatSpec => - - behavior of "StorageBackend (StringInterning)" - - it should "store and load string-interning entries" in { - val dtos = Vector( - DbDto.StringInterningDto(2, "a"), - DbDto.StringInterningDto(3, "b"), - DbDto.StringInterningDto(4, "c"), - DbDto.StringInterningDto(5, "d"), - ) - - val interningIdsBeforeBegin = executeSql( - backend.stringInterning.loadStringInterningEntries(0, 5) - ) - executeSql(ingest(dtos, _)) - val interningIdsFull = executeSql(backend.stringInterning.loadStringInterningEntries(0, 5)) - val interningIdsOverFetch = executeSql( - backend.stringInterning.loadStringInterningEntries(0, 10) - ) - val interningIdsEmpty = executeSql( - backend.stringInterning.loadStringInterningEntries(5, 10) - ) - val interningIdsSubset = executeSql( - backend.stringInterning.loadStringInterningEntries(3, 10) - ) - - val expectedFullList = List( - 2 -> "a", - 3 -> "b", - 4 -> "c", - 5 -> "d", - ) - interningIdsBeforeBegin shouldBe Nil - interningIdsFull shouldBe expectedFullList - interningIdsOverFetch shouldBe expectedFullList - interningIdsEmpty shouldBe Nil - interningIdsSubset shouldBe expectedFullList.drop(2) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsTimestamps.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsTimestamps.scala deleted file mode 100644 index 620270b82c..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsTimestamps.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.digitalasset.canton.platform.store.backend.EventStorageBackend.SequentialIdBatch.IdRange -import com.digitalasset.canton.platform.store.backend.common.EventPayloadSourceForUpdatesLedgerEffects -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.sql.Connection -import java.time.Instant -import java.util.TimeZone - -private[backend] trait StorageBackendTestsTimestamps extends Matchers with StorageBackendSpec { - this: AnyFlatSpec => - - behavior of "StorageBackend (timestamps)" - - import StorageBackendTestValues.* - - it should "correctly read ledger effective time using rawEvents" in { - val let = timestampFromInstant(Instant.now) - val consuming = dtosConsumingExercise( - event_offset = 1L, - event_sequential_id = 1L, - ledger_effective_time = let.micros, - ) - - executeSql(backend.parameter.initializeParameters(someIdentityParams, loggerFactory)) - - executeSql(ingest(consuming.toVector, _)) - executeSql(updateLedgerEnd(offset(1), 1L)) - - val events = backend.event.fetchEventPayloadsLedgerEffects( - EventPayloadSourceForUpdatesLedgerEffects.Deactivate - )( - eventSequentialIds = IdRange(1L, 10L), - requestingPartiesForTx = Some(Set.empty), - requestingPartiesForReassignment = Some(Set.empty), - ) - val events1 = executeSql(events) - val events2 = executeSql(withDefaultTimeZone("GMT-1")(events)) - val events3 = executeSql(withDefaultTimeZone("GMT+1")(events)) - - withClue("UTC")( - events1 - .collect { case ex: EventStorageBackend.RawExercisedEvent => ex } - .head - .ledgerEffectiveTime shouldBe let - ) - withClue("GMT-1")( - events2 - .collect { case ex: EventStorageBackend.RawExercisedEvent => ex } - .head - .ledgerEffectiveTime shouldBe let - ) - withClue("GMT+1")( - events3 - .collect { case ex: EventStorageBackend.RawExercisedEvent => ex } - .head - .ledgerEffectiveTime shouldBe let - ) - } - - // Some JDBC operations depend on the JVM default time zone. - // In particular, TIMESTAMP WITHOUT TIME ZONE columns are interpreted in the local time zone of the client. - private def withDefaultTimeZone[T](tz: String)(f: Connection => T)(connection: Connection): T = { - val previousDefaultTimeZone = TimeZone.getDefault - TimeZone.setDefault(TimeZone.getTimeZone(tz)) - try { - f(connection) - } finally { - TimeZone.setDefault(previousDefaultTimeZone) - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsUserManagement.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsUserManagement.scala deleted file mode 100644 index c6d2aaad90..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/StorageBackendTestsUserManagement.scala +++ /dev/null @@ -1,520 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.jwt.JwksUrl -import com.digitalasset.canton.ledger.api.UserRight.{CanActAs, CanReadAs, ParticipantAdmin} -import com.digitalasset.canton.ledger.api.{IdentityProviderConfig, IdentityProviderId, UserRight} -import com.digitalasset.canton.platform.store.backend.localstore.UserManagementStorageBackend -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, OptionValues} - -import java.sql.SQLException -import java.util.UUID - -private[backend] trait StorageBackendTestsUserManagement - extends Matchers - with Inside - with StorageBackendSpec - with OptionValues - with ParticipantResourceMetadataTests { - this: AnyFlatSpec => - - behavior of "StorageBackend (user management)" - - // Representative values for each kind of user right - private val right1 = ParticipantAdmin - private val right2 = CanActAs(Ref.Party.assertFromString("party_act_as_1")) - private val right3 = CanReadAs(Ref.Party.assertFromString("party_read_as_1")) - private val zeroMicros: Long = 0 - private val idpId = IdentityProviderId.Id(Ref.LedgerString.assertFromString("idp1")) - private val idpConfig = IdentityProviderConfig( - identityProviderId = idpId, - isDeactivated = false, - jwksUrl = JwksUrl("http//identityprovider.org/"), - issuer = "issuer", - audience = Some("audience"), - ) - - private def tested = backend.userManagement - - override def newResource(): TestedResource = new TestedResource { - private val user = newDbUser() - - override def createResourceAndReturnInternalId(): Int = { - val internalId = executeSql(tested.createUser(user)) - internalId - } - - override def fetchResourceVersion(): Long = - executeSql(tested.getUser(user.id)).value.payload.resourceVersion - } - - override def resourceVersionTableName: String = "lapi_users" - - override def resourceAnnotationsTableName: String = "lapi_user_annotations" - - it should "update existing user's primaryParty attribute" in { - val user = newDbUser(createdAt = 123, primaryPartyOverride = Some(None)) - val internalId = executeSql(tested.createUser(user)) - val party1 = newParty - // Change None -> party1 - executeSql( - tested.updateUserPrimaryParty(internalId, primaryPartyO = Some(party1)) - ) shouldBe true - executeSql((tested.getUser(user.id))).value.payload.primaryPartyO shouldBe Some(party1) - // Change party1 -> None - executeSql(tested.updateUserPrimaryParty(internalId, primaryPartyO = None)) shouldBe true - executeSql((tested.getUser(user.id))).value.payload.primaryPartyO shouldBe None - // Repeated change party1 -> None - executeSql(tested.updateUserPrimaryParty(internalId, primaryPartyO = None)) shouldBe true - executeSql((tested.getUser(user.id))).value.payload.primaryPartyO shouldBe None - } - - it should "update existing user's isDeactivated attribute" in { - val user = newDbUser(createdAt = 123, primaryPartyOverride = Some(None), isDeactivated = false) - val internalId = executeSql(tested.createUser(user)) - // Deactivate - executeSql(tested.updateUserIsDeactivated(internalId, isDeactivated = true)) shouldBe true - executeSql((tested.getUser(user.id))).value.payload.isDeactivated shouldBe true - // Activate - executeSql(tested.updateUserIsDeactivated(internalId, isDeactivated = false)) shouldBe true - executeSql((tested.getUser(user.id))).value.payload.isDeactivated shouldBe false - // Deactivate again - executeSql(tested.updateUserIsDeactivated(internalId, isDeactivated = true)) shouldBe true - executeSql((tested.getUser(user.id))).value.payload.isDeactivated shouldBe true - } - - it should "update user's identityProviderId" in { - executeSql( - backend.identityProviderStorageBackend.createIdentityProviderConfig( - idpConfig - ) - ) - // create with the default idp - val user = newDbUser( - createdAt = 123, - userId = "userId1", - identityProviderId = IdentityProviderId.Default, - ) - val internalId = executeSql(tested.createUser(user)) - executeSql( - (tested.getUser(user.id)) - ).value.payload.identityProviderId shouldBe IdentityProviderId.Default.toDb - // update to idp1 - executeSql(tested.updateUserIdp(internalId, identityProviderId = idpId.toDb)) shouldBe true - executeSql((tested.getUser(user.id))).value.payload.identityProviderId shouldBe idpId.toDb - // update to idp1 again - executeSql(tested.updateUserIdp(internalId, identityProviderId = idpId.toDb)) shouldBe true - executeSql((tested.getUser(user.id))).value.payload.identityProviderId shouldBe idpId.toDb - // update to the default idp - executeSql( - tested.updateUserIdp(internalId, identityProviderId = IdentityProviderId.Default.toDb) - ) shouldBe true - executeSql( - (tested.getUser(user.id)) - ).value.payload.identityProviderId shouldBe IdentityProviderId.Default.toDb - // update on non-existent user - executeSql( - tested.updateUserIdp(100000, identityProviderId = IdentityProviderId.Default.toDb) - ) shouldBe false - } - - it should "handle created_at and granted_at attributes correctly" in { - val user = newDbUser(createdAt = 123) - val internalId = executeSql(tested.createUser(user)) - executeSql(tested.addUserRight(internalId, right1, grantedAt = 234)) - executeSql(tested.getUser(user.id)).map(_.payload.createdAt) shouldBe Some(123) - executeSql(tested.getUserRights(internalId)).headOption.map(_.grantedAt) shouldBe Some(234) - } - - it should "count number of user rights per user" in { - val userA = newDbUser() - val userB = newDbUser() - val idA: Int = executeSql(tested.createUser(userA)) - val idB: Int = executeSql(tested.createUser(userB)) - - def countA: Int = executeSql(tested.countUserRights(idA)) - - def countB: Int = executeSql(tested.countUserRights(idB)) - - val _ = executeSql(tested.addUserRight(idB, UserRight.ParticipantAdmin, grantedAt = zeroMicros)) - val _ = - executeSql( - tested.addUserRight( - idB, - UserRight.CanActAs(Ref.Party.assertFromString("act1")), - grantedAt = zeroMicros, - ) - ) - val _ = - executeSql( - tested.addUserRight( - idB, - UserRight.CanReadAs(Ref.Party.assertFromString("read1")), - grantedAt = zeroMicros, - ) - ) - countA shouldBe zeroMicros - countB shouldBe 3 - val _ = executeSql(tested.addUserRight(idA, UserRight.ParticipantAdmin, grantedAt = zeroMicros)) - countA shouldBe 1 - countB shouldBe 3 - val _ = - executeSql( - tested.addUserRight( - idA, - UserRight.CanActAs(Ref.Party.assertFromString("act1")), - grantedAt = zeroMicros, - ) - ) - val _ = - executeSql( - tested.addUserRight( - idA, - UserRight.CanActAs(Ref.Party.assertFromString("act2")), - grantedAt = zeroMicros, - ) - ) - val _ = - executeSql( - tested.addUserRight( - idA, - UserRight.CanReadAs(Ref.Party.assertFromString("read1")), - grantedAt = zeroMicros, - ) - ) - val _ = - executeSql( - tested.addUserRight( - idA, - UserRight.CanReadAs(Ref.Party.assertFromString("read2")), - grantedAt = zeroMicros, - ) - ) - countA shouldBe 5 - countB shouldBe 3 - val _ = executeSql( - tested.deleteUserRight(idA, UserRight.CanActAs(Ref.Party.assertFromString("act2"))) - ) - countA shouldBe 4 - countB shouldBe 3 - } - - it should "use invalid party string to mark absence of party" in { - intercept[IllegalArgumentException]( - Ref.Party.assertFromString("!") - ).getMessage shouldBe "non expected character 0x21 in Daml-LF Party \"!\"" - } - - it should "create user (createUser)" in { - val user1 = newDbUser() - val user2 = newDbUser() - val internalId1 = executeSql(tested.createUser(user1)) - // Attempting to add a duplicate user - assertThrows[SQLException](executeSql(tested.createUser(user1))) - val internalId2 = executeSql(tested.createUser(user2)) - val _ = - executeSql(tested.createUser(newDbUser(primaryPartyOverride = Some(None)))) - internalId1 should not equal internalId2 - } - - it should "handle user ops (getUser, deleteUser)" in { - val user1 = newDbUser() - val user2 = newDbUser() - val _ = executeSql(tested.createUser(user1)) - val getExisting = executeSql(tested.getUser(user1.id)) - val deleteExisting = executeSql(tested.deleteUser(user1.id)) - val deleteNonexistent = executeSql(tested.deleteUser(user2.id)) - val getDeleted = executeSql(tested.getUser(user1.id)) - val getNonexistent = executeSql(tested.getUser(user2.id)) - getExisting.value.payload shouldBe user1 - deleteExisting shouldBe true - deleteNonexistent shouldBe false - getDeleted shouldBe None - getNonexistent shouldBe None - } - - it should "get all users (getUsers) ordered by id" in { - executeSql( - backend.identityProviderStorageBackend.createIdentityProviderConfig( - idpConfig - ) - ) - val user1 = newDbUser(userId = "user_id_1", identityProviderId = idpId) - val user2 = newDbUser(userId = "user_id_2") - val user3 = newDbUser(userId = "user_id_3") - executeSql( - tested.getUsersOrderedById( - fromExcl = None, - maxResults = 10, - identityProviderId = IdentityProviderId.Default, - ) - ) shouldBe empty - val _ = executeSql(tested.createUser(user3)) - val _ = executeSql(tested.createUser(user1)) - executeSql( - tested.getUsersOrderedById( - fromExcl = None, - maxResults = 10, - identityProviderId = IdentityProviderId.Default, - ) - ) - .map(_.payload) shouldBe Seq( - user3 - ) - executeSql( - tested.getUsersOrderedById( - fromExcl = None, - maxResults = 10, - identityProviderId = idpId, - ) - ) - .map(_.payload) shouldBe Seq( - user1 - ) - val _ = executeSql(tested.createUser(user2)) - executeSql( - tested.getUsersOrderedById( - fromExcl = None, - maxResults = 10, - identityProviderId = IdentityProviderId.Default, - ) - ) - .map(_.payload) shouldBe Seq( - user2, - user3, - ) - } - - it should "get all users (getUsers) ordered by id using binary collation" in { - val user1 = newDbUser(userId = "a") - val user2 = newDbUser(userId = "a!") - val user3 = newDbUser(userId = "b") - val user4 = newDbUser(userId = "a_") - val user5 = newDbUser(userId = "!a") - val user6 = newDbUser(userId = "_a") - val users = Seq(user1, user2, user3, user4, user5, user6) - users.foreach(user => executeSql(tested.createUser(user))) - executeSql( - tested.getUsersOrderedById( - fromExcl = None, - maxResults = 10, - identityProviderId = IdentityProviderId.Default, - ) - ) - .map(_.payload.id) shouldBe Seq("!a", "_a", "a", "a!", "a_", "b") - } - - it should "get a page of users (getUsers) ordered by id" in { - val user1 = newDbUser(userId = "user_id_1") - val user2 = newDbUser(userId = "user_id_2") - val user3 = newDbUser(userId = "user_id_3") - // Note: user4 doesn't exist and won't be created - val user5 = newDbUser(userId = "user_id_5") - val user6 = newDbUser(userId = "user_id_6") - val user7 = newDbUser(userId = "user_id_7") - executeSql( - tested.getUsersOrderedById( - fromExcl = None, - maxResults = 10, - identityProviderId = IdentityProviderId.Default, - ) - ) shouldBe empty - // Creating users in a random order - val _ = executeSql(tested.createUser(user5)) - val _ = executeSql(tested.createUser(user1)) - val _ = executeSql(tested.createUser(user7)) - val _ = executeSql(tested.createUser(user3)) - val _ = executeSql(tested.createUser(user6)) - val _ = executeSql(tested.createUser(user2)) - // Get first 2 elements - executeSql( - tested.getUsersOrderedById( - fromExcl = None, - maxResults = 2, - identityProviderId = IdentityProviderId.Default, - ) - ) - .map(_.payload) shouldBe Seq( - user1, - user2, - ) - // Get 3 users after user1 - executeSql( - tested.getUsersOrderedById( - maxResults = 3, - fromExcl = Some(user1.id), - identityProviderId = IdentityProviderId.Default, - ) - ) - .map(_.payload) shouldBe Seq( - user2, - user3, - user5, - ) - // Get up to 10000 users after user1 - executeSql( - tested.getUsersOrderedById( - maxResults = 10000, - fromExcl = Some(user1.id), - identityProviderId = IdentityProviderId.Default, - ) - ) map (_.payload) shouldBe Seq( - user2, - user3, - user5, - user6, - user7, - ) - // Get some users after a non-existing user id - executeSql( - tested.getUsersOrderedById( - maxResults = 2, - fromExcl = Some(Ref.UserId.assertFromString("user_id_4")), - identityProviderId = IdentityProviderId.Default, - ) - ).map(_.payload) shouldBe Seq(user5, user6) - // Get no users when requesting with after set the last existing user - executeSql( - tested.getUsersOrderedById( - maxResults = 2, - fromExcl = Some(user7.id), - identityProviderId = IdentityProviderId.Default, - ) - ) shouldBe empty - // Get no users when requesting with after set beyond the last existing user - executeSql( - tested.getUsersOrderedById( - maxResults = 2, - fromExcl = Some(Ref.UserId.assertFromString("user_id_8")), - identityProviderId = IdentityProviderId.Default, - ) - ) shouldBe empty - } - - it should "handle adding rights to non-existent user" in { - val nonExistentUserInternalId = 123 - val allUsers = executeSql( - tested.getUsersOrderedById( - maxResults = 10, - fromExcl = None, - identityProviderId = IdentityProviderId.Default, - ) - ) - val rightExists = executeSql(tested.userRightExists(nonExistentUserInternalId, right2)) - allUsers shouldBe empty - rightExists shouldBe false - } - - it should "handle adding duplicate rights" in { - val user1 = newDbUser() - val adminRight = ParticipantAdmin - val readAsRight = CanReadAs(Ref.Party.assertFromString("party_read_as_1")) - val actAsRight = CanActAs(Ref.Party.assertFromString("party_act_as_1")) - val internalId = executeSql(tested.createUser(user = user1)) - executeSql(tested.addUserRight(internalId, adminRight, grantedAt = zeroMicros)) - // Attempting to add a duplicate user admin right - assertThrows[SQLException]( - executeSql(tested.addUserRight(internalId, adminRight, grantedAt = zeroMicros)) - ) - executeSql(tested.addUserRight(internalId, readAsRight, grantedAt = zeroMicros)) - // Attempting to add a duplicate user readAs right - assertThrows[SQLException]( - executeSql(tested.addUserRight(internalId, readAsRight, grantedAt = zeroMicros)) - ) - executeSql(tested.addUserRight(internalId, actAsRight, grantedAt = zeroMicros)) - // Attempting to add a duplicate user actAs right - assertThrows[SQLException]( - executeSql(tested.addUserRight(internalId, actAsRight, grantedAt = zeroMicros)) - ) - } - - it should "handle removing absent rights" in { - val user1 = newDbUser() - val internalId = executeSql(tested.createUser(user1)) - val delete1 = executeSql(tested.deleteUserRight(internalId, right1)) - val delete2 = executeSql(tested.deleteUserRight(internalId, right2)) - val delete3 = executeSql(tested.deleteUserRight(internalId, right3)) - delete1 shouldBe false - delete2 shouldBe false - delete3 shouldBe false - } - - it should "handle multiple rights (getUserRights, addUserRight, deleteUserRight)" in { - val user1 = newDbUser() - val internalId = executeSql(tested.createUser(user1)) - val rights1 = executeSql(tested.getUserRights(internalId)) - executeSql(tested.addUserRight(internalId, right1, grantedAt = zeroMicros)) - executeSql(tested.addUserRight(internalId, right2, grantedAt = zeroMicros)) - executeSql(tested.addUserRight(internalId, right3, grantedAt = zeroMicros)) - val rights2 = executeSql(tested.getUserRights(internalId)) - val deleteRight2 = executeSql(tested.deleteUserRight(internalId, right2)) - val rights3 = executeSql(tested.getUserRights(internalId)) - val deleteRight3 = executeSql(tested.deleteUserRight(internalId, right3)) - val rights4 = executeSql(tested.getUserRights(internalId)) - rights1 shouldBe empty - rights2.map(_.apiRight) should contain theSameElementsAs Seq(right1, right2, right3) - deleteRight2 shouldBe true - rights3.map(_.apiRight) should contain theSameElementsAs Seq(right1, right3) - deleteRight3 shouldBe true - rights4.map(_.apiRight) should contain theSameElementsAs Seq(right1) - } - - it should "add and delete a single right (userRightExists, addUserRight, deleteUserRight, getUserRights)" in { - val user1 = newDbUser() - val internalId = executeSql(tested.createUser(user1)) - // no rights - val rightExists0 = executeSql(tested.userRightExists(internalId, right1)) - val rights0 = executeSql(tested.getUserRights(internalId)) - // add one rights - executeSql(tested.addUserRight(internalId, right1, grantedAt = zeroMicros)) - val rightExists1 = executeSql(tested.userRightExists(internalId, right1)) - val rights1 = executeSql(tested.getUserRights(internalId)) - // delete - val deleteRight = executeSql(tested.deleteUserRight(internalId, right1)) - val rightExists2 = executeSql(tested.userRightExists(internalId, right1)) - val rights2 = executeSql(tested.getUserRights(internalId)) - // no rights - rightExists0 shouldBe false - rights0 shouldBe empty - rightExists1 shouldBe true - rights1.map(_.apiRight) should contain theSameElementsAs Seq(right1) - // deleted right - deleteRight shouldBe true - rightExists2 shouldBe false - rights2 shouldBe empty - } - - private def newDbUser( - userId: String = "", - isDeactivated: Boolean = false, - primaryPartyOverride: Option[Option[Ref.Party]] = None, - identityProviderId: IdentityProviderId = IdentityProviderId.Default, - primaryPartyAuthentication: Boolean = false, - resourceVersion: Long = 0, - createdAt: Long = zeroMicros, - ): UserManagementStorageBackend.DbUserPayload = { - val uuid = UUID.randomUUID.toString - val primaryParty = primaryPartyOverride.getOrElse( - Some(Ref.Party.assertFromString(s"primary_party_$uuid")) - ) - val userIdStr = if (userId != "") userId else s"user_id_$uuid" - UserManagementStorageBackend.DbUserPayload( - id = Ref.UserId.assertFromString(userIdStr), - primaryPartyO = primaryParty, - isDeactivated = isDeactivated, - resourceVersion = resourceVersion, - primaryPartyAuthentication = primaryPartyAuthentication, - identityProviderId = identityProviderId.toDb, - createdAt = createdAt, - ) - } - - private def newParty: Ref.Party = - Ref.Party.assertFromString(s"party_${UUID.randomUUID.toString}") - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/UpdateToDbDtoSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/UpdateToDbDtoSpec.scala deleted file mode 100644 index c029e8df1d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/UpdateToDbDtoSpec.scala +++ /dev/null @@ -1,2994 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend - -import com.daml.metrics.api.MetricsContext -import com.daml.platform.v1.index.StatusDetails -import com.digitalasset.canton.RepairCounter -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.DeduplicationPeriod.{DeduplicationDuration, DeduplicationOffset} -import com.digitalasset.canton.data.{CantonTimestamp, LedgerTimeBoundaries, Offset} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.Update.ContractInfo -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationEvent.{ - Added, - ChangedTo, - Revoked, -} -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.AuthorizationLevel.* -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.TopologyEvent.PartyToParticipantAuthorization -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective.{ - AuthorizationEvent, - TopologyEvent, -} -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId.{ - DedicatedRepresentativePackageId, - SameAsContractPackageId, -} -import com.digitalasset.canton.ledger.participant.state.{ - Reassignment, - ReassignmentInfo, - TestAcsChangeFactory, - TransactionMeta, - Update, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.platform.store.backend.Conversions.{ - authorizationEventInt, - participantPermissionInt, -} -import com.digitalasset.canton.platform.store.backend.DbDto.IdFilter -import com.digitalasset.canton.platform.store.backend.StorageBackendTestValues.{ - someExternalTransactionHash, - somePackageId, -} -import com.digitalasset.canton.platform.store.backend.UpdateToDbDto.templateIdWithPackageName -import com.digitalasset.canton.platform.store.dao.events.{ - CompressionStrategy, - FieldCompressionStrategy, - LfValueSerialization, -} -import com.digitalasset.canton.platform.{ContractId, Create, Exercise} -import com.digitalasset.canton.protocol.{ - ContractInstance, - ExampleContractFactory, - ReassignmentId, - TestUpdateId, -} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.canton.tracing.TraceContext.Implicits.Empty.emptyTraceContext -import com.digitalasset.canton.tracing.{SerializableTraceContext, TraceContext} -import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.{Ref, Time} -import com.digitalasset.daml.lf.transaction.test.TestNodeBuilder.CreateKey -import com.digitalasset.daml.lf.transaction.test.{ - NodeIdTransactionBuilder, - TestNodeBuilder, - TransactionBuilder, -} -import com.digitalasset.daml.lf.transaction.{CreationTime, GlobalKey, GlobalKeyWithMaintainers} -import com.digitalasset.daml.lf.value.Value -import com.google.rpc.status.Status as StatusProto -import io.grpc.Status -import org.scalatest.matchers.should.Matchers -import org.scalatest.prop.TableDrivenPropertyChecks.* -import org.scalatest.wordspec.AnyWordSpec - -import java.time.{Duration, Instant} -import java.util.UUID - -// Note: this suite contains hand-crafted updates that are impossible to produce on some ledgers -// (e.g., because the ledger removes rollback nodes before sending them to the index database). -// Should you ever consider replacing this suite by something else, make sure all functionality is still covered. -class UpdateToDbDtoSpec extends AnyWordSpec with Matchers { - - import TraceContext.Implicits.Empty.* - import TransactionBuilder.Implicits.{defaultPackageId as _, *} - import UpdateToDbDtoSpec.* - - // Shadow illegal definition in TransactionBuilder.Implicits - implicit val defaultPackageId: Ref.PackageId = somePackageId - - object TxBuilder { - def apply(): NodeIdTransactionBuilder & TestNodeBuilder = new NodeIdTransactionBuilder - with TestNodeBuilder - } - - "UpdateToDbDto" should { - - "handle CommandRejected (sequenced rejection)" in { - val status = StatusProto.of(Status.Code.ABORTED.value(), "test reason", Seq.empty) - val completionInfo = someCompletionInfo - val update = state.Update.SequencedCommandRejected( - completionInfo, - state.Update.CommandRejected.FinalReason(status), - someSynchronizerId1, - CantonTimestamp.ofEpochMicro(1234567), - isTransaction = true, - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = 1234567L, - publication_time = 0, - user_id = someUserId, - submitters = Set(someParty), - command_id = someCommandId, - update_id = None, - rejection_status_code = Some(status.code), - rejection_status_message = Some(status.message), - rejection_status_details = Some(StatusDetails.of(status.details).toByteArray), - submission_id = Some(someSubmissionId), - deduplication_offset = None, - deduplication_duration_seconds = None, - deduplication_duration_nanos = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - ) - } - - "handle CommandRejected (local rejection)" in { - val status = StatusProto.of(Status.Code.ABORTED.value(), "test reason", Seq.empty) - val messageUuid = UUID.randomUUID() - val completionInfo = someCompletionInfo - val update = state.Update.UnSequencedCommandRejected( - completionInfo, - state.Update.CommandRejected.FinalReason(status), - someSynchronizerId1, - someRecordTime, - messageUuid, - isTransaction = true, - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = someUserId, - submitters = Set(someParty), - command_id = someCommandId, - update_id = None, - rejection_status_code = Some(status.code), - rejection_status_message = Some(status.message), - rejection_status_details = Some(StatusDetails.of(status.details).toByteArray), - submission_id = Some(someSubmissionId), - deduplication_offset = None, - deduplication_duration_seconds = None, - deduplication_duration_nanos = None, - synchronizer_id = someSynchronizerId1, - message_uuid = Some(messageUuid.toString), - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - ) - } - - val updateId = TestUpdateId("mock_hash") - val updateIdByteArray = updateId.toProtoPrimitive.toByteArray - - // We only care about distinguishing between repair and sequencer transactions for create nodes - // since for create nodes the representative package-id assignment policies are different between the two - def handleAcsDeltaTransactionAcceptedWithSingleCreateNode( - isAcsDelta: Boolean, - isRepairTransaction: Boolean, - ): Unit = { - assert( - isRepairTransaction && isAcsDelta || !isRepairTransaction, - "Repair transaction is implicitly an ACS delta", - ) - val updateName = - if (isRepairTransaction) classOf[state.Update.RepairTransactionAccepted].getSimpleName - else classOf[state.Update.SequencedTransactionAccepted].getSimpleName - s"handle $updateName (single create node, isAcsDelta = $isAcsDelta)" in { - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val contractTemplate = Ref.Identifier.assertFromString("P:M:T") - val keyValue = Value.ValueUnit - val contract = ExampleContractFactory.build( - stakeholders = Set("signatory1", "signatory2", "signatory3", "observer").map( - Ref.Party.assertFromString - ), - signatories = - Set("signatory1", "signatory2", "signatory3").map(Ref.Party.assertFromString), - templateId = contractTemplate, - argument = Value.ValueUnit, - keyOpt = Some( - GlobalKeyWithMaintainers.assertBuild( - templateId = contractTemplate, - value = keyValue, - valueHash = crypto.Hash.hashPrivateKey(keyValue.toString), - maintainers = Set("signatory2", "signatory3").map(Ref.Party.assertFromString), - packageName = ExampleContractFactory.packageName, - ) - ), - ) - val createNode = contract.inst.toCreateNode - val createNodeId = builder.add(createNode) - val transaction = builder.buildCommitted() - val update = - if (isRepairTransaction) - state.Update.RepairTransactionAccepted( - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - repairCounter = RepairCounter(1337), - contractInfos = Map(contract.contractId -> someContractInfos(contract)), - ) - else - state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(contractActivenessChanged = isAcsDelta), - contractInfos = Map( - contract.contractId -> someContractInfos(contract, SameAsContractPackageId) - ), - ) - val dtos = updateToDtos(update) - - val dtoCreate = DbDto.EventActivate( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Option.when(!isRepairTransaction)(completionInfo.commandId), - submitters = Option.when(!isRepairTransaction)(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = - Option.when(!isRepairTransaction)(externalTransactionHash.unwrap.toByteArray), - traffic_cost = - Option.when(!isRepairTransaction)(someCompletionInfo.paidTrafficCost.value), - event_type = PersistentEventType.Create.asInt, - event_sequential_id = 0, - node_id = createNodeId.index, - additional_witnesses = Some( - if (isAcsDelta) Set.empty - else Set("signatory1", "signatory2", "signatory3", "observer") - ), - source_synchronizer_id = None, - reassignment_counter = None, - reassignment_id = None, - representative_package_id = - if (isRepairTransaction) someRepresentativePackageId - else createNode.templateId.packageId, - notPersistedContractId = createNode.coid, - internal_contract_id = 42L, - create_key_hash = Some( - GlobalKey - .assertBuild( - contractTemplate, - createNode.packageName, - keyValue, - crypto.Hash.hashPrivateKey(keyValue.toString), - ) - .hash - .bytes - .toHexString - ), - ) - val dtoCompletion = DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - val dtoTransactionMeta = DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - - dtos.head shouldEqual dtoCreate - if (!isRepairTransaction) { - dtos(5) shouldEqual dtoCompletion - dtos(6) shouldEqual dtoTransactionMeta - } else { - dtos(5) shouldEqual dtoTransactionMeta - } - Set(dtos(1), dtos(2), dtos(3), dtos(4)) should contain theSameElementsAs - (if (isAcsDelta) - Set( - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory1", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory2", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory3", - first_per_sequential_id = false, - ) - ), - DbDto - .IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer", - first_per_sequential_id = true, - ) - ), - ) - else - Set( - DbDto.IdFilterActivateWitness( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory1", - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterActivateWitness( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory2", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateWitness( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory3", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateWitness( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer", - first_per_sequential_id = false, - ) - ), - )) - - if (isRepairTransaction) - dtos.size shouldEqual 6 - else - dtos.size shouldEqual 7 - } - } - - handleAcsDeltaTransactionAcceptedWithSingleCreateNode( - isAcsDelta = true, - isRepairTransaction = false, - ) - handleAcsDeltaTransactionAcceptedWithSingleCreateNode( - isAcsDelta = true, - isRepairTransaction = true, - ) - - "handle SequencedTransactionAccepted (single create node, isAcsDelta = false)" in { - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val contractTemplate = Ref.Identifier.assertFromString("P:M:T") - val keyValue = Value.ValueUnit - val contract = ExampleContractFactory.build( - stakeholders = - Set("signatory1", "signatory2", "signatory3", "observer").map(Ref.Party.assertFromString), - signatories = Set("signatory1", "signatory2", "signatory3").map(Ref.Party.assertFromString), - templateId = contractTemplate, - argument = Value.ValueUnit, - keyOpt = Some( - GlobalKeyWithMaintainers.assertBuild( - templateId = contractTemplate, - value = keyValue, - valueHash = crypto.Hash.hashPrivateKey(keyValue.toString), - maintainers = Set("signatory2", "signatory3").map(Ref.Party.assertFromString), - packageName = ExampleContractFactory.packageName, - ) - ), - ) - val createNode = contract.inst.toCreateNode - val createNodeId = builder.add(createNode) - val transaction = builder.buildCommitted() - val update = - state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(false), - contractInfos = Map( - contract.contractId -> someContractInfos(contract, SameAsContractPackageId) - ), - ) - val dtos = updateToDtos(update) - - val dtoCreate = DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.WitnessedCreate.asInt, - event_sequential_id = 0, - node_id = createNodeId.index, - additional_witnesses = Set("signatory1", "signatory2", "signatory3", "observer"), - consuming = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - representative_package_id = Some(createNode.templateId.packageId), - contract_id = None, - internal_contract_id = Some(42L), - template_id = None, - package_id = None, - ledger_effective_time = None, - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ) - val dtoCompletion = DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - val dtoTransactionMeta = DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - - dtos.head shouldEqual dtoCreate - dtos(5) shouldEqual dtoCompletion - dtos(6) shouldEqual dtoTransactionMeta - Set(dtos(1), dtos(2), dtos(3), dtos(4)) should contain theSameElementsAs - Set( - DbDto.IdFilterVariousWitness( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory1", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterVariousWitness( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory2", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterVariousWitness( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory3", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterVariousWitness( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer", - first_per_sequential_id = true, - ) - ), - ) - - dtos.size shouldEqual 7 - } - - s"handle TransactionAccepted (single consuming exercise node, isAcsDelta = true)" in { - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val contract = ExampleContractFactory.build( - stakeholders = Set("signatory", "observer").map(Ref.Party.assertFromString), - signatories = Set(Ref.Party.assertFromString("signatory")), - templateId = Ref.Identifier.assertFromString("P:M:T"), - argument = Value.ValueUnit, - ) - val exerciseNode = - builder.exercise( - contract = contract.inst.toCreateNode, - choice = "someChoice", - consuming = true, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val exerciseNodeId = builder.add(exerciseNode) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = CantonTimestamp.ofEpochMicro(120), - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map( - exerciseNode.targetCoid -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = contract.inst, - internalContractId = 43L, - ), - representativePackageId = SameAsContractPackageId, - ) - ), - ) - val dtos = updateToDtos(update) - - dtos.head shouldEqual - DbDto.EventDeactivate( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = 120, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - traffic_cost = someTrafficCost, - event_type = PersistentEventType.ConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeId.index, - deactivated_event_sequential_id = None, - additional_witnesses = Some(Set.empty), - exercise_choice = exerciseNode.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdConsumingArg, emptyArray)), - exercise_result = Some(compressArrayWith(compressionAlgorithmIdConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeId.index), - exercise_argument_compression = compressionAlgorithmIdConsumingArg, - exercise_result_compression = compressionAlgorithmIdConsumingRes, - reassignment_id = None, - assignment_exclusivity = None, - target_synchronizer_id = None, - reassignment_counter = None, - contract_id = exerciseNode.targetCoid, - internal_contract_id = None, - template_id = templateIdWithPackageName(exerciseNode), - package_id = exerciseNode.templateId.packageId, - stakeholders = Set("signatory", "observer"), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - ) - dtos(3) shouldEqual - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = 120, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - dtos(4) shouldEqual - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = 120, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - - Set(dtos(1), dtos(2)) should contain theSameElementsAs - Set( - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "signatory", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "observer", - first_per_sequential_id = true, - ) - ), - ) - - dtos.size shouldEqual 5 - } - - s"handle TransactionAccepted (single consuming exercise node, isAcsDelta = false)" in { - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val exerciseNode = { - val createNode = builder.create( - id = builder.newCid, - templateId = "M:T", - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - ) - builder.exercise( - contract = createNode, - choice = "someChoice", - consuming = true, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - } - val exerciseNodeId = builder.add(exerciseNode) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = CantonTimestamp.ofEpochMicro(120), - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(false), - contractInfos = Map.empty, - ) - val dtos = updateToDtos(update) - - dtos.head shouldEqual - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = 120, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.WitnessedConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeId.index, - additional_witnesses = Set("signatory", "observer"), - consuming = Some(true), - exercise_choice = exerciseNode.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdConsumingArg, emptyArray)), - exercise_result = Some(compressArrayWith(compressionAlgorithmIdConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeId.index), - exercise_argument_compression = compressionAlgorithmIdConsumingArg, - exercise_result_compression = compressionAlgorithmIdConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNode.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNode)), - package_id = Some(exerciseNode.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ) - dtos(3) shouldEqual - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = 120, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - dtos(4) shouldEqual - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = 120, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - - Set(dtos(1), dtos(2)) should contain theSameElementsAs - Set( - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "observer", - first_per_sequential_id = false, - ) - ), - ) - - dtos.size shouldEqual 5 - } - - "handle TransactionAccepted (single non-consuming exercise node)" in { - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val exerciseNode = { - val createNode = builder.create( - id = builder.newCid, - templateId = "M:T", - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - ) - builder.exercise( - contract = createNode, - choice = "someChoice", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - } - val exerciseNodeId = builder.add(exerciseNode) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map.empty, - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNode.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNode.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNode)), - package_id = Some(exerciseNode.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ), - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ), - ) - } - - "handle TransactionAccepted (create node divulged)" in { - // Previous transaction - // └─ #1 Create - // Transaction - // └─ #2 Exercise (choice A) - // ├─ #3 Exercise (choice B) - // └─ #4 Create (C) - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val contract = ExampleContractFactory.build( - stakeholders = Set("signatory", "observer").map(Ref.Party.assertFromString), - signatories = Set(Ref.Party.assertFromString("signatory")), - templateId = Ref.Identifier.assertFromString("P:M:T"), - argument = Value.ValueUnit, - ) - val createNode = contract.inst.toCreateNode - val exerciseNodeA = builder.exercise( - contract = createNode, - choice = "A", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val exerciseNodeB = builder.exercise( - contract = createNode, - choice = "B", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val contractC = ExampleContractFactory.build( - stakeholders = Set("signatory2").map(Ref.Party.assertFromString), - signatories = Set(Ref.Party.assertFromString("signatory2")), - templateId = Ref.Identifier.assertFromString("P:M:T2"), - argument = Value.ValueUnit, - ) - val createNodeC = contractC.inst.toCreateNode - val exerciseNodeAId = builder.add(exerciseNodeA) - val exerciseNodeBId = builder.add(exerciseNodeB, exerciseNodeAId) - val createNodeCId = builder.add(createNodeC, exerciseNodeAId) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(false), - contractInfos = Map( - createNodeC.coid -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = contractC.inst, - internalContractId = 42L, - ), - representativePackageId = SameAsContractPackageId, - ) - ), - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeAId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNodeA.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(createNodeCId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNodeA.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNodeA)), - package_id = Some(exerciseNodeA.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNodeA), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeBId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNodeB.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeBId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNodeB.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNodeB)), - package_id = Some(exerciseNodeB.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNodeB), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.WitnessedCreate.asInt, - event_sequential_id = 0, - node_id = createNodeCId.index, - additional_witnesses = Set("signatory", "signatory2"), - consuming = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - representative_package_id = Some(createNode.templateId.packageId), - contract_id = None, - internal_contract_id = Some(42L), - template_id = None, - package_id = None, - ledger_effective_time = None, - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(createNodeC), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(createNodeC), - party_id = "signatory2", - first_per_sequential_id = false, - ) - ), - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ), - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ), - ) - } - - "handle TransactionAccepted (nested create node, ACSDelta = true)" in { - // Previous transaction - // └─ #1 Create - // Transaction - // └─ #2 Exercise (choice A) - // ├─ #3 Exercise (choice B) - // └─ #4 Create (C) - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val createNode = builder.create( - id = builder.newCid, - templateId = "M:T", - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - ) - val exerciseNodeA = builder.exercise( - contract = createNode, - choice = "A", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val exerciseNodeB = builder.exercise( - contract = createNode, - choice = "B", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val contractC = ExampleContractFactory.build( - stakeholders = Set("signatory2").map(Ref.Party.assertFromString), - signatories = Set(Ref.Party.assertFromString("signatory2")), - templateId = Ref.Identifier.assertFromString("P:M:T2"), - argument = Value.ValueUnit, - keyOpt = Some( - GlobalKeyWithMaintainers( - globalKey = GlobalKey.assertBuild( - templateId = Ref.Identifier.assertFromString("P:M:T2"), - packageName = ExampleContractFactory.packageName, - key = Value.ValueUnit, - keyHash = crypto.Hash.hashPrivateKey("dummy-key-hash"), - ), - maintainers = Set("signatory2"), - ) - ), - ) - val createNodeC = contractC.inst.toCreateNode - val exerciseNodeAId = builder.add(exerciseNodeA) - val exerciseNodeBId = builder.add(exerciseNodeB, exerciseNodeAId) - val createNodeCId = builder.add(createNodeC, exerciseNodeAId) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map( - createNodeC.coid -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = contractC.inst, - internalContractId = 42L, - ), - representativePackageId = SameAsContractPackageId, - ) - ), - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeAId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNodeA.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(createNodeCId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNodeA.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNodeA)), - package_id = Some(exerciseNodeA.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNodeA), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeBId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNodeB.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeBId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNodeB.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNodeB)), - package_id = Some(exerciseNodeB.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNodeB), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.EventActivate( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - traffic_cost = someTrafficCost, - event_type = PersistentEventType.Create.asInt, - event_sequential_id = 0, - node_id = createNodeCId.index, - additional_witnesses = Some(Set("signatory")), - source_synchronizer_id = None, - reassignment_counter = None, - reassignment_id = None, - representative_package_id = createNodeC.templateId.packageId, - notPersistedContractId = createNodeC.coid, - internal_contract_id = 42L, - create_key_hash = Some( - GlobalKey - .assertBuild( - Ref.Identifier.assertFromString("P:M:T2"), - createNodeC.packageName, - Value.ValueUnit, - crypto.Hash.hashPrivateKey("dummy-key-hash"), - ) - .hash - .bytes - .toHexString - ), - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(createNodeC), - party_id = "signatory2", - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterActivateWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(createNodeC), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ), - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ), - ) - } - - "handle TransactionAccepted (nested exercise nodes)" in { - // Previous transaction - // └─ #1 Create - // Transaction - // └─ #2 Exercise (choice A) - // ├─ #3 Exercise (choice B) - // └─ #4 Exercise (choice C) - // └─ #5 Exercise (choice D) - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val createNode = builder.create( - id = builder.newCid, - templateId = "M:T", - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - ) - val exerciseNodeA = builder.exercise( - contract = createNode, - choice = "A", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val exerciseNodeB = builder.exercise( - contract = createNode, - choice = "B", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val exerciseNodeC = builder.exercise( - contract = createNode, - choice = "C", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val exerciseNodeD = builder.exercise( - contract = createNode, - choice = "D", - consuming = false, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set.empty, - byKey = false, - ) - val exerciseNodeAId = builder.add(exerciseNodeA) - val exerciseNodeBId = builder.add(exerciseNodeB, exerciseNodeAId) - val exerciseNodeCId = builder.add(exerciseNodeC, exerciseNodeAId) - val exerciseNodeDId = builder.add(exerciseNodeD, exerciseNodeCId) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map.empty, - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeAId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNodeA.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeDId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNodeA.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNodeA)), - package_id = Some(exerciseNodeA.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNodeA), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeBId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNodeB.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeBId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNodeB.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNodeB)), - package_id = Some(exerciseNodeB.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNodeB), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeCId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNodeC.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeDId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNodeC.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNodeC)), - package_id = Some(exerciseNodeC.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNodeC), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.EventVariousWitnessed( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - event_type = PersistentEventType.NonConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeDId.index, - additional_witnesses = Set("signatory"), - consuming = Some(false), - exercise_choice = exerciseNodeD.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingArg, emptyArray)), - exercise_result = - Some(compressArrayWith(compressionAlgorithmIdNonConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeDId.index), - exercise_argument_compression = compressionAlgorithmIdNonConsumingArg, - exercise_result_compression = compressionAlgorithmIdNonConsumingRes, - representative_package_id = None, - contract_id = Some(exerciseNodeD.targetCoid), - internal_contract_id = None, - template_id = Some(templateIdWithPackageName(exerciseNodeD)), - package_id = Some(exerciseNodeD.templateId.packageId), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - traffic_cost = Some(completionInfo.paidTrafficCost.value), - ), - DbDto.IdFilterVariousWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNodeD), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ), - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ), - ) - } - - "handle TransactionAccepted (fetch and lookup nodes)" in { - // Previous transaction - // └─ #1 Create - // Transaction - // ├─ #1 Fetch - // ├─ #2 Fetch by key - // └─ #3 Lookup by key - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val builder = TxBuilder() - val createNode = builder.create( - id = builder.newCid, - templateId = "M:T", - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - key = CreateKey.SignatoryMaintainerKey( - Value.ValueUnit, - crypto.Hash.hashPrivateKey("dummy-key-hash"), - ), - ) - val fetchNode = builder.fetch( - contract = createNode, - byKey = false, - ) - val fetchByKeyNode = builder.fetch( - contract = createNode, - byKey = true, - ) - val lookupByKeyNode = builder.lookupByKey( - contract = createNode - ) - builder.add(fetchNode) - builder.add(fetchByKeyNode) - builder.add(lookupByKeyNode) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map.empty, - ) - val dtos = updateToDtos(update) - - // Note: fetch and lookup nodes are not indexed - dtos should contain theSameElementsInOrderAs List( - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ), - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ), - ) - } - - "handle TransactionAccepted (single exercise node with divulgence)" in { - // Previous transaction - // └─ #1 Create - // Transaction - // └─ #2 Exercise (divulges #1 to 'divulgee') - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - - val builder = TxBuilder() - val createNode = builder.create( - id = builder.newCid, - templateId = "M:T", - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - ) - val exerciseNode = builder.exercise( - contract = createNode, - choice = "someChoice", - consuming = true, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set("divulgee"), - byKey = false, - ) - val exerciseNodeId = builder.add(exerciseNode) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map.empty, - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.EventDeactivate( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - traffic_cost = someTrafficCost, - event_type = PersistentEventType.ConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeId.index, - deactivated_event_sequential_id = None, - additional_witnesses = Some(Set("divulgee")), - exercise_choice = exerciseNode.choiceId, - exercise_choice_interface_id = None, - exercise_argument = - Some(compressArrayWith(compressionAlgorithmIdConsumingArg, emptyArray)), - exercise_result = Some(compressArrayWith(compressionAlgorithmIdConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeId.index), - exercise_argument_compression = compressionAlgorithmIdConsumingArg, - exercise_result_compression = compressionAlgorithmIdConsumingRes, - reassignment_id = None, - assignment_exclusivity = None, - target_synchronizer_id = None, - reassignment_counter = None, - contract_id = exerciseNode.targetCoid, - internal_contract_id = None, - template_id = templateIdWithPackageName(exerciseNode), - package_id = exerciseNode.templateId.packageId, - stakeholders = Set("signatory", "observer"), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "signatory", - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "observer", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterDeactivateWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "divulgee", - first_per_sequential_id = true, - ) - ), - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ), - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ), - ) - } - - "handle TransactionAccepted (transaction with local divulgence)" in { - // Transaction - // ├─ #1 Create - // └─ #2 Exercise (divulges #1 to 'divulgee') - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val interfaceId = toIdentifier("M:I") - val contract = ExampleContractFactory.build( - stakeholders = Set("signatory", "observer").map(Ref.Party.assertFromString), - signatories = Set(Ref.Party.assertFromString("signatory")), - templateId = Ref.Identifier.assertFromString("P:M:T"), - argument = Value.ValueUnit, - ) - val createNode = contract.inst.toCreateNode - val exerciseNode = builder.exercise( - contract = createNode, - choice = "someChoice", - consuming = true, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set("divulgee"), - byKey = false, - interfaceId = Some(interfaceId), - ) - val createNodeId = builder.add(createNode) - val exerciseNodeId = builder.add(exerciseNode) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map( - contract.contractId -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = contract.inst, - internalContractId = 42L, - ), - representativePackageId = SameAsContractPackageId, - ) - ), - ) - val dtos = updateToDtos(update) - - dtos.head shouldEqual DbDto.EventActivate( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - traffic_cost = someTrafficCost, - event_type = PersistentEventType.Create.asInt, - event_sequential_id = 0, - node_id = createNodeId.index, - additional_witnesses = Some(Set.empty), - source_synchronizer_id = None, - reassignment_counter = None, - reassignment_id = None, - representative_package_id = createNode.templateId.packageId, - notPersistedContractId = createNode.coid, - internal_contract_id = 42L, - create_key_hash = None, - ) - Set(dtos(1), dtos(2)) should contain theSameElementsAs Set( - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer", - first_per_sequential_id = true, - ) - ), - ) - dtos(3) shouldEqual DbDto.EventDeactivate( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - traffic_cost = someTrafficCost, - event_type = PersistentEventType.ConsumingExercise.asInt, - event_sequential_id = 0, - node_id = exerciseNodeId.index, - deactivated_event_sequential_id = None, - additional_witnesses = Some(Set("divulgee")), - exercise_choice = Some(exerciseNode.choiceId), - exercise_choice_interface_id = Some(interfaceId), - exercise_argument = Some(compressArrayWith(compressionAlgorithmIdConsumingArg, emptyArray)), - exercise_result = Some(compressArrayWith(compressionAlgorithmIdConsumingRes, emptyArray)), - exercise_actors = Some(Set("signatory")), - exercise_last_descendant_node_id = Some(exerciseNodeId.index), - exercise_argument_compression = compressionAlgorithmIdConsumingArg, - exercise_result_compression = compressionAlgorithmIdConsumingRes, - reassignment_id = None, - assignment_exclusivity = None, - target_synchronizer_id = None, - reassignment_counter = None, - contract_id = exerciseNode.targetCoid, - internal_contract_id = None, - template_id = templateIdWithPackageName(exerciseNode), - package_id = exerciseNode.templateId.packageId, - stakeholders = Set("signatory", "observer"), - ledger_effective_time = Some(transactionMeta.ledgerEffectiveTime.micros), - ) - dtos(4) shouldEqual DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "observer", - first_per_sequential_id = true, - ) - ) - dtos(5) shouldEqual DbDto.IdFilterDeactivateStakeholder( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "signatory", - first_per_sequential_id = false, - ) - ) - dtos(6) shouldEqual DbDto.IdFilterDeactivateWitness( - IdFilter( - event_sequential_id = 0, - template_id = templateIdWithPackageName(exerciseNode), - party_id = "divulgee", - first_per_sequential_id = true, - ) - ) - dtos(7) shouldEqual DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - dtos(8) shouldEqual DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - dtos.size shouldEqual 9 - } - - "handle TransactionAccepted (rollback node)" in { - // Transaction - // └─ #1 Rollback - // ├─ #2 Create - // └─ #3 Exercise (divulges #2 to divulgee) - // - Create and Exercise events must not be visible - val completionInfo = someCompletionInfo - val transactionMeta = someTransactionMeta - val builder = TxBuilder() - val rollbackNode = builder.rollback() - val createNode = builder.create( - id = builder.newCid, - templateId = "M:T", - argument = Value.ValueUnit, - signatories = List("signatory"), - observers = List("observer"), - ) - val exerciseNode = builder.exercise( - contract = createNode, - choice = "someChoice", - consuming = true, - actingParties = Set("signatory"), - argument = Value.ValueUnit, - result = Some(Value.ValueUnit), - choiceObservers = Set("divulgee"), - byKey = false, - ) - val rollbackNodeId = builder.add(rollbackNode) - builder.add(createNode, rollbackNodeId) - builder.add(exerciseNode, rollbackNodeId) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map.empty, - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ), - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ), - ) - } - - "handle TransactionAccepted (no submission info)" in { - // Transaction that is missing a SubmitterInfo - // This happens if a transaction was submitted through a different participant - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val contract = ExampleContractFactory.build( - stakeholders = Set("signatory", "observer").map(Ref.Party.assertFromString), - signatories = Set(Ref.Party.assertFromString("signatory")), - templateId = Ref.Identifier.assertFromString("P:M:T"), - argument = Value.ValueUnit, - ) - val createNode = contract.inst.toCreateNode - val createNodeId = builder.add(createNode) - val transaction = builder.buildCommitted() - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = None, - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map( - contract.contractId -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = contract.inst, - internalContractId = 42L, - ), - representativePackageId = SameAsContractPackageId, - ) - ), - ) - val dtos = updateToDtos(update) - - dtos.head shouldEqual DbDto.EventActivate( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = None, - submitters = None, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - traffic_cost = None, - event_type = PersistentEventType.Create.asInt, - event_sequential_id = 0, - node_id = createNodeId.index, - additional_witnesses = Some(Set.empty), - source_synchronizer_id = None, - reassignment_counter = None, - reassignment_id = None, - representative_package_id = createNode.templateId.packageId, - notPersistedContractId = createNode.coid, - internal_contract_id = 42L, - create_key_hash = None, - ) - Set(dtos(1), dtos(2)) should contain theSameElementsAs Set( - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer", - first_per_sequential_id = true, - ) - ), - ) - dtos.size shouldEqual 4 - } - - val deduplicationPeriods = Table( - ( - "Deduplication period", - "Expected deduplication offset", - "Expected deduplication duration seconds", - "Expected deduplication duration nanos", - ), - (None, None, None, None), - ( - Some(DeduplicationOffset(None)), - Some(0L), - None, - None, - ), - ( - Some(DeduplicationDuration(Duration.ofDays(1L).plusNanos(100 * 1000))), - None, - Some(Duration.ofDays(1L).toMinutes * 60L), - Some(100 * 1000), - ), - ) - - "handle CommandRejected (all deduplication data)" in { - val status = StatusProto.of(Status.Code.ABORTED.value(), "test reason", Seq.empty) - forAll(deduplicationPeriods) { - case ( - deduplicationPeriod, - expectedDeduplicationOffset, - expectedDeduplicationDurationSeconds, - expectedDeduplicationDurationNanos, - ) => - forAll(Table("isTransaction", true, false)) { isTransaction => - val completionInfo = - someCompletionInfo.copy(optDeduplicationPeriod = deduplicationPeriod) - val update = state.Update.SequencedCommandRejected( - completionInfo, - state.Update.CommandRejected.FinalReason(status), - someSynchronizerId1, - someRecordTime, - isTransaction = isTransaction, - ) - val dtos = updateToDtos(update) - - dtos should contain theSameElementsInOrderAs List( - DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = someUserId, - submitters = Set(someParty), - command_id = someCommandId, - update_id = None, - rejection_status_code = Some(status.code), - rejection_status_message = Some(status.message), - rejection_status_details = Some(StatusDetails.of(status.details).toByteArray), - submission_id = Some(someSubmissionId), - deduplication_offset = expectedDeduplicationOffset, - deduplication_duration_seconds = expectedDeduplicationDurationSeconds, - deduplication_duration_nanos = expectedDeduplicationDurationNanos, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = isTransaction, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - ) - } - - } - } - - "handle TransactionAccepted (all deduplication data)" in { - val transactionMeta = someTransactionMeta - val externalTransactionHash = someExternalTransactionHash - val builder = TxBuilder() - val contract = ExampleContractFactory.build( - stakeholders = Set("signatory", "observer").map(Ref.Party.assertFromString), - signatories = Set(Ref.Party.assertFromString("signatory")), - templateId = Ref.Identifier.assertFromString("P:M:T"), - argument = Value.ValueUnit, - ) - val createNode = contract.inst.toCreateNode - val createNodeId = builder.add(createNode) - val transaction = builder.buildCommitted() - - forAll(deduplicationPeriods) { - case ( - deduplicationPeriod, - expectedDeduplicationOffset, - expectedDeduplicationDurationSeconds, - expectedDeduplicationDurationNanos, - ) => - val completionInfo = someCompletionInfo.copy(optDeduplicationPeriod = deduplicationPeriod) - val update = state.Update.SequencedTransactionAccepted( - completionInfoO = Some(completionInfo), - transactionMeta = transactionMeta, - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = someSynchronizerId1, - recordTime = someRecordTime, - externalTransactionHash = Some(externalTransactionHash), - acsChangeFactory = TestAcsChangeFactory(), - contractInfos = Map( - contract.contractId -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - inst = contract.inst, - internalContractId = 42L, - ), - representativePackageId = SameAsContractPackageId, - ) - ), - ) - val dtos = updateToDtos(update) - - dtos.head shouldEqual DbDto.EventActivate( - event_offset = someOffset.unwrap, - update_id = updateIdByteArray, - workflow_id = transactionMeta.workflowId, - command_id = Some(completionInfo.commandId), - submitters = Some(completionInfo.actAs.toSet), - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - trace_context = serializedEmptyTraceContext, - external_transaction_hash = Some(externalTransactionHash.unwrap.toByteArray), - traffic_cost = someTrafficCost, - event_type = PersistentEventType.Create.asInt, - event_sequential_id = 0, - node_id = createNodeId.index, - additional_witnesses = Some(Set.empty), - source_synchronizer_id = None, - reassignment_counter = None, - reassignment_id = None, - representative_package_id = createNode.templateId.packageId, - notPersistedContractId = createNode.coid, - internal_contract_id = 42L, - create_key_hash = None, - ) - Set(dtos(1), dtos(2)) should contain theSameElementsAs Set( - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer", - first_per_sequential_id = true, - ) - ), - ) - dtos(3) shouldEqual DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = Some(someSubmissionId), - deduplication_offset = expectedDeduplicationOffset, - deduplication_duration_seconds = expectedDeduplicationDurationSeconds, - deduplication_duration_nanos = expectedDeduplicationDurationNanos, - synchronizer_id = someSynchronizerId1, - message_uuid = None, - is_transaction = true, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - dtos(4) shouldEqual DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = someSynchronizerId1, - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - dtos.size shouldEqual 5 - } - } - - "handle ReassignmentAccepted - Assign" in { - val completionInfo = someCompletionInfo - val templateId = Ref.Identifier.assertFromString("P:M:T") - val keyValue = Value.ValueUnit - val contract = ExampleContractFactory.build( - stakeholders = Set("signatory", "observer", "observer2").map(Ref.Party.assertFromString), - signatories = Set(Ref.Party.assertFromString("signatory")), - templateId = templateId, - argument = Value.ValueUnit, - createdAt = CreationTime.CreatedAt(Time.Timestamp.assertFromLong(17000000)), - keyOpt = Some( - GlobalKeyWithMaintainers.assertBuild( - templateId = templateId, - value = keyValue, - valueHash = crypto.Hash.hashPrivateKey(keyValue.toString), - maintainers = Set("signatory").map(Ref.Party.assertFromString), - packageName = ExampleContractFactory.packageName, - ) - ), - ) - val createNode = contract.inst.toCreateNode - - val targetSynchronizerId = Target(SynchronizerId.tryFromString("x::synchronizer2")) - val update = state.Update.SequencedReassignmentAccepted( - optCompletionInfo = Some(completionInfo), - workflowId = Some(someWorkflowId), - updateId = updateId, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = Source(SynchronizerId.tryFromString("x::synchronizer1")), - targetSynchronizer = targetSynchronizerId, - submitter = Option(someParty), - reassignmentId = ReassignmentId.tryCreate("001000000000"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Assign( - reassignmentCounter = 1500L, - nodeId = 0, - persistedContractInstance = PersistedContractInstance( - internalContractId = 42L, - inst = contract.inst, - ), - ) - ), - recordTime = someRecordTime, - synchronizerId = targetSynchronizerId.unwrap, - acsChangeFactory = TestAcsChangeFactory(), - ) - - val dtos = updateToDtos(update) - - dtos.head shouldEqual DbDto.EventActivate( - event_offset = someOffset.unwrap, - update_id = update.updateId.toProtoPrimitive.toByteArray, - workflow_id = Some(someWorkflowId), - command_id = Some(completionInfo.commandId), - submitters = Option(Set(someParty)), - record_time = someRecordTime.toMicros, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer2"), - trace_context = serializedEmptyTraceContext, - external_transaction_hash = None, - traffic_cost = someTrafficCost, - event_type = PersistentEventType.Assign.asInt, - event_sequential_id = 0, - node_id = 0, - additional_witnesses = None, - source_synchronizer_id = Some(SynchronizerId.tryFromString("x::synchronizer1")), - reassignment_counter = Some(1500L), - reassignment_id = Some(ReassignmentId.tryCreate("001000000000").toBytes.toByteArray), - representative_package_id = createNode.templateId.packageId, - notPersistedContractId = createNode.coid, - internal_contract_id = 42L, - create_key_hash = Some( - crypto.Hash.hashPrivateKey(keyValue.toString).bytes.toHexString - ), - ) - dtos(4) shouldEqual DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = someRecordTime.toMicros, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer2"), - message_uuid = None, - is_transaction = false, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - dtos(5) shouldEqual DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer2"), - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - Set(dtos(1), dtos(2), dtos(3)) should contain theSameElementsAs Set( - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer", - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterActivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer2", - first_per_sequential_id = false, - ) - ), - ) - dtos.size shouldEqual 6 - } - - "handle ReassignmentAccepted - Unassign" in { - val completionInfo = someCompletionInfo - val builder = TxBuilder() - val contractId = builder.newCid - val createNode = builder - .create( - id = contractId, - templateId = "M:T", - argument = Value.ValueUnit, - signatories = Set("signatory"), - observers = Set("observer"), - ) - - val sourceSynchronizerId = Source(SynchronizerId.tryFromString("x::synchronizer1")) - val update = state.Update.SequencedReassignmentAccepted( - optCompletionInfo = Some(completionInfo), - workflowId = Some(someWorkflowId), - updateId = updateId, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = sourceSynchronizerId, - targetSynchronizer = Target(SynchronizerId.tryFromString("x::synchronizer2")), - submitter = Option(someParty), - reassignmentId = ReassignmentId.tryCreate("001000000000"), - isReassigningParticipant = true, - ), - reassignment = Reassignment.Batch( - Reassignment.Unassign( - contractId = contractId, - templateId = createNode.templateId, - packageName = createNode.packageName, - stakeholders = - List("signatory12", "observer23", "asdasdasd").map(Ref.Party.assertFromString), - assignmentExclusivity = Some(Time.Timestamp.assertFromLong(123456)), - reassignmentCounter = 1500L, - nodeId = 0, - ) - ), - recordTime = CantonTimestamp.ofEpochMicro(120), - synchronizerId = sourceSynchronizerId.unwrap, - acsChangeFactory = TestAcsChangeFactory(), - ) - - val dtos = updateToDtos(update) - - dtos.head shouldEqual DbDto.EventDeactivate( - event_offset = someOffset.unwrap, - update_id = update.updateId.toProtoPrimitive.toByteArray, - workflow_id = Some(someWorkflowId), - command_id = Some(completionInfo.commandId), - submitters = Some(Set(someParty)), - record_time = 120L, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer1"), - trace_context = serializedEmptyTraceContext, - external_transaction_hash = None, - traffic_cost = someTrafficCost, - event_type = PersistentEventType.Unassign.asInt, - event_sequential_id = 0, - node_id = 0, - deactivated_event_sequential_id = None, - additional_witnesses = None, - exercise_choice = None, - exercise_choice_interface_id = None, - exercise_argument = None, - exercise_result = None, - exercise_actors = None, - exercise_last_descendant_node_id = None, - exercise_argument_compression = None, - exercise_result_compression = None, - reassignment_id = Some(ReassignmentId.tryCreate("001000000000").toBytes.toByteArray), - assignment_exclusivity = Some(123456L), - target_synchronizer_id = Some(SynchronizerId.tryFromString("x::synchronizer2")), - reassignment_counter = Some(1500L), - contract_id = createNode.coid, - internal_contract_id = None, - template_id = templateIdWithPackageName(createNode), - package_id = createNode.templateId.packageId, - stakeholders = Set("signatory12", "observer23", "asdasdasd"), - ledger_effective_time = None, - ) - dtos(4) shouldEqual DbDto.CommandCompletion( - completion_offset = someOffset.unwrap, - record_time = 120L, - publication_time = 0, - user_id = completionInfo.userId, - submitters = completionInfo.actAs.toSet, - command_id = completionInfo.commandId, - update_id = Some(updateIdByteArray), - rejection_status_code = None, - rejection_status_message = None, - rejection_status_details = None, - submission_id = completionInfo.submissionId, - deduplication_offset = None, - deduplication_duration_nanos = None, - deduplication_duration_seconds = None, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer1"), - message_uuid = None, - is_transaction = false, - trace_context = serializedEmptyTraceContext, - traffic_cost = completionInfo.paidTrafficCost.value, - ) - dtos(5) shouldEqual DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = 120L, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer1"), - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - Set(dtos(1), dtos(2), dtos(3)) should contain theSameElementsAs Set( - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "signatory12", - first_per_sequential_id = true, - ) - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "observer23", - first_per_sequential_id = false, - ) - ), - DbDto.IdFilterDeactivateStakeholder( - IdFilter( - 0L, - templateIdWithPackageName(createNode), - "asdasdasd", - first_per_sequential_id = false, - ) - ), - ) - dtos.size shouldEqual 6 - } - - "handle TopologyTransactionEffective - PartyToParticipantAuthorization" in { - val submissionParty = Ref.Party.assertFromString("SubmissionParty") - val confirmationParty = Ref.Party.assertFromString("ConfirmationParty") - val observationParty = Ref.Party.assertFromString("ObservationParty") - - val events = Set[TopologyEvent]( - PartyToParticipantAuthorization( - party = submissionParty, - participant = someParticipantId, - authorizationEvent = Added(Submission), - ), - PartyToParticipantAuthorization( - party = confirmationParty, - participant = someParticipantId, - authorizationEvent = Added(Confirmation), - ), - PartyToParticipantAuthorization( - party = observationParty, - participant = someParticipantId, - authorizationEvent = Added(Observation), - ), - PartyToParticipantAuthorization( - party = submissionParty, - participant = otherParticipantId, - authorizationEvent = ChangedTo(Submission), - ), - PartyToParticipantAuthorization( - party = confirmationParty, - participant = otherParticipantId, - authorizationEvent = ChangedTo(Confirmation), - ), - PartyToParticipantAuthorization( - party = observationParty, - participant = otherParticipantId, - authorizationEvent = ChangedTo(Observation), - ), - PartyToParticipantAuthorization( - party = someParty, - participant = someParticipantId, - authorizationEvent = Revoked, - ), - ) - - val update = state.Update.TopologyTransactionEffective( - updateId = updateId, - events = events, - synchronizerId = someSynchronizerId1, - effectiveTime = someRecordTime, - ) - - def eventPartyToParticipant( - partyId: String, - participantId: String, - authorizationEvent: AuthorizationEvent, - ) = - DbDto.EventPartyToParticipant( - event_sequential_id = 0, - event_offset = someOffset.unwrap, - update_id = update.updateId.toProtoPrimitive.toByteArray, - party_id = Ref.Party.assertFromString(partyId), - participant_id = Ref.ParticipantId.assertFromString(participantId), - participant_permission = participantPermissionInt(authorizationEvent), - participant_authorization_event = authorizationEventInt(authorizationEvent), - synchronizer_id = someSynchronizerId1, - record_time = someRecordTime.toMicros, - trace_context = serializedEmptyTraceContext, - ) - - val dtos = updateToDtos(update) - - dtos should contain( - eventPartyToParticipant( - partyId = submissionParty, - participantId = someParticipantId, - authorizationEvent = Added(Submission), - ) - ) - dtos should contain( - eventPartyToParticipant( - partyId = confirmationParty, - participantId = someParticipantId, - authorizationEvent = Added(Confirmation), - ) - ) - dtos should contain( - eventPartyToParticipant( - partyId = observationParty, - participantId = someParticipantId, - authorizationEvent = Added(Observation), - ) - ) - dtos should contain( - eventPartyToParticipant( - partyId = submissionParty, - participantId = otherParticipantId, - authorizationEvent = ChangedTo(Submission), - ) - ) - dtos should contain( - eventPartyToParticipant( - partyId = confirmationParty, - participantId = otherParticipantId, - authorizationEvent = ChangedTo(Confirmation), - ) - ) - dtos should contain( - eventPartyToParticipant( - partyId = observationParty, - participantId = otherParticipantId, - authorizationEvent = ChangedTo(Observation), - ) - ) - dtos should contain( - eventPartyToParticipant( - partyId = someParty, - participantId = someParticipantId, - authorizationEvent = Revoked, - ) - ) - dtos should contain( - DbDto.TransactionMeta( - update_id = updateIdByteArray, - event_offset = someOffset.unwrap, - publication_time = 0, - record_time = someRecordTime.toMicros, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer1"), - event_sequential_id_first = 0, - event_sequential_id_last = 0, - ) - ) - } - - "handle SequencerIndexMoved" in { - val update = state.Update.SequencerIndexMoved( - synchronizerId = someSynchronizerId1, - recordTime = CantonTimestamp.ofEpochMicro(2000), - ) - val dtos = updateToDtos(update) - - dtos.head shouldEqual DbDto.SequencerIndexMoved( - synchronizerId = someSynchronizerId1 - ) - dtos.size shouldEqual 1 - } - - } - - private def updateToDtos(update: Update) = - UpdateToDbDto( - someParticipantId, - valueSerialization, - compressionStrategy, - LedgerApiServerMetrics.ForTesting, - )( - MetricsContext.Empty - )( - someOffset - )(update).toList -} - -object UpdateToDbDtoSpec { - private val emptyArray = Array.emptyByteArray - - // These tests do not check the correctness of the LF value serialization. - // All LF values are serialized into empty arrays in this suite. - private val valueSerialization = new LfValueSerialization { - override def serialize( - contractId: ContractId, - contractArgument: Value.VersionedValue, - ): Array[Byte] = emptyArray - - /** Returns (contract argument, contract key) */ - override def serialize(create: Create): (Array[Byte], Option[Array[Byte]]) = - (emptyArray, create.keyOpt.map(_ => emptyArray)) - - /** Returns (choice argument, exercise result, contract key) */ - override def serialize( - exercise: Exercise - ): (Array[Byte], Option[Array[Byte]], Option[Array[Byte]]) = - ( - emptyArray, - exercise.exerciseResult.map(_ => emptyArray), - exercise.keyOpt.map(_ => emptyArray), - ) - } - - // These test do not check the correctness of compression. - // All values are compressed using a dummy (identity) algorithm in this suite. - private val compressionAlgorithmIdConsumingArg = Some(13) - private val compressionAlgorithmIdConsumingRes = Some(14) - private val compressionAlgorithmIdNonConsumingArg = Some(15) - private val compressionAlgorithmIdNonConsumingRes = Some(16) - private val compressionStrategy: CompressionStrategy = CompressionStrategy( - new FieldCompressionStrategy( - compressionAlgorithmIdConsumingArg, - compressArrayWith(compressionAlgorithmIdConsumingArg, _), - ), - new FieldCompressionStrategy( - compressionAlgorithmIdConsumingRes, - compressArrayWith(compressionAlgorithmIdConsumingRes, _), - ), - new FieldCompressionStrategy( - compressionAlgorithmIdNonConsumingArg, - compressArrayWith(compressionAlgorithmIdNonConsumingArg, _), - ), - new FieldCompressionStrategy( - compressionAlgorithmIdNonConsumingRes, - compressArrayWith(compressionAlgorithmIdNonConsumingRes, _), - ), - ) - - private def compressArrayWith(id: Option[Int], x: Array[Byte]) = - x ++ Array(id.getOrElse(-1).toByte) - - private val someParticipantId = - Ref.ParticipantId.assertFromString("UpdateToDbDtoSpecParticipant") - private val otherParticipantId = - Ref.ParticipantId.assertFromString("UpdateToDbDtoSpecRemoteParticipant") - private val someOffset = Offset.tryFromLong(12345678L) - private val nonNegativeTrafficCost: NonNegativeLong = NonNegativeLong.tryCreate(31380L) - private val someTrafficCost: Option[Long] = Some(nonNegativeTrafficCost.value) - private val someRecordTime = - CantonTimestamp( - Time.Timestamp.assertFromInstant(Instant.parse("2000-01-01T00:00:00.000000Z")) - ) - private val someUserId = - Ref.UserId.assertFromString("UpdateToDbDtoSpecUserId") - private val someCommandId = Ref.CommandId.assertFromString("UpdateToDbDtoSpecCommandId") - private val someSubmissionId = - Ref.SubmissionId.assertFromString("UpdateToDbDtoSpecSubmissionId") - private val someWorkflowId = Ref.WorkflowId.assertFromString("UpdateToDbDtoSpecWorkflowId") - private val someParty = Ref.Party.assertFromString("UpdateToDbDtoSpecParty") - private val someHash = - crypto.Hash.assertFromString("01cf85cfeb36d628ca2e6f583fa2331be029b6b28e877e1008fb3f862306c086") - private val someCompletionInfo = state.CompletionInfo( - actAs = List(someParty), - userId = someUserId, - commandId = someCommandId, - optDeduplicationPeriod = None, - submissionId = Some(someSubmissionId), - paidTrafficCost = nonNegativeTrafficCost, - ) - private val someSynchronizerId1 = SynchronizerId.tryFromString("x::synchronizer1") - val someTransactionMeta: TransactionMeta = state.TransactionMeta( - ledgerEffectiveTime = Time.Timestamp.assertFromLong(2), - workflowId = Some(someWorkflowId), - preparationTime = Time.Timestamp.assertFromLong(3), - submissionSeed = someHash, - timeBoundaries = LedgerTimeBoundaries.unconstrained, - optUsedPackages = None, - optNodeSeeds = None, - optByKeyNodes = None, - ) - private val someRepresentativePackageId = Ref.PackageId.assertFromString("rp-id") - private def someContractInfos( - contract: ContractInstance, - representativePackageId: RepresentativePackageId = DedicatedRepresentativePackageId( - someRepresentativePackageId - ), - ) = - ContractInfo( - persistedContractInstance = PersistedContractInstance( - internalContractId = 42L, - inst = contract.inst, - ), - representativePackageId = representativePackageId, - ) - - implicit private val DbDtoEqual: org.scalactic.Equality[DbDto] = ScalatestEqualityHelpers.DbDtoEq - - private val serializedEmptyTraceContext = - SerializableTraceContext(emptyTraceContext).toSerializedDamlProto -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/common/ComposableQuerySpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/common/ComposableQuerySpec.scala deleted file mode 100644 index f50288eb5b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/common/ComposableQuerySpec.scala +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.common - -import anorm.ParameterValue -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.QueryPart -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class ComposableQuerySpec extends AnyWordSpec with Matchers { - import ComposableQuerySpec.TestInterpolation - import ComposableQuery.SqlStringInterpolation - - "flattenComposite" should { - - "flatten correctly in a nested happy path case" in { - test"a ${10} b ${cSQL"${11L} ${cSQL"${"12"}"}"} c" shouldBe ( - ( - List("a ", " b ", " ", " c"), - List( - ParameterValue.from(10), - ParameterValue.from(11L), - ParameterValue.from("12"), - ), - ), - ) - } - - "flatten correctly with no interpolation" in { - test"ac" shouldBe ( - ( - List("ac"), - List(), - ), - ) - } - - "flatten correctly with empty string" in { - test"" shouldBe ( - ( - List(""), - List(), - ), - ) - } - - "flatten correctly with starting with a value" in { - test"${1}ac" shouldBe ( - ( - List("", "ac"), - List( - ParameterValue.from(1) - ), - ), - ) - } - - "flatten correctly with ending with a value" in { - test"ac${1}" shouldBe ( - ( - List("ac", ""), - List( - ParameterValue.from(1) - ), - ), - ) - } - - "flatten correctly with starting with a nested value which starts with value" in { - test"${cSQL"${1}b"}ac" shouldBe ( - ( - List("", "bac"), - List( - ParameterValue.from(1) - ), - ), - ) - } - - "flatten correctly with ending with a nested value which ends with value" in { - test"ac${cSQL"b${1}"}" shouldBe ( - ( - List("acb", ""), - List( - ParameterValue.from(1) - ), - ), - ) - } - - "flatten correctly with starting with a nested value which starts with string" in { - test"${cSQL"d${1}b"}ac" shouldBe ( - ( - List("d", "bac"), - List( - ParameterValue.from(1) - ), - ), - ) - } - - "flatten correctly with ending with a nested value which ends with string" in { - test"ac${cSQL"${1}b"}" shouldBe ( - ( - List("ac", "b"), - List( - ParameterValue.from(1) - ), - ), - ) - } - - "flatten correctly with a multiple nested case" in { - val simpleNested = cSQL"a ${1} b" - val twoSimpleNested = cSQL"c ${2} d ${3} e" - val twoOneLevelNested = cSQL"f $simpleNested g $simpleNested h" - val oneTwoLevelNested = cSQL"i $twoOneLevelNested j" - test"k $simpleNested l $twoSimpleNested m $twoOneLevelNested n $oneTwoLevelNested o" shouldBe ( - ( - List( - "k a ", - " b l c ", - " d ", - " e m f a ", - " b g a ", - " b h n i f a ", - " b g a ", - " b h j o", - ), - List( - ParameterValue.from(1), - ParameterValue.from(2), - ParameterValue.from(3), - ParameterValue.from(1), - ParameterValue.from(1), - ParameterValue.from(1), - ParameterValue.from(1), - ), - ), - ) - } - } - -} - -object ComposableQuerySpec { - - implicit class TestInterpolation(val sc: StringContext) extends AnyVal { - def test(args: QueryPart*): (Seq[String], Seq[ParameterValue]) = - ComposableQuery.flattenComposite(sc.parts, args) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/h2/H2DataSourceStorageBackendSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/h2/H2DataSourceStorageBackendSpec.scala deleted file mode 100644 index 3cc3842cb7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/backend/h2/H2DataSourceStorageBackendSpec.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.backend.h2 - -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class H2DataSourceStorageBackendSpec extends AnyWordSpec with Matchers { - - "H2StorageBackend" should { - "extractUserPasswordAndRemoveFromUrl" should { - - "strip user from url with user" in { - H2DataSourceStorageBackend.extractUserPasswordAndRemoveFromUrl( - "url;user=harry" - ) shouldBe (("url", Some("harry"), None)) - } - - "strip user from url with user in the middle" in { - H2DataSourceStorageBackend.extractUserPasswordAndRemoveFromUrl( - "url;user=harry;password=weak" - ) shouldBe (("url", Some("harry"), Some("weak"))) - } - - "only strip password if user absent" in { - H2DataSourceStorageBackend.extractUserPasswordAndRemoveFromUrl( - "url;password=weak" - ) shouldBe (("url", None, Some("weak"))) - } - - "not touch other properties" in { - H2DataSourceStorageBackend.extractUserPasswordAndRemoveFromUrl( - "url;alpha=1;beta=2;gamma=3" - ) shouldBe (("url;alpha=1;beta=2;gamma=3", None, None)) - } - - "match upper-case user and password keys" in { - H2DataSourceStorageBackend.extractUserPasswordAndRemoveFromUrl( - "url;USER=sally;PASSWORD=supersafe" - ) shouldBe (("url", Some("sally"), Some("supersafe"))) - } - - "match mixed-case user and password keys" in { - H2DataSourceStorageBackend.extractUserPasswordAndRemoveFromUrl( - "url;User=sally;Password=supersafe" - ) shouldBe (("url", Some("sally"), Some("supersafe"))) - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/AchsStateCacheSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/AchsStateCacheSpec.scala deleted file mode 100644 index 26de8aca1d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/AchsStateCacheSpec.scala +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.digitalasset.canton.logging.SuppressingLogger -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsLastPointers, - AchsState, -} -import com.digitalasset.canton.tracing.TraceContext -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class AchsStateCacheSpec extends AnyFlatSpec with Matchers { - - private val loggerFactory: SuppressingLogger = SuppressingLogger(getClass) - implicit val traceContext: TraceContext = TraceContext.empty - - behavior of "AchsStateCache" - - it should "start with empty state" in { - val cache = new AchsStateCache(loggerFactory) - cache.get() shouldBe AchsState( - validAt = 0, - lastPointers = AchsLastPointers(lastRemoved = 0, lastPopulated = 0), - ) - } - - it should "set and get state" in { - val cache = new AchsStateCache(loggerFactory) - val state = AchsState( - validAt = 10L, - lastPointers = AchsLastPointers(lastRemoved = 5L, lastPopulated = 3L), - ) - cache.set(state) - cache.get() shouldBe state - } - - it should "updateLastPointers" in { - val cache = new AchsStateCache(loggerFactory) - cache.set( - AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 10L, lastPopulated = 5L), - ) - ) - cache.updateLastPointers(AchsLastPointers(lastRemoved = 50L, lastPopulated = 30L)) - cache.get() shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 50L, lastPopulated = 30L), - ) - } - - it should "updateValidAt preserves lastRemoved and lastPopulated" in { - val cache = new AchsStateCache(loggerFactory) - cache.set( - AchsState( - validAt = 10L, - lastPointers = AchsLastPointers(lastRemoved = 5L, lastPopulated = 3L), - ) - ) - cache.updateValidAt(99L) - val state = cache.get() - state.lastPointers.lastRemoved shouldBe 5L - state.lastPointers.lastPopulated shouldBe 3L - } - - it should "handle sequence of updateValidAt and updateLastPointers" in { - val cache = new AchsStateCache(loggerFactory) - cache.updateValidAt(100L) - cache.get() shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 0L, lastPopulated = 0L), - ) - - cache.updateLastPointers(AchsLastPointers(lastRemoved = 60L, lastPopulated = 40L)) - cache.get() shouldBe AchsState( - validAt = 100L, - lastPointers = AchsLastPointers(lastRemoved = 60L, lastPopulated = 40L), - ) - - cache.updateValidAt(200L) - cache.get() shouldBe AchsState( - validAt = 200L, - lastPointers = AchsLastPointers(lastRemoved = 60L, lastPopulated = 40L), - ) - - cache.updateLastPointers(AchsLastPointers(lastRemoved = 160L, lastPopulated = 140L)) - cache.get() shouldBe AchsState( - validAt = 200L, - lastPointers = AchsLastPointers(lastRemoved = 160L, lastPopulated = 140L), - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/ContractStateCachesSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/ContractStateCachesSpec.scala deleted file mode 100644 index 54bd4c6c40..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/ContractStateCachesSpec.scala +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import cats.data.NonEmptyVector -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.dao.events.ContractStateEvent -import com.digitalasset.canton.{HasExecutionContext, TestEssentials} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.value.Value.ValueInt64 -import org.mockito.MockitoSugar -import org.scalatest.OptionValues -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.concurrent.atomic.AtomicLong - -class ContractStateCachesSpec - extends AnyFlatSpec - with Matchers - with MockitoSugar - with OptionValues - with TestEssentials - with HasExecutionContext { - behavior of classOf[ContractStateCaches].getSimpleName - - "build" should "set the cache index to the initialization index" in { - val cacheInitializationEventSeqId = 1337L - @SuppressWarnings(Array("com.digitalasset.canton.GlobalExecutionContext")) - val contractStateCaches = ContractStateCaches.build( - cacheInitializationEventSeqId, - maxContractsCacheSize = 1L, - maxKeyCacheSize = 1L, - metrics = LedgerApiServerMetrics.ForTesting, - loggerFactory, - ) - - contractStateCaches.keyState.cacheEventSeqIdIndex shouldBe cacheInitializationEventSeqId - contractStateCaches.contractState.cacheEventSeqIdIndex shouldBe cacheInitializationEventSeqId - } - - "push" should "update the caches with a batch of events" in new TestScope { - val previousCreate = createEvent(withKey = true) - - val create1 = createEvent(withKey = false) - val create2 = createEvent(withKey = true) - val archive1 = archiveEvent(create1) - val archivedPrevious = archiveEvent(previousCreate) - - val batch = NonEmptyVector.of(create1, create2, archive1, archivedPrevious) - - val expectedContractStateUpdates = Map( - create1.contractId -> ContractStateStatus.Archived, - create2.contractId -> ContractStateStatus.Active, - previousCreate.contractId -> ContractStateStatus.Archived, - ) - val expectedKeyStateUpdates = Map( - create2.globalKey.value -> keyAssigned(create2), - previousCreate.globalKey.value -> ContractKeyStateValue.Unassigned, - ) - - contractStateCaches.push(batch, 4) - verify(contractStateCache).putBatch(4, expectedContractStateUpdates) - verify(keyStateCache).putBatch(4, expectedKeyStateUpdates) - } - - "push" should "update the key state cache even if no key updates" in new TestScope { - val create1 = createEvent(withKey = false) - - val batch = NonEmptyVector.of(create1) - val expectedContractStateUpdates = Map(create1.contractId -> ContractStateStatus.Active) - - contractStateCaches.push(batch, 2) - verify(contractStateCache).putBatch(2, expectedContractStateUpdates) - verify(keyStateCache).putBatch(2, Map.empty) - } - - "reset" should "reset the caches on `reset`" in new TestScope { - private val someOffset = Some( - LedgerEnd( - lastOffset = Offset.tryFromLong(112243L), - lastEventSeqId = 125, - lastStringInterningId = 0, - lastPublicationTime = CantonTimestamp.MinValue, - ) - ) - - contractStateCaches.reset(someOffset) - verify(keyStateCache).reset(125) - verify(contractStateCache).reset(125) - } - - private trait TestScope { - private val contractIdx: AtomicLong = new AtomicLong(0) - private val keyIdx: AtomicLong = new AtomicLong(0) - - val keyStateCache: StateCache[Key, ContractKeyStateValue] = - mock[StateCache[Key, ContractKeyStateValue]] - val contractStateCache: StateCache[ContractId, ContractStateStatus] = - mock[StateCache[ContractId, ContractStateStatus]] - - val contractStateCaches = new ContractStateCaches( - keyStateCache, - contractStateCache, - loggerFactory, - ) - - def createEvent( - withKey: Boolean - ): ContractStateEvent.Created = { - val cId = contractId(contractIdx.incrementAndGet()) - val templateId = Identifier.assertFromString(s"some:template:name") - val packageName = Ref.PackageName.assertFromString("pkg-name") - val keyValue = keyIdx.incrementAndGet() - val key = Option.when(withKey)( - Key.assertBuild( - templateId, - packageName, - ValueInt64(keyValue), - crypto.Hash.hashPrivateKey(keyValue.toString), - ) - ) - ContractStateEvent.Created(cId, key) - } - - def archiveEvent( - create: ContractStateEvent.Created - ): ContractStateEvent.Archived = - ContractStateEvent.Archived( - contractId = create.contractId, - globalKey = create.globalKey, - ) - } - - private def keyAssigned(create: ContractStateEvent.Created) = - ContractKeyStateValue.Assigned(create.contractId) - - private def contractId(id: Long): ContractId = - ContractId.V1(Hash.hashPrivateKey(id.toString)) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/InMemoryFanoutBufferSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/InMemoryFanoutBufferSpec.scala deleted file mode 100644 index 7be2eb2b32..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/InMemoryFanoutBufferSpec.scala +++ /dev/null @@ -1,740 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.daml.ledger.api.v2.completion.Completion -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.participant.state.ReassignmentInfo -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer.BufferSlice.LastBufferChunkSuffix -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer.{ - BackwardBufferSlice, - BufferSlice, - UnorderedException, -} -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.protocol.{ReassignmentId, TestUpdateId, UpdateId} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.ReassignmentTag -import com.digitalasset.daml.lf.data.Time -import org.scalatest.Succeeded -import org.scalatest.compatible.Assertion -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import java.util.concurrent.Executors -import scala.collection.Searching.{Found, InsertionPoint} -import scala.collection.{View, immutable} -import scala.concurrent.duration.DurationInt -import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutorService, Future} - -class InMemoryFanoutBufferSpec - extends AnyWordSpec - with Matchers - with ScalaCheckDrivenPropertyChecks - with BaseTest { - private val offsetIdx = Vector(2, 4, 6, 8, 10) - private val firstOffset = offset(1L) - private val offsets = offsetIdx.map(i => offset(i.toLong)) - private val someSynchronizerId = SynchronizerId.tryFromString("some::synchronizer id") - - private val IdentityFilter: TransactionLogUpdate => Option[TransactionLogUpdate] = - tracedUpdate => Some(tracedUpdate) - - inside(offsets) { case Seq(offset1, offset2, offset3, offset4, offset5) => - val txAccepted1 = txAccepted(1L, offset1) - val reassignmentAccepted2 = reassignmentAccepted(2L, offset2) - val txAccepted3 = txAccepted(3L, offset3) - val topologyTxAccepted4 = topologyTxAccepted(4L, offset4) - val bufferValues = Seq(txAccepted1, reassignmentAccepted2, txAccepted3, topologyTxAccepted4) - val txAccepted5 = txAccepted(5L, offset5) - val bufferElements = offsets.zip(bufferValues) - val LastOffset = offset4 - - inside(bufferElements) { case Seq(entry1, entry2, entry3, entry4) => - "push" when { - "max buffer size reached" should { - "drop oldest" in withBuffer(3) { buffer => - // Assert data structure sizes - buffer._bufferLog.size shouldBe 3 - buffer._lookupMap.size shouldBe 3 - - buffer.slice(firstOffset, LastOffset, IdentityFilter) shouldBe LastBufferChunkSuffix( - bufferedStartExclusive = offset2, - slice = Vector(entry3, entry4), - ) - - // Assert that all the entries are visible by lookup - verifyLookupPresent(buffer, reassignmentAccepted2, txAccepted3, topologyTxAccepted4) - - buffer.push(txAccepted5) - // Assert data structure sizes respect their limits after pushing a new element - buffer._bufferLog.size shouldBe 3 - buffer._lookupMap.size shouldBe 3 - - buffer.slice(firstOffset, offset5, IdentityFilter) shouldBe LastBufferChunkSuffix( - bufferedStartExclusive = offset3, - slice = Vector(entry4, offset5 -> txAccepted5), - ) - - // Assert that the new entry is visible by lookup - verifyLookupPresent(buffer, txAccepted5) - // Assert oldest entry is evicted - verifyLookupAbsent(buffer, reassignmentAccepted2) - } - } - - "element with smaller offset added" should { - "throw" in withBuffer(3) { buffer => - intercept[UnorderedException[Int]] { - buffer.push(txAccepted1) - }.getMessage shouldBe s"Elements appended to the buffer should have strictly increasing offsets: $offset4 vs $offset1" - } - } - - "element with equal offset added" should { - "throw" in withBuffer(3) { buffer => - intercept[UnorderedException[Int]] { - buffer.push(topologyTxAccepted4) - }.getMessage shouldBe s"Elements appended to the buffer should have strictly increasing offsets: $offset4 vs $offset4" - } - } - - "maxBufferSize is 0" should { - "not enqueue the update" in withBuffer(0) { buffer => - buffer.push(txAccepted5) - buffer.slice(firstOffset, offset5, IdentityFilter) shouldBe LastBufferChunkSuffix( - bufferedStartExclusive = offset5, - slice = Vector.empty, - ) - buffer._bufferLog shouldBe empty - } - } - - "maxBufferSize is -1" should { - "not enqueue the update" in withBuffer(-1) { buffer => - buffer.push(txAccepted5) - buffer.slice(firstOffset, offset5, IdentityFilter) shouldBe LastBufferChunkSuffix( - bufferedStartExclusive = offset5, - slice = Vector.empty, - ) - buffer._bufferLog shouldBe empty - } - } - - s"does not update the lookupMap with ${TransactionLogUpdate.TransactionRejected.getClass.getSimpleName}" in withBuffer( - 4 - ) { buffer => - // Assert that all the entries are visible by lookup - verifyLookupPresent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - - // Enqueue a rejected transaction - buffer.push(txRejected(5L, offset5)) - - // Assert the last element is evicted on full buffer - verifyLookupAbsent(buffer, txAccepted1) - - // Assert that the buffer does not include the rejected transaction - buffer._lookupMap should contain theSameElementsAs Map( - reassignmentAccepted2.updateId -> reassignmentAccepted2, - txAccepted3.updateId -> txAccepted3, - topologyTxAccepted4.updateId -> topologyTxAccepted4, - ) - } - } - - "slice" when { - "filters" in withBuffer() { buffer => - buffer.slice(offset2, offset4, Some(_).filterNot(_ == entry3._2)) shouldBe BufferSlice - .Inclusive( - Vector(entry2, entry4) - ) - } - - "called with startInclusive gteq than the buffer start" should { - "return an Inclusive slice" in withBuffer() { buffer => - buffer.slice(offset1, succ(offset3), IdentityFilter) shouldBe BufferSlice.Inclusive( - Vector(entry1, entry2, entry3) - ) - buffer.slice(offset2, succ(offset3), IdentityFilter) shouldBe BufferSlice.Inclusive( - Vector(entry2, entry3) - ) - buffer.slice(offset2, offset4, IdentityFilter) shouldBe BufferSlice.Inclusive( - Vector(entry2, entry3, entry4) - ) - buffer.slice(succ(offset1), offset4, IdentityFilter) shouldBe BufferSlice.Inclusive( - Vector(entry2, entry3, entry4) - ) - } - - "return an Inclusive chunk result if resulting slice is bigger than maxFetchSize" in withBuffer( - maxFetchSize = 2 - ) { buffer => - buffer.slice(offset2, offset4, IdentityFilter) shouldBe BufferSlice.Inclusive( - Vector(entry2, entry3) - ) - } - } - - "called with endInclusive lteq startInclusive" should { - "return an empty Inclusive slice if startInclusive is greater than buffer start and endInclusive" in withBuffer() { - buffer => - buffer.slice(offset2, offset1, IdentityFilter) shouldBe BufferSlice.Inclusive( - Vector.empty - ) - } - "return an Inclusive slice if startInclusive is greater than buffer start and equal to endInclusive" in withBuffer() { - buffer => - buffer.slice(offset2, offset2, IdentityFilter) shouldBe BufferSlice.Inclusive( - Vector(entry2) - ) - } - "return an empty LastBufferChunkSuffix slice if startExclusive is before buffer start" in withBuffer( - maxBufferSize = 2 - ) { buffer => - buffer.slice(offset1, offset1, IdentityFilter) shouldBe LastBufferChunkSuffix( - offset1, - Vector.empty, - ) - buffer.slice(offset2, offset1, IdentityFilter) shouldBe LastBufferChunkSuffix( - offset1, - Vector.empty, - ) - } - } - "called with startInclusive before the buffer start" should { - "return a LastBufferChunkSuffix slice" in withBuffer() { buffer => - buffer.slice( - firstOffset, - offset3, - IdentityFilter, - ) shouldBe LastBufferChunkSuffix( - offset1, - Vector(entry2, entry3), - ) - buffer.slice( - firstOffset, - succ(offset3), - IdentityFilter, - ) shouldBe LastBufferChunkSuffix( - offset1, - Vector(entry2, entry3), - ) - } - - "return the last filtered chunk as LastBufferChunkSuffix slice if resulting slice is bigger than maxFetchSize" in withBuffer( - maxFetchSize = 2 - ) { buffer => - buffer.slice( - firstOffset, - offset4, - IdentityFilter, - ) shouldBe LastBufferChunkSuffix( - offset2, - Vector(entry3, entry4), - ) - } - } - - "called after push from a different thread" should { - "always see the most recent updates" in withBuffer( - 1000, - Vector.empty, - maxFetchSize = 1000, - ) { buffer => - (0 until 1000).foreach { idx => - val updateOffset = offset(idx.toLong) - buffer.push(txAccepted(idx.toLong, updateOffset)) - } // fill buffer to max size - - val pushExecutor, sliceExecutor = - ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(1)) - - (0 until 1000).foreach { idx => - val expected = ((idx + 901) to (1000 + idx)).map { idx => - val updateOffset = offset(idx.toLong) - updateOffset -> txAccepted(idx.toLong, updateOffset) - } - - implicit val ec: ExecutionContextExecutorService = pushExecutor - - Await.result( - // Simulate different thread accesses for push/slice - awaitable = { - val lastInsertedIdx = (1000 + idx).toLong - val updateOffset = offset(lastInsertedIdx) - for { - _ <- Future( - buffer.push(txAccepted(lastInsertedIdx, updateOffset)) - )( - pushExecutor - ) - _ <- Future( - buffer.slice( - offset((901 + idx).toLong), - offset(lastInsertedIdx), - IdentityFilter, - ) - )( - sliceExecutor - ) - .map(_.slice should contain theSameElementsInOrderAs expected)(sliceExecutor) - } yield Succeeded - }, - atMost = 1.seconds, - ) - } - Succeeded - } - } - } - - "sliceBackwards" when { - "called with startInclusive gteq than the buffer start" should { - "return an Final slice" in withBuffer() { buffer => - buffer.sliceBackwards( - offset1, - succ(offset3), - IdentityFilter, - ) shouldBe BackwardBufferSlice - .FinalSlice( - Vector(entry3, entry2, entry1) - ) - buffer.sliceBackwards( - offset2, - succ(offset3), - IdentityFilter, - ) shouldBe BackwardBufferSlice - .FinalSlice( - Vector(entry3, entry2) - ) - buffer.sliceBackwards(offset2, offset4, IdentityFilter) shouldBe BackwardBufferSlice - .FinalSlice( - Vector(entry4, entry3, entry2) - ) - buffer.sliceBackwards( - succ(offset1), - offset4, - IdentityFilter, - ) shouldBe BackwardBufferSlice - .FinalSlice( - Vector(entry4, entry3, entry2) - ) - } - - "return an PartialSlice chunk result if resulting slice is bigger than maxFetchSize" in withBuffer( - maxFetchSize = 2 - ) { buffer => - buffer.sliceBackwards(offset2, offset4, IdentityFilter) shouldBe BackwardBufferSlice - .PartialSlice( - Vector(entry4, entry3) - ) - } - } - - "called with endInclusive lteq startInclusive" should { - "return an empty FinalSlice if startInclusive is greater than buffer start and endInclusive" in withBuffer() { - buffer => - buffer.sliceBackwards(offset2, offset1, IdentityFilter) shouldBe BackwardBufferSlice - .FinalSlice( - Vector.empty - ) - } - "return an FinalSlice if startInclusive is greater than buffer start and equal to endInclusive" in withBuffer() { - buffer => - buffer.sliceBackwards(offset2, offset2, IdentityFilter) shouldBe BackwardBufferSlice - .FinalSlice( - Vector(entry2) - ) - } - "return an empty PartialSlice if startExclusive is before buffer start" in withBuffer( - maxBufferSize = 2 - ) { buffer => - buffer.sliceBackwards(offset1, offset1, IdentityFilter) shouldBe BackwardBufferSlice - .PartialSlice( - Vector.empty - ) - buffer.sliceBackwards(offset2, offset1, IdentityFilter) shouldBe BackwardBufferSlice - .PartialSlice( - Vector.empty - ) - } - } - "called with startInclusive before the buffer start" should { - "return a PartialSlice" in withBuffer() { buffer => - buffer.sliceBackwards( - firstOffset, - offset3, - IdentityFilter, - ) shouldBe BackwardBufferSlice.PartialSlice( - Vector(entry3, entry2, entry1) - ) - buffer.sliceBackwards( - firstOffset, - succ(offset3), - IdentityFilter, - ) shouldBe BackwardBufferSlice.PartialSlice( - Vector(entry3, entry2, entry1) - ) - } - - "return Inclusive slice if resulting slice is bigger than maxFetchSize" in withBuffer( - maxFetchSize = 2 - ) { buffer => - buffer.sliceBackwards( - firstOffset, - offset4, - IdentityFilter, - ) shouldBe BackwardBufferSlice.PartialSlice( - Vector(entry4, entry3) - ) - } - } - } - - "prune" when { - "element found" should { - "prune inclusive" in withBuffer() { buffer => - verifyLookupPresent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - - buffer.prune(offset3) - - buffer.slice(firstOffset, LastOffset, IdentityFilter) shouldBe LastBufferChunkSuffix( - offset4, - bufferElements.drop(4), - ) - - verifyLookupAbsent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - ) - verifyLookupPresent(buffer, topologyTxAccepted4) - } - } - - "element not present" should { - "prune inclusive" in withBuffer() { buffer => - verifyLookupPresent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - - buffer.prune(offset(6)) - buffer.slice(firstOffset, LastOffset, IdentityFilter) shouldBe LastBufferChunkSuffix( - offset4, - bufferElements.drop(4), - ) - verifyLookupAbsent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - ) - verifyLookupPresent(buffer, topologyTxAccepted4) - } - } - - "element before series" should { - "not prune" in withBuffer() { buffer => - verifyLookupPresent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - - buffer.prune(offset(1)) - buffer.slice(firstOffset, LastOffset, IdentityFilter) shouldBe LastBufferChunkSuffix( - offset1, - bufferElements.drop(1), - ) - - verifyLookupPresent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - } - } - - "element after series" should { - "prune all" in withBuffer() { buffer => - verifyLookupPresent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - - buffer.prune(offset5) - buffer.slice(firstOffset, LastOffset, IdentityFilter) shouldBe LastBufferChunkSuffix( - LastOffset, - Vector.empty, - ) - - verifyLookupAbsent( - buffer, - txAccepted1, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - } - } - - "one element in buffer" should { - "prune all" in withBuffer( - 1, - Vector(offset(1) -> reassignmentAccepted2.copy(offset = offset(1))), - ) { buffer => - verifyLookupPresent( - buffer, - reassignmentAccepted2.copy(offset = offset(1)), - ) - - buffer.prune(offset(1)) - buffer.slice(firstOffset, offset(1), IdentityFilter) shouldBe LastBufferChunkSuffix( - offset(1), - Vector.empty, - ) - - verifyLookupAbsent(buffer, reassignmentAccepted2) - } - } - } - - "flush" should { - "remove all entries from the buffer" in withBuffer(3) { buffer => - verifyLookupPresent( - buffer, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - - buffer.slice(firstOffset, LastOffset, IdentityFilter) shouldBe LastBufferChunkSuffix( - bufferedStartExclusive = offset2, - slice = Vector(entry3, entry4), - ) - - buffer.flush() - - buffer._bufferLog shouldBe empty - buffer._lookupMap shouldBe empty - buffer.slice(firstOffset, LastOffset, IdentityFilter) shouldBe LastBufferChunkSuffix( - bufferedStartExclusive = LastOffset, - slice = Vector.empty, - ) - verifyLookupAbsent( - buffer, - reassignmentAccepted2, - txAccepted3, - topologyTxAccepted4, - ) - } - } - - "indexAfter" should { - "yield the index gt the searched entry" in { - InMemoryFanoutBuffer.indexAfter(InsertionPoint(3)) shouldBe 3 - InMemoryFanoutBuffer.indexAfter(Found(3)) shouldBe 4 - } - } - - "filterAndChunkSlice" should { - "return an Inclusive result with filter" in { - val input = Vector(entry1, entry2, entry3, entry4).view - - InMemoryFanoutBuffer.filterAndChunkSlice[TransactionLogUpdate]( - sliceView = input, - filter = tracedUpdate => Option(tracedUpdate).filterNot(_ == entry2._2), - maxChunkSize = 3, - ) shouldBe Vector(entry1, entry3, entry4) - - InMemoryFanoutBuffer.filterAndChunkSlice[TransactionLogUpdate]( - sliceView = View.empty, - filter = tu => Some(tu), - maxChunkSize = 3, - ) shouldBe Vector.empty - } - } - - "lastFilteredChunk" should { - val input = Vector(entry1, entry2, entry3, entry4) - - "return a LastBufferChunkSuffix with the last maxChunkSize-sized chunk from the slice with filter" in { - InMemoryFanoutBuffer.lastFilteredChunk[TransactionLogUpdate]( - bufferSlice = input, - filter = tu => Option(tu).filterNot(_ == entry2._2), - maxChunkSize = 1, - ) shouldBe LastBufferChunkSuffix(entry3._1, Vector(entry4)) - - InMemoryFanoutBuffer.lastFilteredChunk[TransactionLogUpdate]( - bufferSlice = input, - filter = tu => Option(tu).filterNot(_ == entry2._2), - maxChunkSize = 2, - ) shouldBe LastBufferChunkSuffix(entry1._1, Vector(entry3, entry4)) - - InMemoryFanoutBuffer.lastFilteredChunk[TransactionLogUpdate]( - bufferSlice = input, - filter = tu => Option(tu).filterNot(_ == entry2._2), - maxChunkSize = 3, - ) shouldBe LastBufferChunkSuffix(entry1._1, Vector(entry3, entry4)) - - InMemoryFanoutBuffer.lastFilteredChunk[TransactionLogUpdate]( - bufferSlice = input, - filter = tu => Some(tu), // No filter - maxChunkSize = 4, - ) shouldBe LastBufferChunkSuffix(entry1._1, Vector(entry2, entry3, entry4)) - } - - "use the slice head as bufferedStartExclusive when filter yields an empty result slice" in { - InMemoryFanoutBuffer.lastFilteredChunk[TransactionLogUpdate]( - bufferSlice = input, - filter = _ => None, - maxChunkSize = 2, - ) shouldBe LastBufferChunkSuffix(entry1._1, Vector.empty) - } - } - } - - def withBuffer( - maxBufferSize: Int = 5, - elems: immutable.Vector[(Offset, TransactionLogUpdate)] = bufferElements, - maxFetchSize: Int = 10, - )(test: InMemoryFanoutBuffer => Assertion): Assertion = { - val buffer = new InMemoryFanoutBuffer( - maxBufferSize, - LedgerApiServerMetrics.ForTesting, - maxBufferedChunkSize = maxFetchSize, - loggerFactory = loggerFactory, - ) - elems.foreach { case (_, event) => buffer.push(event) } - test(buffer) - } - } - - private def offset(idx: Long): Offset = { - val base = 1000000000L - Offset.tryFromLong(base + idx) - } - - private def succ(offset: Offset): Offset = offset.increment - - private def txAccepted(idx: Long, offset: Offset) = - TransactionLogUpdate.TransactionAccepted( - updateId = TestUpdateId(s"tx-$idx").toHexString, - workflowId = s"workflow-$idx", - effectiveAt = Time.Timestamp.Epoch, - offset = offset, - events = Vector.empty, - completionStreamResponseO = None, - commandId = "", - synchronizerId = someSynchronizerId.toProtoPrimitive, - recordTime = Time.Timestamp.Epoch, - externalTransactionHash = None, - ) - - private def txRejected(idx: Long, offset: Offset) = - TransactionLogUpdate.TransactionRejected( - offset = offset, - completionStreamResponse = CompletionStreamResponse.defaultInstance.withCompletion( - Completion.defaultInstance.copy( - actAs = Seq(s"submitter-$idx") - ) - ), - ) - - private def reassignmentAccepted(idx: Long, offset: Offset) = - TransactionLogUpdate.ReassignmentAccepted( - updateId = TestUpdateId(s"reassignment-$idx").toHexString, - workflowId = s"workflow-$idx", - offset = offset, - completionStreamResponseO = None, - commandId = "", - recordTime = Time.Timestamp.Epoch, - reassignmentInfo = ReassignmentInfo( - sourceSynchronizer = ReassignmentTag.Source(someSynchronizerId), - targetSynchronizer = ReassignmentTag.Target(someSynchronizerId), - submitter = None, - reassignmentId = ReassignmentId.tryCreate("0001"), - isReassigningParticipant = false, - ), - reassignment = null, - synchronizerId = someSynchronizerId.toProtoPrimitive, - ) - - private def topologyTxAccepted(idx: Long, offset: Offset) = - TransactionLogUpdate.TopologyTransactionEffective( - updateId = TestUpdateId(s"topology-tx-$idx").toHexString, - offset = offset, - effectiveTime = Time.Timestamp.Epoch, - synchronizerId = someSynchronizerId.toProtoPrimitive, - events = Vector.empty, - ) - - private def verifyLookupPresent( - buffer: InMemoryFanoutBuffer, - txs: TransactionLogUpdate* - ): Assertion = - txs.foldLeft(succeed) { - case (Succeeded, tx) => - buffer.lookup( - LookupKey.ByUpdateId(getUpdateId(tx)) - ) shouldBe Some(tx) - buffer.lookup(LookupKey.ByOffset(tx.offset)) shouldBe Some(tx) - case (failed, _) => failed - } - - private def verifyLookupAbsent( - buffer: InMemoryFanoutBuffer, - txs: TransactionLogUpdate* - ): Assertion = - txs.foldLeft(succeed) { - case (Succeeded, tx) => - buffer.lookup( - LookupKey.ByUpdateId(getUpdateId(tx)) - ) shouldBe None - buffer.lookup(LookupKey.ByOffset(tx.offset)) shouldBe None - case (failed, _) => failed - } - - private def getUpdateId(tx: TransactionLogUpdate): UpdateId = { - val updateStr = tx match { - case txAccepted: TransactionLogUpdate.TransactionAccepted => txAccepted.updateId - case _: TransactionLogUpdate.TransactionRejected => - throw new RuntimeException("did not expect a TransactionRejected") - case reassignment: TransactionLogUpdate.ReassignmentAccepted => reassignment.updateId - case topologyTransaction: TransactionLogUpdate.TopologyTransactionEffective => - topologyTransaction.updateId - } - UpdateId.fromLedgerString(updateStr).valueOrFail("invalid update id") - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStoreRaceTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStoreRaceTests.scala deleted file mode 100644 index 87651e8c9b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStoreRaceTests.scala +++ /dev/null @@ -1,529 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import cats.data.NonEmptyVector -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.TestEssentials -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.{ - Active, - Archived, - ExistingContractStatus, -} -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.participant.store.memory.InMemoryContractStore -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.store.cache.MutableCacheBackedContractStoreRaceTests.{ - IndexViewContractsReader, - assert_sync_vs_async_race_contract, - assert_sync_vs_async_race_key, - buildContractStore, - generateWorkload, - test, -} -import com.digitalasset.canton.platform.store.dao.events.ContractStateEvent -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader.* -import com.digitalasset.canton.platform.store.{LedgerApiContractStore, LedgerApiContractStoreImpl} -import com.digitalasset.canton.protocol.{ - ContractInstance, - ExampleContractFactory, - ExampleTransactionFactory, -} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.transaction.GlobalKey -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.ValueInt64 -import org.apache.pekko.Done -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.Source -import org.scalatest.Assertions.fail -import org.scalatest.concurrent.ScalaFutures.convertScalaFuture -import org.scalatest.flatspec.AsyncFlatSpec - -import java.util.concurrent.Executors -import scala.annotation.tailrec -import scala.collection.immutable.{TreeMap, VectorMap} -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Random - -class MutableCacheBackedContractStoreRaceTests - extends AsyncFlatSpec - with PekkoBeforeAndAfterAll - with TestEssentials { - behavior of "Mutable state cache updates" - - private val unboundedExecutionContext = - ExecutionContext.fromExecutor(Executors.newCachedThreadPool()) - - it should "preserve causal monotonicity under contention for key state" in { - val workload = generateWorkload(keysCount = 10L, contractsCount = 1000L) - val indexViewContractsReader = IndexViewContractsReader()(unboundedExecutionContext) - val inMemoryContractStore = new InMemoryContractStore( - timeouts = timeouts, - loggerFactory = loggerFactory, - )(unboundedExecutionContext) - val participantContractStore = LedgerApiContractStoreImpl( - inMemoryContractStore, - loggerFactory, - LedgerApiServerMetrics.ForTesting, - ) - val contractStore = - buildContractStore( - indexViewContractsReader, - unboundedExecutionContext, - loggerFactory, - participantContractStore, - ) - - for { - _ <- test( - indexViewContractsReader, - participantContractStore, - workload, - unboundedExecutionContext, - ) { ec => event => - assert_sync_vs_async_race_key(contractStore)(event)(ec) - } - } yield succeed - } - - it should "preserve causal monotonicity under contention for contract state" in { - val workload = generateWorkload(keysCount = 10L, contractsCount = 1000L) - val indexViewContractsReader = IndexViewContractsReader()(unboundedExecutionContext) - val inMemoryContractStore = new InMemoryContractStore( - timeouts = timeouts, - loggerFactory = loggerFactory, - )(unboundedExecutionContext) - val participantContractStore = LedgerApiContractStoreImpl( - inMemoryContractStore, - loggerFactory, - LedgerApiServerMetrics.ForTesting, - ) - val contractStore = - buildContractStore( - indexViewContractsReader, - unboundedExecutionContext, - loggerFactory, - participantContractStore, - ) - - for { - _ <- test( - indexViewContractsReader, - participantContractStore, - workload, - unboundedExecutionContext, - ) { ec => event => - assert_sync_vs_async_race_contract(contractStore)(event)(ec) - } - } yield succeed - } -} - -private object MutableCacheBackedContractStoreRaceTests { - private implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace.ForTesting - private val stakeholders = Set(Ref.Party.assertFromString("some-stakeholder")) - - private def test( - indexViewContractsReader: IndexViewContractsReader, - participantContractStore: LedgerApiContractStoreImpl, - workload: Seq[Long => SimplifiedContractStateEvent], - unboundedExecutionContext: ExecutionContext, - )( - assert: ExecutionContext => SimplifiedContractStateEvent => Future[Unit] - )(implicit materializer: Materializer): Future[Done] = - Source - .fromIterator(() => workload.iterator) - .statefulMapConcat { () => - var counter = 0L - - eventCtor => { - counter += 1 - Iterator(eventCtor(counter)) - } - } - .map { event => - indexViewContractsReader.update(event) - update(participantContractStore, event)(unboundedExecutionContext).futureValue - event - } - .mapAsync(1)( - // Validate the view's contents (test sanity-check) - assertIndexState(indexViewContractsReader, _)(unboundedExecutionContext) - ) - .mapAsync(1)(assert(unboundedExecutionContext)) - .run() - - private def assert_sync_vs_async_race_key( - contractStore: MutableCacheBackedContractStore - )(event: SimplifiedContractStateEvent)(implicit ec: ExecutionContext): Future[Unit] = { - val contractStateEvent = toContractStateEvent(event) - - // Start async key lookup - // Use Future.delegate here to ensure immediate control handover to the next statement - val keyLookupF = Future.delegate(contractStore.lookupContractKey(stakeholders, event.key)) - // Update the mutable contract state cache synchronously - contractStore.contractStateCaches.push(NonEmptyVector.of(contractStateEvent), event.eventSeqId) - - for { - // Lookup after synchronous update - firstAsyncLookupResult <- contractStore.lookupContractKey(stakeholders, event.key) - _ <- keyLookupF - // Lookup after asynchronous update - secondAsyncLookupResult <- contractStore.lookupContractKey(stakeholders, event.key) - } yield { - assertKeyAssignmentAfterAppliedEvent(firstAsyncLookupResult)(event) - assertKeyAssignmentAfterAppliedEvent(secondAsyncLookupResult)(event) - } - } - - private def assert_sync_vs_async_race_contract( - contractStore: MutableCacheBackedContractStore - )(event: SimplifiedContractStateEvent)(implicit ec: ExecutionContext): Future[Unit] = { - val contractStateEvent = toContractStateEvent(event) - - // Start async contract lookup - // Use Future.delegate here to ensure immediate control handover to the next statement - val keyLookupF = - Future.delegate(contractStore.lookupActiveContract(stakeholders, event.contractId)) - // Update the mutable contract state cache synchronously - contractStore.contractStateCaches.push(NonEmptyVector.of(contractStateEvent), event.eventSeqId) - - for { - // Lookup after synchronous update - firstAsyncLookupResult <- contractStore.lookupActiveContract(stakeholders, event.contractId) - _ <- keyLookupF - // Lookup after asynchronous update - secondAsyncLookupResult <- contractStore.lookupActiveContract(stakeholders, event.contractId) - } yield { - assertContractIdAssignmentAfterAppliedEvent(firstAsyncLookupResult)(event) - assertContractIdAssignmentAfterAppliedEvent(secondAsyncLookupResult)(event) - } - } - - private def assertKeyAssignmentAfterAppliedEvent( - assignment: Option[ContractId] - )(event: SimplifiedContractStateEvent): Unit = - assignment match { - case Some(contractId) if (event.contractId != contractId) || !event.created => - fail(message = - s"Key state corruption for ${event.key}: " + - s"expected ${if (event.created) s"assignment to ${event.contractId} -> ${event.contract}" - else "unassigned"}, " + - s"but got assignment to $contractId" - ) - case None if event.created => - fail(message = - s"Key state corruption for ${event.key}: expected assignment to ${event.contractId} -> ${event.contract} " + - "but got unassigned instead" - ) - case _ => () - } - - private def assertContractIdAssignmentAfterAppliedEvent( - assignment: Option[FatContract] - )(event: SimplifiedContractStateEvent): Unit = - assignment match { - case Some(actualContract) if (event.contract != actualContract) || !event.created => - fail(message = - s"Contract state corruption for ${event.contractId}: " + - s"expected ${if (event.created) s"active contract (${event.contract})" - else "non-active contract"}, but got assignment to $actualContract" - ) - case None if event.created => - fail(message = - s"Contract state corruption for ${event.contractId}: expected active contract ${event.contract} " + - "but got non-active contract" - ) - case _ => () - } - - private def assertIndexState( - indexViewContractsReader: IndexViewContractsReader, - event: SimplifiedContractStateEvent, - )(implicit ec: ExecutionContext) = - for { - _ <- indexViewContractsReader - .lookupKeyState(event.key, event.eventSeqId) - .map { - case KeyAssigned(contractId) if contractId == event.contractId && event.created => - case KeyUnassigned if !event.created => - case actual => - fail( - s"Test bug: actual $actual after event $event: index view: ${indexViewContractsReader.keyStateStore - .get(event.key)}" - ) - } - _ <- indexViewContractsReader - .lookupContractState(event.contractId, event.eventSeqId) - .map { - case Some(Active) if event.created => - case Some(Archived) if !event.created => - case actual => - fail( - s"Test bug: actual $actual after event $event: index view: ${indexViewContractsReader.contractStateStore - .get(event.contractId)}" - ) - } - } yield event - - private def generateWorkload( - keysCount: Long, - contractsCount: Long, - ): Seq[Long => SimplifiedContractStateEvent] = { - val keys = (0L until keysCount).map { keyIdx => - keyIdx -> Key.assertBuild( - Identifier.assertFromString("pkgId:module:entity"), - Ref.PackageName.assertFromString("pkg-name"), - ValueInt64(keyIdx), - crypto.Hash.hashPrivateKey(keyIdx.toString), - ) - }.toMap - - val keysToContracts = keys.map { case (keyIdx, key) => - val contractLifecyclesForKey = contractsCount / keysCount - key -> (0L until contractLifecyclesForKey) - .map { contractIdx => - val globalContractIdx = keyIdx * contractLifecyclesForKey + contractIdx - val contractRef = contract(globalContractIdx, key) - (contractRef.contractId, contractRef) - } - .foldLeft(VectorMap.empty[ContractId, FatContract]) { case (r, (k, v)) => - r.updated(k, v) - } - } - - val updates = - keysToContracts.map { case (key, contracts) => - contracts.flatMap { case (_, contractRef) => - Vector( - (eventSeqId: Long) => - SimplifiedContractStateEvent( - eventSeqId = eventSeqId, - contract = contractRef, - created = true, - key = key, - ), - (eventSeqId: Long) => - SimplifiedContractStateEvent( - eventSeqId = eventSeqId, - contract = contractRef, - created = false, - key = key, - ), - ) - } - } - - interleaveRandom(updates) - } - - private def interleaveRandom( - indexContractsUpdates: Iterable[Iterable[Long => SimplifiedContractStateEvent]] - ): Seq[Long => SimplifiedContractStateEvent] = { - @tailrec - def interleaveIteratorsRandom[T](acc: Vector[T], col: Set[Iterator[T]]): Vector[T] = - if (col.isEmpty) acc - else { - val vCol = col.toVector - val randomIteratorIndex = Random.nextInt(vCol.size) - val targetIterator = vCol(randomIteratorIndex) - if (targetIterator.hasNext) interleaveIteratorsRandom(acc :+ targetIterator.next(), col) - else interleaveIteratorsRandom(acc, col - targetIterator) - } - - interleaveIteratorsRandom( - Vector.empty[Long => SimplifiedContractStateEvent], - indexContractsUpdates.map(_.iterator).toSet, - ) - } - - final case class SimplifiedContractStateEvent( - eventSeqId: Long, - contract: FatContract, - created: Boolean, - key: Key, - ) { - val contractId: ContractId = contract.contractId - } - - private def contract(idx: Long, key: GlobalKey): FatContract = { - val templateId = Identifier.assertFromString("pkgId:module:entity") - val packageName = Ref.PackageName.assertFromString("pkg-name") - val contractArgument = Value.ValueInt64(idx) - ExampleContractFactory - .build( - packageName = packageName, - templateId = templateId, - argument = contractArgument, - signatories = stakeholders, - stakeholders = stakeholders, - keyOpt = Some(KeyWithMaintainers(key, Set.empty)), - overrideContractId = Some(ExampleTransactionFactory.suffixedId(idx.toInt, 0)), - ) - .inst - } - - private def buildContractStore( - indexViewContractsReader: IndexViewContractsReader, - ec: ExecutionContext, - loggerFactory: NamedLoggerFactory, - participantContractStore: LedgerApiContractStore, - ) = { - val metrics = LedgerApiServerMetrics.ForTesting - new MutableCacheBackedContractStore( - contractsReader = indexViewContractsReader, - contractStateCaches = ContractStateCaches.build( - initialCacheEventSeqIdIndex = 0L, - maxContractsCacheSize = 1L, - maxKeyCacheSize = 1L, - metrics = metrics, - loggerFactory = loggerFactory, - )(ec), - contractStore = participantContractStore, - ledgerEndCache = MutableLedgerEndCache(), - loggerFactory = loggerFactory, - maxLookupLimit = 10, - )(ec) - } - - private val toContractStateEvent: SimplifiedContractStateEvent => ContractStateEvent = { - case SimplifiedContractStateEvent(_eventSeqId, contract, created, key) => - if (created) - ContractStateEvent.Created(contract.contractId, Some(key)) - else - ContractStateEvent.Archived(contract.contractId, Some(key)) - } - - final case class ContractLifecycle( - contract: FatContract, - createdAt: Long, - archivedAt: Option[Long], - ) - - // Simplified view of the index which models the evolution of the key and contracts state - private final case class IndexViewContractsReader()(implicit ec: ExecutionContext) - extends LedgerDaoContractsReader { - private type CreatedAt = Long - @volatile private[cache] var contractStateStore = Map.empty[ContractId, ContractLifecycle] - @volatile private[cache] var keyStateStore = Map.empty[Key, TreeMap[CreatedAt, ContractId]] - - // Evolves the index state - // Non-thread safe - def update(event: SimplifiedContractStateEvent): Unit = - if (event.created) { - // On create - contractStateStore = contractStateStore.updatedWith(event.contractId) { - case None => - Some( - ContractLifecycle( - contract = event.contract, - createdAt = event.eventSeqId, - archivedAt = None, - ) - ) - case lastState @ Some(_) => - fail(s"Contract state update conflict: last state $lastState vs even $event") - } - - keyStateStore = keyStateStore.updatedWith(event.key) { - case None => Some(TreeMap(event.eventSeqId -> event.contractId)) - case Some(assignments) => - val (lastContractAssignedAt, currentContractId) = assignments.last - val lastContract = contractStateStore(currentContractId) - val createdAt = event.eventSeqId - if (lastContractAssignedAt < createdAt && lastContract.archivedAt.exists(_ < createdAt)) - Some(assignments + (createdAt -> event.contractId)) - else fail(s"Key state update conflict: last state $lastContract vs event $event") - } - } else { - // On archive - contractStateStore = contractStateStore.updatedWith(event.contractId) { - case Some(contractLifecycle @ ContractLifecycle(contract, createdAt, None)) - if event.eventSeqId > createdAt && event.contractId == contract.contractId => - Some(contractLifecycle.copy(archivedAt = Some(event.eventSeqId))) - case lastState => - fail(s"Contract state update conflict: last state $lastState vs even $event") - } - - keyStateStore = keyStateStore.updatedWith(event.key) { - case Some(assignments) => - val (currentCreatedAt, currentContractId) = assignments.last - val lastContractAssignment = contractStateStore(currentContractId) - val archivedAt = event.eventSeqId - if (currentCreatedAt < archivedAt && lastContractAssignment.archivedAt.nonEmpty) - Some(assignments + (archivedAt -> event.contractId)) - else - fail(s"Key state update conflict: last state $lastContractAssignment vs event $event") - case faultyState => - fail(s"Key state update conflict: $faultyState vs event $event") - } - } - - override def lookupContractState(contractId: ContractId, notEarlierThanEventSeqId: Long)( - implicit loggingContext: LoggingContextWithTrace - ): Future[Option[ExistingContractStatus]] = - Future { - val _ = loggingContext - contractStateStore - .get(contractId) - .flatMap { case ContractLifecycle(_, createdAt, maybeArchivedAt) => - if (notEarlierThanEventSeqId < createdAt) None - else if (maybeArchivedAt.forall(_ > notEarlierThanEventSeqId)) - Some(ContractStateStatus.Active) - else Some(ContractStateStatus.Archived) - } - }(ec) - - override def lookupKeyState(key: Key, notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[KeyState] = Future { - val _ = loggingContext - keyStateStore - .get(key) - .map(_.maxBefore(notEarlierThanEventSeqId + 1) match { - case Some((_, contractId)) => - contractStateStore(contractId).archivedAt match { - case Some(archivedAt) if archivedAt <= notEarlierThanEventSeqId => KeyUnassigned - case _ => KeyAssigned(contractId) - } - case None => KeyUnassigned - }) - .getOrElse(KeyUnassigned) - }(ec) - - override def lookupKeyStatesFromDb(keys: Seq[Key], notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Map[Key, Long]] = ??? // not used in this test - - override def lookupNonUniqueKey( - key: Key, - notEarlierThanEventSeqId: CreatedAt, - nextPageToken: Option[CreatedAt], - limit: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[(Vector[ContractId], Option[Long])] = - ??? // not used in this test - } - - def update(contractStore: LedgerApiContractStoreImpl, event: SimplifiedContractStateEvent)( - implicit ec: ExecutionContext - ): Future[Unit] = - if (event.created) { - val contract = - ContractInstance - .create(event.contract) - .getOrElse(fail(s"Failed creating contract ${event.contract.contractId}")) - contractStore.participantContractStore - .storeContracts(Seq(contract)) - .failOnShutdownToAbortException("update") - .map(_ => ()) - } else Future.unit -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStoreSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStoreSpec.scala deleted file mode 100644 index 6e364000af..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/MutableCacheBackedContractStoreSpec.scala +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import cats.data.NonEmptyVector -import com.daml.ledger.resources.Resource -import com.digitalasset.canton.config.ProcessingTimeout -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.ExistingContractStatus -import com.digitalasset.canton.ledger.participant.state.index.{ContractState, ContractStateStatus} -import com.digitalasset.canton.logging.{ - LoggingContextWithTrace, - NamedLoggerFactory, - SuppressionRule, -} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.participant.store.memory.InMemoryContractStore -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.store.cache.MutableCacheBackedContractStoreSpec.* -import com.digitalasset.canton.platform.store.dao.events.ContractStateEvent -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader.{ - KeyAssigned, - KeyUnassigned, -} -import com.digitalasset.canton.platform.store.{LedgerApiContractStore, LedgerApiContractStoreImpl} -import com.digitalasset.canton.protocol.ExampleContractFactory -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.{HasExecutionContext, TestEssentials} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.{Ref, Time} -import com.digitalasset.daml.lf.transaction.CreationTime -import com.digitalasset.daml.lf.value.Value.{ContractId, ValueText} -import org.mockito.MockitoSugar -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec -import org.slf4j.event.Level - -import scala.annotation.nowarn -import scala.concurrent.{ExecutionContext, Future} - -class MutableCacheBackedContractStoreSpec - extends AsyncWordSpec - with Matchers - with MockitoSugar - with TestEssentials - with HasExecutionContext { - - implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace.ForTesting - - "push" should { - "update the contract state caches" in { - val contractStateCaches = mock[ContractStateCaches] - val contractStore = new MutableCacheBackedContractStore( - contractsReader = mock[LedgerDaoContractsReader], - contractStateCaches = contractStateCaches, - loggerFactory = loggerFactory, - contractStore = mock[LedgerApiContractStore], - ledgerEndCache = MutableLedgerEndCache(), - maxLookupLimit = 10, - ) - - val event1 = ContractStateEvent.Archived( - contractId = ContractId.V1(Hash.hashPrivateKey("cid")), - globalKey = None, - ) - val event2 = event1 - val updateBatch = NonEmptyVector.of(event1, event2) - - contractStore.contractStateCaches.push(updateBatch, 10) - verify(contractStateCaches).push(updateBatch, 10) - - succeed - } - } - - "lookupNonUniqueContractKey" should { - "cap the limit and log when the requested limit exceeds the configured max" in { - val maxLimit = 5 - val requestedLimit = 20 - val contractsReader = mock[LedgerDaoContractsReader] - val mockContractStore = mock[LedgerApiContractStore] - val ledgerEndCache = MutableLedgerEndCache() - - when( - contractsReader.lookupNonUniqueKey(any[Key], any[Long], any[Option[Long]], any[Int])( - any[LoggingContextWithTrace] - ) - ).thenReturn( - Future.successful( - (Vector.empty[ContractId], Option.empty[Long]) - ) - ) - - when( - mockContractStore.lookupBatchedContractIdsNonReadThrough(any[Vector[Long]])( - any[TraceContext] - ) - ) - .thenReturn(Future.successful(Map.empty[Long, ContractId])) - - val store = new MutableCacheBackedContractStore( - contractsReader = contractsReader, - contractStateCaches = mock[ContractStateCaches], - loggerFactory = loggerFactory, - contractStore = mockContractStore, - ledgerEndCache = ledgerEndCache, - maxLookupLimit = maxLimit, - ) - - loggerFactory.assertLogs(SuppressionRule.Level(Level.INFO))( - store - .lookupNonUniqueContractKey( - readers = Set(party("alice")), - key = globalKey("some-key"), - pageToken = None, - limit = requestedLimit, - ) - .futureValue, - _.infoMessage should include( - s"Lookup limit $requestedLimit exceeds configured cap of $maxLimit" - ), - ) - - // Verify the capped limit was passed to the reader - verify(contractsReader).lookupNonUniqueKey( - any[Key], - any[Long], - eqTo(None), - eqTo(maxLimit), - )(any[LoggingContextWithTrace]) - - succeed - } - - } - - "lookupActiveContract" should { - "read-through the contract state cache" in { - val spyContractsReader = spy(ContractsReaderFixture()) - - for { - store <- contractStore( - cachesSize = 1L, - loggerFactory = loggerFactory, - spyContractsReader, - ).asFuture - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId1 - cId2_lookup <- store.lookupActiveContract(Set(charlie), cId_2) - another_cId2_lookup <- store.lookupActiveContract(Set(charlie), cId_2) - - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId2 - cId3_lookup <- store.lookupActiveContract(Set(bob), cId_3) - another_cId3_lookup <- store.lookupActiveContract(Set(bob), cId_3) - - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId3 - nonExistentCId = cId_5 - nonExistentCId_lookup <- store.lookupActiveContract(Set.empty, nonExistentCId) - another_nonExistentCId_lookup <- store.lookupActiveContract(Set.empty, nonExistentCId) - } yield { - cId2_lookup shouldBe Option.empty - another_cId2_lookup shouldBe Option.empty - - cId3_lookup.map(_.templateId) shouldBe Some(contract3.inst.templateId) - another_cId3_lookup.map(_.templateId) shouldBe Some(contract3.inst.templateId) - - nonExistentCId_lookup shouldBe Option.empty - another_nonExistentCId_lookup shouldBe Option.empty - - // The cache is evicted BOTH on the number of entries AND memory pressure - // So even though a read-through populates missing entries, - // they can be immediately evicted by GCs and lead to subsequent misses. - // Hence, verify atLeastOnce for LedgerDaoContractsReader.lookupContractState - verify(spyContractsReader, atLeastOnce).lookupContractState(cId_2, eventSeqId1) - verify(spyContractsReader, atLeastOnce).lookupContractState(cId_3, eventSeqId2) - verify(spyContractsReader, atLeastOnce).lookupContractState(nonExistentCId, eventSeqId3) - succeed - } - } - - "read-through the cache without storing negative lookups" in { - val spyContractsReader = spy(ContractsReaderFixture()) - for { - store <- contractStore( - cachesSize = 1L, - loggerFactory = loggerFactory, - spyContractsReader, - ).asFuture - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId1 - negativeLookup_cId6 <- store.lookupActiveContract(Set(alice), cId_6) - positiveLookup_cId6 <- store.lookupActiveContract(Set(alice), cId_6) - } yield { - negativeLookup_cId6 shouldBe Option.empty - positiveLookup_cId6 shouldBe Option.empty - - verify(spyContractsReader, times(wantedNumberOfInvocations = 1)) - .lookupContractState(cId_6, eventSeqId1) - succeed - } - } - - "present the contract state if visible at specific cache offsets (with no cache)" in { - for { - store <- contractStore(cachesSize = 0L, loggerFactory).asFuture - cId1_lookup0 <- store.lookupActiveContract(Set(alice), cId_1) - cId2_lookup0 <- store.lookupActiveContract(Set(bob), cId_2) - - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId1 - cId1_lookup1 <- store.lookupActiveContract(Set(alice), cId_1) - cid1_lookup1_archivalNotDivulged <- store.lookupActiveContract(Set(charlie), cId_1) - - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId2 - cId2_lookup2 <- store.lookupActiveContract(Set(bob), cId_2) - cid2_lookup2_divulged <- store.lookupActiveContract(Set(charlie), cId_2) - cid2_lookup2_nonVisible <- store.lookupActiveContract(Set(charlie), cId_2) - } yield { - cId1_lookup0.map(_.templateId) shouldBe Some(contract1.inst.templateId) - cId2_lookup0 shouldBe Option.empty - - cId1_lookup1 shouldBe Option.empty - cid1_lookup1_archivalNotDivulged shouldBe None - - cId2_lookup2.map(_.templateId) shouldBe Some(contract2.inst.templateId) - cid2_lookup2_divulged shouldBe None - cid2_lookup2_nonVisible shouldBe Option.empty - } - } - } - - "lookupContractKey" should { - "read-through the key state cache" in { - val spyContractsReader = spy(ContractsReaderFixture()) - val unassignedKey = globalKey("unassigned") - - for { - store <- contractStore( - cachesSize = 1L, - loggerFactory = loggerFactory, - spyContractsReader, - ).asFuture - assigned_firstLookup <- store.lookupContractKey(Set(alice), someKey) - assigned_secondLookup <- store.lookupContractKey(Set(alice), someKey) - - _ = store.contractStateCaches.keyState.cacheEventSeqIdIndex = eventSeqId1 - unassigned_firstLookup <- store.lookupContractKey(Set(alice), unassignedKey) - unassigned_secondLookup <- store.lookupContractKey(Set(alice), unassignedKey) - } yield { - assigned_firstLookup shouldBe Some(cId_1) - assigned_secondLookup shouldBe Some(cId_1) - - unassigned_firstLookup shouldBe Option.empty - unassigned_secondLookup shouldBe Option.empty - - verify(spyContractsReader).lookupKeyState(someKey, eventSeqId0)(loggingContext) - // looking up the key state will prefetch and use the contract state - verify(spyContractsReader).lookupContractState(cId_1, eventSeqId0)(loggingContext) - verify(spyContractsReader).lookupContractState(cId_1, eventSeqId0)(loggingContext) - verify(spyContractsReader).lookupKeyState(unassignedKey, eventSeqId1)(loggingContext) - verifyNoMoreInteractions(spyContractsReader) - succeed - } - } - - "present the key state if visible at specific cache offsets (with no cache)" in { - for { - store <- contractStore(cachesSize = 0L, loggerFactory).asFuture - key_lookup0 <- store.lookupContractKey(Set(alice), someKey) - - _ = store.contractStateCaches.keyState.cacheEventSeqIdIndex = eventSeqId1 - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId1 - key_lookup1 <- store.lookupContractKey(Set(alice), someKey) - - _ = store.contractStateCaches.keyState.cacheEventSeqIdIndex = eventSeqId2 - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId2 - key_lookup2 <- store.lookupContractKey(Set(bob), someKey) - key_lookup2_notVisible <- store.lookupContractKey(Set(charlie), someKey) - - _ = store.contractStateCaches.keyState.cacheEventSeqIdIndex = eventSeqId3 - _ = store.contractStateCaches.contractState.cacheEventSeqIdIndex = eventSeqId3 - key_lookup3 <- store.lookupContractKey(Set(bob), someKey) - } yield { - key_lookup0 shouldBe Some(cId_1) - key_lookup1 shouldBe Option.empty - key_lookup2 shouldBe Some(cId_2) - key_lookup2_notVisible shouldBe Option.empty - key_lookup3 shouldBe Option.empty - } - } - } - - "lookupContractStateWithoutDivulgence" should { - - "resolve lookup from cache" in { - for { - store <- contractStore(cachesSize = 2L, loggerFactory).asFuture - _ = store.contractStateCaches.contractState.putBatch( - eventSeqId2, - Map( - // Populate the cache with an active contract - cId_4 -> ContractStateStatus.Active, - // Populate the cache with an archived contract - cId_5 -> ContractStateStatus.Archived, - ), - ) - activeContractLookupResult <- store.lookupContractState(cId_4) - archivedContractLookupResult <- store.lookupContractState(cId_5) - nonExistentContractLookupResult <- store.lookupContractState(cId_7) - } yield { - activeContractLookupResult shouldBe ContractState.Active(contract4.inst) - archivedContractLookupResult shouldBe ContractState.Archived - nonExistentContractLookupResult shouldBe ContractState.NotFound - } - } - - "resolve lookup from the ContractsReader when not cached" in { - for { - store <- contractStore(cachesSize = 0L, loggerFactory).asFuture - activeContractLookupResult <- store.lookupContractState(cId_4) - archivedContractLookupResult <- store.lookupContractState(cId_5) - nonExistentContractLookupResult <- store.lookupContractState(cId_7) - } yield { - activeContractLookupResult shouldBe ContractState.Active(contract4.inst) - archivedContractLookupResult shouldBe ContractState.Archived - nonExistentContractLookupResult shouldBe ContractState.NotFound - } - } - } -} - -@nowarn("msg=match may not be exhaustive") -object MutableCacheBackedContractStoreSpec { - private val eventSeqId0 = 1L - private val eventSeqId1 = 2L - private val eventSeqId2 = 3L - private val eventSeqId3 = 4L - - private val Seq(alice, bob, charlie) = Seq("alice", "bob", "charlie").map(party) - - private val someKey = globalKey("key1") - - private val exStakeholders = Set(bob, alice) - private val exSignatories = Set(alice) - private val exMaintainers = Set(alice) - private val someKeyWithMaintainers = KeyWithMaintainers(someKey, exMaintainers) - - private val timeouts = ProcessingTimeout() - - private val Seq(t1, t2, t3, t4, t5, t6, t7) = (1 to 7).map { id => - Time.Timestamp.assertFromLong(id.toLong * 1000L) - } - - private val ( - Seq(cId_1, cId_2, cId_3, cId_4, cId_5, cId_6, cId_7), - Seq(contract1, contract2, contract3, contract4, _, contract6, _), - ) = - Seq( - contract(Set(alice), t1), - contract(exStakeholders, t2), - contract(exStakeholders, t3), - contract(exStakeholders, t4), - contract(exStakeholders, t5), - contract(Set(alice), t6), - contract(exStakeholders, t7), - ).map(c => c.contractId -> c).unzip - - private def contractStore( - cachesSize: Long, - loggerFactory: NamedLoggerFactory, - readerFixture: LedgerDaoContractsReader = ContractsReaderFixture(), - )(implicit ec: ExecutionContext, traceContext: TraceContext) = { - val metrics = LedgerApiServerMetrics.ForTesting - val startIndexExclusive = eventSeqId0 - val contractStore = new MutableCacheBackedContractStore( - readerFixture, - contractStateCaches = ContractStateCaches - .build(startIndexExclusive, cachesSize, cachesSize, metrics, loggerFactory), - loggerFactory = loggerFactory, - contractStore = inMemoryContractStore(loggerFactory), - ledgerEndCache = MutableLedgerEndCache(), - maxLookupLimit = 10, - ) - - Resource.successful(contractStore) - } - - @SuppressWarnings(Array("org.wartremover.warts.FinalCaseClass")) // This class is spied in tests - case class ContractsReaderFixture() extends LedgerDaoContractsReader { - @volatile private var initialResultForCid6 = - Future.successful(Option.empty[ExistingContractStatus]) - - override def lookupKeyState(key: Key, notEarlierThanEventSeqId: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[LedgerDaoContractsReader.KeyState] = (key, notEarlierThanEventSeqId) match { - case (`someKey`, `eventSeqId0`) => Future.successful(KeyAssigned(cId_1)) - case (`someKey`, `eventSeqId2`) => Future.successful(KeyAssigned(cId_2)) - case _ => Future.successful(KeyUnassigned) - } - - override def lookupKeyStatesFromDb(keys: Seq[Key], notEarlierThanOffset: Long)(implicit - loggingContext: LoggingContextWithTrace - ): Future[Map[Key, Long]] = ??? // not used in this test - - override def lookupContractState(contractId: ContractId, notEarlierThanEventSeqId: Long)( - implicit loggingContext: LoggingContextWithTrace - ): Future[Option[ExistingContractStatus]] = - (contractId, notEarlierThanEventSeqId) match { - case (`cId_1`, `eventSeqId0`) => activeContract - case (`cId_1`, validAt) if validAt > eventSeqId0 => archivedContract - case (`cId_2`, validAt) if validAt >= eventSeqId1 => - activeContract - case (`cId_3`, _) => activeContract - case (`cId_4`, _) => activeContract - case (`cId_5`, _) => archivedContract - case (`cId_6`, _) => - // Simulate store being populated from one query to another - val result = initialResultForCid6 - initialResultForCid6 = activeContract - result - case _ => Future.successful(Option.empty) - } - - override def lookupNonUniqueKey( - key: Key, - notEarlierThanEventSeqId: Long, - nextPageToken: Option[Long], - limit: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[(Vector[ContractId], Option[Long])] = - ??? // not used in this test - } - - def inMemoryContractStore( - loggerFactory: NamedLoggerFactory - )(implicit - executionContext: ExecutionContext, - traceContext: TraceContext, - ): LedgerApiContractStore = { - val store = new InMemoryContractStore(timeouts, loggerFactory) - val contracts = Seq( - contract1, - contract2, - contract3, - contract4, - contract6, - ) - store.storeContracts(contracts).discard - LedgerApiContractStoreImpl(store, loggerFactory, LedgerApiServerMetrics.ForTesting) - } - - private def contract( - stakeholders: Set[Party], - ledgerEffectiveTime: Time.Timestamp, - key: Option[KeyWithMaintainers] = Some(someKeyWithMaintainers), - ) = - ExampleContractFactory.build( - createdAt = CreationTime.CreatedAt(ledgerEffectiveTime), - signatories = exSignatories, - stakeholders = stakeholders, - keyOpt = key, - ) - - private val activeContract: Future[Option[ExistingContractStatus]] = - Future.successful(Some(ContractStateStatus.Active)) - - private val archivedContract: Future[Option[ExistingContractStatus]] = - Future.successful(Some(ContractStateStatus.Archived)) - - private def party(name: String): Party = Party.assertFromString(name) - - private def globalKey(desc: String): Key = - Key.assertBuild( - Identifier.assertFromString("some:template:name"), - Ref.PackageName.assertFromString("pkg-name"), - ValueText(desc), - crypto.Hash.hashPrivateKey(desc), - ) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/StateCacheSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/StateCacheSpec.scala deleted file mode 100644 index 8a41237173..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/cache/StateCacheSpec.scala +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.cache - -import com.daml.metrics.CacheMetrics -import com.daml.metrics.api.noop.{NoOpMetricsFactory, NoOpTimer} -import com.daml.metrics.api.{MetricInfo, MetricName, MetricQualification} -import com.digitalasset.canton.caching.{CaffeineCache, ConcurrentCache, SizedCache} -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import com.github.benmanes.caffeine.cache.Caffeine -import org.mockito.MockitoSugar -import org.scalatest.Assertion -import org.scalatest.concurrent.Eventually -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.concurrent.TimeUnit -import scala.concurrent.duration.{FiniteDuration, *} -import scala.concurrent.{Future, Promise} -import scala.util.Success - -class StateCacheSpec - extends AsyncFlatSpec - with Matchers - with MockitoSugar - with Eventually - with BaseTest - with HasExecutionContext { - - private val className = classOf[StateCache[?, ?]].getSimpleName - - private val cacheUpdateTimer = NoOpTimer( - MetricInfo(MetricName("state_update"), "", MetricQualification.Debug) - ) - - behavior of s"$className.putAsync" - - it should "asynchronously store the update" in { - val cache = mock[ConcurrentCache[String, String]] - val someEventSeqId = 0L - val stateCache = StateCache[String, String]( - initialCacheEventSeqIdIndex = someEventSeqId, - emptyLedgerState = "", - cache = cache, - registerUpdateTimer = cacheUpdateTimer, - loggerFactory = loggerFactory, - ) - - val asyncUpdatePromise = Promise[String]() - val putAsyncResult = - stateCache.putAsync( - "key", - { - case `someEventSeqId` => asyncUpdatePromise.future - case _ => fail() - }, - ) - asyncUpdatePromise.completeWith(Future.successful("value")) - - for { - _ <- putAsyncResult - } yield { - verify(cache).put("key", "value") - // Async update should not insert in the cache - verifyNoMoreInteractions(cache) - succeed - } - } - - it should "store the latest key update in face of conflicting pending updates" in { - val `number of competing updates` = 100L - val `number of keys in cache` = 100L - - val stateCache = buildStateCache(`number of keys in cache`) - - val insertions = prepare(`number of competing updates`, `number of keys in cache`) - - val (insertionFutures, insertionDuration) = insertTimed(stateCache)(insertions) - - insertionDuration should be < 1.second - - insertions.foreach { case (_, (promise, value)) => promise.complete(Success(value)) } - - for { - result <- Future.sequence(insertionFutures.toVector) - } yield { - result should not be empty - assertCacheElements(stateCache)(insertions, `number of competing updates`) - } - } - - it should "putAsync 50_000 values for the same key in 1 second" in { - - val `number of competing updates` = 50000L - val `number of keys in cache` = 1L - - // if this test runs in CI, we might struggle due to noisy neighbors. as we can't really control - // the environment in every case, we just retry the test a few times. - // if the algorithm has been made slow, it will never succeed, so the test will fail - eventually { - val stateCache = buildStateCache(`number of keys in cache`) - - val insertions = prepare(`number of competing updates`, `number of keys in cache`) - - val (insertionFutures, insertionDuration) = insertTimed(stateCache)(insertions) - - insertionDuration should be < 1.second - - insertions.foreach { case (_, (promise, value)) => promise.complete(Success(value)) } - - for { - result <- Future.sequence(insertionFutures.toVector) - } yield { - result should not be empty - assertCacheElements(stateCache)(insertions, `number of competing updates`) - } - } - } - - behavior of s"$className.put" - - it should "synchronously update the cache in front of older asynchronous updates" in { - val cache = mock[ConcurrentCache[String, String]] - val initialEventSeqId = 0L - val stateCache = StateCache[String, String]( - initialCacheEventSeqIdIndex = initialEventSeqId, - emptyLedgerState = "", - cache = cache, - registerUpdateTimer = cacheUpdateTimer, - loggerFactory = loggerFactory, - ) - - val asyncUpdatePromise = Promise[String]() - val putAsyncResult = - stateCache.putAsync( - "key", - { - case `initialEventSeqId` => asyncUpdatePromise.future - case _ => fail() - }, - ) - stateCache.putBatch( - 2L, - Map("key" -> "value", "key2" -> "value2"), - ) - asyncUpdatePromise.completeWith(Future.successful("should not update the cache")) - - for { - _ <- putAsyncResult - } yield { - verify(cache).putAll(Map("key" -> "value", "key2" -> "value2")) - // Async update with older `validAt` should not insert in the cache - verifyNoMoreInteractions(cache) - succeed - } - } - - it should "not update the cache if called with a non-increasing `validAt`" in { - val cache = mock[ConcurrentCache[String, String]] - val stateCache = StateCache[String, String](0L, "", cache, cacheUpdateTimer, loggerFactory) - - stateCache.putBatch(2L, Map("key" -> "value")) - loggerFactory.assertLogs( - within = { - // `Put` at a decreasing validAt - stateCache.putBatch(1L, Map("key" -> "earlier value")) - stateCache - .putBatch(2L, Map("key" -> "value at same validAt")) - }, - assertions = _.warningMessage should include( - "Ignoring incoming synchronous update at an index at event sequential ID(1) equal to or before the cache index (2)" - ), - _.warningMessage should include( - "Ignoring incoming synchronous update at an index at event sequential ID(2) equal to or before the cache index (2)" - ), - ) - - verify(cache).putAll(Map("key" -> "value")) - verifyNoMoreInteractions(cache) - succeed - } - - behavior of s"$className.reset" - - it should "correctly reset the state cache" in { - val stateCache = - new StateCache[String, String]( - initialCacheEventSeqIdIndex = 1L, - emptyLedgerState = "", - cache = SizedCache.from( - SizedCache.Configuration(2), - new CacheMetrics( - new MetricName(Vector("test")), - NoOpMetricsFactory, - ), - ), - registerUpdateTimer = cacheUpdateTimer, - loggerFactory = loggerFactory, - ) - - val syncUpdateKey = "key" - val asyncUpdateKey = "other_key" - - // Add eagerly an entry into the cache - stateCache.putBatch( - 2L, - Map(syncUpdateKey -> "some initial value"), - ) - stateCache.get(syncUpdateKey) shouldBe Some("some initial value") - - // Register async update to the cache - val asyncUpdatePromise = Promise[String]() - val putAsyncF = - loggerFactory.assertLogs( - within = stateCache.putAsync( - asyncUpdateKey, - Map(2L -> asyncUpdatePromise.future), - ), - assertions = _.warningMessage should include( - "Pending updates tracker for other_key not registered. This could be due to a transient error causing a restart in the index service." - ), - ) - - // Reset the cache - stateCache.reset(1L) - // Complete async update - asyncUpdatePromise.completeWith(Future.successful("some value")) - - // Assert the cache is empty after completion of the async update - putAsyncF.map { _ => - stateCache.cacheEventSeqIdIndex shouldBe 1L - stateCache.get(syncUpdateKey) shouldBe None - stateCache.get(asyncUpdateKey) shouldBe None - } - } - - private def buildStateCache(cacheSize: Long): StateCache[String, String] = - StateCache[String, String]( - initialCacheEventSeqIdIndex = 0L, - emptyLedgerState = "", - cache = CaffeineCache[String, String]( - Caffeine - .newBuilder() - .maximumSize(cacheSize), - None, - ), - registerUpdateTimer = cacheUpdateTimer, - loggerFactory = loggerFactory, - ) - - private def prepare( - `number of competing updates`: Long, - `number of keys in cache`: Long, - ): Seq[(String, (Promise[String], String))] = - for { - i <- 1L to `number of keys in cache` - j <- 1L to `number of competing updates` - } yield (s"key-$i", (Promise[String](), s"value-$j")) - - private def assertCacheElements(stateCache: StateCache[String, String])( - insertions: Seq[(String, (Promise[String], String))], - numberOfCompetingUpdates: Long, - ): Assertion = { - insertions - .map(_._1) - .toSet - .foreach((key: String) => - stateCache - .get(key) - .getOrElse(s"Missing $key") shouldBe s"value-$numberOfCompetingUpdates" - ) - stateCache.pendingUpdates shouldBe empty - } - - private def insertTimed(stateCache: StateCache[String, String])( - insertions: Seq[(String, (Promise[String], String))] - ): (Seq[Future[Unit]], FiniteDuration) = - time { - var cacheIdx = 0L - insertions.map { case (key, (promise, _)) => - cacheIdx += 1L - val validAt = cacheIdx - stateCache.cacheEventSeqIdIndex = validAt - stateCache - .putAsync( - key, - { - case `validAt` => promise.future - case incorrect => fail(s"expected $validAt but was $incorrect") - }, - ) - .map(_ => ()) - } - } - - private def time[T](f: => T): (T, FiniteDuration) = { - val start = System.nanoTime() - val r = f - val duration = FiniteDuration((System.nanoTime() - start) / 1000000L, TimeUnit.MILLISECONDS) - (r, duration) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/BufferedStreamsReaderSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/BufferedStreamsReaderSpec.scala deleted file mode 100644 index 7af81f5673..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/BufferedStreamsReaderSpec.scala +++ /dev/null @@ -1,640 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer -import com.digitalasset.canton.platform.store.dao.BufferedStreamsReader.FetchFromPersistence -import com.digitalasset.canton.platform.store.dao.BufferedStreamsReaderSpec.* -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.protocol.TestUpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.{BaseTest, HasExecutionContext, HasExecutorServiceGeneric} -import com.digitalasset.daml.lf.data.Time.Timestamp -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.apache.pekko.{Done, NotUsed} -import org.scalatest.Assertion -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import scala.annotation.nowarn -import scala.collection.mutable.ArrayBuffer -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.chaining.* - -class BufferedStreamsReaderSpec - extends AnyWordSpec - with Matchers - with PekkoBeforeAndAfterAll - with TestFixtures - with HasExecutionContext { - - "stream (static)" when { - "buffer filter" should { - "return filtered elements (Inclusive slice)" in new StaticTestScope { - run( - transactionsBuffer = inMemoryFanoutBuffer, - startInclusive = offset2, - endInclusive = offset3, - bufferSliceFilter = noFilterBufferSlice(_).filterNot( - _.updateId == TestUpdateId("tx-3").toHexString - ), - descendingOrder = false, - ) - streamElements should contain theSameElementsInOrderAs Seq( - offset2 -> TestUpdateId("tx-2").toHexString - ) - } - } - - "request within buffer range inclusive" should { - "fetch from buffer" in new StaticTestScope { - run( - transactionsBuffer = inMemoryFanoutBuffer, - startInclusive = offset2, - endInclusive = offset3, - descendingOrder = false, - ) - streamElements should contain theSameElementsInOrderAs Seq( - offset2 -> TestUpdateId("tx-2").toHexString, - offset3 -> TestUpdateId("tx-3").toHexString, - ) - } - } - - "request within buffer range inclusive (multiple chunks)" should { - "correctly fetch from buffer" in new StaticTestScope { - run( - transactionsBuffer = inMemoryFanoutBufferWithSmallChunkSize, - startInclusive = offset2, - endInclusive = offset3, - descendingOrder = false, - ) - - streamElements should contain theSameElementsInOrderAs Seq( - offset2 -> TestUpdateId("tx-2").toHexString, - offset3 -> TestUpdateId("tx-3").toHexString, - ) - } - } - - "request before buffer start" should { - "fetch from buffer and storage" in new StaticTestScope { - val filterMock = new Object - - val anotherResponseForOffset1 = "(1) Response fetched from storage" - val anotherResponseForOffset2 = "(2) Response fetched from storage" - - val fetchFromPersistence = buildFetchFromPersistence( - expectedStartInclusive = offset0, - expectedEndInclusive = offset2, - expectedDescendingOrder = false, - expectedFilter = `filterMock`, - thenReturnStream = Source( - Seq(offset1 -> anotherResponseForOffset1, offset2 -> anotherResponseForOffset2) - ), - ) - - run( - transactionsBuffer = inMemoryFanoutBufferWithSmallChunkSize, - startInclusive = offset0, - endInclusive = offset3, - descendingOrder = false, - fetchFromPersistence = fetchFromPersistence, - persistenceFetchArgs = filterMock, - bufferSliceFilter = noFilterBufferSlice, - ) - - streamElements should contain theSameElementsInOrderAs Seq( - offset1 -> anotherResponseForOffset1, - offset2 -> anotherResponseForOffset2, - offset3 -> TestUpdateId("tx-3").toHexString, - ) - } - - "fetch from buffer and storage chunked with buffer filter" in new StaticTestScope { - val filterMock = new Object - - val anotherResponseForOffset1 = "(1) Response fetched from storage" - - val fetchFromPersistence = buildFetchFromPersistence( - expectedStartInclusive = offset0, - expectedEndInclusive = offset1, - expectedDescendingOrder = false, - expectedFilter = `filterMock`, - thenReturnStream = Source(Seq(offset1 -> anotherResponseForOffset1)), - ) - - run( - startInclusive = offset0, - endInclusive = offset3, - descendingOrder = false, - fetchFromPersistence = fetchFromPersistence, - persistenceFetchArgs = filterMock, - bufferSliceFilter = noFilterBufferSlice(_).filterNot( - _.updateId == TestUpdateId("tx-3").toHexString - ), - ) - - streamElements should contain theSameElementsInOrderAs Seq( - offset1 -> anotherResponseForOffset1, - offset2 -> TestUpdateId("tx-2").toHexString, - ) - } - } - - "request before buffer bounds" should { - "fetch only from storage" in new StaticTestScope { - val filterMock = new Object - - val fetchedElements = Vector( - offset1 -> "Some API response from persistence", - offset2 -> "Another API response from persistence", - ) - - val fetchFromPersistence = buildFetchFromPersistence( - expectedStartInclusive = offset1, - expectedEndInclusive = offset2, - expectedDescendingOrder = false, - expectedFilter = `filterMock`, - thenReturnStream = Source(fetchedElements), - ) - - run( - transactionsBuffer = smallInMemoryFanoutBuffer, - startInclusive = offset1, - endInclusive = offset2, - descendingOrder = false, - fetchFromPersistence = fetchFromPersistence, - persistenceFetchArgs = filterMock, - ) - - streamElements should contain theSameElementsInOrderAs fetchedElements - } - } - } - - "stream (dynamic)" when { - "catching up from buffer begin (exclusive)" should { - "return the correct ranges" in new DynamicTestScope() { - runF( - for { - // Prepopulate stores - _ <- updateStores(maxBufferSize) - // Stream from the beginning and assert - _ <- stream(1, maxBufferSize, descendingOrder = false) - } yield succeed - ) - } - } - - "catching up from buffer (inclusive)" should { - "return the correct ranges" in new DynamicTestScope() { - runF( - for { - // Prepopulate stores - _ <- updateStores(maxBufferSize) - // Stream from the middle and assert - _ <- stream(maxBufferSize / 2 + 1, maxBufferSize, descendingOrder = false) - } yield succeed - ) - } - } - - def testConsumerFallingBehind( - bufferSize: Int, - bufferChunkSize: Int, - consumerSubscriptionFrom: Int, - updateAgainWithCount: Int, - ) = new DynamicTestScope(maxBufferSize = bufferSize, maxBufferChunkSize = bufferChunkSize) { - runF( - for { - // Prepopulate stores - _ <- updateStores(count = bufferSize) - // Start stream subscription - (assertFirst1000, unblockConsumer) = streamWithHandle( - startInclusiveIdx = consumerSubscriptionFrom, - endInclusiveIdx = bufferSize, - descendingOrder = false, - ) - // Feed the buffer and effectively force the consumer to fall behind - _ <- updateStores(count = updateAgainWithCount) - _ = unblockConsumer() - _ <- assertFirst1000 - } yield succeed - ) - } - - val bufferSize = 100 - val bufferChunkSize = 10 - - "falling completely behind" should { - "return the correct ranges when starting from the beginning" in { - testConsumerFallingBehind( - bufferSize = bufferSize, - bufferChunkSize = bufferChunkSize, - consumerSubscriptionFrom = 1, - updateAgainWithCount = bufferSize, - ) - } - - "return the correct ranges when starting from an offset originally in the buffer at subscription time" in { - testConsumerFallingBehind( - bufferSize = bufferSize, - bufferChunkSize = bufferChunkSize, - consumerSubscriptionFrom = bufferSize / 2 + 1, - updateAgainWithCount = bufferSize, - ) - } - } - } - - "stream (reverse)" when { - "requested part is contained within buffer " should { - "not request form persistence" in new StaticTestScope { - run( - startInclusive = offset1, - endInclusive = offset3, - descendingOrder = true, - ) - streamElements should contain theSameElementsInOrderAs Seq( - offset3 -> TestUpdateId("tx-3").toHexString, - offset2 -> TestUpdateId("tx-2").toHexString, - offset1 -> TestUpdateId("tx-1").toHexString, - ) - } - } - - "requested range is disjoint with buffer" should { - "request whole range from persistence" in new StaticTestScope { - val filterMock = new Object - val fetchFromPersistenceMock = buildFetchFromPersistence( - expectedStartInclusive = offset0.decrement.value, - expectedEndInclusive = offset0, - expectedDescendingOrder = true, - expectedFilter = filterMock, - thenReturnStream = Source( - Seq( - offset0.decrement.value -> TestUpdateId("tx").toHexString - ) - ), - ) - run( - startInclusive = offset0.decrement.value, - endInclusive = offset0, - descendingOrder = true, - fetchFromPersistence = fetchFromPersistenceMock, - persistenceFetchArgs = filterMock, - ) - - streamElements should contain theSameElementsInOrderAs Seq( - offset0.decrement.value -> TestUpdateId("tx").toHexString - ) - } - } - - "requested range is partially contained in buffer" should { - "request missing part from persistence" in new StaticTestScope { - val filterMock = new Object - val fetchFromPersistenceMock = buildFetchFromPersistence( - expectedStartInclusive = offset0, - expectedEndInclusive = offset0, - expectedDescendingOrder = true, - expectedFilter = filterMock, - thenReturnStream = Source(Seq(offset0 -> TestUpdateId("tx-0").toHexString)), - ) - - run( - transactionsBuffer = inMemoryFanoutBuffer, - startInclusive = offset0, - endInclusive = offset3, - descendingOrder = true, - fetchFromPersistence = fetchFromPersistenceMock, - persistenceFetchArgs = filterMock, - ) - - streamElements should contain theSameElementsInOrderAs Seq( - offset3 -> TestUpdateId("tx-3").toHexString, - offset2 -> TestUpdateId("tx-2").toHexString, - offset1 -> TestUpdateId("tx-1").toHexString, - offset0 -> TestUpdateId("tx-0").toHexString, - ) - } - } - - val bufferSize = 5 - val bufferChunkSize = 2 - "requested range is initially contained in buffer, after first fetch buffer moves forward" should { - "request missing part from persistence" in new DynamicTestScope( - maxBufferSize = bufferSize, - maxBufferChunkSize = bufferChunkSize, - ) { - runF( - for { - // Prepopulate stores - _ <- updateStores(count = bufferSize) - // Start stream subscription - (assertResult, unblockConsumer) = streamWithHandle( - startInclusiveIdx = 1, - endInclusiveIdx = bufferSize, - descendingOrder = true, - ) - // Feed the buffer and effectively make the next chunk in reverse outside of it - _ <- updateStores(count = bufferSize) - _ = unblockConsumer() - _ <- assertResult - } yield succeed - ) - } - } - } -} - -object BufferedStreamsReaderSpec { - - trait TestFixtures - extends Matchers - with ScalaFutures - with BaseTest - with HasExecutorServiceGeneric { self: PekkoBeforeAndAfterAll => - - implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace.ForTesting - - implicit val ec: ExecutionContext = executorService - - val metrics = LedgerApiServerMetrics.ForTesting - val Seq(offset0, offset1, offset2, offset3) = - (0 to 3) map { idx => offset(idx.toLong) }: @nowarn("msg=match may not be exhaustive") - val offsetUpdates: Seq[TransactionLogUpdate.TransactionAccepted] = - (1L to 3L).map(transaction) - val offsetUpdatesAfterBeginning: Seq[TransactionLogUpdate.TransactionAccepted] = - (5L to 8L).map(transaction) - - val noFilterBufferSlice - : TransactionLogUpdate => Option[TransactionLogUpdate.TransactionAccepted] = - tracedUpdate => - tracedUpdate match { - case update: TransactionLogUpdate.TransactionAccepted => Some(update) - case _ => None - } - - val inMemoryFanoutBuffer: InMemoryFanoutBuffer = new InMemoryFanoutBuffer( - maxBufferSize = 3, - metrics = metrics, - maxBufferedChunkSize = 3, - loggerFactory = loggerFactory, - ).tap(inMemoryFanoutBuffer => offsetUpdates.foreach(inMemoryFanoutBuffer.push)) - - val inMemoryFanoutBufferWithSmallChunkSize: InMemoryFanoutBuffer = new InMemoryFanoutBuffer( - maxBufferSize = 3, - metrics = metrics, - maxBufferedChunkSize = 1, - loggerFactory = loggerFactory, - ).tap(inMemoryFanoutBuffer => offsetUpdates.foreach(inMemoryFanoutBuffer.push)) - - val smallInMemoryFanoutBuffer: InMemoryFanoutBuffer = new InMemoryFanoutBuffer( - maxBufferSize = 1, - metrics = metrics, - maxBufferedChunkSize = 1, - loggerFactory = loggerFactory, - ).tap(inMemoryFanoutBuffer => offsetUpdates.foreach(inMemoryFanoutBuffer.push)) - - trait StaticTestScope { - - val streamElements: ArrayBuffer[(Offset, String)] = - ArrayBuffer.empty[(Offset, String)] - - private val failingPersistenceFetch = new FetchFromPersistence[Object, String] { - override def apply( - startInclusive: Offset, - endInclusive: Offset, - descendingOrder: Boolean, - filter: Object, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, String), NotUsed] = fail( - s"Unexpected call to fetch from persistence startInclusive=$startInclusive, endInclusive=$endInclusive, descendingOrder=$descendingOrder" - ) - } - - def run( - startInclusive: Offset, - endInclusive: Offset, - descendingOrder: Boolean, - transactionsBuffer: InMemoryFanoutBuffer = inMemoryFanoutBufferWithSmallChunkSize, - fetchFromPersistence: FetchFromPersistence[Object, String] = failingPersistenceFetch, - persistenceFetchArgs: Object = new Object, - bufferSliceFilter: TransactionLogUpdate => Option[ - TransactionLogUpdate.TransactionAccepted - ] = noFilterBufferSlice, - ): Done = - new BufferedStreamsReader[Object, String]( - inMemoryFanoutBuffer = transactionsBuffer, - fetchFromPersistence = fetchFromPersistence, - bufferedStreamEventsProcessingParallelism = 2, - metrics = metrics, - streamName = "some_tx_stream", - loggerFactory, - )(executorService) - .stream[TransactionLogUpdate.TransactionAccepted]( - startInclusive = startInclusive, - endInclusive = endInclusive, - persistenceFetchArgs = persistenceFetchArgs, - bufferFilter = bufferSliceFilter, - toApiResponse = tx => Future.successful(tx.updateId), - descendingOrder = descendingOrder, - skipPruningChecks = false, - ) - .runWith(Sink.foreach(streamElements.addOne)) - .futureValue - - def buildFetchFromPersistence( - expectedStartInclusive: Offset, - expectedEndInclusive: Offset, - expectedDescendingOrder: Boolean, - expectedFilter: Object, - thenReturnStream: Source[(Offset, String), NotUsed], - ): FetchFromPersistence[Object, String] = - new FetchFromPersistence[Object, String] { - override def apply( - startInclusive: Offset, - endInclusive: Offset, - descendingOrder: Boolean, - filter: Object, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, String), NotUsed] = - (startInclusive, endInclusive, filter, descendingOrder, skipPruningChecks) match { - case ( - `expectedStartInclusive`, - `expectedEndInclusive`, - `expectedFilter`, - `expectedDescendingOrder`, - false, - ) => - thenReturnStream - case unexpected => - fail(s"Unexpected fetch transactions subscription start: $unexpected") - } - } - } - - class DynamicTestScope(val maxBufferSize: Int = 100, val maxBufferChunkSize: Int = 10) { - @volatile private var persistenceStore = - Vector.empty[(Offset, TransactionLogUpdate.TransactionAccepted)] - @volatile private var ledgerEndIndex = 0L - private val inMemoryFanoutBuffer = - new InMemoryFanoutBuffer(maxBufferSize, metrics, maxBufferChunkSize, loggerFactory) - - private val fetchFromPersistence = new FetchFromPersistence[Object, String] { - override def apply( - startInclusive: Offset, - endInclusive: Offset, - descendingOrder: Boolean, - filter: Object, - skipPruningChecks: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Source[(Offset, String), NotUsed] = - if (startInclusive > endInclusive) fail("startExclusive after endInclusive") - else if (endInclusive > offset(ledgerEndIndex)) - fail("endInclusive after ledgerEnd") - else if (!descendingOrder) - persistenceStore - .dropWhile(_._1 < startInclusive) - .takeWhile(_._1 <= endInclusive) - .map { case (o, tx) => o -> tx.updateId } - .pipe(Source(_)) - else - persistenceStore - .dropWhile(_._1 < startInclusive) - .takeWhile(_._1 <= endInclusive) - .reverse - .map { case (o, tx) => o -> tx.updateId } - .pipe(Source(_)) - } - - private val streamReader = new BufferedStreamsReader[Object, String]( - inMemoryFanoutBuffer = inMemoryFanoutBuffer, - fetchFromPersistence = fetchFromPersistence, - bufferedStreamEventsProcessingParallelism = 2, - metrics = metrics, - streamName = "some_tx_stream", - loggerFactory, - ) - - def updateStores(count: Int): Future[Done] = { - val (done, handle) = { - val blockingPromise = Promise[Unit]() - val unblockHandle: () => Unit = () => blockingPromise.success(()) - - val done = Source - .fromIterator(() => (ledgerEndIndex + 1L to count + ledgerEndIndex).iterator) - .async - .mapAsync(1) { idx => - blockingPromise.future.map(_ => idx) - } - .async - .runForeach(updateFixtures) - - done -> unblockHandle - } - handle() - done - } - - def stream( - startInclusiveIdx: Int, - endInclusiveIdx: Int, - descendingOrder: Boolean, - ): Future[Assertion] = { - val (done, handle) = streamWithHandle(startInclusiveIdx, endInclusiveIdx, descendingOrder) - handle() - done - } - - def streamWithHandle( - startInclusiveIdx: Int, - endInclusiveIdx: Int, - descendingOrder: Boolean, - ): (Future[Assertion], () => Unit) = { - val blockingPromise = Promise[Unit]() - val unblockHandle: () => Unit = () => blockingPromise.success(()) - - val assertReadStream = streamReader - .stream[TransactionLogUpdate.TransactionAccepted]( - startInclusive = offset(startInclusiveIdx.toLong), - endInclusive = offset(endInclusiveIdx.toLong), - persistenceFetchArgs = new Object, // Not used - bufferFilter = noFilterBufferSlice, // Do not filter - toApiResponse = tx => Future.successful(tx.updateId), - descendingOrder = descendingOrder, - skipPruningChecks = false, - ) - .async - .mapAsync(1) { idx => - blockingPromise.future.map(_ => idx) - } - .async - .runWith(Sink.seq) - .map { result => - withClue(s"[$startInclusiveIdx, $endInclusiveIdx]") { - result.size shouldBe endInclusiveIdx - startInclusiveIdx + 1 - } - val expectedElements = { - val elements = (startInclusiveIdx.toLong to endInclusiveIdx.toLong) map { idx => - offset(idx) -> TestUpdateId(s"tx-$idx").toHexString - } - if (descendingOrder) - elements.reverse - else - elements - } - result should contain theSameElementsInOrderAs expectedElements - } - - assertReadStream -> unblockHandle - } - - def runF(f: => Future[Assertion]): Assertion = - f.futureValue - - private def updateFixtures(idx: Long): Unit = { - val offsetAt = offset(idx) - val tx = transaction(idx) - persistenceStore = persistenceStore.appended(offsetAt -> tx) - inMemoryFanoutBuffer.push(tx) - ledgerEndIndex = idx - } - } - } - - private val someSynchronizerId = SynchronizerId.tryFromString("some::synchronizer id") - - private def transaction(i: Long) = - TransactionLogUpdate.TransactionAccepted( - updateId = TestUpdateId(s"tx-$i").toHexString, - commandId = "", - workflowId = "", - effectiveAt = Timestamp.Epoch, - offset = offset(i), - events = Vector(null), - completionStreamResponseO = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - recordTime = Timestamp.Epoch, - externalTransactionHash = None, - )(TraceContext.empty) - - private def offset(idx: Long): Offset = { - val base = 1000000000L - Offset.tryFromLong(base + idx) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/BufferedUpdatePointwiseReaderSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/BufferedUpdatePointwiseReaderSpec.scala deleted file mode 100644 index 7cccab56db..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/BufferedUpdatePointwiseReaderSpec.scala +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.cache.InMemoryFanoutBuffer -import com.digitalasset.canton.platform.store.dao.BufferedUpdatePointwiseReader.{ - FetchUpdatePointwiseFromPersistence, - ToApiResponse, -} -import com.digitalasset.canton.platform.store.interfaces.TransactionLogUpdate -import com.digitalasset.canton.protocol.{TestUpdateId, UpdateId} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.Party -import com.digitalasset.daml.lf.data.Time.Timestamp -import org.mockito.MockitoSugar -import org.scalatest.flatspec.AsyncFlatSpec - -import scala.concurrent.Future -import scala.language.implicitConversions - -class BufferedUpdatePointwiseReaderSpec extends AsyncFlatSpec with MockitoSugar with BaseTest { - private val className = classOf[BufferedUpdatePointwiseReader[?, ?]].getSimpleName - - private implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace( - loggerFactory - ) - - private val requestingParties = Set("p1", "p2").map(Ref.Party.assertFromString) - private val someSynchronizerId = SynchronizerId.tryFromString("some::synchronizer id") - - private val bufferedUpdateId1 = TestUpdateId("bufferedTid_1") - private val bufferedUpdateId2 = TestUpdateId("bufferedTid_2") - private val notBufferedUpdateId = TestUpdateId("notBufferedTid") - private val unknownUpdateId = TestUpdateId("unknownUpdateId") - - private val bufferedOffset1 = Offset.firstOffset - private val bufferedOffset2 = bufferedOffset1.increment - private val notBufferedOffset = bufferedOffset2.increment - private val unknownOffset = notBufferedOffset.increment - - private val bufferedTransaction1 = tx(bufferedUpdateId1, bufferedOffset1) - private val bufferedTransaction2 = tx(bufferedUpdateId2, bufferedOffset2) - - private val inMemoryFanout = mock[InMemoryFanoutBuffer] - when(inMemoryFanout.lookup(toLookupKey(bufferedUpdateId1))).thenReturn(Some(bufferedTransaction1)) - when(inMemoryFanout.lookup(toLookupKey(bufferedUpdateId2))).thenReturn(Some(bufferedTransaction2)) - when(inMemoryFanout.lookup(toLookupKey(notBufferedUpdateId))).thenReturn(None) - when(inMemoryFanout.lookup(toLookupKey(unknownUpdateId))).thenReturn(None) - - when(inMemoryFanout.lookup(toLookupKey(bufferedOffset1))).thenReturn(Some(bufferedTransaction1)) - when(inMemoryFanout.lookup(toLookupKey(bufferedOffset2))).thenReturn(Some(bufferedTransaction2)) - when(inMemoryFanout.lookup(toLookupKey(notBufferedOffset))).thenReturn(None) - when(inMemoryFanout.lookup(toLookupKey(unknownOffset))).thenReturn(None) - - private val toApiResponse = mock[ToApiResponse[Set[Party], UpdateId]] - when(toApiResponse.apply(bufferedTransaction1, requestingParties, loggingContext)) - .thenReturn(Future.successful(Some(bufferedUpdateId1))) - when(toApiResponse.apply(bufferedTransaction2, requestingParties, loggingContext)) - .thenReturn(Future.successful(None)) - - private val fetchFromPersistence = - new FetchUpdatePointwiseFromPersistence[(LookupKey, Set[Party]), UpdateId] { - override def apply( - queryParam: (LookupKey, Set[Party]), - loggingContext: LoggingContextWithTrace, - ): Future[Option[UpdateId]] = - queryParam._1 match { - case LookupKey.ByUpdateId(`notBufferedUpdateId`) | - LookupKey.ByOffset(`notBufferedOffset`) => - Future.successful(Some(notBufferedUpdateId)) - case LookupKey.ByUpdateId(`unknownUpdateId`) | LookupKey.ByOffset(`unknownOffset`) => - Future.successful(None) - case other => fail(s"Unexpected $other lookup key") - } - } - - private val bufferedUpdateReader = - new BufferedUpdatePointwiseReader[(LookupKey, Set[Party]), UpdateId]( - fetchFromPersistence = fetchFromPersistence, - fetchFromBuffer = queryParam => inMemoryFanout.lookup(queryParam._1), - toApiResponse = (tx, queryParam, lc) => toApiResponse(tx, queryParam._2, lc), - ) - - s"$className.fetch" should "convert to API response and return if update buffered" in { - for { - response1 <- bufferedUpdateReader.fetch(toLookupKey(bufferedUpdateId1) -> requestingParties) - response2 <- bufferedUpdateReader.fetch(toLookupKey(bufferedUpdateId2) -> requestingParties) - response3 <- bufferedUpdateReader.fetch(toLookupKey(bufferedOffset1) -> requestingParties) - response4 <- bufferedUpdateReader.fetch(toLookupKey(bufferedOffset2) -> requestingParties) - } yield { - response1 shouldBe Some(bufferedUpdateId1) - response2 shouldBe None - response3 shouldBe response1 - response4 shouldBe response2 - verify(toApiResponse, times(2)).apply(bufferedTransaction1, requestingParties, loggingContext) - verify(toApiResponse, times(2)).apply(bufferedTransaction2, requestingParties, loggingContext) - succeed - } - } - - s"$className.fetch" should "delegate to persistence fetch if update not buffered" in { - for { - response1 <- bufferedUpdateReader.fetch(toLookupKey(notBufferedUpdateId) -> requestingParties) - response2 <- bufferedUpdateReader.fetch(toLookupKey(unknownUpdateId) -> requestingParties) - response3 <- bufferedUpdateReader.fetch(toLookupKey(notBufferedOffset) -> requestingParties) - response4 <- bufferedUpdateReader.fetch(toLookupKey(unknownOffset) -> requestingParties) - } yield { - response1 shouldBe Some(notBufferedUpdateId) - response2 shouldBe None - response3 shouldBe response1 - response4 shouldBe response2 - verifyZeroInteractions(toApiResponse) - succeed - } - } - - private def tx(discriminator: UpdateId, offset: Offset) = - TransactionLogUpdate.TransactionAccepted( - updateId = discriminator.toHexString, - workflowId = "", - commandId = "", - effectiveAt = Timestamp.Epoch, - offset = offset, - events = Vector(null), - completionStreamResponseO = None, - synchronizerId = someSynchronizerId.toProtoPrimitive, - recordTime = Timestamp.Epoch, - externalTransactionHash = None, - ) - - protected implicit def toLedgerString(s: String): Ref.LedgerString = - Ref.LedgerString.assertFromString(s) - - private def toLookupKey(str: UpdateId): LookupKey = LookupKey.ByUpdateId(str) - - private def toLookupKey(offset: Offset): LookupKey = LookupKey.ByOffset(offset) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/EventProjectionPropertiesSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/EventProjectionPropertiesSpec.scala deleted file mode 100644 index ad9b9d9665..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/EventProjectionPropertiesSpec.scala +++ /dev/null @@ -1,1199 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.ledger.api.{ - CumulativeFilter, - EventFormat, - InterfaceFilter, - TemplateFilter, - TemplateWildcardFilter, -} -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties.Projection -import com.digitalasset.canton.platform.store.dao.EventProjectionPropertiesSpec.Scope -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{ - FullIdentifier, - Identifier, - IdentifierConverter, - NameTypeConRef, - Party, -} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class EventProjectionPropertiesSpec extends AnyFlatSpec with Matchers { - behavior of "EventProjectionProperties" - - it should "propagate verbose flag" in new Scope { - EventProjectionProperties( - eventFormat = noFilter.copy(verbose = true), - interfaceImplementedBy = noInterface, - resolveTypeConRef = noTemplatesForPackageName, - ).verbose shouldBe true - EventProjectionProperties( - eventFormat = noFilter.copy(verbose = false), - interfaceImplementedBy = noInterface, - resolveTypeConRef = noTemplatesForPackageName, - ).verbose shouldBe false - } - - it should "project nothing in case of empty filters" in new Scope { - EventProjectionProperties( - eventFormat = noFilter.copy(verbose = true), - interfaceImplementedBy = noInterface, - resolveTypeConRef = noTemplatesForPackageName, - ) - .render(Set(party), id) shouldBe Projection(Set.empty, false) - } - - it should "project nothing in case of not matching template filter" in new Scope { - EventProjectionProperties( - eventFormat = templateWildcardFilter(), - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - .render(Set(party), id) shouldBe Projection(Set.empty, false) - - EventProjectionProperties( - eventFormat = templateWildcardPartyWildcardFilter(), - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - .render(Set(party), id) shouldBe Projection(Set.empty, false) - } - - behavior of "projecting interfaces" - - projectingInterfacesTests(withPartyWildcard = false) - projectingInterfacesTests(withPartyWildcard = true) - - behavior of "projecting created_event_blob" - - projectingBlobTests(withPartyWildcard = false) - projectingBlobTests(withPartyWildcard = true) - - it should "project created_event_blob for everything if set as default" in new Scope { - private val transactionFilter = EventFormat( - filtersByParty = Map( - party -> - CumulativeFilter( - templateFilters = Set(TemplateFilter(template1, false)), - interfaceFilters = Set.empty, - templateWildcardFilter = None, - ), - party2 -> - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1.toNameTypeConRef, - false, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ), - party3 -> CumulativeFilter.templateWildcardFilter(), - ), - filtersForAnyParty = Some(CumulativeFilter.templateWildcardFilter(true)), - verbose = true, - ) - val testee = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - testee.render(Set(party), template1).createdEventBlob shouldBe true - testee.render(Set(party2), template1).createdEventBlob shouldBe true - testee.render(Set(party3), template1).createdEventBlob shouldBe true - } - - behavior of "combining projections" - - it should "project created_event_blob and contractArguments in case of match by interface, template-id and" + - "package-name-scoped template when interface filters are defined with party-wildcard and template filters by party" in new Scope { - private val templateFilters = - CumulativeFilter( - templateFilters = Set( - template1Filter.copy(includeCreatedEventBlob = true), - TemplateFilter(packageNameScopedTemplate, includeCreatedEventBlob = false), - ), - interfaceFilters = Set.empty, - templateWildcardFilter = None, - ) - private val interfaceFilters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - interfaceTypeRef = iface1Ref, - includeView = false, - includeCreatedEventBlob = true, - ) - ), - templateWildcardFilter = None, - ) - - private val transactionFilter = - EventFormat( - filtersByParty = Map(party -> templateFilters), - filtersForAnyParty = Some(interfaceFilters), - verbose = true, - ) - - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - templatesForPackageName, - ) - - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - - // createdEventBlob enabled as it's matched by the package-id scoped template filter for template1 with createdEventBlob = true - // which internally translates to package-name which is the same with that of template2 - eventProjectionProperties.render(Set(party), template2) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - } - - it should "project created_event_blob and contractArguments in case of match by interface, template-id and" + - "package-name-scoped template when template filters are defined with party-wildcard and interface filters by party" in new Scope { - private val templateFilters = - CumulativeFilter( - templateFilters = Set( - template1Filter.copy(includeCreatedEventBlob = true), - TemplateFilter(packageNameScopedTemplate, includeCreatedEventBlob = false), - ), - interfaceFilters = Set.empty, - templateWildcardFilter = None, - ) - private val interfaceFilters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - interfaceTypeRef = iface1Ref, - includeView = false, - includeCreatedEventBlob = true, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = - EventFormat( - filtersByParty = Map(party -> interfaceFilters), - filtersForAnyParty = Some(templateFilters), - verbose = true, - ) - - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - templatesForPackageName, - ) - - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - - // createdEventBlob enabled as it's matched by the package-id scoped template filter for template1 with createdEventBlob = true - // which internally translates to package-name which is the same with that of template2 - eventProjectionProperties.render(Set(party), template2) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - } - - it should "project created_event_blob and interface in case of match by interface and template when filters exist in party-wildcard and by party" in new Scope { - private val templateFilters = - CumulativeFilter( - templateFilters = Set( - template1Filter.copy(includeCreatedEventBlob = true) - ), - interfaceFilters = Set.empty, - templateWildcardFilter = None, - ) - private val interfaceFilters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - interfaceTypeRef = iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ) - - private val transactionFilter = - EventFormat( - filtersByParty = Map(party -> templateFilters), - filtersForAnyParty = Some(interfaceFilters), - verbose = true, - ) - private val transactionFilterSwapped = - EventFormat( - filtersByParty = Map(party -> interfaceFilters), - filtersForAnyParty = Some(templateFilters), - verbose = true, - ) - - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - templatesForPackageName, - ) - private val eventProjectionPropertiesSwapped = EventProjectionProperties( - eventFormat = transactionFilterSwapped, - interfaceImplementedBy = interfaceImpl, - templatesForPackageName, - ) - - eventProjectionProperties.render(Set(party), template1) shouldBe Projection( - interfaces = Set(iface1), - createdEventBlob = true, - ) - eventProjectionPropertiesSwapped.render(Set(party), template1) shouldBe Projection( - interfaces = Set(iface1), - createdEventBlob = true, - ) - } - - def projectingInterfacesTests(withPartyWildcard: Boolean) = { - val details = - if (withPartyWildcard) " (with party-wildcard filters)" else " (with filters by party)" - - it should "project interface in case of match by interface id and witness" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - - eventProjectionProperties.render(Set(party), template1) shouldBe Projection( - interfaces = Set(iface1), - createdEventBlob = false, - ) - eventProjectionProperties.render(Set(party2), template1) shouldBe Projection( - interfaces = if (!withPartyWildcard) Set.empty else Set(iface1), - createdEventBlob = false, - ) - } - - it should "project interface in case of match by interface id and witness with alwaysPopulateArguments" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - - eventProjectionProperties.render(Set(party), template1) shouldBe Projection( - interfaces = Set(iface1), - createdEventBlob = false, - ) - eventProjectionProperties.render(Set(party2), template1) shouldBe Projection( - interfaces = if (!withPartyWildcard) Set.empty else Set(iface1), - createdEventBlob = false, - ) - } - - it should "not project interface in case of match by interface id but not witness with alwaysPopulateArguments" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - .render(Set(party2), template1) shouldBe Projection( - interfaces = if (!withPartyWildcard) Set.empty else Set(iface1), - createdEventBlob = false, - ) - } - - it should "project an interface and template in case of match by interface id, template and witness" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - .render(Set(party), template1) shouldBe Projection( - interfaces = Set(iface1), - createdEventBlob = false, - ) - } - - it should "project an interface and template in case of match by interface id, template and witness with alwaysPopulateArguments" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set(template1Filter), - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - .render(Set(party), template1) shouldBe Projection( - interfaces = Set(iface1), - createdEventBlob = false, - ) - } - - it should "project multiple interfaces in case of match by multiple interface ids and witness" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ), - InterfaceFilter( - iface2Ref, - includeView = true, - includeCreatedEventBlob = false, - ), - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - eventProjectionProperties.render(Set(party), template1) shouldBe Projection( - interfaces = Set(iface1, iface2), - createdEventBlob = false, - ) - eventProjectionProperties.render(Set(party2), template1) shouldBe Projection( - interfaces = if (!withPartyWildcard) Set.empty else Set(iface1, iface2), - createdEventBlob = false, - ) - } - - if (withPartyWildcard) { - it should "project multiple interfaces in case of match by multiple interface ids and witness when combined with party-wildcard" in new Scope { - private val filter1 = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ) - private val filter2 = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface2Ref, - includeView = true, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = - EventFormat(Map(party -> filter1), Some(filter2), verbose = true) - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - eventProjectionProperties.render(Set(party), template1) shouldBe Projection( - Set(iface1, iface2), - false, - ) - eventProjectionProperties.render(Set(party2), template1) shouldBe Projection( - Set(iface2), - false, - ) - } - } - - it should "deduplicate projected interfaces and include the view" ++ details in new Scope { - private val filter1 = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = false, - includeCreatedEventBlob = false, - ), - InterfaceFilter( - iface2Ref, - includeView = true, - includeCreatedEventBlob = false, - ), - ), - templateWildcardFilter = None, - ) - private val filter2 = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - includeView = true, - includeCreatedEventBlob = false, - ), - InterfaceFilter( - iface2Ref, - includeView = true, - includeCreatedEventBlob = false, - ), - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map( - party -> filter1, - party2 -> filter2, - ), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map( - party2 -> filter2 - ), - filtersForAnyParty = Some(filter1), - verbose = true, - ) - } - - EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - .render(Set(party, party2), template1) shouldBe Projection( - Set(iface1, iface2), - false, - ) - } - - } - - def projectingBlobTests(withPartyWildcard: Boolean) = { - val details = - if (withPartyWildcard) " (with party-wildcard filters)" else " (with filters by party)" - - it should "project created_event_blob in case of match by interface" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - interfaceTypeRef = iface1Ref, - includeView = false, - includeCreatedEventBlob = true, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - eventProjectionProperties.render( - Set(party2), - template1, - ) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = withPartyWildcard, - ) - } - - it should "project created_event_blob in case of match by template-wildcard" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - interfaceTypeRef = iface1Ref, - includeView = false, - includeCreatedEventBlob = false, - ) - ), - templateWildcardFilter = Some(TemplateWildcardFilter(includeCreatedEventBlob = true)), - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - eventProjectionProperties.render( - Set(party2), - template1, - ) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = withPartyWildcard, - ) - } - - it should "project created_event_blob in case of match by interface, template-id and package-name-scoped template" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set( - template1Filter.copy(includeCreatedEventBlob = true), - TemplateFilter(packageNameScopedTemplate, includeCreatedEventBlob = false), - ), - interfaceFilters = Set( - InterfaceFilter( - interfaceTypeRef = iface1Ref, - includeView = false, - includeCreatedEventBlob = true, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - templatesForPackageName, - ) - - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - - // createdEventBlob enabled as it's matched by the package-id scoped template filter with createdEventBlob = true - // (it internally translates to package-name which is the same with that of template2) - eventProjectionProperties.render(Set(party), template2) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - } - - it should "project created_event_blob in case of match by interface and template with include the view" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set(template1Filter.copy(includeCreatedEventBlob = true)), - interfaceFilters = Set(InterfaceFilter(iface1Ref, true, true)), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection( - interfaces = Set(iface1), - createdEventBlob = true, - ) - eventProjectionProperties.render( - witnesses = Set(party2), - templateId = template1, - ) shouldBe Projection( - interfaces = if (!withPartyWildcard) Set.empty else Set(iface1), - createdEventBlob = withPartyWildcard, - ) - - } - - it should "project created_event_blob in case of at least a single interface requesting it" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - false, - includeCreatedEventBlob = true, - ), - InterfaceFilter( - iface2Ref, - false, - includeCreatedEventBlob = false, - ), - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - - EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ).render( - Set(party), - template1, - ) shouldBe Projection(Set.empty, true) - - } - - it should "project created_event_blob in case of match by interface, template-id (but both without flag enabled) and " + - "package-name-scoped template (flag enabled)" ++ details in new Scope { - val template2Filter: TemplateFilter = - TemplateFilter(template2, includeCreatedEventBlob = false) - private val filters = - CumulativeFilter( - templateFilters = Set( - template1Filter, - template2Filter, - TemplateFilter(packageNameScopedTemplate, includeCreatedEventBlob = true), - ), - interfaceFilters = Set( - InterfaceFilter(iface1Ref, false, false), - InterfaceFilter(iface2Ref, false, false), - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - - private val eventProjectionProperties: EventProjectionProperties = - EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = Map( - template1 -> Set(template1Full), - template2 -> Set(template2Full), - iface1Ref -> Set(iface1), - iface2Ref -> Set(iface2), - packageNameScopedTemplate -> Set(template2Full), - ), - ) - - // createdEventBlob enabled as it's matched by the package-named scoped template filter with createdEventBlob = true - // (template1 internally translates to package-name which is the same with that of template2) - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection(Set.empty, true) - - eventProjectionProperties.render( - Set(party), - template3, - ) shouldBe Projection(Set.empty, false) - - // createdEventBlob enabled as it's matched by the package-name scoped template filter with createdEventBlob = true - eventProjectionProperties.render(Set(party), template2) shouldBe Projection( - interfaces = Set.empty, - createdEventBlob = true, - ) - } - - it should "not project created_event_blob in case of no match by interface" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - false, - includeCreatedEventBlob = true, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ).render( - Set(party), - template2, - ) shouldBe Projection(Set.empty, true) - - EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ).render( - Set(party), - template3, - ) shouldBe Projection(Set.empty, false) - - } - - it should "project created_event_blob for wildcard templates, if it is specified explicitly via interface filter" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set( - InterfaceFilter( - iface1Ref, - false, - includeCreatedEventBlob = true, - ) - ), - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection(Set.empty, true) - eventProjectionProperties.render( - Set(party), - template2, - ) shouldBe Projection(Set.empty, true) - eventProjectionProperties.render( - Set(party), - template3, - ) shouldBe Projection(Set.empty, false) - } - - it should "project created_event_blob for wildcard templates, if it is specified explicitly via template filter" ++ details in new Scope { - private val filters = - CumulativeFilter( - templateFilters = Set(TemplateFilter(template1, true)), - interfaceFilters = Set.empty, - templateWildcardFilter = None, - ) - private val transactionFilter = withPartyWildcard match { - case false => - EventFormat( - filtersByParty = Map(party -> filters), - filtersForAnyParty = None, - verbose = true, - ) - case true => - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some(filters), - verbose = true, - ) - } - private val eventProjectionProperties = EventProjectionProperties( - eventFormat = transactionFilter, - interfaceImplementedBy = interfaceImpl, - resolveTypeConRef = noTemplatesForPackageName, - ) - - eventProjectionProperties.render( - Set(party), - template1, - ) shouldBe Projection(Set.empty, true) - eventProjectionProperties.render( - Set(party), - template2, - ) shouldBe Projection(Set.empty, true) - eventProjectionProperties.render( - Set(party), - template3, - ) shouldBe Projection(Set.empty, false) - } - - } - -} -object EventProjectionPropertiesSpec { - trait Scope { - val packageRefName = Ref.PackageRef.Name(Ref.PackageName.assertFromString("PackageName")) - val qualifiedName: Ref.QualifiedName = Ref.QualifiedName.assertFromString("ModuleName:template") - val packageNameScopedTemplate: Ref.NameTypeConRef = - Ref.NameTypeConRef(packageRefName, qualifiedName) - - val template1Full: FullIdentifier = Identifier - .assertFromString("PackageId2:ModuleName:template") - .toFullIdentifier(packageRefName.name) - val template1: NameTypeConRef = template1Full.toNameTypeConRef - val template1Filter: TemplateFilter = - TemplateFilter(template1, includeCreatedEventBlob = false) - val template2Full: FullIdentifier = Identifier - .assertFromString("PackageId1:ModuleName:template") - .toFullIdentifier(packageRefName.name) - val template2: NameTypeConRef = template2Full.toNameTypeConRef - val template3Full: FullIdentifier = Identifier - .assertFromString("PackageId1:ModuleName:template3") - .toFullIdentifier(packageRefName.name) - val template3: NameTypeConRef = template3Full.toNameTypeConRef - val template3Ref: Ref.TypeConRef = template3Full.toIdentifier.toRef - val id: NameTypeConRef = - NameTypeConRef.assertFromString(s"$packageRefName:ModuleName:id") - val iface1: FullIdentifier = Identifier - .assertFromString("PackageId:ModuleName:iface1") - .toFullIdentifier(packageRefName.name) - val iface1Ref: NameTypeConRef = iface1.toNameTypeConRef - val iface2: FullIdentifier = Identifier - .assertFromString("PackageId:ModuleName:iface2") - .toFullIdentifier(packageRefName.name) - val iface2Ref: NameTypeConRef = iface2.toNameTypeConRef - val packageNameScopedIface1 = Ref.NameTypeConRef(packageRefName, iface1.qualifiedName) - - val noInterface: FullIdentifier => Set[FullIdentifier] = _ => Set.empty[FullIdentifier] - val noTemplatesForPackageName: NameTypeConRef => Set[FullIdentifier] = - Map( - template1 -> Set(template1Full), - template2 -> Set(template2Full), - template3 -> Set(template3Full), - iface1Ref -> Set(iface1), - iface2Ref -> Set(iface2), - ) - val templatesForPackageName: NameTypeConRef => Set[FullIdentifier] = - Map( - template1 -> Set(template1Full), - template2 -> Set(template2Full), - iface1Ref -> Set(iface1), - iface2Ref -> Set(iface2), - packageNameScopedTemplate -> Set(template1Full, template2Full), - packageNameScopedIface1 -> Set(iface1), - ) - - val interfaceImpl: FullIdentifier => Set[FullIdentifier] = { - case `iface1` => Set(template1Full) - case `iface2` => Set(template1Full, template2Full, template3Full) - case _ => Set.empty - } - val party: Party = Party.assertFromString("party") - val party2: Party = Party.assertFromString("party2") - val party3: Party = Party.assertFromString("party3") - val noFilter = EventFormat( - filtersByParty = Map(), - filtersForAnyParty = None, - verbose = true, - ) - def templateWildcardFilter(includeCreatedEventBlob: Boolean = false) = EventFormat( - filtersByParty = Map( - party -> - CumulativeFilter( - Set.empty, - Set.empty, - Some(TemplateWildcardFilter(includeCreatedEventBlob = includeCreatedEventBlob)), - ) - ), - filtersForAnyParty = None, - verbose = true, - ) - def templateWildcardPartyWildcardFilter(includeCreatedEventBlob: Boolean = false) = - EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some( - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set.empty, - templateWildcardFilter = - Some(TemplateWildcardFilter(includeCreatedEventBlob = includeCreatedEventBlob)), - ) - ), - verbose = true, - ) - val emptyCumulativeFilters = EventFormat( - filtersByParty = Map( - party -> - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set.empty, - templateWildcardFilter = None, - ) - ), - filtersForAnyParty = None, - verbose = true, - ) - val emptyCumulativePartyWildcardFilters = EventFormat( - filtersByParty = Map.empty, - filtersForAnyParty = Some( - CumulativeFilter( - templateFilters = Set.empty, - interfaceFilters = Set.empty, - templateWildcardFilter = None, - ) - ), - verbose = true, - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/HikariJdbcConnectionProviderSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/HikariJdbcConnectionProviderSpec.scala deleted file mode 100644 index 10c0e5eb78..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/HikariJdbcConnectionProviderSpec.scala +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.resources.ResourceContext -import com.digitalasset.canton.HasExecutionContext -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.logging.SuppressionRule.{FullSuppression, LoggerNameContains} -import com.digitalasset.canton.logging.{NamedLogging, SuppressingLogger} -import com.digitalasset.canton.platform.config.ServerRole -import com.digitalasset.canton.platform.indexer.ha.TestConnection -import com.digitalasset.canton.tracing.TraceContext -import org.scalatest.concurrent.Eventually -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.time.{Seconds, Span} - -import java.io.PrintWriter -import java.sql.Connection -import java.util.concurrent.Executor -import java.util.concurrent.atomic.AtomicBoolean -import java.util.logging.Logger -import javax.naming.OperationNotSupportedException -import javax.sql.DataSource -import scala.concurrent.Future -import scala.concurrent.duration.FiniteDuration - -class HikariJdbcConnectionProviderSpec - extends AsyncFlatSpec - with Matchers - with NamedLogging - with HasExecutionContext - with Eventually { - - override implicit def patienceConfig: PatienceConfig = PatienceConfig(scaled(Span(5, Seconds))) - val loggerFactory: SuppressingLogger = SuppressingLogger(getClass) - private implicit val tc: TraceContext = TraceContext.empty - - behavior of "HikariJdbcConnectionProvider" - - // in case of a depleted Hikari pool getting the connection can take connectionTimeout - it should "not wait for releasing for a long-running health check" in { - val connectionGate = new AtomicBoolean(true) - val dataSourceFixture = new DataSource { - private def notSupported = throw new OperationNotSupportedException - override def getConnection: Connection = - if (connectionGate.get()) new TestConnection { - override def isClosed: Boolean = - // connection is signalled as closed after we gate connections - if (connectionGate.get()) super.isClosed - else true - - // connection is signalled as invalid after we gate connections - override def isValid(i: Int): Boolean = connectionGate.get() - - // the rest of the overrides here are needed to successfully mock a connection to Hikari - override def isReadOnly: Boolean = false - - override def getAutoCommit: Boolean = true - - override def setAutoCommit(b: Boolean): Unit = () - - override def getTransactionIsolation: Int = Connection.TRANSACTION_READ_COMMITTED - - override def getNetworkTimeout: Int = 5000 - - override def setNetworkTimeout(executor: Executor, i: Int): Unit = () - - override def commit(): Unit = () - - override def rollback(): Unit = () - - override def clearWarnings(): Unit = () - } - else throw new Exception("no connection available at the moment") - - override def getConnection(username: String, password: String): Connection = notSupported - - override def getLogWriter: PrintWriter = notSupported - - override def setLogWriter(out: PrintWriter): Unit = notSupported - - override def setLoginTimeout(seconds: Int): Unit = () - - override def getLoginTimeout: Int = 0 - - override def unwrap[T](iface: Class[T]): T = notSupported - - override def isWrapperFor(iface: Class[?]): Boolean = notSupported - - override def getParentLogger: Logger = notSupported - } - val connectionProviderOwner = - for { - hikariDataSource <- HikariDataSourceOwner( - dataSource = dataSourceFixture, - serverRole = ServerRole.Testing(this.getClass), - minimumIdle = 10, - maxPoolSize = 10, - connectionTimeout = FiniteDuration(10, "seconds"), - ) - _ <- DataSourceConnectionProvider.owner( - dataSource = hikariDataSource, - logMarker = "test", - loggerFactory = loggerFactory, - ) - } yield hikariDataSource - - implicit val resourceContext = ResourceContext(implicitly) - - val suppressionRules = FullSuppression && - LoggerNameContains("HealthCheckTask") - loggerFactory.suppress(suppressionRules) { - connectionProviderOwner - .use { hikariDataSource => - logger.info("HikariJdbcConnectionProvider initialized") - def getConnection = Future(hikariDataSource.getConnection.close()) - for { - _ <- Future.sequence(1.to(20).map(_ => getConnection)) - } yield { - logger.info( - "HikariJdbcConnectionProvider is functional: fetched and returned 20 connections" - ) - val start = System.currentTimeMillis() - connectionGate.set(false); - Threading.sleep( - 600 - ) // so that the Hikari isValid bypass default duration of 500 millis pass (otherwise getting connection from the pool is not necessary checked) - val failingConnection = getConnection - Threading.sleep(500) // so that health check already hangs (this is polled every second) - failingConnection.isCompleted shouldBe false // failing connection will hang for 10 seconds - (start, failingConnection) - } - } - .flatMap { case (start, failingConnection) => - val released = System.currentTimeMillis() - released - start should be > 1099L - released - start should be < 1600L // because we are not waiting for the healthcheck to be finished - failingConnection.isCompleted shouldBe false // failing connection will hang for 10 seconds - failingConnection.failed.map(_ => start) - } - .map { start => - val failedConnectionFinished = System.currentTimeMillis() - failedConnectionFinished - start should be > 9999L - eventually { - val logEntries = loggerFactory.fetchRecordedLogEntries - logEntries.size shouldBe 1 - logEntries(0).debugMessage should include( - "Hikari connection health check failed after health checking stopped with" - ) - } - } - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoActiveContractsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoActiveContractsSpec.scala deleted file mode 100644 index d55044a656..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoActiveContractsSpec.scala +++ /dev/null @@ -1,666 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.api.v2.event.CreatedEvent -import com.daml.ledger.api.v2.state_service.GetActiveContractsResponse -import com.digitalasset.canton.ledger.api.messages.state.AcsRangeInfo -import com.digitalasset.canton.ledger.api.util.LfEngineToApi -import com.digitalasset.canton.platform.TemplatePartiesFilter -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties.{ - Projection, - UseOriginalViewPackageId, -} -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{Identifier, IdentifierConverter, Party} -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.scalatest.* -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.UUID -import scala.concurrent.Future - -private[dao] trait JdbcLedgerDaoActiveContractsSpec - extends OptionValues - with Inside - with Inspectors - with LoneElement { - this: AsyncFlatSpec with Matchers with JdbcLedgerDaoSuite => - - behavior of "JdbcLedgerDao (getActiveContracts)" - - it should "serve the correct contracts after a series of transactions" in { - for { - before <- ledgerDao.lookupLedgerEnd() - (_, t1) <- store(singleCreate) - (_, t2) <- store(singleCreate) - (_, _) <- store(singleExercise(nonTransient(t2).loneElement)) - (_, _) <- store(fullyTransient()) - (_, t5) <- store(singleCreate) - (_, t6) <- store(singleCreate) - after <- ledgerDao.lookupLedgerEnd() - activeContractsBefore <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = before.map(_.lastOffset), - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice, bob, charlie))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - activeContractsAfter <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = after.map(_.lastOffset), - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice, bob, charlie))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val activeContracts = activeContractsAfter.toSet.diff(activeContractsBefore.toSet) - activeContracts should have size 3 - activeContracts.map(_.contractId) shouldBe Set( - nonTransient(t1).loneElement.coid, - nonTransient(t5).loneElement.coid, - nonTransient(t6).loneElement.coid, - ) - } - } - - it should "serve a stable result based on the input offset" in { - for { - ledgerEnd <- ledgerDao.lookupLedgerEnd() - offset = ledgerEnd.map(_.lastOffset) - activeContractsBefore <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = offset, - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice, bob, charlie))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - (_, _) <- store(singleCreate) - (_, c) <- store(singleCreate) - (_, _) <- store(singleExercise(nonTransient(c).loneElement)) - (_, _) <- store(fullyTransient()) - (_, _) <- store(singleCreate) - (_, _) <- store(singleCreate) - activeContractsAfter <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = offset, - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice, bob, charlie))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - activeContractsAfter.toSet.diff(activeContractsBefore.toSet) should have size 0 - } - } - - it should "filter correctly for a single party" in { - val party1 = Party.assertFromString(UUID.randomUUID.toString) - val party2 = Party.assertFromString(UUID.randomUUID.toString) - for { - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (party1, someTemplateId, someContractArgument), - (party2, otherTemplateId, otherContractArgument), - (party1, otherTemplateId, otherContractArgument), - ), - ) - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map(otherTemplateIdFull.toNameTypeConRef -> Some(Set(party1))), - Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = - Map(Some(party1) -> Map(Some(otherTemplateIdFull.toNameTypeConRef) -> Projection())), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val create = result.loneElement - create.witnessParties.loneElement shouldBe party1 - create.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - } - - it should "filter correctly by multiple parties with the same template" in { - val party1 = Party.assertFromString(UUID.randomUUID.toString) - val party2 = Party.assertFromString(UUID.randomUUID.toString) - for { - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (party1, someTemplateId, someContractArgument), - (party2, otherTemplateId, otherContractArgument), - (party1, otherTemplateId, otherContractArgument), - ), - ) - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - otherTemplateIdFull.toNameTypeConRef -> Some(Set(party1, party2)) - ), - Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map(Some(otherTemplateIdFull.toNameTypeConRef) -> Projection()), - Some(party2) -> Map(Some(otherTemplateIdFull.toNameTypeConRef) -> Projection()), - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val activeContracts = result.toArray - activeContracts should have length 2 - - val create1 = activeContracts(0) - create1.witnessParties.loneElement shouldBe party2 - create1.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - - val create2 = activeContracts(1) - create2.witnessParties.loneElement shouldBe party1 - create2.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - } - - it should "filter correctly by party-wildcard with the same template" in { - val party1 = Party.assertFromString(UUID.randomUUID.toString) - val party2 = Party.assertFromString(UUID.randomUUID.toString) - for { - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (party1, someTemplateId, someContractArgument), - (party2, otherTemplateId2, otherContractArgument), - (party1, otherTemplateId2, otherContractArgument), - ), - ) - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - otherTemplateId2Full.toNameTypeConRef -> None - ), - Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map(Some(otherTemplateId2Full.toNameTypeConRef) -> Projection()), - Some(party2) -> Map(Some(otherTemplateId2Full.toNameTypeConRef) -> Projection()), - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val activeContracts = result.toArray - activeContracts should have length 2 - - val create1 = activeContracts(0) - create1.witnessParties.loneElement shouldBe party2 - create1.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId2) - - val create2 = activeContracts(1) - create2.witnessParties.loneElement shouldBe party1 - create2.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId2) - } - } - - it should "filter correctly by multiple parties with different templates" in { - val party1 = Party.assertFromString(UUID.randomUUID.toString) - val party2 = Party.assertFromString(UUID.randomUUID.toString) - for { - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (party1, someTemplateId, someContractArgument), - (party2, otherTemplateId, otherContractArgument), - (party1, otherTemplateId, otherContractArgument), - ), - ) - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - someTemplateIdFull.toNameTypeConRef -> Some(Set(party1)), - otherTemplateIdFull.toNameTypeConRef -> Some(Set(party2)), - ), - Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map(Some(otherTemplateIdFull.toNameTypeConRef) -> Projection()), - Some(party2) -> Map(Some(otherTemplateIdFull.toNameTypeConRef) -> Projection()), - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val activeContracts = result.toArray - activeContracts should have length 2 - - val create2 = activeContracts(0) - create2.witnessParties.loneElement shouldBe party1 - create2.templateId.value shouldBe LfEngineToApi.toApiIdentifier(someTemplateId) - - val create1 = activeContracts(1) - create1.witnessParties.loneElement shouldBe party2 - create1.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - } - - it should "filter correctly by party-wildcard with different templates" in { - val party1 = Party.assertFromString(UUID.randomUUID.toString) - val party2 = Party.assertFromString(UUID.randomUUID.toString) - for { - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (party1, otherTemplateId3, someContractArgument), - (party2, otherTemplateId4, otherContractArgument), - (party1, otherTemplateId4, otherContractArgument), - ), - ) - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - otherTemplateId3Full.toNameTypeConRef -> None, - otherTemplateId4Full.toNameTypeConRef -> None, - ), - Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = false - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val activeContracts = result.toArray - activeContracts should have length 3 - - val create2 = activeContracts(0) - create2.witnessParties.loneElement shouldBe party1 - create2.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId3) - - val create1 = activeContracts(1) - create1.witnessParties.loneElement shouldBe party2 - create1.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId4) - } - } - - it should "filter correctly by multiple parties with different template and wildcards" in { - val party1 = Party.assertFromString(UUID.randomUUID.toString) - val party2 = Party.assertFromString(UUID.randomUUID.toString) - for { - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (party1, someTemplateId, someContractArgument), - (party2, otherTemplateId, otherContractArgument), - (party1, otherTemplateId, otherContractArgument), - ), - ) - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - someTemplateIdFull.toNameTypeConRef -> Some(Set(party1)) - ), - Some(Set(party2)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map(Some(someTemplateIdFull.toNameTypeConRef) -> Projection()) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val activeContracts = result.toArray - activeContracts should have length 2 - - val create2 = activeContracts(0) - create2.witnessParties.loneElement shouldBe party1 - create2.templateId.value shouldBe LfEngineToApi.toApiIdentifier(someTemplateId) - - val create1 = activeContracts(1) - create1.witnessParties.loneElement shouldBe party2 - create1.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - } - - it should "filter correctly by multiple parties with different template and party- and template- wildcards" in { - val party1 = Party.assertFromString(UUID.randomUUID.toString) - val party2 = Party.assertFromString(UUID.randomUUID.toString) - for { - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (party1, otherTemplateId5, otherContractArgument5), - (party2, otherTemplateId, otherContractArgument), - (party1, otherTemplateId, otherContractArgument), - ), - ) - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - relation = Map( - otherTemplateId5Full.toNameTypeConRef -> None - ), - templateWildcardParties = Some(Set(party2)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map(Some(otherTemplateId5Full.toNameTypeConRef) -> Projection()) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val activeContracts = result.toArray - activeContracts should have length 2 - - val create2 = activeContracts(0) - create2.witnessParties.loneElement shouldBe party1 - create2.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId5) - - val create1 = activeContracts(1) - create1.witnessParties.loneElement shouldBe party2 - create1.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - } - - it should "filter correctly with unknown parties and templates" in { - val party1 = Party.assertFromString(UUID.randomUUID.toString) - val party2 = Party.assertFromString(UUID.randomUUID.toString) - - // Adding an unknown party and/or template to the filter should not - // affect the results - val unknownParty = Party.assertFromString(UUID.randomUUID.toString) - val unknownTemplate = Identifier.assertFromString("pkg:Mod:Template") - val unknownTemplateFull = - unknownTemplate.toFullIdentifier(Ref.PackageName.assertFromString("pkg-name")) - - for { - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (party1, someTemplateId, someContractArgument), - (party2, otherTemplateId, otherContractArgument), - (party1, otherTemplateId, otherContractArgument), - ), - ) - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - someTemplateIdFull.toNameTypeConRef -> Some(Set(party1)) - ), - Some(Set(party2)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map(Some(someTemplateIdFull.toNameTypeConRef) -> Projection()) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - resultUnknownParty <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - someTemplateIdFull.toNameTypeConRef -> Some(Set(party1)) - ), - Some(Set(party2, unknownParty)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map(Some(someTemplateIdFull.toNameTypeConRef) -> Projection()) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - resultUnknownTemplate <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - someTemplateIdFull.toNameTypeConRef -> Some(Set(party1)), - unknownTemplateFull.toNameTypeConRef -> Some(Set(party1)), - ), - Some(Set(party2)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map( - Some(someTemplateIdFull.toNameTypeConRef) -> Projection(), - Some(unknownTemplateFull.toNameTypeConRef) -> Projection(), - ) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - resultUnknownTemplatePartyWildcard <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - relation = Map( - someTemplateIdFull.toNameTypeConRef -> Some(Set(party1)), - unknownTemplateFull.toNameTypeConRef -> None, - ), - templateWildcardParties = Some(Set(party2)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map( - Some(someTemplateIdFull.toNameTypeConRef) -> Projection() - ), - None -> Map( - Some(unknownTemplateFull.toNameTypeConRef) -> Projection() - ), - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - resultUnknownPartyAndTemplate <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - someTemplateIdFull.toNameTypeConRef -> Some(Set(party1)), - unknownTemplateFull.toNameTypeConRef -> Some(Set(party1)), - ), - Some(Set(party2, unknownParty)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(party1) -> Map( - Some(someTemplateIdFull.toNameTypeConRef) -> Projection(), - Some(unknownTemplateFull.toNameTypeConRef) -> Projection(), - ) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - resultUnknownsOnly <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter( - Map( - unknownTemplateFull.toNameTypeConRef -> Some(Set(unknownParty)) - ), - Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true, - witnessTemplateProjections = Map( - Some(unknownParty) -> Map( - Some(unknownTemplateFull.toNameTypeConRef) -> Projection() - ) - ), - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - result should have length 2 - resultUnknownParty should contain theSameElementsAs result - resultUnknownTemplate should contain theSameElementsAs result - resultUnknownTemplatePartyWildcard should contain theSameElementsAs result - resultUnknownPartyAndTemplate should contain theSameElementsAs result - - resultUnknownsOnly shouldBe empty - } - } - - it should "not set the offset" in { - for { - _ <- store(singleCreate) - _ <- store(singleCreate) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - activeContracts <- ledgerDao.updateReader - .getActiveContracts( - activeAt = ledgerEnd.map(_.lastOffset), - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - .runWith(Sink.seq) - - } yield { - activeContracts should not be empty - } - } - - it should "serve the correct events" in { - for { - before <- ledgerDao.lookupLedgerEnd() - (offset1, t1) <- store(singleCreate) - after <- ledgerDao.lookupLedgerEnd() - activeContractsBefore <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = before.map(_.lastOffset), - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice, bob, charlie))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - activeContractsAfter <- activeContractsOf( - ledgerDao.updateReader - .getActiveContracts( - activeAt = after.map(_.lastOffset), - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice, bob, charlie))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - rangeInfo = AcsRangeInfo.empty, - ) - ) - } yield { - val activeContract = activeContractsAfter.toSet.diff(activeContractsBefore.toSet).loneElement - activeContract.offset shouldBe offset1.unwrap - activeContract.nodeId shouldBe 0 - } - } - - private def activeContractsOf( - source: Source[GetActiveContractsResponse, NotUsed] - ): Future[Seq[CreatedEvent]] = - source.runWith(Sink.seq).map(_.flatMap(_.contractEntry.activeContract.flatMap(_.createdEvent))) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackend.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackend.scala deleted file mode 100644 index 182876acff..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackend.scala +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner} -import com.daml.metrics.api.noop.NoOpMetricsFactory -import com.daml.metrics.api.{HistogramInventory, MetricName} -import com.daml.resources.PureResource -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.withNewLoggingContext -import com.digitalasset.canton.logging.SuppressingLogger -import com.digitalasset.canton.metrics.{LedgerApiServerHistograms, LedgerApiServerMetrics} -import com.digitalasset.canton.participant.store.memory.InMemoryContractStore -import com.digitalasset.canton.platform.config.{ - ActiveContractsServiceStreamsConfig, - ServerRole, - UpdatesStreamsConfig, -} -import com.digitalasset.canton.platform.store.DbSupport.{ConnectionPoolConfig, DbConfig} -import com.digitalasset.canton.platform.store.backend.StorageBackendFactory -import com.digitalasset.canton.platform.store.cache.{AchsStateCache, MutableLedgerEndCache} -import com.digitalasset.canton.platform.store.dao.JdbcLedgerDaoBackend.TestParticipantId -import com.digitalasset.canton.platform.store.dao.events.{ - CompressionStrategy, - ContractLoader, - LfValueTranslation, -} -import com.digitalasset.canton.platform.store.interning.StringInterningView -import com.digitalasset.canton.platform.store.{ - DbSupport, - DbType, - FlywayMigrations, - LedgerApiContractStoreImpl, -} -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.archive.DamlLf.Archive -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.engine.{Engine, EngineConfig} -import com.digitalasset.daml.lf.language.LanguageVersion -import io.opentelemetry.api.OpenTelemetry -import org.scalatest.Suite - -import scala.concurrent.duration.DurationInt -import scala.concurrent.{Await, ExecutionContext, Future} - -object JdbcLedgerDaoBackend { - - private val TestParticipantIdRef = - Ref.ParticipantId.assertFromString("test-participant") - - private val TestParticipantId: ParticipantId = - ParticipantId(TestParticipantIdRef) - -} - -private[dao] trait JdbcLedgerDaoBackend extends PekkoBeforeAndAfterAll with BaseTest { - self: Suite => - - // AsyncFlatSpec is with serial execution context - private implicit val ec: ExecutionContext = system.dispatcher - - protected def dbType: DbType - - protected def jdbcUrl: String - - protected def loadPackage: Ref.PackageId => Future[Option[Archive]] - - protected def daoOwner( - eventsPageSize: Int, - eventsProcessingParallelism: Int, - acsIdPageSize: Int, - acsIdFetchingParallelism: Int, - acsContractFetchingParallelism: Int, - ): ResourceOwner[LedgerDao] = { - val loggerFactory: SuppressingLogger = SuppressingLogger(getClass) - implicit val traceContext: TraceContext = TraceContext.empty - val metrics = - new LedgerApiServerMetrics( - new LedgerApiServerHistograms(MetricName("test"))(new HistogramInventory()), - NoOpMetricsFactory, - ) - val dbType = DbType.jdbcType(jdbcUrl) - val storageBackendFactory = StorageBackendFactory.of(dbType, loggerFactory) - val dbConfig = DbConfig( - jdbcUrl, - connectionPool = ConnectionPoolConfig( - connectionPoolSize = 16, - connectionTimeout = 250.millis, - ), - ) - for { - _ <- new ResourceOwner[Unit] { - override def acquire()(implicit context: ResourceContext): Resource[Unit] = - PureResource( - new FlywayMigrations(dbConfig.jdbcUrl, loggerFactory = loggerFactory)( - ec, - traceContext, - ).migrate() - ) - } - dbSupport <- DbSupport.owner( - serverRole = ServerRole.Testing(getClass), - metrics = metrics, - dbConfig = dbConfig, - loggerFactory = loggerFactory, - ) - contractLoader <- ContractLoader.create( - participantContractStore = contractStore, - contractStorageBackend = dbSupport.storageBackendFactory.createContractStorageBackend( - stringInterningView, - ledgerEndCache, - ), - dbDispatcher = dbSupport.dbDispatcher, - metrics = metrics, - // not making these configuration is only needed in canton. here we populating with sensible defaults - maxQueueSize = 10000, - maxBatchSize = 50, - parallelism = 5, - loggerFactory = loggerFactory, - ) - } yield { - val engine = Some( - new Engine(EngineConfig(LanguageVersion.stableLfVersions), loggerFactory) - ) - new JdbcLedgerWriteDao( - dbDispatcher = dbSupport.dbDispatcher, - sequentialIndexer = SequentialWriteDao( - participantId = JdbcLedgerDaoBackend.TestParticipantIdRef, - metrics = metrics, - compressionStrategy = CompressionStrategy.none(metrics), - ledgerEndCache = ledgerEndCache, - stringInterningView = stringInterningView, - ingestionStorageBackend = storageBackendFactory.createIngestionStorageBackend, - parameterStorageBackend = - storageBackendFactory.createParameterStorageBackend(stringInterningView), - loggerFactory = loggerFactory, - ), - queryExecutionContext = ec, - commandExecutionContext = ec, - metrics = metrics, - participantId = JdbcLedgerDaoBackend.TestParticipantIdRef, - readStorageBackend = dbSupport.storageBackendFactory - .readStorageBackend(ledgerEndCache, stringInterningView, loggerFactory), - parameterStorageBackend = - dbSupport.storageBackendFactory.createParameterStorageBackend(stringInterningView), - ledgerEndCache = ledgerEndCache, - completionsPageSize = 1000, - activeContractsServiceStreamsConfig = ActiveContractsServiceStreamsConfig( - maxPayloadsPerPayloadsPage = eventsPageSize, - maxIdsPerIdPage = acsIdPageSize, - maxPagesPerIdPagesBuffer = 1, - maxWorkingMemoryInBytesForIdPages = 100 * 1024 * 1024, - maxParallelActiveIdQueries = acsIdFetchingParallelism, - maxParallelPayloadCreateQueries = acsContractFetchingParallelism, - contractProcessingParallelism = eventsProcessingParallelism, - ), - updatesStreamsConfig = UpdatesStreamsConfig.default, - globalMaxEventIdQueries = 20, - globalMaxEventPayloadQueries = 10, - tracer = OpenTelemetry.noop().getTracer("test"), - loggerFactory = loggerFactory, - incompleteOffsets = (_, _, _) => FutureUnlessShutdown.pure(Vector.empty), - contractLoader = contractLoader, - lfValueTranslation = new LfValueTranslation( - metrics = metrics, - engineO = engine, - loadPackage = (packageId, _) => loadPackage(packageId), - loggerFactory = loggerFactory, - ), - contractStore = contractStore, - achsStateCache = new AchsStateCache(loggerFactory), - scheduler = system.scheduler, - ) - } - } - - type LedgerDao = LedgerReadDao & LedgerWriteDao - - protected final var ledgerDao: LedgerDao = _ - protected var ledgerEndCache: MutableLedgerEndCache = _ - protected var contractStore: LedgerApiContractStoreImpl = _ - protected var stringInterningView: StringInterningView = _ - - // `dbDispatcher` and `ledgerDao` depend on the `postgresFixture` which is in turn initialized `beforeAll` - private var resource: Resource[LedgerDao] = _ - - override protected def beforeAll(): Unit = { - super.beforeAll() - // We use the dispatcher here because the default Scalatest execution context is too slow. - implicit val resourceContext: ResourceContext = ResourceContext(system.dispatcher) - ledgerEndCache = MutableLedgerEndCache() - val inMemoryContractStore = new InMemoryContractStore(timeouts, loggerFactory) - contractStore = LedgerApiContractStoreImpl( - inMemoryContractStore, - loggerFactory, - LedgerApiServerMetrics.ForTesting, - ) - stringInterningView = new StringInterningView(loggerFactory) - resource = withNewLoggingContext() { implicit loggingContext => - for { - dao <- daoOwner( - eventsPageSize = 4, - eventsProcessingParallelism = 4, - acsIdPageSize = 4, - acsIdFetchingParallelism = 2, - acsContractFetchingParallelism = 2, - ).acquire() - _ <- Resource.fromFuture(dao.initialize(TestParticipantId)) - initialLedgerEnd <- Resource.fromFuture(dao.lookupLedgerEnd()) - _ = ledgerEndCache.set(initialLedgerEnd) - } yield dao - }(TraceContext.empty) - ledgerDao = Await.result(resource.asFuture, 180.seconds) - } - - override protected def afterAll(): Unit = { - Await.result(resource.release(), 10.seconds) - super.afterAll() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackendH2Database.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackendH2Database.scala deleted file mode 100644 index f099924194..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackendH2Database.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.platform.store.DbType -import org.scalatest.AsyncTestSuite - -private[dao] trait JdbcLedgerDaoBackendH2Database extends JdbcLedgerDaoBackend { - this: AsyncTestSuite => - - override protected val dbType: DbType = DbType.H2Database - - override protected val jdbcUrl: String = - s"jdbc:h2:mem:${getClass.getSimpleName.toLowerCase};db_close_delay=-1" -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackendPostgresql.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackendPostgresql.scala deleted file mode 100644 index 883aa003d7..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoBackendPostgresql.scala +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.platform.store.DbType -import com.digitalasset.canton.platform.store.testing.postgresql.PostgresAroundAll -import org.scalatest.AsyncTestSuite - -private[dao] trait JdbcLedgerDaoBackendPostgresql - extends JdbcLedgerDaoBackend - with PostgresAroundAll { - this: AsyncTestSuite => - - override protected val dbType: DbType = DbType.Postgres - - override protected def jdbcUrl: String = postgresDatabase.url -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoCompletionsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoCompletionsSpec.scala deleted file mode 100644 index 39b253bc4d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoCompletionsSpec.scala +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.api.v2.command_completion_service.CompletionStreamResponse -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.data.Offset.firstOffset -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.platform.store.dao.JdbcLedgerDaoCompletionsSpec.* -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.google.rpc.status.Status as RpcStatus -import io.grpc.Status -import org.apache.pekko.stream.scaladsl.Sink -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{LoneElement, OptionValues} - -import java.util.UUID -import scala.concurrent.Future - -private[dao] trait JdbcLedgerDaoCompletionsSpec extends OptionValues with LoneElement { - this: AsyncFlatSpec with Matchers with JdbcLedgerDaoSuite => - - behavior of "JdbcLedgerDao (completions)" - - it should "return the expected completion for an accepted transaction" in { - for { - from <- ledgerDao.lookupLedgerEnd() - (offset, tx) <- store(singleCreate) - to <- ledgerDao.lookupLedgerEnd() - (_, response) <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - tx.userId.value, - tx.actAs.toSet, - ) - .runWith(Sink.head) - } yield { - offsetOf(response) shouldBe offset - - val completion = response.completionResponse.completion.toList.head - - completion.updateId shouldBe tx.updateId.toHexString - completion.commandId shouldBe tx.commandId.value - completion.status.value.code shouldBe io.grpc.Status.Code.OK.value() - } - } - - it should "return the expected completion for an accepted multi-party transaction" in { - for { - from <- ledgerDao.lookupLedgerEnd() - (_, tx) <- store(multiPartySingleCreate) - to <- ledgerDao.lookupLedgerEnd() - // Response 1: querying as all submitters - (_, response1) <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - tx.userId.value, - tx.actAs.toSet, - ) - .runWith(Sink.head) - // Response 2: querying as a proper subset of all submitters - (_, response2) <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - tx.userId.value, - Set(tx.actAs.head), - ) - .runWith(Sink.head) - // Response 3: querying as a proper superset of all submitters - (_, response3) <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - tx.userId.value, - tx.actAs.toSet + "UNRELATED", - ) - .runWith(Sink.head) - } yield { - response1.completionResponse.completion.toList.head.commandId shouldBe tx.commandId.value - response2.completionResponse.completion.toList.head.commandId shouldBe tx.commandId.value - response3.completionResponse.completion.toList.head.commandId shouldBe tx.commandId.value - } - } - - it should "return the expected completion for a rejection" in { - val paidTrafficCost = NonNegativeLong.tryCreate(54354) - val expectedCmdId = UUID.randomUUID.toString - val rejection = new state.Update.CommandRejected.FinalReason( - RpcStatus.of(Status.Code.ABORTED.value(), "Stop.", Seq.empty) - ) - for { - from <- ledgerDao.lookupLedgerEnd() - offset <- storeRejection(rejection, expectedCmdId, paidTrafficCost = paidTrafficCost) - to <- ledgerDao.lookupLedgerEnd() - (_, response) <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - parties, - ) - .runWith(Sink.head) - } yield { - offsetOf(response) shouldBe offset - - val completion = response.completionResponse.completion.toList.head - - completion.updateId shouldBe empty - completion.commandId shouldBe expectedCmdId - completion.status shouldBe Some(rejection.status) - completion.paidTrafficCost shouldBe paidTrafficCost.value - } - } - - it should "return the expected completion for a multi-party rejection" in { - val expectedCmdId = UUID.randomUUID.toString - val rejection = new state.Update.CommandRejected.FinalReason( - RpcStatus.of(Status.Code.ALREADY_EXISTS.value(), "No thanks.", Seq.empty) - ) - for { - from <- ledgerDao.lookupLedgerEnd() - _ <- storeMultiPartyRejection(rejection, expectedCmdId) - to <- ledgerDao.lookupLedgerEnd() - // Response 1: querying as all submitters - (_, response1) <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - parties, - ) - .runWith(Sink.head) - // Response 2: querying as a proper subset of all submitters - (_, response2) <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - Set(parties.head), - ) - .runWith(Sink.head) - // Response 3: querying as a proper superset of all submitters - (_, response3) <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - parties + "UNRELATED", - ) - .runWith(Sink.head) - } yield { - response1.completionResponse.completion.toList.head.commandId shouldBe expectedCmdId - response2.completionResponse.completion.toList.head.commandId shouldBe expectedCmdId - response3.completionResponse.completion.toList.head.commandId shouldBe expectedCmdId - } - } - - it should "not return completions if the user id is wrong" in { - val rejection = new state.Update.CommandRejected.FinalReason( - RpcStatus.of(Status.Code.INTERNAL.value(), "Internal error.", Seq.empty) - ) - for { - from <- ledgerDao.lookupLedgerEnd() - _ <- storeRejection(rejection) - to <- ledgerDao.lookupLedgerEnd() - response <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId = Ref.UserId.assertFromString("WRONG"), - parties, - ) - .runWith(Sink.seq) - } yield { - response shouldBe Seq.empty - } - } - - it should "not return completions if the parties do not match" in { - val rejection = new state.Update.CommandRejected.FinalReason( - RpcStatus.of(Status.Code.OUT_OF_RANGE.value(), "Too far.", Seq.empty) - ) - for { - from <- ledgerDao.lookupLedgerEnd() - _ <- storeRejection(rejection) - to <- ledgerDao.lookupLedgerEnd() - response1 <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - Set("WRONG"), - ) - .runWith(Sink.seq) - response2 <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - Set("WRONG1", "WRONG2", "WRONG3"), - ) - .runWith(Sink.seq) - } yield { - response1 shouldBe Seq.empty - response2 shouldBe Seq.empty - } - } - - it should "not return completions if the parties do not match (multi-party submission)" in { - val rejection = new state.Update.CommandRejected.FinalReason( - RpcStatus.of(Status.Code.PERMISSION_DENIED.value(), "Forbidden.", Seq.empty) - ) - for { - from <- ledgerDao.lookupLedgerEnd() - _ <- storeMultiPartyRejection(rejection) - to <- ledgerDao.lookupLedgerEnd() - response1 <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - Set("WRONG"), - ) - .runWith(Sink.seq) - response2 <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - Set("WRONG1", "WRONG2", "WRONG3"), - ) - .runWith(Sink.seq) - } yield { - response1 shouldBe Seq.empty - response2 shouldBe Seq.empty - } - } - - it should "allow arbitrarily large rejection reasons" in { - val rejection = new state.Update.CommandRejected.FinalReason( - RpcStatus.of(Status.Code.ABORTED.value(), (0 to 3999).map(_ => " ").mkString(""), Seq.empty) - ) - for { - from <- ledgerDao.lookupLedgerEnd() - _ <- storeMultiPartyRejection(rejection) - to <- ledgerDao.lookupLedgerEnd() - response1 <- ledgerDao.completions - .getCommandCompletions( - from.fold(firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - userId, - Set("WRONG"), - ) - .runWith(Sink.seq) - } yield { - response1 shouldBe Seq.empty - } - } - - private def storeRejection( - reason: state.Update.CommandRejected.RejectionReasonTemplate, - commandId: Ref.CommandId = UUID.randomUUID().toString, - submissionId: Ref.SubmissionId = UUID.randomUUID().toString, - paidTrafficCost: NonNegativeLong = NonNegativeLong.zero, - ): Future[Offset] = { - val offset = nextOffset() - ledgerDao - .storeRejection( - completionInfo = Some( - state.CompletionInfo( - actAs = List(party1), - userId = userId, - commandId = commandId, - optDeduplicationPeriod = None, - submissionId = Some(submissionId), - paidTrafficCost = paidTrafficCost, - ) - ), - recordTime = Timestamp.now(), - offset, - reason = reason, - ) - .map(_ => offset) - } - - private def storeMultiPartyRejection( - reason: state.Update.CommandRejected.RejectionReasonTemplate, - commandId: Ref.CommandId = UUID.randomUUID().toString, - submissionId: Ref.SubmissionId = UUID.randomUUID().toString, - ): Future[Offset] = { - lazy val offset = nextOffset() - ledgerDao - .storeRejection( - completionInfo = Some( - state.CompletionInfo( - actAs = List(party1, party2, party3), - userId = userId, - commandId = commandId, - optDeduplicationPeriod = None, - submissionId = Some(submissionId), - paidTrafficCost = NonNegativeLong.zero, - ) - ), - recordTime = Timestamp.now(), - offset, - reason = reason, - ) - .map(_ => offset) - } -} - -private[dao] object JdbcLedgerDaoCompletionsSpec { - - private val userId = Ref.UserId.assertFromString("JdbcLedgerDaoCompletionsSpec") - private val party1 = Ref.Party.assertFromString("JdbcLedgerDaoCompletionsSpec1") - private val party2 = Ref.Party.assertFromString("JdbcLedgerDaoCompletionsSpec2") - private val party3 = Ref.Party.assertFromString("JdbcLedgerDaoCompletionsSpec3") - private val parties = Set(party1, party2, party3) - - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) - private def offsetOf(response: CompletionStreamResponse): Offset = - Offset.tryFromLong( - response.completionResponse.completion.get.offset - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoContractsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoContractsSpec.scala deleted file mode 100644 index 307cce9ce3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoContractsSpec.scala +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import cats.syntax.parallel.* -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.{Active, Archived} -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader -import com.digitalasset.canton.platform.store.interfaces.LedgerDaoContractsReader.{ - KeyAssigned, - KeyState, - KeyUnassigned, -} -import com.digitalasset.canton.util.FutureInstances.* -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.transaction.{GlobalKey, GlobalKeyWithMaintainers} -import com.digitalasset.daml.lf.value.Value.{ContractId, ValueText} -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, LoneElement, OptionValues} - -import scala.concurrent.Future - -private[dao] trait JdbcLedgerDaoContractsSpec extends LoneElement with Inside with OptionValues { - this: AsyncFlatSpec with Matchers with JdbcLedgerDaoSuite => - - private def contractsReader = ledgerDao.contractsReader - - behavior of "JdbcLedgerDao (contracts)" - - it should "be able to persist and load contracts with the right visibility" in { - for { - (offset, tx) <- createAndStoreContract( - submittingParties = Set(alice), - signatories = Set(alice, bob), - stakeholders = Set(alice, bob), - key = None, - ) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - result <- contractsReader.lookupContractState( - nonTransient(tx).loneElement, - ledgerEnd.map(_.lastEventSeqId).getOrElse(0L), - ) - } yield { - result shouldBe Some(Active) - } - } - - it should "store contracts with a transient contract in the global divulgence and do not fetch it" in { - for { - (_, tx) <- store(fullyTransientWithChildren, contractActivenessChanged = false) - ledgerEnd <- ledgerDao.lookupLedgerEnd() - contractId1 = created(tx).head - contractId2 = created(tx).tail.loneElement - result1 <- contractsReader.lookupContractState( - contractId1, - ledgerEnd.map(_.lastEventSeqId).getOrElse(0L), - ) - result2 <- contractsReader.lookupContractState( - contractId2, - ledgerEnd.map(_.lastEventSeqId).getOrElse(0L), - ) - } yield { - result1 shouldBe empty - result2 shouldBe empty - } - } - - it should "present the contract state at a specific event sequential id" in { - for { - (_, tx) <- store(singleCreate(create(signatories = Set(alice)))) - contractId = nonTransient(tx).loneElement - _ <- store(singleNonConsumingExercise(contractId)) - Some(ledgerEndAtCreate) <- ledgerDao.lookupLedgerEnd() - _ <- store(txArchiveContract(alice, (contractId, None))) - Some(ledgerEndAfterArchive) <- ledgerDao.lookupLedgerEnd() - queryAfterCreate <- contractsReader.lookupContractState( - contractId, - ledgerEndAtCreate.lastEventSeqId, - ) - queryAfterArchive <- contractsReader.lookupContractState( - contractId, - ledgerEndAfterArchive.lastEventSeqId, - ) - } yield { - queryAfterCreate.value shouldBe Active - queryAfterArchive.value shouldBe Archived - } - } - - it should "present the contract key state at a specific event sequential id" in { - val aTextValue = ValueText(scala.util.Random.nextString(10)) - - val key = GlobalKeyWithMaintainers.assertBuild( - someTemplateId, - aTextValue, - crypto.Hash.hashPrivateKey("dummy-key-hash"), - Set(alice, bob), - somePackageName, - ) - - for { - (_, tx) <- createAndStoreContract( - submittingParties = Set(alice), - signatories = Set(alice, bob), - stakeholders = Set(alice, bob), - key = Some(key), - ) - contractId = nonTransient(tx).loneElement - _ <- store(singleNonConsumingExercise(contractId)) - ledgerEndAtCreate <- ledgerDao.lookupLedgerEnd() - _ <- store(txArchiveContract(alice, (contractId, None))) - ledgerEndAfterArchive <- ledgerDao.lookupLedgerEnd() - queryAfterCreate <- contractsReader.lookupKeyState( - key.globalKey, - ledgerEndAtCreate.value.lastEventSeqId, - ) - queryAfterArchive <- contractsReader.lookupKeyState( - key.globalKey, - ledgerEndAfterArchive.value.lastEventSeqId, - ) - } yield { - queryAfterCreate match { - case LedgerDaoContractsReader.KeyAssigned(fetchedContractId) => - fetchedContractId shouldBe contractId - case _ => fail("Key should be assigned") - } - queryAfterArchive shouldBe LedgerDaoContractsReader.KeyUnassigned - } - } - - it should "support batch reading contract keys at the right offset" in { - - def genContractWithKey(string: String = scala.util.Random.nextString(5)) = { - val aTextValue = ValueText(string) - val key = GlobalKeyWithMaintainers.assertBuild( - someTemplateId, - aTextValue, - crypto.Hash.hashPrivateKey(string), - Set(alice), - somePackageName, - ) - createAndStoreContract( - submittingParties = Set(alice), - signatories = Set(alice, bob), - stakeholders = Set(alice, bob), - key = Some(key), - ).map { case (offset, entry) => - val contractId = nonTransient(entry).loneElement - (aTextValue, key, contractId, offset) - } - } - - def fetchAll( - keys: Seq[GlobalKey], - eventSeqId: Long, - ): Future[(Map[GlobalKey, KeyState], Map[GlobalKey, KeyState])] = { - val oneByOneF = keys - .parTraverse { key => - contractsReader.lookupKeyState(key, eventSeqId).map(state => key -> state) - } - .map(_.toMap) - val togetherF = contractsReader - .lookupKeyStatesFromDb(keys, eventSeqId) - .flatMap(resultsWithInternalContractIds => - contractStore - .lookupBatchedContractIdsNonReadThrough(resultsWithInternalContractIds.values) - .map(internalToContractIds => - keys.map { key => - key -> resultsWithInternalContractIds - .get(key) - .flatMap(internalToContractIds.get) - .map(KeyAssigned.apply) - .getOrElse(KeyUnassigned) - }.toMap - ) - ) - for { - oneByOne <- oneByOneF - together <- togetherF - } yield (oneByOne, together) - } - - def verifyMatch( - results: (Map[GlobalKey, KeyState], Map[GlobalKey, KeyState]), - expected: Map[GlobalKeyWithMaintainers, Option[ContractId]], - ) = { - val (oneByOne, together) = results - oneByOne shouldBe together - oneByOne.map { - case (k, KeyAssigned(cid)) => (k, Some(cid)) - case (k, KeyUnassigned) => (k, None) - } shouldBe expected.map { case (k, v) => (k.globalKey, v) } - } - - for { - // have AA at offsetA - (textA, keyA, cidA, _) <- genContractWithKey() - eventSeqIdA <- ledgerDao.lookupLedgerEnd().map(_.value.lastEventSeqId) - _ <- store(singleNonConsumingExercise(cidA)) - // have AA,BB at offsetB - (_, keyB, cidB, _) <- genContractWithKey() - eventSeqIdB <- ledgerDao.lookupLedgerEnd().map(_.value.lastEventSeqId) - // have BB at offsetPreC - (_, _) <- store(txArchiveContract(alice, (cidA, None))) - eventSeqIdPreC <- ledgerDao.lookupLedgerEnd().map(_.value.lastEventSeqId) - // have BB, CC at offsetC - (_, keyC, cidC, _) <- genContractWithKey() - eventSeqIdC <- ledgerDao.lookupLedgerEnd().map(_.value.lastEventSeqId) - // have AA, BB, CC at offsetA2 - (_, keyA2, cidA2, _) <- genContractWithKey(textA.value) - eventSeqIdA2 <- ledgerDao.lookupLedgerEnd().map(_.value.lastEventSeqId) - // have AA, BB at offset D - (_, _) <- store(txArchiveContract(alice, (cidC, None))) - eventSeqIdD <- ledgerDao.lookupLedgerEnd().map(_.value.lastEventSeqId) - // have AA at offsetE - (_, _) <- store(txArchiveContract(alice, (cidB, None))) - eventSeqIdE <- ledgerDao.lookupLedgerEnd().map(_.value.lastEventSeqId) - allKeys = Seq(keyA, keyB, keyC).map(_.globalKey) - atEventSeqIdA <- fetchAll(allKeys, eventSeqIdA) - atEventSeqIdB <- fetchAll(allKeys, eventSeqIdB) - atEventSeqIdPreC <- fetchAll(allKeys, eventSeqIdPreC) - atEventSeqIdC <- fetchAll(allKeys, eventSeqIdC) - atEventSeqIdA2 <- fetchAll(allKeys, eventSeqIdA2) - atEventSeqIdD <- fetchAll(allKeys, eventSeqIdD) - atEventSeqIdE <- fetchAll(allKeys, eventSeqIdE) - } yield { - keyA shouldBe keyA2 - verifyMatch(atEventSeqIdA, Map(keyA -> Some(cidA), keyB -> None, keyC -> None)) - verifyMatch(atEventSeqIdB, Map(keyA -> Some(cidA), keyB -> Some(cidB), keyC -> None)) - verifyMatch(atEventSeqIdPreC, Map(keyA -> None, keyB -> Some(cidB), keyC -> None)) - verifyMatch(atEventSeqIdC, Map(keyA -> None, keyB -> Some(cidB), keyC -> Some(cidC))) - verifyMatch(atEventSeqIdA2, Map(keyA -> Some(cidA2), keyB -> Some(cidB), keyC -> Some(cidC))) - verifyMatch(atEventSeqIdD, Map(keyA -> Some(cidA2), keyB -> Some(cidB), keyC -> None)) - verifyMatch(atEventSeqIdE, Map(keyA -> Some(cidA2), keyB -> None, keyC -> None)) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoExceptionSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoExceptionSpec.scala deleted file mode 100644 index 50fc2887e4..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoExceptionSpec.scala +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.ledger.participant.state.index.ContractStateStatus.Active -import com.digitalasset.daml.lf.transaction.test.TreeTransactionBuilder.* -import com.digitalasset.daml.lf.transaction.test.{ - TestIdFactory, - TestNodeBuilder, - TreeTransactionBuilder, -} -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, LoneElement, OptionValues} - -/** There are two important parts to cover with Daml exceptions: - * - Create and exercise nodes under rollback nodes should not be indexed - * - Lookup and fetch nodes under rollback nodes may lead to divulgence - */ -private[dao] trait JdbcLedgerDaoExceptionSpec - extends LoneElement - with Inside - with OptionValues - with TestIdFactory { - this: AsyncFlatSpec with Matchers with JdbcLedgerDaoSuite => - - private def contractsReader = ledgerDao.contractsReader - - behavior of "JdbcLedgerDao (exceptions)" - - it should "not find contracts created under rollback nodes" in { - - val createNode1 = createNode( - signatories = Set(alice), - stakeholders = Set(alice), - ) - val createNode2 = createNode( - signatories = Set(alice), - stakeholders = Set(alice), - ) - - val cid1 = createNode1.coid - val cid2 = createNode2.coid - - val tx = TreeTransactionBuilder.toCommittedTransaction( - TestNodeBuilder - .rollback() - .withChildren(createNode1), - createNode2, - ) - val offsetAndEntry = fromTransaction(tx) - - for { - (_, _) <- store(offsetAndEntry) - eventSeqId <- ledgerDao.lookupLedgerEnd().map(_.value.lastEventSeqId) - result1 <- contractsReader.lookupContractState(cid1, eventSeqId) - result2 <- contractsReader.lookupContractState(cid2, eventSeqId) - } yield { - result1 shouldBe None - result2.value shouldBe Active - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoPartiesSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoPartiesSpec.scala deleted file mode 100644 index 65458fa5b9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoPartiesSpec.scala +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.util.MonadUtil -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Time.Timestamp -import org.scalatest.OptionValues -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util.UUID - -private[dao] trait JdbcLedgerDaoPartiesSpec { - this: AsyncFlatSpec with Matchers with JdbcLedgerDaoSuite with OptionValues => - - behavior of "JdbcLedgerDao (parties)" - - it should "store and retrieve all parties" in { - val alice = IndexerPartyDetails( - party = Ref.Party.assertFromString(s"Alice-${UUID.randomUUID()}"), - isLocal = true, - ) - val bob = IndexerPartyDetails( - party = Ref.Party.assertFromString(s"Bob-${UUID.randomUUID()}"), - isLocal = true, - ) - for { - response <- storePartyEntry(alice, nextOffset()) - _ = response should be(PersistenceResponse.Ok) - response <- storePartyEntry(bob, nextOffset()) - _ = response should be(PersistenceResponse.Ok) - parties <- ledgerDao.listKnownParties(None, filterParty = None, 1000) - } yield { - parties should contain.allOf(alice, bob) - } - } - - it should "filter parties" in { - val randomSuffix = UUID.randomUUID() - val alice = IndexerPartyDetails( - party = "FilterAlice::" + randomSuffix, - isLocal = true, - ) - val bob = IndexerPartyDetails( - party = "FilterBob::" + randomSuffix, - isLocal = true, - ) - val storeF = MonadUtil.sequentialTraverse_( - Seq(alice, bob) - )(storePartyEntry(_, nextOffset())) - for { - _ <- storeF - onlyAlicePartial <- ledgerDao.listKnownParties( - None, - filterParty = Some(String185.tryCreate("FilterAlice")), - 1000, - ) - onlyAliceFull <- ledgerDao.listKnownParties( - None, - filterParty = Some(String185.tryCreate(alice.party)), - 1000, - ) - bothFilter <- ledgerDao.listKnownParties( - None, - filterParty = Some(String185.tryCreate("Filter")), - 1000, - ) - } yield { - onlyAlicePartial shouldBe List(alice) - onlyAliceFull shouldBe List(alice) - bothFilter shouldBe List(alice, bob) - } - } - - it should "retrieve all parties in two chunks" in { - val randomSuffix = UUID.randomUUID() - def genParty(name: String) = - IndexerPartyDetails( - party = Ref.Party.assertFromString(s"$name-$randomSuffix"), - isLocal = true, - ) - val newParties = List("Wes", "Zeb", "Les", "Mel").map(genParty) - - for { - partiesBefore <- ledgerDao.listKnownParties(None, filterParty = None, 1000) - _ <- MonadUtil.sequentialTraverse_(newParties)(storePartyEntry(_, nextOffset())) - parties1 <- ledgerDao.listKnownParties(None, filterParty = None, partiesBefore.size + 1) - parties2 <- ledgerDao.listKnownParties( - parties1.lastOption.map(_.party), - filterParty = None, - partiesBefore.size + newParties.size, - ) - } yield { - parties1 ++ parties2 should contain.allElementsOf(newParties) - parties1 ++ parties2 should contain.allElementsOf(partiesBefore) - parties1.size + parties2.size should equal(newParties.size + partiesBefore.size) - } - } - - it should "retrieve zero parties" in { - for { - noPartyDetails <- ledgerDao.getParties(Seq.empty) - } yield { - noPartyDetails should be(Seq.empty) - } - } - - it should "retrieve a single party, if they exist" in { - val party = Ref.Party.assertFromString(s"Carol-${UUID.randomUUID()}") - val nonExistentParty = UUID.randomUUID().toString - val carol = IndexerPartyDetails( - party = party, - isLocal = true, - ) - for { - response <- storePartyEntry(carol, nextOffset()) - _ = response should be(PersistenceResponse.Ok) - carolPartyDetails <- ledgerDao.getParties(Seq(party)) - noPartyDetails <- ledgerDao.getParties(Seq(nonExistentParty)) - } yield { - carolPartyDetails should be(Seq(carol)) - noPartyDetails should be(Seq.empty) - } - } - - it should "retrieve multiple parties" in { - val danParty = Ref.Party.assertFromString(s"Dan-${UUID.randomUUID()}") - val eveParty = Ref.Party.assertFromString(s"Eve-${UUID.randomUUID()}") - val nonExistentParty = UUID.randomUUID().toString - val dan = IndexerPartyDetails( - party = danParty, - isLocal = true, - ) - val eve = IndexerPartyDetails( - party = eveParty, - isLocal = true, - ) - for { - response <- storePartyEntry(dan, nextOffset()) - _ = response should be(PersistenceResponse.Ok) - response <- storePartyEntry(eve, nextOffset()) - _ = response should be(PersistenceResponse.Ok) - parties <- ledgerDao.getParties(Seq(danParty, eveParty, nonExistentParty)) - } yield { - parties should contain.only(dan, eve) - } - } - - private def storePartyEntry( - partyDetails: IndexerPartyDetails, - offset: Offset, - submissionIdOpt: Option[Ref.SubmissionId] = Some(UUID.randomUUID().toString), - recordTime: Timestamp = Timestamp.now(), - ) = - ledgerDao - .storePartyAdded( - offset, - submissionIdOpt, - recordTime, - partyDetails, - ) - .map { response => - previousOffset.set(Some(offset)) - response - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSpecH2.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSpecH2.scala deleted file mode 100644 index b74aacf0cd..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSpecH2.scala +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -// Aggregate all specs in a single run to not start a new database fixture for each one -final class JdbcLedgerDaoSpecH2 - extends AsyncFlatSpec - with Matchers - with JdbcLedgerDaoSuite - with JdbcLedgerDaoBackendH2Database - with JdbcLedgerDaoActiveContractsSpec - with JdbcLedgerDaoCompletionsSpec - with JdbcLedgerDaoContractsSpec - with JdbcLedgerDaoExceptionSpec - with JdbcLedgerDaoPartiesSpec - with JdbcLedgerDaoTransactionsSpec - with JdbcLedgerDaoTransactionsWriterSpec diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSpecPostgres.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSpecPostgres.scala deleted file mode 100644 index 4e4d17d670..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSpecPostgres.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -// Aggregate all specs in a single run to not start a new database fixture for each one - -final class JdbcLedgerDaoSpecPostgres - extends AsyncFlatSpec - with Matchers - with JdbcLedgerDaoSuite - with JdbcLedgerDaoBackendPostgresql - with JdbcLedgerDaoActiveContractsSpec - with JdbcLedgerDaoCompletionsSpec - with JdbcLedgerDaoContractsSpec - with JdbcLedgerDaoExceptionSpec - with JdbcLedgerDaoPartiesSpec - with JdbcLedgerDaoTransactionsSpec - with JdbcLedgerDaoTransactionsWriterSpec diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSuite.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSuite.scala deleted file mode 100644 index 58cf146246..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoSuite.scala +++ /dev/null @@ -1,863 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.api.testtool.TestDars -import com.digitalasset.canton.config.RequireTypes.NonNegativeLong -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.TemplateFilter -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.store.entries.LedgerEntry -import com.digitalasset.canton.protocol.{ExampleContractFactory, TestUpdateId} -import com.digitalasset.daml.lf.archive.{DamlLf, DarParser, Decode} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.{ - Identifier, - IdentifierConverter, - PackageId, - PackageName, - PackageVersion, - Party, -} -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, FrontStack, ImmArray, Ref, Time} -import com.digitalasset.daml.lf.language.LanguageVersion -import com.digitalasset.daml.lf.transaction.test.{NodeIdTransactionBuilder, TransactionBuilder} -import com.digitalasset.daml.lf.transaction.{SerializationVersion as LfSerializationVersion, *} -import com.digitalasset.daml.lf.value.Value as LfValue -import com.digitalasset.daml.lf.value.Value.{ContractId, ThinContractInstance, ValueText} -import org.apache.pekko.stream.scaladsl.Sink -import org.scalatest.{AsyncTestSuite, OptionValues} - -import java.util.UUID -import java.util.concurrent.atomic.{AtomicLong, AtomicReference} -import scala.concurrent.Future -import scala.language.implicitConversions - -private[dao] trait JdbcLedgerDaoSuite extends JdbcLedgerDaoBackend with OptionValues { - this: AsyncTestSuite => - - protected implicit final val loggingContext: LoggingContextWithTrace = - LoggingContextWithTrace.ForTesting - - val previousOffset: AtomicReference[Option[Offset]] = - new AtomicReference[Option[Offset]](Option.empty) - - protected final val nextOffset: () => Offset = { - val counter = new AtomicLong(1) - () => { - Offset.tryFromLong(counter.getAndIncrement()) - } - } - - private[this] lazy val dar = DarParser.assertReadArchiveFromFile(TestDars.v2_2.ModelTestDar.file) - - protected final lazy val packageMap = - dar.all.map(archive => archive.getHash -> archive).toMap - - private val testPackageId: Ref.PackageId = Ref.PackageId.assertFromString(dar.main.getHash) - override def loadPackage: PackageId => Future[Option[DamlLf.Archive]] = pkgId => - Future.successful(packageMap.get(pkgId)) - - protected val testLanguageVersion: LanguageVersion = - Decode.assertDecodeArchive(dar.main)._2.languageVersion - - protected final val alice = Party.assertFromString("Alice") - protected final val bob = Party.assertFromString("Bob") - protected final val charlie = Party.assertFromString("Charlie") - protected final val david = Party.assertFromString("David") - protected final val emma = Party.assertFromString("Emma") - - protected final val defaultUserId = "default-app-id" - protected final val defaultWorkflowId = "default-workflow-id" - - // Note: *identifiers* and *values* defined below MUST correspond to community/ledger/ledger-common-dars/src/main/daml/model/Test.daml - // This is because some tests request values in verbose mode, which requires filling in missing type information, - // which in turn requires loading Daml-LF packages with valid Daml-LF types that correspond to the Daml-LF values. - // - // On the other hand, *transactions* do not need to correspond to valid transactions that could be produced by the - // above mentioned Daml code, e.g., signatories/stakeholders may not correspond to the contract/choice arguments. - // This is because JdbcLedgerDao is only concerned with serialization, and does not verify the Daml ledger model. - private def testIdentifier(name: String) = Identifier( - testPackageId, - Ref.QualifiedName( - Ref.ModuleName.assertFromString("Test"), - Ref.DottedName.assertFromString(name), - ), - ) - - protected final val someTemplateId = testIdentifier("ParameterShowcase") - protected final val somePackageName = PackageName.assertFromString("model-tests") - protected final val someTemplateIdFull = someTemplateId.toFullIdentifier(somePackageName) - protected final val somePackageVersion = PackageVersion.assertFromString("1.0") - protected final val someTemplateIdFilter = - TemplateFilter(someTemplateIdFull.toNameTypeConRef, includeCreatedEventBlob = false) - protected final val someValueText = LfValue.ValueText("some text") - protected final val someValueInt = LfValue.ValueInt64(1) - protected final val someValueNumeric = - LfValue.ValueNumeric(com.digitalasset.daml.lf.data.Numeric.assertFromString("1.1")) - protected final val someNestedOptionalInteger = LfValue.ValueRecord( - None, - ImmArray( - None -> LfValue.ValueVariant( - None, - Ref.Name.assertFromString("SomeInteger"), - someValueInt, - ) - ), - ) - protected final val someContractArgument = LfValue.ValueRecord( - None, - ImmArray( - None -> LfValue.ValueParty(alice), - None -> someValueInt, - None -> someValueNumeric, - None -> someValueText, - None -> LfValue.ValueBool(true), - None -> LfValue.ValueTimestamp(Time.Timestamp.Epoch), - None -> someNestedOptionalInteger, - None -> LfValue.ValueList(FrontStack(someValueInt)), - None -> LfValue.ValueOptional(Some(someValueText)), - ), - ) - protected final val someChoiceName = Ref.Name.assertFromString("Choice1") - protected final val someChoiceArgument = LfValue.ValueRecord( - Some(testIdentifier(someChoiceName)), - ImmArray( - None -> someValueInt, - None -> someValueNumeric, - None -> someValueText, - None -> LfValue.ValueBool(true), - None -> LfValue.ValueTimestamp(Time.Timestamp.Epoch), - None -> someNestedOptionalInteger, - None -> LfValue.ValueList(FrontStack(someValueInt)), - None -> LfValue.ValueOptional(Some(someValueText)), - ), - ) - protected final val someChoiceResult = - LfValue.ValueContractId(ContractId.V1(Hash.hashPrivateKey("#1"), Bytes.assertFromString("00"))) - - protected final def someContractKey(party: Party, value: String): LfValue.ValueRecord = - LfValue.ValueRecord( - None, - ImmArray( - None -> LfValue.ValueParty(party), - None -> LfValue.ValueText(value), - ), - ) - - private[this] val txVersion = LfSerializationVersion.V1 - private[this] def newBuilder(): NodeIdTransactionBuilder = new NodeIdTransactionBuilder - - protected final val someContractInstance = - ThinContractInstance( - packageName = somePackageName, - template = someTemplateId, - arg = someContractArgument, - ) - protected final val someVersionedContractInstance = Versioned(txVersion, someContractInstance) - - protected final val otherTemplateId = testIdentifier("Dummy") - protected final val otherTemplateIdFull = - otherTemplateId.toFullIdentifier(Ref.PackageName.assertFromString("model-tests")) - protected final val otherTemplateIdFilter = - TemplateFilter(otherTemplateIdFull.toNameTypeConRef, includeCreatedEventBlob = false) - protected final val otherContractArgument = LfValue.ValueRecord( - None, - ImmArray(None -> LfValue.ValueParty(alice)), - ) - - protected final val otherTemplateId2 = testIdentifier("DummyFactory") - protected final val otherTemplateId2Full = - otherTemplateId2.toFullIdentifier(Ref.PackageName.assertFromString("model-tests")) - protected final val otherTemplateId3 = testIdentifier("DummyContractFactory") - protected final val otherTemplateId3Full = - otherTemplateId3.toFullIdentifier(Ref.PackageName.assertFromString("model-tests")) - protected final val otherTemplateId4 = testIdentifier("DummyWithParam") - protected final val otherTemplateId4Full = - otherTemplateId4.toFullIdentifier(Ref.PackageName.assertFromString("model-tests")) - - protected final val otherTemplateId5 = testIdentifier("DummyWithAnnotation") - protected final val otherTemplateId5Full = - otherTemplateId5.toFullIdentifier(Ref.PackageName.assertFromString("model-tests")) - protected final val otherContractArgument5 = LfValue.ValueRecord( - None, - ImmArray(None -> LfValue.ValueParty(alice), None -> someValueText), - ) - - private[dao] def store( - completionInfo: Option[state.CompletionInfo], - tx: LedgerEntry.Transaction, - offset: Offset, - contractActivenessChanged: Boolean, - ): Future[(Offset, LedgerEntry.Transaction)] = - for { - _ <- ledgerDao.storeTransaction( - completionInfo = completionInfo, - workflowId = tx.workflowId, - updateId = tx.updateId, - ledgerEffectiveTime = tx.ledgerEffectiveTime, - offset = offset, - transaction = tx.transaction, - recordTime = tx.recordedAt, - contractActivenessChanged = contractActivenessChanged, - ) - } yield offset -> tx - - protected implicit def toParty(s: String): Party = Party.assertFromString(s) - - protected implicit def toLedgerString(s: String): Ref.LedgerString = - Ref.LedgerString.assertFromString(s) - - implicit def toUserId(s: String): Ref.UserId = - Ref.UserId.assertFromString(s) - - protected final def create( - signatories: Set[Party] = Set(alice, bob), - templateId: Identifier = someTemplateId, - contractArgument: LfValue = someContractArgument, - observers: Set[Party] = Set.empty, - ): Node.Create = - createNode(signatories, signatories ++ observers, None, templateId, contractArgument) - - protected final def createNode( - signatories: Set[Party], - stakeholders: Set[Party], - key: Option[GlobalKeyWithMaintainers] = None, - templateId: Identifier = someTemplateId, - contractArgument: LfValue = someContractArgument, - overrideContractId: Option[ContractId] = None, - ): Node.Create = - ExampleContractFactory - .build( - templateId = templateId, - packageName = somePackageName, - argument = contractArgument, - signatories = signatories, - stakeholders = stakeholders, - keyOpt = key, - overrideContractId = overrideContractId, - ) - .inst - .toCreateNode - - protected final def exerciseNode( - targetCid: ContractId, - key: Option[GlobalKeyWithMaintainers] = None, - ): Node.Exercise = - Node.Exercise( - targetCoid = targetCid, - templateId = someTemplateId, - packageName = somePackageName, - interfaceId = None, - choiceId = someChoiceName, - consuming = true, - actingParties = Set(alice), - chosenValue = someChoiceArgument, - stakeholders = Set(alice, bob), - signatories = Set(alice, bob), - choiceObservers = Set.empty, - choiceAuthorizers = None, - children = ImmArray.Empty, - exerciseResult = Some(someChoiceResult), - keyOpt = key, - byKey = false, - version = txVersion, - ) - - protected final def fetchNode( - contractId: ContractId, - party: Party = alice, - ): Node.Fetch = - Node.Fetch( - coid = contractId, - templateId = someTemplateId, - packageName = somePackageName, - actingParties = Set(party), - signatories = Set(party), - stakeholders = Set(party), - keyOpt = None, - byKey = false, - version = txVersion, - interfaceId = None, - ) - - // Ids of all contracts created in a transaction - both transient and non-transient - protected def created(tx: LedgerEntry.Transaction): Set[ContractId] = - tx.transaction.fold(Set.empty[ContractId]) { - case (set, (_, create: Node.Create)) => - set + create.coid - case (set, _) => - set - } - - // All non-transient contracts created in a transaction - protected def nonTransient(tx: LedgerEntry.Transaction): Set[ContractId] = - tx.transaction.fold(Set.empty[ContractId]) { - case (set, (_, create: Node.Create)) => - set + create.coid - case (set, (_, exercise: Node.Exercise)) if exercise.consuming => - set - exercise.targetCoid - case (set, _) => - set - } - - protected final def singleCreate: (Offset, LedgerEntry.Transaction) = - singleCreate(create()) - - protected final def multiPartySingleCreate: (Offset, LedgerEntry.Transaction) = - singleCreate(create(Set(alice, bob)), List(alice, bob)) - - protected final def singleCreate( - create: Node.Create, - actAs: List[Party] = List(alice), - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val eid = txBuilder.add(create) - val offset = nextOffset() - val id = offset.unwrap - val let = Timestamp.now() - offset -> LedgerEntry.Transaction( - commandId = Some(s"commandId$id"), - updateId = TestUpdateId(s"trId$id"), - userId = Some("userId1"), - submissionId = Some(s"submissionId$id"), - actAs = actAs, - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map(eid -> (create.signatories union create.stakeholders)), - ) - } - - protected final def noSubmitterInfo( - transaction: LedgerEntry.Transaction - ): LedgerEntry.Transaction = - transaction.copy(commandId = None, actAs = List.empty, userId = None) - - protected final def fromTransaction( - transaction: CommittedTransaction, - actAs: List[Party] = List(alice), - ): (Offset, LedgerEntry.Transaction) = { - val offset = nextOffset() - val id = offset.unwrap - val let = Timestamp.now() - offset -> LedgerEntry.Transaction( - commandId = Some(s"commandId$id"), - updateId = TestUpdateId(s"trId$id"), - userId = Some("userId1"), - submissionId = Some(s"submissionId$id"), - actAs = actAs, - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = transaction, - explicitDisclosure = Map.empty, - ) - } - - protected final def createTestKey( - maintainers: Set[Party] - ): GlobalKeyWithMaintainers = { - val aTextValue = ValueText(scala.util.Random.nextString(10)) - GlobalKeyWithMaintainers.assertBuild( - someTemplateId, - aTextValue, - crypto.Hash.hashPrivateKey(aTextValue.toString), - maintainers, - somePackageName, - ) - } - - protected final def createAndStoreContract( - submittingParties: Set[Party], - signatories: Set[Party], - stakeholders: Set[Party], - key: Option[GlobalKeyWithMaintainers], - contractArgument: LfValue = someContractArgument, - contractActivenessChanged: Boolean = true, - ): Future[(Offset, LedgerEntry.Transaction)] = - store( - singleCreate( - create = createNode( - signatories = signatories, - stakeholders = stakeholders, - key = key, - contractArgument = contractArgument, - ), - actAs = submittingParties.toList, - ), - contractActivenessChanged = contractActivenessChanged, - ) - - protected def singleExercise( - targetCid: ContractId, - key: Option[GlobalKeyWithMaintainers] = None, - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val nid = txBuilder.add(exerciseNode(targetCid, key)) - val offset = nextOffset() - val id = offset.unwrap - val let = Timestamp.now() - offset -> LedgerEntry.Transaction( - commandId = Some(s"commandId$id"), - updateId = TestUpdateId(s"trId$id"), - userId = Some("userId1"), - submissionId = Some(s"submissionId$id"), - actAs = List("Alice"), - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map(nid -> Set("Alice", "Bob")), - ) - } - - protected def multiPartySingleExercise( - targetCid: ContractId - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val nid = txBuilder.add(exerciseNode(targetCid)) - val offset = nextOffset() - val id = offset.unwrap - val let = Timestamp.now() - offset -> LedgerEntry.Transaction( - commandId = Some(s"commandId$id"), - updateId = TestUpdateId(s"trId$id"), - userId = Some("userId1"), - submissionId = Some(s"submissionId$id"), - actAs = List(alice, bob, charlie), - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map(nid -> Set(alice, bob)), - ) - } - - protected def singleNonConsumingExercise( - targetCid: ContractId - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val nid = txBuilder.add(exerciseNode(targetCid).copy(consuming = false)) - val offset = nextOffset() - val id = offset.unwrap - val let = Timestamp.now() - offset -> LedgerEntry.Transaction( - commandId = Some(s"commandId$id"), - updateId = TestUpdateId(s"trId$id"), - userId = Some("userId1"), - submissionId = Some(s"submissionId$id"), - actAs = List("Alice"), - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map(nid -> Set("Alice", "Bob")), - ) - } - - protected def exerciseWithChild( - targetCid: ContractId - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val exerciseId = txBuilder.add(exerciseNode(targetCid)) - val childId = txBuilder.add(create(), exerciseId) - val tx = txBuilder.buildCommitted() - val offset = nextOffset() - val id = offset.unwrap - val txId = s"trId$id" - val let = Timestamp.now() - offset -> LedgerEntry.Transaction( - commandId = Some(s"commandId$id"), - updateId = TestUpdateId(txId), - userId = Some("userId1"), - submissionId = Some(s"submissionId$id"), - actAs = List("Alice"), - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = tx, - explicitDisclosure = Map(exerciseId -> Set("Alice", "Bob"), childId -> Set("Alice", "Bob")), - ) - } - - protected def fullyTransient( - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val c = create() - val cid = c.coid - val createId = txBuilder.add(c) - val exerciseId = txBuilder.add(exerciseNode(cid)) - val let = Timestamp.now() - nextOffset() -> LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID().toString), - updateId = TestUpdateId(UUID.randomUUID().toString), - userId = Some("userId1"), - submissionId = Some(UUID.randomUUID.toString), - actAs = List(alice), - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map( - createId -> Set(alice, bob), - exerciseId -> Set(alice, bob), - ), - ) - } - - // The transient contract is divulged to `charlie` as a non-stakeholder actor on the - // root exercise node that causes the creation of a transient contract - protected def fullyTransientWithChildren: (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val root = create() - val transient = create() - val rootCreateId = txBuilder.add(root) - val rootExerciseId = txBuilder.add(exerciseNode(root.coid).copy(actingParties = Set(charlie))) - val createTransientId = txBuilder.add(transient, rootExerciseId) - val consumeTransientId = txBuilder.add(exerciseNode(transient.coid), rootExerciseId) - val let = Timestamp.now() - nextOffset() -> LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID.toString), - updateId = TestUpdateId(UUID.randomUUID().toString), - userId = Some("userId1"), - submissionId = Some(UUID.randomUUID.toString), - actAs = List(alice), - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map( - rootCreateId -> Set(alice, bob), - rootExerciseId -> Set(alice, bob, charlie), - createTransientId -> Set(alice, bob, charlie), - consumeTransientId -> Set(alice, bob, charlie), - ), - ) - } - - /** Creates the following transaction - * - * {{{ - * Create A --> Exercise A - * | | - * | | - * v v - * Create B Create C - * }}} - * - * - A is visible to Charlie - * - B is visible to Alice and Charlie - * - C is visible to Bob and Charlie - */ - protected def partiallyVisible: (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val createId = txBuilder.add( - createNode( - signatories = Set(charlie), - stakeholders = Set(charlie), - ) - ) - val exerciseId = txBuilder.add( - exerciseNode(txBuilder.newCid).copy( - actingParties = Set(charlie), - signatories = Set(charlie), - stakeholders = Set(charlie), - ) - ) - val childCreateId1 = txBuilder.add( - create(), - exerciseId, - ) - val childCreateId2 = txBuilder.add( - create(), - exerciseId, - ) - val let = Timestamp.now() - nextOffset() -> LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID().toString), - updateId = TestUpdateId(UUID.randomUUID().toString), - userId = Some("userId1"), - submissionId = Some(UUID.randomUUID().toString), - actAs = List(charlie), - workflowId = Some("workflowId"), - ledgerEffectiveTime = let, - recordedAt = let, - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map( - createId -> Set(charlie), - exerciseId -> Set(charlie), - childCreateId1 -> Set(alice, charlie), - childCreateId2 -> Set(bob, charlie), - ), - ) - } - - /** Creates a transactions with multiple top-level creates. - * - * Every contract will be signed by a fixed "operator" and each contract will have a further - * signatory and a template as defined by signatoriesAndTemplates. - * - * @throws IllegalArgumentException - * if signatoryAndTemplate is empty - */ - protected def multipleCreates( - operator: String, - signatoriesAndTemplates: Seq[(Party, Identifier, LfValue)], - ): (Offset, LedgerEntry.Transaction) = { - require(signatoriesAndTemplates.nonEmpty, "multipleCreates cannot create empty transactions") - val txBuilder = newBuilder() - val disclosure = for { - entry <- signatoriesAndTemplates - (signatory, template, argument) = entry - contract = create(Set(signatory), template, argument) - parties = Set[Party](operator, signatory) - nodeId = txBuilder.add(contract) - } yield nodeId -> parties - nextOffset() -> LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID().toString), - updateId = TestUpdateId(UUID.randomUUID.toString), - userId = Some("userId1"), - submissionId = Some(UUID.randomUUID.toString), - actAs = List(operator), - workflowId = Some("workflowId"), - ledgerEffectiveTime = Timestamp.now(), - recordedAt = Timestamp.now(), - transaction = txBuilder.buildCommitted(), - explicitDisclosure = disclosure.toMap, - ) - } - - protected final def store( - offsetAndTx: (Offset, LedgerEntry.Transaction), - contractActivenessChanged: Boolean = true, - ): Future[(Offset, LedgerEntry.Transaction)] = { - val (offset, entry) = offsetAndTx - val info = completionInfoFrom(entry) - store(info, entry, offset, contractActivenessChanged) - } - - protected def completionInfoFrom(entry: LedgerEntry.Transaction): Option[state.CompletionInfo] = - for { - actAs <- if (entry.actAs.isEmpty) None else Some(entry.actAs) - userId <- entry.userId - commandId <- entry.commandId - submissionId <- entry.submissionId - } yield state.CompletionInfo( - actAs, - userId, - commandId, - None, - Some(submissionId), - paidTrafficCost = NonNegativeLong.zero, - ) - - protected final def storeSync( - commands: Vector[(Offset, LedgerEntry.Transaction)] - ): Future[Vector[(Offset, LedgerEntry.Transaction)]] = { - - import com.daml.scalautil.TraverseFMSyntax.* - import scalaz.std.scalaFuture.* - import scalaz.std.vector.* - - // force synchronous future processing with Free monad - // to provide the guarantees that all transactions persisted in the specified order - commands traverseFM (store(_)) - } - - /** A transaction that creates the given key */ - protected final def txCreateContractWithKey( - party: Party, - key: String, - txUuid: Option[String] = None, - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val createNodeId = txBuilder.add( - createNode( - templateId = someTemplateId, - contractArgument = someContractArgument, - signatories = Set(party), - stakeholders = Set(party), - key = Some( - GlobalKeyWithMaintainers - .assertBuild( - someTemplateId, - someContractKey(party, key), - crypto.Hash.hashPrivateKey(key), - Set(party), - somePackageName, - ) - ), - ) - ) - nextOffset() -> - LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID().toString), - updateId = TestUpdateId(txUuid.getOrElse(UUID.randomUUID.toString)), - userId = Some(defaultUserId), - submissionId = Some(UUID.randomUUID().toString), - actAs = List(party), - workflowId = Some(defaultWorkflowId), - ledgerEffectiveTime = Timestamp.now(), - recordedAt = Timestamp.now(), - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map(createNodeId -> Set(party)), - ) - } - - /** A transaction that archives the given contract with the given key */ - protected final def txArchiveContract( - party: Party, - contract: (ContractId, Option[String]), - ): (Offset, LedgerEntry.Transaction) = { - val (contractId, maybeKey) = contract - val txBuilder = newBuilder() - val archiveNodeId = txBuilder.add( - Node.Exercise( - targetCoid = contractId, - templateId = someTemplateId, - packageName = somePackageName, - interfaceId = None, - choiceId = Ref.ChoiceName.assertFromString("Archive"), - consuming = true, - actingParties = Set(party), - chosenValue = LfValue.ValueUnit, - stakeholders = Set(party), - signatories = Set(party), - choiceObservers = Set.empty, - choiceAuthorizers = None, - children = ImmArray.Empty, - exerciseResult = Some(LfValue.ValueUnit), - keyOpt = maybeKey.map { k => - val keyValue = someContractKey(party, k) - GlobalKeyWithMaintainers - .assertBuild( - someTemplateId, - keyValue, - crypto.Hash.hashPrivateKey(keyValue.toString), - Set(party), - somePackageName, - ) - }, - byKey = false, - version = txVersion, - ) - ) - nextOffset() -> LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID().toString), - updateId = TestUpdateId(UUID.randomUUID.toString), - userId = Some(defaultUserId), - submissionId = Some(UUID.randomUUID().toString), - actAs = List(party), - workflowId = Some(defaultWorkflowId), - ledgerEffectiveTime = Timestamp.now(), - recordedAt = Timestamp.now(), - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map(archiveNodeId -> Set(party)), - ) - } - - /** A transaction that looks up a key */ - protected final def txLookupByKey( - party: Party, - key: String, - result: Option[ContractId], - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val keyValue = someContractKey(party, key) - val lookupByKeyNodeId = txBuilder.add( - Node.LookupByKey( - templateId = someTemplateId, - packageName = somePackageName, - key = GlobalKeyWithMaintainers - .assertBuild( - someTemplateId, - keyValue, - crypto.Hash.hashPrivateKey(keyValue.toString), - Set(party), - somePackageName, - ), - result = result, - version = txVersion, - ) - ) - nextOffset() -> LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID().toString), - updateId = TestUpdateId(UUID.randomUUID.toString), - userId = Some(defaultUserId), - submissionId = Some(UUID.randomUUID().toString), - actAs = List(party), - workflowId = Some(defaultWorkflowId), - ledgerEffectiveTime = Timestamp.now(), - recordedAt = Timestamp.now(), - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map(lookupByKeyNodeId -> Set(party)), - ) - } - - protected final def txFetch( - party: Party, - contractId: ContractId, - ): (Offset, LedgerEntry.Transaction) = { - val txBuilder = newBuilder() - val fetchNodeId = txBuilder.add( - Node.Fetch( - coid = contractId, - templateId = someTemplateId, - packageName = somePackageName, - actingParties = Set(party), - signatories = Set(party), - stakeholders = Set(party), - keyOpt = None, - byKey = false, - version = txVersion, - interfaceId = None, - ) - ) - nextOffset() -> LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID().toString), - updateId = TestUpdateId(UUID.randomUUID.toString), - userId = Some(defaultUserId), - submissionId = Some(UUID.randomUUID().toString), - actAs = List(party), - workflowId = Some(defaultWorkflowId), - ledgerEffectiveTime = Timestamp.now(), - recordedAt = Timestamp.now(), - transaction = txBuilder.buildCommitted(), - explicitDisclosure = Map(fetchNodeId -> Set(party)), - ) - } - - protected final def emptyTransaction(party: Party): (Offset, LedgerEntry.Transaction) = - nextOffset() -> LedgerEntry.Transaction( - commandId = Some(UUID.randomUUID().toString), - updateId = TestUpdateId(UUID.randomUUID.toString), - userId = Some(defaultUserId), - submissionId = Some(UUID.randomUUID().toString), - actAs = List(party), - workflowId = Some(defaultWorkflowId), - ledgerEffectiveTime = Timestamp.now(), - recordedAt = Timestamp.now(), - transaction = TransactionBuilder.EmptyCommitted, - explicitDisclosure = Map.empty, - ) - - // Returns the command ids and status of completed commands between two offsets - protected def getCompletions( - startInclusive: Offset, - endInclusive: Offset, - userId: String, - parties: Set[Party], - ): Future[Seq[(String, Int)]] = - ledgerDao.completions - .getCommandCompletions(startInclusive, endInclusive, userId, parties) - .map(_._2.completionResponse.completion.toList.head) - .map(c => c.commandId -> c.status.value.code) - .runWith(Sink.seq) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoTransactionsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoTransactionsSpec.scala deleted file mode 100644 index 3be28eedfb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoTransactionsSpec.scala +++ /dev/null @@ -1,1042 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.ledger.api.v2.event.CreatedEvent -import com.daml.ledger.api.v2.transaction.Transaction -import com.daml.ledger.api.v2.update_service.GetUpdatesResponse -import com.daml.ledger.resources.ResourceContext -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.ledger.api.TransactionShape.AcsDelta -import com.digitalasset.canton.ledger.api.util.{LfEngineToApi, TimestampConversion} -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.platform.store.backend.common.UpdatePointwiseQueries.LookupKey -import com.digitalasset.canton.platform.store.dao.EventProjectionProperties.UseOriginalViewPackageId -import com.digitalasset.canton.platform.store.entries.LedgerEntry -import com.digitalasset.canton.platform.store.utils.EventOps.EventOps -import com.digitalasset.canton.platform.{ - InternalEventFormat, - InternalTransactionFormat, - InternalUpdateFormat, - TemplatePartiesFilter, -} -import com.digitalasset.canton.protocol.TestUpdateId -import com.digitalasset.daml.lf.data.Ref.Party -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.transaction.Node -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.scalacheck.Gen -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Inside, LoneElement, OptionValues} - -import scala.concurrent.Future - -private[dao] trait JdbcLedgerDaoTransactionsSpec extends OptionValues with Inside with LoneElement { - this: AsyncFlatSpec with Matchers with JdbcLedgerDaoSuite => - - import JdbcLedgerDaoTransactionsSpec.* - - behavior of "JdbcLedgerDao (lookupUpdateById, lookupUpdateByOffset)" - - it should "return nothing for a mismatching update id" in { - for { - (_, tx) <- store(singleCreate) - result <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByUpdateId(TestUpdateId("WRONG")), - internalUpdateFormat = updateFormatForWildcardParties(tx.actAs.toSet), - ) - } yield { - result shouldBe None - } - } - - it should "return nothing for a mismatching offset" in { - for { - (_, tx) <- store(singleCreate) - result <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByOffset(Offset.tryFromLong(12345678L)), - internalUpdateFormat = updateFormatForWildcardParties(tx.actAs.toSet), - ) - } yield { - result shouldBe None - } - } - - it should "return nothing for a mismatching party" in { - for { - (offset, tx) <- store(singleCreate) - resultById <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByUpdateId(tx.updateId), - internalUpdateFormat = updateFormatForWildcardParties(Set("WRONG")), - ) - resultByOffset <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByOffset(offset), - internalUpdateFormat = updateFormatForWildcardParties(Set("WRONG")), - ) - } yield { - resultById shouldBe None - resultByOffset shouldBe resultById - } - } - - it should "return the expected transaction for a correct request (create)" in { - for { - (offset, tx) <- store(singleCreate) - resultById <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByUpdateId(tx.updateId), - internalUpdateFormat = updateFormatForWildcardParties(tx.actAs.toSet), - ) - resultByOffset <- ledgerDao.updateReader - .lookupUpdateBy(LookupKey.ByOffset(offset), updateFormatForWildcardParties(tx.actAs.toSet)) - } yield { - inside(resultById.value.update.transaction) { case Some(transaction) => - transaction.commandId shouldBe tx.commandId.value - transaction.offset shouldBe offset.unwrap - TimestampConversion.toLf( - transaction.effectiveAt.value, - TimestampConversion.ConversionMode.Exact, - ) shouldBe tx.ledgerEffectiveTime - transaction.updateId shouldBe tx.updateId.toHexString - transaction.workflowId shouldBe tx.workflowId.getOrElse("") - inside(transaction.events.loneElement.event.created) { case Some(created) => - inside(tx.transaction.nodes.headOption) { case Some((nodeId, createNode: Node.Create)) => - created.offset shouldBe offset.unwrap - created.nodeId shouldBe nodeId.index - created.witnessParties should contain only (tx.actAs*) - created.contractKey shouldBe None - created.createArguments shouldNot be(None) - created.signatories should contain theSameElementsAs createNode.signatories - created.observers should contain theSameElementsAs createNode.stakeholders.diff( - createNode.signatories - ) - created.templateId shouldNot be(None) - } - } - } - resultByOffset shouldBe resultById - } - } - - it should "return the expected transaction for a correct request (exercise)" in { - for { - (_, create) <- store(singleCreate) - (offset, exercise) <- store(singleExercise(nonTransient(create).loneElement)) - resultById <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByUpdateId(exercise.updateId), - internalUpdateFormat = updateFormatForWildcardParties(exercise.actAs.toSet), - ) - resultByOffset <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByOffset(offset), - internalUpdateFormat = updateFormatForWildcardParties(exercise.actAs.toSet), - ) - } yield { - inside(resultById.value.update.transaction) { case Some(transaction) => - transaction.commandId shouldBe exercise.commandId.value - transaction.offset shouldBe offset.unwrap - transaction.updateId shouldBe exercise.updateId.toHexString - TimestampConversion.toLf( - transaction.effectiveAt.value, - TimestampConversion.ConversionMode.Exact, - ) shouldBe exercise.ledgerEffectiveTime - transaction.workflowId shouldBe exercise.workflowId.getOrElse("") - inside(transaction.events.loneElement.event.archived) { case Some(archived) => - inside(exercise.transaction.nodes.headOption) { - case Some((nodeId, exerciseNode: Node.Exercise)) => - archived.offset shouldBe offset.unwrap - archived.nodeId shouldBe nodeId.index - archived.witnessParties should contain only (exercise.actAs*) - archived.contractId shouldBe exerciseNode.targetCoid.coid - archived.templateId shouldNot be(None) - } - } - } - resultByOffset shouldBe resultById - } - } - - it should "show command IDs to the original submitters (lookupUpdateById)" in { - val signatories = Set(alice, bob) - val stakeholders = Set(alice, bob, charlie) // Charlie is only stakeholder - val actAs = List(alice, bob, david) // David is submitter but not signatory - for { - (_, tx) <- store(singleCreate(createNode(signatories, stakeholders), actAs)) - // Response 1: querying as all submitters - result1 <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByUpdateId(tx.updateId), - internalUpdateFormat = updateFormatForWildcardParties(Set(alice, bob, david)), - ) - // Response 2: querying as a proper subset of all submitters - result2 <- ledgerDao.updateReader - .lookupUpdateBy( - lookupKey = LookupKey.ByUpdateId(tx.updateId), - internalUpdateFormat = updateFormatForWildcardParties(Set(alice, david)), - ) - // Response 3: querying as a proper superset of all submitters - result3 <- ledgerDao.updateReader - .lookupUpdateBy( - LookupKey.ByUpdateId(tx.updateId), - internalUpdateFormat = updateFormatForWildcardParties(Set(alice, bob, charlie, david)), - ) - } yield { - result1.value.update.transaction.value.commandId shouldBe tx.commandId.value - result2.value.update.transaction.value.commandId shouldBe tx.commandId.value - result3.value.update.transaction.value.commandId shouldBe tx.commandId.value - } - } - - it should "show command IDs to the original submitters (lookupUpdateByOffset)" in { - val signatories = Set(alice, bob) - val stakeholders = Set(alice, bob, charlie) // Charlie is only stakeholder - val actAs = List(alice, bob, david) // David is submitter but not signatory - for { - (offset, tx) <- store(singleCreate(createNode(signatories, stakeholders), actAs)) - // Response 1: querying as all submitters - result1 <- ledgerDao.updateReader - .lookupUpdateBy( - LookupKey.ByOffset(offset), - internalUpdateFormat = updateFormatForWildcardParties(Set(alice, bob, david)), - ) - // Response 2: querying as a proper subset of all submitters - result2 <- ledgerDao.updateReader - .lookupUpdateBy( - LookupKey.ByOffset(offset), - internalUpdateFormat = updateFormatForWildcardParties(Set(alice, david)), - ) - // Response 3: querying as a proper superset of all submitters - result3 <- ledgerDao.updateReader - .lookupUpdateBy( - LookupKey.ByOffset(offset), - internalUpdateFormat = updateFormatForWildcardParties(Set(alice, bob, charlie, david)), - ) - } yield { - result1.value.update.transaction.value.commandId shouldBe tx.commandId.value - result2.value.update.transaction.value.commandId shouldBe tx.commandId.value - result3.value.update.transaction.value.commandId shouldBe tx.commandId.value - } - } - - it should "hide command IDs from non-submitters" in { - val signatories = Set(alice, bob) - val stakeholders = Set(alice, bob, charlie) // Charlie is only stakeholder - val actAs = List(alice, bob, david) // David is submitter but not signatory - for { - (offset, tx) <- store(singleCreate(createNode(signatories, stakeholders), actAs)) - resultById <- ledgerDao.updateReader - .lookupUpdateBy( - LookupKey.ByUpdateId(tx.updateId), - updateFormatForWildcardParties(Set(charlie)), - ) - resultByOffset <- ledgerDao.updateReader - .lookupUpdateBy(LookupKey.ByOffset(offset), updateFormatForWildcardParties(Set(charlie))) - } yield { - resultById.value.update.transaction.value.commandId shouldBe "" - resultByOffset shouldBe resultById - } - } - - it should "hide transactions with transient contracts" in { - for { - (offset, tx) <- store(fullyTransient(), contractActivenessChanged = false) - resultById <- ledgerDao.updateReader - .lookupUpdateBy( - LookupKey.ByUpdateId(tx.updateId), - updateFormatForWildcardParties(tx.actAs.toSet), - ) - resultByOffset <- ledgerDao.updateReader - .lookupUpdateBy(LookupKey.ByOffset(offset), updateFormatForWildcardParties(tx.actAs.toSet)) - } yield { - resultById shouldBe empty - resultByOffset shouldBe empty - } - } - - behavior of "JdbcLedgerDao (getUpdates with AcsDelta)" - - it should "match the results of lookupFlatTransactionById" in { - for { - (from, to, transactions) <- storeTestFixture() - lookups <- lookupIndividually(transactions, Set(alice, bob, charlie)) - result <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from, - endInclusive = to, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice, bob, charlie))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - } yield { - comparable(result) should contain theSameElementsInOrderAs comparable(lookups) - } - } - - it should "filter correctly by party" in { - for { - from <- ledgerDao.lookupLedgerEnd() - (_, tx) <- store( - multipleCreates( - charlie, - Seq( - (alice, someTemplateId, someContractArgument), - (bob, someTemplateId, someContractArgument), - ), - ) - ) - to <- ledgerDao.lookupLedgerEnd() - individualLookupForAlice <- lookupIndividually(Seq(tx), as = Set(alice)) - individualLookupForBob <- lookupIndividually(Seq(tx), as = Set(bob)) - individualLookupForCharlie <- lookupIndividually(Seq(tx), as = Set(charlie)) - resultForAlice <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter(Map.empty, Some(Set(alice))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - resultForBob <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter(Map.empty, Some(Set(bob))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - resultForCharlie <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter(Map.empty, Some(Set(charlie))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - } yield { - individualLookupForAlice should contain theSameElementsInOrderAs resultForAlice - individualLookupForBob should contain theSameElementsInOrderAs resultForBob - individualLookupForCharlie should contain theSameElementsInOrderAs resultForCharlie - } - } - - it should "filter correctly for a single party" in { - for { - from <- ledgerDao.lookupLedgerEnd() - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (alice, someTemplateId, someContractArgument), - (bob, otherTemplateId, otherContractArgument), - (alice, otherTemplateId, otherContractArgument), - ), - ) - ) - to <- ledgerDao.lookupLedgerEnd() - result <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter( - Map(otherTemplateIdFull.toNameTypeConRef -> Some(Set(alice))), - Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - } yield { - inside(result.loneElement.events.loneElement.event.created) { case Some(create) => - create.witnessParties.loneElement shouldBe alice - create.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - } - } - - it should "filter correctly by multiple parties with the same template" in { - for { - from <- ledgerDao.lookupLedgerEnd() - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (alice, someTemplateId, someContractArgument), - (bob, otherTemplateId, otherContractArgument), - (alice, otherTemplateId, otherContractArgument), - ), - ) - ) - to <- ledgerDao.lookupLedgerEnd() - result <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter( - relation = Map( - otherTemplateIdFull.toNameTypeConRef -> Some(Set(alice, bob)) - ), - templateWildcardParties = Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - resultPartyWildcard <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter( - relation = Map(otherTemplateIdFull.toNameTypeConRef -> None), - templateWildcardParties = Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - - } yield { - val events = result.loneElement.events.toArray - events should have length 2 - inside(events(0).event.created) { case Some(create) => - create.witnessParties.loneElement shouldBe bob - create.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - inside(events(1).event.created) { case Some(create) => - create.witnessParties.loneElement shouldBe alice - create.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - // clear out commandId and paidTrafficCost since submitter is not in the querying parties for flat transactions in the non-wildcard query - resultPartyWildcard.loneElement.copy( - commandId = "", - paidTrafficCost = None, - ) shouldBe result.loneElement - } - } - - it should "filter correctly by multiple parties with different templates" in { - for { - from <- ledgerDao.lookupLedgerEnd() - _ <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (alice, someTemplateId, someContractArgument), - (bob, otherTemplateId, otherContractArgument), - (alice, otherTemplateId, otherContractArgument), - ), - ) - ) - to <- ledgerDao.lookupLedgerEnd() - result <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter( - relation = Map( - otherTemplateIdFull.toNameTypeConRef -> Some(Set(bob)), - someTemplateIdFull.toNameTypeConRef -> Some(Set(alice)), - ), - templateWildcardParties = Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - resultPartyWildcard <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter( - relation = Map( - otherTemplateIdFull.toNameTypeConRef -> None, - someTemplateIdFull.toNameTypeConRef -> None, - ), - templateWildcardParties = Some(Set.empty), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - } yield { - val events = result.loneElement.events.toArray - events should have length 2 - inside(events(0).event.created) { case Some(create) => - create.witnessParties.loneElement shouldBe alice - create.templateId.value shouldBe LfEngineToApi.toApiIdentifier(someTemplateId) - } - inside(events(1).event.created) { case Some(create) => - create.witnessParties.loneElement shouldBe bob - create.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - val eventsPartyWildcard = resultPartyWildcard.loneElement.events.toArray - eventsPartyWildcard should have length 3 - } - } - - it should "filter correctly by multiple parties with different template and wildcards" in { - for { - from <- ledgerDao.lookupLedgerEnd() - (_, _) <- store( - multipleCreates( - operator = "operator", - signatoriesAndTemplates = Seq( - (alice, someTemplateId, someContractArgument), - (bob, otherTemplateId, otherContractArgument), - (alice, otherTemplateId, otherContractArgument), - ), - ) - ) - to <- ledgerDao.lookupLedgerEnd() - result <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter( - Map( - otherTemplateIdFull.toNameTypeConRef -> Some(Set(alice)) - ), - Some(Set(bob)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - resultPartyWildcard <- transactionsOf( - ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter( - Map( - otherTemplateIdFull.toNameTypeConRef -> None - ), - Some(Set(bob)), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - ) - } yield { - val events = result.loneElement.events.toArray - events should have length 2 - inside(events(0).event.created) { case Some(create) => - create.witnessParties.loneElement shouldBe bob - create.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - inside(events(1).event.created) { case Some(create) => - create.witnessParties.loneElement shouldBe alice - create.templateId.value shouldBe LfEngineToApi.toApiIdentifier(otherTemplateId) - } - // clear out commandId and paidTrafficCost since submitter is not in the querying parties for flat transactions in the non-wildcard query - resultPartyWildcard.loneElement.copy( - commandId = "", - paidTrafficCost = None, - ) shouldBe result.loneElement - } - } - - it should "return all events in the expected order" in { - for { - from <- ledgerDao.lookupLedgerEnd() - (_, create) <- store(singleCreate) - firstContractId = nonTransient(create).loneElement - (offset, exercise) <- store(exerciseWithChild(firstContractId)) - result <- ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = offset, - internalUpdateFormat = updateFormat( - filter = TemplatePartiesFilter(Map.empty, Some(exercise.actAs.toSet)), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - .runWith(Sink.seq) - } yield { - import com.daml.ledger.api.v2.event.Event - import com.daml.ledger.api.v2.event.Event.Event.{Archived, Created} - - val txs = extractAllTransactions(result) - - inside(txs) { case Vector(tx1, tx2) => - tx1.updateId shouldBe create.updateId.toHexString - tx2.updateId shouldBe exercise.updateId.toHexString - inside(tx1.events) { case Seq(Event(Created(createdEvent))) => - createdEvent.contractId shouldBe firstContractId.coid - } - inside(tx2.events) { case Seq(Event(Archived(archivedEvent)), Event(Created(_))) => - archivedEvent.contractId shouldBe firstContractId.coid - } - } - } - } - - it should "return the expected flat transaction for the specified offset range" in { - for { - (_, create1) <- store(singleCreate) - (offset1, exercise) <- store(singleExercise(nonTransient(create1).loneElement)) - (offset2, create2) <- store(singleCreate) - result <- ledgerDao.updateReader - .getUpdates( - startInclusive = offset1.increment, - endInclusive = offset2, - internalUpdateFormat = updateFormat( - TemplatePartiesFilter(Map.empty, Some(exercise.actAs.toSet)), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - .runWith(Sink.seq) - - } yield { - import com.daml.ledger.api.v2.event.Event - import com.daml.ledger.api.v2.event.Event.Event.Created - - inside(extractAllTransactions(result)) { case Vector(tx) => - tx.updateId shouldBe create2.updateId.toHexString - inside(tx.events) { case Seq(Event(Created(createdEvent))) => - createdEvent.contractId shouldBe nonTransient(create2).loneElement.coid - } - } - } - } - - it should "return error when offset range is from the future" in { - val commands: Vector[(Offset, LedgerEntry.Transaction)] = Vector.fill(3)(singleCreate) - val beginOffsetFromTheFuture = nextOffset() - val endOffsetFromTheFuture = nextOffset() - - for { - _ <- storeSync(commands) - - result <- ledgerDao.updateReader - .getUpdates( - startInclusive = beginOffsetFromTheFuture.increment, - endInclusive = endOffsetFromTheFuture, - internalUpdateFormat = updateFormat( - TemplatePartiesFilter(Map.empty, Some(Set(alice))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - .runWith(Sink.seq) - .failed - - } yield { - result.getMessage should include("is beyond ledger end offset") - } - } - - it should "return all transactions in the specified offset range when iterating with gaps in the offsets assigned to events and a page size that ensures a page ends in such a gap" in { - // Simulates a gap in the offsets assigned to events, as they - // can be assigned to party allocation, package uploads and - // configuration updates as well - def offsetGap(): Vector[(Offset, LedgerEntry.Transaction)] = { - nextOffset() - Vector.empty[(Offset, LedgerEntry.Transaction)] - } - - // the order of `nextOffset()` calls is important - val beginOffset = nextOffset() - - val commandsWithOffsetGaps: Vector[(Offset, LedgerEntry.Transaction)] = - Vector(singleCreate) ++ offsetGap() ++ - Vector.fill(2)(singleCreate) ++ offsetGap() ++ - Vector.fill(3)(singleCreate) ++ offsetGap() ++ offsetGap() ++ - Vector.fill(5)(singleCreate) - - val endOffset = nextOffset() - - commandsWithOffsetGaps should have length 11L - - for { - _ <- storeSync(commandsWithOffsetGaps) - // just for having the ledger end bumped - _ <- ledgerDao.storePartyAdded( - endOffset, - None, - Timestamp.now(), - IndexerPartyDetails(alice, true), - ) - - // `pageSize = 2` and the offset gaps in the `commandWithOffsetGaps` above are to make sure - // that streaming works with event pages separated by offsets that don't have events in the store - response <- createLedgerDaoResourceOwner( - pageSize = 2, - eventsProcessingParallelism = 8, - acsIdPageSize = 2, - acsIdFetchingParallelism = 2, - acsContractFetchingParallelism = 2, - ).use( - _.updateReader - .getUpdates( - startInclusive = beginOffset.increment, - endInclusive = endOffset, - internalUpdateFormat = updateFormat( - TemplatePartiesFilter(Map.empty, Some(Set(alice))), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - .runWith(Sink.seq) - )(ResourceContext(executionContext)) - - readTxs = extractAllTransactions(response) - } yield { - val readTxOffsets: Vector[Long] = readTxs.map(_.offset) - readTxOffsets shouldBe readTxOffsets.sorted - readTxOffsets shouldBe commandsWithOffsetGaps.map(_._1.unwrap) - } - } - - it should "fall back to limit-based query with consistent results" in { - val txSeqLength = 1000 - txSeqTrial( - trials = 10, - txSeq = unfilteredTxSeq(length = txSeqLength), - codePath = Gen oneOf getFlatTransactionCodePaths, - ) - } - - private[this] def txSeqTrial( - trials: Int, - txSeq: Gen[Vector[Boolean]], - codePath: Gen[FlatTransactionCodePath], - ) = { - import com.daml.scalautil.TraverseFMSyntax.* - import scalaz.std.list.* - import scalaz.std.scalaFuture.* - - val trialData = Gen - .listOfN(trials, Gen.zip(txSeq, codePath)) - .sample getOrElse sys.error("impossible Gen failure") - - trialData - .traverseFM { case (boolSeq, cp) => - for { - from <- ledgerDao.lookupLedgerEnd() - commands <- storeSync(boolSeq map (if (_) cp.makeMatching() else cp.makeNonMatching())) - matchingOffsets = commands zip boolSeq collect { case ((off, _), true) => - off.unwrap - } - to <- ledgerDao.lookupLedgerEnd() - response <- ledgerDao.updateReader - .getUpdates( - startInclusive = from.fold(Offset.firstOffset)(_.lastOffset.increment), - endInclusive = to.value.lastOffset, - internalUpdateFormat = updateFormat( - cp.filter, - EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - descendingOrder = false, - ) - .runWith(Sink.seq) - readOffsets = response flatMap { case (_, gtr) => Seq(gtr.getTransaction.offset) } - readCreates = extractAllTransactions(response) flatMap (_.events) - } yield try { - readCreates.size should ===(boolSeq count identity) - // we check that the offsets from the DB match the ones we had before - // submission as a substitute for actually inspecting the events (indeed, - // so many of the events are = as written that this would not be useful) - readOffsets should ===(matchingOffsets) - } catch { - case ae: org.scalatest.exceptions.TestFailedException => - throw ae modifyMessage (_ map { msg => - msg + - "\n Random parameters:" + - s"\n actual frequency: ${boolSeq.count(identity)}/${boolSeq.size}" + - s"\n code path: ${cp.label}" + - s"\n Please copy the above 4 lines to https://github.com/digital-asset/daml/issues/7521" + - s"\n along with which of (JdbcLedgerDaoPostgresqlSpec, JdbcLedgerDaoH2DatabaseSpec) failed" - }) - } - } - .map(_.foldLeft(succeed)((_, r) => r)) - } - - /* - it should "get all transactions in order, 48%, onlyWildcardParties" in { - val frequency = 48 - val txSeqLength = 1000 - val path = "onlyWildcardParties" - txSeqTrial( - 250, - unfilteredTxFrequencySeq(txSeqLength, frequencyPct = frequency), - getFlatTransactionCodePaths find (_.label == path) getOrElse fail(s"$path not found")) - } - */ - - private def storeTestFixture(): Future[(Offset, Offset, Seq[LedgerEntry.Transaction])] = - for { - from <- ledgerDao.lookupLedgerEnd() - (_, t1) <- store(singleCreate) - (_, t2) <- store(singleCreate) - (_, t3) <- store(singleExercise(nonTransient(t2).loneElement)) - (_, t4) <- store(fullyTransient()) - to <- ledgerDao.lookupLedgerEnd() - } yield ( - from.fold(Offset.firstOffset)(_.lastOffset.increment), - to.value.lastOffset, - Seq(t1, t2, t3, t4), - ) - - private def lookupIndividually( - transactions: Seq[LedgerEntry.Transaction], - as: Set[Party], - ): Future[Seq[Transaction]] = - Future - .sequence( - transactions.map(tx => - ledgerDao.updateReader - .lookupUpdateBy(LookupKey.ByUpdateId(tx.updateId), updateFormatForWildcardParties(as)) - ) - ) - .map(_.flatMap(_.toList.flatMap(_.update.transaction.toList))) - - private def transactionsOf( - source: Source[(Offset, GetUpdatesResponse), NotUsed] - ): Future[Seq[Transaction]] = - source - .map(_._2) - .runWith(Sink.seq) - .map(_.map(_.getTransaction)) - - // Ensure two sequences of transactions are comparable: - // - witnesses do not have to appear in a specific order - private def comparable(txs: Seq[Transaction]): Seq[Transaction] = - txs.map(tx => tx.copy(events = tx.events.map(_.modifyWitnessParties(_.sorted)))) - - private def extractAllTransactions( - responses: Seq[(Offset, GetUpdatesResponse)] - ): Vector[Transaction] = - responses.foldLeft(Vector.empty[Transaction])((b, a) => b :+ a._2.getTransaction) - - private def createLedgerDaoResourceOwner( - pageSize: Int, - eventsProcessingParallelism: Int, - acsIdPageSize: Int, - acsIdFetchingParallelism: Int, - acsContractFetchingParallelism: Int, - ) = - daoOwner( - eventsPageSize = pageSize, - eventsProcessingParallelism = eventsProcessingParallelism, - acsIdPageSize = acsIdPageSize, - acsIdFetchingParallelism = acsIdFetchingParallelism, - acsContractFetchingParallelism = acsContractFetchingParallelism, - ) - - // TODO(i12297): SC much of this is repeated because we're more concerned here - // with whether each query is tested than whether the specifics of the - // predicate are accurate. To test the latter, the creation would have - // to have much more detail and should be a pair of Gen[Offset => LedgerEntry.Transaction] - // rather than a pair of simple side-effecting procedures that always - // produce more or less the same data. If we aren't interested in testing - // the latter at any point, we can remove most of this. - private val getFlatTransactionCodePaths: Seq[FlatTransactionCodePath] = { - import JdbcLedgerDaoTransactionsSpec.FlatTransactionCodePath as Mk - Seq( - Mk( - "singleWildcardParty", - TemplatePartiesFilter(Map.empty, Some(Set(alice))), - () => singleCreate(create(signatories = Set(alice))), - () => singleCreate(create(signatories = Set(bob))), - ce => (ce.signatories ++ ce.observers) contains alice, - ), - Mk( - "singlePartyWithTemplates", - TemplatePartiesFilter( - Map(someTemplateIdFull.toNameTypeConRef -> Some(Set(alice))), - Some(Set.empty), - ), - () => singleCreate(create(signatories = Set(alice))), - () => singleCreate(create(signatories = Set(bob))), - ce => - ((ce.signatories ++ ce.observers) contains alice), // TODO(i12297): && ce.templateId == Some(someTemplateId.toString) - ), - Mk( - "onlyWildcardParties", - TemplatePartiesFilter(Map.empty, Some(Set(alice, bob))), - () => singleCreate(create(signatories = Set(alice))), - () => singleCreate(create(signatories = Set(charlie))), - ce => (ce.signatories ++ ce.observers) exists Set(alice, bob), - ), - Mk( - "sameTemplates", - TemplatePartiesFilter( - Map(someTemplateIdFull.toNameTypeConRef -> Some(Set(alice, bob))), - Some(Set.empty), - ), - () => singleCreate(create(signatories = Set(alice))), - () => singleCreate(create(signatories = Set(charlie))), - ce => (ce.signatories ++ ce.observers) exists Set(alice, bob), - ), - Mk( - "mixedTemplates", - TemplatePartiesFilter( - Map( - someTemplateIdFull.toNameTypeConRef -> Some(Set(alice)), - otherTemplateIdFull.toNameTypeConRef -> Some(Set(bob)), - ), - Some(Set.empty), - ), - () => singleCreate(create(signatories = Set(alice))), - () => singleCreate(create(signatories = Set(charlie))), - ce => (ce.signatories ++ ce.observers) exists Set(alice, bob), - ), - Mk( - "mixedTemplatesWithWildcardParties", - TemplatePartiesFilter( - Map(someTemplateIdFull.toNameTypeConRef -> Some(Set(alice))), - Some(Set(bob)), - ), - () => singleCreate(create(signatories = Set(alice))), - () => singleCreate(create(signatories = Set(charlie))), - ce => (ce.signatories ++ ce.observers) exists Set(alice, bob), - ), - ) - } -} - -private[dao] object JdbcLedgerDaoTransactionsSpec { - private final case class FlatTransactionCodePath( - label: String, - filter: TemplatePartiesFilter, - makeMatching: () => (Offset, LedgerEntry.Transaction), - makeNonMatching: () => (Offset, LedgerEntry.Transaction), - // TODO(i12297): SC we don't need discriminate unless we test the event contents - // instead of just the offsets - discriminate: CreatedEvent => Boolean = _ => false, - ) - - private def unfilteredTxSeq(length: Int): Gen[Vector[Boolean]] = - Gen.oneOf(1, 2, 5, 10, 20, 50, 100) flatMap { invFreq => - unfilteredTxFrequencySeq(length, frequencyPct = 100 / invFreq) - } - - private def unfilteredTxFrequencySeq(length: Int, frequencyPct: Int): Gen[Vector[Boolean]] = - Gen.containerOfN[Vector, Boolean]( - length, - Gen.frequency((frequencyPct, true), (100 - frequencyPct, false)), - ) - - private def updateFormat( - filter: TemplatePartiesFilter, - eventProjectionProperties: EventProjectionProperties, - ) = { - val eventFormat = InternalEventFormat( - templatePartiesFilter = filter, - eventProjectionProperties = eventProjectionProperties, - ) - val txFormat = Some( - InternalTransactionFormat(internalEventFormat = eventFormat, transactionShape = AcsDelta) - ) - InternalUpdateFormat( - includeTransactions = txFormat, - includeReassignments = None, - includeTopologyEvents = None, - ) - } - - private def transactionFormatForWildcardParties( - requestingParties: Set[Party] - ): InternalTransactionFormat = - InternalTransactionFormat( - internalEventFormat = InternalEventFormat( - templatePartiesFilter = TemplatePartiesFilter( - relation = Map.empty, - templateWildcardParties = Some(requestingParties), - ), - eventProjectionProperties = EventProjectionProperties( - verbose = true - )(interfaceViewPackageUpgrade = UseOriginalViewPackageId), - ), - transactionShape = AcsDelta, - ) - - private def updateFormatForWildcardParties( - requestingParties: Set[Party] - ): InternalUpdateFormat = - InternalUpdateFormat( - includeTransactions = Some(transactionFormatForWildcardParties(requestingParties)), - includeReassignments = None, - includeTopologyEvents = None, - ) - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoTransactionsWriterSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoTransactionsWriterSpec.scala deleted file mode 100644 index 8c43c4e02d..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerDaoTransactionsWriterSpec.scala +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.data.Offset -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{LoneElement, OptionValues} - -private[dao] trait JdbcLedgerDaoTransactionsWriterSpec extends LoneElement with OptionValues { - this: AsyncFlatSpec with Matchers with JdbcLedgerDaoSuite => - - private val ok = io.grpc.Status.Code.OK.value() - - behavior of "JdbcLedgerDao (TransactionsWriter)" - - it should "serialize a valid positive lookupByKey" in { - val keyValue = "positive-lookup-by-key" - - for { - from <- ledgerDao.lookupLedgerEnd() - (_, create) <- store(txCreateContractWithKey(alice, keyValue)) - createdContractId = nonTransient(create).loneElement - (_, lookup) <- store(txLookupByKey(alice, keyValue, Some(createdContractId))) - to <- ledgerDao.lookupLedgerEnd() - completions <- getCompletions( - from.fold(Offset.firstOffset)(_.lastOffset.increment), - to.map(_.lastOffset).getOrElse(fail("ledger end should not have been empty")), - defaultUserId, - Set(alice), - ) - } yield { - completions should contain.allOf( - create.commandId.value -> ok, - lookup.commandId.value -> ok, - ) - } - } - - it should "serialize a valid fetch" in { - val keyValue = "valid-fetch" - - for { - from <- ledgerDao.lookupLedgerEnd() - (_, create) <- store(txCreateContractWithKey(alice, keyValue)) - createdContractId = nonTransient(create).loneElement - (_, fetch) <- store(txFetch(alice, createdContractId)) - to <- ledgerDao.lookupLedgerEnd() - completions <- getCompletions( - from.fold(Offset.firstOffset)(_.lastOffset.increment), - to.map(_.lastOffset).getOrElse(fail("ledger end should not have been empty")), - defaultUserId, - Set(alice), - ) - } yield { - completions should contain.allOf( - create.commandId.value -> ok, - fetch.commandId.value -> ok, - ) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerWriteDao.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerWriteDao.scala deleted file mode 100644 index bd015d6397..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/JdbcLedgerWriteDao.scala +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.config.CantonRequireTypes.String185 -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.health.{HealthStatus, ReportsHealth} -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.TestAcsChangeFactory -import com.digitalasset.canton.ledger.participant.state.Update.TransactionAccepted.RepresentativePackageId.SameAsContractPackageId -import com.digitalasset.canton.ledger.participant.state.Update.{ - ContractInfo, - TopologyTransactionEffective, -} -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.LoggingContextWithTrace.implicitExtractTraceContext -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.participant.store.PersistedContractInstance -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.platform.config.{ - ActiveContractsServiceStreamsConfig, - IndexServiceConfig, - UpdatesStreamsConfig, -} -import com.digitalasset.canton.platform.store.* -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.backend.{ParameterStorageBackend, ReadStorageBackend} -import com.digitalasset.canton.platform.store.cache.{AchsStateCache, LedgerEndCache} -import com.digitalasset.canton.platform.store.dao.events.* -import com.digitalasset.canton.protocol.{ContractInstance, TestUpdateId, UpdateId} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.data.{Bytes, Ref} -import com.digitalasset.daml.lf.transaction.CreationTime.CreatedAt -import com.digitalasset.daml.lf.transaction.{CommittedTransaction, Node} -import io.opentelemetry.api.trace.Tracer -import org.apache.pekko.actor.Scheduler - -import java.util.UUID -import scala.concurrent.{ExecutionContext, Future} - -private class JdbcLedgerWriteDao( - dbDispatcher: DbDispatcher & ReportsHealth, - queryExecutionContext: ExecutionContext, - commandExecutionContext: ExecutionContext, - metrics: LedgerApiServerMetrics, - sequentialIndexer: SequentialWriteDao, - participantId: Ref.ParticipantId, - readStorageBackend: ReadStorageBackend, - parameterStorageBackend: ParameterStorageBackend, - ledgerEndCache: LedgerEndCache, - completionsPageSize: Int, - activeContractsServiceStreamsConfig: ActiveContractsServiceStreamsConfig, - updatesStreamsConfig: UpdatesStreamsConfig, - globalMaxEventIdQueries: Int, - globalMaxEventPayloadQueries: Int, - tracer: Tracer, - val loggerFactory: NamedLoggerFactory, - incompleteOffsets: ( - Offset, - Option[Set[Ref.Party]], - TraceContext, - ) => FutureUnlessShutdown[Vector[Offset]], - contractLoader: ContractLoader, - lfValueTranslation: LfValueTranslation, - contractStore: LedgerApiContractStoreImpl, - achsStateCache: AchsStateCache, - scheduler: Scheduler, -)(implicit ec: ExecutionContext) - extends LedgerReadDao - with LedgerWriteDao - with NamedLogging { - - private val readDao = new JdbcLedgerDao( - dbDispatcher = dbDispatcher, - queryExecutionContext = queryExecutionContext, - commandExecutionContext = commandExecutionContext, - metrics = metrics, - readStorageBackend = readStorageBackend, - parameterStorageBackend = parameterStorageBackend, - ledgerEndCache = ledgerEndCache, - completionsPageSize = completionsPageSize, - activeContractsServiceStreamsConfig = activeContractsServiceStreamsConfig, - updatesStreamsConfig = updatesStreamsConfig, - globalMaxEventIdQueries = globalMaxEventIdQueries, - globalMaxEventPayloadQueries = globalMaxEventPayloadQueries, - tracer = tracer, - loggerFactory = loggerFactory, - incompleteOffsets = incompleteOffsets, - contractLoader = contractLoader, - lfValueTranslation = lfValueTranslation, - contractStore = contractStore, - achsStateCache = achsStateCache, - scheduler = scheduler, - contractPruningDelayBeforeRetry = - IndexServiceConfig.DefaultContractPruningDelayBeforeRetry.underlying, - contractPruningMaxRetries = IndexServiceConfig.DefaultContractPruningMaxRetries, - ) - - override def currentHealth(): HealthStatus = dbDispatcher.currentHealth() - - override def lookupParticipantId()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[ParticipantId]] = readDao.lookupParticipantId() - - override def lookupLedgerEnd()(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[LedgerEnd]] = readDao.lookupLedgerEnd() - - override def initialize( - participantId: ParticipantId - )(implicit loggingContext: LoggingContextWithTrace): Future[Unit] = - dbDispatcher - .executeSql(metrics.index.db.initializeLedgerParameters)( - parameterStorageBackend.initializeParameters( - ParameterStorageBackend.IdentityParams( - participantId = participantId - ), - loggerFactory, - ) - ) - - private val NonLocalParticipantId = - Ref.ParticipantId.assertFromString("RESTRICTED_NON_LOCAL_PARTICIPANT_ID") - - override def storePartyAdded( - offset: Offset, - submissionIdOpt: Option[SubmissionId], - recordTime: Timestamp, - partyDetails: IndexerPartyDetails, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[PersistenceResponse] = { - logger.info("Storing party entry") - dbDispatcher.executeSql(metrics.index.db.storePartyEntryDbMetrics) { implicit conn => - sequentialIndexer.store( - conn, - offset, - Some( - state.Update.TopologyTransactionEffective( - updateId = TestUpdateId(UUID.randomUUID().toString), - events = Set( - TopologyTransactionEffective.TopologyEvent.PartyToParticipantAuthorization( - party = partyDetails.party, - participant = if (partyDetails.isLocal) participantId else NonLocalParticipantId, - authorizationEvent = TopologyTransactionEffective.AuthorizationEvent.Added( - TopologyTransactionEffective.AuthorizationLevel.Confirmation - ), - ) - ), - synchronizerId = SynchronizerId.tryFromString("invalid::deadbeef"), - effectiveTime = CantonTimestamp(recordTime), - ) - ), - ) - PersistenceResponse.Ok - } - } - - override def storeRejection( - completionInfo: Option[state.CompletionInfo], - recordTime: Timestamp, - offset: Offset, - reason: state.Update.CommandRejected.RejectionReasonTemplate, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[PersistenceResponse] = - dbDispatcher - .executeSql(metrics.index.db.storeRejectionDbMetrics) { implicit conn => - sequentialIndexer.store( - conn, - offset, - completionInfo.map(info => - state.Update.SequencedCommandRejected( - recordTime = CantonTimestamp(recordTime), - completionInfo = info, - reasonTemplate = reason, - synchronizerId = SynchronizerId.tryFromString("invalid::deadbeef"), - isTransaction = true, - ) - ), - ) - PersistenceResponse.Ok - } - - override def getParties( - parties: Seq[Party] - )(implicit loggingContext: LoggingContextWithTrace): Future[List[IndexerPartyDetails]] = - readDao.getParties(parties) - - override def listKnownParties( - fromExcl: Option[Party], - filterParty: Option[String185], - maxResults: Int, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[List[IndexerPartyDetails]] = readDao.listKnownParties(fromExcl, filterParty, maxResults) - - /** Prunes the events and command completions tables. - * - * @param pruneUpToInclusive - * Offset up to which to prune archived history inclusively. - */ - override def prune( - previousPruneUpToInclusive: Option[Offset], - previousIncompleteReassignmentOffsets: Vector[Offset], - pruneUpToInclusive: Offset, - incompleteReassignmentOffsets: Vector[Offset], - )(implicit loggingContext: LoggingContextWithTrace): Future[Unit] = readDao.prune( - previousPruneUpToInclusive, - previousIncompleteReassignmentOffsets, - pruneUpToInclusive, - incompleteReassignmentOffsets, - ) - - override val updateReader: UpdateReader = readDao.updateReader - - override val contractsReader: ContractsReader = readDao.contractsReader - - override def eventsReader: LedgerDaoEventsReader = readDao.eventsReader - - override def isPruningInProgress: Boolean = readDao.isPruningInProgress - - override val completions: CommandCompletionsReader = readDao.completions - - /** This is a combined store transaction method to support tests !!! Usage of this is discouraged - */ - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def storeTransaction( - completionInfo: Option[state.CompletionInfo], - workflowId: Option[WorkflowId], - updateId: UpdateId, - ledgerEffectiveTime: Timestamp, - offset: Offset, - transaction: CommittedTransaction, - recordTime: Timestamp, - contractActivenessChanged: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[PersistenceResponse] = for { - _ <- Future.successful(logger.info("Storing contracts into participant contract store")) - contracts = transaction.nodes.values - .collect { case create: Node.Create => create } - .map(FatContract.fromCreateNode(_, CreatedAt(ledgerEffectiveTime), Bytes.Empty)) - .map( - ContractInstance - .create(_) - .fold( - error => throw new IllegalArgumentException(s"Invalid contract: $error"), - identity, - ) - ) - .toSeq - internalContractIds <- contractStore.participantContractStore - .storeContracts(contracts) - .failOnShutdownToAbortException("storeTransaction") - - _ <- Future.successful(logger.info("Storing transaction")) - _ <- dbDispatcher - .executeSql(metrics.index.db.storeTransactionDbMetrics) { implicit conn => - sequentialIndexer.store( - conn, - offset, - Some( - state.Update.SequencedTransactionAccepted( - completionInfoO = completionInfo, - transactionMeta = state.TransactionMeta( - ledgerEffectiveTime = ledgerEffectiveTime, - workflowId = workflowId, - preparationTime = null, // not used for DbDto generation - submissionSeed = null, // not used for DbDto generation - timeBoundaries = null, // not used for DbDto generation - optUsedPackages = None, // not used for DbDto generation - optNodeSeeds = None, // not used for DbDto generation - optByKeyNodes = None, // not used for DbDto generation - ), - transactionInfo = state.Update.TransactionAccepted.TransactionInfo(transaction), - updateId = updateId, - synchronizerId = SynchronizerId.tryFromString("invalid::deadbeef"), - recordTime = CantonTimestamp(recordTime), - externalTransactionHash = None, - acsChangeFactory = - TestAcsChangeFactory(contractActivenessChanged = contractActivenessChanged), - contractInfos = contracts.map { c => - c.contractId -> ContractInfo( - persistedContractInstance = PersistedContractInstance( - internalContractId = internalContractIds(c.contractId), - inst = c.inst, - ), - representativePackageId = SameAsContractPackageId, - ) - }.toMap, - ) - ), - ) - } - } yield { - PersistenceResponse.Ok - } - - override def indexDbPrunedUpTo(implicit - loggingContext: LoggingContextWithTrace - ): Future[Option[Offset]] = - readDao.indexDbPrunedUpTo -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/LedgerWriteDao.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/LedgerWriteDao.scala deleted file mode 100644 index 5d8b70dd35..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/LedgerWriteDao.scala +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.data.Offset -import com.digitalasset.canton.health.ReportsHealth -import com.digitalasset.canton.ledger.api.ParticipantId -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.index.IndexerPartyDetails -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.transaction.CommittedTransaction - -import scala.concurrent.Future - -private[platform] trait LedgerWriteDao extends ReportsHealth { - - /** Initializes the database with the given ledger identity. If the database was already - * intialized, instead compares the given identity parameters to the existing ones, and returns a - * Future failed with [[MismatchException]] if they don't match. - * - * This method is idempotent. This method is NOT safe to call concurrently. - * - * This method must succeed at least once before other LedgerWriteDao methods may be used. - * - * @param participantId - * the participant id to be stored - */ - def initialize( - participantId: ParticipantId - )(implicit loggingContext: LoggingContextWithTrace): Future[Unit] - - def storeRejection( - completionInfo: Option[state.CompletionInfo], - recordTime: Timestamp, - offset: Offset, - reason: state.Update.CommandRejected.RejectionReasonTemplate, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[PersistenceResponse] - - /** Stores a party allocation or rejection thereof. */ - def storePartyAdded( - offset: Offset, - submissionIdOpt: Option[SubmissionId], - recordTime: Timestamp, - partyDetails: IndexerPartyDetails, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[PersistenceResponse] - - /** This is a combined store transaction method to support only tests !!! Usage of this is - * discouraged. - */ - def storeTransaction( - completionInfo: Option[state.CompletionInfo], - workflowId: Option[WorkflowId], - updateId: UpdateId, - ledgerEffectiveTime: Timestamp, - offset: Offset, - transaction: CommittedTransaction, - recordTime: Timestamp, - contractActivenessChanged: Boolean, - )(implicit - loggingContext: LoggingContextWithTrace - ): Future[PersistenceResponse] - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/PaginatingAsyncStreamSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/PaginatingAsyncStreamSpec.scala deleted file mode 100644 index f09dbb70c9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/PaginatingAsyncStreamSpec.scala +++ /dev/null @@ -1,604 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - IdFilterPageQuery, - IdPage, - IdPageBounds, - IdPageQuery, - PaginationFromTo, - PaginationInput, -} -import com.digitalasset.canton.platform.store.dao.events.IdPageSizing -import com.digitalasset.canton.tracing.TraceContext -import org.apache.pekko.stream.scaladsl.Sink -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.sql.Connection -import scala.concurrent.Future - -class PaginatingAsyncStreamSpec - extends AsyncFlatSpec - with Matchers - with BaseTest - with PekkoBeforeAndAfterAll { - private val paginatingAsyncStream = new PaginatingAsyncStream(loggerFactory) - - behavior of "streamIdsFromSeekPaginationWithoutIdFilter" - - it should "stream in forward order with increasing page size" in { - val ids = (1L to 50L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 50L, - ids, - descendingOrder = false, - ).map { case (result, queries) => - result shouldBe ids - queries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 50), 1), - PaginationInput(PaginationFromTo.ascending(1, 50), 4), - PaginationInput(PaginationFromTo.ascending(5, 50), 16), - PaginationInput(PaginationFromTo.ascending(21, 50), 20), - PaginationInput(PaginationFromTo.ascending(41, 50), 20), - PaginationInput(PaginationFromTo.ascending(50, 50), 20), - ) - } - } - - it should "stream IDs in forward order with constant page size" in { - val ids = (1L to 50L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 20, maxPageSize = 20), - 0L, - 50L, - ids, - descendingOrder = false, - ).map { case (result, queries) => - result shouldBe ids - queries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 50), 20), - PaginationInput(PaginationFromTo.ascending(20, 50), 20), - PaginationInput(PaginationFromTo.ascending(40, 50), 20), - PaginationInput(PaginationFromTo.ascending(50, 50), 20), - ) - } - } - - it should "stream empty range in forward order" in { - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 0L, - Vector.empty, - descendingOrder = false, - ).map { case (result, queries) => - result shouldBe Vector.empty - queries shouldBe Vector(PaginationInput(PaginationFromTo.ascending(0, 0), 1)) - } - } - - it should "stream single element in forward order" in { - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 2, maxPageSize = 10), - 0L, - 1L, - Vector(1L), - descendingOrder = false, - ).map { case (result, queries) => - result shouldBe Vector(1L) - queries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 1), 2), - PaginationInput(PaginationFromTo.ascending(1, 1), 8), - ) - } - } - - it should "stream forward order with id set being subset requested range" in { - val ids = (5L to 15L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 3, maxPageSize = 10), - 0L, - 30L, - ids, - descendingOrder = false, - ).map { case (result, _) => - result shouldBe ids - } - } - - it should "stream forward order with id set wider than requested range" in { - val ids = (1L to 50L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 3, maxPageSize = 5), - 5L, - 20L, - ids, - descendingOrder = false, - ) - .map { case (result, _) => - result shouldBe (6 to 20) - } - } - - it should "stream IDs in backward order when range matches id set" in { - val ids = (1L to 100L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 100L, - ids, - descendingOrder = true, - ) - .map { case (result, _) => - result shouldBe ids.reverse - } - } - - it should "stream IDs in backward order when range is wider than id set" in { - val ids = (10L to 50L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 100L, - ids, - descendingOrder = true, - ) - .map { case (result, _) => - result shouldBe ids.reverse - } - } - - it should "stream empty range in backward order" in { - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 0L, - (1L to 10L).toVector, - descendingOrder = true, - ).map { case (result, queries) => - result shouldBe Vector.empty - queries shouldBe Vector(PaginationInput(PaginationFromTo.descending(0, 0), 1)) - } - } - - it should "stream empty set in backward order" in { - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 1L, - 100L, - Vector.empty, - descendingOrder = true, - ).map { case (result, _) => - result shouldBe Vector.empty - } - } - - it should "stream descending order with id set being subset requested range" in { - val ids = (5L to 15L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 3, maxPageSize = 10), - 0L, - 30L, - ids, - descendingOrder = true, - ).map { case (result, _) => - result shouldBe ids.reverse - } - } - - it should "stream descending order with id set wider than requested range" in { - val ids = (1L to 50L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 3, maxPageSize = 5), - 5L, - 20L, - ids, - descendingOrder = true, - ) - .map { case (result, _) => - result shouldBe (6 to 20).reverse - } - } - - private def runStreamWithoutIdFilter( - idPageSizing: IdPageSizing, - initialFromIdExclusive: Long, - initialEndInclusive: Long, - ids: Vector[Long], - descendingOrder: Boolean, - ): Future[(Vector[Long], Vector[PaginationInput])] = { - val queries = Vector.newBuilder[PaginationInput] - paginatingAsyncStream - .streamIdsFromSeekPaginationWithoutIdFilter( - idStreamName = "test-stream", - idPageSizing = idPageSizing, - idPageBufferSize = 1, - initialFromIdExclusive = initialFromIdExclusive, - initialEndInclusive = initialEndInclusive, - descendingOrder = descendingOrder, - )(new IdPageQuery { - override def fetchPage( - connection: Connection - )(input: PaginationInput): PaginatingAsyncStream.IdPage = { - if (descendingOrder != input.fromTo.descending) { - throw new IllegalArgumentException( - s"Got PaginationInput request with different descending setting (${input.fromTo.descending}) then the test's ($descendingOrder" - ) - } - queries.addOne(input) - val resultIdsPlusOne = if (descendingOrder) { - ids - .filter(id => - id < input.fromTo.fromExclusive && id >= input.fromTo.toInclusive - ) // In backward query end is exclusive! - .reverse - .take(input.limit + 1) - } else { - ids - .filter(id => id > input.fromTo.fromExclusive && id <= input.fromTo.toInclusive) - .take(input.limit + 1) - } - IdPage( - ids = resultIdsPlusOne.take(input.limit), - lastPage = resultIdsPlusOne.sizeIs < input.limit + 1, - ) - } - })(f => Future.successful(f(mock[Connection]))) - .runWith(Sink.seq[Long]) - .map(result => (result.toVector, queries.result())) - } - - it should "stream descending order with length smaller than min page size" in { - val ids = (1L to 50L).toVector - runStreamWithoutIdFilter( - IdPageSizing(minPageSize = 4, maxPageSize = 5), - 2L, - 4L, - ids, - descendingOrder = true, - ).map { case (result, _) => - result shouldBe Vector(4, 3) - } - } - - behavior of "streamIdsFromSeekPaginationWithIdFilter" - - it should "stream in forward order with increasing page size" in { - val ids = (1L to 50L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 50L, - ids, - descendingOrder = false, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe ids - boundQueries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 50), 1), - PaginationInput(PaginationFromTo.ascending(1, 50), 4), - PaginationInput(PaginationFromTo.ascending(5, 50), 16), - PaginationInput(PaginationFromTo.ascending(21, 50), 20), - PaginationInput(PaginationFromTo.ascending(41, 50), 20), - ) - pageQueries shouldBe Vector( - PaginationFromTo.ascending(0, 1), - PaginationFromTo.ascending(1, 5), - PaginationFromTo.ascending(5, 21), - PaginationFromTo.ascending(21, 41), - PaginationFromTo.ascending(41, 50), - ) - } - } - - it should "stream IDs in forward order with constant page size" in { - val ids = (1L to 50L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 20, maxPageSize = 20), - 0L, - 50L, - ids, - descendingOrder = false, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe ids - boundQueries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 50), 20), - PaginationInput(PaginationFromTo.ascending(20, 50), 20), - PaginationInput(PaginationFromTo.ascending(40, 50), 20), - ) - pageQueries shouldBe Vector( - PaginationFromTo.ascending(0, 20), - PaginationFromTo.ascending(20, 40), - PaginationFromTo.ascending(40, 50), - ) - } - } - - it should "stream empty range in forward order" in { - runStreamWithIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 0L, - Vector.empty, - descendingOrder = false, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe Vector.empty - boundQueries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 0), 1) - ) - pageQueries shouldBe Vector() - } - } - - it should "stream single element in forward order" in { - runStreamWithIdFilter( - IdPageSizing(minPageSize = 2, maxPageSize = 10), - 0L, - 1L, - Vector(1L), - descendingOrder = false, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe Vector(1L) - boundQueries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 1), 2) - ) - pageQueries shouldBe Vector( - PaginationFromTo.ascending(0, 1) - ) - } - } - - it should "stream forward order with id set being subset requested range" in { - val ids = (5L to 15L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 3, maxPageSize = 10), - 0L, - 30L, - ids, - descendingOrder = false, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe ids - boundQueries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 30L), 3), - PaginationInput(PaginationFromTo.ascending(7, 30L), 10), - ) - pageQueries shouldBe Vector( - PaginationFromTo.ascending(0, 7), - PaginationFromTo.ascending(7, 30), - ) - } - } - - it should "stream forward order with id set wider than requested range" in { - val ids = (1L to 50L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 3, maxPageSize = 5), - 5L, - 20L, - ids, - descendingOrder = false, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe (6 to 20) - boundQueries shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(5L, 20L), 3), - PaginationInput(PaginationFromTo.ascending(8L, 20L), 5), - PaginationInput(PaginationFromTo.ascending(13L, 20L), 5), - PaginationInput(PaginationFromTo.ascending(18L, 20L), 5), - ) - pageQueries shouldBe Vector( - PaginationFromTo.ascending(5L, 8L), - PaginationFromTo.ascending(8L, 13L), - PaginationFromTo.ascending(13L, 18L), - PaginationFromTo.ascending(18L, 20L), - ) - } - } - - it should "stream IDs in backward order when range matches id set" in { - val ids = (1L to 100L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 100L, - ids, - descendingOrder = true, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe ids.reverse - boundQueries shouldBe Vector( - PaginationInput(PaginationFromTo.descending(0L, 100L), 1), - PaginationInput(PaginationFromTo.descending(0L, 99L), 4), - PaginationInput(PaginationFromTo.descending(0L, 95L), 16), - PaginationInput(PaginationFromTo.descending(0L, 79L), 20), - PaginationInput(PaginationFromTo.descending(0L, 59L), 20), - PaginationInput(PaginationFromTo.descending(0L, 39L), 20), - PaginationInput(PaginationFromTo.descending(0L, 19L), 20), - ) - pageQueries shouldBe Vector( - PaginationFromTo.descending(99L, 100L), - PaginationFromTo.descending(95L, 99L), - PaginationFromTo.descending(79L, 95L), - PaginationFromTo.descending(59L, 79L), - PaginationFromTo.descending(39L, 59L), - PaginationFromTo.descending(19L, 39L), - PaginationFromTo.descending(0L, 19L), - ) - } - } - - it should "stream IDs in backward order when range is wider than id set" in { - val ids = (10L to 50L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 100L, - ids, - descendingOrder = true, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe ids.reverse - boundQueries shouldBe Vector( - PaginationInput(PaginationFromTo.descending(0L, 100L), 1), - PaginationInput(PaginationFromTo.descending(0L, 49L), 4), - PaginationInput(PaginationFromTo.descending(0L, 45L), 16), - PaginationInput(PaginationFromTo.descending(0L, 29L), 20), - ) - pageQueries shouldBe Vector( - PaginationFromTo.descending(49L, 100L), - PaginationFromTo.descending(45L, 49L), - PaginationFromTo.descending(29L, 45L), - PaginationFromTo.descending(0L, 29L), - ) - } - } - - it should "stream empty range in backward order" in { - runStreamWithIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 0L, - 0L, - (1L to 10L).toVector, - descendingOrder = true, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe Vector.empty - boundQueries shouldBe Vector(PaginationInput(PaginationFromTo.descending(0, 0), 1)) - pageQueries shouldBe Vector.empty - } - } - - it should "stream empty set in backward order" in { - runStreamWithIdFilter( - IdPageSizing(minPageSize = 1, maxPageSize = 20), - 1L, - 100L, - Vector.empty, - descendingOrder = true, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe Vector.empty - boundQueries shouldBe Vector(PaginationInput(PaginationFromTo.descending(1, 100), 1)) - pageQueries shouldBe Vector.empty - } - } - - it should "stream descending order with id set being subset requested range" in { - val ids = (5L to 15L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 3, maxPageSize = 10), - 0L, - 30L, - ids, - descendingOrder = true, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe ids.reverse - } - } - - it should "stream descending order with id set wider than requested range" in { - val ids = (1L to 50L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 3, maxPageSize = 5), - 5L, - 20L, - ids, - descendingOrder = true, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe (6 to 20).reverse - } - } - - it should "stream descending order with length smaller than min page size" in { - val ids = (1L to 50L).toVector - runStreamWithIdFilter( - IdPageSizing(minPageSize = 4, maxPageSize = 5), - 2L, - 4L, - ids, - descendingOrder = true, - ).map { case (result, boundQueries, pageQueries) => - result shouldBe Vector(4, 3) - } - } - - private def runStreamWithIdFilter( - idPageSizing: IdPageSizing, - initialFromIdExclusive: Long, - initialEndInclusive: Long, - ids: Vector[Long], - descendingOrder: Boolean, - idFilterQueryParallelism: Int = 1, - ): Future[(Vector[Long], Vector[PaginationInput], Vector[PaginationFromTo])] = { - val boundQueries = Vector.newBuilder[PaginationInput] - val pageQueries = Vector.newBuilder[PaginationFromTo] - paginatingAsyncStream - .streamIdsFromSeekPaginationWithIdFilter( - idStreamName = "test-stream", - idPageSizing = idPageSizing, - idPageBufferSize = 1, - initialFromIdExclusive = initialFromIdExclusive, - initialEndInclusive = initialEndInclusive, - descendingOrder = descendingOrder, - )(new IdFilterPageQuery { - override def fetchPageBounds( - connection: Connection - )(input: PaginationInput): Option[PaginatingAsyncStream.IdPageBounds] = { - boundQueries.addOne(input) - if (descendingOrder) { - val unfilteredIds = ids - .filter(id => id < input.fromTo.fromExclusive && id >= input.fromTo.toInclusive) - .reverse - .take(input.limit + 1) - val lastPage = unfilteredIds.sizeIs < input.limit + 1 - unfilteredIds.lastOption.map(last => - IdPageBounds( - fromTo = - if (lastPage) input.fromTo - else - input.fromTo.copy( - toInclusive = last + 1 - ), - lastPage = lastPage, - ) - ) - } else { - val unfilteredIds = ids - .filter(id => id > input.fromTo.fromExclusive && id <= input.fromTo.toInclusive) - .take(input.limit + 1) - val lastPage = unfilteredIds.sizeIs < input.limit + 1 - unfilteredIds.lastOption.map(last => - IdPageBounds( - fromTo = - if (lastPage) input.fromTo - else - input.fromTo.copy( - toInclusive = last - 1 - ), - lastPage = lastPage, - ) - ) - } - } - - override def fetchPage(connection: Connection)(fromTo: PaginationFromTo): Vector[Long] = { - pageQueries.addOne(fromTo) - val filtered = ids.filter(id => - if (fromTo.descending) - id < fromTo.fromExclusive && id >= fromTo.toInclusive - else - id > fromTo.fromExclusive && id <= fromTo.toInclusive - ) - if (fromTo.descending) filtered.reverse else filtered - } - })( - executeFetchBounds = f => Future.successful(f(mock[Connection])), - idFilterQueryParallelism = idFilterQueryParallelism, - executeFetchPage = f => Future.successful(f(mock[Connection])), - )(TraceContext.empty) - .runWith(Sink.seq[Long]) - .map(result => (result.toVector, boundQueries.result(), pageQueries.result())) - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/SequentialWriteDao.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/SequentialWriteDao.scala deleted file mode 100644 index 9583f538d2..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/SequentialWriteDao.scala +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.daml.metrics.api.MetricsContext -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.ledger.participant.state.Update -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.backend.{ - DbDto, - IngestionStorageBackend, - ParameterStorageBackend, - UpdateToDbDto, -} -import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache -import com.digitalasset.canton.platform.store.dao.events.{CompressionStrategy, LfValueTranslation} -import com.digitalasset.canton.platform.store.interning.{ - InternizingStringInterningView, - StringInterning, -} -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.util.Mutex -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.value.Value.ContractId - -import java.sql.Connection -import scala.collection.mutable -import scala.concurrent.Future -import scala.util.chaining.scalaUtilChainingOps - -trait SequentialWriteDao { - def store(connection: Connection, offset: Offset, update: Option[Update]): Unit -} - -object SequentialWriteDao { - def apply( - participantId: Ref.ParticipantId, - metrics: LedgerApiServerMetrics, - compressionStrategy: CompressionStrategy, - ledgerEndCache: MutableLedgerEndCache, - stringInterningView: StringInterning with InternizingStringInterningView, - ingestionStorageBackend: IngestionStorageBackend[?], - parameterStorageBackend: ParameterStorageBackend, - loggerFactory: NamedLoggerFactory, - ): SequentialWriteDao = - MetricsContext.withMetricLabels("participant_id" -> participantId) { implicit mc => - SequentialWriteDaoImpl( - ingestionStorageBackend = ingestionStorageBackend, - parameterStorageBackend = parameterStorageBackend, - updateToDbDtos = offset => - UpdateToDbDto( - participantId = participantId, - translation = new LfValueTranslation( - metrics = metrics, - engineO = None, - loadPackage = (_, _) => Future.successful(None), - loggerFactory = loggerFactory, - ), - compressionStrategy = compressionStrategy, - metrics, - )(mc)(offset), - ledgerEndCache = ledgerEndCache, - stringInterningView = stringInterningView, - ) - } - -} - -private[dao] final case class SequentialWriteDaoImpl[DbBatch]( - ingestionStorageBackend: IngestionStorageBackend[DbBatch], - parameterStorageBackend: ParameterStorageBackend, - updateToDbDtos: Offset => Update => Iterator[DbDto], - ledgerEndCache: MutableLedgerEndCache, - stringInterningView: StringInterning with InternizingStringInterningView, -) extends SequentialWriteDao { - - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var lastEventSeqId: Long = _ - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var lastStringInterningId: Int = _ - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var lastEventSeqIdInitialized = false - @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var previousTransactionMetaToEventSeqId: Long = _ - - private val lock = new Mutex() - private val acs: mutable.HashMap[(SynchronizerId, ContractId), (Long, Long)] = mutable.HashMap() - - private def lazyInit(connection: Connection): Unit = - if (!lastEventSeqIdInitialized) { - val ledgerEnd = parameterStorageBackend.ledgerEnd(connection) - lastEventSeqId = ledgerEnd.map(_.lastEventSeqId).getOrElse(0) - previousTransactionMetaToEventSeqId = ledgerEnd.map(_.lastEventSeqId).getOrElse(0) - lastStringInterningId = ledgerEnd.map(_.lastStringInterningId).getOrElse(0) - lastEventSeqIdInitialized = true - } - - private def nextEventSeqId: Long = { - lastEventSeqId += 1 - lastEventSeqId - } - - private def adaptEventSeqIds(dbDtos: Iterator[DbDto]): Vector[DbDto] = - dbDtos.map { - case e: DbDto.EventActivate => - val eventSeqId = nextEventSeqId - acs - .put(e.synchronizer_id -> e.notPersistedContractId, eventSeqId -> e.internal_contract_id) - .discard - e.copy(event_sequential_id = eventSeqId) - case e: DbDto.EventDeactivate => - val (deactivatedEventSeqId, internalContractId) = - acs.get(e.synchronizer_id -> e.contract_id) match { - case Some(deactivatedEventSeqId -> internalContractId) => - acs.remove(e.synchronizer_id -> e.contract_id).discard - Some(deactivatedEventSeqId) -> Some(internalContractId) - case None => - None -> None - } - e.copy( - event_sequential_id = nextEventSeqId, - deactivated_event_sequential_id = deactivatedEventSeqId, - internal_contract_id = internalContractId, - ) - case e: DbDto.EventVariousWitnessed => - e.copy(event_sequential_id = nextEventSeqId) - case e: DbDto.IdFilterDbDto => - e.withEventSequentialId(lastEventSeqId) - case e: DbDto.TransactionMeta => - val dto = e.copy( - event_sequential_id_first = (previousTransactionMetaToEventSeqId + 1), - event_sequential_id_last = lastEventSeqId, - ) - previousTransactionMetaToEventSeqId = lastEventSeqId - dto - case notEvent => notEvent - }.toVector - - override def store(connection: Connection, offset: Offset, update: Option[Update]): Unit = - (lock.exclusive { - lazyInit(connection) - - val dbDtos = update - .map(updateToDbDtos(offset)) - .map(adaptEventSeqIds) - .getOrElse(Vector.empty) - - val dbDtosWithStringInterning = - dbDtos - .pipe(stringInterningView.distinctNewRawStrings) - .pipe(stringInterningView.internize) - .map(DbDto.StringInterningDto.from) - .pipe(newEntries => - newEntries.lastOption - .fold(dbDtos) { last => - lastStringInterningId = last.internalId - dbDtos ++ newEntries - } - ) - - dbDtosWithStringInterning - .pipe(ingestionStorageBackend.batch(_, stringInterningView)) - .pipe(ingestionStorageBackend.insertBatch(connection, _)) - - parameterStorageBackend.updateLedgerEnd( - ParameterStorageBackend.LedgerEnd( - lastOffset = offset, - lastEventSeqId = lastEventSeqId, - lastStringInterningId = lastStringInterningId, - lastPublicationTime = CantonTimestamp.MinValue, - ) - )(connection) - - ledgerEndCache.set( - Some( - LedgerEnd( - lastOffset = offset, - lastEventSeqId = lastEventSeqId, - lastStringInterningId = lastStringInterningId, - lastPublicationTime = CantonTimestamp.MinValue, - ) - ) - ) - }) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/SequentialWriteDaoSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/SequentialWriteDaoSpec.scala deleted file mode 100644 index c74aaee0ef..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/SequentialWriteDaoSpec.scala +++ /dev/null @@ -1,464 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao - -import com.digitalasset.canton.crypto.HashAlgorithm.Sha256 -import com.digitalasset.canton.crypto.{Hash, HashPurpose} -import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.ledger.participant.state -import com.digitalasset.canton.ledger.participant.state.Update.TopologyTransactionEffective -import com.digitalasset.canton.ledger.participant.state.{SynchronizerIndex, Update} -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.{ - AchsLastPointers, - LedgerEnd, -} -import com.digitalasset.canton.platform.store.backend.{ - DbDto, - IngestionStorageBackend, - ParameterStorageBackend, -} -import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache -import com.digitalasset.canton.platform.store.dao.SequentialWriteDaoSpec.* -import com.digitalasset.canton.platform.store.interning.{ - InternizingStringInterningView, - StringInterning, - StringInterningDomain, - StringInterningProvider, -} -import com.digitalasset.canton.protocol.TestUpdateId -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.canton.tracing.SerializableTraceContextConverter.SerializableTraceContextExtension -import com.digitalasset.canton.tracing.{SerializableTraceContext, TraceContext} -import com.digitalasset.canton.util.Mutex -import com.digitalasset.daml.lf.data.Ref -import com.digitalasset.daml.lf.data.Ref.{NameTypeConRef, PackageId, Party, UserId} -import com.digitalasset.daml.lf.value.Value.ContractId -import com.google.protobuf.ByteString -import org.mockito.MockitoSugar.mock -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.sql.Connection -import java.util.UUID - -class SequentialWriteDaoSpec extends AnyFlatSpec with Matchers { - - behavior of "SequentialWriteDaoImpl" - private val lock = new Mutex() - - it should "store correctly in a happy path case" in { - val storageBackendCaptor = - new StorageBackendCaptor(Some(LedgerEnd(offset(1), 5, 1, CantonTimestamp.MinValue))) - val ledgerEndCache = MutableLedgerEndCache() - val testee = SequentialWriteDaoImpl( - parameterStorageBackend = storageBackendCaptor, - ingestionStorageBackend = storageBackendCaptor, - updateToDbDtos = updateToDbDtoFixture, - ledgerEndCache = ledgerEndCache, - stringInterningView = stringInterningViewFixture, - ) - testee.store(someConnection, offset(2L), singlePartyFixture) - ledgerEndCache().map(_.lastOffset) shouldBe Some(offset(2L)) - ledgerEndCache().map(_.lastEventSeqId) shouldBe Some(5L) - testee.store(someConnection, offset(3L), allEventsFixture) - ledgerEndCache().map(_.lastOffset) shouldBe Some(offset(3L)) - ledgerEndCache().map(_.lastEventSeqId) shouldBe Some(7L) - testee.store(someConnection, offset(4L), None) - ledgerEndCache().map(_.lastOffset) shouldBe Some(offset(4L)) - ledgerEndCache().map(_.lastEventSeqId) shouldBe Some(7L) - testee.store(someConnection, offset(5L), partyAndCreateFixture) - ledgerEndCache().map(_.lastOffset) shouldBe Some(offset(5L)) - ledgerEndCache().map(_.lastEventSeqId) shouldBe Some(8L) - - storageBackendCaptor.captured(0) shouldBe someParty - storageBackendCaptor - .captured(1) shouldBe LedgerEnd( - offset(2L), - 5, - 1, - CantonTimestamp.MinValue, - ) - storageBackendCaptor - .captured(2) - .asInstanceOf[DbDto.EventActivate] - .event_sequential_id shouldBe 6 - storageBackendCaptor - .captured(3) - .asInstanceOf[DbDto.IdFilterActivateStakeholder] - .idFilter - .event_sequential_id shouldBe 6 - storageBackendCaptor - .captured(4) - .asInstanceOf[DbDto.IdFilterActivateStakeholder] - .idFilter - .event_sequential_id shouldBe 6 - storageBackendCaptor - .captured(5) - .asInstanceOf[DbDto.EventDeactivate] - .event_sequential_id shouldBe 7 - storageBackendCaptor - .captured(6) shouldBe LedgerEnd( - offset(3L), - 7, - 1, - CantonTimestamp.MinValue, - ) - storageBackendCaptor - .captured(7) shouldBe LedgerEnd( - offset(4L), - 7, - 1, - CantonTimestamp.MinValue, - ) - storageBackendCaptor.captured(8) shouldBe someParty - storageBackendCaptor - .captured(9) - .asInstanceOf[DbDto.EventActivate] - .event_sequential_id shouldBe 8 - storageBackendCaptor - .captured(10) shouldBe LedgerEnd( - offset(5L), - 8, - 1, - CantonTimestamp.MinValue, - ) - storageBackendCaptor.captured should have size 11 - } - - it should "start event_seq_id from 1" in { - val storageBackendCaptor = new StorageBackendCaptor(LedgerEnd.beforeBegin) - val ledgerEndCache = MutableLedgerEndCache() - val testee = SequentialWriteDaoImpl( - parameterStorageBackend = storageBackendCaptor, - ingestionStorageBackend = storageBackendCaptor, - updateToDbDtos = updateToDbDtoFixture, - ledgerEndCache = ledgerEndCache, - stringInterningView = stringInterningViewFixture, - ) - testee.store(someConnection, offset(3L), None) - ledgerEndCache().map(_.lastOffset) shouldBe Some(offset(3L)) - ledgerEndCache().map(_.lastEventSeqId) shouldBe Some(0L) - testee.store(someConnection, offset(4L), partyAndCreateFixture) - ledgerEndCache().map(_.lastOffset) shouldBe Some(offset(4L)) - ledgerEndCache().map(_.lastEventSeqId) shouldBe Some(1L) - - storageBackendCaptor - .captured(0) shouldBe LedgerEnd( - offset(3L), - 0, - 0, - CantonTimestamp.MinValue, - ) - storageBackendCaptor.captured(1) shouldBe someParty - storageBackendCaptor - .captured(2) - .asInstanceOf[DbDto.EventActivate] - .event_sequential_id shouldBe 1 - storageBackendCaptor - .captured(3) shouldBe LedgerEnd( - offset(4L), - 1, - 0, - CantonTimestamp.MinValue, - ) - storageBackendCaptor.captured should have size 4 - } - - class StorageBackendCaptor(initialLedgerEnd: Option[ParameterStorageBackend.LedgerEnd]) - extends IngestionStorageBackend[Vector[DbDto]] - with ParameterStorageBackend { - - var captured: Vector[Any] = Vector.empty - - override def batch(dbDtos: Vector[DbDto], stringInterning: StringInterning): Vector[DbDto] = - dbDtos - - override def insertBatch(connection: Connection, batch: Vector[DbDto]): Unit = ( - lock.exclusive { - connection shouldBe someConnection - captured = captured ++ batch - } - ) - - override def deletePartiallyIngestedData(ledgerEnd: Option[ParameterStorageBackend.LedgerEnd])( - connection: Connection - ): Unit = - throw new UnsupportedOperationException - - override def updateLedgerEnd( - params: ParameterStorageBackend.LedgerEnd, - synchronizerIndexes: Map[SynchronizerId, SynchronizerIndex], - )(connection: Connection): Unit = - (lock.exclusive { - connection shouldBe someConnection - captured = captured :+ params - }) - - private var ledgerEndCalled = false - override def ledgerEnd(connection: Connection): Option[ParameterStorageBackend.LedgerEnd] = - (lock.exclusive { - connection shouldBe someConnection - ledgerEndCalled shouldBe false - ledgerEndCalled = true - initialLedgerEnd - }) - - override def initializeParameters( - params: ParameterStorageBackend.IdentityParams, - loggerFactory: NamedLoggerFactory, - )( - connection: Connection - ): Unit = - throw new UnsupportedOperationException - - override def ledgerIdentity( - connection: Connection - ): Option[ParameterStorageBackend.IdentityParams] = - throw new UnsupportedOperationException - - override def updatePrunedUptoInclusive(prunedUpToInclusive: Offset)( - connection: Connection - ): Unit = - throw new UnsupportedOperationException - - override def prunedUpToInclusive(connection: Connection): Option[Offset] = - throw new UnsupportedOperationException - - override def prunedUpToInclusiveAndLedgerEnd( - connection: Connection - ): ParameterStorageBackend.PruneUptoInclusiveAndLedgerEnd = - throw new UnsupportedOperationException - - override def cleanSynchronizerIndex(synchronizerId: SynchronizerId)( - connection: Connection - ): Option[SynchronizerIndex] = - throw new UnsupportedOperationException - - override def updatePostProcessingEnd(postProcessingEnd: Option[Offset])( - connection: Connection - ): Unit = - throw new UnsupportedOperationException - - override def postProcessingEnd(connection: Connection): Option[Offset] = - throw new UnsupportedOperationException - - override def fetchACHSState(connection: Connection): Option[ParameterStorageBackend.AchsState] = - throw new UnsupportedOperationException - - override def insertACHSState(achsState: ParameterStorageBackend.AchsState)( - connection: Connection - ): Unit = throw new UnsupportedOperationException - - override def updateACHSValidAt(validAt: Long)(connection: Connection): Unit = - throw new UnsupportedOperationException - - override def updateACHSLastPointers(lastPointers: AchsLastPointers)( - connection: Connection - ): Unit = - throw new UnsupportedOperationException - - override def clearACHSState(connection: Connection): Unit = - throw new UnsupportedOperationException - - override def clearAchsData(connection: Connection): Unit = - throw new UnsupportedOperationException - - } -} - -object SequentialWriteDaoSpec { - - private val serializableTraceContext = - SerializableTraceContext(TraceContext.empty).toSerializedDamlProto - - private val externalTransactionHash = - Hash - .digest(HashPurpose.PreparedSubmission, ByteString.copyFromUtf8("mock_hash"), Sha256) - .unwrap - .toByteArray - - private def offset(l: Long): Offset = Offset.tryFromLong(l) - - private def hashCid(key: String): ContractId = - ContractId.V1(com.digitalasset.daml.lf.crypto.Hash.hashPrivateKey(key)) - - private def someUpdate(key: String) = Some( - state.Update.TopologyTransactionEffective( - updateId = TestUpdateId(UUID.randomUUID().toString), - events = Set( - TopologyTransactionEffective.TopologyEvent.PartyToParticipantAuthorization( - party = Ref.Party.assertFromString(key), - participant = Ref.ParticipantId.assertFromString("participant"), - authorizationEvent = TopologyTransactionEffective.AuthorizationEvent.Added( - TopologyTransactionEffective.AuthorizationLevel.Confirmation - ), - ) - ), - synchronizerId = SynchronizerId.tryFromString("invalid::deadbeef"), - effectiveTime = CantonTimestamp.now(), - )(TraceContext.empty) - ) - - private val someParty = DbDto.PartyEntry( - ledger_offset = 1, - recorded_at = 0, - submission_id = null, - party = Some(Ref.Party.assertFromString("party")), - typ = "accept", - rejection_reason = None, - is_local = Some(true), - ) - - private val someEventActivate = DbDto.EventActivate( - event_offset = 1, - update_id = new Array[Byte](0), - command_id = None, - workflow_id = None, - submitters = None, - node_id = 3, - representative_package_id = Ref.PackageId.fromInt(3), - create_key_hash = None, - event_sequential_id = 0, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer"), - trace_context = serializableTraceContext, - record_time = 0, - external_transaction_hash = Some(externalTransactionHash), - traffic_cost = Some(1565L), - internal_contract_id = 42L, - event_type = 1, - additional_witnesses = Some(Set.empty), - source_synchronizer_id = None, - reassignment_id = None, - reassignment_counter = None, - notPersistedContractId = hashCid("24"), - ) - - private val someEventDeactivate = DbDto.EventDeactivate( - event_offset = 1, - update_id = new Array[Byte](0), - ledger_effective_time = None, - command_id = None, - workflow_id = None, - submitters = None, - node_id = 3, - contract_id = hashCid("24"), - template_id = Ref.NameTypeConRef.assertFromString("#p:m:t"), - package_id = Ref.PackageId.fromInt(2), - exercise_choice = Some(Ref.ChoiceName.assertFromString("choice")), - exercise_choice_interface_id = None, - exercise_argument = Some(Array.empty), - exercise_result = None, - exercise_actors = Some(Set.empty), - exercise_last_descendant_node_id = Some(3), - exercise_argument_compression = None, - exercise_result_compression = None, - event_sequential_id = 0, - synchronizer_id = SynchronizerId.tryFromString("x::synchronizer"), - trace_context = serializableTraceContext, - record_time = 0, - external_transaction_hash = Some(externalTransactionHash), - traffic_cost = Some(1565L), - deactivated_event_sequential_id = None, - event_type = 3, - additional_witnesses = None, - internal_contract_id = None, - stakeholders = Set.empty, - reassignment_id = None, - reassignment_counter = None, - assignment_exclusivity = None, - target_synchronizer_id = None, - ) - - val singlePartyFixture: Option[Update.TopologyTransactionEffective] = - someUpdate("singleParty") - val partyAndCreateFixture: Option[Update.TopologyTransactionEffective] = - someUpdate("partyAndCreate") - val allEventsFixture: Option[Update.TopologyTransactionEffective] = - someUpdate("allEventsFixture") - - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) - private val someUpdateToDbDtoFixture: Map[Ref.Party, List[DbDto]] = Map( - Ref.Party.assertFromString("singleParty") -> List(someParty), - Ref.Party.assertFromString("partyAndCreate") -> List(someParty, someEventActivate), - Ref.Party.assertFromString("allEventsFixture") -> List( - someEventActivate, - DbDto - .IdFilter( - 0L, - Ref.NameTypeConRef.assertFromString("#p:m:t"), - Ref.Party.assertFromString("party"), - first_per_sequential_id = true, - ) - .activateStakeholder, - DbDto - .IdFilter( - 0L, - Ref.NameTypeConRef.assertFromString("#p:m:t"), - Ref.Party.assertFromString("party"), - first_per_sequential_id = false, - ) - .activateStakeholder, - someEventDeactivate, - ), - ) - - private val updateToDbDtoFixture: Offset => Update => Iterator[DbDto] = - _ => { - case r: Update.TopologyTransactionEffective => - val party = r.events - .collectFirst { - case pa: Update.TopologyTransactionEffective.TopologyEvent.PartyToParticipantAuthorization => - pa.party - } - .getOrElse(throw new IllegalStateException()) - someUpdateToDbDtoFixture(party).iterator - case _ => throw new Exception - } - - private val stringInterningViewFixture: StringInterning with InternizingStringInterningView = - new StringInterning with InternizingStringInterningView { - override def templateId: StringInterningDomain[NameTypeConRef] = - ??? - - override def packageId: StringInterningDomain[PackageId] = - ??? - - override def party: StringInterningDomain[Party] = ??? - - override def synchronizerId: StringInterningDomain[SynchronizerId] = - ??? - - override def userId: StringInterningDomain[UserId] = ??? - - override def participantId: StringInterningDomain[Ref.ParticipantId] = - ??? - - override def choiceName: StringInterningDomain[Ref.ChoiceName] = - ??? - - override def interfaceId: StringInterningDomain[Ref.Identifier] = - ??? - - override private[platform] def distinctNewRawStrings( - interningProviders: Iterable[StringInterningProvider] - ): Iterable[String] = - Nil - - /** @return - * If some of the entries were not part of the view: they will be added, and these will be - * returned as a interned-id and raw, prefixed string pairs. - * @note - * This method is thread-safe. This method should be called from Indexer, which maintains - * consistency between StringInterning view and persistence. - */ - override private[platform] def internize( - distinctRawStrings: Iterable[String] - ): Iterable[(Int, String)] = - Iterator.iterate(1)(_ + 1).zip(distinctRawStrings).toVector - } - - private val someConnection = mock[Connection] - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/ACSReaderSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/ACSReaderSpec.scala deleted file mode 100644 index 10a4016f25..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/ACSReaderSpec.scala +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream -import com.digitalasset.canton.platform.store.dao.PaginatingAsyncStream.{ - IdPage, - IdPageQuery, - PaginationFromTo, - PaginationInput, -} -import com.digitalasset.canton.platform.store.dao.events.EventIdsUtils.* -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.{Assertion, BeforeAndAfterAll} - -import java.sql.Connection -import scala.concurrent.duration.DurationInt -import scala.concurrent.{Await, ExecutionContext, Future} - -class ACSReaderSpec extends AsyncFlatSpec with BaseTest with BeforeAndAfterAll { - - private val actorSystem = ActorSystem() - private implicit val materializer: Materializer = Materializer(actorSystem) - private implicit val ec: ExecutionContext = actorSystem.dispatcher - - private val paginatingAsyncStream = new PaginatingAsyncStream(loggerFactory) - - override def afterAll(): Unit = { - Await.result(actorSystem.terminate(), 10.seconds) - () - } - - behavior of "IdQueryConfiguration" - - it should "compute correct parameters for a realistic case" in { - def realisticConfigForFilterSize(filterSize: Int) = IdPageSizing.calculateFrom( - maxIdPageSize = 10000, - workingMemoryInBytesForIdPages = 100 * 1024 * 1024, - numOfDecomposedFilters = filterSize, - numOfPagesInIdPageBuffer = 1, - loggerFactory = loggerFactory, - ) - // progression: 200 800 3200 10000 10000... - realisticConfigForFilterSize(1) shouldBe IdPageSizing(200, 10000) - realisticConfigForFilterSize(10) shouldBe IdPageSizing(200, 10000) - realisticConfigForFilterSize(100) shouldBe IdPageSizing(200, 10000) - // 200 800 3200 6553 6553... - realisticConfigForFilterSize(1000) shouldBe IdPageSizing(200, 6553) - // 200 655 655... - realisticConfigForFilterSize(10000) shouldBe IdPageSizing(200, 655) - loggerFactory.assertLogs( - within = { - realisticConfigForFilterSize(100000) shouldBe IdPageSizing(65, 65) - realisticConfigForFilterSize(1000000) shouldBe IdPageSizing(10, 10) - realisticConfigForFilterSize(10000000) shouldBe IdPageSizing(10, 10) - }, - assertions = _.warningMessage should include( - "Calculated maximum ID page size supporting API stream memory limits [65] is very low" - ), - _.warningMessage should include( - "Calculated maximum ID page size supporting API stream memory limits [6] is too low" - ), - _.warningMessage should include( - "Calculated maximum ID page size supporting API stream memory limits [0] is too low" - ), - ) - - } - - it should "compute correct parameters, if maxIdPageSize is lower than recommended (200), then maxIdPageSize is preferred" in { - def configWith(filterSize: Int) = IdPageSizing.calculateFrom( - maxIdPageSize = 150, - workingMemoryInBytesForIdPages = 100 * 1024 * 1024, - numOfDecomposedFilters = filterSize, - numOfPagesInIdPageBuffer = 1, - loggerFactory = loggerFactory, - ) - configWith(1) shouldBe IdPageSizing(150, 150) - configWith(10) shouldBe IdPageSizing(150, 150) - configWith(100) shouldBe IdPageSizing(150, 150) - configWith(1000) shouldBe IdPageSizing(150, 150) - configWith(10000) shouldBe IdPageSizing(150, 150) - loggerFactory.assertLogs( - within = { - configWith(100000) shouldBe IdPageSizing(65, 65) - configWith(1000000) shouldBe IdPageSizing(10, 10) - configWith(10000000) shouldBe IdPageSizing(10, 10) - }, - assertions = _.warningMessage should include( - "Calculated maximum ID page size supporting API stream memory limits [65] is very low" - ), - _.warningMessage should include( - "Calculated maximum ID page size supporting API stream memory limits [6] is too low" - ), - _.warningMessage should include( - "Calculated maximum ID page size supporting API stream memory limits [0] is too low" - ), - ) - } - - it should "compute correct parameters, if maxIdPageSize is lower than minimum (10), then maxIdPageSize is preferred" in { - def configWith(filterSize: Int) = IdPageSizing.calculateFrom( - maxIdPageSize = 4, - workingMemoryInBytesForIdPages = 100 * 1024 * 1024, - numOfDecomposedFilters = filterSize, - numOfPagesInIdPageBuffer = 1, - loggerFactory = loggerFactory, - ) - configWith(1) shouldBe IdPageSizing(4, 4) - configWith(10) shouldBe IdPageSizing(4, 4) - configWith(100) shouldBe IdPageSizing(4, 4) - configWith(1000) shouldBe IdPageSizing(4, 4) - configWith(10000) shouldBe IdPageSizing(4, 4) - configWith(100000) shouldBe IdPageSizing(4, 4) - configWith(1000000) shouldBe IdPageSizing(4, 4) - loggerFactory.assertLogs( - configWith(10000000) shouldBe IdPageSizing(4, 4), - _.warningMessage should include( - "Calculated maximum ID page size supporting API stream memory limits [0] is too low" - ), - ) - } - - behavior of "idSource" - - it should "stream data exponentially" in { - testIdSource( - IdPageSizing( - minPageSize = 1, - maxPageSize = 20, - ), - Range(1, 70).map(_.toLong).toVector, - ).map( - _ shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 69), 1), - PaginationInput(PaginationFromTo.ascending(1, 69), 4), - PaginationInput(PaginationFromTo.ascending(5, 69), 16), - PaginationInput(PaginationFromTo.ascending(21, 69), 20), - PaginationInput(PaginationFromTo.ascending(41, 69), 20), - PaginationInput(PaginationFromTo.ascending(61, 69), 20), - PaginationInput(PaginationFromTo.ascending(69, 69), 20), - ) - ) - } - - it should "stream data constantly" in { - testIdSource( - IdPageSizing( - minPageSize = 20, - maxPageSize = 20, - ), - Range(1, 70).map(_.toLong).toVector, - ).map( - _ shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 69), 20), - PaginationInput(PaginationFromTo.ascending(20, 69), 20), - PaginationInput(PaginationFromTo.ascending(40, 69), 20), - PaginationInput(PaginationFromTo.ascending(60, 69), 20), - PaginationInput(PaginationFromTo.ascending(69, 69), 20), - ) - ) - } - - it should "stream data exponentially, if maxPageSize never reached" in { - testIdSource( - IdPageSizing( - minPageSize = 1, - maxPageSize = 20, - ), - Range(1, 6).map(_.toLong).toVector, - ).map( - _ shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 5), 1), - PaginationInput(PaginationFromTo.ascending(1, 5), 4), - PaginationInput(PaginationFromTo.ascending(5, 5), 16), - ) - ) - } - - it should "stream empty data" in { - testIdSource( - IdPageSizing( - minPageSize = 1, - maxPageSize = 20, - ), - Vector.empty, - ).map( - _ shouldBe Vector( - PaginationInput(PaginationFromTo.ascending(0, 0), 1) - ) - ) - } - - behavior of "mergeSort" - - it should "sort correctly zero sources" in testMergeSort { - Vector.empty - } - - it should "sort correctly one source" in testMergeSort { - Vector( - sortedRandomInts(10) - ) - } - - it should "sort correctly one empty source" in testMergeSort { - Vector( - sortedRandomInts(0) - ) - } - - it should "sort correctly 2 sources with same size" in testMergeSort { - Vector( - sortedRandomInts(10), - sortedRandomInts(10), - ) - } - - it should "sort correctly 2 sources with different size" in testMergeSort { - Vector( - sortedRandomInts(5), - sortedRandomInts(10), - ) - } - - it should "sort correctly 2 sources one of them empty" in testMergeSort { - Vector( - sortedRandomInts(0), - sortedRandomInts(10), - ) - } - - it should "sort correctly 2 sources both of them empty" in testMergeSort { - Vector( - sortedRandomInts(0), - sortedRandomInts(0), - ) - } - - it should "sort correctly 10 sources, random size" in testMergeSort( - Vector.fill(10)(sortedRandomInts(10)), - times = 100, - ) - - behavior of "statefulDeduplicate" - - it should "deduplicate a stream correctly" in { - Source(Vector(1, 1, 2, 2, 2, 3, 4, 4, 5, 6, 7, 0, 0, 0)) - .statefulMapConcat(statefulDeduplicate) - .runWith(Sink.seq) - .map(_ shouldBe Vector(1, 2, 3, 4, 5, 6, 7, 0)) - } - - it should "preserve a stream of unique numbers" in { - Source(Vector(1, 2, 3, 4, 5, 6, 7, 0)) - .statefulMapConcat(statefulDeduplicate) - .runWith(Sink.seq) - .map(_ shouldBe Vector(1, 2, 3, 4, 5, 6, 7, 0)) - } - - it should "work for empty stream" in { - Source(Vector.empty) - .statefulMapConcat(statefulDeduplicate) - .runWith(Sink.seq) - .map(_ shouldBe Vector.empty) - } - - it should "work for one sized stream" in { - Source(Vector(1)) - .statefulMapConcat(statefulDeduplicate) - .runWith(Sink.seq) - .map(_ shouldBe Vector(1)) - } - - it should "work if only duplications present" in { - Source(Vector(1, 1, 1, 1)) - .statefulMapConcat(statefulDeduplicate) - .runWith(Sink.seq) - .map(_ shouldBe Vector(1)) - } - - private def sortedRandomInts(length: Int): Vector[Int] = - Vector.fill(length)(scala.util.Random.nextInt(10)).sorted - - private def testMergeSort(in: => Vector[Vector[Int]], times: Int = 5): Future[Assertion] = { - val testInput = in - EventIdsUtils - .mergeSort[Int]( - sources = testInput.map(Source.apply) - ) - .runWith(Sink.seq) - .map(_ shouldBe testInput.flatten.sorted) - .flatMap { result => - if (times == 0) Future.successful(result) - else testMergeSort(in, times - 1) - } - } - - private def testIdSource( - idQueryConfiguration: IdPageSizing, - ids: Vector[Long], - ): Future[Vector[PaginationInput]] = { - val queries = Vector.newBuilder[PaginationInput] - paginatingAsyncStream - .streamIdsFromSeekPaginationWithoutIdFilter( - idStreamName = "test-stream", - idPageSizing = idQueryConfiguration, - idPageBufferSize = 1, - initialFromIdExclusive = 0L, - initialEndInclusive = ids.lastOption.getOrElse(0), - descendingOrder = false, - )(new IdPageQuery { - override def fetchPage(connection: Connection)(input: PaginationInput): IdPage = { - assert(!input.fromTo.descending) - queries.addOne(input) - val idsPlusOne = ids - .dropWhile(_ <= input.fromTo.fromExclusive) - .take(input.limit + 1) - IdPage( - ids = idsPlusOne.take(input.limit), - lastPage = idsPlusOne.sizeIs < input.limit + 1, - ) - } - })(f => Future.successful(f(mock[Connection]))) - .runWith(Sink.seq[Long]) - .map { result => - result shouldBe ids - queries.result() - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/GroupContiguousSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/GroupContiguousSpec.scala deleted file mode 100644 index 57795171fc..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/GroupContiguousSpec.scala +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import org.apache.pekko.stream.scaladsl.{Sink, Source} -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks - -final class GroupContiguousSpec - extends AsyncFlatSpec - with Matchers - with ScalaCheckPropertyChecks - with ScalaFutures - with PekkoBeforeAndAfterAll { - import UpdateReader.groupContiguous - - behavior of "groupContiguous" - - override def spanScaleFactor: Double = 10 // Give some extra slack on CI - - it should "be equivalent to grouping on inputs with an ordered key" in forAll { - (pairs: List[(Int, String)]) => - val sortedPairs = pairs.sortBy(_._1) - val grouped = groupContiguous(Source(sortedPairs))(by = _._1) - whenReady(grouped.runWith(Sink.seq[Vector[(Int, String)]])) { - _ should contain theSameElementsAs pairs.groupBy(_._1).values - } - } - - it should "be equivalent to grouping on inputs with a contiguous key" in { - val pairsWithContiguousKeys = List(1 -> "baz", 0 -> "foo", 0 -> "bar", 0 -> "quux") - val grouped = groupContiguous(Source(pairsWithContiguousKeys))(by = _._1) - whenReady(grouped.runWith(Sink.seq[Vector[(Int, String)]])) { - _.map(_.toSet) should contain theSameElementsAs pairsWithContiguousKeys - .groupBy(_._1) - .map(_._2.toSet) - } - } - - it should "behave as expected when grouping inputs without a contiguous key" in { - val pairs = List(0 -> "foo", 0 -> "bar", 1 -> "baz", 0 -> "quux") - val grouped = groupContiguous(Source(pairs))(by = _._1) - whenReady(grouped.runWith(Sink.seq[Vector[(Int, String)]])) { - _.map(_.toSet) should contain theSameElementsAs Vector( - Set(0 -> "foo", 0 -> "bar"), - Set(1 -> "baz"), - Set(0 -> "quux"), - ) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/InputContractPackagesTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/InputContractPackagesTest.scala deleted file mode 100644 index ca861c720f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/InputContractPackagesTest.scala +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.protocol.ExampleTransactionFactory.{ - exerciseNode, - fetchNode, - queryByKeyNode, - templateId, -} -import com.digitalasset.canton.protocol.{ExampleContractFactory, LfGlobalKey, LfTemplateId} -import com.digitalasset.canton.util.LfTransactionBuilder.defaultPackageName -import com.digitalasset.canton.{BaseTest, LfPackageId} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.transaction.test.TestIdFactory -import com.digitalasset.daml.lf.transaction.test.TreeTransactionBuilder.{ - NodeOps, - toVersionedTransaction, -} -import com.digitalasset.daml.lf.value.Value -import org.scalatest.wordspec.AnyWordSpec - -class InputContractPackagesTest extends AnyWordSpec with BaseTest with TestIdFactory { - - import InputContractPackages.* - - val (cid1, cid2, cid3) = (newCid, newCid, newCid) - val (p1, p2, p3) = (newPackageId, newPackageId, newPackageId) - def t(pId: LfPackageId): LfTemplateId = LfTemplateId(pId, templateId.qualifiedName) - - "InputContractPackages.forTransaction" should { - - "extract package ids associated with nodes" in { - - val globalKey = LfGlobalKey.assertBuild( - t(p2), - defaultPackageName, - Value.ValueUnit, - crypto.Hash.hashPrivateKey("dummy-key-hash"), - ) - - val example = toVersionedTransaction( - exerciseNode(cid1, templateId = t(p1)).withChildren( - queryByKeyNode(globalKey, resolution = Vector(cid2)), - fetchNode(cid3, templateId = t(p3)), - ) - ).transaction - - forTransaction(example) shouldBe Map( - cid1 -> Set(p1), - cid2 -> Set(p2), - cid3 -> Set(p3), - ) - } - - "return multiple package where the same contract is bound to different packages" in { - - val example = toVersionedTransaction( - exerciseNode(cid1, templateId = t(p1)).withChildren( - exerciseNode(cid1, templateId = t(p2)), - exerciseNode(cid1, templateId = t(p3)), - ) - ).transaction - - forTransaction(example) shouldBe Map( - cid1 -> Set(p1, p2, p3) - ) - } - - } - - "InputContractPackages.mergeToExactTuple" should { - "work where both maps have identical keys" in { - strictZipByKey(Map(1 -> "a", 2 -> "b"), Map(1 -> 3.0, 2 -> 4.0)) shouldBe Right( - Map(1 -> ("a", 3.0), 2 -> ("b", 4.0)) - ) - } - "fail where the key sets are unequal" in { - inside(strictZipByKey(Map(1 -> "a", 2 -> "b"), Map(2 -> 4.0, 3 -> 5.0))) { - case Left(mismatch) => mismatch shouldBe Set(1, 3) - } - } - - } - - "InputContractPackages.forTransactionWithContracts" should { - - val cid = newCid - val inst = ExampleContractFactory.build() - val tx = toVersionedTransaction( - exerciseNode(cid, templateId = t(p1)) - ).transaction - - "combine transaction contracts with contracts instances map" in { - forTransactionWithContracts(tx, Map(cid -> inst)) shouldBe Right( - Map(cid -> (inst.inst, Set(p1))) - ) - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/LfEnricherSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/LfEnricherSpec.scala deleted file mode 100644 index 9ca5328dae..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/LfEnricherSpec.scala +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.digitalasset.canton.examples.java.trailingnone.TrailingNone -import com.digitalasset.canton.logging.LoggingContextWithTrace -import com.digitalasset.canton.metrics.LedgerApiServerMetrics -import com.digitalasset.canton.platform.packages.DeduplicatingPackageLoader -import com.digitalasset.canton.util.TestEngine -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import com.digitalasset.daml.lf.transaction.Node -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.{ValueParty, ValueRecord} -import org.scalatest.Assertion -import org.scalatest.wordspec.AnyWordSpec - -import java.util.Optional -import scala.concurrent.Future - -class LfEnricherSpec extends AnyWordSpec with HasExecutionContext with BaseTest { - - private implicit val lc: LoggingContextWithTrace = LoggingContextWithTrace.ForTesting - - "LfEnricher" should { - - "remove trailing None fields when enriching values" in { - - val testEngine = new TestEngine(Seq(CantonExamplesPath), loggerFactory = loggerFactory) - val underTest = LfEnricher( - engine = testEngine.engine, - forbidLocalContractIds = false, - metrics = LedgerApiServerMetrics.ForTesting, - packageLoader = new DeduplicatingPackageLoader(), - loadPackage = ( - packageId, - _, - ) => Future.successful(testEngine.packageStore.getArchive(packageId)), - ) - - val alice = "alice" - val command = new TrailingNone(alice, Optional.empty()).create.commands.loneElement - val (tx, _) = testEngine.submitAndConsume(command, alice) - val createNode = tx.nodes.values.collect { case e: Node.Create => e }.loneElement - val inst = testEngine.suffix(createNode) - - // Contract is not enriched at this point - inside(inst.createArg) { - case ValueRecord(None, _) => succeed - case other => fail(s"Expected ValueRecord, got $other") - } - - def checkNoTrailingNoneFields(createArg: Value): Assertion = - inside(createArg) { - case ValueRecord(Some(_), fields) => - inside(fields.toList) { - case (Some("p"), ValueParty(`alice`)) :: Nil => succeed - case other => fail(s"Did not expect: $other") - } - succeed - case other => fail(s"Expected enriched contract, got: $other") - } - - // Enrich contract value - checkNoTrailingNoneFields( - underTest.enrichContractValue(inst.templateId, inst.createArg).futureValue - ) - - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/PekkoStreamParallelBatchedLoaderSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/PekkoStreamParallelBatchedLoaderSpec.scala deleted file mode 100644 index f034210c89..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/PekkoStreamParallelBatchedLoaderSpec.scala +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import com.daml.testing.utils.PekkoBeforeAndAfterAll -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.logging.LoggingContextWithTrace -import org.apache.pekko.stream.scaladsl.Source -import org.scalatest.flatspec.AsyncFlatSpec - -import java.util.concurrent.atomic.AtomicInteger -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -class PekkoStreamParallelBatchedLoaderSpec - extends AsyncFlatSpec - with BaseTest - with PekkoBeforeAndAfterAll { - - // AsyncFlatSpec is with serial execution context - private implicit val ec: ExecutionContext = system.dispatcher - - private implicit val loggingContext: LoggingContextWithTrace = LoggingContextWithTrace.empty - - it should "not batch if no backpressure" in { - val testee = new PekkoStreamParallelBatchedLoader[Int, Int]( - batchLoad = in => { - Future(in.size shouldBe 1) - .map(_ => in.map(_._1).map(x => x -> x).toMap) - }, - createQueue = () => Source.queue(10), - parallelism = 5, - maxBatchSize = 10, - loggerFactory = loggerFactory, - ) - - val inputs = 1.to(100) - - for { - _ <- inputs.foldLeft(Future.successful(succeed)) { case (f, num) => - f.flatMap(_ => - testee - .load(num) - .map(_ shouldBe Some(num)) - ) - } - _ <- testee.closeAsync() // teardown - } yield succeed - } - - it should "batch if backpressure" in { - val fullBatchCounter = new AtomicInteger(0) - val testee = new PekkoStreamParallelBatchedLoader[Int, Int]( - batchLoad = in => { - Future { - Threading.sleep(10) - fullBatchCounter.incrementAndGet() - succeed - } - .map(_ => in.map(_._1).map(x => x -> x).toMap) - }, - createQueue = () => Source.queue(1000), - parallelism = 5, - maxBatchSize = 10, - loggerFactory = loggerFactory, - ) - - val inputs = 1.to(100) - - for { - _ <- Future.sequence( - inputs.map(num => testee.load(num).map(_ shouldBe Some(num))) - ) - _ <- testee.closeAsync() // teardown - } yield { - fullBatchCounter.get() should be > 8 - } - } - - it should "throw backpressure error if queue exhausted" in { - val testee = new PekkoStreamParallelBatchedLoader[Int, Int]( - batchLoad = in => { - Future { - Threading.sleep(10) - succeed - } - .map(_ => in.map(_._1).map(x => x -> x).toMap) - }, - createQueue = () => Source.queue(1), - parallelism = 5, - maxBatchSize = 10, - loggerFactory = loggerFactory, - ) - - val inputs = 1.to(100) - - for { - _ <- Future - .sequence( - inputs.map(num => testee.load(num).map(_ shouldBe Some(num))) - ) - .failed - .map(_.getMessage should include("PARTICIPANT_BACKPRESSURE")) - _ <- testee.closeAsync() // teardown - } yield succeed - } - - it should "keep working if batch loading throws error" in { - val testee = new PekkoStreamParallelBatchedLoader[Int, Int]( - batchLoad = in => { - Future { - if (in.head._1 == 50) throw new Exception("boom") - else succeed - } - .map(_ => in.map(_._1).map(x => x -> x).toMap) - }, - createQueue = () => Source.queue(1000), - parallelism = 5, - maxBatchSize = 1, - loggerFactory = loggerFactory, - ) - - val inputs = 1.to(100) - - for { - _ <- Future.sequence( - inputs.map(num => - testee.load(num).transform { - case Success(value) => Success(value shouldBe Some(num)) - case Failure(value) => - Try { - num shouldBe 50 - value.getMessage should include("boom") - } - } - ) - ) - _ <- testee.closeAsync() // teardown - } yield succeed - } - - it should "keep working if batch loading throws error before async barrier" in { - val testee = new PekkoStreamParallelBatchedLoader[Int, Int]( - batchLoad = in => { - if (in.head._1 == 50) throw new Exception("boom") - Future(succeed) - .map(_ => in.map(_._1).map(x => x -> x).toMap) - }, - createQueue = () => Source.queue(1000), - parallelism = 5, - maxBatchSize = 1, - loggerFactory = loggerFactory, - ) - - val inputs = 1.to(100) - - for { - _ <- Future.sequence( - inputs.map(num => - testee.load(num).transform { - case Success(value) => Success(value shouldBe Some(num)) - case Failure(value) => - Try { - num shouldBe 50 - value.getMessage should include("boom") - } - } - ) - ) - _ <- testee.closeAsync() // teardown - } yield succeed - } - - it should "reject load requests after closed" in { - val testee = new PekkoStreamParallelBatchedLoader[Int, Int]( - batchLoad = _ => { - throw new Exception("boom") - }, - createQueue = () => Source.queue(1000), - parallelism = 5, - maxBatchSize = 1, - loggerFactory = loggerFactory, - ) - - for { - _ <- testee.closeAsync() - error <- testee.load(10).failed - } yield { - error.getMessage should include("Queue closed") - } - } - - it should "process enqueued items before closed" in { - val testee = new PekkoStreamParallelBatchedLoader[Int, Int]( - batchLoad = in => { - Future { - Threading.sleep(10) - succeed - } - .map(_ => in.map(_._1).map(x => x -> x).toMap) - }, - createQueue = () => Source.queue(1000), - parallelism = 5, - maxBatchSize = 10, - loggerFactory = loggerFactory, - ) - - val inputFs = 1.to(100).map(num => testee.load(num).map(_ shouldBe Some(num))) - - for { - _ <- testee.closeAsync() - _ <- Future.sequence(inputFs) - } yield { - succeed - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/UtilsSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/UtilsSpec.scala deleted file mode 100644 index 375ad54a34..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/events/UtilsSpec.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.dao.events - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class UtilsSpec extends AnyFlatSpec with Matchers { - - it should "compute largest smaller power of two" in { - Utils.largestSmallerOrEqualPowerOfTwo(31) shouldBe 16 - Utils.largestSmallerOrEqualPowerOfTwo(16) shouldBe 16 - Utils.largestSmallerOrEqualPowerOfTwo(9) shouldBe 8 - Utils.largestSmallerOrEqualPowerOfTwo(8) shouldBe 8 - Utils.largestSmallerOrEqualPowerOfTwo(7) shouldBe 4 - Utils.largestSmallerOrEqualPowerOfTwo(5) shouldBe 4 - Utils.largestSmallerOrEqualPowerOfTwo(3) shouldBe 2 - Utils.largestSmallerOrEqualPowerOfTwo(2) shouldBe 2 - Utils.largestSmallerOrEqualPowerOfTwo(1) shouldBe 1 - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/entries/LedgerEntry.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/entries/LedgerEntry.scala deleted file mode 100644 index 9540c7c1a9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/entries/LedgerEntry.scala +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.entries - -import com.digitalasset.canton.platform.* -import com.digitalasset.canton.protocol.UpdateId -import com.digitalasset.daml.lf.data.Relation -import com.digitalasset.daml.lf.data.Time.Timestamp -import com.digitalasset.daml.lf.transaction.{CommittedTransaction, NodeId} - -private[platform] sealed abstract class LedgerEntry extends Product with Serializable - -private[platform] object LedgerEntry { - - final case class Transaction( - commandId: Option[CommandId], - updateId: UpdateId, - userId: Option[UserId], - submissionId: Option[SubmissionId], - actAs: List[Party], - workflowId: Option[WorkflowId], - ledgerEffectiveTime: Timestamp, - recordedAt: Timestamp, - transaction: CommittedTransaction, - explicitDisclosure: Relation[NodeId, Party], - ) extends LedgerEntry -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/RawStringInterningSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/RawStringInterningSpec.scala deleted file mode 100644 index f4def98b67..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/RawStringInterningSpec.scala +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interning - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class RawStringInterningSpec extends AnyFlatSpec with Matchers { - - behavior of "RawStringInterning.from" - - it should "start empty" in { - val current = RawStringInterning.from(Nil) - current.map shouldBe empty - current.idMap shouldBe empty - current.lastId shouldBe 0 - } - - it should "append empty entries to existing cache" in { - val previous = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) - val current = RawStringInterning.from(Nil, previous) - current.map shouldBe previous.map - current.idMap shouldBe previous.idMap - current.lastId shouldBe previous.lastId - } - - it should "append non-empty entries to empty cache" in { - val current = RawStringInterning.from(List(1 -> "one")) - current.map shouldBe Map("one" -> 1) - current.idMap shouldBe Map(1 -> "one") - current.lastId shouldBe 1 - } - - it should "append non-empty entries to non-empty cache" in { - val previous = RawStringInterning( - Map("one" -> 1, "two" -> 2), - Map(1 -> "one", 2 -> "two"), - 2, - ) - val current = RawStringInterning.from(List(3 -> "three"), previous) - current.map shouldBe Map("one" -> 1, "two" -> 2, "three" -> 3) - current.idMap shouldBe Map(1 -> "one", 2 -> "two", 3 -> "three") - current.lastId shouldBe 3 - } - - it should "complain about negative IDs" in { - val current = RawStringInterning.from(List(1 -> "one")) - an[IllegalArgumentException] shouldBe thrownBy( - RawStringInterning.from(List(-1 -> "minus one"), current) - ) - } - - behavior of "RawStringInterning.newEntries" - - it should "return an empty result if the input and previous state is empty" in { - val current = RawStringInterning.from(Nil) - val newEntries = RawStringInterning.newEntries(Vector.empty, current) - newEntries shouldBe empty - } - - it should "return an empty result if the input is empty" in { - val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) - val newEntries = RawStringInterning.newEntries(Vector.empty, current) - newEntries shouldBe empty - } - - it should "return an empty result if the input only contains duplicates" in { - val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) - val newEntries = RawStringInterning.newEntries(Vector("one"), current) - newEntries shouldBe empty - } - - it should "return a new entry if the input is an unknown string" in { - val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) - val newEntries = RawStringInterning.newEntries(Vector("two"), current) - newEntries shouldBe Vector(2 -> "two") - } - - it should "not return a new entry for known strings" in { - val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) - val newEntries = RawStringInterning.newEntries(Vector("one", "two"), current) - newEntries shouldBe Vector(2 -> "two") - } - - it should "not handle duplicate unknown strings" in { - val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) - val newEntries = - RawStringInterning.newEntries(Vector("two", "two", "two"), current) - newEntries shouldBe Vector(2 -> "two", 3 -> "two", 4 -> "two") - } - - it should "handle mixed input" in { - val current = RawStringInterning(Map("one" -> 1, "two" -> 2), Map(1 -> "one", 2 -> "two"), 2) - val newEntries = RawStringInterning.newEntries( - Vector("one", "three", "two", "four"), - current, - ) - newEntries shouldBe Vector(3 -> "three", 4 -> "four") - } - - it should "detect overflows" in { - val current = - RawStringInterning(Map("max" -> Int.MaxValue), Map(Int.MaxValue -> "max"), Int.MaxValue) - an[ArithmeticException] shouldBe thrownBy( - RawStringInterning.newEntries(Vector("overflow"), current) - ) - } - - behavior of "RawStringInterning.resetTo" - - it should "remove entries after the lastPersistedStringInterningId on `resetTo`" in { - val current = RawStringInterning(Map("one" -> 1, "two" -> 2), Map(1 -> "one", 2 -> "two"), 2) - val purgedStringInterning = - RawStringInterning.resetTo(lastPersistedStringInterningId = 1, current) - purgedStringInterning shouldBe RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) - } - - it should "not remove entries if lastPersistedStringInterningId is lteq lastId on `resetTo`" in { - val current = RawStringInterning(Map("one" -> 1, "two" -> 2), Map(1 -> "one", 2 -> "two"), 2) - val purgedStringInterning = - RawStringInterning.resetTo(lastPersistedStringInterningId = 2, current) - purgedStringInterning shouldBe current - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/StringInterningDomainSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/StringInterningDomainSpec.scala deleted file mode 100644 index d5eb5ec133..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/StringInterningDomainSpec.scala +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interning - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class StringInterningDomainSpec extends AnyFlatSpec with Matchers { - - behavior of "StringInterningDomain.prefixing" - - case class StringBox(value: String) - object StringBox { - def from(raw: String): StringBox = StringBox(raw) - def to(boxed: StringBox): String = boxed.value - } - - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) - class StaticStringInterningAccessor( - idToString: Map[Int, String], - stringToId: Map[String, Int], - ) extends StringInterningAccessor[String] { - override def internalize(t: String): Int = tryInternalize(t).get - override def tryInternalize(t: String): Option[Int] = stringToId.get(t) - override def externalize(id: Int): String = tryExternalize(id).get - override def tryExternalize(id: Int): Option[String] = idToString.get(id) - } - - object StaticStringInterningAccessor { - def apply(entries: Seq[(Int, String)]): StaticStringInterningAccessor = - new StaticStringInterningAccessor( - idToString = entries.toMap, - stringToId = entries.map(_.swap).toMap, - ) - } - - it should "handle a known string " in { - val accessor = StaticStringInterningAccessor(List(1 -> ".one", 2 -> ".two")) - val domain = StringInterningDomain.prefixing(".", accessor, StringBox.from, StringBox.to) - - domain.tryExternalize(2) shouldBe Some(StringBox("two")) - domain.externalize(2) shouldBe StringBox("two") - domain.tryInternalize(StringBox("two")) shouldBe Some(2) - domain.internalize(StringBox("two")) shouldBe 2 - - domain.unsafe.tryExternalize(2) shouldBe Some("two") - domain.unsafe.externalize(2) shouldBe "two" - domain.unsafe.tryInternalize("two") shouldBe Some(2) - domain.unsafe.internalize("two") shouldBe 2 - } - - it should "handle an unknown string" in { - val accessor = StaticStringInterningAccessor(List(1 -> ".one", 2 -> ".two")) - val domain = StringInterningDomain.prefixing(".", accessor, StringBox.from, StringBox.to) - - domain.tryExternalize(3) shouldBe empty - domain.tryInternalize(StringBox("three")) shouldBe empty - - domain.unsafe.tryExternalize(3) shouldBe empty - domain.unsafe.tryInternalize("three") shouldBe empty - } - - it should "work when two different domains share an accessor" in { - val accessor = StaticStringInterningAccessor(List(1 -> "aX", 2 -> "bX")) - val domainA = StringInterningDomain.prefixing("a", accessor, StringBox.from, StringBox.to) - val domainB = StringInterningDomain.prefixing("b", accessor, StringBox.from, StringBox.to) - - domainA.internalize(StringBox("X")) shouldBe 1 - domainB.internalize(StringBox("X")) shouldBe 2 - domainA.externalize(1) shouldBe StringBox("X") - domainB.externalize(2) shouldBe StringBox("X") - - domainA.unsafe.internalize("X") shouldBe 1 - domainB.unsafe.internalize("X") shouldBe 2 - domainA.unsafe.externalize(1) shouldBe "X" - domainB.unsafe.externalize(2) shouldBe "X" - } - - it should "work when two identical domains share an accessor" in { - val accessor = StaticStringInterningAccessor(List(1 -> ".one", 2 -> ".two")) - val domainA = StringInterningDomain.prefixing(".", accessor, StringBox.from, StringBox.to) - val domainB = StringInterningDomain.prefixing(".", accessor, StringBox.from, StringBox.to) - - domainA.internalize(StringBox("one")) shouldBe 1 - domainB.internalize(StringBox("one")) shouldBe 1 - domainA.externalize(2) shouldBe StringBox("two") - domainB.externalize(2) shouldBe StringBox("two") - - domainA.unsafe.internalize("one") shouldBe 1 - domainB.unsafe.internalize("one") shouldBe 1 - domainA.unsafe.externalize(2) shouldBe "two" - domainB.unsafe.externalize(2) shouldBe "two" - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/StringInterningViewSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/StringInterningViewSpec.scala deleted file mode 100644 index aa9aef93d5..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/interning/StringInterningViewSpec.scala +++ /dev/null @@ -1,522 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.interning - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.topology.SynchronizerId -import com.digitalasset.daml.lf.data.Ref -import org.scalatest.Assertion -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.concurrent.Future -import scala.util.Try -import scala.util.chaining.scalaUtilChainingOps - -class StringInterningViewSpec extends AsyncFlatSpec with Matchers with BaseTest { - - behavior of "StringInterningView" - - it should "provide working cache by extending" in { - val testee = new StringInterningView(loggerFactory) - partyAbsent(testee, "p1") - partyAbsent(testee, "p2") - partyAbsent(testee, "22::same:name") - templateAbsent(testee, "#22:t:a") - templateAbsent(testee, "#22:t:b") - synchronizerIdAbsent(testee, "x::synchronizer1") - synchronizerIdAbsent(testee, "x::synchronizer2") - synchronizerIdAbsent(testee, "22::same:name") - packageIdAbsent(testee, "pkg-1") - packageIdAbsent(testee, "pkg-2") - userIdAbsent(testee, "usr1") - userIdAbsent(testee, "usr2") - participantIdAbsent(testee, "pn-1") - participantIdAbsent(testee, "pn-2") - choiceNameAbsent(testee, "ChoiceName") - interfaceIdAbsent(testee, "pkg:inter:face") - - testee - .distinctNewRawStrings( - List( - new StringInterningProvider { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - Iterator("p1", "p2", "22::same:name") - .map(Ref.Party.assertFromString) - .foreach(builder.addParty) - Iterator("#22:t:a", "#22:t:b") - .map(Ref.NameTypeConRef.assertFromString) - .foreach(builder.addTemplateId) - Iterator("22::same:name", "x::synchronizer1", "x::synchronizer2") - .map(SynchronizerId.tryFromString) - .foreach(builder.addSynchronizerId) - Iterator("pkg-1", "pkg-2") - .map(Ref.PackageId.assertFromString) - .foreach(builder.addPackageId) - Iterator("usr1", "usr2") - .map(Ref.UserId.assertFromString) - .foreach(builder.addUserId) - Iterator("pn-1", "pn-2") - .map(Ref.ParticipantId.assertFromString) - .foreach(builder.addParticipantId) - Iterator("ChoiceName") - .map(Ref.ChoiceName.assertFromString) - .foreach(builder.addChoiceName) - Iterator("pkg:inter:face") - .map(Ref.Identifier.assertFromString) - .foreach(builder.addInterfaceId) - } - } - ) - ) - .pipe(testee.internize) - .toSeq - .sortBy(_._1) shouldBe Vector( - 1 -> "p|p1", - 2 -> "p|p2", - 3 -> "p|22::same:name", - 4 -> "t|#22:t:a", - 5 -> "t|#22:t:b", - 6 -> "d|22::same:name", - 7 -> "d|x::synchronizer1", - 8 -> "d|x::synchronizer2", - 9 -> "i|pkg-1", - 10 -> "i|pkg-2", - 11 -> "u|usr1", - 12 -> "u|usr2", - 13 -> "n|pn-1", - 14 -> "n|pn-2", - 15 -> "c|ChoiceName", - 16 -> "f|pkg:inter:face", - ) - partyPresent(testee, "p1", 1) - partyPresent(testee, "p2", 2) - partyPresent(testee, "22::same:name", 3) - partyAbsent(testee, "unknown") - templatePresent(testee, "#22:t:a", 4) - templatePresent(testee, "#22:t:b", 5) - templateAbsent(testee, "#22:unkno:wn") - synchronizerIdPresent(testee, "22::same:name", 6) - synchronizerIdPresent(testee, "x::synchronizer1", 7) - synchronizerIdPresent(testee, "x::synchronizer2", 8) - synchronizerIdAbsent(testee, "x::synchronizerunknown") - packageIdPresent(testee, "pkg-1", 9) - packageIdPresent(testee, "pkg-2", 10) - packageIdAbsent(testee, "pkg-unknown") - userIdPresent(testee, "usr1", 11) - userIdPresent(testee, "usr2", 12) - userIdAbsent(testee, "usr-unknown") - participantIdPresent(testee, "pn-1", 13) - participantIdPresent(testee, "pn-2", 14) - participantIdAbsent(testee, "pn-unknown") - choiceNamePresent(testee, "ChoiceName", 15) - choiceNameAbsent(testee, "CnUnknown") - interfaceIdPresent(testee, "pkg:inter:face", 16) - interfaceIdAbsent(testee, "inter:face:unknown") - } - - it should "extend working view correctly" in { - val testee = new StringInterningView(loggerFactory) - partyAbsent(testee, "p1") - partyAbsent(testee, "p2") - partyAbsent(testee, "22::same:name") - templateAbsent(testee, "#22:t:a") - templateAbsent(testee, "#22:t:b") - synchronizerIdAbsent(testee, "22::same:name") - synchronizerIdAbsent(testee, "x::synchronizer1") - synchronizerIdAbsent(testee, "x::synchronizer2") - packageIdAbsent(testee, "pkg-1") - packageIdAbsent(testee, "pkg-2") - userIdAbsent(testee, "usr1") - userIdAbsent(testee, "usr2") - choiceNameAbsent(testee, "ChoiceName") - interfaceIdAbsent(testee, "pkg:inter:face") - testee - .distinctNewRawStrings( - List( - new StringInterningProvider { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - Iterator("p1", "p2", "22::same:name") - .map(Ref.Party.assertFromString) - .foreach(builder.addParty) - Iterator("#22:t:a") - .map(Ref.NameTypeConRef.assertFromString) - .foreach(builder.addTemplateId) - Iterator("x::synchronizer1", "x::synchronizer2") - .map(SynchronizerId.tryFromString) - .foreach(builder.addSynchronizerId) - Iterator("pkg-1") - .map(Ref.PackageId.assertFromString) - .foreach(builder.addPackageId) - Iterator("usr1") - .map(Ref.UserId.assertFromString) - .foreach(builder.addUserId) - Iterator("pn-1", "pn-2") - .map(Ref.ParticipantId.assertFromString) - .foreach(builder.addParticipantId) - Iterator("ChoiceName") - .map(Ref.ChoiceName.assertFromString) - .foreach(builder.addChoiceName) - Iterator("pkg:inter:face") - .map(Ref.Identifier.assertFromString) - .foreach(builder.addInterfaceId) - } - } - ) - ) - .pipe(testee.internize) shouldBe Vector( - 1 -> "p|p1", - 2 -> "p|p2", - 3 -> "p|22::same:name", - 4 -> "t|#22:t:a", - 5 -> "d|x::synchronizer1", - 6 -> "d|x::synchronizer2", - 7 -> "i|pkg-1", - 8 -> "u|usr1", - 9 -> "n|pn-1", - 10 -> "n|pn-2", - 11 -> "c|ChoiceName", - 12 -> "f|pkg:inter:face", - ) - partyPresent(testee, "p1", 1) - partyPresent(testee, "p2", 2) - partyPresent(testee, "22::same:name", 3) - partyAbsent(testee, "unknown") - templatePresent(testee, "#22:t:a", 4) - templateAbsent(testee, "#22:t:b") - templateAbsent(testee, "#22:unkno:wn") - synchronizerIdAbsent(testee, "22::same:name") - synchronizerIdPresent(testee, "x::synchronizer1", 5) - synchronizerIdPresent(testee, "x::synchronizer2", 6) - synchronizerIdAbsent(testee, "x::synchronizerunknown") - packageIdPresent(testee, "pkg-1", 7) - packageIdAbsent(testee, "pkg-2") - packageIdAbsent(testee, "pkg-unknown") - userIdPresent(testee, "usr1", 8) - userIdAbsent(testee, "usr2") - choiceNamePresent(testee, "ChoiceName", 11) - choiceNameAbsent(testee, "CnUnknown") - interfaceIdPresent(testee, "pkg:inter:face", 12) - interfaceIdAbsent(testee, "inter:face:unknown") - testee - .distinctNewRawStrings( - List( - new StringInterningProvider { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - Iterator("p1", "p2") - .map(Ref.Party.assertFromString) - .foreach(builder.addParty) - Iterator("#22:t:a", "#22:t:b") - .map(Ref.NameTypeConRef.assertFromString) - .foreach(builder.addTemplateId) - Iterator("22::same:name", "x::synchronizer1", "x::synchronizer3") - .map(SynchronizerId.tryFromString) - .foreach(builder.addSynchronizerId) - Iterator("pkg-1", "pkg-2") - .map(Ref.PackageId.assertFromString) - .foreach(builder.addPackageId) - Iterator("usr1", "usr2") - .map(Ref.UserId.assertFromString) - .foreach(builder.addUserId) - Iterator("pn-1", "pn-2", "pn-3") - .map(Ref.ParticipantId.assertFromString) - .foreach(builder.addParticipantId) - Iterator("ChoiceName") - .map(Ref.ChoiceName.assertFromString) - .foreach(builder.addChoiceName) - Iterator("pkg:inter:face", "pkg:inter:face2") - .map(Ref.Identifier.assertFromString) - .foreach(builder.addInterfaceId) - } - } - ) - ) - .pipe(testee.internize) shouldBe Vector( - 13 -> "t|#22:t:b", - 14 -> "d|22::same:name", - 15 -> "d|x::synchronizer3", - 16 -> "i|pkg-2", - 17 -> "u|usr2", - 18 -> "n|pn-3", - 19 -> "f|pkg:inter:face2", - ) - partyPresent(testee, "p1", 1) - partyPresent(testee, "p2", 2) - partyPresent(testee, "22::same:name", 3) - partyAbsent(testee, "unknown") - templatePresent(testee, "#22:t:a", 4) - templatePresent(testee, "#22:t:b", 13) - templateAbsent(testee, "#22:unkno:wn") - synchronizerIdPresent(testee, "x::synchronizer1", 5) - synchronizerIdPresent(testee, "x::synchronizer2", 6) - synchronizerIdPresent(testee, "22::same:name", 14) - synchronizerIdPresent(testee, "x::synchronizer3", 15) - synchronizerIdAbsent(testee, "x::synchronizerunknown") - packageIdPresent(testee, "pkg-1", 7) - packageIdPresent(testee, "pkg-2", 16) - packageIdAbsent(testee, "pkg-unknown") - userIdPresent(testee, "usr1", 8) - userIdPresent(testee, "usr2", 17) - userIdAbsent(testee, "usr-unknown") - participantIdPresent(testee, "pn-1", 9) - participantIdPresent(testee, "pn-2", 10) - participantIdPresent(testee, "pn-3", 18) - participantIdAbsent(testee, "pn-unknown") - choiceNamePresent(testee, "ChoiceName", 11) - interfaceIdPresent(testee, "pkg:inter:face2", 19) - choiceNameAbsent(testee, "CnUnknown") - interfaceIdPresent(testee, "pkg:inter:face", 12) - interfaceIdAbsent(testee, "inter:face:unknown") - } - - it should "correctly load prefixing entries in the view on `update`" in { - val testee = new StringInterningView(loggerFactory) - partyAbsent(testee, "p1") - partyAbsent(testee, "p2") - partyAbsent(testee, "22::same:name") - synchronizerIdAbsent(testee, "x::synchronizer1") - synchronizerIdAbsent(testee, "x::synchronizer2") - synchronizerIdAbsent(testee, "22::same:name") - testee - .update(Some(6)) { (from, to) => - from shouldBe 0 - to shouldBe 6 - Future.successful( - Vector( - 1 -> "p|p1", - 2 -> "p|p2", - 3 -> "p|22::same:name", - 4 -> "d|x::synchronizer1", - 5 -> "d|x::synchronizer2", - 6 -> "d|22::same:name", - 7 -> "i|pkg-1", - 8 -> "i|pkg-2", - ) - ) - } - .map { _ => - partyPresent(testee, "p1", 1) - partyPresent(testee, "p2", 2) - partyPresent(testee, "22::same:name", 3) - partyAbsent(testee, "unknown") - synchronizerIdPresent(testee, "x::synchronizer1", 4) - synchronizerIdPresent(testee, "x::synchronizer2", 5) - synchronizerIdPresent(testee, "22::same:name", 6) - templateAbsent(testee, "#22:unk:nown") - packageIdPresent(testee, "pkg-1", 7) - packageIdPresent(testee, "pkg-2", 8) - packageIdAbsent(testee, "pkg-unknown") - } - } - - it should "be able to update working view correctly" in { - val testee = new StringInterningView(loggerFactory) - partyAbsent(testee, "p1") - partyAbsent(testee, "p2") - partyAbsent(testee, "22:same:name") - templateAbsent(testee, "#22:t:a") - templateAbsent(testee, "#22:t:b") - templateAbsent(testee, "#22:same:name") - packageIdAbsent(testee, "pkg-1") - testee - .distinctNewRawStrings( - List( - new StringInterningProvider { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - Iterator("p1", "p2") - .map(Ref.Party.assertFromString) - .foreach(builder.addParty) - Iterator("x::synchronizer1") - .map(SynchronizerId.tryFromString) - .foreach(builder.addSynchronizerId) - Iterator("pkg-1") - .map(Ref.PackageId.assertFromString) - .foreach(builder.addPackageId) - } - } - ) - ) - .pipe(testee.internize) - partyPresent(testee, "p1", 1) - partyPresent(testee, "p2", 2) - partyAbsent(testee, "22:same:name") - templateAbsent(testee, "#22:t:a") - templateAbsent(testee, "#22:t:b") - templateAbsent(testee, "#22:same:name") - packageIdPresent(testee, "pkg-1", 4) - packageIdAbsent(testee, "pkg-2") - testee - .update(Some(11)) { (from, to) => - from shouldBe 4 - to shouldBe 11 - Future.successful( - Vector( - 6 -> "p|22:same:name", - 7 -> "t|#22:t:a", - 8 -> "t|#22:t:b", - 9 -> "t|#22:same:name", - 10 -> "i|pkg-2", - ) - ) - } - .map { _ => - partyPresent(testee, "p1", 1) - partyPresent(testee, "p2", 2) - packageIdPresent(testee, "pkg-1", 4) - partyPresent(testee, "22:same:name", 6) - partyAbsent(testee, "unknown") - templatePresent(testee, "#22:t:a", 7) - templatePresent(testee, "#22:t:b", 8) - templatePresent(testee, "#22:same:name", 9) - packageIdPresent(testee, "pkg-2", 10) - packageIdAbsent(testee, "pkg-unknown") - templateAbsent(testee, "#22:unk:nown") - } - } - - it should "remove entries if lastStringInterningId is greater than lastId" in { - val testee = new StringInterningView(loggerFactory) - testee - .distinctNewRawStrings( - List( - new StringInterningProvider { - override def provideInternedStrings(builder: StringInterningBuilder): Unit = { - Iterator("p1", "p2", "22::same:name") - .map(Ref.Party.assertFromString) - .foreach(builder.addParty) - Iterator("#22:t:a", "#22:t:b") - .map(Ref.NameTypeConRef.assertFromString) - .foreach(builder.addTemplateId) - Iterator("22::same:name", "x::synchronizer1", "x::synchronizer2") - .map(SynchronizerId.tryFromString) - .foreach(builder.addSynchronizerId) - Iterator("pkg-1") - .map(Ref.PackageId.assertFromString) - .foreach(builder.addPackageId) - } - } - ) - ) - .pipe(testee.internize) shouldBe Vector( - 1 -> "p|p1", - 2 -> "p|p2", - 3 -> "p|22::same:name", - 4 -> "t|#22:t:a", - 5 -> "t|#22:t:b", - 6 -> "d|22::same:name", - 7 -> "d|x::synchronizer1", - 8 -> "d|x::synchronizer2", - 9 -> "i|pkg-1", - ) - partyPresent(testee, "p1", 1) - partyPresent(testee, "p2", 2) - partyPresent(testee, "22::same:name", 3) - partyAbsent(testee, "unknown") - templatePresent(testee, "#22:t:a", 4) - templatePresent(testee, "#22:t:b", 5) - templateAbsent(testee, "#22:unkno:wn") - synchronizerIdPresent(testee, "22::same:name", 6) - synchronizerIdPresent(testee, "x::synchronizer1", 7) - synchronizerIdPresent(testee, "x::synchronizer2", 8) - packageIdPresent(testee, "pkg-1", 9) - packageIdAbsent(testee, "pkg-unknown") - - testee - .update(Some(4))((_, _) => - fail("should not be called if lastStringInterningId is greater than lastId") - ) - .map { _ => - partyPresent(testee, "p1", 1) - partyPresent(testee, "p2", 2) - partyPresent(testee, "22::same:name", 3) - partyAbsent(testee, "unknown") - templatePresent(testee, "#22:t:a", 4) - templateAbsent(testee, "#22:t:b") - templateAbsent(testee, "#22:unkno:wn") - synchronizerIdAbsent(testee, "22::same:name") - synchronizerIdAbsent(testee, "x::synchronizer1") - synchronizerIdAbsent(testee, "x::synchronizer2") - packageIdAbsent(testee, "pkg-1") - packageIdAbsent(testee, "pkg-2") - packageIdAbsent(testee, "pkg-unknown") - } - } - - private def partyPresent(view: StringInterning, party: String, id: Int) = - interningEntryPresent(view.party, party, id, Ref.Party.assertFromString) - - private def partyAbsent(view: StringInterning, party: String) = - interningEntryAbsent(view.party, party, Ref.Party.assertFromString) - - private def templatePresent(view: StringInterning, template: String, id: Int) = - interningEntryPresent(view.templateId, template, id, Ref.NameTypeConRef.assertFromString) - - private def templateAbsent(view: StringInterning, template: String) = - interningEntryAbsent(view.templateId, template, Ref.NameTypeConRef.assertFromString) - - private def synchronizerIdPresent(view: StringInterning, synchronizerId: String, id: Int) = - interningEntryPresent(view.synchronizerId, synchronizerId, id, SynchronizerId.tryFromString) - - private def synchronizerIdAbsent(view: StringInterning, synchronizerId: String) = - interningEntryAbsent(view.synchronizerId, synchronizerId, SynchronizerId.tryFromString) - - private def packageIdPresent(view: StringInterning, packageId: String, id: Int) = - interningEntryPresent(view.packageId, packageId, id, Ref.PackageId.assertFromString) - - private def packageIdAbsent(view: StringInterning, packageId: String) = - interningEntryAbsent(view.packageId, packageId, Ref.PackageId.assertFromString) - - private def userIdPresent(view: StringInterning, userId: String, id: Int) = - interningEntryPresent(view.userId, userId, id, Ref.UserId.assertFromString) - - private def userIdAbsent(view: StringInterning, userId: String) = - interningEntryAbsent(view.userId, userId, Ref.UserId.assertFromString) - - private def participantIdPresent(view: StringInterning, participantId: String, id: Int) = - interningEntryPresent(view.participantId, participantId, id, Ref.ParticipantId.assertFromString) - - private def participantIdAbsent(view: StringInterning, participantId: String) = - interningEntryAbsent(view.participantId, participantId, Ref.ParticipantId.assertFromString) - - private def choiceNamePresent(view: StringInterning, choiceName: String, id: Int) = - interningEntryPresent(view.choiceName, choiceName, id, Ref.ChoiceName.assertFromString) - - private def choiceNameAbsent(view: StringInterning, choiceName: String) = - interningEntryAbsent(view.choiceName, choiceName, Ref.ChoiceName.assertFromString) - - private def interfaceIdPresent(view: StringInterning, interfaceId: String, id: Int) = - interningEntryPresent(view.interfaceId, interfaceId, id, Ref.Identifier.assertFromString) - - private def interfaceIdAbsent(view: StringInterning, interfaceId: String) = - interningEntryAbsent(view.interfaceId, interfaceId, Ref.Identifier.assertFromString) - - private def interningEntryPresent[T]( - interningDomain: StringInterningDomain[T], - stringValue: String, - id: Int, - toTypedValue: String => T, - ): Assertion = { - val typedValue = toTypedValue(stringValue) - interningDomain.internalize(typedValue) shouldBe id - interningDomain.tryInternalize(typedValue) shouldBe Some(id) - interningDomain.externalize(id) shouldBe typedValue - interningDomain.tryExternalize(id) shouldBe Some(typedValue) - interningDomain.unsafe.internalize(stringValue) shouldBe id - interningDomain.unsafe.tryInternalize(stringValue) shouldBe Some(id) - interningDomain.unsafe.externalize(id) shouldBe stringValue - interningDomain.unsafe.tryExternalize(id) shouldBe Some(stringValue) - } - - private def interningEntryAbsent[T]( - interningDomain: StringInterningDomain[T], - stringValue: String, - toTypedValue: String => T, - ): Assertion = { - val typedValue = toTypedValue(stringValue) - Try(interningDomain.internalize(typedValue)).isFailure shouldBe true - interningDomain.tryInternalize(typedValue) shouldBe None - Try(interningDomain.unsafe.internalize(stringValue)).isFailure shouldBe true - interningDomain.unsafe.tryInternalize(stringValue) shouldBe None - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/DbConnectionAndDataSourceAroundEach.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/DbConnectionAndDataSourceAroundEach.scala deleted file mode 100644 index 05a778b150..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/DbConnectionAndDataSourceAroundEach.scala +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.migration - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.platform.store.DbType -import com.digitalasset.canton.platform.store.backend.{ - DataSourceStorageBackend, - StorageBackendFactory, -} -import org.scalatest.{BeforeAndAfterEach, Suite} - -import java.sql.Connection -import javax.sql.DataSource -import scala.annotation.tailrec -import scala.util.{Failure, Success, Try} - -trait DbConnectionAroundEachBase { - implicit def connection: Connection - implicit def dbType: DbType - implicit def dataSource: DataSource - protected def jdbcUrl: String -} - -trait DbConnectionAndDataSourceAroundEach - extends BeforeAndAfterEach - with DbConnectionAroundEachBase - with BaseTest { - self: Suite => - - implicit var connection: Connection = _ - - private val dataSourceBackend = - StorageBackendFactory.of(dbType, loggerFactory).createDataSourceStorageBackend - implicit var dataSource: DataSource = _ - - override protected def beforeEach(): Unit = { - super.beforeEach() - dataSource = dataSourceBackend.createDataSource( - dataSourceConfig = DataSourceStorageBackend.DataSourceConfig(jdbcUrl), - loggerFactory = loggerFactory, - ) - connection = retry(20, 1000) { - val c = dataSource.getConnection - dataSourceBackend.checkDatabaseAvailable(c) - c - } - } - - override protected def afterEach(): Unit = { - connection.close() - super.afterEach() - } - - @tailrec - private def retry[T](max: Int, sleep: Long)(t: => T): T = - Try(t) match { - case Success(value) => value - case Failure(_) if max > 0 => - Threading.sleep(sleep) - retry(max - 1, sleep)(t) - case Failure(exception) => throw exception - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/DbDataTypes.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/DbDataTypes.scala deleted file mode 100644 index cf459d2c20..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/DbDataTypes.scala +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.migration - -import com.digitalasset.canton.platform.store.DbType -import com.digitalasset.canton.platform.store.migration.MigrationTestSupport.DbDataType - -import java.sql.ResultSet - -class DbDataTypes(dbType: DbType) { - - case object Integer extends DbDataType { - override def get(resultSet: ResultSet, index: Int): Any = resultSet.getInt(index) - override def put(value: Any): String = value.asInstanceOf[Int].toString - } - - case object BigInt extends DbDataType { - override def get(resultSet: ResultSet, index: Int): Any = resultSet.getLong(index) - override def put(value: Any): String = value.asInstanceOf[Long].toString - } - - case object Str extends DbDataType { - override def get(resultSet: ResultSet, index: Int): Any = resultSet.getString(index) - override def put(value: Any): String = s"'${value.asInstanceOf[String]}'" - } - - case object Bool extends DbDataType { - override def get(resultSet: ResultSet, index: Int): Any = resultSet.getBoolean(index) - override def put(value: Any): String = value.asInstanceOf[Boolean].toString - } - - case object Bytea extends DbDataType { - override def get(resultSet: ResultSet, index: Int): Any = resultSet.getBytes(index).toVector - override def put(value: Any): String = { - val hexes = value - .asInstanceOf[Vector[Byte]] - .map(_.toInt.toHexString) - .map { - case hexByte if hexByte.length == 1 => s"0$hexByte" - case hexByte => hexByte - } - dbType match { - case DbType.Postgres => hexes.mkString("E'\\\\x", "", "'") - case other => sys.error(s"Unsupported db type: $other") - } - } - } - - case object StringArray extends DbDataType { - override def get(resultSet: ResultSet, index: Int): Any = - resultSet - .getArray(index) - .getArray - .asInstanceOf[Array[String]] - .toVector - - override def put(value: Any): String = { - val array = value.asInstanceOf[Vector[String]] - dbType match { - case DbType.Postgres => array.map(x => s"'$x'").mkString("ARRAY[", ", ", "]::TEXT[]") - case other => sys.error(s"Unsupported db type: $other") - } - } - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/MigrationTestSupport.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/MigrationTestSupport.scala deleted file mode 100644 index e394840b5a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/MigrationTestSupport.scala +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.migration - -import com.digitalasset.canton.platform.store.DbType -import com.digitalasset.canton.platform.store.FlywayMigrations.locations -import org.flywaydb.core.Flyway - -import java.sql.{Connection, ResultSet} -import javax.sql.DataSource -import scala.util.Using -import scala.util.control.NonFatal - -object MigrationTestSupport { - def migrateTo(version: String)(implicit dataSource: DataSource, dbType: DbType): Unit = { - Flyway - .configure() - .locations(locations(dbType)*) - .dataSource(dataSource) - .target(version) - .load() - .migrate() - () - } - - trait DbDataType { - def get(resultSet: ResultSet, index: Int): Any - def put(value: Any): String - def optional: DbDataType = DbDataType.Optional(this) - } - - object DbDataType { - final case class Optional(delegate: DbDataType) extends DbDataType { - override def get(resultSet: ResultSet, index: Int): Any = - if (resultSet.getObject(index) == null) None - else Some(delegate.get(resultSet, index)) - - override def put(value: Any): String = value.asInstanceOf[Option[Any]] match { - case Some(someValue) => delegate.put(someValue) - case None => "null" - } - } - } - - final case class TableSchema( - tableName: String, - orderByColumn: String, - columns: Map[String, DbDataType], - ) { - val columnsList: List[String] = columns.keySet.toList - - def ++(entries: (String, DbDataType)*): TableSchema = - copy(columns = columns ++ entries) - - def --(cols: String*): TableSchema = - copy(columns = columns -- cols) - } - - object TableSchema { - def apply(tableName: String, orderByColumn: String)( - entries: (String, DbDataType)* - ): TableSchema = - TableSchema(tableName, orderByColumn, entries.toMap) - } - - type Row = Map[String, Any] - - def row(entries: (String, Any)*): Row = entries.toMap[String, Any] - - implicit class RowOps(val r: Row) extends AnyVal { - def updateIn[T](key: String)(f: T => Any): Row = r + (key -> f(r(key).asInstanceOf[T])) - } - - implicit class VectorRowOps(val r: Vector[Row]) extends AnyVal { - def updateInAll[T](key: String)(f: T => Any): Vector[Row] = r.map(_.updateIn(key)(f)) - } - - def insertMany( - inputs: (TableSchema, Seq[Row])* - )(implicit connection: Connection): Unit = - inputs.foreach { case (tableSchema, rows) => - insert(tableSchema, rows*) - } - - def insert(tableSchema: TableSchema, rows: Row*)(implicit connection: Connection): Unit = - rows.foreach { row => - assert( - tableSchema.columns.keySet == row.keySet, { - val onlyInTable = tableSchema.columns.keySet.removedAll(row.keySet) - val onlyInRow = row.keySet.removedAll(tableSchema.columns.keySet) - s"table name: ${tableSchema.tableName} - columns only in the table's schema $onlyInTable; columns only in the row's schema: $onlyInRow" - }, - ) - val values = - tableSchema.columnsList.map(column => - try - tableSchema.columns(column).put(row(column)) - catch { - case NonFatal(e) => - throw new RuntimeException(s"Could not convert value for column: '$column'", e) - } - ) - val insertStatement = - s"""INSERT INTO ${tableSchema.tableName} - |(${tableSchema.columnsList.mkString(", ")}) - |VALUES (${values.mkString(", ")})""".stripMargin - try - Using.resource(connection.createStatement())(_.execute(insertStatement)) - catch { - case NonFatal(e) => - throw new RuntimeException(s"Error while executing query: $insertStatement", e) - } - () - } - - @SuppressWarnings(Array("org.wartremover.warts.While")) - def fetchTable(tableSchema: TableSchema)(implicit connection: Connection): Vector[Row] = { - val query = - s"""SELECT ${tableSchema.columnsList.mkString(", ")} - |FROM ${tableSchema.tableName} - |ORDER BY ${tableSchema.orderByColumn} - |""".stripMargin - Using.resource(connection.createStatement())(statement => - Using.resource(statement.executeQuery(query)) { resultSet => - val resultBuilder = Vector.newBuilder[Map[String, Any]] - while (resultSet.next()) { - val row = tableSchema.columnsList.zipWithIndex.map { case (column, i) => - column -> tableSchema.columns(column).get(resultSet, i + 1) - }.toMap - resultBuilder.addOne(row) - } - resultBuilder.result() - } - ) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/postgres/PostgresAroundEachForMigrations.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/postgres/PostgresAroundEachForMigrations.scala deleted file mode 100644 index f24c9a1e80..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/postgres/PostgresAroundEachForMigrations.scala +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.migration.postgres - -import com.digitalasset.canton.platform.store.DbType -import com.digitalasset.canton.platform.store.migration.DbConnectionAndDataSourceAroundEach -import com.digitalasset.canton.platform.store.testing.postgresql.PostgresAroundEach -import org.scalatest.Suite - -/** Creates a fresh data source and connection for each test case - */ -trait PostgresAroundEachForMigrations - extends DbConnectionAndDataSourceAroundEach - with PostgresAroundEach { - self: Suite => - override implicit def dbType: DbType = DbType.Postgres -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/postgres/RemovalOfJavaMigrationsPostgres.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/postgres/RemovalOfJavaMigrationsPostgres.scala deleted file mode 100644 index c073711b5a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/migration/postgres/RemovalOfJavaMigrationsPostgres.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.migration.postgres - -import com.daml.testing.utils.TestResourceContext -import com.digitalasset.canton.TestEssentials -import com.digitalasset.canton.platform.store.FlywayMigrations -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.matchers.should.Matchers - -class RemovalOfJavaMigrationsPostgres - extends AsyncFlatSpec - with Matchers - with TestResourceContext - with PostgresAroundEachForMigrations - with TestEssentials { - - behavior of "Flyway migrations after the removal of Java migrations" - - it should "migrate an empty database to the latest schema" in { - val migration = - new FlywayMigrations(postgresDatabase.url, loggerFactory = loggerFactory) - for { - _ <- migration.migrate() - } yield { - succeed - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAround.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAround.scala deleted file mode 100644 index e1b4adb04a..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAround.scala +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.testing.postgresql - -import org.postgresql.ds.PGSimpleDataSource -import org.slf4j.LoggerFactory -import org.testcontainers.postgresql.PostgreSQLContainer - -import java.sql.Statement -import java.util.UUID -import java.util.concurrent.atomic.AtomicReference -import scala.util.Using - -trait PostgresAround { - import PostgresAround.* - - private val server: AtomicReference[PostgresServer] = new AtomicReference - private val ownedServerContainer: AtomicReference[Option[PostgreSQLContainer]] = - new AtomicReference(None) - - protected def connectToPostgresqlServer(): Unit = { - val isCI = sys.env.contains("CI") - val isMachine = sys.env.contains("MACHINE") - val forceTestContainer = sys.env.contains("DB_FORCE_TEST_CONTAINER") - - if (!forceTestContainer && (isCI && !isMachine)) { - // using specified resource - val hostName = sys.env.getOrElse("POSTGRES_HOST", "localhost") - val port = 5432 - server.set( - PostgresServer( - hostName = hostName, - port = port, - userName = env("POSTGRES_USER"), - password = env("POSTGRES_PASSWORD"), - baseDatabase = env("POSTGRES_DB"), - ) - ) - logger.info(s"Using PostgreSQL on $hostName:$port.") - } else { - // using own temporal resource - val container = new PostgreSQLContainer(s"${PostgreSQLContainer.IMAGE}:17") - ownedServerContainer.set(Some(container)) - logger.info(s"Starting PostgreSQL Container...") - container.start() - logger.info(s"PostgreSQL Container started.") - val hostName = container.getHost - val port = container.getFirstMappedPort - server.set( - PostgresServer( - hostName = hostName, - port = port, - userName = container.getUsername, - password = container.getPassword, - baseDatabase = container.getDatabaseName, - ) - ) - logger.info(s"Using PostgreSQL Container on $hostName:$port.") - } - - } - - protected def disconnectFromPostgresqlServer(): Unit = - ownedServerContainer.get().foreach { container => - logger.info(s"Stopping PostgreSQL Container...") - container.close() - logger.info(s"PostgreSQL Container stopped.") - } - - protected def createNewRandomDatabase(): PostgresDatabase = { - val database = executeAdminStatement(server.get()) { statement => - val databaseName = UUID.randomUUID().toString - statement.execute(s"CREATE DATABASE \"$databaseName\"") - statement.execute(s"CREATE USER \"$databaseName-user\" WITH PASSWORD 'user'") - statement.execute( - s"GRANT ALL PRIVILEGES ON DATABASE \"$databaseName\" TO \"$databaseName-user\"" - ) - PostgresDatabase(server.get(), databaseName, s"$databaseName-user", "user") - } - executeAdminStatement(server.get().copy(baseDatabase = database.databaseName))( - _.execute(s"GRANT ALL ON SCHEMA public TO \"${database.userName}\"") - ) - database - } - - protected def dropDatabase(database: PostgresDatabase): Unit = - executeAdminStatement(server.get()) { statement => - statement.execute(s"DROP DATABASE \"${database.databaseName}\"") - statement.execute(s"DROP USER \"${database.databaseName}-user\"") - } - -} - -object PostgresAround { - private val logger = LoggerFactory.getLogger(getClass) - - private def executeAdminStatement[T]( - connectedPostgresServer: PostgresServer - )(body: Statement => T): T = { - val baseDatabase = - PostgresDatabase( - connectedPostgresServer, - connectedPostgresServer.baseDatabase, - connectedPostgresServer.userName, - connectedPostgresServer.password, - ) - Using.resource { - val dataSource = new PGSimpleDataSource() - dataSource.setUrl(baseDatabase.url) - dataSource.getConnection - } { connection => - Using.resource(connection.createStatement())(body) - } - } - - private def env(name: String): String = - sys.env.getOrElse(name, sys.error(s"Environment variable not set [$name]")) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundAll.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundAll.scala deleted file mode 100644 index 42a6b56e6b..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundAll.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.testing.postgresql - -import org.scalatest.{BeforeAndAfterAll, Suite} - -trait PostgresAroundAll extends PostgresAroundSuite with BeforeAndAfterAll { - self: Suite => - - override protected def beforeAll(): Unit = { - // We start PostgreSQL before calling `super` because _generally_ the database needs to be up - // before everything else. - connectToPostgresqlServer() - createNewDatabase() - super.beforeAll() - } - - override protected def afterAll(): Unit = { - super.afterAll() - disconnectFromPostgresqlServer() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundEach.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundEach.scala deleted file mode 100644 index 3f5ee956ec..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundEach.scala +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.testing.postgresql - -import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite} - -trait PostgresAroundEach - extends PostgresAroundSuite - with BeforeAndAfterAll - with BeforeAndAfterEach { - self: Suite => - - override protected def beforeAll(): Unit = { - // We start PostgreSQL before calling `super` because _generally_ the database needs to be up - // before everything else. - connectToPostgresqlServer() - super.beforeAll() - } - - override protected def afterAll(): Unit = { - super.afterAll() - disconnectFromPostgresqlServer() - } - - override protected def beforeEach(): Unit = { - // We create the database before calling `super` for the same reasons as above. - createNewDatabase() - super.beforeEach() - } - - override protected def afterEach(): Unit = { - super.afterEach() - dropDatabase() - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundSuite.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundSuite.scala deleted file mode 100644 index 65d00d4b2f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresAroundSuite.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.testing.postgresql - -import org.scalatest.Suite - -trait PostgresAroundSuite extends PostgresAround { - self: Suite => - - @volatile - private var database: Option[PostgresDatabase] = None - - protected def jdbcUrl: String = postgresDatabase.url - - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) - protected def postgresDatabase: PostgresDatabase = database.get - - protected def lockIdSeed: Int = - 1000 // For postgres each test-suite uses different DB, so no unique lock-ids needed - - protected def createNewDatabase(): PostgresDatabase = { - database = Some(createNewRandomDatabase()) - postgresDatabase - } - - protected def dropDatabase(): Unit = { - dropDatabase(postgresDatabase) - database = None - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresDatabase.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresDatabase.scala deleted file mode 100644 index 4755feff33..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresDatabase.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.testing.postgresql - -final case class PostgresDatabase private[postgresql] ( - private val server: PostgresServer, - databaseName: String, - userName: String, - password: String, -) { - def hostName: String = server.hostName - - def port: Int = server.port - - def urlWithoutCredentials: String = - s"jdbc:postgresql://$hostName:$port/$databaseName" - - def url: String = - s"$urlWithoutCredentials?user=$userName&password=$password" - - override def toString: String = url -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresResource.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresResource.scala deleted file mode 100644 index f70df3eeed..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresResource.scala +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.testing.postgresql - -import com.daml.resources.{AbstractResourceOwner, HasExecutionContext, ReleasableResource, Resource} - -import scala.concurrent.Future - -object PostgresResource { - def owner[Context: HasExecutionContext](): AbstractResourceOwner[Context, PostgresDatabase] = - new AbstractResourceOwner[Context, PostgresDatabase] with PostgresAround { - override def acquire()(implicit context: Context): Resource[Context, PostgresDatabase] = - ReleasableResource(Future { - connectToPostgresqlServer() - createNewRandomDatabase() - })(_ => Future(disconnectFromPostgresqlServer())) - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresServer.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresServer.scala deleted file mode 100644 index 24ee3411e3..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/testing/postgresql/PostgresServer.scala +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.testing.postgresql - -final case class PostgresServer( - hostName: String, - port: Int, - userName: String, - password: String, - baseDatabase: String, -) diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/utils/ConcurrencyLimiterSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/utils/ConcurrencyLimiterSpec.scala deleted file mode 100644 index e32d3eff17..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/utils/ConcurrencyLimiterSpec.scala +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.platform.store.utils - -import com.digitalasset.canton.concurrent.Threading -import org.scalatest.flatspec.AsyncFlatSpec -import org.scalatest.{Assertion, Assertions} - -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger -import scala.concurrent.{ExecutionContext, Future} - -final class ConcurrencyLimiterSpec extends AsyncFlatSpec { - behavior of "QueueBasedConcurrencyLimiter" - - it should "work with parallelism of 1" in { - ConcurrencyLimiterSpec.runTest( - createLimiter = ec => new QueueBasedConcurrencyLimiter(1, ec), - waitTimeMillis = 1, - threads = 32, - items = 100, - parallelism = 1, - expectedParallelism = Some(1), - ) - } - - it should "work with parallelism of 4" in { - ConcurrencyLimiterSpec.runTest( - createLimiter = ec => new QueueBasedConcurrencyLimiter(4, ec), - waitTimeMillis = 1, - threads = 32, - items = 100, - parallelism = 4, - expectedParallelism = Some(4), - ) - } - - it should "limit the parallelism to the level of the execution context" in { - ConcurrencyLimiterSpec.runTest( - createLimiter = ec => new QueueBasedConcurrencyLimiter(8, ec), - // Ensure the futures don't complete too fast, so that the target parallelism can be reached - waitTimeMillis = 10, - threads = 4, - items = 100, - parallelism = 8, - expectedParallelism = Some(4), - ) - } - - it should "limit the parallelism to the number of work items" in { - ConcurrencyLimiterSpec.runTest( - createLimiter = ec => new QueueBasedConcurrencyLimiter(8, ec), - waitTimeMillis = 1000, - threads = 16, - items = 4, - parallelism = 8, - expectedParallelism = Some(4), - ) - } - - it should "work if the futures complete instantly" in { - ConcurrencyLimiterSpec.runTest( - createLimiter = ec => new QueueBasedConcurrencyLimiter(4, ec), - waitTimeMillis = 0, // Test futures complete instantly - threads = 32, - items = 10000, - parallelism = 4, - expectedParallelism = None, // Futures complete too fast to reach target parallelism - ) - } -} - -object ConcurrencyLimiterSpec extends Assertions { - def runTest( - createLimiter: ExecutionContext => ConcurrencyLimiter, - waitTimeMillis: Long, - threads: Int, - items: Int, - parallelism: Int, - expectedParallelism: Option[Int], - ): Future[Assertion] = { - // EC for running the test Futures - val threadPoolExecutor: ExecutionContext = - ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(threads)) - // EC for all other work - implicit val ec: ExecutionContext = - ExecutionContext.fromExecutorService(Executors.newWorkStealingPool()) - - val running = new AtomicInteger(0) - val limiter = createLimiter(ec) - - val results = (1 to items).map(i => - limiter.execute( - // Note: this future is running in the thread pool executor that is capable of running many tasks in parallel - // The limiter is responsible for not starting too many futures in parallel - Future { - val before = running.getAndIncrement() - assert( - before < parallelism, - s"Task $i started although already $before tasks were running", - ) - - // Simulate some work - if (waitTimeMillis > 0) { - Threading.sleep(waitTimeMillis) - } - - val after = running.decrementAndGet() - assert(after < parallelism, s"Task $i finished while $after other tasks are running") - - before - }(threadPoolExecutor) - ) - ) - - Future - .sequence(results) - .map { xs => - val actualParallelism = xs.max + 1 - expectedParallelism.foreach(expected => - assert( - actualParallelism == expected, - s"$expected were expected to run in parallel, but only up to $actualParallelism task were found to be running in parallel", - ) - ) - succeed - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/ConcurrentBufferedProcessLogger.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/ConcurrentBufferedProcessLogger.scala deleted file mode 100644 index 802597caeb..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/ConcurrentBufferedProcessLogger.scala +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.util - -import java.util.concurrent.ConcurrentLinkedQueue -import scala.sys.process.ProcessLogger - -// to be merged with BufferedProcessLogger from canton -class ConcurrentBufferedLogger extends ProcessLogger { - private val buffer = new ConcurrentLinkedQueue[String] - - override def out(s: => String): Unit = buffer.add(s) - override def err(s: => String): Unit = buffer.add(s) - override def buffer[T](f: => T): T = f - - def output(linePrefix: String = ""): String = - buffer.toArray.map(l => s"$linePrefix$l").mkString(System.lineSeparator) -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/ContractValidatorTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/ContractValidatorTest.scala deleted file mode 100644 index 9004bff3ff..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/ContractValidatorTest.scala +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.util - -import cats.syntax.either.* -import com.digitalasset.canton.crypto.TestSalt -import com.digitalasset.canton.crypto.provider.symbolic.SymbolicPureCrypto -import com.digitalasset.canton.examples.java.cycle.Cycle -import com.digitalasset.canton.protocol.{CantonContractIdV1Version, *} -import com.digitalasset.canton.{ - BaseTest, - FailOnShutdown, - HasExecutionContext, - LfPackageName, - LfPartyId, -} -import com.digitalasset.daml.lf.crypto -import com.digitalasset.daml.lf.data.ImmArray -import com.digitalasset.daml.lf.transaction.CreationTime.CreatedAt -import com.digitalasset.daml.lf.transaction.{CreationTime, FatContractInstance, Versioned} -import com.digitalasset.daml.lf.value.Value -import com.digitalasset.daml.lf.value.Value.ValueText -import org.scalatest.Assertion -import org.scalatest.wordspec.AsyncWordSpec - -import java.time.Duration -import scala.concurrent.Future - -class ContractValidatorTest - extends AsyncWordSpec - with BaseTest - with HasExecutionContext - with FailOnShutdown { - - private val alice = LfPartyId.assertFromString("Alice") - - private val pureCrypto = new SymbolicPureCrypto() - - forEvery(CantonContractIdVersion.allV1) { authContractIdVersion => - // val authContractIdVersion: Canton - val testEngine = - new TestEngine( - packagePaths = Seq(CantonExamplesPath), - iterationsBetweenInterruptions = 10, - cantonContractIdVersion = authContractIdVersion, - loggerFactory = loggerFactory, - ) - - val underTest = - ContractValidator(pureCrypto, testEngine.engine, testEngine.packageResolver) - - def assertAuthenticationError(invalid: FatContractInstance): Future[Assertion] = - assertErrorRegex(invalid, s"AuthenticationFailed.*${invalid.contractId.coid}") - - def assertTypeMismatch(invalid: FatContractInstance): Future[Assertion] = - assertErrorRegex(invalid, s"TranslationFailed.*TypeMismatch") - - def assertValidationFailure(invalid: FatContractInstance): Future[Assertion] = - assertErrorRegex(invalid, s"ValidationFailed.*${invalid.contractId.coid}.*") - - def assertTranslationFailure(invalid: FatContractInstance): Future[Assertion] = - if (authContractIdVersion == AuthenticatedContractIdVersionV10) - _assertErrorRegex(invalid, s"AuthenticationFailed.*${invalid.contractId.coid}") - else - _assertErrorRegex(invalid, s"TranslationFailed.*${invalid.contractId.coid}.*") - - def assertErrorRegex( - invalid: FatContractInstance, - v12errorRegex: String, - ): Future[Assertion] = { - val errorRegex = - if (authContractIdVersion == AuthenticatedContractIdVersionV12) v12errorRegex - else s"AuthenticationFailed.*${invalid.contractId.coid}" - - _assertErrorRegex(invalid, errorRegex) - } - - def _assertErrorRegex( - invalid: FatContractInstance, - errorRegex: String, - ): Future[Assertion] = - underTest - .authenticate(invalid, invalid.templateId.packageId) - .value - .map(e => - inside(e) { case Left(error) => - error should include regex errorRegex - } - ) - - s"ContractAuthenticatorImpl with $authContractIdVersion" when { - - val (createTx, _) = - testEngine.submitAndConsume(new Cycle("id", alice).create().commands.loneElement, alice) - val createNode = createTx.nodes.values.collect { case c: LfNodeCreate => c }.loneElement - val contractInstance = ContractInstance.create(testEngine.suffix(createNode)).value - val targetPackageId = contractInstance.templateId.packageId - - "using a valid contract id" should { - "correctly authenticate the contract" in { - underTest - .authenticate(contractInstance.inst, targetPackageId) - .value - .map(_ shouldBe Either.unit) - } - } - - "using values with unexpected trailing none fields" should { - val unNormalizedArg = Value.ValueRecord( - None, - contractInstance.inst.createArg - .asInstanceOf[Value.ValueRecord] - .fields - .slowAppend(ImmArray.from(Seq((None, Value.ValueOptional(None))))), - ) - - val unNormalizedContract = ExampleContractFactory - .modify[CreationTime.CreatedAt](contractInstance, arg = Some(unNormalizedArg)) - - "fail to authenticate" in { - assertTranslationFailure(unNormalizedContract.inst) - } - } - - "using an invalid contract id" should { - "fail authentication" in { - val invalidContractId = ExampleContractFactory.buildContractId() - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt](contractInstance, contractId = Some(invalidContractId)) - .inst - assertAuthenticationError(invalid) - } - } - - "using a changed salt/authentication data" should { - "fail authentication" in { - val authenticationData = ContractAuthenticationDataV1(TestSalt.generateSalt(42))( - authContractIdVersion - ).toLfBytes - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt](contractInstance, authenticationData = Some(authenticationData)) - .inst - assertAuthenticationError(invalid) - } - } - - "using a changed ledger time" should { - "fail authentication" in { - val changedTime = - CreatedAt(contractInstance.inst.createdAt.time.add(Duration.ofDays(1L))) - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt](contractInstance, createdAt = Some(changedTime)) - .inst - assertAuthenticationError(invalid) - } - } - - "using a changed contract argument" should { - "fail authentication" in { - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt](contractInstance, arg = Some(ValueText("changed"))) - .inst - assertTypeMismatch(invalid) - } - } - - "using a changed template-id" should { - import com.digitalasset.canton.examples.java.iou.Iou - "fail authentication" in { - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt]( - contractInstance, - templateId = Some(testEngine.toRefIdentifier(Iou.TEMPLATE_ID_WITH_PACKAGE_ID)), - ) - .inst - assertTypeMismatch(invalid) - } - } - - "using a changed package-name" should { - "fail authentication" in { - val expected = "definitely-changed-package-name" - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt]( - contractInstance, - packageName = Some(LfPackageName.assertFromString(expected)), - ) - .inst - assertErrorRegex( - invalid, - s"ValidationFailed.*${invalid.contractId.coid}.*package name mismatch.*$expected", - ) - } - } - - "using changed signatories" should { - "fail authentication" in { - val changedSignatory: LfPartyId = - LfPartyId.assertFromString("changed::signatory") - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt]( - contractInstance, - metadata = Some( - ContractMetadata.tryCreate( - signatories = contractInstance.metadata.signatories + changedSignatory, - stakeholders = contractInstance.metadata.stakeholders + changedSignatory, - maybeKeyWithMaintainersVersioned = - contractInstance.metadata.maybeKeyWithMaintainersVersioned, - ) - ), - ) - .inst - assertValidationFailure(invalid) - } - } - - "using changed observers" should { - "fail authentication" in { - val changedObserver: LfPartyId = - LfPartyId.assertFromString("changed::observer") - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt]( - contractInstance, - metadata = Some( - ContractMetadata.tryCreate( - signatories = contractInstance.metadata.signatories, - stakeholders = contractInstance.metadata.stakeholders + changedObserver, - maybeKeyWithMaintainersVersioned = - contractInstance.metadata.maybeKeyWithMaintainersVersioned, - ) - ), - ) - .inst - assertValidationFailure(invalid) - } - } - - } - - // TODO(i16065): Re-enable contract key tests - val keyEnabledContractIdVersions = Seq.empty[CantonContractIdV1Version] - - forEvery(keyEnabledContractIdVersions) { authContractIdVersion => - s"Contract key validations" when { - - val keyWithMaintainers = ExampleContractFactory.buildKeyWithMaintainers() - val contractInstanceWithKey = ExampleContractFactory.build[CreatedAt]( - cantonContractIdVersion = authContractIdVersion, - keyOpt = Some(keyWithMaintainers), - ) - - "using a changed key value" should { - "fail authentication" in { - val changeKey = keyWithMaintainers.copy(globalKey = - LfGlobalKey.assertBuild( - contractInstanceWithKey.templateId, - contractInstanceWithKey.inst.packageName, - ValueText("changed"), - crypto.Hash.hashPrivateKey("dummy-key-hash"), - ) - ) - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt]( - contractInstanceWithKey, - metadata = Some( - ContractMetadata.tryCreate( - signatories = contractInstanceWithKey.metadata.signatories, - stakeholders = contractInstanceWithKey.metadata.stakeholders, - maybeKeyWithMaintainersVersioned = - Some(Versioned(contractInstanceWithKey.inst.version, changeKey)), - ) - ), - ) - .inst - assertAuthenticationError(invalid) - } - } - - "using a changed key maintainers" should { - "fail authentication" ignore { - val changeKey = keyWithMaintainers.copy(maintainers = Set.empty) - val invalid: FatContractInstance = ExampleContractFactory - .modify[CreatedAt]( - contractInstanceWithKey, - metadata = Some( - ContractMetadata.tryCreate( - signatories = contractInstanceWithKey.metadata.signatories, - stakeholders = contractInstanceWithKey.metadata.stakeholders, - maybeKeyWithMaintainersVersioned = - Some(Versioned(contractInstanceWithKey.inst.version, changeKey)), - ) - ), - ) - .inst - assertAuthenticationError(invalid) - } - } - } - } - } -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/TestEngine.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/TestEngine.scala deleted file mode 100644 index 7c6d48d26f..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/TestEngine.scala +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.util - -import cats.implicits.toTraverseOps -import com.daml.ledger.api.v2.commands.Commands.DeduplicationPeriod.Empty -import com.daml.metrics.ExecutorServiceMetrics -import com.daml.metrics.api.noop.NoOpMetricsFactory -import com.digitalasset.canton.FutureHelpers -import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.crypto.provider.symbolic.SymbolicPureCrypto -import com.digitalasset.canton.crypto.{HashOps, HmacOps, Salt, TestSalt} -import com.digitalasset.canton.ledger.api.validation.ValidateUpgradingPackageResolutions.ValidatedCommandPackageResolutionsSnapshot -import com.digitalasset.canton.ledger.api.validation.{ - CommandsValidator, - ValidateUpgradingPackageResolutions, -} -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown -import com.digitalasset.canton.logging.NoLogging.noTracingLogger -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NoLogging} -import com.digitalasset.canton.protocol.* -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.PackageConsumer.PackageResolver -import com.digitalasset.canton.util.TestContractHasher.SyncContractHasher -import com.digitalasset.canton.util.TestEngine.{InMemoryPackageStore, TxAndMeta} -import com.digitalasset.daml.lf.archive -import com.digitalasset.daml.lf.archive.DamlLf -import com.digitalasset.daml.lf.command.ReplayCommand -import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref.{PackageId, ParticipantId, Party, QualifiedName} -import com.digitalasset.daml.lf.data.{Ref, Time} -import com.digitalasset.daml.lf.engine.* -import com.digitalasset.daml.lf.engine.ResultNeedContract.Response -import com.digitalasset.daml.lf.language.Ast.Package -import com.digitalasset.daml.lf.language.{Ast, LanguageVersion} -import com.digitalasset.daml.lf.transaction.* -import com.digitalasset.daml.lf.value.Value.ContractId -import com.digitalasset.daml.lf.value.{ContractIdVersion, Value} -import io.grpc.StatusRuntimeException -import org.scalatest.{EitherValues, OptionValues} - -import java.io.File -import java.time.{Duration, Instant} -import scala.annotation.tailrec - -/** Allows API commands to be applied directly to the engine. - */ -class TestEngine( - packagePaths: Seq[String], - participantId: ParticipantId = ParticipantId.assertFromString("TestParticipantId"), - userId: String = "TestUserId", - commandId: String = "TestCmdId", - iterationsBetweenInterruptions: Long = 1000, - cantonContractIdVersion: CantonContractIdV1Version = CantonContractIdVersion.maxV1, - contractStateMode: NextGenContractStateMachine.Mode = NextGenContractStateMachine.Mode.NoKey, - loggerFactory: NamedLoggerFactory, -) extends EitherValues - with OptionValues { - - private val validateUpgradingPackageResolutions = new ValidateUpgradingPackageResolutions { - override def apply(rawUserPackageIdPreferences: Seq[String])(implicit - errorLoggingContext: ErrorLoggingContext - ): Either[StatusRuntimeException, ValidatedCommandPackageResolutionsSnapshot] = - Right( - ValidatedCommandPackageResolutionsSnapshot( - packageStore.packageMap, - packageStore.packagePreference, - ) - ) - } - - private val commandsValidator = new CommandsValidator( - validateUpgradingPackageResolutions = validateUpgradingPackageResolutions - ) - - val packageResolver: PackageResolver = new PackageResolver { - override protected def resolveInternal(packageId: PackageId)(implicit - traceContext: TraceContext - ): FutureUnlessShutdown[Option[Package]] = - FutureUnlessShutdown.pure(packageStore.getPackage(packageId)) - } - - val packageStore: InMemoryPackageStore = packagePaths.foldLeft(InMemoryPackageStore()) { (s, p) => - s.withDarFile(new File(p)).value - } - - private implicit val logger: ErrorLoggingContext = NoLogging - - private val zeroHash = LfHash.assertFromByteArray(Array.ofDim[Byte](Hash.underlyingHashLength)) - val randomHash: () => LfHash = LfHash.secureRandom(zeroHash) - - private val nextSalt: () => Salt = { - val it = Iterator.from(0) - () => TestSalt.generateSalt(it.next()) - } - - val cryptoOps: HashOps & HmacOps = new SymbolicPureCrypto() - - val unicumGenerator = new UnicumGenerator(cryptoOps) - - private val testInstant = Instant.now - private val testTimestamp = Time.Timestamp.assertFromInstant(testInstant) - private val maxDeduplicationDuration = Duration.ZERO - - val engine = new Engine( - EngineConfig( - allowedLanguageVersions = LanguageVersion.allLfVersions, - iterationsBetweenInterruptions = iterationsBetweenInterruptions, - ), - loggerFactory, - ) - - def hashAndConsume( - c: LfNodeCreate, - method: Hash.HashingMethod = cantonContractIdVersion.contractHashingMethod, - ): LfHash = - consume(engine.hashCreateNode(c, identity, method)) - - private val valueEnricher = Enricher(engine) - - def consume[T]( - initial: Result[T], - contracts: Map[ContractId, FatContractInstance] = Map.empty, - ): T = { - @tailrec - def go(need: Result[T]): T = - need match { - case ResultDone(result) => result - case ResultPrefetch(_, _, resume) => - go(resume()) - case ResultNeedPackage(packageId, resume) => - go(resume(packageStore.getPackage(packageId))) - case ResultNeedContract(acoid, resume) => - go(resume(contracts.get(acoid) match { - case Some(contractInstance) => - Response.ContractFound( - contractInstance, - Hash.HashingMethod.UpgradeFriendly, - _ => true, - ) - case None => - Response.ContractNotFound - })) - case ResultInterruption(continue, _) => - go(continue()) - case other => throw new IllegalStateException(s"Did not expect $other") - } - go(initial) - } - - def validateCommand( - command: com.daml.ledger.javaapi.data.Command, - actAs: String, - disclosedContracts: Seq[FatContractInstance] = Seq.empty, - ): com.digitalasset.canton.ledger.api.Commands = { - - val protoCommand: com.daml.ledger.api.v2.commands.Command = - com.daml.ledger.api.v2.commands.Command.fromJavaProto(command.toProtoCommand) - - val commands: com.daml.ledger.api.v2.commands.Commands = - com.daml.ledger.api.v2.commands.Commands( - workflowId = "", - userId = userId, - commandId = commandId, - commands = Seq(protoCommand), - deduplicationPeriod = Empty, - minLedgerTimeAbs = None, - minLedgerTimeRel = None, - actAs = Seq(actAs), - readAs = Nil, - submissionId = "", - disclosedContracts = disclosedContracts.map(disclose), - synchronizerId = "", - packageIdSelectionPreference = Nil, - prefetchContractKeys = Nil, - tapsMaxPasses = None, - ) - - val engineCommands: com.digitalasset.canton.ledger.api.Commands = - commandsValidator - .validateCommands( - commands = commands, - currentLedgerTime = testInstant, - currentUtcTime = testInstant, - maxDeduplicationDuration = maxDeduplicationDuration, - ) - .value - - engineCommands - } - - def submitAndConsume( - command: com.daml.ledger.javaapi.data.Command, - actAs: String, - contracts: Seq[FatContractInstance] = Seq.empty, - )(implicit traceContext: TraceContext): (SubmittedTransaction, Transaction.Metadata) = { - - val engineCommands = validateCommand(command, actAs) - - val result: Result[TxAndMeta] = engine.submit( - packageMap = engineCommands.packageMap, - packagePreference = engineCommands.packagePreferenceSet, - submitters = Set(Ref.Party.assertFromString(actAs)), - cmds = engineCommands.commands, - participantId = participantId, - submissionSeed = randomHash(), - readAs = Set.empty, - prefetchKeys = Seq.empty, - contractIdVersion = ContractIdVersion.V1, - contractStateMode = contractStateMode, - ) - - val contractMap = contracts.map(c => c.contractId -> c).toMap - - consume(result, contracts = contractMap) - - } - - def suffix(create: Node.Create): LfFatContractInst = { - - val salt = nextSalt() - - val unicum = unicumGenerator - .recomputeUnicum( - contractSalt = salt, - ledgerCreateTime = CreationTime.CreatedAt(testTimestamp), - metadata = ContractMetadata.tryCreate( - create.signatories, - create.stakeholders, - create.keyOpt.map(Versioned(create.version, _)), - ), - contractHash = hashAndConsume(create), - ) - .value - - val discriminator = create.coid.asInstanceOf[LfContractId.V1].discriminator - - val contractId = cantonContractIdVersion.fromDiscriminator(discriminator, unicum) - - val suffixed = create.mapCid(_ => contractId) - - val authenticationData = - ContractAuthenticationDataV1(salt)(cantonContractIdVersion).toLfBytes - - FatContractInstance.fromCreateNode( - suffixed, - CreationTime.CreatedAt(testTimestamp), - authenticationData, - ) - } - - def recomputeUnicum( - fat: FatContractInstance, - recomputeIdVersion: CantonContractIdV1Version, - ): Unicum = { - val contractHash = hashAndConsume(fat.toCreateNode, recomputeIdVersion.contractHashingMethod) - unicumGenerator.recomputeUnicum(fat, recomputeIdVersion, contractHash).value - } - - def disclose(fat: FatContractInstance): com.daml.ledger.api.v2.commands.DisclosedContract = { - val t = fat.templateId - com.daml.ledger.api.v2.commands.DisclosedContract( - templateId = Some( - com.daml.ledger.api.v2.value.Identifier( - t.packageId, - t.qualifiedName.module.dottedName, - t.qualifiedName.name.dottedName, - ) - ), - contractId = fat.contractId.coid, - createdEventBlob = TransactionCoder.encodeFatContractInstance(fat).value, - synchronizerId = "", - ) - } - - def reinterpretAndConsume( - submitters: Set[Ref.Party], - command: ReplayCommand, - nodeSeed: Hash, - contracts: Map[ContractId, FatContractInstance] = Map.empty, - packageResolution: Map[Ref.PackageName, Ref.PackageId] = Map.empty, - preparationTime: Time.Timestamp = testTimestamp, - ledgerEffectiveTime: Time.Timestamp = testTimestamp, - contractStateMode: NextGenContractStateMachine.Mode, - )(implicit traceContext: TraceContext): TxAndMeta = { - - val result = engine.reinterpret( - submitters = submitters, - command = command, - nodeSeed = Some(nodeSeed), - preparationTime = preparationTime, - ledgerEffectiveTime = ledgerEffectiveTime, - packageResolution = packageResolution, - contractIdVersion = ContractIdVersion.V1, - contractStateMode = contractStateMode, - ) - consume(result, contracts) - } - - def reinterpretReplayNode( - testNodeId: NodeId, - tx: SubmittedTransaction, - meta: Transaction.Metadata, - contracts: Map[ContractId, FatContractInstance] = Map.empty, - ledgerTime: Time.Timestamp = testTimestamp, - contractStateMode: NextGenContractStateMachine.Mode, - )(implicit traceContext: TraceContext): (SubmittedTransaction, Transaction.Metadata) = { - - val nodeSeeds = Map.from(meta.nodeSeeds.toList) - val node = tx.nodes.get(testNodeId).value - val (replayCmd, submitters) = replayCommand(node) - - val nodeSeed = nodeSeeds(testNodeId) - - val packageResolution = tx.nodes.values - .collect { - case ex: Node.Exercise => ex.interfaceId.map(_ => ex.packageName -> ex.templateId.packageId) - case _ => None - } - .flatten - .toMap - - reinterpretAndConsume( - submitters = submitters, - command = replayCmd, - nodeSeed = nodeSeed, - contracts = contracts, - packageResolution = packageResolution, - preparationTime = meta.preparationTime, - ledgerEffectiveTime = ledgerTime, - contractStateMode = contractStateMode, - ) - } - - def replayCommand(node: LfNode): (ReplayCommand, Set[Party]) = - node match { - case create: LfNodeCreate => - ( - ReplayCommand.Create( - create.templateId, - create.arg, - ), - create.requiredAuthorizers, - ) - case ex: LfNodeExercises => - ( - ReplayCommand.Exercise( - templateId = ex.templateId, - interfaceId = ex.interfaceId, - contractId = ex.targetCoid, - choiceId = ex.choiceId, - argument = ex.chosenValue, - ), - ex.requiredAuthorizers, - ) - case other => throw new UnsupportedOperationException(s"Do not support $other") - } - - def toRefIdentifier(i: com.daml.ledger.javaapi.data.Identifier): Ref.Identifier = - Ref.Identifier( - Ref.PackageId.assertFromString(i.getPackageId), - QualifiedName( - Ref.ModuleName.assertFromString(i.getModuleName), - Ref.DottedName.assertFromString(i.getEntityName), - ), - ) - - def enrichContract(identifier: com.daml.ledger.javaapi.data.Identifier, value: Value)(implicit - traceContext: TraceContext - ): Value = - consume(valueEnricher.enrichContract(toRefIdentifier(identifier), value), Map.empty) - - def extractAuthenticationData(fat: FatContractInstance): ContractAuthenticationData = { - val contractIdVersion = CantonContractIdVersion.tryCantonContractIdVersion(fat.contractId) - ContractAuthenticationData.fromLfBytes(contractIdVersion, fat.authenticationData).value - } - -} - -object TestEngine extends FutureHelpers with EitherValues { - - private type TxAndMeta = (SubmittedTransaction, Transaction.Metadata) - - def syncContractHasher( - loggerFactory: NamedLoggerFactory, - packagePaths: String* - ): SyncContractHasher = { - val testEngine = new TestEngine(packagePaths, loggerFactory = loggerFactory) - val hasher = ContractHasher(testEngine.engine, testEngine.packageResolver) - new TestContractHasher.SyncContractHasher { - private val ec = - Threading.singleThreadedExecutor( - "TestEngine.syncContractHasher", - noTracingLogger, - new ExecutorServiceMetrics(NoOpMetricsFactory), - ) - override def hash(create: LfNodeCreate, hashingMethod: Hash.HashingMethod): LfHash = - hasher - .hash(create, hashingMethod, PackageResolver.ignoreMissingPackage)(ec, TraceContext.empty) - .value - .futureValueUS - .value - } - } - - final case class InMemoryPackageStore( - packages: Map[PackageId, (DamlLf.Archive, Ast.Package)] = Map.empty - ) { - - val packageMap: Map[Ref.PackageId, (Ref.PackageName, Ref.PackageVersion)] = - packages.view.mapValues { case (_, p) => (p.metadata.name, p.metadata.version) }.toMap - - val packagePreference: Set[PackageId] = packages.keySet - - def getPackage(packageId: PackageId): Option[Ast.Package] = - packages.get(packageId).map(_._2) - - def getArchive(packageId: PackageId): Option[DamlLf.Archive] = - packages.get(packageId).map(_._1) - - def withDarFile(file: File): Either[String, InMemoryPackageStore] = - for { - dar <- archive.DarParser - .readArchiveFromFile(file) - .left - .map(t => s"Failed to parse DAR from $file: $t") - packages <- addArchives(dar.all) - } yield packages - - private def addArchives(archives: List[DamlLf.Archive]): Either[String, InMemoryPackageStore] = - archives - .traverse(proto => - try { - Right((proto, archive.Decode.assertDecodeArchive(proto)._2)) - } catch { - case err: archive.Error => Left(s"Could not parse archive ${proto.getHash}: $err") - } - ) - .map(pkgs => - pkgs.foldLeft(this) { case (store, (archive, pkg)) => - val pkgId = PackageId.assertFromString(archive.getHash) - store.addPackage(pkgId, archive, pkg) - } - ) - - private def addPackage( - pkgId: PackageId, - archive: DamlLf.Archive, - pkg: Ast.Package, - ): InMemoryPackageStore = - InMemoryPackageStore(packages + (pkgId -> (archive, pkg))) - - } - -} diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/api/TimestampConversionTest.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/api/TimestampConversionTest.scala deleted file mode 100644 index 74755e14e9..0000000000 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/util/api/TimestampConversionTest.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.util.api - -import com.digitalasset.canton.ledger.api.util.TimestampConversion -import com.google.protobuf.timestamp.Timestamp -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import java.time.Clock - -class TimestampConversionTest extends AnyWordSpec with Matchers { - - private val instant = Clock.systemUTC().instant() - private val timestamp = Timestamp(instant.getEpochSecond, instant.getNano) - - "TimestampConversion" should { - - "convert proto Timestamps to Instant" in { - - TimestampConversion.toInstant(timestamp) shouldEqual instant - } - - "convert Instants to proto Timestamps" in { - - TimestampConversion.fromInstant(instant) shouldEqual timestamp - } - - } -} diff --git a/canton/community/ledger/ledger-json-api/src/main/resources/ledger-api/proto-data.yml b/canton/community/ledger/ledger-json-api/src/main/resources/ledger-api/proto-data.yml index 70ac8b53e8..17ac6d06c9 100644 --- a/canton/community/ledger/ledger-json-api/src/main/resources/ledger-api/proto-data.yml +++ b/canton/community/ledger/ledger-json-api/src/main/resources/ledger-api/proto-data.yml @@ -1484,6 +1484,25 @@ fileComments: fieldComments: completion: '' offset_checkpoint: '' + GetCompletionsRequest: + message: + comments: null + fieldComments: + parties: |- + If specified, only completions of commands are included, which have at least one of the ``act_as`` parties + in the given set of parties. + Only Ledger API users with CanReadAsAnyParty permission allowed to provide no ``parties``. + Must be a valid PartyIdString (as described in ``value.proto``). + + Optional: can be empty + begin_exclusive: |- + This optional field indicates the minimum offset for completions. This can be used to resume an earlier completion stream. + If not set the ledger uses the ledger begin offset instead. + If specified, it must be a valid absolute offset (positive integer) or zero (ledger begin offset). + If the ledger has been pruned, this parameter must be specified and greater than the pruning offset. (the pruning + offset is accessible on the StateService.GetLatestPrunedOffsets endpoint) + + Optional oneOfs: CompletionStreamRequest: {} CompletionStreamResponse: @@ -1493,10 +1512,17 @@ fileComments: fieldComments: completion: '' offset_checkpoint: '' + GetCompletionsRequest: {} services: CommandCompletionService: - name: CompletionStream - comments: Subscribe to command completion events. + comments: |- + Deprecated: please use ``GetCompletions`` instead. + Subscribe to command completion events. + - name: GetCompletions + comments: |- + Subscribe to command completion events. + This streaming endpoint provides more flexibility in filtering than the predecessor ``CompletionStream``. command_service.proto: messages: SubmitAndWaitForReassignmentRequest: @@ -4818,7 +4844,7 @@ fileComments: ``next_page_token`` of the last ``GetUpdatesPageResponse``. To achieve correct paging: subsequent requests must - - be executed on the same participant with the same version of canton, + - be executed on the same participant, - have the same begin_offset_exclusive, - have the same end_offset_inclusive, - have the same update_format and diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsCommandService.scala b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsCommandService.scala index 5d1d9c455b..a0d06fe66c 100644 --- a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsCommandService.scala +++ b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsCommandService.scala @@ -128,6 +128,15 @@ class JsCommandService( commandCompletionStream, timeoutOpenEndedStream = (_: command_completion_service.CompletionStreamRequest) => true, ), + websocket( + JsCommandService.commandCompletionsEndpoint, + commandCompletionsStream, + ), + asList( + JsCommandService.commandCompletionsListEndpoint, + commandCompletionsStream, + timeoutOpenEndedStream = (_: command_completion_service.GetCompletionsRequest) => true, + ), ) private def commandCompletionStream( @@ -144,6 +153,20 @@ class JsCommandService( ) } + private def commandCompletionsStream( + caller: CallerContext + ): TracedInput[Unit] => Flow[ + command_completion_service.GetCompletionsRequest, + command_completion_service.CompletionStreamResponse, + NotUsed, + ] = _ => { + implicit val tc: TraceContext = caller.traceContext() + prepareSingleWsStream( + commandCompletionServiceClient(caller.token()).getCompletions, + Future.successful[command_completion_service.CompletionStreamResponse], + ) + } + def submitAndWait(callerContext: CallerContext): TracedInput[JsCommands] => Future[ Either[JsCantonError, SubmitAndWaitResponse] ] = req => { @@ -402,6 +425,33 @@ object JsCommandService extends DocumentationEndpoints { """.stripMargin.trim) .inStreamListParamsAndDescription() + val commandCompletionsEndpoint = + commands.get + .in(sttp.tapir.stringToPath("command-completions")) + .out( + webSocketBody[ + command_completion_service.GetCompletionsRequest, + CodecFormat.Json, + Either[JsCantonError, command_completion_service.CompletionStreamResponse], + CodecFormat.Json, + ](PekkoStreams) + ) + .protoRef(command_completion_service.CommandCompletionServiceGrpc.METHOD_GET_COMPLETIONS) + + val commandCompletionsListEndpoint = + commands.post + .in(sttp.tapir.stringToPath("command-completions")) + .in(jsonBody[command_completion_service.GetCompletionsRequest]) + .out(jsonBody[Seq[command_completion_service.CompletionStreamResponse]]) + .description(s"""| + |Query completions list (blocking call) + | + |${createProtoRef( + command_completion_service.CommandCompletionServiceGrpc.METHOD_GET_COMPLETIONS + )} + """.stripMargin.trim) + .inStreamListParamsAndDescription() + override def documentation: Seq[AnyEndpoint] = Seq( submitAndWait, submitAndWaitForTransactionEndpoint, @@ -411,6 +461,8 @@ object JsCommandService extends DocumentationEndpoints { submitReassignmentAsyncEndpoint, completionStreamEndpoint, completionListEndpoint, + commandCompletionsEndpoint, + commandCompletionsListEndpoint, ) } @@ -460,6 +512,9 @@ object JsCommandServiceCodecs { implicit val commandCompletionRW: Codec[command_completion_service.CompletionStreamRequest] = deriveRelaxedCodec + implicit val getCompletionsRequestRW: Codec[command_completion_service.GetCompletionsRequest] = + deriveRelaxedCodec + implicit val reassignmentCommandsRW: Codec[reassignment_commands.ReassignmentCommands] = deriveRelaxedCodec diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUpdateService.scala b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUpdateService.scala index bed979ed1f..c7db577d09 100644 --- a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUpdateService.scala +++ b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUpdateService.scala @@ -340,7 +340,7 @@ class JsUpdateService( toGetUpdatesRequest(request, forTrees = false) } via prepareSingleWsStream( - updateServiceClient(caller.token())(TraceContext.empty).getUpdates, + updateServiceClient(caller.token())(tc).getUpdates, (r: update_service.GetUpdatesResponse) => protocolConverters.GetUpdatesResponse.toJson(r), ) } diff --git a/canton/community/ledger/ledger-json-api/src/test/resources/json-api-docs/asyncapi.yaml b/canton/community/ledger/ledger-json-api/src/test/resources/json-api-docs/asyncapi.yaml index c4d194bb4e..8d4ced1e0c 100644 --- a/canton/community/ledger/ledger-json-api/src/test/resources/json-api-docs/asyncapi.yaml +++ b/canton/community/ledger/ledger-json-api/src/test/resources/json-api-docs/asyncapi.yaml @@ -1,15 +1,17 @@ asyncapi: 2.6.0 info: title: JSON Ledger API WebSocket endpoints - version: 3.5.1-SNAPSHOT + version: 3.5.7-SNAPSHOT description: |- This specification version fixes the API inconsistencies where certain fields marked as required in the spec are in fact optional. If you use code generation tool based on this file, you might need to adjust the existing application code to handle those fields as optional. If you do not want to change your client code, continue using the OpenAPI specification for the latest Canton 3.4 patch release. - MINIMUM_CANTON_VERSION=3.5.1 + MINIMUM_CANTON_VERSION=3.5.7 channels: /v2/commands/completions: - description: Subscribe to command completion events. + description: |- + Deprecated: please use ``GetCompletions`` instead. + Subscribe to command completion events. subscribe: operationId: onV2CommandsCompletions message: @@ -21,6 +23,21 @@ channels: bindings: ws: method: GET + /v2/commands/command-completions: + description: |- + Subscribe to command completion events. + This streaming endpoint provides more flexibility in filtering than the predecessor ``CompletionStream``. + subscribe: + operationId: onV2CommandsCommand-completions + message: + $ref: '#/components/messages/Either_JsCantonError_CompletionStreamResponse' + publish: + operationId: sendV2CommandsCommand-completions + message: + $ref: '#/components/messages/GetCompletionsRequest' + bindings: + ws: + method: GET /v2/state/active-contracts: description: |- Returns a stream of the snapshot of the active contracts and incomplete (un)assignments at a ledger offset. @@ -540,6 +557,32 @@ components: type: array items: $ref: '#/components/schemas/SynchronizerTime' + GetCompletionsRequest: + title: GetCompletionsRequest + type: object + properties: + parties: + description: |- + If specified, only completions of commands are included, which have at least one of the ``act_as`` parties + in the given set of parties. + Only Ledger API users with CanReadAsAnyParty permission allowed to provide no ``parties``. + Must be a valid PartyIdString (as described in ``value.proto``). + + Optional: can be empty + type: array + items: + type: string + beginExclusive: + description: |- + This optional field indicates the minimum offset for completions. This can be used to resume an earlier completion stream. + If not set the ledger uses the ledger begin offset instead. + If specified, it must be a valid absolute offset (positive integer) or zero (ledger begin offset). + If the ledger has been pruned, this parameter must be specified and greater than the pruning offset. (the pruning + offset is accessible on the StateService.GetLatestPrunedOffsets endpoint) + + Optional + type: integer + format: int64 GetActiveContractsRequest: title: GetActiveContractsRequest description: |- @@ -2466,6 +2509,10 @@ components: payload: $ref: '#/components/schemas/Either_JsCantonError_CompletionStreamResponse' contentType: application/json + GetCompletionsRequest: + payload: + $ref: '#/components/schemas/GetCompletionsRequest' + contentType: application/json GetActiveContractsRequest: payload: $ref: '#/components/schemas/GetActiveContractsRequest' diff --git a/canton/community/ledger/ledger-json-api/src/test/resources/json-api-docs/openapi.yaml b/canton/community/ledger/ledger-json-api/src/test/resources/json-api-docs/openapi.yaml index b4d1fe8d40..343a8f8391 100644 --- a/canton/community/ledger/ledger-json-api/src/test/resources/json-api-docs/openapi.yaml +++ b/canton/community/ledger/ledger-json-api/src/test/resources/json-api-docs/openapi.yaml @@ -1,12 +1,12 @@ openapi: 3.0.3 info: title: JSON Ledger API HTTP endpoints - version: 3.5.1-SNAPSHOT + version: 3.5.7-SNAPSHOT description: |- This specification version fixes the API inconsistencies where certain fields marked as required in the spec are in fact optional. If you use code generation tool based on this file, you might need to adjust the existing application code to handle those fields as optional. If you do not want to change your client code, continue using the OpenAPI specification for the latest Canton 3.4 patch release. - MINIMUM_CANTON_VERSION=3.5.1 + MINIMUM_CANTON_VERSION=3.5.7 paths: /v2/commands/submit-and-wait: post: @@ -214,6 +214,7 @@ paths: description: |- Query completions list (blocking call) + Deprecated: please use ``GetCompletions`` instead. Subscribe to command completion events. Notice: This endpoint should be used for small results set. When number of results exceeded node configuration limit (`http-list-max-elements-limit`) @@ -269,6 +270,67 @@ paths: security: - httpAuth: [] - apiKeyAuth: [] + /v2/commands/command-completions: + post: + description: |- + Query completions list (blocking call) + + Subscribe to command completion events. + This streaming endpoint provides more flexibility in filtering than the predecessor ``CompletionStream``. + Notice: This endpoint should be used for small results set. + When number of results exceeded node configuration limit (`http-list-max-elements-limit`) + there will be an error (`413 Content Too Large`) returned. + Increasing this limit may lead to performance issues and high memory consumption. + Consider using websockets (asyncapi) for better efficiency with larger results. + operationId: postV2CommandsCommand-completions + parameters: + - name: limit + in: query + description: maximum number of elements to return, this param is ignored if + is bigger than server setting + required: false + schema: + type: integer + format: int64 + - name: stream_idle_timeout_ms + in: query + description: timeout to complete and send result if no new elements are received + (for open ended streams) + required: false + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetCompletionsRequest' + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CompletionStreamResponse' + '400': + description: 'Invalid value, Invalid value for: body, Invalid value for: + query parameter limit, Invalid value for: query parameter stream_idle_timeout_ms' + content: + text/plain: + schema: + type: string + default: + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/JsCantonError' + security: + - httpAuth: [] + - apiKeyAuth: [] /v2/events/events-by-contract-id: post: description: |- @@ -4393,6 +4455,32 @@ components: Optional: can be empty type: string + GetCompletionsRequest: + title: GetCompletionsRequest + type: object + properties: + parties: + description: |- + If specified, only completions of commands are included, which have at least one of the ``act_as`` parties + in the given set of parties. + Only Ledger API users with CanReadAsAnyParty permission allowed to provide no ``parties``. + Must be a valid PartyIdString (as described in ``value.proto``). + + Optional: can be empty + type: array + items: + type: string + beginExclusive: + description: |- + This optional field indicates the minimum offset for completions. This can be used to resume an earlier completion stream. + If not set the ledger uses the ledger begin offset instead. + If specified, it must be a valid absolute offset (positive integer) or zero (ledger begin offset). + If the ledger has been pruned, this parameter must be specified and greater than the pruning offset. (the pruning + offset is accessible on the StateService.GetLatestPrunedOffsets endpoint) + + Optional + type: integer + format: int64 GetConnectedSynchronizersResponse: title: GetConnectedSynchronizersResponse type: object @@ -4815,7 +4903,7 @@ components: ``next_page_token`` of the last ``GetUpdatesPageResponse``. To achieve correct paging: subsequent requests must - - be executed on the same participant with the same version of canton, + - be executed on the same participant, - have the same begin_offset_exclusive, - have the same end_offset_inclusive, - have the same update_format and diff --git a/canton/community/ledger/ledger-json-client/src/test/scala/com/digitalasset/canton/openapi/OpenapiTypesTest.scala b/canton/community/ledger/ledger-json-client/src/test/scala/com/digitalasset/canton/openapi/OpenapiTypesTest.scala index acb7d26a3c..f363acb878 100644 --- a/canton/community/ledger/ledger-json-client/src/test/scala/com/digitalasset/canton/openapi/OpenapiTypesTest.scala +++ b/canton/community/ledger/ledger-json-client/src/test/scala/com/digitalasset/canton/openapi/OpenapiTypesTest.scala @@ -423,6 +423,12 @@ class OpenapiTypesTest extends AnyWordSpec with Matchers { Mapping[LegacyDTOs.GetActiveContractsRequest, openapi.GetActiveContractsRequest]( openapi.GetActiveContractsRequest.fromJson ), + Mapping[ + v2.command_completion_service.GetCompletionsRequest, + openapi.GetCompletionsRequest, + ]( + openapi.GetCompletionsRequest.fromJson + ), Mapping[ v2.state_service.GetConnectedSynchronizersResponse, openapi.GetConnectedSynchronizersResponse, diff --git a/canton/community/lib/Blake2b/src/main/java/org/bouncycastle/crypto/digests/canton/Blake2bDigest.java b/canton/community/lib/Blake2b/src/main/java/org/bouncycastle/crypto/digests/canton/Blake2bDigest.java deleted file mode 100644 index 152855d19f..0000000000 --- a/canton/community/lib/Blake2b/src/main/java/org/bouncycastle/crypto/digests/canton/Blake2bDigest.java +++ /dev/null @@ -1,456 +0,0 @@ -package org.bouncycastle.crypto.digests.canton; - -/* The BLAKE2 cryptographic hash function was designed by Jean- - Philippe Aumasson, Samuel Neves, Zooko Wilcox-O'Hearn, and Christian - Winnerlein. - - Reference Implementation and Description can be found at: https://blake2.net/ - Internet Draft: https://tools.ietf.org/html/draft-saarinen-blake2-02 - - This implementation does not support the Tree Hashing Mode. - - For unkeyed hashing, developers adapting BLAKE2 to ASN.1 - based - message formats SHOULD use the OID tree at x = 1.3.6.1.4.1.1722.12.2. - - Algorithm | Target | Collision | Hash | Hash ASN.1 | - Identifier | Arch | Security | nn | OID Suffix | - ---------------+--------+-----------+------+------------+ - id-blake2b160 | 64-bit | 2**80 | 20 | x.1.20 | - id-blake2b256 | 64-bit | 2**128 | 32 | x.1.32 | - id-blake2b384 | 64-bit | 2**192 | 48 | x.1.48 | - id-blake2b512 | 64-bit | 2**256 | 64 | x.1.64 | - ---------------+--------+-----------+------+------------+ - */ - -import org.bouncycastle.crypto.ExtendedDigest; -import org.bouncycastle.util.Arrays; -import org.bouncycastle.util.Longs; -import org.bouncycastle.util.Pack; - - -/** - * This is a modified version of BouncyCastle's implementation of the cryptographic hash function Blakbe2b. - * It's changed to support additional parameters that are required to implement Blake2xb on top of Blake2x. - * Conversely, it disables other features (keying mechanism, personalization) not needed for Blake2xb - */ -public class Blake2bDigest - implements ExtendedDigest -{ - // Blake2b Initialization Vector: - private final static long[] blake2b_IV = - // Produced from the square root of primes 2, 3, 5, 7, 11, 13, 17, 19. - // The same as SHA-512 IV. - { - 0x6a09e667f3bcc908L, 0xbb67ae8584caa73bL, 0x3c6ef372fe94f82bL, - 0xa54ff53a5f1d36f1L, 0x510e527fade682d1L, 0x9b05688c2b3e6c1fL, - 0x1f83d9abfb41bd6bL, 0x5be0cd19137e2179L - }; - - // Message word permutations: - private final static byte[][] blake2b_sigma = - { - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, - {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}, - {11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4}, - {7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8}, - {9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13}, - {2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9}, - {12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11}, - {13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10}, - {6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5}, - {10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, - {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3} - }; - - private static int ROUNDS = 12; // to use for Catenas H' - private final static int BLOCK_LENGTH_BYTES = 128;// bytes - - // General parameters: - private int digestLength = 64; // 1- 64 bytes - private int keyLength = 0; // 0 - 64 bytes for keyed hashing for MAC - private byte[] salt = null;// new byte[16]; - private byte[] personalization = null;// new byte[16]; - - // the key - private byte[] key = null; - - // Tree hashing parameters. While the tree mode is not supported, these are still used to implement the - // Blake2xb XOF on top of Blake2b - private int fanout = 1; // 0-255 - private int depth = 1; // 1 - 255 - private int leafLength= 0; - private long nodeOffset = 0L; - private int nodeDepth = 0; - private int innerHashLength = 0; - - // whenever this buffer overflows, it will be processed - // in the compress() function. - // For performance issues, long messages will not use this buffer. - private byte[] buffer = null;// new byte[BLOCK_LENGTH_BYTES]; - // Position of last inserted byte: - private int bufferPos = 0;// a value from 0 up to 128 - - private long[] internalState = new long[16]; // In the Blake2b paper it is - // called: v - private long[] chainValue = null; // state vector, in the Blake2b paper it - // is called: h - - private long t0 = 0L; // holds last significant bits, counter (counts bytes) - private long t1 = 0L; // counter: Length up to 2^128 are supported - private long f0 = 0L; // finalization flag, for last block: ~0L - - // For Tree Hashing Mode, not used here: - // private long f1 = 0L; // finalization flag, for last node: ~0L - - public Blake2bDigest(Blake2bDigest digest) - { - this.bufferPos = digest.bufferPos; - this.buffer = Arrays.clone(digest.buffer); - this.keyLength = digest.keyLength; - this.key = Arrays.clone(digest.key); - this.digestLength = digest.digestLength; - this.chainValue = Arrays.clone(digest.chainValue); - this.personalization = Arrays.clone(digest.personalization); - this.salt = Arrays.clone(digest.salt); - this.t0 = digest.t0; - this.t1 = digest.t1; - this.f0 = digest.f0; - this.internalState = digest.internalState; - this.fanout = digest.fanout; - this.depth = digest.depth; - this.leafLength = digest.leafLength; - this.nodeOffset = digest.nodeOffset; - this.nodeDepth = digest.nodeDepth; - this.innerHashLength = digest.innerHashLength; - } - - public static byte[] digest(byte[] bytes, int digestBytes, int fanout, int depth, int leafLength, long nodeOffset, int nodeDepth, int innerHashLength) { - byte[] out = new byte[digestBytes]; - Blake2bDigest d = new Blake2bDigest(digestBytes, fanout, depth, leafLength, nodeOffset, nodeDepth, innerHashLength); - d.update(bytes, 0, bytes.length); - d.doFinal(out, 0); - return out; - } - - public Blake2bDigest(int digestBytes, int fanout, int depth, int leafLength, long nodeOffset, int nodeDepth, int innerHashLength) - { - if (digestBytes < 0 || digestBytes > 64) - { - throw new IllegalArgumentException( - "BLAKE2b digest byte length must be between 0 and 64 (inclusive)"); - } - - buffer = new byte[BLOCK_LENGTH_BYTES]; - keyLength = 0; - this.digestLength = digestBytes; - this.fanout = fanout; - this.depth = depth; - this.leafLength = leafLength; - this.nodeOffset = nodeOffset; - this.nodeDepth = nodeDepth; - this.innerHashLength = innerHashLength; - init(); - } - - // initialize chainValue - private void init() - { - if (chainValue == null) - { - chainValue = new long[8]; - - chainValue[0] = blake2b_IV[0] - ^ (digestLength | (keyLength << 8) | (fanout << 16) | (depth << 24) | (((long) leafLength) << 32)); - chainValue[1] = blake2b_IV[1] ^ nodeOffset ; - chainValue[2] = blake2b_IV[2] ^ (nodeDepth | (innerHashLength << 8)); - chainValue[3] = blake2b_IV[3]; - chainValue[4] = blake2b_IV[4]; - chainValue[5] = blake2b_IV[5]; - if (salt != null) - { - chainValue[4] ^= Pack.littleEndianToLong(salt, 0); - chainValue[5] ^= Pack.littleEndianToLong(salt, 8); - } - - chainValue[6] = blake2b_IV[6]; - chainValue[7] = blake2b_IV[7]; - if (personalization != null) - { - chainValue[6] ^= Pack.littleEndianToLong(personalization, 0); - chainValue[7] ^= Pack.littleEndianToLong(personalization, 8); - } - } - } - - private void initializeInternalState() - { - // initialize v: - System.arraycopy(chainValue, 0, internalState, 0, chainValue.length); - System.arraycopy(blake2b_IV, 0, internalState, chainValue.length, 4); - internalState[12] = t0 ^ blake2b_IV[4]; - internalState[13] = t1 ^ blake2b_IV[5]; - internalState[14] = f0 ^ blake2b_IV[6]; - internalState[15] = blake2b_IV[7];// ^ f1 with f1 = 0 - } - - /** - * update the message digest with a single byte. - * - * @param b the input byte to be entered. - */ - public void update(byte b) - { - int remainingLength = 0; // left bytes of buffer - - // process the buffer if full else add to buffer: - remainingLength = BLOCK_LENGTH_BYTES - bufferPos; - if (remainingLength == 0) - { // full buffer - t0 += BLOCK_LENGTH_BYTES; - if (t0 == 0) - { // if message > 2^64 - t1++; - } - compress(buffer, 0); - Arrays.fill(buffer, (byte)0);// clear buffer - buffer[0] = b; - bufferPos = 1; - } - else - { - buffer[bufferPos] = b; - bufferPos++; - return; - } - } - - /** - * update the message digest with a block of bytes. - * - * @param message the byte array containing the data. - * @param offset the offset into the byte array where the data starts. - * @param len the length of the data. - */ - public void update(byte[] message, int offset, int len) - { - - if (message == null || len == 0) - { - return; - } - - int remainingLength = 0; // left bytes of buffer - - if (bufferPos != 0) - { // commenced, incomplete buffer - - // complete the buffer: - remainingLength = BLOCK_LENGTH_BYTES - bufferPos; - if (remainingLength < len) - { // full buffer + at least 1 byte - System.arraycopy(message, offset, buffer, bufferPos, - remainingLength); - t0 += BLOCK_LENGTH_BYTES; - if (t0 == 0) - { // if message > 2^64 - t1++; - } - compress(buffer, 0); - bufferPos = 0; - Arrays.fill(buffer, (byte)0);// clear buffer - } - else - { - System.arraycopy(message, offset, buffer, bufferPos, len); - bufferPos += len; - return; - } - } - - // process blocks except last block (also if last block is full) - int messagePos; - int blockWiseLastPos = offset + len - BLOCK_LENGTH_BYTES; - for (messagePos = offset + remainingLength; messagePos < blockWiseLastPos; messagePos += BLOCK_LENGTH_BYTES) - { // block wise 128 bytes - // without buffer: - t0 += BLOCK_LENGTH_BYTES; - if (t0 == 0) - { - t1++; - } - compress(message, messagePos); - } - - // fill the buffer with left bytes, this might be a full block - System.arraycopy(message, messagePos, buffer, 0, offset + len - - messagePos); - bufferPos += offset + len - messagePos; - } - - /** - * close the digest, producing the final digest value. The doFinal - * call leaves the digest reset. - * Key, salt and personal string remain. - * - * @param out the array the digest is to be copied into. - * @param outOffset the offset into the out array the digest is to start at. - */ - public int doFinal(byte[] out, int outOffset) - { - - f0 = 0xFFFFFFFFFFFFFFFFL; - t0 += bufferPos; - if (bufferPos > 0 && t0 == 0) - { - t1++; - } - compress(buffer, 0); - Arrays.fill(buffer, (byte)0);// Holds eventually the key if input is null - Arrays.fill(internalState, 0L); - - for (int i = 0; i < chainValue.length && (i * 8 < digestLength); i++) - { - byte[] bytes = Pack.longToLittleEndian(chainValue[i]); - - if (i * 8 < digestLength - 8) - { - System.arraycopy(bytes, 0, out, outOffset + i * 8, 8); - } - else - { - System.arraycopy(bytes, 0, out, outOffset + i * 8, digestLength - (i * 8)); - } - } - - Arrays.fill(chainValue, 0L); - - reset(); - - return digestLength; - } - - /** - * Reset the digest back to it's initial state. - * The key, the salt and the personal string will - * remain for further computations. - */ - public void reset() - { - bufferPos = 0; - f0 = 0L; - t0 = 0L; - t1 = 0L; - chainValue = null; - Arrays.fill(buffer, (byte)0); - if (key != null) - { - System.arraycopy(key, 0, buffer, 0, key.length); - bufferPos = BLOCK_LENGTH_BYTES; // zero padding - } - init(); - } - - private void compress(byte[] message, int messagePos) - { - - initializeInternalState(); - - long[] m = new long[16]; - for (int j = 0; j < 16; j++) - { - m[j] = Pack.littleEndianToLong(message, messagePos + j * 8); - } - - for (int round = 0; round < ROUNDS; round++) - { - - // G apply to columns of internalState:m[blake2b_sigma[round][2 * - // blockPos]] /+1 - G(m[blake2b_sigma[round][0]], m[blake2b_sigma[round][1]], 0, 4, 8, 12); - G(m[blake2b_sigma[round][2]], m[blake2b_sigma[round][3]], 1, 5, 9, 13); - G(m[blake2b_sigma[round][4]], m[blake2b_sigma[round][5]], 2, 6, 10, 14); - G(m[blake2b_sigma[round][6]], m[blake2b_sigma[round][7]], 3, 7, 11, 15); - // G apply to diagonals of internalState: - G(m[blake2b_sigma[round][8]], m[blake2b_sigma[round][9]], 0, 5, 10, 15); - G(m[blake2b_sigma[round][10]], m[blake2b_sigma[round][11]], 1, 6, 11, 12); - G(m[blake2b_sigma[round][12]], m[blake2b_sigma[round][13]], 2, 7, 8, 13); - G(m[blake2b_sigma[round][14]], m[blake2b_sigma[round][15]], 3, 4, 9, 14); - } - - // update chain values: - for (int offset = 0; offset < chainValue.length; offset++) - { - chainValue[offset] = chainValue[offset] ^ internalState[offset] ^ internalState[offset + 8]; - } - } - - private void G(long m1, long m2, int posA, int posB, int posC, int posD) - { - - internalState[posA] = internalState[posA] + internalState[posB] + m1; - internalState[posD] = Longs.rotateRight(internalState[posD] ^ internalState[posA], 32); - internalState[posC] = internalState[posC] + internalState[posD]; - internalState[posB] = Longs.rotateRight(internalState[posB] ^ internalState[posC], 24); // replaces 25 of BLAKE - internalState[posA] = internalState[posA] + internalState[posB] + m2; - internalState[posD] = Longs.rotateRight(internalState[posD] ^ internalState[posA], 16); - internalState[posC] = internalState[posC] + internalState[posD]; - internalState[posB] = Longs.rotateRight(internalState[posB] ^ internalState[posC], 63); // replaces 11 of BLAKE - } - - /** - * return the algorithm name - * - * @return the algorithm name - */ - public String getAlgorithmName() - { - return "BLAKE2b"; - } - - /** - * return the size, in bytes, of the digest produced by this message digest. - * - * @return the size, in bytes, of the digest produced by this message digest. - */ - public int getDigestSize() - { - return digestLength; - } - - /** - * Return the size in bytes of the internal buffer the digest applies it's compression - * function to. - * - * @return byte length of the digests internal buffer. - */ - public int getByteLength() - { - return BLOCK_LENGTH_BYTES; - } - - /** - * Overwrite the key - * if it is no longer used (zeroization) - */ - public void clearKey() - { - if (key != null) - { - Arrays.fill(key, (byte)0); - Arrays.fill(buffer, (byte)0); - } - } - - /** - * Overwrite the salt (pepper) if it - * is secret and no longer used (zeroization) - */ - public void clearSalt() - { - if (salt != null) - { - Arrays.fill(salt, (byte)0); - } - } -} diff --git a/canton/community/lib/magnolify/src/main/scala/magnolify/scalacheck/shrink/DerivedShrink.scala b/canton/community/lib/magnolify/src/main/scala/magnolify/scalacheck/shrink/DerivedShrink.scala deleted file mode 100644 index 220b6d6f0e..0000000000 --- a/canton/community/lib/magnolify/src/main/scala/magnolify/scalacheck/shrink/DerivedShrink.scala +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. -// Proprietary code. All rights reserved. - -package magnolify.scalacheck.shrink - -import org.scalacheck.Shrink -import org.scalacheck.util.Buildable - -import scala.annotation.nowarn -import scala.concurrent.duration.{Duration, FiniteDuration} - -/** A copy of [[org.scalacheck.Shrink]] so that we can get rid of the - * [[org.scalacheck.Shrink.shrinkAny]] implicit that would be picked up by the derivation macro. - * Unfortunately, there does not seem to be any way to prevent the compiler from picking up this - * implicit from the companion object. Even introducing a another copy that causes an ambiguity - * does not seem to work. - */ -trait DerivedShrink[A] { - def shrink: Shrink[A] -} - -@nowarn("cat=deprecation") -object DerivedShrink { - def apply[A](implicit ev: DerivedShrink[A]): DerivedShrink[A] = ev - - def of[A](s: Shrink[A]): DerivedShrink[A] = new DerivedShrink[A] { - override def shrink: Shrink[A] = s - } - - def from[A](f: A => Stream[A]): DerivedShrink[A] = of(Shrink(f)) - - // Copies of the pre-defined shrink instances - // No need to copy over the implicits for Tuples and Either as they can be auto-derived. - - implicit def shrinkContainer[C[_], T](implicit - v: C[T] => Traversable[T], - s: DerivedShrink[T], - b: Buildable[T, C[T]], - ): DerivedShrink[C[T]] = of(Shrink.shrinkContainer[C, T](v, s.shrink, b)) - - implicit def shrinkContainer2[C[_, _], T, U](implicit - v: C[T, U] => Traversable[(T, U)], - s: DerivedShrink[(T, U)], - b: Buildable[(T, U), C[T, U]], - ): DerivedShrink[C[T, U]] = of(Shrink.shrinkContainer2[C, T, U](v, s.shrink, b)) - - implicit def shrinkFractional[T: Fractional]: DerivedShrink[T] = - of(Shrink.shrinkFractional[T]) - - implicit def shrinkIntegral[T: Integral]: DerivedShrink[T] = - of(Shrink.shrinkIntegral[T]) - - implicit lazy val shrinkString: DerivedShrink[String] = of(Shrink.shrinkString) - - // Not equivalent to the auto-generated one because Some can be shrunk to None - implicit def shrinkOption[T: DerivedShrink]: DerivedShrink[Option[T]] = - of(Shrink.shrinkOption[T](DerivedShrink[T].shrink)) - - implicit val shrinkFiniteDuration: DerivedShrink[FiniteDuration] = - of(Shrink.shrinkFiniteDuration) - - implicit val shrinkDuration: DerivedShrink[Duration] = of(Shrink.shrinkDuration) -} diff --git a/canton/community/lib/magnolify/src/main/scala/magnolify/scalacheck/shrink/package.scala b/canton/community/lib/magnolify/src/main/scala/magnolify/scalacheck/shrink/package.scala deleted file mode 100644 index 98e3ec755c..0000000000 --- a/canton/community/lib/magnolify/src/main/scala/magnolify/scalacheck/shrink/package.scala +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. -// Proprietary code. All rights reserved. - -package magnolify.scalacheck - -import magnolia1.{CaseClass, Magnolia, SealedTrait} -import org.scalacheck.Shrink - -import scala.annotation.nowarn -import scala.language.experimental.macros -import scala.reflect.macros.whitebox - -package object shrink { - object semiauto { - - /** Semi-automatic derivation of [[DerivedShrink]] instances for case classes and sealed traits - * thereof. - */ - @nowarn("cat=deprecation") - object DerivedShrinkDerivation { - type Typeclass[T] = DerivedShrink[T] - - def join[T](caseClass: CaseClass[Typeclass, T]): Typeclass[T] = DerivedShrink.from { x => - // Shrink each parameter individually rather than looking at all combinations - // similar to how `Shrink.shrinkTuple*` works. This makes sense because if a shrunk value - // is a witness to the property violation, the shrinking algorithm will try to shrink this value again. - caseClass.parameters.toStream.flatMap { param => - param.typeclass.shrink - .shrink(param.dereference(x)) - .map { shrunkParamVal => - caseClass.construct { p => - if (p == param) shrunkParamVal else p.dereference(x) - } - } - } - } - - def split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] = DerivedShrink.from { x => - sealedTrait.split(x)(subtype => subtype.typeclass.shrink.shrink(subtype.cast(x))) - } - - implicit def apply[T]: Typeclass[T] = macro Magnolia.gen[T] - } - - /** Semi-automatic derivation of [[org.scalacheck.Shrink]] instances for case classes and sealed - * traits thereof. Derivation goes via [[magnolify.scalacheck.shrink.DerivedShrink]] so that - * derivation does not fall back to the unshrinkable [[org.scalacheck.Shrink.shrinkAny]] - * default. This means that implicits for [[DerivedShrink]] must be in scope for all - * non-derived data types, even if the derivation is for [[org.scalacheck.Shrink]]. - */ - object ShrinkDerivation { - def genShrinkMacro[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = { - import c.universe.* - val wtt = weakTypeTag[T] - q"""_root_.magnolify.scalacheck.shrink.semiauto.DerivedShrinkDerivation.apply[$wtt].shrink""" - } - - def apply[T]: Shrink[T] = macro genShrinkMacro[T] - } - } - - /** Automatic derivation of [[DerivedShrink]] and [[org.scalacheck.Shrink]] instances for case - * classes and sealed traits thereof. - */ - object auto { - - implicit def genShrink[T]: Shrink[T] = macro semiauto.ShrinkDerivation.genShrinkMacro[T] - - def genDeriveShrinkMacro[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = { - import c.universe.* - val wtt = weakTypeTag[T] - q"""_root_.magnolify.scalacheck.shrink.semiauto.DerivedShrinkDerivation.apply[$wtt]""" - } - - /** This implicit must be in scope for fully automatic derivation so that the compiler picks it - * up when asked to derive instances for argument types. - */ - implicit def genDerivedShrink[T]: DerivedShrink[T] = macro genDeriveShrinkMacro[T] - } -} diff --git a/canton/community/lib/magnolify/src/test/scala/magnolify/scalacheck/shrink/ShrinkDerivationTest.scala b/canton/community/lib/magnolify/src/test/scala/magnolify/scalacheck/shrink/ShrinkDerivationTest.scala deleted file mode 100644 index c9572ea441..0000000000 --- a/canton/community/lib/magnolify/src/test/scala/magnolify/scalacheck/shrink/ShrinkDerivationTest.scala +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. -// Proprietary code. All rights reserved. - -package magnolify.scalacheck.shrink - -import org.scalacheck.Shrink -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import scala.annotation.nowarn - -class ShrinkDerivationTest extends AnyWordSpec with Matchers { - import ShrinkDerivationTest.* - - "ShrinkDerivation" should { - "derive a Shrink instance for a case class" in { - import magnolify.scalacheck.shrink.semiauto.* - - val shrinkFoo = ShrinkDerivation[Foo] - val shrunkFoos = shrinkFoo.shrink(Foo(4, -2)) - // Should behave exactly like the existing shrinks for tuples - val expected = Shrink - .shrinkTuple2[Int, Int](Shrink.shrinkIntegral[Int], Shrink.shrinkIntegral[Int]) - .shrink((4, -2)) - .map((Foo.apply _).tupled) - - shrunkFoos shouldBe expected - } - - "auto-derive a Shrink for a recursive case class" in { - // This test works only with fully automatic derivation because - // because it checks that we pick up the custom DerivedShrink instance for Option. - // With semi-automatic derivation, magnolia generates its own DerivedShrink instance - // for Option through which the recursion goes through. - // The latter does not shrink Some to None though. - import magnolify.scalacheck.shrink.auto.* - - val shrinkRec = implicitly[Shrink[Rec]] - val shrunkRecs = shrinkRec.shrink(Rec(4, Some(Rec(1, None)))) - - @nowarn("msg=dead code following this construct") - val expected = Shrink - .shrinkTuple2[Int, Option[(Int, Option[Nothing])]]( - // Let's be very explicit about the Shrink implicits as auto-derivation is in scope - Shrink.shrinkIntegral, - Shrink.shrinkOption( - Shrink.shrinkTuple2[Int, Option[Nothing]]( - Shrink.shrinkIntegral, - Shrink.shrinkOption[Nothing](Shrink.shrinkAny[Nothing]), - ) - ), - ) - .shrink((4, Some((1, None)))) - .map { - case (a, None) => Rec(a, None) - case (a, Some((b, None))) => Rec(a, Some(Rec(b, None))) - case (_, Some((_, Some(nothing)))) => nothing - } - - shrunkRecs shouldBe expected - } - - "semiauto-derive a Shrink for a recursive case class" in { - // This test uses semi-automatic derivation - // and therefore generates its own DerivedShrink instance for Option following the sealed-trait construction. - // Accordingly, Option behaves like an Either[Unit, *]. - import magnolify.scalacheck.shrink.semiauto.* - - val shrinkRec = ShrinkDerivation[Rec] - val shrunkRecs = shrinkRec.shrink(Rec(4, Some(Rec(1, None)))) - - @nowarn("msg=dead code following this construct") - val expected = Shrink - .shrinkTuple2[Int, Either[Unit, (Int, Option[Nothing])]]( - Shrink.shrinkIntegral, - Shrink.shrinkEither( - Shrink.shrinkAny[Unit], - Shrink.shrinkTuple2[Int, Option[Nothing]]( - Shrink.shrinkIntegral, - Shrink.shrinkOption[Nothing](Shrink.shrinkAny[Nothing]), - ), - ), - ) - .shrink((4, Right((1, None)))) - .map { - case (a, Left(_)) => Rec(a, None) - case (a, Right((b, None))) => Rec(a, Some(Rec(b, None))) - case (_, Right((_, Some(nothing)))) => nothing - } - - shrunkRecs shouldBe expected - - } - - "derive a Shrink for nested case classes" in { - import magnolify.scalacheck.shrink.auto.* - - val nested = Nested(Foo(4, 0), Rec(-2, None)) - val shrunkNested = Shrink.shrink(nested) - - val expected = Shrink - .shrinkTuple2[(Int, Int), Int]( - Shrink.shrinkTuple2(Shrink.shrinkIntegral, Shrink.shrinkIntegral), - Shrink.shrinkIntegral, - ) - .shrink(((4, 0), -2)) - .map { case ((a, b), c) => Nested(Foo(a, b), Rec(c, None)) } - - shrunkNested shouldBe expected - } - - } -} - -object ShrinkDerivationTest { - private final case class Foo(a: Int, b: Int) - private final case class Rec(a: Int, rec: Option[Rec]) - private final case class Nested(a: Foo, b: Rec) -} diff --git a/canton/community/lib/scalatest/src/main/scala/org/scalatest/AssertionsUtil.scala b/canton/community/lib/scalatest/src/main/scala/org/scalatest/AssertionsUtil.scala deleted file mode 100644 index 0f49ed546c..0000000000 --- a/canton/community/lib/scalatest/src/main/scala/org/scalatest/AssertionsUtil.scala +++ /dev/null @@ -1,19 +0,0 @@ -package org.scalatest - -import org.scalactic.source - -import scala.language.experimental.macros - -trait AssertionsUtil extends Assertions { - - /** Generalizes [[Assertions.assertTypeError]] in that the type error message can be inspected. - * - * [[Assertions.assertTypeError]](code) is equivalent to [[assertOnTypeError]](code)(_ => - * succeed). - */ - def assertOnTypeError(code: String)(assertion: String => Assertion)(implicit - pos: source.Position - ): Assertion = macro AssertionsUtilMacros.assertOnTypeErrorImpl -} - -object AssertionsUtil extends AssertionsUtil diff --git a/canton/community/lib/scalatest/src/main/scala/org/scalatest/AssertionsUtilMacros.scala b/canton/community/lib/scalatest/src/main/scala/org/scalatest/AssertionsUtilMacros.scala deleted file mode 100644 index 930b23758d..0000000000 --- a/canton/community/lib/scalatest/src/main/scala/org/scalatest/AssertionsUtilMacros.scala +++ /dev/null @@ -1,71 +0,0 @@ -package org.scalatest - -import org.scalactic.source -import org.scalatest.CompileMacro.{containsAnyValNullStatement, getCodeStringFromCodeExpression} -import org.scalatest.exceptions.{StackDepthException, TestFailedException} - -import scala.annotation.nowarn -import scala.reflect.macros.{Context, ParseException, TypecheckException} - -object AssertionsUtilMacros { - // Generalized version of org.scalatest.CompileMacro.assertTypeErrorImpl. Modifications are marked with MODIFIED - @nowarn("msg=dead code following this construct") - @nowarn("cat=deprecation") - @nowarn("msg=unused value of type") - def assertOnTypeErrorImpl(c: Context)(code: c.Expr[String])( - // MODIFIED: This parameter is new - assertion: c.Expr[String => Assertion] - )( - pos: c.Expr[source.Position] - ): c.Expr[Assertion] = { - import c.universe.* - - // extract code snippet - val codeStr = getCodeStringFromCodeExpression(c)( - // MODIFIED: Using the appropriate name here - "assertOnTypeError", - code, - ) - - try { - val tree = c.parse("{ " + codeStr + " }") - if (!containsAnyValNullStatement(c)(List(tree))) { - c.typecheck(tree) // parse and type check code snippet - // If reach here, type check passes, let's generate code to throw TestFailedException - val messageExpr = c.literal(Resources.expectedTypeErrorButGotNone(codeStr)) - reify { - throw new TestFailedException( - (_: StackDepthException) => Some(messageExpr.splice), - None, - pos.splice, - ) - } - } else { - - reify { - // statement such as val i: Int = null, compile fails as expected, generate code to return Succeeded - Succeeded - } - } - } catch { - case e: TypecheckException => - // MODIFIED: Evaluate the given assertion on the exception's message instead of always succeeding - val errorMessage = c.literal(e.msg) - reify { - assertion.splice(errorMessage.splice) - } - case e: ParseException => - // parse error, generate code to throw TestFailedException - val messageExpr = - c.literal(Resources.expectedTypeErrorButGotParseError(e.getMessage, codeStr)) - reify { - throw new TestFailedException( - (_: StackDepthException) => Some(messageExpr.splice), - None, - pos.splice, - ) - } - } - } - -} diff --git a/canton/community/lib/scalatest/src/test/scala/org/scalatest/AssertionsUtilTest.scala b/canton/community/lib/scalatest/src/test/scala/org/scalatest/AssertionsUtilTest.scala deleted file mode 100644 index 7a7443e9d2..0000000000 --- a/canton/community/lib/scalatest/src/test/scala/org/scalatest/AssertionsUtilTest.scala +++ /dev/null @@ -1,36 +0,0 @@ -package org.scalatest - -import org.scalatest.exceptions.TestFailedException -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import scala.util.Try - -class AssertionsUtilTest extends AnyWordSpec with Matchers with EitherValues { - - "AssertionsUtil.assertOnTypeError" should { - "check for type errors" in { - AssertionsUtil.assertOnTypeError("val i: String = 5") { typeError => - typeError should (include("type mismatch") and include("5") and include("java.lang.String")) - } - - AssertionsUtil.assertOnTypeError("implicitly[Int =:= String]") { typeError => - typeError should include("Cannot prove that Int =:= String") - } - - Try( - AssertionsUtil.assertOnTypeError("val i: Int = 5")(_ => succeed) - ).toEither.swap.value shouldBe a[TestFailedException] - - Try(AssertionsUtil.assertOnTypeError("val i: String = 5") { typeError => - typeError should include("foobar") - }).toEither.swap.value shouldBe a[TestFailedException] - } - - "fail on parse errors" in { - val err = - Try(AssertionsUtil.assertOnTypeError("val i = 5)")(_ => succeed)).toEither.swap.value - err.getMessage should include("Expected a type error, but got the following parse error:") - } - } -} diff --git a/canton/community/lib/slick/LICENSE.txt b/canton/community/lib/slick/LICENSE.txt deleted file mode 100644 index 0fb1082e73..0000000000 --- a/canton/community/lib/slick/LICENSE.txt +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2011-2021 Lightbend, Inc. - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/canton/community/lib/slick/src/main/scala/slick/jdbc/canton/StaticQuery.scala b/canton/community/lib/slick/src/main/scala/slick/jdbc/canton/StaticQuery.scala deleted file mode 100644 index e78c8b850b..0000000000 --- a/canton/community/lib/slick/src/main/scala/slick/jdbc/canton/StaticQuery.scala +++ /dev/null @@ -1,133 +0,0 @@ -package slick.jdbc.canton - -import slick.dbio.{Effect, NoStream} -import slick.jdbc.{ - GetResult, - Invoker, - PositionedParameters, - PositionedResult, - SetParameter, - StatementInvoker, - StreamingInvokerAction, -} -import slick.sql.SqlStreamingAction - -import java.sql.PreparedStatement -import scala.collection.mutable -import scala.collection.mutable.ArrayBuffer -import scala.language.implicitConversions - -/** Fork of slick's sql interpolation. - * - * `sqlu` now returns an action indicating a write effect. - * `sql` now returns a forked SQL action builder where `as` results in an action with read effect and - * `asUpdate` in one with a write effect. - */ -class ActionBasedSQLInterpolation(val s: StringContext) extends AnyVal { - - /** Build a SQLActionBuilder via string interpolation */ - def sql(params: TypedParameter[?]*): SQLActionBuilder = SQLActionBuilder.parse(s.parts, params) - - /** Build an Action for an UPDATE statement via string interpolation */ - def sqlu( - params: TypedParameter[?]* - ): SqlStreamingAction[Vector[Int], Int, Effect.Write]#ResultAction[Int, NoStream, Effect.Write] = - sql(params *).asUpdate -} - -object ActionBasedSQLInterpolation { - - object Implicits { - implicit def actionBasedSQLInterpolationCanton(s: StringContext): ActionBasedSQLInterpolation = - new ActionBasedSQLInterpolation(s) - } -} - -class TypedParameter[T](val param: T, val setParameter: SetParameter[T]) { - def applied: SetParameter[Unit] = setParameter.applied(param) -} - -object TypedParameter { - implicit def typedParameter[T](param: T)(implicit - setParameter: SetParameter[T] - ): TypedParameter[T] = - new TypedParameter[T](param, setParameter) -} - -object SQLActionBuilder { - def parse(strings: Seq[String], typedParams: Seq[TypedParameter[?]]): SQLActionBuilder = - if (strings.sizeIs == 1) - SQLActionBuilder(strings.head, SetParameter.SetUnit) - else { - val b = new mutable.StringBuilder - val remaining = new ArrayBuffer[SetParameter[Unit]] - typedParams.zip(strings.iterator.to(Iterable)).foreach { zipped => - val p = zipped._1.param - var literal = false - - def decode(s: String): String = - if (s.endsWith("##")) decode(s.substring(0, s.length - 2)) + "#" - else if (s.endsWith("#")) { - literal = true - s.substring(0, s.length - 1) - } else - s - - b.append(decode(zipped._2)) - if (literal) b.append(p.toString) - else { - b.append('?') - remaining += zipped._1.applied - } - } - b.append(strings.last) - SQLActionBuilder(b.toString, (u, pp) => remaining.foreach(_(u, pp))) - } -} - -case class SQLActionBuilder(sql: String, setParameter: SetParameter[Unit]) { - - private def asInternal[R, E <: Effect](implicit - getResult: GetResult[R] - ): SqlStreamingAction[Vector[R], R, E] = - new StreamingInvokerAction[Vector[R], R, Effect] { - def statements: Iterable[String] = List(sql) - - protected[this] def createInvoker(statements: Iterable[String]): Invoker[R] = - new StatementInvoker[R] { - val getStatement = statements.head - - protected def setParam(st: PreparedStatement): Unit = - setParameter((), new PositionedParameters(st)) - - protected def extractValue(rs: PositionedResult): R = getResult(rs) - } - - protected[this] def createBuilder: collection.mutable.Builder[R, Vector[R]] = - Vector.newBuilder[R] - } - - def as[R](implicit getResult: GetResult[R]): SqlStreamingAction[Vector[R], R, Effect.Read] = - asInternal[R, Effect.Read] - - def asUpdate: SqlStreamingAction[Vector[Int], Int, Effect.Write]#ResultAction[ - Int, - NoStream, - Effect.Write, - ] = - asInternal[Int, Effect.Write](GetResult.GetUpdateValue).head - - def asUpdateReturning[R](implicit - rconv: GetResult[R] - ): SqlStreamingAction[Vector[R], R, Effect.Read & Effect.Write] = - asInternal[R, Effect.Read & Effect.Write] - - def concat(b: SQLActionBuilder): SQLActionBuilder = - SQLActionBuilder( - sql + b.sql, - (p, pp) => { - setParameter(p, pp) - b.setParameter(p, pp) - }, - ) -} diff --git a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/AllowTraverseSingleContainer.scala b/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/AllowTraverseSingleContainer.scala deleted file mode 100644 index ec3d4783b5..0000000000 --- a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/AllowTraverseSingleContainer.scala +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton - -import scala.annotation.StaticAnnotation - -final class AllowTraverseSingleContainer extends StaticAnnotation diff --git a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotDiscardLikeFuture.scala b/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotDiscardLikeFuture.scala deleted file mode 100644 index 5a9e4cf8a8..0000000000 --- a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotDiscardLikeFuture.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton - -import scala.annotation.StaticAnnotation - -/** Annotated type constructors will be treated like a [[scala.concurrent.Future]] when looking for - * discarded futures. - */ -final class DoNotDiscardLikeFuture extends StaticAnnotation diff --git a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotReturnFromSynchronizedLikeFuture.scala b/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotReturnFromSynchronizedLikeFuture.scala deleted file mode 100644 index 7b5e9b0675..0000000000 --- a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotReturnFromSynchronizedLikeFuture.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton - -import scala.annotation.StaticAnnotation - -/** Annotated type constructors will be treated like a [[scala.concurrent.Future]] when looking at - * the return types of synchronized blocks. - */ -final class DoNotReturnFromSynchronizedLikeFuture extends StaticAnnotation diff --git a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotTraverseLikeFuture.scala b/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotTraverseLikeFuture.scala deleted file mode 100644 index dec4239396..0000000000 --- a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/DoNotTraverseLikeFuture.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton - -import scala.annotation.StaticAnnotation - -/** Annotated type constructors will be treated like a [[scala.concurrent.Future]] when looking for - * traverse-like calls with such an applicative instance. - */ -final class DoNotTraverseLikeFuture extends StaticAnnotation diff --git a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/FutureTransformer.scala b/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/FutureTransformer.scala deleted file mode 100644 index 9e13e9aaee..0000000000 --- a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/FutureTransformer.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton - -import scala.annotation.StaticAnnotation - -/** Annotation for computation transformer type constructors (e.g., a monad transformer) so that if - * it will be treated future-like when applied to a future-like computation type. - * - * @param transformedTypeArgumentPosition - * The type argument position for the computation type that is transformed - */ -final case class FutureTransformer(transformedTypeArgumentPosition: Int) extends StaticAnnotation diff --git a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/GrpcServiceInvocationMethod.scala b/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/GrpcServiceInvocationMethod.scala deleted file mode 100644 index 2d6f6803d2..0000000000 --- a/canton/community/lib/wartremover-annotations/src/main/scala/com/digitalasset/canton/GrpcServiceInvocationMethod.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton - -import scala.annotation.StaticAnnotation - -/** Annotation for methods and constructors. Implementations of such method (and any overrides) are - * not checked. Neither are the arguments to calls of such a method. - */ -final class GrpcServiceInvocationMethod extends StaticAnnotation diff --git a/canton/community/microbench/src/main/scala/com/digitalasset/canton/AcsCommitmentBenchmark.scala b/canton/community/microbench/src/main/scala/com/digitalasset/canton/AcsCommitmentBenchmark.scala index 2fd6d53899..0cb8f63565 100644 --- a/canton/community/microbench/src/main/scala/com/digitalasset/canton/AcsCommitmentBenchmark.scala +++ b/canton/community/microbench/src/main/scala/com/digitalasset/canton/AcsCommitmentBenchmark.scala @@ -12,14 +12,7 @@ import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} import com.digitalasset.canton.BaseTest.* import com.digitalasset.canton.concurrent.{FutureSupervisor, Threading} import com.digitalasset.canton.config.RequireTypes.{NonNegativeProportion, PositiveInt} -import com.digitalasset.canton.config.{ - BatchingConfig, - CommitmentSendDelay, - DefaultProcessingTimeouts, - NonNegativeDuration, - PositiveDurationSeconds, - TestingConfigInternal, -} +import com.digitalasset.canton.config.{BatchingConfig, CommitmentSendDelay, DefaultProcessingTimeouts, NonNegativeDuration, PositiveDurationSeconds, TestingConfigInternal} import com.digitalasset.canton.crypto.provider.symbolic.SymbolicCrypto import com.digitalasset.canton.crypto.{LtHash16, SyncCryptoClient, SynchronizerSnapshotSyncCryptoApi} import com.digitalasset.canton.data.{CantonTimestamp, CantonTimestampSecond} @@ -34,6 +27,7 @@ import com.digitalasset.canton.participant.util.TimeOfChange import com.digitalasset.canton.platform.store.interning.MockStringInterning import com.digitalasset.canton.protocol.* import com.digitalasset.canton.protocol.messages.{AcsCommitment, CommitmentPeriod, DefaultOpenEnvelope, SignedProtocolMessage} +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.client.{SendCallback, SequencerClientSend} import com.digitalasset.canton.sequencing.protocol.{AggregationRule, Batch, MessageId, OpenEnvelope, Recipients} @@ -393,6 +387,7 @@ class AcsCommitmentBenchmark any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], any[Boolean], )(any[TraceContext], any[MetricsContext]) ).thenReturn(EitherT.pure(())): Unit diff --git a/canton/community/mock-kms-driver/src/main/scala/com/digitalasset/canton/crypto/kms/mock/v1/MockKmsDriverFactory.scala b/canton/community/mock-kms-driver/src/main/scala/com/digitalasset/canton/crypto/kms/mock/v1/MockKmsDriverFactory.scala index a0e6292f7d..53133bd476 100644 --- a/canton/community/mock-kms-driver/src/main/scala/com/digitalasset/canton/crypto/kms/mock/v1/MockKmsDriverFactory.scala +++ b/canton/community/mock-kms-driver/src/main/scala/com/digitalasset/canton/crypto/kms/mock/v1/MockKmsDriverFactory.scala @@ -4,6 +4,8 @@ package com.digitalasset.canton.crypto.kms.mock.v1 import cats.syntax.either.* +import com.daml.metrics.api.noop.NoOpMetricsFactory +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} import com.daml.nonempty.NonEmpty import com.digitalasset.canton.buildinfo.BuildInfo import com.digitalasset.canton.config @@ -24,6 +26,14 @@ import com.digitalasset.canton.crypto.store.memory.{ } import com.digitalasset.canton.crypto.{CryptoSchemes, KeyName} import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.metrics.{ + CryptoMetrics, + DecryptionHistograms, + DecryptionMetrics, + KmsMetrics, + SigningHistograms, + SigningMetrics, +} import com.digitalasset.canton.version.ReleaseProtocolVersion import org.slf4j.Logger import pureconfig.configurable.{genericMapReader, genericMapWriter} @@ -103,6 +113,22 @@ class MockKmsDriverFactory extends KmsDriverFactory { CachingConfigs.defaultPublicKeyConversionCache, cryptoPrivateStore, cryptoPublicStore, + new CryptoMetrics( + new SigningMetrics( + new SigningHistograms(MetricName("signing-test"))(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty), + new DecryptionMetrics( + new DecryptionHistograms(MetricName("decryption-test"))(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty), + Some( + new KmsMetrics( + MetricName("test"), + NoOpMetricsFactory, + )(MetricsContext.Empty) + ), + ), timeouts, namedLoggerFactory, ) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala index ac3cb2d8a3..3b512df9f8 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala @@ -62,6 +62,7 @@ import com.digitalasset.canton.participant.synchronizer.grpc.GrpcSynchronizerReg import com.digitalasset.canton.participant.topology.* import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker import com.digitalasset.canton.platform.apiserver.services.admin.PackageUpgradeValidator +import com.digitalasset.canton.platform.apiserver.services.command.TrafficEnforcementBackend import com.digitalasset.canton.platform.store.LedgerApiContractStoreImpl import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend import com.digitalasset.canton.protocol.StaticSynchronizerParameters @@ -251,6 +252,7 @@ class ParticipantNodeBootstrap( dryRunSnapshot.getOrElse(PackageMetadata()), forceFlags, disableUpgradeValidation = parameters.disableUpgradeValidation, + protocolVersion = authorizedStore.protocolVersion, ) override def checkCannotDisablePartyWithActiveContracts( @@ -400,7 +402,7 @@ class ParticipantNodeBootstrap( ips, crypto, cryptoConfig, - Some(arguments.metrics.kmsMetrics), + arguments.metrics.cryptoMetrics, parameters.cachingConfigs.publicKeyConversionCache, timeouts, futureSupervisor, @@ -595,6 +597,34 @@ class ParticipantNodeBootstrap( loggerFactory, ) + trafficEnforcementBackendContainerO = Option.when(config.trafficEnforcement.enabled)( + new LifeCycleContainer( + stateName = "traffic-enforcement-backend", + create = () => + FutureUnlessShutdown.pure( + TrafficEnforcementBackend( + trafficEnforcementServerConfig = + config.trafficEnforcement.trafficEnforcementServer, + processingTimeout = timeouts, + loggerFactory = loggerFactory, + ) + ), + loggerFactory = loggerFactory, + ) + ) + + _ <- trafficEnforcementBackendContainerO.traverseTap { trafficEnforcementBackendContainer => + // only initialize traffic enforcement backend if participant is becoming active + if (isActive) { + EitherT.right[String](trafficEnforcementBackendContainer.initializeNext()) + } else { + logger.info("Traffic enforcement backend is not initialized due to inactive state") + EitherT.rightT[FutureUnlessShutdown, String](()) + } + } + + trafficEnforcementBackendO = trafficEnforcementBackendContainerO.map(_.asEval) + synchronizerRegistry = new GrpcSynchronizerRegistry( participantId, syncPersistentStateManager, @@ -670,6 +700,7 @@ class ParticipantNodeBootstrap( chunkSize = purgeCfg.chunkSize, synchronizerConnectionConfigStore, syncPersistentStateManager, + parameters.batchingConfig, timeouts, loggerFactory, ) @@ -725,6 +756,7 @@ class ParticipantNodeBootstrap( ledgerApiIndexerContainer, connectedSynchronizersLookupContainer, () => triggerDeclarativeChange(), + trafficEnforcementBackendO, ) _ <- @@ -765,6 +797,7 @@ class ParticipantNodeBootstrap( participantId = participantId.toLf, participantNodePersistentState = persistentState, sync = sync, + trafficEnforcementBackendO = trafficEnforcementBackendO, pruningConfig = parameters.stores, tracerProvider = tracerProvider, updateServiceConfig = arguments.config.ledgerApi.updateService, @@ -886,6 +919,9 @@ class ParticipantNodeBootstrap( addCloseable(ledgerApiServerContainer.currentAutoCloseable()) addCloseable(ledgerApiDependentServices) addCloseable(mutablePackageMetadataView) + trafficEnforcementBackendContainerO.foreach(trafficEnforcementBackendContainer => + addCloseable(trafficEnforcementBackendContainer.currentAutoCloseable()) + ) // return values ParticipantServices( @@ -897,6 +933,7 @@ class ParticipantNodeBootstrap( ledgerApiServerContainer = ledgerApiServerContainer, startableStoppableLedgerApiDependentServices = ledgerApiDependentServices, participantTopologyDispatcher = topologyDispatcher, + trafficEnforcementBackendContainerO = trafficEnforcementBackendContainerO, ) } } @@ -983,6 +1020,8 @@ object ParticipantNodeBootstrap { persistentStateContainer: LifeCycleContainer[ParticipantNodePersistentState], mutablePackageMetadataView: MutablePackageMetadataViewImpl, ledgerApiIndexerContainer: LifeCycleContainer[LedgerApiIndexer], + // None if traffic enforcement is disabled + trafficEnforcementBackendContainerO: Option[LifeCycleContainer[TrafficEnforcementBackend]], cantonSyncService: CantonSyncService, schedulers: Schedulers, ledgerApiServerContainer: LifeCycleContainer[LedgerApiServer], diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNodeParameters.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNodeParameters.scala index 38a1d1eb79..8d2ec14673 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNodeParameters.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNodeParameters.scala @@ -41,7 +41,7 @@ final case class ParticipantNodeParameters( commitmentReduceParallelism: NonNegativeInt, commitmentUseDbSnapshotForParticipantLookup: Boolean, autoSyncProtocolFeatureFlags: Boolean, - alphaMultiSynchronizerSupport: Boolean, + enableAllLedgerApiReassignments: Boolean, commitAfterFailedActivenessCheck: Boolean, validateLegacyContractsV11: Boolean, ) extends CantonNodeParameters @@ -109,7 +109,7 @@ object ParticipantNodeParameters { commitmentReduceParallelism = NonNegativeInt.zero, commitmentUseDbSnapshotForParticipantLookup = false, autoSyncProtocolFeatureFlags = true, - alphaMultiSynchronizerSupport = false, + enableAllLedgerApiReassignments = false, commitAfterFailedActivenessCheck = false, validateLegacyContractsV11 = true, ) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/data/LateLsuRequest.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/data/LateLsuRequest.scala index 8200b548af..e719182615 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/data/LateLsuRequest.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/data/LateLsuRequest.scala @@ -101,9 +101,9 @@ object LateLsuRequest { ) _ <- Either.cond( - successorConfig.synchronizerId.forall(_ == successorPsid), + successorConfig.psid.forall(_ == successorPsid), (), - s"Config synchronizer ID (${successorConfig.synchronizerId}) does not match the requested successor ID ($successorPsid)", + s"Config synchronizer ID (${successorConfig.psid}) does not match the requested successor ID ($successorPsid)", ) } yield () } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/data/ManualLsuRequest.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/data/ManualLsuRequest.scala index 71f5bdc0b8..970e8ae5bd 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/data/ManualLsuRequest.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/data/ManualLsuRequest.scala @@ -113,9 +113,9 @@ object ManualLsuRequest { case _: SequencerSuccessors => ().asRight case NewConfig(config) => Either.cond( - config.synchronizerId.forall(_ == successorPsid), + config.psid.forall(_ == successorPsid), (), - s"successor_psid ($successorPsid) differs from the one in the new synchronizer config (${config.synchronizerId})", + s"successor_psid ($successorPsid) differs from the one in the new synchronizer config (${config.psid})", ) } } yield () diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcParticipantInspectionService.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcParticipantInspectionService.scala index 36b8a86e8f..2041f31911 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcParticipantInspectionService.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcParticipantInspectionService.scala @@ -417,12 +417,14 @@ class GrpcParticipantInspectionService( override def openCommitment( request: v30.OpenCommitmentRequest, responseObserver: StreamObserver[v30.OpenCommitmentResponse], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient( (out: OutputStream) => openCommitment(request, out), responseObserver, byteString => v30.OpenCommitmentResponse(byteString), ) + } private def openCommitment( request: v30.OpenCommitmentRequest, @@ -605,12 +607,14 @@ class GrpcParticipantInspectionService( override def inspectCommitmentContracts( request: v30.InspectCommitmentContractsRequest, responseObserver: StreamObserver[v30.InspectCommitmentContractsResponse], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient( (out: OutputStream) => inspectCommitmentContracts(request, out), responseObserver, byteString => v30.InspectCommitmentContractsResponse(byteString), ) + } private def inspectCommitmentContracts( request: v30.InspectCommitmentContractsRequest, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcSynchronizerConnectivityService.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcSynchronizerConnectivityService.scala index 8ade10072b..cd8e19b69c 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcSynchronizerConnectivityService.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcSynchronizerConnectivityService.scala @@ -420,7 +420,11 @@ class GrpcSynchronizerConnectivityService( storedConnectionConfig <- EitherT .fromEither[FutureUnlessShutdown]( - sync.getSynchronizerConnectionConfigForAlias(alias, onlyActive = true) + sync.getSynchronizerConnectionConfigForAlias( + alias, + onlyActive = true, + operation = "get synchronizer id", + ) ) .leftMap(_ => SyncServiceUnknownSynchronizer.Error(alias)) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspection.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspection.scala index f364076287..32b60235aa 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspection.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspection.scala @@ -3,13 +3,11 @@ package com.digitalasset.canton.participant.admin.inspection -import anorm.SqlStringInterpolation import cats.Eval import cats.data.{EitherT, OptionT} import cats.syntax.either.* import cats.syntax.functorFilter.* import cats.syntax.traverse.* -import com.daml.metrics.DatabaseMetrics import com.daml.nameof.NameOf.functionFullName import com.daml.nonempty.NonEmpty import com.digitalasset.canton.concurrent.FutureSupervisor @@ -25,7 +23,7 @@ import com.digitalasset.canton.data.{ import com.digitalasset.canton.ledger.participant.state.SynchronizerIndex import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, UnlessShutdown} import com.digitalasset.canton.logging.pretty.PrettyPrinting -import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcUSExtended import com.digitalasset.canton.participant.admin.inspection.SyncStateInspection.{ InFlightCount, @@ -909,19 +907,6 @@ final class SyncStateInspection( ) .onShutdown(throw new RuntimeException("onlyForTestingMoveLedgerEndBackToScratch")) - @VisibleForTesting - def deleteContract(internalContractId: Long)(implicit traceContext: TraceContext): Int = - timeouts.inspection.await(functionFullName) { - val removedContracts = - participantNodePersistentState.value.ledgerApiStore.ledgerApiDbSupport.dbDispatcher - .executeSql(DatabaseMetrics.ForTesting("deleteContract"))(conn => - SQL"DELETE FROM par_contracts WHERE internal_contract_id=$internalContractId" - .executeUpdate()(conn) - )(LoggingContextWithTrace.ForTesting) - participantNodePersistentState.value.contractStore.contractsPruned(List(internalContractId)) - removedContracts - } - @VisibleForTesting def internalContractIdOf( contractId: ContractId diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/party/PartyReplicationTopologyWorkflow.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/party/PartyReplicationTopologyWorkflow.scala index efe297d0e3..bcf1de1c49 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/party/PartyReplicationTopologyWorkflow.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/party/PartyReplicationTopologyWorkflow.scala @@ -293,6 +293,7 @@ class PartyReplicationTopologyWorkflow( // Don't specify signing keys to let the topology manager figure out the TP keys as it is complicated // for code outside the topology manager to determine the signing keys in general topologies. signingKeys = Seq.empty, + namespacesToSignFor = Seq(targetParticipantId.namespace), forceFlags = ForceFlags.none, ) .map { proposalSignedByTP => @@ -332,6 +333,7 @@ class PartyReplicationTopologyWorkflow( mapping = existingProposalO.map(_.mapping).getOrElse(ptpProposal), serial = Some(serial), signingKeys = Seq.empty, // Rely on topology manager to use the right TP signing keys + namespacesToSignFor = Seq(targetParticipantId.namespace), protocolVersion = topologyManager.managerVersion.serialization, expectFullAuthorization = false, forceChanges = ForceFlags.none, @@ -544,6 +546,7 @@ class PartyReplicationTopologyWorkflow( serial = Some(serial), signingKeys = Seq.empty, // Rely on topology manager to use the right TP signing keys + namespacesToSignFor = Seq(targetParticipantId.namespace), protocolVersion = topologyManager.managerVersion.serialization, expectFullAuthorization = true, // expect full authorization when onboarding is done diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/ChangeAssignation.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/ChangeAssignation.scala index 85edc4f83a..35e1f03f80 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/ChangeAssignation.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/ChangeAssignation.scala @@ -418,6 +418,7 @@ private final class ChangeAssignation( assignmentExclusivity = None, reassignmentCounter = reassign.counter.v, nodeId = idx, + keyOpt = reassign.contract.contractKeyWithMaintainers, ) }), repairCounter = sourceTor.repairCounter, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairService.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairService.scala index 10536d3a4c..56543f9c29 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairService.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairService.scala @@ -189,7 +189,7 @@ final class RepairService( )(implicit traceContext: TraceContext): Either[String, Unit] = { logger.info( s"Purging ${contractIds.length} contracts from $synchronizerAlias with ignoreAlreadyPurged=$ignoreAlreadyPurged. " + - s"Mode: ${if (parameters.alphaMultiSynchronizerSupport) "Alpha Multi Synchronizer (Unassignment)" + s"Mode: ${if (parameters.enableAllLedgerApiReassignments) "Alpha Multi Synchronizer (Unassignment)" else "Standard (Archive Transaction)"}" ) @@ -233,7 +233,7 @@ final class RepairService( toc = repair.tryExactlyOneTimeOfRepair.toToc _ <- - if (parameters.alphaMultiSynchronizerSupport) { + if (parameters.enableAllLedgerApiReassignments) { for { operationsE <- EitherT.fromEither[FutureUnlessShutdown]( contractIds @@ -732,6 +732,7 @@ final class RepairService( assignmentExclusivity = None, reassignmentCounter = reassignmentCounter.unwrap, nodeId = nodeId, + keyOpt = c.metadata.maybeKeyWithMaintainers, ) } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairServiceContractsImporter.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairServiceContractsImporter.scala index 25364c3b14..7f1980e73a 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairServiceContractsImporter.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairServiceContractsImporter.scala @@ -269,7 +269,7 @@ final class RepairServiceContractsImporter( } // Publish events to the indexer .mapAsync(1) { contractsToAddWithInternalContractIds => - if (nodeParameters.alphaMultiSynchronizerSupport) { + if (nodeParameters.enableAllLedgerApiReassignments) { publishAssignedEvents( synchronizerId, synchronizer.currentRecordTime, @@ -365,7 +365,7 @@ final class RepairServiceContractsImporter( .toEitherT[FutureUnlessShutdown] contractsWithUnexpectedReassignmentCounter = - if (nodeParameters.alphaMultiSynchronizerSupport) { + if (nodeParameters.enableAllLedgerApiReassignments) { Nil } else { contractsUnfiltered.filter(_.reassignmentCounter != ReassignmentCounter.Genesis) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/config/ParticipantInitConfig.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/config/ParticipantInitConfig.scala index ef6bb9ecbd..6972ddb16c 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/config/ParticipantInitConfig.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/config/ParticipantInitConfig.scala @@ -9,9 +9,6 @@ import com.digitalasset.canton.participant.config.ParticipantInitConfig.Particip /** Init configuration specific to participant nodes * @param ledgerApi * ledgerApi related init config - * @param state - * optional state config, pointing to a state file which will be applied to the node whenever it - * changes */ final case class ParticipantInitConfig( identity: IdentityConfig = IdentityConfig.Auto(), diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/config/ParticipantNodeConfig.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/config/ParticipantNodeConfig.scala index 09085c543c..e7d10d1bf5 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/config/ParticipantNodeConfig.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/config/ParticipantNodeConfig.scala @@ -28,6 +28,7 @@ import com.digitalasset.canton.platform.config.{ PartyManagementServiceConfig, StateServiceConfig, TopologyAwarePackageSelectionConfig, + TrafficEnforcementConfig, UpdateServiceConfig, UserManagementServiceConfig, } @@ -95,6 +96,7 @@ final case class ParticipantNodeConfig( override val monitoring: NodeMonitoringConfig = NodeMonitoringConfig(), override val topology: TopologyConfig = TopologyConfig(), alphaDynamic: DeclarativeParticipantConfig = DeclarativeParticipantConfig(), + trafficEnforcement: TrafficEnforcementConfig = TrafficEnforcementConfig(), ) extends LocalNodeConfig with BaseParticipantConfig with ConfigDefaults[Option[DefaultPorts], ParticipantNodeConfig] { @@ -381,17 +383,25 @@ object TestingTimeServiceConfig { * @param autoSyncProtocolFeatureFlags * When true (default), protocol feature flags will be automatically updated when the node * connects to a synchronizer. - * @param alphaMultiSynchronizerSupport - * Determines whether ACS imports use Create/Archive or Assign/Unassign events. Only enable if - * your Ledger API consumers can process (un)assign events and require non-zero reassignment - * counters. - * - false (Default): Uses Create/Archive; resets reassignment counters to zero. - * - true: Uses Assign/Unassign; preserves existing reassignment counters. + * @param enableAllLedgerApiReassignments + * Determines whether ACS imports use Created or Assigned events. Similarly, determines whether + * the repair service uses Created/Archive events or Assigned/Unassigned events for add and purge + * respectively. Only enable if your Ledger API consumers can process (un)assigned events and + * require non-zero reassignment counters. + * - false (Default): Uses Created/Archive; resets reassignment counters to zero. + * - true: Uses Assigned/Unassigned; preserves existing reassignment counters. + * + * Note: If multi-synchronizer is enabled via the EnableMultiSynchronizer flag, then Assigned and + * Unassigned event will be emitted when processing reassignments messages from the synchronizer + * regardless of the value of enableAllLedgerApiReassignments. * @param commitAfterFailedActivenessCheck * For internal testing only. Do not enable this in production. * @param validateLegacyContractsV11 * Enables an extra validation for contracts with contract id version V11. Keep this enabled in * production. + * @param connectToSynchronizersOnStartup + * If true, connects to synchronizers that have manualConnect=false on startup. Default: true. + * Has impact only if manual-start is false. */ final case class ParticipantNodeParameterConfig( adminWorkflow: AdminWorkflowConfig = AdminWorkflowConfig(), @@ -429,10 +439,11 @@ final case class ParticipantNodeParameterConfig( commitmentReduceParallelism: NonNegativeInt = NonNegativeInt.one, commitmentUseDbSnapshotForParticipantLookup: Boolean = false, autoSyncProtocolFeatureFlags: Boolean = true, - alphaMultiSynchronizerSupport: Boolean = false, + enableAllLedgerApiReassignments: Boolean = false, commitAfterFailedActivenessCheck: Boolean = false, lsu: LsuConfig = LsuConfig(), validateLegacyContractsV11: Boolean = true, + connectToSynchronizersOnStartup: Boolean = true, ) extends LocalNodeParametersConfig /** Config for LSU. @@ -441,9 +452,7 @@ final case class ParticipantNodeParameterConfig( * Whether to automatically perform LSU. Default is true. * @param lsuRetry * Config for the retries of the LSU operation. Retries are done aggressively. - * @param handshakeRetry - * Config for the retries of the handshake prior to LSU. Retries are infrequent since the - * handshake runs as a non-urgent background task. + * * @param sequencerIdsRetrievalRetry * Config for the retries of the task that fetches the sequencer ids. * @param purgeObsoleteTopology @@ -458,11 +467,7 @@ final case class LsuConfig( maxDelay = config.NonNegativeDuration.ofSeconds(5), maxRetries = Int.MaxValue, ), - handshakeRetry: ExponentialBackoffConfig = ExponentialBackoffConfig( - initialDelay = config.NonNegativeFiniteDuration.ofMinutes(1), - maxDelay = config.NonNegativeDuration.ofMinutes(5), - maxRetries = Int.MaxValue, - ), + handshake: LsuHandshake = LsuHandshake(), sequencerIdsRetrievalRetry: ExponentialBackoffConfig = ExponentialBackoffConfig( initialDelay = config.NonNegativeFiniteDuration.ofSeconds(10), maxDelay = config.NonNegativeDuration.ofSeconds(30), @@ -471,10 +476,36 @@ final case class LsuConfig( purgeObsoleteTopology: Option[PurgeConfig] = None, ) +/** Config for the handshake with the successor. + * + * @param retry + * Config for the retries of the handshake prior to LSU. Retries are infrequent since the + * handshake runs as a non-urgent background task. + * @param minimumDuration + * If defined: after a successful handshake, will continue to perform handshake with the + * sequencers for the specified duration. Should not be too big (in the order of a few seconds). + * @param periodicCheck + * Duration between two checks whether the wait should be interrupted. Has an impact only if the + * following two conditions hold: + * - minimumDuration is non-empty + * - is smaller than minimumDuration + */ +final case class LsuHandshake( + retry: ExponentialBackoffConfig = ExponentialBackoffConfig( + initialDelay = config.NonNegativeFiniteDuration.ofMinutes(1), + maxDelay = config.NonNegativeDuration.ofMinutes(5), + maxRetries = Int.MaxValue, + ), + minimumDuration: Option[config.NonNegativeFiniteDuration] = Some( + config.NonNegativeFiniteDuration.ofSeconds(5) + ), + periodicCheck: config.NonNegativeFiniteDuration = config.NonNegativeFiniteDuration.ofSeconds(1), +) + /** Control incremental purges * * @param chunkSize - * The amount of data that should be removed per purge iteration + * The amount of data that should be removed per purge iteration. * @param cron * A cron expression, defining when the purges can take place * @param maxDuration @@ -485,6 +516,8 @@ final case class PurgeConfig( chunkSize: PositiveInt = PurgeConfig.DefaultChunkSize, cron: String = PurgeConfig.DefaultCron, maxDuration: config.PositiveFiniteDuration = PurgeConfig.DefaultMaxDuration, + purgeableStoresListValidity: config.NonNegativeFiniteDuration = + config.NonNegativeFiniteDuration.ofMinutes(1), ) object PurgeConfig { diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/event/AcsChangeListener.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/event/AcsChangeListener.scala index e6e088d1da..a7fc735264 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/event/AcsChangeListener.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/event/AcsChangeListener.scala @@ -10,7 +10,7 @@ import com.digitalasset.canton.ledger.participant.state.{ AcsChangeFactoryImpl, ContractStakeholdersAndReassignmentCounter, } -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} import com.digitalasset.canton.logging.{HasLoggerName, NamedLoggingContext} import com.digitalasset.canton.participant.protocol.conflictdetection.CommitSet import com.digitalasset.canton.tracing.TraceContext @@ -29,15 +29,18 @@ trait AcsChangeListener { * active contract set change descriptor */ def publish(toc: RecordTime, acsChange: AcsChange)(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): Unit def publish(acsChanges: NonEmpty[Seq[(RecordTime, AcsChange)]])(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): FutureUnlessShutdown[Unit] def publish(toc: RecordTime, acsChangeFactoryO: Option[AcsChangeFactory])(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): Unit } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/AcsCommitmentPublicationPostProcessor.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/AcsCommitmentPublicationPostProcessor.scala index ba63d69693..a1060f3336 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/AcsCommitmentPublicationPostProcessor.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/AcsCommitmentPublicationPostProcessor.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.ledger.participant.state.{ SynchronizerIndex, Update, } +import com.digitalasset.canton.lifecycle.CloseContext import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.participant.event.RecordTime import com.digitalasset.canton.participant.sync.ConnectedSynchronizersLookupContainer @@ -22,7 +23,8 @@ import com.digitalasset.canton.tracing.TraceContext class AcsCommitmentPublicationPostProcessor( connectedSynchronizersLookupContainer: ConnectedSynchronizersLookupContainer, override val loggerFactory: NamedLoggerFactory, -) extends NamedLogging +)(implicit closeContext: CloseContext) + extends NamedLogging with (Update => Unit) { def apply(update: Update): Unit = { @@ -42,7 +44,8 @@ class AcsCommitmentPublicationPostProcessor( )( // The trace context is deliberately generated here instead of continuing the one for the Update // to unlink the asynchronous acs commitment processing from message processing trace. - TraceContext.createNew("publish_acs_commitment") + TraceContext.createNew("publish_acs_commitment"), + closeContext, ) ) @@ -72,7 +75,8 @@ class AcsCommitmentPublicationPostProcessor( )( // The trace context is deliberately generated here instead of continuing the one for the Update // to unlink the asynchronous acs commitment processing from message processing trace. - TraceContext.createNew("publish_acs_commitment_upgrade_time") + TraceContext.createNew("publish_acs_commitment_upgrade_time"), + closeContext, ) ) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiIndexer.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiIndexer.scala index 90fe3349e8..c7d2bf1fe3 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiIndexer.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiIndexer.scala @@ -27,6 +27,7 @@ import com.digitalasset.canton.platform.indexer.parallel.{ } import com.digitalasset.canton.platform.indexer.{ IndexerConfig, + IndexerParams, IndexerQueueProxy, IndexerState, JdbcIndexer, @@ -49,6 +50,7 @@ import com.digitalasset.canton.util.PekkoUtil.{ IndexingFutureQueue, RecoveringFutureQueueImpl, RecoveringQueueMetrics, + ShutdownInProgress, } import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer @@ -152,7 +154,7 @@ object LedgerApiIndexer { ) .afterReleased(initializationLogger.info("Ledger API Indexer stopped.")) healthStatusRef = new AtomicReference[HealthStatus](Unhealthy) - (indexerCreateFunction, initializationKillSwitch) <- new JdbcIndexer.Factory( + indexerCreateFunction <- new JdbcIndexer.Factory( ledgerApiIndexerConfig.ledgerParticipantId, DbSupport.ParticipantDataSourceConfig(ledgerApiStore.value.ledgerApiStorage.jdbcUrl), ledgerApiIndexerConfig.indexerConfig, @@ -179,29 +181,35 @@ object LedgerApiIndexer { postProcessor, sequentialPostProcessor, contractStore.value, - achsInitInterceptor = identity, - ).initialized().map { case (indexer, initializationKillSwitch) => - ( - (repairMode: Boolean) => - (commit: Commit) => { - val result = indexer(repairMode)(commit) - result.onComplete { - case Success(indexer) => - healthStatusRef.set(Healthy) - indexer.futureQueue.done.onComplete(_ => healthStatusRef.set(Unhealthy)) + ).initialized().map { indexer => (params: IndexerParams) => + val result = indexer(params) + result.flatMap(identity).onComplete { + case Success(indexer) => + healthStatusRef.set(Healthy) + indexer.futureQueue.done.onComplete(_ => healthStatusRef.set(Unhealthy)) - case _ => - healthStatusRef.set(Unhealthy) - } - result - }, - initializationKillSwitch, - ) + case _ => + healthStatusRef.set(Unhealthy) + } + result } - normalIndexerCreateFunction = indexerCreateFunction(false) + normalIndexerCreateFunction = + (commit: Commit) => + (shutdownRequested: ShutdownInProgress) => + indexerCreateFunction( + IndexerParams( + repairMode = false, + commit = commit, + shutdownRequested = shutdownRequested, + ) + ) repairIndexerCreateFunction = // for repair indexer no commit functionality, and forcing repair instantiation - () => indexerCreateFunction(true)(_ => ()) + // also ACHS is skipped in repair mode, so no need to provide shutdownRequested probe + () => + indexerCreateFunction( + IndexerParams(repairMode = true, commit = _ => (), shutdownRequested = () => false) + ).flatMap(identity) recoveringQueueFactory = () => { new RecoveringFutureQueueImpl[Update]( maxBlockedOffer = ledgerApiIndexerConfig.indexerConfig.queueMaxBlockedOffer, @@ -224,7 +232,6 @@ object LedgerApiIndexer { uncommittedMeter = metrics.indexer.indexerQueueUncommitted, ), consumerFactory = normalIndexerCreateFunction, - initializationKillSwitch = initializationKillSwitch, ) } _ = initializationLogger.debug("Waiting for the indexer to initialize the database.") diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiServer.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiServer.scala index 85fe6691b1..20558ed5ad 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiServer.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiServer.scala @@ -62,6 +62,7 @@ import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTrack import com.digitalasset.canton.platform.apiserver.ratelimiting.RateLimitingInterceptorFactory import com.digitalasset.canton.platform.apiserver.services.ApiContractService import com.digitalasset.canton.platform.apiserver.services.admin.Utils +import com.digitalasset.canton.platform.apiserver.services.command.TrafficEnforcementBackend import com.digitalasset.canton.platform.apiserver.{ ApiServiceOwner, InProcessGrpcName, @@ -129,6 +130,7 @@ class LedgerApiServer( ledgerApiIndexer: Eval[LedgerApiIndexer], pruningConfig: ParticipantStoreConfig, updateServiceConfig: UpdateServiceConfig, + trafficEnforcementBackendO: Option[Eval[TrafficEnforcementBackend]], warnOnJwtScopeUsage: Boolean, val loggerFactory: NamedLoggerFactory, )(implicit @@ -422,6 +424,7 @@ class LedgerApiServer( apiLoggingConfig = cantonParameterConfig.loggingConfig.api, apiContractService = apiContractService, safeToPruneCommitmentState = pruningConfig.safeToPruneCommitmentState, + trafficEnforcementBackendO = trafficEnforcementBackendO.map(_.value), ) _ <- startHttpApiIfEnabled( timedSyncService, @@ -584,6 +587,7 @@ object LedgerApiServer { participantId: LedgerParticipantId, participantNodePersistentState: Eval[ParticipantNodePersistentState], sync: CantonSyncService, + trafficEnforcementBackendO: Option[Eval[TrafficEnforcementBackend]], pruningConfig: ParticipantStoreConfig, tracerProvider: TracerProvider, updateServiceConfig: UpdateServiceConfig, @@ -641,6 +645,7 @@ object LedgerApiServer { loggerFactory = loggerFactory, pruningConfig = pruningConfig, updateServiceConfig = updateServiceConfig, + trafficEnforcementBackendO = trafficEnforcementBackendO, warnOnJwtScopeUsage = warnOnJwtScopeUsage, ).owner() new ResourceOwnerFlagCloseableOps(ledgerApiServerOwner) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiStore.scala index 005c56ecc5..a8a2a70b44 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiStore.scala @@ -9,22 +9,18 @@ import com.daml.metrics.DatabaseMetrics import com.digitalasset.canton.concurrent.ExecutionContextIdlenessExecutorService import com.digitalasset.canton.config.{ProcessingTimeout, StorageConfig} import com.digitalasset.canton.data.{CantonTimestamp, Offset} -import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.ledger.participant.state.SynchronizerIndex import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLoggerFactory} import com.digitalasset.canton.metrics.LedgerApiServerMetrics import com.digitalasset.canton.participant.ledger.api.LedgerApiStore.LastSynchronizerOffset import com.digitalasset.canton.platform.config.ServerRole -import com.digitalasset.canton.platform.store.backend.DataSourceStorageBackend.DataSourceConfig import com.digitalasset.canton.platform.store.backend.EventStorageBackend.{ RawParticipantAuthorization, SequentialIdBatch, SynchronizerOffset, } import com.digitalasset.canton.platform.store.backend.ParameterStorageBackend.LedgerEnd -import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation -import com.digitalasset.canton.platform.store.backend.common.QueryStrategy import com.digitalasset.canton.platform.store.backend.postgresql.PostgresDataSourceConfig import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache import com.digitalasset.canton.platform.store.interning.StringInterningView @@ -95,45 +91,6 @@ class LedgerApiStore( integrityStorageBackend.moveLedgerEndBackToScratch() ) - @VisibleForTesting - private def withConnectionForTest(testFunction: Connection => Unit) = { - val conn = ledgerApiDbSupport.storageBackendFactory.createDataSourceStorageBackend - .createDataSource( - dataSourceConfig = DataSourceConfig(ledgerApiStorage.jdbcUrl), - loggerFactory = loggerFactory, - ) - .getConnection - conn.setAutoCommit(false) - testFunction(conn) - new Object { - def commitAndClose(): Unit = { - conn.commit() - conn.close() - } - } - } - - @VisibleForTesting - def lockPruning = withConnectionForTest( - QueryStrategy.withoutNetworkTimeout( - eventStorageBackend.lockExclusivelyPruningProcessingTable - )(_, noTracingLogger) - ) - - @VisibleForTesting - def readLockContract(internalContractId: Long) = withConnectionForTest( - QueryStrategy.withoutNetworkTimeout( - eventStorageBackend.readLockInternalContractIds(Set(internalContractId))(_).discard - )(_, noTracingLogger) - ) - - @VisibleForTesting - def writeLockContract(internalContractId: Long) = withConnectionForTest( - QueryStrategy.withoutNetworkTimeout( - eventStorageBackend.writeLockInternalContractIds(cSQL"= $internalContractId")(_) - )(_, noTracingLogger) - ) - @VisibleForTesting def numberOfAcceptedTransactionsFor(synchronizerId: SynchronizerId)(implicit traceContext: TraceContext, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/metrics/ParticipantMetrics.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/metrics/ParticipantMetrics.scala index c099d28f4f..08ae77247c 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/metrics/ParticipantMetrics.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/metrics/ParticipantMetrics.scala @@ -37,6 +37,8 @@ class ParticipantHistograms(val parent: MetricName)(implicit private[metrics] val dbStorage: DbStorageHistograms = new DbStorageHistograms(parent) + private[metrics] val signing: SigningHistograms = new SigningHistograms(parent) + private[metrics] val decryption: DecryptionHistograms = new DecryptionHistograms(parent) private[metrics] val sequencerClient: SequencerClientHistograms = new SequencerClientHistograms( parent ) @@ -91,8 +93,19 @@ class ParticipantMetrics( override def grpcMetrics: GrpcServerMetricsX = (ledgerApiServer.grpc, ledgerApiServer.requests) override def healthMetrics: HealthMetrics = ledgerApiServer.health override def storageMetrics: DbStorageMetrics = dbStorage - val dbStorage = new DbStorageMetrics(inventory.dbStorage, openTelemetryMetricsFactory) - val kmsMetrics: KmsMetrics = new KmsMetrics(prefix, openTelemetryMetricsFactory) + + val dbStorage: DbStorageMetrics = + new DbStorageMetrics(inventory.dbStorage, openTelemetryMetricsFactory) + + override def cryptoMetrics: CryptoMetrics = crypto + + val crypto: CryptoMetrics = + new CryptoMetrics( + new SigningMetrics(inventory.signing, openTelemetryMetricsFactory), + new DecryptionMetrics(inventory.decryption, openTelemetryMetricsFactory), + Some(new KmsMetrics(prefix, openTelemetryMetricsFactory)), + ) + val phase: Timer = openTelemetryMetricsFactory.timer(inventory.phase.info) // Private constructor to avoid being instantiated multiple times by accident @@ -139,9 +152,7 @@ class ParticipantMetrics( new ConnectedSynchronizerMetrics( inventory.connectedSynchronizer, openTelemetryMetricsFactory, - )( - mc.withExtraLabels("synchronizer" -> alias.unwrap) - ) + )(mc.withExtraLabels("synchronizer" -> alias.unwrap)) ), ) .value diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/AbstractMessageProcessor.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/AbstractMessageProcessor.scala index 8957c868c1..7cc2974546 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/AbstractMessageProcessor.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/AbstractMessageProcessor.scala @@ -26,6 +26,7 @@ import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequest import com.digitalasset.canton.sequencing.client.{ SendAsyncClientError, SendCallback, + SequencerClient, SequencerClientSend, } import com.digitalasset.canton.sequencing.protocol.{Batch, MessageId, Recipients} @@ -133,6 +134,7 @@ abstract class AbstractMessageProcessor( ), messageId = messageId.getOrElse(MessageId.randomMessageId()), callback = SendCallback.log(s"Response message for request [$requestId]", logger), + trafficCostValidator = SequencerClient.TrafficCostValidator.NoTrafficCostValidation, amplify = true, // We want to use a shorter patience for the confirmation responses useConfirmationResponseAmplificationParameters = true, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessor.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessor.scala index 8ee311d6c7..9e268b55b5 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessor.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessor.scala @@ -80,6 +80,7 @@ import com.google.common.annotations.VisibleForTesting import java.util.UUID import java.util.concurrent.atomic.AtomicInteger +import scala.annotation.unused import scala.concurrent.ExecutionContext import scala.util.{Failure, Success} @@ -173,6 +174,11 @@ abstract class ProtocolProcessor[ val recentSnapshot = crypto.create(topologySnapshot) val explicitMediatorGroupIndex = steps.explicitMediatorGroup(submissionParam) + + logger.debug( + s"Topology snapshot timestamp at submission: ${recentSnapshot.ipsSnapshot.timestamp}" + ) + for { _ <- steps.validateSubmittersNotOnboarding(submissionParam, topologySnapshot, participantId) @@ -187,9 +193,7 @@ abstract class ProtocolProcessor[ ) (submission, pendingSubmission) = submissionData - _ = logger.debug( - s"Topology snapshot timestamp at submission: ${recentSnapshot.ipsSnapshot.timestamp}" - ) + result <- { submission match { case untracked: steps.UntrackedSubmission => @@ -448,6 +452,16 @@ abstract class ProtocolProcessor[ protected def metricsContextForSubmissionParam(submissionParam: SubmissionParam): MetricsContext + @unused("default implementation") + protected def validateLocalTrafficCost( + submissionParam: SubmissionParam + )( + trafficCost: Long, + traceContext: TraceContext, + ): FutureUnlessShutdown[Unit] = + // TODO(#33681): Remove default implementation + FutureUnlessShutdown.unit + /** Submit the batch to the sequencer. Also registers `submissionParam` as pending submission. */ private def submitInternal( @@ -497,6 +511,8 @@ abstract class ProtocolProcessor[ maxSequencingTime = maxSequencingTime, ), messageId = messageId, + trafficCostValidator = (trafficCost: Long, traceContext: TraceContext) => + validateLocalTrafficCost(submissionParam)(trafficCost, traceContext), amplify = true, callback = res => sendResultP.trySuccess(res).discard, ) @@ -945,7 +961,7 @@ abstract class ProtocolProcessor[ snapshot.ipsSnapshot .participantsWithSupportedFeature( Set(participantId), - ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer, + ParticipantTopologyFeatureFlag.EnableMultiSynchronizer, ) .map(_.headOption.nonEmpty) ) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/TransactionProcessor.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/TransactionProcessor.scala index 5bb09c15d0..ced5191dd0 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/TransactionProcessor.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/TransactionProcessor.scala @@ -4,6 +4,7 @@ package com.digitalasset.canton.participant.protocol import cats.data.EitherT +import cats.implicits.toTraverseOps import com.daml.metrics.api.MetricsContext import com.digitalasset.base.error.{ Alarm, @@ -19,6 +20,7 @@ import com.digitalasset.canton.config.{ProcessingTimeout, TestingConfigInternal} import com.digitalasset.canton.crypto.* import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.ViewType.TransactionViewType +import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.* import com.digitalasset.canton.error.CantonErrorGroups.ParticipantErrorGroup.TransactionErrorGroup.SubmissionErrorGroup import com.digitalasset.canton.ledger.error.groups.ConsistencyErrors @@ -46,6 +48,7 @@ import com.digitalasset.canton.participant.protocol.validation.{ import com.digitalasset.canton.participant.sync.SyncEphemeralState import com.digitalasset.canton.participant.util.DAMLe import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker +import com.digitalasset.canton.platform.apiserver.services.command.TrafficEnforcementBackend import com.digitalasset.canton.protocol.* import com.digitalasset.canton.protocol.WellFormedTransaction.WithoutSuffixes import com.digitalasset.canton.sequencing.client.{SendAsyncClientError, SequencerClient} @@ -83,6 +86,7 @@ class TransactionProcessor( override val testingConfig: TestingConfigInternal, promiseFactory: PromiseUnlessShutdownFactory, participantNodeParameters: ParticipantNodeParameters, + trafficEnforcementBackendO: Option[TrafficEnforcementBackend], )(implicit val ec: ExecutionContext) extends ProtocolProcessor[ TransactionProcessingSteps.SubmissionParam, @@ -146,6 +150,21 @@ class TransactionProcessor( "type" -> "send-confirmation-request", ) + override protected def validateLocalTrafficCost( + submissionParam: TransactionProcessingSteps.SubmissionParam + )( + trafficCost: Long, + traceContext: TraceContext, + ): FutureUnlessShutdown[Unit] = + trafficEnforcementBackendO + .traverse( + _.validateTraffic( + actAs = submissionParam.submitterInfo.actAs, + trafficCost = trafficCost, + )(traceContext) + ) + .map(_.discard) + def submit( submitterInfo: SubmitterInfo, transactionMeta: TransactionMeta, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/ReassignmentValidation.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/ReassignmentValidation.scala index 902fe85091..5def12d337 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/ReassignmentValidation.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/ReassignmentValidation.scala @@ -98,7 +98,7 @@ object ReassignmentValidation { participantWithMultiSynchronizerEnabled <- EitherT.right( topologySnapshot.participantsWithSupportedFeature( participants, - feature = ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer, + feature = ParticipantTopologyFeatureFlag.EnableMultiSynchronizer, ) ) _ <- EitherT.fromEither[FutureUnlessShutdown]( diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/UnassignmentValidationResult.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/UnassignmentValidationResult.scala index db85008ef2..c27e24a23e 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/UnassignmentValidationResult.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/UnassignmentValidationResult.scala @@ -118,6 +118,7 @@ final case class UnassignmentValidationResult( assignmentExclusivity = assignmentExclusivity.map(_.unwrap.toLf), reassignmentCounter = reassign.counter.unwrap, nodeId = idx, + keyOpt = reassign.contract.contractKeyWithMaintainers, ) }), recordTime = recordTime, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala index ea57977f55..d97f672c94 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala @@ -455,7 +455,7 @@ class AcsCommitmentProcessor private ( private def processBufferedAtInit( timestamp: Option[CantonTimestampSecond] - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = dbQueue.executeUS( timestamp.traverse_(ts => processBuffered(ts, endExclusive = false)), "processBufferedAtInit", @@ -589,7 +589,8 @@ class AcsCommitmentProcessor private ( toc: RecordTime, acsChange: AcsChange, )(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): Unit = publishInternal(PublishTickData.Regular(toc, () => FutureUnlessShutdown.pure(acsChange))) @@ -760,7 +761,8 @@ class AcsCommitmentProcessor private ( } override def publish(acsChanges: NonEmpty[Seq[(RecordTime, AcsChange)]])(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): FutureUnlessShutdown[Unit] = collapseAndPublishAcsChanges(acsChanges).map(changes => changes.foreach { case (rt, change) => @@ -772,7 +774,8 @@ class AcsCommitmentProcessor private ( toc: RecordTime, acsChangeFactoryO: Option[AcsChangeFactory], )(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): Unit = publishInternal( PublishTickData.Regular( @@ -786,7 +789,7 @@ class AcsCommitmentProcessor private ( */ def publishForUpgradeTime( upgradeTime: CantonTimestamp - )(implicit traceContext: TraceContext): Unit = + )(implicit traceContext: TraceContext, closeContext: CloseContext): Unit = publishInternal(PublishTickData.PersistRunningCommitmentsAtUpgradeTime(upgradeTime)) private def computeAcsChange(toc: RecordTime, acsChangeFactoryO: Option[AcsChangeFactory])( @@ -822,7 +825,8 @@ class AcsCommitmentProcessor private ( private def publishInternal( publishTickData: PublishTickData )(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): Unit = { @tailrec def go(): Unit = @@ -846,7 +850,7 @@ class AcsCommitmentProcessor private ( RecordTime(timestamp = effectiveTime, tieBreaker = 0), () => FutureUnlessShutdown.pure(AcsChange.empty), ) - )(traced.traceContext) + )(traced.traceContext, closeContext) } // now, iterate (there might have been several effective time updates) go() @@ -920,7 +924,8 @@ class AcsCommitmentProcessor private ( private def publishTick( publishTickData: PublishTickData )(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): Unit = publishTickData match { case PublishTickData.Regular(toc, acsChangeF) => publishTickInternal(toc, acsChangeF) @@ -933,7 +938,8 @@ class AcsCommitmentProcessor private ( toc: RecordTime, acsChangeF: () => FutureUnlessShutdown[AcsChange], )(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): Unit = { if (!lastPublished.forall(_ < toc)) throw new IllegalStateException( @@ -1344,7 +1350,7 @@ class AcsCommitmentProcessor private ( def processBatch( timestamp: CantonTimestamp, batch: Traced[Seq[OpenEnvelope[SignedProtocolMessage[AcsCommitment]]]], - ): HandlerResult = + )(implicit closeContext: CloseContext): HandlerResult = batch.withTraceContext(implicit traceContext => processBatchInternal(timestamp, _)) /** Process incoming commitments. @@ -1371,7 +1377,7 @@ class AcsCommitmentProcessor private ( def processBatchInternal( timestamp: CantonTimestamp, batch: Seq[OpenEnvelope[SignedProtocolMessage[AcsCommitment]]], - )(implicit traceContext: TraceContext): HandlerResult = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): HandlerResult = { if (batch.sizeIs != 1) { Errors.InternalError @@ -1653,7 +1659,7 @@ class AcsCommitmentProcessor private ( @VisibleForTesting private[pruning] def indicateLocallyProcessed( period: CommitmentPeriod - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = { endOfLastProcessedPeriod = Some(period.toInclusive) for { // mark that we're done with processing this period; safe to do at any point after the commitment has been sent @@ -1676,7 +1682,7 @@ class AcsCommitmentProcessor private ( private def checkSignedMessage( timestamp: CantonTimestamp, envelope: OpenEnvelope[SignedProtocolMessage[AcsCommitment]], - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = { val message = envelope.protocolMessage logger.info( s"Checking commitment (purportedly by) ${message.message.sender} for period ${message.message.period}" @@ -1724,7 +1730,7 @@ class AcsCommitmentProcessor private ( private def checkCommitment( commitment: AcsCommitment - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = { val fut = dbQueue .executeUS( { @@ -1774,7 +1780,7 @@ class AcsCommitmentProcessor private ( private def processBuffered( timestamp: CantonTimestampSecond, endExclusive: Boolean, - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = { logger.debug(s"Processing buffered commitments until $timestamp ${if (endExclusive) "exclusive" else "inclusive"}") for { @@ -1849,9 +1855,9 @@ class AcsCommitmentProcessor private ( private def checkMatchAndMarkSafe( remote: List[AcsCommitmentData] - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = { logger.info(s"Processing ${remote.size} remote commitments") - remote.parTraverse_ { cmt => + MonadUtil.parTraverseWithLimit_(threadCount)(remote) { cmt => for { commitments <- store.getComputed(cmt.period, cmt.sender) // check if we were in a catch-up phase @@ -1881,7 +1887,7 @@ class AcsCommitmentProcessor private ( lastProcessedCatchUpCommitmentTimestamp: Option[CantonTimestampSecond], completedPeriod: CommitmentPeriod, filterInJustMismatches: Boolean = false, - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = { logger.debug( s"$participantId checkMatchAndMarkSafeOrFixDuringCatchUp for period $completedPeriod" ) @@ -1945,23 +1951,24 @@ class AcsCommitmentProcessor private ( } // we mark all counter-commitments - _ <- analyzedCounterCommitments.parTraverse_ { analyzedCounterCommitment => - val counterCommitment = analyzedCounterCommitment.merge - val safe = analyzedCounterCommitment.isRight - logger.debug( - s"Marked as ${if (safe) "safe" else "unsafe"} commitment $cmt against counterComm $counterCommitment" - ) - val cmtPeriodsNE = NonEmptyUtil.fromElement(counterCommitment.period) - for { - _ <- - if (safe) store.markSafe(counterCommitment.sender, cmtPeriodsNE) - else store.markUnsafe(counterCommitment.sender, cmtPeriodsNE) - } yield { - // max to ensure that this metric increases monotonically - metrics.lastIncomingProcessed.updateValue( - _ max counterCommitment.period.toInclusive.toMicros + _ <- MonadUtil.parTraverseWithLimit_(threadCount)(analyzedCounterCommitments) { + analyzedCounterCommitment => + val counterCommitment = analyzedCounterCommitment.merge + val safe = analyzedCounterCommitment.isRight + logger.debug( + s"Marked as ${if (safe) "safe" else "unsafe"} commitment $cmt against counterComm $counterCommitment" ) - } + val cmtPeriodsNE = NonEmptyUtil.fromElement(counterCommitment.period) + for { + _ <- + if (safe) store.markSafe(counterCommitment.sender, cmtPeriodsNE) + else store.markUnsafe(counterCommitment.sender, cmtPeriodsNE) + } yield { + // max to ensure that this metric increases monotonically + metrics.lastIncomingProcessed.updateValue( + _ max counterCommitment.period.toInclusive.toMicros + ) + } } (mismatches, matching) = analyzedCounterCommitments.separate @@ -2466,7 +2473,7 @@ class AcsCommitmentProcessor private ( private def markOutstandingIfNonEmpty( completedPeriod: CommitmentPeriod, participants: Set[ParticipantId], - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = NonEmpty.from(participants).traverse_ { counterParticipants => for { splitPeriod <- sortedReconciliationIntervalsProvider.splitCommitmentPeriod( @@ -2488,24 +2495,30 @@ class AcsCommitmentProcessor private ( commitments: Iterable[(CommitmentPeriod, HashedCommitmentType)], lastPruningTime: Option[PruningStatus], possibleCatchUp: Boolean, - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = for { splitPeriods <- sortedReconciliationIntervalsProvider.splitCommitmentPeriod(cmt.period) reconIntervals <- getReconciliationIntervals(cmt.period.toInclusive.forgetRefinement) reconIntervalLength = reconIntervals.intervals.headOption.fold(0L)( _.intervalLength.duration.toMillis ) + + isMatch = matches( + cmt, + commitments, + lastPruningTime.map(_.timestamp), + possibleCatchUp, + reconIntervalLength, + ) + _ <- splitPeriods.traverse_ { periods => - val isMatch = - matches( - cmt, - commitments, - lastPruningTime.map(_.timestamp), - possibleCatchUp, - reconIntervalLength, - ) - if (isMatch) store.markSafe(cmt.sender, periods) - else store.markUnsafe(cmt.sender, periods) + MonadUtil.batchedSequentialTraverseNE_( + batchingConfig.parallelism, + batchingConfig.maxItemsInBatch, + )(periods) { chunk => + if (isMatch) store.markSafe(cmt.sender, chunk) + else store.markUnsafe(cmt.sender, chunk) + } } } yield () @@ -2962,7 +2975,10 @@ object AcsCommitmentProcessor extends HasLoggerName { // end has moved, which should mean that all topology events for a given timestamp have been processed before // processing the ACS change for the same timestamp FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( - processor.processBufferedAtInit(endOfLastProcessedPeriod), + processor.processBufferedAtInit(endOfLastProcessedPeriod)( + traceContext, + initializationFlagCloseable.closeContext, + ), "processing of buffered commitments at init failed", ) loggingContext.info( diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/replica/ParticipantReplicaManager.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/replica/ParticipantReplicaManager.scala index e41586ab20..4a02dd7fd7 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/replica/ParticipantReplicaManager.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/replica/ParticipantReplicaManager.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.participant.replica +import cats.implicits.toTraverseOps import com.digitalasset.canton.concurrent.FutureSupervisor import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.lifecycle.FutureUnlessShutdown @@ -71,6 +72,14 @@ class ParticipantReplicaManager( ) _ <- participantServices.ledgerApiIndexerContainer.initializeNext() _ = logger.info("Participant replica is becoming active: Ledger API Indexer started") + _ <- participantServices.trafficEnforcementBackendContainerO.traverse( + _.initializeNext().map(_ => + logger.info( + "Participant replica is becoming active: Traffic enforcement backend started" + ) + ) + ) + _ <- participantServices.cantonSyncService.refreshCaches() _ = logger.info( "Participant replica is becoming active: CantonSyncService caches refreshed" @@ -144,6 +153,13 @@ class ParticipantReplicaManager( _ = logger.info( "Participant replica is becoming passive: CantonSyncService caches cleared" ) + _ = participantServices.trafficEnforcementBackendContainerO.foreach { + trafficEnforcementBackend => + trafficEnforcementBackend.closeCurrent() + logger.info( + "Participant replica is becoming passive: Traffic enforcement backend stopped" + ) + } } yield () case None => diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/scheduler/ParticipantPurgeStoresAfterLsuScheduler.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/scheduler/ParticipantPurgeStoresAfterLsuScheduler.scala index 7c276e500c..6b8f5b5582 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/scheduler/ParticipantPurgeStoresAfterLsuScheduler.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/scheduler/ParticipantPurgeStoresAfterLsuScheduler.scala @@ -4,24 +4,23 @@ package com.digitalasset.canton.participant.scheduler import com.digitalasset.canton.concurrent.ExecutionContextIdlenessExecutorService -import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.RequireTypes.PositiveInt +import com.digitalasset.canton.config.{BatchingConfig, ProcessingTimeout} import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.participant.scheduler.ParticipantPurgeStoresAfterLsuScheduler.GetPurgeableStores import com.digitalasset.canton.participant.store.SynchronizerConnectionConfigStore -import com.digitalasset.canton.participant.store.SynchronizerConnectionConfigStore.LsuSource import com.digitalasset.canton.participant.sync.SyncPersistentStateManager import com.digitalasset.canton.scheduler.{IndividualSchedule, JobSchedule, JobScheduler} -import com.digitalasset.canton.store.ChunkPurgeable import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.MonadUtil import scala.concurrent.Future final class ParticipantPurgeStoresAfterLsuScheduler( schedule: Option[JobSchedule], - getPurgeableStores: GetPurgeableStores, + purgeableStoresComputation: PostLsuPurgeableStoresComputation, chunkSize: PositiveInt, + batchingConfig: BatchingConfig, timeouts: ProcessingTimeout, override val loggerFactory: NamedLoggerFactory, )(implicit @@ -30,16 +29,16 @@ final class ParticipantPurgeStoresAfterLsuScheduler( override protected def schedulerJob( schedule: IndividualSchedule - )(implicit traceContext: TraceContext): FutureUnlessShutdown[JobScheduler.ScheduledRunResult] = { - val purgeableStores = getPurgeableStores() - val deletions = purgeableStores.map(_.deleteDataChunk(chunkSize)) - + )(implicit traceContext: TraceContext): FutureUnlessShutdown[JobScheduler.ScheduledRunResult] = for { - deletedSomething <- FutureUnlessShutdown.sequence(deletions) + purgeableStores <- purgeableStoresComputation.compute() + _ = logger.debug(s"Purgeable stores: ${purgeableStores.map(_.name)}") + deletedSomething <- MonadUtil.parTraverseWithLimit(batchingConfig.pruningParallelism)( + purgeableStores + )(_.deleteDataChunk(chunkSize)) } yield { if (deletedSomething.contains(true)) JobScheduler.MoreWorkToPerform else JobScheduler.Done } - } override protected def initializeSchedule()(implicit traceContext: TraceContext @@ -47,42 +46,28 @@ final class ParticipantPurgeStoresAfterLsuScheduler( } object ParticipantPurgeStoresAfterLsuScheduler { - trait GetPurgeableStores { - def apply(): Seq[ChunkPurgeable] - } - def create( schedule: Option[JobSchedule], chunkSize: PositiveInt, synchronizerConnectionConfigStore: SynchronizerConnectionConfigStore, syncPersistentStateManager: SyncPersistentStateManager, + batchingConfig: BatchingConfig, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContextIdlenessExecutorService ): ParticipantPurgeStoresAfterLsuScheduler = { - val getPurgeableStores: GetPurgeableStores = () => { - val purgeableSynchronizers = synchronizerConnectionConfigStore - .getAll() - // keep only synchronizer that were LSUed - .filter(_.status == LsuSource) - .flatMap(_.configuredPsid.toOption) - // and that have an active physical connection - .filter(psid => synchronizerConnectionConfigStore.getActive(psid.logical).isRight) - - purgeableSynchronizers.flatMap { psid => - syncPersistentStateManager - .get(psid) - .toList - .flatMap(_.purgeableStores) - } - } + val purgeableStoresComputation = new PostLsuPurgeableStoresComputation( + synchronizerConnectionConfigStore, + syncPersistentStateManager, + ) new ParticipantPurgeStoresAfterLsuScheduler( schedule, - getPurgeableStores, + purgeableStoresComputation, chunkSize, + batchingConfig, timeouts, loggerFactory, ) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/scheduler/PostLsuPurgeableStoresComputation.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/scheduler/PostLsuPurgeableStoresComputation.scala new file mode 100644 index 0000000000..3b02aa574c --- /dev/null +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/scheduler/PostLsuPurgeableStoresComputation.scala @@ -0,0 +1,70 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.participant.scheduler + +import cats.syntax.contravariantSemigroupal.* +import cats.syntax.functorFilter.* +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.participant.store.SynchronizerConnectionConfigStore +import com.digitalasset.canton.participant.store.SynchronizerConnectionConfigStore.LsuSource +import com.digitalasset.canton.participant.sync.SyncPersistentStateManager +import com.digitalasset.canton.store.ChunkPurgeable +import com.digitalasset.canton.topology.PhysicalSynchronizerId +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.MonadUtil + +import scala.concurrent.ExecutionContext + +/** Computes which stores can be purged after LSU. + */ +class PostLsuPurgeableStoresComputation( + synchronizerConnectionConfigStore: SynchronizerConnectionConfigStore, + syncPersistentStateManager: SyncPersistentStateManager, +)(implicit + ec: ExecutionContext +) { + + def compute()(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Seq[ChunkPurgeable]] = { + val persistentStates = syncPersistentStateManager.getAll + + val successorPerPsid: Map[PhysicalSynchronizerId, PhysicalSynchronizerId] = + synchronizerConnectionConfigStore + .getAll() + .mapFilter { config => + (config.predecessor, config.configuredPsid.toOption).mapN { case (predecessor, psid) => + predecessor.psid -> psid + } + } + .toMap + + val candidates = synchronizerConnectionConfigStore + .getAll() + // keep only synchronizer that were LSUed + .filter(_.status == LsuSource) + .flatMap(_.configuredPsid.toOption) + // and that have an active physical connection + .filter(psid => synchronizerConnectionConfigStore.getActive(psid.logical).isRight) + + // Consider only synchronizer that have successor topology initialized + // so that purging does not get in the way of local copy. + MonadUtil + .sequentialTraverse(candidates) { psid => + successorPerPsid + .get(psid) + .flatMap(persistentStates.get) match { + case Some(successorPersistentState) => + successorPersistentState.connectivityStatusStore.isTopologyInitialized().map { + case true => + persistentStates.get(psid).fold(Seq.empty[ChunkPurgeable])(_.purgeableStores) + case false => Nil + } + + case None => FutureUnlessShutdown.pure(Seq.empty) + } + } + .map(_.flatten) + } +} diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/AcsCommitmentStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/AcsCommitmentStore.scala index fad6586b53..495aec6d7f 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/AcsCommitmentStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/AcsCommitmentStore.scala @@ -60,7 +60,8 @@ trait AcsCommitmentStore periods: NonEmpty[immutable.Iterable[CommitmentPeriod]], counterParticipants: NonEmpty[Set[ParticipantId]], )(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): FutureUnlessShutdown[Unit] /** Marks a period as processed and thus its end as a safe point for crash-recovery. @@ -70,7 +71,8 @@ trait AcsCommitmentStore * The period must be after the time point returned by [[lastComputedAndSent]]. */ def markComputedAndSent(period: CommitmentPeriod)(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): FutureUnlessShutdown[Unit] /** Store a received ACS commitment. To be called by the ACS commitment processor only. @@ -104,7 +106,7 @@ trait AcsCommitmentStore def markSafe( counterParticipant: ParticipantId, periods: NonEmpty[immutable.Iterable[CommitmentPeriod]], - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = markPeriod( counterParticipant, periods, @@ -127,7 +129,7 @@ trait AcsCommitmentStore def markUnsafe( counterParticipant: ParticipantId, periods: NonEmpty[immutable.Iterable[CommitmentPeriod]], - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = markPeriod( counterParticipant, periods, @@ -149,7 +151,7 @@ trait AcsCommitmentStore counterParticipant: ParticipantId, periods: NonEmpty[immutable.Iterable[CommitmentPeriod]], matchingState: CommitmentPeriodStateInOutstanding, - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] val runningCommitments: IncrementalCommitmentStore diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SubmissionTrackerStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SubmissionTrackerStore.scala index 589394be90..98dbff3229 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SubmissionTrackerStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SubmissionTrackerStore.scala @@ -8,6 +8,7 @@ import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown} import com.digitalasset.canton.logging.NamedLogging import com.digitalasset.canton.protocol.{RequestId, RootHash} import com.digitalasset.canton.store.{ChunkPurgeable, PrunableByTime, Purgeable} +import com.digitalasset.canton.topology.PhysicalSynchronizerId import com.digitalasset.canton.tracing.TraceContext import com.google.common.annotations.VisibleForTesting @@ -19,6 +20,8 @@ trait SubmissionTrackerStore with ChunkPurgeable { override protected def kind: String = "tracked submissions" + def psid: PhysicalSynchronizerId + def name: String = s"$kind ($psid)" /** Register a fresh request in the store. * @return diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SynchronizerConnectivityStatusStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SynchronizerConnectivityStatusStore.scala index 2ce71b91db..44f3b15326 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SynchronizerConnectivityStatusStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SynchronizerConnectivityStatusStore.scala @@ -34,7 +34,7 @@ trait SynchronizerConnectivityStatusStore { def setTopologyInitialized()(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] - def isTopologyInitialized(implicit traceContext: TraceContext): FutureUnlessShutdown[Boolean] + def isTopologyInitialized()(implicit traceContext: TraceContext): FutureUnlessShutdown[Boolean] } object SynchronizerConnectivityStatusStore { diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbAcsCommitmentStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbAcsCommitmentStore.scala index e54dd5bf41..3ca9233478 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbAcsCommitmentStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbAcsCommitmentStore.scala @@ -188,7 +188,8 @@ class DbAcsCommitmentStore( periods: NonEmpty[immutable.Iterable[CommitmentPeriod]], counterParticipants: NonEmpty[Set[ParticipantId]], )(implicit - traceContext: TraceContext + traceContext: TraceContext, + externalCloseContext: CloseContext, ): FutureUnlessShutdown[Unit] = { logger.debug( s"Marking $periods as outstanding for ${counterParticipants.size} remote participants" @@ -210,15 +211,25 @@ class DbAcsCommitmentStore( ( ?, ?, ?, ?, ?) on conflict do nothing""" - storage.queryAndUpdate( - DbStorage.bulkOperation_(insertOutstanding, crossProduct, storage.profile)(setParams), - operationName = "commitments: storeOutstanding", - ) + CloseContext.withCombinedContext(closeContext, externalCloseContext, timeouts, logger) { + combinedCloseContext => + storage.queryAndUpdate( + DbStorage.bulkOperation_(insertOutstanding, crossProduct, storage.profile)(setParams), + operationName = "commitments: storeOutstanding", + )( + traceContext, + combinedCloseContext, + DbStorage.RowsAltered.ofUnit, + ) + } } override def markComputedAndSent( period: CommitmentPeriod - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit + traceContext: TraceContext, + externalCloseContext: CloseContext, + ): FutureUnlessShutdown[Unit] = { val timestamp = period.toInclusive val upsertQuery = storage.profile match { case _: DbStorage.Profile.H2 => @@ -228,7 +239,14 @@ class DbAcsCommitmentStore( on conflict (synchronizer_idx) do update set ts = $timestamp""" } - storage.update_(upsertQuery, operationName = "commitments: markComputedAndSent") + CloseContext.withCombinedContext(closeContext, externalCloseContext, timeouts, logger) { + combinedCloseContext => + storage.update_(upsertQuery, operationName = "commitments: markComputedAndSent")( + traceContext, + combinedCloseContext, + DbStorage.RowsAltered.ofInt, + ) + } } private def buildParticipantFilter( @@ -304,7 +322,8 @@ class DbAcsCommitmentStore( periods: NonEmpty[immutable.Iterable[CommitmentPeriod]], matchingState: CommitmentPeriodStateInOutstanding, )(implicit - traceContext: TraceContext + traceContext: TraceContext, + externalCloseContext: CloseContext, ): FutureUnlessShutdown[Unit] = { val upsertQuery = storage.profile match { @@ -338,22 +357,25 @@ class DbAcsCommitmentStore( """ } - storage.queryAndUpdate( - DbStorage.bulkOperation_(upsertQuery, periods, storage.profile) { pp => period => - pp >> indexedSynchronizer - pp >> period.fromExclusive - pp >> period.toInclusive - pp >> counterParticipant - pp >> matchingState - // when par_outstanding_acs_commitments.matching_state = ? then excluded.matching_state - pp >> CommitmentPeriodState.Outstanding - // when par_outstanding_acs_commitments.matching_state = ? and excluded.matching_state = ? then excluded.matching_state - pp >> CommitmentPeriodState.Mismatched - pp >> CommitmentPeriodState.Matched - }, - operationName = - s"commitments: marking until ${periods.last1.toInclusive} with state $matchingState for $counterParticipant", - ) + CloseContext.withCombinedContext(closeContext, externalCloseContext, timeouts, logger) { + combinedCloseContext => + storage.queryAndUpdate( + DbStorage.bulkOperation_(upsertQuery, periods, storage.profile) { pp => period => + pp >> indexedSynchronizer + pp >> period.fromExclusive + pp >> period.toInclusive + pp >> counterParticipant + pp >> matchingState + // when par_outstanding_acs_commitments.matching_state = ? then excluded.matching_state + pp >> CommitmentPeriodState.Outstanding + // when par_outstanding_acs_commitments.matching_state = ? and excluded.matching_state = ? then excluded.matching_state + pp >> CommitmentPeriodState.Mismatched + pp >> CommitmentPeriodState.Matched + }, + operationName = + s"commitments: marking until ${periods.last1.toInclusive} with state $matchingState for $counterParticipant", + )(traceContext, combinedCloseContext, DbStorage.RowsAltered.ofUnit) + } } override def doPrune( diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSubmissionTrackerStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSubmissionTrackerStore.scala index ec8aa3b6aa..76958eec7b 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSubmissionTrackerStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSubmissionTrackerStore.scala @@ -18,6 +18,7 @@ import com.digitalasset.canton.store.{ IndexedString, PrunableByTimeParameters, } +import com.digitalasset.canton.topology.PhysicalSynchronizerId import com.digitalasset.canton.tracing.TraceContext import slick.jdbc.SetParameter import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton @@ -35,6 +36,8 @@ class DbSubmissionTrackerStore( with DbPrunableByTimeSynchronizer[IndexedPhysicalSynchronizer] with DbStore { + override def psid: PhysicalSynchronizerId = indexedSynchronizer.psid + override protected[this] implicit def setParameterIndexedSynchronizer : SetParameter[IndexedPhysicalSynchronizer] = IndexedString.setParameterIndexedString override protected[this] def partitionColumn: String = "physical_synchronizer_idx" diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSynchronizerConnectionConfigStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSynchronizerConnectionConfigStore.scala index e1fa6a53c9..aac4149206 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSynchronizerConnectionConfigStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSynchronizerConnectionConfigStore.scala @@ -225,7 +225,7 @@ class DbSynchronizerConnectionConfigStore private[store] ( if existingConfigs.exists(c => c.config == config && c.configuredPsid.isDefined && c.predecessor == synchronizerPredecessor && c.status == status ) => - logger.debug( + logger.info( s"Not adding connection for ($synchronizerAlias, $configuredPsid) to the store because ($synchronizerAlias, ${existingConfigs .map(_.configuredPsid)}) already exists" ) @@ -357,8 +357,8 @@ class DbSynchronizerConnectionConfigStore private[store] ( val alias = config.synchronizerAlias val id = ConfigIdentifier.WithAlias(config.synchronizerAlias, configuredPsid) - logger.debug( - s"Inserting connection for ($alias, $configuredPsid) into the store" + logger.info( + s"Inserting connection for ($alias, $configuredPsid) into the DB" ) lazy val insertAction: DbAction.WriteOnly[Int] = @@ -445,7 +445,7 @@ class DbSynchronizerConnectionConfigStore private[store] ( ): EitherT[FutureUnlessShutdown, Error, Unit] = { val alias = config.synchronizerAlias - logger.debug(s"Inserting connection for ($alias, $configuredPsid) into the store") + logger.info(s"Inserting connection for ($alias, $configuredPsid) into the store: $config") val queries = putInternalQuery(config, status, configuredPsid, synchronizerPredecessor) @@ -477,7 +477,7 @@ class DbSynchronizerConnectionConfigStore private[store] ( ): EitherT[FutureUnlessShutdown, MissingConfigForSynchronizer, Unit] = { val synchronizerAlias = config.synchronizerAlias - logger.debug(s"Replacing configuration for ($synchronizerAlias, $configuredPsid)") + logger.info(s"Replacing configuration for ($synchronizerAlias, $configuredPsid) with $config") val updateAction = configuredPsid.toOption match { case Some(psid) => @@ -522,8 +522,8 @@ class DbSynchronizerConnectionConfigStore private[store] ( val data = Map( "insert data" -> insert.toString, - "overrideSequencerConnections" -> overrideSequencerConnections.toString, "overridePredecessor" -> overridePredecessor.toString, + "overrideSequencerConnections" -> overrideSequencerConnections.toString, ) logger.info(s"Upserting connection config for synchronizer $psid, with data $data") @@ -536,7 +536,7 @@ class DbSynchronizerConnectionConfigStore private[store] ( .modify(value => overrideSequencerConnections.getOrElse(value)) .focus(_.predecessor) .modify(value => overridePredecessor.fold(value)(Some(_))) - .focus(_.config.synchronizerId) + .focus(_.config.psid) .replace(Some(psid)) if (updatedStoredConfig != storedConfig) @@ -692,7 +692,7 @@ class DbSynchronizerConnectionConfigStore private[store] ( traceContext: TraceContext ): EitherT[FutureUnlessShutdown, Error, Unit] = { - logger.debug(s"Setting status of ($alias, $configuredPsid) to $status") + logger.info(s"Setting status of ($alias, $configuredPsid) to $status") val updateAction = for { _ <- checkStatusConsistent(configuredPsid, alias, status) @@ -729,7 +729,7 @@ class DbSynchronizerConnectionConfigStore private[store] ( )(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, Error, Unit] = { - logger.debug(s"Set physical synchronizer id for $alias to $psid") + logger.info(s"Set physical synchronizer id for $alias to $psid") val queries: EitherT[dbio.DBIO, Error, Unit] = for { storedConfigToUpdateO <- getRowToSetPsid(alias, psid) @@ -745,7 +745,7 @@ class DbSynchronizerConnectionConfigStore private[store] ( case Some(_) => setPsidInternal(alias, psid) case None => - logger.debug( + logger.info( s"Physical synchronizer id for $alias is already set to $psid" ) EitherT.pure[DBIO, Error](()) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSynchronizerConnectivityStatusStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSynchronizerConnectivityStatusStore.scala index e7754fd04c..5c27f84e11 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSynchronizerConnectivityStatusStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSynchronizerConnectivityStatusStore.scala @@ -83,7 +83,7 @@ class DbSynchronizerConnectivityStatusStore( functionFullName, ) - def isTopologyInitialized(implicit traceContext: TraceContext): FutureUnlessShutdown[Boolean] = + def isTopologyInitialized()(implicit traceContext: TraceContext): FutureUnlessShutdown[Boolean] = storage .query( sql"""select is_topology_initialized diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemoryAcsCommitmentStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemoryAcsCommitmentStore.scala index c92d2560f5..a9457bca29 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemoryAcsCommitmentStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemoryAcsCommitmentStore.scala @@ -129,7 +129,8 @@ class InMemoryAcsCommitmentStore( periods: NonEmpty[immutable.Iterable[CommitmentPeriod]], counterParticipants: NonEmpty[Set[ParticipantId]], )(implicit - traceContext: TraceContext + traceContext: TraceContext, + closeContext: CloseContext, ): FutureUnlessShutdown[Unit] = { if (counterParticipants.nonEmpty) { _outstanding.updateAndGet(os => @@ -144,7 +145,7 @@ class InMemoryAcsCommitmentStore( override def markComputedAndSent( period: CommitmentPeriod - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = { val timestamp = period.toInclusive lastComputed.set(Some(timestamp)) FutureUnlessShutdown.unit @@ -169,7 +170,7 @@ class InMemoryAcsCommitmentStore( counterParticipant: ParticipantId, periods: NonEmpty[immutable.Iterable[CommitmentPeriod]], matchingState: CommitmentPeriodStateInOutstanding, - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + )(implicit traceContext: TraceContext, closeContext: CloseContext): FutureUnlessShutdown[Unit] = { val periodSets = periods.toSet _outstanding.updateAndGet { currentOutstanding => currentOutstanding.map { diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySubmissionTrackerStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySubmissionTrackerStore.scala index f08879df90..f302edf245 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySubmissionTrackerStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySubmissionTrackerStore.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.participant.store.SubmissionTrackerStore import com.digitalasset.canton.protocol.{RequestId, RootHash} import com.digitalasset.canton.store.memory.InMemoryPrunableByTime +import com.digitalasset.canton.topology.PhysicalSynchronizerId import com.digitalasset.canton.tracing.TraceContext import com.google.common.annotations.VisibleForTesting @@ -21,6 +22,7 @@ import scala.collection.concurrent.TrieMap import scala.concurrent.ExecutionContext class InMemorySubmissionTrackerStore( + override val psid: PhysicalSynchronizerId, override protected val loggerFactory: NamedLoggerFactory, override val timeouts: ProcessingTimeout, )(implicit diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySyncPersistentState.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySyncPersistentState.scala index ce6cc12caf..137af03c9d 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySyncPersistentState.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySyncPersistentState.scala @@ -103,7 +103,7 @@ class InMemoryPhysicalSyncPersistentState( val requestJournalStore = new InMemoryRequestJournalStore(loggerFactory) val connectivityStatusStore = new InMemorySynchronizerConnectivityStatusStore() val sendTrackerStore = new InMemorySendTrackerStore() - val submissionTrackerStore = new InMemorySubmissionTrackerStore(loggerFactory, timeouts) + val submissionTrackerStore = new InMemorySubmissionTrackerStore(psid, loggerFactory, timeouts) override val topologyStore = new InMemoryTopologyStore( diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySynchronizerConnectionConfigStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySynchronizerConnectionConfigStore.scala index 77b94a2547..1fd488aa29 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySynchronizerConnectionConfigStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySynchronizerConnectionConfigStore.scala @@ -326,8 +326,8 @@ class InMemorySynchronizerConnectionConfigStore( ): EitherT[FutureUnlessShutdown, Error, StoredSynchronizerConnectionConfig] = { val data = Map( "insert data" -> insert.toString, - "overrideSequencerConnections" -> overrideSequencerConnections.toString, "overridePredecessor" -> overridePredecessor.toString, + "overrideSequencerConnections" -> overrideSequencerConnections.toString, ) logger.info(s"Upserting connection config for synchronizer $psid, with data $data") @@ -352,7 +352,7 @@ class InMemorySynchronizerConnectionConfigStore( .modify(value => overrideSequencerConnections.getOrElse(value)) .focus(_.predecessor) .modify(value => overridePredecessor.fold(value)(Some(_))) - .focus(_.config.synchronizerId) + .focus(_.config.psid) .replace(Some(psid)) configuredSynchronizerMap @@ -470,7 +470,7 @@ class InMemorySynchronizerConnectionConfigStore( if (isChangeNeeded) EitherT.fromEither[FutureUnlessShutdown](performChange()) else { - logger.debug( + logger.info( s"Physical synchronizer id for $alias is already set to $psid" ) EitherTUtil.unitUS[Error] diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySynchronizerConnectivityStatusStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySynchronizerConnectivityStatusStore.scala index 9b9b4df0ea..4da7e1d31e 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySynchronizerConnectivityStatusStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySynchronizerConnectivityStatusStore.scala @@ -44,7 +44,7 @@ class InMemorySynchronizerConnectivityStatusStore extends SynchronizerConnectivi FutureUnlessShutdown.unit } - override def isTopologyInitialized(implicit + override def isTopologyInitialized()(implicit traceContext: TraceContext ): FutureUnlessShutdown[Boolean] = FutureUnlessShutdown.pure(topologyInitialized.get) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/CantonSyncService.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/CantonSyncService.scala index 56dc0185c9..eac9e43865 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/CantonSyncService.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/CantonSyncService.scala @@ -95,6 +95,7 @@ import com.digitalasset.canton.participant.sync.SynchronizerConnectionsManager.{ import com.digitalasset.canton.participant.synchronizer.* import com.digitalasset.canton.participant.topology.* import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker +import com.digitalasset.canton.platform.apiserver.services.command.TrafficEnforcementBackend import com.digitalasset.canton.platform.apiserver.services.command.interactive.CostEstimationHints import com.digitalasset.canton.protocol.* import com.digitalasset.canton.protocol.WellFormedTransaction.WithoutSuffixes @@ -175,6 +176,7 @@ class CantonSyncService( protected val loggerFactory: NamedLoggerFactory, testingConfig: TestingConfigInternal, val ledgerApiIndexer: LifeCycleContainer[LedgerApiIndexer], + trafficEnforcementBackendO: Option[Eval[TrafficEnforcementBackend]], connectedSynchronizersLookupContainer: ConnectedSynchronizersLookupContainer, )(implicit ec: ExecutionContextExecutor, mat: Materializer, val tracer: Tracer) extends state.SyncService @@ -208,6 +210,7 @@ class CantonSyncService( engine, commandProgressTracker, syncEphemeralStateFactory, + trafficEnforcementBackendO, clock, resourceManagementService, parameters, @@ -1013,11 +1016,12 @@ class CantonSyncService( SyncServiceError.SyncServiceUnknownSynchronizer.UnknownPhysicalSynchronizerId(psid) ) _ <- Either.cond( - configForPsid.status != Active, + configForPsid.status == Active, (), SyncServiceError.SyncServiceSynchronizerIsNotActive.Error( configForPsid.config.synchronizerAlias, Seq(configForPsid.configuredPsid -> configForPsid.status), + operation = "modify synchronizer", ), ) } yield KnownPhysicalSynchronizerId(psid) @@ -1353,12 +1357,14 @@ class CantonSyncService( def getSynchronizerConnectionConfigForAlias( synchronizerAlias: SynchronizerAlias, onlyActive: Boolean, + operation: String, )(implicit traceContext: TraceContext ): Either[SyncServiceError, StoredSynchronizerConnectionConfig] = connectionsManager.getSynchronizerConnectionConfigForAlias( synchronizerAlias, onlyActive = onlyActive, + operation = operation, ) /** Perform a handshake with the given synchronizer. Does only the static (protocol version, @@ -1875,6 +1881,7 @@ object CantonSyncService { ledgerApiIndexer: LifeCycleContainer[LedgerApiIndexer], connectedSynchronizersLookupContainer: ConnectedSynchronizersLookupContainer, triggerDeclarativeChange: () => Unit, + trafficEnforcementBackendO: Option[Eval[TrafficEnforcementBackend]], )(implicit ec: ExecutionContextExecutor, mat: Materializer, tracer: Tracer): CantonSyncService = { // Set initial replica state @@ -1910,6 +1917,7 @@ object CantonSyncService { loggerFactory, testingConfig, ledgerApiIndexer, + trafficEnforcementBackendO, connectedSynchronizersLookupContainer, ) syncService diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/ConnectedSynchronizer.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/ConnectedSynchronizer.scala index 0e96a485f9..4a5eafa6e3 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/ConnectedSynchronizer.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/ConnectedSynchronizer.scala @@ -76,6 +76,7 @@ import com.digitalasset.canton.participant.traffic.{ } import com.digitalasset.canton.participant.util.{DAMLe, TimeOfChange} import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker +import com.digitalasset.canton.platform.apiserver.services.command.TrafficEnforcementBackend import com.digitalasset.canton.platform.apiserver.services.command.interactive.CostEstimationHints import com.digitalasset.canton.protocol.* import com.digitalasset.canton.protocol.WellFormedTransaction.WithoutSuffixes @@ -170,6 +171,7 @@ class ConnectedSynchronizer( journalGarbageCollector: JournalGarbageCollector, val acsCommitmentProcessor: AcsCommitmentProcessor, clock: Clock, + trafficEnforcementBackendO: Option[Eval[TrafficEnforcementBackend]], promiseUSFactory: DefaultPromiseUnlessShutdownFactory, metrics: ConnectedSynchronizerMetrics, futureSupervisor: FutureSupervisor, @@ -293,6 +295,7 @@ class ConnectedSynchronizer( testingConfig = testingConfig, promiseUSFactory, parameters, + trafficEnforcementBackendO.map(_.value), ) private val unassignmentProcessor: UnassignmentProcessor = new UnassignmentProcessor( @@ -709,6 +712,7 @@ class ConnectedSynchronizer( .modify(_ ++ requiredFlagsForPV), serial = Some(existingSynchronizerTrustCertificate.serial.increment), signingKeys = Seq.empty, + namespacesToSignFor = Seq.empty, protocolVersion = protocolVersion, expectFullAuthorization = false, forceChanges = ForceFlags.none, @@ -1167,6 +1171,7 @@ class ConnectedSynchronizer( assignmentProcessor, badRootHashMessagesRequestProcessor, topologyProcessor, + topologyClient, topologyManager, ephemeral.timeTracker, // need to close time tracker before synchronizer handle, as it might otherwise send messages synchronizerHandle, @@ -1245,6 +1250,7 @@ object ConnectedSynchronizer { reassignmentCoordination: ReassignmentCoordination, commandProgressTracker: CommandProgressTracker, clock: Clock, + trafficEnforcementBackendO: Option[Eval[TrafficEnforcementBackend]], promiseUSFactory: DefaultPromiseUnlessShutdownFactory, connectedSynchronizerMetrics: ConnectedSynchronizerMetrics, futureSupervisor: FutureSupervisor, @@ -1272,6 +1278,7 @@ object ConnectedSynchronizer { reassignmentCoordination: ReassignmentCoordination, commandProgressTracker: CommandProgressTracker, clock: Clock, + trafficEnforcementBackendO: Option[Eval[TrafficEnforcementBackend]], promiseUSFactory: DefaultPromiseUnlessShutdownFactory, connectedSynchronizerMetrics: ConnectedSynchronizerMetrics, futureSupervisor: FutureSupervisor, @@ -1383,6 +1390,7 @@ object ConnectedSynchronizer { journalGarbageCollector, acsCommitmentProcessor, clock, + trafficEnforcementBackendO, promiseUSFactory, connectedSynchronizerMetrics, futureSupervisor, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/LogicalSynchronizerUpgrade.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/LogicalSynchronizerUpgrade.scala index fbbd67fc8d..ced2525ccf 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/LogicalSynchronizerUpgrade.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/LogicalSynchronizerUpgrade.scala @@ -815,7 +815,7 @@ sealed trait ManualLogicalSynchronizerUpgrade[Req <: ManualLsuRequest] ) case LogicalSynchronizerUpgrade.NewConfig(config) => - EitherT.pure(config.copy(synchronizerId = Some(successorPsid))) + EitherT.pure(config.copy(psid = Some(successorPsid))) } } @@ -1368,6 +1368,6 @@ object LogicalSynchronizerUpgrade { } } yield currentConfig.config.copy( sequencerConnections = newSequencerConnections, - synchronizerId = Some(successorPsid), + psid = Some(successorPsid), ) } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncServiceError.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncServiceError.scala index bfc4983f3c..9897b76d93 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncServiceError.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncServiceError.scala @@ -127,7 +127,7 @@ object SyncServiceError extends SyncServiceErrorGroup { final case class UnknownPhysicalSynchronizerId(psid: PhysicalSynchronizerId)(implicit val loggingContext: ErrorLoggingContext ) extends CantonError.Impl( - cause = s"The synchronizer with alias physical synchronizer id $psid is unknown." + cause = s"The synchronizer with physical synchronizer id $psid is unknown." ) with SyncServiceError } @@ -311,10 +311,12 @@ object SyncServiceError extends SyncServiceErrorGroup { final case class Error( synchronizerAlias: SynchronizerAlias, inactive: Seq[(ConfiguredPhysicalSynchronizerId, SynchronizerConnectionConfigStore.Status)], + operation: String, )(implicit val loggingContext: ErrorLoggingContext ) extends CantonError.Impl( - cause = s"$synchronizerAlias is not active and can therefore not be connected to." + cause = + s"$synchronizerAlias is not active which prevents operation `$operation` from being performed." ) with SyncServiceError } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SynchronizerConnectionsManager.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SynchronizerConnectionsManager.scala index 82edb0e0c7..86a6e6bd1e 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SynchronizerConnectionsManager.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SynchronizerConnectionsManager.scala @@ -64,6 +64,7 @@ import com.digitalasset.canton.participant.synchronizer.* import com.digitalasset.canton.participant.topology.* import com.digitalasset.canton.participant.topology.client.MissingKeysAlerter import com.digitalasset.canton.platform.apiserver.execution.CommandProgressTracker +import com.digitalasset.canton.platform.apiserver.services.command.TrafficEnforcementBackend import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.resource.DbExceptionRetryPolicy import com.digitalasset.canton.sequencing.SequencerConnectionValidation @@ -80,7 +81,6 @@ import com.digitalasset.canton.topology.client.{ import com.digitalasset.canton.tracing.{Spanning, TraceContext, Traced} import com.digitalasset.canton.util.* import com.digitalasset.canton.util.OptionUtils.OptionExtension -import com.digitalasset.canton.util.Thereafter.syntax.* import com.digitalasset.canton.util.retry.Backoff import com.digitalasset.daml.lf.engine.Engine import com.google.common.collect.{BiMap, HashBiMap} @@ -130,6 +130,7 @@ private[sync] class SynchronizerConnectionsManager( engine: Engine, commandProgressTracker: CommandProgressTracker, syncEphemeralStateFactory: SyncEphemeralStateFactory, + trafficEnforcementBackendO: Option[Eval[TrafficEnforcementBackend]], clock: Clock, resourceManagementService: ResourceManagementService, parameters: ParticipantNodeParameters, @@ -269,7 +270,7 @@ private[sync] class SynchronizerConnectionsManager( sequencerInfoLoader // TODO(i27622): use the connection pool to validate the config .validateSequencerConnection( config.synchronizerAlias, - config.synchronizerId, + config.psid, config.sequencerConnections, sequencerConnectionValidation, ) @@ -327,6 +328,7 @@ private[sync] class SynchronizerConnectionsManager( succeeded <- performSynchronizerConnectionOrHandshake( con, connectSynchronizer = ConnectSynchronizer.ReconnectSynchronizers, + skipStatusCheck = false, ).transform { case Left(SyncServiceFailedSynchronizerConnection(_, parent)) if ignoreFailures => // if the error is retryable, we'll reschedule an automatic retry so this synchronizer gets connected eventually @@ -489,7 +491,11 @@ private[sync] class SynchronizerConnectionsManager( EitherT .fromEither[FutureUnlessShutdown]( - getSynchronizerConnectionConfigForAlias(synchronizerAlias, onlyActive = true) + getSynchronizerConnectionConfigForAlias( + synchronizerAlias, + onlyActive = true, + operation = "connect", + ) ) .flatMap { _ => val initial = if (keepRetrying) { @@ -539,6 +545,7 @@ private[sync] class SynchronizerConnectionsManager( performSynchronizerConnectionOrHandshake( synchronizerAlias, connectSynchronizer, + skipStatusCheck = false, ).transform { case Left(SyncServiceError.SyncServiceFailedSynchronizerConnection(_, err)) if keepRetrying && err.retryable.nonEmpty => @@ -618,7 +625,7 @@ private[sync] class SynchronizerConnectionsManager( /** Get the synchronizer connection corresponding to the alias. Fail if no connection can be * found. If `onlyActive` is true, enforces that exactly one active connection exists, otherwise * throws an invalid state exception. If `onlyActive` is false and multiple connections exist, - * takes the one with the highest PSID. + * takes the one with the highest psid. * * @param synchronizerAlias * Synchronizer alias @@ -628,6 +635,7 @@ private[sync] class SynchronizerConnectionsManager( def getSynchronizerConnectionConfigForAlias( synchronizerAlias: SynchronizerAlias, onlyActive: Boolean, + operation: String, )(implicit traceContext: TraceContext ): Either[SyncServiceError, StoredSynchronizerConnectionConfig] = @@ -643,7 +651,11 @@ private[sync] class SynchronizerConnectionsManager( case Nil => Left( SyncServiceError.SyncServiceSynchronizerIsNotActive - .Error(synchronizerAlias, inactive.map(c => (c.configuredPsid, c.status))) + .Error( + synchronizerAlias, + inactive.map(c => (c.configuredPsid, c.status)), + operation = operation, + ) ) case single :: Nil => Right(single) @@ -659,11 +671,19 @@ private[sync] class SynchronizerConnectionsManager( } /** MUST be synchronized using the [[connectQueue]] + * + * @param synchronizerAlias + * Alias of the synchronizer + * @param connectSynchronizer + * Operation to be performed + * @param skipStatusCheck + * If true, allows to connect/handshake with an inactive synchronizer. Should be false except + * for Hard Domain Migration. */ def performSynchronizerConnectionOrHandshake( synchronizerAlias: SynchronizerAlias, connectSynchronizer: ConnectSynchronizer, - skipStatusCheck: Boolean = false, + skipStatusCheck: Boolean, )(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, SyncServiceError, PhysicalSynchronizerId] = @@ -719,6 +739,7 @@ private[sync] class SynchronizerConnectionsManager( getSynchronizerConnectionConfigForAlias( synchronizerAlias, onlyActive = !skipStatusCheck, + operation = "handshake", ) ) _ = logger.debug( @@ -786,7 +807,10 @@ private[sync] class SynchronizerConnectionsManager( s"Performing handshake with synchronizer with id ${synchronizerConnectionConfig.configuredPsid} and config: ${synchronizerConnectionConfig.config}" ) connectionInfo <- EitherT( - synchronizerRegistry.pureHandshake(synchronizerConnectionConfig) + synchronizerRegistry.pureHandshake( + synchronizerConnectionConfig, + lsuHandshakeConfig = Option.when(isLsu)(parameters.lsuConfig.handshake), + ) ) .leftMap[SyncServiceError](err => SyncServiceError.SyncServiceFailedSynchronizerConnection( @@ -880,95 +904,96 @@ private[sync] class SynchronizerConnectionsManager( } yield logger.info(s"Successfully performed pending LSU operation for $successorPsid") } - /** Used to synchronize handshake loops per successor physical synchronizer id */ - private val lsuHandshakeLoops = new TrieMap[PhysicalSynchronizerId, PromiseUnlessShutdown[ - Either[SyncServiceError, Option[StaticSynchronizerParameters]] - ]]() + /** Used to chain handshakes per successor physical synchronizer id */ + private val lsuHandshakesQueue = + new NonGarbageCollectedShardedSequentialProcessingQueue[PhysicalSynchronizerId]( + name = "lsu-handshake-successor", + futureSupervisor = futureSupervisor, + timeouts = timeouts, + loggerFactory = loggerFactory, + logTaskTiming = false, + failureMode = FailureMode.ContinueAfterFailure, + ) /** Performs handshake with the successor synchronizer. Retry until the handshake is successful. */ - def performLsuHandshakeWithRetries( + private def performLsuHandshakeWithRetries( initialPendingOperation: PendingOperation[PendingLsuOperation, PhysicalSynchronizerId] )(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, SyncServiceError, Option[StaticSynchronizerParameters]] = { val successorPsid = initialPendingOperation.operation.successorPsid - syncPersistentStateManager.get(successorPsid) match { - case Some(state) => - logger.info("Static synchronizer parameters found. Handshake is not needed.") - EitherT.pure(Some(state.staticSynchronizerParameters)) + /* + Transform the result to abort if the operation is not needed anymore. + Recall that retries are stopped on a Right + */ + def transformError( + error: SyncServiceError + ): EitherT[FutureUnlessShutdown, SyncServiceError, Option[StaticSynchronizerParameters]] = + pendingLsuOperationsStore + .get( + initialPendingOperation.synchronizer, + initialPendingOperation.key, + initialPendingOperation.name, + ) + .value + .map { + case Some(`initialPendingOperation`) => + val isRetryable = error.retryable.isDefined - case None => - val newPromise = PromiseUnlessShutdown - .unsupervised[Either[SyncServiceError, Option[StaticSynchronizerParameters]]]() - lsuHandshakeLoops.putIfAbsent(successorPsid, newPromise) match { - case Some(existingPromise) => - EitherT(existingPromise.futureUS) - - case None => - Backoff - .fromConfig( - logger = logger, - hasSynchronizeWithClosing = this, - config = parameters.lsuConfig.handshakeRetry, - operationName = s"lsu-handshake-with-$successorPsid", + // e.g., transient network or pool errors + if (isRetryable) { + logger.info( + s"Unable to perform handshake with $successorPsid: $error. Will retry." ) - .unlessShutdown( - performPureHandshake(successorPsid, isLsu = true) - .map(Some(_)) - /* - Transform the result to abort if the operation is not needed anymore. - Recall that retries are stopped on a Right - */ - .leftFlatMap { error => - pendingLsuOperationsStore - .get( - initialPendingOperation.synchronizer, - initialPendingOperation.key, - initialPendingOperation.name, - ) - .value - .map { - case Some(`initialPendingOperation`) => - val isRetryable = error.retryable.isDefined - - // e.g., transient network or pool errors - if (isRetryable) { - logger.info( - s"Unable to perform handshake with $successorPsid: $error. Will retry." - ) - Left(error) // Retry - } else { - logger.warn( - s"Unable to perform handshake with $successorPsid: $error. Error is not retryable." - ) - Right(Option.empty[StaticSynchronizerParameters]) - } - - case Some(other) => // The pending operation is different. Possibly a cancellation followed by a new LSU. - logger.info( - s"Found $other pending LSU operation that is different from the initial $initialPendingOperation. Not retrying." - ) - - Right(Option.empty[StaticSynchronizerParameters]) - - case None => // LSU was cancelled or the operation was completed elsewhere - logger.info( - s"No pending LSU operation found for ${initialPendingOperation.synchronizer}. Not retrying." - ) - - Right(Option.empty[StaticSynchronizerParameters]) - } - .pipe(EitherT(_)) - } - .value, - DbExceptionRetryPolicy, + Left(error) // Retry + } else { + logger.warn( + s"Unable to perform handshake with $successorPsid: $error. Error is not retryable." ) - .pipe(EitherT(_)) - .thereafter(_ => lsuHandshakeLoops.remove(successorPsid).discard) + Right(Option.empty[StaticSynchronizerParameters]) + } + + // The pending operation is different. Possibly a cancellation followed by a new LSU. + case Some(other) => + logger.info( + s"Found $other pending LSU operation that is different from the initial $initialPendingOperation. Not retrying." + ) + + Right(Option.empty[StaticSynchronizerParameters]) + + case None => // LSU was cancelled or the operation was completed elsewhere + logger.info( + s"No pending LSU operation found for ${initialPendingOperation.synchronizer}. Not retrying." + ) + + Right(Option.empty[StaticSynchronizerParameters]) } - } + .pipe(EitherT(_)) + + def task() + : EitherT[FutureUnlessShutdown, SyncServiceError, Option[StaticSynchronizerParameters]] = + Backoff + .fromConfig( + logger = logger, + hasSynchronizeWithClosing = this, + config = parameters.lsuConfig.handshake.retry, + operationName = s"lsu-handshake-with-$successorPsid", + ) + .unlessShutdown( + performPureHandshake(successorPsid, isLsu = true) + .map(Some(_)) + .leftFlatMap(transformError) + .value, + DbExceptionRetryPolicy, + ) + .pipe(EitherT(_)) + + lsuHandshakesQueue.executeEUS(successorPsid)( + task(), + s"lsu-handshake-with-$successorPsid", + ) } /** Connect the sync service to the given synchronizer. */ @@ -1017,6 +1042,7 @@ private[sync] class SynchronizerConnectionsManager( getSynchronizerConnectionConfigForAlias( synchronizerAlias, onlyActive = !skipStatusCheck, + operation = "connect", ) ) _ = logger.debug( @@ -1170,6 +1196,7 @@ private[sync] class SynchronizerConnectionsManager( reassignmentCoordination, commandProgressTracker, clock, + trafficEnforcementBackendO, promiseUSFactory, connectedSynchronizerMetrics, futureSupervisor, @@ -1530,6 +1557,7 @@ private[sync] class SynchronizerConnectionsManager( ephemeralHealth, sequencerClientHealth, acsCommitmentProcessorHealth, + lsuHandshakesQueue, ) LifeCycle.close(instances*)(logger) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SynchronizerMigration.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SynchronizerMigration.scala index 8cd98b03ee..dabdb636b0 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SynchronizerMigration.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SynchronizerMigration.scala @@ -136,7 +136,7 @@ class SynchronizerMigration( sourceConnection <- EitherT.fromEither[FutureUnlessShutdown](sourceConnectionE).map(Source(_)) // check that synchronizer id (in config) matches observed synchronizer id - _ <- target.unwrap.synchronizerId.traverse_ { expectedSynchronizerId => + _ <- target.unwrap.psid.traverse_ { expectedSynchronizerId => EitherT.cond[FutureUnlessShutdown]( expectedSynchronizerId.logical == targetSynchronizerId.unwrap, (), @@ -200,7 +200,7 @@ class SynchronizerMigration( sequencerInfoLoader .loadAndAggregateSequencerEndpoints( synchronizerConnectionConfig.synchronizerAlias, - synchronizerConnectionConfig.synchronizerId, + synchronizerConnectionConfig.psid, synchronizerConnectionConfig.sequencerConnections, SequencerConnectionValidation.Active, )(traceContext, CloseContext(this)) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerConnectionConfig.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerConnectionConfig.scala index c321e99bfd..8afe83418d 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerConnectionConfig.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerConnectionConfig.scala @@ -31,7 +31,7 @@ import monocle.syntax.all.* * could be provided for each individual sequencer. * @param manualConnect * if set to true (default false), the synchronizer is not connected automatically on startup. - * @param synchronizerId + * @param psid * if the synchronizer id is known, then it can be passed as an argument. during the handshake, * the participant will check that the synchronizer id on the remote port is indeed the one given * in the configuration. the synchronizer id can not be faked by a synchronizer. therefore, this @@ -57,7 +57,7 @@ final case class SynchronizerConnectionConfig( sequencerConnections: SequencerConnections, manualConnect: Boolean = false, // TODO(#26021) Consider accepting both lsid and psid - synchronizerId: Option[PhysicalSynchronizerId] = None, + psid: Option[PhysicalSynchronizerId] = None, priority: Int = 0, initialRetryDelay: Option[NonNegativeFiniteDuration] = None, maxRetryDelay: Option[NonNegativeFiniteDuration] = None, @@ -104,7 +104,7 @@ final case class SynchronizerConnectionConfig( val unknownAliases = otherAliasToConnection.keySet.diff(sequencerConnections.aliasToConnection.keySet) for { - updatedSynchronizerId <- mergeOrRequireEqual(synchronizerId, otherSynchronizerId) + updatedSynchronizerId <- mergeOrRequireEqual(psid, otherSynchronizerId) _ <- Either.cond( unknownAliases.isEmpty, (), @@ -157,7 +157,7 @@ final case class SynchronizerConnectionConfig( sequencerConnections.sequencerConnectionPoolDelays, ) } yield this.copy( - synchronizerId = updatedSynchronizerId, + psid = updatedSynchronizerId, sequencerConnections = updatedSequencerConnections, ) case _ => @@ -178,9 +178,9 @@ final case class SynchronizerConnectionConfig( override protected def pretty: Pretty[SynchronizerConnectionConfig] = prettyOfClass( param("synchronizer", _.synchronizerAlias), + paramIfDefined("physicalSynchronizerId", _.psid), param("sequencerConnections", _.sequencerConnections), param("manualConnect", _.manualConnect), - paramIfDefined("physicalSynchronizerId", _.synchronizerId), paramIfDefined("priority", x => Option.when(x.priority != 0)(x.priority)), paramIfDefined("initialRetryDelay", _.initialRetryDelay), paramIfDefined("maxRetryDelay", _.maxRetryDelay), @@ -197,7 +197,7 @@ final case class SynchronizerConnectionConfig( synchronizerAlias = synchronizerAlias.unwrap, sequencerConnections = sequencerConnections.toProtoV30.some, manualConnect = manualConnect, - physicalSynchronizerId = synchronizerId.fold("")(_.toProtoPrimitive), + physicalSynchronizerId = psid.fold("")(_.toProtoPrimitive), priority = priority, initialRetryDelay = initialRetryDelay.map(_.toProtoPrimitive), maxRetryDelay = maxRetryDelay.map(_.toProtoPrimitive), @@ -240,7 +240,7 @@ object SynchronizerConnectionConfig sequencerConnections <- ProtoConverter .required("sequencerConnections", sequencerConnectionsPO) .flatMap(SequencerConnections.fromProtoV30) - synchronizerId <- OptionUtil + psidO <- OptionUtil .emptyStringAsNone(synchronizerId) .traverse(PhysicalSynchronizerId.fromProtoPrimitive(_, "physical_synchronizer_id")) initialRetryDelay <- initialRetryDelayP.traverse( @@ -258,7 +258,7 @@ object SynchronizerConnectionConfig alias, sequencerConnections, manualConnect, - synchronizerId, + psidO, priority, initialRetryDelay, maxRetryDelay, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerRegistry.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerRegistry.scala index 0e27ed3e3d..8d69e841e3 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerRegistry.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerRegistry.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.error.* import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.ErrorLoggingContext import com.digitalasset.canton.networking.grpc.GrpcError +import com.digitalasset.canton.participant.config.LsuHandshake import com.digitalasset.canton.participant.store.{ StoredSynchronizerConnectionConfig, SyncPersistentState, @@ -55,12 +56,16 @@ trait SynchronizerRegistry extends AutoCloseable { /** Performs the handshake with the synchronizer. * + * @param lsuHandshakeConfig + * If the handshake is with the successor in the context of an LSU, config for the handshake. + * None for regular handshake. * @return * The aggregate information of the sequencers and the updated list of sequencer connections * (with sequencer ids set). */ def pureHandshake( - storedConfig: StoredSynchronizerConnectionConfig + storedConfig: StoredSynchronizerConnectionConfig, + lsuHandshakeConfig: Option[LsuHandshake], )(implicit traceContext: TraceContext ): FutureUnlessShutdown[ diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerRegistryHelpers.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerRegistryHelpers.scala index 963d2fcbb7..1826a64f57 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerRegistryHelpers.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/SynchronizerRegistryHelpers.scala @@ -360,7 +360,7 @@ trait SynchronizerRegistryHelpers extends FlagCloseable with NamedLogging with H traceContext: TraceContext, ): EitherT[FutureUnlessShutdown, SynchronizerRegistryError, Unit] = synchronizeWithClosing("check-for-synchronizer-topology-initialization")( - EitherT.right[SynchronizerRegistryError](connectivityStatusStore.isTopologyInitialized) + EitherT.right[SynchronizerRegistryError](connectivityStatusStore.isTopologyInitialized()) ).flatMap { case true => EitherT.right[SynchronizerRegistryError](FutureUnlessShutdown.unit) @@ -386,7 +386,7 @@ trait SynchronizerRegistryHelpers extends FlagCloseable with NamedLogging with H )(implicit loggingContext: ErrorLoggingContext ): Either[SynchronizerIdMismatch.Error, Unit] = - config.synchronizerId match { + config.psid match { case None => Either.unit case Some(configuredSynchronizerId) => Either.cond( @@ -521,24 +521,17 @@ object SynchronizerRegistryHelpers { predecessorSyncStateO .traverse_ { case (predecessor, predecessorSyncState) => for { - isTopologyInitialized <- persistentState.connectivityStatusStore.isTopologyInitialized + isTopologyInitialized <- persistentState.connectivityStatusStore.isTopologyInitialized() shouldCopyTopology = !isTopologyInitialized && !predecessor.isLateUpgrade _ <- if (shouldCopyTopology) { - loggingContext.info( - s"LSU to ${persistentState.psid.suffix}: About to copy topology" - ) - for { _ <- persistentState.topologyStore .copyFromPredecessorSynchronizerStore( predecessorSyncState.topologyStore ) - _ = loggingContext.info( - s"LSU to ${persistentState.psid.suffix}: Done copying topology" - ) _ <- persistentState.connectivityStatusStore.setTopologyInitialized() } yield () } else { diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/grpc/GrpcSynchronizerRegistry.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/grpc/GrpcSynchronizerRegistry.scala index 118bb30fd4..1ce6fc39cc 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/grpc/GrpcSynchronizerRegistry.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/synchronizer/grpc/GrpcSynchronizerRegistry.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.participant.synchronizer.grpc +import cats.Monad import cats.data.EitherT import cats.syntax.either.* import com.daml.grpc.adapter.ExecutionSequencerFactory @@ -17,10 +18,11 @@ import com.digitalasset.canton.crypto.{ SyncCryptoApiParticipantProvider, SynchronizerCryptoClient, } -import com.digitalasset.canton.data.SynchronizerPredecessor +import com.digitalasset.canton.data.{CantonTimestamp, SynchronizerPredecessor} import com.digitalasset.canton.lifecycle.* import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.participant.ParticipantNodeParameters +import com.digitalasset.canton.participant.config.LsuHandshake import com.digitalasset.canton.participant.metrics.ParticipantMetrics import com.digitalasset.canton.participant.store.memory.PackageMetadataView import com.digitalasset.canton.participant.store.{ @@ -46,12 +48,12 @@ import com.digitalasset.canton.sequencing.client.{ ReplayConfig, RichSequencerClient, } -import com.digitalasset.canton.time.Clock +import com.digitalasset.canton.time.{Clock, WallClock} import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.client.SynchronizerTopologyClientWithInit import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.ErrorUtil import com.digitalasset.canton.util.Thereafter.syntax.ThereafterAsyncOps +import com.digitalasset.canton.util.{EitherTUtil, ErrorUtil} import com.digitalasset.canton.version.ProtocolVersionCompatibility import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer @@ -96,6 +98,9 @@ class GrpcSynchronizerRegistry( with HasFutureSupervision with NamedLogging { + // Used to control the retry loop/timeout for handshake + private val wallClock = new WallClock(timeouts, loggerFactory) + override protected def timeouts: ProcessingTimeout = participantNodeParameters.processingTimeouts private class GrpcSynchronizerHandle( @@ -226,7 +231,9 @@ class GrpcSynchronizerRegistry( .connectedSynchronizerMetrics(storedConfig.config.synchronizerAlias) .sequencerClient .connectionPool, - metricsContext = MetricsContext.Empty, + metricsContext = storedConfig.configuredPsid.toOption + .map(psid => MetricsContext("psid" -> psid.toProtoPrimitive)) + .getOrElse(MetricsContext.Empty), futureSupervisor = futureSupervisor, timeouts = timeouts, loggerFactory = synchronizerLoggerFactory, @@ -235,7 +242,7 @@ class GrpcSynchronizerRegistry( connectionPoolFactory .createFromOldConfig( sequencerConnections = storedConfig.config.sequencerConnections, - expectedPsidO = storedConfig.config.synchronizerId, + expectedPsidO = storedConfig.config.psid, tracingConfig = participantNodeParameters.tracing, name = "main", ) @@ -360,20 +367,65 @@ class GrpcSynchronizerRegistry( } yield info override def pureHandshake( - storedConfig: StoredSynchronizerConnectionConfig + storedConfig: StoredSynchronizerConnectionConfig, + lsuHandshakeConfig: Option[LsuHandshake], )(implicit traceContext: TraceContext ): FutureUnlessShutdown[ Either[SynchronizerRegistryError, SequencerAggregatedInfo] ] = { + val expectedSequencers = storedConfig.config.sequencerConnections.aliasToConnection.keySet + val connectionPoolE = getConnectionPool(storedConfig) - connectHandshakeGeneric( - connectionPoolE, - storedConfig.config, - storedConfig.predecessor, - ).thereafter { _ => - connectionPoolE.foreach(_.close()) - }.value + /* + Wait until one of the following conditions is met: + - all sequencers in the config observed in the pool + - waitUntil time is reached + - service is closing + */ + def waiter( + connectionPool: SequencerConnectionPool, + waitUntil: CantonTimestamp, + step: config.NonNegativeFiniteDuration, + ): FutureUnlessShutdown[Unit] = { + val sequencersInPool = connectionPool.getAllSequencerIds.keySet + + def check(): Either[Unit, Unit] = + if (expectedSequencers.subsetOf(sequencersInPool)) + logger.debug(s"Stopping the wait: all $expectedSequencers found in the pool").asRight + else if (wallClock.now >= waitUntil) + logger.debug("Stopping the wait because max waiting time is reached.").asRight + else if (isClosing) + logger.debug("Stopping the wait because of shutdown.").asRight + else ().asLeft + + Monad[FutureUnlessShutdown].tailRecM[Unit, Unit](()) { _ => + wallClock.scheduleAfter(_ => check(), step.asJava) + } + } + + (for { + connectionPool <- connectionPoolE.toEitherT[FutureUnlessShutdown] + + res <- connectHandshakeGeneric( + connectionPoolE, + storedConfig.config, + storedConfig.predecessor, + ) + + _ <- lsuHandshakeConfig match { + case Some(LsuHandshake(_, Some(minimumDuration), periodicCheck)) => + val waitUntil = wallClock.now.plus(minimumDuration.asJava) + + logger.debug(s"Handshake was successful. Starting to wait until $waitUntil") + EitherT.right[SynchronizerRegistryError]( + waiter(connectionPool, waitUntil, step = periodicCheck) + ) + + case _ => EitherTUtil.unitUS[SynchronizerRegistryError] + } + + } yield res).thereafter(_ => connectionPoolE.foreach(_.close())).value } } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/PackageOps.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/PackageOps.scala index 91cf1f6a74..2a83f91c97 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/PackageOps.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/PackageOps.scala @@ -42,7 +42,7 @@ import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.topology.transaction.* import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.{ContinueAfterFailure, SimpleExecutionQueue} +import com.digitalasset.canton.util.{FailureMode, SimpleExecutionQueue} import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.{LfPackageId, config} import com.digitalasset.daml.lf.data.Ref.PackageId @@ -132,7 +132,7 @@ class PackageOpsImpl( timeouts, loggerFactory, logTaskTiming = false, - failureMode = ContinueAfterFailure, + failureMode = FailureMode.ContinueAfterFailure, ) override def checkPackageUnused(packageId: PackageId)(implicit @@ -519,6 +519,7 @@ class PackageOpsImpl( mapping = mapping, serial = Some(nextSerial), signingKeys = Seq.empty, + namespacesToSignFor = Seq.empty, protocolVersion = initialProtocolVersion, expectFullAuthorization = true, forceChanges = forceFlags, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyDispatcher.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyDispatcher.scala index c94062e128..d324eb834b 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyDispatcher.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyDispatcher.scala @@ -202,6 +202,7 @@ class ParticipantTopologyDispatcher( ), serial = None, signingKeys = Seq.empty, + namespacesToSignFor = Seq.empty, protocolVersion = state.staticSynchronizerParameters.protocolVersion, expectFullAuthorization = true, waitToBecomeEffective = None, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyValidation.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyValidation.scala index e5e2851707..8360169103 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyValidation.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyValidation.scala @@ -33,6 +33,7 @@ import com.digitalasset.canton.topology.{ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.MonadUtil import com.digitalasset.canton.util.ShowUtil.* +import com.digitalasset.canton.version.ProtocolVersion import scala.concurrent.ExecutionContext @@ -46,6 +47,7 @@ trait ParticipantTopologyValidation extends NamedLogging { dryRunSnapshot: PackageMetadata, forceFlags: ForceFlags, disableUpgradeValidation: Boolean, + protocolVersion: ProtocolVersion, )(implicit traceContext: TraceContext, ec: ExecutionContext, @@ -63,6 +65,8 @@ trait ParticipantTopologyValidation extends NamedLogging { packageMetadataSnapshot, dryRunSnapshot, forceFlags, + // Protocol versions 35 and above do not require anymore that a vetted package's dependencies are themselves vetted as well + checkDependenciesVetting = protocolVersion <= ProtocolVersion.v34, ) _ <- EitherT.fromEither[FutureUnlessShutdown] { if ( @@ -256,6 +260,7 @@ trait ParticipantTopologyValidation extends NamedLogging { packageMetadataSnapshot: PackageMetadata, dryRunSnapshot: PackageMetadata, forceFlags: ForceFlags, + checkDependenciesVetting: Boolean, )(implicit traceContext: TraceContext, ec: ExecutionContext, @@ -291,7 +296,10 @@ trait ParticipantTopologyValidation extends NamedLogging { val unvettedDeps = (dependenciesOfAdded -- vettedPackagesTarget) ++ removedDeps if (unknownToBeAdded.nonEmpty && !forceFlags.permits(ForceFlag.AllowUnknownPackage)) Left(CannotVetDueToMissingPackages.Missing(unknownToBeAdded)) - else if (unvettedDeps.nonEmpty && !forceFlags.permits(ForceFlag.AllowUnvettedDependencies)) + else if ( + checkDependenciesVetting && !forceFlags + .permits(ForceFlag.AllowUnvettedDependencies) && unvettedDeps.nonEmpty + ) Left(DependenciesNotVetted.Reject(unvettedDeps)) else Right(()) }) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/PartyOps.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/PartyOps.scala index 24ddcb488e..9a134df383 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/PartyOps.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/PartyOps.scala @@ -128,6 +128,7 @@ object PartyOps { updatedPTP, serial = nextSerial, signingKeys = Seq.empty, + namespacesToSignFor = Seq.empty, synchronizerId.protocolVersion, expectFullAuthorization = true, waitToBecomeEffective = None, @@ -187,7 +188,8 @@ object PartyOps { topologyManager .extendSignature( signed, - Seq(participantId.fingerprint), + signingKeys = Seq.empty, + namespacesToSignFor = Seq(participantId.namespace), ForceFlags.none, ) .map(Some(_)) @@ -199,7 +201,8 @@ object PartyOps { op = TopologyChangeOp.Replace, mapping = unsigned.mapping, serial = Some(unsigned.serial), - signingKeys = Seq(participantId.fingerprint), + signingKeys = Seq.empty, + namespacesToSignFor = Seq(participantId.namespace), protocolVersion = synchronizerId.protocolVersion, expectFullAuthorization = false, waitToBecomeEffective = None, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/SequencerConnectionSuccessorListener.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/SequencerConnectionSuccessorListener.scala index 6e0bb56a93..aed83eda04 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/SequencerConnectionSuccessorListener.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/SequencerConnectionSuccessorListener.scala @@ -186,6 +186,10 @@ class SequencerConnectionSuccessorListener( .handshakeWithSuccessor(pendingOperation) .value .flatMap { + /* + This branch is usually not expected to run because handshakeWithSuccessor above is + retried until a right is returned by default. This can be changed through config. + */ case Left(error) => val isRetryable = error.retryable.isDefined @@ -243,7 +247,7 @@ class SequencerConnectionSuccessorListener( syncPersistentStateManager, metrics, ) - } yield logger.info(s"Successfully copied topology from predecessor to $successorPsid")) + } yield ()) .valueOr { error => logger.warn(s"Failed to copy topology from predecessor to $successorPsid: $error") } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyComponentFactory.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyComponentFactory.scala index 1f66a78953..5e8539a004 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyComponentFactory.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyComponentFactory.scala @@ -205,12 +205,13 @@ class TopologyComponentFactory( traceContext: TraceContext ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = validatePackageVetting( - currentlyVettedPackages, - nextPackageIds, - packageMetadataView, - dryRunSnapshot.getOrElse(PackageMetadata()), - forceFlags, - disableUpgradeValidation, + currentlyVettedPackages = currentlyVettedPackages, + nextPackageIds = nextPackageIds, + packageMetadataView = packageMetadataView, + dryRunSnapshot = dryRunSnapshot.getOrElse(PackageMetadata()), + forceFlags = forceFlags, + disableUpgradeValidation = disableUpgradeValidation, + protocolVersion = store.protocolVersion, ) override def checkCannotDisablePartyWithActiveContracts( diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyLookup.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyLookup.scala index 5864190753..35678e0927 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyLookup.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyLookup.scala @@ -95,12 +95,11 @@ final class TopologyLookup( EitherT.pure[FutureUnlessShutdown, ParticipantTopologyManagerError](psid) } - snapshot <- topologyClientFor(psid).biflatMap( - _ => offlineTopologyClient(psid).map(_.approximateTimestamp), + snapshot <- topologyClientO(psid).fold(offlineTopologyClient(psid).map(_.approximateTimestamp))( topologyClient => EitherT.rightT[FutureUnlessShutdown, ParticipantTopologyManagerError]( topologyClient.approximateTimestamp - ), + ) ) } yield snapshot @@ -148,10 +147,9 @@ final class TopologyLookup( case psid: PhysicalSynchronizerId => EitherT.pure[FutureUnlessShutdown, ParticipantTopologyManagerError](psid) } - client <- topologyClientFor(psid).biflatMap( - _ => offlineTopologyClient(psid), - topologyClient => - EitherT.pure[FutureUnlessShutdown, ParticipantTopologyManagerError](topologyClient), + + client <- topologyClientO(psid).fold(offlineTopologyClient(psid))( + EitherT.pure[FutureUnlessShutdown, ParticipantTopologyManagerError](_) ) } yield client @@ -208,22 +206,4 @@ final class TopologyLookup( TopologyManagerError.TopologyStoreUnknown.Failure(SynchronizerStore(psid)) ) ) - - /** Returns the topology manager for the given psid. Fails if the node is not connected to the - * synchronizer. - */ - private def topologyClientFor(psid: PhysicalSynchronizerId)(implicit - traceContext: TraceContext, - ec: ExecutionContext, - ): EitherT[ - FutureUnlessShutdown, - ParticipantTopologyManagerError, - SynchronizerTopologyClient, - ] = - EitherT.fromOption[FutureUnlessShutdown]( - topologyClientO(psid), - ParticipantTopologyManagerError.IdentityManagerParentError( - TopologyManagerError.TopologyStoreUnknown.Failure(SynchronizerStore(psid)) - ), - ) } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/traffic/TrafficCostEstimator.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/traffic/TrafficCostEstimator.scala index 5d9204e4ee..b2dd9a1389 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/traffic/TrafficCostEstimator.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/traffic/TrafficCostEstimator.scala @@ -472,7 +472,7 @@ object TrafficCostEstimator { // compute its cost. SignatureDelegationValidityPeriod( approximateTimestampForSigning.getOrElse(topologySnapshot.timestamp), - SessionSigningKeysConfig.default.keyValidityDuration, + SessionSigningKeysConfig.enabled.keyValidityDuration, ), signature, ) diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/PackageOpsTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/PackageOpsTest.scala index 323adcb0cb..151fe08e3c 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/PackageOpsTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/PackageOpsTest.scala @@ -263,6 +263,7 @@ class PackageOpsTest extends PackageOpsTestBase { any[TopologyMapping], any[Option[PositiveInt]], any[Seq[Fingerprint]], + any[Seq[Namespace]], any[ProtocolVersion], anyBoolean, any[ForceFlags], @@ -320,6 +321,7 @@ class PackageOpsTest extends PackageOpsTestBase { any[TopologyMapping], any[Option[PositiveInt]], any[Seq[Fingerprint]], + any[Seq[Namespace]], any[ProtocolVersion], anyBoolean, any[ForceFlags], @@ -466,6 +468,7 @@ class PackageOpsTest extends PackageOpsTestBase { ), eqTo(Some(txSerial.tryAdd(1))), eqTo(Seq.empty), + eqTo(Seq.empty), eqTo(testedProtocolVersion), eqTo(true), eqTo(ForceFlags.none), diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/party/PartyOnboardingClearanceSchedulerTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/party/PartyOnboardingClearanceSchedulerTest.scala index 59ec9b3b0b..5a0978ebb7 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/party/PartyOnboardingClearanceSchedulerTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/party/PartyOnboardingClearanceSchedulerTest.scala @@ -101,7 +101,6 @@ class PartyOnboardingClearanceSchedulerTest private lazy val testData = new TopologyStoreTestData( testedProtocolVersion, loggerFactory, - this.directExecutionContext, ) private lazy val partyId = testData.party1 private lazy val participantId = testData.p1Id diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/party/PartyReplicationTopologyWorkflowTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/party/PartyReplicationTopologyWorkflowTest.scala index 31382b0e55..aa818a67a0 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/party/PartyReplicationTopologyWorkflowTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/party/PartyReplicationTopologyWorkflowTest.scala @@ -37,6 +37,7 @@ import com.digitalasset.canton.topology.transaction.{ } import com.digitalasset.canton.topology.{ ForceFlags, + Namespace, ParticipantId, PartyId, SynchronizerId, @@ -118,7 +119,7 @@ class PartyReplicationTopologyWorkflowTest ) private val topologyStoreTestData = - new TopologyStoreTestData(testedProtocolVersion, loggerFactory, executionContext) + new TopologyStoreTestData(testedProtocolVersion, loggerFactory) private def topologyWorkflow(p: ParticipantId = tp): PartyReplicationTopologyWorkflow = new PartyReplicationTopologyWorkflow( @@ -196,6 +197,7 @@ class PartyReplicationTopologyWorkflowTest mapping = ptpProposal, serial = Some(serial), signingKeys = Seq.empty, + namespacesToSignFor = Seq(params.targetParticipantId.namespace), protocolVersion = testedProtocolVersion, expectFullAuthorization = false, forceChanges = ForceFlags.none, @@ -236,6 +238,7 @@ class PartyReplicationTopologyWorkflowTest topologyManager.extendSignature( any[SignedTopologyTransaction[TopologyChangeOp.Replace, PartyToParticipant]], signingKeys = eqTo(Seq.empty), + namespacesToSignFor = eqTo(Seq(params.targetParticipantId.namespace)), eqTo(ForceFlags.none), )(anyTraceContext) ).thenReturn( @@ -349,19 +352,27 @@ class PartyReplicationTopologyWorkflowTest mapping = ptpProposalMissingOnboardingFlag, serial = Some(serial), signingKeys = Seq.empty, + namespacesToSignFor = Seq(params.targetParticipantId.namespace), protocolVersion = testedProtocolVersion, expectFullAuthorization = true, forceChanges = ForceFlags.none, waitToBecomeEffective = None, ) - ).thenAnswer[TopologyChangeOp, TopologyMapping, Option[PositiveInt], Seq[ - Fingerprint - ], ProtocolVersion, Boolean, ForceFlags, Option[NonNegativeFiniteDuration]] { - case (_, mapping, _, _, _, _, _, _) => - // Have the topology manager mock store the transaction in test topology store. - EitherT.right[TopologyManagerError]( - add(topologyStore)(tsSerial, serial, mapping) - ) + ).thenAnswer[ + TopologyChangeOp, + TopologyMapping, + Option[PositiveInt], + Seq[Fingerprint], + Seq[Namespace], + ProtocolVersion, + Boolean, + ForceFlags, + Option[NonNegativeFiniteDuration], + ] { case (_, mapping, _, _, _, _, _, _, _) => + // Have the topology manager mock store the transaction in test topology store. + EitherT.right[TopologyManagerError]( + add(topologyStore)(tsSerial, serial, mapping) + ) } for { diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessorTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessorTest.scala index 576c9370fd..ed2ab2f2e0 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessorTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessorTest.scala @@ -64,6 +64,7 @@ import com.digitalasset.canton.protocol.Phase37Processor.PublishUpdateViaRecordO import com.digitalasset.canton.protocol.messages.* import com.digitalasset.canton.resource.MemoryStorage import com.digitalasset.canton.sequencing.client.SendResult.Success +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.client.{ SendAsyncClientError, @@ -160,6 +161,7 @@ class ProtocolProcessorTest any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = any[Boolean], )(anyTraceContext, any[MetricsContext]) @@ -492,6 +494,7 @@ class ProtocolProcessorTest any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = any[Boolean], )(anyTraceContext, any[MetricsContext]) diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/SubmissionTrackerTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/SubmissionTrackerTest.scala index 6b33a91d1c..8f78f28106 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/SubmissionTrackerTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/SubmissionTrackerTest.scala @@ -9,7 +9,7 @@ import com.digitalasset.canton.crypto.TestHash import com.digitalasset.canton.data.{CantonTimestamp, SubmissionTrackerData} import com.digitalasset.canton.participant.store.memory.InMemorySubmissionTrackerStore import com.digitalasset.canton.protocol.{RequestId, RootHash} -import com.digitalasset.canton.topology.{ParticipantId, UniqueIdentifier} +import com.digitalasset.canton.topology.{DefaultTestIdentities, ParticipantId, UniqueIdentifier} import com.digitalasset.canton.util.FutureInstances.* import com.digitalasset.canton.util.FutureUtil import com.digitalasset.canton.util.Thereafter.syntax.* @@ -43,7 +43,11 @@ final class SubmissionTrackerTest (1 to 1000).map(i => RequestId(CantonTimestamp.Epoch.plusSeconds(i.toLong))) private lazy val submissionTrackerStore = - new InMemorySubmissionTrackerStore(loggerFactory, timeouts) + new InMemorySubmissionTrackerStore( + DefaultTestIdentities.physicalSynchronizerId, + loggerFactory, + timeouts, + ) private lazy val submissionTracker: SubmissionTracker = SubmissionTracker( participantId, submissionTrackerStore, diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ViewMessageDecrypterTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ViewMessageDecrypterTest.scala index 28e3465af1..d6edc715d8 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ViewMessageDecrypterTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ViewMessageDecrypterTest.scala @@ -32,6 +32,7 @@ import com.digitalasset.canton.data.{ } import com.digitalasset.canton.ledger.participant.state.SubmitterInfo import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.participant.protocol.ProcessingSteps.DecryptedViews import com.digitalasset.canton.participant.protocol.submission.TransactionTreeFactory.{ ContractInstanceOfId, @@ -125,6 +126,7 @@ class ViewMessageDecrypterTest extends BaseTestWordSpec with HasExecutionContext CacheConfig(PositiveNumeric.tryCreate(1)), new InMemoryCryptoPrivateStore(testedReleaseProtocolVersion, loggerFactory), new InMemoryCryptoPublicStore(loggerFactory), + CommonMockMetrics.cryptoMetrics, timeouts, loggerFactory, ) diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/scheduler/ParticipantPurgeStoresAfterLsuSchedulerTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/scheduler/ParticipantPurgeStoresAfterLsuSchedulerTest.scala index e1f1ab7987..59904c0fdf 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/scheduler/ParticipantPurgeStoresAfterLsuSchedulerTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/scheduler/ParticipantPurgeStoresAfterLsuSchedulerTest.scala @@ -4,7 +4,9 @@ package com.digitalasset.canton.participant.scheduler import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.config.BatchingConfig import com.digitalasset.canton.config.RequireTypes.{Port, PositiveInt} +import com.digitalasset.canton.data.{CantonTimestamp, SynchronizerPredecessor} import com.digitalasset.canton.discard.Implicits.* import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.networking.Endpoint @@ -18,6 +20,7 @@ import com.digitalasset.canton.participant.store.SynchronizerConnectionConfigSto import com.digitalasset.canton.participant.store.memory.{ InMemoryRegisteredSynchronizersStore, InMemorySynchronizerConnectionConfigStore, + InMemorySynchronizerConnectivityStatusStore, } import com.digitalasset.canton.participant.sync.SyncPersistentStateManager import com.digitalasset.canton.participant.synchronizer.{ @@ -102,10 +105,16 @@ final class ParticipantPurgeStoresAfterLsuSchedulerTest val schedule = new SteppingSchedule() + val purgeableStoresComputation = mock[PostLsuPurgeableStoresComputation] + + when(purgeableStoresComputation.compute()(any[TraceContext])) + .thenReturn(FutureUnlessShutdown.pure(Seq(store1, store2))) + val scheduler = new ParticipantPurgeStoresAfterLsuScheduler( schedule = Some(schedule), - getPurgeableStores = () => Seq(store1, store2), + purgeableStoresComputation = purgeableStoresComputation, chunkSize = PositiveInt.tryCreate(2), + BatchingConfig(), timeouts, loggerFactory, ) @@ -161,13 +170,17 @@ final class ParticipantPurgeStoresAfterLsuSchedulerTest sequencerId = None, ) - def setStatus(psid: PhysicalSynchronizerId, status: Status): Assertion = { + def setStatus( + psid: PhysicalSynchronizerId, + status: Status, + predecessor: Option[SynchronizerPredecessor], + ): Assertion = { val cfg = new SynchronizerConnectionConfig( alias, SequencerConnections.single(sequencerConnection), ) // Ensure it's there. - configStore.upsert(psid, (cfg, status, None)).futureValueUS.value.discard + configStore.upsert(psid, (cfg, status, predecessor)).futureValueUS.value.discard configStore .setStatus(alias, KnownPhysicalSynchronizerId(psid), status) .futureValueUS @@ -184,19 +197,27 @@ final class ParticipantPurgeStoresAfterLsuSchedulerTest val stateManager = mock[SyncPersistentStateManager] val oldPersistentState = mock[SyncPersistentState] val newPersistentState = mock[SyncPersistentState] + val newConnectivityStatusStore = new InMemorySynchronizerConnectivityStatusStore() + newConnectivityStatusStore.setTopologyInitialized().futureValueUS + when(oldPersistentState.purgeableStores).thenReturn(Seq(oldStore)) when(newPersistentState.purgeableStores).thenReturn(Seq(newStore)) + when(newPersistentState.connectivityStatusStore).thenReturn(newConnectivityStatusStore) + when(stateManager.getAll).thenReturn( + Map(oldPsid -> oldPersistentState, newPsid -> newPersistentState) + ) when(stateManager.get(oldPsid)).thenReturn(Some(oldPersistentState)) when(stateManager.get(newPsid)).thenReturn(Some(newPersistentState)) - ParticipantPurgeStoresAfterLsuScheduler.create( - Some(schedule), - PositiveInt.two, - configStore, - stateManager, - timeouts, - loggerFactory, + new ParticipantPurgeStoresAfterLsuScheduler( + schedule = Some(schedule), + purgeableStoresComputation = + new PostLsuPurgeableStoresComputation(configStore, stateManager), + chunkSize = PositiveInt.two, + batchingConfig = BatchingConfig(), + timeouts = timeouts, + loggerFactory = loggerFactory, ) } val f = scheduler.start() @@ -214,7 +235,7 @@ final class ParticipantPurgeStoresAfterLsuSchedulerTest newStore.size shouldBe 1 // Now old store becomes active. No status for new store yet. - setStatus(oldPsid, Active) + setStatus(oldPsid, Active, predecessor = None) } schedule.step { result => @@ -222,7 +243,12 @@ final class ParticipantPurgeStoresAfterLsuSchedulerTest (result, oldStore.size, newStore.size) shouldBe (Done, 1, 1) // Now new synchronizer is registered, and gets status LsuTarget. - setStatus(newPsid, LsuTarget) + setStatus( + newPsid, + LsuTarget, + predecessor = + Some(SynchronizerPredecessor(oldPsid, CantonTimestamp.Epoch, isLateUpgrade = false)), + ) } schedule.step { result => @@ -230,7 +256,7 @@ final class ParticipantPurgeStoresAfterLsuSchedulerTest (result, oldStore.size, newStore.size) shouldBe (Done, 1, 1) // Now it's upgrade time and the old synchronizer gets status LsuSource - setStatus(oldPsid, LsuSource) + setStatus(oldPsid, LsuSource, predecessor = None) } schedule.step { result => @@ -238,7 +264,12 @@ final class ParticipantPurgeStoresAfterLsuSchedulerTest (result, oldStore.size, newStore.size) shouldBe (Done, 1, 1) // Next at upgrade time the new synchronizer gets status Active - setStatus(newPsid, Active) + setStatus( + newPsid, + Active, + predecessor = + Some(SynchronizerPredecessor(oldPsid, CantonTimestamp.Epoch, isLateUpgrade = false)), + ) } schedule.step { result => diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/SynchronizerConnectionConfigStoreTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/SynchronizerConnectionConfigStoreTest.scala index 5f9ec8fc6c..42825feeff 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/SynchronizerConnectionConfigStoreTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/SynchronizerConnectionConfigStoreTest.scala @@ -60,7 +60,7 @@ import org.scalatest.wordspec.AsyncWordSpec import scala.util.Random trait SynchronizerConnectionConfigStoreTest extends FailOnShutdown { - this: AsyncWordSpec with BaseTest with HasExecutionContext => + this: AsyncWordSpec & BaseTest & HasExecutionContext => private val uid = DefaultTestIdentities.uid private val psid = @@ -450,9 +450,9 @@ trait SynchronizerConnectionConfigStoreTest extends FailOnShutdown { "return error when trying to have multiple active configs for a synchronizer alias" in { val c1 = config val psid_1 = psid.copy(serial = NonNegativeInt.one) - val c2 = config.copy(synchronizerId = Some(psid_1)) + val c2 = config.copy(psid = Some(psid_1)) - c1.synchronizerId should not be c2.synchronizerId + c1.psid should not be c2.psid for { sut <- mk @@ -868,10 +868,7 @@ trait SynchronizerConnectionConfigStoreTest extends FailOnShutdown { sut <- sutF // First insert - insertResult <- sut - .upsert(daDev, key) - .valueOrFail("initial insert") - .map(getData) + insertResult <- sut.upsert(daDev, key).valueOrFail("initial insert").map(getData) queryAfterInsert <- queryData().valueOrFail("get initial ports") _ = insertResult shouldBe Data( Map( @@ -888,11 +885,7 @@ trait SynchronizerConnectionConfigStoreTest extends FailOnShutdown { Endpoint("host3", Port.tryCreate(700)), )(initialConfig.sequencerConnections) addEndpointResult <- sut - .upsert( - daDev, - key, - overrideSequencerConnections = sequencerConnections2.some, - ) + .upsert(daDev, key, overrideSequencerConnections = sequencerConnections2.some) .valueOrFail("initial update") .map(getData) queryAfterAddEndpoint <- queryData().valueOrFail("get ports after update") @@ -908,11 +901,7 @@ trait SynchronizerConnectionConfigStoreTest extends FailOnShutdown { // Idempotency idempotencyResult <- sut - .upsert( - daDev, - key, - overrideSequencerConnections = sequencerConnections2.some, - ) + .upsert(daDev, key, overrideSequencerConnections = sequencerConnections2.some) .valueOrFail("idempotency") .map(getData) queryAfterIdempotency <- queryData().valueOrFail("get ports idempotency") @@ -922,11 +911,7 @@ trait SynchronizerConnectionConfigStoreTest extends FailOnShutdown { // Remove endpoint for sequencer2 sequencerConnections3 = removeEndpoint(sequencerAlias2, 501)(sequencerConnections2) removeEndpointResult <- sut - .upsert( - daDev, - key, - overrideSequencerConnections = sequencerConnections3.some, - ) + .upsert(daDev, key, overrideSequencerConnections = sequencerConnections3.some) .valueOrFail("remove endpoint") .map(getData) queryAfterRemoveEndpoint <- queryData().valueOrFail("get ports remove endpoints") diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/SynchronizerConnectivityStatusStoreTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/SynchronizerConnectivityStatusStoreTest.scala index 98a2994196..db8bc7f008 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/SynchronizerConnectivityStatusStoreTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/SynchronizerConnectivityStatusStoreTest.scala @@ -33,7 +33,7 @@ trait SynchronizerConnectivityStatusStoreTest extends FailOnShutdown { for { _ <- store.setParameters(params) last <- store.lastParameters - initialized <- store.isTopologyInitialized + initialized <- store.isTopologyInitialized() } yield { last shouldBe Some(params) initialized shouldBe false @@ -92,15 +92,15 @@ trait SynchronizerConnectivityStatusStoreTest extends FailOnShutdown { val params = defaultStaticSynchronizerParameters for { _ <- store.setParameters(params) - initialized1 <- store.isTopologyInitialized + initialized1 <- store.isTopologyInitialized() _ = initialized1 shouldBe false _ <- store.setTopologyInitialized() - initialized2 <- store.isTopologyInitialized + initialized2 <- store.isTopologyInitialized() _ = initialized2 shouldBe true _ <- store.setParameters(params) - initialized3 <- store.isTopologyInitialized + initialized3 <- store.isTopologyInitialized() } yield initialized3 shouldBe true } } diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/db/DbContractStoreTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/db/DbContractStoreTest.scala index 5011fed41f..232e1d41f3 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/db/DbContractStoreTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/db/DbContractStoreTest.scala @@ -59,7 +59,7 @@ trait DbContractStoreTest extends AsyncWordSpec with BaseTest with ContractStore for { p0 <- store.lookupPersisted(contractId).failOnShutdown - _ <- store.lookupPersistedIfCached(contractId) shouldBe Some(None) + _ <- eventually()(store.lookupPersistedIfCached(contractId) shouldBe Some(None)) _ <- store.storeContract(contract).failOnShutdown p <- store.lookupPersisted(contractId).failOnShutdown pByIid <- store.lookupBatchedNonReadThrough(Seq(p.value.internalContractId)).failOnShutdown diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/memory/SubmissionTrackerStoreTestInMemory.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/memory/SubmissionTrackerStoreTestInMemory.scala index 628c350a24..a24f28f38a 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/memory/SubmissionTrackerStoreTestInMemory.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/memory/SubmissionTrackerStoreTestInMemory.scala @@ -4,11 +4,16 @@ package com.digitalasset.canton.participant.store.memory import com.digitalasset.canton.participant.store.SubmissionTrackerStoreTest +import com.digitalasset.canton.topology.DefaultTestIdentities final class SubmissionTrackerStoreTestInMemory extends SubmissionTrackerStoreTest { "InMemorySubmissionTrackerStore" should { behave like submissionTrackerStore(() => - new InMemorySubmissionTrackerStore(loggerFactory, timeouts) + new InMemorySubmissionTrackerStore( + DefaultTestIdentities.physicalSynchronizerId, + loggerFactory, + timeouts, + ) ) } } diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/QueueBasedSynchronizerOutboxTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/QueueBasedSynchronizerOutboxTest.scala index 8bbc499542..3060df3acf 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/QueueBasedSynchronizerOutboxTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/QueueBasedSynchronizerOutboxTest.scala @@ -307,6 +307,7 @@ class QueueBasedSynchronizerOutboxTest tx.mapping, tx.serial.some, signingKeys = Seq(publicKey.fingerprint), + namespacesToSignFor = Seq.empty, testedProtocolVersion, expectFullAuthorization = true, waitToBecomeEffective = waitToBecomeEffective, diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/StoreBasedSynchronizerOutboxTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/StoreBasedSynchronizerOutboxTest.scala index 21887c7615..6a73a90f5e 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/StoreBasedSynchronizerOutboxTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/StoreBasedSynchronizerOutboxTest.scala @@ -255,6 +255,7 @@ class StoreBasedSynchronizerOutboxTest tx.mapping, tx.serial.some, signingKeys = Seq(publicKey.fingerprint), + namespacesToSignFor = Seq.empty, testedProtocolVersion, expectFullAuthorization = false, waitToBecomeEffective = waitToBecomeEffective, diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala index 65a9906158..61e8955f50 100644 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala +++ b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala @@ -37,7 +37,7 @@ final class TopologyLookupTest val persistentState = mock[SyncPersistentState] val topologyStoreTestData = - new TopologyStoreTestData(testedProtocolVersion, loggerFactory, parallelExecutionContext) + new TopologyStoreTestData(testedProtocolVersion, loggerFactory)(parallelExecutionContext) val topologyStore = new InMemoryTopologyStore[SynchronizerStore]( SynchronizerStore(psid), diff --git a/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto b/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto index ad3e72ffeb..7da4e1d486 100644 --- a/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto +++ b/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto @@ -57,6 +57,9 @@ message PeerEndpoint { PlainTextPeerEndpoint plain_text = 3; TlsPeerEndpoint tls = 4; } + // Ignored when storing an endpoint, but returned when listing configured endpoints: + // if the peer authenticated as a sequencer over this endpoint, this field will contain its sequencer ID. + optional string sequencer_id = 5; } message PeerEndpointId { @@ -166,6 +169,10 @@ message GetOrderingTopologyResponse { com.digitalasset.canton.synchronizer.sequencing.sequencer.bftordering.v30.DynamicSequencingParametersPayload dynamic_sequencing_parameters_payload = 3; com.digitalasset.canton.synchronizer.sequencing.sequencer.bftordering.v31.DynamicSequencingParametersPayload dynamic_sequencing_parameters_payload31 = 4; } + // The sequencer IDs of the BFT ordering nodes in the network that are consensus leaders. + repeated string leader_sequencer_ids = 5; + // The sequencer IDs of the BFT ordering nodes in the network that are blacklisted. + repeated string blacklisted_sequencer_ids = 6; } message SetPerformanceMetricsEnabledRequest { diff --git a/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto b/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto index 5b8d1c0cc4..e574ca078e 100644 --- a/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto +++ b/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto @@ -84,6 +84,7 @@ message StoreResponse { message BatchRequest { bytes batch_id = 1; + int64 epoch_number = 2; } message BatchResponse { diff --git a/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v31/bft_ordering_sequencing_parameters.proto b/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v31/bft_ordering_sequencing_parameters.proto index dfc907164d..d5baf2f6ca 100644 --- a/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v31/bft_ordering_sequencing_parameters.proto +++ b/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v31/bft_ordering_sequencing_parameters.proto @@ -20,6 +20,8 @@ message BlacklistLeaderSelectionPolicy { oneof how_long_to_blacklist { HowLongLinear how_long_linear = 1; HowLongNoBlacklisting how_long_no_blacklisting = 2; + HowLongLinearWithParameters how_long_linear_with_parameters = 5; + HowLongExponential how_long_exponential = 6; } oneof how_many_can_we_blacklist { HowManyNumFaultsTolerated how_many_num_faults_tolerated = 3; @@ -31,6 +33,15 @@ message HowLongLinear { optional int64 maximum_epoch_length_blacklisted = 1; } message HowLongNoBlacklisting {} +message HowLongLinearWithParameters { + int64 slope = 1; + int64 initial_value = 2; + optional int64 maximum_epoch_length_blacklisted = 3; +} +message HowLongExponential { + int64 initial_value = 1; + optional int64 maximum_epoch_length_blacklisted = 2; +} message HowManyNumFaultsTolerated {} message HowManyNoBlacklisting {} diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/BlockSequencerStateManager.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/BlockSequencerStateManager.scala index ea6c02a51f..6002de4b80 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/BlockSequencerStateManager.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/BlockSequencerStateManager.scala @@ -12,28 +12,33 @@ import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.health.{AtomicHealthComponent, ComponentHealthState, HealthComponent} import com.digitalasset.canton.lifecycle.UnlessShutdown.Outcome import com.digitalasset.canton.lifecycle.{ CloseContext, FlagCloseable, FutureUnlessShutdown, HasCloseContext, + HasRunOnClosing, PromiseUnlessShutdown, UnlessShutdown, } -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} import com.digitalasset.canton.metrics.InstrumentedGraph.BufferedFlow import com.digitalasset.canton.sequencing.traffic.TrafficConsumed import com.digitalasset.canton.synchronizer.block import com.digitalasset.canton.synchronizer.block.AsyncWriter.AsyncAppendWorkHandle -import com.digitalasset.canton.synchronizer.block.BlockSequencerStateManager.HeadState +import com.digitalasset.canton.synchronizer.block.BlockSequencerStateManager.AccumulatedStatePersistingBlocks import com.digitalasset.canton.synchronizer.block.data.{ BlockEphemeralState, BlockInfo, SequencerBlockStore, } import com.digitalasset.canton.synchronizer.block.update.* -import com.digitalasset.canton.synchronizer.block.update.BlockUpdateGenerator.BlockChunk +import com.digitalasset.canton.synchronizer.block.update.BlockUpdateGenerator.{ + AccumulatedStateProcessingBlocks, + BlockChunk, +} import com.digitalasset.canton.synchronizer.metrics.BlockMetrics import com.digitalasset.canton.synchronizer.sequencer.{ BlockSequencerStreamInstrumentationConfig, @@ -75,7 +80,11 @@ class SequencerUnexpectedStateChange(message: String = "Sequencer state has unex */ trait BlockSequencerStateManagerBase extends FlagCloseable { - def getHeadState: HeadState + /** Head state of the persistence step */ + def getPersistenceHeadState: AccumulatedStatePersistingBlocks + + /** Head state of the processing step */ + def getProcessingHeadState: AccumulatedStateProcessingBlocks /** Flow to turn [[BlockEvents]] of one block into a series of [[update.OrderedBlockUpdate]]s that * are to be persisted subsequently using [[applyBlockUpdate]]. @@ -94,6 +103,11 @@ trait BlockSequencerStateManagerBase extends FlagCloseable { def waitForAcknowledgementToComplete(member: Member, timestamp: CantonTimestamp)(implicit traceContext: TraceContext ): Future[Unit] + + /** Health of the background writer. Reports a fatal state when a background write fails and the + * writer can no longer make progress. + */ + def asyncWriterHealth: HealthComponent } /** Async block sequencer writer control parameters @@ -333,6 +347,16 @@ private class BlockSequencerStateAsyncWriter( )(implicit executionContext: ExecutionContext, closeContext: CloseContext) extends NamedLogging { + /** Health of the writer. It transitions to a fatal state whenever a write fails, which never + * recovers as further writes are blocked to avoid an inconsistent store. + */ + val health: AtomicHealthComponent = + new BlockSequencerStateAsyncWriter.WriterHealth( + BlockSequencerStateAsyncWriter.healthName, + closeContext.context, + logger, + ) + private def mkWriter[Q <: Iterable[?]]( addToQueue: (Q, Q) => Q, writeQueue: Q => FutureUnlessShutdown[Unit], @@ -358,6 +382,7 @@ private class BlockSequencerStateAsyncWriter( ).initCause(exception) ) ) + health.fatalOccurred(s"Write $name failed - no further writes to avoid inconsistent store") } } private def mkPromise[A](description: String) = @@ -482,17 +507,32 @@ private class BlockSequencerStateAsyncWriter( } +private object BlockSequencerStateAsyncWriter { + val healthName: String = "block-sequencer-async-writer" + + /** Atomic health component reporting the state of the writer. */ + class WriterHealth( + override val name: String, + override protected val associatedHasRunOnClosing: HasRunOnClosing, + override protected val logger: TracedLogger, + ) extends AtomicHealthComponent { + override protected def initialHealthState: ComponentHealthState = ComponentHealthState.Ok() + } +} + class BlockSequencerStateManager( val store: SequencerBlockStore, val trafficConsumedStore: TrafficConsumedStore, + initialHeadBlockO: Option[BlockEphemeralState], asyncWriterParameters: AsyncWriterParameters, enableInvariantCheck: Boolean, + streamInstrumentationConfig: BlockSequencerStreamInstrumentationConfig, + enablePrevalidation: Boolean, + prevalidationParallelism: PositiveInt, + blockMetrics: BlockMetrics, override protected val timeouts: ProcessingTimeout, futureSupervisor: FutureSupervisor, protected val loggerFactory: NamedLoggerFactory, - headState: AtomicReference[HeadState], - streamInstrumentationConfig: BlockSequencerStreamInstrumentationConfig, - blockMetrics: BlockMetrics, )(implicit executionContext: ExecutionContext) extends BlockSequencerStateManagerBase with NamedLogging @@ -500,6 +540,17 @@ class BlockSequencerStateManager( import BlockSequencerStateManager.* + private val initialHeadBlock = initialHeadBlockO.getOrElse(BlockEphemeralState.empty) + + private val persistenceHeadState = new AtomicReference[AccumulatedStatePersistingBlocks]( + AccumulatedStatePersistingBlocks.fullyProcessed(initialHeadBlock) + ) + private val processingHeadState = new AtomicReference[AccumulatedStateProcessingBlocks]( + BlockUpdateGenerator.AccumulatedStateProcessingBlocks.fromEphemeralState( + initialHeadBlock + ) + ) + private val asyncWriter = new BlockSequencerStateAsyncWriter( store = store, @@ -509,19 +560,21 @@ class BlockSequencerStateManager( loggerFactory, ) + override def asyncWriterHealth: HealthComponent = asyncWriter.health + private val memberAcknowledgementPromises = TrieMap[Member, NonEmpty[SortedMap[CantonTimestamp, Traced[Promise[Unit]]]]]() - override def getHeadState: HeadState = headState.get() + override def getPersistenceHeadState: AccumulatedStatePersistingBlocks = + persistenceHeadState.get() + + override def getProcessingHeadState: AccumulatedStateProcessingBlocks = processingHeadState.get() override def processBlock( bug: BlockUpdateGenerator ): Flow[Traced[BlockEvents], Traced[OrderedBlockUpdate], NotUsed] = { - val head = getHeadState - val bugState = { - import TraceContext.Implicits.Empty.* - bug.internalStateFor(head.blockEphemeralState) - } + + val bugState = processingHeadState.get() def finalFlow[In, Out, Mat]( original: Flow[In, Out, Mat], @@ -534,16 +587,67 @@ class BlockSequencerStateManager( )(MetricsContext("element" -> flowName)) else original - Flow[Traced[BlockEvents]] + val stage1 = Flow[Traced[BlockEvents]] .via( - finalFlow(checkBlockHeight(head.block.height), "check_block_height") + finalFlow(checkBlockHeight(initialHeadBlock.latestBlock.height), "check_block_height") ) .via( finalFlow(chunkBlock(bug), "chunk_block") ) - .via( - finalFlow(processChunk(bug)(bugState), "process_chunk") - ) + + val stage2 = + if (enablePrevalidation) + stage1.via( + finalFlow( + prevalidateSignatures( + bug + ), + "prevalidate_signatures", + ) + ) + else stage1 + + stage2.via( + finalFlow(processChunk(bug)(bugState), "process_chunk") + ) + } + + /** Prevalidate signatures in parallel + * + * Signature checking is relatively expensive. We therefore want to avoid doing this within a + * sequential stage. At the same time, we need to check not only the correctness of the + * signature, but also that the key used to sign was valid at the time of signing. The key check + * needs to be done using a consistent and correct topology snapshot, which in the current + * pipeline is only really known in the sequential stage. + * + * We therefore split and perform the expensive validation in parallel before the sequential + * stage, which makes sense as keys normally don't change often. In the sequential stage we then + * just check the key validity. + * + * We only deal with the happy path here. Any failure scenario will be handled consistently in + * the sequential step. + * + * This has two issues: + * - the unhappy path is less efficient and can therefore be used as an attack vector. + * - we perform the signature check before further validation, which means that an attacker + * might be able to cause more expensive work before failing the signature check. + * + * However, both of these issues are only an issue if considered in isolation: + * - an honest sequencer will filter out any malicious transaction submitted by a member before + * ordering + * - a dishonest sequencer can use its bandwidth to waste resources by submitting bad + * transactions that will pass all checks and then fail in the signature check anyway. + * Generally we deal with non-compliant sequencers through non-repudiability and + * reporting/alerting/blacklisting. + */ + private def prevalidateSignatures( + bug: BlockUpdateGenerator + ): Flow[Traced[BlockChunk], Traced[BlockChunk], NotUsed] = { + implicit val traceContext: TraceContext = TraceContext.empty + Flow[Traced[BlockChunk]].mapAsyncAndDrainUS(prevalidationParallelism.value)(_.withTraceContext { + implicit traceContext => chunk => + bug.prevalidateSignatures(chunk, prevalidationParallelism).map(Traced(_)) + }) } private def checkBlockHeight( @@ -599,12 +703,19 @@ class BlockSequencerStateManager( }) private def processChunk(bug: BlockUpdateGenerator)( - initialState: bug.InternalState + initialState: BlockUpdateGenerator.AccumulatedStateProcessingBlocks ): Flow[Traced[BlockChunk], Traced[OrderedBlockUpdate], NotUsed] = { implicit val traceContext: TraceContext = TraceContext.empty Flow[Traced[BlockChunk]].statefulMapAsyncUSAndDrain(initialState) { (state, tracedChunk) => implicit val traceContext: TraceContext = tracedChunk.traceContext - tracedChunk.traverse(blockChunk => Nested(bug.processBlockChunk(state, blockChunk))).value + tracedChunk + .traverse(blockChunk => + Nested(bug.processBlockChunk(state, blockChunk).map { case (state, update) => + processingHeadState.set(state) + (state, update) + }) + ) + .value } } @@ -612,26 +723,27 @@ class BlockSequencerStateManager( dbSequencerIntegration: SequencerIntegration ): Flow[Traced[BlockUpdate], Traced[CantonTimestamp], NotUsed] = { implicit val traceContext = TraceContext.empty - Flow[Traced[BlockUpdate]].statefulMapAsyncUSAndDrain(getHeadState) { (priorHead, update) => - implicit val traceContext = update.traceContext - val currentBlockNumber = priorHead.block.height + 1 - val fut = update.value match { - case chunk: ChunkUpdate => - val chunkNumber = priorHead.chunk.chunkNumber + 1 - LoggerUtil.clueUSF( - s"Adding block updates for chunk $chunkNumber for block $currentBlockNumber. " + - s"Contains ${chunk.acknowledgements.size} acks, " + - s"and ${chunk.inFlightAggregationUpdates.size} in-flight aggregation updates" - )(handleChunkUpdate(priorHead, chunk, dbSequencerIntegration)(traceContext)) - case complete: CompleteBlockUpdate => - // TODO(#18401): Consider: wait for the DBS watermark to be updated to the blocks last timestamp - // in a supervisory manner, to detect things not functioning properly - LoggerUtil.clueUSF( - s"Storing completion of block $currentBlockNumber" - )(handleComplete(priorHead, complete.block)(traceContext)) - } - fut - .map(newHead => newHead -> Traced(newHead.block.lastTs)) + Flow[Traced[BlockUpdate]].statefulMapAsyncUSAndDrain(getPersistenceHeadState) { + (priorHead, update) => + implicit val traceContext = update.traceContext + val currentBlockNumber = priorHead.block.height + 1 + val fut = update.value match { + case chunk: ChunkUpdate => + val chunkNumber = priorHead.chunk.chunkNumber + 1 + LoggerUtil.clueUSF( + s"Adding block updates for chunk $chunkNumber for block $currentBlockNumber. " + + s"Contains ${chunk.acknowledgements.size} acks, " + + s"and ${chunk.inFlightAggregationUpdates.size} in-flight aggregation updates" + )(handleChunkUpdate(priorHead, chunk, dbSequencerIntegration)(traceContext)) + case complete: CompleteBlockUpdate => + // TODO(#18401): Consider: wait for the DBS watermark to be updated to the blocks last timestamp + // in a supervisory manner, to detect things not functioning properly + LoggerUtil.clueUSF( + s"Storing completion of block $currentBlockNumber" + )(handleComplete(priorHead, complete.block)(traceContext)) + } + fut + .map(newHead => newHead -> Traced(newHead.block.lastTs)) } } @@ -658,12 +770,12 @@ class BlockSequencerStateManager( .future private def handleChunkUpdate( - priorHead: HeadState, + priorHead: AccumulatedStatePersistingBlocks, update: ChunkUpdate, dbSequencerIntegration: SequencerIntegration, )(implicit batchTraceContext: TraceContext - ): FutureUnlessShutdown[HeadState] = { + ): FutureUnlessShutdown[AccumulatedStatePersistingBlocks] = { val priorState = priorHead.chunk val chunkNumber = priorState.chunkNumber + 1 val currentBlockNumber = priorHead.block.height + 1 @@ -678,7 +790,6 @@ class BlockSequencerStateManager( val newState = ChunkState( chunkNumber, - update.inFlightAggregations, lastTs, update.lastSequencerEventTimestamp.orElse(priorState.latestSequencerEventTimestamp), ) @@ -724,9 +835,9 @@ class BlockSequencerStateManager( ) } - private def handleComplete(priorHead: HeadState, newBlock: BlockInfo)(implicit - blockTraceContext: TraceContext - ): FutureUnlessShutdown[HeadState] = { + private def handleComplete(priorHead: AccumulatedStatePersistingBlocks, newBlock: BlockInfo)( + implicit blockTraceContext: TraceContext + ): FutureUnlessShutdown[AccumulatedStatePersistingBlocks] = { val chunkState = priorHead.chunk assert( chunkState.lastTs <= newBlock.lastTs, @@ -739,11 +850,10 @@ class BlockSequencerStateManager( val newState = BlockEphemeralState( newBlock, - chunkState.inFlightAggregations, + InFlightAggregations.empty, ) checkInvariantIfEnabled(newState) - val newHead = HeadState.fullyProcessed(newState) - + val newHead = AccumulatedStatePersistingBlocks.fullyProcessed(newState) // write is async. future only forwarded to inject future failed in case we are unable to write asyncWriter .finalizeBlockUpdate(newBlock) @@ -754,10 +864,13 @@ class BlockSequencerStateManager( } - private def updateHeadState(prior: HeadState, next: HeadState)(implicit + private def updateHeadState( + prior: AccumulatedStatePersistingBlocks, + next: AccumulatedStatePersistingBlocks, + )(implicit traceContext: TraceContext ): Unit = - if (!headState.compareAndSet(prior, next)) { + if (!persistenceHeadState.compareAndSet(prior, next)) { // The write flow should not call this method concurrently so this situation should never happen. // If it does, this means that the ephemeral state has been updated since this update was generated, // and that the persisted state is now likely inconsistent. @@ -851,36 +964,38 @@ object BlockSequencerStateManager { store: SequencerBlockStore, trafficConsumedStore: TrafficConsumedStore, asyncWriterParameters: AsyncWriterParameters, + streamInstrumentationConfig: BlockSequencerStreamInstrumentationConfig, enableInvariantCheck: Boolean, + enablePrevalidation: Boolean, + prevalidationParallelism: PositiveInt, + blockMetrics: BlockMetrics, timeouts: ProcessingTimeout, futureSupervisor: FutureSupervisor, loggerFactory: NamedLoggerFactory, - streamInstrumentationConfig: BlockSequencerStreamInstrumentationConfig, - blockMetrics: BlockMetrics, )(implicit executionContext: ExecutionContext, traceContext: TraceContext, ): BlockSequencerStateManager = { - val logger = loggerFactory.getTracedLogger(getClass) - val headBlock = initialHeadBlockO.getOrElse(BlockEphemeralState.empty) - val headState = new AtomicReference[HeadState]({ - logger.debug( + loggerFactory + .getTracedLogger(getClass) + .info( s"Initialized the block sequencer with head block ${headBlock.latestBlock}" ) - HeadState.fullyProcessed(headBlock) - }) + new BlockSequencerStateManager( store = store, trafficConsumedStore = trafficConsumedStore, + initialHeadBlockO = initialHeadBlockO, asyncWriterParameters = asyncWriterParameters, enableInvariantCheck = enableInvariantCheck, + streamInstrumentationConfig = streamInstrumentationConfig, + enablePrevalidation = enablePrevalidation, + prevalidationParallelism = prevalidationParallelism, + blockMetrics = blockMetrics, timeouts = timeouts, futureSupervisor = futureSupervisor, loggerFactory = loggerFactory, - headState = headState, - streamInstrumentationConfig = streamInstrumentationConfig, - blockMetrics = blockMetrics, ) } @@ -891,7 +1006,6 @@ object BlockSequencerStateManager { */ final case class ChunkState( chunkNumber: Long, - inFlightAggregations: InFlightAggregations, lastTs: CantonTimestamp, latestSequencerEventTimestamp: Option[CantonTimestamp], ) @@ -902,7 +1016,6 @@ object BlockSequencerStateManager { def initial(block: BlockEphemeralState): ChunkState = ChunkState( initialChunkCounter, - block.inFlightAggregations, block.latestBlock.lastTs, block.latestBlock.latestSequencerEventTimestamp, ) @@ -914,27 +1027,14 @@ object BlockSequencerStateManager { * Describes the state after the latest block that was fully processed. * @param chunk * Describes the state after the last chunk of the block that is currently being processed. - * When the latest block is fully processed, but no chunks of the next block, then this is - * `ChunkState.initial` based on the last block's - * [[com.digitalasset.canton.synchronizer.block.data.BlockEphemeralState]]. */ - final case class HeadState( + final case class AccumulatedStatePersistingBlocks( block: BlockInfo, chunk: ChunkState, - ) { - def blockEphemeralState(implicit - loggingContext: ErrorLoggingContext - ): BlockEphemeralState = { - ErrorUtil.requireState( - chunk.chunkNumber == ChunkState.initialChunkCounter, - s"Cannot construct a BlockEphemeralState if there are partial block updates from ${chunk.chunkNumber} chunks.", - ) - BlockEphemeralState(block, chunk.inFlightAggregations) - } - } + ) - object HeadState { - def fullyProcessed(block: BlockEphemeralState): HeadState = - HeadState(block.latestBlock, ChunkState.initial(block)) + object AccumulatedStatePersistingBlocks { + def fullyProcessed(block: BlockEphemeralState): AccumulatedStatePersistingBlocks = + AccumulatedStatePersistingBlocks(block.latestBlock, ChunkState.initial(block)) } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessor.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessor.scala index 88ccb432d8..bb0e711837 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessor.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessor.scala @@ -11,8 +11,13 @@ import com.daml.metrics.api.MetricsContext import com.daml.nonempty.{NonEmpty, NonEmptyUtil} import com.digitalasset.base.error.BaseAlarm import com.digitalasset.canton.SequencerCounter -import com.digitalasset.canton.config.BatchingConfig -import com.digitalasset.canton.crypto.{HashPurpose, SyncCryptoClient, SynchronizerCryptoClient} +import com.digitalasset.canton.config.RequireTypes.PositiveInt +import com.digitalasset.canton.crypto.{ + HashPurpose, + SyncCryptoClient, + SynchronizerCryptoClient, + SynchronizerSnapshotSyncCryptoApi, +} import com.digitalasset.canton.data.{CantonTimestamp, LogicalUpgradeTime} import com.digitalasset.canton.discard.Implicits.* import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} @@ -22,6 +27,7 @@ import com.digitalasset.canton.sequencing.client.SequencedEventValidator import com.digitalasset.canton.sequencing.protocol.* import com.digitalasset.canton.synchronizer.block.LedgerBlockEvent import com.digitalasset.canton.synchronizer.block.LedgerBlockEvent.{Acknowledgment, Send} +import com.digitalasset.canton.synchronizer.block.update.BlockUpdateGenerator.AccumulatedStateProcessingBlocks import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.* import com.digitalasset.canton.synchronizer.sequencer.Sequencer.SignedSubmissionRequest @@ -40,27 +46,32 @@ import io.opentelemetry.api.trace.Tracer import scala.collection.mutable import scala.concurrent.{ExecutionContext, Future} -import BlockUpdateGeneratorImpl.{SequencedPreValidatedSubmissionResult, State} +import BlockUpdateGeneratorImpl.SequencedPreValidatedSubmissionResult import SequencedSubmissionsValidator.SequencedSubmissionsValidationResult +final case class BlockProcessingParameters( + orderingTimeFixMode: OrderingTimeFixMode, + lsuSequencingBounds: Option[LsuSequencingBounds], + parallelism: PositiveInt, + enablePrevalidation: Boolean, +) + /** Processes a chunk of events in a block, yielding a [[ChunkUpdate]]. */ final class BlockChunkProcessor( synchronizerSyncCryptoApi: SynchronizerCryptoClient, sequencerId: SequencerId, rateLimitManager: SequencerRateLimitManager, - orderingTimeFixMode: OrderingTimeFixMode, - lsuSequencingBounds: Option[LsuSequencingBounds], - batchingConfig: BatchingConfig, - override val loggerFactory: NamedLoggerFactory, + parameters: BlockProcessingParameters, metrics: SequencerMetrics, memberValidator: SequencerMemberValidator, + override val loggerFactory: NamedLoggerFactory, )(implicit closeContext: CloseContext, tracer: Tracer) extends NamedLogging with Spanning { private val protocolVersion = synchronizerSyncCryptoApi.psid.protocolVersion - + private val lsuSequencingBounds = parameters.lsuSequencingBounds private val inFlightAggregationHandler = new InFlightAggregationHandler( memberValidator, synchronizerSyncCryptoApi, @@ -71,10 +82,11 @@ final class BlockChunkProcessor( private val submissionRequestValidator = new SubmissionRequestValidator( inFlightAggregationHandler, - batchingConfig, - loggerFactory, memberValidator = memberValidator, protocolVersion, + parameters.enablePrevalidation, + parameters.parallelism, + loggerFactory, ) private val sequencedSubmissionsValidator = @@ -89,29 +101,38 @@ final class BlockChunkProcessor( loggerFactory, ) + def prevalidateLedgerBlockEvent( + approxCryptoSnapshot: SynchronizerSnapshotSyncCryptoApi, + event: LedgerBlockEvent, + )(implicit + traceContext: TraceContext, + executionContext: ExecutionContext, + ): FutureUnlessShutdown[TracedPossiblyPrevalidated[LedgerBlockEvent]] = + submissionRequestValidator.prevalidateEvent(approxCryptoSnapshot, event) + def processDataChunk( - state: BlockUpdateGeneratorImpl.State, + state: BlockUpdateGenerator.AccumulatedStateProcessingBlocks, height: Long, index: Int, - chunkEvents: NonEmpty[Seq[Traced[LedgerBlockEvent]]], + chunkEvents: NonEmpty[Seq[TracedPossiblyPrevalidated[LedgerBlockEvent]]], announcedLsu: Option[AnnouncedLsu], )(implicit ec: ExecutionContext, traceContext: TraceContext, - ): FutureUnlessShutdown[(BlockUpdateGeneratorImpl.State, ChunkUpdate)] = { + ): FutureUnlessShutdown[(BlockUpdateGenerator.AccumulatedStateProcessingBlocks, ChunkUpdate)] = { val (lastTsBeforeValidation, fixedTsChanges) = fixTimestampsAndDropSendsAfterUpgradeTime(state, chunkEvents, announcedLsu) logChunkDetails(state, height, index, fixedTsChanges) val orderingRequests = - fixedTsChanges.collect { case (ts, ev @ Traced(sendEvent: Send)) => + fixedTsChanges.collect { case (ts, ev @ TracedPossiblyPrevalidated(sendEvent: Send, _)) => // Discard the timestamp of the `Send` event as we're using the adjusted timestamp (ts, ev.map(_ => sendEvent.signedSubmissionRequest), sendEvent.orderingSequencerId) } FutureUtil.doNotAwait( - recordSubmissionMetrics(fixedTsChanges.map(_._2)), + recordSubmissionMetrics(fixedTsChanges.map(_._2.tracedValue)), "submission metric updating failed", ) @@ -156,7 +177,6 @@ final class BlockChunkProcessor( invalidAcks, inFlightAggregationUpdates, lastSequencerEventTimestamp, - finalInFlightAggregationsWithAggregationExpiry, reversedOutcomes.reverse, ) @@ -173,7 +193,7 @@ final class BlockChunkProcessor( .getOrElse(state.lastChunkTs) newState = - BlockUpdateGeneratorImpl.State( + BlockUpdateGenerator.AccumulatedStateProcessingBlocks( state.lastBlockTs, lastChunkTsOfSuccessfulEvents, lastSequencerEventTimestamp.orElse(state.latestSequencerEventTimestamp), @@ -184,10 +204,10 @@ final class BlockChunkProcessor( } private def logChunkDetails( - state: State, + state: AccumulatedStateProcessingBlocks, height: Long, index: Int, - assignedTimestamps: Seq[(CantonTimestamp, Traced[LedgerBlockEvent])], + assignedTimestamps: Seq[(CantonTimestamp, TracedPossiblyPrevalidated[LedgerBlockEvent])], )(implicit traceContext: TraceContext): Unit = noTracingLogger.whenInfoEnabled { val sb = new mutable.StringBuilder() @@ -233,11 +253,14 @@ final class BlockChunkProcessor( } def emitTick( - state: BlockUpdateGeneratorImpl.State, + state: BlockUpdateGenerator.AccumulatedStateProcessingBlocks, height: Long, tickAtLeastAt: CantonTimestamp, groupRecipient: Either[AllMembersOfSynchronizer.type, SequencersOfSynchronizer.type], - )(implicit ec: ExecutionContext, tc: TraceContext): FutureUnlessShutdown[(State, ChunkUpdate)] = { + )(implicit + ec: ExecutionContext, + tc: TraceContext, + ): FutureUnlessShutdown[(AccumulatedStateProcessingBlocks, ChunkUpdate)] = { // The block orderer requests a topology tick to advance the topology processor's time knowledge // whenever it assesses that it may need to retrieve an up-to-date topology snapshot at a certain // sequencing timestamp, and it does so by setting it as in a `RawLedgerBlock`, promising that @@ -327,7 +350,6 @@ final class BlockChunkProcessor( invalidAcknowledgements = Seq.empty, inFlightAggregationUpdates = Map.empty, lastSequencerEventTimestamp = Some(tickSequencingTimestamp), - inFlightAggregations = unexpiredInFlightAggregations, submissionsOutcomes = Seq(tickSubmissionOutcome), ) @@ -336,12 +358,12 @@ final class BlockChunkProcessor( } private def fixTimestampsAndDropSendsAfterUpgradeTime( - state: State, - chunk: NonEmpty[Seq[Traced[LedgerBlockEvent]]], + state: AccumulatedStateProcessingBlocks, + chunk: NonEmpty[Seq[TracedPossiblyPrevalidated[LedgerBlockEvent]]], announcedLsu: Option[AnnouncedLsu], )(implicit traceContext: TraceContext - ): (CantonTimestamp, Seq[(CantonTimestamp, Traced[LedgerBlockEvent])]) = { + ): (CantonTimestamp, Seq[(CantonTimestamp, TracedPossiblyPrevalidated[LedgerBlockEvent])]) = { val (lastTsBeforeValidation, revFixedTsChanges) = // With this logic, we assign to the initial non-Send events the same timestamp as for the last // block. This means that we will include these events in the ephemeral state of the previous block @@ -350,7 +372,7 @@ final class BlockChunkProcessor( // assigned a sequencing time that corresponds to an actual (i.e. `Send`) event and that is also surely // at or after the acknowledged timestamp. This has no effect whatsoever on transaction processing. chunk.forgetNE.foldLeft[ - (CantonTimestamp, Seq[(CantonTimestamp, Traced[LedgerBlockEvent])]) + (CantonTimestamp, Seq[(CantonTimestamp, TracedPossiblyPrevalidated[LedgerBlockEvent])]) ]((state.lastChunkTs, Seq.empty)) { case ((lastTs, events), event) => event.value match { case send: Send => @@ -372,7 +394,8 @@ final class BlockChunkProcessor( (lastTs, (lastTs, event) +: events) } } - val fixedTsChanges: Seq[(CantonTimestamp, Traced[LedgerBlockEvent])] = revFixedTsChanges.reverse + val fixedTsChanges: Seq[(CantonTimestamp, TracedPossiblyPrevalidated[LedgerBlockEvent])] = + revFixedTsChanges.reverse (lastTsBeforeValidation, fixedTsChanges) } @@ -390,7 +413,7 @@ final class BlockChunkProcessor( announcedLsu: Option[AnnouncedLsu], )(implicit traceContext: TraceContext): Option[CantonTimestamp] = { val invariant = providedTimestamp > lastTs - orderingTimeFixMode match { + parameters.orderingTimeFixMode match { case OrderingTimeFixMode.ValidateOnly => // only check the invariant, if the provided timestamp is before the upgrade time @@ -421,12 +444,14 @@ final class BlockChunkProcessor( sequencersSequencerCounter: Option[SequencerCounter], height: Long, index: Int, - submissionRequests: Seq[(CantonTimestamp, Traced[SignedSubmissionRequest], SequencerId)], + submissionRequests: Seq[ + (CantonTimestamp, TracedPossiblyPrevalidated[SignedSubmissionRequest], SequencerId) + ], skipFreshInFlightValidationCheck: AggregationId => Boolean, )(implicit executionContext: ExecutionContext ): FutureUnlessShutdown[Seq[SequencedPreValidatedSubmissionResult]] = - MonadUtil.parTraverseWithLimit(batchingConfig.parallelism)(submissionRequests.zipWithIndex) { + MonadUtil.parTraverseWithLimit(parameters.parallelism)(submissionRequests.zipWithIndex) { case ((sequencingTimestamp, tracedSubmissionRequest, orderingSequencerId), requestIndex) => tracedSubmissionRequest.withTraceContext { implicit traceContext => signedSubmissionRequest => @@ -540,8 +565,8 @@ final class BlockChunkProcessor( } private def processAcknowledgements( - state: State, - fixedTsChanges: Seq[(CantonTimestamp, Traced[LedgerBlockEvent])], + state: AccumulatedStateProcessingBlocks, + fixedTsChanges: Seq[(CantonTimestamp, TracedPossiblyPrevalidated[LedgerBlockEvent])], )(implicit ec: ExecutionContext, traceContext: TraceContext, @@ -566,8 +591,9 @@ final class BlockChunkProcessor( synchronizerSuccessorO <- snapshot.ipsSnapshot .announcedLsu() .map(_.map { case (successor, _) => successor }) - allAcknowledgements = fixedTsChanges.collect { case (_, t @ Traced(Acknowledgment(_, ack))) => - t.map(_ => ack) + allAcknowledgements = fixedTsChanges.collect { + case (_, t @ TracedPossiblyPrevalidated(Acknowledgment(_, ack), _)) => + t.map(_ => ack) } (goodTsAcks, futureAcks) = allAcknowledgements.partition { tracedSignedAck => // In this condition we allow acks of timestamps that are in the future @@ -600,38 +626,53 @@ final class BlockChunkProcessor( SequencerError.InvalidAcknowledgementTimestamp.Error(member, timestamp, state.lastBlockTs) (member, timestamp, error) }) - sigChecks <- FutureUnlessShutdown.sequence(goodTsAcks.map(_.withTraceContext { - implicit traceContext => signedAck => - val ack = signedAck.content - for { - snapshotToVerify <- EitherT.right( - if (protocolVersion < ProtocolVersion.v35) - FutureUnlessShutdown.pure(snapshot) - else { - SyncCryptoClient.getSnapshotForTimestamp( - synchronizerSyncCryptoApi, - ack.timestamp, - previousTimestampO, - ) - } - ) - acksR <- signedAck - .verifySignature( - snapshotToVerify, - ack.member, - HashPurpose.AcknowledgementSignature, - ) - .leftMap(error => - ( - ack.member, - ack.timestamp, - SequencerError.InvalidAcknowledgementSignature - .Error(signedAck, state.lastBlockTs, error): BaseAlarm, - ) + sigChecks <- FutureUnlessShutdown.sequence( + goodTsAcks.map(prevalidatedEvent => + { + implicit val traceContext: TraceContext = prevalidatedEvent.traceContext + val signedAck = prevalidatedEvent.value + val ack = signedAck.content + for { + snapshotToVerify <- EitherT.right( + if (protocolVersion < ProtocolVersion.v35) + FutureUnlessShutdown.pure(snapshot) + else { + SyncCryptoClient.getSnapshotForTimestamp( + synchronizerSyncCryptoApi, + ack.timestamp, + previousTimestampO, + ) + } ) - .map(_ => (ack.member, ack.timestamp)) - } yield acksR - }.value)) + // if the signature check already passed the prevalidation, then we restrict our check to ensuring that + // the key used is still valid (now that we have access to the actual topology snapshot) + acksR <- + (if (prevalidatedEvent.prevalidated) + signedAck + .verifyKeyUsage( + snapshotToVerify, + ack.member, + ) + else + signedAck + .verifySignature( + snapshotToVerify, + ack.member, + HashPurpose.AcknowledgementSignature, + )) + .leftMap(error => + ( + ack.member, + ack.timestamp, + SequencerError.InvalidAcknowledgementSignature + .Error(signedAck, state.lastBlockTs, error): BaseAlarm, + ) + ) + .map(_ => (ack.member, ack.timestamp)) + } yield acksR + }.value + ) + ) (invalidSigAcks, validSigAcks) = sigChecks.separate acksByMember = validSigAcks // Look for the highest acked timestamp by each member diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdate.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdate.scala index 22dd86f064..944e1f56e3 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdate.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdate.scala @@ -51,8 +51,6 @@ final case class CompleteBlockUpdate(block: BlockInfo) extends OrderedBlockUpdat * aggregations. * @param lastSequencerEventTimestamp * The highest timestamp of an event in `events` addressed to the sequencer, if any. - * @param inFlightAggregations - * Updated inFlightAggregations to be used for processing subsequent chunks. * @param submissionsOutcomes * A list of internal block sequencer states after processing submissions for the chunk. This is * used by the unified sequencer to generate and write events in the database sequencer. @@ -62,13 +60,11 @@ final case class ChunkUpdate( invalidAcknowledgements: Seq[(Member, CantonTimestamp, BaseAlarm)] = Seq.empty, inFlightAggregationUpdates: InFlightAggregationUpdates = Map.empty, lastSequencerEventTimestamp: Option[CantonTimestamp], - inFlightAggregations: InFlightAggregations, submissionsOutcomes: Seq[SubmissionOutcome] = Seq.empty, ) extends OrderedBlockUpdate object ChunkUpdate { val noop = ChunkUpdate( - lastSequencerEventTimestamp = None, - inFlightAggregations = InFlightAggregations.empty, + lastSequencerEventTimestamp = None ) } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdateGenerator.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdateGenerator.scala index 03439c8995..6ee69e804a 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdateGenerator.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdateGenerator.scala @@ -6,7 +6,7 @@ package com.digitalasset.canton.synchronizer.block.update import cats.syntax.either.* import cats.syntax.functorFilter.* import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.config.BatchingConfig +import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.crypto.{SyncCryptoApi, SynchronizerCryptoClient} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps @@ -28,7 +28,6 @@ import com.digitalasset.canton.synchronizer.block.data.{BlockEphemeralState, Blo import com.digitalasset.canton.synchronizer.block.{BlockEvents, LedgerBlockEvent, RawLedgerBlock} import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.Sequencer.SignedSubmissionRequest -import com.digitalasset.canton.synchronizer.sequencer.block.BlockSequencerFactory.OrderingTimeFixMode import com.digitalasset.canton.synchronizer.sequencer.errors.SequencerError.{ InvalidLedgerEvent, SequencingTimeNotAdmissible, @@ -42,8 +41,8 @@ import com.digitalasset.canton.synchronizer.sequencer.traffic.SequencerRateLimit import com.digitalasset.canton.synchronizer.sequencer.{AnnouncedLsu, SubmissionOutcome} import com.digitalasset.canton.topology.* import com.digitalasset.canton.tracing.{Spanning, TraceContext, Traced} -import com.digitalasset.canton.util.MaxBytesToDecompress import com.digitalasset.canton.util.collection.IterableUtil +import com.digitalasset.canton.util.{MaxBytesToDecompress, MonadUtil, TracedPossiblyPrevalidated} import com.digitalasset.canton.version.ProtocolVersion import io.opentelemetry.api.trace.Tracer @@ -78,30 +77,61 @@ import scala.concurrent.ExecutionContext trait BlockUpdateGenerator { import BlockUpdateGenerator.* - type InternalState - - def internalStateFor(state: BlockEphemeralState): InternalState - def extractBlockEvents(tracedBlock: Traced[RawLedgerBlock]): Traced[BlockEvents] + /** Optimistically checks the sender signature and envelope signatures for validity (but not + * whether the key is still valid) + */ + def prevalidateSignatures(chunk: BlockChunk, parallelism: PositiveInt)(implicit + traceContext: TraceContext, + executionContext: ExecutionContext, + ): FutureUnlessShutdown[BlockChunk] + def chunkBlock(block: BlockEvents)(implicit traceContext: TraceContext ): immutable.Iterable[BlockChunk] - def processBlockChunk(state: InternalState, chunk: BlockChunk)(implicit + def processBlockChunk(state: AccumulatedStateProcessingBlocks, chunk: BlockChunk)(implicit ec: ExecutionContext, traceContext: TraceContext, - ): FutureUnlessShutdown[(InternalState, OrderedBlockUpdate)] + ): FutureUnlessShutdown[(AccumulatedStateProcessingBlocks, OrderedBlockUpdate)] } object BlockUpdateGenerator { + /** Internal state + * + * @param latestPendingTopologyTransactionTimestamp + * is used to determine whether a topology tick should be emitted at the end of the block, so + * it is updated whenever we see a topology transaction. We only use it to decide if we should + * emit a tick at the end of a block. It may be incorrect if a topology tx was rejected, but + * that doesn't matter much from the perspective of "ticking" the topology. + */ + final case class AccumulatedStateProcessingBlocks( + lastBlockTs: CantonTimestamp, + lastChunkTs: CantonTimestamp, + latestSequencerEventTimestamp: Option[CantonTimestamp], + latestPendingTopologyTransactionTimestamp: Option[CantonTimestamp], + inFlightAggregations: InFlightAggregations, + ) + object AccumulatedStateProcessingBlocks { + def fromEphemeralState(state: BlockEphemeralState): AccumulatedStateProcessingBlocks = + AccumulatedStateProcessingBlocks( + lastBlockTs = state.latestBlock.lastTs, + lastChunkTs = state.latestBlock.lastTs, + latestSequencerEventTimestamp = state.latestBlock.latestSequencerEventTimestamp, + latestPendingTopologyTransactionTimestamp = + state.latestBlock.latestPendingTopologyTransactionTimestamp, + inFlightAggregations = state.inFlightAggregations, + ) + } + sealed trait BlockChunk extends Product with Serializable final case class NextChunk( blockHeight: Long, chunkIndex: Int, - events: NonEmpty[Seq[Traced[LedgerBlockEvent]]], + events: NonEmpty[Seq[TracedPossiblyPrevalidated[LedgerBlockEvent]]], ) extends BlockChunk /** @param baseBlockSequencingTime @@ -121,14 +151,12 @@ class BlockUpdateGeneratorImpl( synchronizerSyncCryptoApi: SynchronizerCryptoClient, sequencerId: SequencerId, rateLimitManager: SequencerRateLimitManager, - orderingTimeFixMode: OrderingTimeFixMode, - lsuSequencingBounds: Option[LsuSequencingBounds], drSequencingTimeUpperBound: Option[DisasterRecoverySequencingTimeUpperBound], getAnnouncedLsu: => Option[AnnouncedLsu], producePostOrderingTopologyTicks: Boolean, - metrics: SequencerMetrics, - batchingConfig: BatchingConfig, consistencyChecks: Boolean, + parameters: BlockProcessingParameters, + metrics: SequencerMetrics, memberValidator: SequencerMemberValidator, protected val loggerFactory: NamedLoggerFactory, )(implicit val closeContext: CloseContext, tracer: Tracer) @@ -136,7 +164,6 @@ class BlockUpdateGeneratorImpl( with NamedLogging with Spanning { import BlockUpdateGenerator.* - import BlockUpdateGeneratorImpl.* private val epsilon = synchronizerSyncCryptoApi.staticSynchronizerParameters.topologyChangeDelay private val protocolVersion = synchronizerSyncCryptoApi.psid.protocolVersion @@ -149,25 +176,12 @@ class BlockUpdateGeneratorImpl( synchronizerSyncCryptoApi, sequencerId, rateLimitManager, - orderingTimeFixMode, - lsuSequencingBounds, - batchingConfig, - loggerFactory, + parameters, metrics, memberValidator = memberValidator, + loggerFactory, ) - override type InternalState = State - - override def internalStateFor(state: BlockEphemeralState): InternalState = State( - lastBlockTs = state.latestBlock.lastTs, - lastChunkTs = state.latestBlock.lastTs, - latestSequencerEventTimestamp = state.latestBlock.latestSequencerEventTimestamp, - latestPendingTopologyTransactionTimestamp = - state.latestBlock.latestPendingTopologyTransactionTimestamp, - inFlightAggregations = state.inFlightAggregations, - ) - /** Return true if the event contains only [[LsuSequencingTestMessage]] and recipients are * mediator groups. Since the method open envelopes, which is resources consuming, should be * called only before upgrade time. @@ -218,7 +232,7 @@ class BlockUpdateGeneratorImpl( case Right(event) => val checksResult = for { - _ <- checkLsuSequencingBounds(event, lsuSequencingBounds) + _ <- checkLsuSequencingBounds(event, parameters.lsuSequencingBounds) _ <- checkDrSequencingTimeUpperBound(event, drSequencingTimeUpperBound) } yield () checksResult.fold( @@ -316,7 +330,12 @@ class BlockUpdateGeneratorImpl( .splitAfter(blockEvents.events)(event => isAddressingSequencers(event.value)) .zipWithIndex .map { case (events, index) => - NextChunk(blockHeight, index, events) + NextChunk( + blockHeight, + index, + // map to prevalidated type + events.map(tv => TracedPossiblyPrevalidated.notValidated(tv.value)(tv.traceContext)), + ) } val chunks = dataChunks ++ Seq(tick) ++ Seq(EndOfBlock(blockHeight)) @@ -338,10 +357,15 @@ class BlockUpdateGeneratorImpl( case _ => false } - override final def processBlockChunk(state: InternalState, chunk: BlockChunk)(implicit + override final def processBlockChunk( + state: BlockUpdateGenerator.AccumulatedStateProcessingBlocks, + chunk: BlockChunk, + )(implicit ec: ExecutionContext, traceContext: TraceContext, - ): FutureUnlessShutdown[(InternalState, OrderedBlockUpdate)] = + ): FutureUnlessShutdown[ + (BlockUpdateGenerator.AccumulatedStateProcessingBlocks, OrderedBlockUpdate) + ] = chunk match { case EndOfBlock(height) => val newState = state.copy(lastBlockTs = state.lastChunkTs) @@ -448,26 +472,35 @@ class BlockUpdateGeneratorImpl( } } + override def prevalidateSignatures( + chunk: BlockChunk, + parallelism: PositiveInt, + )(implicit + traceContext: TraceContext, + executionContext: ExecutionContext, + ): FutureUnlessShutdown[BlockChunk] = chunk match { + case NextChunk(blockHeight, chunkIndex, events) => + val snapshot = synchronizerSyncCryptoApi.headSnapshot + MonadUtil + .parTraverseWithLimit(parallelism)(events)(_.withTraceContext { + implicit traceContext => event => + blockChunkProcessor.prevalidateLedgerBlockEvent(snapshot, event) + }) + .map { events => + NextChunk( + blockHeight, + chunkIndex, + NonEmpty.from(events).getOrElse(sys.error("cannot be empty")), + ) + } + + case other => FutureUnlessShutdown.pure(other) + } + } object BlockUpdateGeneratorImpl { - /** Internal state - * - * @param latestPendingTopologyTransactionTimestamp - * is used to determine whether a topology tick should be emitted at the end of the block, so - * it is updated whenever we see a topology transaction. We only use it to decide if we should - * emit a tick at the end of a block. It may be incorrect if a topology tx was rejected, but - * that doesn't matter much from the perspective of "ticking" the topology. - */ - private[block] final case class State( - lastBlockTs: CantonTimestamp, - lastChunkTs: CantonTimestamp, - latestSequencerEventTimestamp: Option[CantonTimestamp], - latestPendingTopologyTransactionTimestamp: Option[CantonTimestamp], - inFlightAggregations: InFlightAggregations, - ) - /** Positive outcome of the pre-validation step * * Case class used to carry over data from the parallel validation into the sequential validation diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/InFlightAggregationHandler.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/InFlightAggregationHandler.scala index 9b09ad5083..82ff3b590a 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/InFlightAggregationHandler.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/InFlightAggregationHandler.scala @@ -399,7 +399,7 @@ class InFlightAggregationHandler( SubmissionOutcome.Reject.logAndCreate( submissionRequest, sequencingTimestamp, - SequencerErrors.AggregateSubmissionAlreadySent(message), + SequencerErrors.AggregateSubmissionAlreadySent.apply(message, protocolVersion), ) case InFlightAggregation.AggregationStuffing(_, at) => val message = diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/SequencedSubmissionsValidator.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/SequencedSubmissionsValidator.scala index 070fc0f025..7b8ba3e029 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/SequencedSubmissionsValidator.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/SequencedSubmissionsValidator.scala @@ -4,7 +4,6 @@ package com.digitalasset.canton.synchronizer.block.update import cats.data.EitherT -import cats.syntax.functor.* import com.digitalasset.canton.crypto.SyncCryptoApi import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.FutureUnlessShutdown @@ -15,6 +14,7 @@ import com.digitalasset.canton.sequencing.protocol.{ MemberRecipientOrBroadcast, SubmissionRequest, } +import com.digitalasset.canton.synchronizer.block.update.BlockUpdateGenerator.AccumulatedStateProcessingBlocks import com.digitalasset.canton.synchronizer.sequencer.* import com.digitalasset.canton.synchronizer.sequencer.Sequencer.SignedSubmissionRequest import com.digitalasset.canton.topology.SequencerId @@ -24,7 +24,7 @@ import com.digitalasset.canton.util.{ErrorUtil, MonadUtil} import scala.concurrent.ExecutionContext -import BlockUpdateGeneratorImpl.{PrevalidationOutcome, SequencedPreValidatedSubmissionResult, State} +import BlockUpdateGeneratorImpl.{PrevalidationOutcome, SequencedPreValidatedSubmissionResult} import SequencedSubmissionsValidator.SequencedSubmissionsValidationResult import SubmissionRequestValidator.{SubmissionRequestValidationResult, TrafficConsumption} @@ -46,7 +46,7 @@ private[update] final class SequencedSubmissionsValidator( ) extends NamedLogging { def sequentialApplySubmissionsAndEmitOutcomes( - state: State, + state: AccumulatedStateProcessingBlocks, height: Long, sequencedValidatedSubmissions: Seq[SequencedPreValidatedSubmissionResult], )(implicit ec: ExecutionContext): FutureUnlessShutdown[SequencedSubmissionsValidationResult] = diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/SubmissionRequestValidator.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/SubmissionRequestValidator.scala index 7cc99524f2..24662cccd2 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/SubmissionRequestValidator.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/SubmissionRequestValidator.scala @@ -7,9 +7,14 @@ import cats.data.{EitherT, WriterT} import cats.kernel.Monoid import cats.syntax.either.* import com.digitalasset.base.error.BaseAlarm -import com.digitalasset.canton.config.BatchingConfig +import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.crypto.SignatureCheckError.GeneralError -import com.digitalasset.canton.crypto.{HashPurpose, SignatureCheckError, SyncCryptoApi} +import com.digitalasset.canton.crypto.{ + HashPurpose, + SignatureCheckError, + SyncCryptoApi, + SynchronizerSnapshotSyncCryptoApi, +} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.CantonBaseError @@ -18,13 +23,14 @@ import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.sequencing.GroupAddressResolver import com.digitalasset.canton.sequencing.protocol.* import com.digitalasset.canton.sequencing.traffic.TrafficReceipt +import com.digitalasset.canton.synchronizer.block.LedgerBlockEvent import com.digitalasset.canton.synchronizer.block.update.BlockUpdateGeneratorImpl.PrevalidationOutcome import com.digitalasset.canton.synchronizer.sequencer.* import com.digitalasset.canton.synchronizer.sequencer.errors.SequencerError import com.digitalasset.canton.synchronizer.sequencer.store.SequencerMemberValidator import com.digitalasset.canton.topology.* -import com.digitalasset.canton.tracing.{TraceContext, Traced} -import com.digitalasset.canton.util.{EitherTUtil, MonadUtil} +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.{EitherTUtil, MonadUtil, TracedPossiblyPrevalidated} import com.digitalasset.canton.version.ProtocolVersion import scala.concurrent.ExecutionContext @@ -35,16 +41,71 @@ import SubmissionRequestValidator.* * * Can run in parallel to other submission requests. * + * @param enablePrevalidation + * if true then we will move the signature validation into its separate stage + * * NOTE RENAME ME INTO PARALLESUBMISSIONREQUESTVALIDATOR */ private[update] final class SubmissionRequestValidator( inFlightAggregationHandler: InFlightAggregationHandler, - batchingConfig: BatchingConfig, - override val loggerFactory: NamedLoggerFactory, memberValidator: SequencerMemberValidator, protocolVersion: ProtocolVersion, + enablePrevalidation: Boolean, + parallelism: PositiveInt, + override val loggerFactory: NamedLoggerFactory, ) extends NamedLogging { + def prevalidateEvent( + approxCryptoSnapshot: SynchronizerSnapshotSyncCryptoApi, + event: LedgerBlockEvent, + )(implicit + traceContext: TraceContext, + executionContext: ExecutionContext, + ): FutureUnlessShutdown[TracedPossiblyPrevalidated[LedgerBlockEvent]] = if (!enablePrevalidation) + FutureUnlessShutdown.pure(TracedPossiblyPrevalidated(event, prevalidated = false)) + else + // pre-validate all signatures mechanically that are used. + // whether the key used is really valid at the time of sequencing is then checked again, but + // that check is much cheaper than checking the signatures + event match { + case send: LedgerBlockEvent.Send => + val submissionSignatureFE = + checkSignatureOnSubmissionRequest( + TracedPossiblyPrevalidated.notValidated(send.signedSubmissionRequest), + approxCryptoSnapshot, + reportError = false, + ).value + val batchSignatureFE = checkClosedEnvelopesSignatures( + approxCryptoSnapshot, + TracedPossiblyPrevalidated.notValidated(send.signedSubmissionRequest.content), + approxCryptoSnapshot.ipsSnapshot.timestamp, + reportError = false, + ).value + for { + submissionIsValidSignature <- submissionSignatureFE + envelopesHaveValidSignature <- batchSignatureFE + } yield { + // if all have valid signatures, then we'll mark this event as prevalidated, which means that the signature + // validation will be skipped and we only need to check that the key used was really the one valid + // at the given time (using verifyKeyOwner) + TracedPossiblyPrevalidated( + send, + prevalidated = submissionIsValidSignature.isRight && envelopesHaveValidSignature.isRight, + ) + } + case ack: LedgerBlockEvent.Acknowledgment => + ack.request + .verifySignature( + approxCryptoSnapshot, + ack.request.content.member, + HashPurpose.AcknowledgementSignature, + ) + .value + .map { result => + TracedPossiblyPrevalidated(ack, prevalidated = result.isRight) + } + } + /** Performs validations that don't affect any state and resolves groups to members. Can and * should be run in parallel on many submissions (e.g. in a chunk). * @@ -59,7 +120,7 @@ private[update] final class SubmissionRequestValidator( */ def performIndependentValidations( sequencingTimestamp: CantonTimestamp, - signedSubmissionRequest: Traced[SignedContent[SubmissionRequest]], + signedSubmissionRequest: TracedPossiblyPrevalidated[SignedContent[SubmissionRequest]], snapshotToValidateSubmissionRequest: SyncCryptoApi, topologySnapshotFromRequestO: Option[SyncCryptoApi], topologyTimestampError: Option[SequencerDeliverError], @@ -286,7 +347,7 @@ private[update] final class SubmissionRequestValidator( ), ): SubmissionOutcome ) - _ <- MonadUtil.parTraverseWithLimit(batchingConfig.parallelism)(groups) { group => + _ <- MonadUtil.parTraverseWithLimit(parallelism)(groups) { group => val nonRegisteredF = memberValidator .areMembersRegisteredAt(group.active ++ group.passive, sequencingTimestamp) @@ -317,14 +378,15 @@ private[update] final class SubmissionRequestValidator( private def checkClosedEnvelopesSignatures( topologyOrSequencingSnapshot: SyncCryptoApi, - submissionRequest: Traced[SubmissionRequest], + submissionRequest: TracedPossiblyPrevalidated[SubmissionRequest], sequencingTimestamp: CantonTimestamp, + reportError: Boolean = true, )(implicit traceContext: TraceContext, executionContext: ExecutionContext, ): EitherT[FutureUnlessShutdown, SubmissionOutcome, Unit] = MonadUtil - .parTraverseWithLimit_(batchingConfig.parallelism)(submissionRequest.value.batch.envelopes) { + .parTraverseWithLimit_(parallelism)(submissionRequest.value.batch.envelopes) { closedEnvelope => EitherT .fromEither[FutureUnlessShutdown]( @@ -334,21 +396,29 @@ private[update] final class SubmissionRequestValidator( } ) .flatMap { closedUncompressedEnvelope => - closedUncompressedEnvelope.verifySignatures( - topologyOrSequencingSnapshot, - submissionRequest.value.sender, - ) + if (submissionRequest.prevalidated) { + closedUncompressedEnvelope.verifyKeyUsage( + topologyOrSequencingSnapshot, + submissionRequest.value.sender, + ) + } else + closedUncompressedEnvelope.verifySignatures( + topologyOrSequencingSnapshot, + submissionRequest.value.sender, + ) } } .leftMap { error => - SequencerError.InvalidEnvelopeSignature - .Error( - submissionRequest.value, - error, - sequencingTimestamp, - topologyOrSequencingSnapshot.ipsSnapshot.timestamp, - ) - .report() + if (reportError) { + SequencerError.InvalidEnvelopeSignature + .Error( + submissionRequest.value, + error, + sequencingTimestamp, + topologyOrSequencingSnapshot.ipsSnapshot.timestamp, + ) + .report() + } SubmissionOutcome.Discard } @@ -384,33 +454,40 @@ private[update] final class SubmissionRequestValidator( } yield res private def checkSignatureOnSubmissionRequest( - signedSubmissionRequest: Traced[SignedContent[SubmissionRequest]], + signedSubmissionRequest: TracedPossiblyPrevalidated[SignedContent[SubmissionRequest]], topologyOrSequencingSnapshot: SyncCryptoApi, + reportError: Boolean = true, )(implicit traceContext: TraceContext, executionContext: ExecutionContext, ): EitherT[FutureUnlessShutdown, SubmissionOutcome, Unit] = { val alarm = for { - _ <- - signedSubmissionRequest.value - .verifySignature( - topologyOrSequencingSnapshot, - signedSubmissionRequest.value.content.sender, - HashPurpose.SubmissionRequestSignature, - ) - .leftMap[BaseAlarm](error => - SequencerError.InvalidSubmissionRequestSignature.Error( - signedSubmissionRequest.value, - error, - topologyOrSequencingSnapshot.ipsSnapshot.timestamp, - signedSubmissionRequest.value.timestampOfSigningKey, - ) + _ <- (if (signedSubmissionRequest.prevalidated) + signedSubmissionRequest.value.verifyKeyUsage( + topologyOrSequencingSnapshot, + signedSubmissionRequest.value.content.sender, + ) + else + signedSubmissionRequest.value + .verifySignature( + topologyOrSequencingSnapshot, + signedSubmissionRequest.value.content.sender, + HashPurpose.SubmissionRequestSignature, + )) + .leftMap[BaseAlarm](error => + SequencerError.InvalidSubmissionRequestSignature.Error( + signedSubmissionRequest.value, + error, + topologyOrSequencingSnapshot.ipsSnapshot.timestamp, + signedSubmissionRequest.value.timestampOfSigningKey, ) + ) } yield () alarm.leftMap { a => - a.report() + if (reportError) + a.report() SubmissionOutcome.Discard } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/ConfirmationRequestAndResponseProcessor.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/ConfirmationRequestAndResponseProcessor.scala index 45703be867..2e431aead0 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/ConfirmationRequestAndResponseProcessor.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/ConfirmationRequestAndResponseProcessor.scala @@ -56,7 +56,6 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( crypto: SynchronizerCryptoClient, timeTracker: SynchronizerTimeTracker, val mediatorState: MediatorState, - asynchronousProcessing: Boolean, protected val loggerFactory: NamedLoggerFactory, override val timeouts: ProcessingTimeout, batchingConfig: BatchingConfig, @@ -70,21 +69,17 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( private val psid = crypto.psid - val processingQueue: ProcessingQueue[RequestId] = - if (asynchronousProcessing) new ShardedSequentialProcessingQueue - else new SynchronousProcessingQueue + val processingQueue: ShardedSequentialProcessingQueue[RequestId] = + new GarbageCollectedShardedSequentialProcessingQueue() override def observeTimestampWithoutEvent(sequencingTimestamp: CantonTimestamp)(implicit traceContext: TraceContext - ): HandlerResult = - if (asynchronousProcessing) handleTimeouts(sequencingTimestamp) - else HandlerResult.synchronous(handleTimeouts(sequencingTimestamp).flatMap(_.unwrap)) + ): HandlerResult = handleTimeouts(sequencingTimestamp) override def handleMediatorEvent( event: MediatorEvent )(implicit traceContext: TraceContext): HandlerResult = - if (asynchronousProcessing) handleMediatorEventAsynchronous(event) - else handleMediatorEventSynchronous(event) + handleMediatorEventAsynchronous(event) private def handleMediatorEventAsynchronous( event: MediatorEvent @@ -94,19 +89,6 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( asyncEventHandling <- doHandleMediatorEvent(event) } yield asyncEventHandling |+| asyncTimeoutHandling - private def handleMediatorEventSynchronous( - event: MediatorEvent - )(implicit traceContext: TraceContext): HandlerResult = { - // to process synchronously, we inline the async results before continuing on to the next step - val future = for { - asyncTimeoutHandling <- handleTimeouts(event.sequencingTimestamp) - _ <- asyncTimeoutHandling.unwrap - asyncEventHandling <- doHandleMediatorEvent(event) - unthrottledAsync <- asyncEventHandling.unwrap - } yield unthrottledAsync - HandlerResult.synchronous(future) - } - private def doHandleMediatorEvent( event: MediatorEvent )(implicit traceContext: TraceContext): HandlerResult = { @@ -146,7 +128,7 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( // inner future: finalizedPromise — completes only after the verdict is persisted to DB HandlerResult.asynchronous( processingQueue - .enqueueForProcessing(event.requestId)( + .executeUS(event.requestId)( processRequest( event.requestId, counter, @@ -155,7 +137,8 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( requestEnvelope, rootHashMessages, batchAlsoContainsTopologyTransaction, - ) + ), + "process request", ) ) case MediatorEvent.Response( @@ -166,7 +149,7 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( recipients, ) => HandlerResult.asynchronousUnit( - processingQueue.enqueueForProcessing(responses.message.requestId)( + processingQueue.executeUS(responses.message.requestId)( processResponses( responseTimestamp, counter, @@ -175,7 +158,8 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( responses, topologyTimestamp, recipients, - ) + ), + "process response", ) ) } @@ -203,20 +187,22 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( requestId: RequestId, timestamp: CantonTimestamp, ): FutureUnlessShutdown[Unit] = { + + // the event causing the timeout is likely unrelated to the transaction we're actually timing out, + // so use the original request trace context + implicit val traceContext = + mediatorState.getPending(requestId).fold(TraceContext.empty)(_.requestTraceContext) + def pendingRequestNotFound: FutureUnlessShutdown[Unit] = { // This is logged at trace, because otherwise this would be logged for each request on DEBUG, which is actually not very helpful and just noise. - noTracingLogger.trace( + logger.trace( s"Pending aggregation for request [$requestId] not found. This implies the request has been finalized since the timeout was scheduled." ) FutureUnlessShutdown.unit } - processingQueue.enqueueForProcessing(requestId)( + processingQueue.executeUS(requestId)( mediatorState.getPending(requestId).fold(pendingRequestNotFound) { responseAggregation => - // the event causing the timeout is likely unrelated to the transaction we're actually timing out, - // so use the original request trace context - implicit val traceContext: TraceContext = responseAggregation.requestTraceContext - logger.info( s"Phase 6: Request ${requestId.unwrap}: Timeout in state ${responseAggregation.state} at $timestamp" ) @@ -227,7 +213,8 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( MonadUtil.whenM(mediatorState.replace(responseAggregation, timedOut))( sendResultIfDone(timedOut, responseAggregation.decisionTime) ) - } + }, + "handle timeout", ) } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/Mediator.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/Mediator.scala index 95a96b5ead..e26dc05b89 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/Mediator.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/Mediator.scala @@ -14,7 +14,6 @@ import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.crypto.SynchronizerCryptoClient import com.digitalasset.canton.data.{CantonTimestamp, SynchronizerSuccessor} -import com.digitalasset.canton.environment.CantonNodeParameters import com.digitalasset.canton.error.MediatorError import com.digitalasset.canton.lifecycle.* import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} @@ -94,10 +93,9 @@ private[mediator] class Mediator( val synchronizerOutboxHandle: SynchronizerOutboxHandle, val timeTracker: SynchronizerTimeTracker, val state: MediatorState, - asynchronousProcessing: Boolean, private[canton] val sequencerCounterTrackerStore: SequencerCounterTrackerStore, sequencedEventStore: SequencedEventStore, - parameters: CantonNodeParameters, + parameters: MediatorNodeParameters, clock: Clock, val metrics: MediatorMetrics, protected val loggerFactory: NamedLoggerFactory, @@ -163,7 +161,13 @@ private[mediator] class Mediator( ) private val verdictSender = - VerdictSender(sequencerClient, syncCrypto, mediatorId, parameters.batchingConfig, loggerFactory) + VerdictSender( + sequencerClient, + syncCrypto, + mediatorId, + parameters.verdictSenderParameters, + loggerFactory, + ) private val processor = new ConfirmationRequestAndResponseProcessor( mediatorId, @@ -171,7 +175,6 @@ private[mediator] class Mediator( syncCrypto, timeTracker, state, - asynchronousProcessing = asynchronousProcessing, loggerFactory, timeouts, parameters.batchingConfig, @@ -344,7 +347,6 @@ private[mediator] class Mediator( verdict: MediatorVerdict.MediatorReject, )(implicit tc: TraceContext): FutureUnlessShutdown[Unit] = { val requestId = RequestId(timestamp) - for { snapshot <- syncCrypto.awaitSnapshot(timestamp) synchronizerParameters <- snapshot.ipsSnapshot diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorConfig.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorConfig.scala index 2c0c0aafad..23b7504add 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorConfig.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorConfig.scala @@ -12,13 +12,10 @@ import com.digitalasset.canton.config.{BatchAggregatorConfig, PositiveFiniteDura * * @param pruning * mediator pruning configuration - * @param asynchronousProcessing - * whether the mediator should process events asynchronously or purely sequential */ final case class MediatorConfig( pruning: MediatorPruningConfig = MediatorPruningConfig(), deduplicationStore: DeduplicationStoreConfig = DeduplicationStoreConfig(), - asynchronousProcessing: Boolean = true, ) /** Configuration for mediator pruning diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorNode.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorNode.scala index 03f73bea3d..c60797038a 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorNode.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorNode.scala @@ -11,7 +11,9 @@ import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.admin.mediator.v30.MediatorStatusServiceGrpc.MediatorStatusService import com.digitalasset.canton.auth.CantonAdminTokenDispenser import com.digitalasset.canton.concurrent.ExecutionContextIdlenessExecutorService +import com.digitalasset.canton.config import com.digitalasset.canton.config.* +import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.connection.GrpcApiInfoService import com.digitalasset.canton.connection.v30.ApiInfoServiceGrpc import com.digitalasset.canton.crypto.{ @@ -46,6 +48,7 @@ import com.digitalasset.canton.sequencing.client.{ } import com.digitalasset.canton.store.* import com.digitalasset.canton.synchronizer.Synchronizer +import com.digitalasset.canton.synchronizer.mediator.VerdictSenderParameters import com.digitalasset.canton.synchronizer.mediator.admin.data.MediatorNodeStatus import com.digitalasset.canton.synchronizer.mediator.admin.gprc.{ InitializeMediatorRequest, @@ -90,6 +93,43 @@ import java.util.concurrent.atomic.AtomicReference import scala.concurrent.Future import scala.util.Success +/** configuration parameters for the delayed verdict sender + * + * In order to reduce the load on the sequencer, we can configure the mediators to delay the + * sending of the verdicts to the sequencer. For every verdict, threshold + immediateExtra + * mediators will send the verdict immediately. + * + * The rest will start to send verdicts after an initial delay plus an incremental delay based on + * their priority in the queue. The priority is determined rotating the order of the mediators in + * the topology state using the request-id as an offset (modulo the number of active mediators). + * + * @param enabled + * if true (default), then verdicts will be sent with a delay + * @param livenessMargin + * how many verdicts will be sent immediately in excess of threshold + * @param immediateBeforeDeadline + * immediately send the verdict if we are approaching the deadline + * @param initialDelay + * how long should a mediator wait before sending + * @param delay + * incremental delay between senders + */ +final case class DelayedVerdictSenderConfig( + enabled: Boolean = true, + livenessMargin: NonNegativeInt = DelayedVerdictSenderConfig.DefaultLivenessMargin, + immediateBeforeDeadline: config.NonNegativeFiniteDuration = + DelayedVerdictSenderConfig.DefaultImmediateBeforeDeadline, + initialDelay: config.NonNegativeFiniteDuration = DelayedVerdictSenderConfig.DefaultInitialDelay, + delay: config.NonNegativeFiniteDuration = DelayedVerdictSenderConfig.DefaultDelay, +) + +object DelayedVerdictSenderConfig { + private val DefaultLivenessMargin = NonNegativeInt.three + private val DefaultImmediateBeforeDeadline = config.NonNegativeFiniteDuration.ofSeconds(5) + private val DefaultInitialDelay = config.NonNegativeFiniteDuration.ofSeconds(3) + private val DefaultDelay = config.NonNegativeFiniteDuration.ofSeconds(1) +} + /** Various parameters for non-standard mediator settings * * @param dontWarnOnDeprecatedPV @@ -103,15 +143,26 @@ final case class MediatorNodeParameterConfig( override val batching: BatchingConfig = BatchingConfig(), override val caching: CachingConfigs = CachingConfigs(), override val watchdog: Option[WatchdogConfig] = None, + delayedVerdictSender: DelayedVerdictSenderConfig = DelayedVerdictSenderConfig(), ) extends ProtocolConfig with LocalNodeParametersConfig final case class MediatorNodeParameters( general: CantonNodeParameters.General, protocol: CantonNodeParameters.Protocol, + delayedVerdictSender: DelayedVerdictSenderConfig, ) extends CantonNodeParameters with HasGeneralCantonNodeParameters - with HasProtocolCantonNodeParameters + with HasProtocolCantonNodeParameters { + def verdictSenderParameters: VerdictSenderParameters = VerdictSenderParameters( + enableDelay = delayedVerdictSender.enabled, + livenessMargin = delayedVerdictSender.livenessMargin, + immediateBeforeDeadline = delayedVerdictSender.immediateBeforeDeadline, + initialDelay = delayedVerdictSender.initialDelay, + delay = delayedVerdictSender.delay, + parallelism = general.batchingConfig.parallelism, + ) +} final case class RemoteMediatorConfig( adminApi: FullClientConfig, @@ -267,22 +318,6 @@ class MediatorNodeBootstrap( ) with GrpcMediatorInitializationService.Callback { - private val connectionPoolFactory = new GrpcSequencerConnectionPoolFactory( - clientProtocolVersions = ProtocolVersionCompatibility.supportedProtocols(parameters), - minimumProtocolVersion = Some(ProtocolVersion.minimum), - authConfig = parameters.sequencerClient.authToken, - params = parameters.sequencerClient.clientChannelParams(parameters.tracing.propagation), - member = mediatorId, - clock = clock, - crypto = crypto, - seedForRandomnessO = arguments.testingConfig.sequencerTransportSeed, - metrics = arguments.metrics.sequencerClient.connectionPool, - metricsContext = MetricsContext.Empty, - futureSupervisor = futureSupervisor, - timeouts = timeouts, - loggerFactory = loggerFactory, - ) - override def getAdminToken: Option[String] = Some(adminTokenDispenser.getCurrentToken.secret) adminServerRegistry @@ -382,6 +417,22 @@ class MediatorNodeBootstrap( logger.info( s"Assigning mediator to ${request.synchronizerId} via sequencers ${request.sequencerConnections}" ) + val connectionPoolFactory = new GrpcSequencerConnectionPoolFactory( + clientProtocolVersions = ProtocolVersionCompatibility.supportedProtocols(parameters), + minimumProtocolVersion = Some(ProtocolVersion.minimum), + authConfig = parameters.sequencerClient.authToken, + params = parameters.sequencerClient.clientChannelParams(parameters.tracing.propagation), + member = mediatorId, + clock = clock, + crypto = crypto, + seedForRandomnessO = arguments.testingConfig.sequencerTransportSeed, + metrics = arguments.metrics.sequencerClient.connectionPool, + metricsContext = MetricsContext("psid" -> request.synchronizerId.toProtoPrimitive), + futureSupervisor = futureSupervisor, + timeouts = timeouts, + loggerFactory = loggerFactory, + ) + for { connectionPool <- EitherT.fromEither[FutureUnlessShutdown]( connectionPoolFactory @@ -535,7 +586,7 @@ class MediatorNodeBootstrap( crypto = crypto.crypto, seedForRandomnessO = arguments.testingConfig.sequencerTransportSeed, metrics = arguments.metrics.sequencerClient.connectionPool, - metricsContext = MetricsContext.Empty, + metricsContext = MetricsContext("psid" -> psid.toProtoPrimitive), futureSupervisor = futureSupervisor, timeouts = timeouts, loggerFactory = synchronizerLoggerFactory, @@ -589,7 +640,7 @@ class MediatorNodeBootstrap( staticSynchronizerParameters, crypto, cryptoConfig, - Some(arguments.metrics.kmsMetrics), + arguments.metrics.cryptoMetrics, parameters.cachingConfigs.publicKeyConversionCache, timeouts, futureSupervisor, diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorRuntimeFactory.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorRuntimeFactory.scala index b35e1971ce..836fb942a3 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorRuntimeFactory.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/MediatorRuntimeFactory.scala @@ -11,7 +11,6 @@ import com.digitalasset.canton.connection.GrpcApiInfoService import com.digitalasset.canton.connection.v30.ApiInfoServiceGrpc import com.digitalasset.canton.crypto.SynchronizerCryptoClient import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.environment.CantonNodeParameters import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, LifeCycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.mediator.admin.v30.{ @@ -126,7 +125,7 @@ object MediatorRuntimeFactory { topologyManagerStatus: TopologyManagerStatus, synchronizerOutboxFactory: SynchronizerOutboxFactory, timeTracker: SynchronizerTimeTracker, - nodeParameters: CantonNodeParameters, + nodeParameters: MediatorNodeParameters, clock: Clock, metrics: MediatorMetrics, config: MediatorConfig, @@ -190,7 +189,6 @@ object MediatorRuntimeFactory { synchronizerOutbox, timeTracker, state, - asynchronousProcessing = config.asynchronousProcessing, sequencerCounterTrackerStore, sequencedEventStore, nodeParameters, diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/ProcessingQueue.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/ProcessingQueue.scala deleted file mode 100644 index 70b90a4e2b..0000000000 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/ProcessingQueue.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.synchronizer.mediator - -import cats.data.Nested -import cats.syntax.functor.* -import com.digitalasset.canton.discard.Implicits.* -import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, PromiseUnlessShutdown} -import com.digitalasset.canton.util.Thereafter.syntax.ThereafterOps -import com.google.common.annotations.VisibleForTesting - -import scala.collection.concurrent.TrieMap -import scala.concurrent.ExecutionContext - -/** A processing queue that allows scheduling work associated with a particular identifier. - * - * @tparam Ident - * The type of the identifiers - */ -trait ProcessingQueue[Ident] { - def enqueueForProcessing[A](id: Ident)( - action: => FutureUnlessShutdown[A] - ): FutureUnlessShutdown[A] -} - -/** A processing queue that immediately executes the action. - * @tparam Ident - * The type of the identifiers - */ -class SynchronousProcessingQueue[Ident] extends ProcessingQueue[Ident] { - override def enqueueForProcessing[A](id: Ident)( - action: => FutureUnlessShutdown[A] - ): FutureUnlessShutdown[A] = action -} - -/** A processing queue that runs units of work for a particular identifier in a sequential manner, - * but allows parallel processing of work for different identifiers. If a unit of work fails or - * throws an exception, subsequent units of work for the same identifier are not executed. - * @tparam Ident - * The type of the identifiers - */ -class ShardedSequentialProcessingQueue[Ident](implicit ec: ExecutionContext) - extends ProcessingQueue[Ident] { - @VisibleForTesting - val processingQueuePerRequest = new TrieMap[Ident, FutureUnlessShutdown[Unit]]() - - override def enqueueForProcessing[A]( - id: Ident - )(action: => FutureUnlessShutdown[A]): FutureUnlessShutdown[A] = { - val processingPromise: PromiseUnlessShutdown[Unit] = PromiseUnlessShutdown.unsupervised() - val processingFuture = processingPromise.futureUS - - val previousProcessingFuture: FutureUnlessShutdown[Unit] = processingQueuePerRequest - .put(id, processingFuture) - .getOrElse(FutureUnlessShutdown.unit) - - previousProcessingFuture.flatMap(_ => action).thereafter { result => - processingPromise.complete(Nested(result).void.value) - // cleanup the processing queue - processingQueuePerRequest - .updateWith(id) { - case Some(`processingFuture`) => - // if the "processing queue" still contains the same future that we put in, we can remove the entry from the map - None - case Some(other) => - // some other future was put into the map, retain it - Some(other) - case None => - // the entry was already removed, nothing to do - None - } - .discard - - } - } -} diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/VerdictSender.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/VerdictSender.scala index c4f8545667..81422e5fcb 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/VerdictSender.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/VerdictSender.scala @@ -9,11 +9,10 @@ import cats.syntax.foldable.* import cats.syntax.functor.* import com.daml.metrics.api.MetricsContext import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.LfPartyId -import com.digitalasset.canton.config.BatchingConfig +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.crypto.{SyncCryptoError, SynchronizerCryptoClient} import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, UnlessShutdown} +import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown, UnlessShutdown} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.protocol.RequestId import com.digitalasset.canton.protocol.messages.* @@ -26,7 +25,9 @@ import com.digitalasset.canton.topology.{MediatorId, ParticipantId} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.{EitherTUtil, ErrorUtil, FutureUnlessShutdownUtil, MonadUtil} import com.digitalasset.canton.version.ProtocolVersion +import com.digitalasset.canton.{LfPartyId, config} +import java.time.Duration import scala.concurrent.ExecutionContext /** Sends confirmation result messages to the informee participants of a request. The result message @@ -47,7 +48,7 @@ private[mediator] trait VerdictSender { batch: Batch[DefaultOpenEnvelope], decisionTime: CantonTimestamp, aggregationRule: Option[AggregationRule], - sendVerdict: Boolean, + sendVerdictWithDelay: Option[Duration], )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] /** Mediator rejects are important for situations where malformed mediator confirmation request or @@ -70,19 +71,28 @@ private[mediator] object VerdictSender { sequencerSend: SequencerClientSend, crypto: SynchronizerCryptoClient, mediatorId: MediatorId, - batchingConfig: BatchingConfig, + parameters: VerdictSenderParameters, loggerFactory: NamedLoggerFactory, - )(implicit executionContext: ExecutionContext): VerdictSender = - new DefaultVerdictSender(sequencerSend, crypto, mediatorId, batchingConfig, loggerFactory) + )(implicit executionContext: ExecutionContext, closeContext: CloseContext): VerdictSender = + new DefaultVerdictSender(sequencerSend, crypto, mediatorId, parameters, loggerFactory) } +final case class VerdictSenderParameters( + enableDelay: Boolean, + livenessMargin: NonNegativeInt, + immediateBeforeDeadline: config.NonNegativeFiniteDuration, + initialDelay: config.NonNegativeFiniteDuration, + delay: config.NonNegativeFiniteDuration, + parallelism: PositiveInt, +) + private[mediator] class DefaultVerdictSender( sequencerSend: SequencerClientSend, crypto: SynchronizerCryptoClient, mediatorId: MediatorId, - batchingConfig: BatchingConfig, + parameters: VerdictSenderParameters, override protected val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) +)(implicit executionContext: ExecutionContext, closeContext: CloseContext) extends VerdictSender with NamedLogging { private val protocolVersion = sequencerSend.protocolVersion @@ -109,7 +119,7 @@ private[mediator] class DefaultVerdictSender( ) ) sendVerdict <- EitherT - .right(shouldSendVerdict(request.mediator, snapshot)) + .right(shouldSendVerdict(requestId, request.mediator, snapshot)) batch <- createResults(requestId, request, verdict) _ <- EitherT.right[SyncCryptoError]( sendResultBatch(requestId, batch, decisionTime, aggregationRule, sendVerdict) @@ -128,7 +138,7 @@ private[mediator] class DefaultVerdictSender( batch: Batch[DefaultOpenEnvelope], decisionTime: CantonTimestamp, aggregationRule: Option[AggregationRule], - sendVerdict: Boolean, + sendVerdictWithDelay: Option[Duration], )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { val callback: SendCallback = { case UnlessShutdown.Outcome(SendResult.Success(_)) => @@ -136,7 +146,9 @@ private[mediator] class DefaultVerdictSender( case UnlessShutdown.Outcome(SendResult.Error(error)) => val reason = error.reason reason match { - case SequencerErrors.AggregateSubmissionAlreadySent(_) => + // The V2 error may appear with PV36 + case SequencerErrors.AggregateSubmissionAlreadySent(_) | + SequencerErrors.AggregateSubmissionAlreadySentV2(_) => logger.info( s"Result message was already sent for $requestId: $reason" ) @@ -155,34 +167,57 @@ private[mediator] class DefaultVerdictSender( logger.debug("Sequencing result processing was aborted due to shutdown") } - val sendET = if (sendVerdict) { + val sendET = if (sendVerdictWithDelay.nonEmpty) { implicit val metricsContext: MetricsContext = MetricsContext("type" -> "send-verdict") - // the result of send request will be logged within the returned future however any error is effectively - // discarded. Any error logged by the eventual callback will most likely occur after the returned future has - // completed. - // we use decision-time for max-sequencing-time as recipients will simply ignore the message if received after - // that point. - EitherTUtil.leftSubflatMap( - sequencerSend - .send( - batch, - timestamps = SendRequestTimestamps( - topologyTimestamp = - if (protocolVersion <= ProtocolVersion.v34) Some(requestId.unwrap) else None, - // We use `clock.now` to stay consistent with how other submission requests are signed. - approximateTimestampForSigning = sequencerSend.clock.now, - maxSequencingTime = decisionTime, - ), - callback = callback, - aggregationRule = aggregationRule, - amplify = true, - ) + + def doSend() = + // the result of send request will be logged within the returned future however any error is effectively + // discarded. Any error logged by the eventual callback will most likely occur after the returned future has + // completed. + // we use decision-time for max-sequencing-time as recipients will simply ignore the message if received after + // that point. + EitherTUtil.leftSubflatMap( + sequencerSend + .send( + batch, + timestamps = SendRequestTimestamps( + topologyTimestamp = + if (protocolVersion <= ProtocolVersion.v34) Some(requestId.unwrap) else None, + // We use `clock.now` to stay consistent with how other submission requests are signed. + approximateTimestampForSigning = sequencerSend.clock.now, + maxSequencingTime = decisionTime, + ), + callback = callback, + aggregationRule = aggregationRule, + amplify = true, + ) + ) { + case RequestRefused(refused) if refused.hasMaxSequencingTimeElapsed => + logger.info("Sequencing result message timed out synchronously.") + Right(()) + case other => + Left(other) + } + + // Delay the submission of the verdict to increase the chance that we don't have to process all mediator verdicts + val delay = sendVerdictWithDelay.getOrElse(Duration.ZERO) + if ( + delay.isZero || !parameters.enableDelay || + // if the request is about to expire, respond immediately to avoid the transaction failing + // all this depends on the local clock being reasonably in sync with the sequencer's clock. + sequencerSend.clock.now + .plus(delay) + .isAfter(decisionTime.minus(parameters.immediateBeforeDeadline.asJava)) ) { - case RequestRefused(refused) if refused.hasMaxSequencingTimeElapsed => - logger.info("Sequencing result message timed out synchronously.") - Right(()) - case other => - Left(other) + doSend() + } else { + logger.debug(s"Delaying sending of verdict by ${delay.toMillis} ms") + EitherT + .right( + sequencerSend.clock + .scheduleAfterCancelledOnShutdown(_ => (), "delayed-send-verdict", delay) + ) + .flatMap(_ => doSend()) } } else { logger.info( @@ -190,7 +225,6 @@ private[mediator] class DefaultVerdictSender( ) EitherTUtil.unitUS } - EitherTUtil .logOnErrorU(sendET, s"Failed to send result to sequencer for request ${requestId.unwrap}") .value @@ -264,20 +298,33 @@ private[mediator] class DefaultVerdictSender( } private def shouldSendVerdict( + requestId: RequestId, mediatorGroup: MediatorGroupRecipient, topologySnapshot: TopologySnapshot, - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Boolean] = { + )(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Option[Duration]] = { val mediatorGroupIndex = mediatorGroup.group topologySnapshot.mediatorGroup(mediatorGroupIndex).map { groupO => - groupO + val group = groupO .getOrElse( // This has been checked in the `validateRequest` ErrorUtil.invalidState( s"Unexpected absent mediator group $mediatorGroupIndex." ) ) - .active - .contains(mediatorId) + val index = group.active.indexOf(mediatorId) + if (index > -1) { + val position = + ((index + requestId.unwrap.toMicros) % (group.active.size)) - group.threshold.value - parameters.livenessMargin.value + // if position is negative, then we want to send immediately as we are part of the first threshold + extra nodes + // otherwise, send with a delay proportional to our position in the group + if (position > -1) { + Some( + parameters.initialDelay.asJava.plusMillis(position * parameters.delay.asJava.toMillis) + ) + } else Some(Duration.ZERO) + } else None } } @@ -348,7 +395,7 @@ private[mediator] class DefaultVerdictSender( if (recipientsByViewTypeAndRootHash.nonEmpty) { for { snapshot <- crypto.awaitSnapshot(requestId.unwrap) - envs <- MonadUtil.parTraverseWithLimit(batchingConfig.parallelism)( + envs <- MonadUtil.parTraverseWithLimit(parameters.parallelism)( recipientsByViewTypeAndRootHash.toSeq ) { case ((viewType, rootHash), flatRecipients) => val rejection = ConfirmationResultMessage.create( @@ -385,7 +432,7 @@ private[mediator] class DefaultVerdictSender( ) } } - _ <- MonadUtil.parTraverseWithLimit_(batchingConfig.parallelism)(batches) { batch => + _ <- MonadUtil.parTraverseWithLimit_(parameters.parallelism)(batches) { batch => mediatorGroupO.traverse_ { // if no mediator could be detected from RHMs, participants will also detect this and there's not need to send a reject mediatorGroup => @@ -401,7 +448,7 @@ private[mediator] class DefaultVerdictSender( ) ) sendVerdict <- - shouldSendVerdict(mediatorGroup, snapshot.ipsSnapshot) + shouldSendVerdict(requestId, mediatorGroup, snapshot.ipsSnapshot) } yield { FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( sendResultBatch( diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/service/GrpcMediatorInspectionService.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/service/GrpcMediatorInspectionService.scala index c46dc5dbeb..d93aa36865 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/service/GrpcMediatorInspectionService.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/service/GrpcMediatorInspectionService.scala @@ -20,7 +20,7 @@ import com.digitalasset.canton.synchronizer.mediator.store.FinalizedResponseStor import com.digitalasset.canton.synchronizer.mediator.{FinalizedResponse, Mediator} import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.util.FutureUtil -import io.grpc.Status +import com.digitalasset.canton.util.GrpcStreamingUtils.withServerCallStreamObserver import io.grpc.stub.{ServerCallStreamObserver, StreamObserver} import org.apache.pekko.stream.Materializer import org.apache.pekko.stream.scaladsl.Source @@ -315,26 +315,6 @@ class GrpcMediatorInspectionService( ) } - /** Ensure observer is a ServerCallStreamObserver - * - * @param observer - * underlying observer - * @param handler - * handler requiring a ServerCallStreamObserver - */ - private def withServerCallStreamObserver[R]( - observer: StreamObserver[R] - )(handler: ServerCallStreamObserver[R] => Unit)(implicit traceContext: TraceContext): Unit = - observer match { - case serverCallStreamObserver: ServerCallStreamObserver[R] => - handler(serverCallStreamObserver) - case _ => - val statusException = - Status.INTERNAL.withDescription("Unknown stream observer request").asException() - logger.warn(statusException.getMessage) - observer.onError(statusException) - } - } object GrpcMediatorInspectionService { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/metrics/BftOrderingMetrics.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/metrics/BftOrderingMetrics.scala index 7eaa343f94..35131f6a09 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/metrics/BftOrderingMetrics.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/metrics/BftOrderingMetrics.scala @@ -12,9 +12,15 @@ import com.digitalasset.canton.environment.BaseMetrics import com.digitalasset.canton.logging.pretty.PrettyNameOnlyCase import com.digitalasset.canton.metrics.ActiveRequestsMetrics.GrpcServerMetricsX import com.digitalasset.canton.metrics.{ + CryptoMetrics, DbStorageHistograms, DbStorageMetrics, DeclarativeApiMetrics, + DecryptionHistograms, + DecryptionMetrics, + KmsMetrics, + SigningHistograms, + SigningMetrics, } import com.digitalasset.canton.synchronizer.metrics.BftOrderingMetrics.updateTimer import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.admin.SequencerBftAdminData.{ @@ -46,6 +52,9 @@ private[metrics] final class BftOrderingHistograms(val parent: MetricName)(impli private[metrics] val dbStorage = new DbStorageHistograms(parent) + private[metrics] val signingHistograms = new SigningHistograms(parent) + private[metrics] val decryptionHistograms = new DecryptionHistograms(parent) + // Private constructor to avoid being instantiated multiple times by accident private[metrics] final class PerformanceHistograms private[BftOrderingHistograms] { private[metrics] val prefix = BftOrderingHistograms.this.prefix :+ "performance" @@ -101,6 +110,13 @@ private[metrics] final class BftOrderingHistograms(val parent: MetricName)(impli "Records the rate and latency it takes to commit a block at the consensus level.", qualification = MetricQualification.Latency, ) + + private[metrics] val viewChangeProgressLatency: Item = Item( + prefix :+ "view-change-progress-latency", + summary = "View change progress latency", + description = "Records the rate and latency it takes to make progress on a view.", + qualification = MetricQualification.Latency, + ) } private[metrics] val consensus = new ConsensusHistograms @@ -202,15 +218,24 @@ class BftOrderingMetrics private[metrics] ( private implicit val metricsContext: MetricsContext = MetricsContext.Empty + override val prefix: MetricName = histograms.prefix + val dbStorage: DbStorageMetrics = new DbStorageMetrics(histograms.dbStorage, openTelemetryMetricsFactory) - override val prefix: MetricName = histograms.prefix + val crypto = new CryptoMetrics( + new SigningMetrics(histograms.signingHistograms, openTelemetryMetricsFactory), + new DecryptionMetrics(histograms.decryptionHistograms, openTelemetryMetricsFactory), + Some(new KmsMetrics(prefix, openTelemetryMetricsFactory)), + ) + override val declarativeApiMetrics: DeclarativeApiMetrics = new DeclarativeApiMetrics(prefix, openTelemetryMetricsFactory) override def storageMetrics: DbStorageMetrics = dbStorage + override def cryptoMetrics: CryptoMetrics = crypto + // Private constructor to avoid being instantiated multiple times by accident final class PerformanceMetrics private[BftOrderingMetrics] { @@ -275,12 +300,14 @@ class BftOrderingMetrics private[metrics] ( // Time spent by consensus messages in the postponed queue during state transfer val PostponedMessagesQueueLatency = "state-transfer-postponed-consensus-messages-queue-latency" + val TotalEpochTransferLatency = "state-transfer-total-epoch-transfer-latency" } } object output { val Fetch = "output-block-fetch-batches" val Inspection = "output-block-inspection" + val Backpressure = "output-backpressure" } } } @@ -410,7 +437,6 @@ class BftOrderingMetrics private[metrics] ( final class GlobalMetrics private[BftOrderingMetrics] { object labels { - val ReportingSequencer: String = "reporting-sequencer" val IsBlockEmpty: String = "is-block-empty" // true or false } @@ -675,6 +701,10 @@ class BftOrderingMetrics private[metrics] ( final class ConsensusMetrics private[BftOrderingMetrics] { private val prefix = histograms.consensus.prefix + object labels { + val Leader = "Leader" + } + val epoch: Gauge[Long] = openTelemetryMetricsFactory.gauge( MetricInfo( prefix :+ "epoch", @@ -752,6 +782,9 @@ class BftOrderingMetrics private[metrics] ( val commitLatency: Timer = openTelemetryMetricsFactory.timer(histograms.consensus.consensusCommitLatency.info) + val viewChangeProgressLatency: Timer = + openTelemetryMetricsFactory.timer(histograms.consensus.viewChangeProgressLatency.info) + // Private constructor to avoid being instantiated multiple times by accident final class RetransmissionsMetrics private[BftOrderingMetrics] { @@ -969,6 +1002,28 @@ class BftOrderingMetrics private[metrics] ( val blockDelay: Timer = openTelemetryMetricsFactory.timer(histograms.output.blockDelay.info) + + val sequencerCoreSubscriptionBufferSize: Gauge[Int] = openTelemetryMetricsFactory.gauge( + MetricInfo( + prefix :+ "sequencer-core-subscription-buffer-size", + summary = "Sequencer core subscription buffer size", + description = "Size of the buffer for the subscription to the sequencer core output, " + + "which is used to apply backpressure to the sequencer core when the output is not consumed fast enough.", + qualification = MetricQualification.Saturation, + ), + 0, + ) + + val currentSequencerCoreBackpressureDelayMillis: Gauge[Long] = + openTelemetryMetricsFactory.gauge( + MetricInfo( + prefix :+ "sequencer-core-backpressure-current-delay-millis", + summary = "Current sequencer core backpressure delay (ms)", + description = "Current sequencer core backpressure delay in milliseconds.", + qualification = MetricQualification.Latency, + ), + 0L, + ) } val output = new OutputMetrics @@ -1239,6 +1294,7 @@ class BftOrderingMetrics private[metrics] ( private val prefix = histograms.p2p.send.prefix object labels { + val SourceSequencer: String = "source-sequencer" val TargetSequencer: String = "target-sequencer" val DroppedAsUnauthenticated: String = "dropped-as-unauthenticated" diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/metrics/SynchronizerMetrics.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/metrics/SynchronizerMetrics.scala index 5d0debfeee..3982aa6fb2 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/metrics/SynchronizerMetrics.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/metrics/SynchronizerMetrics.scala @@ -22,12 +22,17 @@ import com.digitalasset.canton.logging.TracedLogger import com.digitalasset.canton.metrics.ActiveRequestsMetrics.GrpcServerMetricsX import com.digitalasset.canton.metrics.{ ActiveRequestsMetrics, + CryptoMetrics, DbStorageHistograms, DbStorageMetrics, DeclarativeApiMetrics, + DecryptionHistograms, + DecryptionMetrics, KmsMetrics, SequencerClientHistograms, SequencerClientMetrics, + SigningHistograms, + SigningMetrics, TrafficConsumptionMetrics, } import com.digitalasset.canton.sequencing.protocol.SubmissionRequestType @@ -46,6 +51,8 @@ class SequencerHistograms(val parent: MetricName)(implicit private[metrics] val prefix = parent :+ "sequencer" private[metrics] val sequencerClient = new SequencerClientHistograms(parent) private[metrics] val dbStorage = new DbStorageHistograms(parent) + private[metrics] val signing: SigningHistograms = new SigningHistograms(parent) + private[metrics] val decryption: DecryptionHistograms = new DecryptionHistograms(parent) private[metrics] val bftOrdering: BftOrderingHistograms = new BftOrderingHistograms(prefix) } @@ -83,8 +90,6 @@ class SequencerMetrics( openTelemetryMetricsFactory, ) - val kmsMetrics: KmsMetrics = new KmsMetrics(histograms.prefix, openTelemetryMetricsFactory) - val eventBuffer: CacheMetrics = new CacheMetrics("events-fan-out-buffer", openTelemetryMetricsFactory) @@ -99,6 +104,8 @@ class SequencerMetrics( override def storageMetrics: DbStorageMetrics = dbStorage + override def cryptoMetrics: CryptoMetrics = crypto + val block: BlockMetrics = new BlockMetrics(prefix, openTelemetryMetricsFactory) val sequencerClient: SequencerClientMetrics = @@ -239,6 +246,13 @@ class SequencerMetrics( val dbStorage: DbStorageMetrics = new DbStorageMetrics(histograms.dbStorage, openTelemetryMetricsFactory) + val crypto: CryptoMetrics = + new CryptoMetrics( + new SigningMetrics(histograms.signing, openTelemetryMetricsFactory), + new DecryptionMetrics(histograms.decryption, openTelemetryMetricsFactory), + Some(new KmsMetrics(prefix, openTelemetryMetricsFactory)), + ) + // Private constructor to avoid being instantiated multiple times by accident final class TrafficControlMetrics private[SequencerMetrics] { private val prefix: MetricName = SequencerMetrics.this.prefix :+ "traffic-control" @@ -414,6 +428,8 @@ class MediatorHistograms(val parent: MetricName)(implicit private[metrics] val prefix = parent :+ "mediator" private[metrics] val sequencerClient = new SequencerClientHistograms(parent) private[metrics] val dbStorage = new DbStorageHistograms(parent) + private[metrics] val signing: SigningHistograms = new SigningHistograms(parent) + private[metrics] val decryption: DecryptionHistograms = new DecryptionHistograms(parent) private[metrics] val responseLatencies: Item = Item( prefix :+ "response-latency", @@ -454,11 +470,18 @@ class MediatorMetrics( val dbStorage: DbStorageMetrics = new DbStorageMetrics(histograms.dbStorage, openTelemetryMetricsFactory) + override def cryptoMetrics: CryptoMetrics = crypto + + val crypto: CryptoMetrics = + new CryptoMetrics( + new SigningMetrics(histograms.signing, openTelemetryMetricsFactory), + new DecryptionMetrics(histograms.decryption, openTelemetryMetricsFactory), + Some(new KmsMetrics(prefix, openTelemetryMetricsFactory)), + ) + val sequencerClient: SequencerClientMetrics = new SequencerClientMetrics(histograms.sequencerClient, openTelemetryMetricsFactory) - val kmsMetrics: KmsMetrics = new KmsMetrics(histograms.prefix, openTelemetryMetricsFactory) - val outstanding: Gauge[Int] = openTelemetryMetricsFactory.gauge( MetricInfo( diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/BaseSequencer.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/BaseSequencer.scala index b1916d8aab..e9a3965012 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/BaseSequencer.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/BaseSequencer.scala @@ -24,6 +24,7 @@ import com.digitalasset.canton.synchronizer.sequencer.errors.{ SequencerAdministrationError, } import com.digitalasset.canton.synchronizer.sequencer.store.PayloadId +import com.digitalasset.canton.synchronizer.sequencer.time.LsuSequencingBounds import com.digitalasset.canton.time.{Clock, PeriodicAction} import com.digitalasset.canton.topology.Member import com.digitalasset.canton.tracing.{Spanning, TraceContext} @@ -47,6 +48,7 @@ abstract class BaseSequencer( clock: Clock, signatureVerifier: SignatureVerifier, protocolVersion: ProtocolVersion, + lsuSequencingBounds: Option[LsuSequencingBounds], protected val disableSubmissionChecksForTesting: Boolean, )(implicit executionContext: ExecutionContext, trace: Tracer) extends Sequencer @@ -105,18 +107,33 @@ abstract class BaseSequencer( override def acknowledgeSigned(signedAcknowledgeRequest: SignedContent[AcknowledgeRequest])( implicit traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, String, Unit] = for { - estimatedSequencingTime <- - if (protocolVersion >= ProtocolVersion.v35) EitherT.rightT[FutureUnlessShutdown, String](None) - else EitherT.right(sequencingTime).map(ts => Some(ts.getOrElse(clock.now))) - _ <- signatureVerifier.verifyAcknowledgeRequestSignature( - signedAcknowledgeRequest, - HashPurpose.AcknowledgementSignature, - estimatedSequencingTime, - protocolVersion, - ) - _ <- EitherT.right(acknowledgeSignedInternal(signedAcknowledgeRequest)) - } yield () + ): EitherT[FutureUnlessShutdown, String, Unit] = { + val ts = signedAcknowledgeRequest.content.timestamp + val shouldDropAckBeforeUpgradeTime = + lsuSequencingBounds.fold(false)(_.upgradeTime >= ts) + + if (shouldDropAckBeforeUpgradeTime) { + logger.debug( + s"Dropping acknowledgement from ${signedAcknowledgeRequest.content.member} because it is before upgrade time" + ) + + EitherTUtil.unitUS + } else { + for { + estimatedSequencingTime <- + if (protocolVersion >= ProtocolVersion.v35) + EitherT.rightT[FutureUnlessShutdown, String](None) + else EitherT.right(sequencingTime).map(ts => Some(ts.getOrElse(clock.now))) + _ <- signatureVerifier.verifyAcknowledgeRequestSignature( + signedAcknowledgeRequest, + HashPurpose.AcknowledgementSignature, + estimatedSequencingTime, + protocolVersion, + ) + _ <- EitherT.right(acknowledgeSignedInternal(signedAcknowledgeRequest)) + } yield () + } + } protected def acknowledgeSignedInternal( signedAcknowledgeRequest: SignedContent[AcknowledgeRequest] diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/DatabaseSequencer.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/DatabaseSequencer.scala index 619c82d92c..4bec6d572d 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/DatabaseSequencer.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/DatabaseSequencer.scala @@ -166,6 +166,7 @@ class DatabaseSequencer( clock, SignatureVerifier(cryptoApi), cryptoApi.psid.protocolVersion, + lsuSequencingBounds, disableSubmissionChecksForTesting, ) with FlagCloseable { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/DirectSequencerConnectionPool.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/DirectSequencerConnectionPool.scala index 7006d0a77e..8cbd25285e 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/DirectSequencerConnectionPool.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/DirectSequencerConnectionPool.scala @@ -5,6 +5,7 @@ package com.digitalasset.canton.synchronizer.sequencer import cats.data.EitherT import cats.syntax.either.* +import com.daml.metrics.api.MetricsContext import com.daml.nameof.NameOf.functionFullName import com.daml.nonempty.NonEmpty import com.digitalasset.canton.SequencerAlias @@ -125,6 +126,8 @@ class DirectSequencerConnectionPool( )(implicit traceContext: TraceContext ): Either[SequencerConnectionPoolError.ThresholdUnreachableError, Unit] = Either.unit + + override val metricsContext: MetricsContext = MetricsContext.Empty } object DirectSequencerConnectionPool { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/LocalSequencerStateEventSignaller.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/LocalSequencerStateEventSignaller.scala index c57a2bc27f..d1ae3dcde6 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/LocalSequencerStateEventSignaller.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/LocalSequencerStateEventSignaller.scala @@ -18,6 +18,10 @@ import com.digitalasset.canton.tracing.{TraceContext, Traced} import com.digitalasset.canton.util.TryUtil.* import org.apache.pekko.NotUsed import org.apache.pekko.stream.* +import org.apache.pekko.stream.SubscriptionWithCancelException.{ + NoMoreElementsNeeded, + StageWasCompleted, +} import org.apache.pekko.stream.scaladsl.Source import java.util.concurrent.ConcurrentHashMap @@ -77,10 +81,17 @@ class LocalSequencerStateEventSignaller( // the queue was closed, so let's remove the entry queues.remove(tracedQueue).discard case QueueOfferResult.Failure(ex) => - logger.info( - s"Unable to queue signal for member $member, because the queue failed with an error.", - ex, - )(tracedQueue.traceContext) + ex match { + case NoMoreElementsNeeded | StageWasCompleted => + logger.info( + s"Not queuing event notification for member $member, because subscription was closed." + )(tracedQueue.traceContext) + case _ => + logger.info( + s"Unable to queue signal for member $member, because the queue failed with an error.", + ex, + )(tracedQueue.traceContext) + } queues.remove(tracedQueue).discard } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/Sequencer.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/Sequencer.scala index d7e75cc2fd..82fb839023 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/Sequencer.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/Sequencer.scala @@ -7,7 +7,11 @@ import cats.data.EitherT import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt} import com.digitalasset.canton.data.{CantonTimestamp, SynchronizerSuccessor} import com.digitalasset.canton.error.CantonBaseError -import com.digitalasset.canton.health.{AtomicHealthElement, CloseableHealthQuasiComponent} +import com.digitalasset.canton.health.{ + AtomicHealthElement, + CloseableHealthQuasiComponent, + HealthComponent, +} import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, HasCloseContext} import com.digitalasset.canton.logging.{HasLoggerName, NamedLogging} import com.digitalasset.canton.resource.Storage @@ -48,6 +52,8 @@ import org.apache.pekko.Done import org.apache.pekko.stream.KillSwitch import org.apache.pekko.stream.scaladsl.Source +import scala.concurrent.Future + /** Errors from pruning */ sealed trait PruningError { def message: String @@ -98,6 +104,10 @@ trait Sequencer @VisibleForTesting private[canton] def orderer: Option[BlockOrderer] + /** Health of the sequencer's background writer, if any. + */ + private[sequencer] def backgroundWriterHealth: Option[HealthComponent] = None + /** True if member is registered in sequencer persistent state / storage (i.e. database). */ def isRegistered(member: Member)(implicit @@ -265,6 +275,18 @@ trait Sequencer def performLsuSequencingTest(mediatorGroupRecipient: MediatorGroupRecipient)(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, CantonBaseError, Unit] + + /** Apply a lock to prevent post processing of events + * + * The sequencer should run checks on the write side (malicious or racy participant) or on the + * post processing side (malicious or racy sequencer). + * + * In order to test the behaviour, we need to be able to control the timing of when transactions + * are really applied during post-processing. + */ + @VisibleForTesting + def applyPostProcessingLockForTesting(continueAfter: Future[Unit]): Unit = ??? + } /** Sequencer pruning interface. @@ -339,6 +361,7 @@ trait SequencerPruning { def pruningStatus(implicit traceContext: TraceContext ): FutureUnlessShutdown[SequencerPruningStatus] + } object Sequencer extends HasLoggerName { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/SequencerNode.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/SequencerNode.scala index 21f8131e12..14d550c82e 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/SequencerNode.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/SequencerNode.scala @@ -240,6 +240,7 @@ class SequencerNodeBootstrap( }) addCloseable(sequencerPublicApiHealthService) addCloseable(sequencerHealth) + addCloseable(asyncWriterHealth) private def createSequencerFactory( protocolVersion: ProtocolVersion @@ -657,7 +658,7 @@ class SequencerNodeBootstrap( staticSynchronizerParameters, crypto, cryptoConfig, - Some(arguments.metrics.kmsMetrics), + arguments.metrics.cryptoMetrics, parameters.cachingConfigs.publicKeyConversionCache, parameters.processingTimeouts, futureSupervisor, @@ -973,6 +974,13 @@ class SequencerNodeBootstrap( SequencerHealthStatus.shutdownStatus, ) + // Deferred health component for the block sequencer's background writer, created during + // initialization. It is used as a fatal dependency of the liveness health service so that the + // node transitions to NOT_SERVING and is restarted if the background writer can no longer make + // progress. Non-block sequencers never set a delegate, so it stays non-fatal. + private lazy val asyncWriterHealth = + MutableHealthComponent(loggerFactory, "block-sequencer-async-writer", timeouts) + // The service exposed by the gRPC health endpoint of sequencer public API // This will be used by sequencer clients who perform client-side load balancing to determine sequencer health private lazy val sequencerPublicApiHealthService = DependenciesHealthService( @@ -993,7 +1001,13 @@ class SequencerNodeBootstrap( ) // We use the storage as a fatal dependency so that we transition liveness to NOT_SERVING if // the storage fails continuously for longer than `failedToFatalDelay`. - val liveness = LivenessHealthService(logger, timeouts, fatalDependencies = Seq(storage)) + // The background writer health is fatal as well: once a background write fails, the writer can + // no longer make progress, so the node must be restarted. + val liveness = LivenessHealthService( + logger, + timeouts, + fatalDependencies = Seq(storage, asyncWriterHealth), + ) (readiness, liveness) } @@ -1042,6 +1056,8 @@ class SequencerNodeBootstrap( .initialize(runtime) // wait for the server to be initialized before reporting a serving health state _ = sequencerHealth.set(runtime.sequencer) + // bind the background writer health (block sequencers only) into the liveness fatal dependency + _ = runtime.sequencer.backgroundWriterHealth.foreach(asyncWriterHealth.set) } yield sequencerNodeServer } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencer.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencer.scala index 3a9619c429..7965e568f5 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencer.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencer.scala @@ -22,7 +22,8 @@ import com.digitalasset.canton.crypto.{ SynchronizerSnapshotSyncCryptoApi, } import com.digitalasset.canton.data.{CantonTimestamp, SynchronizerSuccessor} -import com.digitalasset.canton.error.CantonBaseError +import com.digitalasset.canton.error.{CantonBaseError, FatalError} +import com.digitalasset.canton.health.HealthComponent import com.digitalasset.canton.lifecycle.* import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.grpc.ClientChannelParams @@ -41,6 +42,8 @@ import com.digitalasset.canton.sequencing.client.{ } import com.digitalasset.canton.sequencing.protocol.* import com.digitalasset.canton.sequencing.protocol.SequencerErrors.{ + AggregateSubmissionAlreadySentV2, + AggregateSubmissionInvalidRule, Overloaded, SubmissionRequestRefused, } @@ -58,7 +61,10 @@ import com.digitalasset.canton.sequencing.traffic.{ import com.digitalasset.canton.sequencing.{GroupAddressResolver, GrpcSequencerConnection} import com.digitalasset.canton.serialization.HasCryptographicEvidence import com.digitalasset.canton.synchronizer.block.data.SequencerBlockStore -import com.digitalasset.canton.synchronizer.block.update.BlockUpdateGeneratorImpl +import com.digitalasset.canton.synchronizer.block.update.{ + BlockProcessingParameters, + BlockUpdateGeneratorImpl, +} import com.digitalasset.canton.synchronizer.block.{BlockSequencerStateManagerBase, RawLedgerBlock} import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.* @@ -77,7 +83,6 @@ import com.digitalasset.canton.synchronizer.sequencer.errors.SequencerError.{ SequencerPastUpgradeTime, } import com.digitalasset.canton.synchronizer.sequencer.store.{PayloadId, SequencerStore} -import com.digitalasset.canton.synchronizer.sequencer.time.LsuSequencingBounds import com.digitalasset.canton.synchronizer.sequencer.traffic.TimestampSelector.* import com.digitalasset.canton.synchronizer.sequencer.traffic.{ LsuTrafficState, @@ -105,6 +110,7 @@ import com.digitalasset.canton.util.{ PekkoUtil, SimpleExecutionQueue, } +import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.{RichGeneratedMessage, SequencerAlias} import io.grpc.ServerServiceDefinition import io.opentelemetry.api.trace.Tracer @@ -122,8 +128,6 @@ import scala.concurrent.duration.* import scala.concurrent.{ExecutionContextExecutor, Future, Promise} import scala.util.{Failure, Success} -import BlockSequencerFactory.OrderingTimeFixMode - class BlockSequencer( blockOrderer: BlockOrderer, name: String, @@ -140,12 +144,11 @@ class BlockSequencer( health: Option[SequencerHealthConfig], clock: Clock, blockRateLimitManager: SequencerRateLimitManager, - orderingTimeFixMode: OrderingTimeFixMode, - lsuSequencingBounds: Option[LsuSequencingBounds], + blockProcessingParameters: BlockProcessingParameters, + parameters: SequencerNodeParameters, metrics: SequencerMetrics, loggerFactory: NamedLoggerFactory, runtimeReady: FutureUnlessShutdown[Unit], - parameters: SequencerNodeParameters, )(implicit executionContext: ExecutionContextExecutor, materializer: Materializer, @@ -172,7 +175,7 @@ class BlockSequencer( metrics, loggerFactory, blockSequencerMode = true, - lsuSequencingBounds, + blockProcessingParameters.lsuSequencingBounds, parameters.drSequencingTimeUpperBound, rateLimitManagerO = Some(blockRateLimitManager), disableSubmissionChecksForTesting = parameters.disableSubmissionChecksForTesting, @@ -183,6 +186,8 @@ class BlockSequencer( private val protocolVersion = cryptoApi.protocolVersion + private def lsuSequencingBounds = blockProcessingParameters.lsuSequencingBounds + private[sequencer] val pruningQueue = new SimpleExecutionQueue( "block-sequencer-pruning-queue", futureSupervisor, @@ -251,17 +256,23 @@ class BlockSequencer( private val trafficPurchasedSubmissionHandler = new TrafficPurchasedSubmissionHandler(clock, loggerFactory) + private val postProcessingLock = new AtomicReference[Future[Unit]](Future.unit) + override def applyPostProcessingLockForTesting( + continueAfter: scala.concurrent.Future[Unit] + ): Unit = + postProcessingLock.set(continueAfter) + override protected def resetWatermarkTo: SequencerWriter.ResetWatermark = lsuSequencingBounds match { case Some(lsuSequencingBounds) => SequencerWriter.ResetWatermarkToTimestamp( - stateManager.getHeadState.block.lastTs + stateManager.getPersistenceHeadState.block.lastTs .max(lsuSequencingBounds.lowerBoundSequencingTimeExclusive) ) case None => SequencerWriter.ResetWatermarkToTimestamp( - stateManager.getHeadState.block.lastTs + stateManager.getPersistenceHeadState.block.lastTs ) } @@ -289,21 +300,19 @@ class BlockSequencer( private val latestLsuSequencerConnectionSuccessorSerial: AtomicInteger = new AtomicInteger(0) private val (killSwitchF, done) = { - val headState = stateManager.getHeadState + val headState = stateManager.getPersistenceHeadState noTracingLogger.info(s"Subscribing to block source from ${headState.block.height + 1}") val updateGenerator = new BlockUpdateGeneratorImpl( cryptoApi, sequencerId, blockRateLimitManager, - orderingTimeFixMode, - lsuSequencingBounds = lsuSequencingBounds, drSequencingTimeUpperBound = parameters.drSequencingTimeUpperBound, getAnnouncedLsu = announcedLsu.get(), - producePostOrderingTopologyTicks, - metrics, - parameters.batchingConfig, + producePostOrderingTopologyTicks = producePostOrderingTopologyTicks, consistencyChecks = parameters.enableAdditionalConsistencyChecks, + parameters = blockProcessingParameters, + metrics = metrics, memberValidator = memberValidator, loggerFactory, )(CloseContext(cryptoApi), tracer) @@ -321,38 +330,53 @@ class BlockSequencer( FutureUnlessShutdown.pure(block) } } - val driverSource = Source - .futureSource(runtimeReady.unwrap.map { - case UnlessShutdown.AbortedDueToShutdown => - noTracingLogger.debug("Not initiating subscription to block source due to shutdown") - Source.empty.viaMat(KillSwitches.single)(Keep.right) - case UnlessShutdown.Outcome(_) => - noTracingLogger.debug("Subscribing to block source") - blockOrderer.subscribe() - }) - .via(pauseAtUpgradeTimeUntilLsuTrafficInitialized) - // Explicit async to make sure that the block processing runs in parallel with the block retrieval - .async - .map(updateGenerator.extractBlockEvents) - .async - .via(stateManager.processBlock(updateGenerator)) - .wireTap { update => - throughputCap.addBlockUpdate(update.value) - } - .async - .via(stateManager.applyBlockUpdate(this)) - .wireTap { lastTs => - circuitBreaker.registerLastBlockTimestamp(lastTs) - } + + val driverSource = { + val step1 = Source + .futureSource(runtimeReady.unwrap.map { + case UnlessShutdown.AbortedDueToShutdown => + noTracingLogger.debug("Not initiating subscription to block source due to shutdown") + Source.empty.viaMat(KillSwitches.single)(Keep.right) + case UnlessShutdown.Outcome(_) => + noTracingLogger.debug("Subscribing to block source") + blockOrderer.subscribe() + }) + .via(pauseAtUpgradeTimeUntilLsuTrafficInitialized) + // Explicit async to make sure that the block processing runs in parallel with the block retrieval + .async + + val step2 = + if (parameters.enableTestingFeatures) + // If necessary apply post-processing lock for testing + step1.mapAsync(parallelism = 1)(x => postProcessingLock.get().map(_ => x)) + else step1 + + step2 + .map(updateGenerator.extractBlockEvents) + .wireTap { update => + throughputCap.addBlockUpdate(update.value) + } + .async + .via(stateManager.processBlock(updateGenerator)) + .async + .via(stateManager.applyBlockUpdate(this)) + .wireTap { lastTs => + circuitBreaker.registerLastBlockTimestamp(lastTs) + } + } PekkoUtil.runSupervised( driverSource.toMat(Sink.ignore)(Keep.both), errorLogMessagePrefix = "Fatally failed to handle state changes", ) } - done onComplete { + done.onComplete { case Success(_) => noTracingLogger.debug("Sequencer flow has shutdown") - case Failure(ex) => noTracingLogger.error("Sequencer flow has failed", ex) + case Failure(ex) => + if (parameters.exitOnFatalFailures) + FatalError.exitOnFatalError("Sequencer flow has failed", ex, logger)(TraceContext.empty) + else + noTracingLogger.error("Sequencer flow has failed", ex) } private def validateMaxSequencingTime( @@ -426,13 +450,13 @@ class BlockSequencer( // Use the timestamp of the latest chunk here, such that top ups that happened in an earlier chunk of the // current block can be reflected in the traffic state used to validate the request { - val headChunkLastTs = stateManager.getHeadState.chunk.lastTs + val headChunkLastTs = stateManager.getPersistenceHeadState.chunk.lastTs lsuSequencingBounds .map(_.upgradeTime) .getOrElse(headChunkLastTs) .max(headChunkLastTs) }, - stateManager.getHeadState.chunk.latestSequencerEventTimestamp + stateManager.getPersistenceHeadState.chunk.latestSequencerEventTimestamp .orElse(lsuSequencingBounds.map(_.upgradeTime)), ) .leftMap { @@ -499,6 +523,42 @@ class BlockSequencer( } } + private def validateAggregationAlreadyDelivered( + submission: SubmissionRequest + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, SequencerDeliverError, Unit] = + submission + .aggregationId(cryptoApi.pureCrypto) + .leftMap { err => + AggregateSubmissionInvalidRule.apply( + s"Failed to compute aggregation ID for submission with id ${submission.messageId}: $err" + ) + } + .flatMap { + case Some((aggregationId, _)) => + Either.cond( + // We can check if the aggregation has already been delivered by looking at the head + // state. If the delivered at is not yet cached (after a crash) we just allow the submission + // to go through for simplicity. + protocolVersion < ProtocolVersion.v35 || // never reject for pv34 + (protocolVersion == ProtocolVersion.v35 && // if pv35, only check if sender code is the set of codes + !parameters.enableRejectDeliveredAggregationsOnPv35 + .contains(submission.sender.code.threeLetterId.str)) || + !stateManager.getProcessingHeadState.inFlightAggregations.byId + .get(aggregationId) + .exists(_.cachedDeliveredAt.exists(_.nonEmpty)), + (), { + val str = + s"Not accepting submission as aggregation ID $aggregationId is already marked as delivered" + logger.debug(str) + AggregateSubmissionAlreadySentV2.apply(str) + }, + ) + case None => Right(()) + } + .toEitherT[FutureUnlessShutdown] + private def rejectSubmissionsIfOverloaded( submission: SubmissionRequest ): EitherT[FutureUnlessShutdown, SequencerDeliverError, Unit] = @@ -544,7 +604,8 @@ class BlockSequencer( val validateET: EitherT[FutureUnlessShutdown, CantonBaseError, Unit] = if (disableSubmissionChecksForTesting) { EitherTUtil.unitUS - } else + } else { + for { _ <- EitherTUtil.condUnitET[FutureUnlessShutdown]( !parameters.delayRequestsBeforeLsuTrafficInit || skipLsuChecks || lsuTrafficInitialized.isCompleted, @@ -560,6 +621,7 @@ class BlockSequencer( _ <- enforceThroughputCap(submission) _ <- rejectSubmissionsIfOverloaded(submission) _ <- validateMaxSequencingTime(submission) + _ <- validateAggregationAlreadyDelivered(submission) // TODO(#19476): Why we don't check group recipients here? approximateSnapshot <- EitherT.liftF( cryptoApi.currentSnapshotApproximation @@ -585,6 +647,7 @@ class BlockSequencer( ) _ <- enforceRateLimiting(signedSubmission).leftWiden[CantonBaseError] } yield () + } validateET.flatMap { _ => EitherT( @@ -625,17 +688,6 @@ class BlockSequencer( rejectAcknowledgementIfOverloaded().leftMap(_.asGrpcError) ) - _ = signedAcknowledgeRequest.content.member match { - case _: ParticipantId => - // Participants should not ack before upgrade time because the events are on the old synchronizer. - EitherTUtil.toFutureUnlessShutdown( - rejectSubmissionsBeforeOrAtSequencingTimeLowerBound().leftMap(_.asGrpcError) - ) - - case _: SequencerId | _: MediatorId => - // Synchronizer nodes can receive messages on the new synchronizer before upgrade time so they can ack - FutureUnlessShutdown.unit - } waitForAcknowledgementF = stateManager.waitForAcknowledgementToComplete( req.member, req.timestamp, @@ -658,7 +710,7 @@ class BlockSequencer( s"$functionFullName($timestamp)", ) .unlessShutdown( - FutureUnlessShutdown.pure(timestamp <= stateManager.getHeadState.block.lastTs), + FutureUnlessShutdown.pure(timestamp <= stateManager.getPersistenceHeadState.block.lastTs), DbExceptionRetryPolicy, ) ) @@ -689,7 +741,7 @@ class BlockSequencer( ): EitherT[FutureUnlessShutdown, TrafficControlError, Seq[TrafficSummary]] = { val timestampsSet = SortedSet.from(timestamps) - val headBlock = stateManager.getHeadState.block + val headBlock = stateManager.getPersistenceHeadState.block val latestSequencedTimestamp = headBlock.lastTs val latestSequencerEventTimestamp = headBlock.latestSequencerEventTimestamp.orElse( lsuSequencingBounds.map(_.upgradeTime) @@ -1047,7 +1099,7 @@ class BlockSequencer( // all the changes of that last block. case LatestSafe => Some( - stateManager.getHeadState.block.lastTs.immediateSuccessor + stateManager.getPersistenceHeadState.block.lastTs.immediateSuccessor /* Just after LSU (before any block is sequenced), we allow querying traffic state at upgrade time. This is safe to do because we know we are past upgrade time and traffic is initialized. @@ -1055,7 +1107,7 @@ class BlockSequencer( .max(lsuSequencingBounds.fold(CantonTimestamp.MinValue)(_.upgradeTime)) ) case LatestApproximate => - Some(clock.now.max(stateManager.getHeadState.block.lastTs.immediateSuccessor)) + Some(clock.now.max(stateManager.getPersistenceHeadState.block.lastTs.immediateSuccessor)) } logger.info(s"Using $timestampO to fetch traffic state with selector $selector") @@ -1063,7 +1115,7 @@ class BlockSequencer( blockRateLimitManager.getStates( requestedMembers, timestampO, - stateManager.getHeadState.block.latestSequencerEventTimestamp.orElse( + stateManager.getPersistenceHeadState.block.latestSequencerEventTimestamp.orElse( lsuSequencingBounds.map(_.upgradeTime) ), // TODO(#18401) set warnIfApproximate to true and check that we don't get warnings @@ -1144,7 +1196,7 @@ class BlockSequencer( ), ) latestSequencerEventTimestamp = - stateManager.getHeadState.block.latestSequencerEventTimestamp.orElse( + stateManager.getPersistenceHeadState.block.latestSequencerEventTimestamp.orElse( lsuSequencingBounds.map(_.upgradeTime) ) result <- blockRateLimitManager.getTrafficStateForMemberAt( @@ -1163,6 +1215,10 @@ class BlockSequencer( override private[canton] def orderer: Some[BlockOrderer] = Some(blockOrderer) + override private[sequencer] def backgroundWriterHealth: Option[HealthComponent] = Some( + stateManager.asyncWriterHealth + ) + override private[sequencer] def updateLsuSuccessor( successor: SynchronizerSuccessor, announcementEffectiveTime: EffectiveTime, @@ -1237,11 +1293,23 @@ class BlockSequencer( ().asRight[String] } - case Right(_value) => - logger.info( - s"Successfully contacted sequencer successor on ${successorPsid.suffix}." - ) - metrics.setLsuContactSuccessorStatus(1, successorPsid) + case Right(bootstrapInfo) => + if (bootstrapInfo.psid != successorPsid) { + logger.warn( + s"Error when contacting successor: expecting psid to be $successorPsid but found ${bootstrapInfo.psid}" + ) + } else if (bootstrapInfo.sequencerId != successor.mapping.sequencerId) { + logger.warn( + s"Error when contacting successor: expecting sequencer id to be ${successor.mapping.sequencerId} but found ${bootstrapInfo.sequencerId}" + ) + } else { + logger.info( + s"Successfully contacted sequencer successor on ${successorPsid.suffix}." + ) + metrics.setLsuContactSuccessorStatus(1, successorPsid) + } + + // Always return a right to stop retries (on successor or fatal errors) ().asRight[String] } @@ -1340,8 +1408,9 @@ class BlockSequencer( // it has become consistent (accounting reached the upgrade time) store.readHeadBlockInfo().map(_.map(_.lastTs)) ) - latestSequencerEventTimestamp = stateManager.getHeadState.block.latestSequencerEventTimestamp - .orElse(lsuSequencingBounds.map(_.upgradeTime)) + latestSequencerEventTimestamp = + stateManager.getPersistenceHeadState.block.latestSequencerEventTimestamp + .orElse(lsuSequencingBounds.map(_.upgradeTime)) _ <- EitherTUtil.condUnitET[FutureUnlessShutdown]( latestPersistedBlockTimeO.exists(_ >= ts), SequencerError.NotAtUpgradeTimeOrBeyond.Error( @@ -1451,10 +1520,10 @@ class BlockSequencer( ) // - Check that the node has not progressed beyond the upgrade time _ <- EitherTUtil.condUnitET[FutureUnlessShutdown]( - stateManager.getHeadState.block.lastTs <= upgradeTime, + stateManager.getPersistenceHeadState.block.lastTs <= upgradeTime, SequencerPastUpgradeTime.Error( cryptoApi.psid, - stateManager.getHeadState.block.lastTs, + stateManager.getPersistenceHeadState.block.lastTs, upgradeTime, ), ) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerFactory.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerFactory.scala index 257809584c..9232d55328 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerFactory.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerFactory.scala @@ -7,6 +7,7 @@ import cats.data.EitherT import cats.syntax.parallel.* import cats.syntax.traverse.* import com.digitalasset.canton.concurrent.FutureSupervisor +import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.crypto.SynchronizerCryptoClient import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, LifeCycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} @@ -126,9 +127,10 @@ abstract class BlockSequencerFactory( health: Option[SequencerHealthConfig], clock: Clock, rateLimitManager: SequencerRateLimitManager, - orderingTimeFixMode: OrderingTimeFixMode, - synchronizerLoggerFactory: NamedLoggerFactory, lsuSequencingBounds: Option[LsuSequencingBounds], + parallelism: PositiveInt, + enablePrevalidation: Boolean, + synchronizerLoggerFactory: NamedLoggerFactory, runtimeReady: FutureUnlessShutdown[Unit], )(implicit executionContext: ExecutionContextExecutor, @@ -176,6 +178,7 @@ abstract class BlockSequencerFactory( synchronizerSyncCryptoApi: SynchronizerCryptoClient, protocolVersion: ProtocolVersion, trafficConfig: SequencerTrafficConfig, + lsuSequencingBounds: Option[LsuSequencingBounds], ): SequencerRateLimitManager = new SequencerRateLimitManagerImpl( trafficPurchasedManager, @@ -188,6 +191,7 @@ abstract class BlockSequencerFactory( protocolVersion, trafficConfig, eventCostCalculator = new EventCostCalculator(loggerFactory), + lsuSequencingBounds = lsuSequencingBounds, ) @nowarn("cat=deprecation") @@ -250,23 +254,27 @@ abstract class BlockSequencerFactory( synchronizerSyncCryptoApi, protocolVersion, trafficConfig, + lsuSequencingBounds, ) _ <- balanceManager.initialize } yield { val synchronizerLoggerFactory = loggerFactory.append("psid", synchronizerSyncCryptoApi.psid.toString) + val stateManager = BlockSequencerStateManager.create( initialHeadO, store, trafficConsumedStore, nodeParameters.asyncWriter, + blockSequencerConfig.streamInstrumentation, nodeParameters.enableAdditionalConsistencyChecks, + nodeParameters.enablePrevalidation, + nodeParameters.batchingConfig.parallelism, + metrics.block, nodeParameters.processingTimeouts, futureSupervisor, synchronizerLoggerFactory, - blockSequencerConfig.streamInstrumentation, - metrics.block, ) val blockOrderer = createBlockOrderer( @@ -289,9 +297,10 @@ abstract class BlockSequencerFactory( health, clock, rateLimitManager, - orderingTimeFixMode, + lsuSequencingBounds = lsuSequencingBounds, + parallelism = nodeParameters.batchingConfig.parallelism, + enablePrevalidation = nodeParameters.enablePrevalidation, synchronizerLoggerFactory, - lsuSequencingBounds, runtimeReady, ) testingInterceptor diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerThroughputCap.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerThroughputCap.scala index e7765ca887..d45fb8e6bb 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerThroughputCap.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerThroughputCap.scala @@ -13,13 +13,12 @@ import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.{FlagCloseable, LifeCycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.sequencing.protocol.SubmissionRequestType -import com.digitalasset.canton.synchronizer.block.update.{ChunkUpdate, OrderedBlockUpdate} +import com.digitalasset.canton.synchronizer.block.{BlockEvents, LedgerBlockEvent} import com.digitalasset.canton.synchronizer.metrics.{SequencerMetrics, ThroughputCapMetrics} import com.digitalasset.canton.synchronizer.sequencer.BlockSequencerConfig.{ IndividualThroughputCapConfig, ThroughputCapConfig, } -import com.digitalasset.canton.synchronizer.sequencer.SubmissionOutcome import com.digitalasset.canton.synchronizer.sequencer.block.BlockSequencerThroughputCap.{ IndividualBlockSequencerThroughputCap, SubmissionRequestEntry, @@ -138,25 +137,27 @@ class BlockSequencerThroughputCap( .getOrElse(Right(())) def addBlockUpdate( - update: OrderedBlockUpdate + update: BlockEvents ): Unit = if (enabled.get()) { - val submissions = update match { - case chunkUpdate: ChunkUpdate => - chunkUpdate.submissionsOutcomes - .collect { case deliver: SubmissionOutcome.Deliver => - deliver - } - .map { deliver => - SubmissionRequestEntry( - deliver.submission.sender, - deliver.submission.requestType, - deliver.sequencingTime, - deliver.submission.toByteString.size().toLong, - ) - } - case _ => Seq.empty + val submissions = update.events.map(_.value).collect { + // Collect all ordered events and count it towards the cap. We need to run this + // before filtering out events (e.g. max sequencing time exceeded or aggregation + // already completed) to ensure that the cap is enforced based on the load on the orderer + // and not based on the successfully procesed events. + // TODO (#19052): A malicious sequencer could send unauthenticated events with the given + // unauthenticated sender to trigger the caps. Therefore, a submission should only + // be attributed to a sender once the sender is authenticated and we ensured that + // this is not a replay by a malicious sequencer. This requires substantial changes + // to the processing pipeline. + case LedgerBlockEvent + .Send(timestamp, signedSubmissionRequest, _, originalPayloadSize) => + SubmissionRequestEntry( + signedSubmissionRequest.content.sender, + signedSubmissionRequest.content.requestType, + timestamp, + originalPayloadSize.toLong, + ) } - addBlockUpdateInternal(submissions) } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/DriverBlockSequencerFactory.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/DriverBlockSequencerFactory.scala index be19bd9406..69d96d961d 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/DriverBlockSequencerFactory.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/DriverBlockSequencerFactory.scala @@ -4,11 +4,13 @@ package com.digitalasset.canton.synchronizer.sequencer.block import com.digitalasset.canton.concurrent.FutureSupervisor +import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.crypto.SynchronizerCryptoClient import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.resource.Storage import com.digitalasset.canton.synchronizer.block.data.SequencerBlockStore +import com.digitalasset.canton.synchronizer.block.update.BlockProcessingParameters import com.digitalasset.canton.synchronizer.block.{ BlockSequencerStateManager, SequencerDriverFactory, @@ -107,9 +109,10 @@ class DriverBlockSequencerFactory[C]( health: Option[SequencerHealthConfig], clock: Clock, rateLimitManager: SequencerRateLimitManager, - orderingTimeFixMode: OrderingTimeFixMode, - synchronizerLoggerFactory: NamedLoggerFactory, lsuSequencingBounds: Option[LsuSequencingBounds], + parallelism: PositiveInt, + enablePrevalidation: Boolean, + synchronizerLoggerFactory: NamedLoggerFactory, runtimeReady: FutureUnlessShutdown[Unit], )(implicit ec: ExecutionContextExecutor, @@ -132,12 +135,16 @@ class DriverBlockSequencerFactory[C]( health, clock, rateLimitManager, - orderingTimeFixMode, - lsuSequencingBounds, + BlockProcessingParameters( + orderingTimeFixMode, + lsuSequencingBounds, + parallelism = parallelism, + enablePrevalidation = enablePrevalidation, + ), + nodeParameters, metrics, synchronizerLoggerFactory, runtimeReady = runtimeReady, - nodeParameters, ) } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminService.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminService.scala index 8ab2a899a9..9e456a0275 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminService.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminService.scala @@ -146,6 +146,8 @@ final class BftOrderingSequencerAdminService( response.nodes.toSeq.sorted, GetOrderingTopologyResponse.DynamicSequencingParameters .DynamicSequencingParametersPayload31(response.sequencingParameters.toProto31), + response.leaders, + response.blacklisted, ) } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/SequencerBftAdminData.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/SequencerBftAdminData.scala index c20e34d2ad..ba70ead4ea 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/SequencerBftAdminData.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/SequencerBftAdminData.scala @@ -46,9 +46,9 @@ object SequencerBftAdminData { endpoint.port.unwrap, endpoint match { case _: P2PGrpcNetworking.PlainTextP2PEndpoint => - ProtoPeerEndpoint.Security.PlainText(ProtoPlainTextPeerEndpoint()) + Security.PlainText(ProtoPlainTextPeerEndpoint()) case P2PGrpcNetworking.TlsP2PEndpoint(clientConfig) => - ProtoPeerEndpoint.Security.Tls( + Security.Tls( ProtoTlsPeerEndpoint( clientConfig.tlsConfig.flatMap(_.trustCollectionFile).map(_.pemBytes), clientConfig.tlsConfig.flatMap(_.clientCert).map { clientCertificate => @@ -60,6 +60,7 @@ object SequencerBftAdminData { ) ) }, + sequencerId = None, ) def endpointIdToProto(endpointId: P2PEndpoint.Id): ProtoPeerEndpointId = @@ -370,6 +371,8 @@ object SequencerBftAdminData { final case class OrderingTopology( currentEpoch: Long, sequencerIds: Seq[SequencerId], + leaderSequencerIds: Seq[SequencerId], + blacklistedSequencerIds: Seq[SequencerId], sequencingParameters: topology.SequencingParameters, ) { @@ -379,6 +382,8 @@ object SequencerBftAdminData { sequencerIds.map(SequencerNodeId.toBftNodeId), GetOrderingTopologyResponse.DynamicSequencingParameters .DynamicSequencingParametersPayload31(sequencingParameters.toProto31), + leaderSequencerIds.map(SequencerNodeId.toBftNodeId), + blacklistedSequencerIds.map(SequencerNodeId.toBftNodeId), ) } @@ -392,6 +397,20 @@ object SequencerBftAdminData { .leftMap(_.toString) } yield sequencerId }.sequence + leaders <- response.leaderSequencerIds.map { sequencerIdString => + for { + sequencerId <- SequencerId + .fromProtoPrimitive(sequencerIdString, "sequencerId") + .leftMap(_.toString) + } yield sequencerId + }.sequence + blacklisted <- response.blacklistedSequencerIds.map { sequencerIdString => + for { + sequencerId <- SequencerId + .fromProtoPrimitive(sequencerIdString, "sequencerId") + .leftMap(_.toString) + } yield sequencerId + }.sequence parsedParameters = response.dynamicSequencingParameters match { case DynamicSequencingParameters.Empty => Left(FieldNotSet("dynamicSequencingParameters")) @@ -401,7 +420,7 @@ object SequencerBftAdminData { topology.SequencingParameters.fromProto31(value) } parameters <- parsedParameters.leftMap(_.toString) - } yield OrderingTopology(response.currentEpoch, sequencers, parameters) + } yield OrderingTopology(response.currentEpoch, sequencers, leaders, blacklisted, parameters) } final case class SequencingParameters( diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftBlockOrderer.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftBlockOrderer.scala index 8c1f7fdc9b..8911f0c62f 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftBlockOrderer.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftBlockOrderer.scala @@ -68,8 +68,11 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.mod } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.p2p.P2PNetworkOutModule import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.p2p.data.P2PEndpointsStore -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.BftOrdererPruningScheduler import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.data.BftOrdererPruningSchedulerStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.{ + BftOrdererPruningScheduler, + PartitionManager, +} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.{ BftBlockOrdererConfig, BftOrderingModuleSystemInitializer, @@ -180,26 +183,7 @@ final class BftBlockOrderer( BftNodeId(standaloneConfig.thisSequencerId) } - // The initial metrics factory, which also pre-initializes histograms (as required by OpenTelemetry), is built - // very early in the Canton bootstrap process, before unique IDs for synchronizer nodes are even available, - // so it doesn't include the sequencer ID in the labels, rather just the node name AKA "instance name". - // - // The instance name, though, coming from the Canton config, is operator-chosen and is, in general, not unique and - // even uncorrelated with the sequencer ID, while the BFT ordering system must refer to nodes uniquely and, thus, - // refers to them only by their sequencer IDs. - // - // Since we want to always be able to correlate the sequencer IDs included as additional metrics context, e.g. in - // consensus voting metrics, with the label used by each sequencer to identify itself as the metrics reporting - // sequencer, we use the sequencer ID for that, rather than the instance name. - // - // Hence, we add to the metrics context this node's sequencer ID as the reporting sequencer. - // Also, we do it as soon as the BFT block orderer is created, so that all BFT ordering sequencers include it in all - // emitted metrics. - private implicit val metricsContext: MetricsContext = - MetricsContext(metrics.global.labels.ReportingSequencer -> thisNode) - - // Initialize the non-compliant behavior meter so that a value appears even if all behavior is compliant. - metrics.security.noncompliant.behavior.mark(0) + private implicit val metricsContext: MetricsContext = MetricsContext.Empty metrics.performance.enabled = config.enablePerformanceMetrics @@ -284,6 +268,15 @@ final class BftBlockOrderer( timeouts, loggerFactory, ) + + val partitionManager: Option[ + (PartitionManager.PartitionCreator[PekkoEnv], PartitionManager.PartitionPruner[PekkoEnv]) + ] = + awaitFuture( + PartitionManager.create(localStorage, timeouts, loggerFactory), + "initialize partition management", + )(TraceContext.empty) + private val epochStore = EpochStore(config.batchAggregator, localStorage, timeouts, loggerFactory) private val outputStore = OutputMetadataStore(localStorage, timeouts, loggerFactory) private val pruningSchedulerStore = @@ -343,8 +336,13 @@ final class BftBlockOrderer( private lazy val blockSubscription = new PekkoBlockSubscription[PekkoEnv]( BlockNumber(sequencerSubscriptionInitialHeight), + // Passing the output module reference as a closure because, due to Scala init shenanigans with lazy vals, + // else it would be _often_ `null`. + () => outputModuleRef, timeouts, loggerFactory, + metrics, + config.sequencerCoreSubscriptionConfig, config.outputEnqueueMaxRetries, config.outputEnqueueMaxRetryDelay, )( @@ -413,6 +411,7 @@ final class BftBlockOrderer( epochStoreReader = epochStore, outputStore, pruningSchedulerStore, + partitionManager, ) val topologyProvider = config.standalone.fold[OrderingTopologyProvider[PekkoEnv]]( @@ -682,6 +681,8 @@ final class BftBlockOrderer( SyncCloseable("p2pEndpointsStore.close()", p2pEndpointsStore.close()), SyncCloseable("pruningScheduler.close()", pruningScheduler.close()), SyncCloseable("pruningSchedulerStore.close()", pruningSchedulerStore.close()), + SyncCloseable("PartitionCreator.close()", partitionManager.map(_._1).foreach(_.close())), + SyncCloseable("PartitionPruner.close()", partitionManager.map(_._2).foreach(_.close())), ) ++ // Shutdown the dedicated local storage if present Option diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftSequencerFactory.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftSequencerFactory.scala index 86cc6b5b98..73afbbda37 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftSequencerFactory.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftSequencerFactory.scala @@ -5,12 +5,14 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.binding import com.daml.metrics.ExecutorServiceMetrics import com.digitalasset.canton.concurrent.FutureSupervisor +import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.crypto.SynchronizerCryptoClient import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.resource.Storage import com.digitalasset.canton.synchronizer.block.BlockSequencerStateManager import com.digitalasset.canton.synchronizer.block.data.SequencerBlockStore +import com.digitalasset.canton.synchronizer.block.update.BlockProcessingParameters import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.DatabaseSequencerConfig.TestingInterceptor import com.digitalasset.canton.synchronizer.sequencer.block.BlockSequencerFactory.OrderingTimeFixMode @@ -117,9 +119,10 @@ class BftSequencerFactory( health: Option[SequencerHealthConfig], clock: Clock, rateLimitManager: SequencerRateLimitManager, - orderingTimeFixMode: OrderingTimeFixMode, - synchronizerLoggerFactory: NamedLoggerFactory, lsuSequencingBounds: Option[LsuSequencingBounds], + parallelism: PositiveInt, + enablePrevalidation: Boolean, + synchronizerLoggerFactory: NamedLoggerFactory, runtimeReady: FutureUnlessShutdown[Unit], )(implicit ec: ExecutionContextExecutor, @@ -142,12 +145,16 @@ class BftSequencerFactory( health, clock, rateLimitManager, - orderingTimeFixMode, - lsuSequencingBounds, + BlockProcessingParameters( + orderingTimeFixMode, + lsuSequencingBounds, + parallelism = parallelism, + enablePrevalidation = enablePrevalidation, + ), + nodeParameters, metrics, synchronizerLoggerFactory, runtimeReady = runtimeReady, - nodeParameters, ) } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionManager.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionManager.scala index 7940d9e8cf..4288ddc206 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionManager.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionManager.scala @@ -329,8 +329,8 @@ private[bftordering] final class P2PGrpcConnectionManager( } } - // Note that we loc gRPC channels with their native `toString` be able to correlated - // with orphaned warnings from gRPC itself and avoid leaking channels. + // Note that we log gRPC channels with their native `toString` to be able to correlate them + // with orphaned warnings (leaks) from gRPC itself. private def connect( p2pEndpoint: P2PEndpoint @@ -351,7 +351,9 @@ private[bftordering] final class P2PGrpcConnectionManager( s"Created a gRPC channel $ch to $p2pEndpointId, starting a connect worker" ) - p2pConnectionEventListener.onConnect(p2pEndpointId) + // Notify the new connection for observability purposes + p2pConnectionEventListener.onConnect(Some(p2pEndpointId)) + val sequencerIdUS = maybeSequencerIdFromAuthenticationPromiseUS.getOrElse( // Authentication is disabled, the peer receiver will use the first message's sentBy to backfill @@ -411,7 +413,7 @@ private[bftordering] final class P2PGrpcConnectionManager( s"P2P endpoint $p2pEndpointId successfully connected and authenticated " + s"as ${sequencerId.toProtoPrimitive}" ) - tryAddPeerEndpoint( + tryAddPeerEndpointAndSender( sequencerId, peerSender, Some(p2pEndpoint), @@ -422,7 +424,8 @@ private[bftordering] final class P2PGrpcConnectionManager( .transformWith { case f @ Failure(exception) => logger.info( - s"Failed adding the P2P endpoint $p2pEndpointId, shutting down the gRPC channel", + s"Failed adding the P2P endpoint $p2pEndpointId, transitioning to disconnected and " + + s"shutting down the gRPC channel $channel", exception, ) val doShutdownChannel = @@ -444,7 +447,7 @@ private[bftordering] final class P2PGrpcConnectionManager( } } - private def tryAddPeerEndpoint( + private def tryAddPeerEndpointAndSender( sequencerId: SequencerId, peerSender: PeerSender, // It may be None if the peer is connecting to us and did not communicate its endpoint @@ -458,16 +461,35 @@ private[bftordering] final class P2PGrpcConnectionManager( maybeP2PEndpointId.map( p2pGrpcConnectionState.associateP2PEndpointIdToBftNodeId(_, bftNodeId) ) match { + case None | Some(Right(())) => - if (p2pGrpcConnectionState.addSenderIfMissing(bftNodeId, peerSender)) { - p2pConnectionEventListener.onSequencerId(bftNodeId, maybeP2PEndpoint) - } else { + if (!p2pGrpcConnectionState.addSenderIfMissing(bftNodeId, peerSender)) { logger.info( s"Completing peer sender $peerSender for $bftNodeId <-> $maybeP2PEndpointId " + "because one already exists" ) completeGrpcStreamObserver(peerSender, logger) } + // Prevents a stuck "sender present, network ref missing" state by ensuring a network ref is + // (re)asserted regardless of whether this connection "wins" the race. + // + // This state is generally possible because, even though the sender and the network ref + // are both gated on successful authentication, they are created and registered asynchronously and + // independently, with potentially different failure reasons and modes. + // + // A "sender present, ref missing" state would be a deadlocked connection-establishment state between two + // nodes, requiring manual intervention (restart, or admin remove/re-add), because: + // + // - A registered sender prevents the establishment of a new connection to the same peer. + // - The network ref is needed to send messages. + // - A registered sender belonging to a failed stream that wasn't cleaned up by remote + // completion (e.g. the network dropping the peer's onError) can currently only be cleaned + // up by gRPC failing to send, which the missing network ref prevents. + // + // Re-asserting here is safe and idempotent: `addNetworkRefIfMissing` doesn't touch the state + // when a ref exists. + p2pConnectionEventListener.onSequencerId(bftNodeId, maybeP2PEndpoint) + case Some(Left(error)) => error match { case Error.CannotAssociateP2PEndpointIdsToSelf(p2pEndpointId, thisBftNodeId) => @@ -578,7 +600,7 @@ private[bftordering] final class P2PGrpcConnectionManager( ) } else { logger.info( - s"Shutting gRPC channel $channelId with authentication context $authenticationContextId " + + s"Shutting down gRPC channel $channelId with authentication context $authenticationContextId " + s"to connect to $p2pEndpointId due to connection status having moved away from 'Connecting'" ) shutdownGrpcChannelIfNeeded(p2pEndpointId, channel, authenticationContextO).map(_ => @@ -997,6 +1019,12 @@ private[bftordering] final class P2PGrpcConnectionManager( metricsContext: MetricsContext, traceContext: TraceContext, ): UnlessShutdown[StreamObserver[BftOrderingMessage]] = { + val maybeCommunicatedEndpoint = + ServerAuthenticatingServerInterceptor.peerEndpointContextKey.get() + + // Notify the new connection for observability purposes + p2pConnectionEventListener.onConnect(maybeCommunicatedEndpoint.map(_.id)) + val peerSender = new PeerSender(sendingStreamObserver) val peerSenderId = peerSender.toString if (!isClosing) { @@ -1032,19 +1060,25 @@ private[bftordering] final class P2PGrpcConnectionManager( logger.info( s"Successfully created a peer receiver $peerReceiverId for an incoming connection" ) - // A connecting node could omit the peer endpoint when P2P endpoint authentication is disabled, - // or send a wrong or different one; in that case, a subsequent send attempt by this node to an endpoint - // of that peer won't find the gRPC channel and will create a new one in the opposite direction that will - // effectively be a duplicate; however, when the sequencer ID of this duplicate connection is received, - // it will be detected as duplicate by the connection state and shut down. - // This also protects against potentially malicious peers that try to establish more than one connection. - val maybeEndpoint = ServerAuthenticatingServerInterceptor.peerEndpointContextKey.get() logger.info( - s"Peer endpoint communicated via the server context: $maybeEndpoint; " + + s"Peer endpoint communicated via the server context: $maybeCommunicatedEndpoint; " + "adding the connection to the state asynchronously as soon as a sequencer ID is available" ) + + // When P2P endpoint authentication is enabled, a connecting node will communicate the externally reachable + // P2P (and authentication) endpoint, which allows sequencer client authentication to take place; if the + // communicated externally reachable P2P endpoint is wrong, authentication will fail and thus the P2P + // connection won't be established. + // When P2P endpoint authentication is disabled, however, a connecting node could skip communicating + // its peer endpoint or send a wrong one (e.g. it may not be aware that the Internet-exposed one is + // different); in that case, a subsequent send attempt by this node to an endpoint of that peer won't find + // the gRPC channel and will try and create a new one in the opposite direction; if successful, it will + // effectively be a duplicate of the incoming connection. + // However, when the sequencer ID of this duplicate connection is received, it will be detected as duplicate + // by the connection state and shut down. + // This also protects against potentially malicious peers that try to establish more than one connection. sequencerIdPromiseUS.futureUS - .map(tryAddPeerEndpoint(_, peerSender, maybeEndpoint)) + .map(tryAddPeerEndpointAndSender(_, peerSender, maybeCommunicatedEndpoint)) .transform( identity, { exception => @@ -1162,18 +1196,18 @@ private[bftordering] object P2PGrpcConnectionManager { UnlessShutdown.Outcome(p2pConnectionsStatus.updated(p2pEndpointId, newState)) ) -> ResultWithLogs( true, // Start connection - Level.DEBUG -> (() => s"Disconnected (not in state) -> $newState"), + Level.INFO -> (() => s"Disconnected (not in state) -> $newState"), ) case Some(status) => status match { case oldState @ P2POutgoingConnectionStatus.DisconnectingFromChannel(ch, acO, cw) => - // Connect worker still active on a gRPC channel and asked to disconnect, cancel request + // Connect worker still active on a gRPC channel and asked to disconnect, cancel disconnect request val newState = P2POutgoingConnectionStatus.ConnectingOnChannel(ch, acO, Some(cw)) State( UnlessShutdown.Outcome(p2pConnectionsStatus.updated(p2pEndpointId, newState)) - ) -> ResultWithLogs(false, Level.DEBUG -> (() => s"$oldState -> $newState")) + ) -> ResultWithLogs(false, Level.INFO -> (() => s"$oldState -> $newState")) case oldState @ P2POutgoingConnectionStatus.Connecting => // Already connecting @@ -1217,7 +1251,7 @@ private[bftordering] object P2PGrpcConnectionManager { ) State( UnlessShutdown.Outcome(p2pConnectionsStatus.updated(p2pEndpointId, newState)) - ) -> ResultWithLogs(true, Level.DEBUG -> (() => s"$oldState -> $newState)")) + ) -> ResultWithLogs(true, Level.INFO -> (() => s"$oldState -> $newState)")) case oldState: P2POutgoingConnectionStatus.ConnectedOnChannel => this -> ResultWithLogs(false, Level.WARN -> (() => s"$oldState (unchanged)")) @@ -1233,7 +1267,7 @@ private[bftordering] object P2PGrpcConnectionManager { // gRPC channel shut down before recording the new channel this -> ResultWithLogs( false, - Level.DEBUG -> (() => "Disconnected (not in state) (unchanged)"), + Level.DEBUG -> (() => "Disconnected (not in state, unchanged)"), ) } @@ -1267,9 +1301,12 @@ private[bftordering] object P2PGrpcConnectionManager { ) State( UnlessShutdown.Outcome(p2pConnectionsStatus.updated(p2pEndpointId, newState)) - ) -> ResultWithLogs((), Level.DEBUG -> (() => s"$oldState -> $newState")) + ) -> ResultWithLogs((), Level.INFO -> (() => s"$oldState -> $newState")) } else { - this -> ResultWithLogs((), Level.WARN -> (() => s"$oldState (unchanged)")) + // A just-created channel could have been disconnected immediately due to connectivity issues, + // and a new connection attempt with a new worker could already be running when we + // try to progress the old one. + this -> ResultWithLogs((), Level.DEBUG -> (() => s"$oldState (unchanged)")) } case oldState: P2POutgoingConnectionStatus.DisconnectingFromChannel => @@ -1323,7 +1360,7 @@ private[bftordering] object P2PGrpcConnectionManager { UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) ) -> ResultWithLogs( false, - Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), + Level.INFO -> (() => s"$oldState -> Disconnected (not in state)"), ) else this -> ResultWithLogs(false, Level.WARN -> (() => s"$oldState (unchanged)")) @@ -1346,7 +1383,7 @@ private[bftordering] object P2PGrpcConnectionManager { case None => this -> ResultWithLogs( false, - Level.WARN -> (() => "Disconnected (not in state) (unchanged)"), + Level.WARN -> (() => "Disconnected (not in state, unchanged)"), ) } @@ -1375,7 +1412,7 @@ private[bftordering] object P2PGrpcConnectionManager { val newState = P2POutgoingConnectionStatus.ConnectedOnChannel(channel, acO) State( UnlessShutdown.Outcome(p2pConnectionsStatus.updated(p2pEndpointId, newState)) - ) -> ResultWithLogs(None, Level.DEBUG -> (() => s"$oldState -> $newState")) + ) -> ResultWithLogs(None, Level.INFO -> (() => s"$oldState -> $newState")) } else { this -> ResultWithLogs( Some(channel -> acO), @@ -1390,7 +1427,7 @@ private[bftordering] object P2PGrpcConnectionManager { UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) ) -> ResultWithLogs( Some(channel -> acO), - Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), + Level.INFO -> (() => s"$oldState -> Disconnected (not in state)"), ) else this -> ResultWithLogs( @@ -1412,7 +1449,7 @@ private[bftordering] object P2PGrpcConnectionManager { case oldState @ P2POutgoingConnectionStatus.Connecting => this -> ResultWithLogs( Some(channel -> authenticationContextO), - Level.WARN -> (() => s"oldState (unchanged)"), + Level.WARN -> (() => s"$oldState (unchanged)"), ) } @@ -1420,7 +1457,7 @@ private[bftordering] object P2PGrpcConnectionManager { // gRPC channel shut down before the running worker was recorded as assigned to it this -> ResultWithLogs( Some(channel -> authenticationContextO), - Level.DEBUG -> (() => "Disconnected (not in state) (unchanged)"), + Level.DEBUG -> (() => "Disconnected (not in state, unchanged)"), ) } @@ -1451,7 +1488,7 @@ private[bftordering] object P2PGrpcConnectionManager { UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) ) -> ResultWithLogs( true, - Level.DEBUG -> (() => + Level.INFO -> (() => s"$oldState (this worker's channel) -> Disconnected (not in state)" ), ) @@ -1467,7 +1504,7 @@ private[bftordering] object P2PGrpcConnectionManager { UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) ) -> ResultWithLogs( true, - Level.DEBUG -> (() => + Level.INFO -> (() => s"$oldState (this worker's channel) -> Disconnected (not in state)" ), ) @@ -1487,7 +1524,7 @@ private[bftordering] object P2PGrpcConnectionManager { ) ) -> ResultWithLogs( true, - Level.DEBUG -> (() => + Level.INFO -> (() => s"$oldState (this worker's channel) -> Disconnected (not in state)" ), ) @@ -1543,18 +1580,17 @@ private[bftordering] object P2PGrpcConnectionManager { UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) ) -> ResultWithLogs( Right(ch -> acO), - Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), + Level.INFO -> (() => s"$oldState -> Disconnected (not in state)"), ) } case oldState @ P2POutgoingConnectionStatus.Connecting => // Let the gRPC channel setup logic orderly abort the connection attempt - State( - UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) - ) -> ResultWithLogs( - Left(FutureUnlessShutdown.unit), - Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), - ) + State(UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId))) + -> ResultWithLogs( + Left(FutureUnlessShutdown.unit), + Level.INFO -> (() => s"$oldState -> Disconnected (not in state)"), + ) case oldState @ P2POutgoingConnectionStatus.ConnectingOnChannel( ch, @@ -1567,17 +1603,16 @@ private[bftordering] object P2PGrpcConnectionManager { UnlessShutdown.Outcome(p2pConnectionsStatus.updated(p2pEndpointId, newState)) ) -> ResultWithLogs( Left(cw), - Level.DEBUG -> (() => s"$oldState -> $newState"), + Level.INFO -> (() => s"$oldState -> $newState"), ) case oldState @ P2POutgoingConnectionStatus.ConnectingOnChannel(ch, acO, None) => // Let the connect worker orderly abort the connection attempt - State( - UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) - ) -> ResultWithLogs( - Right(ch -> acO), - Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), - ) + State(UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId))) + -> ResultWithLogs( + Right(ch -> acO), + Level.INFO -> (() => s"$oldState -> Disconnected (not in state)"), + ) case oldState @ P2POutgoingConnectionStatus.DisconnectingFromChannel(_, _, cw) => // Let the connect worker finish aborting the connection attempt diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionState.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionState.scala index a4d8d1d377..78b6bbfc81 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionState.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionState.scala @@ -151,9 +151,12 @@ final class P2PGrpcConnectionState( networkRef.close() } if (result.getOrElse(false)) { // Changes made + val trimmedPrevState = prevState.only(p2pEndpointId, bftNodeId) + val trimmedNewState = newState.only(p2pEndpointId, bftNodeId) logger.info( - "P2P connection state before and after `associateP2PEndpointIdToBftNodeId`: " + - s"${BeforeAndAfter(prevState, newState)}" + "Relevant P2P connection state before and after " + + s"`associateP2PEndpointIdToBftNodeId($p2pEndpointId, $bftNodeId)`: " + + s"${BeforeAndAfter(trimmedPrevState, trimmedNewState)}" ) } else { logger.debug( @@ -170,23 +173,28 @@ final class P2PGrpcConnectionState( bftNodeId: BftNodeId, peerSender: PeerSender, )(implicit traceContext: TraceContext): Boolean = { - val (prevState, newState, added) = + val (prevState, newState, existingPeerSenderO) = AtomicUtil .updateAndGetComputed(stateRef)(_.addSenderIfMissing(bftNodeId, peerSender)) .logAndExtract( logger, prefix = s"Adding peer sender $peerSender for BFT node ID $bftNodeId: ", ) - if (added) { + existingPeerSenderO.fold { + val trimmedPrevState = prevState.only(bftNodeId, peerSender) + val trimmedNewState = newState.only(bftNodeId, peerSender) logger.info( - s"P2P connection state before and after `addSenderIfMissing`: ${BeforeAndAfter(prevState, newState)}" + s"Added peer sender $peerSender for BFT node ID $bftNodeId, " + + s"relevant P2P connection state before and after `addSenderIfMissing($bftNodeId, $peerSender)`: " + + s"${BeforeAndAfter(trimmedPrevState, trimmedNewState)}" ) - } else { + true + } { existingPeerSender => logger.debug( - s"No association added for BFT node ID $bftNodeId and peer sender $peerSender because one already exists" + s"No association added for BFT node ID $bftNodeId and peer sender $peerSender because one already exists: $existingPeerSender" ) + false } - added } override def addNetworkRefIfMissing( @@ -218,8 +226,11 @@ final class P2PGrpcConnectionState( logger.debug(s"Created network ref ${objId(networkRef)} for BFT node ID $bftNodeId") } } + val trimmedPrevState = prevState.only(p2pAddressId) + val trimmedNewState = newState.only(p2pAddressId) logger.info( - s"P2P connection state before and after `addNetworkRefIfMissing`: ${BeforeAndAfter(prevState, newState)}" + s"Relevant P2P connection state before and after `addNetworkRefIfMissing($p2pAddressId)`: " + + s"${BeforeAndAfter(trimmedPrevState, trimmedNewState)}" ) } } @@ -258,9 +269,12 @@ final class P2PGrpcConnectionState( } } + val trimmedPrevState = prevState.only(p2pAddressId) + val trimmedNewState = newState.only(p2pAddressId) logger.info( - "P2P connection state before and after `shutdownConnectionAndReturnPeerSender`: " + - s"${BeforeAndAfter(prevState, newState)}" + "Relevant P2P connection state before and after " + + s"`shutdownConnectionAndReturnPeerSender($p2pAddressId, $clearNetworkRefAssociations, $closeNetworkRef)`: " + + s"${BeforeAndAfter(trimmedPrevState, trimmedNewState)}" ) peerSenderO @@ -274,9 +288,11 @@ final class P2PGrpcConnectionState( .updateAndGetComputed(stateRef)(_.unassociateSenderAndReturnEndpointIds(peerSender)) .logAndExtract(logger, prefix = s"Unassociating sender $peerSender: ") if (result.nonEmpty) { + val trimmedPrevState = prevState.only(peerSender) + val trimmedNewState = newState.only(peerSender) logger.info( - "P2P connection state before and after `unassociateSenderAndReturnEndpointIds`: " + - s"${BeforeAndAfter(prevState, newState)}" + s"Relevant P2P connection state before and after `unassociateSenderAndReturnEndpointIds($peerSender)`: " + + s"${BeforeAndAfter(trimmedPrevState, trimmedNewState)}" ) } else { logger.debug(s"No association change for sender $peerSender: $result") @@ -434,19 +450,21 @@ object P2PGrpcConnectionState { def addSenderIfMissing( bftNodeId: BftNodeId, peerSender: PeerSender, - ): (State, ResultWithLogs[(State, State, Boolean)]) = { + ): (State, ResultWithLogs[(State, State, Option[PeerSender])]) = { var updatedState = this var annotation = "" val result = - if (!bftNodeIdToPeerSender.contains(bftNodeId)) { - annotation = s"Associating peer sender $bftNodeId <-> $peerSender" - updatedState = biAssociateBftNodeIdWithPeerSender(bftNodeId, peerSender) - true - } else { - annotation = - s"Not associating peer sender $bftNodeId <-> $peerSender because one for this node already exists" - false - } + bftNodeIdToPeerSender + .get(bftNodeId) + .fold[Option[PeerSender]] { + annotation = s"Associating $bftNodeId <-> $peerSender" + updatedState = biAssociateBftNodeIdWithPeerSender(bftNodeId, peerSender) + None + } { existingPeerSender => + annotation = + s"Not associating $bftNodeId <-> $peerSender because one for this node already exists" + Some(existingPeerSender) + } updatedState -> ResultWithLogs( (this, updatedState, result), Level.DEBUG -> (() => annotation), @@ -789,5 +807,104 @@ object P2PGrpcConnectionState { Level.DEBUG -> (() => s"Not removing network ref for $bftNodeId (as requested)"), ) } + + def only(p2pEndpointId: P2PEndpoint.Id, bftNodeId: BftNodeId): State = + copy( + bftNodeIdToPeerSender = bftNodeIdToPeerSender.filter { case (nodeId, _) => + nodeId == bftNodeId + }, + peerSenderToBftNodeId = peerSenderToBftNodeId.filter { case (_, nodeId) => + nodeId == bftNodeId + }, + p2pEndpointIdToBftNodeId = p2pEndpointIdToBftNodeId.filter { case (endpointId, nodeId) => + endpointId == p2pEndpointId || nodeId == bftNodeId + }, + bftNodeIdToNetworkRef = bftNodeIdToNetworkRef.filter { case (nodeId, _) => + nodeId == bftNodeId + }, + p2pEndpointIdToNetworkRef = p2pEndpointIdToNetworkRef.filter { case (endpointId, _) => + endpointId == p2pEndpointId + }, + ) + + def only(bftNodeId: BftNodeId, peerSender: PeerSender): State = + copy( + bftNodeIdToPeerSender = bftNodeIdToPeerSender.filter { case (nodeId, sender) => + nodeId == bftNodeId || sender == peerSender + }, + peerSenderToBftNodeId = peerSenderToBftNodeId.filter { case (sender, nodeId) => + nodeId == bftNodeId || sender == peerSender + }, + p2pEndpointIdToBftNodeId = p2pEndpointIdToBftNodeId.filter { case (_, nodeId) => + nodeId == bftNodeId + }, + bftNodeIdToNetworkRef = bftNodeIdToNetworkRef.filter { case (nodeId, _) => + nodeId == bftNodeId + }, + p2pEndpointIdToNetworkRef = p2pEndpointIdToNetworkRef.filter { case (endpointId, _) => + p2pEndpointIdToBftNodeId.get(endpointId).contains(bftNodeId) + }, + ) + + def only(p2pAddressId: P2PAddress.Id): State = + p2pAddressId match { + case Left(p2pEndpointId) => + copy( + bftNodeIdToPeerSender = bftNodeIdToPeerSender.filter { case (nodeId, _) => + p2pEndpointIdToBftNodeId.get(p2pEndpointId).contains(nodeId) + }, + peerSenderToBftNodeId = peerSenderToBftNodeId.filter { case (_, nodeId) => + p2pEndpointIdToBftNodeId.get(p2pEndpointId).contains(nodeId) + }, + p2pEndpointIdToBftNodeId = p2pEndpointIdToBftNodeId.filter { case (endpointId, _) => + endpointId == p2pEndpointId + }, + bftNodeIdToNetworkRef = bftNodeIdToNetworkRef.filter { case (nodeId, _) => + p2pEndpointIdToBftNodeId.get(p2pEndpointId).contains(nodeId) + }, + p2pEndpointIdToNetworkRef = p2pEndpointIdToNetworkRef.filter { case (endpointId, _) => + endpointId == p2pEndpointId + }, + ) + case Right(bftNodeId) => + copy( + bftNodeIdToPeerSender = bftNodeIdToPeerSender.filter { case (nodeId, _) => + nodeId == bftNodeId + }, + peerSenderToBftNodeId = peerSenderToBftNodeId.filter { case (_, nodeId) => + nodeId == bftNodeId + }, + p2pEndpointIdToBftNodeId = p2pEndpointIdToBftNodeId.filter { case (_, nodeId) => + nodeId == bftNodeId + }, + bftNodeIdToNetworkRef = bftNodeIdToNetworkRef.filter { case (nodeId, _) => + nodeId == bftNodeId + }, + p2pEndpointIdToNetworkRef = p2pEndpointIdToNetworkRef.filter { case (endpointId, _) => + p2pEndpointIdToBftNodeId.get(endpointId).contains(bftNodeId) + }, + ) + } + + def only(peerSender: PeerSender): State = + copy( + bftNodeIdToPeerSender = bftNodeIdToPeerSender.filter { case (_, sender) => + sender == peerSender + }, + peerSenderToBftNodeId = peerSenderToBftNodeId.filter { case (sender, _) => + sender == peerSender + }, + p2pEndpointIdToBftNodeId = p2pEndpointIdToBftNodeId.filter { case (_, nodeId) => + peerSenderToBftNodeId.get(peerSender).contains(nodeId) + }, + bftNodeIdToNetworkRef = bftNodeIdToNetworkRef.filter { case (nodeId, _) => + peerSenderToBftNodeId.get(peerSender).contains(nodeId) + }, + p2pEndpointIdToNetworkRef = p2pEndpointIdToNetworkRef.filter { case (endpointId, _) => + peerSenderToBftNodeId.get(peerSender).exists { nodeId => + p2pEndpointIdToBftNodeId.get(endpointId).contains(nodeId) + } + }, + ) } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcStreamingReceiver.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcStreamingReceiver.scala index 2733e9e946..b568f5bdfb 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcStreamingReceiver.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcStreamingReceiver.scala @@ -69,6 +69,10 @@ abstract class P2PGrpcStreamingReceiver( updateTimer( metrics.p2p.send.grpcLatency, Duration.between(sendInstant.asJavaInstant, Instant.now), + )( + metricsContext.withExtraLabels( + metrics.p2p.send.labels.SourceSequencer -> message.sentBy + ) ) ) if (!sequencerIdPromiseUS.isCompleted) { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/BftBlockOrdererConfig.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/BftBlockOrdererConfig.scala index 8077b7107a..58e85e5a49 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/BftBlockOrdererConfig.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/BftBlockOrdererConfig.scala @@ -46,7 +46,10 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.Bft DefaultOutputFetchMinimumDelay, DefaultOutputFetchTimeout, DefaultOutputFetchTimeoutCap, + DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, + DefaultSequencerCoreSubscriptionConfig, P2PNetworkConfig, + SequencerCoreSubscriptionConfig, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.time.BftTime import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.EpochLength @@ -153,6 +156,8 @@ import scala.concurrent.duration.* * useful in deployments with heavy execution context contention between the sequencer and BFT * ordering layer. If set to the default [[scala.None]], the BFT orderer shares the same * execution context as the sequencer. + * @param sequencerCoreSubscriptionConfig + * Configuration for the subscription of the sequencer core to the BFT block orderer. */ final case class BftBlockOrdererConfig( segmentLengthForPv34: Option[Long] = None, @@ -183,6 +188,7 @@ final case class BftBlockOrdererConfig( outputFetchTimeoutCap: FiniteDuration = DefaultOutputFetchTimeoutCap, outputEnqueueMaxRetries: Int = DefaultOutputEnqueueMaxRetries, outputEnqueueMaxRetryDelay: FiniteDuration = DefaultOutputEnqueueMaxRetryDelay, + outputSizeOfChunkOfEpochsToLoadAtStart: Int = DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, blockingDbReadTimeout: FiniteDuration = DefaultBlockingDbReadTimeout, initialNetwork: Option[P2PNetworkConfig] = None, standalone: Option[BftBlockOrderingStandaloneNetworkConfig] = None, @@ -191,6 +197,8 @@ final case class BftBlockOrdererConfig( enablePerformanceMetrics: Boolean = true, batchAggregator: BatchAggregatorConfig = BatchAggregatorConfig(), dedicatedExecutionContextDivisor: Option[Int] = DefaultDedicatedExecutionContextDivisor, + sequencerCoreSubscriptionConfig: SequencerCoreSubscriptionConfig = + DefaultSequencerCoreSubscriptionConfig, ) { private val maxRequestsPerBlock = maxBatchesPerBlockProposal * maxRequestsInBatch require( @@ -228,6 +236,7 @@ object BftBlockOrdererConfig { val DefaultOutputFetchTimeoutCap: FiniteDuration = 5.second val DefaultOutputEnqueueMaxRetries: Int = retry.Forever val DefaultOutputEnqueueMaxRetryDelay: FiniteDuration = 5.seconds + val DefaultOutputSizeOfChunkOfEpochsToLoadAtStart: Int = 10 val DefaultBlockingDbReadTimeout: FiniteDuration = 1.minute val DefaultDedicatedExecutionContextDivisor: Option[Int] = None @@ -235,6 +244,9 @@ object BftBlockOrdererConfig { val DefaultAuthenticationTokenManagerConfig: AuthenticationTokenManagerConfig = AuthenticationTokenManagerConfig() + val DefaultSequencerCoreSubscriptionConfig: SequencerCoreSubscriptionConfig = + SequencerCoreSubscriptionConfig() + /** Configuration for peer-to-peer network settings * * @param serverEndpoint @@ -398,4 +410,29 @@ object BftBlockOrdererConfig { signingPublicKeyProtoFile: File, ) + /** Configuration for the subscription of the sequencer core to the BFT block orderer. This + * includes settings for the Pekko buffer and for the additional backpressure buffering mechanism + * between the sequencer core and the orderer, which prevents the sequencer core to be + * overwhelmed if the orderer is consistently faster. + * + * @param pekkoQueueSourceBufferSize + * The buffer size of the Pekko source queue that provides blocks from the orderer to the + * sequencer core. + * @param pauseOrdererThresholdBufferSize + * The threshold buffer size after which the sequencer core signals the orderer to pause. + * @param resumeOrdererThresholdBufferSize + * The threshold buffer size at or below which the sequencer core signals the orderer to resume + * after a pause. + */ + final case class SequencerCoreSubscriptionConfig( + pekkoQueueSourceBufferSize: Int = 5000, + pauseOrdererThresholdBufferSize: Int = 5000, + resumeOrdererThresholdBufferSize: Int = 1000, + ) { + require( + pauseOrdererThresholdBufferSize >= resumeOrdererThresholdBufferSize, + s"The pause threshold buffer size ($pauseOrdererThresholdBufferSize) must be greater than or equal " + + s"to the resume threshold buffer size ($resumeOrdererThresholdBufferSize).", + ) + } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/BftOrderingModuleSystemInitializer.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/BftOrderingModuleSystemInitializer.scala index 226bdb9b14..4947d4211b 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/BftOrderingModuleSystemInitializer.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/BftOrderingModuleSystemInitializer.scala @@ -52,8 +52,11 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.mod P2PNetworkInModule, P2PNetworkOutModule, } -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PruningModule import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.data.BftOrdererPruningSchedulerStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.{ + PartitionManager, + PruningModule, +} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.* import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.Module.{ SystemInitializationResult, @@ -309,6 +312,7 @@ private[bftordering] class BftOrderingModuleSystemInitializer[ requestInspector, epochChecker, previousStoredBlock = outputPreviousStoredBlock, + stores.partitionManager.map(_._1), ), pruning = () => new PruningModule( @@ -316,6 +320,7 @@ private[bftordering] class BftOrderingModuleSystemInitializer[ clock, loggerFactory, timeouts, + stores.partitionManager.map(_._2), ), ) ).initialize(moduleSystem, createP2PNetworkManager) @@ -372,7 +377,14 @@ private[bftordering] class BftOrderingModuleSystemInitializer[ previousTopology, ) - val maybeOnboardingTopologyAndCryptoProvider = maybeOnboardingTopologyQueryTimestamp + val effectiveOnboardingTopologyQueryTimestamp = + maybeOnboardingTopologyQueryTimestamp.orElse { + Option + .when(!initialTopology.contains(node))(reconstructOwnActivationTime(moduleSystem)) + .flatten + } + + val maybeOnboardingTopologyAndCryptoProvider = effectiveOnboardingTopologyQueryTimestamp .map(onboardingTopologyQueryTimestamp => getOrderingTopologyAt(moduleSystem, Some(onboardingTopologyQueryTimestamp), "onboarding") ) @@ -474,6 +486,17 @@ private[bftordering] class BftOrderingModuleSystemInitializer[ s"Fetch $topologyDesignation ordering topology for bootstrap", ).getOrElse(failBootstrap(s"Failed to fetch $topologyDesignation ordering topology")) + private def reconstructOwnActivationTime( + moduleSystem: ModuleSystem[E] + )(implicit traceContext: TraceContext): Option[TopologyActivationTime] = { + val (headTopology, _) = getOrderingTopologyAt(moduleSystem, None, "head") + awaitFuture( + moduleSystem, + orderingTopologyProvider.getFirstKnownAt(headTopology.activationTime), + "Fetch this node's activation time for onboarding crash recovery", + ).flatMap(_.get(node)) + } + private def fetchLatestEpoch( moduleSystem: ModuleSystem[E], includeInProgress: Boolean, @@ -513,6 +536,9 @@ object BftOrderingModuleSystemInitializer { epochStoreReader: EpochStoreReader[E], outputStore: OutputMetadataStore[E], pruningSchedulerStore: BftOrdererPruningSchedulerStore[E], + partitionManager: Option[ + (PartitionManager.PartitionCreator[E], PartitionManager.PartitionPruner[E]) + ], ) /** In case of onboarding, the topology query timestamps look as follows: diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModule.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModule.scala index 93982bb965..91194e30f1 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModule.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModule.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.Bft import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.integration.canton.crypto.CryptoProvider import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.integration.canton.crypto.CryptoProvider.AuthenticatedMessageType import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore.BatchIdAndEpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.{ HasDelayedInit, shortType, @@ -33,7 +34,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor OrderingBlock, ProofOfAvailability, } -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.OrderedBlockForOutput +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.OrderingMode import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.{ Membership, MessageAuthorizer, @@ -349,14 +350,17 @@ final class AvailabilityModule[E <: Env[E]]( case LocalDissemination.RemoteBatchAcknowledgeVerified(batchId, from, signature) => logger.debug( - s"$actingOnMessageType: $from sent valid ACK for batch $batchId, " + + s"$actingOnMessageType: $from sent valid ACK for batch $batchId " + + s"(long-term key ${signature.authorizingLongTermKey.unwrap}), " + "updating batches ready for ordering" ) disseminationProtocolState.disseminationProgress.get(batchId).foreach { progress => setProgress( actingOnMessageType, batchId, - progress.addAck(AvailabilityAck(from, signature)), + // The active topology could have changed while the signature was being verified, so we need to + // review the progress to make sure we don't add stale ACKs + progress.addAck(AvailabilityAck(from, signature)).changeMembership(activeMembership), ) } attemptSatisfyingProposalRequestIfNotWaitingForDelayedResponse(actingOnMessageType) @@ -469,7 +473,11 @@ final class AvailabilityModule[E <: Env[E]]( val newProgress = progress.changeMembership(activeMembership) val needsReSigning = newProgress.needsSigning if (needsReSigning) - fetchBatchesAndThenSelfSend(Seq(tracedBatchId))( + fetchBatchesAndThenSelfSend( + Seq( + tracedBatchId.map(batchId => BatchIdAndEpochNumber(batchId, batch.epochNumber)) + ) + )( // Will trigger signing and then further dissemination Availability.LocalDissemination.LocalBatchesStored(_) ) @@ -546,7 +554,7 @@ final class AvailabilityModule[E <: Env[E]]( )(implicit traceContext: TraceContext, context: E#ActorContextT[Availability.Message[E]], - ): Seq[BatchId] = + ): Map[EpochNumber, Set[BatchId]] = if (currentEpoch < lastKnownEpochNumber) { abort( s"Trying to update lastKnownEpochNumber in Availability module to $currentEpoch which is lower than the current value $lastKnownEpochNumber" @@ -571,7 +579,7 @@ final class AvailabilityModule[E <: Env[E]]( disseminationProtocolState.disseminationQuotas.expireEpoch(initialEpochNumber, expiredEpoch) val evictionEpoch = EpochNumber(expiredEpoch - batchValidityDuration) disseminationProtocolState.disseminationQuotas.evictBatches(evictionEpoch) - } else Seq.empty + } else Map.empty private def updateLastKnownEpochNumberAndEvictExpiredBatches( messageType: => String, @@ -811,17 +819,18 @@ final class AvailabilityModule[E <: Env[E]]( updateAllDisseminationProgressBasedOnActiveMembership(actingOnMessageType) val now = clock.now.toInstant - val batchesThatNeedSigning = mutable.ListBuffer[Traced[BatchId]]() + val batchesThatNeedSigning = mutable.ListBuffer[Traced[BatchIdAndEpochNumber]]() val batchesThatNeedMoreDissemination = - mutable.ListBuffer[(Traced[BatchId], DisseminationStatus.InProgress)]() + mutable.ListBuffer[(Traced[BatchIdAndEpochNumber], DisseminationStatus.InProgress)]() // Continue all in-progress disseminations disseminationProtocolState.disseminationInProgressView .map(_._2) .foreach { disseminationProgress => - val tracedBatchId = disseminationProgress.tracedBatchId + val tracedBatchInfo = disseminationProgress.tracedBatchId + .map(batchId => BatchIdAndEpochNumber(batchId, disseminationProgress.epochNumber)) if (topologyChangedSinceLastProposalRequest && disseminationProgress.needsSigning) { - batchesThatNeedSigning.addOne(tracedBatchId) + batchesThatNeedSigning.addOne(tracedBatchInfo) } else { if ( disseminationProgress @@ -830,7 +839,7 @@ final class AvailabilityModule[E <: Env[E]]( .nonEmpty ) { batchesThatNeedMoreDissemination - .addOne(tracedBatchId -> disseminationProgress) + .addOne(tracedBatchInfo -> disseminationProgress) .discard } } @@ -848,7 +857,7 @@ final class AvailabilityModule[E <: Env[E]]( Availability.LocalDissemination.LocalBatchesStoredSigned( batches.zip(batchesThatNeedMoreDissemination.map(_._2)).map { case ((batchId, batch), _) => - // "signature = None" will trigger further dissemination without re-signing + // "signature = None" will trigger further dissemination without resigning Availability.LocalDissemination .LocalBatchStoredSigned(batchId, batch, currentMembership, signature = None) } @@ -894,22 +903,23 @@ final class AvailabilityModule[E <: Env[E]]( } private def fetchBatchesAndThenSelfSend( - batchIds: Iterable[Traced[BatchId]] + batchInfos: Iterable[Traced[BatchIdAndEpochNumber]] )(f: Seq[(Traced[BatchId], OrderingRequestBatch)] => LocalDissemination)(implicit context: E#ActorContextT[Availability.Message[E]], traceContext: TraceContext, ): Unit = - pipeToSelf(availabilityStore.fetchBatches(batchIds.map(_.value).toSeq)) { + pipeToSelf(availabilityStore.fetchBatches(batchInfos.map(_.value).toSeq)) { case Failure(error) => abort("Failed to fetch batches", error) case Success(AvailabilityStore.MissingBatches(missingBatchIds)) => abort(s"Some batches couldn't be fetched: $missingBatchIds") case Success(AvailabilityStore.AllBatches(batches)) => - val batchIdToTracedMap = batchIds.view.map(x => x.value -> x).toMap + val batchIdToTracedMap = batchInfos.view.map(x => x.value -> x).toMap val batchesWithTraced = batches.map { case (batchId, batch) => + val batchInfo = BatchIdAndEpochNumber(batchId, batch.epochNumber) val tracedBatchId = - batchIdToTracedMap.getOrElse(batchId, Traced(batchId)(TraceContext.empty)) - tracedBatchId -> batch + batchIdToTracedMap.getOrElse(batchInfo, Traced(batchInfo)(TraceContext.empty)) + tracedBatchId.map(_.batchId) -> batch } f(batchesWithTraced) } @@ -1106,8 +1116,8 @@ final class AvailabilityModule[E <: Env[E]]( case Availability.LocalOutputFetch.FetchedBlockDataFromStorage(request, result) => result match { - case AvailabilityStore.MissingBatches(missingBatchIds) => - request.missingBatches.filterInPlace(missingBatchIds.contains) + case AvailabilityStore.MissingBatches(missingBatches) => + request.missingBatches.filterInPlace(missingBatches.map(_.batchId).contains) if (request.missingBatches.isEmpty) { // this case can happen if: // * we stored a missing batch after the fetch request @@ -1130,7 +1140,7 @@ final class AvailabilityModule[E <: Env[E]]( fetchBatchDataFromNodes( messageType, proofOfAvailability, - request.blockForOutput.mode, + request.blockForOutput.orderingMode, ) } } else { @@ -1150,7 +1160,9 @@ final class AvailabilityModule[E <: Env[E]]( locally { implicit val traceContext: TraceContext = request.traceContext dependencies.output.asyncSend( - Output.BlockDataFetched(CompleteBlockData(request.blockForOutput, batches)) + Output.BlockDataFetched( + CompleteBlockData(request.blockForOutput, batches.map(b => b._1 -> b._2)) + ) ) } } @@ -1193,7 +1205,7 @@ final class AvailabilityModule[E <: Env[E]]( logger.info(s"$messageType: $batchId was not missing") } - case Availability.LocalOutputFetch.FetchRemoteBatchDataTimeout(batchId) => + case Availability.LocalOutputFetch.FetchRemoteBatchDataTimeout(batchId, epochNumber) => if (outputFetchProtocolState.pendingRemoteBatchIdsToStore.contains(batchId)) { logger.info(s"Won't retry fetching remote batch $batchId, because it is being stored") return @@ -1225,7 +1237,7 @@ final class AvailabilityModule[E <: Env[E]]( // If these batches cannot be retrieved, e.g. because the topology has changed too much and/or // the nodes in the PoA are unreachable indefinitely, we'll need to resort (possibly manually) // to state transfer incl. the batch payloads (when it is implemented). - if (status.mode.isStateTransfer) + if (status.orderingMode.isStateTransfer) extractNodes(None, useActiveTopology = true) else extractNodes(Some(status.originalProof.acks)) @@ -1246,7 +1258,7 @@ final class AvailabilityModule[E <: Env[E]]( batchId, missingBatchStatus, ) - startDownload(batchId, node, missingBatchStatus.calculateTimeout()) + startDownload(batchId, epochNumber, node, missingBatchStatus.calculateTimeout()) // This message is only used for tests case Availability.LocalOutputFetch.FetchBatchDataFromNodes(proofOfAvailability, mode) => @@ -1263,7 +1275,7 @@ final class AvailabilityModule[E <: Env[E]]( lazy val messageType = shortType(outputFetchMessage) outputFetchMessage match { - case Availability.RemoteOutputFetch.FetchRemoteBatchData(batchId, from) => + case Availability.RemoteOutputFetch.FetchRemoteBatchData(batchId, batchEpochNumber, from) => outputFetchProtocolState.incomingBatchRequests .updateWith(batchId) { case Some(value) => @@ -1278,7 +1290,11 @@ final class AvailabilityModule[E <: Env[E]]( // It's safe to run the fetch before setting the `incomingBatchRequests` entry // because modules are single-threaded and the completion message will be // processed afterward. - pipeToSelf(availabilityStore.fetchBatches(Seq(batchId))) { + pipeToSelf( + availabilityStore.fetchBatches( + Seq(BatchIdAndEpochNumber(batchId, batchEpochNumber)) + ) + ) { case Failure(exception) => abort(s"failed to fetch batch $batchId", exception) case Success(result) => @@ -1338,7 +1354,7 @@ final class AvailabilityModule[E <: Env[E]]( private def fetchBatchDataFromNodes( actingOnMessageType: => String, proofOfAvailability: ProofOfAvailability, - mode: OrderedBlockForOutput.Mode, + orderingMode: OrderingMode, )(implicit context: E#ActorContextT[Availability.Message[E]], traceContext: TraceContext, @@ -1354,7 +1370,7 @@ final class AvailabilityModule[E <: Env[E]]( return } val (node, remainingNodes) = - if (mode.isStateTransfer) + if (orderingMode.isStateTransfer) extractNodes(acks = None, useActiveTopology = true) else extractNodes(Some(proofOfAvailability.acks)) @@ -1368,13 +1384,18 @@ final class AvailabilityModule[E <: Env[E]]( remainingNodes, numberOfAttempts = 1, jitterStream = jitterConstructor(config, random), - mode, + orderingMode, ) outputFetchProtocolState.localOutputMissingBatches.update( proofOfAvailability.batchId, missingBatchStatus, ) - startDownload(proofOfAvailability.batchId, node, missingBatchStatus.calculateTimeout()) + startDownload( + proofOfAvailability.batchId, + proofOfAvailability.epochNumber, + node, + missingBatchStatus.calculateTimeout(), + ) } private def updateOutputFetchStatus( @@ -1400,10 +1421,14 @@ final class AvailabilityModule[E <: Env[E]]( context: E#ActorContextT[Availability.Message[E]], traceContext: TraceContext, ): Unit = { - val batchIds = request.blockForOutput.orderedBlock.batchRefs.map(_.batchId) - pipeToSelf(availabilityStore.fetchBatches(batchIds)) { + val proofs = request.blockForOutput.orderedBlock.batchRefs + pipeToSelf( + availabilityStore.fetchBatches( + proofs.map(poa => BatchIdAndEpochNumber(poa.batchId, poa.epochNumber)) + ) + ) { case Failure(exception) => - abort(s"Failed to load batches $batchIds", exception) + abort(s"Failed to load batches ${proofs.map(_.batchId)}", exception) case Success(result) => Availability.LocalOutputFetch.FetchedBlockDataFromStorage(request, result) } @@ -1411,6 +1436,7 @@ final class AvailabilityModule[E <: Env[E]]( private def startDownload( batchId: BatchId, + epochNumber: EpochNumber, node: BftNodeId, timeout: FiniteDuration, )(implicit @@ -1423,11 +1449,12 @@ final class AvailabilityModule[E <: Env[E]]( context .delayedEvent( timeout, - Availability.LocalOutputFetch.FetchRemoteBatchDataTimeout(batchId), + Availability.LocalOutputFetch.FetchRemoteBatchDataTimeout(batchId, epochNumber), ) .discard send( - Availability.RemoteOutputFetch.FetchRemoteBatchData.create(batchId, from = thisNode), + Availability.RemoteOutputFetch.FetchRemoteBatchData + .create(batchId, epochNumber, from = thisNode), node, ) } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/BatchDisseminationNodeQuotaTracker.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/BatchDisseminationNodeQuotaTracker.scala index bff7325abc..5e4065b9ca 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/BatchDisseminationNodeQuotaTracker.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/BatchDisseminationNodeQuotaTracker.scala @@ -71,10 +71,10 @@ class BatchDisseminationNodeQuotaTracker { */ def evictBatches( evictionEpoch: EpochNumber - ): Seq[BatchId] = { + ): Map[EpochNumber, Set[BatchId]] = { val range = expiredEpochs.rangeTo(evictionEpoch) - val batchesToEvict = range.values.toSeq.flatten + val result = range.toMap range.keySet.foreach(expiredEpochs.remove(_).discard) - batchesToEvict + result } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/OutputFetchProtocolState.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/OutputFetchProtocolState.scala index 1c984ac240..f9bd83a8b3 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/OutputFetchProtocolState.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/OutputFetchProtocolState.scala @@ -9,7 +9,10 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor BatchId, ProofOfAvailability, } -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.OrderedBlockForOutput +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.{ + OrderedBlockForOutput, + OrderingMode, +} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.retry.Jitter @@ -51,7 +54,7 @@ final case class MissingBatchStatus( remainingNodesToTry: Seq[BftNodeId], numberOfAttempts: Int, jitterStream: JitterStream, - mode: OrderedBlockForOutput.Mode, + orderingMode: OrderingMode, ) { def calculateTimeout(): FiniteDuration = jitterStream.next(numberOfAttempts) } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/AvailabilityStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/AvailabilityStore.scala index 986bf7ddec..52fb0d813d 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/AvailabilityStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/AvailabilityStore.scala @@ -19,7 +19,7 @@ import com.google.common.annotations.VisibleForTesting import scala.concurrent.ExecutionContext -import AvailabilityStore.FetchBatchesResult +import AvailabilityStore.{BatchIdAndEpochNumber, FetchBatchesResult} trait AvailabilityStore[E <: Env[E]] extends AutoCloseable { def addBatch(batchId: BatchId, batch: OrderingRequestBatch)(implicit @@ -27,12 +27,12 @@ trait AvailabilityStore[E <: Env[E]] extends AutoCloseable { ): E#FutureUnlessShutdownT[Boolean] protected def addBatchActionName(batchId: BatchId): String = s"Add batch $batchId" - def fetchBatches(batches: Seq[BatchId])(implicit + def fetchBatches(batches: Seq[BatchIdAndEpochNumber])(implicit traceContext: TraceContext ): E#FutureUnlessShutdownT[FetchBatchesResult] protected val fetchBatchesActionName: String = "Fetch batches" - def gc(staleBatchIds: Seq[BatchId])(implicit + def gc(staleBatchIds: Map[EpochNumber, Set[BatchId]])(implicit traceContext: TraceContext ): E#FutureUnlessShutdownT[Unit] protected def gcName: String = s"remove batches" @@ -55,7 +55,7 @@ trait AvailabilityStore[E <: Env[E]] extends AutoCloseable { object AvailabilityStore { sealed trait FetchBatchesResult - final case class MissingBatches(batchIds: Set[BatchId]) extends FetchBatchesResult + final case class MissingBatches(batches: Set[BatchIdAndEpochNumber]) extends FetchBatchesResult final case class AllBatches(batches: Seq[(BatchId, OrderingRequestBatch)]) extends FetchBatchesResult @@ -63,6 +63,8 @@ object AvailabilityStore { final case class NumberOfRecords(batches: Long) object NumberOfRecords { val empty = NumberOfRecords(0L) } + final case class BatchIdAndEpochNumber(batchId: BatchId, epochNumber: EpochNumber) + def apply( batchAggregatorConfig: BatchAggregatorConfig, cachingConfigs: CachingConfigs, diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/db/DbAvailabilityStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/db/DbAvailabilityStore.scala index ff4d3e165d..cc411cf693 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/db/DbAvailabilityStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/db/DbAvailabilityStore.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db +import cats.syntax.parallel.* import com.daml.nameof.NameOf.functionFullName import com.daml.nonempty.NonEmpty import com.digitalasset.canton.caching.{CaffeineCache, ConcurrentCache} @@ -21,6 +22,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings PekkoFutureUnlessShutdown, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore.BatchIdAndEpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.EpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.OrderingRequestBatch import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.availability.BatchId @@ -79,20 +81,21 @@ class DbAvailabilityStore( private val fetchBatchAggregator = { val processor = - new BatchAggregator.Processor[BatchId, Option[OrderingRequestBatch]] { + new BatchAggregator.Processor[BatchIdAndEpochNumber, Option[OrderingRequestBatch]] { override def kind: String = "availability-lookup-batch" override def logger: TracedLogger = DbAvailabilityStore.this.logger - override def executeBatch(items: NonEmpty[Seq[Traced[BatchId]]])(implicit + override def executeBatch(items: NonEmpty[Seq[Traced[BatchIdAndEpochNumber]]])(implicit traceContext: TraceContext, callerCloseContext: CloseContext, ): FutureUnlessShutdown[immutable.Iterable[Option[OrderingRequestBatch]]] = lookupBatches(items.map(_.value)) - override def prettyItem: Pretty[BatchId] = { + override def prettyItem: Pretty[BatchIdAndEpochNumber] = { import com.digitalasset.canton.logging.pretty.PrettyUtil.* - prettyOfClass[BatchId]( - param("batchId", _.hash) + prettyOfClass[BatchIdAndEpochNumber]( + param("batchId", _.batchId.hash), + param("epochNumber", _.epochNumber), ) } } @@ -177,10 +180,10 @@ class DbAvailabilityStore( case _: Postgres => """insert into ord_availability_batch values (?, ?, ?) - on conflict (id) do nothing""" + on conflict (epoch_number, id) do nothing""" case _: H2 => """merge into ord_availability_batch using dual - on (id = ?1) + on (id = ?1 and epoch_number = ?3) when not matched then insert (id, batch, epoch_number) values (?1, ?2, ?3)""" @@ -222,40 +225,61 @@ class DbAvailabilityStore( } } - private def lookupBatches(batches: NonEmpty[Seq[BatchId]])(implicit + private def lookupBatches(batches: NonEmpty[Seq[BatchIdAndEpochNumber]])(implicit traceContext: TraceContext - ): FutureUnlessShutdown[Seq[Option[OrderingRequestBatch]]] = { - import DbStorage.Implicits.BuilderChain.* + ): FutureUnlessShutdown[Seq[Option[OrderingRequestBatch]]] = storage.synchronizeWithClosing("lookup-batches") { - val query = - sql"""select id, batch from ord_availability_batch where """ ++ DbStorage.toInClause( - "id", - batches, + import DbStorage.Implicits.BuilderChain.* + + val (batchsWithEpoch0, otherBatches) = batches.partition(_.epochNumber == EpochNumber(0)) + + // If a batch is requested with epoch number 0, it can be one of the following cases: + // - It was remotely requested by a node on an older version, whose remote batch request did not yet include an + // epoch number, in which case the protobuf request by default deserealizes the empty epoch number field as 0. + // In that case, we don't want to use the epoch number in the query, just the batch id. + // - The epoch number 0 was deliberately requested. Because we cannot differentiate between the 2 cases, we will + // still perform this query without the epoch number, which will be of worse performance, but hopefully not that + // bad considering that at that point, we probably don't have too much data in this table yet. + val query1 = NonEmpty + .from(batchsWithEpoch0.map(_.batchId)) + .map(batchIds => + (sql"""select id, batch from ord_availability_batch where """ ++ DbStorage + .toInClause("id", batchIds)).as[(BatchId, OrderingRequestBatch)] ) - storage - .query( - query.as[(BatchId, OrderingRequestBatch)], - functionFullName, + + // Otherwise if a non zero epoch number is given, this is included in the query, which improves its performance + // significantly, especially in the presence of many partitions. + val query2 = NonEmpty + .from(otherBatches) + .map(batchInfos => + sql"""select id, batch from ord_availability_batch where (id, epoch_number) IN (#${batchInfos + .map(p => s"('${batchIdToPrimitive.toDbPrimitive(p.batchId)}', ${p.epochNumber})") + .mkString(", ")})""".as[(BatchId, OrderingRequestBatch)] ) - .map(_.toMap) - .map(result => batches.toSeq.map(result.get)) + + import cats.syntax.parallel.* + + (query1.toList ++ query2.toList) + .parTraverse(query => storage.query(query, functionFullName).map(_.toMap)) + .map(maps => maps.foldLeft(Map[BatchId, OrderingRequestBatch]())(_ ++ _)) + .map(result => batches.toSeq.map(_.batchId).map(result.get)) } - } - override def fetchBatches(batches: Seq[BatchId])(implicit + override def fetchBatches(batches: Seq[BatchIdAndEpochNumber])(implicit traceContext: TraceContext ): PekkoFutureUnlessShutdown[AvailabilityStore.FetchBatchesResult] = { val name = fetchBatchesActionName - val (missingBatchIds, presentInCacheIds) = batches.partitionMap { id => - lookupBatchCache.getIfPresent(id) match { - case Some(value) => Right(id -> value) - case None => Left(id) + val (missingBatches, presentInCacheBatches) = batches.partitionMap { batch => + lookupBatchCache.getIfPresent(batch.batchId) match { + case Some(value) => Right(batch -> value) + case None => Left(batch) } } - val presentCache = presentInCacheIds.toMap - NonEmpty.from(missingBatchIds) match { + val presentCache = presentInCacheBatches.toMap + + NonEmpty.from(missingBatches) match { case None => // All batches are already in cache no lookup required @@ -263,7 +287,9 @@ class DbAvailabilityStore( name, () => FutureUnlessShutdown.pure( - AvailabilityStore.AllBatches(batches.map(id => id -> presentCache(id))) + AvailabilityStore.AllBatches( + batches.map(batchInfo => batchInfo.batchId -> presentCache(batchInfo)) + ) ), ) case Some(oneOrMoreBatchesMissing) => @@ -273,18 +299,20 @@ class DbAvailabilityStore( batchesThatWeHave => val (stillMissing, newBatchMappingsNotInCache) = oneOrMoreBatchesMissing.zip(batchesThatWeHave).partitionMap { - case (batchId, None) => - Left(batchId) - case (batchId, Some(value)) => - Right(batchId -> value) + case (batchInfo, None) => + Left(batchInfo) + case (batchInfo, Some(value)) => + Right(batchInfo -> value) } val resultMap = newBatchMappingsNotInCache.toMap - lookupBatchCache.putAll(resultMap) + lookupBatchCache.putAll(resultMap.map { case (poa, requestBatch) => + poa.batchId -> requestBatch + }) if (stillMissing.nonEmpty) { AvailabilityStore.MissingBatches(stillMissing.toSet) } else { - AvailabilityStore.AllBatches(batches.map { id => - id -> presentCache.getOrElse(id, resultMap(id)) + AvailabilityStore.AllBatches(batches.map { batchInfo => + batchInfo.batchId -> presentCache.getOrElse(batchInfo, resultMap(batchInfo)) }) } } @@ -293,24 +321,28 @@ class DbAvailabilityStore( } - override def gc(staleBatchIds: Seq[BatchId])(implicit + override def gc(staleBatchIds: Map[EpochNumber, Set[BatchId]])(implicit traceContext: TraceContext ): PekkoFutureUnlessShutdown[Unit] = PekkoFutureUnlessShutdown( gcName, () => - NonEmpty.from(staleBatchIds) match { - case Some(oneOrMoreBatchIds) => - import DbStorage.Implicits.BuilderChain.* - storage - .update_( - (sql"""delete from ord_availability_batch where """ ++ DbStorage - .toInClause("id", oneOrMoreBatchIds)).asUpdate, - functionFullName, - ) - .map(_ => lookupBatchCache.invalidateAll(staleBatchIds)) - case None => FutureUnlessShutdown.unit - }, + staleBatchIds.toSeq + .parTraverse { case (epochNumber, batchIds) => + NonEmpty.from(batchIds) match { + case Some(oneOrMoreBatchIds) => + import DbStorage.Implicits.BuilderChain.* + storage + .update_( + (sql"""delete from ord_availability_batch where epoch_number = $epochNumber and """ ++ DbStorage + .toInClause("id", oneOrMoreBatchIds)).asUpdate, + functionFullName, + ) + .map(_ => lookupBatchCache.invalidateAll(batchIds)) + case None => FutureUnlessShutdown.unit + } + } + .map(_ => ()), orderingStage = Some(functionFullName), ) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/memory/InMemoryAvailabilityStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/memory/InMemoryAvailabilityStore.scala index ecd34fccb9..dcff6c7c79 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/memory/InMemoryAvailabilityStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/memory/InMemoryAvailabilityStore.scala @@ -10,6 +10,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings PekkoFutureUnlessShutdown, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore.BatchIdAndEpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.Env import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.EpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.OrderingRequestBatch @@ -21,7 +22,8 @@ import scala.collection.concurrent.TrieMap import scala.util.{Success, Try} abstract class GenericInMemoryAvailabilityStore[E <: Env[E]]( - allKnownBatchesById: TrieMap[BatchId, OrderingRequestBatch] = TrieMap.empty + @VisibleForTesting + private[data] val allKnownBatchesById: TrieMap[BatchId, OrderingRequestBatch] = TrieMap.empty ) extends AvailabilityStore[E] { def createFuture[A](action: String)(x: () => Try[A]): E#FutureUnlessShutdownT[A] @@ -36,27 +38,29 @@ abstract class GenericInMemoryAvailabilityStore[E <: Env[E]]( } override def fetchBatches( - batches: Seq[BatchId] + batches: Seq[BatchIdAndEpochNumber] )(implicit traceContext: TraceContext ): E#FutureUnlessShutdownT[AvailabilityStore.FetchBatchesResult] = createFuture(fetchBatchesActionName) { () => Try { val keys = allKnownBatchesById.keySet - val missing = batches.filterNot(batchId => keys.contains(batchId)) + val missing = batches.filterNot(batch => keys.contains(batch.batchId)) if (missing.isEmpty) { - AvailabilityStore.AllBatches(batches.map(id => id -> allKnownBatchesById(id))) + AvailabilityStore.AllBatches( + batches.map(batch => batch.batchId -> allKnownBatchesById(batch.batchId)) + ) } else { AvailabilityStore.MissingBatches(missing.toSet) } } } - override def gc(staleBatchIds: Seq[BatchId])(implicit + override def gc(staleBatchIds: Map[EpochNumber, Set[BatchId]])(implicit traceContext: TraceContext ): E#FutureUnlessShutdownT[Unit] = createFuture(gcName) { () => - staleBatchIds.foreach { staleBatchId => + staleBatchIds.values.flatten.foreach { staleBatchId => val _ = allKnownBatchesById.remove(staleBatchId) } Success(()) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssConsensusModule.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssConsensusModule.scala index 8fafc1d25c..53e03e7e18 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssConsensusModule.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssConsensusModule.scala @@ -39,6 +39,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor CommitCertificate, OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.snapshot.SequencerSnapshotAdditionalInfo import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.{ @@ -145,6 +146,15 @@ final class IssConsensusModule[E <: Env[E]]( private var consensusWaitingForEpochCompletionSince: Option[Instant] = None private var consensusWaitingForEpochStartSince: Option[Instant] = None + // At genesis (e.g. during LSU) we start and store epoch 0, but we aren't closing a previous epoch; in this situation, + // if the node is slow to start, it's possible that we start state transfer for epoch -1 before epoch 0 is stored, + // but state transfer doesn't support it, so we must wait for the message sent when an epoch is stored to be sure + // we transition into catch-up state transfer at epoch 0 and not -1. + // Also, we must avoid leaking epoch stored messages into the state transfer behavior in general, as + // it could be already state transferring, in which case we'd be violating its internal invariants. + @VisibleForTesting + private[iss] var storingNewEpoch: Boolean = false + @VisibleForTesting private[iss] def getActiveTopologyInfo: OrderingTopologyInfo[E] = activeTopologyInfo @@ -244,10 +254,13 @@ final class IssConsensusModule[E <: Env[E]]( newMembership, newCryptoProvider: CryptoProvider[E], ) => + storingNewEpoch = false + val newEpochNumber = newEpochInfo.number + // Despite being generated internally by Consensus, we delay this event (a) for uniformity with other // Output module events, and (b) to prevent tests from bypassing the delayed queue when sending this event ifInitCompleted(newEpochStored) { _ => - logger.debug(s"Stored new epoch ${newEpochInfo.number}") + logger.debug(s"Stored new epoch $newEpochNumber") // Reset any topology remembered while waiting for the previous (completed) epoch to be stored. newEpochTopology = None @@ -347,6 +360,8 @@ final class IssConsensusModule[E <: Env[E]]( GetOrderingTopologyResponse( epochState.epoch.info.number, activeTopologyInfo.currentMembership.orderingTopology.nodes, + activeTopologyInfo.currentMembership.leaders, + activeTopologyInfo.currentMembership.blacklistedNodes, activeTopologyInfo.currentMembership.orderingTopology.sequencingParameters, ) ) @@ -423,7 +438,11 @@ final class IssConsensusModule[E <: Env[E]]( val thisNodeEpochNumber = epochState.epoch.info.number - val updatedEpoch = catchupDetector.updateLatestKnownNodeEpoch(from, epochNumber) + val updatedEpoch = if (from == actualSender) { + catchupDetector.updateLatestKnownNodeEpoch(from, epochNumber) + } else { + false + } lazy val pbftMessageType = shortType(msg.underlyingNetworkMessage.message) if (epochNumber < thisNodeEpochNumber) { @@ -513,7 +532,7 @@ final class IssConsensusModule[E <: Env[E]]( commitCertificate.prePrepare.message.viewNumber, blockSegment.originalLeader, blockNumber == epochState.epoch.info.lastBlockNumber, - OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ) ) ) @@ -568,6 +587,9 @@ final class IssConsensusModule[E <: Env[E]]( newEpochTopology match { case Some(Consensus.NewEpochTopology(newEpochNumber, newMembership, cryptoProvider)) => + logger.info( + s"Completed epoch $completeEpochNumber, new epoch topology already available for epoch $newEpochNumber" + ) emitEpochStartLatency() val currentEpochInfo = epochState.epoch.info val newEpochInfo = currentEpochInfo.next( @@ -586,6 +608,9 @@ final class IssConsensusModule[E <: Env[E]]( cryptoProvider, ) case None => + logger.info( + s"Completed epoch $completeEpochNumber, but no new epoch topology is available yet" + ) // We don't have the new topology for the new epoch yet: wait for it to arrive from the output module. () } @@ -648,10 +673,15 @@ final class IssConsensusModule[E <: Env[E]]( epochState.emitEpochStats(metrics, currentEpochInfo) logger.debug(s"Storing new epoch $newEpochInfo") + storingNewEpoch = true pipeToSelf(epochStore.startEpoch(newEpochInfo)) { case Failure(exception) => Consensus.ConsensusMessage.AsyncException(exception) case Success(_) => - Consensus.NewEpochStored(newEpochInfo, newMembership, cryptoProvider) + Consensus.NewEpochStored( + newEpochInfo, + newMembership, + cryptoProvider, + ) } } else { logger.info( @@ -805,20 +835,29 @@ final class IssConsensusModule[E <: Env[E]]( if (updatedEpoch && minimumEndEpochNumber.isDefined) { // if epochState is closed, we have probably just finished an epoch and are waiting for new topology. // So we should wait with state transfer until we are in the new epoch. - if (!epochState.isClosing) { + if (epochState.isClosing) { logger.info( - s"Switching to catch-up state transfer (up to at least $minimumEndEpochNumber) while in epoch $currentEpochNumber; " + + s"Not switching to catch-up state transfer because epochState is closing " + + s"(probably just finished an epoch), but otherwise we would have since: " + + s"(up to at least $minimumEndEpochNumber) while in epoch $currentEpochNumber; " + s"latestCompletedEpoch is $latestCompletedEpochNumber and message epoch is $pbftMessageEpochNumber" ) - startStateTransfer(currentEpochNumber, StateTransferType.Catchup, minimumEndEpochNumber) - true - } else { + false + } else if (storingNewEpoch) { logger.info( - s"Not switching to catch-up state transfer because epochState is closing (probably just finished an epoch), but otherwise we would have since:" + + s"Not switching to catch-up state transfer because we are storing a new epoch, " + + s"but otherwise we would have since: " + s"(up to at least $minimumEndEpochNumber) while in epoch $currentEpochNumber; " + s"latestCompletedEpoch is $latestCompletedEpochNumber and message epoch is $pbftMessageEpochNumber" ) false + } else { + logger.info( + s"Switching to catch-up state transfer (up to at least $minimumEndEpochNumber) while in epoch $currentEpochNumber; " + + s"latestCompletedEpoch is $latestCompletedEpochNumber and message epoch is $pbftMessageEpochNumber" + ) + startStateTransfer(currentEpochNumber, StateTransferType.Catchup, minimumEndEpochNumber) + true } } else { false diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssSegmentModule.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssSegmentModule.scala index 931e1212d6..7f5cd4c193 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssSegmentModule.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssSegmentModule.scala @@ -47,7 +47,7 @@ import com.digitalasset.canton.version.ProtocolVersion import com.google.common.annotations.VisibleForTesting import io.opentelemetry.api.trace.{Span, Tracer} -import java.time.Instant +import java.time.{Duration, Instant} import scala.collection.mutable import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Success, Try} @@ -80,11 +80,27 @@ class IssSegmentModule[E <: Env[E]]( private val thisNode = epoch.currentMembership.myId + private case class ViewChangeMetric() extends TimeoutManager.TimeoutMetric { + + private val metricsContextForThisSegment: MetricsContext = metricsContext.withExtraLabels( + metrics.consensus.labels.Leader -> segmentState.leader + ) + + override def scheduleChangedAfter( + duration: Duration + ): Unit = + BftOrderingMetrics.updateTimer( + metrics.consensus.viewChangeProgressLatency, + duration, + )(metricsContextForThisSegment) + } + private val viewChangeTimeoutManager = new TimeoutManager[E, ConsensusSegment.Message, BlockNumber]( loggerFactory, segmentState.epoch.currentMembership.orderingTopology.sequencingParameters.pbftViewChangeTimeout.toScala, segmentState.segment.firstBlockNumber, + Some(ViewChangeMetric()), ) private val blockStartTimeoutManager = @@ -92,6 +108,7 @@ class IssSegmentModule[E <: Env[E]]( loggerFactory, emptyBlockCreationTimeout, segmentState.segment.firstBlockNumber, + None, ) private val rehydrationMessages = @@ -707,9 +724,7 @@ class IssSegmentModule[E <: Env[E]]( logger.debug(s"Consensus requesting a new proposal for block $forBlock from local availability") maybeOriginalLeaderSegmentState.foreach(_.startWaitingForAvailabilityResponse()) waitingForProposalSince = Some(Instant.now()) - blockStartTimeoutManager.scheduleTimeout( - ConsensusSegment.Internal.BlockInactivityTimeout - ) + blockStartTimeoutManager.scheduleTimeout(ConsensusSegment.Internal.BlockInactivityTimeout) availability.asyncSend( Availability.Consensus.CreateProposal( forBlock, diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/TimeoutManager.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/TimeoutManager.scala index efc826c165..7a8c01f5d3 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/TimeoutManager.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/TimeoutManager.scala @@ -6,12 +6,14 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.mo import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.TimeoutManager.TimeoutMetric import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.{ CancellableEvent, Env, } import com.digitalasset.canton.tracing.TraceContext +import java.time.{Duration, Instant} import scala.concurrent.duration.FiniteDuration /** Manages cancellable timeouts on behalf of another module; it is parametric in the type of the @@ -22,11 +24,12 @@ class TimeoutManager[E <: Env[E], ParentModuleMessageT, TimeoutIdT]( override val loggerFactory: NamedLoggerFactory, timeout: FiniteDuration, timeoutId: TimeoutIdT, + timeoutMetric: Option[TimeoutMetric], )(implicit metricsContext: MetricsContext) extends NamedLogging { @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var timeoutCancellable: Option[CancellableEvent] = None + private var timeoutCancellable: Option[(Instant, CancellableEvent)] = None def scheduleTimeout[TimeoutMessageT <: ParentModuleMessageT]( timeoutEvent: TimeoutMessageT @@ -35,25 +38,39 @@ class TimeoutManager[E <: Env[E], ParentModuleMessageT, TimeoutIdT]( traceContext: TraceContext, ): Unit = { val cancellableEvent = context.delayedEvent(timeout, timeoutEvent) + val timeNow = Instant.now() timeoutCancellable match { - case Some(previousTimeout) => + case Some((previousTime, previousTimeout)) => previousTimeout.cancel().discard + val duration = Duration.between(previousTime, timeNow) logger.debug( s"Rescheduling timeout w/ duration: $timeout; previous event: $previousTimeout; new event: $timeoutEvent" ) + timeoutMetric.foreach(_.scheduleChangedAfter(duration)) case None => logger.debug( s"Scheduling new timeout w/ duration: $timeout; new event: $timeoutEvent" ) } - timeoutCancellable = Some(cancellableEvent) + timeoutCancellable = Some(timeNow -> cancellableEvent) } def cancelTimeout()(implicit traceContext: TraceContext): Unit = { - timeoutCancellable.foreach { timeout => + timeoutCancellable.foreach { case (previousTime, timeout) => + timeoutMetric.foreach( + _.scheduleChangedAfter( + Duration.between(previousTime, Instant.now()) + ) + ) logger.debug(s"Canceling timeout w/ ID: $timeoutId") timeout.cancel().discard } timeoutCancellable = None } } + +object TimeoutManager { + trait TimeoutMetric { + def scheduleChangedAfter(duration: Duration): Unit + } +} diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/EpochStoreReader.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/EpochStoreReader.scala index 9ceeafec87..1f0a27b24d 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/EpochStoreReader.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/EpochStoreReader.scala @@ -4,10 +4,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.Env -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.{ - BlockNumber, - EpochNumber, -} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.EpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.OrderedBlockForOutput import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.iss.EpochInfo import com.digitalasset.canton.tracing.TraceContext @@ -22,9 +19,15 @@ trait EpochStoreReader[E <: Env[E]] { s"Load epoch $epochNumber info" def loadOrderedBlocks( - initialBlockNumber: BlockNumber + initialEpochNumber: EpochNumber, + limit: Int, )(implicit traceContext: TraceContext): E#FutureUnlessShutdownT[Seq[OrderedBlockForOutput]] + protected def loadOrderedBlocksActionName(initialEpochNumber: EpochNumber, limit: Int): String = + s"Load ordered blocks starting from epoch $initialEpochNumber (limit $limit)" - protected def loadOrderedBlocksActionName(initialBlockNumber: BlockNumber): String = - s"Load ordered blocks starting from $initialBlockNumber" + def lastEpochWithCompletedBlock(lowerBound: EpochNumber)(implicit + traceContext: TraceContext + ): E#FutureUnlessShutdownT[Option[EpochNumber]] + protected def lastEpochWithCompletedBlockActionName: String = + "Load epoch which has a completed block" } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/db/DbEpochStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/db/DbEpochStore.scala index ccffdcb26d..d0d8d0cab7 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/db/DbEpochStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/db/DbEpochStore.scala @@ -42,6 +42,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor CommitCertificate, OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.ConsensusSegment.ConsensusMessage import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.ConsensusSegment.ConsensusMessage.{ @@ -55,7 +56,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.utils.Mi import com.digitalasset.canton.synchronizer.sequencing.sequencer.bftordering.v30 import com.digitalasset.canton.synchronizer.sequencing.sequencer.bftordering.v30.ConsensusMessage as ProtoConsensusMessage import com.digitalasset.canton.tracing.{TraceContext, Traced} -import com.digitalasset.canton.util.{BatchAggregator, FutureUnlessShutdownUtil} +import com.digitalasset.canton.util.BatchAggregator import com.digitalasset.canton.{ProtoDeserializationError, RichGeneratedMessage} import com.google.protobuf.ByteString import slick.jdbc.{GetResult, PositionedResult, SetParameter} @@ -201,22 +202,13 @@ class DbEpochStore( override def completeEpoch( epochNumber: EpochNumber )(implicit traceContext: TraceContext): PekkoFutureUnlessShutdown[Unit] = - createFuture(completeEpochActionName(epochNumber), orderingStage = functionFullName) { - // asynchronously delete all in-progress messages after an epoch ends - FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( - storage - .update_( - sqlu"""delete from ord_pbft_messages_in_progress where epoch_number <= $epochNumber""", - functionFullName, - ), - failureMessage = "could not delete in-progress pbft messages from previous epoch(s)", - ) + createFuture(completeEpochActionName(epochNumber), orderingStage = functionFullName)( // synchronously update the completed epoch to no longer be in progress storage.update_( sqlu"""update ord_epochs set in_progress = false where epoch_number = $epochNumber""", functionFullName, ) - } + ) override def latestEpoch(includeInProgress: Boolean)(implicit traceContext: TraceContext @@ -249,7 +241,7 @@ class DbEpochStore( .query( sql"""select message from ord_pbft_messages_completed pbft_message - where pbft_message.block_number = ${epoch.lastBlockNumber} and pbft_message.discriminator = $CommitMessageDiscriminator + where pbft_message.epoch_number = ${epoch.number} and pbft_message.block_number = ${epoch.lastBlockNumber} and pbft_message.discriminator = $CommitMessageDiscriminator order by pbft_message.from_sequencer_id """.as[SignedMessage[Commit]], functionFullName, @@ -336,7 +328,7 @@ class DbEpochStore( case _: Postgres => """insert into ord_pbft_messages_in_progress(block_number, epoch_number, view_number, message, discriminator, from_sequencer_id) values (?, ?, ?, ?, ?, ?) - on conflict (block_number, view_number, discriminator, from_sequencer_id) do nothing + on conflict (epoch_number, block_number, view_number, discriminator, from_sequencer_id) do nothing """ case _: H2 => """merge into ord_pbft_messages_in_progress @@ -510,10 +502,11 @@ class DbEpochStore( }.map(_.headOption) override def loadOrderedBlocks( - initialBlockNumber: BlockNumber + initialEpochNumber: EpochNumber, + limit: Int, )(implicit traceContext: TraceContext): PekkoFutureUnlessShutdown[Seq[OrderedBlockForOutput]] = createFuture( - loadOrderedBlocksActionName(initialBlockNumber), + loadOrderedBlocksActionName(initialEpochNumber, limit), orderingStage = functionFullName, ) { storage @@ -527,7 +520,8 @@ class DbEpochStore( on epoch.epoch_number = completed_message.epoch_number where completed_message.discriminator = $PrePrepareMessageDiscriminator and - completed_message.block_number >= $initialBlockNumber + completed_message.epoch_number >= $initialEpochNumber and + completed_message.epoch_number < ${initialEpochNumber + limit} order by completed_message.block_number """.as[(PrePrepare, EpochInfo)](tryReadPrePrepareMessageAndEpochInfo), @@ -544,12 +538,29 @@ class DbEpochStore( prePrepare.viewNumber, prePrepare.from, epochInfo.lastBlockNumber == prePrepare.blockMetadata.blockNumber, - OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ) } } } + override def lastEpochWithCompletedBlock(lowerBound: EpochNumber)(implicit + traceContext: TraceContext + ): PekkoFutureUnlessShutdown[Option[EpochNumber]] = + createFuture(lastEpochWithCompletedBlockActionName, orderingStage = functionFullName) { + storage.query( + sql"""select epoch_number + from ord_pbft_messages_completed + where epoch_number >= $lowerBound + order by epoch_number desc + limit 1""" + .as[Long] + .headOption + .map(_.map(EpochNumber(_))), + functionFullName, + ) + } + override def loadNumberOfRecords(implicit traceContext: TraceContext ): PekkoFutureUnlessShutdown[EpochStore.NumberOfRecords] = diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/memory/InMemoryEpochStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/memory/InMemoryEpochStore.scala index 3ebaa72bd4..c2f532ffa8 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/memory/InMemoryEpochStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/memory/InMemoryEpochStore.scala @@ -31,6 +31,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor CommitCertificate, OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.ConsensusSegment.ConsensusMessage import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.ConsensusSegment.ConsensusMessage.{ @@ -300,12 +301,14 @@ abstract class GenericInMemoryEpochStore[E <: Env[E]] } override def loadOrderedBlocks( - initialBlockNumber: BlockNumber + initialEpochNumber: EpochNumber, + limit: Int, )(implicit traceContext: TraceContext): E#FutureUnlessShutdownT[Seq[OrderedBlockForOutput]] = - createFuture(loadOrderedBlocksActionName(initialBlockNumber)) { () => + createFuture(loadOrderedBlocksActionName(initialEpochNumber, limit)) { () => blocks.view - .filter { case (blockNumber, _) => - blockNumber >= initialBlockNumber + .filter { case (_, block) => + val epochNumber = block.prePrepare.message.blockMetadata.epochNumber + epochNumber >= initialEpochNumber && epochNumber < EpochNumber(initialEpochNumber + limit) } .values .foldLeft[Try[Seq[OrderedBlockForOutput]]](Success(Seq.empty)) { @@ -329,7 +332,7 @@ abstract class GenericInMemoryEpochStore[E <: Env[E]] prePrepare.message.viewNumber, prePrepare.from, isBlockLastInEpoch, - OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ) ) case None => @@ -344,6 +347,18 @@ abstract class GenericInMemoryEpochStore[E <: Env[E]] .map(_.sortBy(_.orderedBlock.metadata.blockNumber)) } + override def lastEpochWithCompletedBlock(lowerBound: EpochNumber)(implicit + traceContext: TraceContext + ): E#FutureUnlessShutdownT[Option[EpochNumber]] = + createFuture(lastEpochWithCompletedBlockActionName) { () => + Success( + blocks.view + .map(_._2.prePrepare.message.blockMetadata.epochNumber) + .filter(_ >= lowerBound) + .maxOption + ) + } + @SuppressWarnings(Array("com.digitalasset.canton.ConcurrentMapSize")) override def loadNumberOfRecords(implicit traceContext: TraceContext diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferBehavior.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferBehavior.scala index 42e575ec32..bc8dc46cd6 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferBehavior.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferBehavior.scala @@ -234,16 +234,22 @@ final class StateTransferBehavior[E <: Env[E]]( ) } - case Consensus.NewEpochStored(newEpochInfo, membership, cryptoProvider: CryptoProvider[E]) => + case Consensus.NewEpochStored( + newEpochInfo, + membership, + cryptoProvider: CryptoProvider[E], + ) => // Mainly so that the onboarding state transfer start epoch is not set as the latest completed epoch initially. // A new event can be introduced to avoid branching. if (newEpochInfo != epochState.epoch.info) { - logger.debug( + logger.info( s"$messageType: setting new epoch ${newEpochInfo.number} during $stateTransferType state transfer" ) setNewEpochState(newEpochInfo, membership, cryptoProvider) } + cleanUpPostponedMessageQueue() + stateTransferManager.stateTransferNewEpoch( newEpochInfo.number, membership, @@ -255,6 +261,8 @@ final class StateTransferBehavior[E <: Env[E]]( Consensus.Admin.GetOrderingTopologyResponse( epochState.epoch.info.number, activeTopologyInfo.currentMembership.orderingTopology.nodes, + activeTopologyInfo.currentMembership.leaders, + activeTopologyInfo.currentMembership.blacklistedNodes, activeTopologyInfo.currentMembership.orderingTopology.sequencingParameters, ) ) @@ -317,7 +325,11 @@ final class StateTransferBehavior[E <: Env[E]]( case Failure(exception) => Consensus.ConsensusMessage.AsyncException(exception) case Success(_) => logger.debug(s"$messageType: stored start epoch $startEpochNumber") - Consensus.NewEpochStored(startEpochInfo, membership, cryptoProvider) + Consensus.NewEpochStored( + startEpochInfo, + membership, + cryptoProvider, + ) } } @@ -412,7 +424,11 @@ final class StateTransferBehavior[E <: Env[E]]( logger.debug( s"$messageType: stored completed epoch $currentEpochNumber and new epoch $newEpochNumber" ) - Consensus.NewEpochStored(newEpochInfo, newMembership, newCryptoProvider) + Consensus.NewEpochStored( + newEpochInfo, + newMembership, + newCryptoProvider, + ) } } @@ -463,28 +479,29 @@ final class StateTransferBehavior[E <: Env[E]]( latestCompletedEpoch, sequencerSnapshotAdditionalInfo = None, ) - val consensusBehavior = new IssConsensusModule[E]( - consensusInitialState, - epochStore, - clock, - metrics, - segmentModuleRefFactory, - new RetransmissionsManager[E]( - thisNode, - dependencies.p2pNetworkOut, - abort, - previousEpochsCommitCerts = Map.empty, - metrics, + val consensusBehavior = + new IssConsensusModule[E]( + consensusInitialState, + epochStore, clock, + metrics, + segmentModuleRefFactory, + new RetransmissionsManager[E]( + thisNode, + dependencies.p2pNetworkOut, + abort, + previousEpochsCommitCerts = Map.empty, + metrics, + clock, + loggerFactory, + ), + random, + dependencies, loggerFactory, - ), - random, - dependencies, - loggerFactory, - timeouts, - futurePbftMessageQueue = initialState.pbftMessageQueue, - postponedConsensusMessageQueue = Some(postponedConsensusMessages), - )()(catchupDetector) + timeouts, + futurePbftMessageQueue = initialState.pbftMessageQueue, + postponedConsensusMessageQueue = Some(postponedConsensusMessages), + )()(catchupDetector) context.become(consensusBehavior) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferManager.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferManager.scala index 8b9ae1579b..2f25337eb0 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferManager.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferManager.scala @@ -30,6 +30,7 @@ import com.digitalasset.canton.tracing.{TraceContext, Traced} import com.digitalasset.canton.util.SingleUseCell import com.digitalasset.canton.version.ProtocolVersion +import java.time.Instant import scala.util.{Failure, Random, Success} /** Manages a single state transfer instance in a client role and multiple state transfer instances @@ -47,8 +48,9 @@ class StateTransferManager[E <: Env[E]]( metrics: BftOrderingMetrics, override val loggerFactory: NamedLoggerFactory, )( - private val maybeCustomTimeoutManager: Option[TimeoutManager[E, Consensus.Message[E], String]] = - None + private val maybeCustomTimeoutManager: Option[ + TimeoutManager[E, Consensus.Message[E], String] + ] = None )(implicit synchronizerProtocolVersion: ProtocolVersion, config: BftBlockOrdererConfig, @@ -57,6 +59,9 @@ class StateTransferManager[E <: Env[E]]( private val stateTransferStartEpoch = new SingleUseCell[EpochNumber] + @SuppressWarnings(Array("org.wartremover.warts.Var")) + private var waitingForEpochTransfer: Option[Instant] = None + private val validator = new StateTransferMessageValidator[E](metrics, loggerFactory) private val messageSender = new StateTransferMessageSender[E]( @@ -71,6 +76,7 @@ class StateTransferManager[E <: Env[E]]( loggerFactory, config.epochStateTransferRetryTimeout, timeoutId = "state transfer", + timeoutMetric = None, ) ) @@ -107,7 +113,7 @@ class StateTransferManager[E <: Env[E]]( abort: String => Nothing )(implicit context: E#ActorContextT[Consensus.Message[E]], traceContext: TraceContext): Unit = { if (inStateTransfer) { - logger.debug(s"State transfer requesting new epoch $newEpochNumber") + logger.info(s"State transfer requesting new epoch $newEpochNumber") } else { logger.info(s"Starting onboarding state transfer from epoch $newEpochNumber") initStateTransfer(newEpochNumber)(abort) @@ -122,6 +128,7 @@ class StateTransferManager[E <: Env[E]]( abort: String => Nothing, )(implicit context: E#ActorContextT[Consensus.Message[E]]): Unit = context.withNewTraceContext { implicit traceContext => + waitingForEpochTransfer = Some(Instant.now) val blockTransferRequest = StateTransferMessage.BlockTransferRequest.create(newEpochNumber, membership.myId) messageSender.signMessage(cryptoProvider, blockTransferRequest) { signedMessage => @@ -191,6 +198,7 @@ class StateTransferManager[E <: Env[E]]( ): Unit = { logger.debug(s"State transfer cancelling a timeout for epoch $epochNumber") timeoutManager.cancelTimeout() + emitEpochTransferLatency() } private def handleStateTransferNetworkMessage( @@ -320,6 +328,18 @@ class StateTransferManager[E <: Env[E]]( logger.debug(s"State transfer sending block $blockMetadata to Output") messageSender.sendBlockToOutput(prePrepare, blockLastInEpoch) } + + private def emitEpochTransferLatency(): Unit = { + import metrics.performance.orderingStageLatency.* + val now = Instant.now() + emitOrderingStageLatency( + labels.stage.values.consensus.stateTransfer.TotalEpochTransferLatency, + // Always emit batch wait latency for dashboard clarity, even if 0 + startInstant = waitingForEpochTransfer.orElse(Some(now)), + endInstant = now, + cleanup = () => waitingForEpochTransfer = None, + ) + } } sealed trait StateTransferMessageResult extends Product with Serializable diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferMessageSender.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferMessageSender.scala index 96a55e2929..170a4a76fb 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferMessageSender.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferMessageSender.scala @@ -18,6 +18,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.{ OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.Consensus.StateTransferMessage import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.ConsensusSegment.ConsensusMessage.PrePrepare @@ -110,7 +111,7 @@ final class StateTransferMessageSender[E <: Env[E]]( prePrepare.viewNumber, prePrepare.from, lastInEpoch, - mode = OrderedBlockForOutput.Mode.FromStateTransfer, + OrderingMode.StateTransfer, ) ) ) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModule.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModule.scala index 66369c9cfd..accebd9922 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModule.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModule.scala @@ -7,7 +7,7 @@ import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} import com.digitalasset.canton.sequencing.protocol.AllMembersOfSynchronizer import com.digitalasset.canton.synchronizer.block.BlockFormat import com.digitalasset.canton.synchronizer.block.BlockFormat.OrderedRequest @@ -21,6 +21,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.int } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.HasDelayedInit import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.EpochStoreReader +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.OutputModule.BlocksRecoveredFromConsensusMessages.LoadPoint import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.OutputModule.{ BlocksRecoveredFromConsensusMessages, DefaultRequestInspector, @@ -40,6 +41,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.mod } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.snapshot.SequencerSnapshotAdditionalInfoProvider import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.time.BftTime +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.{ BftNodeId, BlockNumber, @@ -65,6 +67,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor GetAdditionalInfo, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.Output.{ + AddMessageChunkFromRestart, AsyncException, BlockDataFetched, BlockDataStored, @@ -72,6 +75,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor Message, MetadataStoredForNewEpoch, NoTopologyAvailable, + ProcessNewEpochTopologyMessagesIfPossible, SequencerSnapshotMessage, Start, TopologyFetched, @@ -85,6 +89,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.{ BlockSubscription, + CancellableEvent, Env, ModuleRef, PureFun, @@ -95,9 +100,10 @@ import com.digitalasset.canton.version.ProtocolVersion import com.google.common.annotations.VisibleForTesting import io.opentelemetry.api.trace.{Span, Tracer} -import java.time.Instant +import java.time.{Duration, Instant} import java.util.concurrent.atomic.AtomicReference import scala.collection.mutable +import scala.concurrent.duration.DurationInt import scala.util.chaining.scalaUtilChainingOps import scala.util.{Failure, Success} @@ -125,6 +131,7 @@ class OutputModule[E <: Env[E]]( epochChecker: EpochChecker = EpochChecker.DefaultEpochChecker, // For testing // Passed from BftBlockOrderer to allow a near-0 latency `GetTime` implementation private[bftordering] val previousStoredBlock: PreviousStoredBlock = new PreviousStoredBlock, + partitionCreator: Option[(PartitionManager.PartitionCreator[E])] = None, )(implicit override val config: BftBlockOrdererConfig, synchronizerProtocolVersion: ProtocolVersion, @@ -206,7 +213,17 @@ class OutputModule[E <: Env[E]]( private val blockSpanMap: mutable.Map[BlockNumber, (Span, TraceContext)] = mutable.Map() - private val blocksRecoveredFromConsensus = new BlocksRecoveredFromConsensusMessages[E] + @VisibleForTesting + private[output] val blocksRecoveredFromConsensus = + new BlocksRecoveredFromConsensusMessages[E]( + epochStoreReader, + config.outputSizeOfChunkOfEpochsToLoadAtStart, + loggerFactory, + ) + + private var backPressureStartInstant: Option[Instant] = None + + private var backPressureDelayedEvent: Option[CancellableEvent] = None @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) override def receiveInternal(message: Message[E])(implicit @@ -216,6 +233,7 @@ class OutputModule[E <: Env[E]]( message match { case Start => + logger.info("Output module starting initialization") startupState.initialLowerBound.foreach { case (epochNumber, blockNumber) => context .blockingAwait( @@ -225,6 +243,27 @@ class OutputModule[E <: Env[E]]( .fold(error => abort(error), _ => ()) } + startupState.previousBftTimeForOnboarding.foreach { previousBftTime => + val boundaryBlockNumber = startupState.initialHeightToProvide - 1 + if (boundaryBlockNumber >= BlockNumber.First) { + logger.info( + s"Onboarding: persisting boundary block $boundaryBlockNumber with BFT time $previousBftTime " + + "to seed BFT-time computation if we crash and restart within the onboarding start epoch" + ) + context.blockingAwait( + store.insertBlockIfMissing( + OutputBlockMetadata( + epochNumber = + EpochNumber(startupState.initialEpochWeHaveLeaderSelectionStateFor - 1), + blockNumber = BlockNumber(boundaryBlockNumber), + blockBftTime = previousBftTime, + ) + ), + config.blockingDbReadTimeout, + ) + } + } + val lastStoredOutputBlockMetadata = context.blockingAwait( store.getLastNonSequentialBlockMetadataStored, @@ -232,6 +271,13 @@ class OutputModule[E <: Env[E]]( ) val lastStoredBlockNumber = lastStoredOutputBlockMetadata.map(_.blockNumber) + // The durable lower bound is the first block (inclusive) this node ever supports serving, set either by + // pruning or, for an onboarded node, when it was onboarded (see `saveOnboardedNodeLowerBound`). We must + // never try to recover from a block below it: such blocks were either pruned or, in the onboarding case, + // never stored by this node at all (it only ever had blocks from its onboarding height onwards). + val lowerBound = + context.blockingAwait(store.getLowerBound(), config.blockingDbReadTimeout) + // The logic to compute `recoverFromBlockNumber` takes into account the following scenarios: // // - `lastAcknowledgedBlockNumber` is `None` and `lastStoredOutputBlockMetadata` is also `None`: the node is @@ -272,7 +318,7 @@ class OutputModule[E <: Env[E]]( ), ).min - val recoverFromBlockNumber = { + val (recoverFromBlockNumber, startingEpochNumberO) = { val firstBlockO = for { blockMetadata <- context.blockingAwait( store.getBlock(recoverFromBlockNumberThatCouldBeInMiddleOfEpoch), @@ -287,13 +333,26 @@ class OutputModule[E <: Env[E]]( } yield { logger.info( s"Output module bootstrap wanted to recover from block $recoverFromBlockNumberThatCouldBeInMiddleOfEpoch " + + s"= min(ack: $lastAcknowledgedBlockNumber, stored: $lastStoredBlockNumber, leader: ${leaderSelectionPolicy.firstBlockWeNeedToAdd}) " + s"which is in epoch $startEpochNumber; " + s"we adjust to first block of that epoch which is $startBlockNumber}" ) - startBlockNumber + (startBlockNumber, Some(startEpochNumber)) } - firstBlockO.getOrElse(recoverFromBlockNumberThatCouldBeInMiddleOfEpoch) + firstBlockO.getOrElse { + lowerBound + .filter(_.blockNumber > recoverFromBlockNumberThatCouldBeInMiddleOfEpoch) + .map { lb => + logger.info( + s"Output module bootstrap wanted to recover from block $recoverFromBlockNumberThatCouldBeInMiddleOfEpoch " + + s"= min(ack: $lastAcknowledgedBlockNumber, stored: $lastStoredBlockNumber, leader: ${leaderSelectionPolicy.firstBlockWeNeedToAdd}) " + + s"which is not stored; recovering instead from the lower bound block ${lb.blockNumber} in epoch ${lb.epochNumber}" + ) + lb.blockNumber -> Some(lb.epochNumber) + } + .getOrElse(recoverFromBlockNumberThatCouldBeInMiddleOfEpoch -> None) + } } logger.info( @@ -329,17 +388,51 @@ class OutputModule[E <: Env[E]]( logger.info( s"Output module bootstrap: [re-]starting, [re-]processing blocks from $recoverFromBlockNumber" ) + val startEpochNumber = + startingEpochNumberO.getOrElse(EpochNumber.First) + + val targetEpochToLoadO = + context.blockingAwait( + epochStoreReader.lastEpochWithCompletedBlock(startEpochNumber), + config.blockingDbReadTimeout, + ) + logger.info( + "Output module bootstrap: " + { + targetEpochToLoadO match { + case Some(value) => s"[re-]processing blocks up and until epoch $value" + case None => "no previous completed block to reprocess" + } + } + ) val orderedBlocksToProcess = context.blockingAwait( - epochStoreReader.loadOrderedBlocks(recoverFromBlockNumber), + epochStoreReader.loadOrderedBlocks( + startEpochNumber, + config.outputSizeOfChunkOfEpochsToLoadAtStart, + ), config.blockingDbReadTimeout, ) - val startEpochNumber = - orderedBlocksToProcess.headOption // Ordered blocks to process, if any, always start with the recover block - .map(_.orderedBlock.metadata.epochNumber) - .getOrElse(EpochNumber.First) logger.info(s"Output module bootstrap: [re-]starting from epoch $startEpochNumber") blocksRecoveredFromConsensus.addMessages(orderedBlocksToProcess) + + targetEpochToLoadO.foreach { targetEpochToLoad => + val limit = config.outputSizeOfChunkOfEpochsToLoadAtStart + // we pick the next load point to be the halfway point, so when should have already loaded the next chunk + // when output module finish processing the chunk. But we will only have at most 1.5 chunks loaded at any + // given time + val nextWhenToLoad = EpochNumber(startEpochNumber + limit / 2) + val nextWhereToLoadFrom = EpochNumber(startEpochNumber + limit) + if (nextWhereToLoadFrom <= targetEpochToLoad) { + // we only set target if the next load would be before, otherwise all the blocks to be loaded can fit in a + // single chunk and there's no need for further loading + blocksRecoveredFromConsensus.setTargetEpoch( + target = targetEpochToLoad, + nextWhenToLoad = nextWhenToLoad, + nextWhereToLoadFrom = nextWhereToLoadFrom, + ) + } + } + // Rehydrate the transient local state containing the previous stored block information (if any) // to ensure that the BFT time is computed correctly even when restarting blocks with // adjusted BFT time. @@ -354,6 +447,21 @@ class OutputModule[E <: Env[E]]( previousBlock.blockBftTime, ) } + + // The previous block might have been pruned (but in that case we do have the current block we try to recover + // from). It is okay to set initial even if we set the previous, as long as we set one of them (or we are in + // genesis). + context + .blockingAwait( + store.getBlock(BlockNumber(recoverFromBlockNumber)), + config.blockingDbReadTimeout, + ) + .foreach { initialBlock => + previousStoredBlock.setInitial( + initialBlock.blockNumber, + initialBlock.blockBftTime, + ) + } val epochMetadata = context.blockingAwait( store.getEpoch(startEpochNumber), @@ -418,11 +526,28 @@ class OutputModule[E <: Env[E]]( blocksRecoveredFromConsensus.releaseMessagesForEpoch(startEpochNumber) } + scheduleBackpressureCheck(context) initCompleted(receiveInternal) + logger.info("Output module initialization complete, ready to process messages") case _ => ifInitCompleted(message) { case Start => + logger.info( + "Output module received Start message, but initialization is already complete, ignoring" + ) + + case ProcessNewEpochTopologyMessagesIfPossible => + scheduleBackpressureCheck(context) + val isSequencerCoreSlow = blockSubscription.isSequencerCoreSlow + val backpressureBufferSize = blockSubscription.bufferSize + logger.info( + "Checking if sequencer core is still slow or if we can process new epoch topology messages " + + s"(backPressureStartInstant = $backPressureStartInstant, " + + s"from block subscription: isSequencerCoreSlow = $isSequencerCoreSlow, " + + s"bufferSize = $backpressureBufferSize)" + ) + processNewEpochTopologyMessagesIfPossible() // From local consensus case BlockOrdered( @@ -518,13 +643,31 @@ class OutputModule[E <: Env[E]]( // be unable to provide it to us. // We fetch the topology once the last block is stored as, based on the returned topology, the last block // might need to be updated with pending topology changes. - if (orderedBlockData.orderedBlockForOutput.isLastInEpoch) + if (orderedBlockData.orderedBlockForOutput.isLastInEpoch) { fetchNewEpochTopologyIfNeeded( orderedBlockData, orderedBlockBftTime, epochCouldAlterOrderingTopology, ) + partitionCreator.foreach { creator => + context.pipeToSelf(creator.createPartitionsIfNeeded(epochNumber)) { + case Success(partitionsCreated) => + if (partitionsCreated > 0) + logger.info( + s"Created $partitionsCreated partitions at epoch $epochNumber and block $orderedBlockNumber" + ) + None + case Failure(exception) => + logger.error( + s"Failed to create partitions at epoch $epochNumber and block $orderedBlockNumber", + exception, + ) + None + } + } + } + // This is just a defensive check, as the block subscription will have the head correctly set to the // initial height and will ignore blocks before that, but we cannot check nor enforce this assumption // in this module due to the generic Peano queue type needed for simulation testing support. @@ -564,7 +707,7 @@ class OutputModule[E <: Env[E]]( "to sequencer subscription" )(blockTraceContext) - blockSubscription.receiveBlock( + val fullyAssembledBlock = BlockFormat.Block( orderedBlockNumber, orderedBlockBftTime.toMicros, @@ -575,7 +718,8 @@ class OutputModule[E <: Env[E]]( .toMicros ), ) - )(blockTraceContext) + + blockSubscription.receiveBlock(fullyAssembledBlock)(blockTraceContext, mc) } case UpdateLeaderSelection(topologyFetched) => @@ -639,6 +783,9 @@ class OutputModule[E <: Env[E]]( case snapshotMessage: SequencerSnapshotMessage => handleSnapshotMessage(snapshotMessage) + case AddMessageChunkFromRestart(messages) => + blocksRecoveredFromConsensus.addMessages(messages) + case AsyncException(exception) => abort(s"Failed to retrieve new epoch's topology", exception) @@ -647,6 +794,44 @@ class OutputModule[E <: Env[E]]( } } + private def scheduleBackpressureCheck( + context: E#ActorContextT[Message[E]] + )(implicit traceContext: TraceContext): Unit = { + val interval = OutputModule.SequencerCoreSlowCheckInterval + backPressureDelayedEvent.foreach { cancellableEvent => + if (cancellableEvent.cancel()) + logger.debug(s"Backpressure check was already scheduled, cancelled it") + } + logger.info(s"Scheduling backpressure check in $interval") + backPressureDelayedEvent = Some( + context + .delayedEvent( + interval, + ProcessNewEpochTopologyMessagesIfPossible, + ) + ) + } + + private def emitBackpressureMetrics(): Unit = { + val now = Instant.now() + locally { + import metrics.output.* + backPressureStartInstant.fold { + currentSequencerCoreBackpressureDelayMillis.updateValue(0L) + } { startInstant => + currentSequencerCoreBackpressureDelayMillis.updateValue( + Duration.between(startInstant, now).toMillis + ) + } + } + import metrics.performance.orderingStageLatency.* + emitOrderingStageLatency( + labels.stage.values.output.Backpressure, + startInstant = backPressureStartInstant, + endInstant = now, + ) + } + private def processFetchedBlocks()(implicit context: E#ActorContextT[Message[E]], traceContext: TraceContext, @@ -817,7 +1002,7 @@ class OutputModule[E <: Env[E]]( val epochEndBftTime = BftTime.epochEndBftTime(epochLastBlockBftTime, lastBlockInEpoch) - val lastBlockMode = lastBlockForOutput.mode + val lastBlockMode = lastBlockForOutput.orderingMode val newEpochNumber = EpochNumber(completedEpochNumber + 1) maybeNewEpochTopologyMessagePeanoQueue @@ -885,7 +1070,6 @@ class OutputModule[E <: Env[E]]( context: E#ActorContextT[Message[E]], traceContext: TraceContext, ): Unit = { - val orderingTopology = newOrderingTopologyAndCryptoProvider.fold(currentEpochOrderingTopology)(_._1) val newEpochLeaders = leaderSelectionPolicy.getLeaders(orderingTopology, newEpochNumber) @@ -895,6 +1079,10 @@ class OutputModule[E <: Env[E]]( val cryptoProvider = newOrderingTopologyAndCryptoProvider.fold(currentEpochCryptoProvider)(_._2) + if (epochMetadataStored) + setEpochMetadataStoredCache(newEpochNumber) + cleanupEpochMetadataStoredCache(newEpochNumber) + logger.debug( s"Inserting NewEpochTopology message for epoch $newEpochNumber into Peano queue, " + s"(head=$newEpochTopologyMessagePeanoQueue)" @@ -903,48 +1091,103 @@ class OutputModule[E <: Env[E]]( newEpochNumber, Consensus.NewEpochTopology(newEpochNumber, newMembership, cryptoProvider), ) - val newEpochTopologyMessages = newEpochTopologyMessagePeanoQueue.pollAvailable() - logger.debug( - s"Polled NewEpochTopology messages: $newEpochTopologyMessages from Peano queue" - ) - newEpochTopologyMessages.foreach { newEpochTopologyMessage => - // It is safe to use and change epoch-related mutable state in this block because: - // - New epoch messages are processed sequentially and in order. - // - Ordered blocks processing, which uses and changes epoch-related mutable state: - // - Also happens sequentially and in order. - // - Furthermore, only blocks for the current epoch are processed. - val newEpochNumber = newEpochTopologyMessage.epochNumber - logger.debug(s"Setting up new epoch $newEpochNumber") - currentEpochCouldAlterOrderingTopology = false - if (epochMetadataStored) - setEpochMetadataStoredCache(newEpochNumber) - cleanupEpochMetadataStoredCache(newEpochNumber) - processingFetchedBlocksInEpoch = Some(newEpochNumber) - - currentEpochOrderingTopology = newEpochTopologyMessage.membership.orderingTopology - currentEpochCryptoProvider = newEpochTopologyMessage.cryptoProvider - val pendingTopologyChanges = currentEpochOrderingTopology.areTherePendingCantonTopologyChanges - logger.debug( - s"Pending topology changes in new ordering topology = $pendingTopologyChanges" - ) - currentEpochCouldAlterOrderingTopology = pendingTopologyChanges.exists(identity) + processNewEpochTopologyMessagesIfPossible() + } - metrics.topology.validators.updateValue(currentEpochOrderingTopology.nodes.size) + private def processNewEpochTopologyMessagesIfPossible()(implicit + context: E#ActorContextT[Message[E]], + traceContext: TraceContext, + ): Unit = { + emitBackpressureMetrics() + // We check directly the subscriptions state, rather than using a notification mechanism through actor messages, + // because the subscription state update is multithreaded and pause/resume messages may be reordered due + // to thread scheduling, potentially causing a deadlock. + // In addition to reading the state, we also get notified by the subscription when processing may be able to + // be resumed via `ProcessNewEpochTopologyMessagesIfPossible` messages; this allows to always and timely + // resume ordering. + val isSequencerCoreSlow = blockSubscription.isSequencerCoreSlow + val backpressureBufferSize = blockSubscription.bufferSize + if ( + isSequencerCoreSlow && backpressureBufferSize > OutputModule.BackpressureBufferResumeThreshold + ) { + backPressureStartInstant.fold { + logger.info( + s"Not processing new epoch topology messages because the sequencer core is slow to consume blocks " + + s"and the backpressure buffer size is $backpressureBufferSize, " + + s"which is above the resume threshold of ${OutputModule.BackpressureBufferResumeThreshold}" + ) + backPressureStartInstant = Some(Instant.now()) + } { startInstant => + val duration = Duration.between(startInstant, Instant.now()) + logger.info( + s"The sequencer core is still slow after $duration, not processing new epoch topology messages yet" + ) + } + } else { + if (isSequencerCoreSlow) + logger.info( + "The subscription reported that the sequencer core is slow but " + + "the buffer size is below our resume threshold, processing new epoch topology messages regardless" + ) + + if (backPressureStartInstant.isDefined) { + logger.info( + s"The sequencer core has caught up enough, processing new epoch topology messages" + ) + backPressureStartInstant = None + } + + // Not using the accessor because this gets called periodically and may not be set + // for a period of time after init. + val newEpochTopologyMessages = + maybeNewEpochTopologyMessagePeanoQueue.get.fold(Seq.empty[NewEpochTopology[E]])( + _.pollAvailable() + ) logger.debug( - s"Sending topology $currentEpochOrderingTopology of a new epoch $newEpochNumber " + - s"to a consensus behavior (epochLength= ${newEpochTopologyMessage.membership.orderingTopology.epochLength})" + s"Polled NewEpochTopology messages: $newEpochTopologyMessages from Peano queue" ) - consensus.asyncSend(newEpochTopologyMessage) - epochChecker.check( - thisNode, - newEpochNumber, - newEpochTopologyMessage.membership, + logger.info( + s"Processing ${newEpochTopologyMessages.size} new epoch topology messages" ) - blocksRecoveredFromConsensus.releaseMessagesForEpoch(newEpochNumber) - processFetchedBlocks() + newEpochTopologyMessages.foreach { newEpochTopologyMessage => + // It is safe to use and change epoch-related mutable state in this block because: + // - New epoch messages are processed sequentially and in order. + // - Ordered blocks processing, which uses and changes epoch-related mutable state: + // - Also happens sequentially and in order. + // - Furthermore, only blocks for the current epoch are processed. + val newEpochNumber = newEpochTopologyMessage.epochNumber + logger.debug(s"Setting up new epoch $newEpochNumber") + currentEpochCouldAlterOrderingTopology = false + processingFetchedBlocksInEpoch = Some(newEpochNumber) + + currentEpochOrderingTopology = newEpochTopologyMessage.membership.orderingTopology + currentEpochCryptoProvider = newEpochTopologyMessage.cryptoProvider + val pendingTopologyChanges = + currentEpochOrderingTopology.areTherePendingCantonTopologyChanges + logger.debug( + s"Pending topology changes in new ordering topology = $pendingTopologyChanges" + ) + currentEpochCouldAlterOrderingTopology = pendingTopologyChanges.exists(identity) + + metrics.topology.validators.updateValue(currentEpochOrderingTopology.nodes.size) + logger.debug( + s"Sending topology $currentEpochOrderingTopology of a new epoch $newEpochNumber " + + s"to a consensus behavior (epochLength= ${newEpochTopologyMessage.membership.orderingTopology.epochLength})" + ) + + consensus.asyncSend(newEpochTopologyMessage) + epochChecker.check( + thisNode, + newEpochNumber, + newEpochTopologyMessage.membership, + ) + blocksRecoveredFromConsensus.releaseMessagesForEpoch(newEpochNumber) + + processFetchedBlocks() + } } } @@ -1006,9 +1249,16 @@ object OutputModule { private[bftordering] final class PreviousStoredBlock { + private val initialBlockAndBftTimeRef = + new AtomicReference[Option[(BlockNumber, CantonTimestamp)]](None) + private val blockNumberAndBftTimeRef = new AtomicReference[Option[(BlockNumber, CantonTimestamp)]](None) + private[bftordering] def getInitialBlockNumberAndBftTime + : Option[(BlockNumber, CantonTimestamp)] = + initialBlockAndBftTimeRef.get() + private[bftordering] def getBlockNumberAndBftTime: Option[(BlockNumber, CantonTimestamp)] = blockNumberAndBftTimeRef.get() @@ -1018,16 +1268,25 @@ object OutputModule { .map(b => s"(block number = ${b._1}, BFT time = ${b._2})") .getOrElse("undefined") + def setInitial(blockNumber: BlockNumber, blockBftTime: CantonTimestamp): Unit = + initialBlockAndBftTimeRef.set(Some(blockNumber -> blockBftTime)) + @VisibleForTesting private[output] def update(blockNumber: BlockNumber, blockBftTime: CantonTimestamp): Unit = blockNumberAndBftTimeRef.set(Some(blockNumber -> blockBftTime)) private[OutputModule] def computeBlockBftTime(orderedBlock: OrderedBlock): CantonTimestamp = - BftTime.blockBftTime( - orderedBlock.canonicalCommitSet, - previousBlockBftTime = - blockNumberAndBftTimeRef.get().map(_._2).getOrElse(CantonTimestamp.Epoch), - ) + initialBlockAndBftTimeRef + .get() + .filter(_._1 == orderedBlock.metadata.blockNumber) + .map(_._2) + .getOrElse( + BftTime.blockBftTime( + orderedBlock.canonicalCommitSet, + previousBlockBftTime = + blockNumberAndBftTimeRef.get().map(_._2).getOrElse(CantonTimestamp.Epoch), + ) + ) } trait RequestInspector { @@ -1086,10 +1345,39 @@ object OutputModule { result } - class BlocksRecoveredFromConsensusMessages[E <: Env[E]] { + class BlocksRecoveredFromConsensusMessages[E <: Env[E]]( + epochStoreReader: EpochStoreReader[E], + limit: Int, + override val loggerFactory: NamedLoggerFactory, + ) extends NamedLogging { + private val blocksToRelease = mutable.Map.empty[EpochNumber, Seq[OrderedBlockForOutput]] + @SuppressWarnings(Array("org.wartremover.warts.Var")) + private var targetEpochO: Option[EpochNumber] = None + @SuppressWarnings(Array("org.wartremover.warts.Var")) + private var nextLoadPointO: Option[LoadPoint] = None + @SuppressWarnings(Array("org.wartremover.warts.Var")) + private var highestReleaseEpoch: Option[EpochNumber] = None + + def setTargetEpoch( + target: EpochNumber, + nextWhenToLoad: EpochNumber, + nextWhereToLoadFrom: EpochNumber, + ): Unit = { + require(targetEpochO.isEmpty) + require(nextWhenToLoad <= nextWhereToLoadFrom) + require(nextWhereToLoadFrom <= target) + targetEpochO = Some(target) + nextLoadPointO = Some( + LoadPoint(nextWhenToLoad, nextWhereToLoadFrom) + ) + } - def addMessages(messages: Seq[OrderedBlockForOutput]): Unit = { + def addMessages(messages: Seq[OrderedBlockForOutput])(implicit + context: E#ActorContextT[Output.Message[E]], + traceContext: TraceContext, + metricsContext: MetricsContext, + ): Unit = { val epochToMessageMap = messages.groupBy(_.orderedBlock.metadata.epochNumber) epochToMessageMap.foreach { case (epochNumber, orderedBlocks) => blocksToRelease @@ -1099,6 +1387,7 @@ object OutputModule { } .discard } + highestReleaseEpoch.foreach(epochNumber => releaseMessagesForEpoch(epochNumber)) } def releaseMessagesForEpoch( @@ -1113,6 +1402,47 @@ object OutputModule { orderedBlocksToProcess.foreach(orderedBlockForOutput => context.self.asyncSend(BlockOrdered(orderedBlockForOutput)) ) + highestReleaseEpoch = Some(epochNumber) + + nextLoadPointO.foreach { nextLoadPoint => + if (nextLoadPoint.whenToLoad == epochNumber) { + nextLoadPointO = nextLoadPoint.computeNextLoadPoint(targetEpochO, limit) + context.pipeToSelf( + epochStoreReader + .loadOrderedBlocks(nextLoadPoint.loadFrom, limit) + ) { + case Failure(exception) => + logger.error("Could not load blocks", exception) + context.abort(exception) + case Success(value) => + Some(AddMessageChunkFromRestart(value)) + } + } + } + } + + @VisibleForTesting + private[output] def nextLoadPoint: Option[LoadPoint] = nextLoadPointO + } + object BlocksRecoveredFromConsensusMessages { + + private[output] final case class LoadPoint(whenToLoad: EpochNumber, loadFrom: EpochNumber) { + + def computeNextLoadPoint(targetEpochO: Option[EpochNumber], limit: Int): Option[LoadPoint] = { + val nextWhenLoad = bumpWithLimit(whenToLoad, limit) + val nextLoadFrom = bumpWithLimit(loadFrom, limit) + if (targetEpochO.exists(nextLoadFrom <= _)) { + Some(LoadPoint(nextWhenLoad, nextLoadFrom)) + } else { + None + } + } + + private def bumpWithLimit(epochNumber: EpochNumber, limit: Int): EpochNumber = + EpochNumber(epochNumber + limit) } } + + private val SequencerCoreSlowCheckInterval = 10.seconds + private val BackpressureBufferResumeThreshold = 1_000 } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModuleMetrics.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModuleMetrics.scala index 4ed6fe4d69..c35d52d99d 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModuleMetrics.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModuleMetrics.scala @@ -8,7 +8,7 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.synchronizer.metrics.BftOrderingMetrics import com.digitalasset.canton.synchronizer.metrics.BftOrderingMetrics.updateTimer import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.CompleteBlockData -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.OrderedBlockForOutput +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.OrderingMode import java.time.{Duration, Instant} @@ -25,10 +25,10 @@ private[output] object OutputModuleMetrics { val requestsOrdered = requests.length.toLong val batchesOrdered = orderedBlockData.batches.length.toLong val blockMode = - orderedBlockData.orderedBlockForOutput.mode match { - case OrderedBlockForOutput.Mode.FromConsensus => + orderedBlockData.orderedBlockForOutput.orderingMode match { + case OrderingMode.Consensus => metrics.output.labels.mode.values.Consensus - case OrderedBlockForOutput.Mode.FromStateTransfer => + case OrderingMode.StateTransfer => metrics.output.labels.mode.values.StateTransfer } val outputMc = mc.withExtraLabels(metrics.output.labels.mode.Key -> blockMode) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/PekkoBlockSubscription.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/PekkoBlockSubscription.scala index bc3d0aa812..a9baec6d93 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/PekkoBlockSubscription.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/PekkoBlockSubscription.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output +import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.{ @@ -17,19 +18,23 @@ import com.digitalasset.canton.lifecycle.{ } import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} import com.digitalasset.canton.synchronizer.block.BlockFormat +import com.digitalasset.canton.synchronizer.metrics.BftOrderingMetrics import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig.{ DefaultOutputEnqueueMaxRetries, DefaultOutputEnqueueMaxRetryDelay, + DefaultSequencerCoreSubscriptionConfig, + SequencerCoreSubscriptionConfig, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.PekkoBlockSubscription.{ - PekkoQueueSourceBufferSize, RetryPolicy, State, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.BlockNumber +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.Output import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.{ BlockSubscription, Env, + ModuleRef, } import com.digitalasset.canton.tracing.{TraceContext, Traced} import com.digitalasset.canton.util.Thereafter.syntax.ThereafterAsyncOps @@ -52,12 +57,16 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} import scala.collection.immutable.Queue import scala.concurrent.ExecutionContext import scala.concurrent.duration.* -import scala.util.{Failure, Success} +import scala.util.{Failure, Success, Try} class PekkoBlockSubscription[E <: Env[E]]( initialHeight: BlockNumber, + getOutputModule: () => ModuleRef[Output.ProcessNewEpochTopologyMessagesIfPossible.type], override val timeouts: ProcessingTimeout, override val loggerFactory: NamedLoggerFactory, + metrics: BftOrderingMetrics, + sequencerCoreSubscriptionConfig: SequencerCoreSubscriptionConfig = + DefaultSequencerCoreSubscriptionConfig, maxRetries: Int = DefaultOutputEnqueueMaxRetries, maxRetryDelay: FiniteDuration = DefaultOutputEnqueueMaxRetryDelay, )(abort: String => Nothing)(implicit @@ -78,7 +87,10 @@ class PekkoBlockSubscription[E <: Env[E]]( ) val queueSource = Source - .queue[Traced[BlockFormat.Block]](PekkoQueueSourceBufferSize, OverflowStrategy.backpressure) + .queue[Traced[BlockFormat.Block]]( + sequencerCoreSubscriptionConfig.pekkoQueueSourceBufferSize, + OverflowStrategy.backpressure, + ) // Normally we'd simply call queueSource.preMaterialize() in order to materialize the queue from here. // We need to do that because we don't have access to the materialized values of the stream that uses // the source returned by subscription() but we need to have access to the queue that gets materialized @@ -104,12 +116,17 @@ class PekkoBlockSubscription[E <: Env[E]]( val blocksPeanoQueue = new PeanoQueue[BlockNumber, Traced[BlockFormat.Block]](initialHeight)(abort) block => { + emitBufferSizeGauge() val blockHeight = block.value.blockHeight logger.debug( s"Inserting block $blockHeight into subscription Peano queue (head=${blocksPeanoQueue.head})" )(block.traceContext) blocksPeanoQueue.insert(BlockNumber(blockHeight), block) - blocksPeanoQueue.pollAvailable() + val polled = blocksPeanoQueue.pollAvailable() + logger.debug( + s"Polled ${polled.size} blocks from subscription Peano queue, new head=${blocksPeanoQueue.head}" + )(block.traceContext) + polled } } .viaMat(KillSwitches.single)(Keep.right) @@ -117,7 +134,7 @@ class PekkoBlockSubscription[E <: Env[E]]( override def receiveBlock( block: BlockFormat.Block - )(implicit traceContext: TraceContext): Unit = { + )(implicit traceContext: TraceContext, metricsContext: MetricsContext): Unit = { val height = block.blockHeight logger.debug( @@ -130,6 +147,8 @@ class PekkoBlockSubscription[E <: Env[E]]( synchronizeWithClosingSync("DABFT enqueue block to sequencer core")( advance(newTracedBlockO = Some(Traced(block))) ).discard + + emitBufferSizeGauge() } override def closeAsync(): Seq[AsyncOrSyncCloseable] = { @@ -145,58 +164,128 @@ class PekkoBlockSubscription[E <: Env[E]]( .toList } + override def isSequencerCoreSlow: Boolean = + stateRef.get().sequencerCoreIsSlow + + override def bufferSize: Int = + stateRef.get().blocksToEnqueue.size + + private def emitBufferSizeGauge(): Unit = + metrics.output.sequencerCoreSubscriptionBufferSize.updateValue( + stateRef.get().blocksToEnqueue.size + ) + // Advances the enqueueing state and starts the next enqueueing task private def advance( newTracedBlockO: Option[Traced[BlockFormat.Block]] = None, taskComplete: Boolean = false, - ): Unit = { + )(implicit traceContext: TraceContext, metricsContext: MetricsContext): Unit = { // Create the promise outside the CAS block, rather than inside it, in order to avoid // registering untriggerable futures with the execution context when CAS fails due contention, // which may constitute a memory leak. - val promise = PromiseUnlessShutdown.unsupervised[Unit]() + val enqueuePekkoSourcePromise = PromiseUnlessShutdown.unsupervised[Unit]() val enqueuedInPekkoQueueSource = new AtomicBoolean(false) - AtomicUtil - .updateAndGetComputed(stateRef) { case State(blocksToEnqueue, taskO) => - // noinspection ConvertibleToMethodValue - val updateBlocksToEnqueue = - newTracedBlockO.fold(blocksToEnqueue)(blocksToEnqueue.enqueue(_)) - - if (taskO.isEmpty || taskComplete) { - val (tracedBlockToEnqueueO, restOfBlocksToEnqueue) = - updateBlocksToEnqueue.dequeueOption.fold( - Option.empty[Traced[BlockFormat.Block]] -> Queue.empty[Traced[BlockFormat.Block]] - ) { case (tracedBlock, restOfBlocks) => - Some(tracedBlock) -> restOfBlocks - } - State( - restOfBlocksToEnqueue, - tracedBlockToEnqueueO.map(tracedBlockToEnqueue => - // When CAS fails due to contention, the promise's continuation future is registered and triggered - // multiple times for the same block height; the `enqueuedInPekkoQueueSource` atomic flag ensures - // that only one of those continuations will actually trigger the enqueueing of the block - // to the Pekko queue, while the others will be no-ops. - // While repeated inserts, even if not in order, would be OK logically because Peano queue - // insertions downstream are idempotent, we must avoid concurrent enqueue calls into to the - // Pekko queue source, which would fail due to either the max insertion concurrency (set to 1) - // or due to internal synchronization of the Pekko queue source. - promise.futureUS.flatMap { _ => - if (enqueuedInPekkoQueueSource.compareAndSet(false, true)) - pekkoEnqueue(tracedBlockToEnqueue) - else - FutureUnlessShutdown.pure(QueueOfferResult.Enqueued) + val (startPekkoEnqueueIfNeeded, resumeIfPossible) = + AtomicUtil + .updateAndGetComputed(stateRef) { case State(blocksToEnqueue, taskO, sequencerCoreIsSlow) => + // noinspection ConvertibleToMethodValue + val updatedBlocksToEnqueue = + newTracedBlockO.fold(blocksToEnqueue)(blocksToEnqueue.enqueue(_)) + + logger.trace( + s"updatedBlocksToEnqueue: size=${updatedBlocksToEnqueue.size}, head=${updatedBlocksToEnqueue.headOption + .map(_.value.blockHeight)}" + ) + + val updatedSequencerCoreIsSlow = + if ( + updatedBlocksToEnqueue.sizeIs > sequencerCoreSubscriptionConfig.pauseOrdererThresholdBufferSize + ) + true + else if ( + updatedBlocksToEnqueue.sizeIs <= sequencerCoreSubscriptionConfig.resumeOrdererThresholdBufferSize + ) + false + else sequencerCoreIsSlow + + logger.trace( + s"updatedSequencerCoreIsSlow: $updatedSequencerCoreIsSlow (was $sequencerCoreIsSlow)" + ) + + val resumeIfPossible = sequencerCoreIsSlow && !updatedSequencerCoreIsSlow + + logger.trace(s"resumeIfPossible: $resumeIfPossible") + + if (taskO.isEmpty || taskComplete) { + logger.trace(s"Pekko enqueue completed") + + val (tracedBlockToEnqueueO, restOfBlocksToEnqueue) = + updatedBlocksToEnqueue.dequeueOption.fold( + Option.empty[Traced[BlockFormat.Block]] -> Queue.empty[Traced[BlockFormat.Block]] + ) { case (tracedBlock, restOfBlocks) => + Some(tracedBlock) -> restOfBlocks } - ), - ) -> Some(() => promise.outcome_(())) - } else { - State(updateBlocksToEnqueue, taskO) -> None + + tracedBlockToEnqueueO.fold(logger.trace(s"No block to enqueue to Pekko queue source")) { + tracedBlockToEnqueue => + logger.trace( + s"Next block to enqueue to Pekko queue source: ${tracedBlockToEnqueue.value.blockHeight}" + ) + } + + val updatedState = + State( + restOfBlocksToEnqueue, + tracedBlockToEnqueueO.map(tracedBlockToEnqueue => + // When CAS fails due to contention, the promise's continuation future is registered and triggered + // multiple times for the same block height; the `enqueuedInPekkoQueueSource` atomic flag ensures + // that only one of those continuations will actually trigger the enqueueing of the block + // to the Pekko queue, while the others will be no-ops. + // While repeated inserts, even if not in order, would be OK logically because Peano queue + // insertions downstream are idempotent, we must avoid concurrent enqueue calls into to the + // Pekko queue source, which would fail due to either the max insertion concurrency (set to 1) + // or due to internal synchronization of the Pekko queue source. + enqueuePekkoSourcePromise.futureUS.flatMap { _ => + if (enqueuedInPekkoQueueSource.compareAndSet(false, true)) { + pekkoEnqueue(tracedBlockToEnqueue) + } else + FutureUnlessShutdown.pure(QueueOfferResult.Enqueued) + } + ), + updatedSequencerCoreIsSlow, + ) + updatedState -> ((() => enqueuePekkoSourcePromise.outcome_(())), resumeIfPossible) + } else { + val updatedState = State(updatedBlocksToEnqueue, taskO, updatedSequencerCoreIsSlow) + updatedState -> ((() => ()), resumeIfPossible) + } } + + startPekkoEnqueueIfNeeded() + + if (resumeIfPossible) { + Try( + getOutputModule().asyncSend(Output.ProcessNewEpochTopologyMessagesIfPossible)( + traceContext, + metricsContext, + ) + ) match { + case Failure(exception) => + logger.error( + "Failed to send ProcessNewEpochTopologyMessagesIfPossible message to output module", + exception, + ) + case Success(value) => + logger.info( + "Detected that sequencer core has caught up, notified the output module to check if it should resume" + ) } - .foreach(_()) // Start the next enqueueing task if it exists + } } private def pekkoEnqueue( tracedBlockToEnqueue: Traced[BlockFormat.Block] - ): FutureUnlessShutdown[QueueOfferResult] = { + )(implicit metricsContext: MetricsContext): FutureUnlessShutdown[QueueOfferResult] = { implicit val success: retry.Success[QueueOfferResult] = retry.Success.always val height = tracedBlockToEnqueue.value.blockHeight locally { @@ -261,14 +350,14 @@ class PekkoBlockSubscription[E <: Env[E]]( object PekkoBlockSubscription { - private val PekkoQueueSourceBufferSize = 5000 - /** The state of the subscription, which consists of the blocks that are waiting to be enqueued to - * the sequencer core via the Pekko queue source, and the current enqueueing task, if it exists. + * the sequencer core via the Pekko queue source, the current enqueueing task, if it exists, and + * whether the buffer with the sequencer core grew too large. */ private final case class State( blocksToEnqueue: Queue[Traced[BlockFormat.Block]] = Queue.empty, runningEnqueueTask: Option[FutureUnlessShutdown[QueueOfferResult]] = None, + sequencerCoreIsSlow: Boolean = false, ) private object RetryPolicy extends ExceptionRetryPolicy { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/db/DbOutputMetadataStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/db/DbOutputMetadataStore.scala index da4d8c47fc..9b903e1598 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/db/DbOutputMetadataStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/db/DbOutputMetadataStore.scala @@ -8,6 +8,8 @@ import com.daml.nameof.NameOf.functionFullName import com.digitalasset.canton.concurrent.DirectExecutionContext import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.resource.DbStorage.DbAction.ReadOnly import com.digitalasset.canton.resource.DbStorage.Profile.{H2, Postgres} @@ -27,6 +29,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor import com.digitalasset.canton.tracing.TraceContext import slick.jdbc.{GetResult, SetParameter} +import java.util.concurrent.atomic.AtomicReference import scala.concurrent.ExecutionContext class DbOutputMetadataStore( @@ -44,6 +47,14 @@ class DbOutputMetadataStore( private val profile = storage.profile private val converters = storage.converters + private val lastBlockStoredCache = new AtomicReference[Option[OutputBlockMetadata]](None) + private def updateLastBlockStoredCache(latest: OutputBlockMetadata): Unit = + lastBlockStoredCache.getAndUpdate { + case None => Some(latest) + case Some(current) if (latest.blockNumber) > current.blockNumber => Some(latest) + case other => other + }.discard + private implicit val readBlock: GetResult[OutputBlockMetadata] = GetResult { r => OutputBlockMetadata( @@ -103,7 +114,7 @@ class DbOutputMetadataStore( ${metadata.blockNumber}, ${metadata.blockBftTime} ) - on conflict (block_number) do nothing""" + on conflict (epoch_number, block_number) do nothing""" case _: H2 => sqlu"""merge into ord_metadata_output_blocks using dual on ( @@ -122,7 +133,8 @@ class DbOutputMetadataStore( ${metadata.blockBftTime} )""" } - val future = () => storage.update_(query, functionFullName) + val future = () => + storage.update_(query, functionFullName).map(_ => updateLastBlockStoredCache(metadata)) PekkoFutureUnlessShutdown(name, future, orderingStage = Some(functionFullName)) } @@ -301,7 +313,17 @@ class DbOutputMetadataStore( limit 1 """.as[OutputBlockMetadata].headOption } yield lastBlockStored - val future = () => storage.query(query, functionFullName) + val future = () => + lastBlockStoredCache.get() match { + case None => + storage + .query(query, functionFullName) + .map { latest => + latest.foreach(updateLastBlockStoredCache) + latest + } + case lastBlock => FutureUnlessShutdown.pure(lastBlock) + } PekkoFutureUnlessShutdown( lastNonSequentialBlockMetadataStoredName, future, diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/memory/InMemoryOutputMetadataStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/memory/InMemoryOutputMetadataStore.scala index aa839ff6be..dd49e20bfe 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/memory/InMemoryOutputMetadataStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/memory/InMemoryOutputMetadataStore.scala @@ -19,6 +19,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor EpochNumber, } import com.digitalasset.canton.tracing.TraceContext +import com.google.common.annotations.VisibleForTesting import java.util.concurrent.atomic.AtomicReference import scala.collection.concurrent.TrieMap @@ -252,6 +253,8 @@ abstract class GenericInMemoryOutputMetadataStore[E <: Env[E]] extends OutputMet ): E#FutureUnlessShutdownT[Option[OutputMetadataStore.LowerBound]] = createFuture(getLowerBoundActionName)(() => Success(lowerBound.get())) + @VisibleForTesting + def lowerBoundForTesting(): Option[OutputMetadataStore.LowerBound] = lowerBound.get() def latestBlock(): Option[BlockNumber] = blocks.keySet.maxOption } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionInitializer.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionInitializer.scala index a4c79885ee..6d7890424f 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionInitializer.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionInitializer.scala @@ -51,7 +51,7 @@ class BlacklistLeaderSelectionInitializer[E <: Env[E]]( state: BlacklistLeaderSelectionPolicyState, orderingTopology: OrderingTopology, ): Seq[BftNodeId] = - BlacklistLeaderSelectionPolicyStateWithTopology(state, orderingTopology) + BlacklistLeaderSelectionPolicyStateWithTopology(state, orderingTopology, protocolVersion) .computeLeaders() def blacklistedNodesFromState( @@ -61,6 +61,7 @@ class BlacklistLeaderSelectionInitializer[E <: Env[E]]( BlacklistLeaderSelectionPolicyStateWithTopology( state, orderingTopology, + protocolVersion, ).computeBlacklistedNodes() def leaderSelectionPolicy( @@ -69,6 +70,7 @@ class BlacklistLeaderSelectionInitializer[E <: Env[E]]( ): LeaderSelectionPolicy[E] = BlacklistLeaderSelectionPolicy.create( blacklistLeaderSelectionPolicyState, orderingTopology, + protocolVersion, store, metrics, loggerFactory, diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicy.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicy.scala index 66b51bbd0e..579b87c13f 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicy.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicy.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor FutureContext, } import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.version.ProtocolVersion import scala.collection.mutable @@ -22,6 +23,7 @@ import scala.collection.mutable class BlacklistLeaderSelectionPolicy[E <: Env[E]]( initialState: BlacklistLeaderSelectionPolicyState, initialOrderingTopology: OrderingTopology, + protocolVersion: ProtocolVersion, store: OutputMetadataStore[E], metrics: BftOrderingMetrics, override val loggerFactory: NamedLoggerFactory, @@ -30,7 +32,11 @@ class BlacklistLeaderSelectionPolicy[E <: Env[E]]( with NamedLogging { private var state = - BlacklistLeaderSelectionPolicyStateWithTopology(initialState, initialOrderingTopology) + BlacklistLeaderSelectionPolicyStateWithTopology( + initialState, + initialOrderingTopology, + protocolVersion, + ) private var blockToLeader: Map[BlockNumber, BftNodeId] = state.computeBlockToLeader() @@ -161,6 +167,7 @@ object BlacklistLeaderSelectionPolicy { def create[E <: Env[E]]( state: BlacklistLeaderSelectionPolicyState, orderingTopology: OrderingTopology, + protocolVersion: ProtocolVersion, store: OutputMetadataStore[E], metrics: BftOrderingMetrics, loggerFactory: NamedLoggerFactory, @@ -168,6 +175,7 @@ object BlacklistLeaderSelectionPolicy { new BlacklistLeaderSelectionPolicy( state, orderingTopology, + protocolVersion, store, metrics, loggerFactory, diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyState.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyState.scala index ebe7469650..98cef2bf37 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyState.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyState.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.mod import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.{ BftNodeId, BlockNumber, + EpochLength, EpochNumber, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.OrderingTopology @@ -30,6 +31,7 @@ import scala.collection.immutable.SortedSet final case class BlacklistLeaderSelectionPolicyStateWithTopology( state: BlacklistLeaderSelectionPolicyState, topology: OrderingTopology, + protocolVersion: ProtocolVersion, ) { def epochNumber: EpochNumber = state.epochNumber def startBlock: BlockNumber = state.startBlock @@ -48,12 +50,9 @@ final case class BlacklistLeaderSelectionPolicyStateWithTopology( ): BlacklistLeaderSelectionPolicyStateWithTopology = { val newBlacklist = updateBlacklist(newTopology, blockToLeader, nodesToPunish) BlacklistLeaderSelectionPolicyStateWithTopology( - BlacklistLeaderSelectionPolicyState.create( - EpochNumber(epochNumber + 1), - BlockNumber(startBlock + topology.epochLength), - newBlacklist, - )(state.representativeProtocolVersion.representative), + state.update(topology.epochLength, newBlacklist, protocolVersion), newTopology, + protocolVersion, ) } private def updateBlacklist( @@ -124,6 +123,17 @@ final case class BlacklistLeaderSelectionPolicyState( blacklist.view.mapValues(_.toProto30).toMap, ) + def update( + epochLength: EpochLength, + newBlacklist: Blacklist, + protocolVersion: ProtocolVersion, + ): BlacklistLeaderSelectionPolicyState = + BlacklistLeaderSelectionPolicyState.create( + EpochNumber(epochNumber + 1), + BlockNumber(startBlock + epochLength), + newBlacklist, + )(protocolVersion) + override protected val companionObj: BlacklistLeaderSelectionPolicyState.type = BlacklistLeaderSelectionPolicyState } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModule.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModule.scala index d3289dbeb7..1d05772f13 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModule.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModule.scala @@ -89,8 +89,10 @@ final class P2PNetworkOutModule[ _.asyncSend(P2PNetworkOut.Network.Authenticated(bftNodeId, maybeP2PEndpoint)) ) - override def onConnect(p2pEndpointId: P2PEndpoint.Id)(implicit traceContext: TraceContext): Unit = - state.maybeSelf.foreach(_.asyncSend(P2PNetworkOut.Network.Connected(p2pEndpointId))) + override def onConnect(maybeP2pEndpointId: Option[P2PEndpoint.Id])(implicit + traceContext: TraceContext + ): Unit = + state.maybeSelf.foreach(_.asyncSend(P2PNetworkOut.Network.Connected(maybeP2pEndpointId))) override def onDisconnect(p2pEndpointId: P2PEndpoint.Id)(implicit traceContext: TraceContext @@ -117,9 +119,12 @@ final class P2PNetworkOutModule[ logger.info("Disconnecting from operator-removed endpoint " + p2pEndpointId) disconnect(p2pEndpointId) - case P2PNetworkOut.Network.Connected(p2pEndpointId) => - if (connectedP2PEndpointIds.add(p2pEndpointId)) { - logger.info(s"P2P endpoint $p2pEndpointId is now connected") + case P2PNetworkOut.Network.Connected(maybeP2pEndpointId) => + if (maybeP2pEndpointId.forall(connectedP2PEndpointIds.add)) { + logger.info( + s"P2P endpoint ${maybeP2pEndpointId.map(_.toString).getOrElse("")} " + + s"is now connected" + ) emitConnectionStateMetricsAndLogEndpointsStatus(notifyMempool = false) } @@ -457,18 +462,31 @@ final class P2PNetworkOutModule[ mc: MetricsContext, traceContext: TraceContext, ): Unit = { - emitConnectedCount(metrics, connectedP2PEndpointIds.size) + val status = getStatus() + val connectedCount = status.endpointStatuses.count { + case PeerConnectionStatus.PeerEndpointStatus( + _, + _, + PeerEndpointHealth( + PeerEndpointHealthStatus.Authenticated(_) | PeerEndpointHealthStatus.Unauthenticated, + _, + ), + ) => + true + case PeerConnectionStatus.PeerIncomingConnection(_) => true + case _ => false + } + emitConnectedCount(metrics, connectedCount) emitAuthenticatedCount(metrics, p2pConnectionState.authenticatedCount.value) - logEmitForwardP2PStatus(notifyMempool) + logEmitForwardP2PStatus(status, notifyMempool) } - private def logEmitForwardP2PStatus(notifyMempool: Boolean)(implicit - context: E#ActorContextT[P2PNetworkOut.Message], - traceContext: TraceContext, - ): Unit = { + private def logEmitForwardP2PStatus( + status: SequencerBftAdminData.PeerNetworkStatus, + notifyMempool: Boolean, + )(implicit traceContext: TraceContext): Unit = { if (notifyMempool) sendConnectivityUpdateToMempool() - val status = getStatus() metrics.p2p.update(status) logger.info(s"P2P endpoints status: $status") } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionManager.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionManager.scala new file mode 100644 index 0000000000..53d20d5c84 --- /dev/null +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionManager.scala @@ -0,0 +1,475 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning + +import cats.data.OptionT +import com.daml.nameof.NameOf.functionFullName +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown} +import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.resource.{DbStorage, DbStore, Storage} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.pekko.PekkoModuleSystem.{ + PekkoEnv, + PekkoFutureUnlessShutdown, +} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.OutputMetadataStore.OutputBlockMetadata +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.Env +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.EpochNumber +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.OrderingRequestBatch +import com.digitalasset.canton.tracing.TraceContext +import com.google.common.annotations.VisibleForTesting +import slick.jdbc.GetResult + +import scala.concurrent.ExecutionContext + +object PartitionManager { + trait PartitionCreator[E <: Env[E]] extends FlagCloseable { + def createPartitionsIfNeeded(epochNumber: EpochNumber)(implicit + traceContext: TraceContext + ): E#FutureUnlessShutdownT[Int] + } + + trait PartitionPruner[E <: Env[E]] extends FlagCloseable { + def prune(epochNumberInclusive: EpochNumber, latestCompletedEpochNumber: EpochNumber)(implicit + traceContext: TraceContext + ): E#FutureUnlessShutdownT[String] + } + + def create(storage: Storage, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory)( + implicit ec: ExecutionContext + ): PekkoFutureUnlessShutdown[Option[(PartitionCreator[PekkoEnv], PartitionPruner[PekkoEnv])]] = + PekkoFutureUnlessShutdown( + functionFullName, + () => + (for { + dbStorage <- OptionT.fromOption[FutureUnlessShutdown]( + Some(storage) + .collect { case dbStorage: DbStorage => (dbStorage, dbStorage.profile) } + .collect { case (dbStorage, _: DbStorage.Profile.Postgres) => dbStorage } + ) + initStore = new PruningManagerInitStore(dbStorage, timeouts, loggerFactory) + ((minPartitionNumbers, maxPartitionNumbers), partitionSize) <- + TraceContext.withNewTraceContext("initialize partition management")( + implicit traceContext => + OptionT.liftF(for { + maps <- initStore.queryLowestAndHighestExistingPartitionNumberPerTableName + partitionSize <- initStore.latestPartitionSizeEntry + } yield (maps, partitionSize)) + ) + } yield ( + new PartitionManager.PartitionCreatorImpl( + dbStorage, + timeouts, + loggerFactory, + maxPartitionNumbers, + partitionSize, + ), + new PartitionManager.PartitionPrunerImpl( + dbStorage, + timeouts, + loggerFactory, + minPartitionNumbers, + partitionSize, + ), + )).value, + orderingStage = Some(functionFullName), + ) + + private[pruning] val outputBlocksTable = "ord_metadata_output_blocks" + private[pruning] val consensusInProgressTable = "ord_pbft_messages_in_progress" + private[pruning] val epochPartitionedTables = + Seq( + "ord_metadata_output_epochs", + outputBlocksTable, + "ord_leader_selection_state", + "ord_epochs", + "ord_pbft_messages_completed", + consensusInProgressTable, + ) + private[pruning] val batchesTable = "ord_availability_batch" + + private[pruning] final case class Partition( + partitionName: String, + tableName: String, + from: Long, + to: Long, + ) + private sealed trait OutputBlockPartitionExclusionConstraint { + def blockMetadata: OutputBlockMetadata + def partitionNumber: Long + } + private final case class LowerBoundExclusionConstraint( + blockMetadata: OutputBlockMetadata, + partitionNumber: Long, + ) extends OutputBlockPartitionExclusionConstraint + private final case class UpperBoundExclusionConstraint( + blockMetadata: OutputBlockMetadata, + partitionNumber: Long, + ) extends OutputBlockPartitionExclusionConstraint + + private[pruning] final case class HighestCreatedPartitionNumbers( + partitionNumber: Long, + batchTablePartitionNumber: Long, + ) + private[pruning] final case class PartitionSizeEntry( + epochNumber: EpochNumber, + partitionNumber: Long, + partitionSize: Int, + ) + + @VisibleForTesting + private[pruning] def newPartitions( + partitionSize: Int, + newEpochNumber: EpochNumber, + maxEpochPartitionNumbers: HighestCreatedPartitionNumbers, + ): (Seq[Partition], HighestCreatedPartitionNumbers) = { + def partition(tableName: String, partitionNumber: Long) = { + val partitionName = s"${tableName}_p$partitionNumber" + val newPartitionFrom = partitionNumber * partitionSize + val newPartitionTo = (partitionNumber + 1) * partitionSize + Partition(partitionName, tableName, newPartitionFrom, newPartitionTo) + } + + // we create partitions one step (partition size) ahead + val nextEpochPartitionNumber = (newEpochNumber + partitionSize) / partitionSize + // we have to support storing batches of epoch number twice the batch validity ahead, so + // the step will be the max between that and the partition size + val nextBatchesTableEpochPartitionNumber = { + val step = Math.max(2 * OrderingRequestBatch.BatchValidityDurationEpochs, partitionSize) + (newEpochNumber + step) / partitionSize + } + + val partitions = (for { + partitionNumber <- (maxEpochPartitionNumbers.partitionNumber + 1) to nextEpochPartitionNumber + tableName <- epochPartitionedTables + } yield partition(tableName, partitionNumber)) + + val batchesPartitions = (for { + partitionNumber <- + (maxEpochPartitionNumbers.batchTablePartitionNumber + 1) to nextBatchesTableEpochPartitionNumber + } yield partition(batchesTable, partitionNumber)) + + ( + (partitions ++ batchesPartitions).sortBy(_.from), + HighestCreatedPartitionNumbers( + Math.max(maxEpochPartitionNumbers.partitionNumber, nextEpochPartitionNumber), + Math.max( + maxEpochPartitionNumbers.batchTablePartitionNumber, + nextBatchesTableEpochPartitionNumber, + ), + ), + ) + } + + private class PartitionCreatorImpl( + val storage: DbStorage, + val timeouts: ProcessingTimeout, + val loggerFactory: NamedLoggerFactory, + partitionNumberMap: Map[String, Long] = Map.empty, + partitionSizeEntry: PartitionSizeEntry, + )(implicit ec: ExecutionContext) + extends PartitionCreator[PekkoEnv] + with DbStore { + + import storage.api.* + + private val partitionSize = partitionSizeEntry.partitionSize + + @SuppressWarnings(Array("org.wartremover.warts.Var")) + var highestEpochPartitionNumbers = HighestCreatedPartitionNumbers( + (partitionNumberMap - batchesTable).values.minOption.getOrElse(-1), + partitionNumberMap.getOrElse(batchesTable, -1), + ) + + override def createPartitionsIfNeeded(newEpochNumber: EpochNumber)(implicit + traceContext: TraceContext + ): PekkoFutureUnlessShutdown[Int] = { + val (partitions, newMaxEpochPartitionNumber) = + newPartitions(partitionSize, newEpochNumber, highestEpochPartitionNumbers) + + def createPartitionDbIo(partition: Partition) = partition match { + case Partition(partitionName, tableName, newPartitionFrom, newPartitionTo) => + sqlu"""create table if not exists #$partitionName partition of #$tableName for values from (#$newPartitionFrom) to (#$newPartitionTo);""" + } + + PekkoFutureUnlessShutdown( + "createPartitionsIfNeeded", + () => + for { + partitionsCreated <- + if (partitions.nonEmpty) { + logger.info( + s"About to create ${partitions.size} partitions for epoch partition number ${highestEpochPartitionNumbers.partitionNumber} at epoch $newEpochNumber" + ) + storage + .update_( + DBIO.sequence(partitions.map(createPartitionDbIo)).transactionally, + functionFullName, + ) + .map { _ => + highestEpochPartitionNumbers = newMaxEpochPartitionNumber + partitions.size + } + } else FutureUnlessShutdown.pure(0) + _ <- maybeCreateExclusionConstraint(newEpochNumber) + } yield partitionsCreated, + orderingStage = Some(functionFullName), + ) + } + + private val outputStore = new DbOutputMetadataStore(storage, timeouts, loggerFactory) + + // Adding exclusion constraints to the output blocks table will help with the queries by block number + // and by bft timestamp that do not include epoch number in the where clause. In those cases + // all partitions would have to be scanned but by having the constraints, the query planner is able + // to exclude block numbers and bft timestamps outside of the constraints definitions. + // We add both lower bound and upper bound constraints on the first and last blocks from each partition. + // Postgres doc: https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-CONSTRAINT-EXCLUSION + private def maybeCreateExclusionConstraint(newEpochNumber: EpochNumber)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Unit] = + for { + exclusionConstraint <- computeExclusionConstraint(newEpochNumber).futureUnlessShutdown() + _ <- exclusionConstraint + .map { constraint => + val partitionName = s"${outputBlocksTable}_p${constraint.partitionNumber}" + val blockMetadata = constraint.blockMetadata + val (constraintName, ineq, name) = constraint match { + case _: LowerBoundExclusionConstraint => (s"${partitionName}_lb", ">=", "lower") + case _: UpperBoundExclusionConstraint => (s"${partitionName}_ub", "<=", "upper") + } + logger.info( + s"About to add $name bound exclusion constraint to partition ${constraint.partitionNumber} of output blocks table for blocks $ineq ${blockMetadata.blockNumber}" + ) + val update = for { + exists <- + sql"""select exists (select 1 from pg_constraint where conname = '#$constraintName' and conrelid = '#$partitionName'::regclass)""" + .as[Boolean] + _ <- + if (!exists.forall(identity)) sqlu"""alter table #$partitionName + add constraint #$constraintName + check (bft_ts #$ineq #${blockMetadata.blockBftTime.toMicros} and block_number #$ineq #${blockMetadata.blockNumber}) + """ + else sqlu"" + } yield () + storage.queryAndUpdate(update, functionFullName).map(_ => ()) + } + .getOrElse(FutureUnlessShutdown.unit) + } yield () + + private def computeExclusionConstraint(newEpochNumber: EpochNumber)(implicit + traceContext: TraceContext + ): PekkoFutureUnlessShutdown[Option[OutputBlockPartitionExclusionConstraint]] = { + val currentEpochPartitionNumber = newEpochNumber / partitionSize + val firstEpochNumber = EpochNumber(currentEpochPartitionNumber * partitionSize) + val lastEpochNumber = EpochNumber(firstEpochNumber + partitionSize - 1) + + if (newEpochNumber == firstEpochNumber) { + outputStore + .getFirstBlockInEpoch(newEpochNumber) + .map(_.map(block => LowerBoundExclusionConstraint(block, currentEpochPartitionNumber))) + } else if (newEpochNumber == lastEpochNumber) { + outputStore + .getLastBlockInEpoch(newEpochNumber) + .map(_.map(block => UpperBoundExclusionConstraint(block, currentEpochPartitionNumber))) + } else PekkoFutureUnlessShutdown.pure(None) + } + } + + private[pruning] final case class HighestPrunedPartitionNumbers( + partitionNumber: Long, + batchTablePartitionNumber: Long, + consensusInProgressTablePartitionNumber: Long, + ) + + private val epochPartitionedTablesForPruning = + epochPartitionedTables.filterNot(_ == consensusInProgressTable) + + @VisibleForTesting + private[pruning] def partitionsToPrune( + partitionSize: Int, + pruneAtInclusiveEpochNumber: EpochNumber, + latestCompletedEpochNumber: EpochNumber, + previous: HighestPrunedPartitionNumbers, + ): (Seq[String], HighestPrunedPartitionNumbers) = { + def computeNumberOfEpochPartitionToPrune(epochNumberToPrune: EpochNumber) = { + val numberOfPartitionIncludingEpoch = epochNumberToPrune / partitionSize + val lastEpochNumberInPartition = (numberOfPartitionIncludingEpoch + 1) * partitionSize - 1 + // if epoch is the last one in partition, we prune from that partition, otherwise we prune from the previous + if (epochNumberToPrune == lastEpochNumberInPartition) numberOfPartitionIncludingEpoch + else numberOfPartitionIncludingEpoch - 1 + } + + val epochPartitionNumberToPrune = computeNumberOfEpochPartitionToPrune( + pruneAtInclusiveEpochNumber + ) + val epochPartitionNumberToPruneForBatchesTable = computeNumberOfEpochPartitionToPrune( + EpochNumber(pruneAtInclusiveEpochNumber - OrderingRequestBatch.BatchValidityDurationEpochs) + ) + val epochPartitionNumberToPruneForConsensusInProgressTable = + computeNumberOfEpochPartitionToPrune(latestCompletedEpochNumber) + + val partitionNames = (for { + partitionNumber <- (previous.partitionNumber + 1 to epochPartitionNumberToPrune) + tableName <- epochPartitionedTablesForPruning + } yield s"${tableName}_p$partitionNumber") + + val batchesTablePartitionNames = (for { + partitionNumber <- + (previous.batchTablePartitionNumber + 1) to epochPartitionNumberToPruneForBatchesTable + } yield s"${batchesTable}_p$partitionNumber") + + val consensusInProgressTablePartitionNames = (for { + partitionNumber <- + (previous.consensusInProgressTablePartitionNumber + 1) to epochPartitionNumberToPruneForConsensusInProgressTable + } yield s"${consensusInProgressTable}_p$partitionNumber") + + val next = HighestPrunedPartitionNumbers( + Math.max(previous.partitionNumber, epochPartitionNumberToPrune), + Math.max(previous.batchTablePartitionNumber, epochPartitionNumberToPruneForBatchesTable), + Math.max( + previous.consensusInProgressTablePartitionNumber, + epochPartitionNumberToPruneForConsensusInProgressTable, + ), + ) + (partitionNames ++ batchesTablePartitionNames ++ consensusInProgressTablePartitionNames, next) + } + + private class PartitionPrunerImpl( + val storage: DbStorage, + val timeouts: ProcessingTimeout, + val loggerFactory: NamedLoggerFactory, + partitionNumberMap: Map[String, Long] = Map.empty, + partitionSizeEntry: PartitionSizeEntry, + )(implicit ec: ExecutionContext) + extends PartitionPruner[PekkoEnv] + with DbStore { + + import storage.api.* + + private val partitionSize = partitionSizeEntry.partitionSize + + @SuppressWarnings(Array("org.wartremover.warts.Var")) + var latestPrunedPartitionNumbers = HighestPrunedPartitionNumbers( + (partitionNumberMap - batchesTable).values.minOption.getOrElse(0L) - 1L, + partitionNumberMap.getOrElse(batchesTable, 0L) - 1L, + partitionNumberMap.getOrElse(consensusInProgressTable, 0L) - 1L, + ) + + override def prune(epochNumberInclusive: EpochNumber, latestCompletedEpochNumber: EpochNumber)( + implicit traceContext: TraceContext + ): PekkoFutureUnlessShutdown[String] = { + val (partitionNames, prunedPartitionNumbers) = + partitionsToPrune( + partitionSize, + epochNumberInclusive, + latestCompletedEpochNumber, + latestPrunedPartitionNumbers, + ) + + PekkoFutureUnlessShutdown( + "prune", + () => + if (partitionNames.nonEmpty) { + logger.info( + s"About to prune ${partitionNames.size} partitions at epoch $epochNumberInclusive" + ) + storage + .update_( + DBIO + .sequence( + partitionNames + .map(partitionName => sqlu"""drop table if exists #$partitionName;""") + ) + .transactionally, + functionFullName, + ) + .map { _ => + latestPrunedPartitionNumbers = prunedPartitionNumbers + s"Pruned ${partitionNames.size} partitions at epoch $epochNumberInclusive" + } + } else + FutureUnlessShutdown.pure( + s"Pruned no partitions at epoch $epochNumberInclusive" + ), + orderingStage = Some(functionFullName), + ) + } + } + + @VisibleForTesting + private[pruning] class PruningManagerInitStore( + val storage: DbStorage, + val timeouts: ProcessingTimeout, + val loggerFactory: NamedLoggerFactory, + )(implicit ec: ExecutionContext) + extends DbStore { + import storage.api.* + + private implicit val getResultTableNameAndPartitionNumbers: GetResult[(String, Long, Long)] = + GetResult(r => (r.nextString(), r.nextLong(), r.nextLong())) + private implicit val getResultPartitionSizeHistory: GetResult[PartitionSizeEntry] = + GetResult(r => PartitionSizeEntry(EpochNumber(r.nextLong()), r.nextLong(), r.nextInt())) + + def latestPartitionSizeEntry(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[PartitionSizeEntry] = { + val query = + sql"""select epoch_number, partition_number, partition_size from ord_partition_size_history + order by epoch_number desc limit 1""".as[PartitionSizeEntry] + storage + .query(query, functionFullName) + .map(_.headOption.getOrElse(sys.error("Can't find partition size data"))) + } + + def queryLowestAndHighestExistingPartitionNumberPerTableName(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[(Map[String, Long], Map[String, Long])] = { + def lowestPartitionNumbersQuery(tableNames: Seq[String]) = { + val tablesSql = tableNames + .map(tableName => s"('$tableName'::regclass)") + .mkString(",\n ") + sql""" + #${s""" + with target_tables(parent_table) as ( + values + $tablesSql + ), + partitions as ( + select + parent.oid::regclass as parent_table, + child.relname as partition_name, + substring(child.relname from '_p([0-9]+)$$')::bigint as partition_number + from target_tables t + join pg_class parent on parent.oid = t.parent_table + join pg_inherits i on i.inhparent = parent.oid + join pg_class child on child.oid = i.inhrelid + ) + select + parent_table::text, + min(partition_number) as lowest_partition_number, + max(partition_number) as highest_partition_number + from partitions + group by parent_table + order by parent_table + """} + """.as[(String, Long, Long)] + } + storage + .query( + lowestPartitionNumbersQuery(batchesTable +: epochPartitionedTables), + functionFullName, + ) + .map { rows => + val minPartitionsMap: Map[String, Long] = + rows.map { case (tableName, minPartition, _) => tableName -> minPartition }.toMap + val maxPartitionsMap: Map[String, Long] = + rows.map { case (tableName, _, maxPartition) => tableName -> maxPartition }.toMap + (minPartitionsMap, maxPartitionsMap) + } + } + } + +} diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PruningModule.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PruningModule.scala index bb5d00a40d..701bae3e48 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PruningModule.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PruningModule.scala @@ -32,6 +32,7 @@ final class PruningModule[E <: Env[E]]( clock: Clock, override val loggerFactory: NamedLoggerFactory, override val timeouts: ProcessingTimeout, + partitionPruner: Option[PartitionManager.PartitionPruner[E]] = None, )(implicit metricsContext: MetricsContext) extends Pruning[E] { @@ -47,10 +48,15 @@ final class PruningModule[E <: Env[E]]( )(implicit context: E#ActorContextT[Pruning.Message], traceContext: TraceContext): Unit = message match { case Pruning.Start => - pipeToSelfOpt(stores.outputStore.getLowerBound()) { - case Success(Some(lowerBound)) => - Some(Pruning.PerformPruning(lowerBound.epochNumber)) - case Success(None) => + pipeToSelfOpt( + context.zipFuture( + stores.outputStore.getLowerBound(), + stores.outputStore.getLastNonSequentialBlockMetadataStored, + ) + ) { + case Success((Some(lowerBound), Some(lastBlock))) => + Some(Pruning.PerformPruning(lowerBound.epochNumber, lastBlock.epochNumber)) + case Success(_) => None case Failure(exception) => Some( @@ -107,7 +113,7 @@ final class PruningModule[E <: Env[E]]( result match { case (Some(block1), Some(block2)) => val epoch = EpochNumber(Math.min(block1.epochNumber, block2.epochNumber)) - Some(Pruning.SaveNewLowerBound(epoch)) + Some(Pruning.SaveNewLowerBound(epoch, latestBlock.epochNumber)) case (Some(_), None) => // Pruning cannot be performed in this case, otherwise we would end up with // fewer blocks than the minimum number of blocks to keep. @@ -136,10 +142,10 @@ final class PruningModule[E <: Env[E]]( ) ) } - case Pruning.SaveNewLowerBound(epochNumber) => + case Pruning.SaveNewLowerBound(epochNumber, latestOngoingEpoch) => logger.debug(s"Saving new lower bound $epochNumber") pipeToSelfOpt(stores.outputStore.saveLowerBound(epochNumber)) { - case Success(Right(())) => Some(Pruning.PerformPruning(epochNumber)) + case Success(Right(())) => Some(Pruning.PerformPruning(epochNumber, latestOngoingEpoch)) case Success(Left(error)) => val msg = s"Failed to save new pruning lower bound: $error" logger.error(msg) @@ -152,29 +158,53 @@ final class PruningModule[E <: Env[E]]( ) ) } - case Pruning.PerformPruning(epochNumber) => + case Pruning.PerformPruning(epochNumber, latestOngoingEpoch) => logger.info(s"Pruning at epoch $epochNumber starting") - val pruneFuture = - context.zipFuture3( - stores.outputStore.prune(epochNumber), - stores.epochStore.prune(epochNumber), - stores.availabilityStore.prune( - EpochNumber(epochNumber - OrderingRequestBatch.BatchValidityDurationEpochs + 1L) - ), - orderingStage = Some("pruning-prune"), - ) - pipeToSelfOpt(pruneFuture) { - case Success( - (outputStorePrunedRecords, epochStorePrunedRecords, availabilityStorePrunedRecords) - ) => - val msg = s"""|Pruning at epoch $epochNumber complete. - |EpochStore: pruned ${epochStorePrunedRecords.epochs} epochs, ${epochStorePrunedRecords.pbftMessagesCompleted} pbft messages. - |OutputStore: pruned ${outputStorePrunedRecords.epochs} epochs and ${outputStorePrunedRecords.blocks} blocks. - |AvailabilityStore: pruned ${availabilityStorePrunedRecords.batches} batches.""".stripMargin - logger.info(msg) - completePruningOperation(msg) - case Failure(exception) => - Some(Pruning.FailedDatabaseOperation("Failed to perform pruning", exception)) + partitionPruner match { + // The partition pruner is only present if using Postgres. + // In that case, tables uses partitions and pruning is done by dropping partitions. + case Some(partitionPruner) => + // We subtract 1 from the pruning epoch to turn the exclusive epoch number into inclusive. + // We subtract 1 from the latest ongoing epoch to turn it into the latest completed epoch + context.pipeToSelf( + partitionPruner + .prune(EpochNumber(epochNumber - 1L), EpochNumber(latestOngoingEpoch - 1L)) + ) { + case Success(msg) => + logger.info(msg) + completePruningOperation(msg) + case Failure(exception) => + Some(Pruning.FailedDatabaseOperation("Failed to perform pruning", exception)) + } + // If using H2 or in memory storage, we default to the old behavior of pruning each store separately + // by deleting all records before the given epoch number. + case _ => + val pruneFuture = + context.zipFuture3( + stores.outputStore.prune(epochNumber), + stores.epochStore.prune(epochNumber), + stores.availabilityStore.prune( + EpochNumber(epochNumber - OrderingRequestBatch.BatchValidityDurationEpochs + 1L) + ), + orderingStage = Some("pruning-prune"), + ) + pipeToSelfOpt(pruneFuture) { + case Success( + ( + outputStorePrunedRecords, + epochStorePrunedRecords, + availabilityStorePrunedRecords, + ) + ) => + val msg = s"""|Pruning at epoch $epochNumber complete. + |EpochStore: pruned ${epochStorePrunedRecords.epochs} epochs, ${epochStorePrunedRecords.pbftMessagesCompleted} pbft messages. + |OutputStore: pruned ${outputStorePrunedRecords.epochs} epochs and ${outputStorePrunedRecords.blocks} blocks. + |AvailabilityStore: pruned ${availabilityStorePrunedRecords.batches} batches.""".stripMargin + logger.info(msg) + completePruningOperation(msg) + case Failure(exception) => + Some(Pruning.FailedDatabaseOperation("Failed to perform pruning", exception)) + } } case Pruning.FailedDatabaseOperation(msg, exception) => logger.error(msg, exception) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/BlockSubscription.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/BlockSubscription.scala index 590e1fe022..49395b48e9 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/BlockSubscription.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/BlockSubscription.scala @@ -3,13 +3,21 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework +import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.synchronizer.block.BlockFormat import com.digitalasset.canton.tracing.{TraceContext, Traced} import org.apache.pekko.stream.KillSwitch import org.apache.pekko.stream.scaladsl.Source trait BlockSubscription { + def subscription(): Source[Traced[BlockFormat.Block], KillSwitch] - def receiveBlock(block: BlockFormat.Block)(implicit traceContext: TraceContext): Unit + def receiveBlock( + block: BlockFormat.Block + )(implicit traceContext: TraceContext, metricsContext: MetricsContext): Unit + + def isSequencerCoreSlow: Boolean + + def bufferSize: Int } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/Module.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/Module.scala index 59acf7242e..145f3145ec 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/Module.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/Module.scala @@ -169,27 +169,42 @@ trait P2PNetworkRef[-P2PMessageT] extends FlagCloseable { ): Unit } +/** Notifies P2P connection management events. + * + * The P2P endpoint may be missing if the connection is incoming and the connecting peer did not + * communicate one. + */ trait P2PConnectionEventListener { - def onConnect(p2pEndpointId: P2PEndpoint.Id)(implicit traceContext: TraceContext): Unit + + def onConnect(maybeP2pEndpointId: Option[P2PEndpoint.Id])(implicit + traceContext: TraceContext + ): Unit + def onDisconnect(p2pEndpointId: P2PEndpoint.Id)(implicit traceContext: TraceContext): Unit - // The P2P endpoint may be None if the connection is incoming and the connecting peer did not communicate one + def onSequencerId(bftNodeId: BftNodeId, maybeP2PEndpoint: Option[P2PEndpoint])(implicit traceContext: TraceContext ): Unit } + object P2PConnectionEventListener { - val NoOp: P2PConnectionEventListener = new P2PConnectionEventListener { - override def onConnect(p2pEndpointId: P2PEndpoint.Id)(implicit - traceContext: TraceContext - ): Unit = () - override def onDisconnect(p2pEndpointId: P2PEndpoint.Id)(implicit - traceContext: TraceContext - ): Unit = () - override def onSequencerId(bftNodeId: BftNodeId, maybeP2PEndpoint: Option[P2PEndpoint])(implicit - traceContext: TraceContext - ): Unit = - () - } + + val NoOp: P2PConnectionEventListener = + new P2PConnectionEventListener { + + override def onConnect(maybeP2pEndpointId: Option[P2PEndpoint.Id])(implicit + traceContext: TraceContext + ): Unit = () + + override def onDisconnect(p2pEndpointId: P2PEndpoint.Id)(implicit + traceContext: TraceContext + ): Unit = () + + override def onSequencerId(bftNodeId: BftNodeId, maybeP2PEndpoint: Option[P2PEndpoint])( + implicit traceContext: TraceContext + ): Unit = + () + } } sealed trait P2PAddress extends Product with Serializable { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/availability/BatchId.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/availability/BatchId.scala index 5fc45cf142..94a47c8d62 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/availability/BatchId.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/availability/BatchId.scala @@ -13,6 +13,8 @@ import com.google.protobuf.ByteString final case class BatchId private (hash: Hash) extends HasCryptographicEvidence { override def getCryptographicEvidence: ByteString = hash.getCryptographicEvidence + + override lazy val toString: String = s"${getClass.getSimpleName}(${hash.toHexString})" } object BatchId { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/ordering/OrderedBlockForOutput.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/ordering/OrderedBlockForOutput.scala index 62b6073af9..b120bf1a9b 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/ordering/OrderedBlockForOutput.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/ordering/OrderedBlockForOutput.scala @@ -21,24 +21,7 @@ final case class OrderedBlockForOutput( viewNumber: ViewNumber, originalLeader: BftNodeId, isLastInEpoch: Boolean, - mode: OrderedBlockForOutput.Mode, + orderingMode: OrderingMode, ) -object OrderedBlockForOutput { - - sealed trait Mode extends Product with Serializable { - - /** If `true`, dissemination will use the current topology for the output pull protocol. */ - def isStateTransfer: Boolean = this match { - case Mode.FromStateTransfer => true - case Mode.FromConsensus => false - } - } - - object Mode { - - case object FromConsensus extends Mode - - case object FromStateTransfer extends Mode - } -} +object OrderedBlockForOutput {} diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/ordering/OrderingMode.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/ordering/OrderingMode.scala new file mode 100644 index 0000000000..3ba6d41e48 --- /dev/null +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/ordering/OrderingMode.scala @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering + +sealed trait OrderingMode extends Product with Serializable { + + /** If `true`, dissemination will use the current topology for the output pull protocol. */ + def isStateTransfer: Boolean = this match { + case OrderingMode.StateTransfer => true + case OrderingMode.Consensus => false + } +} + +object OrderingMode { + + case object Consensus extends OrderingMode + + case object StateTransfer extends OrderingMode +} diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/topology/BlacklistLeaderSelectionPolicyConfig.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/topology/BlacklistLeaderSelectionPolicyConfig.scala index 7d7e7f8638..5e7b0aef3d 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/topology/BlacklistLeaderSelectionPolicyConfig.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/topology/BlacklistLeaderSelectionPolicyConfig.scala @@ -58,6 +58,23 @@ object BlacklistLeaderSelectionPolicyConfig { ParsingResult.pure(HowLongToBlacklist.Linear(value.maximumEpochLengthBlacklisted)) case v31.BlacklistLeaderSelectionPolicy.HowLongToBlacklist.HowLongNoBlacklisting(value) => ParsingResult.pure(HowLongToBlacklist.NoBlacklisting) + case v31.BlacklistLeaderSelectionPolicy.HowLongToBlacklist + .HowLongLinearWithParameters(value) => + ParsingResult.pure( + HowLongToBlacklist.LinearWithParameters( + slope = value.slope, + initialValue = value.initialValue, + maximumEpochBlacklisted = value.maximumEpochLengthBlacklisted, + ) + ) + case v31.BlacklistLeaderSelectionPolicy.HowLongToBlacklist + .HowLongExponential(value) => + ParsingResult.pure( + HowLongToBlacklist.Exponential( + initialValue = value.initialValue, + maximumEpochBlacklisted = value.maximumEpochLengthBlacklisted, + ) + ) } howManyCanWeBlacklist <- proto.howManyCanWeBlacklist match { case v31.BlacklistLeaderSelectionPolicy.HowManyCanWeBlacklist.Empty => @@ -104,6 +121,78 @@ object BlacklistLeaderSelectionPolicyConfig { maximumEpochBlacklisted.getOrElse(epochsLeftUntilNextTrial).min(epochsLeftUntilNextTrial) } + // X |-> min(slope*X+initialValue, maximumEpochBlacklisted) + final case class LinearWithParameters( + maximumEpochBlacklisted: Option[Long], + slope: Long, + initialValue: Long, + ) extends HowLongToBlacklist { + override protected def pretty: Pretty[LinearWithParameters] = prettyOfClass( + param("maximumEpochBlacklisted", _.maximumEpochBlacklisted), + param("slope", _.slope), + param("initialValue", _.initialValue), + ) + + override def punishNodeThatFailed(failedEpochSoFar: Long): BlacklistStatus = + BlacklistStatus.Blacklisted.create( + failedAttemptsBefore = failedEpochSoFar, + epochsLeftUntilNewTrial = updateLeftUntilNextTrial( + slope * failedEpochSoFar + initialValue + ), + ) + + override def updateLeftUntilNextTrial(epochsLeftUntilNextTrial: Long): Long = + maximumEpochBlacklisted.getOrElse(epochsLeftUntilNextTrial).min(epochsLeftUntilNextTrial) + + override def toProto: v31.BlacklistLeaderSelectionPolicy.HowLongToBlacklist = + v31.BlacklistLeaderSelectionPolicy.HowLongToBlacklist.HowLongLinearWithParameters( + v31.HowLongLinearWithParameters( + slope = slope, + initialValue = initialValue, + maximumEpochLengthBlacklisted = maximumEpochBlacklisted, + ) + ) + } + + final case class Exponential( + maximumEpochBlacklisted: Option[Long], + initialValue: Long, + ) extends HowLongToBlacklist { + override protected def pretty: Pretty[Exponential] = prettyOfClass( + param("maximumEpochBlacklisted", _.maximumEpochBlacklisted), + param("initialValue", _.initialValue), + ) + + override def punishNodeThatFailed(failedEpochSoFar: Long): BlacklistStatus = + BlacklistStatus.Blacklisted.create( + failedAttemptsBefore = failedEpochSoFar, + epochsLeftUntilNewTrial = updateLeftUntilNextTrial( + fixOverflow( + pow(failedEpochSoFar) + initialValue + ) + ), + ) + + private def fixOverflow(x: Long): Long = + if (x < 0) Long.MaxValue else x + + private def pow(x: Long): Long = { + val temp = scala.math.pow(2, x.toDouble) + if (temp < 0.0 || temp > Long.MaxValue.toDouble) Long.MaxValue else temp.toLong + } + + override def updateLeftUntilNextTrial(epochsLeftUntilNextTrial: Long): Long = + maximumEpochBlacklisted.getOrElse(epochsLeftUntilNextTrial).min(epochsLeftUntilNextTrial) + + override def toProto: v31.BlacklistLeaderSelectionPolicy.HowLongToBlacklist = + v31.BlacklistLeaderSelectionPolicy.HowLongToBlacklist.HowLongExponential( + v31.HowLongExponential( + initialValue = initialValue, + maximumEpochLengthBlacklisted = maximumEpochBlacklisted, + ) + ) + } + case object NoBlacklisting extends HowLongToBlacklist { override final def pretty: Pretty[this.type] = prettyOfObject[this.type] override def punishNodeThatFailed(failedEpochSoFar: Long): BlacklistStatus = diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/topology/Membership.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/topology/Membership.scala index b053b3704f..3a5b9d0d87 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/topology/Membership.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/topology/Membership.scala @@ -27,6 +27,7 @@ final case class Membership( param("myId", _.myId.doubleQuoted), param("orderingTopology", _.orderingTopology), param("leaders", _.leaders.map(_.doubleQuoted)), + param("blacklistedNodes", _.blacklistedNodes.map(_.doubleQuoted)), ) } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Availability.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Availability.scala index 17c6f5c983..a5b377bbc0 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Availability.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Availability.scala @@ -20,7 +20,10 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor BatchId, ProofOfAvailability, } -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.OrderedBlockForOutput +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.{ + OrderedBlockForOutput, + OrderingMode, +} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.Membership import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.{ MessageFrom, @@ -283,10 +286,11 @@ object Availability { final case class FetchBatchDataFromNodes( proofOfAvailability: ProofOfAvailability, - mode: OrderedBlockForOutput.Mode, + orderingMode: OrderingMode, ) extends LocalOutputFetch - final case class FetchRemoteBatchDataTimeout(batchId: BatchId) extends LocalOutputFetch + final case class FetchRemoteBatchDataTimeout(batchId: BatchId, epochNumber: EpochNumber) + extends LocalOutputFetch final case class AttemptedBatchDataLoadForNode( batchId: BatchId, @@ -299,6 +303,7 @@ object Availability { object RemoteOutputFetch { final case class FetchRemoteBatchData private ( batchId: BatchId, + epochNumber: EpochNumber, from: BftNodeId, )( override val representativeProtocolVersion: RepresentativeProtocolVersion[ @@ -313,7 +318,7 @@ object Availability { protected override def toProtoV30: v30.AvailabilityMessage = v30.AvailabilityMessage( v30.AvailabilityMessage.Message.BatchRequest( - v30.BatchRequest(batchId.hash.getCryptographicEvidence) + v30.BatchRequest(batchId.hash.getCryptographicEvidence, epochNumber) ) ) @@ -356,16 +361,22 @@ object Availability { for { id <- BatchId.fromProto(value.batchId) rpv <- protocolVersionRepresentativeFor(SupportedVersions.ProtoData) - } yield Availability.RemoteOutputFetch.FetchRemoteBatchData(id, from)( + epochNumber = EpochNumber(value.epochNumber) + } yield Availability.RemoteOutputFetch.FetchRemoteBatchData( + id, + epochNumber, + from, + )( rpv, deserializedFrom = Some(bytes), ) def create( batchId: BatchId, + epochNumber: EpochNumber, from: BftNodeId, )(implicit synchronizerProtocolVersion: ProtocolVersion): FetchRemoteBatchData = - FetchRemoteBatchData(batchId, from)( + FetchRemoteBatchData(batchId, epochNumber, from)( protocolVersionRepresentativeFor(synchronizerProtocolVersion), deserializedFrom = None, ) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Consensus.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Consensus.scala index 8e85b61087..70019677c5 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Consensus.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Consensus.scala @@ -64,6 +64,8 @@ object Consensus { final case class GetOrderingTopologyResponse( epochNumber: EpochNumber, nodes: Set[BftNodeId], + leaders: Seq[BftNodeId], + blacklisted: Seq[BftNodeId], sequencingParameters: SequencingParameters, ) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Output.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Output.scala index 0d757def1d..0c189ee1a0 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Output.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Output.scala @@ -26,6 +26,12 @@ object Output { final case object Start extends Message[Nothing] + /** Sent by the sequencer core subscription to the output module when processing may be able to be + * resumed after a sequencer core slowdown in consuming blocks, allowing to always and timely + * resume ordering. + */ + final case object ProcessNewEpochTopologyMessagesIfPossible extends Message[Nothing] + // From local consensus final case class BlockOrdered(orderedBlockForOutput: OrderedBlockForOutput) extends Message[Nothing] @@ -56,6 +62,9 @@ object Output { cryptoProvider: CryptoProvider[E], ) extends Message[E] + final case class AddMessageChunkFromRestart(value: Seq[OrderedBlockForOutput]) + extends Message[Nothing] + final case class AsyncException(error: Throwable) extends Message[Nothing] final case object NoTopologyAvailable extends Message[Nothing] diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/P2PNetworkOut.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/P2PNetworkOut.scala index 7c7f281a24..380d8406ef 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/P2PNetworkOut.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/P2PNetworkOut.scala @@ -30,7 +30,7 @@ object P2PNetworkOut { sealed trait Network extends Message object Network { - final case class Connected(p2pEndpointId: P2PEndpoint.Id) extends Network + final case class Connected(maybeP2pEndpointId: Option[P2PEndpoint.Id]) extends Network final case class Disconnected(p2pEndpointId: P2PEndpoint.Id) extends Network final case class Authenticated(bftNodeId: BftNodeId, maybeP2PEndpoint: Option[P2PEndpoint]) extends Network diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Pruning.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Pruning.scala index 33994cf955..44c2de2b6d 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Pruning.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/Pruning.scala @@ -33,8 +33,10 @@ object Pruning { retention: FiniteDuration, minBlocksToKeep: Int, ) extends Message - final case class SaveNewLowerBound(epoch: EpochNumber) extends Message - final case class PerformPruning(epoch: EpochNumber) extends Message + final case class SaveNewLowerBound(epoch: EpochNumber, latestOngoingEpoch: EpochNumber) + extends Message + final case class PerformPruning(epoch: EpochNumber, latestOngoingEpoch: EpochNumber) + extends Message final case class FailedDatabaseOperation(msg: String, exception: Throwable) extends Message final case class StartPruningSchedule(schedule: BftOrdererPruningSchedule) extends Message diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/utils/FairBoundedQueue.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/utils/FairBoundedQueue.scala index d79a901273..a4d09f1bff 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/utils/FairBoundedQueue.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/utils/FairBoundedQueue.scala @@ -137,18 +137,19 @@ class FairBoundedQueue[ItemType]( def dequeueAll( predicate: ItemType => Boolean )(implicit metricsContext: MetricsContext): Seq[ItemType] = { - val (dequeuedItems, remainingNodesToItems) = - arrivalOrder.foldLeft((Seq[ItemType](), Seq[(BftNodeId, ItemType)]())) { - case (dequeuedItems -> remainingNodesToItems, nodeId -> enqueuedAt) => - val item = nodeQueues(nodeId).dequeue() - if (predicate(item)) { - sizeGauge.foreach(_.updateValue(size - 1)) - emitOrderingStageLatency(enqueuedAt) - (dequeuedItems :+ item, remainingNodesToItems) - } else { - (dequeuedItems, remainingNodesToItems :+ (nodeId -> item)) - } + + val dequeuedItems = mutable.Buffer[ItemType]() + val remainingNodesToItems = mutable.Buffer[(BftNodeId, ItemType)]() + arrivalOrder.foreach { case (nodeId -> enqueuedAt) => + val item = nodeQueues(nodeId).dequeue() + if (predicate(item)) { + sizeGauge.foreach(_.updateValue(size - 1)) + emitOrderingStageLatency(enqueuedAt) + dequeuedItems.append(item) + } else { + remainingNodesToItems.append(nodeId -> item) } + } // Rebuild the underlying structures. arrivalOrder.clear() @@ -156,7 +157,7 @@ class FairBoundedQueue[ItemType]( enqueue(nodeId, item).discard } - dequeuedItems + dequeuedItems.toSeq } def size: Int = arrivalOrder.size diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/config/SequencerNodeParameterConfig.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/config/SequencerNodeParameterConfig.scala index 609ad6af01..0291a6a52e 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/config/SequencerNodeParameterConfig.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/config/SequencerNodeParameterConfig.scala @@ -50,6 +50,10 @@ final case class AsyncWriterConfig( * Configures behavior of sendAsync requests until the traffic is initialized during LSU: If * true, the sequencer will delay processing of the requests. If false, the sequencer will * synchronously reject the requests with an error. + * @param enableRejectDeliveredAggregationsOnPv35 + * No effect on pv34. On pv36 enabled for all node types. On pv35, the sequencer will reject + * aggregations that have already been completed eagerly for all configured member types, default + * is MED. * @param disableSubmissionChecksForTesting * If true, disable checks on the write path of the sequencer in order to allow testing the same * checks on the post-processing path (malicious sequencer node tests). Only to be used for @@ -58,6 +62,9 @@ final case class AsyncWriterConfig( * If true, then we won't check whether the client binaries are really supported during * handshake. This is normally only useful for unstable protocol versions to avoid accidental * ledger forks. + * @param enablePrevalidation + * If true (as of 3.6), we will use pre-validation to move the signature validation into a + * separate parallel stage instead of the sequential step. */ final case class SequencerNodeParameterConfig( override val alphaVersionSupport: Boolean = false, @@ -75,8 +82,10 @@ final case class SequencerNodeParameterConfig( lsuRepair: LsuRepair = LsuRepair(), lsu: SequencerLsuConfig = SequencerLsuConfig(), delayRequestsBeforeLsuTrafficInit: Boolean = false, + enableRejectDeliveredAggregationsOnPv35: Seq[String] = Seq("MED"), disableSubmissionChecksForTesting: Boolean = false, disableReleaseVersionHandshakeCheck: Boolean = false, + enablePrevalidation: Boolean = true, ) extends ProtocolConfig with LocalNodeParametersConfig diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/config/SequencerNodeParameters.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/config/SequencerNodeParameters.scala index d13ef51160..2cbfcd4b30 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/config/SequencerNodeParameters.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/config/SequencerNodeParameters.scala @@ -49,9 +49,16 @@ trait SequencerParameters { * strictly greater to this value will not be delivered. Important notes: * - SHOULD be set only in disaster recovery scenarios. * - MUST be the same value in all sequencers of a synchronizer + * @param enableRejectDeliveredAggregationsOnPv35 + * No effect on pv34. On pv35, if true, the sequencer will reject aggregations that have already + * been delivered for mediators. On pv36, this is always enabled, for all nodes. + * @param disableSubmissionChecksForTesting + * Whether to disable submission checks for testing purposes. This should only be used in tests. * @param disableReleaseVersionHandshakeCheck * If set to true, then the sequencer will skip checking that the client binary aligns 100% with * the server binary when the server is running an unstable protocol version. + * @param enablePrevalidation + * if true then we will prevalidate signatures in a separate stage before processing */ final case class SequencerNodeParameters( general: CantonNodeParameters.General, @@ -67,9 +74,11 @@ final case class SequencerNodeParameters( maxSubscriptionsPerMember: PositiveInt = PositiveInt.tryCreate(5), drSequencingTimeUpperBound: Option[DisasterRecoverySequencingTimeUpperBound] = None, delayRequestsBeforeLsuTrafficInit: Boolean, + enableRejectDeliveredAggregationsOnPv35: Seq[String], disableSubmissionChecksForTesting: Boolean = false, disableReleaseVersionHandshakeCheck: Boolean = false, lsuConfig: SequencerLsuConfig, + enablePrevalidation: Boolean = true, ) extends CantonNodeParameters with HasGeneralCantonNodeParameters with HasProtocolCantonNodeParameters diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/time/LsuSequencingBounds.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/time/LsuSequencingBounds.scala index d5369b3588..814b70bf35 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/time/LsuSequencingBounds.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/time/LsuSequencingBounds.scala @@ -6,7 +6,6 @@ package com.digitalasset.canton.synchronizer.sequencer.time import cats.data.EitherT import cats.syntax.option.* import cats.syntax.traverse.* -import com.daml.nonempty.NonEmpty import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.ErrorLoggingContext @@ -14,7 +13,7 @@ import com.digitalasset.canton.synchronizer.sequencer.config.LsuSequencingBounds import com.digitalasset.canton.topology.processing.SequencedTime import com.digitalasset.canton.topology.store.TopologyStore import com.digitalasset.canton.topology.store.TopologyStoreId.SynchronizerStore -import com.digitalasset.canton.topology.transaction.{LsuAnnouncement, TopologyMapping} +import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.{EitherTUtil, ErrorUtil} import com.google.common.annotations.VisibleForTesting @@ -58,7 +57,7 @@ object LsuSequencingBounds { lsuSequencingBoundsOverride: LsuSequencingBoundsOverride, store: TopologyStore[SynchronizerStore], )(implicit - errorLoggingContext: ErrorLoggingContext, + traceContext: TraceContext, ec: ExecutionContext, ): EitherT[FutureUnlessShutdown, String, LsuSequencingBounds] = { @@ -66,7 +65,7 @@ object LsuSequencingBounds { lsuSequencingBoundsOverride for { - upgradeTimeFromStoreO <- EitherT.liftF(findUpgradeTimeFromPredecessor(store)) + upgradeTimeFromStoreO <- EitherT.liftF(store.findUpgradeTimeFromPredecessor()) _ <- EitherTUtil.condUnitET[FutureUnlessShutdown]( upgradeTimeFromStoreO.isEmpty, "LsuSequencingBoundsOverride cannot be set if an LSU announcement exists in the topology store", @@ -100,7 +99,7 @@ object LsuSequencingBounds { implicit val traceContext = errorLoggingContext.traceContext for { - upgradeTimeO <- findUpgradeTimeFromPredecessor(store) + upgradeTimeO <- store.findUpgradeTimeFromPredecessor() lsuSequencingBounds <- upgradeTimeO .traverse { upgradeTime => @@ -125,35 +124,4 @@ object LsuSequencingBounds { .map(_.flatten) } yield lsuSequencingBounds } - - private def findUpgradeTimeFromPredecessor( - store: TopologyStore[SynchronizerStore] - )(implicit - errorLoggingContext: ErrorLoggingContext, - ec: ExecutionContext, - ): FutureUnlessShutdown[Option[CantonTimestamp]] = { - val psid = store.storeId.psid - implicit val traceContext = errorLoggingContext.traceContext - - store - .findPositiveTransactions( - CantonTimestamp.MaxValue, - asOfInclusive = false, - isProposal = false, - types = Seq(TopologyMapping.Code.LsuAnnouncement), - filterUid = Some(NonEmpty(Seq, psid.uid)), - filterNamespace = None, - ) - .map( - _.collectOfMapping[LsuAnnouncement].result - .filter(_.mapping.successorSynchronizerId == psid) - .toList match { - case Nil => None - case one :: Nil => one.mapping.upgradeTime.some - - case _moreThanOne => - ErrorUtil.invalidState("Found more than one LsuAnnouncement mapping") - } - ) - } } diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/DirectSequencerSubscription.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/DirectSequencerSubscription.scala index 77af20ca89..2e447058a5 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/DirectSequencerSubscription.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/DirectSequencerSubscription.scala @@ -53,7 +53,7 @@ private[service] class DirectSequencerSubscription[E]( .mapAsync(1) { eventOrError => externalCompletionRef.get match { case None => - synchronizeWithClosing("direct-sequencer-subscription-handler") { + unlessClosing { handler(eventOrError) }.onShutdown { Either.unit diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/GrpcSequencerAdministrationService.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/GrpcSequencerAdministrationService.scala index 7a22829428..1d324bb261 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/GrpcSequencerAdministrationService.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/GrpcSequencerAdministrationService.scala @@ -219,10 +219,10 @@ class GrpcSequencerAdministrationService( override def onboardingState( request: v30.OnboardingStateRequest, responseObserver: StreamObserver[OnboardingStateResponse], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient( (out: OutputStream) => { - implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext val res = for { memberOrTimestamp <- memberOrTimestamp( @@ -247,14 +247,15 @@ class GrpcSequencerAdministrationService( responseObserver, byteString => OnboardingStateResponse(byteString), ) + } override def onboardingStateV2( request: OnboardingStateV2Request, responseObserver: StreamObserver[OnboardingStateV2Response], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient( (out: OutputStream) => { - implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext val res = for { memberOrTimestamp <- memberOrTimestamp( request.request.sequencerUid, @@ -298,6 +299,7 @@ class GrpcSequencerAdministrationService( responseObserver, byteString => OnboardingStateV2Response(byteString), ) + } private def memberOrTimestamp(memberP: Option[String], timestampP: Option[Timestamp])(implicit traceContext: TraceContext diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/GrpcSequencerService.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/GrpcSequencerService.scala index 399314de8b..80cdca46a9 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/GrpcSequencerService.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/service/GrpcSequencerService.scala @@ -61,6 +61,7 @@ import com.digitalasset.canton.tracing.{ TraceContextGrpc, Traced, } +import com.digitalasset.canton.util.GrpcStreamingUtils.withServerCallStreamObserver import com.digitalasset.canton.util.Thereafter.syntax.* import com.digitalasset.canton.util.{ EitherTUtil, @@ -73,7 +74,7 @@ import com.digitalasset.canton.version.ProtocolVersion import com.github.blemale.scaffeine.{Cache, Scaffeine} import com.google.common.annotations.VisibleForTesting import io.grpc.Status -import io.grpc.stub.{ServerCallStreamObserver, StreamObserver} +import io.grpc.stub.StreamObserver import org.apache.pekko.stream.scaladsl.{Keep, Source} import org.apache.pekko.stream.{ Materializer, @@ -756,26 +757,6 @@ class GrpcSequencerService( subscription.initialize().map(_ => subscription) } - /** Ensure observer is a ServerCalLStreamObserver - * - * @param observer - * underlying observer - * @param handler - * handler requiring a ServerCallStreamObserver - */ - private def withServerCallStreamObserver[R]( - observer: StreamObserver[R] - )(handler: ServerCallStreamObserver[R] => Unit)(implicit traceContext: TraceContext): Unit = - observer match { - case serverCallStreamObserver: ServerCallStreamObserver[R] => - handler(serverCallStreamObserver) - case _ => - val statusException = - Status.INTERNAL.withDescription("Unknown stream observer request").asException() - logger.warn(statusException.getMessage) - observer.onError(statusException) - } - private def checkAuthenticatedMemberPermissionWithCurrentMember( member: Member, currentMember: Option[Member], diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/SequencerRateLimitManagerImpl.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/SequencerRateLimitManagerImpl.scala index ba2e274e8a..b1fdde7f13 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/SequencerRateLimitManagerImpl.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/SequencerRateLimitManagerImpl.scala @@ -27,6 +27,7 @@ import com.digitalasset.canton.sequencing.traffic.EventCostCalculator.EventCostD import com.digitalasset.canton.sequencing.traffic.TrafficConsumedManager.NotEnoughTraffic import com.digitalasset.canton.sequencing.{GroupAddressResolver, TrafficControlParameters} import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics +import com.digitalasset.canton.synchronizer.sequencer.time.LsuSequencingBounds import com.digitalasset.canton.synchronizer.sequencer.traffic.SequencerRateLimitError.SequencingCostValidationError import com.digitalasset.canton.synchronizer.sequencer.traffic.{ SequencerRateLimitError, @@ -59,6 +60,7 @@ class SequencerRateLimitManagerImpl( sequencerMemberRateLimiterFactory: TrafficConsumedManagerFactory = DefaultTrafficConsumedManagerFactory, eventCostCalculator: EventCostCalculator, + lsuSequencingBounds: Option[LsuSequencingBounds], )(implicit executionContext: ExecutionContext) extends SequencerRateLimitManager with NamedLogging @@ -615,17 +617,14 @@ class SequencerRateLimitManagerImpl( FutureUnlessShutdown, SequencerRateLimitError, TrafficConsumedManager, - ]( - getOrCreateTrafficConsumedManager(sender) - ) + ](getOrCreateTrafficConsumedManager(sender)) + trafficPurchased <- getTrafficPurchased( sequencingTime, latestSequencerEventTimestamp, warnIfApproximate, - )( - sender - ) - .leftWiden[SequencerRateLimitError] + )(sender).leftWiden[SequencerRateLimitError] + currentTrafficConsumed = rateLimiter.getTrafficConsumed currentTrafficConsumedTs = currentTrafficConsumed.sequencingTimestamp // If the sequencing timestamp is after the current state, go ahead and try to consume @@ -833,10 +832,24 @@ class SequencerRateLimitManagerImpl( override def resetStateTo( timestampExclusive: CantonTimestamp - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = - trafficConsumedStore - .deleteRecordsPastTimestamp(timestampExclusive) - .map(_ => trafficConsumedPerMember.clear()) + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + + // See https://github.com/DACH-NY/canton/issues/33473 for context + // TODO(#33472) This should be revisited + val shouldResetState = lsuSequencingBounds.forall(_.upgradeTime <= timestampExclusive) + + if (shouldResetState) + trafficConsumedStore + .deleteRecordsPastTimestamp(timestampExclusive) + .map(_ => trafficConsumedPerMember.clear()) + else { + logger.info( + s"Not resetting state because timestamp $timestampExclusive is before upgrade time (${lsuSequencingBounds + .map(_.upgradeTime)})" + ) + FutureUnlessShutdown.unit + } + } } object SequencerRateLimitManagerImpl { diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/integration/tests/nightly/bftordering/BftOrderingExplorativeSimulationTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/integration/tests/nightly/bftordering/BftOrderingExplorativeSimulationTest.scala index 6ea1bd48ad..39a7da75d0 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/integration/tests/nightly/bftordering/BftOrderingExplorativeSimulationTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/integration/tests/nightly/bftordering/BftOrderingExplorativeSimulationTest.scala @@ -75,11 +75,15 @@ class BftOrderingExplorativeSimulationTest extends BftOrderingSimulationTest { private val shortTime: PowerDistribution = PowerDistribution(0.milliseconds, 100.milliseconds) private val longTime: PowerDistribution = PowerDistribution(1.second, 5.seconds) - private def generateStage(segmentLength: SegmentLength): SimulationTestStageSettings = { + private def generateStage( + segmentLength: SegmentLength, + numberInitialNodes: Int, + ): SimulationTestStageSettings = { val numberOfNodesToOnboard = randomWeightedOneOf( 10 -> 0, 3 -> 1, ) + val numberOfNodes = numberInitialNodes + numberOfNodesToOnboard SimulationTestStageSettings( simulationSettings = SimulationSettings( LocalSettings( @@ -115,12 +119,14 @@ class BftOrderingExplorativeSimulationTest extends BftOrderingSimulationTest { ), phaseDurations = PhaseDurations( faulty = durationOfFirstPhaseWithFaults, - recovery = + recovery = (10 seconds).plus( if (numberOfNodesToOnboard > 0) - (segmentLength.length.value * 2) seconds + // We add some extra times if a node was onboarded + (segmentLength.length.value * numberOfNodes * 4) seconds else { 0 seconds - }, + } + ), ), ), TopologySettings( @@ -148,13 +154,14 @@ class BftOrderingExplorativeSimulationTest extends BftOrderingSimulationTest { ) ) ) + val numberOfInitialNodes = randomEquallyWeightedOneOf(2, 4, 5) SimulationTestSettings( - numberOfInitialNodes = randomEquallyWeightedOneOf(2, 4, 5), + numberOfInitialNodes = numberOfInitialNodes, segmentLength = segmentLength, stages = NonEmpty( Seq, - generateStage(segmentLength), - generateStage(segmentLength), + generateStage(segmentLength, numberOfInitialNodes), + generateStage(segmentLength, numberOfInitialNodes), ), ) } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/BlockSequencerStateAsyncWriterTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/BlockSequencerStateAsyncWriterTest.scala index c984790ea0..bba93f89d8 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/BlockSequencerStateAsyncWriterTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/BlockSequencerStateAsyncWriterTest.scala @@ -285,6 +285,7 @@ class BlockSequencerStateAsyncWriterTest val trafficWriteP = PromiseUnlessShutdown.unsupervised[Unit]() val boooh = new Exception("booh") trafficConsumed.updateAndGet(_.copy(writeReturn = Seq(trafficWriteP.futureUS))).discard + writer.health.getState.isOk shouldBe true unwrap(for { _ <- syncWrite(trafficConsumed)(writer.append(Seq(tc1), Map(), EitherT.pure(()))) _ = loggerFactory.assertLogs( @@ -299,6 +300,7 @@ class BlockSequencerStateAsyncWriterTest // then the future will complete immediately. ret.value.isCompleted shouldBe true ret.failOnShutdown.value.failed.futureValue.getCause shouldBe boooh + writer.health.getState.isFatal shouldBe true } }, _.errorMessage should include("Background write failed"), diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessorTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessorTest.scala index a182bbb668..066aa343a8 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessorTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessorTest.scala @@ -4,7 +4,8 @@ package com.digitalasset.canton.synchronizer.block.update import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.config.{BatchingConfig, ProcessingTimeout} +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.{CloseContext, FlagCloseable} import com.digitalasset.canton.sequencing.protocol.{ @@ -55,12 +56,15 @@ class BlockChunkProcessorTest extends AsyncWordSpec with BaseTest { syncCryptoApiFake, sequencerId, rateLimitManagerMock, - OrderingTimeFixMode.ValidateOnly, - None, - BatchingConfig(), - loggerFactory, + BlockProcessingParameters( + OrderingTimeFixMode.ValidateOnly, + None, + parallelism = PositiveInt.two, + enablePrevalidation = true, + ), SequencerTestMetrics, memberValidatorMock, + loggerFactory, ) def emitTick( @@ -68,7 +72,7 @@ class BlockChunkProcessorTest extends AsyncWordSpec with BaseTest { ) = blockChunkProcessor .emitTick( - state = BlockUpdateGeneratorImpl.State( + state = BlockUpdateGenerator.AccumulatedStateProcessingBlocks( lastBlockTs = aTimestamp, lastChunkTs = aTimestamp, latestSequencerEventTimestamp = None, diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdateGeneratorImplTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdateGeneratorImplTest.scala index f0d894fe13..60fe6060c1 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdateGeneratorImplTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/block/update/BlockUpdateGeneratorImplTest.scala @@ -3,7 +3,8 @@ package com.digitalasset.canton.synchronizer.block.update -import com.digitalasset.canton.config.{BatchingConfig, ProcessingTimeout} +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.{CloseContext, FlagCloseable, FutureUnlessShutdown} import com.digitalasset.canton.sequencing.protocol.ProtocolObjectTestUtils.{ @@ -28,6 +29,7 @@ import com.digitalasset.canton.synchronizer.sequencer.traffic.SequencerRateLimit import com.digitalasset.canton.topology.DefaultTestIdentities.{physicalSynchronizerId, sequencerId} import com.digitalasset.canton.topology.TestingIdentityFactory import com.digitalasset.canton.tracing.{TraceContext, Traced} +import com.digitalasset.canton.util.TracedPossiblyPrevalidated import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.{BaseTest, HasExecutionContext, HasExecutorService} import org.scalatest.Assertion @@ -42,7 +44,7 @@ class BlockUpdateGeneratorImplTest with HasExecutorService with HasTopologyTransactionTestFactory { - implicit val closeContext: CloseContext = CloseContext( + implicit lazy val closeContext: CloseContext = CloseContext( FlagCloseable.withCloseContext(logger, ProcessingTimeout()) ) @@ -84,6 +86,13 @@ class BlockUpdateGeneratorImplTest } ) + private lazy val parameters = BlockProcessingParameters( + OrderingTimeFixMode.ValidateOnly, + lsuSequencingBounds = None, + parallelism = PositiveInt.two, + enablePrevalidation = true, + ) + "BlockUpdateGeneratorImpl.extractBlockEvents" should { "filter out events" when { "the sequencing time is before or at the minimum sequencing time" in { @@ -102,20 +111,23 @@ class BlockUpdateGeneratorImplTest syncCryptoApiFake, sequencerId, rateLimitManagerMock, - OrderingTimeFixMode.ValidateOnly, - lsuSequencingBounds = Some( - LsuSequencingBounds - .unsafeCreate( - sequencingTimeLowerBoundExclusive, - sequencingTimeLowerBoundExclusive, - ) - ), drSequencingTimeUpperBound = None, getAnnouncedLsu = None, producePostOrderingTopologyTicks = false, - SequencerTestMetrics, - BatchingConfig(), consistencyChecks = true, + parameters = BlockProcessingParameters( + OrderingTimeFixMode.ValidateOnly, + lsuSequencingBounds = Some( + LsuSequencingBounds + .unsafeCreate( + sequencingTimeLowerBoundExclusive, + sequencingTimeLowerBoundExclusive, + ) + ), + parallelism = PositiveInt.two, + enablePrevalidation = true, + ), + SequencerTestMetrics, memberValidatorMock, loggerFactory, ) @@ -225,14 +237,12 @@ class BlockUpdateGeneratorImplTest syncCryptoApiFake, sequencerId, rateLimitManagerMock, - OrderingTimeFixMode.ValidateOnly, - lsuSequencingBounds = None, drSequencingTimeUpperBound = None, getAnnouncedLsu = None, producePostOrderingTopologyTicks = false, - SequencerTestMetrics, - BatchingConfig(), consistencyChecks = true, + parameters = parameters, + SequencerTestMetrics, memberValidatorMock, loggerFactory, ) @@ -280,14 +290,12 @@ class BlockUpdateGeneratorImplTest ), sequencerId, mock[SequencerRateLimitManager], - OrderingTimeFixMode.ValidateOnly, - lsuSequencingBounds = None, drSequencingTimeUpperBound = None, getAnnouncedLsu = None, producePostOrderingTopologyTicks = false, - SequencerTestMetrics, - BatchingConfig(), consistencyChecks = true, + parameters = parameters, + SequencerTestMetrics, mock[SequencerMemberValidator], loggerFactory, ) @@ -327,8 +335,9 @@ class BlockUpdateGeneratorImplTest 1L, 0, Seq( - Traced( - LedgerBlockEvent.Send(`sequencerAddressedEventTimestamp`, _, _, _) + TracedPossiblyPrevalidated( + LedgerBlockEvent.Send(`sequencerAddressedEventTimestamp`, _, _, _), + _, ) ), ), @@ -359,14 +368,12 @@ class BlockUpdateGeneratorImplTest ), sequencerId, mock[SequencerRateLimitManager], - OrderingTimeFixMode.ValidateOnly, - lsuSequencingBounds = None, drSequencingTimeUpperBound = None, getAnnouncedLsu = None, producePostOrderingTopologyTicks = true, - SequencerTestMetrics, - BatchingConfig(), consistencyChecks = true, + parameters = parameters, + SequencerTestMetrics, mock[SequencerMemberValidator], loggerFactory, ) @@ -402,8 +409,9 @@ class BlockUpdateGeneratorImplTest 1L, 0, Seq( - Traced( - LedgerBlockEvent.Send(`sequencerAddressedEventTimestamp`, _, _, _) + TracedPossiblyPrevalidated( + LedgerBlockEvent.Send(`sequencerAddressedEventTimestamp`, _, _, _), + _, ) ), ), @@ -432,19 +440,17 @@ class BlockUpdateGeneratorImplTest ), sequencerId, mock[SequencerRateLimitManager], - OrderingTimeFixMode.ValidateOnly, - lsuSequencingBounds = None, drSequencingTimeUpperBound = None, getAnnouncedLsu = None, producePostOrderingTopologyTicks = true, - SequencerTestMetrics, - BatchingConfig(), consistencyChecks = true, + parameters = parameters, + SequencerTestMetrics, mock[SequencerMemberValidator], loggerFactory, ) - val state = BlockUpdateGeneratorImpl.State( + val state = BlockUpdateGenerator.AccumulatedStateProcessingBlocks( lastBlockTs = aTimestamp.immediatePredecessor, lastChunkTs = aTimestamp, latestSequencerEventTimestamp = None, diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ConfirmationRequestAndResponseProcessorTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ConfirmationRequestAndResponseProcessorTest.scala index ac5eb26ef0..7c9a2a601a 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ConfirmationRequestAndResponseProcessorTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ConfirmationRequestAndResponseProcessorTest.scala @@ -271,6 +271,8 @@ class ConfirmationRequestAndResponseProcessorTest result.asScala.map(_.batch).toList } + private val clock = mock[Clock] + val verdictSender: TestVerdictSender = new TestVerdictSender( syncCryptoApi, @@ -284,7 +286,7 @@ class ConfirmationRequestAndResponseProcessorTest val mediatorState = new MediatorState( new InMemoryFinalizedResponseStore(loggerFactory), new InMemoryMediatorDeduplicationStore(loggerFactory, timeouts), - mock[Clock], + clock, MediatorTestMetrics, testedProtocolVersion, timeouts, @@ -296,10 +298,6 @@ class ConfirmationRequestAndResponseProcessorTest syncCryptoApi, timeTracker, mediatorState, - // this test calls processRequest and processResponses directly, - // which is not affected by the asynchronous processing flag, which is handled in the enclosing scope - // calling these methods - asynchronousProcessing = true, loggerFactory, timeouts, BatchingConfig(), diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/DefaultVerdictSenderTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/DefaultVerdictSenderTest.scala index c30d66c165..43d9754962 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/DefaultVerdictSenderTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/DefaultVerdictSenderTest.scala @@ -4,11 +4,11 @@ package com.digitalasset.canton.synchronizer.mediator import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.config.BatchingConfig -import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, NonNegativeLong, PositiveInt} import com.digitalasset.canton.crypto.{Signature, SynchronizerCryptoClient} import com.digitalasset.canton.data.{CantonTimestamp, ViewType} import com.digitalasset.canton.error.MediatorError.MalformedMessage +import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} import com.digitalasset.canton.protocol.messages.{ DefaultOpenEnvelope, InformeeMessage, @@ -30,6 +30,7 @@ import com.digitalasset.canton.sequencing.protocol.{ OpenEnvelope, Recipients, } +import com.digitalasset.canton.time.Clock import com.digitalasset.canton.topology.MediatorGroup.MediatorGroupIndex import com.digitalasset.canton.topology.transaction.ParticipantPermission import com.digitalasset.canton.topology.{ @@ -42,11 +43,18 @@ import com.digitalasset.canton.topology.{ TestingTopology, UniqueIdentifier, } +import com.digitalasset.canton.version.HasTestCloseContext.makeTestCloseContext import com.digitalasset.canton.version.ProtocolVersion -import com.digitalasset.canton.{BaseTest, HasExecutionContext, ProtocolVersionChecksAsyncWordSpec} +import com.digitalasset.canton.{ + BaseTest, + HasExecutionContext, + ProtocolVersionChecksAsyncWordSpec, + config, +} import org.scalatest.wordspec.AsyncWordSpec -import scala.concurrent.Future +import java.time.Duration +import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* class DefaultVerdictSenderTest @@ -55,12 +63,13 @@ class DefaultVerdictSenderTest with HasExecutionContext with BaseTest { - private val activeMediator1 = MediatorId(UniqueIdentifier.tryCreate("mediator", "one")) - private val activeMediator2 = MediatorId(UniqueIdentifier.tryCreate("mediator", "two")) - private val passiveMediator3 = MediatorId(UniqueIdentifier.tryCreate("mediator", "three")) + private lazy implicit val testCloseContext: CloseContext = makeTestCloseContext(logger) + private lazy val activeMediator1 = MediatorId(UniqueIdentifier.tryCreate("mediator", "one")) + private lazy val activeMediator2 = MediatorId(UniqueIdentifier.tryCreate("mediator", "two")) + private lazy val passiveMediator3 = MediatorId(UniqueIdentifier.tryCreate("mediator", "three")) - private val mediatorGroupRecipient = MediatorGroupRecipient(MediatorGroupIndex.zero) - private val mediatorGroup: MediatorGroup = MediatorGroup( + private lazy val mediatorGroupRecipient = MediatorGroupRecipient(MediatorGroupIndex.zero) + private lazy val defaultMediatorGroup: MediatorGroup = MediatorGroup( index = mediatorGroupRecipient.group, active = Seq(activeMediator1, activeMediator2), passive = Seq( @@ -68,9 +77,9 @@ class DefaultVerdictSenderTest ), threshold = PositiveInt.tryCreate(2), ) - private val expectedMediatorGroupAggregationRule = Some( + private lazy val expectedMediatorGroupAggregationRule = Some( AggregationRule.activeMediators( - NonEmpty.mk(Seq, mediatorGroup.active(0), mediatorGroup.active.tail*), + NonEmpty.mk(Seq, defaultMediatorGroup.active(0), defaultMediatorGroup.active.tail*), NonNegativeInt.zero, PositiveInt.tryCreate(2), testedProtocolVersion, @@ -164,12 +173,61 @@ class DefaultVerdictSenderTest aggregationRule shouldBe expectedMediatorGroupAggregationRule } } + "delay responses" in { + // In this test, we set the threshold to 1. As the requestId is deterministic, we know + // that mediator2 will be the "spare mediator" and as such should submit the response with + // a delay of 250ms (as per configuration). + val tester = TestHelper( + mediatorId = activeMediator2, + transactionMediatorGroup = mediatorGroupRecipient, + mediatorGroup = defaultMediatorGroup.copy(threshold = PositiveInt.one), + ) + when( + tester.clock.scheduleAfterCancelledOnShutdown( + any[CantonTimestamp => Unit], + any[String], + any[Duration], + )(any[ExecutionContext], any[CloseContext]) + ) + .thenAnswer[CantonTimestamp => Unit, String, Duration] { case (_, _, duration) => + duration.toMillis shouldBe 250 + FutureUnlessShutdown.unit + } + tester.sendApproval() map { _ => + tester.interceptedMessages should have size 1 + verify(tester.clock, times(1)).scheduleAfterCancelledOnShutdown( + any[CantonTimestamp => Unit], + any[String], + any[Duration], + )(any[ExecutionContext], any[CloseContext]) + val (_, aggregationRule) = tester.interceptedMessages.loneElement + if (testedProtocolVersion > ProtocolVersion.v34) + aggregationRule shouldBe expectedMediatorGroupAggregationRule + succeed + } + } + "not delay responses if response deadline is approaching" in { + // Very similar to above but here we assume that the response deadline is approaching, + // so we fire using all cannons. + val tester = TestHelper( + mediatorId = activeMediator2, + transactionMediatorGroup = mediatorGroupRecipient, + mediatorGroup = defaultMediatorGroup.copy(threshold = PositiveInt.one), + immediateBeforeDeadline = + NonNegativeLong.tryCreate(120L), // request / now is epoch, deadline is epoch + 120s + ) + tester.sendApproval() map { _ => + tester.interceptedMessages should have size 1 + } + } } } private case class TestHelper( mediatorId: MediatorId, transactionMediatorGroup: MediatorGroupRecipient, + mediatorGroup: MediatorGroup = defaultMediatorGroup, + immediateBeforeDeadline: NonNegativeLong = NonNegativeLong.zero, ) { val psid: PhysicalSynchronizerId = SynchronizerId( @@ -206,6 +264,8 @@ class DefaultVerdictSenderTest val requestIdTs = CantonTimestamp.Epoch val requestId = RequestId(requestIdTs) val decisionTime = requestIdTs.plusSeconds(120) + val clock = mock[Clock] + when(clock.now).thenReturn(requestIdTs.plusSeconds(1)) val initialSynchronizerParameters = TestSynchronizerParameters.defaultDynamic @@ -260,7 +320,7 @@ class DefaultVerdictSenderTest } private val sequencerClientSend: TestSequencerClientSend = new TestSequencerClientSend( - wallClock + clock ) def interceptedMessages: Seq[(Batch[DefaultOpenEnvelope], Option[AggregationRule])] = @@ -272,7 +332,15 @@ class DefaultVerdictSenderTest sequencerClientSend, synchronizerSyncCryptoApi, mediatorId, - BatchingConfig(), + VerdictSenderParameters( + enableDelay = true, + livenessMargin = NonNegativeInt.zero, + immediateBeforeDeadline = + config.NonNegativeFiniteDuration.ofSeconds(immediateBeforeDeadline.value), + initialDelay = config.NonNegativeFiniteDuration.ofMillis(250), + delay = config.NonNegativeFiniteDuration.ofMillis(100), + parallelism = PositiveInt.two, + ), loggerFactory, ) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/MediatorEventDeduplicatorTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/MediatorEventDeduplicatorTest.scala index 9b7dcf9419..888a47beac 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/MediatorEventDeduplicatorTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/MediatorEventDeduplicatorTest.scala @@ -377,7 +377,7 @@ class MediatorEventDeduplicatorTest batch: Batch[DefaultOpenEnvelope], decisionTime: CantonTimestamp, aggregationRule: Option[AggregationRule], - sendVerdict: Boolean, + sendVerdictWithDelay: Option[Duration], )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = FutureUnlessShutdown.never diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ProcessingQueueTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ProcessingQueueTest.scala deleted file mode 100644 index 5d7ce6a61b..0000000000 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ProcessingQueueTest.scala +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.synchronizer.mediator - -import cats.syntax.parallel.* -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, PromiseUnlessShutdown} -import com.digitalasset.canton.util.LoggerUtil -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import org.scalatest.wordspec.AnyWordSpec - -import java.util.concurrent.atomic.{AtomicBoolean, AtomicLong} -import scala.concurrent.duration.* - -class ProcessingQueueTest extends AnyWordSpec with BaseTest with HasExecutionContext { - - "ProcessingQueue" should { - - "execute actions for the same ID sequentially" in { - val queue = new ShardedSequentialProcessingQueue[String] - val id = "request-1" - - val firstActionStarted = PromiseUnlessShutdown.unsupervised[Unit]() - val firstActionCanComplete = PromiseUnlessShutdown.unsupervised[Unit]() - val secondActionStarted = new AtomicBoolean(false) - - // Enqueue first action - val f1 = queue.enqueueForProcessing(id) { - firstActionStarted.outcome_(()) - firstActionCanComplete.futureUS - } - - // Ensure first action has started - firstActionStarted.futureUS.flatMap { _ => - // Enqueue second action - val f2 = queue.enqueueForProcessing(id) { - secondActionStarted.set(true) - FutureUnlessShutdown.unit - } - - // Verify second action hasn't started yet because f1 is pending - secondActionStarted.get() shouldBe false - - // Complete the first action - firstActionCanComplete.outcome_(()) - - for { - _ <- f1 - _ <- f2 - } yield { - secondActionStarted.get() shouldBe true - queue.processingQueuePerRequest.get(id) shouldBe None - } - }.futureValueUS - } - - "execute actions for different IDs concurrently" in { - val queue = new ShardedSequentialProcessingQueue[String] - val id1 = "id-1" - val id2 = "id-2" - - val id1Started = PromiseUnlessShutdown.unsupervised[Unit]() - val id1Blocked = PromiseUnlessShutdown.unsupervised[Unit]() - - // Start action for ID 1 (blocked) - val f1 = queue.enqueueForProcessing(id1) { - id1Started.outcome_(()) - id1Blocked.futureUS - } - - id1Started.futureUS.futureValueUS - // Action for ID 2 should be able to run and complete immediately even if ID 1 is blocked - queue.enqueueForProcessing(id2)(FutureUnlessShutdown.unit).futureValueUS - queue.processingQueuePerRequest.contains(id2) shouldBe false - - // ID 2 completed - id1Blocked.outcome_(()) // Now release ID 1 - f1.futureValueUS - queue.processingQueuePerRequest.contains(id1) shouldBe false - } - - "clean up the map entry only if it's the last future in the chain" in { - val queue = new ShardedSequentialProcessingQueue[String] - val id = "cleanup-test" - - val p1 = PromiseUnlessShutdown.unsupervised[Unit]() - val p2 = PromiseUnlessShutdown.unsupervised[Unit]() - - val f1 = queue.enqueueForProcessing(id)(p1.futureUS) - val f2 = queue.enqueueForProcessing(id)(p2.futureUS) - - // The map should contain the future for f2 (the latest one) - queue.processingQueuePerRequest.get(id) should not be empty - - p1.outcome_(()) - f1.futureValueUS - // After f1 completes, the map should STILL contain an entry because f2 is pending - queue.processingQueuePerRequest.get(id) should not be empty - - p2.outcome_(()) - f2.futureValueUS - // After f2 completes, the map should be empty - queue.processingQueuePerRequest.get(id) shouldBe None - } - - "stop processing the queue if a previous action fails" in { - val queue = new ShardedSequentialProcessingQueue[String] - val id = "error-test" - - val firstActionStarted = PromiseUnlessShutdown.unsupervised[Unit]() - val firstActionCompletes = PromiseUnlessShutdown.unsupervised[Unit]() - - val f1 = queue.enqueueForProcessing(id) { - firstActionStarted.outcome_(()) - firstActionCompletes.futureUS - } - - // Wait for the first action to start and then register another action - firstActionStarted.futureUS.futureValueUS - val f2executed = new AtomicBoolean(false) - val f2 = queue.enqueueForProcessing(id) { - FutureUnlessShutdown.unit.map(_ => f2executed.set(true)) - } - val exception = new RuntimeException("Boom!") - firstActionCompletes.failure(exception) - - f1.failed.futureValueUS shouldBe exception - // f2 should fail with the same exception - f1.failed.futureValueUS shouldBe f2.failed.futureValueUS - // and f2 should not have been run - f2executed.get() shouldBe false - // but still the queue should be cleaned up. - queue.processingQueuePerRequest.get(id) shouldBe None - } - } - - "not deadlock" in { - val processingQueue = new ShardedSequentialProcessingQueue[Int] - val counter = new AtomicLong(1000) - val workF = (1 to counter.intValue()).toList.parTraverse { _ => - processingQueue.enqueueForProcessing(0) { - FutureUnlessShutdown.unit.map(_ => counter.decrementAndGet().discard) - } - } - workF.futureValueUS - counter.get shouldBe 0L - } - - "scale linearly with a large number of unique IDs" in { - val queue = new ShardedSequentialProcessingQueue[Int] - val numUniqueIds = 10000 - val actionsPerId = 5 - - val startTime = System.nanoTime() - - // Dispatch 50,000 total actions across 10,000 unique IDs - val allTasks = (1 to numUniqueIds).toList.parTraverse_ { id => - (1 to actionsPerId).toList.parTraverse_ { _ => - queue.enqueueForProcessing(id)(FutureUnlessShutdown.unit) - } - } - - allTasks.futureValueUS - val duration = (System.nanoTime() - startTime).nanos - - // Basic validation: All map entries should be cleared - queue.processingQueuePerRequest shouldBe empty - - // Optional: Log performance metrics - logger.info( - s"Processed ${numUniqueIds * actionsPerId} tasks across $numUniqueIds IDs in ${LoggerUtil - .roundDurationForHumans(duration)}" - ) - - // Ensure it doesn't take an unreasonable amount of time (e.g., > 10s for 50k simple tasks) - // On a Macbook Pro M2 Max, this takes <1 s - duration should be < 10.seconds - } - -} diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ShardedSequentialProcessingQueueTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ShardedSequentialProcessingQueueTest.scala new file mode 100644 index 0000000000..69eeb2c335 --- /dev/null +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ShardedSequentialProcessingQueueTest.scala @@ -0,0 +1,317 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.synchronizer.mediator + +import cats.syntax.parallel.* +import com.digitalasset.canton.concurrent.FutureSupervisor +import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, PromiseUnlessShutdown} +import com.digitalasset.canton.logging.pretty.Pretty +import com.digitalasset.canton.util.{ + FailureMode, + GarbageCollectedShardedSequentialProcessingQueue, + LoggerUtil, + NonGarbageCollectedShardedSequentialProcessingQueue, + ShardedSequentialProcessingQueue, +} +import com.digitalasset.canton.{BaseTest, HasExecutionContext} +import org.scalatest.wordspec.AnyWordSpec + +import java.util.concurrent.atomic.{AtomicBoolean, AtomicLong} +import scala.concurrent.duration.* + +sealed trait ShardedSequentialProcessingQueueTest + extends AnyWordSpec + with BaseTest + with HasExecutionContext { + protected def newQueue[Ident: Pretty](): ShardedSequentialProcessingQueue[Ident] + + import com.digitalasset.canton.logging.pretty.PrettyInstances.* + + protected implicit val pString: Pretty[String] = prettyString + + protected def processingQueue(): Unit = { + "execute actions for the same ID sequentially" in { + val queue = newQueue[String]() + val id = "request-1" + + val firstActionStarted = PromiseUnlessShutdown.unsupervised[Unit]() + val firstActionCanComplete = PromiseUnlessShutdown.unsupervised[Unit]() + val secondActionStarted = new AtomicBoolean(false) + + // Enqueue first action + val f1 = queue.executeUS(id)( + { + firstActionStarted.outcome_(()) + firstActionCanComplete.futureUS + }, + "task", + ) + + // Ensure first action has started + firstActionStarted.futureUS.flatMap { _ => + // Enqueue second action + val f2 = queue.executeUS(id)( + { + secondActionStarted.set(true) + FutureUnlessShutdown.unit + }, + "task", + ) + + // Verify second action hasn't started yet because f1 is pending + secondActionStarted.get() shouldBe false + + // Complete the first action + firstActionCanComplete.outcome_(()) + + for { + _ <- f1 + _ <- f2 + } yield { + secondActionStarted.get() shouldBe true + queue.isQueueEmpty(id) shouldBe true + } + }.futureValueUS + } + + "execute actions for different IDs concurrently" in { + val queue = newQueue[String]() + val id1 = "id-1" + val id2 = "id-2" + + val id1Started = PromiseUnlessShutdown.unsupervised[Unit]() + val id1Blocked = PromiseUnlessShutdown.unsupervised[Unit]() + + // Start action for ID 1 (blocked) + val f1 = queue.executeUS(id1)( + { + id1Started.outcome_(()) + id1Blocked.futureUS + }, + "task", + ) + + id1Started.futureUS.futureValueUS + // Action for ID 2 should be able to run and complete immediately even if ID 1 is blocked + queue.executeUS(id2)(FutureUnlessShutdown.unit, "task").futureValueUS + queue.isQueueEmpty(id2) shouldBe true + + // ID 2 completed + id1Blocked.outcome_(()) // Now release ID 1 + f1.futureValueUS + queue.isQueueEmpty(id1) shouldBe true + } + + "not deadlock" in { + val processingQueue = newQueue[Int]() + val counter = new AtomicLong(1000) + val workF = (1 to counter.intValue()).toList.parTraverse { _ => + processingQueue.executeUS(0)( + FutureUnlessShutdown.unit.map(_ => counter.decrementAndGet().discard), + "task", + ) + } + workF.futureValueUS + counter.get shouldBe 0L + } + } +} + +final class GarbageCollectedShardedSequentialProcessingQueueTest + extends ShardedSequentialProcessingQueueTest { + + override protected def newQueue[Ident: Pretty](): ShardedSequentialProcessingQueue[Ident] = + new GarbageCollectedShardedSequentialProcessingQueue[Ident]() + + "GarbageCollectedShardedSequentialProcessingQueue" should { + behave like processingQueue() + + "stop processing the queue if a previous action fails" in { + val queue = new GarbageCollectedShardedSequentialProcessingQueue[String]() + val id = "error-test" + + val firstActionStarted = PromiseUnlessShutdown.unsupervised[Unit]() + val firstActionCompletes = PromiseUnlessShutdown.unsupervised[Unit]() + + val f1 = queue.executeUS(id)( + { + firstActionStarted.outcome_(()) + firstActionCompletes.futureUS + }, + "task", + ) + + // Wait for the first action to start and then register another action + firstActionStarted.futureUS.futureValueUS + val f2executed = new AtomicBoolean(false) + val f2 = queue.executeUS(id)( + FutureUnlessShutdown.unit.map(_ => f2executed.set(true)), + "task", + ) + val exception = new RuntimeException("Boom!") + firstActionCompletes.failure(exception) + + f1.failed.futureValueUS shouldBe exception + // f2 should fail with the same exception + f1.failed.futureValueUS shouldBe f2.failed.futureValueUS + // and f2 should not have been run + f2executed.get() shouldBe false + // but still the queue should be cleaned up. + queue.processingQueuePerId.get(id) shouldBe None + + } + + "clean up the map entry only if it's the last future in the chain" in { + val queue = new GarbageCollectedShardedSequentialProcessingQueue[String]() + val id = "cleanup-test" + + val p1 = PromiseUnlessShutdown.unsupervised[Unit]() + val p2 = PromiseUnlessShutdown.unsupervised[Unit]() + + val f1 = queue.executeUS(id)(p1.futureUS, "task") + val f2 = queue.executeUS(id)(p2.futureUS, "task") + + // The map should contain the future for f2 (the latest one) + queue.processingQueuePerId.get(id) should not be empty + + p1.outcome_(()) + f1.futureValueUS + // After f1 completes, the map should STILL contain an entry because f2 is pending + queue.processingQueuePerId.get(id) should not be empty + + p2.outcome_(()) + f2.futureValueUS + // After f2 completes, the map should be empty + queue.processingQueuePerId.get(id) shouldBe None + } + + "scale linearly with a large number of unique IDs" in { + val queue = new GarbageCollectedShardedSequentialProcessingQueue[Int] + val numUniqueIds = 10000 + val actionsPerId = 5 + + val startTime = System.nanoTime() + + // Dispatch 50,000 total actions across 10,000 unique IDs + val allTasks = (1 to numUniqueIds).toList.parTraverse_ { id => + (1 to actionsPerId).toList.parTraverse_ { _ => + queue.executeUS(id)(FutureUnlessShutdown.unit, "task") + } + } + + allTasks.futureValueUS + val duration = (System.nanoTime() - startTime).nanos + + // Basic validation: All map entries should be cleared + queue.processingQueuePerId shouldBe empty + + // Optional: Log performance metrics + logger.info( + s"Processed ${numUniqueIds * actionsPerId} tasks across $numUniqueIds IDs in ${LoggerUtil + .roundDurationForHumans(duration)}" + ) + + // Ensure it doesn't take an unreasonable amount of time (e.g., > 10s for 50k simple tasks) + // On a Macbook Pro M2 Max, this takes <1 s + duration should be < 10.seconds + } + } + +} + +final class NonGarbageCollectedShardedSequentialProcessingQueueTest + extends ShardedSequentialProcessingQueueTest { + + override protected def newQueue[Ident: Pretty](): ShardedSequentialProcessingQueue[Ident] = + new NonGarbageCollectedShardedSequentialProcessingQueue[Ident]( + name = "test-queue", + futureSupervisor = FutureSupervisor.Noop, + timeouts = timeouts, + loggerFactory = loggerFactory, + logTaskTiming = false, + failureMode = FailureMode.StopAfterFailure, + ) + + "ShardedSequentialProcessingQueue" should { + behave like processingQueue() + + "stop processing the queue if a previous action fails" in { + val queue = newQueue[String]() + val id = "error-test" + + val firstActionStarted = PromiseUnlessShutdown.unsupervised[Unit]() + val firstActionCompletes = PromiseUnlessShutdown.unsupervised[Unit]() + + val f1 = queue.executeUS(id)( + { + firstActionStarted.outcome_(()) + firstActionCompletes.futureUS + }, + "first task", + ) + + // Wait for the first action to start and then register another action + firstActionStarted.futureUS.futureValueUS + val f2executed = new AtomicBoolean(false) + val f2 = queue.executeUS(id)( + FutureUnlessShutdown.unit.map(_ => f2executed.set(true)), + "second task", + ) + + loggerFactory.assertLogs( + { + val exception = new RuntimeException("Boom!") + firstActionCompletes.failure(exception) + + f1.failed.futureValueUS shouldBe exception + // f2 should fail with the same exception + f1.failed.futureValueUS shouldBe f2.failed.futureValueUS + // and f2 should not have been run + f2executed.get() shouldBe false + }, + _.errorMessage shouldBe "Task 'second task' will not run because of failure of previous task", + ) + + } + + "allow to continue processing the queue if a previous action fails" in { + val queue = new NonGarbageCollectedShardedSequentialProcessingQueue[String]( + name = "test-queue", + futureSupervisor = FutureSupervisor.Noop, + timeouts = timeouts, + loggerFactory = loggerFactory, + logTaskTiming = false, + failureMode = FailureMode.ContinueAfterFailure, + ) + val id = "error-test" + + val firstActionStarted = PromiseUnlessShutdown.unsupervised[Unit]() + val firstActionCompletes = PromiseUnlessShutdown.unsupervised[Unit]() + + val f1 = queue.executeUS(id)( + { + firstActionStarted.outcome_(()) + firstActionCompletes.futureUS + }, + "first task", + ) + + // Wait for the first action to start and then register another action + firstActionStarted.futureUS.futureValueUS + val f2executed = new AtomicBoolean(false) + val f2 = queue.executeUS(id)( + FutureUnlessShutdown.unit.map(_ => f2executed.set(true)), + "second task", + ) + + val exception = new RuntimeException("Boom!") + firstActionCompletes.failure(exception) + + f1.failed.futureValueUS shouldBe exception + f2.futureValueUS + f2executed.get() shouldBe true + } + } +} diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/TestVerdictSender.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/TestVerdictSender.scala index fe857cc70c..ea7f65ff25 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/TestVerdictSender.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/TestVerdictSender.scala @@ -3,10 +3,11 @@ package com.digitalasset.canton.synchronizer.mediator -import com.digitalasset.canton.config.BatchingConfig +import com.digitalasset.canton.config +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.crypto.SynchronizerCryptoClient import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.protocol.RequestId import com.digitalasset.canton.protocol.messages.* @@ -25,12 +26,19 @@ class TestVerdictSender( mediatorId: MediatorId, sequencerSend: SequencerClientSend, loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) +)(implicit executionContext: ExecutionContext, closeContext: CloseContext) extends DefaultVerdictSender( sequencerSend, crypto, mediatorId, - BatchingConfig(), + VerdictSenderParameters( + enableDelay = true, + livenessMargin = NonNegativeInt.zero, + immediateBeforeDeadline = config.NonNegativeFiniteDuration.ofSeconds(1), + initialDelay = config.NonNegativeFiniteDuration.ofSeconds(1), + delay = config.NonNegativeFiniteDuration.ofSeconds(1), + parallelism = PositiveInt.two, + ), loggerFactory, ) { diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/BaseSequencerTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/BaseSequencerTest.scala index cb33393d4f..f5a28a1666 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/BaseSequencerTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/BaseSequencerTest.scala @@ -110,6 +110,7 @@ class BaseSequencerTest extends AsyncWordSpec with BaseTest with FailOnShutdown EitherTUtil.unitUS }, testedProtocolVersion, + lsuSequencingBounds = None, disableSubmissionChecksForTesting = false, ) with FlagCloseable { diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/SequencerApiTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/SequencerApiTest.scala index a85caee546..51d957a9f9 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/SequencerApiTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/SequencerApiTest.scala @@ -34,6 +34,7 @@ import com.digitalasset.canton.time.{Clock, SimClock} import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.util.{ErrorUtil, PekkoUtil} +import com.digitalasset.canton.version.ProtocolVersion import com.google.protobuf.ByteString import com.google.rpc.status.Status import org.apache.pekko.actor.ActorSystem @@ -46,6 +47,7 @@ import org.slf4j.event.Level import java.time.Duration import java.util.UUID import scala.annotation.nowarn +import scala.concurrent.Promise import scala.concurrent.duration.{DurationInt, FiniteDuration} abstract class SequencerApiTest @@ -328,6 +330,8 @@ abstract class SequencerApiTest } def testAggregation: Boolean = supportAggregation + def testAggregationPV35: Boolean = + supportAggregation && testedProtocolVersion > ProtocolVersion.v34 "aggregate submission requests" onlyRunWhen testAggregation in { env => import env.* @@ -400,7 +404,7 @@ abstract class SequencerApiTest } } - "bounce on write path aggregate submissions with maxSequencingTime exceeding bound" onlyRunWhen testAggregation in { + "bounce on write path aggregate submissions with maxSequencingTime exceeding bound" onlyRunWhen testAggregationPV35 in { env => import env.* @@ -453,6 +457,39 @@ abstract class SequencerApiTest } } + "bounce on write path aggregate submissions dedup" onlyRunWhen testAggregationPV35 in { env => + import env.* + + val messageContent = "bounce-sender-dedup-message" + // TODO(i10412): See above + val aggregationRule = + AggregationRule.senderDedup(p6, testedProtocolVersion) + + val request1 = createSendRequest( + p6, + messageContent, + Recipients.cc(p10), + maxSequencingTime = CantonTimestamp.Epoch.add(Duration.ofMinutes(1)), + aggregationRule = Some(aggregationRule), + ) + + val signed = sign(request1) + for { + // first succeeds + _ <- sequencer + .sendAsyncSigned(signed) + .valueOrFail("Sent async for participant1") + _ <- readForMembers(Seq(p6), sequencer) + // second bounces + deduped <- sequencer + .sendAsyncSigned(signed) + .leftOrFail("A sendAsync of duplicate aggregation submission") + } yield { + deduped.code.id shouldBe SequencerErrors.AggregateSubmissionAlreadySent.id + succeed + } + } + "bounce on read path aggregate submissions with maxSequencingTime exceeding bound" onlyRunWhen testAggregation in { env => import env.* @@ -546,36 +583,45 @@ abstract class SequencerApiTest testedProtocolVersion, ) + val lockP = Promise[Unit]() + for { envs1 <- envelopes.parTraverse(signEnvelope(p11Crypto, _)) request1 = mkRequest(p11, messageId1, envs1) envs2 <- envelopes.parTraverse(signEnvelope(p12Crypto, _)) request2 = mkRequest(p12, messageId2, envs2) + envs3 <- envelopes.parTraverse(signEnvelope(p13Crypto, _)) + request3 = mkRequest(p13, messageId3, envs3) _ <- sequencer .sendAsyncSigned(sign(request1)) .valueOrFail("Sent async for participant11") reads11 <- readForMembers(Seq(p11), sequencer) + _ = sequencer.applyPostProcessingLockForTesting(lockP.future) _ <- sequencer .sendAsyncSigned(sign(request2)) .valueOrFail("Sent async for participant13") + _ <- sequencer + .sendAsyncSigned(sign(request3)) + .valueOrFail("Sent async for participant13") + _ = lockP.success(()) reads12 <- readForMembers(Seq(p12, p13), sequencer) reads12a <- readForMembers( Seq(p11), sequencer, startTimestamp = firstEventTimestamp(p11)(reads11).map(_.immediateSuccessor), ) - - // participant13 is late to the party and its request is refused - envs3 <- envelopes.parTraverse(signEnvelope(p13Crypto, _)) - request3 = mkRequest(p13, messageId3, envs3) - _ <- sequencer - .sendAsyncSigned(sign(request3)) - .valueOrFail("Sent async for participant13") reads13 <- readForMembers( Seq(p13), sequencer, startTimestamp = firstEventTimestamp(p13)(reads12).map(_.immediateSuccessor), ) + // if participant13 sends after processing, he'll see the already sent error + _ <- + if (testAggregationPV35) + sequencer + .sendAsyncSigned(sign(request3)) + .leftOrFail("Send async should fail with already sent") + else FutureUnlessShutdown.unit } yield { checkMessages( Seq( @@ -627,6 +673,11 @@ abstract class SequencerApiTest include(s"The aggregatable request with aggregation ID") and include("was previously delivered at") ) + case SequencerErrors.AggregateSubmissionAlreadySentV2(reason) => + reason should ( + include(s"The aggregatable request with aggregation ID") and + include("was previously delivered at") + ) } } } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerTest.scala index 0e504b157e..41b257525d 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerTest.scala @@ -5,7 +5,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block import cats.data.EitherT import com.digitalasset.canton.concurrent.FutureSupervisor -import com.digitalasset.canton.config.RequireTypes.PositiveDouble +import com.digitalasset.canton.config.RequireTypes.{PositiveDouble, PositiveInt} import com.digitalasset.canton.config.{ CachingConfigs, DefaultProcessingTimeouts, @@ -15,6 +15,7 @@ import com.digitalasset.canton.config.{ import com.digitalasset.canton.crypto.SynchronizerCryptoClient import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.environment.CantonNodeParameters +import com.digitalasset.canton.health.HealthComponent import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.TracedLogger import com.digitalasset.canton.resource.MemoryStorage @@ -29,6 +30,7 @@ import com.digitalasset.canton.synchronizer.block.BlockSequencerStateManager.Chu import com.digitalasset.canton.synchronizer.block.data.memory.InMemorySequencerBlockStore import com.digitalasset.canton.synchronizer.block.data.{BlockEphemeralState, BlockInfo} import com.digitalasset.canton.synchronizer.block.update.{ + BlockProcessingParameters, BlockUpdate, BlockUpdateGenerator, OrderedBlockUpdate, @@ -192,11 +194,12 @@ final class BlockSequencerTest health = None, clock = new SimClock(loggerFactory = loggerFactory), blockRateLimitManager = defaultRateLimiter, - orderingTimeFixMode = OrderingTimeFixMode.MakeStrictlyIncreasing, - lsuSequencingBounds = None, - metrics = SequencerMetrics.noop(this.getClass.getName), - loggerFactory = loggerFactory, - runtimeReady = FutureUnlessShutdown.unit, + blockProcessingParameters = BlockProcessingParameters( + orderingTimeFixMode = OrderingTimeFixMode.MakeStrictlyIncreasing, + lsuSequencingBounds = None, + parallelism = PositiveInt.two, + enablePrevalidation = true, + ), parameters = SequencerNodeParameters( general = MockedNodeParameters.cantonNodeParameters( ProcessingTimeout() @@ -210,8 +213,13 @@ final class BlockSequencerTest asyncWriter = AsyncWriterParameters(), timeAdvancingTopology = TimeAdvancingTopologyConfig(), delayRequestsBeforeLsuTrafficInit = false, + enableRejectDeliveredAggregationsOnPv35 = Seq.empty, lsuConfig = SequencerLsuConfig(), + enablePrevalidation = true, ), + metrics = SequencerMetrics.noop(this.getClass.getName), + loggerFactory = loggerFactory, + runtimeReady = FutureUnlessShutdown.unit, ) override def close(): Unit = { @@ -288,12 +296,21 @@ final class BlockSequencerTest ): Flow[Traced[BlockUpdate], Traced[CantonTimestamp], NotUsed] = Flow[Traced[BlockUpdate]].map(_.map(_ => CantonTimestamp.MinValue)) - override def getHeadState: BlockSequencerStateManager.HeadState = - BlockSequencerStateManager.HeadState( + override def getPersistenceHeadState + : BlockSequencerStateManager.AccumulatedStatePersistingBlocks = + BlockSequencerStateManager.AccumulatedStatePersistingBlocks( BlockInfo.initial, ChunkState.initial(BlockEphemeralState.empty), ) + override def getProcessingHeadState: BlockUpdateGenerator.AccumulatedStateProcessingBlocks = ??? + + override def asyncWriterHealth: HealthComponent = + new HealthComponent.AlwaysHealthyComponent( + "fake-block-sequencer-async-writer", + BlockSequencerTest.this.logger, + ) + override protected def timeouts: ProcessingTimeout = BlockSequencerTest.this.timeouts override protected def logger: TracedLogger = BlockSequencerTest.this.logger diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminServiceTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminServiceTest.scala index a851eddc57..2fbfc0c620 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminServiceTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminServiceTest.scala @@ -199,7 +199,13 @@ class BftOrderingSequencerAdminServiceTest extends AsyncWordSpec with BftSequenc val resultPromise = Promise[Consensus.Admin.GetOrderingTopologyResponse]() resultPromise.success( Consensus.Admin - .GetOrderingTopologyResponse(EpochNumber.First, Set.empty, SequencingParameters.Default) + .GetOrderingTopologyResponse( + EpochNumber.First, + Set.empty, + Seq.empty, + Seq.empty, + SequencingParameters.Default, + ) ) val bftOrderingSequencerAdminService = new BftOrderingSequencerAdminService( diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/PekkoBlockSubscriptionTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/PekkoBlockSubscriptionTest.scala index 61efecf512..2bc8a821bd 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/PekkoBlockSubscriptionTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/PekkoBlockSubscriptionTest.scala @@ -3,11 +3,20 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.canton.sequencing +import com.daml.metrics.api.MetricsContext +import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.synchronizer.block.BlockFormat -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.BftSequencerBaseTest +import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.pekko.PekkoModuleSystem.PekkoEnv +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig.SequencerCoreSubscriptionConfig import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.PekkoBlockSubscription +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.ModuleRef import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.BlockNumber +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.Output +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.{ + BftSequencerBaseTest, + fakeModuleExpectingSilence, +} import com.digitalasset.canton.{HasActorSystem, HasExecutionContext} import org.apache.pekko.Done import org.apache.pekko.stream.Materializer @@ -20,12 +29,21 @@ class PekkoBlockSubscriptionTest with HasExecutionContext { "PekkoBlockSubscription" should { + "give blocks in correct order" in { - val blockSubscription = new PekkoBlockSubscription[PekkoEnv]( - BlockNumber(0), - timeouts, - loggerFactory, - )(x => fail(x))(parallelExecutionContext, implicitly[Materializer]) + val blockSubscription = + new PekkoBlockSubscription[PekkoEnv]( + BlockNumber(0), + () => fakeModuleExpectingSilence, + timeouts, + loggerFactory, + SequencerMetrics.noop(getClass.getSimpleName).bftOrdering, + SequencerCoreSubscriptionConfig( // Do not pause + pekkoQueueSourceBufferSize = 10000, + pauseOrdererThresholdBufferSize = 10000, + resumeOrdererThresholdBufferSize = 10000, + ), + )(x => fail(x))(parallelExecutionContext, implicitly[Materializer]) val numberOfBlocksToMake = 10000L @@ -48,5 +66,52 @@ class PekkoBlockSubscriptionTest subscriberF.futureValue shouldBe Done } + + "pause the orderer if the buffer size exceeds the pause threshold and resume it when it is lower to or equal than the " in { + val outputMock = mock[ModuleRef[Output.ProcessNewEpochTopologyMessagesIfPossible.type]] + val blockSubscription = + new PekkoBlockSubscription[PekkoEnv]( + BlockNumber(0), + () => outputMock, + timeouts, + loggerFactory, + SequencerMetrics.noop(getClass.getSimpleName).bftOrdering, + SequencerCoreSubscriptionConfig( // pause soon + pekkoQueueSourceBufferSize = 0, + pauseOrdererThresholdBufferSize = 1, + resumeOrdererThresholdBufferSize = 1, + ), + )(x => fail(x))(parallelExecutionContext, implicitly[Materializer]) + + always() { + blockSubscription.isSequencerCoreSlow shouldBe false + } + + val unblockConsumerPromise = scala.concurrent.Promise[Unit]() + blockSubscription.subscription().mapAsync(1)(_ => unblockConsumerPromise.future).run().discard + + // The Pekko queue source, even with no buffer and backpressure policy, + // takes a while to backpressure the producer by blocking the `offer` future + (0L until 100L) + .map { i => + BlockFormat.Block(i, 0, Seq.empty) + } + .foreach(blockSubscription.receiveBlock) + + eventually() { + blockSubscription.isSequencerCoreSlow shouldBe true + } + + unblockConsumerPromise.success(()) + + eventually() { + blockSubscription.isSequencerCoreSlow shouldBe false + } + verify(outputMock, timeout(millis = 1_000).atLeastOnce()) + .asyncSend(eqTo(Output.ProcessNewEpochTopologyMessagesIfPossible))( + anyTraceContext, + any[MetricsContext], + ) + } } } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/topology/CantonOrderingTopologyProviderTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/topology/CantonOrderingTopologyProviderTest.scala index 3b36dd54ab..56f3880260 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/topology/CantonOrderingTopologyProviderTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/topology/CantonOrderingTopologyProviderTest.scala @@ -85,6 +85,10 @@ class CantonOrderingTopologyProviderTest case HowLongToBlacklist.Linear(_) => HowLongToBlacklist.NoBlacklisting case HowLongToBlacklist.NoBlacklisting => HowLongToBlacklist.Linear(Some(1L)) + case _: HowLongToBlacklist.LinearWithParameters => + HowLongToBlacklist.NoBlacklisting + case _: HowLongToBlacklist.Exponential => + HowLongToBlacklist.NoBlacklisting } ) Table[Option[Long], Option[Long], SegmentLength, Option[ diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/PruningModuleTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/PruningModuleTest.scala index f3c7a7a4fc..b2ae763686 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/PruningModuleTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/PruningModuleTest.scala @@ -108,10 +108,16 @@ class PruningModuleTest extends AnyWordSpec with BftSequencerBaseTest { when(outputStore.getLowerBound()(traceContext)).thenReturn(() => Some(OutputMetadataStore.LowerBound(EpochNumber(10), BlockNumber(10))) ) + when(outputStore.getLastNonSequentialBlockMetadataStored(traceContext)).thenReturn(() => + Some(latestBlock) + ) module.receiveInternal(Pruning.Start) - context.runPipedMessages() should contain only Pruning.PerformPruning(EpochNumber(10)) + context.runPipedMessages() should contain only Pruning.PerformPruning( + EpochNumber(10), + EpochNumber(100), + ) } "do nothing on startup if no previous pruning point exists" in { @@ -122,6 +128,9 @@ class PruningModuleTest extends AnyWordSpec with BftSequencerBaseTest { val module = createPruningModule[ProgrammableUnitTestEnv](outputStore = outputStore) when(outputStore.getLowerBound()(traceContext)).thenReturn(() => None) + when(outputStore.getLastNonSequentialBlockMetadataStored(traceContext)).thenReturn(() => + None + ) module.receiveInternal(Pruning.Start) @@ -169,7 +178,10 @@ class PruningModuleTest extends AnyWordSpec with BftSequencerBaseTest { Pruning.ComputePruningPoint(latestBlock, retentionPeriod, minNumberOfBlocksToKeep) ) - context.runPipedMessages() should contain only Pruning.SaveNewLowerBound(EpochNumber(40)) + context.runPipedMessages() should contain only Pruning.SaveNewLowerBound( + EpochNumber(40), + EpochNumber(100), + ) } "when missing pruning point from retentionPeriod, prune nothing" in { @@ -228,15 +240,18 @@ class PruningModuleTest extends AnyWordSpec with BftSequencerBaseTest { when(outputStore.saveLowerBound(EpochNumber(40))(traceContext)).thenReturn(() => Right(())) - module.receiveInternal(Pruning.SaveNewLowerBound(EpochNumber(40))) - context.runPipedMessages() should contain only Pruning.PerformPruning(EpochNumber(40)) + module.receiveInternal(Pruning.SaveNewLowerBound(EpochNumber(40), EpochNumber(100))) + context.runPipedMessages() should contain only Pruning.PerformPruning( + EpochNumber(40), + EpochNumber(100), + ) } "perform pruning by pruning stores" in { implicit val context: ProgrammableUnitTestContext[Pruning.Message] = new ProgrammableUnitTestContext() val module = moduleReadyToBePruned() - module.receiveInternal(Pruning.PerformPruning(EpochNumber(40))) + module.receiveInternal(Pruning.PerformPruning(EpochNumber(40), EpochNumber(50))) context.runPipedMessages() shouldBe empty } @@ -268,7 +283,7 @@ class PruningModuleTest extends AnyWordSpec with BftSequencerBaseTest { context.lastDelayedMessage should contain((1, KickstartPruning(30 seconds, 100, None))) // kick-starting pruning without a promise indicates this is a scheduled operation module.receiveInternal(KickstartPruning(30 seconds, 100, None)) - module.receiveInternal(Pruning.PerformPruning(EpochNumber(40))) + module.receiveInternal(Pruning.PerformPruning(EpochNumber(40), EpochNumber(100))) context.runPipedMessages() should contain(SchedulePruning) } @@ -316,7 +331,7 @@ class PruningModuleTest extends AnyWordSpec with BftSequencerBaseTest { )(traceContext) ).thenReturn(() => AvailabilityStore.NumberOfRecords(10L)) - module.receiveInternal(Pruning.PerformPruning(EpochNumber(40))) + module.receiveInternal(Pruning.PerformPruning(EpochNumber(40), EpochNumber(100))) context.runPipedMessages() shouldBe empty requestPromise.isCompleted shouldBe true @@ -400,6 +415,7 @@ class PruningModuleTest extends AnyWordSpec with BftSequencerBaseTest { mock[EpochStoreReader[E]], outputStore, mock[BftOrdererPruningSchedulerStore[E]], + None, ) val pruning = new PruningModule[E]( stores, diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleDisseminationTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleDisseminationTest.scala index 819022acdf..63cd86feb6 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleDisseminationTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleDisseminationTest.scala @@ -448,7 +448,7 @@ class AvailabilityModuleDisseminationTest log => { log.level shouldBe Level.WARN log.message should include regex - """Batch BatchId\(SHA-256:[^)]+\) from 'node1' contains more requests \(1\) than allowed \(0\), skipping""" + """Batch BatchId\([^)]+\) from 'node1' contains more requests \(1\) than allowed \(0\), skipping""" }, ) @@ -476,7 +476,7 @@ class AvailabilityModuleDisseminationTest log.level shouldBe Level.WARN val validTags = OrderingRequest.ValidTags.mkString(", ") log.message should include regex - """Batch BatchId\(SHA-256:[^)]+\) from 'node1' contains requests with invalid tags, """ + + """Batch BatchId\([^)]+\) from 'node1' contains requests with invalid tags, """ + s"valid tags are: \\($validTags\\); skipping" }, ) @@ -504,7 +504,7 @@ class AvailabilityModuleDisseminationTest log => { log.level shouldBe Level.WARN log.message should include regex ( - """Batch BatchId\(SHA-256:[^)]+\) from 'node1' contains one or more batches that exceed the maximum allowed request size bytes \(0\), skipping""" + """Batch BatchId\([^)]+\) from 'node1' contains one or more batches that exceed the maximum allowed request size bytes \(0\), skipping""" ) }, ) @@ -533,7 +533,7 @@ class AvailabilityModuleDisseminationTest log => { log.level shouldBe Level.WARN log.message should include regex - """Batch BatchId\(SHA-256:[^)]+\) from 'node1' contains an expired batch at epoch number 0 which is 500 epochs or more older than last known epoch 501, skipping""" + """Batch BatchId\([^)]+\) from 'node1' contains an expired batch at epoch number 0 which is 500 epochs or more older than last known epoch 501, skipping""" }, ) @@ -550,7 +550,7 @@ class AvailabilityModuleDisseminationTest log => { log.level shouldBe Level.WARN log.message should include regex - """Batch BatchId\(SHA-256:[^)]+\) from 'node1' contains a batch whose epoch number 1501 is too far in the future compared to last known epoch 501, skipping""" + """Batch BatchId\([^)]+\) from 'node1' contains a batch whose epoch number 1501 is too far in the future compared to last known epoch 501, skipping""" }, ) @@ -770,7 +770,7 @@ class AvailabilityModuleDisseminationTest ) // the batch got evicted - verify(availabilityStore).gc(Seq(ABatchId)) + verify(availabilityStore).gc(Map(anEpochNumber -> Set(ABatchId))) } } @@ -1022,14 +1022,17 @@ class AvailabilityModuleDisseminationTest ) ) + val newAcks = + ABatchDisseminationProgressNode0To6WithNode0Vote._2.acks + AvailabilityAck( + node1Ack.from, + node1Ack.signature, + ) disseminationProtocolState.disseminationProgress should - contain only ABatchDisseminationProgressNode0To6WithNonQuorumVotes._1 -> ABatchDisseminationProgressNode0To6WithNonQuorumVotes._2 + contain only ABatchDisseminationProgressNode0To6WithNode0Vote._1 -> ABatchDisseminationProgressNode0To6WithNode0Vote._2 .copy( - acks = - ABatchDisseminationProgressNode0To6WithNonQuorumVotes._2.acks + AvailabilityAck( - node1Ack.from, - node1Ack.signature, - ) + acks = newAcks, + previousAcks = + Some(newAcks), // Receiving acks cause a review in the most recent topology ) disseminationProtocolState.nextToBeProvidedToConsensus.maxBatchesPerProposal shouldBe None } @@ -1166,14 +1169,16 @@ class AvailabilityModuleDisseminationTest ) ) + val newAcks = ABatchDisseminationProgressNode0To6WithNode0Vote._2.acks + AvailabilityAck( + node1Ack.from, + node1Ack.signature, + ) disseminationProtocolState.disseminationProgress should - contain only ABatchDisseminationProgressNode0To6WithNonQuorumVotes._1 -> ABatchDisseminationProgressNode0To6WithNonQuorumVotes._2 + contain only ABatchDisseminationProgressNode0To6WithNode0Vote._1 -> ABatchDisseminationProgressNode0To6WithNode0Vote._2 .copy( - acks = - ABatchDisseminationProgressNode0To6WithNonQuorumVotes._2.acks + AvailabilityAck( - node1Ack.from, - node1Ack.signature, - ) + acks = newAcks, + previousAcks = + Some(newAcks), // Receiving acks cause a review in the most recent topology ) disseminationProtocolState.nextToBeProvidedToConsensus shouldBe ANextToBeProvidedToConsensus } @@ -1223,16 +1228,18 @@ class AvailabilityModuleDisseminationTest ) ) + val acks = + disseminationProgress._2.acks + AvailabilityAck( + node1Ack.from, + node1Ack.signature, + ) val expectedDisseminationProgress = disseminationProgress._1 -> disseminationProgress._2 // Verifying an inbound ack advances progress, which re-syncs `sentTo` with the acks received .copy( - acks = - ABatchDisseminationProgressNode0To6WithNonQuorumVotes._2.acks + AvailabilityAck( - node1Ack.from, - node1Ack.signature, - ) + acks = acks, + previousAcks = Some(acks), ) disseminationProtocolState.disseminationProgress should contain only expectedDisseminationProgress disseminationProtocolState.nextToBeProvidedToConsensus shouldBe ANextToBeProvidedToConsensus diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleOutputFetchTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleOutputFetchTest.scala index 82f894f1e3..aa5a628d37 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleOutputFetchTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleOutputFetchTest.scala @@ -8,6 +8,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.BftSeque import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.integration.canton.crypto.CryptoProvider import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.integration.canton.crypto.CryptoProvider.AuthenticatedMessageType import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore.BatchIdAndEpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.{ FakePipeToSelfCellUnitTestContext, FakePipeToSelfCellUnitTestEnv, @@ -25,6 +26,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.{ OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.* import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.Availability.LocalDissemination.LocalBatchStoredSigned @@ -66,7 +68,7 @@ class AvailabilityModuleOutputFetchTest availability.receive( LocalOutputFetch.FetchBatchDataFromNodes( ProofOfAvailabilityNode1And2AcksNode1And2InTopology, - OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ) ) @@ -96,14 +98,14 @@ class AvailabilityModuleOutputFetchTest availability.receive( LocalOutputFetch.FetchBatchDataFromNodes( ProofOfAvailabilityNode1And2AcksNode1And2InTopology, - OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ) ) outputFetchProtocolState.localOutputMissingBatches should contain only ABatchId -> AMissingBatchStatusNode1And2AcksWithNode2ToTry outputFetchProtocolState.incomingBatchRequests should be(empty) context.delayedMessages should contain( - LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId) + LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId, anEpochNumber) ) p2pNetworkOutCell.get() shouldBe None @@ -112,7 +114,9 @@ class AvailabilityModuleOutputFetchTest p2pNetworkOutCell.get() should contain( P2PNetworkOut.Multicast( P2PNetworkOut.BftOrderingNetworkMessage.AvailabilityMessage( - RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, Node0).fakeSign + RemoteOutputFetch.FetchRemoteBatchData + .create(ABatchId, anEpochNumber, Node0) + .fakeSign ), Set(Node1), ) @@ -130,7 +134,9 @@ class AvailabilityModuleOutputFetchTest val availability = createAndStartAvailability[IgnoringUnitTestEnv]( outputFetchProtocolState = outputFetchProtocolState ) - availability.receive(RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, Node2)) + availability.receive( + RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, anEpochNumber, Node2) + ) outputFetchProtocolState.localOutputMissingBatches should be(empty) outputFetchProtocolState.incomingBatchRequests should contain only ABatchId -> Set( @@ -152,13 +158,17 @@ class AvailabilityModuleOutputFetchTest outputFetchProtocolState = outputFetchProtocolState, availabilityStore = availabilityStore, ) - availability.receive(RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, Node1)) + availability.receive( + RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, anEpochNumber, Node1) + ) outputFetchProtocolState.localOutputMissingBatches should be(empty) outputFetchProtocolState.incomingBatchRequests should contain only ABatchId -> Set( Node1 ) - verify(availabilityStore).fetchBatches(Seq(ABatchId)) + verify(availabilityStore).fetchBatches( + Seq(BatchIdAndEpochNumber(ABatchId, anEpochNumber)) + ) } } @@ -378,7 +388,7 @@ class AvailabilityModuleOutputFetchTest log => { log.level shouldBe Level.WARN log.message should include regex ( - """Batch BatchId\(SHA-256:[^)]+\) from 'node1' contains more requests \(1\) than allowed \(0\), skipping""" + """Batch BatchId\([^)]+\) from 'node1' contains more requests \(1\) than allowed \(0\), skipping""" ) }, ) @@ -417,7 +427,9 @@ class AvailabilityModuleOutputFetchTest val availability = createAndStartAvailability[IgnoringUnitTestEnv]( outputFetchProtocolState = outputFetchProtocolState ) - availability.receive(LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId)) + availability.receive( + LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId, anEpochNumber) + ) outputFetchProtocolState.localOutputMissingBatches should be(empty) outputFetchProtocolState.incomingBatchRequests should be(empty) @@ -447,13 +459,15 @@ class AvailabilityModuleOutputFetchTest cryptoProvider = ProgrammableUnitTestEnv.noSignatureCryptoProvider, p2pNetworkOut = fakeCellModule(p2pNetworkOutCell), ) - availability.receive(LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId)) + availability.receive( + LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId, anEpochNumber) + ) outputFetchProtocolState.localOutputMissingBatches should contain only ABatchId -> AMissingBatchStatusNode1And2AcksWithNoAttemptsLeft outputFetchProtocolState.incomingBatchRequests should be(empty) context.delayedMessages should contain( - LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId) + LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId, anEpochNumber) ) p2pNetworkOutCell.get() shouldBe None @@ -462,7 +476,9 @@ class AvailabilityModuleOutputFetchTest p2pNetworkOutCell.get() should contain( P2PNetworkOut.Multicast( P2PNetworkOut.BftOrderingNetworkMessage.AvailabilityMessage( - RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, Node0).fakeSign + RemoteOutputFetch.FetchRemoteBatchData + .create(ABatchId, anEpochNumber, Node0) + .fakeSign ), Set(Node1), ) @@ -506,7 +522,7 @@ class AvailabilityModuleOutputFetchTest val p2pNetworkOutCell = new AtomicReference[Option[P2PNetworkOut.Message]](None) val cryptoProvider = mock[CryptoProvider[ProgrammableUnitTestEnv]] val fetchRemoteBatchData = - RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, Node0) + RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, anEpochNumber, Node0) when( cryptoProvider.signMessage( fetchRemoteBatchData, @@ -527,7 +543,9 @@ class AvailabilityModuleOutputFetchTest p2pNetworkOut = fakeCellModule(p2pNetworkOutCell), ) loggerFactory.assertLoggedWarningsAndErrorsSeq( - availability.receive(LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId)), + availability.receive( + LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId, anEpochNumber) + ), forEvery(_) { entry => entry.message should include("got fetch timeout") entry.message should include("no nodes") @@ -539,7 +557,7 @@ class AvailabilityModuleOutputFetchTest contain only ABatchId -> newMissingBatchStatus outputFetchProtocolState.incomingBatchRequests should be(empty) context.delayedMessages should contain( - LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId) + LocalOutputFetch.FetchRemoteBatchDataTimeout(ABatchId, anEpochNumber) ) p2pNetworkOutCell.get() shouldBe None @@ -553,7 +571,9 @@ class AvailabilityModuleOutputFetchTest p2pNetworkOutCell.get() should contain( P2PNetworkOut.Multicast( P2PNetworkOut.BftOrderingNetworkMessage.AvailabilityMessage( - RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, Node0).fakeSign + RemoteOutputFetch.FetchRemoteBatchData + .create(ABatchId, anEpochNumber, Node0) + .fakeSign ), Set(expectedSendTo), ) @@ -604,7 +624,7 @@ class AvailabilityModuleOutputFetchTest Seq(Node1), numberOfAttempts = 1, jitterStream = jitterStream, - mode = OrderedBlockForOutput.Mode.FromConsensus, + orderingMode = OrderingMode.Consensus, ) ) val storage = TrieMap[BatchId, OrderingRequestBatch]() @@ -696,7 +716,7 @@ class AvailabilityModuleOutputFetchTest Seq(Node1), numberOfAttempts = 1, jitterStream = jitterStream, - mode = OrderedBlockForOutput.Mode.FromConsensus, + orderingMode = OrderingMode.Consensus, ) ) implicit val context @@ -710,7 +730,9 @@ class AvailabilityModuleOutputFetchTest cryptoProvider = ProgrammableUnitTestEnv.noSignatureCryptoProvider, ) val result = mock[AvailabilityStore.FetchBatchesResult] - when(availabilityStore.fetchBatches(Seq(ABatchId))) thenReturn (() => result) + when( + availabilityStore.fetchBatches(Seq(BatchIdAndEpochNumber(ABatchId, anEpochNumber))) + ) thenReturn (() => result) availability.receive(message) @@ -748,7 +770,7 @@ class AvailabilityModuleOutputFetchTest Seq(Node1), numberOfAttempts = 1, jitterStream = jitterStream, - mode = OrderedBlockForOutput.Mode.FromConsensus, + orderingMode = OrderingMode.Consensus, ) ) implicit val context @@ -815,11 +837,11 @@ class AvailabilityModuleOutputFetchTest "record the missing batches and ask other node for missing data" in { forAll( - Table[OrderedBlockForOutput.Mode, BftNodeId]( + Table[OrderingMode, BftNodeId]( ("block mode", "expected send to"), - (OrderedBlockForOutput.Mode.FromConsensus, Node1), + (OrderingMode.Consensus, Node1), // Ignore nodes from the PoA, use the current topology - (OrderedBlockForOutput.Mode.FromStateTransfer, Node3), + (OrderingMode.StateTransfer, Node3), ) ) { (blockMode, expectedSendTo) => val outputFetchProtocolState = new MainOutputFetchProtocolState() @@ -847,7 +869,7 @@ class AvailabilityModuleOutputFetchTest ViewNumber.First, isLastInEpoch = false, // Irrelevant for availability originalLeader = Node0, - mode = blockMode, + orderingMode = blockMode, ), mutable.SortedSet(ABatchId), traceContext, @@ -858,7 +880,9 @@ class AvailabilityModuleOutputFetchTest Availability.LocalOutputFetch .FetchedBlockDataFromStorage( request, - AvailabilityStore.MissingBatches(Set(ABatchId)), + AvailabilityStore.MissingBatches( + Set(BatchIdAndEpochNumber(ABatchId, anEpochNumber)) + ), ) ) @@ -875,7 +899,7 @@ class AvailabilityModuleOutputFetchTest P2PNetworkOut.send( P2PNetworkOut.BftOrderingNetworkMessage.AvailabilityMessage( Availability.RemoteOutputFetch.FetchRemoteBatchData - .create(ABatchId, from = Node0) + .create(ABatchId, anEpochNumber, from = Node0) .fakeSign ), expectedSendTo, @@ -907,7 +931,7 @@ class AvailabilityModuleOutputFetchTest availability.receive( Availability.LocalOutputFetch.FetchedBlockDataFromStorage( singleBatchMissingRequest, - AvailabilityStore.MissingBatches(Set(ABatchId)), + AvailabilityStore.MissingBatches(Set(BatchIdAndEpochNumber(ABatchId, anEpochNumber))), ) ), log => { @@ -941,7 +965,7 @@ class AvailabilityModuleOutputFetchTest ) availability.receive( - Availability.RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, Node0) + Availability.RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, anEpochNumber, Node0) ) cellContextFake.get() shouldBe defined @@ -970,7 +994,7 @@ class AvailabilityModuleOutputFetchTest ) availability.receive( - Availability.RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, Node0) + Availability.RemoteOutputFetch.FetchRemoteBatchData.create(ABatchId, anEpochNumber, Node0) ) cellContextFake.get() shouldBe defined diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleTestUtils.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleTestUtils.scala index d436146cd9..fe00a0945d 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleTestUtils.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/AvailabilityModuleTestUtils.scala @@ -10,6 +10,7 @@ import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.integration.canton.crypto.CryptoProvider import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.AvailabilityModule.quorum +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore.BatchIdAndEpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.memory.GenericInMemoryAvailabilityStore import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.{ BaseIgnoringUnitTestEnv, @@ -32,6 +33,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.{ OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.OrderingTopology.NodeTopologyInfo import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.{ @@ -105,6 +107,7 @@ private[availability] trait AvailabilityModuleTestUtils { self: BftSequencerBase anEpochNumber, ) protected val ABatchId = BatchId.from(ABatch) + protected val ABatchTuple = BatchIdAndEpochNumber(ABatchId, anEpochNumber) protected val ANonEmptyBatchId = BatchId.from(ANonEmptyBatch) protected val ABatchIdWithInvalidTags = BatchId.from(ABatchWithInvalidTags) protected val OrderingTopologyNode0 = OrderingTopology.forTesting(Set(Node0)) @@ -133,7 +136,7 @@ private[availability] trait AvailabilityModuleTestUtils { self: BftSequencerBase ), ViewNumber.First, isLastInEpoch = false, // Irrelevant for availability - mode = OrderedBlockForOutput.Mode.FromConsensus, + orderingMode = OrderingMode.Consensus, originalLeader = Node0, ) protected val AnotherOrderedBlockForOutput = OrderedBlockForOutput( @@ -147,7 +150,7 @@ private[availability] trait AvailabilityModuleTestUtils { self: BftSequencerBase ), ViewNumber.First, isLastInEpoch = false, // Irrelevant for availability - mode = OrderedBlockForOutput.Mode.FromConsensus, + orderingMode = OrderingMode.Consensus, originalLeader = Node0, ) protected val ACompleteBlock = CompleteBlockData( @@ -349,7 +352,7 @@ private[availability] trait AvailabilityModuleTestUtils { self: BftSequencerBase remainingNodesToTry = Seq(Node1), numberOfAttempts = 1, jitterStream = jitterStream, - mode = OrderedBlockForOutput.Mode.FromConsensus, + orderingMode = OrderingMode.Consensus, ) protected val AMissingBatchStatusNode1And2AcksWithNode2ToTry = AMissingBatchStatusNode1And2AcksWithNode1ToTry @@ -363,7 +366,7 @@ private[availability] trait AvailabilityModuleTestUtils { self: BftSequencerBase ABatchId -> AMissingBatchStatusNode1And2AcksWithNode1ToTry protected val AMissingBatchStatusFromStateTransferWithNoAttemptsLeft = AMissingBatchStatusNode1And2AcksWithNoAttemptsLeft - .copy(mode = OrderedBlockForOutput.Mode.FromStateTransfer) + .copy(orderingMode = OrderingMode.StateTransfer) protected val ANextToBeProvidedToConsensus = NextToBeProvidedToConsensus( BlockNumber.First, diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/BatchDisseminationNodeQuotaTrackerTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/BatchDisseminationNodeQuotaTrackerTest.scala index 76c7707ef6..42422c4007 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/BatchDisseminationNodeQuotaTrackerTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/BatchDisseminationNodeQuotaTrackerTest.scala @@ -106,7 +106,7 @@ class BatchDisseminationNodeQuotaTrackerTest extends AsyncWordSpec with BaseTest tracker.evictBatches(evictionEpoch = epoch1) shouldBe empty // evict batches from epoch 2 - tracker.evictBatches(evictionEpoch = epoch2) shouldBe Seq(batchId3) + tracker.evictBatches(evictionEpoch = epoch2) shouldBe Map(epoch2 -> Set(batchId3)) // nothing to evict from epoch 3 yet because it hasn't expired yet tracker.evictBatches(evictionEpoch = epoch3) shouldBe empty @@ -114,7 +114,7 @@ class BatchDisseminationNodeQuotaTrackerTest extends AsyncWordSpec with BaseTest tracker.expireEpoch(initialEpoch = epoch1, expirationEpoch = epoch3) // now that epoch is expired, we can evict from it - tracker.evictBatches(evictionEpoch = epoch3) shouldBe Seq(batchId4, batchId5) + tracker.evictBatches(evictionEpoch = epoch3) shouldBe Map(epoch3 -> Set(batchId4, batchId5)) } "still accept previously accepted batches even if quota is full" in { diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/AvailabilityStoreTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/AvailabilityStoreTest.scala index dbf3d5068b..e7589b2bda 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/AvailabilityStoreTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/AvailabilityStoreTest.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.mo import com.digitalasset.canton.synchronizer.block.BlockFormat import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.BftSequencerBaseTest import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.pekko.PekkoModuleSystem.PekkoEnv +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore.BatchIdAndEpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.EpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.availability.BatchId import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.{ @@ -29,11 +30,14 @@ trait AvailabilityStoreTest extends AsyncWordSpec with BftSequencerBaseTest { OrderingRequest(BlockFormat.SendTag, messageId = "mid3", ByteString.copyFromUtf8("payload3")) ) - private val batch1 = OrderingRequestBatch.create(Seq(request1, request3), EpochNumber.First) + private val batch1Epoch = EpochNumber.First + private val batch1 = OrderingRequestBatch.create(Seq(request1, request3), batch1Epoch) private val batch1Id = BatchId.from(batch1) - private val batch2 = OrderingRequestBatch.create(Seq(request2, request3), EpochNumber(1L)) + private val batch2Epoch = EpochNumber(1L) + private val batch2 = OrderingRequestBatch.create(Seq(request2, request3), batch2Epoch) private val batch2Id = BatchId.from(batch2) - private val batchEmpty = OrderingRequestBatch.create(Seq(), EpochNumber(2L)) + private val batchEmptyEpoch = EpochNumber(2L) + private val batchEmpty = OrderingRequestBatch.create(Seq(), batchEmptyEpoch) private val batchEmptyId = BatchId.from(batchEmpty) private val missingBatchId1 = BatchId.createForTesting("A missing batchId") @@ -41,11 +45,14 @@ trait AvailabilityStoreTest extends AsyncWordSpec with BftSequencerBaseTest { "AvailbilityStore" should { "fail retrieve non-inserted batchId" in { val store = createStore() - for { - fetchedBatch <- store.fetchBatches(Seq(missingBatchId1)) + fetchedBatch <- store.fetchBatches( + Seq(BatchIdAndEpochNumber(missingBatchId1, EpochNumber.First)) + ) } yield { - fetchedBatch shouldBe AvailabilityStore.MissingBatches(Set(missingBatchId1)) + fetchedBatch shouldBe AvailabilityStore.MissingBatches( + Set(BatchIdAndEpochNumber(missingBatchId1, EpochNumber.First)) + ) } } @@ -54,9 +61,11 @@ trait AvailabilityStoreTest extends AsyncWordSpec with BftSequencerBaseTest { for { _ <- store.addBatch(batch1Id, batch1) - fetchedBatch <- store.fetchBatches(Seq(batch1Id)) + fetchedBatch <- store.fetchBatches(Seq(BatchIdAndEpochNumber(batch1Id, batch1Epoch))) } yield { - fetchedBatch shouldBe AvailabilityStore.AllBatches(Seq(batch1Id -> batch1)) + fetchedBatch shouldBe AvailabilityStore.AllBatches( + Seq(batch1Id -> batch1) + ) } } @@ -65,9 +74,13 @@ trait AvailabilityStoreTest extends AsyncWordSpec with BftSequencerBaseTest { for { _ <- store.addBatch(batchEmptyId, batchEmpty) - fetchedBatch <- store.fetchBatches(Seq(batchEmptyId)) + fetchedBatch <- store.fetchBatches( + Seq(BatchIdAndEpochNumber(batchEmptyId, batchEmptyEpoch)) + ) } yield { - fetchedBatch shouldBe AvailabilityStore.AllBatches(Seq(batchEmptyId -> batchEmpty)) + fetchedBatch shouldBe AvailabilityStore.AllBatches( + Seq(batchEmptyId -> batchEmpty) + ) } } @@ -87,10 +100,20 @@ trait AvailabilityStoreTest extends AsyncWordSpec with BftSequencerBaseTest { _ <- store.addBatch(batch1Id, batch1) _ <- store.addBatch(batch2Id, batch2) _ <- store.addBatch(batchEmptyId, batchEmpty) - batches <- store.fetchBatches(Seq(batch1Id, batchEmptyId, batch2Id)) + batches <- store.fetchBatches( + Seq( + BatchIdAndEpochNumber(batch1Id, batch1Epoch), + BatchIdAndEpochNumber(batchEmptyId, batchEmptyEpoch), + BatchIdAndEpochNumber(batch2Id, batch2Epoch), + ) + ) } yield { batches shouldBe AvailabilityStore.AllBatches( - Seq(batch1Id -> batch1, batchEmptyId -> batchEmpty, batch2Id -> batch2) + Seq( + batch1Id -> batch1, + batchEmptyId -> batchEmpty, + batch2Id -> batch2, + ) ) } } @@ -101,9 +124,17 @@ trait AvailabilityStoreTest extends AsyncWordSpec with BftSequencerBaseTest { for { _ <- store.addBatch(batch1Id, batch1) _ <- store.addBatch(batchEmptyId, batchEmpty) - batches <- store.fetchBatches(Seq(batch1Id, batchEmptyId, missingBatchId1)) + batches <- store.fetchBatches( + Seq( + BatchIdAndEpochNumber(batch1Id, batch1Epoch), + BatchIdAndEpochNumber(batchEmptyId, batchEmptyEpoch), + BatchIdAndEpochNumber(missingBatchId1, EpochNumber.First), + ) + ) } yield { - batches shouldBe AvailabilityStore.MissingBatches(Set(missingBatchId1)) + batches shouldBe AvailabilityStore.MissingBatches( + Set(BatchIdAndEpochNumber(missingBatchId1, EpochNumber.First)) + ) } } @@ -112,9 +143,17 @@ trait AvailabilityStoreTest extends AsyncWordSpec with BftSequencerBaseTest { for { _ <- store.addBatch(batch1Id, batch1) - batches <- store.fetchBatches(Seq(missingBatchId1, batch1Id, batch1Id)) + batches <- store.fetchBatches( + Seq( + BatchIdAndEpochNumber(missingBatchId1, EpochNumber.First), + BatchIdAndEpochNumber(batch1Id, batch1Epoch), + BatchIdAndEpochNumber(batch1Id, batch1Epoch), + ) + ) } yield { - batches shouldBe AvailabilityStore.MissingBatches(Set(missingBatchId1)) + batches shouldBe AvailabilityStore.MissingBatches( + Set(BatchIdAndEpochNumber(missingBatchId1, EpochNumber.First)) + ) } } @@ -124,9 +163,11 @@ trait AvailabilityStoreTest extends AsyncWordSpec with BftSequencerBaseTest { for { _ <- store.addBatch(batch1Id, batch1) _ <- store.addBatch(batch1Id, batch2) - batches <- store.fetchBatches(Seq(batch1Id)) + batches <- store.fetchBatches(Seq(BatchIdAndEpochNumber(batch1Id, batch2Epoch))) } yield { - batches shouldBe AvailabilityStore.AllBatches(Seq(batch1Id -> batch1)) + batches shouldBe AvailabilityStore.AllBatches( + Seq(batch1Id -> batch1) + ) } } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/model/Command.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/model/Command.scala index 0ae897a114..50c12704d1 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/model/Command.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/model/Command.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.model +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore.BatchIdAndEpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.EpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.OrderingRequestBatch import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.availability.BatchId @@ -12,9 +13,9 @@ sealed trait Command object Command { final case class AddBatch(batchId: BatchId, batch: OrderingRequestBatch) extends Command - final case class FetchBatches(batches: Seq[BatchId]) extends Command + final case class FetchBatches(batches: Seq[BatchIdAndEpochNumber]) extends Command - final case class GC(staleBatchIds: Seq[BatchId]) extends Command + final case class GC(staleBatchIds: Map[EpochNumber, Set[BatchId]]) extends Command final case class Prune(epoch: EpochNumber) extends Command } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/model/Generator.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/model/Generator.scala index d1d7b46df7..909051337c 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/model/Generator.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/availability/data/model/Generator.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.model +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.AvailabilityStore.BatchIdAndEpochNumber import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.memory.InMemoryAvailabilityStore import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.model.Command.{ AddBatch, @@ -26,15 +27,27 @@ class Generator(random: Random, inMemoryStore: InMemoryAvailabilityStore) { type Gen[A] = Unit => A + private def randomBatchId = BatchId.createForTesting(random.nextLong().toString) + def genBatchId: Gen[BatchId] = _ => { if (inMemoryStore.isEmpty || random.nextBoolean()) { - BatchId.createForTesting(random.nextLong().toString) + randomBatchId } else { val ix = random.nextInt(inMemoryStore.size) inMemoryStore.keys.toSeq(ix) } } + def genStaleBatchIds: Gen[Map[EpochNumber, Set[BatchId]]] = _ => + if (inMemoryStore.isEmpty || random.nextBoolean()) + Map(genEpochNumber.apply(()) -> Set(randomBatchId)) + else { + val ix = random.nextInt(inMemoryStore.size) + val (batchId, OrderingRequestBatch(_, epochNumber)) = + inMemoryStore.allKnownBatchesById.toSeq(ix) + Map(epochNumber -> Set(batchId)) + } + def genSeq[A](gen: Gen[A]): Gen[Seq[A]] = _ => { (0 until random.nextInt(10)).map(_ => gen.apply(())) } @@ -66,12 +79,15 @@ class Generator(random: Random, inMemoryStore: InMemoryAvailabilityStore) { )(genSynchronizerProtocolVersion.apply(())) } + def genBatchIdAndEpochNumber: Gen[BatchIdAndEpochNumber] = _ => + BatchIdAndEpochNumber(genBatchId.apply(()), genEpochNumber.apply(())) + def generateCommand: Gen[Command] = _ => { random.nextInt(4) match { case 0 => - GC(genSeq(genBatchId).apply(())) + GC(genStaleBatchIds.apply(())) case 1 => - FetchBatches(genSeq(genBatchId).apply(())) + FetchBatches(genSeq(genBatchIdAndEpochNumber).apply(())) case 2 => Prune(genEpochNumber(())) case _ => diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssConsensusModuleTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssConsensusModuleTest.scala index d240867dde..c4d8deef9a 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssConsensusModuleTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/IssConsensusModuleTest.scala @@ -18,12 +18,15 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.mod BootstrapEpochNumber, bootstrapEpoch, } -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.EpochStore import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.EpochStore.{ Block, EpochInProgress, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.memory.GenericInMemoryEpochStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.{ + Bootstrap, + EpochStore, +} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.retransmissions.RetransmissionsManager import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.statetransfer.{ CatchupDetector, @@ -55,6 +58,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor CommitCertificate, OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.snapshot.{ NodeActiveAt, @@ -71,6 +75,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor PbftUnverifiedNetworkMessage, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.Consensus.{ + NewEpochStored, NewEpochTopology, ProtocolMessage, RetransmissionsMessage, @@ -794,7 +799,7 @@ class IssConsensusModuleTest prePrepare.viewNumber, leaderOfBlock, isLastBlockInEpoch, - OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ) ) ) @@ -961,7 +966,7 @@ class IssConsensusModuleTest succeed } - "start catch-up if the detector says so" in { + "start catch-up if the detector says so and no epoch is being stored" in { Table[ProtocolMessage]( "message", PbftUnverifiedNetworkMessage( @@ -1030,81 +1035,225 @@ class IssConsensusModuleTest } } - "not start catch-up if the detector says so but we are advancing epoch" in { - val epochOfFutureMessage = EpochNumber(7L) - val futureMessageToTriggerDetector = PbftUnverifiedNetworkMessage( - SignedMessage( - PrePrepare.create( // Just to trigger the catch-up check - BlockMetadata.mk(epochOfFutureMessage, BlockNumber(100L)), - ViewNumber.First, - OrderingBlock(oneRequestOrderingBlock.proofs), - CanonicalCommitSet(Set.empty), - from = allIds(1), - actualSender = Some(allIds(1)), + "not start catch-up" when { + "we get future messages that are retransmitted" in { + val futureMessageToTriggerDetector = PbftUnverifiedNetworkMessage( + SignedMessage( + PrePrepare.create( // Just to trigger the catch-up check + BlockMetadata.mk(EpochNumber(7L), BlockNumber(100L)), + ViewNumber.First, + OrderingBlock(oneRequestOrderingBlock.proofs), + CanonicalCommitSet(Set.empty), + from = allIds(1), + actualSender = Some(allIds(2)), // actual sender is different + ), + Signature.noSignature, + ) + ) + + val catchupDetectorMock = mock[CatchupDetector] + when(catchupDetectorMock.shouldCatchUpTo(EpochNumber(-1))).thenReturn(None) + + val (context, consensus) = + createIssConsensusModule( + p2pNetworkOutModuleRef = fakeIgnoringModule, + maybeCatchupDetector = Some(catchupDetectorMock), + ) + implicit val ctx: ContextType = context + + consensus.receive(Consensus.Start) + consensus.receive(futureMessageToTriggerDetector) + + // we want to make sure we don't update the catchupDetector, but the code will always call [[shouldCatchUpTo]] + // that will not update state so it is okay. We verify that this is the only call we make + verify(catchupDetectorMock).shouldCatchUpTo(EpochNumber(-1)) + verifyZeroInteractions(catchupDetectorMock) + succeed + } + + "the detector says so but we are advancing epoch" in { + val epochOfFutureMessage = EpochNumber(7L) + val futureMessageToTriggerDetector = PbftUnverifiedNetworkMessage( + SignedMessage( + PrePrepare.create( // Just to trigger the catch-up check + BlockMetadata.mk(epochOfFutureMessage, BlockNumber(100L)), + ViewNumber.First, + OrderingBlock(oneRequestOrderingBlock.proofs), + CanonicalCommitSet(Set.empty), + from = allIds(1), + actualSender = Some(allIds(1)), + ), + Signature.noSignature, + ) + ) + val currentEpochState = EpochStore.Epoch( + EpochInfo( + EpochNumber.First, + BlockNumber.First, + epochLength, + TopologyActivationTime(aTimestamp), ), - Signature.noSignature, + Seq.empty, ) - ) - val currentEpochState = EpochStore.Epoch( - EpochInfo( - EpochNumber.First, - BlockNumber.First, - epochLength, - TopologyActivationTime(aTimestamp), - ), - Seq.empty, - ) - val stateTransferManagerMock = mock[StateTransferManager[ProgrammableUnitTestEnv]] - val retransmissionsManagerMock = mock[RetransmissionsManager[ProgrammableUnitTestEnv]] - val segmentModuleMock = mock[ModuleRef[ConsensusSegment.Message]] - val catchupDetectorMock = mock[CatchupDetector] - when(catchupDetectorMock.updateLatestKnownNodeEpoch(any[BftNodeId], any[EpochNumber])) - .thenReturn(true) - val catchUpToEpochNumber = Some(EpochNumber(7)) - when(catchupDetectorMock.shouldCatchUpTo(any[EpochNumber])(any[TraceContext])) - .thenReturn(catchUpToEpochNumber) + val stateTransferManagerMock = mock[StateTransferManager[ProgrammableUnitTestEnv]] + val retransmissionsManagerMock = mock[RetransmissionsManager[ProgrammableUnitTestEnv]] + val segmentModuleMock = mock[ModuleRef[ConsensusSegment.Message]] + val catchupDetectorMock = mock[CatchupDetector] + when(catchupDetectorMock.updateLatestKnownNodeEpoch(any[BftNodeId], any[EpochNumber])) + .thenReturn(true) + val catchUpToEpochNumber = Some(EpochNumber(7)) + when(catchupDetectorMock.shouldCatchUpTo(any[EpochNumber])(any[TraceContext])) + .thenReturn(catchUpToEpochNumber) - val epochStore = mock[EpochStore[ProgrammableUnitTestEnv]] - when(epochStore.latestEpoch(anyBoolean)(anyTraceContext)) - .thenReturn(() => Some(currentEpochState)) - val (context, consensus) = - createIssConsensusModule( - p2pNetworkOutModuleRef = fakeIgnoringModule, - preConfiguredInitialEpochState = Some { context => - newEpochState(currentEpochState, context) - }, - epochStore = epochStore, - segmentModuleFactoryFunction = _ => segmentModuleMock, - maybeOnboardingStateTransferManager = Some(stateTransferManagerMock), - maybeCatchupDetector = Some(catchupDetectorMock), - maybeRetransmissionsManager = Some(retransmissionsManagerMock), + val epochStore = mock[EpochStore[ProgrammableUnitTestEnv]] + when(epochStore.latestEpoch(anyBoolean)(anyTraceContext)) + .thenReturn(() => Some(currentEpochState)) + val (context, consensus) = + createIssConsensusModule( + p2pNetworkOutModuleRef = fakeIgnoringModule, + preConfiguredInitialEpochState = Some { context => + newEpochState(currentEpochState, context) + }, + epochStore = epochStore, + segmentModuleFactoryFunction = _ => segmentModuleMock, + maybeOnboardingStateTransferManager = Some(stateTransferManagerMock), + maybeCatchupDetector = Some(catchupDetectorMock), + maybeRetransmissionsManager = Some(retransmissionsManagerMock), + ) + implicit val ctx: ContextType = context + + consensus.receive(Consensus.Start) + + // simulate epoch completed + consensus.getEpochState.isClosing shouldBe false + consensus.receive(CompleteEpochStored(currentEpochState, Seq.empty)) + consensus.getEpochState.isClosing shouldBe true + // we don't start new epoch because we are missing the next topology + verify(epochStore, never).startEpoch(any[EpochInfo])(any[TraceContext]) + + // now we get message that triggers catchupDetector (that would normally start state transfer) + consensus.receive(futureMessageToTriggerDetector) + + verify(catchupDetectorMock, times(1)) + .updateLatestKnownNodeEpoch(allIds(1), EpochNumber(epochOfFutureMessage)) + verify(catchupDetectorMock, times(1)) + .shouldCatchUpTo(eqTo(EpochNumber.First))(any[TraceContext]) + verify(retransmissionsManagerMock, never) + .handleMessage( + any[OrderingTopologyInfo[ProgrammableUnitTestEnv]], + any[RetransmissionsMessage], + )(any[ContextType], any[TraceContext]) + + // but we are not doing state transfer because we are currently advancing epoch + context.extractBecomes() shouldBe empty + } + + "the detector says so but we are storing a new epoch" in { + val epochOfFutureMessage = EpochNumber(7L) + val futureMessageToTriggerDetector = PbftUnverifiedNetworkMessage( + SignedMessage( + PrePrepare.create( // Just to trigger the catch-up check + BlockMetadata.mk(epochOfFutureMessage, BlockNumber(100L)), + ViewNumber.First, + OrderingBlock(oneRequestOrderingBlock.proofs), + CanonicalCommitSet(Set.empty), + from = allIds(1), + actualSender = Some(allIds(1)), + ), + Signature.noSignature, + ) ) - implicit val ctx: ContextType = context + val currentEpochState = EpochStore.Epoch( + Bootstrap.bootstrapEpochInfo(TopologyActivationTime(aTimestamp)), + Seq.empty, + ) + val stateTransferManagerMock = mock[StateTransferManager[ProgrammableUnitTestEnv]] + val retransmissionsManagerMock = mock[RetransmissionsManager[ProgrammableUnitTestEnv]] + val segmentModuleMock = mock[ModuleRef[ConsensusSegment.Message]] + val catchupDetectorMock = mock[CatchupDetector] + when(catchupDetectorMock.updateLatestKnownNodeEpoch(any[BftNodeId], any[EpochNumber])) + .thenReturn(true) + val catchUpToEpochNumber = Some(EpochNumber(7)) + when(catchupDetectorMock.shouldCatchUpTo(any[EpochNumber])(any[TraceContext])) + .thenReturn(catchUpToEpochNumber) - consensus.receive(Consensus.Start) + val epochStore = mock[EpochStore[ProgrammableUnitTestEnv]] + when(epochStore.latestEpoch(anyBoolean)(anyTraceContext)).thenReturn(() => + Some(currentEpochState) + ) + val (context, consensus) = + createIssConsensusModule( + p2pNetworkOutModuleRef = fakeIgnoringModule, + preConfiguredInitialEpochState = Some { context => + newEpochState(currentEpochState, context) + }, + epochStore = epochStore, + segmentModuleFactoryFunction = _ => segmentModuleMock, + maybeOnboardingStateTransferManager = Some(stateTransferManagerMock), + maybeCatchupDetector = Some(catchupDetectorMock), + maybeRetransmissionsManager = Some(retransmissionsManagerMock), + ) + implicit val ctx: ContextType = context - // simulate epoch completed - consensus.getEpochState.isClosing shouldBe false - consensus.receive(CompleteEpochStored(currentEpochState, Seq.empty)) - consensus.getEpochState.isClosing shouldBe true - // we don't start new epoch because we are missing the next topology - verify(epochStore, never).startEpoch(any[EpochInfo])(any[TraceContext]) + consensus.receive(Consensus.Start) + + consensus.getEpochState.isClosing shouldBe false + consensus.storingNewEpoch shouldBe false + // simulate starting the first epoch (whose topology is self-sent by consensus) + consensus.receive( + NewEpochTopology( + EpochNumber.First, + aMembership, + failingCryptoProvider, + ) + ) + consensus.getEpochState.isClosing shouldBe false + consensus.storingNewEpoch shouldBe true + + // now we get message that triggers catchupDetector (that would normally start state transfer) + consensus.receive(futureMessageToTriggerDetector) + + verify(catchupDetectorMock, times(1)) + .updateLatestKnownNodeEpoch(allIds(1), EpochNumber(epochOfFutureMessage)) + verify(catchupDetectorMock, times(1)) + .shouldCatchUpTo(eqTo(Bootstrap.BootstrapEpochNumber))(any[TraceContext]) + verify(retransmissionsManagerMock, never) + .handleMessage( + any[OrderingTopologyInfo[ProgrammableUnitTestEnv]], + any[RetransmissionsMessage], + )(any[ContextType], any[TraceContext]) + + // but we are not doing state transfer because we are currently storing an epoch + context.extractBecomes() shouldBe empty - // now we get message that triggers catchupDetector (that would normally start state transfer) - consensus.receive(futureMessageToTriggerDetector) - - verify(catchupDetectorMock, times(1)) - .updateLatestKnownNodeEpoch(allIds(1), EpochNumber(epochOfFutureMessage)) - verify(catchupDetectorMock, times(1)) - .shouldCatchUpTo(eqTo(EpochNumber.First))(any[TraceContext]) - verify(retransmissionsManagerMock, never) - .handleMessage( - any[OrderingTopologyInfo[ProgrammableUnitTestEnv]], - any[RetransmissionsMessage], - )(any[ContextType], any[TraceContext]) - - // but we are not doing state transfer because we are currently advancing epoch - context.extractBecomes() shouldBe empty + // simulate completing storing the new epoch + consensus.receive( + NewEpochStored( + EpochInfo( + EpochNumber.First, + BlockNumber.First, + epochLength, + TopologyActivationTime(aTimestamp), + ), + aMembership, + failingCryptoProvider, + ) + ) + consensus.storingNewEpoch shouldBe false + + // triggers catchupDetector again after completing storing the new epoch + consensus.receive(futureMessageToTriggerDetector) + + verify(catchupDetectorMock, times(2)) + .updateLatestKnownNodeEpoch(allIds(1), EpochNumber(epochOfFutureMessage)) + verify(catchupDetectorMock, times(1)) + .shouldCatchUpTo(eqTo(EpochNumber.First))(any[TraceContext]) + + // Switch to catchup state transfer because we are no longer storing an epoch + context.extractBecomes() should not be empty + + succeed + } } "drop remote PBFT messages" when { diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/StateTransferBehaviorTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/StateTransferBehaviorTest.scala index b5d7484be9..18a3c446c2 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/StateTransferBehaviorTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/StateTransferBehaviorTest.scala @@ -358,7 +358,7 @@ class StateTransferBehaviorTest succeed } - "receiving a new epoch stored message" should { + "receiving an internal new epoch stored message" should { "set the epoch state, communicate the membership to the P2P output module, " + "clean up the postponed message queue, and start state-transferring the epoch" in { val stateTransferManagerMock = mock[StateTransferManager[ProgrammableUnitTestEnv]] @@ -436,6 +436,8 @@ class StateTransferBehaviorTest Consensus.Admin.GetOrderingTopologyResponse( Bootstrap.BootstrapEpochNumber, aMembership.orderingTopology.nodes, + aMembership.leaders, + aMembership.blacklistedNodes, aMembership.orderingTopology.sequencingParameters, ) ) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/TimeoutManagerTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/TimeoutManagerTest.scala index 607cae9029..724b388190 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/TimeoutManagerTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/TimeoutManagerTest.scala @@ -37,6 +37,7 @@ class TimeoutManagerTest extends AsyncWordSpec with BftSequencerBaseTest { loggerFactory, BftBlockOrdererConfig().consensusBlockCompletionTimeout, timeoutId = BlockNumber.First, + timeoutMetric = None, ) val context = new ProgrammableUnitTestContext[ConsensusSegment.Message]() diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/EpochStoreTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/EpochStoreTest.scala index 6b331636ba..3fef8d6923 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/EpochStoreTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/data/EpochStoreTest.scala @@ -31,6 +31,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor CommitCertificate, OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.ConsensusSegment.ConsensusMessage.{ Commit, @@ -320,11 +321,68 @@ trait EpochStoreTest extends AsyncWordSpec { prePrepare(epochNumber = EpochNumber.First, blockNumber = BlockNumber(1)), Seq.empty, ) - blocks <- store.loadOrderedBlocks(initialBlockNumber = BlockNumber.First) + blocks <- store.loadOrderedBlocks(initialEpochNumber = EpochNumber.First, 10) } yield { blocks should contain theSameElementsInOrderAs expectedOrderedBlocks } } + + "load should respect limit" in { + val store = createStore() + val epoch0 = EpochInfo.forTesting(EpochNumber.First, BlockNumber.First, length = 2) + + val expectedOrderedBlocks = + Seq( + orderedBlock(BlockNumber.First, isLastInEpoch = false), + orderedBlock(BlockNumber(1), isLastInEpoch = true), + ) + + for { + _ <- store.startEpoch(epoch0) + _ <- store.addOrderedBlockAtomically( + prePrepare(epochNumber = EpochNumber.First, blockNumber = BlockNumber.First), + Seq.empty, + ) + _ <- store.addOrderedBlockAtomically( + prePrepare(epochNumber = EpochNumber.First, blockNumber = BlockNumber(1)), + Seq.empty, + ) + _ <- store.addOrderedBlockAtomically( + prePrepare(epochNumber = EpochNumber(1), blockNumber = BlockNumber(2)), + Seq.empty, + ) + blocks <- store.loadOrderedBlocks(initialEpochNumber = EpochNumber.First, limit = 1) + } yield { + blocks should contain theSameElementsInOrderAs expectedOrderedBlocks + } + } + } + + "last completed block" should { + "return epoch of last completed block" in { + val store = createStore() + + val lowerBound = EpochNumber(5) + val epochNumber = EpochNumber(13) + + for { + _ <- store.addOrderedBlockAtomically( + prePrepare(epochNumber = EpochNumber.First, blockNumber = BlockNumber.First), + Seq.empty, + ) + _ <- store.addOrderedBlockAtomically( + prePrepare(epochNumber = lowerBound, blockNumber = BlockNumber(1)), + Seq.empty, + ) + _ <- store.addOrderedBlockAtomically( + prePrepare(epochNumber = epochNumber, blockNumber = BlockNumber(2)), + Seq.empty, + ) + epoch <- store.lastEpochWithCompletedBlock(lowerBound) + } yield { + epoch shouldBe Some(epochNumber) + } + } } "prune" should { @@ -494,6 +552,6 @@ object EpochStoreTest { ViewNumber.First, BftNodeId("address"), isLastInEpoch, - mode = OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ) } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferManagerTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferManagerTest.scala index 2f53da343a..29e11a153f 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferManagerTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/statetransfer/StateTransferManagerTest.scala @@ -39,6 +39,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.{ OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.{ Membership, @@ -392,7 +393,7 @@ class StateTransferManagerTest extends AnyWordSpec with BftSequencerBaseTest { prePrepare.viewNumber, originalLeader = commitCert.prePrepare.from, isLastInEpoch = true, - mode = OrderedBlockForOutput.Mode.FromStateTransfer, + orderingMode = OrderingMode.StateTransfer, ) ) ) @@ -433,7 +434,9 @@ class StateTransferManagerTest extends AnyWordSpec with BftSequencerBaseTest { "cancel a timeout" when { "an epoch is transferred" in { val timeoutManager = mock[ - TimeoutManager[ProgrammableUnitTestEnv, Consensus.Message[ProgrammableUnitTestEnv], String] + TimeoutManager[ProgrammableUnitTestEnv, Consensus.Message[ + ProgrammableUnitTestEnv + ], String] ] val stateTransferManager = createStateTransferManager[ProgrammableUnitTestEnv]( diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/validation/PbftMessageValidatorImplTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/validation/PbftMessageValidatorImplTest.scala index bb38c9480c..373575a4ff 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/validation/PbftMessageValidatorImplTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/validation/PbftMessageValidatorImplTest.scala @@ -382,7 +382,7 @@ class PbftMessageValidatorImplTest extends AnyWordSpec with BftSequencerBaseTest aMembership, aMembership, Left( - "The proof of availability for batch BatchId(SHA-256:d624dc8a1022...) " + + "The proof of availability for batch BatchId(1220d624dc8a10220e0cd82eb3f65b1567fcbfe652c490b65977f689d1f07642e122) " + "in PrePrepare for block (epochNumber=1, blockNumber=12) " + "has 0 dissemination acknowledgements, but it should have at least 1" ), @@ -458,7 +458,7 @@ class PbftMessageValidatorImplTest extends AnyWordSpec with BftSequencerBaseTest aMembership, aMembership, Left( - "The proof of availability for batch BatchId(SHA-256:d624dc8a1022...) " + + "The proof of availability for batch BatchId(1220d624dc8a10220e0cd82eb3f65b1567fcbfe652c490b65977f689d1f07642e122) " + "in PrePrepare for block (epochNumber=1, blockNumber=12) has duplicated dissemination acknowledgements" ), ), @@ -472,7 +472,7 @@ class PbftMessageValidatorImplTest extends AnyWordSpec with BftSequencerBaseTest aMembership, aMembership, Left( - "The dissemination acknowledgement for batch BatchId(SHA-256:d624dc8a1022...) from 'otherId' is invalid " + + "The dissemination acknowledgement for batch BatchId(1220d624dc8a10220e0cd82eb3f65b1567fcbfe652c490b65977f689d1f07642e122) from 'otherId' is invalid " + "because 'otherId' is not in the current topology (epoch 1, nodes Set(self))" ), ), @@ -486,7 +486,7 @@ class PbftMessageValidatorImplTest extends AnyWordSpec with BftSequencerBaseTest aMembership, aMembershipWithoutKeys, Left( - "The dissemination acknowledgement for batch BatchId(SHA-256:d624dc8a1022...) from 'self' is invalid " + + "The dissemination acknowledgement for batch BatchId(1220d624dc8a10220e0cd82eb3f65b1567fcbfe652c490b65977f689d1f07642e122) from 'self' is invalid " + "because the signing key 'no-fingerprint' is not valid for 'self' in the current topology " + "(epoch 1, nodes Set(self)); the keys valid for 'self' in the current topology are Set()" ), diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/BlocksRecoveredFromConsensusMessagesTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/BlocksRecoveredFromConsensusMessagesTest.scala new file mode 100644 index 0000000000..ec760a0779 --- /dev/null +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/BlocksRecoveredFromConsensusMessagesTest.scala @@ -0,0 +1,204 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output + +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.BftSequencerBaseTest +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.EpochStoreReader +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.BlocksRecoveredFromConsensusMessagesTest.TestStep +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.OutputModule.BlocksRecoveredFromConsensusMessages.LoadPoint +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.{ + ProgrammableUnitTestContext, + ProgrammableUnitTestEnv, +} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.{ + BftNodeId, + BlockNumber, + EpochNumber, + ViewNumber, +} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.bfttime.CanonicalCommitSet +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.iss.BlockMetadata +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.{ + OrderedBlock, + OrderedBlockForOutput, + OrderingMode, +} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.Output +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.modules.Output.{ + AddMessageChunkFromRestart, + BlockOrdered, +} +import org.scalatest.wordspec.AnyWordSpecLike + +class BlocksRecoveredFromConsensusMessagesTest extends AnyWordSpecLike with BftSequencerBaseTest { + + "BlocksRecoveredFromConsensusMessages" should { + "release messages and load new chunks" in { + implicit val context: ProgrammableUnitTestContext[Output.Message[ProgrammableUnitTestEnv]] = + new ProgrammableUnitTestContext(resolveAwaits = true) + + val limit = 10 + val epochStoreReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] + val outputModule = mock[Output[ProgrammableUnitTestEnv]] + val blocksRecoveredFromConsensusMessages = + new OutputModule.BlocksRecoveredFromConsensusMessages[ProgrammableUnitTestEnv]( + epochStoreReader, + limit, + loggerFactory, + ) + + val targetEpoch = EpochNumber(18) + val loadEpoch = EpochNumber(5) + val loadFrom = EpochNumber(10) + + blocksRecoveredFromConsensusMessages.setTargetEpoch( + target = targetEpoch, + nextWhenToLoad = loadEpoch, + nextWhereToLoadFrom = loadFrom, + ) + + blocksRecoveredFromConsensusMessages.addMessages( + Seq(mkBlock(4), mkBlock(5), mkBlock(6), mkBlock(9)) + ) + + val steps = Seq( + TestStep(4, Seq(mkBlock(4)), None), + TestStep( + 5, + Seq(mkBlock(5)), + Some(10 -> Seq(mkBlock(10), mkBlock(14), mkBlock(18))), + ), // first load point + TestStep(6, Seq(mkBlock(6)), None), + TestStep(9, Seq(mkBlock(9)), None), + TestStep(10, Seq(mkBlock(10)), None), + TestStep(14, Seq(mkBlock(14)), None), + TestStep(15, Seq.empty, None), // second load point but we will not load anything + TestStep(18, Seq(mkBlock(18)), None), + TestStep(19, Seq.empty, None), + TestStep( + 25, + Seq.empty, + None, + ), // would be third load point, but we are above target so we should not call epochStoreReader + ) + + steps.foreach { testStep => + testStep.messagesLoaded.foreach { case (epochToLoadFrom, messages) => + when(epochStoreReader.loadOrderedBlocks(EpochNumber(epochToLoadFrom.toLong), limit)) + .thenReturn(() => messages) + } + } + + steps.foreach { step => + blocksRecoveredFromConsensusMessages.releaseMessagesForEpoch(EpochNumber(step.epochNumber)) + + context.extractSelfMessages() shouldBe step.messagesReleased.map(BlockOrdered(_)) + context.runPipedMessagesUntilNoMorePiped(outputModule) + + step.messagesLoaded.foreach { case (_, messages) => + verify(outputModule, times(1)).receive(AddMessageChunkFromRestart(messages)) + blocksRecoveredFromConsensusMessages.addMessages(messages) + + } + } + + verify(epochStoreReader, never).loadOrderedBlocks(EpochNumber(15), limit) + verify(epochStoreReader, never).loadOrderedBlocks(EpochNumber(25), limit) + } + + "set new target when reaching load point" in { + implicit val context: ProgrammableUnitTestContext[Output.Message[ProgrammableUnitTestEnv]] = + new ProgrammableUnitTestContext(resolveAwaits = true) + val limit = 10 + val epochStoreReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] + val blocksRecoveredFromConsensusMessages = + new OutputModule.BlocksRecoveredFromConsensusMessages[ProgrammableUnitTestEnv]( + epochStoreReader, + limit, + loggerFactory, + ) + val targetEpoch = EpochNumber(28) + val loadEpoch = EpochNumber(5) + val loadFrom = EpochNumber(10) + + when(epochStoreReader.loadOrderedBlocks(loadEpoch, limit)).thenReturn(() => Seq.empty) + + blocksRecoveredFromConsensusMessages.setTargetEpoch(targetEpoch, loadEpoch, loadFrom) + + val nextLoadEpoch = EpochNumber(15) + val nextHighestLoad = EpochNumber(20) + + when(epochStoreReader.loadOrderedBlocks(nextLoadEpoch, limit)).thenReturn(() => Seq.empty) + + blocksRecoveredFromConsensusMessages.releaseMessagesForEpoch(loadEpoch) + context.extractSelfMessages() shouldBe Seq.empty + verify(epochStoreReader, times(1)).loadOrderedBlocks(loadFrom, limit) + // next load point is set without needing to finish loading + blocksRecoveredFromConsensusMessages.nextLoadPoint shouldBe Some( + LoadPoint(nextLoadEpoch, nextHighestLoad) + ) + + blocksRecoveredFromConsensusMessages.releaseMessagesForEpoch(nextLoadEpoch) + context.extractSelfMessages() shouldBe Seq.empty + verify(epochStoreReader, times(1)).loadOrderedBlocks(nextHighestLoad, limit) + + // next load point is set without needing to finish loading + blocksRecoveredFromConsensusMessages.nextLoadPoint shouldBe None + } + + "release messages if the chunk is loaded late" in { + implicit val context: ProgrammableUnitTestContext[Output.Message[ProgrammableUnitTestEnv]] = + new ProgrammableUnitTestContext(resolveAwaits = true) + val limit = 10 + val epochStoreReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] + val blocksRecoveredFromConsensusMessages = + new OutputModule.BlocksRecoveredFromConsensusMessages[ProgrammableUnitTestEnv]( + epochStoreReader, + limit, + loggerFactory, + ) + + blocksRecoveredFromConsensusMessages.releaseMessagesForEpoch(EpochNumber(10)) + context.selfMessages shouldBe Seq.empty + context.runPipedMessages() shouldBe Seq.empty + + blocksRecoveredFromConsensusMessages.addMessages(Seq(mkBlock(10))) + context.selfMessages shouldBe Seq(BlockOrdered(mkBlock(10))) + context.runPipedMessages() shouldBe Seq.empty + + } + + "calculate next load point correctly" in { + Table( + ("target", "result"), + (19, None), // will be loaded when we load from 10 + (20, Some(LoadPoint(EpochNumber(15), EpochNumber(20)))), // requires one more load from 20 + ).forEvery { case (target, result) => + LoadPoint(EpochNumber(5), EpochNumber(10)) + .computeNextLoadPoint(Some(EpochNumber(target.toLong)), 10) shouldBe result + } + } + } + + private def mkBlock(i: Long): OrderedBlockForOutput = + OrderedBlockForOutput( + OrderedBlock( + BlockMetadata(EpochNumber(i), BlockNumber(i)), + Seq.empty, + CanonicalCommitSet.empty, + ), + ViewNumber.First, + BftNodeId("node"), + isLastInEpoch = true, + OrderingMode.Consensus, + ) +} + +object BlocksRecoveredFromConsensusMessagesTest { + final case class TestStep( + epochNumber: Long, + messagesReleased: Seq[OrderedBlockForOutput], + messagesLoaded: Option[(Int, Seq[OrderedBlockForOutput])], + ) +} diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModuleTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModuleTest.scala index 415c2e4c90..ee4986ede0 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModuleTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/OutputModuleTest.scala @@ -13,7 +13,10 @@ import com.digitalasset.canton.synchronizer.block.BlockFormat.OrderedRequest import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.BftSequencerBaseTest.FakeSigner import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig -import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig.DefaultEpochLength +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig.{ + DefaultEpochLength, + DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, +} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.integration.canton.crypto.CryptoProvider import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.integration.canton.topology.{ OrderingTopologyProvider, @@ -24,6 +27,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.mod Bootstrap, EpochStoreReader, } +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.OutputModule.BlocksRecoveredFromConsensusMessages.LoadPoint import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.OutputModule.{ DefaultRequestInspector, FixedResultRequestInspector, @@ -71,6 +75,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.ordering.{ OrderedBlock, OrderedBlockForOutput, + OrderingMode, } import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.OrderingTopology.NodeTopologyInfo import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.{ @@ -314,11 +319,19 @@ class OutputModuleTest new AtomicReference[Option[Availability.Message[ProgrammableUnitTestEnv]]](None) val orderedBlocksReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] when( - orderedBlocksReader.loadOrderedBlocks(initialBlockNumber = BlockNumber.First)(traceContext) + orderedBlocksReader.loadOrderedBlocks( + initialEpochNumber = EpochNumber.First, + DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, + )( + traceContext + ) ).thenReturn(() => Seq(initialBlock)) when( orderedBlocksReader.loadEpochInfo(EpochNumber.First)(traceContext) ).thenReturn(() => None) + when( + orderedBlocksReader.lastEpochWithCompletedBlock(EpochNumber.First)(traceContext) + ).thenReturn(() => Some(EpochNumber.First)) val outputAfterRestart = createOutputModule[ProgrammableUnitTestEnv]( initialHeight = secondBlockNumber, @@ -395,6 +408,7 @@ class OutputModuleTest } val store = mock[OutputMetadataStore[ProgrammableUnitTestEnv]] + when(store.getLowerBound()(traceContext)).thenReturn(() => None) when(store.getEpoch(EpochNumber.First)(traceContext)).thenReturn(() => Some(OutputEpochMetadata(EpochNumber.First, couldAlterOrderingTopology = true)) ) @@ -423,11 +437,17 @@ class OutputModuleTest // The output module will recover from the recovery block, if any, to rebuilt its volatile state. val orderedBlocksReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] when( - orderedBlocksReader.loadOrderedBlocks(recoverFromBlockNumber)(traceContext) + orderedBlocksReader.loadOrderedBlocks( + EpochNumber.First, + DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, + )(traceContext) ).thenReturn(() => Seq(lastStoredBlock).flatten) when( orderedBlocksReader.loadEpochInfo(secondEpochNumber)(traceContext) ).thenReturn(() => None) + when( + orderedBlocksReader.lastEpochWithCompletedBlock(EpochNumber.First)(traceContext) + ).thenReturn(() => Some(EpochNumber.First)) // The previous block's BFT time will be rehydrated for BFT time computation. val previousStoredBlockNumber = BlockNumber(recoverFromBlockNumber - 1) val previousStoredBlockBftTime = aTimestamp.minusSeconds(1) @@ -452,7 +472,10 @@ class OutputModuleTest verify(store, times(1)).getLastNonSequentialBlockMetadataStored(traceContext) verify(orderedBlocksReader, times(1)) - .loadOrderedBlocks(initialBlockNumber = recoverFromBlockNumber)( + .loadOrderedBlocks( + initialEpochNumber = EpochNumber.First, + DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, + )( traceContext ) context.selfMessages should contain theSameElementsInOrderAs @@ -478,6 +501,7 @@ class OutputModuleTest new ProgrammableUnitTestContext(resolveAwaits = true) val store = mock[OutputMetadataStore[ProgrammableUnitTestEnv]] + when(store.getLowerBound()(traceContext)).thenReturn(() => None) val lastStoredCompletedBlock = secondBlockNumber when(store.getEpoch(EpochNumber.First)(traceContext)).thenReturn(() => Some(OutputEpochMetadata(EpochNumber.First, couldAlterOrderingTopology = false)) @@ -528,7 +552,10 @@ class OutputModuleTest // from the recovery one. val orderedBlocksReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] when( - orderedBlocksReader.loadOrderedBlocks(expectedRecoverFromBlockNumber)(traceContext) + orderedBlocksReader.loadOrderedBlocks( + EpochNumber.First, + DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, + )(traceContext) ).thenReturn(() => Seq(recoverBlock, lastStoredBlock, nextEpochOrderedBlock)) // Output is recovering from a block in the first epoch, so it's expected to load the first epoch's info. @@ -544,6 +571,9 @@ class OutputModuleTest ) ) ) + when( + orderedBlocksReader.lastEpochWithCompletedBlock(EpochNumber.First)(traceContext) + ).thenReturn(() => Some(secondEpochNumber)) val orderingTopologyProvider = mock[OrderingTopologyProvider[ProgrammableUnitTestEnv]] when( @@ -583,126 +613,145 @@ class OutputModuleTest } "recover correctly from a non-0 epoch" in { - implicit val context: ProgrammableUnitTestContext[Output.Message[ProgrammableUnitTestEnv]] = - new ProgrammableUnitTestContext(resolveAwaits = true) - val store = mock[OutputMetadataStore[ProgrammableUnitTestEnv]] - val lastStoredCompletedBlock = fourthBlockNumber - when(store.getEpoch(secondEpochNumber)(traceContext)).thenReturn(() => - Some(OutputEpochMetadata(secondEpochNumber, couldAlterOrderingTopology = true)) - ) - when(store.getLastNonSequentialBlockMetadataStored(traceContext)).thenReturn(() => - Some( - OutputBlockMetadata( - epochNumber = secondEpochNumber, - blockNumber = lastStoredCompletedBlock, - blockBftTime = aTimestamp, - ) - ) - ) - val expectedRecoverFromBlockNumber = thirdBlockNumber + Table( + ("last completed block (consensus)", "expected next load point"), + (thirdEpochNumber, None), + (EpochNumber(10), None), // last epoch where we only need one load + ( + EpochNumber(11), + Some(LoadPoint(EpochNumber(6), EpochNumber(11))), + ), // Boundary, it will make sure to load last one + ).forEvery { case (lastCompletedBlockConsensus, expectedLoadPoint) => + implicit val context: ProgrammableUnitTestContext[Output.Message[ProgrammableUnitTestEnv]] = + new ProgrammableUnitTestContext(resolveAwaits = true) - // See how `recoverFromBlockNumberThatCouldBeInMiddleOfEpoch` is defined - when(store.getBlock(thirdBlockNumber)(traceContext)).thenReturn(() => - Some( - OutputBlockMetadata( - epochNumber = secondEpochNumber, - blockNumber = expectedRecoverFromBlockNumber, - blockBftTime = aTimestamp, + val store = mock[OutputMetadataStore[ProgrammableUnitTestEnv]] + when(store.getLowerBound()(traceContext)).thenReturn(() => None) + val lastStoredCompletedBlock = fourthBlockNumber + when(store.getEpoch(secondEpochNumber)(traceContext)).thenReturn(() => + Some(OutputEpochMetadata(secondEpochNumber, couldAlterOrderingTopology = true)) + ) + when(store.getLastNonSequentialBlockMetadataStored(traceContext)).thenReturn(() => + Some( + OutputBlockMetadata( + epochNumber = secondEpochNumber, + blockNumber = lastStoredCompletedBlock, + blockBftTime = aTimestamp, + ) ) ) - ) + val expectedRecoverFromBlockNumber = thirdBlockNumber - val recoverBlock = - anOrderedBlockForOutput( - epochNumber = secondEpochNumber, - blockNumber = expectedRecoverFromBlockNumber, - lastInEpoch = false, - ) - val lastStoredBlock = - anOrderedBlockForOutput( - epochNumber = secondEpochNumber, - blockNumber = lastStoredCompletedBlock, - lastInEpoch = true, - ) - val nextEpochOrderedBlock = - anOrderedBlockForOutput( - epochNumber = thirdEpochNumber, - blockNumber = fifthBlockNumber, - lastInEpoch = false, + // See how `recoverFromBlockNumberThatCouldBeInMiddleOfEpoch` is defined + when(store.getBlock(thirdBlockNumber)(traceContext)).thenReturn(() => + Some( + OutputBlockMetadata( + epochNumber = secondEpochNumber, + blockNumber = expectedRecoverFromBlockNumber, + blockBftTime = aTimestamp, + ) + ) ) - // The output module will recover from the recovery block, if any, to rebuilt its volatile state. - // However, consensus has ordered beyond the recovery epoch, and output will re-process all blocks - // from the recovery one. - val orderedBlocksReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] - when( - orderedBlocksReader.loadOrderedBlocks(expectedRecoverFromBlockNumber)(traceContext) - ).thenReturn(() => Seq(recoverBlock, lastStoredBlock, nextEpochOrderedBlock)) + val recoverBlock = + anOrderedBlockForOutput( + epochNumber = secondEpochNumber, + blockNumber = expectedRecoverFromBlockNumber, + lastInEpoch = false, + ) + val lastStoredBlock = + anOrderedBlockForOutput( + epochNumber = secondEpochNumber, + blockNumber = lastStoredCompletedBlock, + lastInEpoch = true, + ) + val nextEpochOrderedBlock = + anOrderedBlockForOutput( + epochNumber = thirdEpochNumber, + blockNumber = fifthBlockNumber, + lastInEpoch = false, + ) - // Output is recovering from a block in the second epoch, so it's expected to load the second epoch's info. - when( - orderedBlocksReader.loadEpochInfo(secondEpochNumber)(traceContext) - ).thenReturn(() => - Some( - EpochInfo( - number = secondEpochNumber, - startBlockNumber = thirdBlockNumber, - length = EpochLength(2), - topologyActivationTime = TopologyActivationTime(aTimestamp), + // The output module will recover from the recovery block, if any, to rebuilt its volatile state. + // However, consensus has ordered beyond the recovery epoch, and output will re-process all blocks + // from the recovery one. + val orderedBlocksReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] + when( + orderedBlocksReader.loadOrderedBlocks( + secondEpochNumber, + DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, + )(traceContext) + ).thenReturn(() => Seq(recoverBlock, lastStoredBlock, nextEpochOrderedBlock)) + when( + orderedBlocksReader.lastEpochWithCompletedBlock(secondEpochNumber)(traceContext) + ).thenReturn(() => Some(lastCompletedBlockConsensus)) + + // Output is recovering from a block in the second epoch, so it's expected to load the second epoch's info. + when( + orderedBlocksReader.loadEpochInfo(secondEpochNumber)(traceContext) + ).thenReturn(() => + Some( + EpochInfo( + number = secondEpochNumber, + startBlockNumber = thirdBlockNumber, + length = EpochLength(2), + topologyActivationTime = TopologyActivationTime(aTimestamp), + ) ) ) - ) - - val orderingTopologyProvider = mock[OrderingTopologyProvider[ProgrammableUnitTestEnv]] - when( - orderingTopologyProvider.getOrderingTopologyAt( - Some(TopologyActivationTime(aTimestamp)), - checkPendingChanges = true, - )(traceContext) - ).thenReturn(() => None) // We care about the call, not the result - // The previous block's BFT time will be rehydrated for BFT time computation. - val previousStoredBlockNumber = BlockNumber(expectedRecoverFromBlockNumber - 1) - val previousStoredBlockBftTime = aTimestamp.minusSeconds(1) - when(store.getBlock(previousStoredBlockNumber)(traceContext)).thenReturn(() => - Option.when(expectedRecoverFromBlockNumber > 0)( - OutputBlockMetadata( - secondEpochNumber, - previousStoredBlockNumber, - previousStoredBlockBftTime, + val orderingTopologyProvider = mock[OrderingTopologyProvider[ProgrammableUnitTestEnv]] + when( + orderingTopologyProvider.getOrderingTopologyAt( + Some(TopologyActivationTime(aTimestamp)), + checkPendingChanges = true, + )(traceContext) + ).thenReturn(() => None) // We care about the call, not the result + + // The previous block's BFT time will be rehydrated for BFT time computation. + val previousStoredBlockNumber = BlockNumber(expectedRecoverFromBlockNumber - 1) + val previousStoredBlockBftTime = aTimestamp.minusSeconds(1) + when(store.getBlock(previousStoredBlockNumber)(traceContext)).thenReturn(() => + Option.when(expectedRecoverFromBlockNumber > 0)( + OutputBlockMetadata( + secondEpochNumber, + previousStoredBlockNumber, + previousStoredBlockBftTime, + ) ) ) - ) - val output = - createOutputModule[ProgrammableUnitTestEnv]( - initialHeight = - lastStoredCompletedBlock, // Sequencer starts at a later epoch than we complete from - initialEpochWeHaveLeaderSelectionStateFor = - thirdEpochNumber, // To trigger the topology load - blacklistLeaderSelectionPolicyState = Some( - BlacklistLeaderSelectionPolicyState.create( - thirdEpochNumber, - fifthBlockNumber, - Map.empty, - )(testedProtocolVersion) - ), - availabilityRef = fakeIgnoringModule, - store = store, - epochStoreReader = orderedBlocksReader, - orderingTopologyProvider = orderingTopologyProvider, - )() + val output = + createOutputModule[ProgrammableUnitTestEnv]( + initialHeight = + lastStoredCompletedBlock, // Sequencer starts at a later epoch than we complete from + initialEpochWeHaveLeaderSelectionStateFor = + thirdEpochNumber, // To trigger the topology load + blacklistLeaderSelectionPolicyState = Some( + BlacklistLeaderSelectionPolicyState.create( + thirdEpochNumber, + fifthBlockNumber, + Map.empty, + )(testedProtocolVersion) + ), + availabilityRef = fakeIgnoringModule, + store = store, + epochStoreReader = orderedBlocksReader, + orderingTopologyProvider = orderingTopologyProvider, + )() - output.receive(Output.Start) + output.receive(Output.Start) - verify(orderedBlocksReader, times(2)).loadEpochInfo(secondEpochNumber)(traceContext) - verify(orderingTopologyProvider).getOrderingTopologyAt( - Some(TopologyActivationTime(aTimestamp)), - checkPendingChanges = true, - )(traceContext) + verify(orderedBlocksReader, times(2)).loadEpochInfo(secondEpochNumber)(traceContext) + verify(orderingTopologyProvider).getOrderingTopologyAt( + Some(TopologyActivationTime(aTimestamp)), + checkPendingChanges = true, + )(traceContext) + output.blocksRecoveredFromConsensus.nextLoadPoint shouldBe expectedLoadPoint - succeed + succeed + } } "uses the correct leader selection policy when restarting from older epoch" in { @@ -719,6 +768,7 @@ class OutputModuleTest implicit val context: ProgrammableUnitTestContext[Output.Message[ProgrammableUnitTestEnv]] = new ProgrammableUnitTestContext(resolveAwaits = true) val store = mock[OutputMetadataStore[ProgrammableUnitTestEnv]] + when(store.getLowerBound()(traceContext)).thenReturn(() => None) val epochStoreReader = mock[EpochStoreReader[ProgrammableUnitTestEnv]] val orderingTopologyProvider = mock[OrderingTopologyProvider[ProgrammableUnitTestEnv]] val leaderSelectionInitializer = mock[LeaderSelectionInitializer[ProgrammableUnitTestEnv]] @@ -766,11 +816,19 @@ class OutputModuleTest ) ) ) - when(epochStoreReader.loadOrderedBlocks(sequencerFirstBlockOfEpoch)).thenReturn(() => + when( + epochStoreReader.loadOrderedBlocks( + sequencerEpochIsAt, + DefaultOutputSizeOfChunkOfEpochsToLoadAtStart, + ) + ).thenReturn(() => Seq( anOrderedBlockForOutput(sequencerEpochIsAt, sequencerBlockIsAt) ) ) + when(epochStoreReader.lastEpochWithCompletedBlock(sequencerEpochIsAt)).thenReturn(() => + Some(outputEpochIsAt) + ) when(store.getBlock(previousBlockNumber)) .thenReturn(() => Some( @@ -781,6 +839,16 @@ class OutputModuleTest ) ) ) + when(store.getBlock(sequencerFirstBlockOfEpoch)) + .thenReturn(() => + Some( + OutputBlockMetadata( + sequencerEpochIsAt, + sequencerFirstBlockOfEpoch, + sequencerTimestampOfEpoch, + ) + ) + ) when(store.getEpoch(sequencerEpochIsAt)).thenReturn(() => Some( OutputEpochMetadata( @@ -830,6 +898,9 @@ class OutputModuleTest output.previousStoredBlock.getBlockNumberAndBftTime shouldBe Some( previousBlockNumber -> previousTimestamp ) + output.previousStoredBlock.getInitialBlockNumberAndBftTime shouldBe Some( + sequencerFirstBlockOfEpoch -> sequencerTimestampOfEpoch + ) output.leaderSelectionPolicy shouldBe sequencerEpochPolicy } @@ -846,8 +917,10 @@ class OutputModuleTest val blockSubscription = new PekkoBlockSubscription[FakePipeToSelfCellUnitTestEnv]( initialHeight = BlockNumber.First, + () => fakeModuleExpectingSilence, timeouts, loggerFactory, + SequencerMetrics.noop(getClass.getSimpleName).bftOrdering, )(fail(_)) val output = createOutputModule[FakePipeToSelfCellUnitTestEnv](store = store)( @@ -913,8 +986,10 @@ class OutputModuleTest val blockSubscription = new PekkoBlockSubscription[FakePipeToSelfCellUnitTestEnv]( secondBlockNumber, + () => fakeModuleExpectingSilence, timeouts, loggerFactory, + SequencerMetrics.noop(getClass.getSimpleName).bftOrdering, )(fail(_)) val output = createOutputModule[FakePipeToSelfCellUnitTestEnv]( store = store, @@ -952,7 +1027,13 @@ class OutputModuleTest val initialHeight = BlockNumber(2L) val blockSubscription = - new PekkoBlockSubscription[IgnoringUnitTestEnv](initialHeight, timeouts, loggerFactory)( + new PekkoBlockSubscription[IgnoringUnitTestEnv]( + initialHeight, + () => fakeModuleExpectingSilence, + timeouts, + loggerFactory, + SequencerMetrics.noop(getClass.getSimpleName).bftOrdering, + )( fail(_) ) val output = createOutputModule[IgnoringUnitTestEnv]( @@ -1000,19 +1081,19 @@ class OutputModuleTest ("Pending Canton topology changes", "block mode"), ( false, - OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ), ( true, - OrderedBlockForOutput.Mode.FromConsensus, + OrderingMode.Consensus, ), ( false, - OrderedBlockForOutput.Mode.FromStateTransfer, + OrderingMode.StateTransfer, ), ( true, - OrderedBlockForOutput.Mode.FromStateTransfer, + OrderingMode.StateTransfer, ), ).forEvery { case (pendingChanges, blockMode) => val store = spy(createOutputMetadataStore[ProgrammableUnitTestEnv]) @@ -1048,14 +1129,14 @@ class OutputModuleTest completeBlockData( BlockNumber.First, commitTimestamp = aTimestamp, - mode = blockMode, + orderingMode = blockMode, ) val blockData2 = // lastInEpoch = true, isRequestToAllMembersOfSynchronizer = false completeBlockData( BlockNumber(BlockNumber.First + 1L), commitTimestamp = anotherTimestamp, lastInEpoch = true, - mode = blockMode, + orderingMode = blockMode, ) output.receive(Output.Start) @@ -1175,7 +1256,7 @@ class OutputModuleTest aTimestamp, lastInEpoch = false, // Do not complete the epoch! EpochNumber.First, - mode = OrderedBlockForOutput.Mode.FromStateTransfer, + orderingMode = OrderingMode.StateTransfer, ) val blockNumber2 = BlockNumber(BlockNumber.First + 1L) val blockData2 = @@ -1183,7 +1264,7 @@ class OutputModuleTest blockNumber2, anotherTimestamp, epochNumber = EpochNumber(EpochNumber.First + 1L), - mode = OrderedBlockForOutput.Mode.FromStateTransfer, + orderingMode = OrderingMode.StateTransfer, ) output.receive(Output.Start) @@ -1413,6 +1494,46 @@ class OutputModuleTest } } + "not send topology to consensus" when { + "the orderer is paused" in { + implicit val context: ProgrammableUnitTestContext[Output.Message[ProgrammableUnitTestEnv]] = + new ProgrammableUnitTestContext(resolveAwaits = true) + val topologyProviderSpy = + spy(new FakeOrderingTopologyProvider[ProgrammableUnitTestEnv]) + val consensusRef = mock[ModuleRef[Consensus.Message[ProgrammableUnitTestEnv]]] + val blockSubscription = new EmptyBlockSubscription() + val output = createOutputModule[ProgrammableUnitTestEnv]( + initialOrderingTopology = OrderingTopology.forTesting(Set(BftNodeId("node1"))), + orderingTopologyProvider = topologyProviderSpy, + consensusRef = consensusRef, + requestInspector = new FixedResultRequestInspector(false), + )(blockSubscription) + + val blockData = + completeBlockData(BlockNumber.First, commitTimestamp = aTimestamp, lastInEpoch = true) + + output.receive(Output.Start) + blockSubscription.setSequencerCoreIsSlow(slow = true, 2000) + output.receive(Output.BlockDataFetched(blockData)) + + context.runPipedMessagesUntilNoMorePiped(output) + + verifyZeroInteractions(consensusRef) + + blockSubscription.setSequencerCoreIsSlow(slow = false, 0) + output.receive(Output.ProcessNewEpochTopologyMessagesIfPossible) + + verify(consensusRef, times(1)).asyncSend( + Consensus.NewEpochTopology( + secondEpochNumber, + Membership.forTesting(BftNodeId("node1")), + any[CryptoProvider[ProgrammableUnitTestEnv]], + ) + )(any[TraceContext], any[MetricsContext]) + succeed + } + } + "get sequencer snapshot additional info" in { implicit val context: ProgrammableUnitTestContext[Output.Message[ProgrammableUnitTestEnv]] = new ProgrammableUnitTestContext(resolveAwaits = true) @@ -1641,7 +1762,7 @@ class OutputModuleTest commitTimestamp: CantonTimestamp, lastInEpoch: Boolean = false, epochNumber: EpochNumber = EpochNumber.First, - mode: OrderedBlockForOutput.Mode = OrderedBlockForOutput.Mode.FromConsensus, + orderingMode: OrderingMode = OrderingMode.Consensus, ): CompleteBlockData = CompleteBlockData( anOrderedBlockForOutput( @@ -1649,7 +1770,7 @@ class OutputModuleTest blockNumber, commitTimestamp, lastInEpoch, - mode, + orderingMode, ), batches = Seq( OrderingRequestBatch.create( @@ -1795,7 +1916,8 @@ object OutputModuleTest { ) extends EmptyBlockSubscription { override def receiveBlock(block: BlockFormat.Block)(implicit - traceContext: TraceContext + traceContext: TraceContext, + metricsContext: MetricsContext, ): Unit = subscriptionBlocks.enqueue(Traced(block)) } @@ -1847,7 +1969,7 @@ object OutputModuleTest { blockNumber: Long = BlockNumber.First, commitTimestamp: CantonTimestamp = aTimestamp, lastInEpoch: Boolean = false, - mode: OrderedBlockForOutput.Mode = OrderedBlockForOutput.Mode.FromConsensus, + orderingMode: OrderingMode = OrderingMode.Consensus, batchIds: Seq[BatchId] = Seq.empty, )(implicit synchronizerProtocolVersion: ProtocolVersion): OrderedBlockForOutput = OrderedBlockForOutput( @@ -1873,6 +1995,6 @@ object OutputModuleTest { ViewNumber.First, BftNodeId.Empty, lastInEpoch, - mode, + orderingMode, ) } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/memory/SimulationOutputMetadataStore.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/memory/SimulationOutputMetadataStore.scala index efd189112f..b86367c837 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/memory/SimulationOutputMetadataStore.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/data/memory/SimulationOutputMetadataStore.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.memory +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.BftNodeId import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.simulation.SimulationModuleSystem.SimulationEnv import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.simulation.future.SimulationFuture import com.digitalasset.canton.tracing.TraceContext @@ -10,7 +11,8 @@ import com.digitalasset.canton.tracing.TraceContext import scala.util.Try final class SimulationOutputMetadataStore( - fail: String => Unit + node: BftNodeId, + fail: String => Unit, ) extends GenericInMemoryOutputMetadataStore[SimulationEnv] { override protected def createFuture[T](action: String)(value: () => Try[T]): SimulationFuture[T] = @@ -20,5 +22,5 @@ final class SimulationOutputMetadataStore( override protected def reportError(errorMessage: String)(implicit traceContext: TraceContext - ): Unit = fail(errorMessage) + ): Unit = fail(s"$node ($traceContext): $errorMessage") } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyStateTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyStateTest.scala index aea5f249e6..45fa2eb4e3 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyStateTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyStateTest.scala @@ -82,7 +82,11 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest "BlacklistLeaderSelectionPolicyState" should { "a clean node" should { "stay clean if not punished" in { - BlacklistLeaderSelectionPolicyStateWithTopology(initState(), orderingTopology) + BlacklistLeaderSelectionPolicyStateWithTopology( + initState(), + orderingTopology, + testedProtocolVersion, + ) .update( orderingTopology, blockToLeaderAll, @@ -92,7 +96,11 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest } "be blacklisted if punished" in { - BlacklistLeaderSelectionPolicyStateWithTopology(initState(), orderingTopology) + BlacklistLeaderSelectionPolicyStateWithTopology( + initState(), + orderingTopology, + testedProtocolVersion, + ) .update( orderingTopology, blockToLeaderAll, @@ -109,6 +117,7 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest n0 -> BlacklistStatus.Blacklisted(1, 2) ), orderingTopology, + testedProtocolVersion, ).update( orderingTopology, blockToLeaderAllWithoutN0, @@ -122,6 +131,7 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest n0 -> BlacklistStatus.Blacklisted(1, 1) ), orderingTopology, + testedProtocolVersion, ).update( orderingTopology, blockToLeaderAllWithoutN0, @@ -137,6 +147,7 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest n0 -> BlacklistStatus.OnTrial(1) ), orderingTopology, + testedProtocolVersion, ).update( orderingTopology, blockToLeaderAll, @@ -150,6 +161,7 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest n0 -> BlacklistStatus.OnTrial(1) ), orderingTopology, + testedProtocolVersion, ).update( orderingTopology, blockToLeaderAll, @@ -163,6 +175,7 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest n0 -> BlacklistStatus.OnTrial(1) ), orderingTopology, + testedProtocolVersion, ).update( orderingTopology, blockToLeaderAllWithoutN0, @@ -175,6 +188,7 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest BlacklistLeaderSelectionPolicyStateWithTopology( initState(n1 -> BlacklistStatus.OnTrial(1), n2 -> BlacklistStatus.Blacklisted(1, 1)), orderingTopology, + testedProtocolVersion, ).computeLeaders() shouldBe Seq(n0, n1, n3) } @@ -182,6 +196,7 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest BlacklistLeaderSelectionPolicyStateWithTopology( initState(n1 -> BlacklistStatus.Blacklisted(2, 2), n2 -> BlacklistStatus.Blacklisted(1, 1)), orderingTopology, + testedProtocolVersion, ) .computeLeaders() shouldBe Seq(n0, n2, n3) } @@ -194,6 +209,7 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest BlacklistLeaderSelectionPolicyConfig.HowManyCanWeBlacklist.NoBlacklisting ) ), + testedProtocolVersion, ).computeLeaders() shouldBe Seq(n0, n1, n2, n3) } @@ -201,65 +217,87 @@ class BlacklistLeaderSelectionPolicyStateTest extends AnyWordSpec with BaseTest "not blacklist further than cap" in { val limit = 10L val failedAttempts = 100L - val topology = makeOrderingTopology( - makeConfig(howLongToBlacklist = - BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear( - Some(limit) - ) - ) - ) - BlacklistLeaderSelectionPolicyStateWithTopology( - initState(n1 -> BlacklistStatus.OnTrial(failedAttempts)), - topology, - ).update( - topology, - blockToLeaderAllWithoutN0, - Set(n1), - ).state shouldBe stateNextEpoch( - n1 -> BlacklistStatus.Blacklisted(failedAttempts + 1, limit) + Table( + "policy", + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear(Some(limit)), + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist + .LinearWithParameters(Some(limit), 10, 10), + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist + .Exponential(Some(limit), 10), ) + .forEvery { policy => + val topology = makeOrderingTopology(makeConfig(howLongToBlacklist = policy)) + BlacklistLeaderSelectionPolicyStateWithTopology( + initState(n1 -> BlacklistStatus.OnTrial(failedAttempts)), + topology, + testedProtocolVersion, + ).update( + topology, + blockToLeaderAllWithoutN0, + Set(n1), + ).state shouldBe stateNextEpoch( + n1 -> BlacklistStatus.Blacklisted(failedAttempts + 1, limit) + ) + } } "don't apply limit if you are below" in { val limit = 100L - val failedAttempts = 10L - val topology = makeOrderingTopology( - makeConfig(howLongToBlacklist = - BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear( - Some(limit) - ) + val failedAttempts = 5L + Table( + ("policy", "next value"), + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Linear( + Some(limit) + ) -> (failedAttempts + 1), + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.LinearWithParameters( + Some(limit), + 5, + 10, + ) -> (5 * (failedAttempts + 1) + 10), + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist.Exponential( + Some(limit), + 5, + ) -> 69L, // 2 ^{5+1} + 5 + ).forEvery { case (policy, nextHowManyLeft) => + val topology = makeOrderingTopology(makeConfig(howLongToBlacklist = policy)) + BlacklistLeaderSelectionPolicyStateWithTopology( + initState(n1 -> BlacklistStatus.OnTrial(failedAttempts)), + topology, + testedProtocolVersion, + ).update( + topology, + blockToLeaderAllWithoutN0, + Set(n1), + ).state shouldBe stateNextEpoch( + n1 -> BlacklistStatus.Blacklisted(failedAttempts + 1, nextHowManyLeft) ) - ) - BlacklistLeaderSelectionPolicyStateWithTopology( - initState(n1 -> BlacklistStatus.OnTrial(failedAttempts)), - topology, - ).update( - topology, - blockToLeaderAllWithoutN0, - Set(n1), - ).state shouldBe stateNextEpoch( - n1 -> BlacklistStatus.Blacklisted(failedAttempts + 1, failedAttempts + 1) - ) + } } "update if config change" in { val oldValue = 100L val newLimit = 10L - BlacklistLeaderSelectionPolicyStateWithTopology( - initState(n0 -> BlacklistStatus.Blacklisted(oldValue, oldValue)), - orderingTopology, - ).update( - makeOrderingTopology( - makeConfig(howLongToBlacklist = - BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist - .Linear(Some(newLimit)) - ) - ), - blockToLeaderAllWithoutN0, - Set.empty, - ).state shouldBe stateNextEpoch( - n0 -> BlacklistStatus.Blacklisted(oldValue, newLimit - 1) - ) + Table( + "policy", + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist + .Linear(Some(newLimit)), + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist + .LinearWithParameters(Some(newLimit), 0L, 0L), + BlacklistLeaderSelectionPolicyConfig.HowLongToBlacklist + .Exponential(Some(newLimit), 0L), + ).forEvery { policy => + BlacklistLeaderSelectionPolicyStateWithTopology( + initState(n0 -> BlacklistStatus.Blacklisted(oldValue, oldValue)), + orderingTopology, + testedProtocolVersion, + ).update( + makeOrderingTopology(makeConfig(howLongToBlacklist = policy)), + blockToLeaderAllWithoutN0, + Set.empty, + ).state shouldBe stateNextEpoch( + n0 -> BlacklistStatus.Blacklisted(oldValue, newLimit - 1) + ) + } } } } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyTest.scala index 80b843cdfa..a11a66ae85 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/leaders/BlacklistLeaderSelectionPolicyTest.scala @@ -48,6 +48,7 @@ class BlacklistLeaderSelectionPolicyTest extends AnyWordSpec with BaseTest { BlacklistLeaderSelectionPolicy.create( state, orderingTopology, + testedProtocolVersion, store, metrics, loggerFactory, @@ -67,6 +68,7 @@ class BlacklistLeaderSelectionPolicyTest extends AnyWordSpec with BaseTest { BlacklistLeaderSelectionPolicy.create( state, orderingTopology, + testedProtocolVersion, store, metrics, loggerFactory, diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModuleTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModuleTest.scala index d7ed32f792..1f77c80b95 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModuleTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModuleTest.scala @@ -124,7 +124,7 @@ class P2PNetworkOutModuleTest extends AnyWordSpec with BftSequencerBaseTest { context.selfMessages should contain theSameElementsInOrderAs Seq[P2PNetworkOut.Network]( - P2PNetworkOut.Network.Connected(otherInitialEndpointsTupled._1.id), + P2PNetworkOut.Network.Connected(Some(otherInitialEndpointsTupled._1.id)), P2PNetworkOut.Network .Authenticated( endpointToTestBftNodeId(otherInitialEndpointsTupled._1), @@ -154,7 +154,7 @@ class P2PNetworkOutModuleTest extends AnyWordSpec with BftSequencerBaseTest { context.selfMessages should contain theSameElementsInOrderAs Seq[P2PNetworkOut.Network]( - P2PNetworkOut.Network.Connected(otherInitialEndpointsTupled._2.id), + P2PNetworkOut.Network.Connected(Some(otherInitialEndpointsTupled._2.id)), P2PNetworkOut.Network.Authenticated( endpointToTestBftNodeId(otherInitialEndpointsTupled._2), Some(otherInitialEndpointsTupled._2), @@ -825,9 +825,7 @@ class P2PNetworkOutModuleTest extends AnyWordSpec with BftSequencerBaseTest { ): Unit = fakeClientP2PNetworkManager .nodeActions(endpoint) - .onConnect( - endpoint.id - ) + .onConnect(Some(endpoint.id)) private def disconnect( fakeClientP2PNetworkManager: FakeP2PNetworkManager, diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionManagerTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionManagerTest.scala new file mode 100644 index 0000000000..f418e5d7ce --- /dev/null +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionManagerTest.scala @@ -0,0 +1,279 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning + +import com.digitalasset.canton.config.{DbConfig, DbParametersConfig, PartitionConfig} +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.resource.DbStorage +import com.digitalasset.canton.store.db.DbStorageSetup.DbBasicConfig +import com.digitalasset.canton.store.db.PostgresTest +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.BftSequencerBaseTest +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.OutputMetadataStore.OutputBlockMetadata +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.{ + HighestCreatedPartitionNumbers, + HighestPrunedPartitionNumbers, + Partition, + PruningManagerInitStore, + newPartitions, + partitionsToPrune, +} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.{ + BlockNumber, + EpochNumber, +} +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.MonadUtil +import org.scalatest.wordspec.AsyncWordSpec +import slick.jdbc.GetResult + +class PartitionManagerTest extends AsyncWordSpec with BftSequencerBaseTest { + "PartitionManager" should { + val partitionSize = 100 + "create partitions one step ahead" in { + // for batches table we create partitions 1000 epochs ahead + inside(newPartitions(partitionSize, EpochNumber(0), HighestCreatedPartitionNumbers(2, 2))) { + case (partitions, HighestCreatedPartitionNumbers(2, 10)) => + partitions should have size 8 + forAll(partitions)(p => p.tableName shouldBe PartitionManager.batchesTable) + } + inside( + newPartitions(partitionSize, EpochNumber(199), HighestCreatedPartitionNumbers(2, 10)) + ) { case (partitions, HighestCreatedPartitionNumbers(2, 11)) => + partitions should have size 1 + forAll(partitions)(p => p.tableName shouldBe PartitionManager.batchesTable) + } + // for other tables we create partitions one step ahead + inside( + newPartitions(partitionSize, EpochNumber(200), HighestCreatedPartitionNumbers(2, 12)) + ) { + case ( + Partition(partitionName, tableName, 300, 400) +: _otherPartitions, + HighestCreatedPartitionNumbers(3, 12), + ) => + partitionName shouldBe s"${tableName}_p3" + } + } + + "prune partitions below current epoch number or block number" in { + // nothing to prune because already pruned + partitionsToPrune( + partitionSize, + EpochNumber(0), + EpochNumber(1000), + HighestPrunedPartitionNumbers(2, -1, 9), + ) shouldBe (Seq.empty, HighestPrunedPartitionNumbers(2, -1, 9)) + + // nothing to prune because already pruned previous partition 2 (from 200 to 299) + partitionsToPrune( + partitionSize, + EpochNumber(398), + EpochNumber(1000), + HighestPrunedPartitionNumbers(2, -1, 9), + ) shouldBe (Seq.empty, HighestPrunedPartitionNumbers(2, -1, 9)) + // because it is the last epochNumber in the partition it will prune partition 3 (from 300 to 399) + inside( + partitionsToPrune( + partitionSize, + EpochNumber(399), + EpochNumber(1000), + HighestPrunedPartitionNumbers(2, -1, 9), + ) + ) { case (partitions, HighestPrunedPartitionNumbers(3, -1, 9)) => + partitions should not contain s"${PartitionManager.batchesTable}_p3" + forAll(partitions)(p => p should endWith("_p3")) + } + // same result as above since haven't yet pruned partition 3 and that is the previous one + inside( + partitionsToPrune( + partitionSize, + EpochNumber(400), + EpochNumber(1100), + HighestPrunedPartitionNumbers(2, -1, 10), + ) + ) { case (partitions, HighestPrunedPartitionNumbers(3, -1, 10)) => + partitions should not contain s"${PartitionManager.batchesTable}_p3" + forAll(partitions)(p => p should endWith("_p3")) + } + + // batches table pruning takes place 500 epochs earlier + inside( + partitionsToPrune( + partitionSize, + EpochNumber(600), + EpochNumber(1200), + HighestPrunedPartitionNumbers(5, -1, 11), + ) + ) { case (partitions, HighestPrunedPartitionNumbers(5, 0, 11)) => + partitions should contain only (s"${PartitionManager.batchesTable}_p0") + } + + // in progress table is pruned based on latest epoch completed + inside( + partitionsToPrune( + partitionSize, + EpochNumber(600), + EpochNumber(1099), + HighestPrunedPartitionNumbers(5, 0, 9), + ) + ) { case (partitions, HighestPrunedPartitionNumbers(5, 0, 10)) => + partitions should contain only (s"${PartitionManager.consensusInProgressTable}_p10") + } + } + } +} + +class PartitionManagerDatabaseTest + extends AsyncWordSpec + with BftSequencerBaseTest + with PostgresTest { + + def create() = PartitionManager.create(storage, timeouts, loggerFactory) + + override def cleanDb(storage: DbStorage)(implicit + tc: TraceContext + ): FutureUnlessShutdown[Unit] = FutureUnlessShutdown.unit + + override def mkDbConfig(basicConfig: DbBasicConfig): DbConfig.Postgres = { + val defaultDbConfig = super.mkDbConfig(basicConfig) + defaultDbConfig.copy(parameters = + DbParametersConfig(partitions = PartitionConfig(initialBftOrdererTablesPartitionSize = 100)) + ) + } + + def howManyPartitionsAreSearched(blockNumber: BlockNumber): FutureUnlessShutdown[Int] = { + import storage.api.* + val explain = + sql"""explain (format json) select * from #${PartitionManager.outputBlocksTable} where block_number = $blockNumber order by block_number limit 1 """ + .as[String](GetResult(_.nextString())) + .map( + _.headOption + .getOrElse("") + .split(s""""Relation Name": "${PartitionManager.outputBlocksTable}_p""") + .length - 1 + ) + storage.query(explain, "explain") + } + + "PartitionManagerDatabase" should { + "prune and creation of partitions does not error" in { + val initStore = new PruningManagerInitStore(storage, timeouts, loggerFactory) + def getMinMaxPartitionNumbers = + initStore.queryLowestAndHighestExistingPartitionNumberPerTableName + + (for { + case Some((creator, pruner)) <- create().futureUnlessShutdown() + + partitionSizeEntry <- initStore.latestPartitionSizeEntry + _ = partitionSizeEntry shouldBe PartitionManager.PartitionSizeEntry(EpochNumber(0), 0, 100) + + (minMap0, maxMap0) <- getMinMaxPartitionNumbers + _ = forAll(minMap0.values)(_ shouldBe (0)) + _ = forAll(maxMap0.values)(_ shouldBe (1)) + + msg <- pruner.prune(EpochNumber(98L), EpochNumber(98L)).futureUnlessShutdown() + (minMap1, maxMap1) <- getMinMaxPartitionNumbers + _ = msg shouldBe "Pruned no partitions at epoch 98" + _ = forAll(minMap1.values)(_ shouldBe (0)) + _ = forAll(maxMap1.values)(_ shouldBe (1)) + + msg2 <- pruner.prune(EpochNumber(99L), EpochNumber(99L)).futureUnlessShutdown() + (minMap2, maxMap2) <- getMinMaxPartitionNumbers + _ = msg2 shouldBe "Pruned 6 partitions at epoch 99" + _ = { + minMap2(PartitionManager.batchesTable) shouldBe (0) + forAll((minMap2 - PartitionManager.batchesTable).values)(_ shouldBe (1)) + } + _ = forAll(maxMap2.values)(_ shouldBe (1)) + + _ <- creator + .createPartitionsIfNeeded(EpochNumber(199)) + .futureUnlessShutdown() + (minMap3, maxMap3) <- getMinMaxPartitionNumbers + _ = minMap3 shouldBe (minMap2) + _ = { + maxMap3(PartitionManager.batchesTable) shouldBe (11) + forAll((maxMap3 - PartitionManager.batchesTable).values)(_ shouldBe (2)) + } + + _ <- creator + .createPartitionsIfNeeded(EpochNumber(200)) + .futureUnlessShutdown() + (minMap4, maxMap4) <- getMinMaxPartitionNumbers + _ = minMap4 shouldBe (minMap2) + _ = { + maxMap4(PartitionManager.batchesTable) shouldBe (12) + forAll((maxMap4 - PartitionManager.batchesTable).values)(_ shouldBe (3)) + } + } yield succeed).onShutdown(fail()) + } + + "exclusion constraints make query scan fewer partitions" in { + val outputStore = new DbOutputMetadataStore(storage, timeouts, loggerFactory) + + (for { + case Some((creator, _)) <- create().futureUnlessShutdown() + + // we create 100 partitions + _ <- creator + .createPartitionsIfNeeded(EpochNumber(9950)) + .futureUnlessShutdown() + + // a block search scans all of them + result1 <- howManyPartitionsAreSearched(BlockNumber(49999)) + _ = result1 shouldBe 100 + + // now we add one block to the beginning of each partition and + // call createPartitionsIfNeeded with the block whose epoch is the first one in the partition, + // which will cause the partition creator to add an exclusion constraint at the lower bound of each partition + _ <- MonadUtil.sequentialTraverse(1 to 99) { i => + val epochNumber = EpochNumber((i * 100).toLong) + val block = OutputBlockMetadata( + epochNumber, + BlockNumber(epochNumber * 10L), + CantonTimestamp.fromProtoPrimitive(epochNumber * 10L).value, + ) + for { + _ <- outputStore.insertBlockIfMissing(block).futureUnlessShutdown() + _ <- creator.createPartitionsIfNeeded(epochNumber).futureUnlessShutdown() + } yield () + } + + // now searching for a block at the middle of all partitions will only scan half of them + result2 <- howManyPartitionsAreSearched(BlockNumber(49999)) + _ = result2 shouldBe 50 + // searching for a recent block is still bad + result3 <- howManyPartitionsAreSearched(BlockNumber(99999)) + _ = result3 shouldBe 100 + // searching for old blocks is very good + result4 <- howManyPartitionsAreSearched(BlockNumber(100)) + _ = result4 shouldBe 1 + + // now we add the upper bounds + _ <- MonadUtil.sequentialTraverse(1 to 99) { i => + val epochNumber = EpochNumber(((i + 1) * 100 - 1).toLong) + val block = OutputBlockMetadata( + epochNumber, + BlockNumber(epochNumber * 10L), + CantonTimestamp.fromProtoPrimitive(epochNumber * 10L).value, + ) + for { + _ <- outputStore.insertBlockIfMissing(block).futureUnlessShutdown() + _ <- creator.createPartitionsIfNeeded(epochNumber).futureUnlessShutdown() + } yield () + } + // now all searches should be fast + result5 <- howManyPartitionsAreSearched(BlockNumber(49999)) + _ = result5 shouldBe 1 + result6 <- howManyPartitionsAreSearched(BlockNumber(99999)) + _ = result6 shouldBe 1 + result7 <- howManyPartitionsAreSearched(BlockNumber(100)) + _ = result7 shouldBe 1 + + } yield succeed).onShutdown(fail()) + } + } + +} diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionMigrationTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionMigrationTest.scala new file mode 100644 index 0000000000..b6aff9459e --- /dev/null +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionMigrationTest.scala @@ -0,0 +1,123 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning + +import cats.data.EitherT +import com.digitalasset.canton.config.{ + DbConfig, + DbParametersConfig, + PartitionConfig, + ProcessingTimeout, +} +import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown, UnlessShutdown} +import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.resource.{DbMigrations, DbStorage} +import com.digitalasset.canton.store.db.DbStorageSetup.DbBasicConfig +import com.digitalasset.canton.store.db.{MigrationMode, PostgresTest} +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.BftSequencerBaseTest +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionMigrationTest.DbMigrationsTargeted +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.ResourceUtil +import org.scalatest.wordspec.AsyncWordSpec + +import scala.concurrent.ExecutionContext +import scala.util.control.NonFatal + +class PartitionMigrationTest extends AsyncWordSpec with BftSequencerBaseTest with PostgresTest { + @SuppressWarnings(Array("org.wartremover.warts.Var", "org.wartremover.warts.Null")) + protected var migration: DbMigrationsTargeted = _ + + override protected def cleanDb(storage: DbStorage)(implicit + tc: TraceContext + ): FutureUnlessShutdown[Unit] = FutureUnlessShutdown.unit + + override def mkDbConfig(basicConfig: DbBasicConfig): DbConfig.Postgres = { + val defaultDbConfig = super.mkDbConfig(basicConfig) + defaultDbConfig.copy(parameters = + DbParametersConfig(partitions = PartitionConfig(initialBftOrdererTablesPartitionSize = 100)) + ) + } + + override def beforeAll(): Unit = + try { + setup = createSetup().initialized() + migration = { + new DbMigrationsTargeted( + setup.config, + migrationMode == MigrationMode.DevVersion, + timeouts, + loggerFactory, + ) + } + val migrationResult = migration.migrateDatabaseAtTargetVersion("5.0") + migrationResult + .valueOr(err => fail(s"Failed to migrate database: $err")) + .onShutdown(fail("DB migration interrupted due to shutdown")) + // we deliberately do not call super.beforeAll(), to avoid the regular migration logic to be called + } catch { + case NonFatal(e) => + logger.error("beforeAll failed", e) + throw e + } + + "Partition Migration" should { + "create partitions and transfer data to partitioned tables" in { + import storage.api.* + import cats.syntax.parallel.* + import com.digitalasset.canton.util.FutureInstances.* + + val epochs = Seq(100, 200, 300, 400, 599) + + for { + _ <- epochs.parTraverse { epoch => + for { + _ <- storage + .update( + sqlu"""insert into ord_metadata_output_blocks(epoch_number, block_number, bft_ts) values ($epoch, $epoch,$epoch) on conflict (block_number) do nothing""", + "add block", + ) + .failOnShutdown + _ <- storage + .update( + sqlu"""insert into ord_epochs(epoch_number, start_block_number, epoch_length, topology_ts, in_progress) values ($epoch, $epoch,10, $epoch, false) on conflict (epoch_number) do nothing""", + "add block", + ) + .failOnShutdown + } yield () + } + _ = migration + .migrateDatabaseAtTargetVersion("latest") + .valueOr(err => fail(s"Failed to migrate database: $err")) + .failOnShutdown + + } yield succeed + } + } + +} + +object PartitionMigrationTest { + + class DbMigrationsTargeted( + dbConfig: DbConfig, + alphaVersionSupport: Boolean, + timeouts: ProcessingTimeout, + loggerFactory: NamedLoggerFactory, + )(implicit ec: ExecutionContext, closeContext: CloseContext) + extends DbMigrations(dbConfig, alphaVersionSupport, timeouts, loggerFactory) { + + def migrateDatabaseAtTargetVersion( + target: String + ): EitherT[UnlessShutdown, DbMigrations.Error, Unit] = + TraceContext.withNewTraceContext("migrate_database") { implicit traceContext => + withDb() { createdDb => + ResourceUtil.withResource(createdDb) { db => + val flyway = + createFlywayConfig(DbMigrations.createDataSource(db.source)).target(target).load() + migrateDatabaseInternal(flyway) + } + } + } + } +} diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/EmptyBlockSubscription.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/EmptyBlockSubscription.scala index 956f0c0614..d5c494e534 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/EmptyBlockSubscription.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/EmptyBlockSubscription.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework +import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.synchronizer.block.BlockFormat import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.BlockSubscription import com.digitalasset.canton.tracing.{TraceContext, Traced} @@ -11,9 +12,23 @@ import org.apache.pekko.stream.{KillSwitch, KillSwitches} class EmptyBlockSubscription extends BlockSubscription { + @volatile private var _sequencerCoreIsSlow: Boolean = false + @volatile private var _bufferSize: Int = 0 + override def subscription(): Source[Traced[BlockFormat.Block], KillSwitch] = Source.empty.viaMat(KillSwitches.single)(Keep.right) - override def receiveBlock(block: BlockFormat.Block)(implicit traceContext: TraceContext): Unit = + override def receiveBlock( + block: BlockFormat.Block + )(implicit traceContext: TraceContext, metricsContext: MetricsContext): Unit = () + + override def isSequencerCoreSlow: Boolean = _sequencerCoreIsSlow + + override def bufferSize: Int = _bufferSize + + def setSequencerCoreIsSlow(slow: Boolean, bufferSize: Int): Unit = { + _sequencerCoreIsSlow = slow + _bufferSize = bufferSize + } } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/SimulationBlockSubscription.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/SimulationBlockSubscription.scala index 20155ac72a..214b0a4ba4 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/SimulationBlockSubscription.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/SimulationBlockSubscription.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework +import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.synchronizer.block.BlockFormat import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.BftNodeId import com.digitalasset.canton.tracing.{TraceContext, Traced} @@ -19,6 +20,12 @@ class SimulationBlockSubscription( override def subscription(): Source[Traced[BlockFormat.Block], KillSwitch] = Source.empty.viaMat(KillSwitches.single)(Keep.right) - override def receiveBlock(block: BlockFormat.Block)(implicit traceContext: TraceContext): Unit = + override def receiveBlock( + block: BlockFormat.Block + )(implicit traceContext: TraceContext, metricsContext: MetricsContext): Unit = queue.addOne(thisNode -> Traced(block)) + + override def isSequencerCoreSlow: Boolean = false + + override def bufferSize: Int = 0 } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/FutureSimulator.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/FutureSimulator.scala index 06cd4609b6..61c08ddb7e 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/FutureSimulator.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/FutureSimulator.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.simulation +import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.ModuleName import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.{ @@ -27,6 +28,7 @@ class FutureSimulator( agenda: Agenda, settings: FutureSettings, state: FutureSimulator.FutureSimulatorState, + loggerFactory: NamedLoggerFactory, ) { private val random = new Random(settings.randomSeed) @@ -68,7 +70,8 @@ class FutureSimulator( futureResultToMessage: Try[FutureT] => Option[MessageT], traceContext: TraceContext, ): Unit = { - val allocator = new FutureSimulatorAllocator(state, settings, agenda, random, nodeId) + val allocator = + new FutureSimulatorAllocator(state, settings, agenda, random, nodeId, loggerFactory) val runningFuture = future.schedule(allocator) runningFuture.addContinuation( Continuation(nodeId, to, runningFuture, futureResultToMessage, traceContext) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/Simulation.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/Simulation.scala index 3969cd7270..0ee6f53977 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/Simulation.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/Simulation.scala @@ -103,6 +103,7 @@ class Simulation[OnboardingDataT, SystemNetworkMessageT, SystemInputMessageT, Cl agenda, simSettings.futureSettings, futureSimulatorState, + loggerFactory, ) // the init functions might have already sent messages that we need to add to the agenda @@ -395,7 +396,7 @@ class Simulation[OnboardingDataT, SystemNetworkMessageT, SystemInputMessageT, Cl ) => implicit val tc: TraceContext = traceContext logger.debug(s"Establish connection '$from' -> '$to' via endpoint $maybeP2PEndpoint") - maybeP2PEndpoint.map(_.id).foreach(p2pConnectionEventListener.onConnect) + p2pConnectionEventListener.onConnect(maybeP2PEndpoint.map(_.id)) p2pConnectionEventListener.onSequencerId(to, maybeP2PEndpoint) val machine = tryGetMachine(from) runNodeCollector(from, EventOriginator.FromNetwork, machine.nodeCollector) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/future/FutureSimulatorAllocator.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/future/FutureSimulatorAllocator.scala index 0a84224259..e09060f3a5 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/future/FutureSimulatorAllocator.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/simulation/future/FutureSimulatorAllocator.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.simulation.future +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.BftOrderingIdentifiers.BftNodeId import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.simulation.FutureSimulator.{ FutureSimulatorState, @@ -15,6 +16,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framewor FutureSettings, RunFuture, } +import com.digitalasset.canton.tracing.TraceContext import scala.util.Random @@ -24,7 +26,9 @@ class FutureSimulatorAllocator( agenda: Agenda, random: Random, nodeId: BftNodeId, -) extends FutureAllocator { + override val loggerFactory: NamedLoggerFactory, +) extends FutureAllocator + with NamedLogging { override def newFuture[T]( future: SimulationFuture[T], @@ -42,6 +46,7 @@ class FutureSimulatorAllocator( None, None, ) + logger.trace(s"Allocated Future (to be scheduled) $runningFuture")(TraceContext.empty) if (waitingFor.isEmpty) { agenda.addOne( RunFuture(nodeId, runningFuture), diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/simulation/BftOrderingSimulationTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/simulation/BftOrderingSimulationTest.scala index 190d1a11a3..607c1d2b7d 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/simulation/BftOrderingSimulationTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/simulation/BftOrderingSimulationTest.scala @@ -179,8 +179,9 @@ trait BftOrderingSimulationTest extends AnyFlatSpec with BftSequencerBaseTest { new SimulationAvailabilityStore(), simulationEpochStore, epochStoreReader = simulationEpochStore, - new SimulationOutputMetadataStore(fail(_)), + new SimulationOutputMetadataStore(endpointToTestBftNodeId(endpoint), fail(_)), new SimulationBftOrdererPruningSchedulerStore(), + None, ) }, initializeImmediately = true, @@ -216,8 +217,9 @@ trait BftOrderingSimulationTest extends AnyFlatSpec with BftSequencerBaseTest { new SimulationAvailabilityStore(), simulationEpochStore, epochStoreReader = simulationEpochStore, - new SimulationOutputMetadataStore(fail(_)), + new SimulationOutputMetadataStore(endpointToTestBftNodeId(endpoint), fail(_)), new SimulationBftOrdererPruningSchedulerStore(), + None, ) } endpointToTestBftNodeId(endpoint) -> SimulationTestNodeData( @@ -362,7 +364,7 @@ trait BftOrderingSimulationTest extends AnyFlatSpec with BftSequencerBaseTest { | | override def generateSettings: SimulationTestSettings = SimulationTestSettings( | numberOfInitialNodes = $numberOfInitialNodes, - | segmentLength = PositiveLong.tryCreate(${simulationTestSettings.segmentLength}), + | segmentLength = SegmentLength(PositiveLong.tryCreate(${simulationTestSettings.segmentLength.length.value})), | stages = NonEmpty( | Seq, | ${stages diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/simulation/topology/SequencerSnapshotOnboardingManager.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/simulation/topology/SequencerSnapshotOnboardingManager.scala index be776eb528..de0eab6adf 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/simulation/topology/SequencerSnapshotOnboardingManager.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/simulation/topology/SequencerSnapshotOnboardingManager.scala @@ -76,26 +76,34 @@ class SequencerSnapshotOnboardingManager( forNode: BftNodeId, ): BftOnboardingData = { val snapshot = nodeToSequencerSnapshotAdditionalInfo.get(forNode) - val blockFromSnapshotOrGenesis = + val pruningLowerBound = for { + storeForNode <- stores.get(forNode) + simulationOutputStore = storeForNode.outputStore.asInstanceOf[SimulationOutputMetadataStore] + lowerBound <- simulationOutputStore.lowerBoundForTesting() + } yield lowerBound.blockNumber + val snapshotLowerBound = snapshot .flatMap { // technically the block we want is somewhere later than this, but this is good enough _.nodeActiveAt.get(forNode).flatMap(_.firstBlockNumberInStartEpoch) } + val lowerBound = + pruningLowerBound + .orElse(snapshotLowerBound) .getOrElse(BlockNumber(0L)) BftOnboardingData( reasonForProvide match { case ReasonForProvide.ProvideForInit => - blockFromSnapshotOrGenesis + lowerBound case ReasonForProvide.ProvideForRestart => val upperBound = model .lastSequencerAcknowledgedBlock(forNode) - .getOrElse(blockFromSnapshotOrGenesis) + .getOrElse(lowerBound) BlockNumber( // the sequencer should be somewhere in between these two points random.between( - blockFromSnapshotOrGenesis, + lowerBound, upperBound + 1L, // make it inclusive ) ) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/time/TimeAdvancingTopologySubscriberV1Test.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/time/TimeAdvancingTopologySubscriberV1Test.scala index 1218136056..9832510bfb 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/time/TimeAdvancingTopologySubscriberV1Test.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/time/TimeAdvancingTopologySubscriberV1Test.scala @@ -16,6 +16,7 @@ import com.digitalasset.canton.protocol.messages.{ DefaultOpenEnvelope, TopologyTransactionsBroadcast, } +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.client.{ SendAsyncClientError, @@ -106,6 +107,7 @@ class TimeAdvancingTopologySubscriberV1Test extends AnyWordSpec with BaseTest { messageId = any[MessageId], aggregationRule = any[Option[AggregationRule]], callback = eqTo(SendCallback.empty), + trafficCostValidator = any[TrafficCostValidator], amplify = eqTo(false), useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -175,6 +177,7 @@ class TimeAdvancingTopologySubscriberV1Test extends AnyWordSpec with BaseTest { messageId = any[MessageId], aggregationRule = eqTo(expectedAggregationRule), callback = eqTo(SendCallback.empty), + trafficCostValidator = any[TrafficCostValidator], amplify = eqTo(false), useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -261,6 +264,7 @@ class TimeAdvancingTopologySubscriberV1Test extends AnyWordSpec with BaseTest { any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -322,6 +326,7 @@ class TimeAdvancingTopologySubscriberV1Test extends AnyWordSpec with BaseTest { any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/time/TimeAdvancingTopologySubscriberV2Test.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/time/TimeAdvancingTopologySubscriberV2Test.scala index 7ccea8a0b2..9c7e368823 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/time/TimeAdvancingTopologySubscriberV2Test.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/time/TimeAdvancingTopologySubscriberV2Test.scala @@ -14,6 +14,7 @@ import com.digitalasset.canton.protocol.messages.{ DefaultOpenEnvelope, TopologyTransactionsBroadcast, } +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.client.{ SendAsyncClientError, @@ -110,6 +111,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -133,6 +135,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { messageId = any[MessageId], aggregationRule = any[Option[AggregationRule]], callback = any[SendCallback], + trafficCostValidator = any[TrafficCostValidator], amplify = eqTo(false), useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -202,6 +205,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { messageId = any[MessageId], aggregationRule = eqTo(Some(expectedAggregationRule)), callback = any[SendCallback], + trafficCostValidator = any[TrafficCostValidator], amplify = eqTo(false), useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -275,6 +279,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { any[MessageId], any[Option[AggregationRule]], any[SendCallback], + trafficCostValidator = any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -311,6 +316,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { any[MessageId], any[Option[AggregationRule]], any[SendCallback], + trafficCostValidator = any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -387,6 +393,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -409,6 +416,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { messageId = any[MessageId], aggregationRule = any[Option[AggregationRule]], callback = any[SendCallback], + trafficCostValidator = any[TrafficCostValidator], amplify = eqTo(false), useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -482,6 +490,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { messageId = any[MessageId], aggregationRule = any[Option[AggregationRule]], callback = any[SendCallback], + trafficCostValidator = any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -505,6 +514,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { messageId = any[MessageId], aggregationRule = any[Option[AggregationRule]], callback = any[SendCallback], + trafficCostValidator = any[TrafficCostValidator], amplify = eqTo(false), useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -568,6 +578,7 @@ class TimeAdvancingTopologySubscriberV2Test extends AnyWordSpec with BaseTest { messageId = any[MessageId], aggregationRule = any[Option[AggregationRule]], callback = any[SendCallback], + trafficCostValidator = any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/sequencer/SequencerStateManagerTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/sequencer/SequencerStateManagerTest.scala index a90330b33a..588116325b 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/sequencer/SequencerStateManagerTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/sequencer/SequencerStateManagerTest.scala @@ -6,12 +6,7 @@ package com.digitalasset.canton.synchronizer.sequencing.sequencer import com.daml.nonempty.NonEmpty import com.digitalasset.canton.concurrent.FutureSupervisor import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.config.{ - BatchingConfig, - CachingConfigs, - DefaultProcessingTimeouts, - TopologyConfig, -} +import com.digitalasset.canton.config.{CachingConfigs, DefaultProcessingTimeouts, TopologyConfig} import com.digitalasset.canton.crypto.* import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} @@ -23,6 +18,7 @@ import com.digitalasset.canton.synchronizer.block.* import com.digitalasset.canton.synchronizer.block.LedgerBlockEvent.* import com.digitalasset.canton.synchronizer.block.data.memory.InMemorySequencerBlockStore import com.digitalasset.canton.synchronizer.block.update.{ + BlockProcessingParameters, BlockUpdateGeneratorImpl, InFlightAggregations, } @@ -224,7 +220,7 @@ class SequencerStateManagerTest alice, CantonTimestamp.MinValue.immediateSuccessor, ) - ts1 = stateManager.getHeadState.block.lastTs.immediatePredecessor + ts1 = stateManager.getPersistenceHeadState.block.lastTs.immediatePredecessor ack <- signedAcknowledgement(alice, ts1) wait2F = stateManager.waitForAcknowledgementToComplete(alice, ts1) _ = handleBlock(initialHeight + 1, ack) @@ -240,7 +236,7 @@ class SequencerStateManagerTest for { submissionReq1 <- senderSignedSubmissionRequest(alice) _ = handleBlock(initialHeight, Send(newTimestamp(), submissionReq1, sequencer)) - ts1 = stateManager.getHeadState.block.lastTs + ts1 = stateManager.getPersistenceHeadState.block.lastTs ts0 = ts1.immediatePredecessor ts2 = ts1.immediateSuccessor wait0F = stateManager.waitForAcknowledgementToComplete(alice, ts0) @@ -267,7 +263,7 @@ class SequencerStateManagerTest for { submissionReq1 <- senderSignedSubmissionRequest(alice) _ = handleBlock(initialHeight, Send(newTimestamp(), submissionReq1, sequencer)) - ts1 = stateManager.getHeadState.block.lastTs + ts1 = stateManager.getPersistenceHeadState.block.lastTs ts0 = ts1.immediatePredecessor wait0F = stateManager.waitForAcknowledgementToComplete(alice, ts0) wait1F = stateManager.waitForAcknowledgementToComplete(alice, ts1) @@ -374,14 +370,17 @@ class SequencerStateManagerTest cryptoApi, sequencer1, defaultRateLimiter, - orderingTimeFixMode = OrderingTimeFixMode.MakeStrictlyIncreasing, - lsuSequencingBounds = None, drSequencingTimeUpperBound = None, getAnnouncedLsu = None, producePostOrderingTopologyTicks = false, - SequencerTestMetrics, - BatchingConfig(), consistencyChecks = true, + parameters = BlockProcessingParameters( + orderingTimeFixMode = OrderingTimeFixMode.MakeStrictlyIncreasing, + lsuSequencingBounds = None, + parallelism = PositiveInt.two, + enablePrevalidation = true, + ), + SequencerTestMetrics, memberValidator = new SequencerMemberValidator { override def isMemberRegisteredAt(member: Member, time: CantonTimestamp)(implicit tc: TraceContext @@ -391,7 +390,7 @@ class SequencerStateManagerTest tc: TraceContext ): FutureUnlessShutdown[Map[Member, Boolean]] = ??? }, - loggerFactory, + loggerFactory = loggerFactory, )(closeContext, NoReportingTracerProvider.tracer) def signedAcknowledgement( @@ -444,12 +443,14 @@ class SequencerStateManagerTest store, trafficConsumedStore, asyncWriterParameters = AsyncWriterParameters(), + BlockSequencerStreamInstrumentationConfig(), enableInvariantCheck = true, + enablePrevalidation = true, + prevalidationParallelism = PositiveInt.two, + SequencerTestMetrics.block, timeouts, futureSupervisor, loggerFactory, - BlockSequencerStreamInstrumentationConfig(), - SequencerTestMetrics.block, )(executorService, traceContext) private val processingTimestampWatermark = diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/RateLimitManagerTesting.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/RateLimitManagerTesting.scala index 09b109a6e8..757194c424 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/RateLimitManagerTesting.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/RateLimitManagerTesting.scala @@ -52,6 +52,7 @@ trait RateLimitManagerTesting { this: BaseTest with HasExecutionContext => testedProtocolVersion, sequencerTrafficConfig, eventCostCalculator = eventCostCalculator, + lsuSequencingBounds = None, ) def mkRateLimiter(store: TrafficPurchasedStore) = @@ -66,6 +67,7 @@ trait RateLimitManagerTesting { this: BaseTest with HasExecutionContext => testedProtocolVersion, sequencerTrafficConfig, eventCostCalculator = new EventCostCalculator(loggerFactory), + lsuSequencingBounds = None, ) def mkRateLimiter( @@ -84,5 +86,6 @@ trait RateLimitManagerTesting { this: BaseTest with HasExecutionContext => testedProtocolVersion, sequencerTrafficConfig, eventCostCalculator = eventCostCalculator, + lsuSequencingBounds = None, ) } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/SequencerRateLimitManagerImplTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/SequencerRateLimitManagerImplTest.scala index 7996000c34..7a4235d2e7 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/SequencerRateLimitManagerImplTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencing/traffic/SequencerRateLimitManagerImplTest.scala @@ -46,7 +46,7 @@ import java.util.UUID import scala.concurrent.Future import scala.language.implicitConversions -class SequencerRateLimitManagerImplTest +final class SequencerRateLimitManagerImplTest extends FixtureAsyncWordSpec with BaseTest with HasExecutionContext diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/MockedNodeParameters.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/MockedNodeParameters.scala index 6ad33a2682..95c58b095d 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/MockedNodeParameters.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/MockedNodeParameters.scala @@ -23,7 +23,7 @@ object MockedNodeParameters { override def enablePreviewFeatures: Boolean = ??? - override def enableTestingFeatures: Boolean = ??? + override def enableTestingFeatures: Boolean = true override def enableAdditionalConsistencyChecks: Boolean = _enableAdditionalConsistencyChecks diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicCrypto.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicCrypto.scala index 30b6268f80..418a2fcf7e 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicCrypto.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicCrypto.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.crypto.store.memory.{ import com.digitalasset.canton.crypto.store.{CryptoPrivateStore, CryptoPublicStore} import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.version.ReleaseProtocolVersion import com.google.protobuf.ByteString @@ -34,6 +35,7 @@ class SymbolicCrypto( privateCrypto, cryptoPrivateStore, cryptoPublicStore, + CommonMockMetrics.cryptoMetrics, timeouts, loggerFactory, ) { diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicPrivateCrypto.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicPrivateCrypto.scala index 5d5b567168..90e42f222c 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicPrivateCrypto.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicPrivateCrypto.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.crypto.store.CryptoPrivateStoreExtended import com.digitalasset.canton.health.ComponentHealthState import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{CommonMockMetrics, DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.tracing.TraceContext import com.google.common.annotations.VisibleForTesting import com.google.protobuf.ByteString @@ -109,4 +110,9 @@ class SymbolicPrivateCrypto( override def name: String = "symbolic-private-crypto" override protected def initialHealthState: ComponentHealthState = ComponentHealthState.Ok() + + private val cryptoMetrics = CommonMockMetrics.cryptoMetrics + + override def signingMetrics: SigningMetrics = cryptoMetrics.signingMetrics + override def decryptionMetrics: DecryptionMetrics = cryptoMetrics.decryptionMetrics } diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicPureCrypto.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicPureCrypto.scala index fc3fc23df6..7650da00ff 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicPureCrypto.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/crypto/provider/symbolic/SymbolicPureCrypto.scala @@ -8,6 +8,12 @@ import com.daml.nonempty.NonEmpty import com.digitalasset.canton.config.CryptoParallelismConfig import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.crypto.* +import com.digitalasset.canton.metrics.{ + CommonMockMetrics, + CryptoMetrics, + DecryptionMetrics, + SigningMetrics, +} import com.digitalasset.canton.serialization.{DeserializationError, DeterministicEncoding} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.{ByteStringUtil, EitherUtil} @@ -53,7 +59,7 @@ class SymbolicPureCrypto extends CryptoPureApi { override def signatureVerificationParallelism: PositiveInt = CryptoParallelismConfig.defaultSignatureVerificationParallelism - override protected[crypto] def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKey: SigningPrivateKey, usage: NonEmpty[Set[SigningKeyUsage]], @@ -344,6 +350,10 @@ class SymbolicPureCrypto extends CryptoPureApi { .map(key => PasswordBasedEncryptionKey(key, salt)) } + val cryptoMetrics: CryptoMetrics = CommonMockMetrics.cryptoMetrics + + override def signingMetrics: SigningMetrics = cryptoMetrics.signingMetrics + override def decryptionMetrics: DecryptionMetrics = cryptoMetrics.decryptionMetrics } object SymbolicPureCrypto { diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/metrics/CommonMockMetrics.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/metrics/CommonMockMetrics.scala index 1d85d3c420..3249a5cd8e 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/metrics/CommonMockMetrics.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/metrics/CommonMockMetrics.scala @@ -20,5 +20,22 @@ object CommonMockMetrics { new DbStorageHistograms(prefix)(new HistogramInventory()), NoOpMetricsFactory, )(MetricsContext.Empty) + object cryptoMetrics + extends CryptoMetrics( + new SigningMetrics( + new SigningHistograms(prefix)(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty), + new DecryptionMetrics( + new DecryptionHistograms(prefix)(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty), + Some( + new KmsMetrics( + prefix, + NoOpMetricsFactory, + )(MetricsContext.Empty) + ), + ) } diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/topology/TestingIdentityFactory.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/topology/TestingIdentityFactory.scala index fe122e9913..48d8b2e427 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/topology/TestingIdentityFactory.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/topology/TestingIdentityFactory.scala @@ -23,6 +23,7 @@ import com.digitalasset.canton.crypto.provider.symbolic.SymbolicCrypto import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.protocol.{ DynamicSynchronizerParameters, StaticSynchronizerParameters, @@ -204,7 +205,7 @@ final case class TestingTopology( val existing = flags.getOrElse(participant, Seq.empty) flags.updated( participant, - (ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer +: existing).distinct, + (ParticipantTopologyFeatureFlag.EnableMultiSynchronizer +: existing).distinct, ) }) @@ -375,7 +376,7 @@ class TestingIdentityFactory( ips(availableUpToInclusive, currentSnapshotApproximationTimestamp), crypto, cryptoConfig, - None, + CommonMockMetrics.cryptoMetrics, CachingConfigs.defaultPublicKeyConversionCache, DefaultProcessingTimeouts.testing, FutureSupervisor.Noop, diff --git a/canton/community/traffic-enforcement/api/protobuf/buf.yaml b/canton/community/traffic-enforcement/api/protobuf/buf.yaml new file mode 100644 index 0000000000..510acf10bc --- /dev/null +++ b/canton/community/traffic-enforcement/api/protobuf/buf.yaml @@ -0,0 +1,5 @@ +version: v1 +build: + excludes: + - com/digitalasset/canton/tea/scalapb + diff --git a/canton/community/traffic-enforcement/api/protobuf/com/digitalasset/canton/tea/scalapb/package.proto b/canton/community/traffic-enforcement/api/protobuf/com/digitalasset/canton/tea/scalapb/package.proto new file mode 100644 index 0000000000..6433cdcb67 --- /dev/null +++ b/canton/community/traffic-enforcement/api/protobuf/com/digitalasset/canton/tea/scalapb/package.proto @@ -0,0 +1,14 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package com.digitalasset.canton.tea; + +import "scalapb/scalapb.proto"; + +option (scalapb.options) = { + scope: PACKAGE + preserve_unknown_fields: false + no_default_values_in_constructor: true +}; diff --git a/canton/community/traffic-enforcement/api/protobuf/com/digitalasset/canton/tea/v1/traffic_service.proto b/canton/community/traffic-enforcement/api/protobuf/com/digitalasset/canton/tea/v1/traffic_service.proto new file mode 100644 index 0000000000..019c7bc960 --- /dev/null +++ b/canton/community/traffic-enforcement/api/protobuf/com/digitalasset/canton/tea/v1/traffic_service.proto @@ -0,0 +1,62 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package com.digitalasset.canton.tea.v1; + +// Service used for book-keeping and managing of traffic accounts for Ledger API users. +service TrafficService { + // Get account state for a given account ID. + // Permissioned only to users having ActAs or ExecuteAs rights for the party ID associated with the account ID. + rpc GetAccount(GetAccountRequest) returns (GetAccountResponse); + + // Update account state for a given account ID. + // Permissioned only to Ledger API admin users. + rpc UpdateAccount(UpdateAccountRequest) returns (UpdateAccountResponse); +} + +message GetAccountRequest { + // Required + string account_id = 1; +} + +message GetAccountResponse { + // Required + string account_id = 1; + + // Required + int64 balance = 2; +} + +message UpdateAccountRequest { + // The account ID whose account to update. + // + // NOTE: Currently, the account ID is tied to and identified by a party ID. + // This constraint is expected to be removed in a future release, allowing for user-defined + // account IDs that are not necessarily tied to a specific party. + // + // Required + string account_id = 1; + + // Balance DELTA to apply to the current balance. + // Negative values will decrease the balance, positive values will increase the balance. + // If unset, the balance will not be updated + // + // Optional + optional int64 balance_delta = 2; + + // Unique identifier to this update request. + // This is used to de-duplicate requests. Caller MUST ensure that this is unique per + // request. The same request with the same deduplication_id will be ignored if it has already been processed. + // + // Required + string deduplication_id = 3; +} + +message UpdateAccountResponse { + // The account state after the update + // + // Required + GetAccountResponse response = 1; +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementApp.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementApp.scala new file mode 100644 index 0000000000..d82af5b3b0 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementApp.scala @@ -0,0 +1,86 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea + +import com.digitalasset.canton.config.CantonRequireTypes.InstanceName +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.lifecycle.{FlagCloseable, LifeCycle} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.platform.config.TrafficEnforcementServerConfig +import com.digitalasset.canton.resource.Storage +import com.digitalasset.canton.tea.TrafficEnforcementApp.TeaGrpcServerName +import com.digitalasset.canton.tea.projection.{EventSource, TeaProjection} +import com.digitalasset.canton.time.Clock +import io.grpc.inprocess.InProcessServerBuilder +import org.apache.pekko.actor.typed.ActorSystem +import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorSystemOps + +import scala.concurrent.ExecutionContext + +/** Top level TEA class. Creates an in-process gRPC server that exposes the + * [[com.digitalasset.canton.tea.v1.TrafficServiceGrpc.TrafficService]] via + * [[TrafficEnforcementServiceGrpc]]. + */ +class TrafficEnforcementApp( + service: TrafficEnforcementService, + node: InstanceName, + override val loggerFactory: NamedLoggerFactory, + override val timeouts: ProcessingTimeout, +)(implicit system: ActorSystem[?]) + extends NamedLogging + with FlagCloseable { + + import system.executionContext + + private val server = InProcessServerBuilder + .forName(s"$TeaGrpcServerName-$node") + .addService(new TrafficEnforcementServiceGrpc(service, loggerFactory)) + .build() + + // Start the in process gRPC server + server.start().discard + + override def onClosed(): Unit = { + val toClose = List( + LifeCycle.toCloseableServer(server, logger, TeaGrpcServerName), + LifeCycle.toCloseableActorSystem(system.classicSystem, logger, timeouts), + ) + LifeCycle.close(toClose*)(logger) + } +} + +object TrafficEnforcementApp { + val TeaGrpcServerName = "TeaGrpcInProcServer" + type TeaAppBuilder = () => TrafficEnforcementApp + + def internal( + forNode: InstanceName, + storage: Storage, + config: TrafficEnforcementServerConfig.Internal, + loggerFactory: NamedLoggerFactory, + timeouts: ProcessingTimeout, + clock: Clock, + )(implicit + ec: ExecutionContext + ): TeaAppBuilder = { () => + // Pekko config to configure the TEA's actor system + val pekkoConfig = config.pekkoConfig(storage) + implicit val system: ActorSystem[Nothing] = + org.apache.pekko.actor + .ActorSystem("TrafficEnforcementAppSystem", pekkoConfig) + .toTyped + + val (projectionFactory, store) = + TeaProjection.create( + storage, + EventSource.LedgerAPI, + config.projection, + loggerFactory, + timeouts, + ) + val service = new TrafficEnforcementService(store, clock, loggerFactory) + new TrafficEnforcementApp(service, forNode, loggerFactory, timeouts) + } +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementService.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementService.scala new file mode 100644 index 0000000000..713969fa8f --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementService.scala @@ -0,0 +1,122 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea + +import cats.data.EitherT +import cats.syntax.either.* +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.tea.TrafficEnforcementService.{ + InvalidArgument, + TrafficEnforcementServiceError, +} +import com.digitalasset.canton.tea.projection.{ + AccountId, + AccountState, + EventId, + EventSource, + EventType, + TeaTrafficStore, +} +import com.digitalasset.canton.tea.v1.{ + GetAccountRequest, + GetAccountResponse, + UpdateAccountRequest, + UpdateAccountResponse, +} +import com.digitalasset.canton.time.Clock +import com.digitalasset.canton.tracing.TraceContext + +import scala.concurrent.ExecutionContext + +/** Transport-agnostic Traffic Enforcement App (TEA) operations. + */ +class TrafficEnforcementService( + store: TeaTrafficStore, + clock: Clock, + override val loggerFactory: NamedLoggerFactory, +)(implicit ec: ExecutionContext) + extends NamedLogging { + + /** Return the local accounts (and their balances) configured for the requested account ID. */ + def getAccount(request: GetAccountRequest)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Either[TrafficEnforcementServiceError, GetAccountResponse]] = { + + val result = for { + accountId <- EitherT.fromEither[FutureUnlessShutdown]( + AccountId + .fromProtoPrimitive(request.accountId) + .leftMap(err => InvalidArgument(request.accountId, err.message)) + ) + balance <- EitherT + .liftF[FutureUnlessShutdown, TrafficEnforcementServiceError, Option[AccountState]]( + store.getBalance(accountId).value + ) + } yield { + balance match { + case Some(value) => GetAccountResponse(value.account.str.unwrap, value.balance) + // Returning balance 0L if the account is unknown + case None => GetAccountResponse(request.accountId, 0L) + } + } + + result.value + } + + /** Update the account state for the given account ID */ + def updateAccount(request: UpdateAccountRequest)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Either[TrafficEnforcementServiceError, UpdateAccountResponse]] = { + def processBalanceDelta(balanceDelta: Long) = for { + accountId <- EitherT.fromEither[FutureUnlessShutdown]( + AccountId + .fromProtoPrimitive(request.accountId) + .leftMap(err => InvalidArgument(request.accountId, err.message)) + ) + eventId <- EitherT.fromEither[FutureUnlessShutdown]( + EventId + .fromProtoPrimitive(request.deduplicationId) + .leftMap(err => InvalidArgument(request.deduplicationId, err.message)) + ) + newBalance <- EitherT + .liftF[FutureUnlessShutdown, TrafficEnforcementServiceError, Option[AccountState]]( + store + .persistDelta( + accountId, + eventId, + EventSource.TeaAPI, + EventType.Usage, + balanceDelta, + clock.now, + ) + .value + ) + } yield { + newBalance match { + case Some(accountState) => + UpdateAccountResponse( + Some(GetAccountResponse(accountState.account.unwrap, accountState.balance)) + ) + case None => + UpdateAccountResponse(None) + } + } + + request.balanceDelta match { + case Some(balanceDelta) => + processBalanceDelta(balanceDelta).value + case None => + FutureUnlessShutdown.pure(Right(UpdateAccountResponse(None))) + } + } +} + +object TrafficEnforcementService { + sealed trait TrafficEnforcementServiceError + final case class NotEnoughTraffic(account: String, balance: Long, cost: Long) + extends TrafficEnforcementServiceError + final case class InvalidArgument(provided: String, error: String) + extends TrafficEnforcementServiceError +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementServiceGrpc.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementServiceGrpc.scala new file mode 100644 index 0000000000..38066f5de6 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementServiceGrpc.scala @@ -0,0 +1,87 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea + +import cats.data.EitherT +import cats.syntax.either.* +import com.digitalasset.canton.ledger.api.grpc.GrpcApiService +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.* +import com.digitalasset.canton.tea.TrafficEnforcementService.{ + InvalidArgument, + NotEnoughTraffic, + TrafficEnforcementServiceError, +} +import com.digitalasset.canton.tea.v1.TrafficServiceGrpc.TrafficService +import com.digitalasset.canton.tea.v1.{ + GetAccountRequest, + GetAccountResponse, + TrafficServiceGrpc, + UpdateAccountRequest, + UpdateAccountResponse, +} +import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} +import io.grpc.{ServerServiceDefinition, Status, StatusRuntimeException} + +import scala.concurrent.{ExecutionContext, Future} + +/** Grpc service implementing [[com.digitalasset.canton.tea.v1.TrafficServiceGrpc.TrafficService]]. + */ +class TrafficEnforcementServiceGrpc( + service: TrafficEnforcementService, + val loggerFactory: NamedLoggerFactory, +)(implicit executionContext: ExecutionContext) + extends TrafficService + with GrpcApiService + with NamedLogging { + + override def getAccount( + request: GetAccountRequest + ): Future[GetAccountResponse] = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + EitherT( + service + .getAccount(request) + .map(_.leftMap(handleError)) + ).asGrpcResponse + } + + override def updateAccount( + request: UpdateAccountRequest + ): Future[UpdateAccountResponse] = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + EitherT( + service + .updateAccount(request) + .map(_.leftMap(handleError)) + ).asGrpcResponse + } + + /** Maps a [[TrafficEnforcementServiceError]] onto the gRPC status returned to the caller. */ + private def handleError( + error: TrafficEnforcementServiceError + )(implicit traceContext: TraceContext): StatusRuntimeException = + error match { + case NotEnoughTraffic(account, balance, cost) => + logger.info( + s"Rejecting traffic reservation for account $account: balance $balance is below cost $cost" + ) + Status.RESOURCE_EXHAUSTED + .withDescription( + s"Not enough traffic for account $account: balance $balance is below cost $cost" + ) + .asRuntimeException() + case InvalidArgument(provided, error) => + val message = s"Invalid argument '$provided': $error" + logger.debug(message) + Status.INVALID_ARGUMENT + .withDescription(message) + .asRuntimeException() + } + + override def bindService(): ServerServiceDefinition = + TrafficServiceGrpc.bindService(this, executionContext) + + override def close(): Unit = () +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/CloseableProjection.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/CloseableProjection.scala new file mode 100644 index 0000000000..8b5796e8fa --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/CloseableProjection.scala @@ -0,0 +1,65 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection + +import cats.Eval +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.lifecycle.{AsyncCloseable, FlagCloseable, LifeCycle} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.tracing.TraceContext +import org.apache.pekko.Done +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.apache.pekko.actor.typed.{ActorRef, ActorSystem, Terminated} +import org.apache.pekko.projection.{ProjectionBehavior, ProjectionId} + +import scala.concurrent.{Future, Promise} + +/** Wrapper class that can cleanly close a projection + */ +class CloseableProjection( + projectionId: ProjectionId, + projectionRef: ActorRef[ProjectionBehavior.Command], + override val loggerFactory: NamedLoggerFactory, + override val timeouts: ProcessingTimeout, +)(implicit system: ActorSystem[?]) + extends NamedLogging + with FlagCloseable { + + override def onClosed(): Unit = { + import TraceContext.Implicits.Empty.* + + LifeCycle.close( + AsyncCloseable(s"projection-$projectionId", stopAndAwait.value, timeouts.closing) + )(logger) + } + + /** Sends a Stop signal to a projection actor and returns a Future that completes when the + * projection has cleanly wound down and terminated. Wrap into Eval.later so it gets memoized + * even if onClosed is called multiple times + */ + private def stopAndAwait: Eval[Future[Done]] = Eval.later { + val promise = Promise[Done]() + + // Spawn a tiny, ephemeral watcher actor whose sole purpose is to monitor the death of the projection + system + .systemActorOf[Nothing]( + Behaviors.setup[Nothing] { context => + context.watch(projectionRef) + + // Send the graceful stop signal + projectionRef ! ProjectionBehavior.Stop + + Behaviors.receiveSignal[Nothing] { case (_, Terminated(`projectionRef`)) => + promise.trySuccess(Done).discard + Behaviors.stopped + } + }, + s"projection-shutdown-watcher-for-${projectionId.name}-${java.util.UUID.randomUUID()}", + ) + .discard + + promise.future + } +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/ProjectionEvent.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/ProjectionEvent.scala new file mode 100644 index 0000000000..c51cfa22cb --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/ProjectionEvent.scala @@ -0,0 +1,180 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection + +import com.digitalasset.canton.config.CantonRequireTypes.{ + LengthLimitedStringWrapper, + LengthLimitedStringWrapperCompanion, + String255, +} +import com.digitalasset.canton.data.CantonTimestamp +import slick.jdbc.{GetResult, SetParameter} + +/** Event changing the balance of an account + * + * @param delta + * amount by which the balance changes. Positive for credits, negative for debits. + * @param timestamp + * timestamp of the event + */ +final case class DeltaEvent( + delta: Long, + timestamp: CantonTimestamp, + eventType: EventType, + eventSource: EventSource, +) +object DeltaEvent { + implicit val getDeltaEvent: GetResult[DeltaEvent] = GetResult { r => + val delta = r.<<[Long] + val updatedAt = r.<<[CantonTimestamp] + val eventType = r.<<[EventType] + val eventSource = r.<<[EventSource] + DeltaEvent(delta, updatedAt, eventType, eventSource) + } +} + +/** Delta event with an offset + * @param deltaEvent + * delta event + * @param offset + * offset of the event + */ +final case class OffsetDeltaEvent(deltaEvent: DeltaEvent, offset: Long) + +/** A projection event coming from an input stream + * @param account + * account tied to the event + * @param event + * event + */ +final case class ProjectionEvent(account: AccountId, event: OffsetDeltaEvent) + +/** State of an account at a given point + * @param account + * account tied to the state + * @param totalDebits + * total debits on the account at this time + * @param totalCredits + * total credits on the account at this time + * @param updatedAt + * timestamp at which the state was updated + */ +final case class AccountState( + account: AccountId, + totalDebits: Long, + totalCredits: Long, + updatedAt: CantonTimestamp, +) { + + /** Traffic balance + */ + def balance: Long = totalCredits - totalDebits +} +object AccountState { + def apply(account: AccountId, balance: Long, updatedAt: CantonTimestamp): AccountState = + AccountState( + account = account, + totalDebits = if (balance < 0) -balance else 0L, + totalCredits = if (balance > 0) balance else 0L, + updatedAt = updatedAt, + ) + + implicit val getAccountStateResult: GetResult[AccountState] = GetResult { r => + val account = r.<<[AccountId] + val totalDebits = r.<<[Long] + val totalCredits = r.<<[Long] + val updatedAt = r.<<[CantonTimestamp] + AccountState(account, totalDebits, totalCredits, updatedAt) + } +} + +/** Represents a type of event. For now only "Usage" is supported which describes standard traffic + * usage. + * @param code + * unique code per event type + */ +sealed abstract class EventType(val code: Short) +object EventType { + + /** Standard traffic usage from transaction processing + */ + case object Usage extends EventType(0) + + val values: Set[EventType] = Set(Usage) + private def fromCode(code: Short): EventType = + values + .find(_.code == code) + .getOrElse( + throw new IllegalArgumentException(s"Unknown EventType code from DB: $code") + ) + + implicit val setEventType: SetParameter[EventType] = + SetParameter { (eventType, positionedParameters) => + positionedParameters.setShort(eventType.code) + } + + implicit val getEventType: GetResult[EventType] = + GetResult { positionedResult => + EventType.fromCode(positionedResult.nextShort()) + } +} + +/** Represents where the event came from. + * @param code + * unique code per event source + */ +sealed abstract class EventSource(val code: Short) +object EventSource { + + /** Events coming from the Ledger API (completion streams for debits so far) + */ + case object LedgerAPI extends EventSource(0) + + /** Events coming from the TEA API (UpdateAccount RPC) + */ + case object TeaAPI extends EventSource(1) + + val values: Set[EventSource] = Set(LedgerAPI, TeaAPI) + + private def fromCode(code: Short): EventSource = + values + .find(_.code == code) + .getOrElse( + throw new IllegalArgumentException(s"Unknown EventSource code from DB: $code") + ) + + implicit val setEventSource: SetParameter[EventSource] = + SetParameter { (eventSource, positionedParameters) => + positionedParameters.setShort(eventSource.code) + } + + implicit val getEventSource: GetResult[EventSource] = + GetResult { positionedResult => + EventSource.fromCode(positionedResult.nextShort()) + } +} + +/** Account Id wrapper class + * @param str + * account Id, limited to 255 characters + */ +final case class AccountId(str: String255) extends LengthLimitedStringWrapper + +object AccountId extends LengthLimitedStringWrapperCompanion[String255, AccountId] { + override def instanceName: String = "AccountId" + override protected def companion: String255.type = String255 + override protected def factoryMethodWrapper(str: String255): AccountId = AccountId(str) +} + +/** Event Id wrapper class + * @param str + * event Id, limited to 255 characters + */ +final case class EventId(str: String255) extends LengthLimitedStringWrapper + +object EventId extends LengthLimitedStringWrapperCompanion[String255, EventId] { + override def instanceName: String = "EventId" + override protected def companion: String255.type = String255 + override protected def factoryMethodWrapper(str: String255): EventId = EventId(str) +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/TeaProjection.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/TeaProjection.scala new file mode 100644 index 0000000000..7985982eb5 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/TeaProjection.scala @@ -0,0 +1,133 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection + +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} +import com.digitalasset.canton.platform.config.TrafficEnforcementServerConfig.ProjectionConfig +import com.digitalasset.canton.resource.{DbStorage, MemoryStorage, Storage} +import com.digitalasset.canton.tea.projection.db.{TeaDbProjection, TeaDbTrafficStore} +import com.digitalasset.canton.tea.projection.memory.{TeaMemoryProjection, TeaMemoryTrafficStore} +import com.digitalasset.canton.tracing.{TraceContext, Traced} +import org.apache.pekko.NotUsed +import org.apache.pekko.actor.typed.{ActorSystem, Behavior} +import org.apache.pekko.projection.scaladsl.SourceProvider +import org.apache.pekko.projection.{ + HandlerRecoveryStrategy, + ProjectionBehavior, + ProjectionId, + StatusObserver, +} +import org.apache.pekko.stream.scaladsl.Source + +import scala.concurrent.{ExecutionContext, Future} + +/** Shared storage backing all TEA ingestion projections. + * + * Single function that builds a projection from a stream source and handler The trait abstracts + * over the in-memory and DB implementations + */ +trait TeaProjection { this: NamedLogging => + + /** Build a projection behavior for a single ingestion stream, reusing the shared storage. */ + def projection( + projectionId: ProjectionId, + grpcSourceFactory: Option[Long] => Source[Traced[ProjectionEvent], ?], + ): Behavior[ProjectionBehavior.Command] + + // Logging observer to get insights into the lifecycle of the projection + protected val loggingObserver: StatusObserver[Traced[ProjectionEvent]] = + new StatusObserver[Traced[ProjectionEvent]] { + override def started(projectionId: ProjectionId): Unit = + logger.info(s"Starting projection for projectionId $projectionId")(TraceContext.empty) + override def failed(projectionId: ProjectionId, cause: Throwable): Unit = + logger.info( + s"Failed projection for projectionId $projectionId. It will be restarted.", + cause, + )(TraceContext.empty) + override def stopped(projectionId: ProjectionId): Unit = + logger.info(s"Stopped projection for projectionId $projectionId")(TraceContext.empty) + override def beforeProcess( + projectionId: ProjectionId, + envelope: Traced[ProjectionEvent], + ): Unit = + logger.trace(s"Ready to process event ${envelope.value} for projectionId $projectionId")( + envelope.traceContext + ) + override def afterProcess( + projectionId: ProjectionId, + envelope: Traced[ProjectionEvent], + ): Unit = + logger.trace(s"Processed event ${envelope.value} for projectionId $projectionId")( + envelope.traceContext + ) + override def offsetProgress(projectionId: ProjectionId, env: Traced[ProjectionEvent]): Unit = + logger.info(s"Stored offset ${env.value.event.offset} for projectionId $projectionId")( + env.traceContext + ) + override def error( + projectionId: ProjectionId, + env: Traced[ProjectionEvent], + cause: Throwable, + recoveryStrategy: HandlerRecoveryStrategy, + ): Unit = + logger.warn( + s"Error during envelope processing of ${env.value} for projectionId $projectionId", + cause, + )(env.traceContext) + } + + /** Create a projection source provider from a source of ProjectionEvent + * @param grpcSourceFactory + * the grpcSourceFactory: takes an optional offset (Long) and returns a source pulling events + * from this offset. The offset provided will be the last one stored (so processed). This + * matches with the LAPI "beginExclusive" semantics: the stream will start at the following + * offset. + */ + protected def createSourceProvider( + logger: TracedLogger, + projectionId: ProjectionId, + grpcSourceFactory: Option[Long] => Source[Traced[ProjectionEvent], ?], + )(implicit ec: ExecutionContext): SourceProvider[Long, Traced[ProjectionEvent]] = + new SourceProvider[Long, Traced[ProjectionEvent]] { + override def source( + offsetProvider: () => Future[Option[Long]] + ): Future[Source[Traced[ProjectionEvent], NotUsed]] = + offsetProvider().map { maybeOffset => + logger.info(s"Starting ingestion stream for $projectionId with offset $maybeOffset")( + TraceContext.empty + ) + grpcSourceFactory(maybeOffset).mapMaterializedValue(_ => NotUsed) + } + override def extractOffset(record: Traced[ProjectionEvent]): Long = record.value.event.offset + override def extractCreationTime(record: Traced[ProjectionEvent]): Long = + record.value.event.deltaEvent.timestamp.toEpochMilli + } +} + +object TeaProjection { + + /** Open the shared storage once, based on the configured storage backend. */ + def create( + storage: Storage, + eventSource: EventSource, + config: ProjectionConfig, + loggerFactory: NamedLoggerFactory, + timeouts: ProcessingTimeout, + )(implicit system: ActorSystem[?]): (TeaProjection, TeaTrafficStore) = { + import system.executionContext + + storage match { + case db: DbStorage => + val store = new TeaDbTrafficStore(db, loggerFactory, timeouts) + val projection: TeaProjection = + new TeaDbProjection(db, loggerFactory, store, eventSource, config) + (projection, store) + case _: MemoryStorage => + val store = new TeaMemoryTrafficStore() + val projection: TeaProjection = new TeaMemoryProjection(loggerFactory, store) + (projection, store) + } + } +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/TeaTrafficStore.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/TeaTrafficStore.scala new file mode 100644 index 0000000000..ce36219e7b --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/TeaTrafficStore.scala @@ -0,0 +1,69 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection + +import cats.data.OptionT +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.tracing.TraceContext + +/** Persistence store for the TEA. Provides methods to update and retrieve traffic for accounts. + */ +trait TeaTrafficStore { + + /** Return the current balance for an account + * @param accountId + * account to retrieve + * @return + * optional account state + */ + def getBalance(accountId: AccountId)(implicit + traceContext: TraceContext + ): OptionT[FutureUnlessShutdown, AccountState] + + /** Insert a new event into the event table, and updates the corresponding account state. The new + * balance be current balance + delta. Delta is positive for credits and negative for debits. + * + * Note: the account state returned may have a timestamp higher than this timestamp. That's + * because events can arrive out of order from different sources from different clocks. To avoid + * the account state timestamp moving back and forth, its timestamp is always kept to the most + * recent event that updated it. + * + * @param accountId + * account to update + * @param delta + * delta to apply + * @param timestamp + * timestamp of the update. There's no guarantee that the timestamp is strictly higher than + * previous entries. + * @return + * optional account state + */ + def persistDelta( + accountId: AccountId, + eventId: EventId, + eventSource: EventSource, + eventType: EventType, + delta: Long, + timestamp: CantonTimestamp, + )(implicit + traceContext: TraceContext + ): OptionT[FutureUnlessShutdown, AccountState] + + // Note: only used internally for testing, need to add pagination and / or streaming when exposed + /** Return events, ordered by timestamp, for an account from the given timestamp forward + * (inclusive). Note that this might NOT be the order in which events were applied to the + * balance, as there's no single clock all the event timestamps come from. + * + * @param accountId + * account to update + * @param fromInclusive + * timestamp to retrieve events from (inclusive) + * @return + * events since fromInclusive for accountId + */ + def getEvents(accountId: AccountId, fromInclusive: CantonTimestamp)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Seq[DeltaEvent]] +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbProjection.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbProjection.scala new file mode 100644 index 0000000000..4bccb521b3 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbProjection.scala @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection.db + +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.platform.config.TrafficEnforcementServerConfig.ProjectionConfig +import com.digitalasset.canton.resource.{DbStorage, DbStorageMulti, DbStorageSingle} +import com.digitalasset.canton.tea.projection.{ + EventId, + EventSource, + EventType, + ProjectionEvent, + TeaProjection, +} +import com.digitalasset.canton.tracing.Traced +import com.typesafe.config.{Config, ConfigFactory} +import org.apache.pekko.Done +import org.apache.pekko.actor.typed.{ActorSystem, Behavior} +import org.apache.pekko.projection.slick.{SlickHandler, SlickProjection} +import org.apache.pekko.projection.{HandlerRecoveryStrategy, ProjectionBehavior, ProjectionId} +import org.apache.pekko.stream.scaladsl.Source +import slick.basic.DatabaseConfig +import slick.dbio.DBIO +import slick.jdbc.JdbcProfile + +import scala.concurrent.ExecutionContext + +/** Pekko projection backed by a JDBC DB. + * @param dbStorage + * dbStorage coming from the participant node. + */ +private[projection] class TeaDbProjection( + dbStorage: DbStorage, + override val loggerFactory: NamedLoggerFactory, + store: TeaDbTrafficStore, + eventSource: EventSource, + config: ProjectionConfig, +)(implicit system: ActorSystem[?]) + extends TeaProjection + with NamedLogging { + implicit val ec: ExecutionContext = system.executionContext + + private val jdbcProfile: JdbcProfile = dbStorage.profile.jdbc + + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) + private val slickProjectionDbConfig: DatabaseConfig[JdbcProfile] = + new DatabaseConfig[JdbcProfile] { + override val profile: JdbcProfile = jdbcProfile + override def db: profile.backend.Database = + (dbStorage match { + case multi: DbStorageMulti => multi.writeDb + case single: DbStorageSingle => single.db + case _ => throw new IllegalArgumentException("unsupported db storage") + }) + // asInstanceOf is ugly but safe, + // the type checker can't verify because the db type here is a dependent type of the profile + // but DbStorage does return a JdbcBackend.Database + .asInstanceOf[profile.backend.Database] + override def config: Config = ConfigFactory.empty() + override def profileName: String = jdbcProfile.getClass.getName + override def profileIsObject: Boolean = true + } + + def projection( + projectionId: ProjectionId, + grpcSourceFactory: Option[Long] => Source[Traced[ProjectionEvent], ?], + ): Behavior[ProjectionBehavior.Command] = ProjectionBehavior { + SlickProjection + .exactlyOnce( + projectionId = projectionId, + sourceProvider = createSourceProvider(logger, projectionId, grpcSourceFactory), + databaseConfig = slickProjectionDbConfig, + handler = () => eventHandler(projectionId), + ) + .withStatusObserver(loggingObserver) + .withRecoveryStrategy( + HandlerRecoveryStrategy + .retryAndFail(config.maxRetries.value, config.retryDelay.asFiniteApproximation) + ) + } + + // Event handler for projection events + // Runs sequentially for each event + // Returns a DBIO that is run in the same transaction as the offset watermark bump, + // giving exactly once semantics and crash recovery + private def eventHandler(projectionId: ProjectionId) = new SlickHandler[Traced[ProjectionEvent]] { + override def process(envelope: Traced[ProjectionEvent]): DBIO[Done] = { + val account = envelope.value.account + val event = envelope.value.event + for { + _ <- store.persistDeltaDBIO( + accountId = account, + eventId = EventId.tryCreate(s"${projectionId.id}-${envelope.value.event.offset}"), + delta = event.deltaEvent.delta, + timestamp = event.deltaEvent.timestamp, + eventType = EventType.Usage, + eventSource = eventSource, + ) + } yield Done + } + } +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbTrafficStore.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbTrafficStore.scala new file mode 100644 index 0000000000..576c3fbdca --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbTrafficStore.scala @@ -0,0 +1,205 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection.db + +import cats.data.OptionT +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.resource.DbStorage.Profile +import com.digitalasset.canton.resource.{DbStorage, DbStore} +import com.digitalasset.canton.tea.projection.{ + AccountId, + AccountState, + DeltaEvent, + EventId, + EventSource, + EventType, + TeaTrafficStore, +} +import com.digitalasset.canton.tracing.TraceContext + +import scala.concurrent.ExecutionContext + +import AccountState.* + +/** Store for DB operations on traffic persistence. + */ +class TeaDbTrafficStore( + override val storage: DbStorage, + override val loggerFactory: NamedLoggerFactory, + override val timeouts: ProcessingTimeout, +)(implicit ec: ExecutionContext) + extends DbStore + with TeaTrafficStore { + import storage.api.* + + implicit val rowsAlteredAccountState: DbStorage.RowsAltered[Option[AccountState]] = _.isDefined + + override def persistDelta( + accountId: AccountId, + eventId: EventId, + eventSource: EventSource, + eventType: EventType, + delta: Long, + timestamp: CantonTimestamp, + )(implicit + traceContext: TraceContext + ): OptionT[FutureUnlessShutdown, AccountState] = OptionT( + storage.queryAndUpdate( + persistDeltaDBIO(accountId, eventId, eventSource, eventType, delta, timestamp), + "persist traffic delta", + ) + ) + + private def insertEventDBIO( + accountId: AccountId, + eventId: EventId, + eventSource: EventSource, + eventType: EventType, + delta: Long, + timestamp: CantonTimestamp, + ): DBIOAction[Option[Long], NoStream, Effect.Write & Effect.Read] = + storage.profile match { + case _: Profile.Postgres => + for { + insertedRows <- sql"""insert into par_traffic_enforcement_event + (account_id, event_id, event_source, event_type, amount, timestamp) values ($accountId, $eventId, $eventSource, $eventType, $delta, $timestamp) + on conflict (event_source, event_id) do nothing + returning sequence_nb""".as[Long] + singleEvent <- insertedRows.toList match { + case Nil => DBIO.successful(None) + case singleton :: Nil => DBIO.successful(Some(singleton)) + case moreThanOne => + DBIO.failed( + new RuntimeException("Inserted more than one row in the traffic event table") + ) + } + } yield singleEvent + // H2 doesn't handle on conflict do nothing so use the merge into syntax and then fetch the event to retrieve + // the generated sequencer_nb + case _: Profile.H2 => + for { + insertedRows <- sqlu""" + merge into par_traffic_enforcement_event t + using (values ($accountId, $eventId, $eventSource, $eventType, $delta, $timestamp)) as v(account_id, event_id, event_source, event_type, amount, timestamp) + on t.event_source = v.event_source and t.event_id = v.event_id + when not matched then + insert (event_id, event_source, event_type, account_id, amount, timestamp) + values (v.event_id, v.event_source, v.event_type, v.account_id, v.amount, v.timestamp) + when matched and 1 = 0 then -- This ensures it NEVER updates existing rows + update set t.event_id = v.event_id + """ + event <- + if (insertedRows == 0) DBIO.successful(None) + else if (insertedRows == 1) { + sql""" + select sequence_nb + from par_traffic_enforcement_event + where event_id = $eventId + """.as[Long].map(_.headOption) + } else + DBIO.failed( + new RuntimeException("Inserted more than one row in the traffic event table") + ) + } yield event + } + + private def updateBalanceDBIO( + accountId: AccountId, + eventType: EventType, + debitAmount: Long, + creditAmount: Long, + sequenceNb: Long, + timestamp: CantonTimestamp, + ) = + storage.profile match { + // We update the balance by trying an insert + // then on conflict we update the total debit / credit values by adding the inserted delta with the existing one. + // Timestamp is updated as greatest of existing + inserted + case _: Profile.Postgres => + sql"""insert into par_traffic_enforcement_balance + (account_id, event_sequence_nb, event_type, total_debits, total_credits, updated_at) values($accountId, $sequenceNb, $eventType, $debitAmount, $creditAmount, $timestamp) + on conflict (account_id, event_type) do update set + event_sequence_nb = excluded.event_sequence_nb, + total_debits = par_traffic_enforcement_balance.total_debits + excluded.total_debits, + total_credits = par_traffic_enforcement_balance.total_credits + excluded.total_credits, + updated_at = greatest(excluded.updated_at, par_traffic_enforcement_balance.updated_at) + returning account_id, total_debits, total_credits, updated_at""" + .as[AccountState] + .map(_.headOption) + case _: Profile.H2 => + for { + _ <- sqlu""" + merge into par_traffic_enforcement_balance t + using (values ($accountId, $sequenceNb, $eventType, $debitAmount, $creditAmount, $timestamp)) as v(account_id, event_sequence_nb, event_type, debit_amt, credit_amt, updated_at) + on t.account_id = v.account_id and t.event_type = v.event_type + when matched then + update set + event_sequence_nb = v.event_sequence_nb, + total_debits = t.total_debits + v.debit_amt, + total_credits = t.total_credits + v.credit_amt, + updated_at = greatest(v.updated_at, t.updated_at) + when not matched then + insert (account_id, event_sequence_nb, event_type, total_debits, total_credits, updated_at) + values (v.account_id, v.event_sequence_nb, v.event_type, v.debit_amt, v.credit_amt, v.updated_at) + """ + balance <- getBalanceDBIO(accountId) + } yield balance + } + + private[db] def persistDeltaDBIO( + accountId: AccountId, + eventId: EventId, + eventSource: EventSource, + eventType: EventType, + delta: Long, + timestamp: CantonTimestamp, + ): DBIOAction[Option[AccountState], NoStream, Effect.Write & Effect.Read] = { + val debitAmount = if (delta < 0) -delta else 0L + val creditAmount = if (delta > 0) delta else 0L + + for { + // Start by inserting the event in the event table + // Deduplicate using the event id, this will tell us whether we need to update the balance table + insertedEvent <- insertEventDBIO(accountId, eventId, eventSource, eventType, delta, timestamp) + balanceUpdate <- + // If the event was inserted (not a duplicate), update the balance table + insertedEvent match { + case Some(sequenceNb) => + updateBalanceDBIO( + accountId, + eventType, + debitAmount, + creditAmount, + sequenceNb, + timestamp, + ) + case None => getBalanceDBIO(accountId) + } + } yield balanceUpdate + } + + def getBalance(accountId: AccountId)(implicit + traceContext: TraceContext + ): OptionT[FutureUnlessShutdown, AccountState] = storage.querySingle( + getBalanceDBIO(accountId), + "get_traffic_balance", + ) + + private def getBalanceDBIO(accountId: AccountId) = + sql"select account_id, total_debits, total_credits, updated_at from par_traffic_enforcement_balance where account_id = $accountId" + .as[AccountState] + .map(_.headOption) + + override def getEvents(accountId: AccountId, fromInclusive: CantonTimestamp)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Seq[DeltaEvent]] = + storage.query( + sql"select amount, timestamp, event_type, event_source from par_traffic_enforcement_event where account_id = $accountId and timestamp >= $fromInclusive order by timestamp" + .as[DeltaEvent], + "get_events", + ) +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/memory/TeaMemoryProjection.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/memory/TeaMemoryProjection.scala new file mode 100644 index 0000000000..0d0a01beb1 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/memory/TeaMemoryProjection.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection.memory + +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.tea.projection.{EventId, ProjectionEvent, TeaProjection} +import com.digitalasset.canton.tracing.Traced +import org.apache.pekko.Done +import org.apache.pekko.actor.typed.Behavior +import org.apache.pekko.projection.scaladsl.Handler +import org.apache.pekko.projection.testkit.scaladsl.TestProjection +import org.apache.pekko.projection.{ProjectionBehavior, ProjectionId} +import org.apache.pekko.stream.scaladsl.Source + +import scala.concurrent.{ExecutionContext, Future} + +/** In memory only projection. Offsets are not persisted. + */ +private[projection] class TeaMemoryProjection( + override val loggerFactory: NamedLoggerFactory, + store: TeaMemoryTrafficStore, +)(implicit + ec: ExecutionContext +) extends TeaProjection + with NamedLogging { + override def projection( + projectionId: ProjectionId, + grpcSourceFactory: Option[Long] => Source[Traced[ProjectionEvent], ?], + ): Behavior[ProjectionBehavior.Command] = { + // The in memory projection comes from pekko test-kit that's why it's called TestProjection + val inMemoryProjection = TestProjection( + projectionId = projectionId, + sourceProvider = createSourceProvider(logger, projectionId, grpcSourceFactory), + handler = () => inMemoryHandler(projectionId), + ) + .withStatusObserver(loggingObserver) + ProjectionBehavior(inMemoryProjection) + } + + private def inMemoryHandler(projectionId: ProjectionId) = new Handler[Traced[ProjectionEvent]] { + override def process(envelope: Traced[ProjectionEvent]): Future[Done] = + store + .persistDeltaInternal( + envelope.value.account, + EventId.tryCreate(s"${projectionId.id}-${envelope.value.event.offset}"), + envelope.value.event.deltaEvent, + ) + .map(_ => Done) + } +} diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/memory/TeaMemoryTrafficStore.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/memory/TeaMemoryTrafficStore.scala new file mode 100644 index 0000000000..1b0d2b31a2 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/memory/TeaMemoryTrafficStore.scala @@ -0,0 +1,123 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection.memory + +import cats.data.OptionT +import cats.implicits.* +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.tea.projection.memory.TeaMemoryTrafficStore.EventKey +import com.digitalasset.canton.tea.projection.{ + AccountId, + AccountState, + DeltaEvent, + EventId, + EventSource, + EventType, + TeaTrafficStore, +} +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.Mutex + +import java.util.concurrent.atomic.AtomicBoolean +import scala.collection.immutable.VectorMap +import scala.concurrent.{ExecutionContext, Future} + +object TeaMemoryTrafficStore { + private final case class EventKey(eventSource: EventSource, eventId: EventId) +} + +class TeaMemoryTrafficStore(implicit ec: ExecutionContext) extends TeaTrafficStore { + + private val lock = new Mutex() + private val balances = scala.collection.mutable.Map.empty[AccountId, AccountState] + // Mapping Account -> Map[EventId -> Event] + // use a VectorMap to maintain insertion order + private val events = + scala.collection.mutable.Map.empty[AccountId, VectorMap[EventKey, DeltaEvent]] + + override def getBalance(accountId: AccountId)(implicit + traceContext: TraceContext + ): OptionT[FutureUnlessShutdown, AccountState] = + OptionT.fromOption[FutureUnlessShutdown](balances.get(accountId)) + + override def persistDelta( + accountId: AccountId, + eventId: EventId, + eventSource: EventSource, + eventType: EventType, + delta: Long, + timestamp: CantonTimestamp, + )(implicit + traceContext: TraceContext + ): OptionT[FutureUnlessShutdown, AccountState] = + OptionT[Future, AccountState]( + persistDeltaInternal(accountId, eventId, DeltaEvent(delta, timestamp, eventType, eventSource)) + ) + .mapK(FutureUnlessShutdown.outcomeK) + + private[memory] def persistDeltaInternal( + account: AccountId, + eventId: EventId, + deltaEvent: DeltaEvent, + ): Future[Option[AccountState]] = Future.successful { + lock.exclusive { + val eventPersisted = new AtomicBoolean(false) + + val debitAmount = if (deltaEvent.delta < 0) -deltaEvent.delta else 0L + val creditAmount = if (deltaEvent.delta > 0) deltaEvent.delta else 0L + + val key = EventKey(deltaEvent.eventSource, eventId) + events + .updateWith(account) { + case None => + eventPersisted.set(true) + Some(VectorMap(key -> deltaEvent)) + case Some(existingEvents) if !existingEvents.contains(key) => + eventPersisted.set(true) + Some(existingEvents.updated(key, deltaEvent)) + case Some(existingEvents) => + Some(existingEvents) + } + .discard + + if (eventPersisted.get()) { + balances + .updateWith(account) { + case None => + Some( + AccountState( + account, + debitAmount, + creditAmount, + deltaEvent.timestamp, + ) + ) + case Some(state) => + Some( + state.copy( + totalDebits = state.totalDebits + debitAmount, + totalCredits = state.totalCredits + creditAmount, + updatedAt = deltaEvent.timestamp.max(state.updatedAt), + ) + ) + } + } else { + balances.get(account) + } + } + } + + override def getEvents(accountId: AccountId, fromInclusive: CantonTimestamp)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Seq[DeltaEvent]] = FutureUnlessShutdown.pure { + events + .get(accountId) + .toList + .flatMap(_.values) + .sortBy(_.timestamp) + .dropWhile(_.timestamp.isBefore(fromInclusive)) + } +} diff --git a/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/TeaProjectionTest.scala b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/TeaProjectionTest.scala new file mode 100644 index 0000000000..09df8e640b --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/TeaProjectionTest.scala @@ -0,0 +1,257 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection + +import com.digitalasset.canton.BaseTest +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.time.SimClock +import com.digitalasset.canton.tracing.Traced +import com.typesafe.config.{Config, ConfigFactory} +import org.apache.pekko.NotUsed +import org.apache.pekko.actor.testkit.typed.scaladsl.ActorTestKit +import org.apache.pekko.actor.typed.{ActorRef, ActorSystem} +import org.apache.pekko.projection.{ProjectionBehavior, ProjectionId} +import org.apache.pekko.stream.scaladsl.Source +import org.scalatest.wordspec.AnyWordSpec + +import java.util.UUID +import java.util.concurrent.ConcurrentLinkedQueue +import scala.concurrent.duration.* +import scala.jdk.CollectionConverters.* + +/** Shared behaviour for the TEA ingestion projection tests + */ +trait TeaProjectionTest extends BaseTest { this: AnyWordSpec => + + /** A storage backend providing the store under test together with a factory that rebuilds a + * projection writing into that same store. Calling [[Backend.newProjection]] more than once + * simulates restarting the node on top of the same storage. + */ + protected trait Backend { + def store: TeaTrafficStore + def newProjection(): TeaProjection + } + + def additionalPekkoConfig: Config = ConfigFactory.empty() + + protected val clock = new SimClock(loggerFactory = loggerFactory) + + /** Whether the backend persists offsets across projection restarts. The DB backends keep the + * offset in the pekko offset store, the in-memory backend does not (it replays from the start + * and relies on store-level deduplication). + */ + protected def offsetsArePersisted: Boolean + + /** Build a fresh backend bound to the given actor system. */ + protected def createBackend()(implicit system: ActorSystem[?]): Backend + + // Stable projection id so that the offset is keyed consistently across restarts. + protected val projectionId: ProjectionId = ProjectionId("debit-ingestion", "grpc-stream") + + // Keep the restart backoff small so crash-recovery tests stay fast. + private def pekkoTestConfig: Config = + ConfigFactory + .parseString( + """pekko.projection.restart-backoff { + | min-backoff = 200 ms + | max-backoff = 1 s + | random-factor = 0.0 + |} + |""".stripMargin + ) + .withFallback(additionalPekkoConfig) + + /** Run a test body with a dedicated actor system and a fresh backend, tearing everything down + * afterwards. + */ + protected def withProjection[A](body: (ActorTestKit, Backend) => A): A = { + val testKit = ActorTestKit(s"tea-projection-${UUID.randomUUID()}", pekkoTestConfig) + try body(testKit, createBackend()(testKit.system)) + finally testKit.shutdownTestKit() + } + + protected def projectionEvent( + account: AccountId, + delta: Long, + offset: Long, + timestamp: CantonTimestamp, + eventType: EventType = EventType.Usage, + eventSource: EventSource = EventSource.LedgerAPI, + ): ProjectionEvent = + ProjectionEvent( + account, + OffsetDeltaEvent(DeltaEvent(delta, timestamp, eventType, eventSource), offset), + ) + + private def tracedSource( + events: Seq[ProjectionEvent] + ): Source[Traced[ProjectionEvent], NotUsed] = + Source(events.toList) + .map(event => Traced.empty(event)) + // Keep the stream open after the (finite) test events so the projection idles in a steady + // state instead of completing and being restarted in a loop. + .concat(Source.never[Traced[ProjectionEvent]]) + + /** A source factory that resumes from the offset it is handed and records every offset it is + * asked to resume from. + */ + protected def recordingSourceFactory( + events: Seq[ProjectionEvent], + resumeOffsets: ConcurrentLinkedQueue[Option[Long]] = new ConcurrentLinkedQueue[Option[Long]](), + ): Option[Long] => Source[Traced[ProjectionEvent], NotUsed] = { maybeOffset => + resumeOffsets.add(maybeOffset) + val fromExclusive = maybeOffset.getOrElse(Long.MinValue) + tracedSource(events.filter(_.event.offset > fromExclusive)) + } + + private def balanceOf(backend: Backend, account: AccountId): Option[AccountState] = + backend.store.getBalance(account).value.futureValueUS + + private def eventsOf( + backend: Backend, + account: AccountId, + from: CantonTimestamp, + ): Seq[DeltaEvent] = + backend.store.getEvents(account, from).futureValueUS + + private def spawnProjection( + testKit: ActorTestKit, + backend: Backend, + sourceFactory: Option[Long] => Source[Traced[ProjectionEvent], NotUsed], + ): ActorRef[ProjectionBehavior.Command] = + testKit.spawn( + backend.newProjection().projection(projectionId, sourceFactory), + s"projection-${UUID.randomUUID()}", + ) + + private def stopProjection( + testKit: ActorTestKit, + ref: ActorRef[ProjectionBehavior.Command], + ): Unit = + testKit.stop(ref, 30.seconds) + + private val alice = AccountId.tryCreate("alice") + + def teaProjection(): Unit = { + "TeaProjection" should { + + "ingest events from the source into the store" in withProjection { (testKit, backend) => + val t1 = clock.now + val t2 = t1.immediateSuccessor + val eventType = EventType.Usage + val eventSource = EventSource.LedgerAPI + val events = Seq( + projectionEvent(alice, 10, offset = 1L, t1), + projectionEvent(alice, -3, offset = 2L, t2), + ) + + val ref = spawnProjection(testKit, backend, recordingSourceFactory(events)) + try { + eventually() { + balanceOf(backend, alice) shouldBe Some(AccountState(alice, 3, 10, t2)) + } + eventsOf(backend, alice, t1) should contain theSameElementsAs Seq( + DeltaEvent(10, t1, eventType, eventSource), + DeltaEvent(-3, t2, eventType, eventSource), + ) + } finally stopProjection(testKit, ref) + } + + "deduplicate events delivered more than once" in withProjection { (testKit, backend) => + val t1 = clock.now + val t2 = t1.immediateSuccessor + val eventType = EventType.Usage + val eventSource = EventSource.LedgerAPI + // Offset 1 is delivered twice; the store deduplicates on the derived event id, so the + // duplicate must neither be counted in the balance nor stored a second time. + val events = Seq( + projectionEvent(alice, 10, offset = 1L, t1), + projectionEvent(alice, 10, offset = 1L, t1), + projectionEvent(alice, 5, offset = 2L, t2), + ) + + val ref = spawnProjection(testKit, backend, recordingSourceFactory(events)) + try { + eventually() { + // 10 (offset 1, counted once) + 5 (offset 2); the duplicate +10 is ignored. + balanceOf(backend, alice) shouldBe Some(AccountState(alice, 15, t2)) + } + eventsOf(backend, alice, t1) should contain theSameElementsAs Seq( + DeltaEvent(10, t1, eventType, eventSource), + DeltaEvent(5, t2, eventType, eventSource), + ) + } finally stopProjection(testKit, ref) + } + + "resume from the last processed offset after a restart" in withProjection { + (testKit, backend) => + val t1 = clock.now + val t2 = t1.immediateSuccessor + val t3 = t2.immediateSuccessor + val events = Seq( + projectionEvent(alice, 10, offset = 1L, t1), + projectionEvent(alice, 20, offset = 2L, t2), + projectionEvent(alice, 30, offset = 3L, t3), + ) + + // First run ingests everything. + val firstRun = spawnProjection(testKit, backend, recordingSourceFactory(events)) + eventually() { + balanceOf(backend, alice) shouldBe Some(AccountState(alice, 60, t3)) + } + stopProjection(testKit, firstRun) + + // Second run: a fresh projection re-subscribes; we record the offset it resumes from. + val resumeOffsets = new ConcurrentLinkedQueue[Option[Long]]() + val secondRun = + spawnProjection(testKit, backend, recordingSourceFactory(events, resumeOffsets)) + try { + eventually() { + resumeOffsets.asScala.toList should not be empty + } + val expectedResumeOffset = if (offsetsArePersisted) Some(3L) else None + resumeOffsets.peek() shouldBe expectedResumeOffset + + // Whatever is replayed, deduplication keeps the balance and the event log correct. + always() { + balanceOf(backend, alice) shouldBe Some(AccountState(alice, 60, t3)) + } + eventsOf(backend, alice, t1) should have size 3 + } finally stopProjection(testKit, secondRun) + } + + "recover and finish pending work after a crash and restart" in withProjection { + (testKit, backend) => + val t1 = clock.now + val t2 = t1.immediateSuccessor + val t3 = t2.immediateSuccessor + val t4 = t3.immediateSuccessor + val allEvents = Seq( + projectionEvent(alice, 10, offset = 1L, t1), + projectionEvent(alice, 20, offset = 2L, t2), + projectionEvent(alice, 30, offset = 3L, t3), + projectionEvent(alice, 40, offset = 4L, t4), + ) + + // The first instance only ingests the first two events before the node "crashes". + val crashed = spawnProjection(testKit, backend, recordingSourceFactory(allEvents.take(2))) + eventually() { + balanceOf(backend, alice) shouldBe Some(AccountState(alice, 30, t2)) + } + stopProjection(testKit, crashed) + + // After the restart the new instance sees the full stream and must finish the pending + // work without re-applying the events that were already processed. + val restarted = spawnProjection(testKit, backend, recordingSourceFactory(allEvents)) + try { + eventually() { + balanceOf(backend, alice) shouldBe Some(AccountState(alice, 100, t4)) + } + // Every event is applied exactly once. + eventsOf(backend, alice, t1) should have size 4 + } finally stopProjection(testKit, restarted) + } + } + } +} diff --git a/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/TeaTrafficStoreTest.scala b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/TeaTrafficStoreTest.scala new file mode 100644 index 0000000000..4916faf5fb --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/TeaTrafficStoreTest.scala @@ -0,0 +1,221 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection + +import com.digitalasset.canton.time.SimClock +import com.digitalasset.canton.{BaseTest, FailOnShutdown, ProtocolVersionChecksAsyncWordSpec} +import org.scalatest.BeforeAndAfterAll +import org.scalatest.wordspec.AsyncWordSpec + +import java.util.UUID + +trait TeaTrafficStoreTest + extends BeforeAndAfterAll + with BaseTest + with ProtocolVersionChecksAsyncWordSpec + with FailOnShutdown { + this: AsyncWordSpec => + + protected val clock = new SimClock(loggerFactory = loggerFactory) + + private def uniqueId = EventId.tryCreate(UUID.randomUUID().toString) + + private val eventType = EventType.Usage + private val eventSource = EventSource.LedgerAPI + private val alice = AccountId.tryCreate("alice") + private val butternut = AccountId.tryCreate("butternut") + + def teaTrafficStore(mk: () => TeaTrafficStore): Unit = { + "TeaTrafficStore" should { + "return empty for unknown account" in { + val store = mk() + store.getBalance(AccountId.tryCreate("unknown")).value.map(_ shouldBe empty) + } + + "insert events in the store" in { + val store = mk() + val timestamp1 = clock.now + val timestamp2 = timestamp1.immediateSuccessor + val timestamp3 = timestamp2.immediateSuccessor + for { + afterPersist1 <- store + .persistDelta(alice, uniqueId, eventSource, eventType, 10, timestamp1) + .value + getBalance1 <- store.getBalance(alice).value + afterPersist2 <- store + .persistDelta(alice, uniqueId, eventSource, eventType, -5, timestamp2) + .value + getBalance2 <- store.getBalance(alice).value + afterPersist3 <- store + .persistDelta(alice, uniqueId, eventSource, eventType, -3, timestamp3) + .value + getBalance3 <- store.getBalance(alice).value + } yield { + val expected1 = AccountState(alice, 10, timestamp1) + val expected2 = AccountState(alice, 5, 10, timestamp2) + val expected3 = AccountState(alice, 8, 10, timestamp3) + + afterPersist1 shouldBe Some(expected1) + getBalance1 shouldBe Some(expected1) + afterPersist2 shouldBe Some(expected2) + getBalance2 shouldBe Some(expected2) + afterPersist3 shouldBe Some(expected3) + getBalance3 shouldBe Some(expected3) + } + } + + "scope by accounts" in { + val store = mk() + val timestamp1 = clock.now + val timestamp2 = timestamp1.immediateSuccessor + for { + _ <- store + .persistDelta(alice, uniqueId, eventSource, eventType, 10, timestamp1) + .value + _ <- store + .persistDelta(butternut, uniqueId, eventSource, eventType, -5, timestamp2) + .value + getBalanceAlice <- store.getBalance(alice).value + getBalanceButternut <- store.getBalance(butternut).value + } yield { + val expectedAlice = AccountState(alice, 10, timestamp1) + val expectedButternut = + AccountState(butternut, totalDebits = 5, totalCredits = 0, timestamp2) + + getBalanceAlice shouldBe Some(expectedAlice) + getBalanceButternut shouldBe Some(expectedButternut) + } + } + + "handle out of order update timestamps" in { + val store = mk() + val timestamp1 = clock.now + val timestamp2 = timestamp1.immediatePredecessor + for { + afterPersist1 <- store + .persistDelta(alice, uniqueId, eventSource, eventType, 10, timestamp1) + .value + getBalance1 <- store.getBalance(alice).value + afterPersist2 <- store + .persistDelta(alice, uniqueId, eventSource, eventType, -3, timestamp2) + .value + getBalance2 <- store.getBalance(alice).value + } yield { + val expected1 = AccountState(alice, 0, 10, timestamp1) + // Should still be timestamp1 because it's more recent + val expected2 = AccountState(alice, 3, 10, timestamp1) + + afterPersist1 shouldBe Some(expected1) + getBalance1 shouldBe Some(expected1) + afterPersist2 shouldBe Some(expected2) + getBalance2 shouldBe Some(expected2) + } + } + + "support negative balances" in { + val store = mk() + val timestamp1 = clock.now + for { + afterPersist <- store + .persistDelta(alice, uniqueId, eventSource, eventType, -10, timestamp1) + .value + getBalance <- store.getBalance(alice).value + } yield { + val expected = AccountState(alice, -10, timestamp1) + + afterPersist shouldBe Some(expected) + getBalance shouldBe Some(expected) + } + } + + "retrieve events" in { + val store = mk() + val timestamp1 = clock.now + val timestamp2 = timestamp1.immediateSuccessor + for { + _ <- store.persistDelta(alice, uniqueId, eventSource, eventType, 10, timestamp1).value + events1 <- store.getEvents(alice, timestamp1) + _ <- store.persistDelta(alice, uniqueId, eventSource, eventType, -5, timestamp2).value + events2 <- store.getEvents(alice, timestamp1) + // Filter by timestamp 2, should drop the first event + events3 <- store.getEvents(alice, timestamp2) + } yield { + events1 should contain theSameElementsInOrderAs Seq( + DeltaEvent(10, timestamp1, eventType, eventSource) + ) + events2 should contain theSameElementsInOrderAs Seq( + DeltaEvent(10, timestamp1, eventType, eventSource), + DeltaEvent(-5, timestamp2, eventType, eventSource), + ) + events3 should contain theSameElementsInOrderAs Seq( + DeltaEvent(-5, timestamp2, eventType, eventSource) + ) + } + } + + "retrieve events ordered by timestamp" in { + val store = mk() + val timestamp1 = clock.now + // Event 2 has a timestamp older than event 1 + val timestamp2 = timestamp1.immediatePredecessor + for { + _ <- store.persistDelta(alice, uniqueId, eventSource, eventType, 10, timestamp1).value + _ <- store.persistDelta(alice, uniqueId, eventSource, eventType, -5, timestamp2).value + events <- store.getEvents(alice, timestamp2) + } yield { + events should contain theSameElementsInOrderAs Seq( + DeltaEvent(-5, timestamp2, eventType, eventSource), + DeltaEvent(10, timestamp1, eventType, eventSource), + ) + } + } + + "deduplicate on (event_source, event_id)" in { + val store = mk() + val timestamp1 = clock.now + for { + _ <- store + .persistDelta(alice, EventId.tryCreate("id-1"), eventSource, eventType, 10, timestamp1) + .value + events1 <- store.getEvents(alice, timestamp1) + getBalance1 <- store.getBalance(alice).value + _ <- store + .persistDelta(alice, EventId.tryCreate("id-1"), eventSource, eventType, 20, timestamp1) + .value + events2 <- store.getEvents(alice, timestamp1) + getBalance2 <- store.getBalance(alice).value + // Unicity is per (event_source, event_id), so a different source should go through + _ <- store + .persistDelta( + alice, + EventId.tryCreate("id-1"), + EventSource.TeaAPI, + eventType, + 20, + timestamp1, + ) + .value + events3 <- store.getEvents(alice, timestamp1) + getBalance3 <- store.getBalance(alice).value + } yield { + getBalance1 shouldBe Some(AccountState(alice, 10, timestamp1)) + // Balance2 should not have changed + getBalance2 shouldBe Some(AccountState(alice, 10, timestamp1)) + getBalance3 shouldBe Some(AccountState(alice, 30, timestamp1)) + + val expectedEvents1 = Seq(DeltaEvent(10, timestamp1, eventType, eventSource)) + events1 should contain theSameElementsInOrderAs expectedEvents1 + events2 should contain theSameElementsInOrderAs expectedEvents1 + events3 should contain theSameElementsInOrderAs expectedEvents1 :+ DeltaEvent( + 20, + timestamp1, + eventType, + EventSource.TeaAPI, + ) + } + } + } + } + +} diff --git a/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/db/DbTeaProjectionTest.scala b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/db/DbTeaProjectionTest.scala new file mode 100644 index 0000000000..658c767db2 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/db/DbTeaProjectionTest.scala @@ -0,0 +1,81 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection.db + +import com.daml.nameof.NameOf.functionFullName +import com.digitalasset.canton.BaseTest +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.platform.config.TrafficEnforcementServerConfig +import com.digitalasset.canton.platform.config.TrafficEnforcementServerConfig.ProjectionConfig +import com.digitalasset.canton.resource.DbStorage +import com.digitalasset.canton.store.db.{DbTest, H2Test, MigrationMode, PostgresTest} +import com.digitalasset.canton.tea.projection.{ + EventSource, + TeaProjection, + TeaProjectionTest, + TeaTrafficStore, +} +import com.digitalasset.canton.tracing.TraceContext +import com.typesafe.config.Config +import org.apache.pekko.actor.typed.ActorSystem +import org.scalatest.wordspec.AnyWordSpec + +trait DbTeaProjectionTest extends AnyWordSpec with BaseTest with TeaProjectionTest { + this: DbTest => + + override def cleanDb( + storage: DbStorage + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + import storage.api.* + storage.update( + DBIO.seq( + sqlu"truncate table par_traffic_enforcement_event", + sqlu"truncate table par_traffic_enforcement_balance", + sqlu"truncate table pekko_projection_offset_store", + sqlu"truncate table pekko_projection_management", + ), + functionFullName, + ) + } + + // The DB backends persist the offset in the pekko offset store, so a restarted projection + // resumes exactly where the previous one left off. + override protected def offsetsArePersisted: Boolean = true + + override def additionalPekkoConfig: Config = + TrafficEnforcementServerConfig.Internal().pekkoConfig(storage.underlying) + + // The store is a stateless query wrapper; data isolation between tests is provided by cleanDb. + // The execution context comes from HasExecutionContext (mixed in via DbTest). + private lazy val dbStore: TeaDbTrafficStore = + new TeaDbTrafficStore(storage, loggerFactory, timeouts) + + override protected def createBackend()(implicit system: ActorSystem[?]): Backend = + new Backend { + override val store: TeaTrafficStore = dbStore + // The slick projection needs the raw single/multi storage, not the idempotency wrapper. + override def newProjection(): TeaProjection = + new TeaDbProjection( + storage.underlying, + loggerFactory, + dbStore, + EventSource.LedgerAPI, + ProjectionConfig(), + ) + } + + "DbTeaProjection" should { + behave like teaProjection() + } +} + +class DbTeaProjectionPostgresTest extends DbTeaProjectionTest with PostgresTest { + // TODO(i33278): remove when migrations are stable + override def migrationMode: MigrationMode = MigrationMode.DevVersion +} + +class DbTeaProjectionH2Test extends DbTeaProjectionTest with H2Test { + // TODO(i33278): remove when migrations are stable + override def migrationMode: MigrationMode = MigrationMode.DevVersion +} diff --git a/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/db/DbTeaTrafficStoreTest.scala b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/db/DbTeaTrafficStoreTest.scala new file mode 100644 index 0000000000..3c43f7529a --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/db/DbTeaTrafficStoreTest.scala @@ -0,0 +1,52 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection.db + +import com.daml.nameof.NameOf.functionFullName +import com.digitalasset.canton.BaseTest +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.resource.DbStorage +import com.digitalasset.canton.store.db.{DbTest, H2Test, MigrationMode, PostgresTest} +import com.digitalasset.canton.tea.projection.TeaTrafficStoreTest +import com.digitalasset.canton.tracing.TraceContext +import org.scalatest.wordspec.AsyncWordSpec + +trait DbTeaTrafficStoreTest extends AsyncWordSpec with BaseTest with TeaTrafficStoreTest { + this: DbTest => + + override def cleanDb( + storage: DbStorage + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { + import storage.api.* + storage.update( + DBIO.seq( + sqlu"truncate table par_traffic_enforcement_event", + sqlu"truncate table par_traffic_enforcement_balance", + sqlu"truncate table pekko_projection_offset_store", + sqlu"truncate table pekko_projection_management", + ), + functionFullName, + ) + } + + "TeaTrafficStore" should { + behave like teaTrafficStore(() => + new TeaDbTrafficStore( + storage, + loggerFactory, + timeouts, + ) + ) + } +} + +class DbTeaTrafficStorePostgresTest extends DbTeaTrafficStoreTest with PostgresTest { + // TODO(i33278): remove when migrations are stable + override def migrationMode: MigrationMode = MigrationMode.DevVersion +} + +class DbTeaTrafficStoreH2Test extends DbTeaTrafficStoreTest with H2Test { + // TODO(i33278): remove when migrations are stable + override def migrationMode: MigrationMode = MigrationMode.DevVersion +} diff --git a/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/memory/MemoryTeaProjectionTest.scala b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/memory/MemoryTeaProjectionTest.scala new file mode 100644 index 0000000000..d38bca5e30 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/memory/MemoryTeaProjectionTest.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection.memory + +import com.digitalasset.canton.BaseTest +import com.digitalasset.canton.tea.projection.{TeaProjection, TeaProjectionTest, TeaTrafficStore} +import org.apache.pekko.actor.typed.ActorSystem +import org.scalatest.wordspec.AnyWordSpec + +import scala.concurrent.ExecutionContext + +class MemoryTeaProjectionTest extends AnyWordSpec with BaseTest with TeaProjectionTest { + + // The in-memory projection uses the pekko TestProjection which does not persist offsets across + // restarts, so a restarted projection replays from the beginning and relies on store-level + // deduplication. + override protected def offsetsArePersisted: Boolean = false + + override protected def createBackend()(implicit system: ActorSystem[?]): Backend = { + implicit val ec: ExecutionContext = system.executionContext + val memoryStore = new TeaMemoryTrafficStore() + new Backend { + override val store: TeaTrafficStore = memoryStore + override def newProjection(): TeaProjection = + new TeaMemoryProjection(loggerFactory, memoryStore) + } + } + + "MemoryTeaProjection" should { + behave like teaProjection() + } +} diff --git a/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/memory/MemoryTeaTrafficStoreTest.scala b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/memory/MemoryTeaTrafficStoreTest.scala new file mode 100644 index 0000000000..114c4e0a57 --- /dev/null +++ b/canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/memory/MemoryTeaTrafficStoreTest.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.tea.projection.memory + +import com.digitalasset.canton.BaseTest +import com.digitalasset.canton.tea.projection.TeaTrafficStoreTest +import org.scalatest.wordspec.AsyncWordSpec + +class MemoryTeaTrafficStoreTest extends AsyncWordSpec with BaseTest with TeaTrafficStoreTest { + + "MemoryTeaTrafficStore" should { + behave like teaTrafficStore(() => new TeaMemoryTrafficStore()) + } + +} diff --git a/canton/community/transcode/daml-examples/src/main/daml/examples/Conformance.daml b/canton/community/transcode/daml-examples/src/main/daml/examples/Conformance.daml index 36a7bf8306..3d869d43d2 100644 --- a/canton/community/transcode/daml-examples/src/main/daml/examples/Conformance.daml +++ b/canton/community/transcode/daml-examples/src/main/daml/examples/Conformance.daml @@ -18,6 +18,8 @@ import qualified DA.TextMap import qualified DA.Time import qualified DA.Date +data Void + template Roundtrip with party: Party @@ -199,6 +201,11 @@ template Roundtrip assert (payload == payload) return payload + nonconsuming choice Primitives_ContractId_Void_1: ContractId Conformance.Void with payload: ContractId Conformance.Void controller party + do + assert (payload == payload) + return payload + nonconsuming choice Enums_Foo_1: Com.Digitalasset.Transcode.Conformance.Data.Enums.Foo with payload: Com.Digitalasset.Transcode.Conformance.Data.Enums.Foo controller party do assert (payload == Com.Digitalasset.Transcode.Conformance.Data.Enums.Bar) diff --git a/canton/community/transcode/daml-lf/src/main/scala/com/digitalasset/transcode/daml_lf/LfSchemaProcessor.scala b/canton/community/transcode/daml-lf/src/main/scala/com/digitalasset/transcode/daml_lf/LfSchemaProcessor.scala index bbee3b45c9..b6bd7f2df8 100644 --- a/canton/community/transcode/daml-lf/src/main/scala/com/digitalasset/transcode/daml_lf/LfSchemaProcessor.scala +++ b/canton/community/transcode/daml-lf/src/main/scala/com/digitalasset/transcode/daml_lf/LfSchemaProcessor.scala @@ -109,6 +109,7 @@ private class LfSchemaProcessor[R]( visitor.constructor(id, Seq.empty, lazyBody()) }, ) + case TUnknown(id, args) => visitor.unknown(id, args.map(fromType)) case TTyConApp(id, cons, params, args) => val ctor = tpeCache.getOrElseUpdate( id, { @@ -190,6 +191,12 @@ private class LfSchemaProcessor[R]( .contains(id.qualifiedName.name) } + private val isUnknown = cached { (id: Ref.Identifier) => + val pkg = getPackage(id.packageId) + !pkg.modules.contains(id.qualifiedName.module) || + !pkg.modules(id.qualifiedName.module).definitions.contains(id.qualifiedName.name) + } + private val getPackageInfo = cached { (id: Ref.PackageId) => getMetadataDetails(getPackage(id), id) } @@ -235,6 +242,12 @@ private class LfSchemaProcessor[R]( dataCons <- condOpt(viewDef) { case Ast.DDataType(_, _, cons) => cons } yield (getIdentifier(id), dataCons) } + private object TUnknown { + def unapply(tpe: Ast.Type): Option[(Identifier, Seq[Ast.Type])] = condOpt(tpe) { + case Util.TTyConApp(id, args) if isUnknown(id) => (getIdentifier(id), args.toSeq) + case Ast.TSynApp(id, args) if isUnknown(id) => (getIdentifier(id), args.toSeq) + } + } private val logger = LoggerFactory.getLogger(getClass.getName) private def warnIfNot(cond: Boolean, msg: => String): Unit = if !cond then logger.warn(msg) diff --git a/canton/community/transcode/daml-lf/src/test/scala/com/digitalasset/transcode/daml_lf/SchemaProcessorSpecDefault.scala b/canton/community/transcode/daml-lf/src/test/scala/com/digitalasset/transcode/daml_lf/SchemaProcessorSpecDefault.scala index cbc2d7434b..516c9a98a6 100644 --- a/canton/community/transcode/daml-lf/src/test/scala/com/digitalasset/transcode/daml_lf/SchemaProcessorSpecDefault.scala +++ b/canton/community/transcode/daml-lf/src/test/scala/com/digitalasset/transcode/daml_lf/SchemaProcessorSpecDefault.scala @@ -3,9 +3,8 @@ package com.digitalasset.transcode.daml_lf +import com.digitalasset.daml.lf.archive.DarSchemaDecoder import com.digitalasset.transcode.DamlExamples -import com.digitalasset.transcode.daml_lf.Util -import com.digitalasset.transcode.daml_lf.synonyms.DarDecoder import com.digitalasset.transcode.schema.* import zio.test.* import zio.test.diff.Diff.DiffOps @@ -15,9 +14,10 @@ import scala.collection.mutable import scala.language.implicitConversions trait SchemaProcessorSpecDefault extends ZIOSpecDefault: - private val dar = DarDecoder.assertReadArchiveFromFile(DamlExamples.darPath.toFile) + // DarSchemaDecoder skips decoding non-serializable types (e.g. Void) + // This is useful to test unknown types private val packages = - dar.all.map((pkgId, pkg) => pkgId -> Util.toSignature(pkg)).toMap + DarSchemaDecoder.assertReadArchiveFromFile(DamlExamples.darPath.toFile).all.toMap private val dictionary = LfSchemaProcessor .process(packages, IdentifierFilter.AcceptAll)(DescriptorVisitor) @@ -152,6 +152,25 @@ trait SchemaProcessorSpecDefault extends ZIOSpecDefault: ) case (left: Descriptor.ContractId, right: Descriptor.ContractId) => go(left.value, right.value) + case (left: Descriptor.Unknown, right: Descriptor.Unknown) => + DiffResult.Nested( + "Unknown", + List( + Some("id") -> left.id.diffed(right.id), + Some("args") -> DiffResult.Nested( + "$", + left.args.toList + .map(Option.apply) + .zipAll(right.args.toList.map(Option.apply), None, None) + .collect { + case (Some(l), Some(r)) => go(l, r) + case (None, Some(r)) => DiffResult.Added(r) + case (Some(l), None) => DiffResult.Removed(l) + } + .map(None -> _), + ), + ), + ) case (left, right) => DiffResult.Different(left, right) diff --git a/canton/community/transcode/daml-lf/src/test/scala/com/digitalasset/transcode/daml_lf/package.scala b/canton/community/transcode/daml-lf/src/test/scala/com/digitalasset/transcode/daml_lf/package.scala deleted file mode 100644 index c380969c31..0000000000 --- a/canton/community/transcode/daml-lf/src/test/scala/com/digitalasset/transcode/daml_lf/package.scala +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.transcode.daml_lf - -package object synonyms: - type Dar[A] = com.digitalasset.daml.lf.archive.Dar[A] - val DarDecoder = com.digitalasset.daml.lf.archive.DarDecoder diff --git a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/CodecVisitor.scala b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/CodecVisitor.scala index 1fa4411f7a..d237377561 100644 --- a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/CodecVisitor.scala +++ b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/CodecVisitor.scala @@ -72,6 +72,13 @@ trait CodecVisitor[T] (typeParams zip appliedArgs).toMap } + override final def unknown(id: Identifier, args: Seq[Type]): Type = new Type: + import Debug.show + override def toDynamicValue(v: T)(using VarMap[Decoder[T]]): DynamicValue = + throw new RuntimeException(s"Failed to decode payload: unknown type ${id.show}") + override def fromDynamicValue(dv: DynamicValue)(using VarMap[Encoder[T]]): T = + throw new RuntimeException(s"Unexpected value of type ${id.show}") + extension [A](array: Array[A]) inline def getMaybe(ix: Int): A = if ix < array.length then array(ix) diff --git a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Debug.scala b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Debug.scala index e0374715e4..9a64fe0ca9 100644 --- a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Debug.scala +++ b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Debug.scala @@ -55,6 +55,15 @@ object Debug { case Descriptor.Date => buf.append("date") case Descriptor.Party => buf.append("party") case ContractId(value) => buf.append("contractId("); go(value); buf.append(")") + case Unknown(id, args) => + buf.append(s""): Unit + if args.nonEmpty then + buf.append("("): Unit + args.zipWithIndex.foreach { (arg, ix) => + go(arg) + if ix < args.length - 1 then buf.append(", ") + } + buf.append(")") buf.append("--- Dictionary ---").append(System.lineSeparator()): Unit buf @@ -123,6 +132,6 @@ object Debug { buf.toSeq.sortBy(x => (x._1.packageName, x._1.moduleName, x._1.entityName)).map(_._2) extension (id: Identifier) - private def show: String = + def show: String = s"${id.packageName}:${id.moduleName}:${id.entityName}#${id.packageVersion}/${id.packageId}" } diff --git a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Descriptor.scala b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Descriptor.scala index 509ff65eb2..67bb9b0ea2 100644 --- a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Descriptor.scala +++ b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Descriptor.scala @@ -183,6 +183,11 @@ object Descriptor: final case class Variable private[Descriptor] (name: TypeVarName) extends Descriptor def variable(name: String): Variable = Variable(TypeVarName(name)) + final case class Unknown private[Descriptor] (id: Identifier, args: SList[Descriptor]) + extends Descriptor + def unknown(id: Identifier, args: Seq[Descriptor]) = Unknown(id, args.toList) + def unknown(id: Identifier): Unknown = Unknown(id, SList.empty) + /** Utility to handle cyclic references */ private object Lazy: private[Descriptor] def apply(compute: => Adt) = new Lazy(compute) @@ -215,6 +220,7 @@ object Descriptor: case TextMap(value) => queue.addOne(value) case GenMap(key, value) => queue.addOne(key); queue.addOne(value) case ContractId(value) => queue.addOne(value) + case Unknown(id, args) => queue.addAll(args) case _ => // do nothing result.toSeq diff --git a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/DescriptorSchemaProcessor.scala b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/DescriptorSchemaProcessor.scala index 07cd52a1ef..e70bdcab25 100644 --- a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/DescriptorSchemaProcessor.scala +++ b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/DescriptorSchemaProcessor.scala @@ -66,5 +66,6 @@ private class DescriptorSchemaProcessor[R]( ) case Descriptor.Application(ctor @ Descriptor.Constructor(id, typeParams, body), args) => visitor.application(handle(ctor), typeParams, args.map(handle)) + case Descriptor.Unknown(id, args) => visitor.unknown(id, args.map(handle)) } diff --git a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/DescriptorVisitor.scala b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/DescriptorVisitor.scala index 83b02e6e6d..a6d2e638e7 100644 --- a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/DescriptorVisitor.scala +++ b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/DescriptorVisitor.scala @@ -40,4 +40,5 @@ object DescriptorVisitor extends SchemaVisitor { args: Seq[Descriptor], ): Descriptor = Descriptor.application(ctor.asInstanceOf[Descriptor.Constructor], args) + def unknown(id: Identifier, args: Seq[Descriptor]): Descriptor = Descriptor.unknown(id, args) } diff --git a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Pickler.scala b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Pickler.scala index 8e09de0ad4..4531eb0647 100644 --- a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Pickler.scala +++ b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/Pickler.scala @@ -31,6 +31,7 @@ object Pickler extends PicklerHelper: .addConcreteType[Constructor] .addConcreteType[Application] .addConcreteType[Variable] + .addConcreteType[Unknown] // Schema's specialized types given Pickler[Choice[Descriptor]] = generatePickler[Choice[Descriptor]] diff --git a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/SchemaVisitor.scala b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/SchemaVisitor.scala index 8a96d226f4..d1634e9dc2 100644 --- a/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/SchemaVisitor.scala +++ b/canton/community/transcode/schema/src/main/scala/com/digitalasset/transcode/schema/SchemaVisitor.scala @@ -125,6 +125,17 @@ trait SchemaVisitor: /** Type Application */ def application(value: Type, typeParams: Seq[TypeVarName], args: Seq[Type]): Type + + /** Unknown Type. A template can reference `ContractId U` where U is unknown because + * non-serializable. This type cannot be used for decoding payloads. + * + * @param id + * The type or type constructor identifier + * @param args + * The type arguments or empty if id is not a type constructor. This cannot be modeled as an + * application because the type param names are unknown. + */ + def unknown(id: Identifier, args: Seq[Type]): Type end SchemaVisitor object SchemaVisitor: @@ -182,6 +193,8 @@ object SchemaVisitor: (left.variant(leftCases), right.variant(rightCases)) override def enumeration(cases: Seq[EnumConName]): Type = (left.enumeration(cases), right.enumeration(cases)) + override def unknown(id: Identifier, args: Seq[Type]): Type = + (left.unknown(id, args.map(_._1)), right.unknown(id, args.map(_._2))) override def list(elem: Type): Type = (left.list(elem._1), right.list(elem._2)) override def optional(elem: Type): Type = (left.optional(elem._1), right.optional(elem._2)) override def textMap(value: Type): Type = (left.textMap(value._1), right.textMap(value._2)) @@ -239,6 +252,7 @@ object SchemaVisitor: ): Type = {} override def variable(name: TypeVarName): Type = {} override def application(value: Type, typeParams: Seq[TypeVarName], args: Seq[Type]): Type = {} + override def unknown(id: Identifier, args: Seq[scala.Unit]): Type = () end Unit trait Delegate[T <: SchemaVisitor, R](protected val delegate: T)( @@ -271,6 +285,7 @@ object SchemaVisitor: delegate.constructor(id, typeParams, value) def application(value: Type, typeParams: Seq[TypeVarName], args: Seq[Type]): Type = delegate.application(value, typeParams, args) + def unknown(id: Identifier, args: Seq[Type]): Type = delegate.unknown(id, args) trait WithResult[R] extends SchemaVisitor: final type Result = R diff --git a/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/data/Primitives.scala b/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/data/Primitives.scala index 2e57e2ddb2..f4aec7b5cb 100644 --- a/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/data/Primitives.scala +++ b/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/data/Primitives.scala @@ -88,3 +88,7 @@ trait Primitives extends TestCase: contractId(constructor(RoundtripId, record("party" -> party))), DV.ContractId("0" * 2 + "1" * 136), ) + addCase( + contractId(unknown(Void)), + DV.ContractId("0" * 2 + "1" * 136), + ) diff --git a/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/generator/DamlRoundtrip.scala b/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/generator/DamlRoundtrip.scala index a5cd42a43a..c7bafad1bc 100644 --- a/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/generator/DamlRoundtrip.scala +++ b/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/generator/DamlRoundtrip.scala @@ -203,6 +203,9 @@ data ${id.entityName} = ${cases.mkString(" | ")} s"DA.Date.date (${date.getYear}) DA.Date.$month ${date.getDayOfMonth}" case Descriptor.Party => "payload" // sentinel, not implemented yet case Descriptor.ContractId(value) => "payload" // sentinel, not implemented yet + case Descriptor.Unknown(id, _) => + import Debug.show + throw RuntimeException(s"Failed to encode value: unknown type ${id.show}") def toDamlRef(d: Descriptor)(using Defs): (Seq[Identifier], String) = d match case Descriptor.List(value) => @@ -238,6 +241,12 @@ data ${id.entityName} = ${cases.mkString(" | ")} id +: imports.flatten, s"${id.moduleName}.${id.entityName} ${refs.map(x => s"($x)").mkString(" ")}", ) + case Descriptor.Unknown(id, args) => + val (imports, refs) = args.map(toDamlRef(_)).unzip + ( + id +: imports.flatten, + s"${id.moduleName}.${id.entityName} ${refs.map(x => s"($x)").mkString(" ")}", + ) private def toId(fqn: String) = Identifier( PackageId("0"), diff --git a/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/package.scala b/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/package.scala index 30912f03da..432ff930ca 100644 --- a/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/package.scala +++ b/canton/community/transcode/test-conformance/src/main/scala/com/digitalasset/transcode/conformance/package.scala @@ -7,6 +7,7 @@ import com.digitalasset.transcode.schema.* package object conformance: val RoundtripId: Identifier = Identifier.fromString("examples:Conformance:Roundtrip") + val Void: Identifier = Identifier.fromString("examples:Conformance:Void") trait TestCase: @SuppressWarnings(Array("org.wartremover.warts.Var")) @@ -88,5 +89,9 @@ package object conformance: case Descriptor.TextMap(value) => s"TextMap_${descriptorPart(value)}" case Descriptor.GenMap(key, value) => s"GenMap_${descriptorPart(key)}_${descriptorPart(value)}" + case Descriptor.Unknown(id, args) => + if args.isEmpty then id.entityName + else s"${id.entityName}_${args.map(descriptorPart).mkString("_")}" + case _ => "__" } diff --git a/canton/community/transcode/test-utils/src/main/scala/com/digitalasset/transcode/utils/propertygenerators/SchemaGenerator.scala b/canton/community/transcode/test-utils/src/main/scala/com/digitalasset/transcode/utils/propertygenerators/SchemaGenerator.scala index 746af4137e..28e350dce0 100644 --- a/canton/community/transcode/test-utils/src/main/scala/com/digitalasset/transcode/utils/propertygenerators/SchemaGenerator.scala +++ b/canton/community/transcode/test-utils/src/main/scala/com/digitalasset/transcode/utils/propertygenerators/SchemaGenerator.scala @@ -70,6 +70,7 @@ object SchemaGenerator: case Descriptor.Enumeration(cases) => Gen.int(0, cases.length - 1).map(DynamicValue.Enumeration) yield result + case Descriptor.Unknown(id, args) => Gen.empty ) // mapping inside Dictionary happens deterministically, so we can traverse over schema by extracting generators, diff --git a/canton/community/upgrading-integration-tests/src/test/scala/com/digitalasset/canton/integration/tests/upgrading/ComplexTopologyAwarePackageSelectionIntegrationTest.scala b/canton/community/upgrading-integration-tests/src/test/scala/com/digitalasset/canton/integration/tests/upgrading/ComplexTopologyAwarePackageSelectionIntegrationTest.scala index be0961a10e..15e1116fc0 100644 --- a/canton/community/upgrading-integration-tests/src/test/scala/com/digitalasset/canton/integration/tests/upgrading/ComplexTopologyAwarePackageSelectionIntegrationTest.scala +++ b/canton/community/upgrading-integration-tests/src/test/scala/com/digitalasset/canton/integration/tests/upgrading/ComplexTopologyAwarePackageSelectionIntegrationTest.scala @@ -79,7 +79,7 @@ final class ComplexTopologyAwarePackageSelectionIntegrationTest ConfigTransforms.updateAllParticipantConfigs_( _.focus(_.ledgerApi.topologyAwarePackageSelection.enabled).replace(true) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/release-notes/3.5.1.md b/canton/release-notes/3.5.1.md new file mode 100644 index 0000000000..d6e59e7cff --- /dev/null +++ b/canton/release-notes/3.5.1.md @@ -0,0 +1,899 @@ +# Release of Canton 3.5.1 + +Canton 3.5.1 has been released on May 27, 2026. + +## What’s New + +### Contract Keys + +#### Overview +Canton 3.5 introduces contract keys. Compared to a similar feature available in Canton 2.x, there are two notable +differences: + +- The keys are not unique, meaning multiple contracts may share the same key. +- Negative lookups are not validated. + +As a consequence, application developers must ensure key uniqueness through external enforcement mechanisms. +Contract keys are available from Daml-LF 2.3 onwards, which itself is available from Protocol Version 35, see below. + +#### Standard library +Daml language now supports several primitives associated with contract keys. In all cases, the contracts are returned +in the following order: +- first the contracts created within a transaction, starting with the most recent, +- then explicitly disclosed contracts, +- then contracts known to the participant in recency order. + +The following primitives are available: + +- ``lookupByKey`` - Available in prelude. It checks whether a contract with the given key exists and if yes, returns + the contract id. If multiple contracts exist, the most recently created is returned. Signature is the same as + in 2.x. +- ``fetchByKey`` - Available in prelude. It fetches the first contract id and contract data associated with the given + contract key. If multiple contracts exist, the most recently created is returned. Signature is the same as in 2.x. +- ``exerciseByKey`` - Available in prelude. Exercise a choice on the first contract associated with the given key. + Signature is the same as in 2.x. +- ``lookupNByKey`` - Available in ``DA.ContractKeys``. It looks up up to n contracts associated with the passed key. + +#### Daml Script +There are Daml Script functions - counterparts of the standard library primitives: + +- ``queryByKey`` - It looks up a contract associated with the passed key and returns its ids and data. It is of type + ``Script``, which means it must appear as top-level instruction as part of a Script. +- ``queryNByKey`` - It looks up up to n contracts associated with the passed key and returns their ids and data. + It is of type ``Script``, which means it must appear as top-level instruction as part of a Script. +- ``exerciseByKeyCmd`` - It exercises a choice on the first contract with the given key. It is of type ``Commands`` and + must therefore be wrapped by a submit operation, and can be combined with other ``Commands``. + + +#### Smart Contract Upgrades (SCU) +To support SCU upgrade for key and maintainer definitions, new guidelines have been added. At upgrade time, the recomputed +key and maintainers are verified to be identical to the upgraded contract’s original key and maintainers. If they +aren't, an upgrade error is raised and the transaction is aborted. +It is forbidden to add or remove a key definition from a template in a later version of that template. This is enforced +at package vetting time. + +#### Ledger API +Following contract-key related extensions have been made to the Ledger API + +- ``contract_key_hash`` has been added to the ``CreatedEvent`` message returned in the ``State-`` and ``UpdateService`` + responses +- ``prefetch_contract_keys`` field present in the ``Command`` and ``PrepareSubmissionRequest`` used by the ``Command-`` + ``CommandSubmission-`` and ``InteractiveSubmissionService`` have been reactivated to allow the caller to request + pre-heating the contract key cache underpinning the command interpretation. Use it when performance tests indicate + that many sequential contract key lookups adversely impact the command interpretation speed. + +#### PQS +In PQS, keys are mere metadata that can be queried like any other metadata. It is possible to query for all contracts +with a given key: + + ``` + select contract_id, payload ->> 'label' + from __contracts + where contract_key = jsonb_build_object(...) + order by created_at_ix + ``` + +### Daml-LF 2.3 + +A new version of Daml-LF is released: Daml-LF 2.3. Its main features are: + +- [`DA.Crypto.Text`](https://docs.digitalasset.com/build/3.5/reference/daml/stdlib/DA-Crypto-Text.html), + originally released in 3.4 in early access (alpha) status, is part of LF 2.3, + which means it is now marked as stable. +- Support for Contract Keys. + +#### Targeting LF 2.3 + +If you want to use new features available in the LF 2.3, select it explicitly as compilation target by setting the +`--target=2.3`, either as direct argument on the command line or as part of a `daml.yaml`: + +``` +sdk-version: 3.5.1 +name: some-name +source: daml +version: 0.0.1 +dependencies: + - daml-prim + - daml-stdlib +build-options: +- --target=2.3 +``` + +After changing the settings, the source code must be recompiled. Please note that this will cause the package id to +change, which should be accompanied by a version change. + +### Logical Synchronizer Upgrades +Logical Synchronizer Upgrades, or LSU, replace the procedure previously used to upgrade the synchronizer (Synchronizer +Upgrade with Downtime). LSU address the following shortcomings of the previous upgrade procedure: + +- Reduced downtime + + The downtime for Daml transactions is in the order of dozens of seconds. + The downtime for topology transactions is in the order of hours before the upgrade. + This will be improved in a subsequent version. + +- No manual coordination for validators + + Operators of validator nodes only need to upgrade their binary before the upgrade time. + The rest of the procedure happens through automation + +- Reduced coordination for SVs + + SVs can progress independently with the preparation of the upgrade and signal their progress using dedicated + topology transactions. + +- Asynchronous upgrade of each validator + + Each validator independently upgrade its binary before the upgrade time. + Each validator automatically perform the upgrade when it processes the last message of the old synchronizer. + +- Preserved local history as well as cryptographic evidence + + Transaction history (including updates and their offsets) is preserved, as well as ACS commitments. + +#### External submissions around upgrade time +External submissions prepared before upgrade time on a synchronizer running protocol version 34 cannot be +submitted after LSU on a synchronizer running protocol version 35: they need to be re-prepared and re-signed. + +### DA BFT Beta + +DA BFT is a new ordering service as part of the synchronizer that will replace the current single-leader CometBFT ordering service on the Global Synchronizer with a parallel, multi-leader consensus architecture, enabling significantly higher transaction throughput and fault tolerance. + +As part of this release DA BFT is ready in beta form for early access testing, but not recommended for production or close-to-production testing yet. + +### Multi Synchronizer Alpha + +Multi-synchronizer support is available in early access and has to be enabled explicitly. +This feature should only be used in test environments. + +To enable contract reassignment across synchronizers, the flag `PARTICIPANT_FEATURE_FLAG_ENABLE_ALPHA_MULTI_SYNCHRONIZER` must be activated on all participants hosting a stakeholder of the contract on both the source and target synchronizers. For a synchronizer, it can be done as follows: + +``` +participant.topology.synchronizer_trust_certificates.propose( + p.id, + synchronizerId, + featureFlags = Seq(ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer), +) +``` + + +## Functional Changes + +### Party Replication + +#### Offline party replication + +Concluding an offline party replication by clearing the onboarding flag now includes two major updates +when using protocol version 35: +- Added crash resilience for ongoing clearances. +- Automatic scheduling for clearances when a participant (re)connects to the synchronizer. + +These changes apply only to the `participant.parties.import_party_acs` and +`participant.parties.clear_party_onboarding_flag` endpoints. + +Note: The replicated party ID must be included in the party ACS import call to enable automatic +scheduling. The original behaviour is retained for protocol version 34. + +#### Party replication onboarding topology event is exposed on Ledger API + +The `PartyToParticipant` topology "onboarding" state used in the process of replicating a party with existing +contracts is now visible via the Ledger API when a party onboards on a synchronizer on protocol version 35 or higher. +Starting with PV=35, the newly introduced `ParticipantAuthorizationOnboarding` Ledger API topology event signals +the beginning of party replication and transitions to `ParticipantAuthorizationAdded` once the party's ACS is fully +visible on the Ledger API. + +#### Preview: Online party replication + +- Added the file-based online party replication command `participant.parties.add_party_with_acs_async` to + be used along with `participant.parties.export_party_acs` and instead of the sequencer-channel-based + `add_party_async` command. +- The online party replication status command now returns status in a very different, "vector-status" format + rather than the old "oneof" style. This impacts the `participant.parties.get_add_party_status` command and + `com.digitalasset.canton.admin.participant.v30.PartyManagementService.GetAddPartyStatus` gRPC response type. +- The participant configuration to enable online party replication has been renamed to + `alpha-online-party-replication-support` from `unsafe-online-party-replication` for consistency with other + alpha features and to reflect that the default file-based mode is more secure not relying on sequencer + channels. +- The sequencer configuration to enable sequencer channels for online party replication has been renamed to + `unsafe-sequencer-channel-support` from `unsafe-enable-online-party-replication` for consistency and to + refer specifically to sequencer channels. + +#### Minor Improvements + +- Onboarding party submission prevention: Ensures a participant does not submit a transaction or reassignment on behalf + of an onboarding party. +- Upgraded gRPC to 1.81.0 and AWS SDK to 2.44.3 to resolve Netty 4.1.130 CVEs (CVE-2026-33870, CVE-2026-33871). + +### New Transaction Hashing Scheme v3 + +- A new hashing scheme version `HASHING_SCHEME_VERSION_V3` has been introduced that includes the transaction's `max_record_time` in the hash computation and covers the new transaction node and fields of contract keys. This new version is avaialble from Protocol Version 35. +- See the [hashing algorithm documentation](https://docs.digitalasset-staging.com/build/3.5/explanations/external-signing/external_signing_hashing_algorithm.html#summary-of-differences-between-v2-and-v3) for the updated version. +- The `max_record_time` is now enforced by all confirming participants. +- The Ledger API and Ledger JSON API prepare `InteractiveSubmissionService` has been modified to take in a specific hashing scheme version in the request. +The default hashing scheme is `HASHING_SCHEME_VERSION_V2`. Integrators are encouraged to move to `HASHING_SCHEME_VERSION_V3` for synchronizers using protocol version 35. +In particular, usage of **contract keys** requires `HASHING_SCHEME_VERSION_V3`. See the versioning [documentation](https://docs.digitalasset-staging.com/build/3.5/explanations/external-signing/external_signing_hashing_algorithm.html#hashing-scheme-version) for details. + +### Active Contracts Head Snapshot (ACHS) + +The Active Contracts Head Snapshot (ACHS) is a new optional feature that maintains a continuously updated snapshot of +the currently active contracts. When enabled, the ACHS accelerates `GetActiveContracts` (ACS) queries by allowing them +to read directly from a pre-computed snapshot rather than scanning the full event log to reconstruct the active set. + +ACHS is disabled by default. To enable it, configure the `achs-config` block under the participant's indexer settings: +``` +canton.participants..parameters.ledger-api-server.indexer.achs-config { + valid-at-distance-target = 1000000 + last-populated-distance-target = 500000 +} +``` + +The `valid-at-distance-target` controls how far behind the ledger end (in event sequential IDs) the snapshot's validity +point is maintained. The ACHS is not used for serving queries below its validity point, logging at INFO level "ACHS for +skipped since validAt (...) already surpassed requested activeAt (...)". If the `valid-at-distance-target` +value is too small, long-running ACS queries may observe the ACHS validity point +moving (mid-stream) past their requested offset, causing the stream to fall back to the slower filter tables query, logging +at INFO level "ACHS stream for fell back to filter tables from (...) since validAt (...) surpassed activeAtEventSeqId (...)". If +the value is too large, the tail portion of the ACS (between the ACHS validity point and the requested offset) must be +resolved from the filter tables, making that last segment more expensive. + +As described above, when the ACHS validity point moves or is past the requested offset, an info-level log message is +emitted indicating that the stream fell back to the filter tables. +Two corresponding metrics, `achs_skips` and `achs_midstream_fallbacks`, are available under `daml.participant.api.index` +to help operators monitor the frequency of these fallbacks and tune the `valid-at-distance-target` accordingly. + +The `last-populated-distance-target` controls the additional lag (in event +sequential IDs) for the population of ACHS in order to store only the long-lived contracts. A larger value reduces +database I/O by skipping short-lived contracts that are created and archived before they would be added to the snapshot. +However, setting it too large increases the cost of the remaining ACS tail, as more data must be fetched from the filter +tables to cover the gap between the last populated point and the ACHS validity point. + +Further tuning parameters include: +- `population-parallelism`: number of parallel threads for adding activations to the ACHS during normal operation. +- `removal-parallelism`: number of parallel threads for removing deactivated activations from the ACHS during normal operation. +- `aggregation-threshold`: minimum batch size (in event sequential IDs) before ACHS maintenance work is emitted. +- `init-parallelism`: number of parallel threads for ACHS population and removal during initialization. +- `init-aggregation-threshold`: minimum batch size (in event sequential IDs) for ACHS maintenance during initialization. +- `buffer-size`: size of the internal buffer between the indexer pipeline and the ACHS maintenance flow. + +The `deactivation_distances` histogram metric which is available under `daml.participant.api.indexer.deactivation_distances` +can help operators understand the distribution of contract lifetimes (the event sequential ID distance between a contract's +activation and its deactivation) and set an appropriate `last-populated-distance-target`. Ideally, the population distance +should be large enough so that most short-lived contracts are already deactivated and thus not added to the snapshot. + +Three gauge metrics are available under `daml.participant.api.indexer` to monitor the ACHS state: +- `achs_valid_at`: the event sequential ID at which the ACHS is currently valid. ACS queries with a requested offset + at or after this value can read directly from the ACHS. +- `achs_last_populated`: the last event sequential ID for which activations were added to the ACHS. +- `achs_last_removed`: the last event sequential ID for which deactivations were looked up and the corresponding + activations were removed from the ACHS. + +### Hardened Error Handling in Sequencer Connect Service + +We have implemented strict error sanitization and rewording for the SequencerConnectService to mitigate information leakage. +Detailed internal error messages are now redacted before being sent to clients. + +If detailed diagnostics are required in a non-production environment, sanitization can be toggled off via: + +``` +canton.monitoring.sanitize-public-error-messages = false +``` + +### Ignoring of offboarded sequencers for submission requests + +In the case where sequencers are offboarded but remain online and kept in the connectivity configuration, it was still possible that members pick them as the target for submission requests. The submission would fail, but the member would incur a delay as it requires retrying. +This has now changed, and offboarded sequencers are ignored when sending submission requests. + +#### API Changes + +The previous method of returning errors via response fields has been removed in favor of canonical gRPC error propagation. +The following fields are now obsolete: + +- `HandshakeResponse.value.failure` +- `VerifyActiveResponse.value.failure` + +Errors are now communicated strictly through `io.grpc.Status` codes to ensure a consistent and secure interface. + +Status codes have changed as follows: + +- SequencerAuthenticationService.challenge newly fails with `INVALID_ARGUMENT` (instead of `FAILED_PRECONDITION`), + if the client does not support the sequencer's protocol version. +- SequencerConnectService newly fails with `INVALID_ARGUMENT` (instead of `FAILED_PRECONDITION`) if a non-participant tries to connect. +- SequencerConnectService.registerOnboardingTopologyTransactions newly fails with `INTERNAL` (instead of `FAILED_PRECONDITIONS`) +- if there are no dynamic synchronizer parameters. +- SequencerConnectService.registerOnboardingTopologyTransactions newly fails with `FAILED_PRECONDITION` if +- the transactions cannot be added to the topology state and sanitization of error messages is enabled. + +### Mediator Crash Fault Tolerance + +The mediator is now crash fault-tolerant and guarantees that all verdicts will eventually be persisted and available on the inspection API. + +### Enhanced Reliability for `GetHighestOffsetByTimestamp` + +Previously, the `GetHighestOffsetByTimestamp` RPC and the `find_highest_offset_by_timestamp` console command could return offsets not yet synced with the participant's local cache. Furthermore, forcing a query with a future timestamp resulted in an error. + +Specific changes: +- The required state is now retrieved atomically via a consistent database snapshot. +- The endpoint now includes an internal barrier (waiting up to 10 seconds) to ensure the local Ledger API cache catches up with the database before returning the offset. +- When `force` is true, requesting a future timestamp now gracefully returns the current ledger end instead of failing. + +No migration required. + +### ACS stream continuation + +The `GetActiveContracts` stream request has been extended with an optional `stream_continuation_token` field that allows +clients to continue an interrupted ACS stream from the last element which made through. The field can be populated with +the `stream_continuation_token` field of the last response element received before the interruption, and the stream will +continue from the next element after that. + +### ACS Ledger API counting + +Introduced a new memory-efficient consoled command `participant.ledger_api.acs.count()` +to count the number of active contracts on a participant node. + +> Note: This command is currently under the Testing feature flag. + +### ACS pagination + +A new, `GetActiveContractsPage` endpoint added to State Service API. This enables the client to retrieve the ACS in +paginated form, by specifying a `max_page_size`. The pages can be accessed sequentially by using the `page_token` +field. The token can be obtained from the `GetActiveContractsPageResponse` of the last page. + +### GetUpdates stream in descending order of events + +The `GetUpdatesRequest` object has new optional parameter `descending_order`. When this parameter is `true` the events +are streamed from the newest to the oldest ones. The pages can be accessed sequentially by using the `page_token` +field. + +### GetUpdates pagination +A new `GetUpdatesPage` endpoint has been added to Update Service API. THis allows retrieval of updates in paginated +form instead of requesting the stream. + +### Improvements for `repair.add` and migration advice + +The `participant.repair.add` admin command has been revised to use the new `ImportAcs` backend, bringing significant +memory performance improvements, stricter default safety validations, and several new parameters. + +#### Important behavioral change: strict `Validation` by default + +Previously, `repair.add` implicitly accepted all injected contracts without re-evaluating their cryptographic hashes. To +prevent accidental data corruption, the command now defaults to **Validation** mode ( +`contractImportMode = ContractImportMode.Validation`). + +- **Impact:** If you have existing scripts or recovery procedures that inject manually modified, synthetic, or + inconsistent contracts (where the payload does not strictly match the `ContractId` hash), they will now fail with a + `"Failed to authenticate contract with id"` error. +- **Migration:** To bypass this cryptographic validation and restore the legacy behavior, explicitly pass the `Accept` + mode in your command call: + ```scala + participant.repair.add( + synchronizerId = mySynchronizer, + protocolVersion = myProtocolVersion, + contracts = myContracts, + contractImportMode = ContractImportMode.Accept // Bypasses strict validation + ) + ``` + +#### New parameters + +The command signature has been expanded to support several optional parameters: + +- `workflowIdPrefix`: Allows you to set a custom prefix for the generated workflow ID to easily track the repair + transactions (defaults to `import-`). +- `contractImportMode`: Choose between `Validation` (default, validates that contract IDs comply with the scheme + associated to the synchronizer where the contracts are assigned), or `Accept` the contracts as they are (if you know + what you are doing). +- `representativePackageIdOverride`: Allows you to remap or override the representative package IDs of the contracts as + they are imported. +- `excludedStakeholders`: When defined, any contract that has one or more of these parties as a stakeholder will not be + added. + +### Improved party and repair ACS imports + +We have completely overhauled the ACS import endpoints for both party replication and participant repair to be +memory-efficient streaming endpoints: + +- Console command `participant.parties.import_party_acs` +- Console command `participant.repair.import_acs` +- gRPC RPC `PartyManagementService.ImportPartyAcs` +- gRPC RPC `ParticipantRepairService.ImportAcs` + +This resolves previous memory limitations, as these endpoints no longer load the entire ACS snapshot into memory at +once. + +#### Action required: Breaking API change + +The `synchronizerId` is now a **mandatory** first parameter for both the `import_party_acs` and `import_acs` console +commands as well as their analogous gRPC endpoints. You will need to update any existing scripts. + +**For `import_party_acs`:** + +- **Old usage:** `participant.parties.import_party_acs("canton-acs-export.gz")` +- **New usage:** `participant.parties.import_party_acs(mySynchronizerId, importFilePath = "canton-acs-export.gz")` + +**For `import_acs`:** + +- **Old usage:** `participant.repair.import_acs("canton-acs-export.gz")` +- **New usage:** `participant.repair.import_acs(mySynchronizerId, importFilePath = "canton-acs-export.gz")` + +Because of the mandatory `synchronizerId` parameter, to import a multi-synchronizer ACS snapshot, you must now call the +endpoint sequentially for each synchronizer your participant is connected to, using the exact same snapshot file. The +import process will ignore any contracts in the snapshot that are associated to a different synchronizer. + +##### Details on the gRPC `ImportAcs` repair endpoint + +The `ImportAcs` and `ImportAcsV2` RPCs have been consolidated, introducing the following breaking changes and migration +steps: + +- **Endpoint removed:** `ImportAcsV2` (along with its request/response messages) is completely removed. All clients must + migrate to the standard `ImportAcs` RPC. +- **Request signature and type changes:** + - Fields `workflow_id_prefix` (2), `contract_import_mode` (3), and `representative_package_id_override` (5) in + `ImportAcsRequest` are now explicitly `optional`. + - A new `optional string synchronizer_id = 6` field was added. + - **Migration (ScalaPB):** Adding `optional` changes generated code from base types to `Option[T]`. Existing clients + will fail to compile and must be updated to wrap assigned values (e.g., `workflowIdPrefix = Some("prefix")`) and + explicitly handle reading `Option` types. +- **Behavioral change (`synchronizer_id`):** When filtering by synchronizer, mismatched contracts are now ignored. This + breaks previous logic that relied on the import strictly aborting upon a mismatch. + +##### Details on the gRPC `ImportPartyAcs` party replication endpoint + +The `ImportPartyAcs` endpoint underwent the exact same consolidation (removing `ImportPartyAcsV2`), streaming semantics +updates, generated code changes (ScalaPB `Option[T]`), and mismatched synchronizer behavior (ignoring rather than +failing) as `ImportAcs`. + +**Key differences specific to `ImportPartyAcs`:** + +- **New capability (`party_id`):** A new `optional string party_id = 6` field was added. Providing this in the first + request of the stream enables automatic, crash-resilient scheduling of the onboarding flag clearance. If omitted, the + participant logs a warning, and the flag must be cleared manually. + +### Topology-Aware Package Selection (TAPS) improvements + +Topology-Aware Package Selection (TAPS) refinement for handling inconsistent vetting states: +- The algorithm now considers a party's package vetting state only for packages required by that party in the interpreted transaction. + It starts with a minimal set of restrictions derived from the command's root nodes and progressively accumulates more restrictions over a configurable number of passes. + This iterative process increases the likelihood of finding a valid package selection set for the routing of the transaction. +- The maximum number of TAPS passes can be set at the request-level via the optional `taps_max_passes` field in `Commands` or `PrepareSubmissionRequest` messages. + If not specified, the default value is taken from the participant configuration via `participants.participant.ledger-api.topology-aware-package-selection.max-passes-default` (defaults to `3`). + A hard limit is enforced by `participants.participant.ledger-api.topology-aware-package-selection.max-passes-limit` (defaults to `4`). +- TAPS now ignores unvetted dependencies of packages that are not required for interpretation. + complying now with the support of unvetted dependencies in the Canton protocol. + +### Ledger API Improvements + +- ApiRequestLogger now also used by Ledger JSON Api. Changes: + - Redundant Request TID removed from logs. + - Additional CLI options added: `--log-access` captures API access logs in a separate file (default: `log/canton_access.log`), and `--log-access-errors` captures API access errors in a separate file (default: `log/canton_access_error.log`). + - Additional config options added: `debugInProcessRequests` logs in-process gRPC requests at DEBUG instead of TRACE, and `prefixGrpcAddresses` prefixes gRPC client addresses with `grpc:` (enabled by default). +- LedgerAPI ListKnownParties supports an optional prefix filter argument filterParty. + The respective JSON API endpoint now additionally supports `identity-provider-id` as + an optional argument, as well as `filter-party`. +- Protect the admin participant from self lock-out. It is now impossible for an admin to remove own admin rights or + delete itself. +- On Ledger API interface subscriptions, the `CreatedEvent.interface_views` now returns the ID of the package containing + the interface implementation that was used to compute the specific interface view as `InterfaceView.implementation_package_id`. +- OffsetCheckpoints are now always generated when an open-ended updates or completions stream is requested, even if there + are no updates. The checkpoint can have the same offset as the exclusive start of the stream, making checkpoints visible + even when starting from the ledger end. This enables client systems to recognize when the ledger end is advancing, + even if the stream of updates is inactive. +- Extended the set of characters allowed in user-id in the ledger api to contain brackets: `()`. + This also makes those characters accepted as part of the `sub` claims in JWT tokens. +- Functionality for managing internal and external parties has been improved, removing previous asymmetry: + - User rights can now be assigned to an external party during allocation. + - External parties can be allocated by the user themselves in the self-administration mode. + Please note that users in self-administration mode can allocate up to N parties, depending on a setting of the parameter + ``` + canton.participants..ledger-api.party-management-service.max-self-allocated-parties + ``` + By default the value of this parameter is 0. +- An IDP administrator can now only allocate parties confined to their own IDP perimeter. + +### New metrics related to LSU + +Some new metrics have been added to monitor the status of an LSU. + +- For sequencers: [daml.received-lsu-sequencing-test-messages](https://docs.digitalasset.com/operate/3.5/reference/metrics.html#daml-received-lsu-sequencing-test-messages) + + Allows to track the number of `LsuSequencingTest` messages received by a mediator, per sender. + +- For participants: [daml.participant.lsu_status](https://docs.digitalasset.com/operate/3.5/reference/metrics.html#daml-participant-lsu-status) + + Exposes the status of an LSU on a participant node. + +- For sequencers: [daml.sequencer.public-api.handshakes](https://docs.digitalasset.com/operate/3.5/reference/metrics.html#daml-sequencer-public-api-handshakes) + + Exposes the number of handshakes per member and status. + Can be used to track how many of the participant nodes already performed handshake with the successor. + +- For sequencers: [daml.sequencer.lsu_contact_successor_status](https://docs.digitalasset.com/operate/3.5/reference/metrics.html#daml-sequencer-lsu-contact-successor-status) + + Exposes the status of the handshake between a sequencer and its successor. + +## Performance Improvements + +### Session Signing Keys + +Session signing keys can now be used to reduce the number of calls to external KMS (Key Management Service) providers. When enabled, session signing keys are generated and cached locally for a limited duration and used for signing operations during their validity period. + +Please read the documentation on [Session Signing Keys](https://docs.digitalasset.com/operate/3.5/howtos/secure/keys/session_signing_keys.html) for details on how to enable and configure this feature. +Session signing keys are only available from Protocol Version 35 and are not enabled by default. + +### Compatible sibling views compression + +In protocol version 35, each envelope in `TransactionConfirmationRequest` contains multiple views grouped by recipients instead of one envelope per view. +Assignment and re-assignments also use this new format, but they always have one view. + +### Single Topology Transaction for External Parties + +Multiple topology transactions for external parties can now be represented with a single `PartyToParticipant` topology transaction. + +The `generateExternalPartyTopology` endpoint on the Ledger API now returns a single `PartyToParticipant` topology transaction to onboard the party. +The transaction contains signing threshold and signing keys. This effectively deprecate the usage of `PartyToKeyMapping`. +For parties with signing keys both in `PartyToParticipant` and `PartyToKeyMapping`, the keys from `PartyToParticipant` take precedence. + +Deprecated usage of `PartyToKeyMapping`. The functionality provided by `PartyToKeyMapping` is now available directly in `PartyToParticipant`. +Please use `PartyToParticipant` for new transactions. `PartyToKeyMapping` is still fully supported in this version (including existing and new transactions). +In future version, creation of new `PartyToKeyMapping` transactions may be disallowed. + +Deprecated `TopologyManagerReadService.ListAll` in favor of `ListAllV2`, which uses an inclusion +list (`include_mappings`) instead of an exclusion list (`exclude_mappings`). This avoids sending +mapping codes unknown to older servers. The console method `topology.transactions.list` now calls +`ListAllV2` by default and only falls back to `ListAll` when targeting a 3.4 node. The +`excludeMappings` and `protocolVersion` parameters of `topology.transactions.list` are deprecated; +use `filterMappings` instead. + +Deprecated `TopologyManagerReadService.ExportTopologySnapshot` and `TopologyManagerWriteService.ImportTopologySnapshot`, +along with their console counterparts `topology.transactions.export_topology_snapshot`, +`topology.transactions.import_topology_snapshot`, `topology.transactions.import_topology_snapshot_from`, +and `topology.transactions.export_identity_transactions`. +Please use the corresponding `V2` variants (`ExportTopologySnapshotV2` / `ImportTopologySnapshotV2`, +`export_topology_snapshotV2`, `import_topology_snapshotV2`, `import_topology_snapshot_fromV2`, +`export_identity_transactionsV2`) instead, which use an updated internal bytestring format. + +Deprecated `SequencerInitializationService.InitializeSequencerFromGenesisState`, +`SequencerInitializationService.InitializeSequencerFromOnboardingState`, +`SequencerAdministrationService.OnboardingState`, and +`TopologyManagerReadService.GenesisState`, along with their console counterparts +`setup.assign_from_genesis_state`, `setup.assign_from_onboarding_state`, +`setup.onboarding_state_for_sequencer`, `setup.onboarding_state_at_timestamp`, +and `topology.transactions.genesis_state`. +Please use the corresponding `V2` variants (`InitializeSequencerFromGenesisStateV2`, +`InitializeSequencerFromOnboardingStateV2`, `OnboardingStateV2`, `GenesisStateV2`, +`assign_from_genesis_stateV2`, `assign_from_onboarding_stateV2`, +`onboarding_state_for_sequencerV2`, `onboarding_state_at_timestampV2`, +`genesis_stateV2`) instead, which use an updated internal bytestring format +that enables streaming ingestion, making snapshot export and import significantly less memory-intensive. + +### Minor Performance Improvements + +- The Postgres connection tuning configuration of the indexer is now separated from the configuration of the Ledger API server + (`canton.participants..ledger-api.postgres-data-source`). + The new configuration section `canton.participants..parameters.ledger-api-server.indexer.postgres-data-source` should + be used instead to tune the indexer's Postgres connections. +- A new indexer pipeline batching strategy added under the feature flag `useWeighetdBatching`. When switched on, the + batches are created using their estimated database processing time using the `submissionBatchInsertionSize` as a limit + for individual batches +- Changed the `CompressedBatch` structure in the sequencer protocol for protocol version 35 to separately keep recipients and envelopes (from `gzip(Seq((recp1, payload1), (recp2, payload2)))` to `gzip(Seq(recp1, recp2)), Seq(gzip(payload1), gzip(payload2)))`). +- Batching configuration now allows setting different parallelism for pruning (currently only for Sequencer pruning): + New option `canton.sequencers.sequencer.parameters.batching.pruning-parallelism` (defaults to `2`) can be used + separately from the general `canton.sequencers.sequencer.parameters.batching.parallelism` setting. +- Made the config option `...topology.use-time-proofs-to-observe-effective-time` work and changed the default to `false`. + Disabling this option activates a more robust time advancement broadcast mechanism on the sequencers, + which however still does not tolerate crashes or big gaps in block sequencing times. The parameters can be configured + in the sequencer via `canton.sequencers..parameters.time-advancing-topology`. +- Additional metrics for the ACS commitment processor: `daml.participant.sync.commitments.last-incoming-received`, `daml.participant.sync.commitments.last-incoming-processed`, `daml.participant.sync.commitments.last-locally-completed`, and `daml.participant.sync.commitments.last-locally-checkpointed`. + +## Breaking Changes + +### Removal of legacy party replication repair console macros + +The original party replication method, which relied on a silent synchronizer, has been superseded by the offline party +replication process. Consequently, the obsolete repair console macros associated with the legacy approach have +been removed. + +Specifically, the following macros are no longer available: +- `step1_hold_and_store_acs` +- `step2_import_acs` + +If you previously relied on the _Silent synchronizer replication procedure_, you will need to transition to the +current offline party replication process. For details, please consult the +[Offline Party Replication documentation](https://docs.digitalasset.com/operate/3.5/howtos/operate/parties/party_replication.html#offline-party-replication) + +### Removal of deprecated, legacy ACS export and import endpoints + +The legacy repair endpoints for the ACS export and import have been removed: + +- Console command `participant.repair.export_acs_old` +- Console command `participant.repair.import_acs_old` +- gRPC rpc `ParticipantRepairService.ExportAcsOld` +- gRPC rpc `ParticipantRepairService.ImportAcsOld` + +#### Migration advice + +Use repair endpoints without the 'old' suffix: + +- Migrate to `participant.repair.export_acs` from `participant.repair.export_acs_old` +- Migrate to `participant.repair.import_acs` from `participant.repair.import_acs_old` +- Migrate to `ParticipantRepairService.ExportAcs` from `ParticipantRepairService.ExportAcsOld` +- Migrate to `ParticipantRepairService.ImportAcs` from `ParticipantRepairService.ImportAcsOld` + +Note that previously created ACS snapshots with the legacy endpoints cannot be imported with the current endpoints as +the underlying data format has completely changed. + +##### Migrating to export_acs + +The most significant change is the removal of the `timestamp` parameter, which has been replaced by a mandatory +`ledgerOffset` parameter. + +**Console parameter changes:** + +- **New mandatory parameter:** `ledgerOffset (Long)`. You must now specify the exact ledger offset for the snapshot + instead of a `timestamp`. +- **Removed parameters:** `partiesOffboarding`, `timestamp` (replaced by `ledgerOffset`), `force`. +- **Renamed parameters:** `outputFile` is now `exportFilePath` (default is `"canton-acs-export.gz"`), + `filterSynchronizerId` is now `synchronizerId`. +- **New optional parameters:** `excludedStakeholders` allows you to omit contracts that have one or more of these + parties as a stakeholder; `contractSynchronizerRenames` allows mapping contracts from one synchronizer to another + during export. + +**gRPC changes for `ExportAcsRequest`:** + +- **`parties` -> `party_ids`:** Field renamed for consistency. If left empty, the endpoint will act as a wildcard and + export the ACS for *all* parties hosted by the participant. +- **`timestamp` -> `ledger_offset` (Breaking):** You must provide an exact `int64 ledger_offset` instead of a timestamp. +- **`filter_synchronizer_id` -> `synchronizer_id`:** Field renamed for consistency. +- **Removed fields:** `force` and `parties_offboarding` have been completely removed. +- **New fields:** `contract_synchronizer_renames` and `excluded_stakeholder_ids`. + +##### Migrating to import_acs + +The import command remains largely the same in basic usage, but introduces new optional parameters for advanced +validation and overrides, alongside strict memory-efficient streaming semantics for gRPC. + +**Console parameter changes:** + +- **Renamed parameter:** `inputFile` is now `importFilePath` (default is `"canton-acs-export.gz"`). +- **New optional parameters:** `contractImportMode` governs contract validation upon import (defaults to + `ContractImportMode.Validation`); `representativePackageIdOverride` allows overriding representative package IDs + during import; `excludedStakeholders` allows omitting contracts that have one or more of these parties as a + stakeholder. + +**gRPC changes for `ImportAcsRequest`:** + +- **Streaming Semantics (Breaking):** The new endpoint requires metadata fields (like `contract_import_mode`, + `synchronizer_id`, etc.) to be populated *only* in the first request of the stream. Subsequent requests must omit + metadata and only contain the binary `acs_snapshot` chunks. +- **New mandatory fields:** `contract_import_mode` and `synchronizer_id` must be explicitly defined in the first stream + request. +- **Removed fields:** `allow_contract_id_suffix_recomputation` is completely removed. +- **New fields:** `excluded_stakeholder_ids` and `representative_package_id_override`. +- **Response update:** `ImportAcsResponse` is now a completely empty message (previously returned a contract ID + mapping). + +### Only PackageName is accepted on Ledger API + +Usage of package id for ledger queries was deprecated and now the validation will fail if used. +The impacted APIs are: + - GetUpdates + - GetUpdateByOffset + - GetUpdateById + - GetActiveContracts + - GetEventsByContractIdRequest + - SubmitAndWaitForTransaction (the optional `transaction_format`) + - SubmitAndWaitForReassignmentRequest + - ExecuteSubmissionAndWaitForTransactionRequest + +### SynchronizerId field update in Externally signed transactions + +In Protocol version 35, the `synchronizer_id` field in externally signed prepared transaction metadata +will be populated with the physical synchronizer ID of the synchronizer on which the transaction will be processed, +instead of the logical synchronizer ID, as is the case in PV 34. +Applications must ensure they do not rely on the format of the `synchronizer_id` value. + +### Changes from NonNegativeLong to Long + +Some console commands using a NonNegativeLong for the offset are changed to accept a Long instead. +Similarly, some console commands returning an offset now return a Long instead of a NonNegativeLong. +It brings consistency and allows to pass the output of `participant.ledger_api.state.end()`. + +Impacted commands: +- `participant.repair.export_acs` +- `participant.parties.find_party_max_activation_offset` +- `participant.parties.find_party_max_deactivation_offset` +- `participant.parties.find_highest_offset_by_timestamp` + +### Removal of automatic recomputation of contract ids upon ACS import + +The ability to recompute contract ids upon ACS import has been removed. + +### Removal of multi-host name resolution tooling + +Support for the multi-host name resolution was removed. +This was only used if synchronizer connectivity defined a sequencer with multiple endpoints, which is not supported with our current sequencers: +we now have multiple sequencers each with exactly one endpoint. + +### Ledger JSON API Spec Corrections + +JSON Ledger API OpenAPI/AsyncAPI spec corrections +- Fields not marked as required in the Ledger API `.proto` specification are now also optional in the OpenAPI/AsyncAPI specifications. + If your client code is using code generated using previous versions of these specifications, it may not compile or function correctly with the new version. To migrate: + - If you prefer not to update your code, continue using the previous specification versions as the JSON API server preserves backward compatibility. + - If you want to use new endpoints, features or leverage the new less strict spec, migrate to the new OpenAPI/AsyncAPI specifications as follows: + - Java clients: No changes are needed if you use the `OpenAPI Generator`. Otherwise, potentially optionality of fields should be handled appropriately for other code generators. + - TypeScript clients: Update your code to handle optional fields, using the `!` or `??` operators as appropriate. +- From Canton 3.5 onwards, OpenAPI/AsyncAPI specification files are suffixed with the Canton version (e.g., `openapi-3.5.0.yaml`). +- Canton 3.5 is compatible with OpenAPI specification files from version 3.4.0 to 3.5.0 (inclusive). + +- The Ledger JSON API server now enforces that only fields marked as required by the Ledger API OpenAPI/AsyncAPI specification are mandatory in request payloads. + +### Change from grpcurl to grpc-health-probe in all Docker images + +The tool used for health check probes changed from grpcurl to grpc-health-probe in all the docker images. + +### Minor Breaking Changes + +- The expert `keep-alive-client` configuration parameter for various client services moved to `channel.keep-alive-client`. +- We reduced the defaults for `setBalanceRequestSubmissionWindowSize` and `defaultMaxSequencingTimeOffset` + to 2 minutes. +- The default OTLP gRPC port that the Canton connects to in order to export the traces has been changed from + 4318 to 4317. This aligns the default configuration of Canton with the default configuration of the OpenTelemetry + Collector. This change affects only the users who have configured an OTLP trace export through + ``` + canton.monitoring.tracing.tracer.exporter.type=otlp + ``` +- Removed the `LastErrorsAppender` along with the Admin API endpoints `StatusService.GetLastErrors` and `StatusServiceGetLastErrorTrace`, as + well as the corresponding console commands `last_errors` and `last_error_trace`. + + +## Deprecations + +### Deprecate scope-based access tokens +- "Scope-based" access tokens, i.e. JWTs without any audience specified, have been deprecated. +- A configuration that does not specify a `target-audience` will log a warning on node startup. +- Configurations that specify both a `target-audience` and a `target-scope` are not supported in this version and will also log a warning on node startup. +- Starting Canton version 3.7, support for "scope-based" tokens will be removed entirely to enforce a valid `aud` field in every incoming JWT. +- The `scope` field will, in a future version, be repurposed to serve exclusively as an additional, optional claim for fine-grained permissions. + +### Removal of the old sequencer connection transports + +The old sequencer connections transports have been removed, and only the new sequencer connection pool remains. +Consequently, the configuration `.sequencer-client.use-new-connection-pool` has been deprecated and no longer has any effect. + +### Deprecate initial protocol version configuration + +The config key `participant.parameters.initial-protocol-version` was unused and has been marked as deprecated. + +### Configuration Deprecations + +- The configuration parameters `topology.use-new-processor` and `topology.use-new-client` have been deprecated and now default to true. Configuring those parameters to false will be ignored. +- The parameter `canton.participants..parameters.package-metadata-view.init-takes-too-long-interval` + is now ignored, and a warning will only be printed once, rather than periodically. +- The parameter `canton.participants..parameters.ledger-api-server.indexer.prepare-package-metadata-time-out-warning` + is now ignored. +- The individual JVM metric flags `classes`, `cpu`, `memoryPools`, `threads`, `gc`, and `buffers` in + `canton.monitoring.metrics.jvm-metrics` are no longer supported since the upgrade to OpenTelemetry instrumentation 2.26.0. + All standard JVM metrics (classes, cpu, memory pools, threads, garbage collector) are now always enabled when + `jvm-metrics.enabled = true`. A new `experimental` flag has been added to control experimental JVM metrics + (e.g. buffer pools). Users who previously set `buffers = true` should migrate to `experimental = true`. + See https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/16087 for details. +- The Zipkin trace exporter configuration `canton.monitoring.tracing.tracer.exporter.type=zipkin` is + deprecated following the OpenTelemetry specification deprecation of Zipkin exporters. The Zipkin exporter + will be removed in a future release. Users should migrate to the OTLP exporter. + See https://opentelemetry.io/blog/2025/deprecating-zipkin-exporters/ for details. +- Removed the feature flag `canton.sequencers..parameters.async-writer.enabled`, as async writing is now + the only supported mode. +- Changed the path for `crypto.kms.session-signing-keys` (deprecated) to `crypto.session-signing-keys` so that session signing key configuration is no longer directly tied to a KMS. However, session signing keys can still only be enabled when using a KMS provider or when running with `non-standard-config=true`. +- `package-dependency-cache` field in `caching` configuration is deprecated. It can be removed safely from node configurations. + +### Ledger JSON API package vetting endpoints + +The Ledger JSON API `v2/package-vetting` endpoint exposes list functionality on the GET method by accepting a request body. This is not recommended by the HTTP specification, hence the endpoint is deprecated. +For consistency, the POST method, used for updating the vetting state, of the same endpoint is also deprecated. + +In turn, two new endpoints are implemented to provide the same functionality: +- `v2/package-vetting/list` accepts a POST request with the same body as the deprecated GET `v2/package-vetting` endpoint and returns the list of vetted packages in the same format. +- `v2/package-vetting/update` accepts a POST request with the same body as the deprecated POST endpoint `v2/package-vetting` and returns the updated vetting state of the package in the same format. + + +### Protocol version parameter in topology list commands + +The `protocolVersion` parameter in all `.topology..list` console commands has been deprecated and will be removed in a future version. + +## Minor Improvements + +### Bugfixes + +- Fixed a mid-crash recovery issue for offline party replication and repair ACS imports. Previously, if an ACS import + was interrupted (for example by a participant node restart or crash), a subsequent recovery attempt could result in + missing contracts on the Ledger API. The recovery process now properly rolls back uncommitted partial states upon + retrying the ACS import, ensuring recovered contracts are completely synchronized across both internal storage + and the Ledger API. +- Fixed a bug where the Ledger API `PackageService.ListVettedPackages` used to return a potentially not yet + effective state of the vetted packages. Now it returns the state of vetted packages effective at the time of the request. +- Sequencer health status used to incorrectly return the synchronizer uid instead of the sequencer uid. +- Prevent Ledger API crashes after running `ParticipantRepairService.PurgeContracts` admin command. + Fixes a critical issue where using the `ParticipantRepairService.PurgeContracts` command (when multi-synchronizer support is disabled) generated malformed + Daml values for the choice argument and choice result of the `Archive` choice of the purge contract events in the Ledger API event store. + This previously caused the Ledger API streams reading the generated `Archive` events to crash. + The repair command now generates correct Daml values for the corresponding entries, that can be safely delivered by the Ledger API. +- Fixed a bug in the repair service's `changeAssignation` where only a single repair counter was allocated when reassigning multiple contracts, + violating the monotonicity expected by the indexer. + +### Ledger API Multi-Synchronizer Events Alpha Support + +Adds a new participant node parameter, `alpha-multi-synchronizer-support` (Boolean). +- **Default (`false`):** Uses standard **Create** and **Archive** events. +- **Enabled (`true`):** Uses **Assign** and **Unassign** events. + +This flag is required in multi-synchronizer environments to preserve the **reassignment counter** of a contract. +Using the default (Create events) resets this counter to zero. + +Note: Multi-synchronizer support is currently in Alpha; most Ledger API consumers may not yet be compatible with +Assign/Unassign events. Only enable this if your application specifically requires non-zero reassignment counters +and can process these event types. + +### Support for adding table settings for PostgreSQL + +Added support for adding table settings for PostgreSQL. One can use a repeatable migration (Flyway feature) in a file +provided to Canton externally. + - Use the new config `repeatable-migrations-paths` under the `canton...storage.parameters` configuration section. + - The config takes a list of directories where repeatable migration files must be placed, paths must be prefixed with `filesystem:` for Flyway to recognize them. + - Example: `canton.sequencers.sequencer1.storage.parameters.repeatable-migrations-paths = ["filesystem:community/common/src/test/resources/test_table_settings"]`. + - Only repeatable migrations are allowed in these directories: files with names starting with `R__` and ending with `.sql`. + - The files cannot be removed once added, but they can be modified (unlike the `V__` versioned schema migrations), and if modified these will be reapplied on each Canton startup. + - The files are applied in lexicographical order. + - Example use case: adding `autovacuum_*` settings to existing tables. + - Only add idempotent changes in repeatable migrations. + +### Offline root namespace key scripts + +Offline root namespace key scripts: +- Renamed `prepare-certs.sh` to `prepare-cert.sh` +- Changed `assemble-certs.sh` to automatically suffix the generated certificate with a `.cert` extension, similarly to what is being done in `prepare-cert.sh` +- Removed the `10-offline-root-namespace-init` example folder as its content is now integrated in the documented how-to: https://docs.digitalasset.com/operate/3.5/howtos/secure/keys/namespace_key.html +- Committed the buf image necessary to run the script to the repository (also available in the release artifact), making usage from the open source repo easier + +### Reliability Improvements + +- Added a field `MaxConcurrentCallsPerConnection` and corresponding default + `defaultMaxConcurrentCallsPerConnection` (set to 100000) to `ServerConfig`. + This corresponds to `max-concurrent-streams-per-connection` in the app configs, e.g., + `docker/canton/images/canton-sequencer/app.conf` and can be changed there. At present + the value for sequencers is configured to be 500 for the public API and 100 for the Admin API. +- Added network timeout and client_connection_check_interval for db operations in the Ledger API server and indexer to avoid + hanging connections for Postgres (see PostgresDataSourceConfig). The defaults are 60 seconds network timeout and + 5 seconds client_connection_check_interval for the Ledger API server, and 20 seconds network timeout and + 5 seconds client_connection_check_interval for the indexer. These values can be configured via the new configuration parameters + `canton.participants..ledger-api.postgres-data-source.network-timeout` for network timeout of the Ledger API + server and `canton.participants..parameters.ledger-api-server.indexer.postgres-data-source.client-connection-check-interval` + for the client_connection_check_interval of the indexer. +- `.replication.connection-pool.connection.client-connection-check-interval` is introduced + that allows configuring the PostgreSQL-specific `client_connection_check_interval` parameter for DB locked connections. + This is a safety mechanism to prevent hanging connections in case of network issues. The default value is 5 seconds. +- The Ledger API now enforces a maximum number of signatures per party that can be provided for external submissions. + This value defaults to 50 and can be changed at the following config path: `canton.participants..ledger-api.interactive-submission-service.maximum-number-of-signatures-per-party` +- Added a new configuration parameter `canton.participants..ledger-api.index-service.max-lookup-limit` that caps the maximum number of contracts returned by a contract key lookup per request. + The default value is 1000. +- When the AcsCommitmentProcessor is initializing, read stakeholder groups from the snapshot in batches of size + `canton.parameters.general.batching.max-stakeholder-groups-batch-size` (default 1000), rather than all at once. + This allows early termination of this initialization if the node is shutting down. +- The release version is now exposed in `NodeStatus.NotInitialized`, so the node version can be retrieved even before the node is initialized. + +## Compatibility + +The following Canton protocol versions are supported: + +| Dependency | Version | +|----------------------------|----------------------------| +| Canton protocol versions | 34, 35 | + +Canton has been tested against the following versions of its dependencies: + +| Dependency | Version | +|----------------------------|----------------------------| +| Java Runtime | OpenJDK 64-Bit Server VM (build 21.0.10+7-nixos, mixed mode, sharing) | +| Postgres | Recommended: PostgreSQL 17.9 (Debian 17.9-1.pgdg13+1) – Also tested: PostgreSQL 14.23 (Debian 14.23-1.pgdg13+1), PostgreSQL 15.18 (Debian 15.18-1.pgdg13+1), PostgreSQL 16.14 (Debian 16.14-1.pgdg13+1) | + + + diff --git a/canton/release-notes/3.5.2.md b/canton/release-notes/3.5.2.md new file mode 100644 index 0000000000..878024e001 --- /dev/null +++ b/canton/release-notes/3.5.2.md @@ -0,0 +1,69 @@ +# Release of Canton 3.5.2 + +Canton 3.5.2 has been released on June 03, 2026. + +## Summary + +This is a maintenance release that fixes an out of memory issue during synchronizer reconnect, addresses security vulnerabilities in the Canton docker base image, as well as minor improvements. + +## Minor Improvements + +- Updated the Docker base image to 1.0.8 to address vulnerabilities in Busybox: CVE-2025-46394, CVE-2025-60876. +- Fix sequencer subscription closing errors `Timeout 9 seconds expired, but readers are still active in Task closing flushing direct-sequencer-subscription`. +- Handle decoding of `ContractId Void` in PQS +- Improve caching of contract key lookups + +## Deprecations + +### Removal of synchronous processing on the mediator +The synchronous processing mode on the mediator has been removed in favor of the asynchronous one +that offers better performance. + +Config path `myMediator.config.asynchronous-processing` has been deprecated as it is on by default now. + +## Bugfixes + +### (26-002, Medium): OOM from high memory usage after disconnecting and reconnecting from/to a synchronizer + +#### Issue Description +Because of the periodic time proof retrieval task (with interval minObservationDuration), the ConnectedSynchronizer +instance cannot be garbage collected right away after a disconnect/reconnect from the synchronizer. + +This leads to higher memory consumption which can lead to OOMs. + +#### Affected Deployments +Participant nodes + +#### Affected Versions +All versions before 3.5.2 + +#### Impact +High memory consumption and potentially OOM of the participant + +#### Symptom +The heap dump shows instances of the ConnectedSynchronizer from previously disconnected synchronizers. +The stack of calls refers to PeriodicAcknowledgements, TimeProofRequestsSubmitterImpl, SynchronizerTimeTracker and finally Clock + +#### Workaround +Restart the participant. + +#### Likeliness +Sometimes + +#### Recommendation +Upgrade to 3.5.2 + +## Compatibility + +The following Canton protocol versions are supported: + +| Dependency | Version | +|----------------------------|----------------------------| +| Canton protocol versions | 34, 35 | + +Canton has been tested against the following versions of its dependencies: + +| Dependency | Version | +|----------------------------|----------------------------| +| Java Runtime | OpenJDK 64-Bit Server VM (build 21.0.12+2-nixos, mixed mode, sharing) | +| Postgres | Recommended: PostgreSQL 17.10 (Debian 17.10-1.pgdg13+1) – Also tested: PostgreSQL 14.23 (Debian 14.23-1.pgdg13+1), PostgreSQL 15.18 (Debian 15.18-1.pgdg13+1), PostgreSQL 16.14 (Debian 16.14-1.pgdg13+1) | diff --git a/canton/release-notes/3.5.3.md b/canton/release-notes/3.5.3.md new file mode 100644 index 0000000000..5a1d1df948 --- /dev/null +++ b/canton/release-notes/3.5.3.md @@ -0,0 +1,30 @@ +# Release of Canton 3.5.3 + +Canton 3.5.3 has been released on June 03, 2026. You can download the Daml Open Source edition from the Daml Connect [Github Release Section](https://github.com/digital-asset/daml/releases/tag/v3.5.3). The Enterprise edition is available on [Artifactory](https://digitalasset.jfrog.io/artifactory/canton-enterprise/canton-enterprise-3.5.3.zip). +Please also consult the [full documentation of this release](https://docs.daml.com/3.5.3/canton/about.html). + +## Summary + +This is a maintenance release that fixes an issue around mediator restart and LSU. + +## Bugfixes + +### Mediator cannot reconnect to sequencer after processing LSU sequencing test message + +During LSU, after processing a sequencing test message, a mediator cannot reconnect to the sequencer. +The connection issue resolves itself when the upgrade time is reached and traffic is initialized on the sequencer. + +## Compatibility + +The following Canton protocol versions are supported: + +| Dependency | Version | +|----------------------------|----------------------------| +| Canton protocol versions | 34, 35 | + +Canton has been tested against the following versions of its dependencies: + +| Dependency | Version | +|----------------------------|----------------------------| +| Java Runtime | OpenJDK 64-Bit Server VM (build 21.0.12+2-nixos, mixed mode, sharing) | +| Postgres | Recommended: PostgreSQL 17.10 (Debian 17.10-1.pgdg13+1) – Also tested: PostgreSQL 14.23 (Debian 14.23-1.pgdg13+1), PostgreSQL 15.18 (Debian 15.18-1.pgdg13+1), PostgreSQL 16.14 (Debian 16.14-1.pgdg13+1) | diff --git a/canton/release-notes/3.5.4.md b/canton/release-notes/3.5.4.md new file mode 100644 index 0000000000..4f7a44abbf --- /dev/null +++ b/canton/release-notes/3.5.4.md @@ -0,0 +1,51 @@ +# Release of Canton 3.5.4 + +Canton 3.5.4 has been released on June 10, 2026. + +## Summary + +This is a maintenance release that fixes a bug around modify synchronizers and brings a few improvements. + +## What’s New + +### Minor Improvements +- Config flag `canton.participants.participant.parameter.alpha-multi-synchronizer-support` was renamed to `enable-all-ledger-api-reassignments`. +- Participant topology feature flag `EnableAlphaMultiSynchronizer` was renamed to `EnableMultiSynchronizer`. +- Improved coordination between the ACS commitment processor and its store, preventing benign `DB_STORAGE_DEGRADATION` warnings during participant shutdown. + +#### Improvements around listing synchronizers + +- Message `ListRegisteredSynchronizersRequest` accepts a new `all_statuses` attribute to allow to retrieve all + registered synchronizers (and not only the active ones). +- Added new console command `participant1.synchronizers.list_all_registered` to list all registered synchronizers. + +#### Improvements around BFT Orderer's database pruning and performance + +- Database tables partitioning was introduced to the database tables of the BFT Orderer +- These changes bring significant improvements to the performance of the pruning operation as well as many other queries that got optimized +- IMPORTANT: these changes require running a new automatic database migration that can potentially take considerable amount of time, depending on how much data is in the affected tables at the time of the migration + +## Bugfixes + +- Fixed an issue that was making `ModifySynchronizer` request to fail if the physical synchronizer id was set. + +## Compatibility + +The following Canton protocol versions are supported: + +| Dependency | Version | +|----------------------------|----------------------------| +| Canton protocol versions | 34, 35 | + +Canton has been tested against the following versions of its dependencies: + +| Dependency | Version | +|----------------------------|----------------------------| +| Java Runtime | OpenJDK 64-Bit Server VM (build 21.0.12+2-nixos, mixed mode, sharing) | +| Postgres | Recommended: PostgreSQL 17.10 (Debian 17.10-1.pgdg13+1) – Also tested: PostgreSQL 14.23 (Debian 14.23-1.pgdg13+1), PostgreSQL 15.18 (Debian 15.18-1.pgdg13+1), PostgreSQL 16.14 (Debian 16.14-1.pgdg13+1) | + + +## What's Coming + +We are currently working on + diff --git a/canton/release-notes/3.5.5.md b/canton/release-notes/3.5.5.md new file mode 100644 index 0000000000..b1dd800d3d --- /dev/null +++ b/canton/release-notes/3.5.5.md @@ -0,0 +1,68 @@ +# Release of Canton 3.5.5 + +Canton 3.5.5 has been released on June 17, 2026. + +## Summary + +This is a maintenance release that fixes an exceptional but severe bug around logical synchronizer upgrades and brings a few improvements. + +## What’s New + +### Dependencies of vetted packages can be unvetted (PV35+) +On synchronizers running protocol versions 35 and above, vetting state change operations on the API (e.g. via ``/package-vetting/update``) +allow dependencies of vetted packages to be unvetted safely, without requiring the use of a force flag. +This is consistent with the transaction protocol, which does not require the dependencies of a Daml transaction node package to be vetted in PV35+. +**Note**: For protocol versions 34 and below, the package dependency vetting restrictions remain unchanged. + +### Minor Improvements + +- Updated docker base image from 1.0.8 to 1.0.9, which bumps grpc-health-probe to v0.4.52. +- Reduce memory footprint with streaming of large gRPC responses by avoiding buffering the whole response. +- Improve log message in the sequencer in case queuing the event signal is not possible due to a closed subscription and not describe that case as an error. +- New participant config `canton.participants.myParticipant.parameters.connect-to-synchronizers-on-startup` allows to disable automatic connect to synchronizers + on startup. + +## Bugfixes + +### (26-003, High): Restarting stuck sequencer yields to sequencer fork + +#### Issue Description +Around LSU: if a sequencer not processing blocks is restarted after upgrade time and before it had the chance to process any block, +it will delete traffic data entries as part of crash recovery. + +#### Affected Deployments +Sequencer nodes + +#### Affected Versions +All versions before 3.5.5 + +#### Impact +Sequencer fork + +#### Symptom +Inability for participants to connect (cannot get consensus on traffic) or participant disconnecting with SEQUENCER_FORK_DETECTED + +#### Workaround +Restore from backup + +#### Likeliness +Exceptional + +#### Recommendation +Upgrade to 3.5.5 + +## Compatibility + +The following Canton protocol versions are supported: + +| Dependency | Version | +|----------------------------|----------------------------| +| Canton protocol versions | 34, 35 | + +Canton has been tested against the following versions of its dependencies: + +| Dependency | Version | +|----------------------------|----------------------------| +| Java Runtime | OpenJDK 64-Bit Server VM (build 21.0.12+2-nixos, mixed mode, sharing) | +| Postgres | Recommended: PostgreSQL 17.10 (Debian 17.10-1.pgdg13+1) – Also tested: PostgreSQL 14.23 (Debian 14.23-1.pgdg13+1), PostgreSQL 15.18 (Debian 15.18-1.pgdg13+1), PostgreSQL 16.14 (Debian 16.14-1.pgdg13+1) | + diff --git a/canton/release-notes/3.5.6.md b/canton/release-notes/3.5.6.md new file mode 100644 index 0000000000..d48bcf9cd8 --- /dev/null +++ b/canton/release-notes/3.5.6.md @@ -0,0 +1,77 @@ +# Release of Canton 3.5.6 + +Canton 3.5.6 has been released on June 24, 2026. + +## Summary + +This is a maintenance release that fixes an exceptional severe bug around logical synchronizer upgrades and brings a few improvements. + +## What’s New + +### Minor Improvements +- Connection pool metrics: + - Add a `psid` label, populated if it is provided when connecting. This should be the case starting from the second connection to a synchronizer, or upon LSU. + - Close the `connection-health` and `subscription-health` metrics associated to the `psid` when the pool is closed, instead of closing all the existing ones when the pool is started. +- Updated com.google.protobuf libs from 3.25.5 --> 3.25.9 +- LSU: A call to `AcknowledgeSigned` with a timestamp before the upgrade time returns immediately, without any acknowledgement being done. +- Fix JDBC query for computing last activations: Under adverse conditions (data corruption) this query might called with empty inputs where it would have failed execution with PostgreSQL server version 14. +- (Potentially) *BREAKING*: Aggregatable submissions are now rejected eagerly to preserve bandwidth. + This means that the submission error code `SEQUENCER_AGGREGATE_SUBMISSION_ALREADY_SENT` may now also + be returned during the synchronous submission of the sequencer, as the state of the aggregation is also + checked before ordering. In addition, the gRPC error code has been modified from `FAILED_PRECONDITION` to + `ALREADY_EXISTS` to better reflect the nature of the error. Clients should be updated to handle this error + code accordingly. Due to backwards compatibility, the old gRPC error code will be returned for PV35 and + before on the async path, and the new capability must only be turned on when all nodes have been + upgraded to a Canton version that supports this change. The new capability can be disabled using `canton.sequencers.seq.parameters.enable-reject-delivered-aggregations-on-pv-35 = []` + for mediators. This can be combined with the new configuration option of the mediator `canton.mediators.mymediator.parameters.delayed-verdict-sender.enabled = false`. + Generally, the sequencer will send out the verdict after reaching the threshold. All subsequent sent verdicts are thrown away. The new option now allows threshold + extra verdicts to be sent immediately, while the rest of the mediators will wait a short amount of time. This allows to reduce the load on the sequencer by 30%, creating more capacity for other transactions. +- Fixed an issue that prevent external parties from being allocated on a participant with an offline root key via the Ledger API's `PartyManagementService.AllocateExternalParty` endpoint. +- Fixed an issue whereby the sequencer would no longer make progress after a failed write. Sequencers in such a state now report 'liveness' unhealthy. +- Require Participant Admin (not just IDP Admin) permissions in order to be able to grant CanReadAsAnyParty and CanExecuteAsAnyParty rights to users. + +## Bugfixes + +### (26-004, High): LSU: Missing synchronization between topology local copy and purging + +#### Issue Description +Because of the lack of synchronization, it can happen that topology purging kicks in before the local copy of the topology state is finished, which result in incorrect topology state for the successor. + +The issue can occur only when topology purging is enabled, which is not the case by default. + +#### Affected Deployments +Participant nodes + +#### Affected Versions +All versions before 3.5.6 + +#### Impact +Topology fork + +#### Symptom + +- Participant nodes issues warning/errors about missing topology transactions that were valid before the LSU. +- Submission of a transaction is rejected because of missing topology transactions that were valid before the LSU. + +#### Workaround +Restore from backup and ensure topology purging is disabled + +#### Likeliness +Exceptional + +#### Recommendation +Upgrade to 3.5.6 + +## Compatibility + +The following Canton protocol versions are supported: + +| Dependency | Version | +|----------------------------|----------------------------| +| Canton protocol versions | 34, 35 | + +Canton has been tested against the following versions of its dependencies: + +| Dependency | Version | +|----------------------------|----------------------------| +| Java Runtime | OpenJDK 64-Bit Server VM (build 21.0.12+2-nixos, mixed mode, sharing) | +| Postgres | Recommended: PostgreSQL 17.10 (Debian 17.10-1.pgdg13+1) – Also tested: PostgreSQL 14.23 (Debian 14.23-1.pgdg13+1), PostgreSQL 15.18 (Debian 15.18-1.pgdg13+1), PostgreSQL 16.14 (Debian 16.14-1.pgdg13+1) | diff --git a/canton/shared_dependencies.json b/canton/shared_dependencies.json index a7e0951f3d..99e6888386 100644 --- a/canton/shared_dependencies.json +++ b/canton/shared_dependencies.json @@ -7,8 +7,8 @@ "com.google.api.grpc:proto-google-common-protos" : "2.59.2", "com.google.code.findbugs:jsr305" : "3.0.2", "com.google.guava:guava" : "33.3.0-jre", - "com.google.protobuf:protobuf-java" : "3.25.5", - "com.google.protobuf:protobuf-java-util" : "3.25.5", + "com.google.protobuf:protobuf-java" : "3.25.9", + "com.google.protobuf:protobuf-java-util" : "3.25.9", "com.lihaoyi:sourcecode_2.13" : "0.3.0", "com.storm-enroute:scalameter-core_2.13" : "0.21", "com.storm-enroute:scalameter_2.13" : "0.21", diff --git a/cluster/compose/localnet/conf/console/app-synchronizer.sc b/cluster/compose/localnet/conf/console/app-synchronizer.sc index 1b782b0d15..bc09c5e5cd 100644 --- a/cluster/compose/localnet/conf/console/app-synchronizer.sc +++ b/cluster/compose/localnet/conf/console/app-synchronizer.sc @@ -1,4 +1,4 @@ -bootstrap.synchronizer( +val appSynchronizerId = bootstrap.synchronizer( synchronizerName = "app-synchronizer", sequencers = Seq(`app-sequencer`), mediators = Seq(`app-mediator`), @@ -14,3 +14,54 @@ utils.retry_until_true { `app-provider`.synchronizers.active("app-synchronizer") && `app-user`.synchronizers.active("app-synchronizer") } + +// Enable the multi-synchronizer topology feature flag on every synchronizer each +// participant is connected to +val multiSyncParticipants = Seq(`app-provider`, `app-user`) + +// Wait until the participants are also connected to the global synchronizer, otherwise +// we would only enable the flag on the app-synchronizer. +utils.retry_until_true { + multiSyncParticipants.forall( + _.synchronizers.list_connected().exists(_.synchronizerId != appSynchronizerId.logical) + ) +} + +val multiSyncFeatureFlag = + SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer +multiSyncParticipants.foreach { participant => + participant.synchronizers.list_connected().map(_.synchronizerId).distinct.foreach { + synchronizerId => + val existingFlags = participant.topology.synchronizer_trust_certificates + .list( + store = Some(TopologyStoreId.Synchronizer(synchronizerId)), + filterUid = participant.id.filterString, + ) + .map(_.item.featureFlags) + .flatten + .distinct + if (!existingFlags.contains(multiSyncFeatureFlag)) { + participant.topology.synchronizer_trust_certificates + .propose( + participant.id, + synchronizerId, + featureFlags = existingFlags :+ multiSyncFeatureFlag, + ) + } + } +} + +// Ensure the flag became effective on all synchronizers before the console exits. +utils.retry_until_true { + multiSyncParticipants.forall { participant => + participant.synchronizers.list_connected().map(_.synchronizerId).distinct.forall { + synchronizerId => + participant.topology.synchronizer_trust_certificates + .list( + store = Some(TopologyStoreId.Synchronizer(synchronizerId)), + filterUid = participant.id.filterString, + ) + .exists(_.item.featureFlags.contains(multiSyncFeatureFlag)) + } + } +} diff --git a/cluster/compose/localnet/env/splice.env b/cluster/compose/localnet/env/splice.env index 7a23f4bcd7..41edb60178 100644 --- a/cluster/compose/localnet/env/splice.env +++ b/cluster/compose/localnet/env/splice.env @@ -1,5 +1,5 @@ # Traffic topups -TARGET_TRAFFIC_THROUGHPUT=20000 +TARGET_TRAFFIC_THROUGHPUT=${TARGET_TRAFFIC_THROUGHPUT:-20000} MIN_TRAFFIC_TOPUP_INTERVAL=1m SPLICE_APP_VALIDATOR_SV_SPONSOR_ADDRESS=http://localhost:5014 diff --git a/cluster/configs/configs/configs/TestNet/approved-sv-id-values.yaml b/cluster/configs/configs/configs/TestNet/approved-sv-id-values.yaml index 197bfcd0bd..2e4293b683 100644 --- a/cluster/configs/configs/configs/TestNet/approved-sv-id-values.yaml +++ b/cluster/configs/configs/configs/TestNet/approved-sv-id-values.yaml @@ -1,13 +1,13 @@ approvedSvIdentities: - name: Digital-Asset-2 publicKey: PUBKEY_0== - rewardWeightBps: 100000 + rewardWeightBps: 100_000 - name: SV1 publicKey: PUBLIC_KEY_1== - rewardWeightBps: 1000000 + rewardWeightBps: 1_000_000 - name: SV2 publicKey: PUBLIC_KEY_2== - rewardWeightBps: 150000 + rewardWeightBps: 150_000 extraBeneficiaries: - beneficiary: "mock-validator-1::123456789012345678901234567890123456789012234567890123456789012345678" weight: 100000 @@ -16,4 +16,4 @@ approvedSvIdentities: - name: Digital-Asset-1 publicKey: THIS_IS_THE_WRONG_KEY # this is the right reward weight though - rewardWeightBps: 150000 + rewardWeightBps: 150_000 diff --git a/cluster/configs/shared/base.yaml b/cluster/configs/shared/base.yaml index 17c3d01f33..aaf2766439 100644 --- a/cluster/configs/shared/base.yaml +++ b/cluster/configs/shared/base.yaml @@ -1,8 +1,5 @@ # Reference configuration options cluster: - hyperdiskSupport: - enabled: true - enabledForInfra: true nodePools: apps: minNodes: 0 @@ -27,6 +24,7 @@ infra: proxyForIstioHttp: false istio: enableIngressAccessLogging: true + enablePublicTokenRegistry: true sequencerFlowControl: # See https://github.com/DACH-NY/canton-network-internal/issues/4901 # The values are somewhat arbitrary so more fine tuning may be appropriate. @@ -123,8 +121,12 @@ monitoring: rate: 25 overMinutes: 5 dsoMissedConfirmations: - threshold: 0.01 + threshold: 0 # alert as soon as there's any confirmation missing windowMinutes: 10 + spliceRateLimits: + usageThreshold: 0.8 + rejectionCountThreshold: 10 + excludedLimiters: [] cloudSql: maintenance: false cometbft: @@ -140,8 +142,8 @@ monitoring: acsCommitments: checkpointDelay: # Usually every 30s but while the commitment every 30min gets computed - # it gets blocked so we allow up to 20min - seconds: 1200 + # it gets blocked so we allow up to 40min + seconds: 2400 completedDelay: # Usually every 30min, we allow up to 1h to avoid false positives. seconds: 3600 @@ -171,6 +173,17 @@ monitoring: # Multiplier applied to a wallet sweep's configured maxBalance to set the alert threshold. # 1.5 means the alert fires if the balance is 50% higher than expected. tolerance: 1.5 + globalSynchronizerHealth: + # Fraction (0-1) of sequenced confirmation requests that were discarded + # (i.e., never processed by the mediator, e.g. due to CometBFT replays) + # above which the alert fires. + discardedConfirmationRequestsThreshold: 0.2 + # Fraction (0-1) of confirmation requests that failed (as observed by the + # mediator, over the last 30m) above which the alert fires. + failedConfirmationRequestsThreshold: 0.1 + # Fire when TPS (approved confirmation requests per second) over the last + # 30m drops below this fraction of the previous 30m. + tpsDropThreshold: 0.5 gcpQuotas: # Quota metric names to exclude from all GCP quota alerts. # Each entry is matched exactly for quota exceeded. @@ -211,7 +224,8 @@ monitoring: ( ( jsonPayload.message=~"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+" AND - -jsonPayload.message=~"(?i)(secret|token|(private|secret)(-)?key|password)=(\"\*\*\*\*\"|hidden)" + -jsonPayload.message=~"(?i)(secret|token|(private|secret)(-)?key|password)=(\"\*\*\*\*\"|hidden)" AND + -jsonPayload.message=~"(?i)page(_|-)?token=[^, ]+" ) OR jsonPayload.message=~"eyJhbGc[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}" OR jsonPayload.message=~"Bearer\s+eyJ[A-Za-z0-9_-]{2,}" @@ -226,7 +240,12 @@ cloudArmor: pathPrefix: /api/scan throttleAcrossAllEndpointsAllIps: withinIntervalSeconds: 60 - maxRequestsBeforeHttp429: 0 + maxRequestsBeforeHttp429: 0 # Keeps scan completely closed to the public + tokenRegistry: + pathPrefix: /registry + throttleAcrossAllEndpointsAllIps: + withinIntervalSeconds: 60 + maxRequestsBeforeHttp429: 200 multiValidator: postgresPvcSize: '100Gi' resources: @@ -250,7 +269,7 @@ multiValidator: sv: scan: externalRateLimits: - !include(./rate-limits/v0-acs.yaml;./rate-limits/unlimited.yaml;./rate-limits/public-banned.yaml) + !include(./rate-limits/v0-acs.yaml;./rate-limits/unlimited.yaml;./rate-limits/public-banned.yaml;./rate-limits/token-registry.yaml) globalLimits: maxTokens: 2147483647 tokensPerFill: 2147483647 diff --git a/cluster/configs/shared/rate-limits/token-registry.yaml b/cluster/configs/shared/rate-limits/token-registry.yaml new file mode 100644 index 0000000000..abda7327bd --- /dev/null +++ b/cluster/configs/shared/rate-limits/token-registry.yaml @@ -0,0 +1,121 @@ +rateLimits: + /registry/allocations/v1: + name: registry-allocations + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/metadata/v1/info: + name: registry-metadata-info + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/metadata/v1/instruments: + name: registry-metadata-instruments + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/allocation-instruction/v1/allocation-factory: + name: registry-allocation-factory + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/transfer-instruction/v1: + name: registry-transfer-instruction + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/transfer-instruction/v1/transfer-factory: + name: registry-transfer-factory + type: limited + maxTokens: 1440 + tokensPerFill: 1440 + fillInterval: 60s + perIpLimits: + maxTokens: 240 + tokensPerFill: 240 + fillInterval: 60s + /registry/allocation/v2/settlement-factory: + name: registry-settlement-factory-v2 + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/allocations/v2: + name: registry-allocations-v2 + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/allocation-instruction/v2/allocation-factory: + name: registry-allocation-factory-v2 + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/allocation-instruction/v2: + name: registry-allocation-instruction-v2 + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/transfer-instruction/v2/transfer-factory: + name: registry-transfer-factory-v2 + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s + /registry/transfer-instruction/v2: + name: registry-transfer-instruction-v2 + type: limited + maxTokens: 720 + tokensPerFill: 720 + fillInterval: 60s + perIpLimits: + maxTokens: 120 + tokensPerFill: 120 + fillInterval: 60s diff --git a/cluster/configs/shared/rate-limits/unlimited.yaml b/cluster/configs/shared/rate-limits/unlimited.yaml index 65b069f8b8..6d9cd05373 100644 --- a/cluster/configs/shared/rate-limits/unlimited.yaml +++ b/cluster/configs/shared/rate-limits/unlimited.yaml @@ -69,6 +69,10 @@ rateLimits: name: state type: unlimited + /api/scan/v2/state: + name: state + type: unlimited + /api/scan/v0/holdings: name: holdings type: unlimited @@ -77,6 +81,10 @@ rateLimits: name: holdings type: unlimited + /api/scan/v2/holdings: + name: holdings + type: unlimited + /api/scan/v0/ans-entries: name: ans-entries type: unlimited @@ -85,10 +93,6 @@ rateLimits: name: dso-party-id type: unlimited - /api/scan/v0/transactions: - name: transactions - type: unlimited - /api/scan/v0/backfilling: name: backfilling type: unlimited @@ -185,6 +189,10 @@ rateLimits: name: listBulkUpdateHistoryObjects type: unlimited + /api/scan/v0/history/bulk/checksums: + name: getBulkHistoryChecksums + type: unlimited + /api/scan/v0/active-synchronizer-serial: name: activeSynchronizerSerial type: unlimited diff --git a/cluster/configs/shared/rate-limits/v0-acs.yaml b/cluster/configs/shared/rate-limits/v0-acs.yaml index f4d90428c0..af956dbdb2 100644 --- a/cluster/configs/shared/rate-limits/v0-acs.yaml +++ b/cluster/configs/shared/rate-limits/v0-acs.yaml @@ -2,7 +2,10 @@ rateLimits: /api/scan/v0/acs: name: acs type: limited - clientIp: true - maxTokens: 10 - tokensPerFill: 5 + maxTokens: 500 # The per-endpoint global limit (shared between all ips) + tokensPerFill: 500 fillInterval: 60s + perIpLimits: # per-IP limit + maxTokens: 10 + tokensPerFill: 5 + fillInterval: 60s diff --git a/cluster/configs/shared/scratch-default-synchronizer-migration.yaml b/cluster/configs/shared/scratch-default-synchronizer-migration.yaml index 43b07dcca9..814030e96c 100644 --- a/cluster/configs/shared/scratch-default-synchronizer-migration.yaml +++ b/cluster/configs/shared/scratch-default-synchronizer-migration.yaml @@ -1,4 +1,5 @@ synchronizerMigration: + splitSvDeploymentEnabled: true frozenMigrationId: 0 active: id: 0 diff --git a/cluster/configs/shared/scratchnet-sv.yaml b/cluster/configs/shared/scratchnet-sv.yaml index 4cd0d393e9..b07cb0aa5e 100644 --- a/cluster/configs/shared/scratchnet-sv.yaml +++ b/cluster/configs/shared/scratchnet-sv.yaml @@ -22,6 +22,7 @@ participant: requests: cpu: "1" sequencer: + enableAntiAffinity: false # formerly the effect of `SEQUENCER_LOW_RESOURCES=true` resources: limits: diff --git a/cluster/configs/shared/scratchnet.yaml b/cluster/configs/shared/scratchnet.yaml index 4c1dc917e8..1438dce184 100644 --- a/cluster/configs/shared/scratchnet.yaml +++ b/cluster/configs/shared/scratchnet.yaml @@ -3,14 +3,8 @@ cluster: nodePools: apps: zones: '*' - additionalApps: - # Second, Intel based, pool is supposed to address GCloud compute resource issues. - - minNodes: 0 - # A high max-nodes by default to support large deployments and hard migrations - # Should be set to a lower number (currently 8) on CI clusters that do neither of those. - maxNodes: 20 - nodeType: n4-standard-16 - zones: '*' + maxNodes: 40 + nodeType: c3d-standard-8 infra: prometheus: retentionDuration: "30d" diff --git a/cluster/deployment/mock/config.yaml b/cluster/deployment/mock/config.yaml index 0ed9511638..71b301eb91 100644 --- a/cluster/deployment/mock/config.yaml +++ b/cluster/deployment/mock/config.yaml @@ -32,8 +32,24 @@ multiValidator: - name: MULTI_PARTICIPANT_ADDITIONAL_CONFIG_MAX_CONNECTIONS value: canton.participants.participant_INDEX.storage.parameters.max-connections = 33 sv: + cometbft: + watchdog: + threshold: 2 + evaluationIntervalSeconds: 900 synchronizer: skipInitialization: true + scan: + externalRateLimits: + rateLimits: + /registry/metadata/v1/info: + perIpLimits: + overrides: + test: + ips: + - 192.68.78.50 + maxTokens: 250 + tokensPerFill: 250 + fillInterval: 60s splitwell: maxDarVersion: '0.1.8' participantPruningSchedule: @@ -41,6 +57,7 @@ splitwell: maxDuration: "5m" retention: "30d" synchronizerMigration: + splitSvDeploymentEnabled: true frozenMigrationId: 2 archived: - id: 5 @@ -145,16 +162,29 @@ monitoring: muteTimeIntervals: - name: sv1-mute objectMatchers: - - [ 'namespace', '=', 'sv1' ] - startTime: '04:00' - endTime: '07:00' - weekdays: - - saturday:sunday + - [ 'namespace', '=', 'sv1-test' ] + timeWindows: + - times: + - startTime: '18:00' + endTime: '24:00' + weekdays: + - wednesday - name: sv-runbook-mute objectMatchers: - - [ 'namespace', '=', 'sv' ] - startTime: '06:00' - endTime: '10:00' + - [ 'namespace', '=', 'sv-test' ] + timeWindows: + - times: + - startTime: '18:00' + endTime: '24:00' + weekdays: + - wednesday + - times: + - startTime: '00:00' + endTime: '06:00' + - startTime: '12:00' + endTime: '13:00' + weekdays: + - thursday operatorDeployment: reference: gitReference: refs/heads/main @@ -176,6 +206,8 @@ loadTester: scaleUpStep: 3 windowStartUTC: "02:30" windowDurationMinutes: 150 +chaosMesh: + podKillSchedule: '@every 120m' svs: sv: !include(sv.yaml) participant: @@ -247,6 +279,10 @@ svs: cantonLogLevel: INFO apiRequestLogLevel: DEBUG infra: + ipWhitelisting: + extraWhitelistedIngress: + - 1.2.3.4/32 + - 2.3.4.5/32 extraCustomResources: deny-onboard-prepare-endpoint: apiVersion: security.istio.io/v1 @@ -274,6 +310,7 @@ infra: key: value anotherKey: anotherValue istio: + enableGeneralIpWhitelist: false istiodValues: # example istiod overrides global: proxy: @@ -296,6 +333,11 @@ cluster: minNodes: 0 maxNodes: 1 zones: '*' + additionalInfra: + - nodeType: n4-standard-8 + minNodes: 1 + maxNodes: 3 + zones: '*' gha: githubRepo: "https://github.com/canton-network/splice" runnerVersion: "0.5.10" diff --git a/cluster/deployment/package-lock.json b/cluster/deployment/package-lock.json new file mode 100644 index 0000000000..a7744aa785 --- /dev/null +++ b/cluster/deployment/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "deployment", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/cluster/deployment/scratchneta/config.resolved.yaml b/cluster/deployment/scratchneta/config.resolved.yaml index f6144013ce..aa12600118 100644 --- a/cluster/deployment/scratchneta/config.resolved.yaml +++ b/cluster/deployment/scratchneta/config.resolved.yaml @@ -9,20 +9,17 @@ cloudArmor: throttleAcrossAllEndpointsAllIps: maxRequestsBeforeHttp429: 0 withinIntervalSeconds: 60 + tokenRegistry: + pathPrefix: '/registry' + throttleAcrossAllEndpointsAllIps: + maxRequestsBeforeHttp429: 200 + withinIntervalSeconds: 60 cluster: - hyperdiskSupport: - enabled: true - enabledForInfra: true nodePools: - additionalApps: - - maxNodes: 20 - minNodes: 0 - nodeType: 'n4-standard-16' - zones: '*' apps: - maxNodes: 20 + maxNodes: 40 minNodes: 0 - nodeType: 'n4d-standard-16' + nodeType: 'c3d-standard-8' zones: '*' infra: maxNodes: 3 @@ -33,6 +30,7 @@ infra: proxyForIstioHttp: false istio: enableIngressAccessLogging: true + enablePublicTokenRegistry: true sequencerFlowControl: initialConnectionWindowSize: 52428800 initialStreamWindowSize: 524288 @@ -46,7 +44,7 @@ monitoring: alerts: acsCommitments: checkpointDelay: - seconds: 1200 + seconds: 2400 completedDelay: seconds: 3600 computeDuration: @@ -71,7 +69,7 @@ monitoring: deployment: pendingPeriodMinutes: 5 dsoMissedConfirmations: - threshold: 0.01 + threshold: 0 windowMinutes: 10 gcpQuotas: excludedApproachingMetrics: [] @@ -85,6 +83,10 @@ monitoring: - 'cloudkms.googleapis.com/external_kms_requests' retestWindowSeconds: 600 rollingWindowSeconds: 600 + globalSynchronizerHealth: + discardedConfirmationRequestsThreshold: 0.2 + failedConfirmationRequestsThreshold: 0.1 + tpsDropThreshold: 0.5 ingestion: thresholdEntriesPerBatch: 80 loadTester: @@ -108,6 +110,10 @@ monitoring: sequencerRateLimits: circuitBreakerStateThreshold: 0.5 rejectionRateThreshold: 0 + spliceRateLimits: + excludedLimiters: [] + rejectionCountThreshold: 10 + usageThreshold: 0.8 trafficBasedRewards: featuredAppRightsLimit: 10000 verdictIngestionBatchSizePendingPeriodMinutes: 60 @@ -119,7 +125,7 @@ monitoring: walletSweep: tolerance: 1.5 enableNoDataAlerts: false - loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" + loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\" AND\n -jsonPayload.message=~\"(?i)page(_|-)?token=[^, ]+\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" muteTimeIntervals: [] enableGrafanaServiceAccountToken: true multiValidator: @@ -212,11 +218,14 @@ sv: name: 'status' type: 'unlimited' /api/scan/v0/acs: - clientIp: true fillInterval: '60s' - maxTokens: 10 + maxTokens: 500 name: 'acs' - tokensPerFill: 5 + perIpLimits: + fillInterval: '60s' + maxTokens: 10 + tokensPerFill: 5 + tokensPerFill: 500 type: 'limited' /api/scan/v0/active-synchronizer-serial: name: 'activeSynchronizerSerial' @@ -281,6 +290,9 @@ sv: /api/scan/v0/history/bulk/acs: name: 'listBulkAcsSnapshotObjects' type: 'unlimited' + /api/scan/v0/history/bulk/checksums: + name: 'getBulkHistoryChecksums' + type: 'unlimited' /api/scan/v0/history/bulk/updates: name: 'listBulkUpdateHistoryObjects' type: 'unlimited' @@ -320,9 +332,6 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' - /api/scan/v0/transactions: - name: 'transactions' - type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' @@ -359,12 +368,138 @@ sv: /api/scan/v1/updates: name: 'v1-updates' type: 'banned' + /api/scan/v2/holdings: + name: 'holdings' + type: 'unlimited' + /api/scan/v2/state: + name: 'state' + type: 'unlimited' /api/scan/v2/updates: name: 'v2-updates' type: 'unlimited' /api/scan/version: name: 'version' type: 'unlimited' + /registry/allocation-instruction/v1/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation/v2/settlement-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-settlement-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/info: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-info' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/instruments: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-instruments' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1/transfer-factory: + fillInterval: '60s' + maxTokens: 1440 + name: 'registry-transfer-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 240 + tokensPerFill: 240 + tokensPerFill: 1440 + type: 'limited' + /registry/transfer-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v2/transfer-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' svs: sv: cometbft: @@ -416,6 +551,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -490,6 +626,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -564,6 +701,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -638,6 +776,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -712,6 +851,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -786,6 +926,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -860,6 +1001,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -934,6 +1076,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1008,6 +1151,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1082,6 +1226,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1156,6 +1301,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1230,6 +1376,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1304,6 +1451,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1378,6 +1526,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1452,6 +1601,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1526,6 +1676,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1600,6 +1751,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1677,6 +1829,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1721,6 +1874,7 @@ synchronizerMigration: spliceRoot: 'splice' version: 'local' frozenMigrationId: 0 + splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/deployment/scratchnetb/config.resolved.yaml b/cluster/deployment/scratchnetb/config.resolved.yaml index f6144013ce..aa12600118 100644 --- a/cluster/deployment/scratchnetb/config.resolved.yaml +++ b/cluster/deployment/scratchnetb/config.resolved.yaml @@ -9,20 +9,17 @@ cloudArmor: throttleAcrossAllEndpointsAllIps: maxRequestsBeforeHttp429: 0 withinIntervalSeconds: 60 + tokenRegistry: + pathPrefix: '/registry' + throttleAcrossAllEndpointsAllIps: + maxRequestsBeforeHttp429: 200 + withinIntervalSeconds: 60 cluster: - hyperdiskSupport: - enabled: true - enabledForInfra: true nodePools: - additionalApps: - - maxNodes: 20 - minNodes: 0 - nodeType: 'n4-standard-16' - zones: '*' apps: - maxNodes: 20 + maxNodes: 40 minNodes: 0 - nodeType: 'n4d-standard-16' + nodeType: 'c3d-standard-8' zones: '*' infra: maxNodes: 3 @@ -33,6 +30,7 @@ infra: proxyForIstioHttp: false istio: enableIngressAccessLogging: true + enablePublicTokenRegistry: true sequencerFlowControl: initialConnectionWindowSize: 52428800 initialStreamWindowSize: 524288 @@ -46,7 +44,7 @@ monitoring: alerts: acsCommitments: checkpointDelay: - seconds: 1200 + seconds: 2400 completedDelay: seconds: 3600 computeDuration: @@ -71,7 +69,7 @@ monitoring: deployment: pendingPeriodMinutes: 5 dsoMissedConfirmations: - threshold: 0.01 + threshold: 0 windowMinutes: 10 gcpQuotas: excludedApproachingMetrics: [] @@ -85,6 +83,10 @@ monitoring: - 'cloudkms.googleapis.com/external_kms_requests' retestWindowSeconds: 600 rollingWindowSeconds: 600 + globalSynchronizerHealth: + discardedConfirmationRequestsThreshold: 0.2 + failedConfirmationRequestsThreshold: 0.1 + tpsDropThreshold: 0.5 ingestion: thresholdEntriesPerBatch: 80 loadTester: @@ -108,6 +110,10 @@ monitoring: sequencerRateLimits: circuitBreakerStateThreshold: 0.5 rejectionRateThreshold: 0 + spliceRateLimits: + excludedLimiters: [] + rejectionCountThreshold: 10 + usageThreshold: 0.8 trafficBasedRewards: featuredAppRightsLimit: 10000 verdictIngestionBatchSizePendingPeriodMinutes: 60 @@ -119,7 +125,7 @@ monitoring: walletSweep: tolerance: 1.5 enableNoDataAlerts: false - loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" + loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\" AND\n -jsonPayload.message=~\"(?i)page(_|-)?token=[^, ]+\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" muteTimeIntervals: [] enableGrafanaServiceAccountToken: true multiValidator: @@ -212,11 +218,14 @@ sv: name: 'status' type: 'unlimited' /api/scan/v0/acs: - clientIp: true fillInterval: '60s' - maxTokens: 10 + maxTokens: 500 name: 'acs' - tokensPerFill: 5 + perIpLimits: + fillInterval: '60s' + maxTokens: 10 + tokensPerFill: 5 + tokensPerFill: 500 type: 'limited' /api/scan/v0/active-synchronizer-serial: name: 'activeSynchronizerSerial' @@ -281,6 +290,9 @@ sv: /api/scan/v0/history/bulk/acs: name: 'listBulkAcsSnapshotObjects' type: 'unlimited' + /api/scan/v0/history/bulk/checksums: + name: 'getBulkHistoryChecksums' + type: 'unlimited' /api/scan/v0/history/bulk/updates: name: 'listBulkUpdateHistoryObjects' type: 'unlimited' @@ -320,9 +332,6 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' - /api/scan/v0/transactions: - name: 'transactions' - type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' @@ -359,12 +368,138 @@ sv: /api/scan/v1/updates: name: 'v1-updates' type: 'banned' + /api/scan/v2/holdings: + name: 'holdings' + type: 'unlimited' + /api/scan/v2/state: + name: 'state' + type: 'unlimited' /api/scan/v2/updates: name: 'v2-updates' type: 'unlimited' /api/scan/version: name: 'version' type: 'unlimited' + /registry/allocation-instruction/v1/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation/v2/settlement-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-settlement-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/info: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-info' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/instruments: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-instruments' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1/transfer-factory: + fillInterval: '60s' + maxTokens: 1440 + name: 'registry-transfer-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 240 + tokensPerFill: 240 + tokensPerFill: 1440 + type: 'limited' + /registry/transfer-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v2/transfer-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' svs: sv: cometbft: @@ -416,6 +551,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -490,6 +626,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -564,6 +701,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -638,6 +776,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -712,6 +851,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -786,6 +926,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -860,6 +1001,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -934,6 +1076,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1008,6 +1151,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1082,6 +1226,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1156,6 +1301,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1230,6 +1376,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1304,6 +1451,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1378,6 +1526,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1452,6 +1601,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1526,6 +1676,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1600,6 +1751,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1677,6 +1829,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1721,6 +1874,7 @@ synchronizerMigration: spliceRoot: 'splice' version: 'local' frozenMigrationId: 0 + splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/deployment/scratchnetc/config.resolved.yaml b/cluster/deployment/scratchnetc/config.resolved.yaml index f6144013ce..aa12600118 100644 --- a/cluster/deployment/scratchnetc/config.resolved.yaml +++ b/cluster/deployment/scratchnetc/config.resolved.yaml @@ -9,20 +9,17 @@ cloudArmor: throttleAcrossAllEndpointsAllIps: maxRequestsBeforeHttp429: 0 withinIntervalSeconds: 60 + tokenRegistry: + pathPrefix: '/registry' + throttleAcrossAllEndpointsAllIps: + maxRequestsBeforeHttp429: 200 + withinIntervalSeconds: 60 cluster: - hyperdiskSupport: - enabled: true - enabledForInfra: true nodePools: - additionalApps: - - maxNodes: 20 - minNodes: 0 - nodeType: 'n4-standard-16' - zones: '*' apps: - maxNodes: 20 + maxNodes: 40 minNodes: 0 - nodeType: 'n4d-standard-16' + nodeType: 'c3d-standard-8' zones: '*' infra: maxNodes: 3 @@ -33,6 +30,7 @@ infra: proxyForIstioHttp: false istio: enableIngressAccessLogging: true + enablePublicTokenRegistry: true sequencerFlowControl: initialConnectionWindowSize: 52428800 initialStreamWindowSize: 524288 @@ -46,7 +44,7 @@ monitoring: alerts: acsCommitments: checkpointDelay: - seconds: 1200 + seconds: 2400 completedDelay: seconds: 3600 computeDuration: @@ -71,7 +69,7 @@ monitoring: deployment: pendingPeriodMinutes: 5 dsoMissedConfirmations: - threshold: 0.01 + threshold: 0 windowMinutes: 10 gcpQuotas: excludedApproachingMetrics: [] @@ -85,6 +83,10 @@ monitoring: - 'cloudkms.googleapis.com/external_kms_requests' retestWindowSeconds: 600 rollingWindowSeconds: 600 + globalSynchronizerHealth: + discardedConfirmationRequestsThreshold: 0.2 + failedConfirmationRequestsThreshold: 0.1 + tpsDropThreshold: 0.5 ingestion: thresholdEntriesPerBatch: 80 loadTester: @@ -108,6 +110,10 @@ monitoring: sequencerRateLimits: circuitBreakerStateThreshold: 0.5 rejectionRateThreshold: 0 + spliceRateLimits: + excludedLimiters: [] + rejectionCountThreshold: 10 + usageThreshold: 0.8 trafficBasedRewards: featuredAppRightsLimit: 10000 verdictIngestionBatchSizePendingPeriodMinutes: 60 @@ -119,7 +125,7 @@ monitoring: walletSweep: tolerance: 1.5 enableNoDataAlerts: false - loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" + loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\" AND\n -jsonPayload.message=~\"(?i)page(_|-)?token=[^, ]+\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" muteTimeIntervals: [] enableGrafanaServiceAccountToken: true multiValidator: @@ -212,11 +218,14 @@ sv: name: 'status' type: 'unlimited' /api/scan/v0/acs: - clientIp: true fillInterval: '60s' - maxTokens: 10 + maxTokens: 500 name: 'acs' - tokensPerFill: 5 + perIpLimits: + fillInterval: '60s' + maxTokens: 10 + tokensPerFill: 5 + tokensPerFill: 500 type: 'limited' /api/scan/v0/active-synchronizer-serial: name: 'activeSynchronizerSerial' @@ -281,6 +290,9 @@ sv: /api/scan/v0/history/bulk/acs: name: 'listBulkAcsSnapshotObjects' type: 'unlimited' + /api/scan/v0/history/bulk/checksums: + name: 'getBulkHistoryChecksums' + type: 'unlimited' /api/scan/v0/history/bulk/updates: name: 'listBulkUpdateHistoryObjects' type: 'unlimited' @@ -320,9 +332,6 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' - /api/scan/v0/transactions: - name: 'transactions' - type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' @@ -359,12 +368,138 @@ sv: /api/scan/v1/updates: name: 'v1-updates' type: 'banned' + /api/scan/v2/holdings: + name: 'holdings' + type: 'unlimited' + /api/scan/v2/state: + name: 'state' + type: 'unlimited' /api/scan/v2/updates: name: 'v2-updates' type: 'unlimited' /api/scan/version: name: 'version' type: 'unlimited' + /registry/allocation-instruction/v1/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation/v2/settlement-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-settlement-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/info: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-info' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/instruments: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-instruments' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1/transfer-factory: + fillInterval: '60s' + maxTokens: 1440 + name: 'registry-transfer-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 240 + tokensPerFill: 240 + tokensPerFill: 1440 + type: 'limited' + /registry/transfer-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v2/transfer-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' svs: sv: cometbft: @@ -416,6 +551,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -490,6 +626,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -564,6 +701,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -638,6 +776,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -712,6 +851,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -786,6 +926,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -860,6 +1001,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -934,6 +1076,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1008,6 +1151,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1082,6 +1226,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1156,6 +1301,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1230,6 +1376,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1304,6 +1451,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1378,6 +1526,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1452,6 +1601,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1526,6 +1676,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1600,6 +1751,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1677,6 +1829,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1721,6 +1874,7 @@ synchronizerMigration: spliceRoot: 'splice' version: 'local' frozenMigrationId: 0 + splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/deployment/scratchnetd/config.resolved.yaml b/cluster/deployment/scratchnetd/config.resolved.yaml index f6144013ce..aa12600118 100644 --- a/cluster/deployment/scratchnetd/config.resolved.yaml +++ b/cluster/deployment/scratchnetd/config.resolved.yaml @@ -9,20 +9,17 @@ cloudArmor: throttleAcrossAllEndpointsAllIps: maxRequestsBeforeHttp429: 0 withinIntervalSeconds: 60 + tokenRegistry: + pathPrefix: '/registry' + throttleAcrossAllEndpointsAllIps: + maxRequestsBeforeHttp429: 200 + withinIntervalSeconds: 60 cluster: - hyperdiskSupport: - enabled: true - enabledForInfra: true nodePools: - additionalApps: - - maxNodes: 20 - minNodes: 0 - nodeType: 'n4-standard-16' - zones: '*' apps: - maxNodes: 20 + maxNodes: 40 minNodes: 0 - nodeType: 'n4d-standard-16' + nodeType: 'c3d-standard-8' zones: '*' infra: maxNodes: 3 @@ -33,6 +30,7 @@ infra: proxyForIstioHttp: false istio: enableIngressAccessLogging: true + enablePublicTokenRegistry: true sequencerFlowControl: initialConnectionWindowSize: 52428800 initialStreamWindowSize: 524288 @@ -46,7 +44,7 @@ monitoring: alerts: acsCommitments: checkpointDelay: - seconds: 1200 + seconds: 2400 completedDelay: seconds: 3600 computeDuration: @@ -71,7 +69,7 @@ monitoring: deployment: pendingPeriodMinutes: 5 dsoMissedConfirmations: - threshold: 0.01 + threshold: 0 windowMinutes: 10 gcpQuotas: excludedApproachingMetrics: [] @@ -85,6 +83,10 @@ monitoring: - 'cloudkms.googleapis.com/external_kms_requests' retestWindowSeconds: 600 rollingWindowSeconds: 600 + globalSynchronizerHealth: + discardedConfirmationRequestsThreshold: 0.2 + failedConfirmationRequestsThreshold: 0.1 + tpsDropThreshold: 0.5 ingestion: thresholdEntriesPerBatch: 80 loadTester: @@ -108,6 +110,10 @@ monitoring: sequencerRateLimits: circuitBreakerStateThreshold: 0.5 rejectionRateThreshold: 0 + spliceRateLimits: + excludedLimiters: [] + rejectionCountThreshold: 10 + usageThreshold: 0.8 trafficBasedRewards: featuredAppRightsLimit: 10000 verdictIngestionBatchSizePendingPeriodMinutes: 60 @@ -119,7 +125,7 @@ monitoring: walletSweep: tolerance: 1.5 enableNoDataAlerts: false - loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" + loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\" AND\n -jsonPayload.message=~\"(?i)page(_|-)?token=[^, ]+\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" muteTimeIntervals: [] enableGrafanaServiceAccountToken: true multiValidator: @@ -212,11 +218,14 @@ sv: name: 'status' type: 'unlimited' /api/scan/v0/acs: - clientIp: true fillInterval: '60s' - maxTokens: 10 + maxTokens: 500 name: 'acs' - tokensPerFill: 5 + perIpLimits: + fillInterval: '60s' + maxTokens: 10 + tokensPerFill: 5 + tokensPerFill: 500 type: 'limited' /api/scan/v0/active-synchronizer-serial: name: 'activeSynchronizerSerial' @@ -281,6 +290,9 @@ sv: /api/scan/v0/history/bulk/acs: name: 'listBulkAcsSnapshotObjects' type: 'unlimited' + /api/scan/v0/history/bulk/checksums: + name: 'getBulkHistoryChecksums' + type: 'unlimited' /api/scan/v0/history/bulk/updates: name: 'listBulkUpdateHistoryObjects' type: 'unlimited' @@ -320,9 +332,6 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' - /api/scan/v0/transactions: - name: 'transactions' - type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' @@ -359,12 +368,138 @@ sv: /api/scan/v1/updates: name: 'v1-updates' type: 'banned' + /api/scan/v2/holdings: + name: 'holdings' + type: 'unlimited' + /api/scan/v2/state: + name: 'state' + type: 'unlimited' /api/scan/v2/updates: name: 'v2-updates' type: 'unlimited' /api/scan/version: name: 'version' type: 'unlimited' + /registry/allocation-instruction/v1/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation/v2/settlement-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-settlement-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/info: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-info' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/instruments: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-instruments' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1/transfer-factory: + fillInterval: '60s' + maxTokens: 1440 + name: 'registry-transfer-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 240 + tokensPerFill: 240 + tokensPerFill: 1440 + type: 'limited' + /registry/transfer-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v2/transfer-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' svs: sv: cometbft: @@ -416,6 +551,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -490,6 +626,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -564,6 +701,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -638,6 +776,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -712,6 +851,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -786,6 +926,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -860,6 +1001,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -934,6 +1076,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1008,6 +1151,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1082,6 +1226,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1156,6 +1301,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1230,6 +1376,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1304,6 +1451,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1378,6 +1526,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1452,6 +1601,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1526,6 +1676,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1600,6 +1751,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1677,6 +1829,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1721,6 +1874,7 @@ synchronizerMigration: spliceRoot: 'splice' version: 'local' frozenMigrationId: 0 + splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/deployment/scratchnete/config.resolved.yaml b/cluster/deployment/scratchnete/config.resolved.yaml index f6144013ce..aa12600118 100644 --- a/cluster/deployment/scratchnete/config.resolved.yaml +++ b/cluster/deployment/scratchnete/config.resolved.yaml @@ -9,20 +9,17 @@ cloudArmor: throttleAcrossAllEndpointsAllIps: maxRequestsBeforeHttp429: 0 withinIntervalSeconds: 60 + tokenRegistry: + pathPrefix: '/registry' + throttleAcrossAllEndpointsAllIps: + maxRequestsBeforeHttp429: 200 + withinIntervalSeconds: 60 cluster: - hyperdiskSupport: - enabled: true - enabledForInfra: true nodePools: - additionalApps: - - maxNodes: 20 - minNodes: 0 - nodeType: 'n4-standard-16' - zones: '*' apps: - maxNodes: 20 + maxNodes: 40 minNodes: 0 - nodeType: 'n4d-standard-16' + nodeType: 'c3d-standard-8' zones: '*' infra: maxNodes: 3 @@ -33,6 +30,7 @@ infra: proxyForIstioHttp: false istio: enableIngressAccessLogging: true + enablePublicTokenRegistry: true sequencerFlowControl: initialConnectionWindowSize: 52428800 initialStreamWindowSize: 524288 @@ -46,7 +44,7 @@ monitoring: alerts: acsCommitments: checkpointDelay: - seconds: 1200 + seconds: 2400 completedDelay: seconds: 3600 computeDuration: @@ -71,7 +69,7 @@ monitoring: deployment: pendingPeriodMinutes: 5 dsoMissedConfirmations: - threshold: 0.01 + threshold: 0 windowMinutes: 10 gcpQuotas: excludedApproachingMetrics: [] @@ -85,6 +83,10 @@ monitoring: - 'cloudkms.googleapis.com/external_kms_requests' retestWindowSeconds: 600 rollingWindowSeconds: 600 + globalSynchronizerHealth: + discardedConfirmationRequestsThreshold: 0.2 + failedConfirmationRequestsThreshold: 0.1 + tpsDropThreshold: 0.5 ingestion: thresholdEntriesPerBatch: 80 loadTester: @@ -108,6 +110,10 @@ monitoring: sequencerRateLimits: circuitBreakerStateThreshold: 0.5 rejectionRateThreshold: 0 + spliceRateLimits: + excludedLimiters: [] + rejectionCountThreshold: 10 + usageThreshold: 0.8 trafficBasedRewards: featuredAppRightsLimit: 10000 verdictIngestionBatchSizePendingPeriodMinutes: 60 @@ -119,7 +125,7 @@ monitoring: walletSweep: tolerance: 1.5 enableNoDataAlerts: false - loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" + loggedSecretsFilter: "(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\" AND\n -jsonPayload.message=~\"(?i)page(_|-)?token=[^, ]+\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n" muteTimeIntervals: [] enableGrafanaServiceAccountToken: true multiValidator: @@ -212,11 +218,14 @@ sv: name: 'status' type: 'unlimited' /api/scan/v0/acs: - clientIp: true fillInterval: '60s' - maxTokens: 10 + maxTokens: 500 name: 'acs' - tokensPerFill: 5 + perIpLimits: + fillInterval: '60s' + maxTokens: 10 + tokensPerFill: 5 + tokensPerFill: 500 type: 'limited' /api/scan/v0/active-synchronizer-serial: name: 'activeSynchronizerSerial' @@ -281,6 +290,9 @@ sv: /api/scan/v0/history/bulk/acs: name: 'listBulkAcsSnapshotObjects' type: 'unlimited' + /api/scan/v0/history/bulk/checksums: + name: 'getBulkHistoryChecksums' + type: 'unlimited' /api/scan/v0/history/bulk/updates: name: 'listBulkUpdateHistoryObjects' type: 'unlimited' @@ -320,9 +332,6 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' - /api/scan/v0/transactions: - name: 'transactions' - type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' @@ -359,12 +368,138 @@ sv: /api/scan/v1/updates: name: 'v1-updates' type: 'banned' + /api/scan/v2/holdings: + name: 'holdings' + type: 'unlimited' + /api/scan/v2/state: + name: 'state' + type: 'unlimited' /api/scan/v2/updates: name: 'v2-updates' type: 'unlimited' /api/scan/version: name: 'version' type: 'unlimited' + /registry/allocation-instruction/v1/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation-instruction/v2/allocation-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocation-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocation/v2/settlement-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-settlement-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/allocations/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-allocations-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/info: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-info' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/metadata/v1/instruments: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-metadata-instruments' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v1/transfer-factory: + fillInterval: '60s' + maxTokens: 1440 + name: 'registry-transfer-factory' + perIpLimits: + fillInterval: '60s' + maxTokens: 240 + tokensPerFill: 240 + tokensPerFill: 1440 + type: 'limited' + /registry/transfer-instruction/v2: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-instruction-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' + /registry/transfer-instruction/v2/transfer-factory: + fillInterval: '60s' + maxTokens: 720 + name: 'registry-transfer-factory-v2' + perIpLimits: + fillInterval: '60s' + maxTokens: 120 + tokensPerFill: 120 + tokensPerFill: 720 + type: 'limited' svs: sv: cometbft: @@ -416,6 +551,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -490,6 +626,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -564,6 +701,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -638,6 +776,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -712,6 +851,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -786,6 +926,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -860,6 +1001,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -934,6 +1076,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1008,6 +1151,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1082,6 +1226,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1156,6 +1301,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1230,6 +1376,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1304,6 +1451,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1378,6 +1526,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1452,6 +1601,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1526,6 +1676,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1600,6 +1751,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1677,6 +1829,7 @@ svs: cpu: '2' memory: '2Gi' sequencer: + enableAntiAffinity: false resources: limits: cpu: '3' @@ -1721,6 +1874,7 @@ synchronizerMigration: spliceRoot: 'splice' version: 'local' frozenMigrationId: 0 + splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/expected/canton-network/expected.json b/cluster/expected/canton-network/expected.json index 14b19d1619..664302dd7f 100644 --- a/cluster/expected/canton-network/expected.json +++ b/cluster/expected/canton-network/expected.json @@ -1,70 +1,4 @@ [ - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "postgresPassword": "" - } - }, - "kind": "Secret", - "metadata": { - "name": "cn-apps-pg-secrets", - "namespace": "sv-1" - }, - "type": "Opaque" - }, - "name": "cn-app-sv-1-cn-apps-pg-secrets", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "json-credentials": "eyJidWNrZXROYW1lIjoiZGF0YS1leHBvcnQtYnVja2V0LW5hbWUiLCJzZWNyZXROYW1lIjoiZGF0YS1leHBvcnQtYnVja2V0LXNhLWtleS1zZWNyZXQiLCJqc29uQ3JlZGVudGlhbHMiOiJkYXRhLWV4cG9ydC1idWNrZXQtc2Eta2V5LXNlY3JldC1jcmVkcyJ9" - } - }, - "kind": "Secret", - "metadata": { - "name": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps", - "namespace": "sv-1" - }, - "type": "Opaque" - }, - "name": "cn-app-sv-1-cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "json-credentials": "eyJwcm9qZWN0SWQiOiJkYS1jbi1zaGFyZWQiLCJidWNrZXROYW1lIjoidG9wb2xvZ3ktc25hcHNob3QtYnVja2V0LW5hbWUiLCJzZWNyZXROYW1lIjoiZ2NwLXRvcG9sb2d5LXNuYXBzaG90LWJ1Y2tldC1zYS1rZXktc2VjcmV0IiwianNvbkNyZWRlbnRpYWxzIjoidG9wb2xvZ3ktc25hcHNob3QtYnVja2V0LXNhLWtleS1zZWNyZXQtY3JlZHMiLCJidWNrZXRTYUtleVNlY3JldCI6ImdjcC10b3BvbG9neS1zbmFwc2hvdC1idWNrZXQtc2Eta2V5LWV4YW1wbGUiLCJidWNrZXRTYUlhbUFjY291bnQiOiJkYS1jbi1leGFtcGxldEBkYS1jbi1zaGFyZWQuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20ifQ==" - } - }, - "kind": "Secret", - "metadata": { - "name": "cn-gcp-bucket-da-cn-shared-cn-topology-snapshots", - "namespace": "sv-1" - }, - "type": "Opaque" - }, - "name": "cn-app-sv-1-cn-gcp-bucket-da-cn-shared-cn-topology-snapshots", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, { "custom": true, "id": "", @@ -87,73 +21,6 @@ "provider": "", "type": "kubernetes:core/v1:Secret" }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "postgresPassword": "" - } - }, - "kind": "Secret", - "metadata": { - "name": "cn-apps-pg-secrets", - "namespace": "sv-da-1" - }, - "type": "Opaque" - }, - "name": "cn-app-sv-da-1-cn-apps-pg-secrets", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "json-credentials": "eyJidWNrZXROYW1lIjoiZGF0YS1leHBvcnQtYnVja2V0LW5hbWUiLCJzZWNyZXROYW1lIjoiZGF0YS1leHBvcnQtYnVja2V0LXNhLWtleS1zZWNyZXQiLCJqc29uQ3JlZGVudGlhbHMiOiJkYXRhLWV4cG9ydC1idWNrZXQtc2Eta2V5LXNlY3JldC1jcmVkcyJ9" - } - }, - "kind": "Secret", - "metadata": { - "name": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps", - "namespace": "sv-da-1" - }, - "type": "Opaque" - }, - "name": "cn-app-sv-da-1-cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "private": "c3ZkYTEtbW9jay1pZC1wcml2YXRlLWtleQ==", - "public": "c3ZkYTEtbW9jay1pZC1wdWJsaWMta2V5" - } - }, - "kind": "Secret", - "metadata": { - "name": "cn-app-sv-key", - "namespace": "sv-da-1" - }, - "type": "Opaque" - }, - "name": "cn-app-sv-da-1-key", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, { "custom": true, "id": "", @@ -183,7 +50,7 @@ } }, "name": "docs-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-docs-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-docs-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -199,7 +66,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -281,4132 +148,235 @@ "type": "kubernetes:core/v1:Namespace" }, { - "custom": false, + "custom": true, "id": "", "inputs": { - "auth0Client": {}, - "decentralizedSynchronizerUpgradeConfig": { - "active": { - "id": 9, - "releaseReference": { - "deploymentDir": "cluster/deployment", - "gitReference": "refs/heads/release-line-0.3.20", - "privateConfigsDir": "cluster/configs/configs-private", - "publicConfigsDir": "cluster/configs/configs", - "pulumiBaseDir": "splice/cluster/pulumi", - "pulumiStacksDir": "cluster/stacks/prod", - "repoUrl": "https://github.com/canton-network/splice", - "spliceRoot": "splice" - }, - "sequencer": { - "dedicatedBftSequencerDb": true, - "enableBftSequencer": true - }, - "version": { - "type": "remote", - "version": "0.3.20" - } - }, - "additionalLegacy": [ - { - "id": 7, - "releaseReference": { - "deploymentDir": "cluster/deployment", - "gitReference": "refs/heads/release-line-0.3.20", - "privateConfigsDir": "cluster/configs/configs-private", - "publicConfigsDir": "cluster/configs/configs", - "pulumiBaseDir": "splice/cluster/pulumi", - "pulumiStacksDir": "cluster/stacks/prod", - "repoUrl": "https://github.com/canton-network/splice", - "spliceRoot": "splice" - }, - "sequencer": { - "dedicatedBftSequencerDb": true, - "enableBftSequencer": false - }, - "version": { - "type": "remote", - "version": "0.3.20" - } - } - ], - "archived": [ - { - "id": 5, - "releaseReference": { - "deploymentDir": "cluster/deployment", - "gitReference": "981227e62011e386cca3cdb5f00b472cf05a12c4", - "pulumiBaseDir": "splice/cluster/pulumi", - "pulumiStacksDir": "cluster/stacks/prod", - "repoUrl": "https://github.com/canton-network/splice", - "spliceRoot": "splice" - }, - "sequencer": { - "dedicatedBftSequencerDb": true, - "enableBftSequencer": false - }, - "version": { - "type": "remote", - "version": "0.3.18" - } - }, - { - "id": 6, - "releaseReference": { - "deploymentDir": "cluster/deployment", - "gitReference": "refs/heads/release-line-0.3.19", - "pulumiBaseDir": "splice/cluster/pulumi", - "pulumiStacksDir": "cluster/stacks/prod", - "repoUrl": "https://github.com/canton-network/splice", - "spliceRoot": "splice" - }, - "sequencer": { - "dedicatedBftSequencerDb": true, - "enableBftSequencer": false - }, - "version": { - "type": "remote", - "version": "0.3.19" - } - } - ], - "frozenMigrationId": 2, - "legacy": { - "id": 8, - "releaseReference": { - "deploymentDir": "cluster/deployment", - "gitReference": "refs/heads/release-line-0.3.20", - "privateConfigsDir": "cluster/configs/configs-private", - "publicConfigsDir": "cluster/configs/configs", - "pulumiBaseDir": "splice/cluster/pulumi", - "pulumiStacksDir": "cluster/stacks/prod", - "repoUrl": "https://github.com/canton-network/splice", - "spliceRoot": "splice" - }, - "sequencer": { - "dedicatedBftSequencerDb": true, - "enableBftSequencer": false - }, - "version": { - "type": "remote", - "version": "0.3.20" - } - }, - "upgrade": { - "id": 10, - "releaseReference": { - "deploymentDir": "cluster/deployment", - "gitReference": "refs/heads/release-line-0.3.21", - "privateConfigsDir": "cluster/configs/configs-private", - "publicConfigsDir": "cluster/configs/configs", - "pulumiBaseDir": "splice/cluster/pulumi", - "pulumiStacksDir": "cluster/stacks/prod", - "repoUrl": "https://github.com/canton-network/splice", - "spliceRoot": "splice" - }, - "sequencer": { - "dedicatedBftSequencerDb": true, - "enableBftSequencer": true - }, - "version": { - "type": "remote", - "version": "0.3.21" - } - } - }, - "disableOnboardingParticipantPromotionDelay": false, - "expectedValidatorOnboardings": [ - { - "expiresIn": "24h", - "name": "splitwell2", - "secret": "splitwellsecret2" - }, - { - "expiresIn": "24h", - "name": "validator12", - "secret": "validator1secret2" - } - ], - "identitiesBackupLocation": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "bucket": { - "bucketName": "da-cn-data-dumps", - "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", - "projectId": "da-cn-devnet", - "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" - } - } - }, - "isDevNet": false, - "periodicBackupConfig": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "backupInterval": "10m", - "location": { - "bucket": { - "bucketName": "da-cn-data-dumps", - "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", - "projectId": "da-cn-devnet", - "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" - } - } - } - }, - "splitPostgresInstances": true, - "topupConfig": { - "minTopupInterval": "1m", - "targetThroughput": 0 - } + "enableServerSideApply": "true" }, - "name": "dso", + "name": "k8s-imgpull-docs-default", "provider": "", - "type": "canton:network:dso" + "type": "pulumi:providers:kubernetes" }, { "custom": true, "id": "", "inputs": { - "enableServerSideApply": "true" + "datasetId": "mock_da2_scan", + "deleteContentsOnDestroy": true, + "friendlyName": "mock_da2_scan Dataset", + "labels": { + "cluster": "mock", + "datastream_id": "legacy" + }, + "location": "europe-west6" }, - "name": "k8s-imgpull-docs-default", + "name": "mock_da2_scan", "provider": "", - "type": "pulumi:providers:kubernetes" + "type": "gcp:bigquery/dataset:Dataset" }, { "custom": true, "id": "", "inputs": { - "enableServerSideApply": "true" + "create": "'SPLICE_ROOT/cluster/pulumi/canton-network/bigquery-cloudsql.sh' create-pub-rep-slot \\\n --private-network-project=\"test-project\" \\\n --compute-region=\"europe-west6\" \\\n --service-account-email=\"undefined\" \\\n --schema-name=\"scan_sv_1\" \\\n --tables-to-replicate-joined=\"update_history_creates, update_history_exercises, scan_verdict_store, scan_verdict_transaction_view_store, app_activity_record_store\" \\\n --postgres-user-name=\"cnadmin\" \\\n --publication-name=\"update_history_datastream_pub\" \\\n --replication-slot-name=\"update_history_datastream_r_slot\" \\\n --replicator-user-name=\"bqdatastream\" \\\n --postgres-instance-name=\"undefined\" \\\n --scan-app-database-name=\"scan_sv_1\" \\\n --flyway-migration-to-wait-for=\"V068__app_activity_record_meta.sql\" \\\n ", + "delete": "'SPLICE_ROOT/cluster/pulumi/canton-network/bigquery-cloudsql.sh' delete-pub-rep-slot \\\n --private-network-project=\"test-project\" \\\n --compute-region=\"europe-west6\" \\\n --service-account-email=\"undefined\" \\\n --schema-name=\"scan_sv_1\" \\\n --tables-to-replicate-joined=\"update_history_creates, update_history_exercises, scan_verdict_store, scan_verdict_transaction_view_store, app_activity_record_store\" \\\n --postgres-user-name=\"cnadmin\" \\\n --publication-name=\"update_history_datastream_pub\" \\\n --replication-slot-name=\"update_history_datastream_r_slot\" \\\n --replicator-user-name=\"bqdatastream\" \\\n --postgres-instance-name=\"undefined\" \\\n --scan-app-database-name=\"scan_sv_1\" \\\n --flyway-migration-to-wait-for=\"V068__app_activity_record_meta.sql\" \\\n " }, - "name": "k8s-imgpull-sv-1-default", + "name": "sv-1-bqdatastream-pub-replicate-slots", "provider": "", - "type": "pulumi:providers:kubernetes" + "type": "command:local:Command" + }, + { + "custom": true, + "id": "sv-1-cn-apps-pg-7ca4614", + "inputs": {}, + "name": "sv-1-cn-apps-pg", + "provider": "", + "type": "gcp:sql/databaseInstance:DatabaseInstance" }, { "custom": true, "id": "", "inputs": { - "enableServerSideApply": "true" + "allows": [ + { + "ports": [ + "5432" + ], + "protocol": "tcp" + } + ], + "destinationRanges": [ + null + ], + "direction": "INGRESS", + "name": "sv-1-datastream-to-nat", + "network": "default", + "priority": 42, + "sourceRanges": [ + "1.2.3.16/29" + ] }, - "name": "k8s-imgpull-sv-da-1-default", + "name": "sv-1-datastream-to-nat", "provider": "", - "type": "pulumi:providers:kubernetes" + "type": "gcp:compute/firewall:Firewall" }, { "custom": true, "id": "", "inputs": { - "apiVersion": "networking.istio.io/v1alpha3", - "kind": "VirtualService", + "bootDisk": { + "initializeParams": { + "image": "debian-cloud/debian-12" + } + }, + "labels": { + "cluster": "mock" + }, + "machineType": "e2-micro", "metadata": { - "name": "cometbft-loopback", - "namespace": "sv-1" + "enable-osconfig": "TRUE", + "enable-oslogin": "true", + "startup-script": "#! /bin/bash\n\nexport DB_ADDR=undefined\nexport DB_PORT=5432\n\n# Enable the VM to receive packets whose destinations do\n# not match any running process local to the VM\necho 1 > /proc/sys/net/ipv4/ip_forward\n\n# Ask the Metadata server for the IP address of the VM nic0\n# network interface:\nmd_url_prefix=\"http://169.254.169.254/computeMetadata/v1/instance\"\nvm_nic_ip=\"$(curl -H \"Metadata-Flavor: Google\" $md_url_prefix/network-interfaces/0/ip)\"\n\n# Clear any existing iptables NAT table entries (all chains):\niptables -t nat -F\n\n# Create a NAT table entry in the prerouting chain, matching\n# any packets with destination database port, changing the destination\n# IP address of the packet to the SQL instance IP address:\niptables -t nat -A PREROUTING \\\n -p tcp --dport $DB_PORT \\\n -j DNAT \\\n --to-destination $DB_ADDR\n\n# Create a NAT table entry in the postrouting chain, matching\n# any packets with destination database port, changing the source IP\n# address of the packet to the NAT VM's primary internal IPv4 address:\niptables -t nat -A POSTROUTING \\\n -p tcp --dport $DB_PORT \\\n -j SNAT \\\n --to-source $vm_nic_ip\n\n# Save iptables configuration:\niptables-save\n" }, - "spec": { - "exportTo": [ - "." - ], - "gateways": [ - "mesh" - ], - "hosts": [ - "mock.global.canton.network.digitalasset.com" - ], - "tcp": [ - { - "match": [ - { - "gateways": [ - "mesh" - ] - } - ], - "route": [ - { - "destination": { - "host": "istio-ingress-cometbft.cluster-ingress.svc.cluster.local" - } - } - ] - } - ] - } + "networkInterfaces": [ + { + "accessConfigs": [ + {} + ], + "network": "default" + } + ], + "zone": "europe-west6-a" }, - "name": "loopback-cometbft-sv-1", + "name": "sv-1-nat-vm", "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" + "type": "gcp:compute/instance:Instance" }, { "custom": true, "id": "", "inputs": { - "apiVersion": "networking.istio.io/v1alpha3", - "kind": "VirtualService", - "metadata": { - "name": "cometbft-loopback", - "namespace": "sv-da-1" + "bigqueryProfile": {}, + "connectionProfileId": "sv-1-scan-bq-cxn", + "displayName": "sv-1-scan-bq-cxn", + "labels": { + "cluster": "mock" }, - "spec": { - "exportTo": [ - "." - ], - "gateways": [ - "mesh" - ], - "hosts": [ - "mock.global.canton.network.digitalasset.com" - ], - "tcp": [ - { - "match": [ - { - "gateways": [ - "mesh" - ] - } - ], - "route": [ - { - "destination": { - "host": "istio-ingress-cometbft.cluster-ingress.svc.cluster.local" - } - } - ] - } - ] - } + "location": "europe-west6" }, - "name": "loopback-cometbft-sv-da-1", + "name": "sv-1-scan-bq-cxn", "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" + "type": "gcp:datastream/connectionProfile:ConnectionProfile" }, { "custom": true, "id": "", "inputs": { - "apiVersion": "networking.istio.io/v1alpha3", - "kind": "ServiceEntry", - "metadata": { - "name": "loopback", - "namespace": "sv-1" - }, - "spec": { - "exportTo": [ - "." - ], - "hosts": [ - "mock.global.canton.network.digitalasset.com" - ], - "ports": [ - { - "name": "http-port", - "number": 80, - "protocol": "HTTP" - }, - { - "name": "tls", - "number": 443, - "protocol": "TLS" - }, - { - "name": "grpc-domain", - "number": 5008, - "protocol": "GRPC" - }, - { - "name": "cometbft-0-0-p2p", - "number": 26006, - "protocol": "TCP" - }, - { - "name": "cometbft-0-1-p2p", - "number": 26016, - "protocol": "TCP" - }, - { - "name": "cometbft-0-2-p2p", - "number": 26026, - "protocol": "TCP" - }, - { - "name": "cometbft-1-0-p2p", - "number": 26106, - "protocol": "TCP" - }, - { - "name": "cometbft-1-1-p2p", - "number": 26116, - "protocol": "TCP" - }, - { - "name": "cometbft-1-2-p2p", - "number": 26126, - "protocol": "TCP" - }, - { - "name": "cometbft-2-0-p2p", - "number": 26206, - "protocol": "TCP" - }, - { - "name": "cometbft-2-1-p2p", - "number": 26216, - "protocol": "TCP" - }, - { - "name": "cometbft-2-2-p2p", - "number": 26226, - "protocol": "TCP" - }, - { - "name": "cometbft-3-0-p2p", - "number": 26306, - "protocol": "TCP" - }, - { - "name": "cometbft-3-1-p2p", - "number": 26316, - "protocol": "TCP" - }, - { - "name": "cometbft-3-2-p2p", - "number": 26326, - "protocol": "TCP" - }, - { - "name": "cometbft-4-0-p2p", - "number": 26406, - "protocol": "TCP" - }, - { - "name": "cometbft-4-1-p2p", - "number": 26416, - "protocol": "TCP" - }, - { - "name": "cometbft-4-2-p2p", - "number": 26426, - "protocol": "TCP" - }, - { - "name": "cometbft-5-0-p2p", - "number": 26506, - "protocol": "TCP" - }, - { - "name": "cometbft-5-1-p2p", - "number": 26516, - "protocol": "TCP" - }, - { - "name": "cometbft-5-2-p2p", - "number": 26526, - "protocol": "TCP" - }, - { - "name": "cometbft-6-0-p2p", - "number": 26606, - "protocol": "TCP" - }, - { - "name": "cometbft-6-1-p2p", - "number": 26616, - "protocol": "TCP" - }, - { - "name": "cometbft-6-2-p2p", - "number": 26626, - "protocol": "TCP" - }, - { - "name": "cometbft-7-0-p2p", - "number": 26706, - "protocol": "TCP" - }, - { - "name": "cometbft-7-1-p2p", - "number": 26716, - "protocol": "TCP" - }, - { - "name": "cometbft-7-2-p2p", - "number": 26726, - "protocol": "TCP" - }, - { - "name": "cometbft-8-0-p2p", - "number": 26806, - "protocol": "TCP" - }, - { - "name": "cometbft-8-1-p2p", - "number": 26816, - "protocol": "TCP" - }, - { - "name": "cometbft-8-2-p2p", - "number": 26826, - "protocol": "TCP" - }, - { - "name": "cometbft-9-0-p2p", - "number": 26906, - "protocol": "TCP" - }, - { - "name": "cometbft-9-1-p2p", - "number": 26916, - "protocol": "TCP" - }, - { - "name": "cometbft-9-2-p2p", - "number": 26926, - "protocol": "TCP" - } - ], - "resolution": "DNS" - } - }, - "name": "loopback-service-entry-sv-1", - "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:ServiceEntry" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "networking.istio.io/v1alpha3", - "kind": "ServiceEntry", - "metadata": { - "name": "loopback", - "namespace": "sv-da-1" - }, - "spec": { - "exportTo": [ - "." - ], - "hosts": [ - "mock.global.canton.network.digitalasset.com" - ], - "ports": [ - { - "name": "http-port", - "number": 80, - "protocol": "HTTP" - }, - { - "name": "tls", - "number": 443, - "protocol": "TLS" - }, - { - "name": "grpc-domain", - "number": 5008, - "protocol": "GRPC" - }, - { - "name": "cometbft-0-0-p2p", - "number": 26006, - "protocol": "TCP" - }, - { - "name": "cometbft-0-1-p2p", - "number": 26016, - "protocol": "TCP" - }, - { - "name": "cometbft-0-2-p2p", - "number": 26026, - "protocol": "TCP" - }, - { - "name": "cometbft-1-0-p2p", - "number": 26106, - "protocol": "TCP" - }, - { - "name": "cometbft-1-1-p2p", - "number": 26116, - "protocol": "TCP" - }, - { - "name": "cometbft-1-2-p2p", - "number": 26126, - "protocol": "TCP" - }, - { - "name": "cometbft-2-0-p2p", - "number": 26206, - "protocol": "TCP" - }, - { - "name": "cometbft-2-1-p2p", - "number": 26216, - "protocol": "TCP" - }, - { - "name": "cometbft-2-2-p2p", - "number": 26226, - "protocol": "TCP" - }, - { - "name": "cometbft-3-0-p2p", - "number": 26306, - "protocol": "TCP" - }, - { - "name": "cometbft-3-1-p2p", - "number": 26316, - "protocol": "TCP" - }, - { - "name": "cometbft-3-2-p2p", - "number": 26326, - "protocol": "TCP" - }, - { - "name": "cometbft-4-0-p2p", - "number": 26406, - "protocol": "TCP" - }, - { - "name": "cometbft-4-1-p2p", - "number": 26416, - "protocol": "TCP" - }, - { - "name": "cometbft-4-2-p2p", - "number": 26426, - "protocol": "TCP" - }, - { - "name": "cometbft-5-0-p2p", - "number": 26506, - "protocol": "TCP" - }, - { - "name": "cometbft-5-1-p2p", - "number": 26516, - "protocol": "TCP" - }, - { - "name": "cometbft-5-2-p2p", - "number": 26526, - "protocol": "TCP" - }, - { - "name": "cometbft-6-0-p2p", - "number": 26606, - "protocol": "TCP" - }, - { - "name": "cometbft-6-1-p2p", - "number": 26616, - "protocol": "TCP" - }, - { - "name": "cometbft-6-2-p2p", - "number": 26626, - "protocol": "TCP" - }, - { - "name": "cometbft-7-0-p2p", - "number": 26706, - "protocol": "TCP" - }, - { - "name": "cometbft-7-1-p2p", - "number": 26716, - "protocol": "TCP" - }, - { - "name": "cometbft-7-2-p2p", - "number": 26726, - "protocol": "TCP" - }, - { - "name": "cometbft-8-0-p2p", - "number": 26806, - "protocol": "TCP" - }, - { - "name": "cometbft-8-1-p2p", - "number": 26816, - "protocol": "TCP" - }, - { - "name": "cometbft-8-2-p2p", - "number": 26826, - "protocol": "TCP" - }, - { - "name": "cometbft-9-0-p2p", - "number": 26906, - "protocol": "TCP" - }, - { - "name": "cometbft-9-1-p2p", - "number": 26916, - "protocol": "TCP" - }, - { - "name": "cometbft-9-2-p2p", - "number": 26926, - "protocol": "TCP" - } - ], - "resolution": "DNS" - } - }, - "name": "loopback-service-entry-sv-da-1", - "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:ServiceEntry" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "networking.istio.io/v1alpha3", - "kind": "VirtualService", - "metadata": { - "name": "direct-loopback-through-ingress-gateway", - "namespace": "sv-1" - }, - "spec": { - "exportTo": [ - "." - ], - "gateways": [ - "mesh" - ], - "hosts": [ - "mock.global.canton.network.digitalasset.com", - "validator.mock.global.canton.network.digitalasset.com", - "*.validator.mock.global.canton.network.digitalasset.com", - "validator1.mock.global.canton.network.digitalasset.com", - "*.validator1.mock.global.canton.network.digitalasset.com", - "splitwell.mock.global.canton.network.digitalasset.com", - "*.splitwell.mock.global.canton.network.digitalasset.com", - "sv-2.mock.global.canton.network.digitalasset.com", - "*.sv-2.mock.global.canton.network.digitalasset.com", - "sv-1.mock.global.canton.network.digitalasset.com", - "*.sv-1.mock.global.canton.network.digitalasset.com", - "sv.mock.global.canton.network.digitalasset.com", - "*.sv.mock.global.canton.network.digitalasset.com" - ], - "http": [ - { - "match": [ - { - "gateways": [ - "mesh" - ] - } - ], - "route": [ - { - "destination": { - "host": "istio-ingress.cluster-ingress.svc.cluster.local" - } - } - ] - } - ], - "tls": [ - { - "match": [ - { - "gateways": [ - "mesh" - ], - "sniHosts": [ - "mock.global.canton.network.digitalasset.com", - "validator.mock.global.canton.network.digitalasset.com", - "*.validator.mock.global.canton.network.digitalasset.com", - "validator1.mock.global.canton.network.digitalasset.com", - "*.validator1.mock.global.canton.network.digitalasset.com", - "splitwell.mock.global.canton.network.digitalasset.com", - "*.splitwell.mock.global.canton.network.digitalasset.com", - "sv-2.mock.global.canton.network.digitalasset.com", - "*.sv-2.mock.global.canton.network.digitalasset.com", - "sv-1.mock.global.canton.network.digitalasset.com", - "*.sv-1.mock.global.canton.network.digitalasset.com", - "sv.mock.global.canton.network.digitalasset.com", - "*.sv.mock.global.canton.network.digitalasset.com" - ] - } - ], - "route": [ - { - "destination": { - "host": "istio-ingress.cluster-ingress.svc.cluster.local" - } - } - ] - } - ] - } - }, - "name": "loopback-virtual-service-sv-1", - "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "networking.istio.io/v1alpha3", - "kind": "VirtualService", - "metadata": { - "name": "direct-loopback-through-ingress-gateway", - "namespace": "sv-da-1" - }, - "spec": { - "exportTo": [ - "." - ], - "gateways": [ - "mesh" - ], - "hosts": [ - "mock.global.canton.network.digitalasset.com", - "validator.mock.global.canton.network.digitalasset.com", - "*.validator.mock.global.canton.network.digitalasset.com", - "validator1.mock.global.canton.network.digitalasset.com", - "*.validator1.mock.global.canton.network.digitalasset.com", - "splitwell.mock.global.canton.network.digitalasset.com", - "*.splitwell.mock.global.canton.network.digitalasset.com", - "sv-2.mock.global.canton.network.digitalasset.com", - "*.sv-2.mock.global.canton.network.digitalasset.com", - "sv-1.mock.global.canton.network.digitalasset.com", - "*.sv-1.mock.global.canton.network.digitalasset.com", - "sv.mock.global.canton.network.digitalasset.com", - "*.sv.mock.global.canton.network.digitalasset.com" - ], - "http": [ - { - "match": [ - { - "gateways": [ - "mesh" - ] - } - ], - "route": [ - { - "destination": { - "host": "istio-ingress.cluster-ingress.svc.cluster.local" - } - } - ] - } - ], - "tls": [ - { - "match": [ - { - "gateways": [ - "mesh" - ], - "sniHosts": [ - "mock.global.canton.network.digitalasset.com", - "validator.mock.global.canton.network.digitalasset.com", - "*.validator.mock.global.canton.network.digitalasset.com", - "validator1.mock.global.canton.network.digitalasset.com", - "*.validator1.mock.global.canton.network.digitalasset.com", - "splitwell.mock.global.canton.network.digitalasset.com", - "*.splitwell.mock.global.canton.network.digitalasset.com", - "sv-2.mock.global.canton.network.digitalasset.com", - "*.sv-2.mock.global.canton.network.digitalasset.com", - "sv-1.mock.global.canton.network.digitalasset.com", - "*.sv-1.mock.global.canton.network.digitalasset.com", - "sv.mock.global.canton.network.digitalasset.com", - "*.sv.mock.global.canton.network.digitalasset.com" - ] - } - ], - "route": [ - { - "destination": { - "host": "istio-ingress.cluster-ingress.svc.cluster.local" - } - } - ] - } - ] - } - }, - "name": "loopback-virtual-service-sv-da-1", - "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" - }, - { - "custom": true, - "id": "", - "inputs": {}, - "name": "mock-sv-1-bulk-hmac", - "provider": "", - "type": "gcp:storage/hmacKey:HmacKey" - }, - { - "custom": true, - "id": "", - "inputs": { - "bucket": "mock-sv-1-bulk", - "member": "serviceAccount:undefined", - "role": "roles/storage.objectUser" - }, - "name": "mock-sv-1-bulk-sa-role", - "provider": "", - "type": "gcp:storage/bucketIAMMember:BucketIAMMember" - }, - { - "custom": true, - "id": "", - "inputs": { - "accountId": "mock-sv-1-bulk-sa", - "displayName": "Service Account for Bulk-Storage Bucket Read/Write Access" - }, - "name": "mock-sv-1-bulk-sa", - "provider": "", - "type": "gcp:serviceaccount/account:Account" - }, - { - "custom": true, - "id": "", - "inputs": { - "location": "europe-west6", - "name": "mock-sv-1-bulk" - }, - "name": "mock-sv-1-bulk", - "provider": "", - "type": "gcp:storage/bucket:Bucket" - }, - { - "custom": true, - "id": "", - "inputs": {}, - "name": "mock-sv-da-1-bulk-hmac", - "provider": "", - "type": "gcp:storage/hmacKey:HmacKey" - }, - { - "custom": true, - "id": "", - "inputs": { - "bucket": "mock-sv-da-1-bulk", - "member": "serviceAccount:undefined", - "role": "roles/storage.objectUser" - }, - "name": "mock-sv-da-1-bulk-sa-role", - "provider": "", - "type": "gcp:storage/bucketIAMMember:BucketIAMMember" - }, - { - "custom": true, - "id": "", - "inputs": { - "accountId": "mock-sv-da-1-bulk-sa", - "displayName": "Service Account for Bulk-Storage Bucket Read/Write Access" - }, - "name": "mock-sv-da-1-bulk-sa", - "provider": "", - "type": "gcp:serviceaccount/account:Account" - }, - { - "custom": true, - "id": "", - "inputs": { - "location": "europe-west6", - "name": "mock-sv-da-1-bulk" - }, - "name": "mock-sv-da-1-bulk", - "provider": "", - "type": "gcp:storage/bucket:Bucket" - }, - { - "custom": true, - "id": "", - "inputs": { - "datasetId": "mock_da2_scan", - "deleteContentsOnDestroy": true, - "friendlyName": "mock_da2_scan Dataset", - "labels": { - "cluster": "mock" - }, - "location": "europe-west6" - }, - "name": "mock_da2_scan", - "provider": "", - "type": "gcp:bigquery/dataset:Dataset" - }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "secret": "c3BsaXR3ZWxsc2VjcmV0Mg==" - } - }, - "kind": "Secret", - "metadata": { - "name": "splice-app-validator-onboarding-splitwell2", - "namespace": "sv-1" - }, - "type": "Opaque" - }, - "name": "splice-app-sv-1-validator-onboarding-splitwell2", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "secret": "dmFsaWRhdG9yMXNlY3JldDI=" - } - }, - "kind": "Secret", - "metadata": { - "name": "splice-app-validator-onboarding-validator12", - "namespace": "sv-1" - }, - "type": "Opaque" - }, - "name": "splice-app-sv-1-validator-onboarding-validator12", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "private": "c3ZkYTEtbW9jay1jb21ldGJmdC1nb3Zlcm5hbmNlLWtleS1wcml2YXRlLWtleQ==", - "public": "c3ZkYTEtbW9jay1jb21ldGJmdC1nb3Zlcm5hbmNlLWtleS1wdWJsaWMta2V5" - } - }, - "kind": "Secret", - "metadata": { - "name": "splice-app-sv-cometbft-governance-key", - "namespace": "sv-da-1" - }, - "type": "Opaque" - }, - "name": "splice-app-sv-da-1-cometbft-governance-key", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "private": "c3ZkYTEtbW9jay1pZC1wcml2YXRlLWtleQ==", - "public": "c3ZkYTEtbW9jay1pZC1wdWJsaWMta2V5" - } - }, - "kind": "Secret", - "metadata": { - "name": "splice-app-sv-key", - "namespace": "sv-da-1" - }, - "type": "Opaque" - }, - "name": "splice-app-sv-da-1-key", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-sv-ledger-api-auth", - "namespace": "sv-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "audience": "https://canton.network.global", - "client-id": "sv1-sv-client-id", - "client-secret": "***", - "ledger-api-user": "sv1-sv-client-id@clients", - "url": "https://canton-network-dev.us.auth0.com/.well-known/openid-configuration" - } - } - }, - "name": "splice-auth0-secret-sv-1-sv", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-validator-ledger-api-auth", - "namespace": "sv-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "audience": "https://canton.network.global", - "client-id": "sv1-validator-client-id", - "client-secret": "***", - "ledger-api-user": "sv1-validator-client-id@clients", - "url": "https://canton-network-dev.us.auth0.com/.well-known/openid-configuration" - } - } - }, - "name": "splice-auth0-secret-sv-1-validator", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-sv-ledger-api-auth", - "namespace": "sv-da-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "audience": "https://canton.network.global", - "client-id": "sv-da-1-sv-client-id", - "client-secret": "***", - "ledger-api-user": "sv-da-1-sv-client-id@clients", - "url": "https://canton-network-dev.us.auth0.com/.well-known/openid-configuration" - } - } - }, - "name": "splice-auth0-secret-sv-da-1-sv", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-validator-ledger-api-auth", - "namespace": "sv-da-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "audience": "https://canton.network.global", - "client-id": "sv-da-1-validator-client-id", - "client-secret": "***", - "ledger-api-user": "sv-da-1-validator-client-id@clients", - "url": "https://canton-network-dev.us.auth0.com/.well-known/openid-configuration" - } - } - }, - "name": "splice-auth0-secret-sv-da-1-validator", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-cns-ui-auth", - "namespace": "sv-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "client-id": "sv-1-cns-ui-client-id", - "url": "https://canton-network-dev.us.auth0.com" - } - } - }, - "name": "splice-auth0-ui-secret-sv-1-cns", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-sv-ui-auth", - "namespace": "sv-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "client-id": "sv-1-sv-ui-client-id", - "url": "https://canton-network-dev.us.auth0.com" - } - } - }, - "name": "splice-auth0-ui-secret-sv-1-sv", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-wallet-ui-auth", - "namespace": "sv-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "client-id": "sv-1-wallet-ui-client-id", - "url": "https://canton-network-dev.us.auth0.com" - } - } - }, - "name": "splice-auth0-ui-secret-sv-1-wallet", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-cns-ui-auth", - "namespace": "sv-da-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "client-id": "sv-da-1-cns-ui-client-id", - "url": "https://canton-network-dev.us.auth0.com" - } - } - }, - "name": "splice-auth0-ui-secret-sv-da-1-cns", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-sv-ui-auth", - "namespace": "sv-da-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "client-id": "sv-da-1-sv-ui-client-id", - "url": "https://canton-network-dev.us.auth0.com" - } - } - }, - "name": "splice-auth0-ui-secret-sv-da-1-sv", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "splice-app-wallet-ui-auth", - "namespace": "sv-da-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "client-id": "sv-da-1-wallet-ui-client-id", - "url": "https://canton-network-dev.us.auth0.com" - } - } - }, - "name": "splice-auth0-ui-secret-sv-da-1-wallet", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": false, - "id": "", - "inputs": { - "appLabel": "scan-app", - "globalLimits": { - "fillInterval": "60s", - "maxTokens": 2147483647, - "tokensPerFill": 2147483647 - }, - "inboundPort": 5012, - "namespace": "sv-1", - "rateLimits": { - "/api/scan/livez": { - "name": "livez", - "type": "unlimited" - }, - "/api/scan/readyz": { - "name": "readyz", - "type": "unlimited" - }, - "/api/scan/status": { - "name": "status", - "type": "unlimited" - }, - "/api/scan/v0/acs": { - "clientIp": true, - "fillInterval": "60s", - "maxTokens": 10, - "name": "acs", - "tokensPerFill": 5, - "type": "limited" - }, - "/api/scan/v0/active-synchronizer-serial": { - "name": "activeSynchronizerSerial", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/previous-sv-reward-weight": { - "name": "sv-previous-sv-reward-weight", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voterequests": { - "name": "sv-voterequests", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voteresults": { - "name": "sv-voteresults", - "type": "unlimited" - }, - "/api/scan/v0/admin/validator": { - "name": "validator-licenses", - "type": "unlimited" - }, - "/api/scan/v0/amulet-config-for-round": { - "name": "amulet-config-for-round", - "type": "banned" - }, - "/api/scan/v0/amulet-price": { - "name": "amulet-price-votes", - "type": "unlimited" - }, - "/api/scan/v0/amulet-rules": { - "name": "amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/ans-entries": { - "name": "ans-entries", - "type": "unlimited" - }, - "/api/scan/v0/ans-rules": { - "name": "ans-rules", - "type": "unlimited" - }, - "/api/scan/v0/backfilling": { - "name": "backfilling", - "type": "unlimited" - }, - "/api/scan/v0/closed-rounds": { - "name": "closed-rounds", - "type": "unlimited" - }, - "/api/scan/v0/domains": { - "name": "domains", - "type": "unlimited" - }, - "/api/scan/v0/dso": { - "name": "dso-info", - "type": "unlimited" - }, - "/api/scan/v0/dso-party-id": { - "name": "dso-party-id", - "type": "unlimited" - }, - "/api/scan/v0/dso-sequencers": { - "name": "dso-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/events": { - "name": "events", - "type": "unlimited" - }, - "/api/scan/v0/external-party-amulet-rules": { - "name": "external-party-amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/feature-support": { - "name": "feature-support", - "type": "unlimited" - }, - "/api/scan/v0/featured-apps": { - "name": "featured-apps", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/acs": { - "name": "listBulkAcsSnapshotObjects", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/updates": { - "name": "listBulkUpdateHistoryObjects", - "type": "unlimited" - }, - "/api/scan/v0/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v0/internal/reward-accounting-process": { - "name": "reward-accounting-process", - "type": "unlimited" - }, - "/api/scan/v0/lsu": { - "name": "lsu", - "type": "unlimited" - }, - "/api/scan/v0/migrations": { - "name": "migrations-schedule", - "type": "unlimited" - }, - "/api/scan/v0/open-and-issuing-mining-rounds": { - "name": "open-and-issuing-mining-rounds", - "type": "unlimited" - }, - "/api/scan/v0/roll-forward-lsu": { - "name": "rollForwardLsu", - "type": "unlimited" - }, - "/api/scan/v0/scans": { - "name": "scans", - "type": "unlimited" - }, - "/api/scan/v0/splice-instance-names": { - "name": "splice-instance-names", - "type": "unlimited" - }, - "/api/scan/v0/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v0/sv-bft-sequencers": { - "name": "bft-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-bootstrapping-transactions": { - "name": "synchronizer-bootstrapping-transactions", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-identities": { - "name": "synchronizer-identities", - "type": "unlimited" - }, - "/api/scan/v0/transactions": { - "name": "transactions", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command": { - "name": "transfer-command-status", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command-counter": { - "name": "transfer-command-counter", - "type": "unlimited" - }, - "/api/scan/v0/transfer-preapprovals": { - "name": "transfer-preapprovals", - "type": "unlimited" - }, - "/api/scan/v0/unclaimed-development-fund-coupons": { - "name": "unclaimed-development-fund-coupons", - "type": "unlimited" - }, - "/api/scan/v0/updates": { - "name": "v0-updates", - "type": "unlimited" - }, - "/api/scan/v0/validators": { - "name": "validators", - "type": "unlimited" - }, - "/api/scan/v0/voterequest": { - "name": "voterequest", - "type": "unlimited" - }, - "/api/scan/v0/voterequests": { - "name": "voterequests", - "type": "unlimited" - }, - "/api/scan/v1/domains": { - "name": "domains-v1", - "type": "unlimited" - }, - "/api/scan/v1/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v1/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v1/updates": { - "name": "v1-updates", - "type": "banned" - }, - "/api/scan/v2/updates": { - "name": "v2-updates", - "type": "unlimited" - }, - "/api/scan/version": { - "name": "version", - "type": "unlimited" - } - } - }, - "name": "splice-sv-1-scan-app-rate-limit", - "provider": "", - "type": "splice:RateLimit" - }, - { - "custom": false, - "id": "", - "inputs": { - "appLabel": "scan-app", - "globalLimits": { - "fillInterval": "60s", - "maxTokens": 2147483647, - "tokensPerFill": 2147483647 - }, - "inboundPort": 5012, - "namespace": "sv-da-1", - "rateLimits": { - "/api/scan/livez": { - "name": "livez", - "type": "unlimited" - }, - "/api/scan/readyz": { - "name": "readyz", - "type": "unlimited" - }, - "/api/scan/status": { - "name": "status", - "type": "unlimited" - }, - "/api/scan/v0/acs": { - "clientIp": true, - "fillInterval": "60s", - "maxTokens": 10, - "name": "acs", - "tokensPerFill": 5, - "type": "limited" - }, - "/api/scan/v0/active-synchronizer-serial": { - "name": "activeSynchronizerSerial", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/previous-sv-reward-weight": { - "name": "sv-previous-sv-reward-weight", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voterequests": { - "name": "sv-voterequests", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voteresults": { - "name": "sv-voteresults", - "type": "unlimited" - }, - "/api/scan/v0/admin/validator": { - "name": "validator-licenses", - "type": "unlimited" - }, - "/api/scan/v0/amulet-config-for-round": { - "name": "amulet-config-for-round", - "type": "banned" - }, - "/api/scan/v0/amulet-price": { - "name": "amulet-price-votes", - "type": "unlimited" - }, - "/api/scan/v0/amulet-rules": { - "name": "amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/ans-entries": { - "name": "ans-entries", - "type": "unlimited" - }, - "/api/scan/v0/ans-rules": { - "name": "ans-rules", - "type": "unlimited" - }, - "/api/scan/v0/backfilling": { - "name": "backfilling", - "type": "unlimited" - }, - "/api/scan/v0/closed-rounds": { - "name": "closed-rounds", - "type": "unlimited" - }, - "/api/scan/v0/domains": { - "name": "domains", - "type": "unlimited" - }, - "/api/scan/v0/dso": { - "name": "dso-info", - "type": "unlimited" - }, - "/api/scan/v0/dso-party-id": { - "name": "dso-party-id", - "type": "unlimited" - }, - "/api/scan/v0/dso-sequencers": { - "name": "dso-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/events": { - "name": "events", - "type": "unlimited" - }, - "/api/scan/v0/external-party-amulet-rules": { - "name": "external-party-amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/feature-support": { - "name": "feature-support", - "type": "unlimited" - }, - "/api/scan/v0/featured-apps": { - "name": "featured-apps", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/acs": { - "name": "listBulkAcsSnapshotObjects", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/updates": { - "name": "listBulkUpdateHistoryObjects", - "type": "unlimited" - }, - "/api/scan/v0/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v0/internal/reward-accounting-process": { - "name": "reward-accounting-process", - "type": "unlimited" - }, - "/api/scan/v0/lsu": { - "name": "lsu", - "type": "unlimited" - }, - "/api/scan/v0/migrations": { - "name": "migrations-schedule", - "type": "unlimited" - }, - "/api/scan/v0/open-and-issuing-mining-rounds": { - "name": "open-and-issuing-mining-rounds", - "type": "unlimited" - }, - "/api/scan/v0/roll-forward-lsu": { - "name": "rollForwardLsu", - "type": "unlimited" - }, - "/api/scan/v0/scans": { - "name": "scans", - "type": "unlimited" - }, - "/api/scan/v0/splice-instance-names": { - "name": "splice-instance-names", - "type": "unlimited" - }, - "/api/scan/v0/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v0/sv-bft-sequencers": { - "name": "bft-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-bootstrapping-transactions": { - "name": "synchronizer-bootstrapping-transactions", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-identities": { - "name": "synchronizer-identities", - "type": "unlimited" - }, - "/api/scan/v0/transactions": { - "name": "transactions", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command": { - "name": "transfer-command-status", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command-counter": { - "name": "transfer-command-counter", - "type": "unlimited" - }, - "/api/scan/v0/transfer-preapprovals": { - "name": "transfer-preapprovals", - "type": "unlimited" - }, - "/api/scan/v0/unclaimed-development-fund-coupons": { - "name": "unclaimed-development-fund-coupons", - "type": "unlimited" - }, - "/api/scan/v0/updates": { - "name": "v0-updates", - "type": "unlimited" - }, - "/api/scan/v0/validators": { - "name": "validators", - "type": "unlimited" - }, - "/api/scan/v0/voterequest": { - "name": "voterequest", - "type": "unlimited" - }, - "/api/scan/v0/voterequests": { - "name": "voterequests", - "type": "unlimited" - }, - "/api/scan/v1/domains": { - "name": "domains-v1", - "type": "unlimited" - }, - "/api/scan/v1/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v1/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v1/updates": { - "name": "v1-updates", - "type": "banned" - }, - "/api/scan/v2/updates": { - "name": "v2-updates", - "type": "unlimited" - }, - "/api/scan/version": { - "name": "version", - "type": "unlimited" - } - } - }, - "name": "splice-sv-da-1-scan-app-rate-limit", - "provider": "", - "type": "splice:RateLimit" - }, - { - "custom": true, - "id": "", - "inputs": { - "create": "'SPLICE_ROOT/cluster/pulumi/canton-network/bigquery-cloudsql.sh' create-pub-rep-slot \\\n --private-network-project=\"test-project\" \\\n --compute-region=\"europe-west6\" \\\n --service-account-email=\"undefined\" \\\n --schema-name=\"scan_sv_1\" \\\n --tables-to-replicate-joined=\"update_history_creates, update_history_exercises, scan_verdict_store, scan_verdict_transaction_view_store, app_activity_record_store\" \\\n --postgres-user-name=\"cnadmin\" \\\n --publication-name=\"update_history_datastream_pub\" \\\n --replication-slot-name=\"update_history_datastream_r_slot\" \\\n --replicator-user-name=\"bqdatastream\" \\\n --postgres-instance-name=\"undefined\" \\\n --scan-app-database-name=\"scan_sv_1\" \\\n --flyway-migration-to-wait-for=\"V068__app_activity_record_meta.sql\" \\\n ", - "delete": "'SPLICE_ROOT/cluster/pulumi/canton-network/bigquery-cloudsql.sh' delete-pub-rep-slot \\\n --private-network-project=\"test-project\" \\\n --compute-region=\"europe-west6\" \\\n --service-account-email=\"undefined\" \\\n --schema-name=\"scan_sv_1\" \\\n --tables-to-replicate-joined=\"update_history_creates, update_history_exercises, scan_verdict_store, scan_verdict_transaction_view_store, app_activity_record_store\" \\\n --postgres-user-name=\"cnadmin\" \\\n --publication-name=\"update_history_datastream_pub\" \\\n --replication-slot-name=\"update_history_datastream_r_slot\" \\\n --replicator-user-name=\"bqdatastream\" \\\n --postgres-instance-name=\"undefined\" \\\n --scan-app-database-name=\"scan_sv_1\" \\\n --flyway-migration-to-wait-for=\"V068__app_activity_record_meta.sql\" \\\n " - }, - "name": "sv-1-bqdatastream-pub-replicate-slots", - "provider": "", - "type": "command:local:Command" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "accessKey": "", - "secretAccessKey": "" - } - }, - "kind": "Secret", - "metadata": { - "name": "splice-app-bulk-storage-credentials", - "namespace": "sv-1" - }, - "type": "Opaque" - }, - "name": "sv-1-bulk-storage-credentials", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "length": 16, - "overrideSpecial": "_%@", - "special": true - }, - "name": "sv-1-cn-apps-pg-cnadmin-passwd", - "provider": "", - "type": "random:index/randomPassword:RandomPassword" - }, - { - "custom": false, - "id": "", - "inputs": { - "active": true, - "alias": "cn-apps-pg", - "cloudSqlConfig": { - "databaseVersion": "POSTGRES_14", - "enabled": true, - "enterprisePlus": false, - "flags": { - "maintenance_work_mem": "2000000", - "max_wal_size": "20480", - "random_page_cost": "1.1", - "temp_file_limit": "2147483647", - "work_mem": "16384" - }, - "maintenanceWindow": { - "day": 2, - "hour": 8 - }, - "protected": true, - "tier": "apps-pg-override-tier-sv-1" - }, - "defaultUserName": "cnadmin", - "deletionProtection": true, - "instanceName": "cn-apps-pg", - "logicalDecoding": true, - "namespace": { - "logicalName": "sv-1", - "ns": { - "__aliases": [], - "__name": "sv-1", - "__providers": {}, - "__pulumiCustomResource": true, - "__pulumiResource": true, - "__pulumiType": "kubernetes:core/v1:Namespace", - "__transformations": [], - "__version": "4.28.0", - "apiVersion": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "id": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "kind": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "metadata": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "spec": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "status": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "urn": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi." - } - }, - "retainDbResourcesOnDelete": false, - "secretName": "cn-apps-pg-secrets" - }, - "name": "sv-1-cn-apps-pg", - "provider": "", - "type": "canton:cloud:postgres" - }, - { - "custom": true, - "id": "", - "inputs": { - "databaseVersion": "POSTGRES_14", - "deletionProtection": false, - "region": "europe-west6", - "settings": { - "activationPolicy": "ALWAYS", - "backupConfiguration": { - "enabled": true, - "pointInTimeRecoveryEnabled": true - }, - "databaseFlags": [ - { - "name": "random_page_cost", - "value": "1.1" - }, - { - "name": "temp_file_limit", - "value": "2147483647" - }, - { - "name": "max_wal_size", - "value": "20480" - }, - { - "name": "maintenance_work_mem", - "value": "2000000" - }, - { - "name": "work_mem", - "value": "16384" - }, - { - "name": "cloudsql.logical_decoding", - "value": "on" - } - ], - "deletionProtectionEnabled": true, - "edition": "ENTERPRISE", - "insightsConfig": { - "queryInsightsEnabled": true - }, - "ipConfiguration": { - "enablePrivatePathForGoogleCloudServices": true, - "ipv4Enabled": false, - "privateNetwork": "projects/test-project/global/networks/default" - }, - "locationPreference": { - "zone": "europe-west6-a" - }, - "maintenanceWindow": { - "day": 2, - "hour": 8 - }, - "tier": "apps-pg-override-tier-sv-1", - "userLabels": { - "cluster": "mock" - } - } - }, - "name": "sv-1-cn-apps-pg", - "provider": "", - "type": "gcp:sql/databaseInstance:DatabaseInstance" - }, - { - "custom": true, - "id": "", - "inputs": { - "allows": [ - { - "ports": [ - "5432" - ], - "protocol": "tcp" - } - ], - "destinationRanges": [ - null - ], - "direction": "INGRESS", - "name": "sv-1-datastream-to-nat", - "network": "default", - "priority": 42, - "sourceRanges": [ - "1.2.3.16/29" - ] - }, - "name": "sv-1-datastream-to-nat", - "provider": "", - "type": "gcp:compute/firewall:Firewall" - }, - { - "custom": true, - "id": "", - "inputs": { - "name": "cantonnet" - }, - "name": "sv-1-db-cn-apps-pg-cantonnet", - "provider": "", - "type": "gcp:sql/database:Database" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "imagePullSecrets": [ - { - "name": "docker-reg-cred" - } - ], - "kind": "ServiceAccount", - "metadata": { - "name": "default", - "namespace": "sv-1" - } - }, - "name": "sv-1-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-default::undefined_id", - "type": "kubernetes:core/v1:ServiceAccountPatch" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "docker-reg-cred", - "namespace": "sv-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" - } - }, - "type": "kubernetes.io/dockerconfigjson" - }, - "name": "sv-1-docker-reg-cred", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-info", - "compat": "true", - "maxHistory": 10, - "name": "info", - "namespace": "sv-1", - "timeout": 600, - "values": { - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "deploymentDetails": { - "configDigest": { - "allowedIpRanges": { - "type": "md5", - "value": "90eedde4a8599204a45dcb972e212c8b" - }, - "approvedSvIdentities": { - "type": "md5", - "value": "5871224b744b45122540483fddbd550f" - } - }, - "network": "test", - "sv": { - "version": "0.3.20" - }, - "synchronizer": { - "current": { - "chainIdSuffix": "4", - "migrationId": 2, - "synchronizerSerialId": 9, - "version": "0.3.20" - }, - "legacy": { - "chainIdSuffix": "4", - "migrationId": 2, - "synchronizerSerialId": 8, - "version": "0.3.20" - }, - "successor": { - "chainIdSuffix": "4", - "migrationId": 2, - "synchronizerSerialId": 10, - "version": "0.3.21" - } - } - }, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "istioVirtualService": { - "gateway": "cluster-ingress/cn-http-gateway", - "host": "info.sv-2.mock.global.canton.network.digitalasset.com" - }, - "runtimeDetails": { - "scanUrl": "http://scan-app.sv-1:5012", - "synchronizerSerialId": 9 - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ] - }, - "version": "0.3.20" - }, - "name": "sv-1-info", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-cluster-ingress-runbook", - "compat": "true", - "maxHistory": 10, - "name": "ingress-sv", - "namespace": "sv-1", - "timeout": 600, - "values": { - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet", - "svIngressName": "sv-2", - "svNamespace": "sv-1" - }, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "ingress": { - "decentralizedSynchronizer": { - "migrationIds": [ - "9", - "8", - "7", - "10" - ] - } - }, - "rateLimit": { - "scan": { - "enable": false - } - }, - "spliceDomainNames": { - "nameServiceDomain": "cns" - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ], - "withSvIngress": true - }, - "version": "0.3.20" - }, - "name": "sv-1-ingress-sv", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, - { - "custom": true, - "id": "", - "inputs": { - "bootDisk": { - "initializeParams": { - "image": "debian-cloud/debian-12" - } - }, - "labels": { - "cluster": "mock" - }, - "machineType": "e2-micro", - "metadata": { - "enable-osconfig": "TRUE", - "enable-oslogin": "true", - "startup-script": "#! /bin/bash\n\nexport DB_ADDR=undefined\nexport DB_PORT=5432\n\n# Enable the VM to receive packets whose destinations do\n# not match any running process local to the VM\necho 1 > /proc/sys/net/ipv4/ip_forward\n\n# Ask the Metadata server for the IP address of the VM nic0\n# network interface:\nmd_url_prefix=\"http://169.254.169.254/computeMetadata/v1/instance\"\nvm_nic_ip=\"$(curl -H \"Metadata-Flavor: Google\" $md_url_prefix/network-interfaces/0/ip)\"\n\n# Clear any existing iptables NAT table entries (all chains):\niptables -t nat -F\n\n# Create a NAT table entry in the prerouting chain, matching\n# any packets with destination database port, changing the destination\n# IP address of the packet to the SQL instance IP address:\niptables -t nat -A PREROUTING \\\n -p tcp --dport $DB_PORT \\\n -j DNAT \\\n --to-destination $DB_ADDR\n\n# Create a NAT table entry in the postrouting chain, matching\n# any packets with destination database port, changing the source IP\n# address of the packet to the NAT VM's primary internal IPv4 address:\niptables -t nat -A POSTROUTING \\\n -p tcp --dport $DB_PORT \\\n -j SNAT \\\n --to-source $vm_nic_ip\n\n# Save iptables configuration:\niptables-save\n" - }, - "networkInterfaces": [ - { - "accessConfigs": [ - {} - ], - "network": "default" - } - ], - "zone": "europe-west6-a" - }, - "name": "sv-1-nat-vm", - "provider": "", - "type": "gcp:compute/instance:Instance" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "networking.istio.io/v1alpha3", - "kind": "EnvoyFilter", - "metadata": { - "annotations": { - "proxy.istio.io/config": "proxyStatsMatcher:\n inclusionRegexps:\n - \".*http_local_rate_limit.*\"" - }, - "name": "scan-app-rate-limit", - "namespace": "sv-1" - }, - "spec": { - "configPatches": [ - { - "applyTo": "HTTP_FILTER", - "match": { - "context": "SIDECAR_INBOUND", - "listener": { - "filterChain": { - "filter": { - "name": "envoy.filters.network.http_connection_manager" - } - } - } - }, - "patch": { - "operation": "INSERT_BEFORE", - "value": { - "name": "envoy.filters.http.local_ratelimit", - "typed_config": { - "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", - "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", - "value": { - "stat_prefix": "http_local_rate_limiter" - } - } - } - } - }, - { - "applyTo": "HTTP_ROUTE", - "match": { - "context": "SIDECAR_INBOUND", - "routeConfiguration": { - "vhost": { - "name": "inbound|http|5012", - "route": { - "action": "ANY" - } - } - } - }, - "patch": { - "operation": "MERGE", - "value": { - "route": { - "rate_limits": [ - { - "actions": [ - { - "header_value_match": { - "descriptor_value": "acs", - "expect_match": true, - "headers": [ - { - "name": ":path", - "string_match": { - "ignore_case": true, - "prefix": "/api/scan/v0/acs" - } - } - ] - } - }, - { - "request_headers": { - "descriptor_key": "client_ip", - "header_name": "x-forwarded-for" - } - } - ] - } - ] - }, - "typed_per_filter_config": { - "envoy.filters.http.local_ratelimit": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", - "descriptors": [ - { - "entries": [ - { - "key": "header_match", - "value": "acs" - }, - { - "key": "client_ip" - } - ], - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 10, - "tokens_per_fill": 5 - } - } - ], - "filter_enabled": { - "default_value": { - "denominator": "HUNDRED", - "numerator": 100 - }, - "runtime_key": "local_rate_limit_enabled" - }, - "filter_enforced": { - "default_value": { - "denominator": "HUNDRED", - "numerator": 100 - }, - "runtime_key": "local_rate_limit_enforced" - }, - "response_headers_to_add": [ - { - "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", - "header": { - "key": "x-local-rate-limit", - "value": "true" - } - } - ], - "stat_prefix": "http_local_rate_limiter", - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 2147483647, - "tokens_per_fill": 2147483647 - } - } - } - } - } - } - ], - "workloadSelector": { - "labels": { - "app": "scan-app" - } - } - } - }, - "name": "sv-1-scan-app-rate-limit", - "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" - }, - { - "custom": true, - "id": "", - "inputs": { - "bigqueryProfile": {}, - "connectionProfileId": "sv-1-scan-bq-cxn", - "displayName": "sv-1-scan-bq-cxn", - "labels": { - "cluster": "mock" - }, - "location": "europe-west6" - }, - "name": "sv-1-scan-bq-cxn", - "provider": "", - "type": "gcp:datastream/connectionProfile:ConnectionProfile" - }, - { - "custom": true, - "id": "", - "inputs": { - "connectionProfileId": "sv-1-scan-update-history-cxn", - "displayName": "sv-1-scan-update-history-cxn", - "labels": { - "cluster": "mock" - }, - "location": "europe-west6", - "postgresqlProfile": { - "database": "scan_sv_1", - "port": 5432, - "username": "bqdatastream" - }, - "privateConnectivity": {} - }, - "name": "sv-1-scan-update-history-cxn", - "provider": "", - "type": "gcp:datastream/connectionProfile:ConnectionProfile" - }, - { - "custom": true, - "id": "", - "inputs": { - "displayName": "sv-1-scan-update-history-datastream-vpc", - "labels": { - "cluster": "mock" - }, - "location": "europe-west6", - "privateConnectionId": "sv-1-scan-update-history-datastream-vpc", - "vpcPeeringConfig": { - "subnet": "1.2.3.16/29", - "vpc": "projects/test-project/global/networks/default" - } - }, - "name": "sv-1-scan-update-history-datastream-vpc", - "provider": "", - "type": "gcp:datastream/privateConnection:PrivateConnection" - }, - { - "custom": true, - "id": "", - "inputs": { - "backfillAll": {}, - "desiredState": "RUNNING", - "destinationConfig": { - "bigqueryDestinationConfig": { - "dataFreshness": "14400s", - "singleTargetDataset": { - "datasetId": "projects/undefined/datasets/mock_da2_scan" - } - } - }, - "displayName": "sv-1-scan-update-history", - "labels": { - "cluster": "mock" - }, - "location": "europe-west6", - "sourceConfig": { - "postgresqlSourceConfig": { - "includeObjects": { - "postgresqlSchemas": [ - { - "postgresqlTables": [ - { - "table": "update_history_creates" - }, - { - "table": "update_history_exercises" - }, - { - "table": "scan_verdict_store" - }, - { - "table": "scan_verdict_transaction_view_store" - }, - { - "table": "app_activity_record_store" - } - ], - "schema": "scan_sv_1" - } - ] - }, - "publication": "update_history_datastream_pub", - "replicationSlot": "update_history_datastream_r_slot" - } - }, - "streamId": "sv-1-scan-update-history" - }, - "name": "sv-1-scan-update-history", - "provider": "", - "type": "gcp:datastream/stream:Stream" - }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-scan", - "compat": "true", - "maxHistory": 10, - "name": "scan", - "namespace": "sv-1", - "timeout": 600, - "values": { - "additionalEnvVars": [ - { - "name": "CUSTOM_MOCK_ENV_VAR_NAME", - "value": "CUSTOM_MOCK_ENV_VAR_VALUE" - } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomScanAppJvmFlag", - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "apiRequestLogLevel": "DEBUG", - "bulkStorage": { - "s3": { - "bucketName": "mock-sv-1-bulk", - "endpoint": "https://storage.googleapis.com", - "region": "europe-west6", - "secretName": "splice-app-bulk-storage-credentials" - } - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "enablePostgresMetrics": true, - "failOnAppVersionMismatch": true, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "isFirstSv": true, - "logAsyncFlush": false, - "logLevel": "WARN", - "metrics": { - "enable": true - }, - "participantAddress": "participant", - "persistence": { - "databaseName": "scan_sv_1", - "port": 5432, - "postgresName": "cn-apps-pg", - "schema": "scan_sv_1", - "secretName": "cn-apps-pg-secrets", - "user": "cnadmin" - }, - "publicUrl": "https://scan.sv-2.mock.global.canton.network.digitalasset.com", - "resources": { - "limits": { - "memory": "2048Mi" - }, - "requests": { - "cpu": "0.5", - "memory": "1536Mi" - } - }, - "spliceInstanceNames": { - "amuletName": "Amulet", - "amuletNameAcronym": "AMT", - "nameServiceName": "Amulet Name Service", - "nameServiceNameAcronym": "ANS", - "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", - "networkName": "Splice" - }, - "synchronizers": { - "current": { - "cantonBft": { - "p2pUrl": "https://sequencer-p2p-9.sv-2.mock.global.canton.network.digitalasset.com" - }, - "mediator": "global-domain-9-mediator", - "sequencer": "global-domain-9-sequencer" - }, - "legacy": { - "mediator": "global-domain-8-mediator", - "sequencer": "global-domain-8-sequencer" - }, - "successor": { - "cantonBft": { - "p2pUrl": "https://sequencer-p2p-10.sv-2.mock.global.canton.network.digitalasset.com" - }, - "mediator": "global-domain-10-mediator", - "sequencer": "global-domain-10-sequencer" - } - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ] - }, - "version": "0.3.20" - }, - "name": "sv-1-scan", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-sv-node", - "compat": "true", - "maxHistory": 10, - "name": "sv-app", - "namespace": "sv-1", - "timeout": 600, - "values": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "additionalEnvVars": [ - { - "name": "CUSTOM_MOCK_ENV_VAR_NAME", - "value": "CUSTOM_MOCK_ENV_VAR_VALUE" - }, - { - "name": "ADDITIONAL_CONFIG_MEDIATOR_PRUNING", - "value": "canton.sv-apps.sv.local-synchronizer-nodes.current.mediator.pruning {\n cron = \"0 0 * * *\"\n max-duration = \"30m\"\n retention = \"7d\"\n }" - }, - { - "name": "ADDITIONAL_CONFIG_CANTON_BFT_PRUNING", - "value": "canton.sv-apps.sv.local-synchronizer-nodes.current.sequencer.canton-bft-pruning {\n cron = \"0 /10 * * * ?\"\n max-duration = \"5m\"\n retention = \"15 days\"\n }" - }, - { - "name": "ADDITIONAL_CONFIG_ADDITIONAL_PACKAGES_TO_UNVET", - "value": "canton.sv-apps.sv.additional-packages-to-unvet.\"splice-wallet-payments\" = [\"0.1.15\", \"0.1.16\"]\ncanton.sv-apps.sv.additional-packages-to-unvet.\"splice-amulet\" = [\"0.1.15\"]" - } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSvAppJvmFlag", - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "apiRequestLogLevel": "DEBUG", - "approvedSvIdentities": [ - { - "name": "Digital-Asset-2", - "publicKey": "sv1-id-public-key", - "rewardWeightBps": 100000 - }, - { - "name": "SV1", - "publicKey": "PUBLIC_KEY_1==", - "rewardWeightBps": 1000000 - }, - { - "extraBeneficiaries": [ - { - "beneficiary": "mock-validator-1::123456789012345678901234567890123456789012234567890123456789012345678", - "weight": 100000 - }, - { - "beneficiary": "Broadridge-validator-1::1220c89a73e7b47f16dfb48a521eeedfe2a8620ea1b19b815ab427388d9f5427faa5", - "weight": 50000 - } - ], - "name": "SV2", - "publicKey": "PUBLIC_KEY_2==", - "rewardWeightBps": 150000 - }, - { - "name": "Digital-Asset-1", - "publicKey": "svda1-mock-id-public-key", - "rewardWeightBps": 150000 - }, - { - "name": "DA-Helm-Test-Node", - "publicKey": "sv-id-public-key", - "rewardWeightBps": 10000 - } - ], - "auth": { - "audience": "https://canton.network.global", - "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json" - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "contactPoint": "sv-support@digitalasset.com", - "delegatelessAutomationExpectedTaskDuration": 5000, - "delegatelessAutomationExpiredRewardCouponBatchSize": 100, - "delegatelessAutomationExpiredRewardCouponNumBatches": 100, - "disableOnboardingParticipantPromotionDelay": false, - "enablePostgresMetrics": true, - "expectedValidatorOnboardings": [ - { - "expiresIn": "24h", - "secretFrom": { - "secretKeyRef": { - "key": "secret", - "name": "splice-app-validator-onboarding-splitwell2", - "optional": false - } - } - }, - { - "expiresIn": "24h", - "secretFrom": { - "secretKeyRef": { - "key": "secret", - "name": "splice-app-validator-onboarding-validator12", - "optional": false - } - } - } - ], - "failOnAppVersionMismatch": true, - "identitiesExport": { - "bucket": { - "bucketName": "da-cn-data-dumps", - "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", - "projectId": "da-cn-devnet", - "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" - }, - "prefix": "mock/sv-1" - }, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "initialAmuletPrice": "0.0517", - "initialPackageConfigJson": "{\"amuletVersion\": \"0.1.4\",\"amuletNameServiceVersion\": \"0.1.4\",\"dsoGovernanceVersion\": \"0.1.7\",\"validatorLifecycleVersion\": \"0.1.1\",\"walletVersion\": \"0.1.4\",\"walletPaymentsVersion\": \"0.1.4\"}", - "initialRound": "0", - "initialSynchronizerFeesConfig": { - "baseRateBurstAmount": 200000, - "baseRateBurstWindowMins": 20, - "extraTrafficPrice": 16.67, - "minTopupAmount": 200000, - "readVsWriteScalingFactor": 4 - }, - "isDevNet": false, - "logAsyncFlush": false, - "logLevel": "WARN", - "maxVettingDelay": "1m", - "metrics": { - "enable": true - }, - "nodeIdentifier": "Digital-Asset-2", - "onboardingFoundingSvRewardWeightBps": 100000, - "onboardingName": "Digital-Asset-2", - "onboardingRoundZeroDuration": "2 h", - "onboardingType": "found-dso", - "participantAddress": "participant", - "periodicTopologySnapshotConfig": { - "backupInterval": "12h", - "location": { - "bucket": { - "bucketName": "cn-topology-snapshots", - "jsonCredentials": "{\"projectId\":\"da-cn-shared\",\"bucketName\":\"topology-snapshot-bucket-name\",\"secretName\":\"gcp-topology-snapshot-bucket-sa-key-secret\",\"jsonCredentials\":\"topology-snapshot-bucket-sa-key-secret-creds\",\"bucketSaKeySecret\":\"gcp-topology-snapshot-bucket-sa-key-example\",\"bucketSaIamAccount\":\"da-cn-examplet@da-cn-shared.iam.gserviceaccount.com\"}", - "projectId": "da-cn-shared", - "secretName": "cn-gcp-bucket-da-cn-shared-cn-topology-snapshots" - }, - "prefix": "mock/sv-1" - } - }, - "permissionedSynchronizer": false, - "persistence": { - "databaseName": "sv_sv_1", - "port": 5432, - "postgresName": "cn-apps-pg", - "schema": "sv_sv_1", - "secretName": "cn-apps-pg-secrets", - "user": "cnadmin" - }, - "pvc": { - "volumeName": "sv-app-global-domain-migration-hd-pvc", - "volumeStorageClass": "hyperdisk-standard-rwo" - }, - "resources": { - "limits": { - "memory": "2Gi" - }, - "requests": { - "cpu": "1", - "memory": "1Gi" - } - }, - "scan": { - "internalUrl": "http://scan-app.sv-1:5012", - "publicUrl": "https://scan.sv-2.mock.global.canton.network.digitalasset.com" - }, - "spliceInstanceNames": { - "amuletName": "Amulet", - "amuletNameAcronym": "AMT", - "nameServiceName": "Amulet Name Service", - "nameServiceNameAcronym": "ANS", - "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", - "networkName": "Splice" - }, - "synchronizers": { - "additionalLegacy": [ - { - "cometBFT": { - "connectionUri": "http://global-domain-7-cometbft-cometbft-rpc:26657", - "enabled": true - }, - "mediatorAddress": "global-domain-7-mediator", - "sequencerAddress": "global-domain-7-sequencer", - "sequencerPruningConfig": { - "enabled": true, - "pruningInterval": "1 hour", - "retentionPeriod": "30 days" - }, - "sequencerPublicUrl": "https://sequencer-7.sv-2.mock.global.canton.network.digitalasset.com" - } - ], - "current": { - "enableBftSequencer": true, - "mediatorAddress": "global-domain-9-mediator", - "sequencerAddress": "global-domain-9-sequencer", - "sequencerPruningConfig": { - "enabled": true, - "pruningInterval": "1 hour", - "retentionPeriod": "30 days" - }, - "sequencerPublicUrl": "https://sequencer-9.sv-2.mock.global.canton.network.digitalasset.com" - }, - "legacy": { - "cometBFT": { - "connectionUri": "http://global-domain-8-cometbft-cometbft-rpc:26657", - "enabled": true - }, - "mediatorAddress": "global-domain-8-mediator", - "sequencerAddress": "global-domain-8-sequencer", - "sequencerPruningConfig": { - "enabled": true, - "pruningInterval": "1 hour", - "retentionPeriod": "30 days" - }, - "sequencerPublicUrl": "https://sequencer-8.sv-2.mock.global.canton.network.digitalasset.com" - }, - "skipInitialization": true, - "successor": { - "enableBftSequencer": true, - "mediatorAddress": "global-domain-10-mediator", - "sequencerAddress": "global-domain-10-sequencer", - "sequencerPruningConfig": { - "enabled": true, - "pruningInterval": "1 hour", - "retentionPeriod": "30 days" - }, - "sequencerPublicUrl": "https://sequencer-10.sv-2.mock.global.canton.network.digitalasset.com" - } - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ] - } - }, - "version": "0.3.20" - }, - "name": "sv-1-sv-app", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, - { - "custom": true, - "id": "", - "inputs": { - "name": "bqdatastream", - "password": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": null - } - }, - "name": "sv-1-user-bqdatastream", - "provider": "", - "type": "gcp:sql/user:User" - }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-validator", - "compat": "true", - "maxHistory": 10, - "name": "validator-sv-1", - "namespace": "sv-1", - "timeout": 600, - "values": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "additionalEnvVars": [ - { - "name": "ADDITIONAL_CONFIG_TOPOLOGY_METRICS_EXPORT", - "value": "canton.validator-apps.validator_backend.automation.topology-metrics-polling-interval = 5m\n" - }, - { - "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", - "value": "canton.validator-apps.validator_backend.participant-pruning-schedule {\n cron = \"0 /10 * * * ?\"\n max-duration = \"5m\"\n retention = \"30d\"\n }" - } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1", - "additionalUsers": [], - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "apiRequestLogLevel": "DEBUG", - "appDars": [], - "auth": { - "audience": "https://canton.network.global", - "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json" - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "contactPoint": "sv-support@digitalasset.com", - "disableAllocateLedgerApiUserParty": true, - "disableAuth": false, - "enablePostgresMetrics": true, - "failOnAppVersionMismatch": true, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "logAsyncFlush": false, - "logLevel": "WARN", - "maxVettingDelay": "1m", - "metrics": { - "enable": true - }, - "nodeIdentifier": "Digital-Asset-2", - "participantAddress": "participant", - "participantIdentitiesDumpPeriodicBackup": { - "backupInterval": "10m", - "location": { - "bucket": { - "bucketName": "da-cn-data-dumps", - "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", - "projectId": "da-cn-devnet", - "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" - }, - "prefix": "mock/sv-1" - } - }, - "persistence": { - "databaseName": "validator_sv_1", - "port": 5432, - "postgresName": "cn-apps-pg", - "schema": "validator_sv_1", - "secretName": "cn-apps-pg-secrets", - "user": "cnadmin" - }, - "pvc": { - "volumeName": "domain-migration-validator-hd-pvc", - "volumeStorageClass": "hyperdisk-standard-rwo" - }, - "resources": { - "limits": { - "memory": "4Gi" - }, - "requests": { - "memory": "2Gi" - } - }, - "scanAddress": "http://scan-app.sv-1:5012", - "spliceInstanceNames": { - "amuletName": "Amulet", - "amuletNameAcronym": "AMT", - "nameServiceName": "Amulet Name Service", - "nameServiceNameAcronym": "ANS", - "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", - "networkName": "Splice" - }, - "svValidator": true, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ], - "topup": { - "enabled": true, - "minTopupInterval": "1m", - "targetThroughput": 0 - }, - "useSequencerConnectionsFromScan": true, - "validatorWalletUsers": [ - "google-oauth2|1234567890", - "auth0|64529b128448ded6aa68048f" - ], - "walletSweep": { - "mock::11111111111111111111111111111111111111111111111111111111111111111111": { - "maxBalanceUSD": 12345, - "minBalanceUSD": 17, - "receiver": "mock-2::222222222222222222222222222222222222222222222222222222222222222222222" - } - } - } - }, - "version": "0.3.20" - }, - "name": "sv-1-validator-sv-1", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Namespace", - "metadata": { - "labels": { - "istio-injection": "enabled" - }, - "name": "sv-1" - } - }, - "name": "sv-1", - "provider": "", - "type": "kubernetes:core/v1:Namespace" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "data": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "accessKey": "", - "secretAccessKey": "" - } - }, - "kind": "Secret", - "metadata": { - "name": "splice-app-bulk-storage-credentials", - "namespace": "sv-da-1" - }, - "type": "Opaque" - }, - "name": "sv-da-1-bulk-storage-credentials", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "length": 16, - "overrideSpecial": "_%@", - "special": true - }, - "name": "sv-da-1-cn-apps-pg-cnadmin-passwd", - "provider": "", - "type": "random:index/randomPassword:RandomPassword" - }, - { - "custom": false, - "id": "", - "inputs": { - "active": true, - "alias": "cn-apps-pg", - "cloudSqlConfig": { - "databaseVersion": "POSTGRES_14", - "enabled": true, - "enterprisePlus": false, - "flags": { - "maintenance_work_mem": "2000000", - "max_wal_size": "20480", - "random_page_cost": "1.1", - "temp_file_limit": "2147483647", - "work_mem": "16384" - }, - "maintenanceWindow": { - "day": 2, - "hour": 8 - }, - "protected": true, - "tier": "apps-pg-override-tier" - }, - "defaultUserName": "cnadmin", - "deletionProtection": true, - "instanceName": "cn-apps-pg", - "logicalDecoding": false, - "namespace": { - "logicalName": "sv-da-1", - "ns": { - "__aliases": [], - "__name": "sv-da-1", - "__providers": {}, - "__pulumiCustomResource": true, - "__pulumiResource": true, - "__pulumiType": "kubernetes:core/v1:Namespace", - "__transformations": [], - "__version": "4.28.0", - "apiVersion": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "id": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "kind": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "metadata": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "spec": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "status": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "urn": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi." - } - }, - "retainDbResourcesOnDelete": false, - "secretName": "cn-apps-pg-secrets" - }, - "name": "sv-da-1-cn-apps-pg", - "provider": "", - "type": "canton:cloud:postgres" - }, - { - "custom": true, - "id": "", - "inputs": { - "databaseVersion": "POSTGRES_14", - "deletionProtection": false, - "region": "europe-west6", - "settings": { - "activationPolicy": "ALWAYS", - "backupConfiguration": { - "enabled": true, - "pointInTimeRecoveryEnabled": true - }, - "databaseFlags": [ - { - "name": "random_page_cost", - "value": "1.1" - }, - { - "name": "temp_file_limit", - "value": "2147483647" - }, - { - "name": "max_wal_size", - "value": "20480" - }, - { - "name": "maintenance_work_mem", - "value": "2000000" - }, - { - "name": "work_mem", - "value": "16384" - } - ], - "deletionProtectionEnabled": true, - "edition": "ENTERPRISE", - "insightsConfig": { - "queryInsightsEnabled": true - }, - "ipConfiguration": { - "enablePrivatePathForGoogleCloudServices": true, - "ipv4Enabled": false, - "privateNetwork": "projects/test-project/global/networks/default" - }, - "locationPreference": { - "zone": "europe-west6-a" - }, - "maintenanceWindow": { - "day": 2, - "hour": 8 - }, - "tier": "apps-pg-override-tier", - "userLabels": { - "cluster": "mock" - } - } - }, - "name": "sv-da-1-cn-apps-pg", - "provider": "", - "type": "gcp:sql/databaseInstance:DatabaseInstance" - }, - { - "custom": true, - "id": "", - "inputs": { - "name": "cantonnet" - }, - "name": "sv-da-1-db-cn-apps-pg-cantonnet", - "provider": "", - "type": "gcp:sql/database:Database" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "imagePullSecrets": [ - { - "name": "docker-reg-cred" - } - ], - "kind": "ServiceAccount", - "metadata": { - "name": "default", - "namespace": "sv-da-1" - } - }, - "name": "sv-da-1-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-default::undefined_id", - "type": "kubernetes:core/v1:ServiceAccountPatch" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "docker-reg-cred", - "namespace": "sv-da-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" - } - }, - "type": "kubernetes.io/dockerconfigjson" - }, - "name": "sv-da-1-docker-reg-cred", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-info", - "compat": "true", - "maxHistory": 10, - "name": "info", - "namespace": "sv-da-1", - "timeout": 600, - "values": { - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "deploymentDetails": { - "configDigest": { - "allowedIpRanges": { - "type": "md5", - "value": "90eedde4a8599204a45dcb972e212c8b" - }, - "approvedSvIdentities": { - "type": "md5", - "value": "5871224b744b45122540483fddbd550f" - } - }, - "network": "test", - "sv": { - "version": "0.3.20" - }, - "synchronizer": { - "current": { - "chainIdSuffix": "4", - "migrationId": 2, - "synchronizerSerialId": 9, - "version": "0.3.20" - }, - "legacy": { - "chainIdSuffix": "4", - "migrationId": 2, - "synchronizerSerialId": 8, - "version": "0.3.20" - }, - "successor": { - "chainIdSuffix": "4", - "migrationId": 2, - "synchronizerSerialId": 10, - "version": "0.3.21" - } - } - }, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "istioVirtualService": { - "gateway": "cluster-ingress/cn-http-gateway", - "host": "info.sv-1.mock.global.canton.network.digitalasset.com" - }, - "runtimeDetails": { - "scanUrl": "http://scan-app.sv-da-1:5012", - "synchronizerSerialId": 9 - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ] - }, - "version": "0.3.20" - }, - "name": "sv-da-1-info", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-cluster-ingress-runbook", - "compat": "true", - "maxHistory": 10, - "name": "ingress-sv", - "namespace": "sv-da-1", - "timeout": 600, - "values": { - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet", - "svIngressName": "sv-1", - "svNamespace": "sv-da-1" - }, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "ingress": { - "decentralizedSynchronizer": { - "migrationIds": [ - "9", - "8", - "7", - "10" - ] - } - }, - "rateLimit": { - "scan": { - "enable": false - } - }, - "spliceDomainNames": { - "nameServiceDomain": "cns" - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ], - "withSvIngress": true + "connectionProfileId": "sv-1-scan-update-history-cxn", + "displayName": "sv-1-scan-update-history-cxn", + "labels": { + "cluster": "mock" }, - "version": "0.3.20" + "location": "europe-west6", + "postgresqlProfile": { + "database": "scan_sv_1", + "port": 5432, + "username": "bqdatastream" + }, + "privateConnectivity": {} }, - "name": "sv-da-1-ingress-sv", + "name": "sv-1-scan-update-history-cxn", "provider": "", - "type": "kubernetes:helm.sh/v3:Release" + "type": "gcp:datastream/connectionProfile:ConnectionProfile" }, { "custom": true, "id": "", "inputs": { - "apiVersion": "networking.istio.io/v1alpha3", - "kind": "EnvoyFilter", - "metadata": { - "annotations": { - "proxy.istio.io/config": "proxyStatsMatcher:\n inclusionRegexps:\n - \".*http_local_rate_limit.*\"" - }, - "name": "scan-app-rate-limit", - "namespace": "sv-da-1" + "displayName": "sv-1-scan-update-history-datastream-vpc", + "labels": { + "cluster": "mock" }, - "spec": { - "configPatches": [ - { - "applyTo": "HTTP_FILTER", - "match": { - "context": "SIDECAR_INBOUND", - "listener": { - "filterChain": { - "filter": { - "name": "envoy.filters.network.http_connection_manager" - } - } - } - }, - "patch": { - "operation": "INSERT_BEFORE", - "value": { - "name": "envoy.filters.http.local_ratelimit", - "typed_config": { - "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", - "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", - "value": { - "stat_prefix": "http_local_rate_limiter" - } - } - } - } - }, - { - "applyTo": "HTTP_ROUTE", - "match": { - "context": "SIDECAR_INBOUND", - "routeConfiguration": { - "vhost": { - "name": "inbound|http|5012", - "route": { - "action": "ANY" - } - } - } - }, - "patch": { - "operation": "MERGE", - "value": { - "route": { - "rate_limits": [ - { - "actions": [ - { - "header_value_match": { - "descriptor_value": "acs", - "expect_match": true, - "headers": [ - { - "name": ":path", - "string_match": { - "ignore_case": true, - "prefix": "/api/scan/v0/acs" - } - } - ] - } - }, - { - "request_headers": { - "descriptor_key": "client_ip", - "header_name": "x-forwarded-for" - } - } - ] - } - ] - }, - "typed_per_filter_config": { - "envoy.filters.http.local_ratelimit": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", - "descriptors": [ - { - "entries": [ - { - "key": "header_match", - "value": "acs" - }, - { - "key": "client_ip" - } - ], - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 10, - "tokens_per_fill": 5 - } - } - ], - "filter_enabled": { - "default_value": { - "denominator": "HUNDRED", - "numerator": 100 - }, - "runtime_key": "local_rate_limit_enabled" - }, - "filter_enforced": { - "default_value": { - "denominator": "HUNDRED", - "numerator": 100 - }, - "runtime_key": "local_rate_limit_enforced" - }, - "response_headers_to_add": [ - { - "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", - "header": { - "key": "x-local-rate-limit", - "value": "true" - } - } - ], - "stat_prefix": "http_local_rate_limiter", - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 2147483647, - "tokens_per_fill": 2147483647 - } - } - } - } - } - } - ], - "workloadSelector": { - "labels": { - "app": "scan-app" - } - } + "location": "europe-west6", + "privateConnectionId": "sv-1-scan-update-history-datastream-vpc", + "vpcPeeringConfig": { + "subnet": "1.2.3.16/29", + "vpc": "projects/test-project/global/networks/default" } }, - "name": "sv-da-1-scan-app-rate-limit", + "name": "sv-1-scan-update-history-datastream-vpc", "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + "type": "gcp:datastream/privateConnection:PrivateConnection" }, { "custom": true, "id": "", "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-scan", - "compat": "true", - "maxHistory": 10, - "name": "scan", - "namespace": "sv-da-1", - "timeout": 600, - "values": { - "additionalEnvVars": [ - { - "name": "CUSTOM_MOCK_ENV_VAR_NAME", - "value": "CUSTOM_MOCK_ENV_VAR_VALUE" - } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomScanAppJvmFlag", - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "apiRequestLogLevel": "DEBUG", - "bulkStorage": { - "s3": { - "bucketName": "mock-sv-da-1-bulk", - "endpoint": "https://storage.googleapis.com", - "region": "europe-west6", - "secretName": "splice-app-bulk-storage-credentials" - } - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "enablePostgresMetrics": true, - "failOnAppVersionMismatch": true, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "isFirstSv": false, - "logAsyncFlush": false, - "logLevel": "INFO", - "metrics": { - "enable": true - }, - "participantAddress": "participant", - "persistence": { - "databaseName": "scan_sv_da_1", - "port": 5432, - "postgresName": "cn-apps-pg", - "schema": "scan_sv_da_1", - "secretName": "cn-apps-pg-secrets", - "user": "cnadmin" - }, - "publicUrl": "https://scan.sv-1.mock.global.canton.network.digitalasset.com", - "resources": { - "limits": { - "memory": "2048Mi" - }, - "requests": { - "cpu": "0.5", - "memory": "1536Mi" - } - }, - "spliceInstanceNames": { - "amuletName": "Amulet", - "amuletNameAcronym": "AMT", - "nameServiceName": "Amulet Name Service", - "nameServiceNameAcronym": "ANS", - "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", - "networkName": "Splice" - }, - "sponsorScanUrl": "http://scan-app.sv-1:5012", - "synchronizers": { - "current": { - "cantonBft": { - "p2pUrl": "https://sequencer-p2p-9.sv-1.mock.global.canton.network.digitalasset.com" - }, - "mediator": "global-domain-9-mediator", - "sequencer": "global-domain-9-sequencer" - }, - "legacy": { - "mediator": "global-domain-8-mediator", - "sequencer": "global-domain-8-sequencer" - }, - "successor": { - "cantonBft": { - "p2pUrl": "https://sequencer-p2p-10.sv-1.mock.global.canton.network.digitalasset.com" - }, - "mediator": "global-domain-10-mediator", - "sequencer": "global-domain-10-sequencer" - } - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" + "backfillAll": {}, + "desiredState": "RUNNING", + "destinationConfig": { + "bigqueryDestinationConfig": { + "dataFreshness": "14400s", + "singleTargetDataset": { + "datasetId": "projects/undefined/datasets/mock_da2_scan" } - ] + } }, - "version": "0.3.20" - }, - "name": "sv-da-1-scan", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-sv-node", - "compat": "true", - "maxHistory": 10, - "name": "sv-app", - "namespace": "sv-da-1", - "timeout": 600, - "values": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "additionalEnvVars": [ - { - "name": "CUSTOM_MOCK_ENV_VAR_NAME", - "value": "CUSTOM_MOCK_ENV_VAR_VALUE" - }, - { - "name": "ADDITIONAL_CONFIG_NO_BFT_SEQUENCER_CONNECTION", - "value": "canton.sv-apps.sv.bft-sequencer-connection = false" - }, - { - "name": "ADDITIONAL_CONFIG_MEDIATOR_PRUNING", - "value": "canton.sv-apps.sv.local-synchronizer-nodes.current.mediator.pruning {\n cron = \"0 0 * * *\"\n max-duration = \"30m\"\n retention = \"7d\"\n }" - }, - { - "name": "ADDITIONAL_CONFIG_CANTON_BFT_PRUNING", - "value": "canton.sv-apps.sv.local-synchronizer-nodes.current.sequencer.canton-bft-pruning {\n cron = \"0 /10 * * * ?\"\n max-duration = \"5m\"\n retention = \"15 days\"\n }" - }, - { - "name": "ADDITIONAL_CONFIG_ADDITIONAL_PACKAGES_TO_UNVET", - "value": "canton.sv-apps.sv.additional-packages-to-unvet.\"splice-wallet-payments\" = [\"0.1.15\", \"0.1.16\"]\ncanton.sv-apps.sv.additional-packages-to-unvet.\"splice-amulet\" = [\"0.1.15\"]" - } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSvAppJvmFlag", - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ + "displayName": "sv-1-scan-update-history", + "labels": { + "cluster": "mock", + "datastream_id": "legacy" + }, + "location": "europe-west6", + "sourceConfig": { + "postgresqlSourceConfig": { + "includeObjects": { + "postgresqlSchemas": [ + { + "postgresqlTables": [ + { + "table": "update_history_creates" + }, + { + "table": "update_history_exercises" + }, { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] + "table": "scan_verdict_store" + }, + { + "table": "scan_verdict_transaction_view_store" + }, + { + "table": "app_activity_record_store" } - ] - } - } - }, - "apiRequestLogLevel": "DEBUG", - "approvedSvIdentities": [ - { - "name": "Digital-Asset-2", - "publicKey": "sv1-id-public-key", - "rewardWeightBps": 100000 - }, - { - "name": "SV1", - "publicKey": "PUBLIC_KEY_1==", - "rewardWeightBps": 1000000 - }, - { - "extraBeneficiaries": [ - { - "beneficiary": "mock-validator-1::123456789012345678901234567890123456789012234567890123456789012345678", - "weight": 100000 - }, - { - "beneficiary": "Broadridge-validator-1::1220c89a73e7b47f16dfb48a521eeedfe2a8620ea1b19b815ab427388d9f5427faa5", - "weight": 50000 - } - ], - "name": "SV2", - "publicKey": "PUBLIC_KEY_2==", - "rewardWeightBps": 150000 - }, - { - "name": "Digital-Asset-1", - "publicKey": "svda1-mock-id-public-key", - "rewardWeightBps": 150000 - }, - { - "name": "DA-Helm-Test-Node", - "publicKey": "sv-id-public-key", - "rewardWeightBps": 10000 - } - ], - "auth": { - "audience": "https://canton.network.global", - "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json" - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "contactPoint": "sv-support@digitalasset.com", - "decentralizedSynchronizerUrl": "http://global-domain-9-sequencer.sv-1:5008", - "delegatelessAutomationExpectedTaskDuration": 5000, - "delegatelessAutomationExpiredRewardCouponBatchSize": 100, - "delegatelessAutomationExpiredRewardCouponNumBatches": 100, - "disableOnboardingParticipantPromotionDelay": false, - "enablePostgresMetrics": true, - "expectedValidatorOnboardings": [], - "failOnAppVersionMismatch": true, - "identitiesExport": { - "bucket": { - "bucketName": "da-cn-data-dumps", - "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", - "projectId": "da-cn-devnet", - "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" - }, - "prefix": "mock/sv-da-1" - }, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "initialAmuletPrice": "0.0517", - "isDevNet": false, - "joinWithKeyOnboarding": { - "sponsorApiUrl": "http://sv-app.sv-1:5014" - }, - "logAsyncFlush": false, - "logLevel": "INFO", - "maxVettingDelay": "1m", - "metrics": { - "enable": true - }, - "nodeIdentifier": "Digital-Asset-1", - "onboardingName": "Digital-Asset-1", - "onboardingType": "join-with-key", - "participantAddress": "participant", - "permissionedSynchronizer": false, - "persistence": { - "databaseName": "sv_sv_da_1", - "port": 5432, - "postgresName": "cn-apps-pg", - "schema": "sv_sv_da_1", - "secretName": "cn-apps-pg-secrets", - "user": "cnadmin" - }, - "pvc": { - "volumeName": "sv-app-global-domain-migration-hd-pvc", - "volumeStorageClass": "hyperdisk-standard-rwo" - }, - "resources": { - "limits": { - "memory": "2Gi" - }, - "requests": { - "cpu": "1", - "memory": "1Gi" - } - }, - "scan": { - "internalUrl": "http://scan-app.sv-da-1:5012", - "publicUrl": "https://scan.sv-1.mock.global.canton.network.digitalasset.com" - }, - "spliceInstanceNames": { - "amuletName": "Amulet", - "amuletNameAcronym": "AMT", - "nameServiceName": "Amulet Name Service", - "nameServiceNameAcronym": "ANS", - "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", - "networkName": "Splice" - }, - "synchronizers": { - "additionalLegacy": [ - { - "cometBFT": { - "connectionUri": "http://global-domain-7-cometbft-cometbft-rpc:26657", - "enabled": true, - "externalGovernanceKey": true - }, - "mediatorAddress": "global-domain-7-mediator", - "sequencerAddress": "global-domain-7-sequencer", - "sequencerPruningConfig": { - "enabled": true, - "pruningInterval": "1 hour", - "retentionPeriod": "30 days" - }, - "sequencerPublicUrl": "https://sequencer-7.sv-1.mock.global.canton.network.digitalasset.com" + ], + "schema": "scan_sv_1" } - ], - "current": { - "enableBftSequencer": true, - "mediatorAddress": "global-domain-9-mediator", - "sequencerAddress": "global-domain-9-sequencer", - "sequencerPruningConfig": { - "enabled": true, - "pruningInterval": "1 hour", - "retentionPeriod": "30 days" - }, - "sequencerPublicUrl": "https://sequencer-9.sv-1.mock.global.canton.network.digitalasset.com" - }, - "legacy": { - "cometBFT": { - "connectionUri": "http://global-domain-8-cometbft-cometbft-rpc:26657", - "enabled": true, - "externalGovernanceKey": true - }, - "mediatorAddress": "global-domain-8-mediator", - "sequencerAddress": "global-domain-8-sequencer", - "sequencerPruningConfig": { - "enabled": true, - "pruningInterval": "1 hour", - "retentionPeriod": "30 days" - }, - "sequencerPublicUrl": "https://sequencer-8.sv-1.mock.global.canton.network.digitalasset.com" - }, - "skipInitialization": true, - "successor": { - "enableBftSequencer": true, - "mediatorAddress": "global-domain-10-mediator", - "sequencerAddress": "global-domain-10-sequencer", - "sequencerPruningConfig": { - "enabled": true, - "pruningInterval": "1 hour", - "retentionPeriod": "30 days" - }, - "sequencerPublicUrl": "https://sequencer-10.sv-1.mock.global.canton.network.digitalasset.com" - } + ] }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ] + "publication": "update_history_datastream_pub", + "replicationSlot": "update_history_datastream_r_slot" } }, - "version": "0.3.20" + "streamId": "sv-1-scan-update-history" }, - "name": "sv-da-1-sv-app", + "name": "sv-1-scan-update-history", "provider": "", - "type": "kubernetes:helm.sh/v3:Release" + "type": "gcp:datastream/stream:Stream" }, { "custom": true, "id": "", "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-validator", - "compat": "true", - "maxHistory": 10, - "name": "validator-sv-da-1", - "namespace": "sv-da-1", - "timeout": 600, - "values": { + "name": "bqdatastream", + "password": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "additionalEnvVars": [ - { - "name": "ADDITIONAL_CONFIG_NO_BFT_SEQUENCER_CONNECTION", - "value": "canton.validator-apps.validator_backend.disable-sv-validator-bft-sequencer-connection = true" - }, - { - "name": "ADDITIONAL_CONFIG_TOPOLOGY_METRICS_EXPORT", - "value": "canton.validator-apps.validator_backend.automation.topology-metrics-polling-interval = 5m\n" - }, - { - "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", - "value": "canton.validator-apps.validator_backend.participant-pruning-schedule {\n cron = \"0 0 * * *\"\n max-duration = \"30m\"\n retention = \"7d\"\n }" - } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1", - "additionalUsers": [], - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "apiRequestLogLevel": "DEBUG", - "appDars": [], - "auth": { - "audience": "https://canton.network.global", - "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json" - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "contactPoint": "sv-support@digitalasset.com", - "disableAllocateLedgerApiUserParty": true, - "disableAuth": false, - "enablePostgresMetrics": true, - "failOnAppVersionMismatch": true, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "logAsyncFlush": false, - "logLevel": "INFO", - "maxVettingDelay": "1m", - "metrics": { - "enable": true - }, - "nodeIdentifier": "Digital-Asset-1", - "participantAddress": "participant", - "participantIdentitiesDumpPeriodicBackup": { - "backupInterval": "10m", - "location": { - "bucket": { - "bucketName": "da-cn-data-dumps", - "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", - "projectId": "da-cn-devnet", - "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" - }, - "prefix": "mock/sv-da-1" - } - }, - "persistence": { - "databaseName": "validator_sv_da_1", - "port": 5432, - "postgresName": "cn-apps-pg", - "schema": "validator_sv_da_1", - "secretName": "cn-apps-pg-secrets", - "user": "cnadmin" - }, - "pvc": { - "volumeName": "domain-migration-validator-hd-pvc", - "volumeStorageClass": "hyperdisk-standard-rwo" - }, - "resources": { - "limits": { - "memory": "4Gi" - }, - "requests": { - "memory": "2Gi" - } - }, - "scanAddress": "http://scan-app.sv-da-1:5012", - "spliceInstanceNames": { - "amuletName": "Amulet", - "amuletNameAcronym": "AMT", - "nameServiceName": "Amulet Name Service", - "nameServiceNameAcronym": "ANS", - "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", - "networkName": "Splice" - }, - "svValidator": true, - "synchronizer": { - "connectionType": "trust-single", - "url": "https://sequencer-9.sv-2.mock.global.canton.network.digitalasset.com" - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ], - "topup": { - "enabled": true, - "minTopupInterval": "1m", - "targetThroughput": 0 - }, - "useSequencerConnectionsFromScan": true, - "validatorWalletUsers": [ - "google-oauth2|1234567890", - "auth0|68c2c41e6470184569e4521e" - ], - "walletSweep": { - "mock-3::33333333333333333333333333333333333333333333333333333333333333333333": { - "maxBalanceUSD": 67890, - "minBalanceUSD": 42, - "receiver": "mock-4::444444444444444444444444444444444444444444444444444444444444444444444" - } - } - } - }, - "version": "0.3.20" + "value": null + } }, - "name": "sv-da-1-validator-sv-da-1", + "name": "sv-1-user-bqdatastream", "provider": "", - "type": "kubernetes:helm.sh/v3:Release" + "type": "gcp:sql/user:User" }, { "custom": true, @@ -4418,39 +388,11 @@ "labels": { "istio-injection": "enabled" }, - "name": "sv-da-1" + "name": "sv-1" } }, - "name": "sv-da-1", + "name": "sv-1", "provider": "", "type": "kubernetes:core/v1:Namespace" - }, - { - "custom": true, - "id": "", - "inputs": { - "name": "cnadmin", - "password": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": null - } - }, - "name": "user-sv-1-cn-apps-pg-cnadmin", - "provider": "", - "type": "gcp:sql/user:User" - }, - { - "custom": true, - "id": "", - "inputs": { - "name": "cnadmin", - "password": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": null - } - }, - "name": "user-sv-da-1-cn-apps-pg-cnadmin", - "provider": "", - "type": "gcp:sql/user:User" } ] diff --git a/cluster/expected/cluster/expected.json b/cluster/expected/cluster/expected.json index 8bf405ca80..a62d43ffd1 100644 --- a/cluster/expected/cluster/expected.json +++ b/cluster/expected/cluster/expected.json @@ -72,6 +72,37 @@ "id": "", "inputs": { "autoscaling": { + "locationPolicy": "ANY", + "maxNodeCount": 3, + "minNodeCount": 1 + }, + "cluster": "cn-mocknet", + "initialNodeCount": 1, + "nodeConfig": { + "labels": { + "cn_infra": "true" + }, + "loggingVariant": "DEFAULT", + "machineType": "n4-standard-8", + "taints": [ + { + "effect": "NO_SCHEDULE", + "key": "cn_infra", + "value": "true" + } + ] + } + }, + "name": "cn-infra-node-pool-1", + "provider": "", + "type": "gcp:container/nodePool:NodePool" + }, + { + "custom": true, + "id": "", + "inputs": { + "autoscaling": { + "locationPolicy": "ANY", "maxNodeCount": 3, "minNodeCount": 1 }, @@ -163,15 +194,5 @@ "name": "hyperdisk-standard-rwo", "provider": "", "type": "kubernetes:storage.k8s.io/v1:StorageClass" - }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" } ] diff --git a/cluster/expected/deployment/expected.json b/cluster/expected/deployment/expected.json index db86445d5c..bd22a5e90e 100644 --- a/cluster/expected/deployment/expected.json +++ b/cluster/expected/deployment/expected.json @@ -919,16 +919,6 @@ "provider": "", "type": "kubernetes:pulumi.com/v1:Stack" }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, { "custom": true, "id": "", @@ -943,7 +933,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "interval": "5m", "recurseSubmodules": true, "ref": { @@ -973,7 +963,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "include": [ { "fromPath": "cluster/stacks/prod/sv-runbook/Pulumi.sv-runbook.mock.yaml", @@ -1082,7 +1072,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "interval": "5m", "recurseSubmodules": true, "ref": { @@ -1112,7 +1102,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "include": [ { "fromPath": "cluster/stacks/prod/sv-canton/Pulumi.sv-canton.sv-1-migration-10.mock.yaml", @@ -1165,7 +1155,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "interval": "5m", "recurseSubmodules": true, "ref": { @@ -1195,7 +1185,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "include": [ { "fromPath": "cluster/stacks/prod/sv-canton/Pulumi.sv-canton.sv-1-migration-5.mock.yaml", @@ -1248,7 +1238,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "interval": "5m", "recurseSubmodules": true, "ref": { @@ -1278,7 +1268,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "include": [ { "fromPath": "cluster/stacks/prod/sv-canton/Pulumi.sv-canton.sv-1-migration-6.mock.yaml", @@ -1331,7 +1321,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "interval": "5m", "recurseSubmodules": true, "ref": { @@ -1361,7 +1351,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "include": [ { "fromPath": "cluster/stacks/prod/sv-canton/Pulumi.sv-canton.sv-1-migration-7.mock.yaml", @@ -1414,7 +1404,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "interval": "5m", "recurseSubmodules": true, "ref": { @@ -1444,7 +1434,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "include": [ { "fromPath": "cluster/stacks/prod/sv-canton/Pulumi.sv-canton.sv-1-migration-8.mock.yaml", @@ -1497,7 +1487,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "interval": "5m", "recurseSubmodules": true, "ref": { @@ -1527,7 +1517,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "include": [ { "fromPath": "cluster/stacks/prod/sv-canton/Pulumi.sv-canton.sv-1-migration-9.mock.yaml", diff --git a/cluster/expected/gcp/expected.json b/cluster/expected/gcp/expected.json index 5353532a0c..73250cb1ae 100644 --- a/cluster/expected/gcp/expected.json +++ b/cluster/expected/gcp/expected.json @@ -206,13 +206,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"adminUser\": \"grafana-keys-admin-user\"\n , \"adminPassword\": \"grafana-keys-admin-password\"}" - } - }, + "inputs": {}, "name": "grafana-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -238,16 +232,6 @@ "provider": "", "type": "cn:gcp:ImportedSecret" }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, { "custom": true, "id": "", @@ -273,13 +257,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"publicKey\": \"sv-id-public-key\", \"privateKey\": \"sv-id-private-key\"}" - } - }, + "inputs": {}, "name": "sv-id-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -308,13 +286,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv1-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv1-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv1-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv1-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -343,13 +315,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv10-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv10-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv10-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv10-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -378,13 +344,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv11-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv11-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv11-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv11-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -413,13 +373,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv12-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv12-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv12-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv12-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -448,13 +402,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv13-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv13-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv13-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv13-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -483,13 +431,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv14-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv14-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv14-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv14-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -518,13 +460,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv15-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv15-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv15-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv15-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -553,13 +489,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv16-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv16-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv16-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv16-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -588,13 +518,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv2-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv2-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv2-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv2-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -623,13 +547,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"publicKey\": \"sv2-id-public-key\", \"privateKey\": \"sv2-id-private-key\"}" - } - }, + "inputs": {}, "name": "sv2-id-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -658,13 +576,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv3-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv3-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv3-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv3-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -693,13 +605,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"publicKey\": \"sv3-id-public-key\", \"privateKey\": \"sv3-id-private-key\"}" - } - }, + "inputs": {}, "name": "sv3-id-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -728,13 +634,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv4-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv4-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv4-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv4-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -763,13 +663,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"publicKey\": \"sv4-id-public-key\", \"privateKey\": \"sv4-id-private-key\"}" - } - }, + "inputs": {}, "name": "sv4-id-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -798,13 +692,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv5-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv5-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv5-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv5-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -833,13 +721,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv6-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv6-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv6-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv6-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -868,13 +750,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv7-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv7-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv7-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv7-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -903,13 +779,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv8-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv8-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv8-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv8-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" @@ -938,13 +808,7 @@ { "custom": true, "id": "", - "inputs": { - "secret": "undefined_id", - "secretData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": "{\"nodePrivateKey\": \"sv9-cometbft-keys-node-private-key\", \"validatorPrivateKey\": \"sv9-cometbft-keys-validator-private-key\"\n , \"validatorPublicKey\": \"sv9-cometbft-keys-validator-public-key\"}" - } - }, + "inputs": {}, "name": "sv9-cometbft-keys-secretversion", "provider": "", "type": "gcp:secretmanager/secretVersion:SecretVersion" diff --git a/cluster/expected/infra/expected.json b/cluster/expected/infra/expected.json index 981e3a4c5f..45bf1df4e5 100644 --- a/cluster/expected/infra/expected.json +++ b/cluster/expected/infra/expected.json @@ -12,7 +12,7 @@ ] }, "name": "LedgerApiScopessv1", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/resourceServerScopes:ResourceServerScopes" }, { @@ -28,7 +28,7 @@ ] }, "name": "LedgerApiScopessvda1", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/resourceServerScopes:ResourceServerScopes" }, { @@ -41,7 +41,7 @@ "tokenLifetime": 86400 }, "name": "LedgerApisv1", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/resourceServer:ResourceServer" }, { @@ -54,18 +54,17 @@ "tokenLifetime": 86400 }, "name": "LedgerApisvda1", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/resourceServer:ResourceServer" }, { "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "Splitwell UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "SplitwellUiAppCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -125,7 +124,7 @@ ] }, "name": "SplitwellUiApp", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { @@ -138,7 +137,7 @@ "tokenLifetime": 86400 }, "name": "SvAppApisv1", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/resourceServer:ResourceServer" }, { @@ -151,18 +150,17 @@ "tokenLifetime": 86400 }, "name": "SvAppApisvda1", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/resourceServer:ResourceServer" }, { "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "ANS UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "SvCnsUiCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -206,18 +204,17 @@ ] }, "name": "SvCnsUi", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "SV UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "SvSvUiCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -261,18 +258,17 @@ ] }, "name": "SvSvUi", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "Wallet UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "SvWalletUiCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -316,7 +312,7 @@ ] }, "name": "SvWalletUi", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { @@ -329,7 +325,7 @@ "tokenLifetime": 86400 }, "name": "ValidatorAppApisv1", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/resourceServer:ResourceServer" }, { @@ -342,7 +338,7 @@ "tokenLifetime": 86400 }, "name": "ValidatorAppApisvda1", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/resourceServer:ResourceServer" }, { @@ -407,6 +403,42 @@ "provider": "", "type": "kubernetes:telemetry.istio.io/v1alpha1:Telemetry" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "security.istio.io/v1beta1", + "kind": "AuthorizationPolicy", + "metadata": { + "name": "allow-public-token-registry", + "namespace": "cluster-ingress" + }, + "spec": { + "action": "ALLOW", + "rules": [ + { + "to": [ + { + "operation": { + "paths": [ + "/registry/*" + ] + } + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "istio-ingress" + } + } + } + }, + "name": "allow-public-token-registry", + "provider": "", + "type": "kubernetes:security.istio.io/v1beta1:AuthorizationPolicy" + }, { "custom": true, "id": "", @@ -691,6 +723,12 @@ "uri": { "prefix": "/cn-release-bundles" } + }, + { + "port": 80, + "uri": { + "prefix": "/cn-release-bundles" + } } ], "route": [ @@ -708,6 +746,9 @@ "match": [ { "port": 443 + }, + { + "port": 80 } ], "route": [ @@ -1256,15 +1297,6 @@ "source": { "remoteIpBlocks": [ "", - "1.2.3.4/32", - "5.6.7.8/32", - "11.12.13.14/32", - "4.3.2.1/32", - "8.7.6.5/32", - "8.7.6.4/32", - "9.8.7.6/32", - "11.22.33.45/32", - "12.34.56.78/32", "10.160.0.0/16" ] } @@ -1440,7 +1472,8 @@ "", "1.2.3.4/32", "5.6.7.8/32", - "11.12.13.14/32" + "11.12.13.14/32", + "2.3.4.5/32" ], "ports": [ { @@ -1907,17 +1940,25 @@ "meshConfig": { "accessLogEncoding": "JSON", "accessLogFile": "", - "accessLogFormat": "{\"trace_id\":\"%REQ(traceparent)%\",\"authority\":\"%REQ(:AUTHORITY)%\",\"bytes_received\":\"%BYTES_RECEIVED%\",\"bytes_sent\":\"%BYTES_SENT%\",\"downstream_local_address\":\"%DOWNSTREAM_LOCAL_ADDRESS%\",\"downstream_remote_address\":\"%DOWNSTREAM_REMOTE_ADDRESS%\",\"duration\":\"%DURATION%\",\"method\":\"%REQ(:METHOD)%\",\"path\":\"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%\",\"protocol\":\"%PROTOCOL%\",\"request_id\":\"%REQ(X-REQUEST-ID)%\",\"requested_server_name\":\"%REQUESTED_SERVER_NAME%\",\"response_code\":\"%RESPONSE_CODE%\",\"response_code_details\":\"%RESPONSE_CODE_DETAILS%\",\"response_flags\":\"%RESPONSE_FLAGS%\",\"start_time\":\"%START_TIME%\",\"upstream_cluster\":\"%UPSTREAM_CLUSTER%\",\"upstream_host\":\"%UPSTREAM_HOST%\",\"upstream_local_address\":\"%UPSTREAM_LOCAL_ADDRESS%\",\"upstream_service_time\":\"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%\",\"user_agent\":\"%REQ(USER-AGENT)%\",\"x_forwarded_for\":\"%REQ(X-FORWARDED-FOR)%\"}", + "accessLogFormat": "{\"trace_id\":\"%REQ(traceparent)%\",\"authority\":\"%REQ(:AUTHORITY)%\",\"bytes_received\":\"%BYTES_RECEIVED%\",\"bytes_sent\":\"%BYTES_SENT%\",\"downstream_local_address\":\"%DOWNSTREAM_LOCAL_ADDRESS%\",\"downstream_remote_address\":\"%DOWNSTREAM_REMOTE_ADDRESS%\",\"duration\":\"%DURATION%\",\"method\":\"%REQ(:METHOD)%\",\"path\":\"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%\",\"protocol\":\"%PROTOCOL%\",\"request_id\":\"%REQ(X-REQUEST-ID)%\",\"requested_server_name\":\"%REQUESTED_SERVER_NAME%\",\"response_code\":\"%RESPONSE_CODE%\",\"response_code_details\":\"%RESPONSE_CODE_DETAILS%\",\"response_flags\":\"%RESPONSE_FLAGS%\",\"start_time\":\"%START_TIME%\",\"upstream_cluster\":\"%UPSTREAM_CLUSTER%\",\"upstream_host\":\"%UPSTREAM_HOST%\",\"upstream_local_address\":\"%UPSTREAM_LOCAL_ADDRESS%\",\"upstream_service_time\":\"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%\",\"user_agent\":\"%REQ(USER-AGENT)%\",\"x_forwarded_for\":\"%REQ(X-FORWARDED-FOR)%\",\"local_rate_limited\":\"%RESP(x-local-rate-limit)%\",\"rate_limit_limit\":\"%RESP(x-ratelimit-limit)%\",\"rate_limit_remaining\":\"%RESP(x-ratelimit-remaining)%\",\"rate_limit_reset\":\"%RESP(x-ratelimit-reset)%\"}", "defaultConfig": { "gatewayTopology": { "numTrustedProxies": 0 }, - "holdApplicationUntilProxyStarts": true + "holdApplicationUntilProxyStarts": true, + "proxyStatsMatcher": { + "inclusionRegexps": [ + ".*http_local_rate_limit.*" + ] + } }, "defaultHttpRetryPolicy": { "attempts": 0 }, - "enablePrometheusMerge": false + "enablePrometheusMerge": false, + "pathNormalization": { + "normalization": "MERGE_SLASHES" + } }, "telemetry": { "enabled": true, @@ -2106,16 +2147,6 @@ "provider": "", "type": "kubernetes:core/v1:Namespace" }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, { "custom": true, "id": "", @@ -2206,6 +2237,66 @@ "provider": "", "type": "gcp:compute/router:Router" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "security.istio.io/v1beta1", + "kind": "AuthorizationPolicy", + "metadata": { + "name": "scan-app-ip-whitelist-0", + "namespace": "cluster-ingress" + }, + "spec": { + "action": "ALLOW", + "rules": [ + { + "from": [ + { + "source": { + "remoteIpBlocks": [ + "", + "1.2.3.4/32", + "5.6.7.8/32", + "11.12.13.14/32", + "4.3.2.1/32", + "8.7.6.5/32", + "8.7.6.4/32", + "9.8.7.6/32", + "11.22.33.45/32", + "12.34.56.78/32", + "2.3.4.5/32" + ] + } + } + ], + "to": [ + { + "operation": { + "hosts": [ + "scan.sv-2.mock.network.canton.global", + "scan.sv-2.mock.global.canton.network.digitalasset.com", + "scan.sv-1.mock.network.canton.global", + "scan.sv-1.mock.global.canton.network.digitalasset.com", + "scan.sv.mock.network.canton.global", + "scan.sv.mock.global.canton.network.digitalasset.com" + ] + } + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "istio-ingress" + } + } + } + }, + "name": "scan-app-ip-whitelist-0", + "provider": "", + "type": "kubernetes:security.istio.io/v1beta1:AuthorizationPolicy" + }, { "custom": true, "id": "", @@ -2275,6 +2366,35 @@ } } } + }, + { + "applyTo": "CLUSTER", + "match": { + "cluster": { + "portNumber": 5010 + } + }, + "patch": { + "operation": "MERGE", + "value": { + "typed_extension_protocol_options": { + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": { + "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions", + "use_downstream_protocol_config": { + "http2_protocol_options": { + "connection_keepalive": { + "interval": "30s", + "timeout": "5s" + }, + "initial_connection_window_size": 52428800, + "initial_stream_window_size": 524288 + }, + "http_protocol_options": {} + } + } + } + } + } } ] } @@ -2283,6 +2403,222 @@ "provider": "", "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "security.istio.io/v1beta1", + "kind": "AuthorizationPolicy", + "metadata": { + "name": "sequencer-p2p-ip-whitelist-0", + "namespace": "cluster-ingress" + }, + "spec": { + "action": "ALLOW", + "rules": [ + { + "from": [ + { + "source": { + "remoteIpBlocks": [ + "", + "1.2.3.4/32", + "5.6.7.8/32", + "11.12.13.14/32", + "2.3.4.5/32" + ] + } + } + ], + "to": [ + { + "operation": { + "hosts": [ + "sequencer-p2p-9.sv-2.mock.network.canton.global", + "sequencer-p2p-9.sv-2.mock.network.canton.global:*", + "sequencer-p2p-9.sv-2.mock.global.canton.network.digitalasset.com", + "sequencer-p2p-9.sv-2.mock.global.canton.network.digitalasset.com:*", + "sequencer-p2p-10.sv-2.mock.network.canton.global", + "sequencer-p2p-10.sv-2.mock.network.canton.global:*", + "sequencer-p2p-10.sv-2.mock.global.canton.network.digitalasset.com", + "sequencer-p2p-10.sv-2.mock.global.canton.network.digitalasset.com:*", + "sequencer-p2p-9.sv-1.mock.network.canton.global", + "sequencer-p2p-9.sv-1.mock.network.canton.global:*", + "sequencer-p2p-9.sv-1.mock.global.canton.network.digitalasset.com", + "sequencer-p2p-9.sv-1.mock.global.canton.network.digitalasset.com:*", + "sequencer-p2p-10.sv-1.mock.network.canton.global", + "sequencer-p2p-10.sv-1.mock.network.canton.global:*", + "sequencer-p2p-10.sv-1.mock.global.canton.network.digitalasset.com", + "sequencer-p2p-10.sv-1.mock.global.canton.network.digitalasset.com:*", + "sequencer-p2p-9.sv.mock.network.canton.global", + "sequencer-p2p-9.sv.mock.network.canton.global:*", + "sequencer-p2p-9.sv.mock.global.canton.network.digitalasset.com", + "sequencer-p2p-9.sv.mock.global.canton.network.digitalasset.com:*", + "sequencer-p2p-10.sv.mock.network.canton.global", + "sequencer-p2p-10.sv.mock.network.canton.global:*", + "sequencer-p2p-10.sv.mock.global.canton.network.digitalasset.com", + "sequencer-p2p-10.sv.mock.global.canton.network.digitalasset.com:*" + ] + } + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "istio-ingress" + } + } + } + }, + "name": "sequencer-p2p-ip-whitelist-0", + "provider": "", + "type": "kubernetes:security.istio.io/v1beta1:AuthorizationPolicy" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "security.istio.io/v1beta1", + "kind": "AuthorizationPolicy", + "metadata": { + "name": "sequencer-pub-ip-whitelist-0", + "namespace": "cluster-ingress" + }, + "spec": { + "action": "ALLOW", + "rules": [ + { + "from": [ + { + "source": { + "remoteIpBlocks": [ + "", + "1.2.3.4/32", + "5.6.7.8/32", + "11.12.13.14/32", + "4.3.2.1/32", + "8.7.6.5/32", + "8.7.6.4/32", + "9.8.7.6/32", + "11.22.33.45/32", + "12.34.56.78/32", + "2.3.4.5/32" + ] + } + } + ], + "to": [ + { + "operation": { + "hosts": [ + "sequencer-9.sv-2.mock.network.canton.global", + "sequencer-9.sv-2.mock.network.canton.global:*", + "sequencer-9.sv-2.mock.global.canton.network.digitalasset.com", + "sequencer-9.sv-2.mock.global.canton.network.digitalasset.com:*", + "sequencer-8.sv-2.mock.network.canton.global", + "sequencer-8.sv-2.mock.network.canton.global:*", + "sequencer-8.sv-2.mock.global.canton.network.digitalasset.com", + "sequencer-8.sv-2.mock.global.canton.network.digitalasset.com:*", + "sequencer-7.sv-2.mock.network.canton.global", + "sequencer-7.sv-2.mock.network.canton.global:*", + "sequencer-7.sv-2.mock.global.canton.network.digitalasset.com", + "sequencer-7.sv-2.mock.global.canton.network.digitalasset.com:*", + "sequencer-10.sv-2.mock.network.canton.global", + "sequencer-10.sv-2.mock.network.canton.global:*", + "sequencer-10.sv-2.mock.global.canton.network.digitalasset.com", + "sequencer-10.sv-2.mock.global.canton.network.digitalasset.com:*", + "sequencer-9.sv-1.mock.network.canton.global", + "sequencer-9.sv-1.mock.network.canton.global:*", + "sequencer-9.sv-1.mock.global.canton.network.digitalasset.com", + "sequencer-9.sv-1.mock.global.canton.network.digitalasset.com:*", + "sequencer-8.sv-1.mock.network.canton.global", + "sequencer-8.sv-1.mock.network.canton.global:*", + "sequencer-8.sv-1.mock.global.canton.network.digitalasset.com", + "sequencer-8.sv-1.mock.global.canton.network.digitalasset.com:*", + "sequencer-7.sv-1.mock.network.canton.global", + "sequencer-7.sv-1.mock.network.canton.global:*", + "sequencer-7.sv-1.mock.global.canton.network.digitalasset.com", + "sequencer-7.sv-1.mock.global.canton.network.digitalasset.com:*", + "sequencer-10.sv-1.mock.network.canton.global", + "sequencer-10.sv-1.mock.network.canton.global:*", + "sequencer-10.sv-1.mock.global.canton.network.digitalasset.com", + "sequencer-10.sv-1.mock.global.canton.network.digitalasset.com:*", + "sequencer-9.sv.mock.network.canton.global", + "sequencer-9.sv.mock.network.canton.global:*", + "sequencer-9.sv.mock.global.canton.network.digitalasset.com", + "sequencer-9.sv.mock.global.canton.network.digitalasset.com:*", + "sequencer-8.sv.mock.network.canton.global", + "sequencer-8.sv.mock.network.canton.global:*", + "sequencer-8.sv.mock.global.canton.network.digitalasset.com", + "sequencer-8.sv.mock.global.canton.network.digitalasset.com:*", + "sequencer-7.sv.mock.network.canton.global", + "sequencer-7.sv.mock.network.canton.global:*", + "sequencer-7.sv.mock.global.canton.network.digitalasset.com", + "sequencer-7.sv.mock.global.canton.network.digitalasset.com:*", + "sequencer-10.sv.mock.network.canton.global", + "sequencer-10.sv.mock.network.canton.global:*", + "sequencer-10.sv.mock.global.canton.network.digitalasset.com", + "sequencer-10.sv.mock.global.canton.network.digitalasset.com:*" + ] + } + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "istio-ingress" + } + } + } + }, + "name": "sequencer-pub-ip-whitelist-0", + "provider": "", + "type": "kubernetes:security.istio.io/v1beta1:AuthorizationPolicy" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "strip-rate-limit-headers", + "namespace": "cluster-ingress" + }, + "spec": { + "configPatches": [ + { + "applyTo": "ROUTE_CONFIGURATION", + "match": { + "context": "GATEWAY" + }, + "patch": { + "operation": "MERGE", + "value": { + "response_headers_to_remove": [ + "x-local-rate-limit", + "x-ratelimit-limit", + "x-ratelimit-remaining", + "x-ratelimit-reset", + "x-envoy-ratelimited" + ] + } + } + } + ], + "workloadSelector": { + "labels": { + "istio": "ingress" + } + } + } + }, + "name": "strip-rate-limit-headers", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, { "custom": true, "id": "", @@ -2334,13 +2670,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2368,13 +2704,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2402,13 +2738,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2436,13 +2772,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2491,6 +2827,132 @@ "provider": "", "type": "kubernetes:security.istio.io/v1:AuthorizationPolicy" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "security.istio.io/v1beta1", + "kind": "AuthorizationPolicy", + "metadata": { + "name": "sv-app-svs-ip-whitelist-0", + "namespace": "cluster-ingress" + }, + "spec": { + "action": "ALLOW", + "rules": [ + { + "from": [ + { + "source": { + "remoteIpBlocks": [ + "", + "1.2.3.4/32", + "5.6.7.8/32", + "11.12.13.14/32", + "2.3.4.5/32" + ] + } + } + ], + "to": [ + { + "operation": { + "hosts": [ + "sv.sv-2.mock.network.canton.global", + "sv.sv-2.mock.global.canton.network.digitalasset.com", + "sv.sv-1.mock.network.canton.global", + "sv.sv-1.mock.global.canton.network.digitalasset.com", + "sv.sv.mock.network.canton.global", + "sv.sv.mock.global.canton.network.digitalasset.com" + ], + "paths": [ + "/api/sv/v0/migration-id", + "/api/sv/v0/onboard/sv/party-migration/authorize", + "/api/sv/v0/onboard/sv/sequencer", + "/api/sv/v0/onboard/sv/start", + "/api/sv/v0/onboard/sv/status/*" + ] + } + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "istio-ingress" + } + } + } + }, + "name": "sv-app-svs-ip-whitelist-0", + "provider": "", + "type": "kubernetes:security.istio.io/v1beta1:AuthorizationPolicy" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "security.istio.io/v1beta1", + "kind": "AuthorizationPolicy", + "metadata": { + "name": "sv-app-validators-ip-whitelist-0", + "namespace": "cluster-ingress" + }, + "spec": { + "action": "ALLOW", + "rules": [ + { + "from": [ + { + "source": { + "remoteIpBlocks": [ + "", + "1.2.3.4/32", + "5.6.7.8/32", + "11.12.13.14/32", + "4.3.2.1/32", + "8.7.6.5/32", + "8.7.6.4/32", + "9.8.7.6/32", + "11.22.33.45/32", + "12.34.56.78/32", + "2.3.4.5/32" + ] + } + } + ], + "to": [ + { + "operation": { + "hosts": [ + "sv.sv-2.mock.network.canton.global", + "sv.sv-2.mock.global.canton.network.digitalasset.com", + "sv.sv-1.mock.network.canton.global", + "sv.sv-1.mock.global.canton.network.digitalasset.com", + "sv.sv.mock.network.canton.global", + "sv.sv.mock.global.canton.network.digitalasset.com" + ], + "paths": [ + "/api/sv/v0/devnet/onboard/validator/prepare", + "/api/sv/v0/dso", + "/api/sv/v0/onboard/validator" + ] + } + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "istio-ingress" + } + } + } + }, + "name": "sv-app-validators-ip-whitelist-0", + "provider": "", + "type": "kubernetes:security.istio.io/v1beta1:AuthorizationPolicy" + }, { "custom": true, "id": "", @@ -2506,13 +2968,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2540,13 +3002,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2574,13 +3036,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2608,13 +3070,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2642,13 +3104,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2676,13 +3138,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2710,13 +3172,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2744,13 +3206,13 @@ "trafficPolicy": { "connectionPool": { "http": { - "http1MaxPendingRequests": 10000, - "http2MaxRequests": 10000, - "maxConcurrentStreams": 10000, + "http1MaxPendingRequests": 20000, + "http2MaxRequests": 20000, + "maxConcurrentStreams": 20000, "maxRequestsPerConnection": 0 }, "tcp": { - "maxConnections": 10000 + "maxConnections": 20000 } }, "loadBalancer": { @@ -2768,11 +3230,10 @@ "id": "", "inputs": { "audience": "https://sv.sv-1.test-stack.canton.network/api", - "clientId": "SV1 SV Backend (Pulumi managed, test-stack)_id", "scopes": [] }, "name": "sv1SvBackendAppAppGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2780,13 +3241,12 @@ "id": "", "inputs": { "audience": "https://ledger_api.sv-1.test-stack.canton.network", - "clientId": "SV1 SV Backend (Pulumi managed, test-stack)_id", "scopes": [ "daml_ledger_api" ] }, "name": "sv1SvBackendAppLedgerGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2794,13 +3254,12 @@ "id": "", "inputs": { "audience": "https://canton.network.global", - "clientId": "SV1 SV Backend (Pulumi managed, test-stack)_id", "scopes": [ "daml_ledger_api" ] }, "name": "sv1SvBackendAppLegacyGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2812,18 +3271,17 @@ "name": "SV1 SV Backend (Pulumi managed, test-stack)" }, "name": "sv1SvBackendApp", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "SV1 UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "sv1UiAppCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -2883,7 +3341,7 @@ ] }, "name": "sv1UiApp", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { @@ -2891,11 +3349,10 @@ "id": "", "inputs": { "audience": "https://validator.sv-1.test-stack.canton.network/api", - "clientId": "SV1 Validator Backend (Pulumi managed, test-stack)_id", "scopes": [] }, "name": "sv1ValidatorBackendAppAppGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2903,13 +3360,12 @@ "id": "", "inputs": { "audience": "https://ledger_api.sv-1.test-stack.canton.network", - "clientId": "SV1 Validator Backend (Pulumi managed, test-stack)_id", "scopes": [ "daml_ledger_api" ] }, "name": "sv1ValidatorBackendAppLedgerGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2917,13 +3373,12 @@ "id": "", "inputs": { "audience": "https://canton.network.global", - "clientId": "SV1 Validator Backend (Pulumi managed, test-stack)_id", "scopes": [ "daml_ledger_api" ] }, "name": "sv1ValidatorBackendAppLegacyGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2935,7 +3390,7 @@ "name": "SV1 Validator Backend (Pulumi managed, test-stack)" }, "name": "sv1ValidatorBackendApp", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { @@ -2943,11 +3398,10 @@ "id": "", "inputs": { "audience": "https://sv.sv-da-1.test-stack.canton.network/api", - "clientId": "SVDA1 SV Backend (Pulumi managed, test-stack)_id", "scopes": [] }, "name": "svda1SvBackendAppAppGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2955,13 +3409,12 @@ "id": "", "inputs": { "audience": "https://ledger_api.sv-da-1.test-stack.canton.network", - "clientId": "SVDA1 SV Backend (Pulumi managed, test-stack)_id", "scopes": [ "daml_ledger_api" ] }, "name": "svda1SvBackendAppLedgerGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2969,13 +3422,12 @@ "id": "", "inputs": { "audience": "https://canton.network.global", - "clientId": "SVDA1 SV Backend (Pulumi managed, test-stack)_id", "scopes": [ "daml_ledger_api" ] }, "name": "svda1SvBackendAppLegacyGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -2987,18 +3439,17 @@ "name": "SVDA1 SV Backend (Pulumi managed, test-stack)" }, "name": "svda1SvBackendApp", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "SVDA1 UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "svda1UiAppCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -3058,7 +3509,7 @@ ] }, "name": "svda1UiApp", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { @@ -3066,11 +3517,10 @@ "id": "", "inputs": { "audience": "https://validator.sv-da-1.test-stack.canton.network/api", - "clientId": "SVDA1 Validator Backend (Pulumi managed, test-stack)_id", "scopes": [] }, "name": "svda1ValidatorBackendAppAppGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -3078,13 +3528,12 @@ "id": "", "inputs": { "audience": "https://ledger_api.sv-da-1.test-stack.canton.network", - "clientId": "SVDA1 Validator Backend (Pulumi managed, test-stack)_id", "scopes": [ "daml_ledger_api" ] }, "name": "svda1ValidatorBackendAppLedgerGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -3092,13 +3541,12 @@ "id": "", "inputs": { "audience": "https://canton.network.global", - "clientId": "SVDA1 Validator Backend (Pulumi managed, test-stack)_id", "scopes": [ "daml_ledger_api" ] }, "name": "svda1ValidatorBackendAppLegacyGrant", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientGrant:ClientGrant" }, { @@ -3110,7 +3558,7 @@ "name": "SVDA1 Validator Backend (Pulumi managed, test-stack)" }, "name": "svda1ValidatorBackendApp", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { @@ -3137,11 +3585,10 @@ "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "Validator1 UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "validator1UiAppCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -3201,18 +3648,17 @@ ] }, "name": "validator1UiApp", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::dev::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "ANS UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "validatorCnsUiCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::validator::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::validator::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -3264,18 +3710,17 @@ ] }, "name": "validatorCnsUi", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::validator::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::validator::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { "custom": true, "id": "", "inputs": { - "authenticationMethod": "none", - "clientId": "Wallet UI (Pulumi managed, test-stack)_id" + "authenticationMethod": "none" }, "name": "validatorWalletUiCredentials", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::validator::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::validator::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/clientCredentials:ClientCredentials" }, { @@ -3327,7 +3772,7 @@ ] }, "name": "validatorWalletUi", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::validator::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:auth0::validator::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "auth0:index/client:Client" }, { diff --git a/cluster/expected/multi-validator/expected.json b/cluster/expected/multi-validator/expected.json index 9a039ab50e..cb34eb1488 100644 --- a/cluster/expected/multi-validator/expected.json +++ b/cluster/expected/multi-validator/expected.json @@ -348,7 +348,7 @@ "env": [ { "name": "CANTON_PARTICIPANT_POSTGRES_SERVER", - "value": "postgres-0" + "value": "postgres-0.multi-validator.svc.cluster.local" }, { "name": "CANTON_PARTICIPANT_POSTGRES_DB", @@ -547,7 +547,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-0 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-0.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " ], "env": [ { @@ -560,7 +560,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -796,7 +796,7 @@ "env": [ { "name": "CANTON_PARTICIPANT_POSTGRES_SERVER", - "value": "postgres-1" + "value": "postgres-1.multi-validator.svc.cluster.local" }, { "name": "CANTON_PARTICIPANT_POSTGRES_DB", @@ -995,7 +995,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-1 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-1.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " ], "env": [ { @@ -1008,7 +1008,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -1244,7 +1244,7 @@ "env": [ { "name": "CANTON_PARTICIPANT_POSTGRES_SERVER", - "value": "postgres-2" + "value": "postgres-2.multi-validator.svc.cluster.local" }, { "name": "CANTON_PARTICIPANT_POSTGRES_DB", @@ -1443,7 +1443,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-2 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-2.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " ], "env": [ { @@ -1456,7 +1456,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -1692,7 +1692,7 @@ "env": [ { "name": "CANTON_PARTICIPANT_POSTGRES_SERVER", - "value": "postgres-3" + "value": "postgres-3.multi-validator.svc.cluster.local" }, { "name": "CANTON_PARTICIPANT_POSTGRES_DB", @@ -1891,7 +1891,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-3 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-3.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " ], "env": [ { @@ -1904,7 +1904,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -2140,7 +2140,7 @@ "env": [ { "name": "CANTON_PARTICIPANT_POSTGRES_SERVER", - "value": "postgres-4" + "value": "postgres-4.multi-validator.svc.cluster.local" }, { "name": "CANTON_PARTICIPANT_POSTGRES_DB", @@ -2339,7 +2339,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-4 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-4.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb participant_2_00\ncreateDb participant_2_01\ncreateDb participant_2_02\ncreateDb participant_2_03\ncreateDb participant_2_04\ncreateDb participant_2_05\ncreateDb participant_2_06\ncreateDb participant_2_07\ncreateDb participant_2_08\ncreateDb participant_2_09\n " ], "env": [ { @@ -2352,7 +2352,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -2646,7 +2646,7 @@ }, { "name": "SPLICE_APP_POSTGRES_HOST", - "value": "postgres-0" + "value": "postgres-0.multi-validator.svc.cluster.local" }, { "name": "SPLICE_APP_POSTGRES_PORT", @@ -2820,7 +2820,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-0 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-0.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " ], "env": [ { @@ -2833,7 +2833,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -3095,7 +3095,7 @@ }, { "name": "SPLICE_APP_POSTGRES_HOST", - "value": "postgres-1" + "value": "postgres-1.multi-validator.svc.cluster.local" }, { "name": "SPLICE_APP_POSTGRES_PORT", @@ -3269,7 +3269,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-1 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-1.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " ], "env": [ { @@ -3282,7 +3282,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -3544,7 +3544,7 @@ }, { "name": "SPLICE_APP_POSTGRES_HOST", - "value": "postgres-2" + "value": "postgres-2.multi-validator.svc.cluster.local" }, { "name": "SPLICE_APP_POSTGRES_PORT", @@ -3718,7 +3718,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-2 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-2.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " ], "env": [ { @@ -3731,7 +3731,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -3993,7 +3993,7 @@ }, { "name": "SPLICE_APP_POSTGRES_HOST", - "value": "postgres-3" + "value": "postgres-3.multi-validator.svc.cluster.local" }, { "name": "SPLICE_APP_POSTGRES_PORT", @@ -4167,7 +4167,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-3 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-3.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " ], "env": [ { @@ -4180,7 +4180,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -4442,7 +4442,7 @@ }, { "name": "SPLICE_APP_POSTGRES_HOST", - "value": "postgres-4" + "value": "postgres-4.multi-validator.svc.cluster.local" }, { "name": "SPLICE_APP_POSTGRES_PORT", @@ -4616,7 +4616,7 @@ "command": [ "bash", "-c", - "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-4 --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " + "\n function createDb() {\n local dbname=\"$1\"\n\n until errmsg=$(psql -h postgres-4.multi-validator.svc.cluster.local --username=cnadmin --dbname=cantonnet -c \"create database $dbname\" 2>&1); do\n if [[ $errmsg == *\"already exists\"* ]]; then\n echo \"Database $dbname already exists. Done.\"\n break\n fi\n\n echo \"trying to create postgres database $dbname, last error: $errmsg\";\n sleep 2;\n done\n }\n\n createDb cantonnet_v_00\ncreateDb cantonnet_v_01\ncreateDb cantonnet_v_02\ncreateDb cantonnet_v_03\ncreateDb cantonnet_v_04\ncreateDb cantonnet_v_05\ncreateDb cantonnet_v_06\ncreateDb cantonnet_v_07\ncreateDb cantonnet_v_08\ncreateDb cantonnet_v_09\n " ], "env": [ { @@ -4629,7 +4629,7 @@ } } ], - "image": "postgres:14", + "image": "postgres:18", "name": "pg-init" } ], @@ -4751,7 +4751,7 @@ } }, "name": "multi-validator-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-multi-validator-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-multi-validator-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -4767,7 +4767,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -4789,79 +4789,12 @@ "type": "random:index/randomPassword:RandomPassword" }, { - "custom": true, - "id": "", - "inputs": { - "length": 16, - "overrideSpecial": "_%@", - "special": true - }, - "name": "multi-validator-postgres-1-passwd", - "provider": "", - "type": "random:index/randomPassword:RandomPassword" - }, - { - "custom": true, - "id": "", - "inputs": { - "length": 16, - "overrideSpecial": "_%@", - "special": true - }, - "name": "multi-validator-postgres-2-passwd", - "provider": "", - "type": "random:index/randomPassword:RandomPassword" - }, - { - "custom": true, - "id": "", - "inputs": { - "length": 16, - "overrideSpecial": "_%@", - "special": true - }, - "name": "multi-validator-postgres-3-passwd", - "provider": "", - "type": "random:index/randomPassword:RandomPassword" - }, - { - "custom": true, - "id": "", - "inputs": { - "length": 16, - "overrideSpecial": "_%@", - "special": true - }, - "name": "multi-validator-postgres-4-passwd", - "provider": "", - "type": "random:index/randomPassword:RandomPassword" - }, - { - "custom": true, + "custom": false, "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Namespace", - "metadata": { - "labels": { - "istio-injection": "enabled" - }, - "name": "multi-validator" - } - }, - "name": "multi-validator", - "provider": "", - "type": "kubernetes:core/v1:Namespace" - }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", + "inputs": {}, + "name": "multi-validator-postgres-0", "provider": "", - "type": "pulumi:pulumi:StackReference" + "type": "canton:network:postgres" }, { "custom": true, @@ -4929,6 +4862,12 @@ } ] }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, "db": { "maxConnections": 1000, "pvcTemplateName": "pg-data-hd", @@ -4957,10 +4896,30 @@ }, "version": "0.3.20" }, - "name": "postgres-0", + "name": "multi-validator-postgres-0", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "length": 16, + "overrideSpecial": "_%@", + "special": true + }, + "name": "multi-validator-postgres-1-passwd", + "provider": "", + "type": "random:index/randomPassword:RandomPassword" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "multi-validator-postgres-1", + "provider": "", + "type": "canton:network:postgres" + }, { "custom": true, "id": "", @@ -5027,6 +4986,12 @@ } ] }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, "db": { "maxConnections": 1000, "pvcTemplateName": "pg-data-hd", @@ -5055,10 +5020,30 @@ }, "version": "0.3.20" }, - "name": "postgres-1", + "name": "multi-validator-postgres-1", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "length": 16, + "overrideSpecial": "_%@", + "special": true + }, + "name": "multi-validator-postgres-2-passwd", + "provider": "", + "type": "random:index/randomPassword:RandomPassword" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "multi-validator-postgres-2", + "provider": "", + "type": "canton:network:postgres" + }, { "custom": true, "id": "", @@ -5125,6 +5110,12 @@ } ] }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, "db": { "maxConnections": 1000, "pvcTemplateName": "pg-data-hd", @@ -5153,10 +5144,30 @@ }, "version": "0.3.20" }, - "name": "postgres-2", + "name": "multi-validator-postgres-2", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "length": 16, + "overrideSpecial": "_%@", + "special": true + }, + "name": "multi-validator-postgres-3-passwd", + "provider": "", + "type": "random:index/randomPassword:RandomPassword" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "multi-validator-postgres-3", + "provider": "", + "type": "canton:network:postgres" + }, { "custom": true, "id": "", @@ -5223,6 +5234,12 @@ } ] }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, "db": { "maxConnections": 1000, "pvcTemplateName": "pg-data-hd", @@ -5251,10 +5268,30 @@ }, "version": "0.3.20" }, - "name": "postgres-3", + "name": "multi-validator-postgres-3", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "length": 16, + "overrideSpecial": "_%@", + "special": true + }, + "name": "multi-validator-postgres-4-passwd", + "provider": "", + "type": "random:index/randomPassword:RandomPassword" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "multi-validator-postgres-4", + "provider": "", + "type": "canton:network:postgres" + }, { "custom": true, "id": "", @@ -5321,6 +5358,12 @@ } ] }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, "db": { "maxConnections": 1000, "pvcTemplateName": "pg-data-hd", @@ -5349,8 +5392,25 @@ }, "version": "0.3.20" }, - "name": "postgres-4", + "name": "multi-validator-postgres-4", "provider": "", "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "istio-injection": "enabled" + }, + "name": "multi-validator" + } + }, + "name": "multi-validator", + "provider": "", + "type": "kubernetes:core/v1:Namespace" } ] diff --git a/cluster/expected/observability/expected.json b/cluster/expected/observability/expected.json index 571499d7a1..4e1fd4ab67 100644 --- a/cluster/expected/observability/expected.json +++ b/cluster/expected/observability/expected.json @@ -73,28 +73,32 @@ "apiVersion": "v1", "data": { "acknowledgement_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: acknowledgements\n folder: canton\n interval: 1m\n rules:\n - uid: aeg75a4mu72tcc\n title: Mediator Acknowledgement Lag\n condition: No recent report\n data:\n - refId: Mediator Acknowledgement Lag\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: time() - (max by (member_prefix) (label_replace(daml_sequencer_block_acknowledgments_micros{member=~\"MED::.*\"}, \"member_prefix\", \"$1\", \"member\", \"(MED::[^:]+::)[^:]+.*\")) / 1e6)\n instant: false\n interval: \"\"\n intervalMs: 30000\n legendFormat: '{{report_publisher}}'\n maxDataPoints: 43200\n range: true\n refId: Mediator Acknowledgement Lag\n - refId: Latest report time lag\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params: []\n type: gt\n operator:\n type: and\n query:\n params:\n - B\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Mediator Acknowledgement Lag\n intervalMs: 1000\n maxDataPoints: 43200\n reducer: last\n refId: Latest report time lag\n settings:\n mode: dropNN\n type: reduce\n - refId: No recent report\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 900\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Latest report time lag\n intervalMs: 1000\n maxDataPoints: 43200\n refId: No recent report\n type: threshold\n dashboardUid: 3ccfda97-fb9c-4413-bc05-5ed6f2c888f7\n panelId: 18\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: 3ccfda97-fb9c-4413-bc05-5ed6f2c888f7\n __panelId__: \"13\"\n description: The mediator {{ $labels.member_prefix }} has not submitted a recent acknowledgement\n severity: critical\n summary: Mediator Acknowledgement lag\n labels: {}\n isPaused: false\n", - "acs-stores_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: acs-size\n folder: splice-stores\n interval: 5m\n rules:\n - uid: df5e4501wuj28a\n title: ACS growth\n condition: D\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: avg_over_time(sum by (node_type) (splice_store_acs_size{namespace=\"sv-1\"})[1d:1h])\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: B\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: avg_over_time(sum by (node_type) (splice_store_acs_size{namespace=\"sv-1\"})[7d:1h] offset 1d) or avg_over_time(sum by (node_type) (splice_store_acs_size{namespace=\"sv-1\"})[1d:1h])\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: B\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: $A/$B\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: math\n - refId: D\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1.2\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n unloadEvaluator:\n params:\n - 1.2\n - 0\n type: lt\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: C\n intervalMs: 1000\n maxDataPoints: 43200\n refId: D\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 15m\n annotations:\n description: sv-1's {{ $labels.node_type }} reports a 20% increase in the last 24h compared to the 7 days average.\n severity: critical\n summary: ACS is growing too fast.\n labels: {}\n isPaused: false\n", - "acs_commitment_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: acs commitment\n folder: canton\n interval: 1m\n rules:\n - uid: ffghab4ms1ds0b\n title: ACS Commitment Checkpoint Delay\n condition: acs commitment checkpoint delay threshold exceeded\n data:\n - refId: acs commitment checkpoint delay\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: min by (namespace, job) (timestamp(daml_participant_sync_commitments_last_locally_checkpointed) - (daml_participant_sync_commitments_last_locally_checkpointed > 0) / 1e6)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: acs commitment checkpoint delay\n - refId: acs commitment checkpoint delay threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1200\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: acs commitment checkpoint delay\n intervalMs: 1000\n maxDataPoints: 43200\n refId: acs commitment checkpoint delay threshold exceeded\n type: threshold\n dashboardUid: eesa90lstfk00b\n panelId: 11\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: eesa90lstfk00b\n __panelId__: \"11\"\n severity: critical\n summary: The job {{ $labels.job }} in namespace {{ $labels.namespace }} is reporting an ACS commitment checkpoint delay\n labels: {}\n isPaused: false\n - uid: ffghab4ms1ds0c\n title: ACS Commitment Delay\n condition: acs commitment delay threshold exceeded\n data:\n - refId: acs commitment delay\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: min by (namespace, job) (timestamp(daml_participant_sync_commitments_last_locally_completed) - (daml_participant_sync_commitments_last_locally_completed > 0) / 1e6)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: acs commitment delay\n - refId: acs commitment delay threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 3600\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: acs commitment delay\n intervalMs: 1000\n maxDataPoints: 43200\n refId: acs commitment delay threshold exceeded\n type: threshold\n dashboardUid: eesa90lstfk00b\n panelId: 10\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: eesa90lstfk00b\n __panelId__: \"10\"\n severity: critical\n summary: The job {{ $labels.job }} in namespace {{ $labels.namespace }} is reporting an ACS commitment delay\n labels: {}\n isPaused: false\n - uid: ffghab4ms1ds0d\n title: ACS Commitment Compute Duration\n condition: acs commitment compute duration threshold exceeded\n data:\n - refId: acs commitment compute duration\n relativeTimeRange:\n from: 1800\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: histogram_quantile(0.99, sum(rate(daml_participant_sync_commitments_compute_duration_seconds[5m])) by (namespace, job, le))\n instant: false\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: true\n refId: acs commitment compute duration\n - refId: acs commitment compute duration max\n datasourceUid: __expr__\n model:\n datasource:\n type: __expr__\n uid: __expr__\n expression: acs commitment compute duration\n intervalMs: 1000\n maxDataPoints: 43200\n reducer: max\n refId: acs commitment compute duration max\n type: reduce\n - refId: acs commitment compute duration threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1200\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: acs commitment compute duration max\n intervalMs: 1000\n maxDataPoints: 43200\n refId: acs commitment compute duration threshold exceeded\n type: threshold\n dashboardUid: eesa90lstfk00b\n panelId: 2\n noDataState: NoData\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: eesa90lstfk00b\n __panelId__: \"2\"\n severity: warning\n summary: The job {{ $labels.job }} in namespace {{ $labels.namespace }} is reporting a high ACS commitment compute duration (p99)\n labels: {}\n isPaused: false\n", + "acs-stores_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: acs-size\n folder: splice-stores\n interval: 5m\n rules:\n - uid: df5e4501wuj28a\n title: ACS growth\n condition: D\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: avg_over_time(sum by (node_type) (splice_history_acs_snapshots_snapshot_size{namespace=\"sv-1\"})[1d:1h])\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: B\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: avg_over_time(sum by (node_type) (splice_history_acs_snapshots_snapshot_size{namespace=\"sv-1\"})[7d:1h] offset 1d) or avg_over_time(sum by (node_type) (splice_history_acs_snapshots_snapshot_size{namespace=\"sv-1\"})[1d:1h])\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: B\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: $A/$B\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: math\n - refId: D\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1.2\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n unloadEvaluator:\n params:\n - 1.2\n - 0\n type: lt\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: C\n intervalMs: 1000\n maxDataPoints: 43200\n refId: D\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 15m\n annotations:\n description: sv-1's {{ $labels.node_type }} reports a 20% increase in the last 24h compared to the 7 days average.\n severity: critical\n summary: ACS is growing too fast.\n labels: {}\n isPaused: false\n", + "acs_commitment_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: acs commitment\n folder: canton\n interval: 1m\n rules:\n - uid: ffghab4ms1ds0b\n title: ACS Commitment Checkpoint Delay\n condition: acs commitment checkpoint delay threshold exceeded\n data:\n - refId: acs commitment checkpoint delay\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: min by (namespace, job) (timestamp(daml_participant_sync_commitments_last_locally_checkpointed) - (daml_participant_sync_commitments_last_locally_checkpointed > 0) / 1e6)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: acs commitment checkpoint delay\n - refId: acs commitment checkpoint delay threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 2400\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: acs commitment checkpoint delay\n intervalMs: 1000\n maxDataPoints: 43200\n refId: acs commitment checkpoint delay threshold exceeded\n type: threshold\n dashboardUid: eesa90lstfk00b\n panelId: 11\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: eesa90lstfk00b\n __panelId__: \"11\"\n severity: critical\n summary: The job {{ $labels.job }} in namespace {{ $labels.namespace }} is reporting an ACS commitment checkpoint delay\n labels: {}\n isPaused: false\n - uid: ffghab4ms1ds0c\n title: ACS Commitment Delay\n condition: acs commitment delay threshold exceeded\n data:\n - refId: acs commitment delay\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: min by (namespace, job) (timestamp(daml_participant_sync_commitments_last_locally_completed) - (daml_participant_sync_commitments_last_locally_completed > 0) / 1e6)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: acs commitment delay\n - refId: acs commitment delay threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 3600\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: acs commitment delay\n intervalMs: 1000\n maxDataPoints: 43200\n refId: acs commitment delay threshold exceeded\n type: threshold\n dashboardUid: eesa90lstfk00b\n panelId: 10\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: eesa90lstfk00b\n __panelId__: \"10\"\n severity: critical\n summary: The job {{ $labels.job }} in namespace {{ $labels.namespace }} is reporting an ACS commitment delay\n labels: {}\n isPaused: false\n - uid: ffghab4ms1ds0d\n title: ACS Commitment Compute Duration\n condition: acs commitment compute duration threshold exceeded\n data:\n - refId: acs commitment compute duration\n relativeTimeRange:\n from: 1800\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: histogram_quantile(0.99, sum(rate(daml_participant_sync_commitments_compute_duration_seconds[5m])) by (namespace, job, le))\n instant: false\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: true\n refId: acs commitment compute duration\n - refId: acs commitment compute duration max\n datasourceUid: __expr__\n model:\n datasource:\n type: __expr__\n uid: __expr__\n expression: acs commitment compute duration\n intervalMs: 1000\n maxDataPoints: 43200\n reducer: max\n refId: acs commitment compute duration max\n type: reduce\n - refId: acs commitment compute duration threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1200\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: acs commitment compute duration max\n intervalMs: 1000\n maxDataPoints: 43200\n refId: acs commitment compute duration threshold exceeded\n type: threshold\n dashboardUid: eesa90lstfk00b\n panelId: 2\n noDataState: NoData\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: eesa90lstfk00b\n __panelId__: \"2\"\n severity: warning\n summary: The job {{ $labels.job }} in namespace {{ $labels.namespace }} is reporting a high ACS commitment compute duration (p99)\n labels: {}\n isPaused: false\n", "acs_snapshots_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: acs-snapshots\n folder: canton-network\n interval: 5m\n rules:\n - uid: adwt1yr5xuscge\n title: Saving ACS snapshot taking too long\n condition: too_long\n data:\n - refId: latency\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n # The metric only has one sample every ~3h (more if catching up), the 10m range in the expression\n # has to be longer than the alert evaluation+pending interval.\n expr: histogram_quantile(0.99, rate(splice_history_acs_snapshots_latency_save_duration_seconds[10m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: latency\n - refId: too_long\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 5400\n - 0\n type: gt\n operator:\n type: and\n query:\n params: [ ]\n reducer:\n params: [ ]\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: latency\n intervalMs: 1000\n maxDataPoints: 43200\n refId: too_long\n type: threshold\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: The time to save an incremental snapshot was too high. This step scales with the size of the ACS.\n runbook_url: \"\"\n severity: warning\n summary: Saving an ACS snapshot took longer than {{ humanizeDuration 5400 }} in {{ index $labels \"namespace\" }}'s Scan\n labels:\n \"\": \"\"\n gcloud_filter: resource.labels.namespace_name=%22{{ index \"namespace\" }}%22%0A\n isPaused: false\n - uid: t2ln0sn2bdfmcd\n title: Updating ACS snapshot taking too long\n condition: too_long\n data:\n - refId: latency\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n # The metric has one sample every ~30sec (more if catching up).\n # Here we alert on the average update latency, as the 99th percentile is overly noisy.\n expr: histogram_avg(rate(splice_history_acs_snapshots_latency_update_duration_seconds[10m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: latency\n - refId: too_long\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 3\n - 0\n type: gt\n operator:\n type: and\n query:\n params: [ ]\n reducer:\n params: [ ]\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: latency\n intervalMs: 1000\n maxDataPoints: 43200\n refId: too_long\n type: threshold\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: The time to update an incremental snapshot was too high. This step scales with the TPS.\n runbook_url: \"\"\n severity: warning\n summary: Updating an ACS snapshot took longer than {{ humanizeDuration 3 }} on average over the past 10min in {{ index $labels \"namespace\" }}'s Scan\n labels:\n \"\": \"\"\n gcloud_filter: resource.labels.namespace_name=%22{{ index \"namespace\" }}%22%0A\n isPaused: false\n", "automation_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: automation\n folder: canton-network\n interval: 5m\n rules:\n - uid: fe73c0e7-dcb3-4975-a7d1-04ed8da087be\n title: Automation Failures\n condition: threshold\n data:\n - refId: total\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by(namespace, node_type, trigger_name, migration) (delta(splice_trigger_completed_total{trigger_name=~\".+\", outcome=~\".+\"}[10m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: total\n - refId: failures\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: sum by(namespace, node_type, trigger_name, migration) (delta(splice_trigger_completed_total{trigger_name=~\".+\", outcome=~\"failure\"}[10m])) or on() vector(0)\n hide: false\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: failures\n - refId: failure_pct\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: ${failures} / ${total} * 100\n hide: false\n intervalMs: 1000\n maxDataPoints: 43200\n refId: failure_pct\n type: math\n - refId: threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: failure_pct\n hide: false\n intervalMs: 1000\n maxDataPoints: 43200\n refId: threshold\n type: threshold\n dashboardUid: a3e1385f-6f03-46d9-908c-34aca0f507a6\n panelId: 14\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: a3e1385f-6f03-46d9-908c-34aca0f507a6\n __panelId__: \"14\"\n description: The {{ index $labels \"trigger_name\" }} for the {{ index $labels \"node_type\" }} app in the {{ index $labels \"namespace\" }} namespace on migration id {{ index $labels \"migration\" }} experienced {{ index $values \"failure_pct\" }}% failures in the last 10 minutes.\n severity: |-\n {{- if (gt $values.failure_pct.Value 50.0) -}}\n critical\n {{- else -}}\n warning\n {{- end -}}\n summary: '{{ index $values \"failure_pct\" }}% fatal errors occurred in {{ index $labels \"namespace\" }} - {{ index $labels \"trigger_name\" }} automation trigger'\n labels:\n gcloud_filter: 'resource.labels.namespace_name=%22{{ index $labels \"namespace\" }}%22%0A%22{{ index $labels \"trigger_name\" }}%22'\n isPaused: false\n - uid: ady2ks9ehbw1sb\n title: Busy task-based automation\n condition: threshold\n data:\n - refId: runs\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by(namespace, node_type, node_name, job, trigger_name, migration, party) (rate(splice_trigger_completed_total{trigger_name!~\"ScanVerdictStoreIngestion|ScanHistoryBackfillingTrigger|AcsSnapshotTrigger|ScanBackfillAggregatesTrigger|TxLogBackfillingTrigger|ReportValidatorLicenseMetricsExportTrigger\"}[5m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: runs\n - refId: threshold\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: runs\n intervalMs: 1000\n maxDataPoints: 43200\n refId: threshold\n type: threshold\n dashboardUid: a3e1385f-6f03-46d9-908c-34aca0f507a6\n panelId: 14\n noDataState: OK\n execErrState: OK\n for: 5m\n annotations:\n __dashboardUid__: a3e1385f-6f03-46d9-908c-34aca0f507a6\n __panelId__: \"14\"\n description: The {{ index $labels \"trigger_name\" }} for the {{ index $labels \"node_type\" }} app in the {{ index $labels \"namespace\" }} namespace on migration id {{ index $labels \"migration\" }} experienced {{ index $values \"runs\" }} runs per second in the last 5 minutes.\n runbook_url: \"\"\n severity: |-\n {{- if and $values.runs (gt $values.runs.Value 2.0) -}}\n critical\n {{- else -}}\n warning\n {{- end -}}\n summary: '{{ index $values \"runs\" }} trigger runs per second occurred in {{ index $labels \"namespace\" }} - {{ index $labels \"trigger_name\" }} automation trigger'\n labels:\n \"\": \"\"\n gcloud_filter: resource.labels.namespace_name=%22{{ index \"namespace\" }}%22%0A%22{{ index \"trigger_name\" }}%22\n isPaused: false\n - uid: edz6eq1kc543ke\n title: Busy polling-based automation\n condition: threshold\n data:\n - refId: runs\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by(namespace, node_type, node_name, job, trigger_name, migration, party) (rate(splice_trigger_iterations_total{trigger_name!~\"ScanVerdictStoreIngestion|ScanHistoryBackfillingTrigger|AcsSnapshotTrigger|ScanBackfillAggregatesTrigger|TxLogBackfillingTrigger\"}[5m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: runs\n - refId: threshold\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: runs\n intervalMs: 1000\n maxDataPoints: 43200\n refId: threshold\n type: threshold\n dashboardUid: a3e1385f-6f03-46d9-908c-34aca0f507a6\n panelId: 14\n noDataState: OK\n execErrState: OK\n for: 5m\n annotations:\n __dashboardUid__: a3e1385f-6f03-46d9-908c-34aca0f507a6\n __panelId__: \"14\"\n description: The {{ index $labels \"trigger_name\" }} for the {{ index $labels \"node_type\" }} app in the {{ index $labels \"namespace\" }} namespace on migration id {{ index $labels \"migration\" }} experienced {{ index $values \"runs\" }} runs per second in the last 5 minutes.\n runbook_url: \"\"\n severity: |-\n {{- if and $values.runs (gt $values.runs.Value 2.0) -}}\n critical\n {{- else -}}\n warning\n {{- end -}}\n summary: '{{ index $values \"runs\" }} trigger runs per second occurred in {{ index $labels \"namespace\" }} - {{ index $labels \"trigger_name\" }} automation trigger'\n labels:\n \"\": \"\"\n gcloud_filter: resource.labels.namespace_name=%22{{ index \"namespace\" }}%22%0A%22{{ index \"trigger_name\" }}%22\n isPaused: false\n - uid: fe12i7xur3eo0d\n title: Backfilling not progressing\n condition: C\n data:\n - refId: Max (rate vs completed)\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by(namespace)(rate(splice_history_backfilling_transaction_count[5m]) > 0 or splice_history_backfilling_completed)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: Max (rate vs completed)\n - refId: C\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1e-11\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: [ ]\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Max (rate vs completed)\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n description: \"\"\n runbook_url: \"\"\n summary: History backfilling is not making any progress in {{ index $labels \"namespace\" }}\n labels:\n \"\": \"\"\n isPaused: false\n - uid: bel66uf182ha8e\n title: TxLog backfilling not progressing\n condition: C\n data:\n - refId: Max (rate vs completed)\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: |-\n max by(namespace)(\n rate(splice_history_txlog_backfilling_transaction_count[5m]) > 0 or\n splice_history_txlog_backfilling_completed or\n # TxLog backfilling can't start until history backfilling is completed\n absent(splice_history_backfilling_completed) or\n # Note: \"== bool\" is a boolean operator, \"==\" is a filter\n (splice_history_backfilling_completed == bool 0)\n )\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: Max (rate vs completed)\n - refId: C\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1e-11\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: [ ]\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Max (rate vs completed)\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n description: \"\"\n runbook_url: \"\"\n summary: TxLog backfilling is not making any progress in {{ index $labels \"namespace\" }}\n labels:\n \"\": \"\"\n isPaused: false\n - uid: fentlrcbcrsaoa\n title: Delegateless trigger contention\n condition: threshold\n data:\n - refId: total_attempts\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum(rate(splice_trigger_attempted_total{isDsoDelegateTrigger=\"true\", node_type=\"sv\"}[30m])) by (namespace)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: total_attempts\n - refId: total_contention\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: sum(rate(splice_trigger_attempted_total{statusCode!~\"OK\", isDsoDelegateTrigger=\"true\", node_type=\"sv\", contentionFailure=\"true\"}[30m])) by (namespace)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: total_contention\n - refId: threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: [ ]\n reducer:\n params: [ ]\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: (${total_contention} > 0.2 * ${total_attempts}) && (${total_contention} > 0)\n intervalMs: 1000\n maxDataPoints: 43200\n refId: threshold\n type: math\n # Alert also reports noData when there is no contention.\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: '{{ index $labels \"namespace\" }} had more than 20% contention in the last 30 minutes.'\n isPaused: false\n - uid: fentlrcbcrsaob\n title: Delegateless trigger non-local contention errors\n condition: threshold\n data:\n - refId: total_attempts\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum(rate(splice_trigger_attempted_total{isDsoDelegateTrigger=\"true\", node_type=\"sv\"}[30m])) by (namespace)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: total_attempts\n - refId: total_non_local_contention_errors\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: sum(rate(splice_trigger_attempted_total{statusCode!~\"OK\", isDsoDelegateTrigger=\"true\", node_type=\"sv\", contentionFailure=\"true\", errorCodeId!~\"UNKNOWN_CONTRACT_SYNCHRONIZERS|LOCAL_.*\"}[30m])) by (namespace)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: total_non_local_contention_errors\n - refId: threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: [ ]\n reducer:\n params: [ ]\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: (${total_non_local_contention_errors} > 0.05 * ${total_attempts}) && (${total_non_local_contention_errors} > 0)\n intervalMs: 1000\n maxDataPoints: 43200\n refId: threshold\n type: math\n # Alert also reports noData when there is no contention.\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: '{{ index $labels \"namespace\" }} had more than 5% non-local contention errors in the last 30 minutes.'\n isPaused: false\n - uid: df3t10f23cm68c\n title: 'Ingestion saturation'\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: histogram_avg(sum by (le, namespace,job,store_name,store_party,synchronizer_id) (rate(splice_store_ingestion_batch_size{namespace=~\"sv\"}[20m])))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 80\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: [ ]\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n noDataState: OK\n execErrState: OK\n for: 30m\n annotations:\n description: |\n The ingestion batch size has exceeded 80 entries/batch for a prolonged time in {{ index $labels \"namespace\" }}'s {{ index $labels \"node_type\" }}.\n This might mean that ingestion is close to not being able to keep up with ledger activity.\n This can also happen when an SV was just onboarded and is catching up with the ACS.\n summary: The ingestion batch size has exceeded the expected size for a prolonged time.\n isPaused: false\n - uid: bgh7m2a8cy3x4a\n title: Automation service unhealthy\n condition: threshold\n data:\n - refId: health\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: min by(namespace, node_type, automation_service) (splice_automation_background_service_health)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: health\n - refId: threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params:\n - threshold\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: health\n intervalMs: 1000\n maxDataPoints: 43200\n refId: threshold\n type: threshold\n dashboardUid: a3e1385f-6f03-46d9-908c-34aca0f507a6\n panelId: 32\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: a3e1385f-6f03-46d9-908c-34aca0f507a6\n __panelId__: \"32\"\n description: The {{ index $labels \"automation_service\" }} background service of the {{ index $labels \"node_type\" }} app in the {{ index $labels \"namespace\" }} namespace has been continuously reporting unhealthy for more than 5 minutes.\n summary: '{{ index $labels \"namespace\" }} - {{ index $labels \"node_type\" }} - {{ index $labels \"automation_service\" }} automation service is unhealthy'\n labels:\n gcloud_filter: 'resource.labels.namespace_name=%22{{ index $labels \"namespace\" }}%22'\n isPaused: false\n", - "cantonbft_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: canton-bft\n folder: canton\n interval: 1m\n rules:\n - uid: c7d1e9f4a6b2\n title: CantonBft Blacklisted Member\n condition: blacklisted_threshold\n data:\n - refId: blacklisted_epochs\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (blacklist_sequencer)(daml_sequencer_bftordering_blacklist_sequencer)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: blacklisted_epochs\n - refId: blacklisted_threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: blacklisted_epochs\n hide: false\n refId: blacklisted_threshold\n type: threshold\n dashboardUid: \"\"\n panelId: 0\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: The CantonBft member {{ $labels.blacklist_sequencer }} is blacklisted.\n severity: warning\n summary: CantonBft member {{ $labels.blacklist_sequencer }} is blacklisted for {{ index $values \"blacklisted_epochs\" }} epochs\n labels: {}\n isPaused: false\n - uid: e2a9c7b4f1d6\n title: CantonBft Ingress Requests Queued\n condition: mempool_size_threshold\n data:\n - refId: mempool_size\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (namespace, job)(daml_sequencer_bftordering_ingress_requests_queued)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: mempool_size\n - refId: mempool_size_threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - $CANTON_BFT_MEMPOOL_SIZE_THRESHOLD\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: mempool_size\n hide: false\n refId: mempool_size_threshold\n type: threshold\n dashboardUid: \"\"\n panelId: 0\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: The number of requests in the mempool of CantonBft for the namespace {{ $labels.namespace }} is above the configured threshold.\n severity: warning\n summary: CantonBft mempool size {{ index $values \"mempool_size\" }} in namespace {{ $labels.namespace }}\n labels: {}\n isPaused: false\n", + "cantonbft_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: canton-bft\n folder: canton\n interval: 1m\n rules:\n - uid: c7d1e9f4a6b2\n title: CantonBft Blacklisted Member\n condition: blacklisted_threshold\n data:\n - refId: blacklisted_epochs\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (blacklist_sequencer)(daml_sequencer_bftordering_blacklist_sequencer)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: blacklisted_epochs\n - refId: blacklisted_threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: blacklisted_epochs\n hide: false\n refId: blacklisted_threshold\n type: threshold\n dashboardUid: \"\"\n panelId: 0\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: The CantonBft member {{ $labels.blacklist_sequencer }} is blacklisted.\n severity: warning\n summary: CantonBft member {{ $labels.blacklist_sequencer }} is blacklisted for {{ index $values \"blacklisted_epochs\" }} epochs\n labels: {}\n isPaused: false\n - uid: e2a9c7b4f1d6\n title: CantonBft Ingress Requests Queued\n condition: mempool_size_threshold\n data:\n - refId: mempool_size\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (namespace, job)(daml_sequencer_bftordering_ingress_requests_queued)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: mempool_size\n - refId: mempool_size_threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 100\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: mempool_size\n hide: false\n refId: mempool_size_threshold\n type: threshold\n dashboardUid: \"\"\n panelId: 0\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: The number of requests in the mempool of CantonBft for the namespace {{ $labels.namespace }} is above the configured threshold.\n severity: warning\n summary: CantonBft mempool size {{ index $values \"mempool_size\" }} in namespace {{ $labels.namespace }}\n labels: {}\n isPaused: false\n", "cometbft_deleted_alerts.yaml": "apiVersion: 1\ndeleteRules:\n - orgId: 1\n # Cometbft Consensus\n uid: fa4b8a18-e4be-4d19-a5af-90c7326fd935\n - orgId: 1\n # Cometbft Height Not Advancing\n uid: a3b47fca-0982-491b-80bb-0c70f04dcd8b\n - orgId: 1\n # Cometbft Not Voting\n uid: a3f23df0-40d5-41d1-8b4b-03aa8c62c030\n - orgId: 1\n # Reached Expected Maximum CometBFT Block Rate\n uid: adl2npxte6u4gd\n - orgId: 1\n # CometBFT is not pruning old blocks\n uid: ddlrp7f1f8l4wf\n", "deleted_alerts.yaml": "apiVersion: 1\ndeleteRules:\n - orgId: 1\n # SV Status - mediator -falling behind\n uid: f15539f2-f6ea-4de4-8f7c-c56434d5bd52\n - orgId: 1\n # wasted traffic (sustained)\n uid: adw5rd048zf9cb\n - orgId: 1\n # wasted traffic\n uid: adw5rd048zf9ca\n - orgId: 1\n # Confirmation Requests By Member (replaced by sequencer caps, #2917)\n uid: 88b8827c8d09\n - orgId: 1\n # Total Confirmation Requests (replaced by sequencer caps, #2917)\n uid: 5dcddc9a5487\n - orgId: 1\n uid: aescandisagree1\ndeleteContactPoints:\n - uid: grafana-default-email\n orgId: 1\n", "deployment_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: deployments\n folder: canton-network\n interval: 1m\n rules:\n - uid: adkhl6u18pqtce\n title: Failing Stacks\n condition: D\n data:\n - refId: A\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n exemplar: true\n expr: splice_deployment_pulumi_stack_status{state=\"failed\"}\n format: time_series\n instant: true\n interval: \"\"\n intervalMs: 30000\n legendFormat: stacks_active\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: D\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params:\n - D\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: D\n type: threshold\n dashboardUid: QP_wDqDnz\n panelId: 11\n noDataState: OK\n execErrState: OK\n for: 5m\n annotations:\n __dashboardUid__: QP_wDqDnz\n __panelId__: \"11\"\n description: The pulumi operator failed to update the {{ $labels.stack }} stack. Check the logs for the deployment.\n runbook_url: \"\"\n severity: critical\n summary: '{{ $labels.stack }} stack failed to update'\n labels:\n \"\": \"\"\n isPaused: false\n - uid: bdndg5g3x4kxsf\n title: Deployments running\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: splice_deployment_pulumi_stack_condition{type=\"Reconciling\"}\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n dashboardUid: QP_wDqDnz\n panelId: 27\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: QP_wDqDnz\n __panelId__: \"27\"\n description: A deployment is currently running for the {{ $labels.stack }} stack.\n runbook_url: \"\"\n severity: info\n summary: Stack {{ $labels.stack }} is being updated\n labels:\n \"\": \"\"\n isPaused: false\n", - "dso_missed_confirmations_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: dso party missed confirmations\n folder: canton-network\n interval: 1m\n rules:\n - uid: ddso1miss0conf0a\n title: DSO Party Missed Confirmations\n condition: missed confirmations threshold exceeded\n data:\n - refId: missed confirmation rate\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by (namespace) (increase(daml_mediator_timeout_non_responsive_participants_total{party=~\"DSO::.*\"}[10m])) / sum by (namespace) (increase(daml_mediator_requests_total[10m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: missed confirmation rate\n - refId: missed confirmations threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.01\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: missed confirmation rate\n intervalMs: 1000\n maxDataPoints: 43200\n refId: missed confirmations threshold exceeded\n type: threshold\n dashboardUid: cnck4jp\n panelId: 1\n noDataState: OK\n execErrState: Alerting\n for: 0m\n annotations:\n __dashboardUid__: cnck4jp\n __panelId__: \"1\"\n severity: critical\n summary: The DSO party in namespace {{ $labels.namespace }} is missing more than 1% of its confirmations over the last 10 minutes.\n labels: {}\n isPaused: false\n", + "dso_missed_confirmations_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: dso party missed confirmations\n folder: canton-network\n interval: 1m\n rules:\n - uid: ddso1miss0conf0a\n title: DSO Party Missed Confirmations\n condition: missed confirmations threshold exceeded\n data:\n - refId: missed_confirmations\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (party) (increase(daml_mediator_timeout_non_responsive_participants_total{party=~\"DSO::.*\"}[10m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: missed_confirmations\n - refId: missed confirmations threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: missed_confirmations\n intervalMs: 1000\n maxDataPoints: 43200\n refId: missed confirmations threshold exceeded\n type: threshold\n dashboardUid: cnck4jp\n panelId: 1\n noDataState: OK\n execErrState: Alerting\n for: 0m\n annotations:\n __dashboardUid__: cnck4jp\n __panelId__: \"1\"\n severity: critical\n summary: The DSO party missed {{ index $values \"missed_confirmations\" }}, more than 0 of its confirmations over the last 10 minutes.\n labels: {}\n isPaused: false\n", "extra_k8s_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: storage\n folder: k8s\n interval: 5m\n rules:\n - uid: adlrbu5kog0sga\n title: KubePersistentVolumeTooFull\n condition: free_space_below_threshold\n data:\n - refId: free_space\n relativeTimeRange:\n from: 360\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: kubelet_volume_stats_available_bytes{job=\"kubelet\",metrics_path=\"/metrics\",namespace=~\".*\"} / kubelet_volume_stats_capacity_bytes{job=\"kubelet\",metrics_path=\"/metrics\",namespace=~\".*\"}\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: free_space\n - refId: free_space_below_threshold\n relativeTimeRange:\n from: 360\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.15\n - 0\n type: lt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: free_space\n intervalMs: 1000\n maxDataPoints: 43200\n refId: free_space_below_threshold\n type: threshold\n dashboardUid: 919b92a8e8041bd567af9edab12c840c\n panelId: 2\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: 919b92a8e8041bd567af9edab12c840c\n __panelId__: \"2\"\n severity: warning\n description: The PersistentVolume claimed by {{ index $labels \"persistentvolumeclaim\" }} in Namespace {{ index $labels \"namespace\" }} is running out of disk space. Currently {{ humanizePercentage (index $values \"free_space\").Value }} is available.\n runbook_url: https://runbooks.prometheus-operator.dev/runbooks/kubernetes/kubepersistentvolumefillingup\n summary: PersistentVolume is too full.\n isPaused: false\n", + "global-sync-health_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: global-synchronizer-health\n folder: canton-network\n interval: 1m\n rules:\n # Confirmation requests that were sequenced but never processed by the mediator\n # (e.g. dropped due to CometBFT replays). There is no dedicated Canton metric for\n # discarded events yet, so we approximate it as the difference between sequenced\n # send-confirmation-request events and requests received by the mediator.\n - uid: gsh0discarded0cr\n title: High Rate of Discarded Confirmation Requests\n condition: discarded fraction threshold exceeded\n data:\n - refId: discarded_fraction\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: (sum by (namespace) (rate(daml_sequencer_block_events_total{type=\"send-confirmation-request\"}[10m])) - sum by (namespace) (rate(daml_mediator_requests_total[10m]))) / sum by (namespace) (rate(daml_sequencer_block_events_total{type=\"send-confirmation-request\"}[10m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: discarded_fraction\n - refId: discarded fraction threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.2\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: discarded_fraction\n intervalMs: 1000\n maxDataPoints: 43200\n refId: discarded fraction threshold exceeded\n type: threshold\n dashboardUid: fe8wt04z620aof\n panelId: 10\n noDataState: OK\n execErrState: OK\n for: 10m\n annotations:\n __dashboardUid__: fe8wt04z620aof\n __panelId__: \"10\"\n runbook_url: \"\"\n severity: warning\n summary: \"{{ $labels.namespace }} discarded more than 0.2 of sequenced confirmation requests over the last 10 minutes\"\n description: 'A significant fraction of sequenced confirmation requests was never processed by the mediator. This typically matches CometBFT replays.'\n labels: {}\n isPaused: false\n # Failure rate of confirmation requests, as observed by the mediator.\n # Can increase for non-network reasons, but is relatively unlikely to.\n - uid: gsh0failedcr0rel\n title: High Failed Confirmation Request Rate\n condition: failure rate threshold exceeded\n data:\n - refId: failure_rate\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: 1 - sum by (namespace) (rate(daml_mediator_approved_requests_total[30m])) / sum by (namespace) (rate(daml_mediator_requests_total{duplicate_reject=\"false\"}[30m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: failure_rate\n - refId: failure rate threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: failure_rate\n intervalMs: 1000\n maxDataPoints: 43200\n refId: failure rate threshold exceeded\n type: threshold\n dashboardUid: fe8wt04z620aof\n panelId: 8\n noDataState: OK\n execErrState: OK\n for: 5m\n annotations:\n __dashboardUid__: fe8wt04z620aof\n __panelId__: \"8\"\n runbook_url: \"\"\n severity: warning\n summary: \"{{ $labels.namespace }} saw more than 0.1 of confirmation requests fail over the last 30 minutes\"\n description: 'Confirmation requests are failing at a high rate. Can happen for non-network reasons, but is relatively unlikely to.'\n labels: {}\n isPaused: false\n # Overall TPS (approved confirmation requests per second), compared to the\n # previous 30m window. Can drop for non-network reasons, but a large relative\n # drop is a strong signal for network issues.\n - uid: gsh0tps0drop0rel\n title: Significant TPS Drop\n condition: tps dropped\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by (namespace) (rate(daml_mediator_approved_requests_total[30m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: B\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by (namespace) (rate(daml_mediator_approved_requests_total[30m] offset 30m))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: B\n - refId: C\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: $A < $B * 0.5\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: math\n - refId: tps dropped\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: C\n intervalMs: 1000\n maxDataPoints: 43200\n refId: tps dropped\n type: threshold\n dashboardUid: fe8wt04z620aof\n panelId: 4\n noDataState: OK\n execErrState: OK\n for: 5m\n annotations:\n __dashboardUid__: fe8wt04z620aof\n __panelId__: \"4\"\n runbook_url: \"\"\n severity: warning\n summary: \"{{ $labels.namespace }} saw TPS drop below 0.5 of the previous 30 minute window\"\n description: 'The rate of approved confirmation requests over the last 30m dropped significantly compared to the previous 30m window. Can happen for non-network reasons, but is relatively unlikely to.'\n labels: {}\n isPaused: false\n", + "istio-rate-limiting_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: istio-rate-limit\n folder: platform\n interval: 5m\n rules:\n - uid: cfurlpifrmvi8w\n title: Enforced requests\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 21600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\"sv.*\"}[$__rate_interval])) by (namespace, pod)\n instant: true\n interval: \"\"\n intervalMs: 30000\n legendFormat: '{{namespace}} - {{pod}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: cnr56dj\n panelId: 9\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n runbook_url: \"\"\n severity: warning\n description: Envoy local rate limiting is active, requests are being rejected. Check the \"Istio Rate Limiting\" dashboard.\n summary: Envoy is rate limiting requests in {{ $labels.namespace }} on pod {{ $labels.pod }}.\n labels:\n \"\": \"\"\n gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0AjsonPayload.response_code=429\n isPaused: false\n", "load-tester_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: load-tester\n folder: canton-network\n interval: 1m\n rules:\n - uid: a1dbc4e8-7941-4351-9a14-f8573fd2be2b\n title: K6 Request Failure Rate Exceeded\n condition: D\n data:\n - refId: A\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: sum(k6_http_reqs_total)\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: B\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n disableTextWrap: false\n editorMode: builder\n expr: sum(k6_http_reqs_total{expected_response=\"false\"}) or on() vector(0)\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: B\n useBackend: false\n - refId: C\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: ($B / $A) * 100\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: math\n - refId: D\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 50\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: C\n intervalMs: 1000\n maxDataPoints: 43200\n refId: D\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 1m\n annotations:\n description: ''\n runbook_url: ''\n summary: The k6 load tester experienced HTTP request failures at a rate past the acceptable threshold\n labels:\n gcloud_filter: ''\n isPaused: false\n - uid: ddogbytusmz9cb\n title: K6 Throughput Below Threshold\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 3600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: rate(k6_transfers_completed_total{scenario=\"generate_load\"}[$__rate_interval])\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: C\n relativeTimeRange:\n from: 3600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.123\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: B\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n - refId: B\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: A\n hide: false\n reducer: mean\n refId: B\n type: reduce\n dashboardUid: ccbb2351-2ae2-462f-ae0e-f2c893ad1028\n panelId: 28\n noDataState: Alerting\n execErrState: Alerting\n for: 15m # give some time for restarts\n annotations:\n __dashboardUid__: ccbb2351-2ae2-462f-ae0e-f2c893ad1028\n __panelId__: \"28\"\n severity: warning\n description: ''\n runbook_url: ''\n summary: Transaction rate from the k6 load tester is lower than expected threshold\n labels:\n '': ''\n isPaused: false\n", "mining-rounds_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: mining-rounds\n folder: canton-network\n interval: 1m\n rules:\n - uid: ae74w21qca2o0e\n title: Open Mining Rounds Not Advancing\n condition: No new round\n data:\n - refId: Round number diff\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: max(max(max_over_time(splice_sv_dso_store_latest_open_mining_round[30m])) by (namespace) - min(min_over_time(splice_sv_dso_store_latest_open_mining_round[30m])) by (namespace))\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: Round number diff\n useBackend: false\n - refId: No new round\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.9\n type: gt\n datasource:\n type: __expr__\n uid: __expr__\n expression: Round number diff\n intervalMs: 1000\n maxDataPoints: 43200\n refId: No new round\n type: threshold\n dashboardUid: ed94a332-4fa7-47f8-982b-fc997381175b\n panelId: 1\n noDataState: Alerting\n execErrState: OK\n for: 1m\n annotations:\n __dashboardUid__: ed94a332-4fa7-47f8-982b-fc997381175b\n __panelId__: \"1\"\n runbook_url: \"\"\n summary: \"The open mining rounds have not advanced in the last 30m\"\n description: 'None of our SV apps have seen the open mining round advancing in the last 30m. Either all of our SVs or a high enough number of SVs to break BFT guarantees are failing'\n severity: critical\n labels:\n priority: high\n isPaused: false\n - uid: ae74w21qca2o0d\n title: Issuing Rounds Not Advancing\n condition: No new round\n data:\n - refId: Round number diff\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: max(max(max_over_time(splice_sv_dso_store_latest_issuing_mining_round[30m])) by (namespace) - min(min_over_time(splice_sv_dso_store_latest_issuing_mining_round[30m])) by (namespace))\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: Round number diff\n useBackend: false\n - refId: No new round\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.9\n type: gt\n datasource:\n type: __expr__\n uid: __expr__\n expression: Round number diff\n intervalMs: 1000\n maxDataPoints: 43200\n refId: No new round\n type: threshold\n dashboardUid: ed94a332-4fa7-47f8-982b-fc997381175b\n panelId: 1\n noDataState: Alerting\n execErrState: OK\n for: 1m\n annotations:\n __dashboardUid__: ed94a332-4fa7-47f8-982b-fc997381175b\n __panelId__: \"2\"\n runbook_url: \"\"\n summary: \"The issuing mining rounds have not advanced in the last 30m\"\n description: 'None of our SV apps have seen the issuing mining round advancing in the last 30m. Either all of our SVs or a high enough number of SVs to break BFT guarantees are failing'\n severity: critical\n labels:\n priority: high\n isPaused: false\n", - "mute_time_intervals.yaml": "apiVersion: 1\nmuteTimes:\n - orgId: 1\n name: sv1-mute\n time_intervals:\n - times:\n - start_time: '04:00'\n end_time: '07:00'\n weekdays:\n - saturday:sunday\n location: UTC\n - orgId: 1\n name: sv-runbook-mute\n time_intervals:\n - times:\n - start_time: '06:00'\n end_time: '10:00'\n location: UTC\n", - "notification_policies.yaml": "apiVersion: 1\npolicies:\n - orgId: 1\n receiver: cn-ci-channel-notification\n routes:\n - receiver: cn-ci-channel-notification\n object_matchers:\n - - namespace\n - '='\n - sv1\n mute_time_intervals:\n - sv1-mute\n - receiver: cn-ci-channel-notification\n object_matchers:\n - - namespace\n - '='\n - sv\n mute_time_intervals:\n - sv-runbook-mute\n - receiver: cn-ci-channel-high-prio-notification\n object_matchers:\n - - priority\n - '='\n - high\n group_wait: 30s\n group_interval: 1m\n repeat_interval: 4h\n continue: true\n - receiver: cn-ci-channel-notification\n object_matchers:\n - - team\n - '!='\n - support\n group_wait: 30s\n group_interval: 10m\n repeat_interval: 4h\n", + "mute_time_intervals.yaml": "apiVersion: 1\nmuteTimes:\n - orgId: 1\n name: sv1-mute\n time_intervals:\n - times:\n - start_time: '18:00'\n end_time: '24:00'\n weekdays:\n - wednesday\n location: UTC\n - orgId: 1\n name: sv-runbook-mute\n time_intervals:\n - times:\n - start_time: '18:00'\n end_time: '24:00'\n weekdays:\n - wednesday\n location: UTC\n - times:\n - start_time: '00:00'\n end_time: '06:00'\n - start_time: '12:00'\n end_time: '13:00'\n weekdays:\n - thursday\n location: UTC\n", + "notification_policies.yaml": "apiVersion: 1\npolicies:\n - orgId: 1\n receiver: cn-ci-channel-notification\n routes:\n - receiver: cn-ci-channel-notification\n object_matchers:\n - - namespace\n - '='\n - sv1-test\n mute_time_intervals:\n - sv1-mute\n - receiver: cn-ci-channel-notification\n object_matchers:\n - - namespace\n - '='\n - sv-test\n mute_time_intervals:\n - sv-runbook-mute\n - receiver: cn-ci-channel-high-prio-notification\n object_matchers:\n - - priority\n - '='\n - high\n group_wait: 30s\n group_interval: 1m\n repeat_interval: 4h\n continue: true\n - receiver: cn-ci-channel-notification\n object_matchers:\n - - team\n - '!='\n - support\n group_wait: 30s\n group_interval: 10m\n repeat_interval: 4h\n", "pruning_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: pruning\n folder: canton-network\n interval: 10m\n rules:\n - uid: ff3sfady2o6bke\n title: Participant pruning\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: daml_pruning_max_event_age\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 720\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 10m\n annotations:\n description: The participant in {{ $labels.namespace }} on migration ID {{ $labels.migration_id }} has not been pruned {{ 30 }} days.\n summary: Participant has not pruned ACS.\n labels:\n \"\": \"\"\n isPaused: false\n - uid: df3sfgne49bswb\n title: Sequencer Pruning\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: daml_sequencer_max_event_age\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 720\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 10m\n annotations:\n description: The sequencer in {{ $labels.namespace }} on migration ID {{ $labels.migration_id }} has not been pruned {{ 30 }} days.\n summary: Sequencer has not been pruned.\n labels:\n \"\": \"\"\n isPaused: false\n - uid: ef3sfj563dq0we\n title: Mediator Pruning\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: daml_mediator_max_event_age\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 720\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 10m\n annotations:\n description: The mediator in {{ $labels.namespace }} on migration ID {{ $labels.migration_id }} has not been pruned {{ 30 }} days.\n summary: Mediator has not been pruned.\n labels:\n \"\": \"\"\n isPaused: false", - "scan_connection_disagreement_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: scan connection consensus\n folder: canton-network\n interval: 10m\n rules:\n - uid: aescandisagreeok1\n title: Scan connection returning successful responses that disagree with BFT consensus\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 1800\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: |-\n sum by (namespace, job, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\"disagree\", success=\"true\", request!~\"getTransferInstructionAcceptContext|getOpenAndIssuingMiningRounds\", http_status!~\"404\"}[30m]))\n /\n sum by (namespace, job, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{request!~\"getTransferInstructionAcceptContext|getOpenAndIssuingMiningRounds\", http_status!~\"404\"}[30m]))\n instant: true\n intervalMs: 1000\n legendFormat: '{{namespace}}:{{job}}:{{node_name}}:{{scan_connection}}:{{request}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.1\n - 0\n type: lte\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: cnndx7p\n panelId: 6\n noDataState: OK\n execErrState: OK\n for: 0m\n annotations:\n __dashboardUid__: cnndx7p\n __panelId__: \"6\"\n description: More than 10% of the BFT consensus comparisons for this request on this scan connection returned a successful (2xx) response that disagreed with the BFT consensus result over the last 30 minutes.\n severity: critical\n summary: Scan connection {{ $labels.scan_connection }} of job {{ $labels.job }} (node {{ $labels.node_name }}) in namespace {{ $labels.namespace }} returned successful responses disagreeing with BFT consensus on {{ index $values \"A\" }} of comparisons for request {{ $labels.request }} in the last 30m (threshold 10%).\n labels: {}\n isPaused: false\n - uid: aescandisagreefail1\n title: Scan connection returning failed responses that disagree with BFT consensus\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 1800\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: |-\n sum by (namespace, job, node_name, scan_connection) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\"disagree\", success=\"false\"}[30m]))\n /\n sum by (namespace, job, node_name, scan_connection) (increase(splice_validator_scan_bft_per_connection_consensus_total{}[30m]))\n instant: true\n intervalMs: 1000\n legendFormat: '{{namespace}}:{{job}}:{{node_name}}:{{scan_connection}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.1\n - 0\n type: lte\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: cnndx7p\n panelId: 6\n noDataState: OK\n execErrState: OK\n for: 0m\n annotations:\n __dashboardUid__: cnndx7p\n __panelId__: \"6\"\n description: More than 10% of the BFT consensus comparisons on this scan connection returned a failed (non-2xx) response that disagreed with the BFT consensus result over the last 30 minutes. This can indicate the scan node is down.\n severity: warning\n summary: Scan connection {{ $labels.scan_connection }} of job {{ $labels.job }} (node {{ $labels.node_name }}) in namespace {{ $labels.namespace }} returned failed responses disagreeing with BFT consensus on {{ index $values \"A\" }} of comparisons in the last 30m (threshold 10%).\n labels: {}\n isPaused: false\n", + "scan_bft_sequencers_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: scan bft sequencer reads\n folder: canton-network\n interval: 10m\n rules:\n - uid: aesvbftscanreadfail1\n title: SV failing to read BFT sequencer list from a scan\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 1800\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: |-\n sum by (namespace, job, target_host) (\n histogram_count(increase(daml_http_client_requests_duration_seconds{http_client=\"HttpScanAppClient\", operation=\"ListBftSequencers\", status_code!~\"2..\"}[30m]))\n )\n /\n sum by (namespace, job, target_host) (\n histogram_count(increase(daml_http_client_requests_duration_seconds{http_client=\"HttpScanAppClient\", operation=\"ListBftSequencers\"}[30m]))\n )\n instant: true\n intervalMs: 1000\n legendFormat: '{{namespace}}:{{job}}:{{target_host}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.5\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.5\n - 0\n type: lte\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: a8a3113a-bccf-4728-b752-dd7a5d6f9bda\n panelId: 2\n noDataState: OK\n execErrState: OK\n for: 0m\n annotations:\n __dashboardUid__: a8a3113a-bccf-4728-b752-dd7a5d6f9bda\n __panelId__: \"2\"\n description: More than 50% of this SV's reads of the BFT sequencer list (/v0/sv-bft-sequencers) from this scan failed (non-2xx response, timeout, or connection error) over the last 30 minutes. The BFT peer reconciler tolerates individual failures safely, but while this scan is unreachable the SV cannot pick up BFT sequencer endpoint changes published by that scan's SV.\n severity: warning\n summary: SV {{ $labels.job }} in namespace {{ $labels.namespace }} failed {{ index $values \"A\" }} of its BFT sequencer list reads from scan {{ $labels.target_host }} in the last 30m.\n labels: {}\n isPaused: false\n", + "scan_connection_disagreement_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: scan connection consensus\n folder: canton-network\n interval: 10m\n rules:\n - uid: aescandisagreeok1\n title: Scan connection returning successful responses that disagree with BFT consensus\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 1800\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: |-\n max by (scan_connection, request) (\n sum by (namespace, job, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\"disagree\", success=\"true\", request!~\"getTransferInstructionAcceptContext|getOpenAndIssuingMiningRounds\"}[30m]))\n /\n sum by (namespace, job, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{request!~\"getTransferInstructionAcceptContext|getOpenAndIssuingMiningRounds\"}[30m]))\n ) > 0\n instant: true\n intervalMs: 1000\n legendFormat: '{{scan_connection}}:{{request}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.1\n - 0\n type: lte\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: cnndx7p\n panelId: 6\n noDataState: OK\n execErrState: OK\n for: 0m\n annotations:\n __dashboardUid__: cnndx7p\n __panelId__: \"6\"\n description: More than 10% of the BFT consensus comparisons for this request on this scan connection returned a successful (2xx) response that disagreed with the BFT consensus result over the last 30 minutes.\n severity: critical\n summary: Scan connection {{ $labels.scan_connection }} returned successful responses disagreeing with BFT consensus on {{ index $values \"A\" }} of comparisons for request {{ $labels.request }} in the last 30m (threshold 10%).\n labels: {}\n isPaused: false\n - uid: aescandisagreefail1\n title: Scan connection returning failed responses that disagree with BFT consensus\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 1800\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: |-\n max by (scan_connection) (\n sum by (namespace, job, node_name, scan_connection) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\"disagree\", success=\"false\", http_status!~\"404\"}[30m]))\n /\n sum by (namespace, job, node_name, scan_connection) (increase(splice_validator_scan_bft_per_connection_consensus_total{http_status!~\"404\"}[30m]))\n ) > 0\n instant: true\n intervalMs: 1000\n legendFormat: '{{scan_connection}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.1\n - 0\n type: lte\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: cnndx7p\n panelId: 6\n noDataState: OK\n execErrState: OK\n for: 0m\n annotations:\n __dashboardUid__: cnndx7p\n __panelId__: \"6\"\n description: More than 10% of the BFT consensus comparisons on this scan connection returned a failed (non-2xx) response that disagreed with the BFT consensus result over the last 30 minutes. This can indicate the scan node is down.\n severity: warning\n summary: Scan connection {{ $labels.scan_connection }} returned failed responses disagreeing with BFT consensus on {{ index $values \"A\" }} of comparisons in the last 30m (threshold 10%).\n labels: {}\n isPaused: false\n", "sequencer_client_delay_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: sequencer client delay\n folder: canton\n interval: 1m\n rules:\n - uid: efe3v7bed6134e\n title: Sequencer Client Delay\n condition: client delay threshold exceeded\n data:\n - refId: client delay\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: min by (namespace, job) (timestamp(daml_sequencer_client_handler_last_sequencing_time_micros) - (daml_sequencer_client_handler_last_sequencing_time_micros>0)/1e6)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: client delay\n - refId: client delay threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 90\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: client delay\n intervalMs: 1000\n maxDataPoints: 43200\n refId: client delay threshold exceeded\n type: threshold\n dashboardUid: ca9df344-c699-4efe-83c2-5fb2639d96d9\n panelId: 3\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: ca9df344-c699-4efe-83c2-5fb2639d96d9\n __panelId__: \"3\"\n summary: The job {{ $labels.job }} in namespace {{ $labels.namespace }} is reporting an increased sequencer client delay.\n severity: critical\n labels: {}\n isPaused: false\n", "sequencer_connection_pool_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: sequencer connection pool\n folder: canton\n interval: 1m\n rules:\n - uid: dfeb9wma1dk3kc\n title: Health of a connection\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 21600\n to: 0\n datasourceUid: prometheus\n model:\n adhocFilters: []\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: max by (connection, namespace, job) (daml_sequencer_client_sequencer_connection_pool_connection_health{namespace=~\".*\", job=~\".*\"})\n instant: true\n interval: \"\"\n intervalMs: 30000\n legendFormat: '{{namespace}}:{{job}}:{{connection}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 2\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: 50503893-d05f-4ece-9ebf-36fd20ffb06f\n panelId: 6\n noDataState: NoData\n execErrState: Error\n for: 1m\n annotations:\n __dashboardUid__: 50503893-d05f-4ece-9ebf-36fd20ffb06f\n __panelId__: \"6\"\n description: A failed connection is periodically retried for availability. A fatal subscription is considered invalid and will never be retried.\n summary: The sequencer connection {{ $labels.connection }} of job {{ $labels.job }} in namespace {{ $labels.namespace }} failed validation.\n isPaused: false\n - uid: dfebau7w2r7cwa\n title: Number of active subscriptions in the subscription pool\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 21600\n to: 0\n datasourceUid: prometheus\n model:\n adhocFilters: []\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: max by (namespace, job) (daml_sequencer_client_sequencer_connection_pool_active_subscriptions{namespace=~\".*\",job=~\".*\"})\n instant: true\n interval: \"\"\n intervalMs: 30000\n legendFormat: '{{namespace}}:{{job}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: B\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: max by (namespace, job) (daml_sequencer_client_sequencer_connection_pool_subscription_threshold{namespace=~\".*\",job=~\".*\"})\n hide: false\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: B\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: $A-$B\n hide: false\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: math\n - refId: D\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: lt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: C\n hide: false\n intervalMs: 1000\n maxDataPoints: 43200\n refId: D\n type: threshold\n dashboardUid: 50503893-d05f-4ece-9ebf-36fd20ffb06f\n panelId: 5\n noDataState: NoData\n execErrState: Error\n for: 5m\n annotations:\n __dashboardUid__: 50503893-d05f-4ece-9ebf-36fd20ffb06f\n __panelId__: \"5\"\n description: The liveness margin determines how many subscriptions on different sequencers are continuously maintained, beyond the minimum number defined by the trust threshold. In other words, the subscription pool will strive to maintain at all times (trust threshold + liveness margin)-many subscriptions active. This provides tolerance to subscriptions falling, enabling the node to continue operating while some sequencers are down.\n summary: The job {{ $labels.job }} in namespace {{ $labels.namespace }} has {{ index $values \"C\" }} less active subscriptions than the sum of trust threshold in the subscription pool\n severity: critical\n labels: {}\n isPaused: false\n", "sequencer_rate_limit_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: sequencer-rate-limits\n folder: canton\n interval: 1m\n rules:\n - uid: a3f7c8e1d492\n title: Sequencer Throughput Cap Rejections\n condition: rejection_rate_threshold\n data:\n - refId: rejection_rate\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by (namespace, job)(rate(daml_sequencer_throughput_cap_confirmation_request_rejections_total[5m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: rejection_rate\n - refId: rejection_rate_threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: rejection_rate\n hide: false\n refId: rejection_rate_threshold\n type: threshold\n dashboardUid: eb26bc60-6c26-408a-b0de-befc19f6915c\n panelId: 3\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: eb26bc60-6c26-408a-b0de-befc19f6915c\n __panelId__: \"3\"\n description: Sequencer throughput cap is rejecting confirmation requests on namespace {{ $labels.namespace }} job {{ $labels.job }}. This means members are exceeding their rate limits.\n severity: warning\n summary: Sequencer throughput cap rejections detected on {{ $labels.namespace }} {{ $labels.job }}\n labels: {}\n isPaused: false\n - uid: b4e8d9f2e5a3\n title: Sequencer Circuit Breaker Open\n condition: circuit_breaker_threshold\n data:\n - refId: circuit_breaker_state\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (namespace, job)(daml_sequencer_circuit_breaker_confirmation_request_state)\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: circuit_breaker_state\n - refId: circuit_breaker_threshold\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.5\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: circuit_breaker_state\n hide: false\n refId: circuit_breaker_threshold\n type: threshold\n dashboardUid: eb26bc60-6c26-408a-b0de-befc19f6915c\n panelId: 10\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: eb26bc60-6c26-408a-b0de-befc19f6915c\n __panelId__: \"10\"\n description: Sequencer circuit breaker for confirmation requests is open on namespace {{ $labels.namespace }} job {{ $labels.job }}. The sequencer is rejecting all incoming confirmation requests.\n severity: critical\n summary: Sequencer circuit breaker OPEN on {{ $labels.namespace }} {{ $labels.job }}\n labels: {}\n isPaused: false\n", + "splice-rate-limiting_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: splice-rate-limiting\n folder: platform\n interval: 5m\n rules:\n - uid: cfvmu2hs596v4d\n title: Splice Rate Limiting Usage\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: |-\n (max by (namespace, node_name, http_service, limiter, limiter_type) (\n splice_rate_limiting_max_limit_per_second{limiter_type!=\"per-attribute\"}\n ) * 0.8)\n -\n sum by (namespace, node_name, http_service, limiter, limiter_type) (\n rate(splice_rate_limiting_total{limiter_type!=\"per-attribute\"}[$__rate_interval])\n )\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: lt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: splice-rate-limit-db\n panelId: 6\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: Incoming request rate for this limiter has exceeded 0.8 of its configured splice_rate_limiting_max_limit_per_second for 5 minutes.\n summary: Rate limiter \"{{ $labels.limiter }}\" on {{ $labels.node_name }} ({{ $labels.http_service }}) in {{ $labels.namespace }} is receiving more requests than 0.8 of its configured limit allows.\n labels:\n gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0AjsonPayload.response_code=429\n isPaused: false\n - uid: chvmb2hs494v1y\n title: Splice Rate Limiting Rejections\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: |-\n sum by (namespace, node_name, http_service, limiter, limiter_type) (\n increase(splice_rate_limiting_total{result!=\"accepted\"}[$__rate_interval])\n )\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 10\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: splice-rate-limit-db\n panelId: 6\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n description: This limiter has been rejecting more than 10 requests with HTTP 429 for 5 minutes.\n summary: Rate limiter \"{{ $labels.limiter }}\" ({{ $labels.limiter_type }}) on {{ $labels.node_name }} ({{ $labels.http_service }}) in {{ $labels.namespace }} is rejecting requests.\n labels:\n gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0AjsonPayload.response_code=429\n isPaused: false\n", "sv-status-report_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: sv status reports\n folder: canton-network\n interval: 1m\n rules:\n - uid: adlmhpz5iv4sgc\n title: Report Creation Time Lag (internal SVs 5m)\n condition: No recent report\n data:\n - refId: Report time lag\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: >-\n time() - max by (report_publisher)\n (splice_sv_status_report_creation_time_us{namespace=~\".*\",\n report_publisher=~\"Digital-Asset-1|Digital-Asset-2|DA-Helm-Test-Node\",\n canton_version=~\".*\"}) / 1000000\n instant: false\n interval: ''\n intervalMs: 30000\n legendFormat: '{{report_publisher}}'\n maxDataPoints: 43200\n range: true\n refId: Report time lag\n - refId: Latest report time lag\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params: []\n type: gt\n operator:\n type: and\n query:\n params:\n - B\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Report time lag\n intervalMs: 1000\n maxDataPoints: 43200\n reducer: last\n refId: Latest report time lag\n settings:\n mode: dropNN\n type: reduce\n - refId: No recent report\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 300\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Latest report time lag\n intervalMs: 1000\n maxDataPoints: 43200\n refId: No recent report\n type: threshold\n dashboardUid: caffa6f7-c421-4579-a839-b026d3b76826\n panelId: 18\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: caffa6f7-c421-4579-a839-b026d3b76826\n __panelId__: '18'\n severity: critical\n description: >-\n The SV {{ $labels.report_publisher }} has not submitted a status\n report recently\n runbook_url: ''\n summary: Status report creation time lag too high\n labels:\n team: canton-network\n isPaused: false\n - uid: bdlmhpz5iv4sgc\n title: Report Creation Time Lag (internal SVs 15m)\n condition: No recent report\n data:\n - refId: Report time lag\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: >-\n time() - max by (report_publisher)\n (splice_sv_status_report_creation_time_us{namespace=~\".*\",\n report_publisher=~\"Digital-Asset-1|Digital-Asset-2|DA-Helm-Test-Node\",\n canton_version=~\".*\"}) / 1000000\n instant: false\n interval: ''\n intervalMs: 30000\n legendFormat: '{{report_publisher}}'\n maxDataPoints: 43200\n range: true\n refId: Report time lag\n - refId: Latest report time lag\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params: []\n type: gt\n operator:\n type: and\n query:\n params:\n - B\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Report time lag\n intervalMs: 1000\n maxDataPoints: 43200\n reducer: last\n refId: Latest report time lag\n settings:\n mode: dropNN\n type: reduce\n - refId: No recent report\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 300\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Latest report time lag\n intervalMs: 1000\n maxDataPoints: 43200\n refId: No recent report\n type: threshold\n dashboardUid: caffa6f7-c421-4579-a839-b026d3b76826\n panelId: 18\n noDataState: Alerting\n execErrState: Alerting\n for: 15m\n annotations:\n __dashboardUid__: caffa6f7-c421-4579-a839-b026d3b76826\n __panelId__: '18'\n severity: critical\n description: >-\n The SV {{ $labels.report_publisher }} has not submitted a status\n report recently\n runbook_url: ''\n summary: Status report creation time lag too high\n labels:\n team: support\n isPaused: false\n - uid: cdlmhpz5iv4sgc\n title: Report Creation Time Lag (external SVs 15m)\n condition: No recent report\n data:\n - refId: Report time lag\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: >-\n time() - max by (report_publisher)\n (splice_sv_status_report_creation_time_us{namespace=~\".*\",\n report_publisher!~\"Digital-Asset-1|Digital-Asset-2|DA-Helm-Test-Node\",\n canton_version=~\".*\"}) / 1000000\n instant: false\n interval: ''\n intervalMs: 30000\n legendFormat: '{{report_publisher}}'\n maxDataPoints: 43200\n range: true\n refId: Report time lag\n - refId: Latest report time lag\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params: []\n type: gt\n operator:\n type: and\n query:\n params:\n - B\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Report time lag\n intervalMs: 1000\n maxDataPoints: 43200\n reducer: last\n refId: Latest report time lag\n settings:\n mode: dropNN\n type: reduce\n - refId: No recent report\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 300\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: Latest report time lag\n intervalMs: 1000\n maxDataPoints: 43200\n refId: No recent report\n type: threshold\n dashboardUid: caffa6f7-c421-4579-a839-b026d3b76826\n panelId: 18\n noDataState: Alerting\n execErrState: Alerting\n for: 15m\n annotations:\n __dashboardUid__: caffa6f7-c421-4579-a839-b026d3b76826\n __panelId__: '18'\n severity: critical\n description: >-\n The SV {{ $labels.report_publisher }} has not submitted a status\n report recently\n runbook_url: ''\n summary: Status report creation time lag too high\n labels:\n team: da\n isPaused: false\n", "templates.yaml": "# config file version\napiVersion: 1\n\n# List of templates to import or update\n# source https://community.grafana.com/t/working-configuration-example-for-alerts-templating-telegram-and-slack/80988\ntemplates:\n - name: slack\n template: |\n {{ define \"slack_title\" }}\n {{ $hasCritical := false }}{{ $hasWarning := false }}{{ $hasInfo := false }}{{ $hasOthers := false }}\n {{- range .Alerts.Firing -}}\n {{- if eq .Annotations.severity \"critical\" -}}\n {{- $hasCritical = true -}}\n {{- else if eq .Annotations.severity \"warning\" -}}\n {{- $hasWarning = true -}}\n {{- else if eq .Annotations.severity \"info\" -}}\n {{- $hasInfo = true -}}\n {{- else -}}\n {{- $hasOthers = true -}}\n {{- end -}}\n {{- end -}}\n\n mock\n {{ if gt (len .Alerts.Firing) 0 }}\n {{- if $hasCritical }}\n 🔥 {{ len .Alerts.Firing }} Alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }} firing\n {{- else if $hasWarning }}\n ⚠️ {{ len .Alerts.Firing }} Alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }} firing\n {{- else }}\n :information_source: {{ len .Alerts.Firing }} Alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }} firing\n {{- end }}\n {{ end }}\n {{ if gt (len .Alerts.Resolved) 0 }} ✅ {{ len .Alerts.Resolved }} alert(s) resolved {{ end }}\n {{ end }}\n\n {{ define \"slack_message\" }}\n {{ $hasCritical := false }}{{ $hasWarning := false }}{{ $hasInfo := false }}{{ $hasOthers := false }}\n {{- range .Alerts.Firing -}}\n {{- if eq .Annotations.severity \"critical\" -}}\n {{- $hasCritical = true -}}\n {{- else if eq .Annotations.severity \"warning\" -}}\n {{- $hasWarning = true -}}\n {{- else if eq .Annotations.severity \"info\" -}}\n {{- $hasInfo = true -}}\n {{- else -}}\n {{- $hasOthers = true -}}\n {{- end -}}\n {{- end -}}\n {{ if $hasCritical }} 🔥Critical alerts {{ range .Alerts.Firing }} {{- if eq .Annotations.severity \"critical\" -}} {{ template \"slack_alert_firing\" .}} {{ end }} {{ end }} {{ end }}\n {{ if $hasWarning }} ⚠️Warning alerts {{ range .Alerts.Firing }} {{- if eq .Annotations.severity \"warning\" -}} {{ template \"slack_alert_firing\" .}} {{ end }} {{ end }} {{ end }}\n {{ if $hasInfo }} :information_source:Info alerts {{ range .Alerts.Firing }} {{- if eq .Annotations.severity \"info\" -}} {{ template \"slack_alert_firing\" .}} {{ end }} {{ end }} {{ end }}\n {{ if $hasOthers }} Other alerts {{ range .Alerts.Firing }} {{- if and (and (ne .Annotations.severity \"info\") (ne .Annotations.severity \"warning\")) (ne .Annotations.severity \"critical\") -}} {{ template \"slack_alert_firing\" . }} {{ end }} {{ end }} {{ end }}\n {{ if gt (len .Alerts.Resolved) 0 }} ✅Resolved Alerts {{ range .Alerts.Resolved }} {{ template \"slack_alert_resolved\" .}} {{ end }} {{ end }}\n {{ end }}\n\n {{ define \"slack_alert_firing\" }}\n *{{ .Labels.alertname }}*\n {{ .Annotations.summary }}\n {{ if .Annotations.description }}{{ .Annotations.description }}{{ end }}\n {{- if .Labels.service }}\n Service: {{ .Labels.service }}\n {{- end }}\n {{ template \"slack_gcloud_log_link\" . }}\n {{ end }}\n\n {{ define \"slack_alert_resolved\" }}\n *{{ .Labels.alertname }}*\n {{ if .Annotations.severity }}{{ .Annotations.severity }}{{ end }}\n {{ .Annotations.summary }}\n {{ if .Annotations.description }}{{ .Annotations.description }}{{ end }}\n {{ end }}\n\n {{ define \"slack_gcloud_log_link\" }}{{ end }}\n\n {{ define \"slack_color\" -}}\n {{ $hasCritical := false }}{{ $hasWarning := false }}{{ $hasInfo := false }}{{ $hasOthers := false }}\n {{- range .Alerts.Firing -}}\n {{- if eq .Annotations.severity \"critical\" -}}\n {{- $hasCritical = true -}}\n {{- else if eq .Annotations.severity \"warning\" -}}\n {{- $hasWarning = true -}}\n {{- else if eq .Annotations.severity \"info\" -}}\n {{- $hasInfo = true -}}\n {{- else -}}\n {{- $hasOthers = true -}}\n {{- end -}}\n {{- end -}}\n {{ if eq .Status \"firing\" -}}\n {{ if $hasCritical -}}\n danger\n {{- else if $hasWarning -}}\n warning\n {{- else -}}\n #439FE0\n {{- end -}}\n {{ else -}}\n good\n {{- end }}\n {{- end }}\n\n {{ define \"support_email_message\" }}\n [ MAINNET-DA2-SVN-CRITICAL-ALERT 9f2b7e1a-4c3d-58b9-9f1e-df9c4a5b6e7d ]\n {{ if gt (len .Alerts.Firing) 0 }}**Firing**\n {{ template \"__text_alert_list\" .Alerts.Firing }}{{ if gt (len .Alerts.Resolved) 0 }}\n {{ end }}{{ end }}{{ if gt (len .Alerts.Resolved) 0 }}**Resolved**\n {{ template \"__text_alert_list\" .Alerts.Resolved }}{{ end }}{{ end }}\n", - "traffic_based_rewards_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: traffic-based-rewards\n folder: canton-network\n interval: 1m\n rules:\n - uid: afht9v3djz2tcf\n title: Number of featured app rights\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (namespace)(pg_stat_user_tables_n_live_tup{relname=\"scan_rewards_reference_store_active\"})\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 10000\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: \"00000039\"\n panelId: 74\n noDataState: OK\n execErrState: Alerting\n for: 60m\n annotations:\n __dashboardUid__: \"00000039\"\n __panelId__: \"74\"\n description: |\n Scan rewards reference store on namespace {{ $labels.namespace }} has too many live rows,\n which is likely because there are too many featured app right contracts.\n This may become a problem as the app activity record computation loads\n all featured app right contracts at round start into memory at once.\n\n Please investigate the reason for there being that many active featured\n app right contracts and determine the best course of action.\n (Note: bumping the alert limit may be OK, if the Scan app has enough memory available.)\n severity: warning\n summary: Scan rewards reference store on namespace {{ $labels.namespace }} has too many live rows.\n labels: {}\n isPaused: false\n - uid: hn7pcll44xugj7\n title: Number of active CalculateRewardsV2 contracts\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (dryRun)(max_over_time(splice_calculate_rewards_v2_active_contracts[5m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: \"cnx7gsn\"\n panelId: 33\n noDataState: OK\n execErrState: Alerting\n for: 1m\n annotations:\n __dashboardUid__: cnx7gsn\n __panelId__: 33\n description: |\n At least one of our sv app instances observed more than one active CalculateRewardsV2 contract\n for dryRun={{ $labels.dryRun }}.\n\n CalculateRewardsV2 contracts are created when a round is closed, and represent a request\n to vote on the root hash of app rewards for a round.\n The contract is archived as soon as 2/3 of all SVs have voted for the same root hash,\n which should typically happen within a minute after the contract is created.\n\n Suggested actions:\n - Check if enough SVs are healthy\n - Investigate the status of traffic-based app rewards automation.\n Look at the \"Traffic-based app rewards (CIP-104)\" dashboard in Grafana, in particular the\n \"Voting on root hash\" section.\n - Check what votes are being cast by other SVs.\n Filter logs for \"not yet executing\" AND \"CRARC_StartProcessingRewardsV2\".\n Look for missing SVs or SVs that are voting for a different root hash.\n severity: critical\n summary: Too many active CalculateRewardsV2 contracts for dryRun={{ $labels.dryRun }}.\n labels: {}\n isPaused: false\n - uid: o4v0a20pvxhd2\n title: Number of active ProcessRewardsV2 contracts\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: min by (dryRun)(min_over_time((splice_process_rewards_v2_active_contracts != -1)[15m:]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: \"cnx7gsn\"\n panelId: 34\n noDataState: OK\n execErrState: Alerting\n for: 1m\n annotations:\n __dashboardUid__: cnx7gsn\n __panelId__: 34\n description: |\n None our sv app instances observed the number of active ProcessRewardsV2 contracts recently\n reaching zero for dryRun={{ $labels.dryRun }}.\n\n ProcessRewardsV2 contracts are nodes of the reward data merkle tree.\n They are converted first into more ProcessRewardsV2 contracts and then finally into RewardCouponV2 contracts\n by the delegate based ProcessRewardsTrigger.\n\n If these contracts don't all disappear within 10min (when the next round starts), it means the creation of\n minting allowances probably can't keep up and is falling behind.\n\n Suggested actions:\n - Investigate the status of traffic-based app rewards automation.\n Look at the \"Traffic-based app rewards (CIP-104)\" dashboard in Grafana, in particular the\n \"Creating minting allowances\" section.\n - Filter logs for \"ProcessRewards.*Trigger\".\n Check why the trigger is not processing the ProcessRewardsV2 contracts.\n severity: critical\n summary: ProcessRewardsV2 contracts are not being archived in time for dryRun={{ $labels.dryRun }}.\n labels: {}\n isPaused: false\n - uid: 3pcvfr7j92x7o\n title: Average verdict ingestion batch size over the last 10min\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: (histogram_sum(sum by(namespace)(rate(splice_scan_verdict_ingestion_batch_size[10m])))/histogram_count(sum by(namespace)(rate(splice_scan_verdict_ingestion_batch_size[10m]))))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 40\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: \"cnx7gsn\"\n panelId: 19\n noDataState: OK\n execErrState: Alerting\n for: 60m\n annotations:\n __dashboardUid__: cnx7gsn\n __panelId__: 19\n description: |\n The average verdict ingestion batch size has been above 40\n for more than for $VERDICT_INGESTION_BATCH_SIZE_PENDING_PERIOD_MINUTES minutes for namespace {{ $labels.namespace }}.\n\n The default maximum batch size is 50, a high average batch size may indicate that ingestion can't keep up.\n\n Suggested actions:\n - If this node is known to be catching up, ignore or silence the alert.\n - Investigate the status of traffic-based app rewards automation.\n Look at the \"Traffic-based app rewards (CIP-104)\" dashboard in Grafana, in particular the\n \"Ingest verdicts and activity records\" section.\n - Filter logs for \"ScanVerdictIngestionService\".\n - If you find that this alert produces too many false positives, adjust the \"verdictIngestionBatchSizeThreshold\"\n in the cluster config.\n severity: warning\n summary: Average verdict ingestion batch size is above $VERDICT_INGESTION_BATCH_SIZE_THRESHOLD for namespace {{ $labels.namespace }}.\n labels: {}\n isPaused: false\n", + "traffic_based_rewards_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: traffic-based-rewards\n folder: canton-network\n interval: 1m\n rules:\n - uid: afht9v3djz2tcf\n title: Number of featured app rights\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (namespace)(pg_stat_user_tables_n_live_tup{relname=\"scan_rewards_reference_store_active\"})\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 10000\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: \"00000039\"\n panelId: 74\n noDataState: OK\n execErrState: Alerting\n for: 60m\n annotations:\n __dashboardUid__: \"00000039\"\n __panelId__: \"74\"\n description: |\n Scan rewards reference store on namespace {{ $labels.namespace }} has too many live rows,\n which is likely because there are too many featured app right contracts.\n This may become a problem as the app activity record computation loads\n all featured app right contracts at round start into memory at once.\n\n Please investigate the reason for there being that many active featured\n app right contracts and determine the best course of action.\n (Note: bumping the alert limit may be OK, if the Scan app has enough memory available.)\n severity: warning\n summary: Scan rewards reference store on namespace {{ $labels.namespace }} has too many live rows.\n labels: {}\n isPaused: false\n - uid: hn7pcll44xugj7\n title: Number of active CalculateRewardsV2 contracts\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (dryRun)(max_over_time(splice_calculate_rewards_v2_active_contracts[5m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 1\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: \"cnx7gsn\"\n panelId: 33\n noDataState: OK\n execErrState: Alerting\n for: 1m\n annotations:\n __dashboardUid__: cnx7gsn\n __panelId__: 33\n description: |\n At least one of our sv app instances observed more than one active CalculateRewardsV2 contract\n for dryRun={{ $labels.dryRun }}.\n\n CalculateRewardsV2 contracts are created when a round is closed, and represent a request\n to vote on the root hash of app rewards for a round.\n The contract is archived as soon as 2/3 of all SVs have voted for the same root hash,\n which should typically happen within a minute after the contract is created.\n\n Suggested actions:\n - Check if enough SVs are healthy\n - Investigate the status of traffic-based app rewards automation.\n Look at the \"Traffic-based app rewards (CIP-104)\" dashboard in Grafana, in particular the\n \"Voting on root hash\" section.\n - Check what votes are being cast by other SVs.\n Filter logs for \"not yet executing\" AND \"CRARC_StartProcessingRewardsV2\".\n Look for missing SVs or SVs that are voting for a different root hash.\n severity: critical\n summary: Too many active CalculateRewardsV2 contracts for dryRun={{ $labels.dryRun }}.\n labels: {}\n isPaused: false\n - uid: o4v0a20pvxhd2\n title: Number of active ProcessRewardsV2 contracts\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: min by (dryRun)(min_over_time((splice_process_rewards_v2_active_contracts != -1)[15m:]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: \"cnx7gsn\"\n panelId: 34\n noDataState: OK\n execErrState: Alerting\n for: 1m\n annotations:\n __dashboardUid__: cnx7gsn\n __panelId__: 34\n description: |\n None our sv app instances observed the number of active ProcessRewardsV2 contracts recently\n reaching zero for dryRun={{ $labels.dryRun }}.\n\n ProcessRewardsV2 contracts are nodes of the reward data merkle tree.\n They are converted first into more ProcessRewardsV2 contracts and then finally into RewardCouponV2 contracts\n by the delegate based ProcessRewardsTrigger.\n\n If these contracts don't all disappear within 10min (when the next round starts), it means the creation of\n minting allowances probably can't keep up and is falling behind.\n\n Suggested actions:\n - Investigate the status of traffic-based app rewards automation.\n Look at the \"Traffic-based app rewards (CIP-104)\" dashboard in Grafana, in particular the\n \"Creating minting allowances\" section.\n - Filter logs for \"ProcessRewards.*Trigger\".\n Check why the trigger is not processing the ProcessRewardsV2 contracts.\n severity: critical\n summary: ProcessRewardsV2 contracts are not being archived in time for dryRun={{ $labels.dryRun }}.\n labels: {}\n isPaused: false\n - uid: 3pcvfr7j92x7o\n title: Average verdict ingestion batch size over the last 10min\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: (histogram_sum(sum by(namespace)(rate(splice_scan_verdict_ingestion_batch_size[10m])))/histogram_count(sum by(namespace)(rate(splice_scan_verdict_ingestion_batch_size[10m]))))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 40\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: \"cnx7gsn\"\n panelId: 19\n noDataState: OK\n execErrState: Alerting\n for: 60m\n annotations:\n __dashboardUid__: cnx7gsn\n __panelId__: 19\n description: |\n The average verdict ingestion batch size has been above 40\n for more than for 60 minutes for namespace {{ $labels.namespace }}.\n\n The default maximum batch size is 50, a high average batch size may indicate that ingestion can't keep up.\n\n Suggested actions:\n - If this node is known to be catching up, ignore or silence the alert.\n - Investigate the status of traffic-based app rewards automation.\n Look at the \"Traffic-based app rewards (CIP-104)\" dashboard in Grafana, in particular the\n \"Ingest verdicts and activity records\" section.\n - Filter logs for \"ScanVerdictIngestionService\".\n - If you find that this alert produces too many false positives, adjust the \"verdictIngestionBatchSizeThreshold\"\n in the cluster config.\n severity: warning\n summary: Average verdict ingestion batch size is above 40 for namespace {{ $labels.namespace }}.\n labels: {}\n isPaused: false\n", "wallet-sweep_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: wallet-sweep\n folder: canton-network\n interval: 10m\n rules:\n - uid: df6rim37tocud0\n title: Wallet sweep from mock-3 to mock-4\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: >-\n splice_wallet_unlocked_amulet_balance{owner=~\"mock-3::33333333.*\"}\n * on() group_left\n sum(splice_amulet_price_latest_open_round_price{namespace=\"sv-1\"})\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 101835\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n description: >-\n Owner {{ $labels.owner }} are not sweeping their wallet as\n configured.\n severity: critical\n summary: Wallet not sweeping as expected.\n labels:\n '': ''\n isPaused: false\n - uid: df6rim37tocud1\n title: Wallet sweep from mock to mock-2\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: >-\n splice_wallet_unlocked_amulet_balance{owner=~\"mock::11111111.*\"}\n * on() group_left\n sum(splice_amulet_price_latest_open_round_price{namespace=\"sv-1\"})\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 18517.5\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n description: >-\n Owner {{ $labels.owner }} are not sweeping their wallet as\n configured.\n severity: critical\n summary: Wallet not sweeping as expected.\n labels:\n '': ''\n isPaused: false\n" }, "kind": "ConfigMap", @@ -115,8 +119,8 @@ "inputs": { "apiVersion": "v1", "data": { - "bft-ordering-performance.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 8,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"Global\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_global_requests_ordering_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests end-to-end ordering latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node, ordering_stage) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-availability-total\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total batch dissemination and consensus time (permanence in availability module)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of blocks ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 17,\n \"w\": 8,\n \"x\": 0,\n \"y\": 13\n },\n \"id\": 107,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node, is_block_empty) (irate(daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}.{{node}} (empty: {{is_block_empty}})\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - blocks/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of batches (proofs of availability) ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 17,\n \"w\": 8,\n \"x\": 8,\n \"y\": 13\n },\n \"id\": 108,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_global_ordered_batches_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"interval\": \"\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - batches/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 17,\n \"w\": 8,\n \"x\": 16,\n \"y\": 13\n },\n \"id\": 109,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by (namespace, job, node) (irate(daml_sequencer_bftordering_global_ordered_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - requests/s\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 30\n },\n \"id\": 12,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-PekkoP2PGrpcConnectionManagingActor\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"PekkoP2PGrpcConnectionManagingActor queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 31\n },\n \"id\": 103,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (daml_sequencer_bftordering_performance_moduleQueueSize_PekkoP2PGrpcConnectionManagingActor{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"})\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"PekkoP2PGrpcConnectionManagingActor queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Latency between sending a message and receiving it on the other side\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node, source_sequencer) (rate(daml_sequencer_bftordering_p2p_send_grpc_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{source_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P gRPC network latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 47\n },\n \"id\": 17,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_p2p_send_network_write_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P network client write latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 47\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node) (rate(daml_sequencer_bftordering_p2p_receive_processing_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P receive processing latency\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Networking\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 7,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-mempool\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Mempool module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 32\n },\n \"id\": 101,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_mempool{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Mempool module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-availability\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Availability module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 38\n },\n \"id\": 102,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_availability{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Availability module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time waited for a batch while there are outstanding consensus proposal requests and no dissemination is in progress\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 44\n },\n \"id\": 24,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-wait\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Time spent waiting for batches\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Cumulative latency of a batch dissemination until a PoA is formed and it can be present multiple times for a given batch if it regresses due to topology changes\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 44\n },\n \"id\": 21,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-dissemination-total\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total dissemination time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Insertion in proposal queue until next event, like re-sign, re-dissemination, [re-]proposal or batch ordered (emitted 1 or more times per batch)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 50\n },\n \"id\": 23,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-queued-for-block-inclusion\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Time waited for batch inclusion in consensus proposal\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of regressions of a single batch due to topology changes\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 50\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_availability_batch_regressions{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}, {{stage}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batch dissemination regressions rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 56\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-hash-batchId\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batch ID hashing latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 56\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-sign-local-batchId\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Local Batch ID signing latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Insertion in batch queue until extraction and batch send to availability\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 62\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"request-queued-for-batch-inclusion\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Request batch inclusion latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 62\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-sign-remote-batchId\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Remote batch ID signing latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 68\n },\n \"id\": 26,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-sign-local-batches\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sign local batches\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 68\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-signature-verify-ack\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batch ID signature verification\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 74\n },\n \"id\": 82,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.addBatch\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert batch\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 74\n },\n \"id\": 28,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.fetchBatches\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Fetch local batches from DB\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 80\n },\n \"id\": 22,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-validation\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batch validation\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Summary of cache hits, misses, and evictions for the Availability store's batch data cache\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 80\n },\n \"hideTimeOverride\": false,\n \"id\": 110,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_cache_hits{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\", cache=\\\"batch-cache\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} Cache Hits\",\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_cache_misses{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\", cache=\\\"batch-cache\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"hide\": false,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} Cache Misses\",\n \"refId\": \"B\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_cache_evictions{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\", cache=\\\"batch-cache\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"hide\": false,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} Cache Evictions\",\n \"refId\": \"C\"\n }\n ],\n \"title\": \"Availability Store Cache Statistics\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Mempool+Availability\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 45,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 33\n },\n \"id\": 80,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-consensus\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 33\n },\n \"id\": 104,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_consensus{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 81,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-segment-module\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Segment module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 40\n },\n \"id\": 105,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_segment_module{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Segment module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Block commit time at the consensus (PBFT) level.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [\n {\n \"options\": {\n \"match\": \"null\",\n \"result\": {\n \"text\": \"N/A\"\n }\n },\n \"type\": \"special\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 2\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 47\n },\n \"hideTimeOverride\": true,\n \"id\": 39,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_consensus_commit_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus Block Latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time elapsed between sending a PrePrepare and seeing that the block has been ordered\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 47\n },\n \"id\": 46,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-segment-proposal-to-commit-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus segment block proposal to commit latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time elapsed between ordered blocks proposed by a segment\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 54\n },\n \"id\": 88,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-segment-block-commit-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus segment block ordering latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time between a proposal request to the availability module needed to continue a segment and a PrePrepare being built\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 54\n },\n \"id\": 87,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-block-proposal-wait\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Wait for proposal\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time between the epoch completion and the start of the next epoch\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 60\n },\n \"id\": 86,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-epoch-start-wait\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Wait for epoch start\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time between the last led segment's completion and the epoch completion\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 60\n },\n \"id\": 83,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-epoch-completion-wait\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Wait for epoch completion\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 67\n },\n \"id\": 63,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.startEpoch\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Start epoch (DB)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 67\n },\n \"id\": 47,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.completeEpoch\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Complete epoch (DB)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 73\n },\n \"id\": 64,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPrePrepare\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert pre-prepare\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 73\n },\n \"id\": 50,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPreparesAtomically\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert prepares\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 80\n },\n \"id\": 52,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addOrderedBlockAtomically\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert ordered block\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 80\n },\n \"id\": 66,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-signature-verify-poa-ack\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Check PoA ack signature\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 86\n },\n \"id\": 68,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-validate-signed-message\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Validate message\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 86\n },\n \"id\": 76,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"sign-BftSignedConsensusMessage\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sign message\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - main protocol\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 33\n },\n \"id\": 89,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 24,\n \"x\": 0,\n \"y\": 34\n },\n \"id\": 51,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_epoch_view_changes{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"View changes occurred (rate)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 41\n },\n \"id\": 84,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addViewChangeMessage\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert view change message\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 41\n },\n \"id\": 77,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-postponed-view-messages-queue-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 48\n },\n \"id\": 97,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_postponed_view_messages_queue_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 48\n },\n \"id\": 95,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_postponed_view_messages_queue_max_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages queue max size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 55\n },\n \"id\": 94,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_postponed_view_messages_dropped_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages dropped (rate)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 55\n },\n \"id\": 100,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_postponed_view_messages_duplicates_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages duplicated (rate)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 62\n },\n \"id\": 112,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, daml_sequencer_bftordering_consensus_view_change_progress_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"})\",\n \"interval\": \"\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{Leader}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Time until progress in view\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - view change\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 34\n },\n \"id\": 90,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 72,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-validate-consensus-certificate\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Validate state transfer consensus certificate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to transfer a given epoch from peer(s)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 80\n },\n \"id\": 111,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\\\"$reporting_sequencer\\\", ordering_stage=~\\\"state-transfer-total-epoch-transfer-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"Data transfer\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\\\"$reporting_sequencer\\\", ordering_stage=~\\\"state-transfer-store-epochs\\\"}[$__rate_interval])))\",\n \"hide\": false,\n \"legendFormat\": \"Complete + start DB query\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Epoch Transfer Duration\",\n \"transformations\": [\n {\n \"id\": \"calculateField\",\n \"options\": {\n \"alias\": \"Total epoch transfer duration\",\n \"binary\": {\n \"left\": \"Data transfer\",\n \"reducer\": \"sum\",\n \"right\": \"Complete + start DB query\"\n },\n \"mode\": \"binary\",\n \"reduce\": {\n \"reducer\": \"sum\"\n }\n }\n }\n ],\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 42\n },\n \"id\": 91,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"state-transfer-postponed-consensus-messages-queue-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed consensus messages queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 42\n },\n \"id\": 96,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_queue_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed consensus messages queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 49\n },\n \"id\": 98,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_queue_max_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed consensus messages queue max size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 49\n },\n \"id\": 99,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_dropped{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed consensus messages dropped (rate)\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - state transfer\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 38,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 36\n },\n \"id\": 85,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_incoming_retransmission_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Incoming retransmission requests rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 36\n },\n \"id\": 40,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_outgoing_retransmission_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Outgoing retransmission requests rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 42\n },\n \"id\": 41,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_retransmitted_messages_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retransmitted messages rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 42\n },\n \"id\": 42,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_retransmitted_commit_certificates_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retransmitted commit certificates rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 48\n },\n \"id\": 78,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"sign-BftSignedRetransmissionMessage\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sign retransmission message\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 48\n },\n \"id\": 79,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"verify-signature-BftSignedRetransmissionMessage\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Verify retransmission message signature\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 54\n },\n \"id\": 43,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_discarded_wrong_epoch_retransmission_responses_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retransmission responses discarded due to wrong epoch - rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 54\n },\n \"id\": 44,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_discarded_rate_limited_retransmission_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retransmission requests discarded due to rate limiting - rate\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - retransmissions\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 36\n },\n \"id\": 59,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 37\n },\n \"id\": 48,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.latestEpoch\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load latest epoch\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 37\n },\n \"id\": 53,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadEpochProgress\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load epoch progress\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 45\n },\n \"id\": 54,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadCompleteBlocks\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load completed blocks\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - CFT\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 37\n },\n \"id\": 29,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 55,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-output\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Output module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 38\n },\n \"id\": 106,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_output{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Output module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time needed to inspect requests within a block\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 45\n },\n \"id\": 36,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"output-block-inspection\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block inspection\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 45\n },\n \"id\": 30,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"output-block-fetch-batches\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total block batches fetch time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 52\n },\n \"id\": 37,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertBlockIfMissing\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert block\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 52\n },\n \"id\": 34,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_topology_query_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Query topology\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 59\n },\n \"id\": 31,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertLeaderSelectionPolicyState\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Save blacklist leader selection policy state\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 59\n },\n \"id\": 32,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertEpochIfMissing\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert epoch metadata\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 66\n },\n \"id\": 113,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"output-backpressure\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sequencer core backpressure duration\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Output\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 60,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 35,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadEpochInfo\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load epoch info\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 47\n },\n \"id\": 56,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadOrderedBlocks\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load ordered blocks\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Output - CFT\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 62,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to compute the pruning point, based on retention factors and current time\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 25,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"pruning-compute-pruning-point\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Compute pruning point\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to evict unordered, but expired, batches in the availability DB\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 40\n },\n \"id\": 27,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.gc\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Availability DB - evict expired batches\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to create new table partitions, when necessary\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 47\n },\n \"id\": 61,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.PartitionCreatorImpl.createPartitionsIfNeeded\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Create partitions\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to drop table partitions that can be pruned\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 47\n },\n \"id\": 67,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.PartitionPrunerImpl.prune\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Prune (Delete partitions)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to start the Partition Manager, which may include creating new partitions\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 47\n },\n \"id\": 69,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.create\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Partition manager startup\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Pruning\",\n \"type\": \"row\"\n }\n ],\n \"refresh\": \"5s\",\n \"schemaVersion\": 38,\n \"style\": \"dark\",\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": false,\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status,namespace}\",\n \"hide\": 0,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"label\": \"Namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"label\": \"Job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"node\",\n \"label\": \"Node\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": \".95\",\n \"value\": \".95\"\n },\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Percentile\",\n \"multi\": false,\n \"name\": \"percentile\",\n \"options\": [\n {\n \"selected\": false,\n \"text\": \".5\",\n \"value\": \".5\"\n },\n {\n \"selected\": false,\n \"text\": \".6\",\n \"value\": \".6\"\n },\n {\n \"selected\": false,\n \"text\": \".7\",\n \"value\": \".7\"\n },\n {\n \"selected\": false,\n \"text\": \".75\",\n \"value\": \".75\"\n },\n {\n \"selected\": false,\n \"text\": \".8\",\n \"value\": \".8\"\n },\n {\n \"selected\": false,\n \"text\": \".9\",\n \"value\": \".9\"\n },\n {\n \"selected\": true,\n \"text\": \".95\",\n \"value\": \".95\"\n },\n {\n \"selected\": false,\n \"text\": \".99\",\n \"value\": \".99\"\n },\n {\n \"selected\": false,\n \"text\": \".999\",\n \"value\": \".999\"\n }\n ],\n \"query\": \".5,.6,.7,.75,.8,.9,.95,.99,.999\",\n \"queryValue\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"custom\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-5m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"BFT ordering (performance)\",\n \"uid\": \"c2237cb1-9018-41ac-8eef-24a28ab28f20\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", - "bft-ordering.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 8,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"collapsed\": false,\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 64,\n \"panels\": [],\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Global overview (instance view)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"ISS proceeds in epochs with stable leader assignments to \\\"segments\\\" and stable topology (i.e., sequencers can be added and remove only at epoch boundaries and leaders are reassigned to segments also at epoch boundaries). This is the current epoch.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 5,\n \"x\": 0,\n \"y\": 1\n },\n \"hideTimeOverride\": false,\n \"id\": 72,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_epoch{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Current Epoch\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size in blocks of the current epoch.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 3,\n \"x\": 5,\n \"y\": 1\n },\n \"hideTimeOverride\": false,\n \"id\": 80,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_epoch_length{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Epoch Length\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of active validators\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 3,\n \"x\": 8,\n \"y\": 1\n },\n \"hideTimeOverride\": false,\n \"id\": 75,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_validators{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Validators\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of connected and authenticated P2P peers for the selected sequencer. It should be > 2/3 at all times.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"percentage\",\n \"steps\": [\n {\n \"color\": \"dark-red\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 67\n },\n {\n \"color\": \"dark-green\",\n \"value\": 100\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 13,\n \"x\": 11,\n \"y\": 1\n },\n \"hideTimeOverride\": true,\n \"id\": 53,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"center\",\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"last\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"textMode\": \"auto\"\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_p2p_connections_connected{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Connected ({{namespace}}.{{node}})\",\n \"range\": false,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_p2p_connections_authenticated{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"hide\": false,\n \"instant\": true,\n \"legendFormat\": \"Authenticated ({{namespace}}.{{node}})\",\n \"range\": false,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Peers count\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 3,\n \"x\": 0,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 99,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_max_tolerated_faults{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Max tolerated faults\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The size of a weak quorum or non-faulty nodes, needed e.g. to disseminate batches\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 3,\n \"x\": 3,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 100,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_weak_quorum{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Weak quorum\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The size of a strong quorum, needed for ordering consensus\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 3,\n \"x\": 6,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 101,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_strong_quorum{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Strong quorum\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Latest block number ordered and available for the read path. When there is no traffic, a block can be empty.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 7,\n \"x\": 9,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 4,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, job, node) (daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\"})\",\n \"format\": \"time_series\",\n \"hide\": false,\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Output Blocks\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total number of requests ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 8,\n \"x\": 16,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 40,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace,job,node) (histogram_sum(daml_sequencer_bftordering_output_block_size_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}))\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Output Requests\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Sequencers that are part of the BFT ordering topology\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisGridShow\": true,\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.axisPlacement\",\n \"value\": \"hidden\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 12,\n \"x\": 0,\n \"y\": 24\n },\n \"id\": 91,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_topology_member{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"interval\": \"\",\n \"legendFormat\": \" (reporter: {{namespace}}.{{node}}) {{sequencer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT ordering members\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Sequencers that are allowed to propose blocks for ordering\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.axisPlacement\",\n \"value\": \"hidden\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 12,\n \"x\": 12,\n \"y\": 24\n },\n \"id\": 95,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_topology_leader{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \" (reporter: {{namespace}}.{{node}}) {{sequencer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT ordering leaders\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 8,\n \"x\": 0,\n \"y\": 45\n },\n \"id\": 96,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_p2p_authenticated_endpoint{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{endpoint}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P authenticated endpoints\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.axisPlacement\",\n \"value\": \"hidden\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 8,\n \"x\": 8,\n \"y\": 45\n },\n \"id\": 97,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_p2p_unauthenticated_endpoint{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{endpoint}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P connected but unauthenticated endpoints\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"hidden\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.axisPlacement\",\n \"value\": \"hidden\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 8,\n \"x\": 16,\n \"y\": 45\n },\n \"id\": 98,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_p2p_disconnected_endpoint{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{endpoint}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P disconnected endpoints\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 66\n },\n \"id\": 87,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_global_requests_ordering_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests end-to-end ordering latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of requests ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 66\n },\n \"hideTimeOverride\": true,\n \"id\": 88,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\",\n \"sum\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_sequencer_bftordering_global_ordered_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - requests/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of requests per consensus block.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 73\n },\n \"hideTimeOverride\": true,\n \"id\": 49,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\",\n \"sum\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_sum(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))) / histogram_count(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests / Block\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of blocks ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 73\n },\n \"hideTimeOverride\": true,\n \"id\": 68,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by (namespace, job, node, is_block_empty) (irate(daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} (empty: {{is_block_empty}})\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Throughput - blocks/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches (proofs of availability) per consensus block. Note that < 1 average batch per block likely means empty blocks are being created.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 80\n },\n \"hideTimeOverride\": true,\n \"id\": 89,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\",\n \"sum\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_sum(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))) / histogram_count(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batches (PoAs) / Block\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of batches (proofs of availability) ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 80\n },\n \"hideTimeOverride\": true,\n \"id\": 90,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_global_ordered_batches_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - batches/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Delay introduced by the sequencer core being too slow consuming blocks from ordering, in milliseconds\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"ms\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 87\n },\n \"hideTimeOverride\": true,\n \"id\": 102,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_sequencer_core_backpressure_current_delay_millis{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer core backpressure delay (ms)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the buffer in the subscription from the orderer to the sequencer core, in blocks\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 87\n },\n \"hideTimeOverride\": true,\n \"id\": 103,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\",\n \"min\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_sequencer_core_subscription_buffer_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\\n\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer core buffer size (blocks #)\",\n \"type\": \"timeseries\"\n },\n\n\n\n\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"How many epochs a sequencer is blacklisted\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 94\n },\n \"id\": 86,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"builder\",\n \"expr\": \"daml_sequencer_bftordering_blacklist_sequencer{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{blacklist_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Blacklisted Sequencers\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the stream buffer at various stages\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 94\n },\n \"id\": 104,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_block_stream_buffer_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}.{{element}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Stream Buffer Metrics\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Wall-clock time of the ordered block being provided to the sequencer minus BFT time of the block\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [\n {\n \"options\": {\n \"match\": \"null\",\n \"result\": {\n \"text\": \"N/A\"\n }\n },\n \"type\": \"special\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 2\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 6,\n \"x\": 12,\n \"y\": 94\n },\n \"hideTimeOverride\": true,\n \"id\": 85,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_delay_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block delay\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the payloads of blocks ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 6,\n \"x\": 18,\n \"y\": 94\n },\n \"hideTimeOverride\": false,\n \"id\": 50,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_bytes{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}.{{mode}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Output Block Payload Size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Percentage of votes received by the currently selected sequencer at every major PBFT stage. Should be > 2/3 at all times, else a leader change (\\\"view change\\\" in PBFT parlance) occurs automatically.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"fillOpacity\": 70,\n \"lineWidth\": 0,\n \"spanNulls\": false\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"max\": 1,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"percentage\",\n \"steps\": [\n {\n \"color\": \"red\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 67\n },\n {\n \"color\": \"#6ED0E0\",\n \"value\": 80\n },\n {\n \"color\": \"dark-green\",\n \"value\": 100\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 28,\n \"w\": 24,\n \"x\": 0,\n \"y\": 101\n },\n \"hideTimeOverride\": false,\n \"id\": 73,\n \"options\": {\n \"alignValue\": \"left\",\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"mergeValues\": true,\n \"rowHeight\": 0.9,\n \"showValue\": \"always\",\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_consensus_prepare_votes_percent{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) Prepare {{voting_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_commit_votes_percent{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"hide\": false,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) Commit {{voting_sequencer}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"PBFT Voting Power %\",\n \"transformations\": [\n {\n \"id\": \"renameByRegex\",\n \"options\": {\n \"regex\": \"Prepare SEQ::(.*)::.*\",\n \"renamePattern\": \"Prepare $1\"\n }\n },\n {\n \"id\": \"renameByRegex\",\n \"options\": {\n \"regex\": \"Commit SEQ::(.*)::.*\",\n \"renamePattern\": \"Commit $1\"\n }\n }\n ],\n \"type\": \"state-timeline\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Count of non-compliant and possibly byzantine protocol behaviors detected by the currently selected sequencer.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 129\n },\n \"hideTimeOverride\": false,\n \"id\": 74,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_sequencer_bftordering_security_noncompliant_behavior_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Non-compliant behaviors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the incoming requests buffer (\\\"mempool\\\") for the selected sequencer. Its maximum size in bytes and requests count can be currently set via static configuration.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 129\n },\n \"hideTimeOverride\": true,\n \"id\": 78,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_ingress_requests_queued{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Mempool Size (requests)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of proposals requested by consensus.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 136\n },\n \"hideTimeOverride\": true,\n \"id\": 77,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_requested_proposals{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block proposals requested by consensus\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches requested to be proposed in consensus.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 136\n },\n \"hideTimeOverride\": true,\n \"id\": 79,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_requested_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block proposals requested by consensus (batches)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches that are being disseminated.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 143\n },\n \"hideTimeOverride\": true,\n \"id\": 82,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_disseminating_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ready_for_consensus=~\\\"false|true\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batches being disseminated\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches requested by the availability module and not yet provided.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 143\n },\n \"hideTimeOverride\": true,\n \"id\": 76,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_mempool_requested_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Outstanding batch requests\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 150\n },\n \"id\": 55,\n \"panels\": [],\n \"repeat\": \"instance\",\n \"repeatDirection\": \"h\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sequencer overview: $reporting_sequencer\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of requests in batches that are being disseminated.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 151\n },\n \"hideTimeOverride\": true,\n \"id\": 83,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_disseminating_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ready_for_consensus=~\\\"false|true\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests being disseminated\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total network ingress for the currently selected sequencer split by sender.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 100,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"links\": [],\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 6,\n \"x\": 12,\n \"y\": 151\n },\n \"id\": 59,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, job, node, source_sequencer) (daml_sequencer_bftordering_p2p_receive_received_bytes_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"})\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{source_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Network Input\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total network egress for the currently selected sequencer split by destination.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 100,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"links\": [],\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 6,\n \"x\": 18,\n \"y\": 151\n },\n \"id\": 58,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, job, node, target_sequencer) (daml_sequencer_bftordering_p2p_send_sent_bytes_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"})\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{target_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Network Output\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of payload bytes in requests within batches that are being disseminated.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 158\n },\n \"hideTimeOverride\": true,\n \"id\": 84,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_disseminating_bytes{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ready_for_consensus=~\\\"false|true\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Bytes being disseminated\",\n \"type\": \"timeseries\"\n }\n ],\n \"refresh\": \"5s\",\n \"schemaVersion\": 38,\n \"style\": \"dark\",\n \"tags\": [\n \"bft-sequencers\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status,namespace}\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Job\",\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Node\",\n \"multi\": true,\n \"name\": \"node\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": \".95\",\n \"value\": \".95\"\n },\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Percentile\",\n \"multi\": false,\n \"name\": \"percentile\",\n \"options\": [\n {\n \"selected\": false,\n \"text\": \".5\",\n \"value\": \".5\"\n },\n {\n \"selected\": false,\n \"text\": \".6\",\n \"value\": \".6\"\n },\n {\n \"selected\": false,\n \"text\": \".7\",\n \"value\": \".7\"\n },\n {\n \"selected\": false,\n \"text\": \".75\",\n \"value\": \".75\"\n },\n {\n \"selected\": false,\n \"text\": \".8\",\n \"value\": \".8\"\n },\n {\n \"selected\": false,\n \"text\": \".9\",\n \"value\": \".9\"\n },\n {\n \"selected\": true,\n \"text\": \".95\",\n \"value\": \".95\"\n },\n {\n \"selected\": false,\n \"text\": \".99\",\n \"value\": \".99\"\n },\n {\n \"selected\": false,\n \"text\": \".999\",\n \"value\": \".999\"\n }\n ],\n \"query\": \".5,.6,.7,.75,.8,.9,.95,.99,.999\",\n \"queryValue\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"custom\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-15m\",\n \"to\": \"now\"\n },\n \"timepicker\": {\n \"refresh_intervals\": [\n \"5s\",\n \"10s\",\n \"30s\",\n \"1m\",\n \"5m\",\n \"15m\",\n \"30m\",\n \"1h\",\n \"2h\",\n \"1d\"\n ],\n \"time_options\": [\n \"5m\",\n \"15m\",\n \"1h\",\n \"6h\",\n \"12h\",\n \"24h\",\n \"2d\",\n \"7d\",\n \"30d\"\n ]\n },\n \"timezone\": \"\",\n \"title\": \"BFT ordering\",\n \"uid\": \"UJyurCTWz\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n" + "bft-ordering-performance.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"id\": 10,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"Global\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Wall-clock duration between the instant a request is received by the CantonBFT orderer and the instant the fully assembled ordered block metadata is stored, just before the request is pushed to the post-ordering stages. Its meaningfulness requires wall-clock synchronization, as not all ordered requests are received and timestamped on this node.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_global_requests_ordering_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests end-to-end ordering latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node, ordering_stage) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-availability-total\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total batch dissemination and consensus time (permanence in availability module)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of blocks ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 17,\n \"w\": 8,\n \"x\": 0,\n \"y\": 13\n },\n \"id\": 107,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node, is_block_empty) (irate(daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}.{{node}} (empty: {{is_block_empty}})\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - blocks/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of batches (proofs of availability) ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 17,\n \"w\": 8,\n \"x\": 8,\n \"y\": 13\n },\n \"id\": 108,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_global_ordered_batches_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"interval\": \"\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - batches/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 17,\n \"w\": 8,\n \"x\": 16,\n \"y\": 13\n },\n \"id\": 109,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by (namespace, job, node) (irate(daml_sequencer_bftordering_global_ordered_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - requests/s\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 30\n },\n \"id\": 12,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-PekkoP2PGrpcConnectionManagingActor\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"PekkoP2PGrpcConnectionManagingActor queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 31\n },\n \"id\": 103,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (daml_sequencer_bftordering_performance_moduleQueueSize_PekkoP2PGrpcConnectionManagingActor{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"})\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"PekkoP2PGrpcConnectionManagingActor queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Latency between sending a message and receiving it on the other side\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node, source_sequencer) (rate(daml_sequencer_bftordering_p2p_send_grpc_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{source_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P gRPC network latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The rate of gRPC message send retries\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 47\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node, target_sequencer) (irate(daml_sequencer_bftordering_p2p_send_sends_retried{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}.{{node}}.{{target_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"gRPC send retry rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 47\n },\n \"id\": 17,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_p2p_send_network_write_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P network client write latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 47\n },\n \"id\": 119,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node) (rate(daml_sequencer_bftordering_p2p_receive_processing_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P receive processing latency\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Networking\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 7,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-mempool\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Mempool module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 32\n },\n \"id\": 101,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_mempool{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Mempool module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-availability\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Availability module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 38\n },\n \"id\": 102,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_availability{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Availability module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time waited for a batch while there are outstanding consensus proposal requests and no dissemination is in progress\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 44\n },\n \"id\": 24,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-wait\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Time spent waiting for batches\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Cumulative latency of a batch dissemination until a PoA is formed and it can be present multiple times for a given batch if it regresses due to topology changes\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 44\n },\n \"id\": 21,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-dissemination-total\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total dissemination time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Insertion in proposal queue until next event, like re-sign, re-dissemination, [re-]proposal or batch ordered (emitted 1 or more times per batch)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 50\n },\n \"id\": 23,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-queued-for-block-inclusion\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Time waited for batch inclusion in consensus proposal\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of regressions of a single batch due to topology changes\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 50\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_availability_batch_regressions{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}, {{stage}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batch dissemination regressions rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 56\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-hash-batchId\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batch ID hashing latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 56\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-sign-local-batchId\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Local Batch ID signing latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Insertion in batch queue until extraction and batch send to availability\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 62\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"request-queued-for-batch-inclusion\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Request batch inclusion latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 62\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-sign-remote-batchId\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Remote batch ID signing latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 68\n },\n \"id\": 26,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-sign-local-batches\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sign local batches\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 68\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"availability-signature-verify-ack\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batch ID signature verification\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 74\n },\n \"id\": 82,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.addBatch\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert batch\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 74\n },\n \"id\": 28,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.fetchBatches\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Fetch local batches from DB\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 80\n },\n \"id\": 22,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"batch-validation\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batch validation\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Summary of cache hits, misses, and evictions for the Availability store's batch data cache\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 80\n },\n \"hideTimeOverride\": false,\n \"id\": 110,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_cache_hits{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\", cache=\\\"batch-cache\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} Cache Hits\",\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_cache_misses{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\", cache=\\\"batch-cache\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"hide\": false,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} Cache Misses\",\n \"refId\": \"B\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_cache_evictions{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\", cache=\\\"batch-cache\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"hide\": false,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} Cache Evictions\",\n \"refId\": \"C\"\n }\n ],\n \"title\": \"Availability Store Cache Statistics\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Batches that have been ordered, but aren't available locally so need to be fetched from other peers.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 86\n },\n \"id\": 116,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node, Leader) (irate(daml_sequencer_bftordering_availability_missing_batches_need_output_fetch_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[1m]))\",\n \"legendFormat\": \"(reporter {{namespace}}.{{node}}): originator {{Leader}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Number of batches that need to be fetched\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time from first output fetch request until we get a response.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 86\n },\n \"id\": 118,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node,From) (rate(daml_sequencer_bftordering_availability_output_fetch_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"(reporter {{namespace}}.{{node}}) from {{From}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Output Fetch Latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The rate of batch fetch timeouts per fetch request recipient\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 94\n },\n \"id\": 120,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"increase(daml_sequencer_bftordering_availability_output_fetch_timeouts{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[1m])\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter {{namespace}}.{{node}}) from {{From}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Output Fetch Timeouts\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Mempool+Availability\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 45,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 33\n },\n \"id\": 80,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-consensus\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 33\n },\n \"id\": 104,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_consensus{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 81,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-segment-module\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Segment module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 40\n },\n \"id\": 105,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_segment_module{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Segment module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Block commit time at the consensus (PBFT) level.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [\n {\n \"options\": {\n \"match\": \"null\",\n \"result\": {\n \"text\": \"N/A\"\n }\n },\n \"type\": \"special\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 2\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 47\n },\n \"hideTimeOverride\": true,\n \"id\": 39,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_consensus_commit_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus Block Latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time elapsed between sending a PrePrepare and seeing that the block has been ordered\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 47\n },\n \"id\": 46,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-segment-proposal-to-commit-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus segment block proposal to commit latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time elapsed between ordered blocks proposed by a segment\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 54\n },\n \"id\": 88,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-segment-block-commit-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Consensus segment block ordering latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time between a proposal request to the availability module needed to continue a segment and a PrePrepare being built\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 54\n },\n \"id\": 87,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-block-proposal-wait\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Wait for proposal\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time between the epoch completion and the start of the next epoch\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 60\n },\n \"id\": 86,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-epoch-start-wait\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Wait for epoch start\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time between the last led segment's completion and the epoch completion\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 60\n },\n \"id\": 83,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-epoch-completion-wait\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Wait for epoch completion\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 67\n },\n \"id\": 63,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.startEpoch\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Start epoch (DB)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 67\n },\n \"id\": 47,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.completeEpoch\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Complete epoch (DB)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 73\n },\n \"id\": 64,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPrePrepare\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert pre-prepare\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 73\n },\n \"id\": 50,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPreparesAtomically\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert prepares\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 80\n },\n \"id\": 52,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addOrderedBlockAtomically\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert ordered block\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 80\n },\n \"id\": 66,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-signature-verify-poa-ack\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Check PoA ack signature\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 86\n },\n \"id\": 68,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-validate-signed-message\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Validate message\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 86\n },\n \"id\": 76,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"sign-BftSignedConsensusMessage\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sign message\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 92\n },\n \"id\": 114,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node, Leader) (rate(daml_sequencer_bftordering_consensus_relative_segment_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"interval\": \"\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{Leader}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Relative peer segment completion delay\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of empty blocks created due to segment flushing, in which a node detects that a strong quorum of peers have already finished their segment, and thus the local segment should be rushed (flushed) to completion to reduce ordering and delivery delays.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 92\n },\n \"id\": 115,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_flushed_blocks_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"interval\": \"\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Segment Flush Block Rate\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - main protocol\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 33\n },\n \"id\": 89,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 24,\n \"x\": 0,\n \"y\": 34\n },\n \"id\": 51,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_epoch_view_changes{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"View changes occurred (rate)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 41\n },\n \"id\": 84,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addViewChangeMessage\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert view change message\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 41\n },\n \"id\": 77,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-postponed-view-messages-queue-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 48\n },\n \"id\": 97,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_postponed_view_messages_queue_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 48\n },\n \"id\": 95,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_postponed_view_messages_queue_max_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages queue max size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 55\n },\n \"id\": 94,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_postponed_view_messages_dropped_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages dropped (rate)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 55\n },\n \"id\": 100,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_postponed_view_messages_duplicates_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed view messages duplicated (rate)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 62\n },\n \"id\": 112,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node, Leader) (rate(daml_sequencer_bftordering_consensus_view_change_progress_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"interval\": \"\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{Leader}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Time until progress in view\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - view change\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 34\n },\n \"id\": 90,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 72,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"consensus-validate-consensus-certificate\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Validate state transfer consensus certificate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to transfer a given epoch from peer(s)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 80\n },\n \"id\": 111,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\", ordering_stage=~\\\"state-transfer-total-epoch-transfer-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"Data transfer\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\", ordering_stage=~\\\"state-transfer-store-epochs\\\"}[$__rate_interval])))\",\n \"hide\": false,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Epoch Transfer Duration\",\n \"transformations\": [\n {\n \"id\": \"calculateField\",\n \"options\": {\n \"alias\": \"Total epoch transfer duration\",\n \"binary\": {\n \"left\": \"Data transfer\",\n \"reducer\": \"sum\",\n \"right\": \"Complete + start DB query\"\n },\n \"mode\": \"binary\",\n \"reduce\": {\n \"reducer\": \"sum\"\n }\n }\n }\n ],\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 42\n },\n \"id\": 91,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"state-transfer-postponed-consensus-messages-queue-latency\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed consensus messages queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 42\n },\n \"id\": 96,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_queue_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed consensus messages queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 49\n },\n \"id\": 98,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_queue_max_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed consensus messages queue max size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 49\n },\n \"id\": 99,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_dropped{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Postponed consensus messages dropped (rate)\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - state transfer\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 38,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 36\n },\n \"id\": 85,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_incoming_retransmission_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Incoming retransmission requests rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 36\n },\n \"id\": 40,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_outgoing_retransmission_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Outgoing retransmission requests rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 42\n },\n \"id\": 41,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_retransmitted_messages_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retransmitted messages rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 42\n },\n \"id\": 42,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_retransmitted_commit_certificates_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retransmitted commit certificates rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 48\n },\n \"id\": 78,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"sign-BftSignedRetransmissionMessage\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sign retransmission message\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 48\n },\n \"id\": 79,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"verify-signature-BftSignedRetransmissionMessage\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Verify retransmission message signature\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 0,\n \"y\": 54\n },\n \"id\": 43,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_discarded_wrong_epoch_retransmission_responses_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retransmission responses discarded due to wrong epoch - rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 12,\n \"x\": 12,\n \"y\": 54\n },\n \"id\": 44,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_consensus_discarded_rate_limited_retransmission_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retransmission requests discarded due to rate limiting - rate\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - retransmissions\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 36\n },\n \"id\": 59,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 37\n },\n \"id\": 48,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.latestEpoch\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load latest epoch\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 37\n },\n \"id\": 53,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadEpochProgress\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load epoch progress\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 45\n },\n \"id\": 54,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadCompleteBlocks\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load completed blocks\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Consensus - CFT\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 37\n },\n \"id\": 29,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 55,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"module-queue-output\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Output module queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 38\n },\n \"id\": 106,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_performance_moduleQueueSize_output{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Output module queue size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time needed to inspect requests within a block\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 45\n },\n \"id\": 36,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"output-block-inspection\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block inspection\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 45\n },\n \"id\": 30,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"output-block-fetch-batches\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total block batches fetch time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 52\n },\n \"id\": 37,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertBlockIfMissing\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert block\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 52\n },\n \"id\": 34,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_topology_query_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Query topology\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 59\n },\n \"id\": 31,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertLeaderSelectionPolicyState\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Save blacklist leader selection policy state\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 59\n },\n \"id\": 32,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertEpochIfMissing\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Insert epoch metadata\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 66\n },\n \"id\": 113,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"output-backpressure\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sequencer core backpressure duration\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Output\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 60,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 35,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadEpochInfo\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load epoch info\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 47\n },\n \"id\": 56,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadOrderedBlocks\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Load ordered blocks\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Output - CFT\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 62,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to compute the pruning point, based on retention factors and current time\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 25,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"pruning-compute-pruning-point\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Compute pruning point\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to evict unordered, but expired, batches in the availability DB\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 40\n },\n \"id\": 27,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.gc\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Availability DB - evict expired batches\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to create new table partitions, when necessary\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 47\n },\n \"id\": 61,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.PartitionCreatorImpl.createPartitionsIfNeeded\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Create partitions\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to drop table partitions that can be pruned\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 47\n },\n \"id\": 67,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.PartitionPrunerImpl.prune\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Prune (Delete partitions)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to start the Partition Manager, which may include creating new partitions\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 47\n },\n \"id\": 69,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ordering_stage=~\\\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.create\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Partition manager startup\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Pruning\",\n \"type\": \"row\"\n }\n ],\n \"refresh\": \"5s\",\n \"schemaVersion\": 38,\n \"style\": \"dark\",\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": false,\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status,namespace}\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Job\",\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Node\",\n \"multi\": true,\n \"name\": \"node\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": \".95\",\n \"value\": \".95\"\n },\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Percentile\",\n \"multi\": false,\n \"name\": \"percentile\",\n \"options\": [\n {\n \"selected\": false,\n \"text\": \".5\",\n \"value\": \".5\"\n },\n {\n \"selected\": false,\n \"text\": \".6\",\n \"value\": \".6\"\n },\n {\n \"selected\": false,\n \"text\": \".7\",\n \"value\": \".7\"\n },\n {\n \"selected\": false,\n \"text\": \".75\",\n \"value\": \".75\"\n },\n {\n \"selected\": false,\n \"text\": \".8\",\n \"value\": \".8\"\n },\n {\n \"selected\": false,\n \"text\": \".9\",\n \"value\": \".9\"\n },\n {\n \"selected\": true,\n \"text\": \".95\",\n \"value\": \".95\"\n },\n {\n \"selected\": false,\n \"text\": \".99\",\n \"value\": \".99\"\n },\n {\n \"selected\": false,\n \"text\": \".999\",\n \"value\": \".999\"\n }\n ],\n \"query\": \".5,.6,.7,.75,.8,.9,.95,.99,.999\",\n \"queryValue\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"custom\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-5m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"BFT ordering (performance)\",\n \"uid\": \"c2237cb1-9018-41ac-8eef-24a28ab28f20\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", + "bft-ordering.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"id\": 8,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"collapsed\": false,\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 64,\n \"panels\": [],\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Global overview (instance view)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"ISS proceeds in epochs with stable leader assignments to \\\"segments\\\" and stable topology (i.e., sequencers can be added and remove only at epoch boundaries and leaders are reassigned to segments also at epoch boundaries). This is the current epoch.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 5,\n \"x\": 0,\n \"y\": 1\n },\n \"hideTimeOverride\": false,\n \"id\": 72,\n \"maxDataPoints\": 100,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_epoch{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Current Epoch\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size in blocks of the current epoch.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 3,\n \"x\": 5,\n \"y\": 1\n },\n \"hideTimeOverride\": false,\n \"id\": 80,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_epoch_length{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Epoch Length\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of active validators\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 3,\n \"x\": 8,\n \"y\": 1\n },\n \"hideTimeOverride\": false,\n \"id\": 75,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_validators{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Validators\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of connected and authenticated P2P peers for the selected sequencer. It should be > 2/3 at all times.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"percentage\",\n \"steps\": [\n {\n \"color\": \"dark-red\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 67\n },\n {\n \"color\": \"dark-green\",\n \"value\": 100\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 13,\n \"x\": 11,\n \"y\": 1\n },\n \"hideTimeOverride\": true,\n \"id\": 53,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"center\",\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"last\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"textMode\": \"auto\"\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_p2p_connections_connected{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Connected ({{namespace}}.{{node}})\",\n \"range\": false,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_p2p_connections_authenticated{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"hide\": false,\n \"instant\": true,\n \"legendFormat\": \"Authenticated ({{namespace}}.{{node}})\",\n \"range\": false,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Peers count\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 3,\n \"x\": 0,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 99,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_max_tolerated_faults{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Max tolerated faults\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The size of a weak quorum or non-faulty nodes, needed e.g. to disseminate batches\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 3,\n \"x\": 3,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 100,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_weak_quorum{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Weak quorum\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The size of a strong quorum, needed for ordering consensus\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 3,\n \"x\": 6,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 101,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_strong_quorum{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Strong quorum\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Latest block number ordered and available for the read path. When there is no traffic, a block can be empty.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 7,\n \"x\": 9,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 4,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, job, node) (daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\"})\",\n \"format\": \"time_series\",\n \"hide\": false,\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Output Blocks\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total number of requests ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 8,\n \"x\": 16,\n \"y\": 12\n },\n \"hideTimeOverride\": false,\n \"id\": 40,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace,job,node) (histogram_sum(daml_sequencer_bftordering_output_block_size_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}))\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Output Requests\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Sequencers that are part of the BFT ordering topology\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisGridShow\": true,\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.axisPlacement\",\n \"value\": \"hidden\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 12,\n \"x\": 0,\n \"y\": 24\n },\n \"id\": 91,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_topology_member{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"interval\": \"\",\n \"legendFormat\": \" (reporter: {{namespace}}.{{node}}) {{sequencer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT ordering members\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Sequencers that are allowed to propose blocks for ordering\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.axisPlacement\",\n \"value\": \"hidden\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 12,\n \"x\": 12,\n \"y\": 24\n },\n \"id\": 95,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_topology_topology_leader{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \" (reporter: {{namespace}}.{{node}}) {{sequencer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT ordering leaders\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 8,\n \"x\": 0,\n \"y\": 45\n },\n \"id\": 96,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_p2p_authenticated_endpoint{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{endpoint}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P authenticated endpoints\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.axisPlacement\",\n \"value\": \"hidden\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 8,\n \"x\": 8,\n \"y\": 45\n },\n \"id\": 97,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_p2p_unauthenticated_endpoint{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{endpoint}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P connected but unauthenticated endpoints\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"hidden\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.axisPlacement\",\n \"value\": \"hidden\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 21,\n \"w\": 8,\n \"x\": 16,\n \"y\": 45\n },\n \"id\": 98,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_p2p_disconnected_endpoint{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{endpoint}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"P2P disconnected endpoints\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Wall-clock duration between the instant a request is received by the CantonBFT orderer and the instant the fully assembled ordered block metadata is stored, just before the request is pushed to the post-ordering stages. Its meaningfulness requires wall-clock synchronization, as not all ordered requests are received and timestamped on this node.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 66\n },\n \"id\": 87,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_global_requests_ordering_latency_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests end-to-end ordering latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of requests ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 66\n },\n \"hideTimeOverride\": true,\n \"id\": 88,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\",\n \"sum\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_sequencer_bftordering_global_ordered_requests_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - requests/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of requests per consensus block.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 73\n },\n \"hideTimeOverride\": true,\n \"id\": 49,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\",\n \"sum\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_sum(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))) / histogram_count(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} ({{mode}})\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests / Block\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of blocks ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 73\n },\n \"hideTimeOverride\": true,\n \"id\": 68,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by (namespace, job, node, is_block_empty) (irate(daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} (empty: {{is_block_empty}})\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Throughput - blocks/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches (proofs of availability) per consensus block. Note that < 1 average batch per block likely means empty blocks are being created.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 80\n },\n \"hideTimeOverride\": true,\n \"id\": 89,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\",\n \"sum\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_sum(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval]))) / histogram_count(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}} ({{mode}})\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batches (PoAs) / Block\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of batches (proofs of availability) ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 80\n },\n \"hideTimeOverride\": true,\n \"id\": 90,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"min\",\n \"max\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"irate(daml_sequencer_bftordering_global_ordered_batches_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Throughput - batches/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Delay introduced by the sequencer core being too slow consuming blocks from ordering, in milliseconds\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"ms\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 87\n },\n \"hideTimeOverride\": true,\n \"id\": 102,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_sequencer_core_backpressure_current_delay_millis{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer core backpressure delay (ms)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the buffer in the subscription from the orderer to the sequencer core, in blocks\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 87\n },\n \"hideTimeOverride\": true,\n \"id\": 103,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\",\n \"min\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_sequencer_core_subscription_buffer_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\\n\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer core buffer size (blocks #)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"How many epochs a sequencer is blacklisted\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 94\n },\n \"id\": 86,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"builder\",\n \"expr\": \"daml_sequencer_bftordering_blacklist_sequencer{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{blacklist_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Blacklisted Sequencers\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 94\n },\n \"hideTimeOverride\": true,\n \"id\": 105,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\",\n \"min\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"increase(daml_sequencer_bftordering_topology_blacklisted_epochs{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[5m])\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}): {{sequencer_id}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Total number of epochs a sequencer was blacklisted in the last 5m\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 94\n },\n \"hideTimeOverride\": true,\n \"id\": 105,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\",\n \"min\",\n \"mean\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_topology_blacklisted_epochs{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\\n\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}): {{sequencer_id}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Total number of epochs a sequencer is blacklisted\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the stream buffer at various stages\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 101\n },\n \"id\": 104,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_block_stream_buffer_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}.{{element}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Stream Buffer Metrics\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Wall-clock time of the ordered block being provided to the sequencer minus BFT time of the block\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [\n {\n \"options\": {\n \"match\": \"null\",\n \"result\": {\n \"text\": \"N/A\"\n }\n },\n \"type\": \"special\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 2\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 6,\n \"x\": 12,\n \"y\": 101\n },\n \"hideTimeOverride\": true,\n \"id\": 85,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_delay_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block delay\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the payloads of blocks ordered and output to the read path.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 6,\n \"x\": 18,\n \"y\": 101\n },\n \"hideTimeOverride\": false,\n \"id\": 50,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_bytes{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}.{{mode}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Output Block Payload Size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Percentage of votes received by the currently selected sequencer at every major PBFT stage. Should be > 2/3 at all times, else a leader change (\\\"view change\\\" in PBFT parlance) occurs automatically.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"fillOpacity\": 70,\n \"lineWidth\": 0,\n \"spanNulls\": false\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"max\": 1,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"percentage\",\n \"steps\": [\n {\n \"color\": \"red\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 67\n },\n {\n \"color\": \"#6ED0E0\",\n \"value\": 80\n },\n {\n \"color\": \"dark-green\",\n \"value\": 100\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 28,\n \"w\": 24,\n \"x\": 0,\n \"y\": 108\n },\n \"hideTimeOverride\": false,\n \"id\": 73,\n \"options\": {\n \"alignValue\": \"left\",\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"mergeValues\": true,\n \"rowHeight\": 0.9,\n \"showValue\": \"always\",\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_consensus_prepare_votes_percent{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) Prepare {{voting_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_bftordering_consensus_commit_votes_percent{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"hide\": false,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) Commit {{voting_sequencer}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"PBFT Voting Power %\",\n \"transformations\": [\n {\n \"id\": \"renameByRegex\",\n \"options\": {\n \"regex\": \"Prepare SEQ::(.*)::.*\",\n \"renamePattern\": \"Prepare $1\"\n }\n },\n {\n \"id\": \"renameByRegex\",\n \"options\": {\n \"regex\": \"Commit SEQ::(.*)::.*\",\n \"renamePattern\": \"Commit $1\"\n }\n }\n ],\n \"type\": \"state-timeline\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Count of non-compliant and possibly byzantine protocol behaviors detected by the currently selected sequencer.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 136\n },\n \"hideTimeOverride\": false,\n \"id\": 74,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, node) (irate(daml_sequencer_bftordering_security_noncompliant_behavior_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node=~\\\"$node\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Non-compliant behaviors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the incoming requests buffer (\\\"mempool\\\") for the selected sequencer. Its maximum size in bytes and requests count can be currently set via static configuration.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 136\n },\n \"hideTimeOverride\": true,\n \"id\": 78,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_ingress_requests_queued{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Mempool Size (requests)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of proposals requested by consensus.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 143\n },\n \"hideTimeOverride\": true,\n \"id\": 77,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_requested_proposals{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block proposals requested by consensus\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches requested to be proposed in consensus.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 143\n },\n \"hideTimeOverride\": true,\n \"id\": 79,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_requested_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block proposals requested by consensus (batches)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches that are being disseminated.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 150\n },\n \"hideTimeOverride\": true,\n \"id\": 82,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_disseminating_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ready_for_consensus=~\\\"false|true\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batches being disseminated\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches requested by the availability module and not yet provided.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 150\n },\n \"hideTimeOverride\": true,\n \"id\": 76,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_mempool_requested_batches{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Outstanding batch requests\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 157\n },\n \"id\": 55,\n \"panels\": [],\n \"repeat\": \"instance\",\n \"repeatDirection\": \"h\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sequencer overview: $reporting_sequencer\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of requests in batches that are being disseminated.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 158\n },\n \"hideTimeOverride\": true,\n \"id\": 83,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_disseminating_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ready_for_consensus=~\\\"false|true\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests being disseminated\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total network ingress for the currently selected sequencer split by sender.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 100,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"links\": [],\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 6,\n \"x\": 12,\n \"y\": 158\n },\n \"id\": 59,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, job, node, source_sequencer) (daml_sequencer_bftordering_p2p_receive_received_bytes_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"})\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{source_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Network Input\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total network egress for the currently selected sequencer split by destination.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 100,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"links\": [],\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 6,\n \"x\": 18,\n \"y\": 158\n },\n \"id\": 58,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, job, node, target_sequencer) (daml_sequencer_bftordering_p2p_send_sent_bytes_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\"})\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"(reporter: {{namespace}}.{{node}}) {{target_sequencer}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Network Output\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of payload bytes in requests within batches that are being disseminated.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": null\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 165\n },\n \"hideTimeOverride\": true,\n \"id\": 84,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.5.12\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_bftordering_availability_disseminating_bytes{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$node\\\",ready_for_consensus=~\\\"false|true\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}.{{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Bytes being disseminated\",\n \"type\": \"timeseries\"\n }\n ],\n \"refresh\": \"30s\",\n \"schemaVersion\": 38,\n \"style\": \"dark\",\n \"tags\": [\n \"bft-sequencers\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status,namespace}\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Job\",\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Node\",\n \"multi\": true,\n \"name\": \"node\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": \".95\",\n \"value\": \".95\"\n },\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Percentile\",\n \"multi\": false,\n \"name\": \"percentile\",\n \"options\": [\n {\n \"selected\": false,\n \"text\": \".5\",\n \"value\": \".5\"\n },\n {\n \"selected\": false,\n \"text\": \".6\",\n \"value\": \".6\"\n },\n {\n \"selected\": false,\n \"text\": \".7\",\n \"value\": \".7\"\n },\n {\n \"selected\": false,\n \"text\": \".75\",\n \"value\": \".75\"\n },\n {\n \"selected\": false,\n \"text\": \".8\",\n \"value\": \".8\"\n },\n {\n \"selected\": false,\n \"text\": \".9\",\n \"value\": \".9\"\n },\n {\n \"selected\": true,\n \"text\": \".95\",\n \"value\": \".95\"\n },\n {\n \"selected\": false,\n \"text\": \".99\",\n \"value\": \".99\"\n },\n {\n \"selected\": false,\n \"text\": \".999\",\n \"value\": \".999\"\n }\n ],\n \"query\": \".5,.6,.7,.75,.8,.9,.95,.99,.999\",\n \"queryValue\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"custom\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-15m\",\n \"to\": \"now\"\n },\n \"timepicker\": {\n \"refresh_intervals\": [\n \"5s\",\n \"10s\",\n \"30s\",\n \"1m\",\n \"5m\",\n \"15m\",\n \"30m\",\n \"1h\",\n \"2h\",\n \"1d\"\n ],\n \"time_options\": [\n \"5m\",\n \"15m\",\n \"1h\",\n \"6h\",\n \"12h\",\n \"24h\",\n \"2d\",\n \"7d\",\n \"30d\"\n ]\n },\n \"timezone\": \"\",\n \"title\": \"BFT ordering\",\n \"uid\": \"UJyurCTWz\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n" }, "kind": "ConfigMap", "metadata": { @@ -140,12 +144,12 @@ "inputs": { "apiVersion": "v1", "data": { - "app-rewards.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"Traffic based app rewards (CIP-104)\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 23,\n \"panels\": [],\n \"title\": \"Summary\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of active CalculateRewardsV2 contracts, as seen by the sv app.\\n\\nOne such contract should be created when a round is closed, and should be archived as soon as SVs finish voting on the root hash of the reward merkle tree (usually within a minute).\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 1\n },\n {\n \"color\": \"red\",\n \"value\": 2\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 33,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace,dryRun)((splice_calculate_rewards_v2_active_contracts{namespace=~\\\"$namespace\\\", dryRun=~\\\"$dryRun\\\"} != -1))\",\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"CalculateRewardsV2 contracts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of active ProcessRewardsV2 contracts, as seen by the sv app.\\n\\nA few such contract should be created when the SVs finish voting on the root hash of the reward merkle tree, and they should be archived as the SVs create the corresponding minting allowances. Within a few minutes, there should be no ProcessRewardsV2 contracts left.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 1\n },\n {\n \"color\": \"red\",\n \"value\": 100\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 34,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, dryRun)(splice_process_rewards_v2_active_contracts{namespace=~\\\"$namespace\\\", dryRun=\\\"$dryRun\\\"} != -1)\",\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"ProcessRewardsV2 contracts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"description\": \"Stacked time series for the number of active RewardCouponV2 contracts, as seen by the sv app. Data is averages across all selected SVs, and disaggregated by contract age into 4 hardcoded buckets.\\n\\nRewardCouponV2 contracts have a TTL of 36h by default, validators may decide to wait a few rounds in order to batch the minting of reward coupons.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 50,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineStyle\": {\n \"fill\": \"solid\"\n },\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 35,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"avg by (bucket, ageLowerBound, ageUpperBound)(splice_reward_coupon_v2_active_contracts{namespace=~\\\"$namespace\\\"} != -1)\",\n \"instant\": false,\n \"legendFormat\": \"{{ageLowerBound}} - {{ageUpperBound}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"RewardCouponV2 contracts, average across all SVs, by contract age\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"description\": \"Leaderboard of parties with hidden reward coupons.\\n\\nHidden coupons are created when a party is due rewards, but has not vetted the required package. The reward coupons are created without the party as observer in that case. As soon as the party vets the required package, the party is added as observer.\\n\\nIf you ever see any parties here, tell their validator node to upgrade or fix their package vetting.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 32,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"max by (namespace)(splice_scan_reward_computation_reward_coupons_v2_hidden_coupons{namespace=~\\\"$namespace\\\"})\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Parties with hidden coupons\",\n \"type\": \"table\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 7,\n \"panels\": [],\n \"title\": \"Ingest verdicts and activity records (ScanVerdictIngestionService in scan app)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested verdict record time and the current wall clock time.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"fixedColor\": \"red\",\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 60\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 18\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\"}) - (splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\"} > 0) / 1e6)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Latency: last ingested verdict lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of ingestion of mediator verdicts per second\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 18\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"rate(splice_scan_verdict_ingestion_count_total{namespace=~\\\"$namespace\\\"}[$__rate_interval])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Traffic: verdicts ingestion rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Average and 99th quantile batch size.\\n\\nVerdicts are streamed in from the mediator, then batched in the scan app. For each batch we do one lookup for traffic summaries in the sequencer, and submit one SQL transaction to store all app activity data.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 40\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 18\n },\n \"id\": 19,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"(histogram_sum(sum by(namespace)(rate(splice_scan_verdict_ingestion_batch_size[10m])))\\n/\\nhistogram_count(sum by(namespace)(rate(splice_scan_verdict_ingestion_batch_size[10m]))))\",\n \"legendFormat\": \"{{namespace}} avg\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Saturation: verdict batch size (10min running average)\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 26\n },\n \"id\": 13,\n \"panels\": [],\n \"title\": \"Calculate reward merkle tree (RewardComputationTrigger in scan app)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took to complete a task\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 27\n },\n \"id\": 24,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.5, rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", trigger_name=\\\"RewardComputationTrigger\\\", trigger_type=\\\"taskbased\\\"}[$__rate_interval]))\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: median trigger task duration\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 27\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_trigger_completed_total{trigger_name=~\\\"RewardComputationTrigger\\\",outcome=~\\\"success|failure\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}} {{outcome}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Traffic: Trigger runs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 27\n },\n \"id\": 21,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(errorCodeId, namespace, node_type, trigger_name) (rate(splice_trigger_attempted_total{namespace=~\\\"$namespace\\\", trigger_name=~\\\"RewardComputationTrigger\\\", statusCode!~\\\"OK\\\"}[$__rate_interval])) > 0\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors: Failed trigger attempts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested record time and the current wallclock time.\\n\\nNote that the last ingested record time metric only updates when the store ingests a new transaction so if there is no activity, the last ingested record time will not advance.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 60\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbScanRewardsReferenceStore\\\"}) - (splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbScanRewardsReferenceStore\\\"} > 0) / 1e3)\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: DbScanRewardsReferenceStore lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches in the latest computed round. Median value from all selected scan apps.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 4,\n \"x\": 8,\n \"y\": 35\n },\n \"id\": 22,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"median\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"quantile(0.5,splice_scan_reward_computation_batches_created_count{namespace=~\\\"$namespace\\\"})\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batches\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of activity records in the latest computed round. Median value from all selected scan apps.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 4,\n \"x\": 12,\n \"y\": 35\n },\n \"id\": 16,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"quantile(0.5, splice_scan_reward_computation_activity_records_count{namespace=~\\\"$namespace\\\"})\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Activity records\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of parties with activity in the latest computed round. Median value from all selected scan apps\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 4,\n \"x\": 16,\n \"y\": 35\n },\n \"id\": 18,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"quantile(0.5, splice_scan_reward_computation_active_parties_count{namespace=~\\\"$namespace\\\"})\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Active parties\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of parties with rewards in the latest computed round. Median value from all selected scan apps.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 4,\n \"x\": 20,\n \"y\": 35\n },\n \"id\": 17,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"quantile(0.5, splice_scan_reward_computation_rewarded_parties_count{namespace=~\\\"$namespace\\\"})\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rewarded parties\",\n \"type\": \"stat\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 43\n },\n \"id\": 3,\n \"panels\": [],\n \"title\": \"Voting on root hash (CalculateRewardsTrigger in sv app)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took to complete a task\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 44\n },\n \"id\": 25,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.5, rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", trigger_name=~\\\"CalculateRewards.*Trigger\\\", dryRun=~\\\"$dryRun\\\", trigger_type=\\\"taskbased\\\"}[$__rate_interval]))\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: median trigger task duration\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested record time and the current wallclock time.\\n\\nNote that the last ingested record time metric only updates when the store ingests a new transaction so if there is no activity, the last ingested record time will not advance. For a party performing reward collection, e.g., the validator operator party you expect at least one transaction every round so the lag should not go above 20min.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 60\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 44\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbSvDsoStore\\\"}) - (splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbSvDsoStore\\\"} > 0) / 1e3)\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: DbSvDsoStore lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"the time it took between the closing of a round, and this SV's confirmation for the CalculateRewardsV2 contract's processing\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 44\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"histogram_avg(rate(splice_calculate_rewards_v2_processing_delay_duration_seconds{namespace=~\\\"$namespace\\\", dryRun=~\\\"$dryRun\\\"}[$__rate_interval]))\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: time between round end and confirming root hash\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 52\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_trigger_completed_total{trigger_name=~\\\"ProcessRewardsTrigger.*\\\", dryRun=\\\"$dryRun\\\", outcome=~\\\"success|failure\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}} {{dryRun}} {{outcome}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Traffic: trigger runs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 52\n },\n \"id\": 26,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(statusCode, namespace, node_type, trigger_name) (rate(splice_trigger_attempted_total{namespace=~\\\"$namespace\\\", trigger_name=~\\\"CalculateRewards.*Trigger\\\", dryRun=\\\"$dryRun\\\", statusCode!~\\\"OK\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{dryRun}} {{statusCode}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors: Failed trigger attempts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of BFT reads of the root hash. BFT reads happens if this SV node started ingesting verdicts after the round on which is being voted.\\n\\nI.e., BFT reads should only happen after the SV node was onboarded or after its verdict store data was reset.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 52\n },\n \"id\": 27,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"rate(splice_calculate_rewards_v2_root_hash_bft_reads{namespace=~\\\"$namespace\\\", dryRun=~\\\"$dryRun\\\"}[$__rate_interval])\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate of reading root hash from other scans\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 60\n },\n \"id\": 5,\n \"panels\": [],\n \"title\": \"Creating minting allowances (ProcessRewardsTrigger in sv app)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took to complete a task\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 61\n },\n \"id\": 29,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.5, rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", trigger_name=~\\\"ProcessRewards.*Trigger\\\", dryRun=~\\\"$dryRun\\\", trigger_type=\\\"taskbased\\\"}[$__rate_interval]))\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: median trigger task duration\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested record time and the current wallclock time.\\n\\nNote that the last ingested record time metric only updates when the store ingests a new transaction so if there is no activity, the last ingested record time will not advance. For a party performing reward collection, e.g., the validator operator party you expect at least one transaction every round so the lag should not go above 20min.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 60\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 61\n },\n \"id\": 30,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbSvDsoStore\\\"}) - (splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbSvDsoStore\\\"} > 0) / 1e3)\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: DbSvDsoStore lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took between the closing of a round, and this SV's processing of a ProcessRewardsV2 contract for that round\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 61\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(rate(splice_process_rewards_v2_processing_delay_duration_seconds{namespace=~\\\"$namespace\\\", dryRun=~\\\"$dryRun\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: time between round end and processing a reward batch\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 69\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_trigger_completed_total{trigger_name=~\\\"ProcessRewards.*Trigger\\\", dryRun=~\\\"$dryRun\\\", outcome=~\\\"success|failure\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}} {{dryRun}} {{outcome}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Traffic: trigger runs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Note: this is a delegate based trigger. It is normal that this trigger fails due to contention as multiple SVs attempt to complete the same task at the same time. Example errors:\\n\\n- FAILED_PRECONDITION: UNKNOWN_CONTRACT_SYNCHRONIZERS: The following contracts have been archived\\n- NOT_FOUND: LOCAL_VERDICT_INACTIVE_CONTRACTS: Rejected transaction is referring to inactive contracts\\n- ABORTED: LOCAL_VERDICT_LOCKED_CONTRACTS: Rejected transaction is referring to locked contracts\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 69\n },\n \"id\": 28,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(statusCode, namespace, node_type, trigger_name) (rate(splice_trigger_attempted_total{namespace=~\\\"$namespace\\\", trigger_name=~\\\"ProcessRewards.*Trigger\\\", dryRun=~\\\"$dryRun\\\", statusCode!~\\\"OK\\\"}[$__rate_interval])) > 0\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{true}} {{statusCode}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors: Failed trigger attempts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of BFT reads of reward data. BFT reads happens if this SV node's RewardComputationTrigger did not (yet) calculate rewards for this round.\\n\\nBFT reads here mean the SV is behind on calculating rewards, but still participates in creating minting allowance contracts.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 69\n },\n \"id\": 31,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"rate(splice_process_rewards_v2_batch_bft_reads_total{namespace=~\\\"$namespace\\\", dryRun=\\\"$dryRun\\\"}[$__rate_interval])\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate of reading reward data from other scans\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_calculate_rewards_v2_processing_delay_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_calculate_rewards_v2_processing_delay_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": true,\n \"current\": {\n \"text\": [\n \"false\"\n ],\n \"value\": [\n \"false\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_calculate_rewards_v2_processing_delay_duration_seconds,dryRun)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"dryRun\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_calculate_rewards_v2_processing_delay_duration_seconds,dryRun)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Traffic-based app rewards (CIP-104)\",\n \"uid\": \"cnx7gsn\",\n \"version\": 41,\n \"weekStart\": \"\"\n}\n", + "app-rewards.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"Traffic based app rewards (CIP-104)\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 23,\n \"panels\": [],\n \"title\": \"Summary\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of active CalculateRewardsV2 contracts, as seen by the sv app.\\n\\nOne such contract should be created when a round is closed, and should be archived as soon as SVs finish voting on the root hash of the reward merkle tree (usually within a minute).\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 1\n },\n {\n \"color\": \"red\",\n \"value\": 2\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 33,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace,dryRun)((splice_calculate_rewards_v2_active_contracts{namespace=~\\\"$namespace\\\", dryRun=~\\\"$dryRun\\\"} != -1))\",\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"CalculateRewardsV2 contracts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of active ProcessRewardsV2 contracts, as seen by the sv app.\\n\\nA few such contract should be created when the SVs finish voting on the root hash of the reward merkle tree, and they should be archived as the SVs create the corresponding minting allowances. Within a few minutes, there should be no ProcessRewardsV2 contracts left.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 1\n },\n {\n \"color\": \"red\",\n \"value\": 100\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 34,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, dryRun)(splice_process_rewards_v2_active_contracts{namespace=~\\\"$namespace\\\", dryRun=\\\"$dryRun\\\"} != -1)\",\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"ProcessRewardsV2 contracts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"description\": \"Stacked time series for the number of active RewardCouponV2 contracts, as seen by the sv app. Data is averages across all selected SVs, and disaggregated by contract age into 4 hardcoded buckets.\\n\\nRewardCouponV2 contracts have a TTL of 36h by default, validators may decide to wait a few rounds in order to batch the minting of reward coupons.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 50,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineStyle\": {\n \"fill\": \"solid\"\n },\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 35,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"avg by (bucket, ageLowerBound, ageUpperBound)(splice_reward_coupon_v2_active_contracts{namespace=~\\\"$namespace\\\"} != -1)\",\n \"instant\": false,\n \"legendFormat\": \"{{ageLowerBound}} - {{ageUpperBound}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"RewardCouponV2 contracts, average across all SVs, by contract age\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"uid\": \"prometheus\"\n },\n \"description\": \"Leaderboard of parties with hidden reward coupons.\\n\\nHidden coupons are created when a party is due rewards, but has not vetted the required package. The reward coupons are created without the party as observer in that case. As soon as the party vets the required package, the party is added as observer.\\n\\nIf you ever see any parties here, tell their validator node to upgrade or fix their package vetting.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 100\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Time\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 200\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 32,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"enablePagination\": true,\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"max by (party)(splice_reward_coupons_v2_hidden_coupons{namespace=~\\\"$namespace\\\"})\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Parties with hidden coupons\",\n \"transformations\": [\n {\n \"id\": \"sortBy\",\n \"options\": {\n \"fields\": {},\n \"sort\": [\n {\n \"desc\": true,\n \"field\": \"Value\"\n }\n ]\n }\n },\n {\n \"id\": \"filterFieldsByName\",\n \"options\": {\n \"include\": {\n \"names\": [\n \"party\",\n \"Value\"\n ]\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 7,\n \"panels\": [],\n \"title\": \"Ingest verdicts and activity records (ScanVerdictIngestionService in scan app)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested verdict record time and the current wall clock time.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"fixedColor\": \"red\",\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 60\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 18\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\"}) - (splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\"} > 0) / 1e6)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Latency: last ingested verdict lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of ingestion of mediator verdicts per second\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 18\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"rate(splice_scan_verdict_ingestion_count_total{namespace=~\\\"$namespace\\\"}[$__rate_interval])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Traffic: verdicts ingestion rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Average and 99th quantile batch size.\\n\\nVerdicts are streamed in from the mediator, then batched in the scan app. For each batch we do one lookup for traffic summaries in the sequencer, and submit one SQL transaction to store all app activity data.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 40\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 18\n },\n \"id\": 19,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"(histogram_sum(sum by(namespace)(rate(splice_scan_verdict_ingestion_batch_size[10m])))\\n/\\nhistogram_count(sum by(namespace)(rate(splice_scan_verdict_ingestion_batch_size[10m]))))\",\n \"legendFormat\": \"{{namespace}} avg\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Saturation: verdict batch size (10min running average)\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 26\n },\n \"id\": 13,\n \"panels\": [],\n \"title\": \"Calculate reward merkle tree (RewardComputationTrigger in scan app)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took to complete a task\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 27\n },\n \"id\": 24,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.5, rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", trigger_name=\\\"RewardComputationTrigger\\\", trigger_type=\\\"taskbased\\\"}[$__rate_interval]))\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: median trigger task duration\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 27\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_trigger_completed_total{trigger_name=~\\\"RewardComputationTrigger\\\",outcome=~\\\"success|failure\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}} {{outcome}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Traffic: Trigger runs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 27\n },\n \"id\": 21,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(errorCodeId, namespace, node_type, trigger_name) (rate(splice_trigger_attempted_total{namespace=~\\\"$namespace\\\", trigger_name=~\\\"RewardComputationTrigger\\\", statusCode!~\\\"OK\\\"}[$__rate_interval])) > 0\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors: Failed trigger attempts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested record time and the current wallclock time.\\n\\nNote that the last ingested record time metric only updates when the store ingests a new transaction so if there is no activity, the last ingested record time will not advance.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 60\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbScanRewardsReferenceStore\\\"}) - (splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbScanRewardsReferenceStore\\\"} > 0) / 1e3)\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: DbScanRewardsReferenceStore lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of batches in the latest computed round. Median value from all selected scan apps.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 4,\n \"x\": 8,\n \"y\": 35\n },\n \"id\": 22,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\"median\"],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"quantile(0.5,splice_scan_reward_computation_batches_created_count{namespace=~\\\"$namespace\\\"})\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Batches\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of activity records in the latest computed round. Median value from all selected scan apps.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 4,\n \"x\": 12,\n \"y\": 35\n },\n \"id\": 16,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\"lastNotNull\"],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"quantile(0.5, splice_scan_reward_computation_activity_records_count{namespace=~\\\"$namespace\\\"})\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Activity records\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of parties with activity in the latest computed round. Median value from all selected scan apps\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 4,\n \"x\": 16,\n \"y\": 35\n },\n \"id\": 18,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\"lastNotNull\"],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"quantile(0.5, splice_scan_reward_computation_active_parties_count{namespace=~\\\"$namespace\\\"})\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Active parties\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of parties with rewards in the latest computed round. Median value from all selected scan apps.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 4,\n \"x\": 20,\n \"y\": 35\n },\n \"id\": 17,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\"lastNotNull\"],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"quantile(0.5, splice_scan_reward_computation_rewarded_parties_count{namespace=~\\\"$namespace\\\"})\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rewarded parties\",\n \"type\": \"stat\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 43\n },\n \"id\": 3,\n \"panels\": [],\n \"title\": \"Voting on root hash (CalculateRewardsTrigger in sv app)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took to complete a task\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 44\n },\n \"id\": 25,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.5, rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", trigger_name=~\\\"CalculateRewards.*Trigger\\\", dryRun=~\\\"$dryRun\\\", trigger_type=\\\"taskbased\\\"}[$__rate_interval]))\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: median trigger task duration\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested record time and the current wallclock time.\\n\\nNote that the last ingested record time metric only updates when the store ingests a new transaction so if there is no activity, the last ingested record time will not advance. For a party performing reward collection, e.g., the validator operator party you expect at least one transaction every round so the lag should not go above 20min.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 60\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 44\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbSvDsoStore\\\"}) - (splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbSvDsoStore\\\"} > 0) / 1e3)\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: DbSvDsoStore lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"the time it took between the closing of a round, and this SV's confirmation for the CalculateRewardsV2 contract's processing\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 44\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"histogram_avg(rate(splice_calculate_rewards_v2_processing_delay_duration_seconds{namespace=~\\\"$namespace\\\", dryRun=~\\\"$dryRun\\\"}[$__rate_interval]))\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: time between round end and confirming root hash\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 52\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_trigger_completed_total{trigger_name=~\\\"ProcessRewardsTrigger.*\\\", dryRun=\\\"$dryRun\\\", outcome=~\\\"success|failure\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}} {{dryRun}} {{outcome}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Traffic: trigger runs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 52\n },\n \"id\": 26,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(statusCode, namespace, node_type, trigger_name) (rate(splice_trigger_attempted_total{namespace=~\\\"$namespace\\\", trigger_name=~\\\"CalculateRewards.*Trigger\\\", dryRun=\\\"$dryRun\\\", statusCode!~\\\"OK\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{dryRun}} {{statusCode}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors: Failed trigger attempts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of BFT reads of the root hash. BFT reads happens if this SV node started ingesting verdicts after the round on which is being voted.\\n\\nI.e., BFT reads should only happen after the SV node was onboarded or after its verdict store data was reset.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 52\n },\n \"id\": 27,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"rate(splice_calculate_rewards_v2_root_hash_bft_reads{namespace=~\\\"$namespace\\\", dryRun=~\\\"$dryRun\\\"}[$__rate_interval])\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate of reading root hash from other scans\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 60\n },\n \"id\": 5,\n \"panels\": [],\n \"title\": \"Creating minting allowances (ProcessRewardsTrigger in sv app)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took to complete a task\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 61\n },\n \"id\": 29,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.5, rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", trigger_name=~\\\"ProcessRewards.*Trigger\\\", dryRun=~\\\"$dryRun\\\", trigger_type=\\\"taskbased\\\"}[$__rate_interval]))\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: median trigger task duration\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested record time and the current wallclock time.\\n\\nNote that the last ingested record time metric only updates when the store ingests a new transaction so if there is no activity, the last ingested record time will not advance. For a party performing reward collection, e.g., the validator operator party you expect at least one transaction every round so the lag should not go above 20min.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 60\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 61\n },\n \"id\": 30,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbSvDsoStore\\\"}) - (splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",store_name=~\\\"DbSvDsoStore\\\"} > 0) / 1e3)\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: DbSvDsoStore lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took between the closing of a round, and this SV's processing of a ProcessRewardsV2 contract for that round\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 61\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(rate(splice_process_rewards_v2_processing_delay_duration_seconds{namespace=~\\\"$namespace\\\", dryRun=~\\\"$dryRun\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency: time between round end and processing a reward batch\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 69\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_trigger_completed_total{trigger_name=~\\\"ProcessRewards.*Trigger\\\", dryRun=~\\\"$dryRun\\\", outcome=~\\\"success|failure\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval])\",\n \"legendFormat\": \"{{namespace}} {{dryRun}} {{outcome}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Traffic: trigger runs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Note: this is a delegate based trigger. It is normal that this trigger fails due to contention as multiple SVs attempt to complete the same task at the same time. Example errors:\\n\\n- FAILED_PRECONDITION: UNKNOWN_CONTRACT_SYNCHRONIZERS: The following contracts have been archived\\n- NOT_FOUND: LOCAL_VERDICT_INACTIVE_CONTRACTS: Rejected transaction is referring to inactive contracts\\n- ABORTED: LOCAL_VERDICT_LOCKED_CONTRACTS: Rejected transaction is referring to locked contracts\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 8,\n \"y\": 69\n },\n \"id\": 28,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(statusCode, namespace, node_type, trigger_name) (rate(splice_trigger_attempted_total{namespace=~\\\"$namespace\\\", trigger_name=~\\\"ProcessRewards.*Trigger\\\", dryRun=~\\\"$dryRun\\\", statusCode!~\\\"OK\\\"}[$__rate_interval])) > 0\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{true}} {{statusCode}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors: Failed trigger attempts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of BFT reads of reward data. BFT reads happens if this SV node's RewardComputationTrigger did not (yet) calculate rewards for this round.\\n\\nBFT reads here mean the SV is behind on calculating rewards, but still participates in creating minting allowance contracts.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 16,\n \"y\": 69\n },\n \"id\": 31,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"rate(splice_process_rewards_v2_batch_bft_reads_total{namespace=~\\\"$namespace\\\", dryRun=\\\"$dryRun\\\"}[$__rate_interval])\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{dryRun}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate of reading reward data from other scans\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_calculate_rewards_v2_processing_delay_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_calculate_rewards_v2_processing_delay_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": true,\n \"current\": {\n \"text\": [\n \"false\"\n ],\n \"value\": [\n \"false\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_calculate_rewards_v2_processing_delay_duration_seconds,dryRun)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"dryRun\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_calculate_rewards_v2_processing_delay_duration_seconds,dryRun)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Traffic-based app rewards (CIP-104)\",\n \"uid\": \"cnx7gsn\",\n \"version\": 41,\n \"weekStart\": \"\"\n}\n", "automations.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"Track the trigger automations being run by Splice Apps\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 30,\n \"panels\": [],\n \"title\": \"Automation Service Health\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"noValue\": \"0\",\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 5,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 34,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"count by (namespace, job, automation_service) (splice_automation_background_service_health{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\"} == 1)\",\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{automation_service}}\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Unhealthy services\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Current health of registered automation background services.\\n-1 = no reading yet (or error reading health), 0 = healthy, 1 = unhealthy.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"color-background\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [\n {\n \"options\": {\n \"0\": {\n \"color\": \"green\",\n \"index\": 1,\n \"text\": \"healthy\"\n },\n \"1\": {\n \"color\": \"red\",\n \"index\": 2,\n \"text\": \"unhealthy\"\n },\n \"-1\": {\n \"color\": \"text\",\n \"index\": 0,\n \"text\": \"unknown\"\n }\n },\n \"type\": \"value\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"text\",\n \"value\": 0\n },\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Time\"\n },\n \"properties\": [\n {\n \"id\": \"custom.hideFrom.viz\",\n \"value\": true\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"exported_service\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 785\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Node\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 109\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Namespace\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 110\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"automation_service\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 296\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"App\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 94\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 19,\n \"x\": 5,\n \"y\": 1\n },\n \"id\": 31,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"Status\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"splice_automation_background_service_health{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\"}\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Background service health\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true,\n \"__name__\": true,\n \"container\": true,\n \"endpoint\": true,\n \"instance\": true,\n \"job\": true,\n \"migration\": true,\n \"otel_scope_name\": true,\n \"pod\": true,\n \"prometheus\": true,\n \"service\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Value\": \"Status\",\n \"instance\": \"Instance\",\n \"namespace\": \"Namespace\",\n \"node_name\": \"Node\",\n \"node_type\": \"App\",\n \"service\": \"Service\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Health of automation background services over time (-1 unknown, 0 healthy, 1 unhealthy).\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"stepAfter\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [\n {\n \"options\": {\n \"0\": {\n \"index\": 1,\n \"text\": \"healthy\"\n },\n \"1\": {\n \"index\": 2,\n \"text\": \"unhealthy\"\n },\n \"-1\": {\n \"index\": 0,\n \"text\": \"unknown\"\n }\n },\n \"type\": \"value\"\n }\n ],\n \"max\": 1,\n \"min\": -1,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 10\n },\n \"id\": 32,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_automation_background_service_health{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\"}\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{node_type}} {{automation_service}} {{exported_service}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Background service health over time\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 20\n },\n \"id\": 33,\n \"panels\": [],\n \"title\": \"Runs\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"runs / second\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byRegexp\",\n \"options\": \"/.*failure/\"\n },\n \"properties\": [\n {\n \"id\": \"custom.fillOpacity\",\n \"value\": 50\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 21\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_trigger_completed_total{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", outcome=~\\\"success|failure\\\"}[$__rate_interval])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"instant\": false,\n \"legendFormat\": \"{{node_name}} {{trigger_name}} {{outcome}} \",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Runs\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 3,\n \"panels\": [],\n \"repeat\": \"trigger_type\",\n \"title\": \"Triggers $trigger_type\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The rate of successful iterations completed\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"noValue\": \"0\",\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 4,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 5,\n \"options\": {\n \"colorMode\": \"none\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"center\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum(histogram_sum(rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", trigger_type=\\\"$trigger_type\\\"}[$__rate_interval])))\",\n \"instant\": true,\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Traffic\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The rate at which the trigger(s) are failing with retryable errors\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 4,\n \"x\": 4,\n \"y\": 32\n },\n \"id\": 7,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_retries_failures{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", trigger_type=\\\"$trigger_type\\\", error_kind=\\\"transient\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Retries\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The rate at which trigger(s) are failing with fatal/non-retryable errors\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 4,\n \"x\": 8,\n \"y\": 32\n },\n \"id\": 8,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_retries_failures{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", trigger_type=\\\"$trigger_type\\\", error_kind=\\\"fatal\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time it took to perform an iteration\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 32\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.95, rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", trigger_type=\\\"$trigger_type\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{node_type}} {{service}} {{trigger_name}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"continuous-RdYlGr\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 90\n },\n {\n \"color\": \"#6ED0E0\",\n \"value\": 100\n },\n {\n \"color\": \"#EF843C\",\n \"value\": 110\n },\n {\n \"color\": \"#E24D42\",\n \"value\": 120\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 8,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 10,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"frameIndex\": 0,\n \"showHeader\": true,\n \"sortBy\": []\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(trigger_name) (histogram_count(rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_type=\\\"$trigger_type\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Top 5 Active Triggers\",\n \"transformations\": [\n {\n \"id\": \"reduce\",\n \"options\": {\n \"includeTimeField\": false,\n \"labelsToFields\": true,\n \"mode\": \"seriesToRows\",\n \"reducers\": [\n \"sum\"\n ]\n }\n },\n {\n \"id\": \"sortBy\",\n \"options\": {\n \"fields\": {},\n \"sort\": [\n {\n \"desc\": true,\n \"field\": \"Total\"\n }\n ]\n }\n },\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Field\": true\n },\n \"indexByName\": {},\n \"renameByName\": {\n \"Total\": \"Traffic\",\n \"trigger_name\": \"Trigger\"\n }\n }\n },\n {\n \"id\": \"limit\",\n \"options\": {\n \"limitField\": 5\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"How busy triggers are within a certain time window\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 16,\n \"x\": 8,\n \"y\": 39\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(trigger_name, instance, node_type, trigger_type) (histogram_sum(rate(splice_trigger_latency_duration_seconds{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", trigger_type=\\\"$trigger_type\\\"}[$__rate_interval])))\",\n \"instant\": false,\n \"legendFormat\": \"{{node_type}} {{instance}} {{trigger_name}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Saturation\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"How busy triggers are within a certain time window\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"ops\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 47\n },\n \"id\": 23,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(trigger_name, instance, node_type, trigger_type, outcome) (rate(splice_trigger_completed_total{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", trigger_type=\\\"$trigger_type\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"legendFormat\": \"{{node_type}} {{instance}} {{trigger_name}} {{outcome}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Completed\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 55\n },\n \"id\": 22,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_trigger_attempted_total{isDsoDelegateTrigger=~\\\"$isDsoDelegateTrigger\\\", trigger_name=~\\\"$trigger_name\\\", namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"total attempts\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_trigger_attempted_total{statusCode!~\\\"OK\\\", isDsoDelegateTrigger=~\\\"$isDsoDelegateTrigger\\\", namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", contentionFailure=\\\"true\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"total contentions\",\n \"range\": true,\n \"refId\": \"B\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Trigger attempts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 63\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"last\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(errorCodeId, namespace, node_type, trigger_name) (rate(splice_trigger_attempted_total{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\", trigger_name=~\\\"$trigger_name\\\", isDsoDelegateTrigger=~\\\"$isDsoDelegateTrigger\\\", contentionFailure=\\\"true\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Failed trigger attempts with contention errors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 71\n },\n \"id\": 21,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"last\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by (errorCodeId, namespace, node_type, trigger_name) (rate(splice_trigger_attempted_total{namespace=~\\\"$namespace\\\",node_type=~\\\"$app\\\",trigger_name=~\\\"$trigger_name\\\",isDsoDelegateTrigger=~\\\"$isDsoDelegateTrigger\\\", statusCode!~\\\"OK\\\", contentionFailure=\\\"false\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"B\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Failed trigger attempts with non-contention errors\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_trigger_completed_total,namespace)\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_trigger_completed_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"scan\",\n \"value\": \"scan\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_trigger_completed_total{namespace=~\\\"$namespace\\\"},node_type)\",\n \"includeAll\": true,\n \"label\": \"App\",\n \"name\": \"app\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_trigger_completed_total{namespace=~\\\"$namespace\\\"},node_type)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_trigger_completed_total{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\"},trigger_name)\",\n \"includeAll\": true,\n \"label\": \"Trigger Name\",\n \"name\": \"trigger_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_trigger_completed_total{namespace=~\\\"$namespace\\\", node_type=~\\\"$app\\\"},trigger_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": [\n \"taskbased\",\n \"polling\"\n ],\n \"value\": [\n \"taskbased\",\n \"polling\"\n ]\n },\n \"includeAll\": false,\n \"multi\": true,\n \"name\": \"trigger_type\",\n \"options\": [],\n \"query\": \"taskbased, polling\",\n \"type\": \"custom\",\n \"valuesFormat\": \"csv\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"definition\": \"label_values(isDsoDelegateTrigger)\",\n \"includeAll\": true,\n \"label\": \"isDsoDelegateTrigger\",\n \"name\": \"isDsoDelegateTrigger\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(isDsoDelegateTrigger)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Automations\",\n \"uid\": \"a3e1385f-6f03-46d9-908c-34aca0f507a6\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", "catchup.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"Client Delay\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"description\": \"Delay on event processing of a sequencer, compared to the collective\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"ms\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 24,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"min by (namespace, job) (daml_sequencer_block_delay{namespace=~\\\"$namespace\\\", component=\\\"sequencer\\\",job=~\\\"global-domain-$migration-sequencer\\\"})\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Client Delay\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"description\": \"Catchup speed of the sequencer in the last 5min\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"ms\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 24,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"max by (namespace, job) (delta(daml_sequencer_block_delay{namespace=~\\\"$namespace\\\", component=\\\"sequencer\\\",job=~\\\"global-domain-$migration-sequencer\\\"}[5m])) / 5\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"processing time {{namespace}}-{{job}} / min\",\n \"range\": true,\n \"refId\": \"B\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Catchup Speed\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"description\": \"Delay on event processing of the participant, compared to the sequencers it is connected to.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 23\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace) (timestamp(daml_sequencer_client_handler_last_sequencing_time_micros{namespace=~\\\"$namespace\\\",component=\\\"participant\\\"}) - ((daml_sequencer_client_handler_last_sequencing_time_micros{namespace=~\\\"$namespace\\\",component=\\\"participant\\\"} > 0) / 1e6))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{node}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Participant Client Delay\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"description\": \"Delay on event processing of a mediator, compared to its corresponding sequencer\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 23\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, job) (timestamp(daml_sequencer_client_handler_last_sequencing_time_micros{namespace=~\\\"$namespace\\\",component=\\\"mediator\\\",job=~\\\"global-domain-$migration-mediator\\\"}) - ((daml_sequencer_client_handler_last_sequencing_time_micros{namespace=~\\\"$namespace\\\",component=\\\"mediator\\\",job=~\\\"global-domain-$migration-mediator\\\"} > 0) / 1e6)) \",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Mediator Client Delay\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"CometBFT\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 24,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, job) (rate(daml_sequencer_block_events_total{namespace=~\\\"$namespace\\\",job=~\\\"global-domain-$migration-sequencer\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}} {{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer processing event Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"rate((daml_sequencer_block_height{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\"} > 0)[$__rate_interval:])\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}} {{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer processing Block Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 24,\n \"x\": 0,\n \"y\": 46\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"rate(cometbft_consensus_height{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-cometbft-cometbft-rpc\\\"}[$__rate_interval])\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Cometbft producing Block Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 52\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_block_height{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Cometbft height \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 52\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_latest_block_height{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-cometbft-cometbft-rpc\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{chain_id}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"CometBFT Block Height\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 60\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_block_syncing{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-cometbft-cometbft-rpc\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"CometBFT Block Syncing\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 60\n },\n \"id\": 15,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"exemplar\": false,\n \"expr\": \"max by(namespace, migration) (label_replace(cometbft_consensus_latest_block_height{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-cometbft-cometbft-rpc\\\"}, \\\"migration\\\", \\\"$1\\\", \\\"job\\\", \\\"global-domain-(\\\\\\\\d)-.*\\\")) - max by(namespace, migration) (label_replace(daml_sequencer_block_height{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\"}, \\\"migration\\\", \\\"$1\\\", \\\"job\\\", \\\"global-domain-(\\\\\\\\d)-.*\\\"))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"{{namespace}}, migration={{migration}}\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"CometBFT blocks to process\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 68\n },\n \"id\": 17,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"exemplar\": false,\n \"expr\": \"max by(namespace, migration) (label_replace(cometbft_consensus_latest_block_height{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-cometbft-cometbft-rpc\\\"}, \\\"migration\\\", \\\"$1\\\", \\\"job\\\", \\\"global-domain-(\\\\\\\\d)-.*\\\")) - max by(namespace, migration) (label_replace(daml_sequencer_block_height{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\"}, \\\"migration\\\", \\\"$1\\\", \\\"job\\\", \\\"global-domain-(\\\\\\\\d)-.*\\\"))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}, migration={{migration}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"CometBFT blocks to process\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"Prometheus\",\n \"value\": \"prometheus\"\n },\n \"includeAll\": false,\n \"name\": \"DS\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"text\": [\n \"sv\",\n \"sv-1\"\n ],\n \"value\": [\n \"sv\",\n \"sv-1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_handler_delay,namespace)\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_sequencer_client_handler_delay,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_handler_delay{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"migration\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_client_handler_delay{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"/global-domain-(?\\\\d+)-sequencer/g\",\n \"regexApplyTo\": \"value\",\n \"sort\": 1,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Global Domain Catchup\",\n \"uid\": \"ca9df344-c699-4efe-83c2-5fb2639d96d9\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", "cometbft-network-status.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"datasource\",\n \"uid\": \"grafana\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"Health status of the CometBFT P2P Network as measured by incoming / outgoing \",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 1283,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"description\": \"Total peer-wise rate of bytes sent or received over all channels over the P2P network\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"fieldMinMax\": false,\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"percentage\",\n \"steps\": [\n {\n \"color\": \"red\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 1\n },\n {\n \"color\": \"green\",\n \"value\": 10\n }\n ]\n },\n \"unit\": \"KBs\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 66,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"last\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"11.1.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (peer_id) (rate(cometbft_p2p_peer_receive_bytes_total{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval]) / 1000) + sum by (peer_id) (rate(cometbft_p2p_peer_send_bytes_total{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval]) / 1000)\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Network Throughput (current)\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"description\": \"Peer-wise rate of bytes received over all channels over the P2P network\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"fieldMinMax\": false,\n \"links\": [],\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 2000\n }\n ]\n },\n \"unit\": \"binBps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 13\n },\n \"id\": 59,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"asc\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (peer_id) (rate(cometbft_p2p_peer_receive_bytes_total{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{peer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Incoming Network Throughput\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"description\": \"Peer-wise rate of bytes sent over all channels over the P2P network\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"fieldMinMax\": false,\n \"links\": [],\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 2000\n }\n ]\n },\n \"unit\": \"binBps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 65,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"asc\"\n }\n },\n \"pluginVersion\": \"10.1.5\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (peer_id) (rate(cometbft_p2p_peer_send_bytes_total{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval]))\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{peer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Outgoing Network Throughput\",\n \"type\": \"timeseries\"\n }\n ],\n \"refresh\": \"\",\n \"schemaVersion\": 39,\n \"tags\": [\n \"Blockchain\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"default\",\n \"value\": \"default\"\n },\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Datasource\",\n \"multi\": false,\n \"name\": \"DS\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"queryValue\": \"\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"definition\": \"label_values(cometbft_p2p_message_receive_bytes_total,namespace)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"namespace\",\n \"multi\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(cometbft_p2p_message_receive_bytes_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 1,\n \"tagValuesQuery\": \"\",\n \"tagsQuery\": \"\",\n \"type\": \"query\",\n \"useTags\": false\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": \"cidaily-0-0.2.0-snapshot.20240725.6534.0.v112d3fc6\",\n \"value\": \"cidaily-0-0.2.0-snapshot.20240725.6534.0.v112d3fc6\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"definition\": \"label_values(cometbft_p2p_message_receive_bytes_total,chain_id)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Chain ID\",\n \"multi\": false,\n \"name\": \"chain_id\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(cometbft_p2p_message_receive_bytes_total,chain_id)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"tagValuesQuery\": \"\",\n \"tagsQuery\": \"\",\n \"type\": \"query\",\n \"useTags\": false\n },\n {\n \"allValue\": \"\",\n \"current\": {\n \"selected\": false,\n \"text\": \"global-domain-0-cometbft-cometbft-rpc\",\n \"value\": \"global-domain-0-cometbft-cometbft-rpc\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"definition\": \"label_values(cometbft_p2p_message_receive_bytes_total{chain_id=\\\"$chain_id\\\"},job)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Instance\",\n \"multi\": false,\n \"name\": \"instance\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(cometbft_p2p_message_receive_bytes_total{chain_id=\\\"$chain_id\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 5,\n \"tagValuesQuery\": \"\",\n \"tagsQuery\": \"\",\n \"type\": \"query\",\n \"useTags\": false\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {\n \"refresh_intervals\": [\n \"5s\",\n \"10s\",\n \"30s\",\n \"1m\",\n \"5m\",\n \"15m\",\n \"30m\",\n \"1h\",\n \"2h\",\n \"1d\"\n ],\n \"time_options\": [\n \"5m\",\n \"15m\",\n \"1h\",\n \"6h\",\n \"12h\",\n \"24h\",\n \"2d\",\n \"7d\",\n \"30d\"\n ]\n },\n \"timezone\": \"\",\n \"title\": \"CometBFT Network Status\",\n \"uid\": \"ddsuu1wnxwetcd\",\n \"version\": 7,\n \"weekStart\": \"\"\n}\n", "cometbft.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"datasource\",\n \"uid\": \"grafana\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"Internet of blockchains\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 64,\n \"panels\": [],\n \"title\": \"$chain_id overview\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 6,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 4,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_height{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block Height\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 6,\n \"x\": 6,\n \"y\": 1\n },\n \"id\": 40,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_total_txs{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Transactions\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [\n {\n \"options\": {\n \"match\": \"null\",\n \"result\": {\n \"text\": \"N/A\"\n }\n },\n \"type\": \"special\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 6,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 65,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(cometbft_state_block_processing_time_sum{chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval]) / rate(cometbft_state_block_processing_time_count{chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval])\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Avg State Block Processing\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 6,\n \"x\": 18,\n \"y\": 1\n },\n \"id\": 47,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_validators_power{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Bonded Tokens\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 12,\n \"x\": 0,\n \"y\": 5\n },\n \"id\": 66,\n \"maxDataPoints\": 100,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_height{chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block Height\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [\n {\n \"options\": {\n \"match\": \"null\",\n \"result\": {\n \"text\": \"N/A\"\n }\n },\n \"type\": \"special\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 2\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 12,\n \"x\": 12,\n \"y\": 5\n },\n \"id\": 39,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.9, sum by(le,namespace,chain_id,job) (rate(cometbft_consensus_block_interval_seconds_bucket{namespace=\\\"$namespace\\\",chain_id=\\\"$chain_id\\\",job=\\\"$instance\\\"}[$__rate_interval])))\",\n \"format\": \"time_series\",\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{namespace}}-{{chain_id}}-0.9\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.99, sum by(le,namespace,chain_id,job) (rate(cometbft_consensus_block_interval_seconds_bucket{namespace=\\\"$namespace\\\",chain_id=\\\"$chain_id\\\",job=\\\"$instance\\\"}[$__rate_interval])))\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{chain_id}}-0.99\",\n \"range\": true,\n \"refId\": \"B\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.999, sum by(le,namespace,chain_id,job) (rate(cometbft_consensus_block_interval_seconds_bucket{namespace=\\\"$namespace\\\",chain_id=\\\"$chain_id\\\",job=\\\"$instance\\\"}[$__rate_interval])))\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{chain_id}}-0.999\",\n \"range\": true,\n \"refId\": \"C\"\n }\n ],\n \"title\": \"Avg Block Time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 30,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\",\n \"max\",\n \"min\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true,\n \"width\": 350\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_validators{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Active\",\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_missing_validators{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Missing\",\n \"range\": true,\n \"refId\": \"B\"\n },\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_byzantine_validators{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Byzantine\",\n \"range\": true,\n \"refId\": \"C\"\n }\n ],\n \"title\": \"Validators\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 30,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 48,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\",\n \"max\",\n \"min\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true,\n \"width\": 350\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_validators_power{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Online\",\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_missing_validators_power{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Missing\",\n \"range\": true,\n \"refId\": \"B\"\n },\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_byzantine_validators_power{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Byzantine\",\n \"range\": true,\n \"refId\": \"C\"\n }\n ],\n \"title\": \"Voting Power\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"continuous-RdYlGr\"\n },\n \"custom\": {\n \"axisPlacement\": \"auto\",\n \"fillOpacity\": 70,\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineWidth\": 0,\n \"spanNulls\": false\n },\n \"decimals\": 2,\n \"mappings\": [],\n \"max\": 1,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#e24d42\",\n \"value\": 0\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 0,\n \"y\": 18\n },\n \"id\": 67,\n \"options\": {\n \"alignValue\": \"left\",\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"mergeValues\": true,\n \"rowHeight\": 0.9,\n \"showValue\": \"auto\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"cometbft_consensus_round_voting_power_percent{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", service=\\\"$instance\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{vote_type}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Consensus round voting power\",\n \"type\": \"state-timeline\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 30,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 12,\n \"x\": 12,\n \"y\": 18\n },\n \"id\": 49,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\",\n \"sum\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_num_txs{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Transactions\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Transactions\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 30,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"links\": [],\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Height for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#447ebc\",\n \"mode\": \"fixed\"\n }\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Total Transactions for last 3 hours\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"#ef843c\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 5,\n \"w\": 12,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 50,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"mean\",\n \"max\"\n ],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_consensus_block_size_bytes{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"Block Size\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Block Size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 2,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 5,\n \"w\": 12,\n \"x\": 12,\n \"y\": 25\n },\n \"id\": 68,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"rate(cometbft_consensus_height{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval])\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Block Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 30\n },\n \"id\": 55,\n \"panels\": [],\n \"repeat\": \"instance\",\n \"title\": \"instance overview: $instance\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"max\": 20,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#e24d42\",\n \"value\": 0\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 2\n },\n {\n \"color\": \"#7eb26d\",\n \"value\": 5\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 6,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 53,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"last\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_p2p_peers{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=~\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Connected Peers\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"max\": 50,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": 0\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 6,\n \"x\": 6,\n \"y\": 31\n },\n \"id\": 56,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"last\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_mempool_size{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=~\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Unconfirmed Transactions\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 6,\n \"x\": 12,\n \"y\": 31\n },\n \"id\": 60,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_mempool_failed_txs{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=~\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Failed Transactions\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"locale\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 4,\n \"w\": 6,\n \"x\": 18,\n \"y\": 31\n },\n \"id\": 61,\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_mempool_recheck_times{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=~\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"30s\",\n \"intervalFactor\": 1,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Recheck Times\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#7eb26d\",\n \"value\": 0\n },\n {\n \"color\": \"#ef843c\",\n \"value\": 10\n },\n {\n \"color\": \"#e24d42\",\n \"value\": 20\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 5,\n \"w\": 12,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 70,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_mempool_size{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=~\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Unconfirmed Transactions (mempool)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"decimals\": 0,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 5,\n \"w\": 12,\n \"x\": 12,\n \"y\": 35\n },\n \"id\": 69,\n \"maxDataPoints\": 100,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"cometbft_mempool_failed_txs{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=~\\\"$instance\\\"}\",\n \"format\": \"time_series\",\n \"instant\": true,\n \"interval\": \"\",\n \"intervalFactor\": 1,\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Failed Transactions (mempool)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"bars\",\n \"fillOpacity\": 100,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"links\": [],\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"binBps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 59,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(cometbft_p2p_peer_receive_bytes_total{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval])\",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{peer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Network Input\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"bars\",\n \"fillOpacity\": 100,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"never\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"links\": [],\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"binBps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 12,\n \"y\": 40\n },\n \"id\": 58,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"uid\": \"$DS\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(cometbft_p2p_peer_send_bytes_total{namespace=\\\"$namespace\\\", chain_id=\\\"$chain_id\\\", job=\\\"$instance\\\"}[$__rate_interval]) \",\n \"format\": \"time_series\",\n \"intervalFactor\": 1,\n \"legendFormat\": \"{{peer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Network Output\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 42,\n \"tags\": [\n \"Blockchain\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"default\",\n \"value\": \"default\"\n },\n \"includeAll\": false,\n \"label\": \"Datasource\",\n \"name\": \"DS\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"definition\": \"label_values(cometbft_consensus_height,namespace)\",\n \"includeAll\": false,\n \"label\": \"namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(cometbft_consensus_height,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"dev-1-6\",\n \"value\": \"dev-1-6\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"definition\": \"label_values(cometbft_consensus_height, chain_id)\",\n \"includeAll\": false,\n \"label\": \"Chain ID\",\n \"name\": \"chain_id\",\n \"options\": [],\n \"query\": \"label_values(cometbft_consensus_height, chain_id)\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"global-domain-1-cometbft-cometbft-rpc\",\n \"value\": \"global-domain-1-cometbft-cometbft-rpc\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"$DS\"\n },\n \"definition\": \"label_values(cometbft_consensus_height{chain_id=\\\"$chain_id\\\"},job)\",\n \"includeAll\": false,\n \"label\": \"Instance\",\n \"name\": \"instance\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(cometbft_consensus_height{chain_id=\\\"$chain_id\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 5,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"CometBFT\",\n \"uid\": \"UJyurCTWy\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", - "global-sync-utilization.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 643,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Successful transactions/s as measured by the mediator\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id)(rate(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id)(rate(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[1h]))\",\n \"hide\": false,\n \"legendFormat\": \"hourly\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Successful transactions/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"total transactions/s including rejected transactions as measured by the mediator\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id) (rate(daml_mediator_requests_total{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id) (rate(daml_mediator_requests_total{namespace=~\\\"$namespace\\\"}[1h]))\",\n \"hide\": false,\n \"legendFormat\": \"hourly\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Total transactions/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 6,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"max\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id) (increase(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[1m]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Approved transactions/min + maximum over the time range\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 12\n },\n \"id\": 7,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"max\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id) (increase(daml_mediator_requests_total{namespace=~\\\"$namespace\\\"}[1m]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total transactions/min + maximum over time range\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The absolute number of transactions on the global synchronizer that are not visible to the DSO, in a sliding window of 30 minutes.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 20\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"(sum(increase(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace) - sum(increase(splice_history_updates_transactions_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace))\",\n \"hide\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Transactions not seen by DSO (absolute)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The percentage of transactions on the global synchronizer that are not visible to the DSO, in a sliding window of 30 minutes.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 20\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(sum(increase(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace) - sum(increase(splice_history_updates_transactions_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace)) / (sum(increase(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace) > sum(increase(splice_history_updates_transactions_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace) or sum(increase(splice_history_updates_transactions_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace))\",\n \"hide\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Transaction not seen by DSO (percentage)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Confirmation requests received by the mediator but rejected either due to timeouts or due to rejected confirmations from some participants\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 28\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"1 - sum by (namespace, migration_id) (rate(daml_mediator_approved_requests_total{namespace=~\\\"sv-1\\\"}[30m])) / sum by (namespace, migration_id)(rate(daml_mediator_requests_total{namespace=~\\\"sv-1\\\"}[30m]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Failed Confirmation Request Rate\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"definition\": \"label_values(daml_mediator_approved_requests_total,namespace)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_mediator_approved_requests_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-90d\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Global Synchronizer Utilization\",\n \"uid\": \"fe8wt04z620aof\",\n \"version\": 3\n}\n", + "global-sync-utilization.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 643,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Successful transactions/s as measured by the mediator\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id)(rate(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id)(rate(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[1h]))\",\n \"hide\": false,\n \"legendFormat\": \"hourly\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Successful transactions/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"total transactions/s including rejected transactions as measured by the mediator\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id) (rate(daml_mediator_requests_total{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id) (rate(daml_mediator_requests_total{namespace=~\\\"$namespace\\\"}[1h]))\",\n \"hide\": false,\n \"legendFormat\": \"hourly\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Total transactions/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 6,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"max\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id) (increase(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[1m]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Approved transactions/min + maximum over the time range\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 12\n },\n \"id\": 7,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"max\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id) (increase(daml_mediator_requests_total{namespace=~\\\"$namespace\\\"}[1m]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total transactions/min + maximum over time range\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The absolute number of transactions on the global synchronizer that are not visible to the DSO, in a sliding window of 30 minutes.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 20\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"(sum(increase(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace) - sum(increase(splice_history_updates_transactions_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace))\",\n \"hide\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Transactions not seen by DSO (absolute)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The percentage of transactions on the global synchronizer that are not visible to the DSO, in a sliding window of 30 minutes.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 20\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(sum(increase(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace) - sum(increase(splice_history_updates_transactions_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace)) / (sum(increase(daml_mediator_approved_requests_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace) > sum(increase(splice_history_updates_transactions_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace) or sum(increase(splice_history_updates_transactions_total{namespace=~\\\"$namespace\\\"}[30m])) by (namespace))\",\n \"hide\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Transaction not seen by DSO (percentage)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Confirmation requests received by the mediator but rejected either due to timeouts or due to rejected confirmations from some participants\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 28\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"1 - sum by (namespace, migration_id) (rate(daml_mediator_approved_requests_total{namespace=~\\\"sv-1\\\"}[30m])) / sum by (namespace, migration_id)(rate(daml_mediator_requests_total{namespace=~\\\"sv-1\\\", duplicate_reject=\\\"false\\\"}[30m]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Failed Confirmation Request Rate (excluding duplicate requests)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Confirmation requests with duplicate confirmation request UUID. These get rejected by the mediator.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 28\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, migration_id)(rate(daml_mediator_requests_total{namespace=~\\\"sv-1\\\", duplicate_reject=\\\"true\\\"}[30m]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Duplicate Confirmation Requests/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Confirmation requests that were sequenced but never processed by the mediator (e.g. dropped due to CometBFT replays): sequenced send-confirmation-request events minus requests received by the mediator.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 36\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace) (rate(daml_sequencer_block_events_total{namespace=~\\\"$namespace\\\",type=\\\"send-confirmation-request\\\"}[$__rate_interval])) - sum by (namespace) (rate(daml_mediator_requests_total{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Discarded confirmation requests/s\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Fraction of sequenced confirmation requests that were never processed by the mediator (e.g. dropped due to CometBFT replays).\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 36\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(sum by (namespace) (rate(daml_sequencer_block_events_total{namespace=~\\\"$namespace\\\",type=\\\"send-confirmation-request\\\"}[10m])) - sum by (namespace) (rate(daml_mediator_requests_total{namespace=~\\\"$namespace\\\"}[10m]))) / sum by (namespace) (rate(daml_sequencer_block_events_total{namespace=~\\\"$namespace\\\",type=\\\"send-confirmation-request\\\"}[10m]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Discarded confirmation requests (fraction)\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"definition\": \"label_values(daml_mediator_approved_requests_total,namespace)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_mediator_approved_requests_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-90d\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Global Synchronizer Utilization\",\n \"uid\": \"fe8wt04z620aof\",\n \"version\": 3\n}\n", "mining-rounds.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 9003,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace) (splice_sv_dso_store_latest_open_mining_round)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{label_name}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Last Open Mining Round\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace) (splice_sv_dso_store_latest_issuing_mining_round)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{label_name}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Last Issuing Mining Round\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": []\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Mining Rounds\",\n \"uid\": \"ed94a332-4fa7-47f8-982b-fc997381175b\",\n \"version\": 1\n}\n", "onboarded_parties.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 228,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 4,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"splice_synchronizer_topology_num_parties{namespace=\\\"$namespace\\\"}\",\n \"instant\": true,\n \"legendFormat\": \"total parties\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Parties onboarded to the global synchronizer\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 10,\n \"x\": 4,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_synchronizer_topology_num_parties{namespace=\\\"$namespace\\\"}\",\n \"legendFormat\": \"total parties\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Parties onboarded to the global synchronizer\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 10,\n \"x\": 14,\n \"y\": 0\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_synchronizer_topology_num_parties_per_participant{namespace=\\\"$namespace\\\"}\",\n \"legendFormat\": \"{{participant_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Parties onboarded to the global synchronizer per participant\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"filterable\": true,\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Parties\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 9,\n \"x\": 0,\n \"y\": 13\n },\n \"id\": 4,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"Parties\"\n }\n ]\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"splice_synchronizer_topology_num_parties_per_participant{namespace=\\\"$namespace\\\"}\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Top hosting participants\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true,\n \"__name__\": true,\n \"endpoint\": true,\n \"instance\": true,\n \"job\": true,\n \"migration\": true,\n \"namespace\": true,\n \"node_name\": true,\n \"node_type\": true,\n \"otel_scope_name\": true,\n \"pod\": true,\n \"service\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Value\": \"Parties\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"filterable\": true,\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Parties\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 7,\n \"x\": 9,\n \"y\": 13\n },\n \"id\": 5,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"Parties\"\n }\n ]\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(participant_id) (delta(splice_synchronizer_topology_num_parties_per_participant{namespace=\\\"$namespace\\\"}[24h])) > 0\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Onboarded parties last 24h\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true,\n \"__name__\": true,\n \"endpoint\": true,\n \"instance\": true,\n \"job\": true,\n \"migration\": true,\n \"namespace\": true,\n \"node_name\": true,\n \"node_type\": true,\n \"otel_scope_name\": true,\n \"pod\": true,\n \"service\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Value\": \"Parties\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"filterable\": true,\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Parties\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 8,\n \"x\": 16,\n \"y\": 13\n },\n \"id\": 6,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"Parties\"\n }\n ]\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(participant_id) (delta(splice_synchronizer_topology_num_parties_per_participant{namespace=\\\"$namespace\\\"}[$__range])) > 0\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Onboarded parties in selected time range\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true,\n \"__name__\": true,\n \"endpoint\": true,\n \"instance\": true,\n \"job\": true,\n \"migration\": true,\n \"namespace\": true,\n \"node_name\": true,\n \"node_type\": true,\n \"otel_scope_name\": true,\n \"pod\": true,\n \"service\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Value\": \"Parties\"\n }\n }\n }\n ],\n \"type\": \"table\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"definition\": \"label_values(splice_synchronizer_topology_num_parties,namespace)\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_synchronizer_topology_num_parties,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-7d\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Onboarded Parties\",\n \"uid\": \"fc6185ce-0c37-48e7-960b-839a037d47bf\",\n \"version\": 1\n}\n", "sequencer-pruning.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 2419,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": true,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(rate(splice_sequencer_pruning_latency_duration_seconds{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Sequencer Pruning Latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"splice_sequencer_pruning_disabled_members{namespace=~\\\"$namespace\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Disabled Members\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 40,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_sequencer_pruning_disabled_members,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_sequencer_pruning_disabled_members,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-2d\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Sequencer Pruning\",\n \"uid\": \"bc8fea13-0bf5-488b-ac31-76e4ec7f4c1f\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", @@ -153,7 +157,8 @@ "sv-status-reports.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"Show the DSO health based on the SV status reports.\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"id\": 3407,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"Report Overview\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Difference between the current time, and the creation time of the last status report (as ingested by any of the selected namespaces)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"neutral\": 0\n },\n \"mappings\": [],\n \"max\": 260,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"yellow\",\n \"value\": 180\n },\n {\n \"color\": \"red\",\n \"value\": 260\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 19,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\",\n \"text\": {}\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"time() - max by(report_publisher) (splice_sv_status_report_creation_time_us) / 1000000\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{report_publisher}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Status Report Creation Time Lag (current)\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Difference between the current time and the maximum report creation time, as ingested by any of the selected namespaces\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"linearThreshold\": 260,\n \"log\": 2,\n \"type\": \"symlog\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"dashed\"\n }\n },\n \"fieldMinMax\": false,\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"orange\",\n \"value\": 180\n },\n {\n \"color\": \"red\",\n \"value\": 260\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"time() - max by (report_publisher) (splice_sv_status_report_creation_time_us{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"}) / 1000000\",\n \"instant\": false,\n \"legendFormat\": \"{{report_publisher}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Report Creation Time Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"neutral\": 0\n },\n \"mappings\": [],\n \"max\": 1.2,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"red\"\n },\n {\n \"color\": \"green\",\n \"value\": 0.4\n },\n {\n \"color\": \"purple\",\n \"value\": 3\n }\n ]\n },\n \"unit\": \"recpm\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 18\n },\n \"id\": 23,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\",\n \"text\": {}\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"rate(max by(report_publisher) (splice_sv_status_report_number{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"})[5m:30s]) * 60\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{report_publisher}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Reporting Frequency (current)\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"First, takes the maximum report number for each SV node (as seen by ANY of the selected namespaces).\\n\\nThen, takes the 5m rate of change in that metric.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"axisSoftMax\": 4,\n \"axisSoftMin\": 0,\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"area\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"red\"\n },\n {\n \"color\": \"green\",\n \"value\": 1.5\n },\n {\n \"color\": \"purple\",\n \"value\": 3\n }\n ]\n },\n \"unit\": \"recpm\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 24,\n \"x\": 0,\n \"y\": 26\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"rate(max by(report_publisher) (splice_sv_status_report_number{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"})[5m:30s]) * 60\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{report_publisher}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Reporting Frequency\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 37\n },\n \"id\": 22,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 24,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, report_publisher) (splice_sv_status_report_creation_time_us{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"}) / 1000\",\n \"instant\": false,\n \"legendFormat\": \"{{report_publisher}} @ {{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Report Creation Time\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Additional Info: Report time\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 12,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 81\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, report_publisher) (splice_sv_status_report_cometbft_height{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"})\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{report_publisher}} @ {{namespace}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"CometBFT Height\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Maximal minus minimal CometBFT height reported by the SV status reports seen from one SV's perspective\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"axisSoftMax\": 600,\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"area\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"orange\",\n \"value\": 500\n },\n {\n \"color\": \"red\",\n \"value\": 1000\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 89\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace) (max by (report_publisher, namespace) (splice_sv_status_report_cometbft_height{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"}) ) - min by (namespace) (max by (report_publisher, namespace) (splice_sv_status_report_cometbft_height{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"}))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"CometBFT Height Lag\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Additional Info: CometBFT\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 21,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_sv_status_report_domain_time_us{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"} / 1000\",\n \"instant\": false,\n \"legendFormat\": \"{{target_node}} - {{ report_publisher }} {{ report_publisher_party }}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Domain Time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"How many seconds of progress on the reported participant domain time is observed every second.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"axisSoftMax\": 1.6,\n \"axisSoftMin\": 0.4,\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"area\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"dark-red\"\n },\n {\n \"color\": \"orange\",\n \"value\": 0.5\n },\n {\n \"color\": \"green\",\n \"value\": 0.8\n },\n {\n \"color\": \"purple\",\n \"value\": 1.5\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 65\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"asc\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"max by (namespace, report_publisher, target_node) (rate(splice_sv_status_report_domain_time_us{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"}[10m])) / 1000000\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{target_node}} - {{report_publisher}} @ {{namespace}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Domain Time Progress\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Additional Info: Domain Time\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 20,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 41\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, report_publisher) (splice_sv_status_report_latest_open_round{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"})\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{report_publisher}} @ {{namespace}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Latest Open Mining Round\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Should not go above 1 when all SVs are healthy, serves as a basic safeguard to see that the round structure makes progress from all SVs' point of view.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"area\"\n }\n },\n \"mappings\": [],\n \"max\": 5,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"orange\",\n \"value\": 4\n },\n {\n \"color\": \"red\",\n \"value\": 10\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 50\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"11.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace) (max by (report_publisher, namespace) (splice_sv_status_report_latest_open_round{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"}) ) - min by (namespace) (max by (report_publisher, namespace) (splice_sv_status_report_latest_open_round{namespace=~\\\"$namespace\\\", report_publisher=~\\\"$sv_party\\\", canton_version=~\\\"$version\\\"}))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latest Open Mining Round Lag\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Additional Info: Mining rounds\",\n \"type\": \"row\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_sv_status_report_creation_time_us,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_sv_status_report_creation_time_us,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_sv_status_report_creation_time_us,report_publisher)\",\n \"description\": \"The SV whose node published the status report.\",\n \"includeAll\": true,\n \"label\": \"report publisher\",\n \"multi\": true,\n \"name\": \"sv_party\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_sv_status_report_creation_time_us,report_publisher)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_sv_status_report_creation_time_us,canton_version)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"version\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_sv_status_report_creation_time_us,canton_version)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-30m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"SV Status Reports\",\n \"uid\": \"caffa6f7-c421-4579-a839-b026d3b76826\",\n \"version\": 1\n}\n", "synchronizer-fees-sv.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 2028,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(type) (rate(daml_sequencer_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", member=~\\\"$member\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Delivered Event Traffic Rate By Event Type\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": [\n {\n \"__systemRef\": \"hideSeriesFrom\",\n \"matcher\": {\n \"id\": \"byNames\",\n \"options\": {\n \"mode\": \"exclude\",\n \"names\": [\n \"send-confirmation-response\",\n \"send-time-proof\",\n \"send-topology\",\n \"send-verdict\",\n \"send-commitment\",\n \"send-confirmation-request\"\n ],\n \"prefix\": \"All except:\",\n \"readOnly\": true\n }\n },\n \"properties\": [\n {\n \"id\": \"custom.hideFrom\",\n \"value\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": true\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 8\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(type) (rate(daml_sequencer_traffic_control_event_rejected_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", member=~\\\"$member\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Rejected Event Traffic Rate By Event Type\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 16\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(member) (daml_sequencer_traffic_control_base_traffic_remainder{namespace=\\\"$namespace\\\", job=\\\"$job\\\", member=~\\\"$member\\\", member=~\\\"(PAR|MED)::.*\\\"})\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Free Tier Traffic Available By Participant/Mediator\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"extra_traffic_remainder\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"${__field.labels.member}\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 26\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(member) (daml_sequencer_traffic_control_extra_traffic_purchased{namespace=\\\"$namespace\\\", job=\\\"$job\\\", member=~\\\"$member\\\", member=~\\\"PAR::.*\\\"})\",\n \"fullMetaSearch\": false,\n \"hide\": true,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"extra_traffic_purchased\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(member) (daml_sequencer_traffic_control_extra_traffic_consumed{namespace=\\\"$namespace\\\", job=\\\"$job\\\", member=~\\\"$member\\\", member=~\\\"PAR::.*\\\"})\",\n \"fullMetaSearch\": false,\n \"hide\": true,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"extra_traffic_consumed\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"name\": \"Expression\",\n \"type\": \"__expr__\",\n \"uid\": \"__expr__\"\n },\n \"expression\": \"$extra_traffic_purchased - $extra_traffic_consumed\",\n \"hide\": false,\n \"refId\": \"extra_traffic_remainder\",\n \"type\": \"math\"\n }\n ],\n \"title\": \"Extra Traffic Available By Participant\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (member) (delta(daml_sequencer_traffic_control_extra_traffic_purchased{namespace=\\\"$namespace\\\",job=\\\"$job\\\",member=~\\\"$member.*\\\"}[24h]))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Traffic Purchased over the last 24h\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of wasted traffic i.e. sequenced events that cost traffic but are not delivered in bytes/s summed across all participants matching the filter\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 45\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job, reason) (rate(daml_sequencer_traffic_control_wasted_traffic_total{namespace=~\\\"$namespace\\\", member=~\\\"$member\\\",member=~\\\"PAR::.*\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Rate of Wasted Traffic\",\n \"type\": \"timeseries\"\n }\n ],\n \"refresh\": \"auto\",\n \"schemaVersion\": 39,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_traffic_control_event_delivered_cost_total,namespace)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Namespace\",\n \"multi\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_traffic_control_event_delivered_cost_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"global-domain-0-sequencer\",\n \"value\": \"global-domain-0-sequencer\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\"},job)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"label\": \"Job\",\n \"multi\": false,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".+\",\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"},member)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"label\": \"Member\",\n \"multi\": true,\n \"name\": \"member\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"},member)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 7,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Synchronizer Fees (SV View)\",\n \"uid\": \"fdnphvrryfq4gf\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", "synchronizer-fees-validator.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 19,\n \"panels\": [],\n \"title\": \"Traffic Consumption By Event Type\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Tracks the traffic cost of events at the time of submission before they've been sequenced.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(type) (rate(daml_sequencer_client_traffic_control_submitted_event_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Submitted Event Traffic Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Tracks the actual cost of traffic after events have been sequenced and successfully delivered.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(type) (rate(daml_sequencer_client_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Delivered Event Traffic Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of requests that are in-flight, overloaded or dropped per second in the sequencer client.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(type) (rate(daml_sequencer_client_submissions_in_flight{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"in-flight {{type}}\",\n \"range\": true,\n \"refId\": \"in_flight\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(type) (rate(daml_sequencer_client_submissions_overloaded{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"legendFormat\": \"overloaded {{type}}\",\n \"range\": true,\n \"refId\": \"overloaded\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(type) (rate(daml_sequencer_client_submissions_dropped{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"legendFormat\": \"dropped {{type}}\",\n \"range\": true,\n \"refId\": \"dropped\"\n }\n ],\n \"title\": \"Event Submission Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Difference in traffic cost for events between submission and successful delivery after sequencing. Small differences can be present due to the time lag between the recorded metrics particularly if there are many requests in-flight to be sequenced.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"avg_over_time((\\n sum by(type) (daml_sequencer_client_traffic_control_submitted_event_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"})\\n -\\n sum by(type) (daml_sequencer_client_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"})\\n)[2m:10s])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Submitted vs Delivered Traffic\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Shows the traffic cost per second of requests that were sequenced but not delivered successfully by the sequencer. The reason for rejection should be visible in the next graph.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(type) (rate(daml_sequencer_client_traffic_control_event_rejected_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Rejected Event Traffic Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Displays the cost per second of events that were sequenced but not delivered successfully aggregated by the reason for the rejection.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 17\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(reason) (rate(daml_sequencer_client_traffic_control_event_rejected_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Rejected Event Traffic Rate By Reason\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 20,\n \"panels\": [],\n \"title\": \"Traffic Consumption Due to Daml Transactions\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Shows the traffic consumed due to confirmation requests submitted by this participant (per application ID) along with the consumption due to confirmation responses..\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 26\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(application_id) (rate(daml_sequencer_client_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\", type=\\\"send-confirmation-request\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"confirmation-request-rate {{application_id}}\",\n \"range\": true,\n \"refId\": \"confirmation_request_rate\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_client_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\", type=\\\"send-confirmation-response\\\"}[$__rate_interval]))\",\n \"instant\": false,\n \"legendFormat\": \"confirmation-response-rate\",\n \"range\": true,\n \"refId\": \"confirmation_resposne_rate\"\n }\n ],\n \"title\": \"Confirmation Request Rate By Application ID\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Difference in traffic cost for confirmation requests and responses between submission and successful delivery after sequencing (per application).\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 26\n },\n \"id\": 21,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"avg_over_time((\\n sum by(application_id) (daml_sequencer_client_traffic_control_submitted_event_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\", type=\\\"send-confirmation-request\\\"})\\n -\\n sum by(application_id) (daml_sequencer_client_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\", type=\\\"send-confirmation-request\\\"})\\n)[2m:10s])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"confirmation-request {{application_id}}\",\n \"range\": true,\n \"refId\": \"confirmation_requests\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"avg_over_time((\\n sum without(target_sequencer) (daml_sequencer_client_traffic_control_submitted_event_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\", type=\\\"send-confirmation-response\\\"})\\n -\\n sum without(member) (daml_sequencer_client_traffic_control_event_delivered_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\", type=\\\"send-confirmation-response\\\"})\\n)[2m:10s])\",\n \"instant\": false,\n \"legendFormat\": \"confirmation-response\",\n \"range\": true,\n \"refId\": \"confirmation_responses\"\n }\n ],\n \"title\": \"Submitted vs Delivered Traffic By Application ID\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 34\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"Extra Traffic\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"extra_traffic_remainder\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"${__field.labels.member}\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 0,\n \"y\": 35\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(member) (daml_sequencer_client_traffic_control_extra_traffic_purchased{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"})\",\n \"fullMetaSearch\": false,\n \"hide\": true,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"extra_traffic_purchased\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(member) (daml_sequencer_client_traffic_control_extra_traffic_consumed{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"})\",\n \"fullMetaSearch\": false,\n \"hide\": true,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"extra_traffic_consumed\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"name\": \"Expression\",\n \"type\": \"__expr__\",\n \"uid\": \"__expr__\"\n },\n \"expression\": \"$extra_traffic_purchased - $extra_traffic_consumed\",\n \"refId\": \"extra_traffic_remainder\",\n \"type\": \"math\"\n }\n ],\n \"title\": \"Extra Traffic Available\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"extra_traffic_remainder\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"${__field.labels.member}\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 12,\n \"y\": 35\n },\n \"id\": 17,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by(member) (daml_sequencer_client_traffic_control_extra_traffic_consumed{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"})\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"extra_traffic_consumed\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Extra Traffic Consumed\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 0,\n \"y\": 44\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (member) (delta(daml_sequencer_client_traffic_control_extra_traffic_purchased{namespace=\\\"$namespace\\\",job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[24h]))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Extra Traffic Purchased over the last 24h\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 12,\n \"y\": 44\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (member) (delta(daml_sequencer_client_traffic_control_extra_traffic_consumed{namespace=\\\"$namespace\\\",job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"}[24h]))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Extra Traffic Consumed over the last 24h\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 53\n },\n \"id\": 15,\n \"panels\": [],\n \"title\": \"Free Traffic\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 54\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(member) (daml_sequencer_client_traffic_control_base_traffic_remainder{namespace=\\\"$namespace\\\", job=\\\"$job\\\", synchronizer=\\\"$synchronizer\\\"})\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Free Tier Traffic Available\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"sv-12\",\n \"value\": \"sv-12\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_traffic_control_submitted_event_cost_total,namespace)\",\n \"includeAll\": false,\n \"label\": \"Namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_client_traffic_control_submitted_event_cost_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"participant\",\n \"value\": \"participant\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_traffic_control_submitted_event_cost_total{namespace=\\\"$namespace\\\"},job)\",\n \"includeAll\": false,\n \"label\": \"Job\",\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_client_traffic_control_submitted_event_cost_total{namespace=\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"global-domain::12209471e1a5...::35-14\",\n \"value\": \"global-domain::12209471e1a5...::35-14\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_traffic_control_submitted_event_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"},synchronizer)\",\n \"includeAll\": false,\n \"label\": \"Synchronizer\",\n \"name\": \"synchronizer\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_client_traffic_control_submitted_event_cost_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"},synchronizer)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Synchronizer Fees (Validator View)\",\n \"uid\": \"fdw1tzuj3277kb\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", - "validator-scan-connections.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"This dashboard provides validators graphs related to their scan connections.\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 15,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (request, namespace, node_name, scan_connection) (rate(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"${namespace}\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", request=~\\\"${per_connection_request}\\\", scan_connection=~\\\"${scan_connection}\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency per connection\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 15,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, node_name, scan_connection, request)(rate(splice_validator_scan_per_connection_calls_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"$per_connection_request\\\", outcome!=\\\"ok\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors per connection\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 0,\n \"y\": 15\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (request, namespace, node_name)(rate(splice_validator_scan_bft_read_latency_duration_seconds{namespace=~\\\"${namespace}\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", request=~\\\"${bft_request}\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT read latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 12,\n \"y\": 15\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (outcome, namespace, node_name, request) (\\n rate(splice_validator_scan_bft_calls_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", request=~\\\"${bft_request}\\\", outcome!=\\\"ok\\\"}[$__rate_interval])\\n)\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{outcome}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT errors\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 29\n },\n \"id\": 100,\n \"panels\": [],\n \"title\": \"BFT Consensus\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 0,\n \"y\": 30\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, node_name, scan_connection, consensus, request, http_status) (rate(splice_validator_scan_bft_per_connection_consensus_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", consensus=\\\"disagree\\\", request=~\\\"${bft_request}\\\"}[$__rate_interval])) > 0\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}} - {{http_status}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT consensus disagreements per connection\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total number of times each scan connection disagreed with the BFT consensus result over the selected time range.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Disagreements\"\n },\n \"properties\": [\n {\n \"id\": \"custom.cellOptions\",\n \"value\": {\n \"type\": \"color-background\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 12,\n \"y\": 30\n },\n \"id\": 6,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"Disagreements\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace,job, node_name, scan_connection, http_status) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\\\"disagree\\\", namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${bft_request}\\\"}[$__range]))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Disagreements per connection (time range)\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"indexByName\": {},\n \"renameByName\": {\n \"Value\": \"Disagreements\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Fraction of BFT consensus comparisons per request on each scan connection that disagreed with the consensus result over the last 30m. Mirrors the warning alert, which fires above 10%.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"line\"\n }\n },\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 0.1\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 0,\n \"y\": 44\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\",\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"(sum by (namespace, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\\\"disagree\\\", namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${bft_request}\\\"}[30m]))\\n/\\nsum by (namespace, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${bft_request}\\\"}[30m]))) > 0\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Disagreement rate per connection & request \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of successful (2xx) responses per request on each scan connection that disagreed with the BFT consensus result over the last 30m. Mirrors the critical alert, which fires above 5.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"line\"\n }\n },\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 5\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 12,\n \"y\": 44\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\",\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\\\"disagree\\\", success=\\\"true\\\", namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${bft_request}\\\"}[30m])) > 0\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Successful responses disagreeing with BFT consensus \",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"15m\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"validator1\"\n ],\n \"value\": [\n \"validator1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{job=~\\\"$job\\\", namespace=~\\\"$namespace\\\"},node_name)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"node_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{job=~\\\"$job\\\", namespace=~\\\"$namespace\\\"},node_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\"},scan_connection)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"scan_connection\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\"},scan_connection)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n \"name\": \"percentile\",\n \"options\": [],\n \"query\": \"0.99,0.95,0.9\",\n \"type\": \"custom\",\n \"valuesFormat\": \"csv\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\"},request)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"per_connection_request\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\"},request)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_bft_read_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\"},request)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"bft_request\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_bft_read_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\"},request)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Validator Scan Connections\",\n \"uid\": \"cnndx7p\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", + "treasury-service.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time items spent in the treasury service queue before being processed.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 27,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($quantile, sum by (namespace, job, owner) (rate(splice_wallet_treasury_queue_latency_duration_seconds[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}}-{{job}}-{{owner}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Treasury service queue latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Size of the queue used by the treasury service automation\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 25,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, job, owner) (splice_wallet_treasury_queue_size{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",owner=~\\\"$owner\\\"})\",\n \"legendFormat\": \"{{namespace}}-{{job}}-{{owner}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Treasury Service Queue Size\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_wallet_treasury_queue_size,namespace)\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_wallet_treasury_queue_size,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_wallet_treasury_queue_size,job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_wallet_treasury_queue_size,job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_wallet_treasury_queue_size,owner)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"owner\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_wallet_treasury_queue_size,owner)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"0.5\",\n \"value\": \"0.5\"\n },\n \"name\": \"quantile\",\n \"options\": [],\n \"query\": \"0.5,0.9,0.99,0.999\",\n \"type\": \"custom\",\n \"valuesFormat\": \"csv\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Treasury Service\",\n \"uid\": \"cn255kr\",\n \"version\": 12,\n \"weekStart\": \"\"\n}\n", + "validator-scan-connections.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"This dashboard provides validators graphs related to their scan connections.\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 15,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (request, namespace, node_name, scan_connection) (rate(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"${namespace}\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", request=~\\\"${per_connection_request}\\\", scan_connection=~\\\"${scan_connection}\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency per connection\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 15,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, node_name, scan_connection, request)(rate(splice_validator_scan_per_connection_calls_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"$per_connection_request\\\", outcome!=\\\"ok\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors per connection\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 0,\n \"y\": 15\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, sum by (request, namespace, node_name)(rate(splice_validator_scan_bft_read_latency_duration_seconds{namespace=~\\\"${namespace}\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", request=~\\\"${bft_request}\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT read latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 12,\n \"y\": 15\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (outcome, namespace, node_name, request) (\\n rate(splice_validator_scan_bft_calls_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", request=~\\\"${bft_request}\\\", outcome!=\\\"ok\\\"}[$__rate_interval])\\n)\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{outcome}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT errors\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 29\n },\n \"id\": 100,\n \"panels\": [],\n \"title\": \"BFT Consensus\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 0,\n \"y\": 30\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, node_name, scan_connection, consensus, request, http_status) (rate(splice_validator_scan_bft_per_connection_consensus_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", consensus=\\\"disagree\\\", request=~\\\"${bft_request}\\\"}[$__rate_interval])) > 0\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}} - {{http_status}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"BFT consensus disagreements per connection\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total number of times each scan connection disagreed with the BFT consensus result over the selected time range.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Disagreements\"\n },\n \"properties\": [\n {\n \"id\": \"custom.cellOptions\",\n \"value\": {\n \"type\": \"color-background\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 12,\n \"y\": 30\n },\n \"id\": 6,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"Disagreements\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace,job, node_name, scan_connection, http_status) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\\\"disagree\\\", namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${bft_request}\\\"}[$__range]))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Disagreements per connection (time range)\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"indexByName\": {},\n \"renameByName\": {\n \"Value\": \"Disagreements\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Fraction of BFT consensus comparisons per request on each scan connection that disagreed with the consensus result over the last 30m. Mirrors the warning alert, which fires above 10%.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"line\"\n }\n },\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 0.1\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 0,\n \"y\": 44\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\",\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"(sum by (namespace, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\\\"disagree\\\", namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${bft_request}\\\"}[30m]))\\n/\\nsum by (namespace, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${bft_request}\\\"}[30m]))) > 0\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Disagreement rate per connection & request \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of successful (2xx) responses per request on each scan connection that disagreed with the BFT consensus result over the last 30m. Mirrors the critical alert, which fires above 5.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 10,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"line\"\n }\n },\n \"mappings\": [],\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 5\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 12,\n \"y\": 44\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\",\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus=\\\"disagree\\\", success=\\\"true\\\", namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${bft_request}\\\"}[30m])) > 0\",\n \"legendFormat\": \"{{namespace}} - {{node_name}} - {{scan_connection}} - {{request}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Successful responses disagreeing with BFT consensus \",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 58\n },\n \"id\": 101,\n \"panels\": [],\n \"title\": \"Request outcomes per scan connection\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Total number of requests to this scan connection over the selected time range, split by outcome (ok, or the failure category with its HTTP status code when available).\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n }\n },\n \"mappings\": []\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"success\"\n },\n \"properties\": [\n {\n \"id\": \"color\",\n \"value\": {\n \"fixedColor\": \"green\",\n \"mode\": \"fixed\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 8,\n \"x\": 0,\n \"y\": 59\n },\n \"id\": 9,\n \"options\": {\n \"displayLabels\": [\n \"percent\",\n \"value\"\n ],\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true,\n \"values\": [\n \"value\"\n ]\n },\n \"pieType\": \"pie\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"repeat\": \"scan_connection\",\n \"repeatDirection\": \"h\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum by (outcome, http_status) (increase(splice_validator_scan_per_connection_calls_total{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\", request=~\\\"${per_connection_request}\\\"}[$__range]))\",\n \"instant\": true,\n \"legendFormat\": \"{{outcome}} - {{http_status}}\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"$scan_connection\",\n \"transformations\": [\n {\n \"id\": \"renameByRegex\",\n \"options\": {\n \"regex\": \"^ok.*$\",\n \"renamePattern\": \"success\"\n }\n },\n {\n \"id\": \"renameByRegex\",\n \"options\": {\n \"regex\": \"^(.*) - (none)?$\",\n \"renamePattern\": \"$1\"\n }\n }\n ],\n \"type\": \"piechart\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"15m\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"validator1\"\n ],\n \"value\": [\n \"validator1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{job=~\\\"$job\\\", namespace=~\\\"$namespace\\\"},node_name)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"node_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{job=~\\\"$job\\\", namespace=~\\\"$namespace\\\"},node_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\"},scan_connection)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"scan_connection\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\"},scan_connection)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n \"name\": \"percentile\",\n \"options\": [],\n \"query\": \"0.99,0.95,0.9\",\n \"type\": \"custom\",\n \"valuesFormat\": \"csv\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\"},request)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"per_connection_request\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_per_connection_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\", scan_connection=~\\\"$scan_connection\\\"},request)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_scan_bft_read_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\"},request)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"bft_request\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_scan_bft_read_latency_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", node_name=~\\\"$node_name\\\"},request)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Validator Scan Connections\",\n \"uid\": \"cnndx7p\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", "validator_licenses.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 3,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"count by (version)(max by(version, contact_point, validator_party) (splice_validator_last_active_at_us{version=~\\\"$validator_version\\\",validator_party=~\\\"$validator_party\\\",contact_point=~\\\"$contact_point\\\"}) / 1000 > $__from)\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"count(max by(version, contact_point, validator_party) (splice_validator_last_active_at_us{version=~\\\"$validator_version\\\",validator_party=~\\\"$validator_party\\\",contact_point=~\\\"$contact_point\\\"}) / 1000 > $__from)\",\n \"legendFormat\": \"total\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Validators by version\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Trend #A\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"Last Active At\"\n },\n {\n \"id\": \"unit\",\n \"value\": \"dateTimeAsIso\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 19,\n \"w\": 24,\n \"x\": 0,\n \"y\": 6\n },\n \"id\": 2,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"version\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by(version, contact_point, validator_party) (splice_validator_last_active_at_us{version=~\\\"$validator_version\\\",validator_party=~\\\"$validator_party\\\",contact_point=~\\\"$contact_point\\\"}) / 1000 > $__from\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Validator Licenses\",\n \"transformations\": [\n {\n \"id\": \"timeSeriesTable\",\n \"options\": {\n \"A\": {\n \"stat\": \"lastNotNull\",\n \"timeField\": \"Time\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 18,\n \"w\": 24,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by(version, contact_point, validator_party) (splice_validator_last_active_at_us{version=~\\\"$validator_version\\\",validator_party=~\\\"$validator_party\\\",contact_point=~\\\"$contact_point\\\"}) / 1000 > $__from\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Validator Licenses\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_last_active_at_us,version)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"validator_version\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_last_active_at_us,version)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 2,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_last_active_at_us,validator_party)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"validator_party\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_last_active_at_us,validator_party)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_validator_last_active_at_us,contact_point)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"contact_point\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_validator_last_active_at_us,contact_point)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Validator Licenses\",\n \"uid\": \"cdpcj4gxackcga\",\n \"version\": 3,\n \"weekStart\": \"\"\n}\n" }, "kind": "ConfigMap", @@ -178,9 +183,9 @@ "inputs": { "apiVersion": "v1", "data": { - "acknowledgements.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 3410,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"Client Delay\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 9,\n \"panels\": [],\n \"title\": \"Canton\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The most recent acknowledgement the sequencer has seen from that member. Acknowledgements are only sent when the member also receives an event addressed to them so a node that never receives anything can lag behind. Sequencers only receive topology transactions which are rare so we filter them out.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 2\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"asc\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_block_acknowledgments_micros{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\",member!~\\\"SEQ::.*\\\",member=~\\\"$member\\\"} / 1000\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} M$migration {{member}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Most recent acknowledgement\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The difference most recent acknowledgement the sequencer has seen from that member and the maximum across all members. Acknowledgements are only sent when the member also receives an event addressed to them so a node that never receives anything can lag behind. Sequencers only receive topology transactions which are rare so we filter them out.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 11,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"(max without (member) (daml_sequencer_block_acknowledgments_micros{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\"}) - ignoring (member) group_right daml_sequencer_block_acknowledgments_micros{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\",member!~\\\"SEQ::.*\\\",member=~\\\"$member\\\"}) / 1000000\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} M$migration {{member}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Acknowledgement lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The increase in acknowledgement time in acknowledgements over the last 10min for a given member. This should usually be around 1. If a node was down, they should show > 1. If the value is < 1 the node might never catch up. Acknowledgements are only sent when the member also receives an event addressed to them so a node that never receives anything can lag behind. Sequencers only receive topology transactions which are rare so we filter them out.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 13,\n \"x\": 11,\n \"y\": 12\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"delta(daml_sequencer_block_acknowledgments_micros{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\",member!~\\\"SEQ::.*\\\",member=~\\\"$member\\\"}[10m]) / (1000000 * 60 * 10)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} M$migration {{member}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Acknowledgement catchup speed\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 22\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"CometBFT\",\n \"type\": \"row\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"Prometheus\",\n \"value\": \"prometheus\"\n },\n \"includeAll\": false,\n \"name\": \"DS\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_handler_delay,namespace)\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_sequencer_client_handler_delay,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_handler_delay{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"migration\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_client_handler_delay{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"/global-domain-(?\\\\d)-sequencer/g\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_block_acknowledgments_micros,member)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"member\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_block_acknowledgments_micros,member)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Acknowledgements\",\n \"uid\": \"3ccfda97-fb9c-4413-bc05-5ed6f2c888f7\",\n \"version\": 3\n}\n", + "acknowledgements.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 3410,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"Client Delay\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 9,\n \"panels\": [],\n \"title\": \"Canton\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The most recent acknowledgement the sequencer has seen from that member. Acknowledgements are only sent when the member also receives an event addressed to them so a node that never receives anything can lag behind. Sequencers only receive topology transactions which are rare so we filter them out.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 2\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"asc\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_block_acknowledgments_micros{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\",member!~\\\"SEQ::.*\\\",member=~\\\"$member\\\"} / 1000\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} M$migration {{member}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Most recent acknowledgement\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The difference most recent acknowledgement the sequencer has seen from that member and the maximum across all members. Acknowledgements are only sent when the member also receives an event addressed to them so a node that never receives anything can lag behind. Sequencers only receive topology transactions which are rare so we filter them out.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 11,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"(max without (member) (daml_sequencer_block_acknowledgments_micros{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\"}) - ignoring (member) group_right daml_sequencer_block_acknowledgments_micros{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\",member!~\\\"SEQ::.*\\\",member=~\\\"$member\\\"}) / 1000000\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} M$migration {{member}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Acknowledgement lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The increase in acknowledgement time in acknowledgements over the last 10min for a given member. This should usually be around 1. If a node was down, they should show > 1. If the value is < 1 the node might never catch up. Acknowledgements are only sent when the member also receives an event addressed to them so a node that never receives anything can lag behind. Sequencers only receive topology transactions which are rare so we filter them out.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 13,\n \"x\": 11,\n \"y\": 12\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"delta(daml_sequencer_block_acknowledgments_micros{namespace=~\\\"$namespace\\\", job=~\\\"global-domain-$migration-sequencer\\\",member!~\\\"SEQ::.*\\\",member=~\\\"$member\\\"}[10m]) / (1000000 * 60 * 10)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} M$migration {{member}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Acknowledgement catchup speed\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 22\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"CometBFT\",\n \"type\": \"row\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"Prometheus\",\n \"value\": \"prometheus\"\n },\n \"includeAll\": false,\n \"name\": \"DS\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_handler_delay,namespace)\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_sequencer_client_handler_delay,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_client_handler_delay{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"migration\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_client_handler_delay{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"/global-domain-(?\\\\d+)-sequencer/g\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_block_acknowledgments_micros,member)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"member\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_block_acknowledgments_micros,member)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Acknowledgements\",\n \"uid\": \"3ccfda97-fb9c-4413-bc05-5ed6f2c888f7\",\n \"version\": 3\n}\n", "acs-commitment-performance.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 6,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 12,\n \"options\": {\n \"colorMode\": \"value\",\n \"graphMode\": \"area\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"auto\",\n \"percentChangeColorMode\": \"standard\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showPercentChange\": false,\n \"textMode\": \"auto\",\n \"wideLayout\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"builder\",\n \"expr\": \"max by(namespace, job) (daml_participant_sync_commitments_active_stakeholder_groups{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"})\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"active stakeholder groups\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Measures the time that the participant node spends computing commitments each reconciliation interval. This measures the full time needed to compute the commitments for all counter parties.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": true,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 18,\n \"x\": 6,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.99, sum(rate(daml_participant_sync_commitments_compute_duration_seconds{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"}[$__rate_interval])) by (namespace, job, le))\",\n \"legendFormat\": \"{{namespace}} {{job}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"ACS Computation Time (0.99 quantile)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Measures the time between the end of a commitment period, and the time when the sequencer observes the corresponding commitment.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"µs\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 24,\n \"x\": 0,\n \"y\": 14\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, job) (daml_participant_sync_commitments_sequencing_time{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"})\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{job}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"ACS Commitment Sequencing Time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Measures how many times the catch-up mode has been triggered over the last hour\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 24,\n \"x\": 0,\n \"y\": 27\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, job)(increase(daml_participant_sync_commitments_catchup_mode_enabled_total{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"}[1h]))\",\n \"legendFormat\": \"{{namespace}} {{job}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"ACS Commitment Catchup in the last hour\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Lag behind the timestamp the commitment checkpoint was created and the timestamp of the checkpoint; periodic lag spikes during commitment computation are expected\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 41\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"min by (namespace, job) ((timestamp(daml_participant_sync_commitments_last_locally_checkpointed{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"}) - ((daml_participant_sync_commitments_last_locally_checkpointed{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"} > 0) / 1e6)))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Commitment Checkpoint Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Lag behind the timestamp of when the last completed commitment interval was processed and the timestamp of the interval\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 41\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"min by (namespace, job) (timestamp(daml_participant_sync_commitments_last_locally_completed{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"}) - ((daml_participant_sync_commitments_last_locally_completed{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"} > 0) / 1e6))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \" Completed ACS Commitment Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Lag behind the timestamp the commitment was received and the timestamp of the commitment\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 49\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"min by (namespace, job) (timestamp(daml_participant_sync_commitments_last_incoming_received{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"}) - ((daml_participant_sync_commitments_last_incoming_received{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"} > 0) / 1e6))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \" Incoming Received ACS Commitment Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Lag behind the timestamp the commitment was processed and the timestamp of the commitment\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 49\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(daml_participant_sync_commitments_last_incoming_processed{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"}) - ((daml_participant_sync_commitments_last_incoming_processed{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"} > 0) / 1e6))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \" Incoming Processed ACS Commitment Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Timestamp of the latest incoming ACS commitment period end that has been received and enqueued, but not yet processed by the participant.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 57\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, job)(daml_participant_sync_commitments_last_incoming_received{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"} != 0)/1e3\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Last Incoming Received ACS Commitment\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Timestamp of the latest incoming ACS commitment period end that was fully processed by the participant.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 57\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, job)(daml_participant_sync_commitments_last_incoming_processed{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"} != 0)/1e3\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Last Incoming Processed ACS Commitment\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Timestamp of the latest locally completed ACS commitment interval. Crash recovery will start reingesting from this timestamp on or from the latest checkpointed ACS commitment interval on, whichever is later.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 65\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, job)(daml_participant_sync_commitments_last_locally_completed{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"} != 0)/1e3\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"C\"\n }\n ],\n \"title\": \"Last Locally Completed ACS Commitment\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Timestamp of the latest checkpointed ACS commitment in microseconds. Crash recovery will start reingesting from this timestamp on or from the latest locally completed ACS commitment interval on, whichever is later.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 65\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace, job)(daml_participant_sync_commitments_last_locally_checkpointed{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"} != 0)/1e3\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"D\"\n }\n ],\n \"title\": \"Last Locally Checkpointed ACS Commitment\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_participant_sync_commitments_compute_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_participant_sync_commitments_compute_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_participant_sync_commitments_compute_duration_seconds,job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_participant_sync_commitments_compute_duration_seconds,job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"ACS Commitment Performance\",\n \"uid\": \"eesa90lstfk00b\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", - "lsu-status.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"LSU Sequencing Tests between Topology Freeze and Upgrade Time\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"Report Overview\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"LSU Sequencing Test Messages Received in the last 5m by sender\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"neutral\": 0\n },\n \"displayName\": \"${__field.labels.sender}\",\n \"mappings\": [],\n \"max\": 15,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"red\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 5\n },\n {\n \"color\": \"green\",\n \"value\": 8\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 19,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\",\n \"text\": {}\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by (sender) (increase(daml_received_lsu_sequencing_test_messages_total{namespace=\\\"$namespace\\\"}[5m]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{sender}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"LSU Sequencing Test Mesages Received in the last 5m\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 20,\n \"w\": 24,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (sender) (rate(daml_received_lsu_sequencing_test_messages_total{namespace=\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{sender}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"LSU Sequencing Test Rate by Sender\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 29\n },\n \"id\": 21,\n \"panels\": [],\n \"title\": \"Handshakes\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 6,\n \"x\": 0,\n \"y\": 30\n },\n \"id\": 22,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"count(max by (namespace, job, member)(daml_sequencer_public_api_handshakes_total{namespace=\\\"$namespace\\\", job=~\\\"$job\\\"}) > 0)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"Unique Members\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Unique Members Handshaking\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 18,\n \"x\": 6,\n \"y\": 30\n },\n \"id\": 23,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by (member) (increase(daml_sequencer_public_api_handshakes_total{namespace=\\\"$namespace\\\", job=~\\\"$job\\\"}[$__range]))\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"{{member}}\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Handshakes per Member\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"indexByName\": {},\n \"renameByName\": {}\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 24,\n \"panels\": [],\n \"title\": \"LSU State\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [\n {\n \"options\": {\n \"0\": {\n \"index\": 0,\n \"text\": \"0: Unset / initial\"\n },\n \"1\": {\n \"index\": 1,\n \"text\": \"1: LSU announcement received\"\n },\n \"2\": {\n \"index\": 2,\n \"text\": \"2: Relevant sequencer successors known\"\n },\n \"3\": {\n \"index\": 3,\n \"text\": \"3: Handshake with successor done\"\n },\n \"4\": {\n \"index\": 4,\n \"text\": \"4: Topology local copy done\"\n },\n \"5\": {\n \"index\": 5,\n \"text\": \"5: LSU is done (node ready to connect to new synchronizer)\"\n }\n },\n \"type\": \"value\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 25,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by (job, node) (daml_participant_lsu_status{namespace=\\\"$namespace\\\"})\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"{{job}} {{node}}\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Participant LSU Status\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"indexByName\": {},\n \"renameByName\": {}\n }\n }\n ],\n \"type\": \"table\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": true,\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_participant_lsu_status,namespace)\",\n \"includeAll\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_participant_lsu_status,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": [\n \"global-domain-8-sequencer\"\n ],\n \"value\": [\n \"global-domain-8-sequencer\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_public_api_handshakes_total{namespace=\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_public_api_handshakes_total{namespace=\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Logical Synchronizer Upgrade\",\n \"uid\": \"cnqlq8j\",\n \"version\": 12,\n \"weekStart\": \"\"\n}\n", + "lsu-status.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"description\": \"LSU Sequencing Tests between Topology Freeze and Upgrade Time\",\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"Report Overview\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"LSU Sequencing Test Messages Received in the last 5m by sender\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"neutral\": 0\n },\n \"displayName\": \"${__field.labels.sender}\",\n \"mappings\": [],\n \"max\": 15,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"red\",\n \"value\": 0\n },\n {\n \"color\": \"yellow\",\n \"value\": 5\n },\n {\n \"color\": \"green\",\n \"value\": 8\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 19,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\",\n \"text\": {}\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by (sender) (increase(daml_received_lsu_sequencing_test_messages_total{namespace=\\\"$namespace\\\"}[5m]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{sender}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"LSU Sequencing Test Mesages Received in the last 5m\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"cps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 20,\n \"w\": 24,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by (sender) (rate(daml_received_lsu_sequencing_test_messages_total{namespace=\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{sender}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"LSU Sequencing Test Rate by Sender\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 29\n },\n \"id\": 21,\n \"panels\": [],\n \"title\": \"Handshakes\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 6,\n \"x\": 0,\n \"y\": 30\n },\n \"id\": 22,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"count(max by (namespace, job, member)(daml_sequencer_public_api_handshakes_total{namespace=\\\"$namespace\\\", job=~\\\"$job\\\"}) > 0)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"Unique Members\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Unique Members Handshaking\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 18,\n \"x\": 6,\n \"y\": 30\n },\n \"id\": 23,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by (member) (increase(daml_sequencer_public_api_handshakes_total{namespace=\\\"$namespace\\\", job=~\\\"$job\\\"}[$__range]))\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"{{member}}\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Handshakes per Member\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"indexByName\": {},\n \"renameByName\": {}\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 39\n },\n \"id\": 24,\n \"panels\": [],\n \"title\": \"LSU State\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [\n {\n \"options\": {\n \"0\": {\n \"index\": 0,\n \"text\": \"0: Unset / initial\"\n },\n \"1\": {\n \"index\": 1,\n \"text\": \"1: LSU announcement received\"\n },\n \"2\": {\n \"index\": 2,\n \"text\": \"2: Relevant sequencer successors known\"\n },\n \"3\": {\n \"index\": 3,\n \"text\": \"3: Handshake with successor done\"\n },\n \"4\": {\n \"index\": 4,\n \"text\": \"4: Topology local copy done\"\n },\n \"5\": {\n \"index\": 5,\n \"text\": \"5: LSU is done (node ready to connect to new synchronizer)\"\n }\n },\n \"type\": \"value\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"successor_psid\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 749\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"namespace\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 144\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"job\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 147\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 544\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 40\n },\n \"id\": 25,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_participant_lsu_status{namespace=\\\"$namespace\\\"}\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"{{job}} {{node}}\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Participant LSU Status\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true,\n \"__name__\": true,\n \"component\": true,\n \"container\": true,\n \"endpoint\": true,\n \"instance\": true,\n \"job\": false,\n \"node\": true,\n \"otel_scope_name\": true,\n \"pod\": true,\n \"service\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {}\n }\n }\n ],\n \"type\": \"table\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": true,\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_participant_lsu_status,namespace)\",\n \"includeAll\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_participant_lsu_status,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_public_api_handshakes_total{namespace=\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_public_api_handshakes_total{namespace=\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Logical Synchronizer Upgrade\",\n \"uid\": \"cnqlq8j\",\n \"version\": 12,\n \"weekStart\": \"\"\n}\n", "node-health.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"panels\": [],\n \"repeat\": \"namespace\",\n \"title\": \"$namespace\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Status of the participant (healthy or not)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"axisPlacement\": \"auto\",\n \"fillOpacity\": 70,\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineWidth\": 0,\n \"spanNulls\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 0\n },\n {\n \"color\": \"green\",\n \"value\": 1\n }\n ]\n },\n \"unit\": \"Alive\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 20,\n \"w\": 24,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 1,\n \"options\": {\n \"alignValue\": \"left\",\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"mergeValues\": true,\n \"rowHeight\": 0.9,\n \"showValue\": \"auto\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_health_status{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{node}} {{component}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Health Status\",\n \"type\": \"state-timeline\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"sv\",\n \"sv-1\",\n \"sv-10\",\n \"sv-11\",\n \"sv-12\",\n \"sv-13\",\n \"sv-14\",\n \"sv-2\",\n \"sv-3\",\n \"sv-4\",\n \"sv-5\",\n \"sv-6\",\n \"sv-7\",\n \"sv-8\",\n \"sv-9\",\n \"sv-da-1\",\n \"validator\",\n \"validator-stable-old\",\n \"validator1\",\n \"splitwell\",\n \"party-allocator\"\n ],\n \"value\": [\n \"sv\",\n \"sv-1\",\n \"sv-10\",\n \"sv-11\",\n \"sv-12\",\n \"sv-13\",\n \"sv-14\",\n \"sv-2\",\n \"sv-3\",\n \"sv-4\",\n \"sv-5\",\n \"sv-6\",\n \"sv-7\",\n \"sv-8\",\n \"sv-9\",\n \"sv-da-1\",\n \"validator\",\n \"validator-stable-old\",\n \"validator1\",\n \"splitwell\",\n \"party-allocator\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_health_status{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Node health\",\n \"uid\": \"cnf4nq4\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", "participant-pruning.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 19523,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Age of the oldest event age at hourly granularity\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"h\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 15,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"min by (namespace, job) (daml_pruning_max_event_age{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\"})\",\n \"legendFormat\": \"{{namespace}}-{{job}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Max Event Age\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"30s\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_pruning_max_event_age,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_pruning_max_event_age,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_pruning_max_event_age,job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_pruning_max_event_age,job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Participant Pruning\",\n \"uid\": \"076b0c9a-d21c-4cc6-8aa4-83414353bbcb\",\n \"version\": 9\n}\n", "participant.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 6,\n \"panels\": [],\n \"title\": \"Sequencer Connection\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Delay on event processing of the participant, compared to the sequencers it is connected to.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"ms\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"A\"\n },\n \"properties\": [\n {\n \"id\": \"custom.lineWidth\",\n \"value\": 5\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"B\"\n },\n \"properties\": [\n {\n \"id\": \"custom.lineStyle\",\n \"value\": {\n \"dash\": [\n 10,\n 10\n ],\n \"fill\": \"dash\"\n }\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_client_handler_delay{namespace=~\\\"$namespace\\\",component=\\\"participant\\\",job=~\\\"$job\\\", node=~\\\"$participant\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"_ overall - {{namespace}} {{job}} {{node}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_client_handler_delay_per_connection{namespace=~\\\"$namespace\\\",component=\\\"participant\\\",job=~\\\"$job\\\", node=~\\\"$participant\\\"}\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{node}} {{sequencer}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Participant Client Delay\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Sequencer submissions currently running. Note this is at the level of a sequencer submission not a Daml transaction.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"submissions\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 0,\n \"y\": 11\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"sum by(synchronizer, type, namespace, job, node) (daml_sequencer_client_submissions_in_flight{namespace=~\\\"$namespace\\\", component=\\\"participant\\\", job=~\\\"$job\\\", node=~\\\"$participant\\\"})\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"1m\",\n \"legendFormat\": \"{{namespace}} {{job}} {{node}} {{sync}} {{type}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"In Flight submissions\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Submissions that were not sequenced\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"epm\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 12,\n \"y\": 11\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"increase(daml_sequencer_client_submissions_dropped{namespace=~\\\"$namespace\\\",component=\\\"participant\\\",job=~\\\"$job\\\", node=~\\\"$participant\\\"}[1m])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"1m\",\n \"legendFormat\": \"{{namespace}} {{job}} {{node}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Dropped Submissiones\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 0,\n \"y\": 21\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_client_handler_max_in_flight_event_batches{namespace=~\\\"$namespace\\\",component=\\\"participant\\\",job=~\\\"$job\\\", node=~\\\"$participant\\\"}\",\n \"instant\": false,\n \"legendFormat\": \"max {{namespace}} {{job}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_client_handler_actual_in_flight_event_batches{namespace=~\\\"$namespace\\\",component=\\\"participant\\\",job=~\\\"$job\\\", node=~\\\"$participant\\\"}\",\n \"legendFormat\": \"actual {{namespace}} {{job}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Sequencer Client Batches \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Events received from the sequencer\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"epm\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 12,\n \"y\": 21\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"rate(daml_sequencer_client_handler_sequencer_events{namespace=~\\\"$namespace\\\",component=\\\"participant\\\",job=~\\\"$job\\\", node=~\\\"$participant\\\"}[$__rate_interval])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"1m\",\n \"legendFormat\": \"{{namespace}} {{job}} {{node}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Events received\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 8,\n \"panels\": [],\n \"title\": \"Commands\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Daml commands through the synchronous CommandService currently in flight\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"submissions\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_participant_api_commands_max_in_flight_length{namespace=~\\\"$namespace\\\",component=\\\"participant\\\",job=~\\\"$job\\\", node=~\\\"$participant\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"interval\": \"1m\",\n \"legendFormat\": \"{{namespace}} {{job}} {{node}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"In Flight Commands\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 42\n },\n \"id\": 14,\n \"panels\": [],\n \"title\": \"Validation Requests\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of requests that are currently being validated. This also covers requests submitted by other participants.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 43\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"daml_participant_inflight_validation_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$participant\\\"}\",\n \"legendFormat\": \"actual {{namespace}} {{job}} {{node}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_participant_max_inflight_validation_requests{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\",node=~\\\"$participant\\\"}\",\n \"instant\": false,\n \"legendFormat\": \"max {{namespace}} {{job}} {{node}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Number of requests being validated\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 51\n },\n \"id\": 5,\n \"panels\": [],\n \"title\": \"Status\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Status of the participant (healthy or not)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"axisPlacement\": \"auto\",\n \"fillOpacity\": 70,\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineWidth\": 0,\n \"spanNulls\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 0\n },\n {\n \"color\": \"green\",\n \"value\": 1\n }\n ]\n },\n \"unit\": \"Alive\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 24,\n \"x\": 0,\n \"y\": 52\n },\n \"id\": 4,\n \"options\": {\n \"alignValue\": \"left\",\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"mergeValues\": true,\n \"rowHeight\": 0.9,\n \"showValue\": \"auto\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${DS}\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_health_status{namespace=~\\\"$namespace\\\",job=~\\\"$job\\\", node=~\\\"$participant\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{node}} {{component}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Health Status\",\n \"type\": \"state-timeline\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"5m\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_participant_api_commands_max_in_flight_length,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_participant_api_commands_max_in_flight_length,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_participant_api_commands_max_in_flight_length{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_participant_api_commands_max_in_flight_length{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 7,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_participant_api_commands_max_in_flight_length{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"participant\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_participant_api_commands_max_in_flight_length{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},node)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 7,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Participant\",\n \"uid\": \"edkzo5ukgeqyoc\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", @@ -190,7 +195,7 @@ "sequencer_messages.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 9,\n \"panels\": [],\n \"title\": \"Messages\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The average per second messages for the last 24h\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n }\n },\n \"mappings\": [],\n \"unit\": \"mps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 7,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 8,\n \"options\": {\n \"displayLabels\": [\n \"name\",\n \"value\",\n \"percent\"\n ],\n \"legend\": {\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"values\": [\n \"percent\",\n \"value\"\n ]\n },\n \"pieType\": \"donut\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": true\n },\n \"sort\": \"desc\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"exemplar\": false,\n \"expr\": \"sum by(type) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[24h]))\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"B\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Last 24h messages / s\",\n \"type\": \"piechart\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"total\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"total\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 17,\n \"x\": 7,\n \"y\": 1\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"sum(rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"sum by(type) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"type\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"sum(rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[1h]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"hourly total\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[1h] offset 30d))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"hourly total 30 days ago\",\n \"range\": true,\n \"refId\": \"B\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Message Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 20,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"sum by(member) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=\\\"send-confirmation-request\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Confirmation Requests Rate by Member\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 4,\n \"x\": 20,\n \"y\": 12\n },\n \"id\": 6,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(sum(delta(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\",job=\\\"$job\\\",type=\\\"send-confirmation-request\\\"}[1h])) by (member))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Confirmation Requests by Member in the last Hour\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Time\": \"\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 20,\n \"x\": 0,\n \"y\": 23\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"sum by(member) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=\\\"send-confirmation-response\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Confirmation Responses Rate by Member\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 4,\n \"x\": 20,\n \"y\": 23\n },\n \"id\": 15,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(sum(delta(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\",job=\\\"$job\\\",type=\\\"send-confirmation-response\\\"}[1h])) by (member))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Confirmation Responses by Member in the last Hour\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Time\": \"\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 34\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\",member=~\\\"PAR::.*\\\"}[$__rate_interval])) by (member)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Message Rate by Participant\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 12,\n \"x\": 0,\n \"y\": 46\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\",member=~\\\"MED::.*\\\"}[$__rate_interval])) by (member)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Message Rate by Mediator\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 12,\n \"x\": 12,\n \"y\": 46\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(member) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", member!~\\\"(MED|PAR)::.*\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Message Rate by Member excluding Mediator/Participant\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 57\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"Message Bytes\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"total\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"total\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 24,\n \"x\": 0,\n \"y\": 58\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(type) (rate(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"type\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[1h]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"hourly total\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[1h] offset 30d))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"hourly total 30 days ago\",\n \"range\": true,\n \"refId\": \"B\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Message Byte Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Average size per sequencer message in the last hour\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"total\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"total\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 24,\n \"x\": 0,\n \"y\": 69\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(increase(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[1h])) / sum(increase(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[1h]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(type) (increase(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[1h])) / sum by(type) (increase(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\"}[1h]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"type\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Average Sequencer Message Size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 20,\n \"x\": 0,\n \"y\": 80\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"topk(\\n 100,\\n avg_over_time(\\n (\\n sum by(member) (\\n rate(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=\\\"send-confirmation-request\\\"}[$__rate_interval])\\n )\\n )[$__range:]\\n )\\n)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Top 100 Confirmation Requests Byte Rate by Member\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_block_events_total,namespace)\",\n \"includeAll\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_block_events_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"global-domain-4-sequencer\",\n \"value\": \"global-domain-4-sequencer\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\"},job)\",\n \"includeAll\": false,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-30m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Sequencer Messages\",\n \"uid\": \"fdjrxql2alblsd\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", "sequencer_subscriptions.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 1115,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"panels\": [],\n \"title\": \"Subscriptions\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 4,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 1,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\"lastNotNull\"],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"exemplar\": false,\n \"expr\": \"daml_sequencer_public_api_subscriptions\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"{{namespace}} M{{migration_id}}\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Subscriptions\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 20,\n \"x\": 4,\n \"y\": 1\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\"lastNotNull\", \"max\"],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_public_api_subscriptions{namespace=~\\\"$namespace\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} M{{migration_id}}\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Number of active subscriptions\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 10\n },\n \"id\": 5,\n \"panels\": [],\n \"title\": \"Subscription Lag\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 11\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"rate(daml_cache_misses{namespace=~\\\"$namespace\\\", cache=\\\"events-fan-out-buffer\\\"}[$__rate_interval]) > 0\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"outside buffer {{namespace}} {{subscriber}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Subscriber cache misses\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 21\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [\"lastNotNull\", \"min\"],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"single\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_public_api_subscription_last_timestamp{namespace=~\\\"$namespace\\\"} / 1000\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{subscriber}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Subscribers\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"µs\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 31\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [\"lastNotNull\", \"max\"],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_sequencer_last_timestamp{namespace=~\\\"$namespace\\\"} - daml_sequencer_head_timestamp{namespace=~\\\"$namespace\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} diff\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Head vs Last Timestamp Diff\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": [\"sv-1\"],\n \"value\": [\"sv-1\"]\n },\n \"definition\": \"label_values(daml_sequencer_public_api_subscriptions,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_public_api_subscriptions,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Sequencer Subscriptions\",\n \"uid\": \"685922ac-4aca-4b28-a1a1-4375a93a514d\",\n \"version\": 3\n}\n", "sequencer_topology_transactions.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"id\": 154,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 7,\n \"panels\": [],\n \"title\": \"Topology\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"total\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"total\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 16,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(type) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=~\\\"send-topology|send-time-proof\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"type\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=~\\\"send-topology|send-time-proof\\\"}[1h]))\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"hourly total\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Topology send rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 8,\n \"x\": 16,\n \"y\": 1\n },\n \"id\": 3,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(sum by(member) (delta(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=\\\"send-topology\\\", member=~\\\"PAR::.*\\\"}[24h])))\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Topology transactions participants last 24h\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Time\": \"\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 16,\n \"x\": 0,\n \"y\": 13\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"sum by(member) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", member=~\\\"PAR::.*\\\", type=\\\"send-topology\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Topology Rate by Participant\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 8,\n \"x\": 16,\n \"y\": 13\n },\n \"id\": 4,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(sum by(member) (delta(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=\\\"send-topology\\\", member!~\\\"PAR::.*\\\"}[24h])))\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Topology transactions non participants last 24h\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Time\": \"\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 26\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"builder\",\n \"expr\": \"sum by(member) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", member!~\\\"PAR::.*\\\", type=\\\"send-topology\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Topology Rate by Non Participant\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 36\n },\n \"id\": 8,\n \"panels\": [],\n \"title\": \"Time Proofs\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 16,\n \"x\": 0,\n \"y\": 37\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Max\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(member) (rate(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=\\\"send-time-proof\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Time Proofs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 8,\n \"x\": 16,\n \"y\": 37\n },\n \"id\": 6,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(sum by(member) (delta(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=\\\"send-time-proof\\\"}[24h])))\",\n \"format\": \"table\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Time Proofs last 24h\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Time\": \"\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 50\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"Bytes\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byFrameRefID\",\n \"options\": \"total\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"total\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 51\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(type) (rate(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=~\\\"send-topology|send-time-proof\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"type\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=~\\\"send-topology|send-time-proof\\\"}[1h]))\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"hourly total\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Topology send rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 63\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"max\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"sum by(member) (rate(daml_sequencer_block_event_bytes_total{namespace=\\\"$namespace\\\", job=\\\"$job\\\", type=~\\\"send-topology|send-time-proof\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"total\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Sequencer Topology Rate by Non Participant\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_block_events_total,namespace)\",\n \"includeAll\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_block_events_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"global-domain-3-sequencer\",\n \"value\": \"global-domain-3-sequencer\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\"},job)\",\n \"includeAll\": false,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_sequencer_block_events_total{namespace=\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-30m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Sequencer Topology Transactions\",\n \"uid\": \"2f351a91-c0b3-4c6b-b5e2-25d8b9cc1304\",\n \"version\": 6\n}\n", - "unresponsive_parties.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Time\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 579\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"{component=\\\"mediator\\\", container=\\\"mediator\\\", endpoint=\\\"cm-metrics\\\", instance=\\\"10.40.11.98:10013\\\", job=\\\"global-domain-7-mediator\\\", migration_id=\\\"7\\\", namespace=\\\"sv-6\\\", node=\\\"mediator\\\", otel_scope_name=\\\"canton\\\", participant=\\\"DA-Helm-Test-Node::12208273e690b127deff0174c189b2251d7b5cf9216cd53ed700f726c8e8792807ca\\\", party=\\\"DSO::12209471e1a52edc2995ad347371597a5872f2704cb2cb4bb330a849e7309598259e\\\", pod=\\\"global-domain-7-mediator-69d6489748-rw56k\\\", service=\\\"global-domain-7-mediator\\\"}\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(max by (party) (increase(daml_mediator_timeout_non_responsive_participants_total{namespace=~\\\"$namespace\\\",party=~\\\"$party\\\",participant=~\\\"$participant\\\"}[1h])))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Number of missed confirmations by party in the last hour\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Value\": \"Value\",\n \"party\": \"Party\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Time\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 579\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 3,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"{component=\\\"mediator\\\", container=\\\"mediator\\\", endpoint=\\\"cm-metrics\\\", instance=\\\"10.40.11.98:10013\\\", job=\\\"global-domain-7-mediator\\\", migration_id=\\\"7\\\", namespace=\\\"sv-6\\\", node=\\\"mediator\\\", otel_scope_name=\\\"canton\\\", participant=\\\"DA-Helm-Test-Node::12208273e690b127deff0174c189b2251d7b5cf9216cd53ed700f726c8e8792807ca\\\", party=\\\"DSO::12209471e1a52edc2995ad347371597a5872f2704cb2cb4bb330a849e7309598259e\\\", pod=\\\"global-domain-7-mediator-69d6489748-rw56k\\\", service=\\\"global-domain-7-mediator\\\"}\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(max by (participant) (increase(daml_mediator_timeout_non_responsive_participants_total{namespace=~\\\"$namespace\\\",party=~\\\"$party\\\",participant=~\\\"$participant\\\"}[1h])))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Number of missed confirmations by participant in the last hour\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Time\": \"\",\n \"Value\": \"Value\",\n \"participant\": \"Participant\",\n \"party\": \"Party\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 16,\n \"w\": 24,\n \"x\": 0,\n \"y\": 10\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"max by (party, participant) (rate(daml_mediator_timeout_non_responsive_participants_total{namespace=~\\\"$namespace\\\",party=~\\\"$party\\\",participant=~\\\"$participant\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{party}}@{{participant}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate of missed confirmations\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,namespace)\",\n \"includeAll\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,party)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"party\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,party)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,participant)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"participant\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,participant)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Unresponsive Parties\",\n \"uid\": \"cnck4jp\",\n \"version\": 7,\n \"weekStart\": \"\"\n}\n" + "unresponsive_parties.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Time\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 579\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"{component=\\\"mediator\\\", container=\\\"mediator\\\", endpoint=\\\"cm-metrics\\\", instance=\\\"10.40.11.98:10013\\\", job=\\\"global-domain-7-mediator\\\", migration_id=\\\"7\\\", namespace=\\\"sv-6\\\", node=\\\"mediator\\\", otel_scope_name=\\\"canton\\\", participant=\\\"DA-Helm-Test-Node::12208273e690b127deff0174c189b2251d7b5cf9216cd53ed700f726c8e8792807ca\\\", party=\\\"DSO::12209471e1a52edc2995ad347371597a5872f2704cb2cb4bb330a849e7309598259e\\\", pod=\\\"global-domain-7-mediator-69d6489748-rw56k\\\", service=\\\"global-domain-7-mediator\\\"}\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(max by (party) (increase(daml_mediator_timeout_non_responsive_participants_total{namespace=~\\\"$namespace\\\",party=~\\\"$party\\\",participant=~\\\"$participant\\\"}[1h])))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Number of missed confirmations by party in the last hour\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Value\": \"Value\",\n \"party\": \"Party\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Time\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 579\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 3,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"{component=\\\"mediator\\\", container=\\\"mediator\\\", endpoint=\\\"cm-metrics\\\", instance=\\\"10.40.11.98:10013\\\", job=\\\"global-domain-7-mediator\\\", migration_id=\\\"7\\\", namespace=\\\"sv-6\\\", node=\\\"mediator\\\", otel_scope_name=\\\"canton\\\", participant=\\\"DA-Helm-Test-Node::12208273e690b127deff0174c189b2251d7b5cf9216cd53ed700f726c8e8792807ca\\\", party=\\\"DSO::12209471e1a52edc2995ad347371597a5872f2704cb2cb4bb330a849e7309598259e\\\", pod=\\\"global-domain-7-mediator-69d6489748-rw56k\\\", service=\\\"global-domain-7-mediator\\\"}\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sort_desc(max by (participant) (increase(daml_mediator_timeout_non_responsive_participants_total{namespace=~\\\"$namespace\\\",party=~\\\"$party\\\",participant=~\\\"$participant\\\"}[1h])))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Number of missed confirmations by participant in the last hour\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true\n },\n \"includeByName\": {},\n \"indexByName\": {},\n \"renameByName\": {\n \"Time\": \"\",\n \"Value\": \"Value\",\n \"participant\": \"Participant\",\n \"party\": \"Party\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 16,\n \"w\": 24,\n \"x\": 0,\n \"y\": 10\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"max by (party, participant) (rate(daml_mediator_timeout_non_responsive_participants_total{namespace=~\\\"$namespace\\\",party=~\\\"$party\\\",participant=~\\\"$participant\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{party}}@{{participant}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate of missed confirmations\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,namespace)\",\n \"includeAll\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,party)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"party\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,party)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,participant)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"participant\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_mediator_timeout_non_responsive_participants_total,participant)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Unresponsive Parties\",\n \"uid\": \"cnck4jp\",\n \"version\": 7,\n \"weekStart\": \"\"\n}\n" }, "kind": "ConfigMap", "metadata": { @@ -267,7 +272,7 @@ "apiVersion": "v1", "data": { "executor_service.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 1171,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 21,\n \"panels\": [],\n \"title\": \"Runtime Metrics\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 23,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.95, sum by(namespace, pod, container, name, component, participant) (rate(daml_executor_runtime_duration_seconds{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{pod}} - {{container}} - {{name}} / {{component}} {{participant}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Task Run Duration (p95}\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 24,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without() (histogram_avg(rate(daml_executor_runtime_duration_seconds{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Task Run Duration (average}\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 28,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.95, sum by(namespace, pod, container, name, component, participant) (rate(daml_executor_runtime_idle_duration_seconds{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{pod}} - {{container}} - {{name}} / {{component}} {{participant}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Task Idle Duration (p95}\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 29,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without() (histogram_avg(rate(daml_executor_runtime_idle_duration_seconds{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Task Idle Duration (average}\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"tasks / s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 26,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without($group_without) (rate(daml_executor_runtime_submitted_total{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval]))\\n\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Submitted Tasks\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"tasks / s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 17\n },\n \"id\": 30,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without($group_without) (rate(daml_executor_runtime_completed_total{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval]))\\n\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Completed Tasks\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 30\n },\n \"id\": 27,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without($group_without) (daml_executor_runtime_running{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"})\\n\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Running Tasks\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 42\n },\n \"id\": 6,\n \"panels\": [],\n \"title\": \"Common Metrics\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"gridPos\": {\n \"h\": 6,\n \"w\": 6,\n \"x\": 0,\n \"y\": 43\n },\n \"id\": 19,\n \"options\": {\n \"code\": {\n \"language\": \"plaintext\",\n \"showLineNumbers\": false,\n \"showMiniMap\": false\n },\n \"content\": \"# Common Executor Service Metrics\\n\\nExecutor Service Metrics that are common between thread pools and fork join pools.\\nThese metrics are outsourced from the Executor Service itself.\",\n \"mode\": \"markdown\"\n },\n \"pluginVersion\": \"11.1.0\",\n \"title\": \"Info\",\n \"type\": \"text\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 49\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_pool_size{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"legendFormat\": \"{{pod}} -- {{name}} -- {{type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Pool Size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"axisSoftMin\": 0,\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 58\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": false\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_threads_active{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"legendFormat\": \"{{pod}} -- {{name}} -- {{type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Active Threads\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"axisSoftMin\": 0,\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 67\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": false\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_tasks_queued{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"legendFormat\": \"{{pod}} -- {{name}} -- {{type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Queued Tasks\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 76\n },\n \"id\": 8,\n \"panels\": [],\n \"title\": \"Thread Pool Metrics\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 77\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_pool_core{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"legendFormat\": \"Core - {{pod}} -- {{name}} -- {{type}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_pool_max{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"hide\": false,\n \"legendFormat\": \"Max - {{pod}} -- {{name}} -- {{type}}\",\n \"range\": true,\n \"refId\": \"B\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_pool_largest{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"hide\": false,\n \"legendFormat\": \"Largest - {{pod}} -- {{name}} -- {{type}}\",\n \"range\": true,\n \"refId\": \"C\"\n }\n ],\n \"title\": \"Pool Sizes\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"tasks/s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 86\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without($group_without) (rate(daml_executor_tasks_submitted{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Tasks Submitted\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"tasks/s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 95\n },\n \"id\": 31,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without($group_without) (rate(daml_executor_tasks_completed{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval]))\",\n \"hide\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Tasks Completed\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 104\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_tasks_queue_remaining{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"legendFormat\": \"Remaining - {{pod}} -- {{name}} -- {{type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Remaining Queue Capacity\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 113\n },\n \"id\": 13,\n \"panels\": [],\n \"title\": \"Fork Join Metrics\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"axisSoftMin\": 0,\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 114\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": false\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_threads_running{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"legendFormat\": \"{{pod}} -- {{name}} -- {{type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Running Threads\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"tasks/s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 123\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without($group_without) (rate(daml_executor_tasks_stolen{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Stolen Tasks\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"tasks/s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 132\n },\n \"id\": 16,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without($group_without) (rate(daml_executor_tasks_executing_queued{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Task Queuing Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \" tasks\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 141\n },\n \"id\": 17,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"daml_executor_tasks_executing_queued{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\", container=\\\"$container\\\", name=~\\\"$name\\\"}\",\n \"hide\": false,\n \"legendFormat\": \"Queued total - {{pod}} -- {{name}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Queued Tasks\",\n \"type\": \"timeseries\"\n }\n ],\n \"refresh\": false,\n \"schemaVersion\": 39,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"Prometheus\",\n \"value\": \"prometheus\"\n },\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"datasource\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"queryValue\": \"\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_executor_pool_size, namespace)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_executor_pool_size, namespace)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_executor_pool_size{namespace=\\\"$namespace\\\"}, pod)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"pod\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_executor_pool_size{namespace=\\\"$namespace\\\"}, pod)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"isNone\": true,\n \"selected\": false,\n \"text\": \"None\",\n \"value\": \"\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_executor_pool_size{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\"}, container)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"container\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_executor_pool_size{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\"}, container)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_executor_pool_size{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\",container=\\\"$container\\\"}, name)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"name\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_executor_pool_size{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\",container=\\\"$container\\\"}, name)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"hide\": 2,\n \"name\": \"group_without\",\n \"query\": \"endpoint, instance,daml_version,job,namespace,container,canton_version\",\n \"skipUrlSync\": false,\n \"type\": \"constant\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"JVM Executor Services\",\n \"uid\": \"AYtzKz2Vz\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", - "jvm.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"id\": 9858,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"CPU\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 6,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 17,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"jvm_cpu_count{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"{{job}}\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Count\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 18,\n \"x\": 6,\n \"y\": 1\n },\n \"id\": 19,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"jvm_cpu_recent_utilization_ratio{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"CPU Utilization\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 10\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(container, pod) (rate(jvm_cpu_time_seconds_total{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{pod}} {{container}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"CPU Usage\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 22\n },\n \"id\": 14,\n \"panels\": [],\n \"title\": \"Threads\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 24,\n \"x\": 0,\n \"y\": 23\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"jvm_thread_count{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{job}} - daemon {{jvm_thread_daemon}} - {{jvm_thread_state}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Threads\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 37\n },\n \"id\": 4,\n \"panels\": [],\n \"title\": \"Garbage Collection\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(gc, container, pod, jvm_gc_name, jvm_gc_action, namespace) (histogram_count(rate(jvm_gc_duration_seconds{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}} {{jvm_gc_name}} {{jvm_gc_action}} {{pod}} {{container}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"GC Runs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 38\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(jvm_gc_action, jvm_gc_name, container, pod, namespace) (histogram_sum(rate(jvm_gc_duration_seconds{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"rate {{namespace}} {{jvm_gc_name}} {{jvm_gc_action}} {{pod}} {{container}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(jvm_gc_action, jvm_gc_name, container, pod, namespace) (histogram_avg(rate(jvm_gc_duration_seconds{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}[$__rate_interval])))\",\n \"hide\": false,\n \"legendFormat\": \"avg {{namespace}} {{jvm_gc_name}} {{jvm_gc_action}} {{pod}} {{container}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"GC Timing\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 51\n },\n \"id\": 7,\n \"panels\": [],\n \"title\": \"Memory\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 52\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_memory_used_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_memory_pool_name}} - {{jvm_memory_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Memory Used\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 52\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_memory_limit_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_memory_pool_name}} - {{jvm_memory_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Memory Limit\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 65\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_memory_used_after_last_gc_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_memory_pool_name}} - {{jvm_memory_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Memory Used After Last GC \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 65\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_memory_committed_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_memory_pool_name}} - {{jvm_memory_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Memory Committed \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 78\n },\n \"id\": 21,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_buffer_memory_usage_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_buffer_pool_name}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Buffer Memory Usage\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 78\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_buffer_memory_limit_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_buffer_pool_name}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Buffer Memory Limit\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 41,\n \"tags\": [\n \"jvm\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"Prometheus\",\n \"value\": \"prometheus\"\n },\n \"includeAll\": false,\n \"name\": \"datasource\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(jvm_cpu_count,namespace)\",\n \"description\": \"K8s namespace\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(jvm_cpu_count,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\"},service)\",\n \"description\": \"Splice app\",\n \"includeAll\": true,\n \"label\": \"App\",\n \"multi\": true,\n \"name\": \"app\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\"},service)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\", service=~\\\"$app\\\"},pod)\",\n \"description\": \"K8s pod\",\n \"includeAll\": true,\n \"label\": \"Pod\",\n \"multi\": true,\n \"name\": \"pod\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\", service=~\\\"$app\\\"},pod)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\", service=~\\\"$app\\\"},container)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"label\": \"Container\",\n \"multi\": true,\n \"name\": \"container\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\", service=~\\\"$app\\\"},container)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"sort\": 1,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-30m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"JVM Metrics\",\n \"uid\": \"rgt7PhA4z\",\n \"version\": 1\n}\n" + "jvm.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"CPU\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 6,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 17,\n \"options\": {\n \"minVizHeight\": 75,\n \"minVizWidth\": 75,\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showThresholdLabels\": false,\n \"showThresholdMarkers\": true,\n \"sizing\": \"auto\"\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"jvm_cpu_count{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": true,\n \"legendFormat\": \"{{job}}\",\n \"range\": false,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Count\",\n \"type\": \"gauge\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 18,\n \"x\": 6,\n \"y\": 1\n },\n \"id\": 19,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"jvm_cpu_recent_utilization_ratio{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{job}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"CPU Utilization\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 10\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(container, pod) (rate(jvm_cpu_time_seconds_total{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{pod}} {{container}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"CPU Usage\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 22\n },\n \"id\": 14,\n \"panels\": [],\n \"title\": \"Threads\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 24,\n \"x\": 0,\n \"y\": 23\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"jvm_thread_count{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"instant\": false,\n \"legendFormat\": \"{{job}} - daemon {{jvm_thread_daemon}} - {{jvm_thread_state}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Threads\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 37\n },\n \"id\": 4,\n \"panels\": [],\n \"title\": \"Garbage Collection\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(gc, container, pod, jvm_gc_name, jvm_gc_action, namespace) (histogram_count(rate(jvm_gc_duration_seconds{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{namespace}} {{jvm_gc_name}} {{jvm_gc_action}} {{pod}} {{container}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"GC Runs\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 38\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(jvm_gc_action, jvm_gc_name, container, pod, namespace) (histogram_sum(rate(jvm_gc_duration_seconds{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"rate {{namespace}} {{jvm_gc_name}} {{jvm_gc_action}} {{pod}} {{container}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(jvm_gc_action, jvm_gc_name, container, pod, namespace) (histogram_avg(rate(jvm_gc_duration_seconds{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"avg {{namespace}} {{jvm_gc_name}} {{jvm_gc_action}} {{pod}} {{container}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"GC Timing\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 51\n },\n \"id\": 7,\n \"panels\": [],\n \"title\": \"Memory\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 52\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_memory_used_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_memory_pool_name}} - {{jvm_memory_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Memory Used\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 52\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_memory_limit_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_memory_pool_name}} - {{jvm_memory_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Memory Limit\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 65\n },\n \"id\": 13,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_memory_used_after_last_gc_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_memory_pool_name}} - {{jvm_memory_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Memory Used After Last GC \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 65\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_memory_committed_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_memory_pool_name}} - {{jvm_memory_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Memory Committed \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 78\n },\n \"id\": 21,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_buffer_memory_used_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_buffer_pool_name}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Buffer Memory Usage\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"bytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 78\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"jvm_buffer_memory_limit_bytes{namespace=~\\\"$namespace\\\", pod=~\\\"$pod\\\", container=~\\\"$container\\\", service=~\\\"$app\\\"}\",\n \"legendFormat\": \"{{pod}} {{container}} - {{jvm_buffer_pool_name}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Buffer Memory Limit\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 42,\n \"tags\": [\n \"jvm\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"Prometheus\",\n \"value\": \"prometheus\"\n },\n \"includeAll\": false,\n \"name\": \"datasource\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(jvm_cpu_count,namespace)\",\n \"description\": \"K8s namespace\",\n \"includeAll\": true,\n \"label\": \"Namespace\",\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(jvm_cpu_count,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\"},service)\",\n \"description\": \"Splice app\",\n \"includeAll\": true,\n \"label\": \"App\",\n \"multi\": true,\n \"name\": \"app\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\"},service)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\", service=~\\\"$app\\\"},pod)\",\n \"description\": \"K8s pod\",\n \"includeAll\": true,\n \"label\": \"Pod\",\n \"multi\": true,\n \"name\": \"pod\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\", service=~\\\"$app\\\"},pod)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\", service=~\\\"$app\\\"},container)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"label\": \"Container\",\n \"multi\": true,\n \"name\": \"container\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(jvm_cpu_count{namespace=~\\\"$namespace\\\", service=~\\\"$app\\\"},container)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 1,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-30m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"JVM Metrics\",\n \"uid\": \"rgt7PhA4z\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n" }, "kind": "ConfigMap", "metadata": { @@ -321,8 +326,9 @@ "grpc.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 1177,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($quantile, sum by(grpc_service_name, grpc_client_type, grpc_method_name, grpc_server_type, grpc_code) (rate(daml_grpc_server_duration_seconds{namespace=\\\"$namespace\\\", service=\\\"$component\\\", grpc_service_name=~\\\"$service\\\", grpc_method_name=~\\\"$method\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{grpc_method_name}} - {{grpc_code}} ({{grpc_client_type}} - {{grpc_server_type}})\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Latency Quantile\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(grpc_server_type, grpc_service_name, grpc_client_type, grpc_method_name) (rate(daml_grpc_server_started_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\", grpc_service_name=~\\\"$service\\\", grpc_method_name=~\\\"$method\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{grpc_method_name}} ({{grpc_client_type}} - {{grpc_server_type}}) \",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests Started\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(grpc_server_type, grpc_service_name, grpc_client_type, grpc_method_name, grpc_code) (rate(daml_grpc_server_handled_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\", grpc_service_name=~\\\"$service\\\", grpc_method_name=~\\\"$method\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{grpc_method_name}} - {{grpc_code}} - ({{grpc_client_type}} - {{grpc_server_type}}) \",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Requests Finished\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 0,\n \"y\": 20\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(grpc_server_type, grpc_service_name, grpc_client_type, grpc_method_name, grpc_code) (rate(daml_grpc_server_handled_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\", grpc_service_name=~\\\"$service\\\", grpc_method_name=~\\\"$method\\\", grpc_code!=\\\"OK\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{grpc_method_name}} - {{grpc_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"bars\",\n \"fillOpacity\": 100,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"normal\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percentunit\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 12,\n \"y\": 20\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(service, grpc_server_type, grpc_service_name, grpc_client_type, grpc_method_name, grpc_code) (rate(daml_grpc_server_handled_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\", grpc_service_name=~\\\"$service\\\", grpc_method_name=~\\\"$method\\\", grpc_code!=\\\"OK\\\"}[1m])) / on(service, grpc_server_type, grpc_service_name, grpc_client_type, grpc_method_name) group_left() sum by(service, grpc_server_type, grpc_service_name, grpc_client_type, grpc_method_name) (rate(daml_grpc_server_handled_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\", grpc_service_name=~\\\"$service\\\", grpc_method_name=~\\\"$method\\\"}[1m]))\",\n \"legendFormat\": \"{{grpc_method_name}} -- {{grpc_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Error Distribution\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(grpc_server_type, grpc_service_name, grpc_client_type, grpc_method_name) (histogram_avg(rate(daml_grpc_server_messages_received_bytes{namespace=\\\"$namespace\\\", service=\\\"$component\\\", grpc_service_name=~\\\"$service\\\", grpc_method_name=~\\\"$method\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{grpc_method_name}} - ({{grpc_client_type}} - {{grpc_server_type}})\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Average Request Payload Size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"log\": 2,\n \"type\": \"log\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"Bps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 12,\n \"x\": 12,\n \"y\": 32\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by(grpc_server_type, grpc_service_name, grpc_client_type, grpc_method_name) (histogram_sum(rate(daml_grpc_server_messages_received_bytes{namespace=\\\"$namespace\\\", service=\\\"$component\\\", grpc_service_name=~\\\"$service\\\", grpc_method_name=~\\\"$method\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"{{grpc_method_name}} - ({{grpc_client_type}} - {{grpc_server_type}})\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Request Payload Throughput\",\n \"type\": \"timeseries\"\n }\n ],\n \"refresh\": \"1m\",\n \"schemaVersion\": 39,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_grpc_server_started_total, namespace)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_grpc_server_started_total, namespace)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"participant-0\",\n \"value\": \"participant-0\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_grpc_server_started_total{namespace=\\\"$namespace\\\"}, service)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"component\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_grpc_server_started_total{namespace=\\\"$namespace\\\"}, service)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": \"com.daml.ledger.api.v2.CommandService\",\n \"value\": \"com.daml.ledger.api.v2.CommandService\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_grpc_server_started_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\"}, grpc_service_name)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"service\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_grpc_server_started_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\"}, grpc_service_name)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_grpc_server_started_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\",grpc_service_name=~\\\"$service\\\"}, grpc_method_name)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"multi\": false,\n \"name\": \"method\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_grpc_server_started_total{namespace=\\\"$namespace\\\", service=\\\"$component\\\",grpc_service_name=~\\\"$service\\\"}, grpc_method_name)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"Prometheus\",\n \"value\": \"prometheus\"\n },\n \"hide\": 2,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"datasource\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"0.95\",\n \"value\": \"0.95\"\n },\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"quantile\",\n \"options\": [\n {\n \"selected\": false,\n \"text\": \"0.5\",\n \"value\": \"0.5\"\n },\n {\n \"selected\": false,\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n {\n \"selected\": true,\n \"text\": \"0.95\",\n \"value\": \"0.95\"\n },\n {\n \"selected\": false,\n \"text\": \"0.99\",\n \"value\": \"0.99\"\n }\n ],\n \"query\": \"0.5,0.9,0.95,0.99\",\n \"skipUrlSync\": false,\n \"type\": \"custom\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"gRPC Endpoints\",\n \"uid\": \"ODlOJFvVk\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", "health.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 2,\n \"id\": 9,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"fillOpacity\": 71,\n \"lineWidth\": 0\n },\n \"mappings\": [\n {\n \"options\": {\n \"0\": {\n \"index\": 1,\n \"text\": \"inactive\"\n },\n \"1\": {\n \"index\": 0,\n \"text\": \"active\"\n }\n },\n \"type\": \"value\"\n }\n ],\n \"max\": 1,\n \"min\": 0,\n \"noValue\": \"0\",\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"dark-red\",\n \"value\": null\n },\n {\n \"color\": \"green\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"colWidth\": 1,\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"rowHeight\": 0.8,\n \"showValue\": \"never\",\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.3.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"builder\",\n \"exemplar\": false,\n \"expr\": \"daml_health_status{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"legendFormat\": \"{{pod}} {{component}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Components' Status\",\n \"type\": \"status-history\"\n }\n ],\n \"refresh\": false,\n \"schemaVersion\": 37,\n \"style\": \"dark\",\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"Prometheus\",\n \"value\": \"Prometheus\"\n },\n \"hide\": 2,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"datasource\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"static_local\",\n \"value\": \"static_local\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_health_status, namespace)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_health_status, namespace)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=\\\"$namespace\\\"}, pod)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"pod\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_health_status{namespace=\\\"$namespace\\\"}, pod)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Health\",\n \"uid\": \"IME_mP2Vk\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", "http.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 1182,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 15,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"label_replace(histogram_count(rate(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\", operation=~\\\"$operation\\\", status_code=~\\\"2.*\\\"}[$__rate_interval])), \\\"source_display\\\", \\\"Source UI: $1\\\", \\\"source_ui\\\", \\\"(.+)\\\")\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_service}} {{operation}} {{status_code}} {{source_display}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Handled Requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 9,\n \"x\": 15,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\", operation=~\\\"$operation\\\", status_code!~\\\"2.*\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_service}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Errors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, rate(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\", operation=~\\\"$operation\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_service}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Request Timing quantile $percentile\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"requests\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 22\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_http_requests_inflight{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\", operation=~\\\"$operation\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_service}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"In Flight Requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_requests_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_requests_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_service)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"service\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_service)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\"},operation)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"operation\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\"},operation)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n \"name\": \"percentile\",\n \"options\": [\n {\n \"selected\": true,\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n {\n \"selected\": false,\n \"text\": \"0.95\",\n \"value\": \"0.95\"\n },\n {\n \"selected\": false,\n \"text\": \"0.99\",\n \"value\": \"0.99\"\n }\n ],\n \"query\": \"0.9,0.95,0.99\",\n \"type\": \"custom\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-5m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Http Server\",\n \"uid\": \"aerbb811lwveod\",\n \"version\": 4\n}\n", - "http_client.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"id\": 15005,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 15,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", operation=~\\\"$operation\\\", status_code=~\\\"2.*\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Client requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 9,\n \"x\": 15,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", operation=~\\\"$operation\\\", status_code!~\\\"2.*\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} {{job}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 11\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\", operation=~\\\"$operation\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} {{job}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Client Requests Timing quantile 0.95 \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 19\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"daml_http_client_requests_inflight{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\", operation=~\\\"$operation\\\"}\\n\",\n \"legendFormat\": \"{{namespace}} {{job}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"In Flight Client Requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"validator1\"\n ],\n \"value\": [\n \"validator1\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\"},operation)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"operation\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\"},operation)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_client)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"http_client\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_client)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n \"name\": \"percentile\",\n \"options\": [\n {\n \"selected\": true,\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n {\n \"selected\": false,\n \"text\": \"0.95\",\n \"value\": \"0.95\"\n },\n {\n \"selected\": false,\n \"text\": \"0.99\",\n \"value\": \"0.99\"\n }\n ],\n \"query\": \"0.9,0.95,0.99\",\n \"type\": \"custom\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Http Client\",\n \"uid\": \"a8a3113a-bccf-4728-b752-dd7a5d6f9bda\",\n \"version\": 3\n}\n", - "rate_limiters.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 510,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\", result!=\\\"accepted\\\"}[$__rate_interval])) by (result, limiter)\",\n \"instant\": false,\n \"legendFormat\": \"{{result}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejections\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n }\n },\n \"mappings\": [],\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"displayLabels\": [\n \"name\",\n \"percent\"\n ],\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"values\": [\n \"percent\",\n \"value\"\n ]\n },\n \"pieType\": \"pie\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(increase(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\"}[$__range])) by (result)\",\n \"instant\": true,\n \"legendFormat\": \"{{result}}\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Requests by Result\",\n \"type\": \"piechart\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 24,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\"}[$__rate_interval])) by (limiter)\",\n \"instant\": false,\n \"legendFormat\": \"{{limiter}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_rate_limiting_max_limit_per_second\",\n \"hide\": false,\n \"instant\": false,\n \"legendFormat\": \"{{limiter}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Total Requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"1m\",\n \"schemaVersion\": 41,\n \"tags\": [\n \"prometheus\",\n \"rate-limiting\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total, namespace)\",\n \"includeAll\": false,\n \"label\": \"Namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(splice_rate_limiting_total, namespace)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"scan-app\"\n ],\n \"value\": [\n \"scan-app\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\"},node_name)\",\n \"includeAll\": true,\n \"label\": \"Node Name\",\n \"multi\": true,\n \"name\": \"node_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\"},node_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"sort\": 3,\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\"},http_service)\",\n \"includeAll\": true,\n \"label\": \"HTTP Service\",\n \"multi\": true,\n \"name\": \"http_service\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\"},http_service)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", node_name=~\\\"$node_name\\\"},limiter)\",\n \"includeAll\": true,\n \"label\": \"Limiter\",\n \"multi\": true,\n \"name\": \"limiter\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", node_name=~\\\"$node_name\\\"},limiter)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"sort\": 2,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Splice Rate Limiting\",\n \"uid\": \"splice-rate-limit-db\",\n \"version\": 3\n}\n" + "http_client.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", operation=~\\\"$operation\\\", status_code=~\\\"2.*\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Client requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\", operation=~\\\"$operation\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Client Requests Timing quantile 0.95 \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 15,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", operation=~\\\"$operation\\\", status_code!~\\\"2.*\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} {{job}} {{http_client}} {{target_host}} {{operation}} {{status}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"status_code\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 125\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"target_host\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 375\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 9,\n \"x\": 15,\n \"y\": 17\n },\n \"id\": 5,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"Value\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"builder\",\n \"exemplar\": false,\n \"expr\": \"sum by(namespace, job, http_client, operation, status_code, target_host) (histogram_count(increase(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", operation=~\\\"$operation\\\", status_code!~\\\"2.*\\\"}[$__range])))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true,\n \"endpoint\": true,\n \"instance\": true\n },\n \"includeByName\": {},\n \"indexByName\": {\n \"Time\": 2,\n \"Value\": 15,\n \"endpoint\": 3,\n \"http_client\": 4,\n \"instance\": 5,\n \"job\": 1,\n \"namespace\": 0,\n \"node_name\": 6,\n \"node_type\": 7,\n \"operation\": 8,\n \"otel_scope_name\": 9,\n \"pod\": 10,\n \"service\": 11,\n \"status\": 12,\n \"status_code\": 13,\n \"target_host\": 14\n },\n \"renameByName\": {}\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 28\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"daml_http_client_requests_inflight{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\", operation=~\\\"$operation\\\"}\\n\",\n \"legendFormat\": \"{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"In Flight Client Requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"validator1\"\n ],\n \"value\": [\n \"validator1\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\"},operation)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"operation\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\"},operation)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_client)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"http_client\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_client)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n \"name\": \"percentile\",\n \"options\": [],\n \"query\": \"0.9,0.95,0.99\",\n \"type\": \"custom\",\n \"valuesFormat\": \"csv\"\n },\n {\n \"baseFilters\": [],\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"filters\": [],\n \"name\": \"filter\",\n \"type\": \"adhoc\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-15m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Http Client\",\n \"uid\": \"a8a3113a-bccf-4728-b752-dd7a5d6f9bda\",\n \"version\": 3,\n \"weekStart\": \"\"\n}\n", + "istio.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of HTTP 429 (Too Many Requests) responses per second, broken down by the namespace reporting the request and the destination service receiving it.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, destination_service) (rate(istio_requests_total{response_code=\\\"429\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Istio total rejected requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Fraction of requests evaluated by the Envoy local rate limit filter that were rejected with HTTP 429, per namespace and pod.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percent\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\\n/\\nsum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enabled{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"legendFormat\": \"{{namespace}} - {{pod}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejection ratio\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of number of requests for which rate limiting was applied (e.g.: 429 returned)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 8\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\",\n \"legendFormat\": \"{{namespace}} - {{pod}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Enforced requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for responses without an available token (but not necessarily enforced)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 8\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_rate_limited{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace,job)\",\n \"legendFormat\": \"{{namespace}} - {{job}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejected requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for number of requests for which the rate limiter was consulted\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 16\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enabled{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\",\n \"legendFormat\": \"{{namespace}} - {{pod}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Enabled requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for number of requests under limit responses from the token bucket\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 16\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_ok{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\",\n \"legendFormat\": \"{{namespace}} - {{pod}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Allowed requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Istio Rate Limiting\",\n \"uid\": \"cnr56dj\",\n \"version\": 3,\n \"weekStart\": \"\"\n}\n", + "rate_limiters.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\", result!=\\\"accepted\\\"}[$__rate_interval])) by (result)\",\n \"instant\": false,\n \"legendFormat\": \"{{ result }}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Rejections\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n }\n },\n \"mappings\": [],\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"displayLabels\": [\n \"name\",\n \"percent\"\n ],\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"values\": [\n \"percent\",\n \"value\"\n ]\n },\n \"pieType\": \"pie\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"sort\": \"desc\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(increase(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\"}[$__range])) by (result)\",\n \"instant\": true,\n \"legendFormat\": \"{{result}}\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Requests by Result\",\n \"type\": \"piechart\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 8\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\"}[$__rate_interval])) by (limiter, limiter_type, limiter_attribute)\",\n \"instant\": false,\n \"legendFormat\": \"{{ limiter }} {{limiter_type}} {{limiter_attribute}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\", result!=\\\"accepted\\\"}[$__rate_interval])) by (result, limiter, limiter_type, limiter_attribute)\",\n \"instant\": false,\n \"legendFormat\": \"{{ limiter }} {{limiter_type}} {{limiter_attribute}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejections\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_rate_limiting_max_limit_per_second{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\"}\",\n \"instant\": false,\n \"legendFormat\": \"{{ limiter }} {{limiter_type}}{{limiter_attribute}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Limits\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Cannot extract the attribute value\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 33\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(rate_limiting_unknown_attribute_not_limited{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\"}[$__rate_interval])) by (limiter, limiter_type, limiter_attribute)\",\n \"instant\": false,\n \"legendFormat\": \"{{ limiter }} {{limiter_type}} {{limiter_attribute}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Attribute Limits not enforced\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"1m\",\n \"schemaVersion\": 42,\n \"tags\": [\n \"prometheus\",\n \"rate-limiting\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total, namespace)\",\n \"includeAll\": false,\n \"label\": \"Namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(splice_rate_limiting_total, namespace)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"scan-app\"\n ],\n \"value\": [\n \"scan-app\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\"},node_name)\",\n \"includeAll\": true,\n \"label\": \"Node Name\",\n \"multi\": true,\n \"name\": \"node_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\"},node_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 3,\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\"},http_service)\",\n \"includeAll\": true,\n \"label\": \"HTTP Service\",\n \"multi\": true,\n \"name\": \"http_service\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\"},http_service)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", node_name=~\\\"$node_name\\\"},limiter)\",\n \"includeAll\": true,\n \"label\": \"Limiter\",\n \"multi\": true,\n \"name\": \"limiter\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", node_name=~\\\"$node_name\\\"},limiter)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 2,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Splice Rate Limiting\",\n \"uid\": \"splice-rate-limit-db\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n" }, "kind": "ConfigMap", "metadata": { @@ -346,11 +352,11 @@ "inputs": { "apiVersion": "v1", "data": { - "acs-size.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 3573,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Trend #A\"\n },\n \"properties\": [\n {\n \"id\": \"displayName\",\n \"value\": \"ACS size\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 26,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"footer\": {\n \"countRows\": false,\n \"fields\": \"\",\n \"reducer\": [\n \"sum\"\n ],\n \"show\": false\n },\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"ACS size\"\n }\n ]\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum by (namespace, store_name, store_party) (splice_store_acs_size{namespace=~\\\"$namespace\\\",store_name=~\\\"$store_name\\\"})\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"ACS Size\",\n \"transformations\": [\n {\n \"id\": \"timeSeriesTable\",\n \"options\": {\n \"A\": {\n \"timeField\": \"Time\"\n }\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\"\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 20,\n \"w\": 24,\n \"x\": 0,\n \"y\": 26\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.0.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum without (__name__,endpoint,instance,job,migration,otel_scope) (splice_store_acs_size{namespace=~\\\"$namespace\\\",store_name=~\\\"$store_name\\\"})\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Splice Store ACS Size\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_store_acs_size,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_store_acs_size,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_store_acs_size,store_name)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"store_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_store_acs_size,store_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Splice Store ACS Size\",\n \"uid\": \"dduss3xr5or28c\",\n \"version\": 1\n}\n", + "acs-size.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Number of rows in the latest saved ACS snapshot, as reported by the SV Scan app. This is the metric behind the 'ACS growth' alert. Note that it counts rows, not contracts: contracts with multiple stakeholders are counted multiple times. A value of -1 means the size of the last snapshot save is unknown.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_history_acs_snapshots_snapshot_size{namespace=~\\\"$namespace\\\"}\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"ACS size (rows in latest saved snapshot)\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 20,\n \"w\": 24,\n \"x\": 0,\n \"y\": 10\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"(increase(splice_store_acs_size_increase_total{namespace=~\\\"$namespace\\\",store_name=~\\\"$store_name\\\"}[24h]))-(increase(splice_store_acs_size_decrease_total{namespace=~\\\"$namespace\\\",store_name=~\\\"$store_name\\\"}[24h]))\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{store_name}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Splice Store ACS Change in last 24h\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_store_acs_size_increase_total,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_store_acs_size_increase_total,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_store_acs_size_increase_total,store_name)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"store_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_store_acs_size_increase_total,store_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"ACS Size\",\n \"uid\": \"dduss3xr5or28c\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n", "acs-snapshots.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 5,\n \"panels\": [],\n \"title\": \"Incremental ACS snapshot generation\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"splice_history_acs_snapshots_latest_record_time_update{namespace=~\\\"$namespace\\\"} / 1000\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Record time of last update\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"splice_history_acs_snapshots_latest_record_time_save{namespace=~\\\"$namespace\\\"} / 1000\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Record time of last save\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to update an incremental snapshot, i.e., the time to update an incremental snapshot by processing ~30sec worth of update history.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"builder\",\n \"expr\": \"histogram_avg(rate(splice_history_acs_snapshots_latency_update_duration_seconds{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Update latency\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Time to save an incremental snapshot, i.e., the time to copy an incremental snapshot to the the big table containing all historical snapshots.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"builder\",\n \"expr\": \"histogram_avg(rate(splice_history_acs_snapshots_latency_save_duration_seconds{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Save latency\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_history_acs_snapshots_latest_record_time_update,namespace)\",\n \"includeAll\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_history_acs_snapshots_latest_record_time_update,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"ACS snapshots\",\n \"uid\": \"cnqnrp4\",\n \"version\": 3,\n \"weekStart\": \"\"\n}\n", "bulk-storage.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"min by(namespace)((splice_history_bulk_storage_latest_acs_snapshot{namespace=~\\\"$namespace\\\"} >-62135596800000) / 1000.0)\",\n \"legendFormat\": \"{{namespace}} ACS Snapshots\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"min by(namespace)((splice_history_bulk_storage_latest_updates_segment{namespace=~\\\"$namespace\\\"} >-62135596800000) / 1000.0)\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} Updates\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Last record time stored\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"splice_history_bulk_storage_object_count{namespace=~\\\"$namespace\\\"}\",\n \"legendFormat\": \"{{namespace}} {{object_type}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Num S3 objects stored\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_history_bulk_storage_updates_count,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_history_bulk_storage_updates_count,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-2d\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Bulk Storage\",\n \"uid\": \"cnxl9bn\",\n \"version\": 3,\n \"weekStart\": \"\"\n}\n", "history-backfilling.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 1387,\n \"links\": [],\n \"panels\": [\n {\n \"fieldConfig\": {\n \"defaults\": {},\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 7,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 21,\n \"options\": {\n \"code\": {\n \"language\": \"plaintext\",\n \"showLineNumbers\": false,\n \"showMiniMap\": false\n },\n \"content\": \"There are three backfilling background tasks:\\n\\n1. `ScanHistoryBackfillingTrigger` backfills regular updates\\n - Data is loaded from peer scan apps using BFT network calls\\n1. `ScanHistoryBackfillingTrigger` backfills import updates\\n - Data is loaded from peer scan apps using BFT network calls\\n1. `TxLogBackfillingTrigger` backfills txlog entries\\n - Data is loaded from the local UpdateHistory\\n\\nThe tasks are completed sequentially in the above order.\\nE.g., txlog backfilling won't start until update history backfilling is complete.\",\n \"mode\": \"markdown\"\n },\n \"pluginVersion\": \"12.1.1\",\n \"title\": \"Overview\",\n \"type\": \"text\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 7\n },\n \"id\": 7,\n \"panels\": [],\n \"title\": \"Updates (scan)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 8\n },\n \"id\": 17,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(splice_history_backfilling_transaction_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"})\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Backfilled transactions count\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 8\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace)(rate(splice_history_backfilling_transaction_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"}[5m]))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate backfilled transactions\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 16\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(splice_history_backfilling_event_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"})\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Backfilled events count\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 16\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(rate(splice_history_backfilling_event_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"}[5m]))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate backfilled events\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"axisPlacement\": \"auto\",\n \"fillOpacity\": 70,\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineWidth\": 0,\n \"spanNulls\": false\n },\n \"mappings\": [],\n \"max\": 1,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"red\",\n \"value\": 0\n },\n {\n \"color\": \"green\",\n \"value\": 1\n }\n ]\n },\n \"unit\": \"bool_yes_no\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 24\n },\n \"id\": 15,\n \"options\": {\n \"alignValue\": \"left\",\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"mergeValues\": true,\n \"rowHeight\": 0.9,\n \"showValue\": \"auto\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(splice_history_backfilling_completed{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"})\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Backfilling completed\",\n \"type\": \"state-timeline\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"unit\",\n \"value\": \"dateTimeAsIso\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 24\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"min by(namespace)((splice_history_backfilling_latest_record_time{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"}>-62135596800000)/1000)\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"Value\"\n }\n ],\n \"title\": \"Last record time backfilled\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 32\n },\n \"id\": 16,\n \"panels\": [],\n \"title\": \"Import Updates (scan)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 33\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(splice_history_import_updates_backfilling_contract_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"})\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Backfilled import contracts count\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 33\n },\n \"id\": 20,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace)(rate(splice_history_import_updates_backfilling_contract_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"}[5m]))\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate backfilled contracts\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"axisPlacement\": \"auto\",\n \"fillOpacity\": 70,\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineWidth\": 0,\n \"spanNulls\": false\n },\n \"mappings\": [],\n \"max\": 1,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"red\",\n \"value\": 0\n },\n {\n \"color\": \"green\",\n \"value\": 1\n }\n ]\n },\n \"unit\": \"bool_yes_no\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 41\n },\n \"id\": 19,\n \"options\": {\n \"alignValue\": \"left\",\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"mergeValues\": true,\n \"rowHeight\": 0.9,\n \"showValue\": \"auto\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(splice_history_import_updates_backfilling_completed{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"})\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Import update backfilling completed\",\n \"type\": \"state-timeline\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"unit\",\n \"value\": \"\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 41\n },\n \"id\": 18,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"min by(namespace)((splice_history_import_updates_backfilling_latest_migration_id{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\"}>-1))\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"Value\"\n }\n ],\n \"title\": \"Last migration id backfilled\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": false,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 49\n },\n \"id\": 8,\n \"panels\": [],\n \"title\": \"TxLog entries (all apps)\",\n \"type\": \"row\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 50\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(splice_history_txlog_backfilling_transaction_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\", service=~\\\"$service\\\"})\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{service}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Processed transactions count\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 50\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by (namespace)(rate(splice_history_txlog_backfilling_transaction_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\", service=~\\\"$service\\\"}[5m]))\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{service}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate processed transactions\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 58\n },\n \"id\": 11,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(splice_history_txlog_backfilling_event_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\", service=~\\\"$service\\\"})\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{service}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Processed events count\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 58\n },\n \"id\": 12,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace)(rate(splice_history_txlog_backfilling_event_count{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\", service=~\\\"$service\\\"}[5m]))\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}}{{service}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rate processed events\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"axisPlacement\": \"auto\",\n \"fillOpacity\": 70,\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineWidth\": 0,\n \"spanNulls\": false\n },\n \"mappings\": [],\n \"max\": 1,\n \"min\": 0,\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"red\",\n \"value\": 0\n },\n {\n \"color\": \"green\",\n \"value\": 1\n }\n ]\n },\n \"unit\": \"bool_yes_no\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 12,\n \"x\": 0,\n \"y\": 66\n },\n \"id\": 13,\n \"options\": {\n \"alignValue\": \"left\",\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"mergeValues\": true,\n \"rowHeight\": 0.9,\n \"showValue\": \"auto\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"max by(namespace, service) (splice_history_txlog_backfilling_completed{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\", service=~\\\"$service\\\"})\",\n \"format\": \"time_series\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{service}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"TxLog backfilling completed\",\n \"type\": \"state-timeline\"\n },\n {\n \"datasource\": {\n \"default\": true,\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"Value\"\n },\n \"properties\": [\n {\n \"id\": \"unit\",\n \"value\": \"dateTimeAsIso\"\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 12,\n \"x\": 12,\n \"y\": 66\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"min by(namespace)((splice_history_txlog_backfilling_latest_record_time{namespace=~\\\"$namespace\\\", migration=~\\\"$migration\\\", service=~\\\"$service\\\"}>-62135596800000)/1000)\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"legendFormat\": \"{{namespace}} {{service}}\",\n \"range\": true,\n \"refId\": \"Value\"\n }\n ],\n \"title\": \"Last record time backfilled\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_history_txlog_backfilling_latest_record_time,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_history_txlog_backfilling_latest_record_time,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_history_txlog_backfilling_latest_record_time,migration)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"migration\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_history_txlog_backfilling_latest_record_time,migration)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": \"$__all\"\n },\n \"definition\": \"label_values(splice_history_txlog_backfilling_completed,service)\",\n \"includeAll\": true,\n \"name\": \"service\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_history_txlog_backfilling_completed,service)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"History backfiling\",\n \"uid\": \"fe12ejqgwdlvke\",\n \"version\": 4\n}\n", - "mediator-verdicts-ingestion.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of ingestion of mediator verdicts per second based on ingestion_count_total\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"rate(splice_scan_verdict_ingestion_count_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", pod=~\\\"$pod_name\\\"}[$__rate_interval])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{pod}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Verdicts Ingestion Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested verdict record time and the current wall clock time.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\", pod_name=~\\\"$pod_name\\\"}) - (splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\", pod_name=~\\\"$pod_name\\\"} > 0) / 1e6)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{pod}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Last Ingested Verdict Time Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\", pod_name=~\\\"$pod_name\\\"} / 1000 unless (splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\", pod_name=~\\\"$pod_name\\\"} / 1000 == 0)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{pod}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Last Ingested Verdict Record Time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Average and 99th quantile batch size.\\n\\nVerdicts are streamed in from the mediator, then batched in the scan app. For each batch we do one lookup for traffic summaries in the sequencer, and submit one SQL transaction to store all app activity data.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(rate(splice_scan_verdict_ingestion_batch_size{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} avg\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.99, rate(splice_scan_verdict_ingestion_batch_size{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} 99th\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Batch size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Average and 99th percentile time to ingest one batch.\\n\\nIncludes data processing and waiting for the SQL transaction to complete.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(rate(splice_scan_verdict_ingestion_latency_duration_seconds{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} avg\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.99, rate(splice_scan_verdict_ingestion_latency_duration_seconds{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} 99th\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Batch latency\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"definition\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,node_name)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"node_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,node_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,pod)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"pod_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,pod)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Mediator Verdicts Ingestion\",\n \"uid\": \"86f70b02-f283-469e-bf90-033dc9f55888\",\n \"version\": 10,\n \"weekStart\": \"\"\n}\n", + "mediator-verdicts-ingestion.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of ingestion of mediator verdicts per second based on ingestion_count_total\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"rate(splice_scan_verdict_ingestion_count_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", pod=~\\\"$pod_name\\\"}[$__rate_interval])\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{pod}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Verdicts Ingestion Rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested verdict record time and the current wall clock time.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\", pod_name=~\\\"$pod_name\\\"}) - (splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\", pod_name=~\\\"$pod_name\\\"} > 0) / 1e6)\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{pod}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Last Ingested Verdict Time Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 9\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"(splice_scan_verdict_ingestion_last_record_time_us{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\", pod_name=~\\\"$pod_name\\\"} > 0) / 1e3\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{pod}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Last Ingested Verdict Record Time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Average and 99th quantile batch size.\\n\\nVerdicts are streamed in from the mediator, then batched in the scan app. For each batch we do one lookup for traffic summaries in the sequencer, and submit one SQL transaction to store all app activity data.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(rate(splice_scan_verdict_ingestion_batch_size{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} avg\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.99, rate(splice_scan_verdict_ingestion_batch_size{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} 99th\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Batch size\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Average and 99th percentile time to ingest one batch.\\n\\nIncludes data processing and waiting for the SQL transaction to complete.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(rate(splice_scan_verdict_ingestion_latency_duration_seconds{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} avg\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.99, rate(splice_scan_verdict_ingestion_latency_duration_seconds{namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} 99th\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Batch latency\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"definition\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,node_name)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"node_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,node_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,pod)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"pod_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_scan_verdict_ingestion_last_record_time_us,pod)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Mediator Verdicts Ingestion\",\n \"uid\": \"86f70b02-f283-469e-bf90-033dc9f55888\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", "store-ingestion-performance.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": { \"type\": \"grafana\", \"uid\": \"-- Grafana --\" },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 2,\n \"id\": null,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": { \"h\": 1, \"w\": 24, \"x\": 0, \"y\": 0 },\n \"id\": 1,\n \"repeat\": \"test\",\n \"repeatDirection\": \"h\",\n \"title\": \"$test\",\n \"type\": \"row\"\n },\n {\n \"title\": \"Total Items\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 0, \"y\": 1 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"short\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"green\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_ingestion_total_items{test=\\\"${test}\\\"}\",\n \"legendFormat\": \"Total Items\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"Total Batches\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 12, \"y\": 1 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"short\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"orange\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_ingestion_total_batches{test=\\\"${test}\\\"}\",\n \"legendFormat\": \"Total Batches\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"Total Time\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 0, \"y\": 9 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"s\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"purple\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_ingestion_total_time_ns{test=\\\"${test}\\\"} / 1e9\",\n \"legendFormat\": \"Total Time (s)\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"Avg Item Time\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 12, \"y\": 9 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"ms\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"blue\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_ingestion_avg_item_time_ns{test=\\\"${test}\\\"} / 1e6\",\n \"legendFormat\": \"Avg Item Time (ms)\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"CPU-to-Wall-Clock Ratio\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 0, \"y\": 17 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"none\",\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n },\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n { \"color\": \"blue\", \"value\": null },\n { \"color\": \"green\", \"value\": 0.5 },\n { \"color\": \"orange\", \"value\": 1.0 },\n { \"color\": \"red\", \"value\": 2.0 }\n ]\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_ingestion_cpu_to_wall_clock_ratio{test=\\\"${test}\\\"}\",\n \"legendFormat\": \"CPU/Wall-clock Ratio (>0.7: CPU-bound, 0.7~0.25: balanced, <0.25:I/O-bound)\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"Peak Heap Memory\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 12, \"y\": 17 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"decbytes\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"semi-dark-purple\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_ingestion_peak_heap_bytes{test=\\\"${test}\\\"}\",\n \"legendFormat\": \"Peak Heap\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": { \"text\": \"All\", \"value\": \"$__all\" },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"definition\": \"label_values(splice_perf_ingestion_total_items, test)\",\n \"hide\": 2,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"test\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_perf_ingestion_total_items, test)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": { \"from\": \"now-30d\", \"to\": \"now\" },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Store Ingestion Performance\",\n \"uid\": \"splice-perf-ingestion\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", "store-ingestion.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"id\": 1141,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.95, sum by(le, namespace,job,store_name,store_party,synchronizer_id) (rate(splice_store_batch_ingestion_latency_duration_seconds{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", store_name=~\\\"$store_name\\\", store_party=~\\\"$store_party\\\"}[$__rate_interval])))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"95% - {{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(rate(splice_store_batch_ingestion_latency_duration_seconds{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", store_name=~\\\"$store_name\\\", store_party=~\\\"$store_party\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"hide\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"avg - {{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"B\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Ingestion timing\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 14,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(splice_store_batch_ingestion_latency_duration_seconds{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\",store_name=~\\\"$store_name\\\",store_party=~\\\"$store_party\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Ingestion rate\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 14\n },\n \"id\": 15,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile(0.99, sum by (le, namespace,job,store_name,store_party,synchronizer_id) (rate(splice_store_ingestion_batch_size{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", store_name=~\\\"$store_name\\\", store_party=~\\\"$store_party\\\"}[$__rate_interval])))\",\n \"legendFormat\": \"99% - {{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"histogram_avg(sum by (le, namespace,job,store_name,store_party,synchronizer_id) (rate(splice_store_ingestion_batch_size{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", store_name=~\\\"$store_name\\\", store_party=~\\\"$store_party\\\"}[$__rate_interval])))\",\n \"hide\": false,\n \"instant\": false,\n \"legendFormat\": \"avg - {{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Batch size\",\n \"type\": \"timeseries\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 23\n },\n \"id\": 12,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"How many events (created / archived) were ingested per $rate_interval\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 12,\n \"x\": 0,\n \"y\": 77\n },\n \"id\": 14,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_store_event_count{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", store_name=~\\\"$store_name\\\", store_party=~\\\"$store_party\\\", event_type=\\\"archived\\\"}[$__rate_interval])\",\n \"hide\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_store_event_count{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", store_name=~\\\"$store_name\\\", store_party=~\\\"$store_party\\\", event_type=\\\"created\\\"}[$__rate_interval])\",\n \"hide\": false,\n \"instant\": false,\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"B\"\n }\n ],\n \"title\": \"Ingestion rate events\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Events\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 24\n },\n \"id\": 7,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last ingested record time and the current wallclock time.\\n\\nNote that the last ingested record time metric only updates when the store ingests a new transaction so if there is no activity, the last ingested record time will not advance. For a party performing reward collection, e.g., the validator operator party you expect at least one transaction every round so the lag should not go above 20min.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\",store_name=~\\\"$store_name\\\",store_party=~\\\"$store_party\\\"}) - (splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\",store_name=~\\\"$store_name\\\",store_party=~\\\"$store_party\\\"} > 0) / 1e3)\",\n \"hide\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Last Ingested Record Time Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The last ingested record time for each store.\\n\\nNote that the last ingested record time metric only updates when the store ingests a new transaction so if no transaction is ingested the last ingested time will not advance. For a party performing reward collection, e.g., the validator operator party you expect at least one transaction every round so the last ingested time should be at most 20min ago.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 25\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"asc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_store_last_ingested_record_time_ms{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\",store_name=~\\\"$store_name\\\",store_party=~\\\"$store_party\\\"}\",\n \"legendFormat\": \"{{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Last Ingested Record Time\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The time difference between the last seen record time and the current wallclock time.\\n\\nThe last seen record time updates regardless of whether an update was ingested or filtered out.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 0,\n \"y\": 38\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"(timestamp(splice_store_last_seen_record_time_ms{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\",store_name=~\\\"$store_name\\\",store_party=~\\\"$store_party\\\"}) - (splice_store_last_seen_record_time_ms{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\",store_name=~\\\"$store_name\\\",store_party=~\\\"$store_party\\\"} > 0) / 1e3)\",\n \"hide\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Last Seen Record Time Lag\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"The most recent record time seen by this store for each synchronizer in milliseconds.\\n\\nThis updates for every entry seen, regardless of whether it was ingested or filtered out.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 13,\n \"w\": 12,\n \"x\": 12,\n \"y\": 38\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"asc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_store_last_seen_record_time_ms{namespace=~\\\"$namespace\\\",node_name=~\\\"$node_name\\\",store_name=~\\\"$store_name\\\",store_party=~\\\"$store_party\\\"}\",\n \"legendFormat\": \"{{namespace}} {{job}} {{store_name}} {{store_party}} {{synchronizer_id}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Last Seen Record Time\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"Record time metrics\",\n \"type\": \"row\"\n },\n {\n \"collapsed\": true,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 9,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"How many txlog entries were ingested per $rate_interval\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 102\n },\n \"id\": 10,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"rate(splice_store_ingested_tx_log_entries_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", store_name=~\\\"$store_name\\\", store_party=~\\\"$store_party\\\"}[$__rate_interval])\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Ingestion rate txlog entries\",\n \"type\": \"timeseries\"\n }\n ],\n \"title\": \"TxLog\",\n \"type\": \"row\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"1m\",\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": [\n \"sv-1\"\n ],\n \"value\": [\n \"sv-1\"\n ]\n },\n \"definition\": \"label_values(splice_store_last_ingested_record_time_ms,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_store_last_ingested_record_time_ms,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(splice_store_last_ingested_record_time_ms,node_name)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"node_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_store_last_ingested_record_time_ms,node_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(splice_store_last_ingested_record_time_ms,store_name)\",\n \"description\": \"The name of the store\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"store_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_store_last_ingested_record_time_ms,store_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allValue\": \".*\",\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(splice_store_last_ingested_record_time_ms,store_party)\",\n \"description\": \"The party that the store is ingesting data for\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"store_party\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_store_last_ingested_record_time_ms,store_party)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Splice Stores Ingestion\",\n \"uid\": \"dec17fpqzdwcge\",\n \"version\": 10\n}\n", "store-read-performance.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": { \"type\": \"grafana\", \"uid\": \"-- Grafana --\" },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 2,\n \"id\": null,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"gridPos\": { \"h\": 1, \"w\": 24, \"x\": 0, \"y\": 0 },\n \"id\": 1,\n \"repeat\": \"test\",\n \"repeatDirection\": \"h\",\n \"title\": \"$test\",\n \"type\": \"row\"\n },\n {\n \"title\": \"Num Updates\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 0, \"y\": 1 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"none\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"green\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_read_num_updates{test=\\\"${test}\\\"}\",\n \"legendFormat\": \"Num Updates\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"Update Size\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 12, \"y\": 1 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"decbytes\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"blue\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_read_update_size_bytes{test=\\\"${test}\\\"}\",\n \"legendFormat\": \"Update Size\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"Total Read Time\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 0, \"y\": 9 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"s\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"purple\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_read_total_time_ns{test=\\\"${test}\\\"} / 1e9\",\n \"legendFormat\": \"Total Read Time\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"CPU-to-Wall-Clock Ratio\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 12, \"y\": 9 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"none\",\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n },\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n { \"color\": \"blue\", \"value\": null },\n { \"color\": \"green\", \"value\": 0.5 },\n { \"color\": \"orange\", \"value\": 1.0 },\n { \"color\": \"red\", \"value\": 2.0 }\n ]\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_read_cpu_to_wall_clock_ratio{test=\\\"${test}\\\"}\",\n \"legendFormat\": \"CPU/Wall-clock Ratio\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n },\n {\n \"title\": \"Peak Heap Memory\",\n \"type\": \"timeseries\",\n \"gridPos\": { \"h\": 8, \"w\": 12, \"x\": 0, \"y\": 17 },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"fieldConfig\": {\n \"defaults\": {\n \"unit\": \"decbytes\",\n \"color\": { \"mode\": \"fixed\", \"fixedColor\": \"semi-dark-purple\" },\n \"custom\": {\n \"drawStyle\": \"line\",\n \"lineWidth\": 2,\n \"pointSize\": 6,\n \"showPoints\": \"always\",\n \"fillOpacity\": 5,\n \"spanNulls\": false\n }\n },\n \"overrides\": []\n },\n \"options\": {\n \"legend\": { \"calcs\": [\"mean\", \"max\", \"lastNotNull\"], \"displayMode\": \"table\", \"placement\": \"bottom\", \"showLegend\": true },\n \"tooltip\": { \"mode\": \"single\", \"sort\": \"none\" }\n },\n \"targets\": [\n {\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"expr\": \"splice_perf_read_peak_heap_bytes{test=\\\"${test}\\\"}\",\n \"legendFormat\": \"Peak Heap\",\n \"range\": true, \"refId\": \"A\"\n }\n ]\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": { \"text\": \"All\", \"value\": \"$__all\" },\n \"datasource\": { \"type\": \"prometheus\", \"uid\": \"prometheus\" },\n \"definition\": \"label_values(splice_perf_read_total_time_ns, test)\",\n \"hide\": 2,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"test\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_perf_read_total_time_ns, test)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": { \"from\": \"now-30d\", \"to\": \"now\" },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Store Read Performance\",\n \"uid\": \"splice-perf-read\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", @@ -580,7 +586,7 @@ "metricRelabelings": [ { "action": "keep", - "regex": "istio_.*", + "regex": "(istio_.*|envoy_.*http_local_rate_limit_.*)", "sourceLabels": [ "__name__" ] @@ -696,7 +702,7 @@ "id": "", "inputs": { "description": "Logs with ClusterUpdate events", - "filter": "\nresource.labels.cluster_name=\"cn-mocknet\"\nresource.type=~\"(gke_cluster|gke_nodepool)\"\njsonPayload.state=~\"STARTED\"", + "filter": "\nresource.labels.cluster_name=\"cn-mocknet\"\nresource.type=~\"(gke_cluster|gke_nodepool)\"\njsonPayload.@type=~\"UpgradeEvent\"", "labelExtractors": { "cluster": "EXTRACT(resource.labels.cluster_name)" }, @@ -799,7 +805,7 @@ "id": "", "inputs": { "description": "Logs containing secrets (JWTs, Bearer tokens, passwords, etc.)", - "filter": "resource.labels.cluster_name=\"cn-mocknet\"\n(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n", + "filter": "resource.labels.cluster_name=\"cn-mocknet\"\n(\n (\n jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=[^, ]+\" AND\n -jsonPayload.message=~\"(?i)(secret|token|(private|secret)(-)?key|password)=(\\\"\\*\\*\\*\\*\\\"|hidden)\" AND\n -jsonPayload.message=~\"(?i)page(_|-)?token=[^, ]+\"\n ) OR\n jsonPayload.message=~\"eyJhbGc[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\\.[A-Za-z0-9_-]{2,}\" OR\n jsonPayload.message=~\"Bearer\\s+eyJ[A-Za-z0-9_-]{2,}\"\n)\n", "labelExtractors": { "cluster": "EXTRACT(resource.labels.cluster_name)", "namespace": "EXTRACT(resource.labels.namespace_name)" @@ -910,7 +916,7 @@ "conditionPrometheusQueryLanguage": { "duration": "0s", "evaluationInterval": "30s", - "query": "sum by (nat_gateway_name) (router_googleapis_com:nat_nat_allocation_failed{monitored_resource=\"nat_gateway\"}) > 0" + "query": "sum by (gateway_name) (router_googleapis_com:nat_nat_allocation_failed{monitored_resource=\"nat_gateway\", gateway_name=~\"nat-mock-gw.*\"}) > 0" }, "displayName": "NAT allocation failed in mock" } @@ -951,9 +957,9 @@ "conditions": [ { "conditionPrometheusQueryLanguage": { - "duration": "0s", + "duration": "1200s", "evaluationInterval": "30s", - "query": "sum by (nat_gateway_name, reason) (router_googleapis_com:nat_dropped_sent_packets_count{monitored_resource=\"nat_gateway\"}) > 0" + "query": "sum by (gateway_name, reason) (router_googleapis_com:nat_dropped_sent_packets_count{monitored_resource=\"nat_gateway\", gateway_name=~\"nat-mock-gw.*\"}) > 30" }, "displayName": "NAT dropped sent packets in mock" } @@ -996,7 +1002,7 @@ "conditionPrometheusQueryLanguage": { "duration": "0s", "evaluationInterval": "30s", - "query": "sum by (nat_gateway_name) ((router_googleapis_com:nat_port_usage{monitored_resource=\"nat_gateway\"} / 64512) * 100) > 80" + "query": "sum by (gateway_name) ((router_googleapis_com:nat_port_usage{monitored_resource=\"nat_gateway\", gateway_name=~\"nat-mock-gw.*\"} / 64512) * 100) > 80" }, "displayName": "NAT port usage high in mock" } diff --git a/cluster/expected/operator/expected.json b/cluster/expected/operator/expected.json index 9b32ac2a92..aab916cbe2 100644 --- a/cluster/expected/operator/expected.json +++ b/cluster/expected/operator/expected.json @@ -485,7 +485,7 @@ } }, "name": "operator-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-operator-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-operator-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -501,7 +501,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -575,16 +575,6 @@ "provider": "", "type": "kubernetes:core/v1:Namespace" }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, { "custom": true, "id": "", @@ -710,7 +700,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "interval": "5m", "recurseSubmodules": true, "ref": { @@ -740,7 +730,7 @@ "namespace": "operator" }, "spec": { - "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard", + "ignore": "**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**", "include": [ { "fromPath": "cluster/stacks/prod/deployment/Pulumi.deployment.mock.yaml", diff --git a/cluster/expected/splitwell/expected.json b/cluster/expected/splitwell/expected.json index f1dacdc6aa..690ed04a87 100644 --- a/cluster/expected/splitwell/expected.json +++ b/cluster/expected/splitwell/expected.json @@ -227,16 +227,6 @@ "provider": "", "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, { "custom": true, "id": "", @@ -501,7 +491,7 @@ } }, "name": "splitwell-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-splitwell-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-splitwell-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -517,7 +507,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -526,99 +516,6 @@ "provider": "", "type": "kubernetes:core/v1:Secret" }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-participant", - "compat": "true", - "maxHistory": 10, - "name": "participant-2", - "namespace": "splitwell", - "timeout": 600, - "values": { - "additionalEnvVars": [ - { - "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", - "value": "# Ignore missing ACS commitment and commitment mismatches\ncanton.participants.participant.parameters.stores.safe-to-prune-commitment-state = \"all\"\n" - } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1", - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "auth": { - "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json", - "targetAudience": "https://canton.network.global" - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "disableAuth": false, - "enableHealthProbes": true, - "enablePostgresMetrics": true, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "metrics": { - "enable": true - }, - "participantAdminUserNameFrom": { - "secretKeyRef": { - "key": "ledger-api-user", - "name": "splice-app-validator-ledger-api-auth", - "optional": false - } - }, - "persistence": { - "databaseName": "participant_2", - "postgresName": "participant-pg", - "schema": "participant", - "secretName": "participant-pg-secrets" - }, - "resources": { - "limits": { - "memory": "8Gi" - }, - "requests": { - "memory": "4Gi" - } - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ] - }, - "version": "0.3.20" - }, - "name": "splitwell-participant-2", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, { "custom": true, "id": "", @@ -724,6 +621,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -748,6 +646,99 @@ "provider": "", "type": "gcp:sql/databaseInstance:DatabaseInstance" }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-participant", + "compat": "true", + "maxHistory": 10, + "name": "participant", + "namespace": "splitwell", + "timeout": 600, + "values": { + "additionalEnvVars": [ + { + "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", + "value": "# Ignore missing ACS commitment and commitment mismatches\ncanton.participants.participant.parameters.stores.safe-to-prune-commitment-state = \"all\"\n" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1", + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "auth": { + "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json", + "targetAudience": "https://canton.network.global" + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "disableAuth": false, + "enableHealthProbes": true, + "enablePostgresMetrics": true, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "metrics": { + "enable": true + }, + "participantAdminUserNameFrom": { + "secretKeyRef": { + "key": "ledger-api-user", + "name": "splice-app-validator-ledger-api-auth", + "optional": false + } + }, + "persistence": { + "databaseName": "participant_2", + "postgresName": "participant-pg", + "schema": "participant", + "secretName": "participant-pg-secrets" + }, + "resources": { + "limits": { + "memory": "8Gi" + }, + "requests": { + "memory": "4Gi" + } + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + }, + "version": "0.3.20" + }, + "name": "splitwell-participant", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, { "custom": true, "id": "", @@ -797,7 +788,7 @@ "migration": { "id": 9 }, - "participantHost": "participant-2", + "participantHost": "participant", "persistence": { "databaseName": "app_splitwell", "port": 5432, @@ -925,6 +916,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -1054,6 +1046,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -1160,7 +1153,7 @@ "optional": false } }, - "participantAddress": "participant-2", + "participantAddress": "participant", "participantIdentitiesDumpPeriodicBackup": { "backupInterval": "10m", "location": { diff --git a/cluster/expected/sv-canton/expected.json b/cluster/expected/sv-canton/expected.json index 3bd648d617..116be57591 100644 --- a/cluster/expected/sv-canton/expected.json +++ b/cluster/expected/sv-canton/expected.json @@ -1631,7 +1631,14 @@ "key": "cn_apps", "operator": "Exists" } - ] + ], + "watchdog": { + "enabled": true, + "evaluationIntervalSeconds": 900, + "mediatorMetricsUrl": "http://global-domain-7-mediator:10013/metrics", + "sequencerMetricsUrl": "http://global-domain-7-sequencer:10013/metrics", + "threshold": 2 + } }, "version": "0.3.20" }, @@ -1743,7 +1750,14 @@ "key": "cn_apps", "operator": "Exists" } - ] + ], + "watchdog": { + "enabled": true, + "evaluationIntervalSeconds": 900, + "mediatorMetricsUrl": "http://global-domain-8-mediator:10013/metrics", + "sequencerMetricsUrl": "http://global-domain-8-sequencer:10013/metrics", + "threshold": 2 + } }, "version": "0.3.20" }, @@ -1910,7 +1924,6 @@ "namespace": "sv-1", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -1941,6 +1954,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -1953,6 +1967,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "global_domain_10_mediator", "port": 5432, @@ -1986,6 +2001,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "externalAddress": "sequencer-p2p-10.sv-2.mock.global.canton.network.digitalasset.com", "externalPort": 443, @@ -2045,7 +2061,6 @@ "namespace": "sv-1", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -2076,6 +2091,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -2088,6 +2104,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "global_domain_7_mediator", "port": 5432, @@ -2121,6 +2138,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "host": "global-domain-7-cometbft-cometbft-rpc.sv-1.svc.cluster.local", "port": 26657, @@ -2176,7 +2194,6 @@ "namespace": "sv-1", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -2207,6 +2224,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -2219,6 +2237,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "global_domain_8_mediator", "port": 5432, @@ -2252,6 +2271,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "host": "global-domain-8-cometbft-cometbft-rpc.sv-1.svc.cluster.local", "port": 26657, @@ -2307,7 +2327,6 @@ "namespace": "sv-1", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -2338,6 +2357,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -2350,6 +2370,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "global_domain_9_mediator", "port": 5432, @@ -2383,6 +2404,7 @@ "value": "PS9_SEQUENCER_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "externalAddress": "sequencer-p2p-9.sv-2.mock.global.canton.network.digitalasset.com", "externalPort": 443, @@ -2529,6 +2551,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -2660,6 +2683,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -2791,6 +2815,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -2922,6 +2947,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -3053,6 +3079,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -3184,6 +3211,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -3315,6 +3343,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -3446,6 +3475,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -3577,6 +3607,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -3708,6 +3739,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -3839,6 +3871,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -3970,6 +4003,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -4101,6 +4135,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -4232,6 +4267,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -4270,7 +4306,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -4296,7 +4332,7 @@ } }, "name": "sv-1-sv-canton-migration-10", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-10::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-10::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -4312,7 +4348,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -4338,7 +4374,7 @@ } }, "name": "sv-1-sv-canton-migration-5", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-5::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-5::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -4354,7 +4390,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -4380,7 +4416,7 @@ } }, "name": "sv-1-sv-canton-migration-6", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-6::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-6::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -4396,7 +4432,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -4422,7 +4458,7 @@ } }, "name": "sv-1-sv-canton-migration-7", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-7::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-7::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -4438,7 +4474,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -4464,7 +4500,7 @@ } }, "name": "sv-1-sv-canton-migration-8", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-8::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-8::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -4480,7 +4516,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -4506,7 +4542,7 @@ } }, "name": "sv-1-sv-canton-migration-9", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-9::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv-canton-migration-9::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -4982,7 +5018,14 @@ "key": "cn_apps", "operator": "Exists" } - ] + ], + "watchdog": { + "enabled": true, + "evaluationIntervalSeconds": 900, + "mediatorMetricsUrl": "http://global-domain-7-mediator:10013/metrics", + "sequencerMetricsUrl": "http://global-domain-7-sequencer:10013/metrics", + "threshold": 2 + } }, "version": "0.3.20" }, @@ -5091,7 +5134,14 @@ "key": "cn_apps", "operator": "Exists" } - ] + ], + "watchdog": { + "enabled": true, + "evaluationIntervalSeconds": 900, + "mediatorMetricsUrl": "http://global-domain-8-mediator:10013/metrics", + "sequencerMetricsUrl": "http://global-domain-8-sequencer:10013/metrics", + "threshold": 2 + } }, "version": "0.3.20" }, @@ -5204,7 +5254,14 @@ "key": "cn_apps", "operator": "Exists" } - ] + ], + "watchdog": { + "enabled": true, + "evaluationIntervalSeconds": 900, + "mediatorMetricsUrl": "http://global-domain-7-mediator:10013/metrics", + "sequencerMetricsUrl": "http://global-domain-7-sequencer:10013/metrics", + "threshold": 2 + } }, "version": "0.3.20" }, @@ -5317,7 +5374,14 @@ "key": "cn_apps", "operator": "Exists" } - ] + ], + "watchdog": { + "enabled": true, + "evaluationIntervalSeconds": 900, + "mediatorMetricsUrl": "http://global-domain-8-mediator:10013/metrics", + "sequencerMetricsUrl": "http://global-domain-8-sequencer:10013/metrics", + "threshold": 2 + } }, "version": "0.3.20" }, @@ -5484,7 +5548,6 @@ "namespace": "sv-da-1", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -5515,6 +5578,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -5527,6 +5591,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "global_domain_10_mediator", "port": 5432, @@ -5560,6 +5625,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "externalAddress": "sequencer-p2p-10.sv-1.mock.global.canton.network.digitalasset.com", "externalPort": 443, @@ -5619,7 +5685,6 @@ "namespace": "sv-da-1", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -5650,6 +5715,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -5662,6 +5728,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "global_domain_7_mediator", "port": 5432, @@ -5695,6 +5762,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "host": "global-domain-7-cometbft-cometbft-rpc.sv-da-1.svc.cluster.local", "port": 26657, @@ -5750,7 +5818,6 @@ "namespace": "sv-da-1", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -5781,6 +5848,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -5793,6 +5861,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "global_domain_8_mediator", "port": 5432, @@ -5826,6 +5895,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "host": "global-domain-8-cometbft-cometbft-rpc.sv-da-1.svc.cluster.local", "port": 26657, @@ -5881,7 +5951,6 @@ "namespace": "sv-da-1", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -5912,6 +5981,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -5924,6 +5994,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "global_domain_9_mediator", "port": 5432, @@ -5957,6 +6028,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "externalAddress": "sequencer-p2p-9.sv-1.mock.global.canton.network.digitalasset.com", "externalPort": 443, @@ -6103,6 +6175,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -6234,6 +6307,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -6365,6 +6439,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -6496,6 +6571,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -6627,6 +6703,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -6758,6 +6835,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -6889,6 +6967,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -7020,6 +7099,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -7151,6 +7231,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -7282,6 +7363,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -7413,6 +7495,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -7544,6 +7627,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -7675,6 +7759,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -7806,6 +7891,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -7844,7 +7930,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -7870,7 +7956,7 @@ } }, "name": "sv-da-1-sv-canton-migration-10", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-10::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-10::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -7886,7 +7972,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -7912,7 +7998,7 @@ } }, "name": "sv-da-1-sv-canton-migration-5", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-5::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-5::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -7928,7 +8014,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -7954,7 +8040,7 @@ } }, "name": "sv-da-1-sv-canton-migration-6", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-6::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-6::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -7970,7 +8056,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -7996,7 +8082,7 @@ } }, "name": "sv-da-1-sv-canton-migration-7", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-7::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-7::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -8012,7 +8098,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -8038,7 +8124,7 @@ } }, "name": "sv-da-1-sv-canton-migration-8", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-8::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-8::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -8054,7 +8140,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -8080,7 +8166,7 @@ } }, "name": "sv-da-1-sv-canton-migration-9", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-9::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv-canton-migration-9::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -8344,7 +8430,6 @@ "namespace": "sv", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -8375,6 +8460,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -8387,6 +8473,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "mediator_10", "port": 5432, @@ -8420,6 +8507,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "externalAddress": "sequencer-p2p-10.sv.mock.global.canton.network.digitalasset.com", "externalPort": 443, @@ -8479,7 +8567,6 @@ "namespace": "sv", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -8510,6 +8597,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -8522,6 +8610,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "mediator_7", "port": 5432, @@ -8555,6 +8644,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "host": "global-domain-7-cometbft-cometbft-rpc.sv.svc.cluster.local", "port": 26657, @@ -8610,7 +8700,6 @@ "namespace": "sv", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -8641,6 +8730,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -8653,6 +8743,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "mediator_8", "port": 5432, @@ -8686,6 +8777,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "host": "global-domain-8-cometbft-cometbft-rpc.sv.svc.cluster.local", "port": 26657, @@ -8741,7 +8833,6 @@ "namespace": "sv", "timeout": 600, "values": { - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -8772,6 +8863,7 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, + "enableAntiAffinity": true, "enablePostgresMetrics": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "logAsyncFlush": false, @@ -8784,6 +8876,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { "databaseName": "mediator_9", "port": 5432, @@ -8817,6 +8910,7 @@ "value": "CUSTOM_MOCK_ENV_VAR_VALUE" } ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { "externalAddress": "sequencer-p2p-9.sv.mock.global.canton.network.digitalasset.com", "externalPort": 443, @@ -8964,6 +9058,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -9096,6 +9191,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -9228,6 +9324,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -9360,6 +9457,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -9492,6 +9590,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -9624,6 +9723,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -9756,6 +9856,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -9888,6 +9989,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -10020,6 +10122,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -10152,6 +10255,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -10284,6 +10388,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -10416,6 +10521,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -10548,6 +10654,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -10680,6 +10787,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -10718,7 +10826,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -10744,7 +10852,7 @@ } }, "name": "sv-sv-canton-migration-10", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-10::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-10::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -10760,7 +10868,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -10786,7 +10894,7 @@ } }, "name": "sv-sv-canton-migration-5", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-5::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-5::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -10802,7 +10910,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -10828,7 +10936,7 @@ } }, "name": "sv-sv-canton-migration-6", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-6::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-6::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -10844,7 +10952,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -10870,7 +10978,7 @@ } }, "name": "sv-sv-canton-migration-7", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-7::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-7::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -10886,7 +10994,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -10912,7 +11020,7 @@ } }, "name": "sv-sv-canton-migration-8", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-8::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-8::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -10928,7 +11036,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -10954,7 +11062,7 @@ } }, "name": "sv-sv-canton-migration-9", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-9::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv-canton-migration-9::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { diff --git a/cluster/expected/sv-runbook/expected.json b/cluster/expected/sv-runbook/expected.json index d42ca06b2b..7ca66b29e0 100644 --- a/cluster/expected/sv-runbook/expected.json +++ b/cluster/expected/sv-runbook/expected.json @@ -16,7 +16,7 @@ } }, "name": "cluster-ingress-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-cluster-ingress-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-cluster-ingress-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -32,7 +32,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -221,7 +221,7 @@ }, "approvedSvIdentities": { "type": "md5", - "value": "5871224b744b45122540483fddbd550f" + "value": "6c876b92df2d92fe7ba83e883d031386" } }, "network": "test", @@ -602,16 +602,6 @@ "provider": "", "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, { "custom": true, "id": "", @@ -623,6 +613,12 @@ "namespace": "sv", "timeout": 600, "values": { + "additionalEnvVars": [ + { + "name": "ADDITIONAL_CONFIG_CLIENT_IP_HEADERS", + "value": "canton.scan-apps.scan-app.parameters.rate-limiting.client-ip-headers = [\"x-envoy-external-address\"]\n" + } + ], "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -850,11 +846,15 @@ "type": "unlimited" }, "/api/scan/v0/acs": { - "clientIp": true, "fillInterval": "60s", - "maxTokens": 10, + "maxTokens": 500, "name": "acs", - "tokensPerFill": 5, + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 10, + "tokensPerFill": 5 + }, + "tokensPerFill": 500, "type": "limited" }, "/api/scan/v0/active-synchronizer-serial": { @@ -941,6 +941,10 @@ "name": "listBulkAcsSnapshotObjects", "type": "unlimited" }, + "/api/scan/v0/history/bulk/checksums": { + "name": "getBulkHistoryChecksums", + "type": "unlimited" + }, "/api/scan/v0/history/bulk/updates": { "name": "listBulkUpdateHistoryObjects", "type": "unlimited" @@ -993,10 +997,6 @@ "name": "synchronizer-identities", "type": "unlimited" }, - "/api/scan/v0/transactions": { - "name": "transactions", - "type": "unlimited" - }, "/api/scan/v0/transfer-command": { "name": "transfer-command-status", "type": "unlimited" @@ -1045,6 +1045,14 @@ "name": "v1-updates", "type": "banned" }, + "/api/scan/v2/holdings": { + "name": "holdings", + "type": "unlimited" + }, + "/api/scan/v2/state": { + "name": "state", + "type": "unlimited" + }, "/api/scan/v2/updates": { "name": "v2-updates", "type": "unlimited" @@ -1052,6 +1060,160 @@ "/api/scan/version": { "name": "version", "type": "unlimited" + }, + "/registry/allocation-instruction/v1/allocation-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-factory", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation-instruction/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-instruction-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation-instruction/v2/allocation-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation/v2/settlement-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-settlement-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocations/v1": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocations", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocations/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocations-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/metadata/v1/info": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-metadata-info", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "overrides": { + "test": { + "fillInterval": "60s", + "ips": [ + "192.68.78.50" + ], + "maxTokens": 250, + "tokensPerFill": 250 + } + }, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/metadata/v1/instruments": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-metadata-instruments", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v1": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-instruction", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v1/transfer-factory": { + "fillInterval": "60s", + "maxTokens": 1440, + "name": "registry-transfer-factory", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 240, + "tokensPerFill": 240 + }, + "tokensPerFill": 1440, + "type": "limited" + }, + "/registry/transfer-instruction/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-instruction-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v2/transfer-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" } } }, @@ -1165,6 +1327,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -1220,6 +1383,10 @@ { "name": "ADDITIONAL_CONFIG_ADDITIONAL_PACKAGES_TO_UNVET", "value": "canton.sv-apps.sv.additional-packages-to-unvet.\"splice-wallet-payments\" = [\"0.1.15\", \"0.1.16\"]\ncanton.sv-apps.sv.additional-packages-to-unvet.\"splice-amulet\" = [\"0.1.15\"]" + }, + { + "name": "ADDITIONAL_CONFIG_CLIENT_IP_HEADERS", + "value": "canton.sv-apps.sv.parameters.rate-limiting.client-ip-headers = [\"x-envoy-external-address\"]\n" } ], "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSvAppJvmFlag", @@ -1259,16 +1426,6 @@ "rewardWeightBps": 1000000 }, { - "extraBeneficiaries": [ - { - "beneficiary": "mock-validator-1::123456789012345678901234567890123456789012234567890123456789012345678", - "weight": 100000 - }, - { - "beneficiary": "Broadridge-validator-1::1220c89a73e7b47f16dfb48a521eeedfe2a8620ea1b19b815ab427388d9f5427faa5", - "weight": 50000 - } - ], "name": "SV2", "publicKey": "PUBLIC_KEY_2==", "rewardWeightBps": 150000 @@ -1439,7 +1596,7 @@ } }, "name": "sv-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -1455,7 +1612,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -1464,6 +1621,40 @@ "provider": "", "type": "kubernetes:core/v1:Secret" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "scan-app-rate-limit-access-log", + "namespace": "sv" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.code == 429" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "scan-app" + } + } + } + }, + "name": "sv-scan-app-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, { "custom": true, "id": "", @@ -1471,9 +1662,6 @@ "apiVersion": "networking.istio.io/v1alpha3", "kind": "EnvoyFilter", "metadata": { - "annotations": { - "proxy.istio.io/config": "proxyStatsMatcher:\n inclusionRegexps:\n - \".*http_local_rate_limit.*\"" - }, "name": "scan-app-rate-limit", "namespace": "sv" }, @@ -1523,6 +1711,25 @@ "value": { "route": { "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "acs", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/api/scan/v0/acs" + } + } + ] + } + } + ] + }, { "actions": [ { @@ -1541,36 +1748,943 @@ } }, { - "request_headers": { - "descriptor_key": "client_ip", - "header_name": "x-forwarded-for" + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 } } ] - } - ] - }, - "typed_per_filter_config": { - "envoy.filters.http.local_ratelimit": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", - "descriptors": [ - { - "entries": [ - { - "key": "header_match", - "value": "acs" - }, - { - "key": "client_ip" + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v1" + } + } + ] } - ], - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 10, - "tokens_per_fill": 5 + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v1" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-info", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/info" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-info", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/info" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-instruments", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/instruments" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-instruments", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/instruments" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v1/allocation-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v1/allocation-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-settlement-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation/v2/settlement-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-settlement-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation/v2/settlement-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "acs" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 500, + "tokens_per_fill": 500 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "acs" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10, + "tokens_per_fill": 5 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address", + "value": "192.68.78.50/32" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 250, + "tokens_per_fill": 250 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-instruments" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-instruments" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 1440, + "tokens_per_fill": 1440 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 240, + "tokens_per_fill": 240 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-settlement-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-settlement-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-instruction-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-instruction-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 } } ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", "filter_enabled": { "default_value": { "denominator": "HUNDRED", @@ -1585,6 +2699,7 @@ }, "runtime_key": "local_rate_limit_enforced" }, + "max_dynamic_descriptors": 10000, "response_headers_to_add": [ { "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", diff --git a/cluster/expected/sv/expected.json b/cluster/expected/sv/expected.json index 88cb9e079f..ddefcfa850 100644 --- a/cluster/expected/sv/expected.json +++ b/cluster/expected/sv/expected.json @@ -1,4 +1,70 @@ [ + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "postgresPassword": "" + } + }, + "kind": "Secret", + "metadata": { + "name": "cn-apps-pg-secrets", + "namespace": "sv-1" + }, + "type": "Opaque" + }, + "name": "cn-app-sv-1-cn-apps-pg-secrets", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "json-credentials": "eyJidWNrZXROYW1lIjoiZGF0YS1leHBvcnQtYnVja2V0LW5hbWUiLCJzZWNyZXROYW1lIjoiZGF0YS1leHBvcnQtYnVja2V0LXNhLWtleS1zZWNyZXQiLCJqc29uQ3JlZGVudGlhbHMiOiJkYXRhLWV4cG9ydC1idWNrZXQtc2Eta2V5LXNlY3JldC1jcmVkcyJ9" + } + }, + "kind": "Secret", + "metadata": { + "name": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps", + "namespace": "sv-1" + }, + "type": "Opaque" + }, + "name": "cn-app-sv-1-cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "json-credentials": "eyJwcm9qZWN0SWQiOiJkYS1jbi1zaGFyZWQiLCJidWNrZXROYW1lIjoidG9wb2xvZ3ktc25hcHNob3QtYnVja2V0LW5hbWUiLCJzZWNyZXROYW1lIjoiZ2NwLXRvcG9sb2d5LXNuYXBzaG90LWJ1Y2tldC1zYS1rZXktc2VjcmV0IiwianNvbkNyZWRlbnRpYWxzIjoidG9wb2xvZ3ktc25hcHNob3QtYnVja2V0LXNhLWtleS1zZWNyZXQtY3JlZHMiLCJidWNrZXRTYUtleVNlY3JldCI6ImdjcC10b3BvbG9neS1zbmFwc2hvdC1idWNrZXQtc2Eta2V5LWV4YW1wbGUiLCJidWNrZXRTYUlhbUFjY291bnQiOiJkYS1jbi1leGFtcGxldEBkYS1jbi1zaGFyZWQuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20ifQ==" + } + }, + "kind": "Secret", + "metadata": { + "name": "cn-gcp-bucket-da-cn-shared-cn-topology-snapshots", + "namespace": "sv-1" + }, + "type": "Opaque" + }, + "name": "cn-app-sv-1-cn-gcp-bucket-da-cn-shared-cn-topology-snapshots", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, { "custom": true, "id": "", @@ -21,6 +87,73 @@ "provider": "", "type": "kubernetes:core/v1:Secret" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "postgresPassword": "" + } + }, + "kind": "Secret", + "metadata": { + "name": "cn-apps-pg-secrets", + "namespace": "sv-da-1" + }, + "type": "Opaque" + }, + "name": "cn-app-sv-da-1-cn-apps-pg-secrets", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "json-credentials": "eyJidWNrZXROYW1lIjoiZGF0YS1leHBvcnQtYnVja2V0LW5hbWUiLCJzZWNyZXROYW1lIjoiZGF0YS1leHBvcnQtYnVja2V0LXNhLWtleS1zZWNyZXQiLCJqc29uQ3JlZGVudGlhbHMiOiJkYXRhLWV4cG9ydC1idWNrZXQtc2Eta2V5LXNlY3JldC1jcmVkcyJ9" + } + }, + "kind": "Secret", + "metadata": { + "name": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps", + "namespace": "sv-da-1" + }, + "type": "Opaque" + }, + "name": "cn-app-sv-da-1-cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "private": "c3ZkYTEtbW9jay1pZC1wcml2YXRlLWtleQ==", + "public": "c3ZkYTEtbW9jay1pZC1wdWJsaWMta2V5" + } + }, + "kind": "Secret", + "metadata": { + "name": "cn-app-sv-key", + "namespace": "sv-da-1" + }, + "type": "Opaque" + }, + "name": "cn-app-sv-da-1-key", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, { "custom": true, "id": "", @@ -93,7 +226,7 @@ "inputs": { "enableServerSideApply": "true" }, - "name": "k8s-imgpull-sv-1-sv", + "name": "k8s-imgpull-sv-1-default", "provider": "", "type": "pulumi:providers:kubernetes" }, @@ -103,7 +236,7 @@ "inputs": { "enableServerSideApply": "true" }, - "name": "k8s-imgpull-sv-da-1-sv", + "name": "k8s-imgpull-sv-1-sv", "provider": "", "type": "pulumi:providers:kubernetes" }, @@ -113,7 +246,7 @@ "inputs": { "enableServerSideApply": "true" }, - "name": "k8s-imgpull-sv-sv", + "name": "k8s-imgpull-sv-da-1-default", "provider": "", "type": "pulumi:providers:kubernetes" }, @@ -121,132 +254,4813 @@ "custom": true, "id": "", "inputs": { - "condition": { - "description": "(managed by Pulumi)", - "expression": "resource.name.startsWith(\"projects/da-cn-devnet/locations/us-central1/keyRings/sv-da-1_participant_mock\")", - "title": "\"sv-da-1_participant_mock\" keyring" - }, - "role": "roles/cloudkms.admin" - }, - "name": "mock-sv-da-1-kms-roles/cloudkms.admin-\"sv-da-1_participant_mock\"-keyring-iam", - "provider": "", - "type": "gcp:projects/iAMMember:IAMMember" - }, - { - "custom": true, - "id": "", - "inputs": { - "condition": { - "description": "(managed by Pulumi)", - "expression": "resource.name.startsWith(\"projects/da-cn-devnet/locations/us-central1/keyRings/sv-da-1_participant_mock\")", - "title": "\"sv-da-1_participant_mock\" keyring" - }, - "role": "roles/cloudkms.cryptoOperator" + "enableServerSideApply": "true" }, - "name": "mock-sv-da-1-kms-roles/cloudkms.cryptoOperator-\"sv-da-1_participant_mock\"-keyring-iam", + "name": "k8s-imgpull-sv-da-1-sv", "provider": "", - "type": "gcp:projects/iAMMember:IAMMember" + "type": "pulumi:providers:kubernetes" }, { "custom": true, "id": "", "inputs": { - "accountId": "mock-sv-da-1-kms", - "description": "(managed by Pulumi)", - "displayName": "KMS Service Account (mock sv-da-1)" + "enableServerSideApply": "true" }, - "name": "mock-sv-da-1-kms-sa", - "provider": "", - "type": "gcp:serviceaccount/account:Account" - }, - { - "custom": false, - "id": "", - "inputs": {}, - "name": "mock-sv-da-1-kms", - "provider": "", - "type": "cn:gcp:ServiceAccount" - }, - { - "custom": true, - "id": "", - "inputs": {}, - "name": "participantKmsServiceAccountKey", + "name": "k8s-imgpull-sv-sv", "provider": "", - "type": "gcp:serviceaccount/key:Key" + "type": "pulumi:providers:kubernetes" }, { "custom": true, "id": "", "inputs": { - "apiVersion": "v1", - "kind": "Secret", + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "VirtualService", "metadata": { - "name": "splice-app-sv-ledger-api-user", + "name": "cometbft-loopback", "namespace": "sv-1" }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "ledger-api-user": "sv1-sv-client-id@clients" - } + "spec": { + "exportTo": [ + "." + ], + "gateways": [ + "mesh" + ], + "hosts": [ + "mock.global.canton.network.digitalasset.com" + ], + "tcp": [ + { + "match": [ + { + "gateways": [ + "mesh" + ] + } + ], + "route": [ + { + "destination": { + "host": "istio-ingress-cometbft.cluster-ingress.svc.cluster.local" + } + } + ] + } + ] } }, - "name": "splice-auth0-user-sv-1-sv-sv", + "name": "loopback-cometbft-sv-1", "provider": "", - "type": "kubernetes:core/v1:Secret" + "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" }, { "custom": true, "id": "", "inputs": { - "apiVersion": "v1", - "kind": "Secret", + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "VirtualService", "metadata": { - "name": "splice-app-sv-ledger-api-user", + "name": "cometbft-loopback", "namespace": "sv-da-1" }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "ledger-api-user": "sv-da-1-sv-client-id@clients" - } + "spec": { + "exportTo": [ + "." + ], + "gateways": [ + "mesh" + ], + "hosts": [ + "mock.global.canton.network.digitalasset.com" + ], + "tcp": [ + { + "match": [ + { + "gateways": [ + "mesh" + ] + } + ], + "route": [ + { + "destination": { + "host": "istio-ingress-cometbft.cluster-ingress.svc.cluster.local" + } + } + ] + } + ] } }, - "name": "splice-auth0-user-sv-da-1-sv-sv", + "name": "loopback-cometbft-sv-da-1", "provider": "", - "type": "kubernetes:core/v1:Secret" + "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" }, { "custom": true, "id": "", "inputs": { - "apiVersion": "v1", - "kind": "Secret", + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "ServiceEntry", "metadata": { - "name": "splice-app-sv-ledger-api-user", - "namespace": "sv" + "name": "loopback", + "namespace": "sv-1" + }, + "spec": { + "exportTo": [ + "." + ], + "hosts": [ + "mock.global.canton.network.digitalasset.com" + ], + "ports": [ + { + "name": "http-port", + "number": 80, + "protocol": "HTTP" + }, + { + "name": "tls", + "number": 443, + "protocol": "TLS" + }, + { + "name": "grpc-domain", + "number": 5008, + "protocol": "GRPC" + }, + { + "name": "cometbft-0-0-p2p", + "number": 26006, + "protocol": "TCP" + }, + { + "name": "cometbft-0-1-p2p", + "number": 26016, + "protocol": "TCP" + }, + { + "name": "cometbft-0-2-p2p", + "number": 26026, + "protocol": "TCP" + }, + { + "name": "cometbft-1-0-p2p", + "number": 26106, + "protocol": "TCP" + }, + { + "name": "cometbft-1-1-p2p", + "number": 26116, + "protocol": "TCP" + }, + { + "name": "cometbft-1-2-p2p", + "number": 26126, + "protocol": "TCP" + }, + { + "name": "cometbft-2-0-p2p", + "number": 26206, + "protocol": "TCP" + }, + { + "name": "cometbft-2-1-p2p", + "number": 26216, + "protocol": "TCP" + }, + { + "name": "cometbft-2-2-p2p", + "number": 26226, + "protocol": "TCP" + }, + { + "name": "cometbft-3-0-p2p", + "number": 26306, + "protocol": "TCP" + }, + { + "name": "cometbft-3-1-p2p", + "number": 26316, + "protocol": "TCP" + }, + { + "name": "cometbft-3-2-p2p", + "number": 26326, + "protocol": "TCP" + }, + { + "name": "cometbft-4-0-p2p", + "number": 26406, + "protocol": "TCP" + }, + { + "name": "cometbft-4-1-p2p", + "number": 26416, + "protocol": "TCP" + }, + { + "name": "cometbft-4-2-p2p", + "number": 26426, + "protocol": "TCP" + }, + { + "name": "cometbft-5-0-p2p", + "number": 26506, + "protocol": "TCP" + }, + { + "name": "cometbft-5-1-p2p", + "number": 26516, + "protocol": "TCP" + }, + { + "name": "cometbft-5-2-p2p", + "number": 26526, + "protocol": "TCP" + }, + { + "name": "cometbft-6-0-p2p", + "number": 26606, + "protocol": "TCP" + }, + { + "name": "cometbft-6-1-p2p", + "number": 26616, + "protocol": "TCP" + }, + { + "name": "cometbft-6-2-p2p", + "number": 26626, + "protocol": "TCP" + }, + { + "name": "cometbft-7-0-p2p", + "number": 26706, + "protocol": "TCP" + }, + { + "name": "cometbft-7-1-p2p", + "number": 26716, + "protocol": "TCP" + }, + { + "name": "cometbft-7-2-p2p", + "number": 26726, + "protocol": "TCP" + }, + { + "name": "cometbft-8-0-p2p", + "number": 26806, + "protocol": "TCP" + }, + { + "name": "cometbft-8-1-p2p", + "number": 26816, + "protocol": "TCP" + }, + { + "name": "cometbft-8-2-p2p", + "number": 26826, + "protocol": "TCP" + }, + { + "name": "cometbft-9-0-p2p", + "number": 26906, + "protocol": "TCP" + }, + { + "name": "cometbft-9-1-p2p", + "number": 26916, + "protocol": "TCP" + }, + { + "name": "cometbft-9-2-p2p", + "number": 26926, + "protocol": "TCP" + } + ], + "resolution": "DNS" + } + }, + "name": "loopback-service-entry-sv-1", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:ServiceEntry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "ServiceEntry", + "metadata": { + "name": "loopback", + "namespace": "sv-da-1" + }, + "spec": { + "exportTo": [ + "." + ], + "hosts": [ + "mock.global.canton.network.digitalasset.com" + ], + "ports": [ + { + "name": "http-port", + "number": 80, + "protocol": "HTTP" + }, + { + "name": "tls", + "number": 443, + "protocol": "TLS" + }, + { + "name": "grpc-domain", + "number": 5008, + "protocol": "GRPC" + }, + { + "name": "cometbft-0-0-p2p", + "number": 26006, + "protocol": "TCP" + }, + { + "name": "cometbft-0-1-p2p", + "number": 26016, + "protocol": "TCP" + }, + { + "name": "cometbft-0-2-p2p", + "number": 26026, + "protocol": "TCP" + }, + { + "name": "cometbft-1-0-p2p", + "number": 26106, + "protocol": "TCP" + }, + { + "name": "cometbft-1-1-p2p", + "number": 26116, + "protocol": "TCP" + }, + { + "name": "cometbft-1-2-p2p", + "number": 26126, + "protocol": "TCP" + }, + { + "name": "cometbft-2-0-p2p", + "number": 26206, + "protocol": "TCP" + }, + { + "name": "cometbft-2-1-p2p", + "number": 26216, + "protocol": "TCP" + }, + { + "name": "cometbft-2-2-p2p", + "number": 26226, + "protocol": "TCP" + }, + { + "name": "cometbft-3-0-p2p", + "number": 26306, + "protocol": "TCP" + }, + { + "name": "cometbft-3-1-p2p", + "number": 26316, + "protocol": "TCP" + }, + { + "name": "cometbft-3-2-p2p", + "number": 26326, + "protocol": "TCP" + }, + { + "name": "cometbft-4-0-p2p", + "number": 26406, + "protocol": "TCP" + }, + { + "name": "cometbft-4-1-p2p", + "number": 26416, + "protocol": "TCP" + }, + { + "name": "cometbft-4-2-p2p", + "number": 26426, + "protocol": "TCP" + }, + { + "name": "cometbft-5-0-p2p", + "number": 26506, + "protocol": "TCP" + }, + { + "name": "cometbft-5-1-p2p", + "number": 26516, + "protocol": "TCP" + }, + { + "name": "cometbft-5-2-p2p", + "number": 26526, + "protocol": "TCP" + }, + { + "name": "cometbft-6-0-p2p", + "number": 26606, + "protocol": "TCP" + }, + { + "name": "cometbft-6-1-p2p", + "number": 26616, + "protocol": "TCP" + }, + { + "name": "cometbft-6-2-p2p", + "number": 26626, + "protocol": "TCP" + }, + { + "name": "cometbft-7-0-p2p", + "number": 26706, + "protocol": "TCP" + }, + { + "name": "cometbft-7-1-p2p", + "number": 26716, + "protocol": "TCP" + }, + { + "name": "cometbft-7-2-p2p", + "number": 26726, + "protocol": "TCP" + }, + { + "name": "cometbft-8-0-p2p", + "number": 26806, + "protocol": "TCP" + }, + { + "name": "cometbft-8-1-p2p", + "number": 26816, + "protocol": "TCP" + }, + { + "name": "cometbft-8-2-p2p", + "number": 26826, + "protocol": "TCP" + }, + { + "name": "cometbft-9-0-p2p", + "number": 26906, + "protocol": "TCP" + }, + { + "name": "cometbft-9-1-p2p", + "number": 26916, + "protocol": "TCP" + }, + { + "name": "cometbft-9-2-p2p", + "number": 26926, + "protocol": "TCP" + } + ], + "resolution": "DNS" + } + }, + "name": "loopback-service-entry-sv-da-1", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:ServiceEntry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "VirtualService", + "metadata": { + "name": "direct-loopback-through-ingress-gateway", + "namespace": "sv-1" + }, + "spec": { + "exportTo": [ + "." + ], + "gateways": [ + "mesh" + ], + "hosts": [ + "mock.global.canton.network.digitalasset.com", + "validator.mock.global.canton.network.digitalasset.com", + "*.validator.mock.global.canton.network.digitalasset.com", + "validator1.mock.global.canton.network.digitalasset.com", + "*.validator1.mock.global.canton.network.digitalasset.com", + "splitwell.mock.global.canton.network.digitalasset.com", + "*.splitwell.mock.global.canton.network.digitalasset.com", + "sv-2.mock.global.canton.network.digitalasset.com", + "*.sv-2.mock.global.canton.network.digitalasset.com", + "sv-1.mock.global.canton.network.digitalasset.com", + "*.sv-1.mock.global.canton.network.digitalasset.com", + "sv.mock.global.canton.network.digitalasset.com", + "*.sv.mock.global.canton.network.digitalasset.com" + ], + "http": [ + { + "match": [ + { + "gateways": [ + "mesh" + ] + } + ], + "route": [ + { + "destination": { + "host": "istio-ingress.cluster-ingress.svc.cluster.local" + } + } + ] + } + ], + "tls": [ + { + "match": [ + { + "gateways": [ + "mesh" + ], + "sniHosts": [ + "mock.global.canton.network.digitalasset.com", + "validator.mock.global.canton.network.digitalasset.com", + "*.validator.mock.global.canton.network.digitalasset.com", + "validator1.mock.global.canton.network.digitalasset.com", + "*.validator1.mock.global.canton.network.digitalasset.com", + "splitwell.mock.global.canton.network.digitalasset.com", + "*.splitwell.mock.global.canton.network.digitalasset.com", + "sv-2.mock.global.canton.network.digitalasset.com", + "*.sv-2.mock.global.canton.network.digitalasset.com", + "sv-1.mock.global.canton.network.digitalasset.com", + "*.sv-1.mock.global.canton.network.digitalasset.com", + "sv.mock.global.canton.network.digitalasset.com", + "*.sv.mock.global.canton.network.digitalasset.com" + ] + } + ], + "route": [ + { + "destination": { + "host": "istio-ingress.cluster-ingress.svc.cluster.local" + } + } + ] + } + ] + } + }, + "name": "loopback-virtual-service-sv-1", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "VirtualService", + "metadata": { + "name": "direct-loopback-through-ingress-gateway", + "namespace": "sv-da-1" + }, + "spec": { + "exportTo": [ + "." + ], + "gateways": [ + "mesh" + ], + "hosts": [ + "mock.global.canton.network.digitalasset.com", + "validator.mock.global.canton.network.digitalasset.com", + "*.validator.mock.global.canton.network.digitalasset.com", + "validator1.mock.global.canton.network.digitalasset.com", + "*.validator1.mock.global.canton.network.digitalasset.com", + "splitwell.mock.global.canton.network.digitalasset.com", + "*.splitwell.mock.global.canton.network.digitalasset.com", + "sv-2.mock.global.canton.network.digitalasset.com", + "*.sv-2.mock.global.canton.network.digitalasset.com", + "sv-1.mock.global.canton.network.digitalasset.com", + "*.sv-1.mock.global.canton.network.digitalasset.com", + "sv.mock.global.canton.network.digitalasset.com", + "*.sv.mock.global.canton.network.digitalasset.com" + ], + "http": [ + { + "match": [ + { + "gateways": [ + "mesh" + ] + } + ], + "route": [ + { + "destination": { + "host": "istio-ingress.cluster-ingress.svc.cluster.local" + } + } + ] + } + ], + "tls": [ + { + "match": [ + { + "gateways": [ + "mesh" + ], + "sniHosts": [ + "mock.global.canton.network.digitalasset.com", + "validator.mock.global.canton.network.digitalasset.com", + "*.validator.mock.global.canton.network.digitalasset.com", + "validator1.mock.global.canton.network.digitalasset.com", + "*.validator1.mock.global.canton.network.digitalasset.com", + "splitwell.mock.global.canton.network.digitalasset.com", + "*.splitwell.mock.global.canton.network.digitalasset.com", + "sv-2.mock.global.canton.network.digitalasset.com", + "*.sv-2.mock.global.canton.network.digitalasset.com", + "sv-1.mock.global.canton.network.digitalasset.com", + "*.sv-1.mock.global.canton.network.digitalasset.com", + "sv.mock.global.canton.network.digitalasset.com", + "*.sv.mock.global.canton.network.digitalasset.com" + ] + } + ], + "route": [ + { + "destination": { + "host": "istio-ingress.cluster-ingress.svc.cluster.local" + } + } + ] + } + ] + } + }, + "name": "loopback-virtual-service-sv-da-1", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" + }, + { + "custom": true, + "id": "", + "inputs": { + "bucket": "mock-sv-1-bulk-committed", + "member": "serviceAccount:undefined", + "role": "roles/storage.objectCreator" + }, + "name": "mock-sv-1-bulk-committed-sa-role-creator", + "provider": "", + "type": "gcp:storage/bucketIAMMember:BucketIAMMember" + }, + { + "custom": true, + "id": "", + "inputs": { + "bucket": "mock-sv-1-bulk-committed", + "member": "serviceAccount:undefined", + "role": "roles/storage.objectViewer" + }, + "name": "mock-sv-1-bulk-committed-sa-role-reader", + "provider": "", + "type": "gcp:storage/bucketIAMMember:BucketIAMMember" + }, + { + "custom": true, + "id": "", + "inputs": { + "location": "europe-west6", + "name": "mock-sv-1-bulk-committed" + }, + "name": "mock-sv-1-bulk-committed", + "provider": "", + "type": "gcp:storage/bucket:Bucket" + }, + { + "custom": true, + "id": "", + "inputs": {}, + "name": "mock-sv-1-bulk-sa-hmac", + "provider": "", + "type": "gcp:storage/hmacKey:HmacKey" + }, + { + "custom": true, + "id": "", + "inputs": { + "accountId": "mock-sv-1-bulk-sa", + "displayName": "Service Account for Bulk-Storage Bucket Read/Write Access" + }, + "name": "mock-sv-1-bulk-sa", + "provider": "", + "type": "gcp:serviceaccount/account:Account" + }, + { + "custom": true, + "id": "", + "inputs": { + "bucket": "mock-sv-1-bulk-staging", + "member": "serviceAccount:undefined", + "role": "roles/storage.objectUser" + }, + "name": "mock-sv-1-bulk-staging-sa-role", + "provider": "", + "type": "gcp:storage/bucketIAMMember:BucketIAMMember" + }, + { + "custom": true, + "id": "", + "inputs": { + "location": "europe-west6", + "name": "mock-sv-1-bulk-staging" + }, + "name": "mock-sv-1-bulk-staging", + "provider": "", + "type": "gcp:storage/bucket:Bucket" + }, + { + "custom": true, + "id": "", + "inputs": { + "bucket": "mock-sv-da-1-bulk-committed", + "member": "serviceAccount:undefined", + "role": "roles/storage.objectCreator" + }, + "name": "mock-sv-da-1-bulk-committed-sa-role-creator", + "provider": "", + "type": "gcp:storage/bucketIAMMember:BucketIAMMember" + }, + { + "custom": true, + "id": "", + "inputs": { + "bucket": "mock-sv-da-1-bulk-committed", + "member": "serviceAccount:undefined", + "role": "roles/storage.objectViewer" + }, + "name": "mock-sv-da-1-bulk-committed-sa-role-reader", + "provider": "", + "type": "gcp:storage/bucketIAMMember:BucketIAMMember" + }, + { + "custom": true, + "id": "", + "inputs": { + "location": "europe-west6", + "name": "mock-sv-da-1-bulk-committed" + }, + "name": "mock-sv-da-1-bulk-committed", + "provider": "", + "type": "gcp:storage/bucket:Bucket" + }, + { + "custom": true, + "id": "", + "inputs": {}, + "name": "mock-sv-da-1-bulk-sa-hmac", + "provider": "", + "type": "gcp:storage/hmacKey:HmacKey" + }, + { + "custom": true, + "id": "", + "inputs": { + "accountId": "mock-sv-da-1-bulk-sa", + "displayName": "Service Account for Bulk-Storage Bucket Read/Write Access" + }, + "name": "mock-sv-da-1-bulk-sa", + "provider": "", + "type": "gcp:serviceaccount/account:Account" + }, + { + "custom": true, + "id": "", + "inputs": { + "bucket": "mock-sv-da-1-bulk-staging", + "member": "serviceAccount:undefined", + "role": "roles/storage.objectUser" + }, + "name": "mock-sv-da-1-bulk-staging-sa-role", + "provider": "", + "type": "gcp:storage/bucketIAMMember:BucketIAMMember" + }, + { + "custom": true, + "id": "", + "inputs": { + "location": "europe-west6", + "name": "mock-sv-da-1-bulk-staging" + }, + "name": "mock-sv-da-1-bulk-staging", + "provider": "", + "type": "gcp:storage/bucket:Bucket" + }, + { + "custom": true, + "id": "", + "inputs": { + "condition": { + "description": "(managed by Pulumi)", + "expression": "resource.name.startsWith(\"projects/da-cn-devnet/locations/us-central1/keyRings/sv-da-1_participant_mock\")", + "title": "\"sv-da-1_participant_mock\" keyring" + }, + "role": "roles/cloudkms.admin" + }, + "name": "mock-sv-da-1-kms-roles/cloudkms.admin-\"sv-da-1_participant_mock\"-keyring-iam", + "provider": "", + "type": "gcp:projects/iAMMember:IAMMember" + }, + { + "custom": true, + "id": "", + "inputs": { + "condition": { + "description": "(managed by Pulumi)", + "expression": "resource.name.startsWith(\"projects/da-cn-devnet/locations/us-central1/keyRings/sv-da-1_participant_mock\")", + "title": "\"sv-da-1_participant_mock\" keyring" + }, + "role": "roles/cloudkms.cryptoOperator" + }, + "name": "mock-sv-da-1-kms-roles/cloudkms.cryptoOperator-\"sv-da-1_participant_mock\"-keyring-iam", + "provider": "", + "type": "gcp:projects/iAMMember:IAMMember" + }, + { + "custom": true, + "id": "", + "inputs": { + "accountId": "mock-sv-da-1-kms", + "description": "(managed by Pulumi)", + "displayName": "KMS Service Account (mock sv-da-1)" + }, + "name": "mock-sv-da-1-kms-sa", + "provider": "", + "type": "gcp:serviceaccount/account:Account" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "mock-sv-da-1-kms", + "provider": "", + "type": "cn:gcp:ServiceAccount" + }, + { + "custom": true, + "id": "", + "inputs": {}, + "name": "participantKmsServiceAccountKey", + "provider": "", + "type": "gcp:serviceaccount/key:Key" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "secret": "c3BsaXR3ZWxsc2VjcmV0Mg==" + } + }, + "kind": "Secret", + "metadata": { + "name": "splice-app-validator-onboarding-splitwell2", + "namespace": "sv-1" + }, + "type": "Opaque" + }, + "name": "splice-app-sv-1-validator-onboarding-splitwell2", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "secret": "dmFsaWRhdG9yMXNlY3JldDI=" + } + }, + "kind": "Secret", + "metadata": { + "name": "splice-app-validator-onboarding-validator12", + "namespace": "sv-1" + }, + "type": "Opaque" + }, + "name": "splice-app-sv-1-validator-onboarding-validator12", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "private": "c3ZkYTEtbW9jay1jb21ldGJmdC1nb3Zlcm5hbmNlLWtleS1wcml2YXRlLWtleQ==", + "public": "c3ZkYTEtbW9jay1jb21ldGJmdC1nb3Zlcm5hbmNlLWtleS1wdWJsaWMta2V5" + } + }, + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-cometbft-governance-key", + "namespace": "sv-da-1" + }, + "type": "Opaque" + }, + "name": "splice-app-sv-da-1-cometbft-governance-key", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "private": "c3ZkYTEtbW9jay1pZC1wcml2YXRlLWtleQ==", + "public": "c3ZkYTEtbW9jay1pZC1wdWJsaWMta2V5" + } + }, + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-key", + "namespace": "sv-da-1" + }, + "type": "Opaque" + }, + "name": "splice-app-sv-da-1-key", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-ledger-api-auth", + "namespace": "sv-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "audience": "https://canton.network.global", + "client-id": "sv1-sv-client-id", + "client-secret": "***", + "ledger-api-user": "sv1-sv-client-id@clients", + "url": "https://canton-network-dev.us.auth0.com/.well-known/openid-configuration" + } + } + }, + "name": "splice-auth0-secret-sv-1-sv", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-validator-ledger-api-auth", + "namespace": "sv-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "audience": "https://canton.network.global", + "client-id": "sv1-validator-client-id", + "client-secret": "***", + "ledger-api-user": "sv1-validator-client-id@clients", + "url": "https://canton-network-dev.us.auth0.com/.well-known/openid-configuration" + } + } + }, + "name": "splice-auth0-secret-sv-1-validator", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-ledger-api-auth", + "namespace": "sv-da-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "audience": "https://canton.network.global", + "client-id": "sv-da-1-sv-client-id", + "client-secret": "***", + "ledger-api-user": "sv-da-1-sv-client-id@clients", + "url": "https://canton-network-dev.us.auth0.com/.well-known/openid-configuration" + } + } + }, + "name": "splice-auth0-secret-sv-da-1-sv", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-validator-ledger-api-auth", + "namespace": "sv-da-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "audience": "https://canton.network.global", + "client-id": "sv-da-1-validator-client-id", + "client-secret": "***", + "ledger-api-user": "sv-da-1-validator-client-id@clients", + "url": "https://canton-network-dev.us.auth0.com/.well-known/openid-configuration" + } + } + }, + "name": "splice-auth0-secret-sv-da-1-validator", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-cns-ui-auth", + "namespace": "sv-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "client-id": "sv-1-cns-ui-client-id", + "url": "https://canton-network-dev.us.auth0.com" + } + } + }, + "name": "splice-auth0-ui-secret-sv-1-cns", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-ui-auth", + "namespace": "sv-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "client-id": "sv-1-sv-ui-client-id", + "url": "https://canton-network-dev.us.auth0.com" + } + } + }, + "name": "splice-auth0-ui-secret-sv-1-sv", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-wallet-ui-auth", + "namespace": "sv-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "client-id": "sv-1-wallet-ui-client-id", + "url": "https://canton-network-dev.us.auth0.com" + } + } + }, + "name": "splice-auth0-ui-secret-sv-1-wallet", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-cns-ui-auth", + "namespace": "sv-da-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "client-id": "sv-da-1-cns-ui-client-id", + "url": "https://canton-network-dev.us.auth0.com" + } + } + }, + "name": "splice-auth0-ui-secret-sv-da-1-cns", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-ui-auth", + "namespace": "sv-da-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "client-id": "sv-da-1-sv-ui-client-id", + "url": "https://canton-network-dev.us.auth0.com" + } + } + }, + "name": "splice-auth0-ui-secret-sv-da-1-sv", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-wallet-ui-auth", + "namespace": "sv-da-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "client-id": "sv-da-1-wallet-ui-client-id", + "url": "https://canton-network-dev.us.auth0.com" + } + } + }, + "name": "splice-auth0-ui-secret-sv-da-1-wallet", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-ledger-api-user", + "namespace": "sv-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "ledger-api-user": "sv1-sv-client-id@clients" + } + } + }, + "name": "splice-auth0-user-sv-1-sv-sv", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-ledger-api-user", + "namespace": "sv-da-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "ledger-api-user": "sv-da-1-sv-client-id@clients" + } + } + }, + "name": "splice-auth0-user-sv-da-1-sv-sv", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "splice-app-sv-ledger-api-user", + "namespace": "sv" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "ledger-api-user": "sv-client-id@clients" + } + } + }, + "name": "splice-auth0-user-sv-sv-sv", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "scan-app", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 2147483647, + "tokensPerFill": 2147483647 + }, + "inboundPort": 5012, + "namespace": "sv-1", + "rateLimits": { + "/api/scan/livez": { + "name": "livez", + "type": "unlimited" + }, + "/api/scan/readyz": { + "name": "readyz", + "type": "unlimited" + }, + "/api/scan/status": { + "name": "status", + "type": "unlimited" + }, + "/api/scan/v0/acs": { + "fillInterval": "60s", + "maxTokens": 500, + "name": "acs", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 10, + "tokensPerFill": 5 + }, + "tokensPerFill": 500, + "type": "limited" + }, + "/api/scan/v0/active-synchronizer-serial": { + "name": "activeSynchronizerSerial", + "type": "unlimited" + }, + "/api/scan/v0/admin/sv/previous-sv-reward-weight": { + "name": "sv-previous-sv-reward-weight", + "type": "unlimited" + }, + "/api/scan/v0/admin/sv/voterequests": { + "name": "sv-voterequests", + "type": "unlimited" + }, + "/api/scan/v0/admin/sv/voteresults": { + "name": "sv-voteresults", + "type": "unlimited" + }, + "/api/scan/v0/admin/validator": { + "name": "validator-licenses", + "type": "unlimited" + }, + "/api/scan/v0/amulet-config-for-round": { + "name": "amulet-config-for-round", + "type": "banned" + }, + "/api/scan/v0/amulet-price": { + "name": "amulet-price-votes", + "type": "unlimited" + }, + "/api/scan/v0/amulet-rules": { + "name": "amulet-rules", + "type": "unlimited" + }, + "/api/scan/v0/ans-entries": { + "name": "ans-entries", + "type": "unlimited" + }, + "/api/scan/v0/ans-rules": { + "name": "ans-rules", + "type": "unlimited" + }, + "/api/scan/v0/backfilling": { + "name": "backfilling", + "type": "unlimited" + }, + "/api/scan/v0/closed-rounds": { + "name": "closed-rounds", + "type": "unlimited" + }, + "/api/scan/v0/domains": { + "name": "domains", + "type": "unlimited" + }, + "/api/scan/v0/dso": { + "name": "dso-info", + "type": "unlimited" + }, + "/api/scan/v0/dso-party-id": { + "name": "dso-party-id", + "type": "unlimited" + }, + "/api/scan/v0/dso-sequencers": { + "name": "dso-sequencers", + "type": "unlimited" + }, + "/api/scan/v0/events": { + "name": "events", + "type": "unlimited" + }, + "/api/scan/v0/external-party-amulet-rules": { + "name": "external-party-amulet-rules", + "type": "unlimited" + }, + "/api/scan/v0/feature-support": { + "name": "feature-support", + "type": "unlimited" + }, + "/api/scan/v0/featured-apps": { + "name": "featured-apps", + "type": "unlimited" + }, + "/api/scan/v0/history/bulk/acs": { + "name": "listBulkAcsSnapshotObjects", + "type": "unlimited" + }, + "/api/scan/v0/history/bulk/checksums": { + "name": "getBulkHistoryChecksums", + "type": "unlimited" + }, + "/api/scan/v0/history/bulk/updates": { + "name": "listBulkUpdateHistoryObjects", + "type": "unlimited" + }, + "/api/scan/v0/holdings": { + "name": "holdings", + "type": "unlimited" + }, + "/api/scan/v0/internal/reward-accounting-process": { + "name": "reward-accounting-process", + "type": "unlimited" + }, + "/api/scan/v0/lsu": { + "name": "lsu", + "type": "unlimited" + }, + "/api/scan/v0/migrations": { + "name": "migrations-schedule", + "type": "unlimited" + }, + "/api/scan/v0/open-and-issuing-mining-rounds": { + "name": "open-and-issuing-mining-rounds", + "type": "unlimited" + }, + "/api/scan/v0/roll-forward-lsu": { + "name": "rollForwardLsu", + "type": "unlimited" + }, + "/api/scan/v0/scans": { + "name": "scans", + "type": "unlimited" + }, + "/api/scan/v0/splice-instance-names": { + "name": "splice-instance-names", + "type": "unlimited" + }, + "/api/scan/v0/state": { + "name": "state", + "type": "unlimited" + }, + "/api/scan/v0/sv-bft-sequencers": { + "name": "bft-sequencers", + "type": "unlimited" + }, + "/api/scan/v0/synchronizer-bootstrapping-transactions": { + "name": "synchronizer-bootstrapping-transactions", + "type": "unlimited" + }, + "/api/scan/v0/synchronizer-identities": { + "name": "synchronizer-identities", + "type": "unlimited" + }, + "/api/scan/v0/transfer-command": { + "name": "transfer-command-status", + "type": "unlimited" + }, + "/api/scan/v0/transfer-command-counter": { + "name": "transfer-command-counter", + "type": "unlimited" + }, + "/api/scan/v0/transfer-preapprovals": { + "name": "transfer-preapprovals", + "type": "unlimited" + }, + "/api/scan/v0/unclaimed-development-fund-coupons": { + "name": "unclaimed-development-fund-coupons", + "type": "unlimited" + }, + "/api/scan/v0/updates": { + "name": "v0-updates", + "type": "unlimited" + }, + "/api/scan/v0/validators": { + "name": "validators", + "type": "unlimited" + }, + "/api/scan/v0/voterequest": { + "name": "voterequest", + "type": "unlimited" + }, + "/api/scan/v0/voterequests": { + "name": "voterequests", + "type": "unlimited" + }, + "/api/scan/v1/domains": { + "name": "domains-v1", + "type": "unlimited" + }, + "/api/scan/v1/holdings": { + "name": "holdings", + "type": "unlimited" + }, + "/api/scan/v1/state": { + "name": "state", + "type": "unlimited" + }, + "/api/scan/v1/updates": { + "name": "v1-updates", + "type": "banned" + }, + "/api/scan/v2/holdings": { + "name": "holdings", + "type": "unlimited" + }, + "/api/scan/v2/state": { + "name": "state", + "type": "unlimited" + }, + "/api/scan/v2/updates": { + "name": "v2-updates", + "type": "unlimited" + }, + "/api/scan/version": { + "name": "version", + "type": "unlimited" + }, + "/registry/allocation-instruction/v1/allocation-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-factory", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation-instruction/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-instruction-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation-instruction/v2/allocation-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation/v2/settlement-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-settlement-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocations/v1": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocations", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocations/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocations-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/metadata/v1/info": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-metadata-info", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "overrides": { + "test": { + "fillInterval": "60s", + "ips": [ + "192.68.78.50" + ], + "maxTokens": 250, + "tokensPerFill": 250 + } + }, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/metadata/v1/instruments": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-metadata-instruments", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v1": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-instruction", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v1/transfer-factory": { + "fillInterval": "60s", + "maxTokens": 1440, + "name": "registry-transfer-factory", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 240, + "tokensPerFill": 240 + }, + "tokensPerFill": 1440, + "type": "limited" + }, + "/registry/transfer-instruction/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-instruction-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v2/transfer-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + } + } + }, + "name": "splice-sv-1-scan-app-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "scan-app", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 2147483647, + "tokensPerFill": 2147483647 + }, + "inboundPort": 5012, + "namespace": "sv-da-1", + "rateLimits": { + "/api/scan/livez": { + "name": "livez", + "type": "unlimited" + }, + "/api/scan/readyz": { + "name": "readyz", + "type": "unlimited" + }, + "/api/scan/status": { + "name": "status", + "type": "unlimited" + }, + "/api/scan/v0/acs": { + "fillInterval": "60s", + "maxTokens": 500, + "name": "acs", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 10, + "tokensPerFill": 5 + }, + "tokensPerFill": 500, + "type": "limited" + }, + "/api/scan/v0/active-synchronizer-serial": { + "name": "activeSynchronizerSerial", + "type": "unlimited" + }, + "/api/scan/v0/admin/sv/previous-sv-reward-weight": { + "name": "sv-previous-sv-reward-weight", + "type": "unlimited" + }, + "/api/scan/v0/admin/sv/voterequests": { + "name": "sv-voterequests", + "type": "unlimited" + }, + "/api/scan/v0/admin/sv/voteresults": { + "name": "sv-voteresults", + "type": "unlimited" + }, + "/api/scan/v0/admin/validator": { + "name": "validator-licenses", + "type": "unlimited" + }, + "/api/scan/v0/amulet-config-for-round": { + "name": "amulet-config-for-round", + "type": "banned" + }, + "/api/scan/v0/amulet-price": { + "name": "amulet-price-votes", + "type": "unlimited" + }, + "/api/scan/v0/amulet-rules": { + "name": "amulet-rules", + "type": "unlimited" + }, + "/api/scan/v0/ans-entries": { + "name": "ans-entries", + "type": "unlimited" + }, + "/api/scan/v0/ans-rules": { + "name": "ans-rules", + "type": "unlimited" + }, + "/api/scan/v0/backfilling": { + "name": "backfilling", + "type": "unlimited" + }, + "/api/scan/v0/closed-rounds": { + "name": "closed-rounds", + "type": "unlimited" + }, + "/api/scan/v0/domains": { + "name": "domains", + "type": "unlimited" + }, + "/api/scan/v0/dso": { + "name": "dso-info", + "type": "unlimited" + }, + "/api/scan/v0/dso-party-id": { + "name": "dso-party-id", + "type": "unlimited" + }, + "/api/scan/v0/dso-sequencers": { + "name": "dso-sequencers", + "type": "unlimited" + }, + "/api/scan/v0/events": { + "name": "events", + "type": "unlimited" + }, + "/api/scan/v0/external-party-amulet-rules": { + "name": "external-party-amulet-rules", + "type": "unlimited" + }, + "/api/scan/v0/feature-support": { + "name": "feature-support", + "type": "unlimited" + }, + "/api/scan/v0/featured-apps": { + "name": "featured-apps", + "type": "unlimited" + }, + "/api/scan/v0/history/bulk/acs": { + "name": "listBulkAcsSnapshotObjects", + "type": "unlimited" + }, + "/api/scan/v0/history/bulk/checksums": { + "name": "getBulkHistoryChecksums", + "type": "unlimited" + }, + "/api/scan/v0/history/bulk/updates": { + "name": "listBulkUpdateHistoryObjects", + "type": "unlimited" + }, + "/api/scan/v0/holdings": { + "name": "holdings", + "type": "unlimited" + }, + "/api/scan/v0/internal/reward-accounting-process": { + "name": "reward-accounting-process", + "type": "unlimited" + }, + "/api/scan/v0/lsu": { + "name": "lsu", + "type": "unlimited" + }, + "/api/scan/v0/migrations": { + "name": "migrations-schedule", + "type": "unlimited" + }, + "/api/scan/v0/open-and-issuing-mining-rounds": { + "name": "open-and-issuing-mining-rounds", + "type": "unlimited" + }, + "/api/scan/v0/roll-forward-lsu": { + "name": "rollForwardLsu", + "type": "unlimited" + }, + "/api/scan/v0/scans": { + "name": "scans", + "type": "unlimited" + }, + "/api/scan/v0/splice-instance-names": { + "name": "splice-instance-names", + "type": "unlimited" + }, + "/api/scan/v0/state": { + "name": "state", + "type": "unlimited" + }, + "/api/scan/v0/sv-bft-sequencers": { + "name": "bft-sequencers", + "type": "unlimited" + }, + "/api/scan/v0/synchronizer-bootstrapping-transactions": { + "name": "synchronizer-bootstrapping-transactions", + "type": "unlimited" + }, + "/api/scan/v0/synchronizer-identities": { + "name": "synchronizer-identities", + "type": "unlimited" + }, + "/api/scan/v0/transfer-command": { + "name": "transfer-command-status", + "type": "unlimited" + }, + "/api/scan/v0/transfer-command-counter": { + "name": "transfer-command-counter", + "type": "unlimited" + }, + "/api/scan/v0/transfer-preapprovals": { + "name": "transfer-preapprovals", + "type": "unlimited" + }, + "/api/scan/v0/unclaimed-development-fund-coupons": { + "name": "unclaimed-development-fund-coupons", + "type": "unlimited" + }, + "/api/scan/v0/updates": { + "name": "v0-updates", + "type": "unlimited" + }, + "/api/scan/v0/validators": { + "name": "validators", + "type": "unlimited" + }, + "/api/scan/v0/voterequest": { + "name": "voterequest", + "type": "unlimited" + }, + "/api/scan/v0/voterequests": { + "name": "voterequests", + "type": "unlimited" + }, + "/api/scan/v1/domains": { + "name": "domains-v1", + "type": "unlimited" + }, + "/api/scan/v1/holdings": { + "name": "holdings", + "type": "unlimited" + }, + "/api/scan/v1/state": { + "name": "state", + "type": "unlimited" + }, + "/api/scan/v1/updates": { + "name": "v1-updates", + "type": "banned" + }, + "/api/scan/v2/holdings": { + "name": "holdings", + "type": "unlimited" + }, + "/api/scan/v2/state": { + "name": "state", + "type": "unlimited" + }, + "/api/scan/v2/updates": { + "name": "v2-updates", + "type": "unlimited" + }, + "/api/scan/version": { + "name": "version", + "type": "unlimited" + }, + "/registry/allocation-instruction/v1/allocation-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-factory", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation-instruction/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-instruction-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation-instruction/v2/allocation-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocation-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocation/v2/settlement-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-settlement-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocations/v1": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocations", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/allocations/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-allocations-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/metadata/v1/info": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-metadata-info", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "overrides": { + "test": { + "fillInterval": "60s", + "ips": [ + "192.68.78.50" + ], + "maxTokens": 250, + "tokensPerFill": 250 + } + }, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/metadata/v1/instruments": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-metadata-instruments", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v1": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-instruction", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v1/transfer-factory": { + "fillInterval": "60s", + "maxTokens": 1440, + "name": "registry-transfer-factory", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 240, + "tokensPerFill": 240 + }, + "tokensPerFill": 1440, + "type": "limited" + }, + "/registry/transfer-instruction/v2": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-instruction-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + }, + "/registry/transfer-instruction/v2/transfer-factory": { + "fillInterval": "60s", + "maxTokens": 720, + "name": "registry-transfer-factory-v2", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 120, + "tokensPerFill": 120 + }, + "tokensPerFill": 720, + "type": "limited" + } + } + }, + "name": "splice-sv-da-1-scan-app-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "accessKey": "", + "secretAccessKey": "" + } + }, + "kind": "Secret", + "metadata": { + "name": "splice-app-bulk-storage-credentials", + "namespace": "sv-1" + }, + "type": "Opaque" + }, + "name": "sv-1-bulk-storage-credentials", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "length": 16, + "overrideSpecial": "_%@", + "special": true + }, + "name": "sv-1-cn-apps-pg-cnadmin-passwd", + "provider": "", + "type": "random:index/randomPassword:RandomPassword" + }, + { + "custom": false, + "id": "", + "inputs": { + "active": true, + "alias": "cn-apps-pg", + "cloudSqlConfig": { + "databaseVersion": "POSTGRES_14", + "enabled": true, + "enterprisePlus": false, + "flags": { + "maintenance_work_mem": "2000000", + "max_wal_size": "20480", + "random_page_cost": "1.1", + "temp_file_limit": "2147483647", + "work_mem": "16384" + }, + "maintenanceWindow": { + "day": 2, + "hour": 8 + }, + "protected": true, + "tier": "apps-pg-override-tier-sv-1" + }, + "defaultUserName": "cnadmin", + "deletionProtection": true, + "instanceName": "cn-apps-pg", + "logicalDecoding": true, + "namespace": { + "logicalName": "sv-1", + "ns": { + "__aliases": [], + "__name": "sv-1", + "__providers": {}, + "__pulumiCustomResource": true, + "__pulumiResource": true, + "__pulumiType": "kubernetes:core/v1:Namespace", + "__transformations": [], + "__version": "4.28.0", + "apiVersion": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "id": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "kind": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "metadata": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "spec": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "status": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "urn": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi." + } + }, + "retainDbResourcesOnDelete": false, + "secretName": "cn-apps-pg-secrets" + }, + "name": "sv-1-cn-apps-pg", + "provider": "", + "type": "canton:cloud:postgres" + }, + { + "custom": true, + "id": "", + "inputs": { + "databaseVersion": "POSTGRES_14", + "deletionProtection": false, + "region": "europe-west6", + "settings": { + "activationPolicy": "ALWAYS", + "backupConfiguration": { + "enabled": true, + "pointInTimeRecoveryEnabled": true + }, + "databaseFlags": [ + { + "name": "random_page_cost", + "value": "1.1" + }, + { + "name": "temp_file_limit", + "value": "2147483647" + }, + { + "name": "max_wal_size", + "value": "20480" + }, + { + "name": "maintenance_work_mem", + "value": "2000000" + }, + { + "name": "work_mem", + "value": "16384" + }, + { + "name": "cloudsql.logical_decoding", + "value": "on" + } + ], + "deletionProtectionEnabled": true, + "edition": "ENTERPRISE", + "insightsConfig": { + "enhancedQueryInsightsEnabled": false, + "queryInsightsEnabled": true + }, + "ipConfiguration": { + "enablePrivatePathForGoogleCloudServices": true, + "ipv4Enabled": false, + "privateNetwork": "projects/test-project/global/networks/default" + }, + "locationPreference": { + "zone": "europe-west6-a" + }, + "maintenanceWindow": { + "day": 2, + "hour": 8 + }, + "tier": "apps-pg-override-tier-sv-1", + "userLabels": { + "cluster": "mock" + } + } + }, + "name": "sv-1-cn-apps-pg", + "provider": "", + "type": "gcp:sql/databaseInstance:DatabaseInstance" + }, + { + "custom": true, + "id": "", + "inputs": { + "name": "cantonnet" + }, + "name": "sv-1-db-cn-apps-pg-cantonnet", + "provider": "", + "type": "gcp:sql/database:Database" + }, + { + "custom": true, + "id": "", + "inputs": { + "name": "cantonnet" + }, + "name": "sv-1-db-participant-pg-cantonnet", + "provider": "", + "type": "gcp:sql/database:Database" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "imagePullSecrets": [ + { + "name": "docker-reg-cred" + } + ], + "kind": "ServiceAccount", + "metadata": { + "name": "default", + "namespace": "sv-1" + } + }, + "name": "sv-1-default", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", + "type": "kubernetes:core/v1:ServiceAccountPatch" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "docker-reg-cred", + "namespace": "sv-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + } + }, + "type": "kubernetes.io/dockerconfigjson" + }, + "name": "sv-1-docker-reg-cred", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-info", + "compat": "true", + "maxHistory": 10, + "name": "info", + "namespace": "sv-1", + "timeout": 600, + "values": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "deploymentDetails": { + "configDigest": { + "allowedIpRanges": { + "type": "md5", + "value": "90eedde4a8599204a45dcb972e212c8b" + }, + "approvedSvIdentities": { + "type": "md5", + "value": "6c876b92df2d92fe7ba83e883d031386" + } + }, + "network": "test", + "sv": { + "version": "0.3.20" + }, + "synchronizer": { + "current": { + "chainIdSuffix": "4", + "migrationId": 2, + "synchronizerSerialId": 9, + "version": "0.3.20" + }, + "legacy": { + "chainIdSuffix": "4", + "migrationId": 2, + "synchronizerSerialId": 8, + "version": "0.3.20" + }, + "successor": { + "chainIdSuffix": "4", + "migrationId": 2, + "synchronizerSerialId": 10, + "version": "0.3.21" + } + } + }, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "istioVirtualService": { + "gateway": "cluster-ingress/cn-http-gateway", + "host": "info.sv-2.mock.global.canton.network.digitalasset.com" + }, + "runtimeDetails": { + "scanUrl": "http://scan-app.sv-1:5012", + "synchronizerSerialId": 9 + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + }, + "version": "0.3.20" + }, + "name": "sv-1-info", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-cluster-ingress-runbook", + "compat": "true", + "maxHistory": 10, + "name": "ingress-sv", + "namespace": "sv-1", + "timeout": 600, + "values": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet", + "svIngressName": "sv-2", + "svNamespace": "sv-1" + }, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "ingress": { + "decentralizedSynchronizer": { + "migrationIds": [ + "9", + "8", + "7", + "10" + ] + } + }, + "rateLimit": { + "scan": { + "enable": false + } + }, + "spliceDomainNames": { + "nameServiceDomain": "cns" + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ], + "withSvIngress": true + }, + "version": "0.3.20" + }, + "name": "sv-1-ingress-sv", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "length": 16, + "overrideSpecial": "_%@", + "special": true + }, + "name": "sv-1-participant-pg-cnadmin-passwd", + "provider": "", + "type": "random:index/randomPassword:RandomPassword" + }, + { + "custom": false, + "id": "", + "inputs": { + "active": true, + "alias": "participant-pg", + "cloudSqlConfig": { + "databaseVersion": "POSTGRES_14", + "enabled": true, + "enterprisePlus": false, + "flags": { + "maintenance_work_mem": "2000000", + "max_wal_size": "20480", + "random_page_cost": "1.1", + "temp_file_limit": "2147483647", + "work_mem": "16384" + }, + "maintenanceWindow": { + "day": 2, + "hour": 8 + }, + "protected": true, + "tier": "db-custom-2-7680" + }, + "defaultUserName": "cnadmin", + "deletionProtection": true, + "disableProtection": false, + "instanceName": "participant-pg", + "logicalDecoding": false, + "namespace": { + "logicalName": "sv-1", + "ns": { + "__aliases": [], + "__name": "sv-1", + "__providers": {}, + "__pulumiCustomResource": true, + "__pulumiResource": true, + "__pulumiType": "kubernetes:core/v1:Namespace", + "__transformations": [], + "__version": "4.28.0", + "apiVersion": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "id": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "kind": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "metadata": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "spec": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "status": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "urn": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi." + } + }, + "retainDbResourcesOnDelete": false, + "secretName": "participant-pg-secrets" + }, + "name": "sv-1-participant-pg", + "provider": "", + "type": "canton:cloud:postgres" + }, + { + "custom": true, + "id": "", + "inputs": { + "databaseVersion": "POSTGRES_14", + "deletionProtection": false, + "region": "europe-west6", + "settings": { + "activationPolicy": "ALWAYS", + "backupConfiguration": { + "enabled": true, + "pointInTimeRecoveryEnabled": true + }, + "databaseFlags": [ + { + "name": "random_page_cost", + "value": "1.1" + }, + { + "name": "temp_file_limit", + "value": "2147483647" + }, + { + "name": "max_wal_size", + "value": "20480" + }, + { + "name": "maintenance_work_mem", + "value": "2000000" + }, + { + "name": "work_mem", + "value": "16384" + } + ], + "deletionProtectionEnabled": true, + "edition": "ENTERPRISE", + "insightsConfig": { + "enhancedQueryInsightsEnabled": false, + "queryInsightsEnabled": true + }, + "ipConfiguration": { + "enablePrivatePathForGoogleCloudServices": true, + "ipv4Enabled": false, + "privateNetwork": "projects/test-project/global/networks/default" + }, + "locationPreference": { + "zone": "europe-west6-a" + }, + "maintenanceWindow": { + "day": 2, + "hour": 8 + }, + "tier": "db-custom-2-7680", + "userLabels": { + "cluster": "mock" + } + } + }, + "name": "sv-1-participant-pg", + "provider": "", + "type": "gcp:sql/databaseInstance:DatabaseInstance" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-participant", + "compat": "true", + "maxHistory": 10, + "name": "participant", + "namespace": "sv-1", + "timeout": 600, + "values": { + "additionalEnvVars": [ + { + "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", + "value": "# Ignore missing ACS commitment and commitment mismatches\ncanton.participants.participant.parameters.stores.safe-to-prune-commitment-state = \"all\"\n" + }, + { + "name": "CUSTOM_MOCK_ENV_VAR_NAME", + "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomParticipantJvmFlag", + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "auth": { + "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json", + "targetAudience": "https://canton.network.global" + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "enableHealthProbes": true, + "enablePostgresMetrics": true, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "logAsyncFlush": false, + "logLevel": "INFO", + "logLevelStdout": "DEBUG", + "metrics": { + "enable": true + }, + "participantAdminUserNameFrom": { + "secretKeyRef": { + "key": "ledger-api-user", + "name": "splice-app-sv-ledger-api-user", + "optional": false + } + }, + "persistence": { + "databaseName": "participant_2", + "port": 5432, + "postgresName": "participant-pg", + "schema": "participant", + "secretName": "participant-pg-secrets" + }, + "resources": { + "limits": { + "memory": "18Gi" + }, + "requests": { + "memory": "12Gi" + } + }, + "serviceAccountName": "sv", + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + }, + "version": "0.3.20" + }, + "name": "sv-1-participant", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "scan-app-rate-limit-access-log", + "namespace": "sv-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.code == 429" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "scan-app" + } + } + } + }, + "name": "sv-1-scan-app-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "scan-app-rate-limit", + "namespace": "sv-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager" + } + } + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5012", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "acs", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/api/scan/v0/acs" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "acs", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/api/scan/v0/acs" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v1" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v1" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-info", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/info" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-info", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/info" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-instruments", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/instruments" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-instruments", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/instruments" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v1/allocation-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v1/allocation-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-settlement-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation/v2/settlement-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-settlement-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation/v2/settlement-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "acs" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 500, + "tokens_per_fill": 500 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "acs" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10, + "tokens_per_fill": 5 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address", + "value": "192.68.78.50/32" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 250, + "tokens_per_fill": 250 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-instruments" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-instruments" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 1440, + "tokens_per_fill": 1440 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 240, + "tokens_per_fill": 240 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-settlement-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-settlement-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-instruction-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-instruction-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "true" + } + } + ], + "stat_prefix": "http_local_rate_limiter", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 2147483647, + "tokens_per_fill": 2147483647 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "scan-app" + } + } + } + }, + "name": "sv-1-scan-app-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-scan", + "compat": "true", + "maxHistory": 10, + "name": "scan", + "namespace": "sv-1", + "timeout": 600, + "values": { + "additionalEnvVars": [ + { + "name": "CUSTOM_MOCK_ENV_VAR_NAME", + "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + }, + { + "name": "ADDITIONAL_CONFIG_CLIENT_IP_HEADERS", + "value": "canton.scan-apps.scan-app.parameters.rate-limiting.client-ip-headers = [\"x-envoy-external-address\"]\n" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomScanAppJvmFlag", + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "apiRequestLogLevel": "DEBUG", + "bulkStorage": { + "committed": { + "bucketName": "mock-sv-1-bulk-committed", + "endpoint": "https://storage.googleapis.com", + "region": "europe-west6", + "secretName": "splice-app-bulk-storage-credentials" + }, + "staging": { + "bucketName": "mock-sv-1-bulk-staging", + "endpoint": "https://storage.googleapis.com", + "region": "europe-west6", + "secretName": "splice-app-bulk-storage-credentials" + } + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "enablePostgresMetrics": true, + "failOnAppVersionMismatch": true, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "isFirstSv": true, + "logAsyncFlush": false, + "logLevel": "WARN", + "metrics": { + "enable": true + }, + "participantAddress": "participant", + "persistence": { + "databaseName": "scan_sv_1", + "port": 5432, + "postgresName": "cn-apps-pg", + "schema": "scan_sv_1", + "secretName": "cn-apps-pg-secrets", + "user": "cnadmin" + }, + "publicUrl": "https://scan.sv-2.mock.global.canton.network.digitalasset.com", + "resources": { + "limits": { + "memory": "2048Mi" + }, + "requests": { + "cpu": "0.5", + "memory": "1536Mi" + } + }, + "spliceInstanceNames": { + "amuletName": "Amulet", + "amuletNameAcronym": "AMT", + "nameServiceName": "Amulet Name Service", + "nameServiceNameAcronym": "ANS", + "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", + "networkName": "Splice" + }, + "synchronizers": { + "current": { + "cantonBft": { + "p2pUrl": "https://sequencer-p2p-9.sv-2.mock.global.canton.network.digitalasset.com" + }, + "mediator": "global-domain-9-mediator", + "sequencer": "global-domain-9-sequencer" + }, + "legacy": { + "mediator": "global-domain-8-mediator", + "sequencer": "global-domain-8-sequencer" + }, + "successor": { + "cantonBft": { + "p2pUrl": "https://sequencer-p2p-10.sv-2.mock.global.canton.network.digitalasset.com" + }, + "mediator": "global-domain-10-mediator", + "sequencer": "global-domain-10-sequencer" + } + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + }, + "version": "0.3.20" + }, + "name": "sv-1-scan", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-sv-node", + "compat": "true", + "maxHistory": 10, + "name": "sv-app", + "namespace": "sv-1", + "timeout": 600, + "values": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "additionalEnvVars": [ + { + "name": "CUSTOM_MOCK_ENV_VAR_NAME", + "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + }, + { + "name": "ADDITIONAL_CONFIG_MEDIATOR_PRUNING", + "value": "canton.sv-apps.sv.local-synchronizer-nodes.current.mediator.pruning {\n cron = \"0 0 * * *\"\n max-duration = \"30m\"\n retention = \"7d\"\n }" + }, + { + "name": "ADDITIONAL_CONFIG_CANTON_BFT_PRUNING", + "value": "canton.sv-apps.sv.local-synchronizer-nodes.current.sequencer.canton-bft-pruning {\n cron = \"0 /10 * * * ?\"\n max-duration = \"5m\"\n retention = \"15 days\"\n }" + }, + { + "name": "ADDITIONAL_CONFIG_ADDITIONAL_PACKAGES_TO_UNVET", + "value": "canton.sv-apps.sv.additional-packages-to-unvet.\"splice-wallet-payments\" = [\"0.1.15\", \"0.1.16\"]\ncanton.sv-apps.sv.additional-packages-to-unvet.\"splice-amulet\" = [\"0.1.15\"]" + }, + { + "name": "ADDITIONAL_CONFIG_CLIENT_IP_HEADERS", + "value": "canton.sv-apps.sv.parameters.rate-limiting.client-ip-headers = [\"x-envoy-external-address\"]\n" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSvAppJvmFlag", + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "apiRequestLogLevel": "DEBUG", + "approvedSvIdentities": [ + { + "name": "Digital-Asset-2", + "publicKey": "sv1-id-public-key", + "rewardWeightBps": 100000 + }, + { + "name": "SV1", + "publicKey": "PUBLIC_KEY_1==", + "rewardWeightBps": 1000000 + }, + { + "name": "SV2", + "publicKey": "PUBLIC_KEY_2==", + "rewardWeightBps": 150000 + }, + { + "name": "Digital-Asset-1", + "publicKey": "svda1-mock-id-public-key", + "rewardWeightBps": 150000 + }, + { + "name": "DA-Helm-Test-Node", + "publicKey": "sv-id-public-key", + "rewardWeightBps": 10000 + } + ], + "auth": { + "audience": "https://canton.network.global", + "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json" + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "contactPoint": "sv-support@digitalasset.com", + "delegatelessAutomationExpectedTaskDuration": 5000, + "delegatelessAutomationExpiredRewardCouponBatchSize": 100, + "delegatelessAutomationExpiredRewardCouponNumBatches": 100, + "disableOnboardingParticipantPromotionDelay": false, + "enablePostgresMetrics": true, + "expectedValidatorOnboardings": [ + { + "expiresIn": "24h", + "secretFrom": { + "secretKeyRef": { + "key": "secret", + "name": "splice-app-validator-onboarding-splitwell2", + "optional": false + } + } + }, + { + "expiresIn": "24h", + "secretFrom": { + "secretKeyRef": { + "key": "secret", + "name": "splice-app-validator-onboarding-validator12", + "optional": false + } + } + } + ], + "failOnAppVersionMismatch": true, + "identitiesExport": { + "bucket": { + "bucketName": "da-cn-data-dumps", + "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", + "projectId": "da-cn-devnet", + "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" + }, + "prefix": "mock/sv-1" + }, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "initialAmuletPrice": "0.0517", + "initialPackageConfigJson": "{\"amuletVersion\": \"0.1.4\",\"amuletNameServiceVersion\": \"0.1.4\",\"dsoGovernanceVersion\": \"0.1.7\",\"validatorLifecycleVersion\": \"0.1.1\",\"walletVersion\": \"0.1.4\",\"walletPaymentsVersion\": \"0.1.4\"}", + "initialRound": "0", + "initialSynchronizerFeesConfig": { + "baseRateBurstAmount": 200000, + "baseRateBurstWindowMins": 20, + "extraTrafficPrice": 16.67, + "minTopupAmount": 200000, + "readVsWriteScalingFactor": 4 + }, + "isDevNet": false, + "logAsyncFlush": false, + "logLevel": "WARN", + "maxVettingDelay": "1m", + "metrics": { + "enable": true + }, + "nodeIdentifier": "Digital-Asset-2", + "onboardingFoundingSvRewardWeightBps": 100000, + "onboardingName": "Digital-Asset-2", + "onboardingRoundZeroDuration": "2 h", + "onboardingType": "found-dso", + "participantAddress": "participant", + "periodicTopologySnapshotConfig": { + "backupInterval": "12h", + "location": { + "bucket": { + "bucketName": "cn-topology-snapshots", + "jsonCredentials": "{\"projectId\":\"da-cn-shared\",\"bucketName\":\"topology-snapshot-bucket-name\",\"secretName\":\"gcp-topology-snapshot-bucket-sa-key-secret\",\"jsonCredentials\":\"topology-snapshot-bucket-sa-key-secret-creds\",\"bucketSaKeySecret\":\"gcp-topology-snapshot-bucket-sa-key-example\",\"bucketSaIamAccount\":\"da-cn-examplet@da-cn-shared.iam.gserviceaccount.com\"}", + "projectId": "da-cn-shared", + "secretName": "cn-gcp-bucket-da-cn-shared-cn-topology-snapshots" + }, + "prefix": "mock/sv-1" + } + }, + "permissionedSynchronizer": false, + "persistence": { + "databaseName": "sv_sv_1", + "port": 5432, + "postgresName": "cn-apps-pg", + "schema": "sv_sv_1", + "secretName": "cn-apps-pg-secrets", + "user": "cnadmin" + }, + "pvc": { + "volumeName": "sv-app-global-domain-migration-hd-pvc", + "volumeStorageClass": "hyperdisk-standard-rwo" + }, + "resources": { + "limits": { + "memory": "2Gi" + }, + "requests": { + "cpu": "1", + "memory": "1Gi" + } + }, + "scan": { + "internalUrl": "http://scan-app.sv-1:5012", + "publicUrl": "https://scan.sv-2.mock.global.canton.network.digitalasset.com" + }, + "spliceInstanceNames": { + "amuletName": "Amulet", + "amuletNameAcronym": "AMT", + "nameServiceName": "Amulet Name Service", + "nameServiceNameAcronym": "ANS", + "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", + "networkName": "Splice" + }, + "synchronizers": { + "additionalLegacy": [ + { + "cometBFT": { + "connectionUri": "http://global-domain-7-cometbft-cometbft-rpc:26657", + "enabled": true + }, + "mediatorAddress": "global-domain-7-mediator", + "sequencerAddress": "global-domain-7-sequencer", + "sequencerPruningConfig": { + "enabled": true, + "pruningInterval": "1 hour", + "retentionPeriod": "30 days" + }, + "sequencerPublicUrl": "https://sequencer-7.sv-2.mock.global.canton.network.digitalasset.com" + } + ], + "current": { + "enableBftSequencer": true, + "mediatorAddress": "global-domain-9-mediator", + "sequencerAddress": "global-domain-9-sequencer", + "sequencerPruningConfig": { + "enabled": true, + "pruningInterval": "1 hour", + "retentionPeriod": "30 days" + }, + "sequencerPublicUrl": "https://sequencer-9.sv-2.mock.global.canton.network.digitalasset.com" + }, + "legacy": { + "cometBFT": { + "connectionUri": "http://global-domain-8-cometbft-cometbft-rpc:26657", + "enabled": true + }, + "mediatorAddress": "global-domain-8-mediator", + "sequencerAddress": "global-domain-8-sequencer", + "sequencerPruningConfig": { + "enabled": true, + "pruningInterval": "1 hour", + "retentionPeriod": "30 days" + }, + "sequencerPublicUrl": "https://sequencer-8.sv-2.mock.global.canton.network.digitalasset.com" + }, + "skipInitialization": true, + "successor": { + "enableBftSequencer": true, + "mediatorAddress": "global-domain-10-mediator", + "sequencerAddress": "global-domain-10-sequencer", + "sequencerPruningConfig": { + "enabled": true, + "pruningInterval": "1 hour", + "retentionPeriod": "30 days" + }, + "sequencerPublicUrl": "https://sequencer-10.sv-2.mock.global.canton.network.digitalasset.com" + } + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + } + }, + "version": "0.3.20" + }, + "name": "sv-1-sv-app", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "sv-docker-reg-cred", + "namespace": "sv-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + } + }, + "type": "kubernetes.io/dockerconfigjson" + }, + "name": "sv-1-sv-docker-reg-cred", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "imagePullSecrets": [ + { + "name": "sv-docker-reg-cred" + } + ], + "kind": "ServiceAccount", + "metadata": { + "name": "sv", + "namespace": "sv-1" + } + }, + "name": "sv-1-sv", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", + "type": "kubernetes:core/v1:ServiceAccountPatch" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-validator", + "compat": "true", + "maxHistory": 10, + "name": "validator-sv-1", + "namespace": "sv-1", + "timeout": 600, + "values": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "additionalEnvVars": [ + { + "name": "ADDITIONAL_CONFIG_TOPOLOGY_METRICS_EXPORT", + "value": "canton.validator-apps.validator_backend.automation.topology-metrics-polling-interval = 5m\n" + }, + { + "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", + "value": "canton.validator-apps.validator_backend.participant-pruning-schedule {\n cron = \"0 /10 * * * ?\"\n max-duration = \"5m\"\n retention = \"30d\"\n }" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1", + "additionalUsers": [], + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "apiRequestLogLevel": "DEBUG", + "appDars": [], + "auth": { + "audience": "https://canton.network.global", + "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json" + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "contactPoint": "sv-support@digitalasset.com", + "disableAllocateLedgerApiUserParty": true, + "disableAuth": false, + "enablePostgresMetrics": true, + "failOnAppVersionMismatch": true, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "logAsyncFlush": false, + "logLevel": "WARN", + "maxVettingDelay": "1m", + "metrics": { + "enable": true + }, + "nodeIdentifier": "Digital-Asset-2", + "participantAddress": "participant", + "participantIdentitiesDumpPeriodicBackup": { + "backupInterval": "10m", + "location": { + "bucket": { + "bucketName": "da-cn-data-dumps", + "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", + "projectId": "da-cn-devnet", + "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" + }, + "prefix": "mock/sv-1" + } + }, + "persistence": { + "databaseName": "validator_sv_1", + "port": 5432, + "postgresName": "cn-apps-pg", + "schema": "validator_sv_1", + "secretName": "cn-apps-pg-secrets", + "user": "cnadmin" + }, + "pvc": { + "volumeName": "domain-migration-validator-hd-pvc", + "volumeStorageClass": "hyperdisk-standard-rwo" + }, + "resources": { + "limits": { + "memory": "4Gi" + }, + "requests": { + "memory": "2Gi" + } + }, + "scanAddress": "http://scan-app.sv-1:5012", + "spliceInstanceNames": { + "amuletName": "Amulet", + "amuletNameAcronym": "AMT", + "nameServiceName": "Amulet Name Service", + "nameServiceNameAcronym": "ANS", + "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", + "networkName": "Splice" + }, + "svValidator": true, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ], + "topup": { + "enabled": true, + "minTopupInterval": "1m", + "targetThroughput": 0 + }, + "useSequencerConnectionsFromScan": true, + "validatorWalletUsers": [ + "google-oauth2|1234567890", + "auth0|64529b128448ded6aa68048f" + ], + "walletSweep": { + "mock::11111111111111111111111111111111111111111111111111111111111111111111": { + "maxBalanceUSD": 12345, + "minBalanceUSD": 17, + "receiver": "mock-2::222222222222222222222222222222222222222222222222222222222222222222222" + } + } + } + }, + "version": "0.3.20" + }, + "name": "sv-1-validator-sv-1", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "istio-injection": "enabled" + }, + "name": "sv-1" + } + }, + "name": "sv-1", + "provider": "", + "type": "kubernetes:core/v1:Namespace" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "data": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "accessKey": "", + "secretAccessKey": "" + } + }, + "kind": "Secret", + "metadata": { + "name": "splice-app-bulk-storage-credentials", + "namespace": "sv-da-1" + }, + "type": "Opaque" + }, + "name": "sv-da-1-bulk-storage-credentials", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "length": 16, + "overrideSpecial": "_%@", + "special": true + }, + "name": "sv-da-1-cn-apps-pg-cnadmin-passwd", + "provider": "", + "type": "random:index/randomPassword:RandomPassword" + }, + { + "custom": false, + "id": "", + "inputs": { + "active": true, + "alias": "cn-apps-pg", + "cloudSqlConfig": { + "databaseVersion": "POSTGRES_14", + "enabled": true, + "enterprisePlus": false, + "flags": { + "maintenance_work_mem": "2000000", + "max_wal_size": "20480", + "random_page_cost": "1.1", + "temp_file_limit": "2147483647", + "work_mem": "16384" + }, + "maintenanceWindow": { + "day": 2, + "hour": 8 + }, + "protected": true, + "tier": "apps-pg-override-tier" + }, + "defaultUserName": "cnadmin", + "deletionProtection": true, + "instanceName": "cn-apps-pg", + "logicalDecoding": false, + "namespace": { + "logicalName": "sv-da-1", + "ns": { + "__aliases": [], + "__name": "sv-da-1", + "__providers": {}, + "__pulumiCustomResource": true, + "__pulumiResource": true, + "__pulumiType": "kubernetes:core/v1:Namespace", + "__transformations": [], + "__version": "4.28.0", + "apiVersion": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "id": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "kind": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "metadata": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "spec": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "status": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", + "urn": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi." + } + }, + "retainDbResourcesOnDelete": false, + "secretName": "cn-apps-pg-secrets" + }, + "name": "sv-da-1-cn-apps-pg", + "provider": "", + "type": "canton:cloud:postgres" + }, + { + "custom": true, + "id": "", + "inputs": { + "databaseVersion": "POSTGRES_14", + "deletionProtection": false, + "region": "europe-west6", + "settings": { + "activationPolicy": "ALWAYS", + "backupConfiguration": { + "enabled": true, + "pointInTimeRecoveryEnabled": true + }, + "databaseFlags": [ + { + "name": "random_page_cost", + "value": "1.1" + }, + { + "name": "temp_file_limit", + "value": "2147483647" + }, + { + "name": "max_wal_size", + "value": "20480" + }, + { + "name": "maintenance_work_mem", + "value": "2000000" + }, + { + "name": "work_mem", + "value": "16384" + } + ], + "deletionProtectionEnabled": true, + "edition": "ENTERPRISE", + "insightsConfig": { + "enhancedQueryInsightsEnabled": false, + "queryInsightsEnabled": true + }, + "ipConfiguration": { + "enablePrivatePathForGoogleCloudServices": true, + "ipv4Enabled": false, + "privateNetwork": "projects/test-project/global/networks/default" + }, + "locationPreference": { + "zone": "europe-west6-a" + }, + "maintenanceWindow": { + "day": 2, + "hour": 8 + }, + "tier": "apps-pg-override-tier", + "userLabels": { + "cluster": "mock" + } + } + }, + "name": "sv-da-1-cn-apps-pg", + "provider": "", + "type": "gcp:sql/databaseInstance:DatabaseInstance" + }, + { + "custom": true, + "id": "", + "inputs": { + "name": "cantonnet" + }, + "name": "sv-da-1-db-cn-apps-pg-cantonnet", + "provider": "", + "type": "gcp:sql/database:Database" + }, + { + "custom": true, + "id": "", + "inputs": { + "name": "cantonnet" + }, + "name": "sv-da-1-db-participant-pg-cantonnet", + "provider": "", + "type": "gcp:sql/database:Database" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "imagePullSecrets": [ + { + "name": "docker-reg-cred" + } + ], + "kind": "ServiceAccount", + "metadata": { + "name": "default", + "namespace": "sv-da-1" + } + }, + "name": "sv-da-1-default", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", + "type": "kubernetes:core/v1:ServiceAccountPatch" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "docker-reg-cred", + "namespace": "sv-da-1" + }, + "stringData": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + } + }, + "type": "kubernetes.io/dockerconfigjson" + }, + "name": "sv-da-1-docker-reg-cred", + "provider": "", + "type": "kubernetes:core/v1:Secret" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-info", + "compat": "true", + "maxHistory": 10, + "name": "info", + "namespace": "sv-da-1", + "timeout": 600, + "values": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "deploymentDetails": { + "configDigest": { + "allowedIpRanges": { + "type": "md5", + "value": "90eedde4a8599204a45dcb972e212c8b" + }, + "approvedSvIdentities": { + "type": "md5", + "value": "6c876b92df2d92fe7ba83e883d031386" + } + }, + "network": "test", + "sv": { + "version": "0.3.20" + }, + "synchronizer": { + "current": { + "chainIdSuffix": "4", + "migrationId": 2, + "synchronizerSerialId": 9, + "version": "0.3.20" + }, + "legacy": { + "chainIdSuffix": "4", + "migrationId": 2, + "synchronizerSerialId": 8, + "version": "0.3.20" + }, + "successor": { + "chainIdSuffix": "4", + "migrationId": 2, + "synchronizerSerialId": 10, + "version": "0.3.21" + } + } + }, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "istioVirtualService": { + "gateway": "cluster-ingress/cn-http-gateway", + "host": "info.sv-1.mock.global.canton.network.digitalasset.com" + }, + "runtimeDetails": { + "scanUrl": "http://scan-app.sv-da-1:5012", + "synchronizerSerialId": 9 + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + }, + "version": "0.3.20" + }, + "name": "sv-da-1-info", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-cluster-ingress-runbook", + "compat": "true", + "maxHistory": 10, + "name": "ingress-sv", + "namespace": "sv-da-1", + "timeout": 600, + "values": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet", + "svIngressName": "sv-1", + "svNamespace": "sv-da-1" + }, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "ingress": { + "decentralizedSynchronizer": { + "migrationIds": [ + "9", + "8", + "7", + "10" + ] + } + }, + "rateLimit": { + "scan": { + "enable": false + } + }, + "spliceDomainNames": { + "nameServiceDomain": "cns" + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ], + "withSvIngress": true }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - "ledger-api-user": "sv-client-id@clients" - } - } - }, - "name": "splice-auth0-user-sv-sv-sv", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "name": "cantonnet" + "version": "0.3.20" }, - "name": "sv-1-db-participant-pg-cantonnet", + "name": "sv-da-1-ingress-sv", "provider": "", - "type": "gcp:sql/database:Database" + "type": "kubernetes:helm.sh/v3:Release" }, { "custom": true, @@ -256,7 +5070,7 @@ "overrideSpecial": "_%@", "special": true }, - "name": "sv-1-participant-pg-cnadmin-passwd", + "name": "sv-da-1-participant-pg-cnadmin-passwd", "provider": "", "type": "random:index/randomPassword:RandomPassword" }, @@ -290,10 +5104,10 @@ "instanceName": "participant-pg", "logicalDecoding": false, "namespace": { - "logicalName": "sv-1", + "logicalName": "sv-da-1", "ns": { "__aliases": [], - "__name": "sv-1", + "__name": "sv-da-1", "__providers": {}, "__pulumiCustomResource": true, "__pulumiResource": true, @@ -312,7 +5126,7 @@ "retainDbResourcesOnDelete": false, "secretName": "participant-pg-secrets" }, - "name": "sv-1-participant-pg", + "name": "sv-da-1-participant-pg", "provider": "", "type": "canton:cloud:postgres" }, @@ -354,6 +5168,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -374,7 +5189,7 @@ } } }, - "name": "sv-1-participant-pg", + "name": "sv-da-1-participant-pg", "provider": "", "type": "gcp:sql/databaseInstance:DatabaseInstance" }, @@ -386,10 +5201,18 @@ "compat": "true", "maxHistory": 10, "name": "participant", - "namespace": "sv-1", + "namespace": "sv-da-1", "timeout": 600, "values": { "additionalEnvVars": [ + { + "name": "GOOGLE_APPLICATION_CREDENTIALS", + "value": "/app/gcp-credentials.json" + }, + { + "name": "ADDITIONAL_CONFIG_SESSION_SIGNING_KEYS", + "value": "canton.participants.participant.crypto.session-signing-keys.enabled = true" + }, { "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", "value": "# Ignore missing ACS commitment and commitment mismatches\ncanton.participants.participant.parameters.stores.safe-to-prune-commitment-state = \"all\"\n" @@ -435,7 +5258,28 @@ }, "enableHealthProbes": true, "enablePostgresMetrics": true, + "extraVolumeMounts": [ + { + "mountPath": "/app/gcp-credentials.json", + "name": "gcp-credentials", + "subPath": "googleCredentials" + } + ], + "extraVolumes": [ + { + "name": "gcp-credentials", + "secret": { + "secretName": "gke-credentials" + } + } + ], "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "kms": { + "keyRingId": "sv-da-1_participant_mock", + "locationId": "us-central1", + "projectId": "da-cn-devnet", + "type": "gcp" + }, "logAsyncFlush": false, "logLevel": "INFO", "logLevelStdout": "DEBUG", @@ -475,7 +5319,7 @@ }, "version": "0.3.20" }, - "name": "sv-1-participant", + "name": "sv-da-1-participant", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, @@ -483,227 +5327,1135 @@ "custom": true, "id": "", "inputs": { - "apiVersion": "v1", - "kind": "Secret", + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", "metadata": { - "name": "sv-docker-reg-cred", - "namespace": "sv-1" - }, - "stringData": { - "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", - "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" - } + "name": "scan-app-rate-limit-access-log", + "namespace": "sv-da-1" }, - "type": "kubernetes.io/dockerconfigjson" - }, - "name": "sv-1-sv-docker-reg-cred", - "provider": "", - "type": "kubernetes:core/v1:Secret" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "imagePullSecrets": [ - { - "name": "sv-docker-reg-cred" + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.code == 429" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "scan-app" + } } - ], - "kind": "ServiceAccount", - "metadata": { - "name": "sv", - "namespace": "sv-1" - } - }, - "name": "sv-1-sv", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-1-sv::undefined_id", - "type": "kubernetes:core/v1:ServiceAccountPatch" - }, - { - "custom": true, - "id": "", - "inputs": { - "apiVersion": "v1", - "kind": "Namespace", - "metadata": { - "labels": { - "istio-injection": "enabled" - }, - "name": "sv-1" } }, - "name": "sv-1", - "provider": "", - "type": "kubernetes:core/v1:Namespace" - }, - { - "custom": true, - "id": "", - "inputs": { - "name": "cantonnet" - }, - "name": "sv-da-1-db-participant-pg-cantonnet", + "name": "sv-da-1-scan-app-rate-limit-access-log", "provider": "", - "type": "gcp:sql/database:Database" + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" }, { "custom": true, "id": "", "inputs": { - "length": 16, - "overrideSpecial": "_%@", - "special": true - }, - "name": "sv-da-1-participant-pg-cnadmin-passwd", - "provider": "", - "type": "random:index/randomPassword:RandomPassword" - }, - { - "custom": false, - "id": "", - "inputs": { - "active": true, - "alias": "participant-pg", - "cloudSqlConfig": { - "databaseVersion": "POSTGRES_14", - "enabled": true, - "enterprisePlus": false, - "flags": { - "maintenance_work_mem": "2000000", - "max_wal_size": "20480", - "random_page_cost": "1.1", - "temp_file_limit": "2147483647", - "work_mem": "16384" - }, - "maintenanceWindow": { - "day": 2, - "hour": 8 - }, - "protected": true, - "tier": "db-custom-2-7680" - }, - "defaultUserName": "cnadmin", - "deletionProtection": true, - "disableProtection": false, - "instanceName": "participant-pg", - "logicalDecoding": false, - "namespace": { - "logicalName": "sv-da-1", - "ns": { - "__aliases": [], - "__name": "sv-da-1", - "__providers": {}, - "__pulumiCustomResource": true, - "__pulumiResource": true, - "__pulumiType": "kubernetes:core/v1:Namespace", - "__transformations": [], - "__version": "4.28.0", - "apiVersion": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "id": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "kind": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "metadata": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "spec": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "status": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi.", - "urn": "Calling [toJSON] on an [Output] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://www.pulumi.com/docs/concepts/inputs-outputs for more details.\nThis function may throw in a future version of @pulumi/pulumi." - } + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "scan-app-rate-limit", + "namespace": "sv-da-1" }, - "retainDbResourcesOnDelete": false, - "secretName": "participant-pg-secrets" - }, - "name": "sv-da-1-participant-pg", - "provider": "", - "type": "canton:cloud:postgres" - }, - { - "custom": true, - "id": "", - "inputs": { - "databaseVersion": "POSTGRES_14", - "deletionProtection": false, - "region": "europe-west6", - "settings": { - "activationPolicy": "ALWAYS", - "backupConfiguration": { - "enabled": true, - "pointInTimeRecoveryEnabled": true - }, - "databaseFlags": [ - { - "name": "random_page_cost", - "value": "1.1" - }, - { - "name": "temp_file_limit", - "value": "2147483647" - }, - { - "name": "max_wal_size", - "value": "20480" - }, + "spec": { + "configPatches": [ { - "name": "maintenance_work_mem", - "value": "2000000" + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager" + } + } + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter" + } + } + } + } }, { - "name": "work_mem", - "value": "16384" + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5012", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "acs", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/api/scan/v0/acs" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "acs", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/api/scan/v0/acs" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v1" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v1" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-info", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/info" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-info", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/info" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-instruments", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/instruments" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-metadata-instruments", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/metadata/v1/instruments" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v1/allocation-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v1/allocation-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-settlement-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation/v2/settlement-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-settlement-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation/v2/settlement-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocations-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocations/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-allocation-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-factory-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "registry-transfer-instruction-v2", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "acs" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 500, + "tokens_per_fill": 500 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "acs" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10, + "tokens_per_fill": 5 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address", + "value": "192.68.78.50/32" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 250, + "tokens_per_fill": 250 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-instruments" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-metadata-instruments" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 1440, + "tokens_per_fill": 1440 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 240, + "tokens_per_fill": 240 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-settlement-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-settlement-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocations-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-instruction-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-allocation-instruction-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-factory-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction-v2" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 720, + "tokens_per_fill": 720 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "registry-transfer-instruction-v2" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 120, + "tokens_per_fill": 120 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "true" + } + } + ], + "stat_prefix": "http_local_rate_limiter", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 2147483647, + "tokens_per_fill": 2147483647 + } + } + } + } + } } ], - "deletionProtectionEnabled": true, - "edition": "ENTERPRISE", - "insightsConfig": { - "queryInsightsEnabled": true - }, - "ipConfiguration": { - "enablePrivatePathForGoogleCloudServices": true, - "ipv4Enabled": false, - "privateNetwork": "projects/test-project/global/networks/default" - }, - "locationPreference": { - "zone": "europe-west6-a" - }, - "maintenanceWindow": { - "day": 2, - "hour": 8 - }, - "tier": "db-custom-2-7680", - "userLabels": { - "cluster": "mock" + "workloadSelector": { + "labels": { + "app": "scan-app" + } } } }, - "name": "sv-da-1-participant-pg", + "name": "sv-da-1-scan-app-rate-limit", "provider": "", - "type": "gcp:sql/databaseInstance:DatabaseInstance" + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" }, { "custom": true, "id": "", "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-participant", + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-scan", "compat": "true", "maxHistory": 10, - "name": "participant", + "name": "scan", "namespace": "sv-da-1", "timeout": 600, "values": { "additionalEnvVars": [ - { - "name": "GOOGLE_APPLICATION_CREDENTIALS", - "value": "/app/gcp-credentials.json" - }, - { - "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", - "value": "# Ignore missing ACS commitment and commitment mismatches\ncanton.participants.participant.parameters.stores.safe-to-prune-commitment-state = \"all\"\n" - }, { "name": "CUSTOM_MOCK_ENV_VAR_NAME", "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + }, + { + "name": "ADDITIONAL_CONFIG_CLIENT_IP_HEADERS", + "value": "canton.scan-apps.scan-app.parameters.rate-limiting.client-ip-headers = [\"x-envoy-external-address\"]\n" } ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomParticipantJvmFlag", + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomScanAppJvmFlag", "affinity": { "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { @@ -727,9 +6479,20 @@ } } }, - "auth": { - "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json", - "targetAudience": "https://canton.network.global" + "apiRequestLogLevel": "DEBUG", + "bulkStorage": { + "committed": { + "bucketName": "mock-sv-da-1-bulk-committed", + "endpoint": "https://storage.googleapis.com", + "region": "europe-west6", + "secretName": "splice-app-bulk-storage-credentials" + }, + "staging": { + "bucketName": "mock-sv-da-1-bulk-staging", + "endpoint": "https://storage.googleapis.com", + "region": "europe-west6", + "secretName": "splice-app-bulk-storage-credentials" + } }, "cluster": { "dnsName": "mock.global.canton.network.digitalasset.com", @@ -737,59 +6500,63 @@ "hostname": "mock.global.canton.network.digitalasset.com", "name": "cn-mocknet" }, - "enableHealthProbes": true, "enablePostgresMetrics": true, - "extraVolumeMounts": [ - { - "mountPath": "/app/gcp-credentials.json", - "name": "gcp-credentials", - "subPath": "googleCredentials" - } - ], - "extraVolumes": [ - { - "name": "gcp-credentials", - "secret": { - "secretName": "gke-credentials" - } - } - ], + "failOnAppVersionMismatch": true, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "kms": { - "keyRingId": "sv-da-1_participant_mock", - "locationId": "us-central1", - "projectId": "da-cn-devnet", - "type": "gcp" - }, + "isFirstSv": false, "logAsyncFlush": false, "logLevel": "INFO", - "logLevelStdout": "DEBUG", "metrics": { "enable": true }, - "participantAdminUserNameFrom": { - "secretKeyRef": { - "key": "ledger-api-user", - "name": "splice-app-sv-ledger-api-user", - "optional": false - } - }, + "participantAddress": "participant", "persistence": { - "databaseName": "participant_2", + "databaseName": "scan_sv_da_1", "port": 5432, - "postgresName": "participant-pg", - "schema": "participant", - "secretName": "participant-pg-secrets" + "postgresName": "cn-apps-pg", + "schema": "scan_sv_da_1", + "secretName": "cn-apps-pg-secrets", + "user": "cnadmin" }, + "publicUrl": "https://scan.sv-1.mock.global.canton.network.digitalasset.com", "resources": { "limits": { - "memory": "18Gi" + "memory": "2048Mi" }, "requests": { - "memory": "12Gi" + "cpu": "0.5", + "memory": "1536Mi" + } + }, + "spliceInstanceNames": { + "amuletName": "Amulet", + "amuletNameAcronym": "AMT", + "nameServiceName": "Amulet Name Service", + "nameServiceNameAcronym": "ANS", + "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", + "networkName": "Splice" + }, + "sponsorScanUrl": "http://scan-app.sv-1:5012", + "synchronizers": { + "current": { + "cantonBft": { + "p2pUrl": "https://sequencer-p2p-9.sv-1.mock.global.canton.network.digitalasset.com" + }, + "mediator": "global-domain-9-mediator", + "sequencer": "global-domain-9-sequencer" + }, + "legacy": { + "mediator": "global-domain-8-mediator", + "sequencer": "global-domain-8-sequencer" + }, + "successor": { + "cantonBft": { + "p2pUrl": "https://sequencer-p2p-10.sv-1.mock.global.canton.network.digitalasset.com" + }, + "mediator": "global-domain-10-mediator", + "sequencer": "global-domain-10-sequencer" } }, - "serviceAccountName": "sv", "tolerations": [ { "effect": "NoSchedule", @@ -800,7 +6567,248 @@ }, "version": "0.3.20" }, - "name": "sv-da-1-participant", + "name": "sv-da-1-scan", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-sv-node", + "compat": "true", + "maxHistory": 10, + "name": "sv-app", + "namespace": "sv-da-1", + "timeout": 600, + "values": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "additionalEnvVars": [ + { + "name": "CUSTOM_MOCK_ENV_VAR_NAME", + "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + }, + { + "name": "ADDITIONAL_CONFIG_NO_BFT_SEQUENCER_CONNECTION", + "value": "canton.sv-apps.sv.bft-sequencer-connection = false" + }, + { + "name": "ADDITIONAL_CONFIG_MEDIATOR_PRUNING", + "value": "canton.sv-apps.sv.local-synchronizer-nodes.current.mediator.pruning {\n cron = \"0 0 * * *\"\n max-duration = \"30m\"\n retention = \"7d\"\n }" + }, + { + "name": "ADDITIONAL_CONFIG_CANTON_BFT_PRUNING", + "value": "canton.sv-apps.sv.local-synchronizer-nodes.current.sequencer.canton-bft-pruning {\n cron = \"0 /10 * * * ?\"\n max-duration = \"5m\"\n retention = \"15 days\"\n }" + }, + { + "name": "ADDITIONAL_CONFIG_ADDITIONAL_PACKAGES_TO_UNVET", + "value": "canton.sv-apps.sv.additional-packages-to-unvet.\"splice-wallet-payments\" = [\"0.1.15\", \"0.1.16\"]\ncanton.sv-apps.sv.additional-packages-to-unvet.\"splice-amulet\" = [\"0.1.15\"]" + }, + { + "name": "ADDITIONAL_CONFIG_CLIENT_IP_HEADERS", + "value": "canton.sv-apps.sv.parameters.rate-limiting.client-ip-headers = [\"x-envoy-external-address\"]\n" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSvAppJvmFlag", + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "apiRequestLogLevel": "DEBUG", + "approvedSvIdentities": [ + { + "name": "Digital-Asset-2", + "publicKey": "sv1-id-public-key", + "rewardWeightBps": 100000 + }, + { + "name": "SV1", + "publicKey": "PUBLIC_KEY_1==", + "rewardWeightBps": 1000000 + }, + { + "name": "SV2", + "publicKey": "PUBLIC_KEY_2==", + "rewardWeightBps": 150000 + }, + { + "name": "Digital-Asset-1", + "publicKey": "svda1-mock-id-public-key", + "rewardWeightBps": 150000 + }, + { + "name": "DA-Helm-Test-Node", + "publicKey": "sv-id-public-key", + "rewardWeightBps": 10000 + } + ], + "auth": { + "audience": "https://canton.network.global", + "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json" + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "contactPoint": "sv-support@digitalasset.com", + "decentralizedSynchronizerUrl": "http://global-domain-9-sequencer.sv-1:5008", + "delegatelessAutomationExpectedTaskDuration": 5000, + "delegatelessAutomationExpiredRewardCouponBatchSize": 100, + "delegatelessAutomationExpiredRewardCouponNumBatches": 100, + "disableOnboardingParticipantPromotionDelay": false, + "enablePostgresMetrics": true, + "expectedValidatorOnboardings": [], + "failOnAppVersionMismatch": true, + "identitiesExport": { + "bucket": { + "bucketName": "da-cn-data-dumps", + "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", + "projectId": "da-cn-devnet", + "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" + }, + "prefix": "mock/sv-da-1" + }, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "initialAmuletPrice": "0.0517", + "isDevNet": false, + "joinWithKeyOnboarding": { + "sponsorApiUrl": "http://sv-app.sv-1:5014" + }, + "logAsyncFlush": false, + "logLevel": "INFO", + "maxVettingDelay": "1m", + "metrics": { + "enable": true + }, + "nodeIdentifier": "Digital-Asset-1", + "onboardingName": "Digital-Asset-1", + "onboardingType": "join-with-key", + "participantAddress": "participant", + "permissionedSynchronizer": false, + "persistence": { + "databaseName": "sv_sv_da_1", + "port": 5432, + "postgresName": "cn-apps-pg", + "schema": "sv_sv_da_1", + "secretName": "cn-apps-pg-secrets", + "user": "cnadmin" + }, + "pvc": { + "volumeName": "sv-app-global-domain-migration-hd-pvc", + "volumeStorageClass": "hyperdisk-standard-rwo" + }, + "resources": { + "limits": { + "memory": "2Gi" + }, + "requests": { + "cpu": "1", + "memory": "1Gi" + } + }, + "scan": { + "internalUrl": "http://scan-app.sv-da-1:5012", + "publicUrl": "https://scan.sv-1.mock.global.canton.network.digitalasset.com" + }, + "spliceInstanceNames": { + "amuletName": "Amulet", + "amuletNameAcronym": "AMT", + "nameServiceName": "Amulet Name Service", + "nameServiceNameAcronym": "ANS", + "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", + "networkName": "Splice" + }, + "synchronizers": { + "additionalLegacy": [ + { + "cometBFT": { + "connectionUri": "http://global-domain-7-cometbft-cometbft-rpc:26657", + "enabled": true, + "externalGovernanceKey": true + }, + "mediatorAddress": "global-domain-7-mediator", + "sequencerAddress": "global-domain-7-sequencer", + "sequencerPruningConfig": { + "enabled": true, + "pruningInterval": "1 hour", + "retentionPeriod": "30 days" + }, + "sequencerPublicUrl": "https://sequencer-7.sv-1.mock.global.canton.network.digitalasset.com" + } + ], + "current": { + "enableBftSequencer": true, + "mediatorAddress": "global-domain-9-mediator", + "sequencerAddress": "global-domain-9-sequencer", + "sequencerPruningConfig": { + "enabled": true, + "pruningInterval": "1 hour", + "retentionPeriod": "30 days" + }, + "sequencerPublicUrl": "https://sequencer-9.sv-1.mock.global.canton.network.digitalasset.com" + }, + "legacy": { + "cometBFT": { + "connectionUri": "http://global-domain-8-cometbft-cometbft-rpc:26657", + "enabled": true, + "externalGovernanceKey": true + }, + "mediatorAddress": "global-domain-8-mediator", + "sequencerAddress": "global-domain-8-sequencer", + "sequencerPruningConfig": { + "enabled": true, + "pruningInterval": "1 hour", + "retentionPeriod": "30 days" + }, + "sequencerPublicUrl": "https://sequencer-8.sv-1.mock.global.canton.network.digitalasset.com" + }, + "skipInitialization": true, + "successor": { + "enableBftSequencer": true, + "mediatorAddress": "global-domain-10-mediator", + "sequencerAddress": "global-domain-10-sequencer", + "sequencerPruningConfig": { + "enabled": true, + "pruningInterval": "1 hour", + "retentionPeriod": "30 days" + }, + "sequencerPublicUrl": "https://sequencer-10.sv-1.mock.global.canton.network.digitalasset.com" + } + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + } + }, + "version": "0.3.20" + }, + "name": "sv-da-1-sv-app", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, @@ -817,7 +6825,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -843,9 +6851,165 @@ } }, "name": "sv-da-1-sv", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-da-1-sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-validator", + "compat": "true", + "maxHistory": 10, + "name": "validator-sv-da-1", + "namespace": "sv-da-1", + "timeout": 600, + "values": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": { + "additionalEnvVars": [ + { + "name": "ADDITIONAL_CONFIG_NO_BFT_SEQUENCER_CONNECTION", + "value": "canton.validator-apps.validator_backend.disable-sv-validator-bft-sequencer-connection = true" + }, + { + "name": "ADDITIONAL_CONFIG_TOPOLOGY_METRICS_EXPORT", + "value": "canton.validator-apps.validator_backend.automation.topology-metrics-polling-interval = 5m\n" + }, + { + "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", + "value": "canton.validator-apps.validator_backend.participant-pruning-schedule {\n cron = \"0 0 * * *\"\n max-duration = \"30m\"\n retention = \"7d\"\n }" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1", + "additionalUsers": [], + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "apiRequestLogLevel": "DEBUG", + "appDars": [], + "auth": { + "audience": "https://canton.network.global", + "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json" + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "contactPoint": "sv-support@digitalasset.com", + "disableAllocateLedgerApiUserParty": true, + "disableAuth": false, + "enablePostgresMetrics": true, + "failOnAppVersionMismatch": true, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "logAsyncFlush": false, + "logLevel": "INFO", + "maxVettingDelay": "1m", + "metrics": { + "enable": true + }, + "nodeIdentifier": "Digital-Asset-1", + "participantAddress": "participant", + "participantIdentitiesDumpPeriodicBackup": { + "backupInterval": "10m", + "location": { + "bucket": { + "bucketName": "da-cn-data-dumps", + "jsonCredentials": "{\"bucketName\":\"data-export-bucket-name\",\"secretName\":\"data-export-bucket-sa-key-secret\",\"jsonCredentials\":\"data-export-bucket-sa-key-secret-creds\"}", + "projectId": "da-cn-devnet", + "secretName": "cn-gcp-bucket-da-cn-devnet-da-cn-data-dumps" + }, + "prefix": "mock/sv-da-1" + } + }, + "persistence": { + "databaseName": "validator_sv_da_1", + "port": 5432, + "postgresName": "cn-apps-pg", + "schema": "validator_sv_da_1", + "secretName": "cn-apps-pg-secrets", + "user": "cnadmin" + }, + "pvc": { + "volumeName": "domain-migration-validator-hd-pvc", + "volumeStorageClass": "hyperdisk-standard-rwo" + }, + "resources": { + "limits": { + "memory": "4Gi" + }, + "requests": { + "memory": "2Gi" + } + }, + "scanAddress": "http://scan-app.sv-da-1:5012", + "spliceInstanceNames": { + "amuletName": "Amulet", + "amuletNameAcronym": "AMT", + "nameServiceName": "Amulet Name Service", + "nameServiceNameAcronym": "ANS", + "networkFaviconUrl": "https://www.hyperledger.org/hubfs/hyperledgerfavicon.png", + "networkName": "Splice" + }, + "svValidator": true, + "synchronizer": { + "connectionType": "trust-single", + "url": "https://sequencer-9.sv-2.mock.global.canton.network.digitalasset.com" + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ], + "topup": { + "enabled": true, + "minTopupInterval": "1m", + "targetThroughput": 0 + }, + "useSequencerConnectionsFromScan": true, + "validatorWalletUsers": [ + "google-oauth2|1234567890", + "auth0|68c2c41e6470184569e4521e" + ], + "walletSweep": { + "mock-3::33333333333333333333333333333333333333333333333333333333333333333333": { + "maxBalanceUSD": 67890, + "minBalanceUSD": 42, + "receiver": "mock-4::444444444444444444444444444444444444444444444444444444444444444444444" + } + } + } + }, + "version": "0.3.20" + }, + "name": "sv-da-1-validator-sv-da-1", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, { "custom": true, "id": "projects/da-cn-devnet/locations/us-central1/keyRings/sv-da-1_participant_mock", @@ -991,6 +7155,7 @@ "deletionProtectionEnabled": false, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -1129,7 +7294,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -1155,7 +7320,7 @@ } }, "name": "sv-sv", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-sv-sv::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -1220,6 +7385,20 @@ "provider": "", "type": "kubernetes:core/v1:ServiceAccount" }, + { + "custom": true, + "id": "", + "inputs": { + "name": "cnadmin", + "password": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": null + } + }, + "name": "user-sv-1-cn-apps-pg-cnadmin", + "provider": "", + "type": "gcp:sql/user:User" + }, { "custom": true, "id": "", @@ -1234,6 +7413,20 @@ "provider": "", "type": "gcp:sql/user:User" }, + { + "custom": true, + "id": "", + "inputs": { + "name": "cnadmin", + "password": { + "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", + "value": null + } + }, + "name": "user-sv-da-1-cn-apps-pg-cnadmin", + "provider": "", + "type": "gcp:sql/user:User" + }, { "custom": true, "id": "", diff --git a/cluster/expected/validator-runbook/expected.json b/cluster/expected/validator-runbook/expected.json index 4a9823646f..b774bbe487 100644 --- a/cluster/expected/validator-runbook/expected.json +++ b/cluster/expected/validator-runbook/expected.json @@ -16,7 +16,7 @@ } }, "name": "cluster-ingress-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-cluster-ingress-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-cluster-ingress-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -32,7 +32,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -292,16 +292,6 @@ "provider": "", "type": "kubernetes:networking.istio.io/v1alpha3:VirtualService" }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, { "custom": true, "id": "", @@ -388,7 +378,7 @@ } }, "name": "validator-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-validator-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-validator-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -404,7 +394,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -420,7 +410,7 @@ "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-participant", "compat": "true", "maxHistory": 10, - "name": "participant-2", + "name": "participant", "namespace": "validator", "timeout": 600, "values": { @@ -480,8 +470,8 @@ }, "persistence": { "databaseName": "participant_2", - "host": "postgres.validator.svc.cluster.local", - "postgresName": "postgres", + "host": "postgres-helmless.validator.svc.cluster.local", + "postgresName": "postgres-helmless", "schema": "participant", "secretName": "postgres-secrets" }, @@ -503,7 +493,7 @@ }, "version": "0.3.20" }, - "name": "validator-participant-2", + "name": "validator-participant", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, @@ -548,7 +538,7 @@ "name": "cn-mocknet" }, "config": { - "jsonLedgerApiUrl": "http://participant-2:7575", + "jsonLedgerApiUrl": "http://participant:7575", "keyDirectory": "/keys", "maxParties": 1234, "parallelism": 321, @@ -582,19 +572,55 @@ "custom": true, "id": "", "inputs": { - "length": 16, - "overrideSpecial": "_%@", - "special": true + "apiVersion": "v1", + "data": { + "PGDATA": "/var/lib/postgresql/data/pgdata", + "POSTGRES_DB": "cantonnet", + "POSTGRES_INITDB_ARGS": "--data-checksums", + "POSTGRES_USER": "cnadmin" + }, + "kind": "ConfigMap", + "metadata": { + "name": "postgres-helmless-configuration", + "namespace": "validator" + } }, - "name": "validator-postgres-passwd", + "name": "validator-postgres-helmless-configuration", "provider": "", - "type": "random:index/randomPassword:RandomPassword" + "type": "kubernetes:core/v1:ConfigMap" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "postgres-helmless", + "namespace": "validator" + }, + "spec": { + "ports": [ + { + "name": "postgresdb", + "port": 5432, + "protocol": "TCP" + } + ], + "selector": { + "app": "postgres-helmless" + } + } + }, + "name": "validator-postgres-helmless-svc", + "provider": "", + "type": "kubernetes:core/v1:Service" }, { "custom": false, "id": "", "inputs": {}, - "name": "validator-postgres", + "name": "validator-postgres-helmless", "provider": "", "type": "canton:network:postgres" }, @@ -602,64 +628,186 @@ "custom": true, "id": "", "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-postgres", - "compat": "true", - "maxHistory": 10, - "name": "postgres", - "namespace": "validator", - "timeout": 600, - "values": { - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": { + "name": "postgres-helmless", + "namespace": "validator" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "postgres-helmless" + } + }, + "serviceName": "postgres-helmless", + "template": { + "metadata": { + "labels": { + "app": "postgres-helmless", + "namespace": "validator" + } + }, + "spec": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } ] } ] } - ] - } + } + }, + "containers": [ + { + "args": [ + "-c", + "max_connections=300", + "-c", + "max_wal_size=2GB" + ], + "env": [ + { + "name": "POSTGRES_PASSWORD", + "valueFrom": { + "secretKeyRef": { + "key": "postgresPassword", + "name": "postgres-secrets" + } + } + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "postgres-helmless-configuration" + } + } + ], + "image": "postgres:18", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "exec": { + "command": [ + "psql", + "-U", + "cnadmin", + "-d", + "template1", + "-c", + "SELECT 1" + ] + }, + "failureThreshold": 3, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "name": "postgres-helmless", + "ports": [ + { + "containerPort": 5432, + "name": "postgresdb", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "memory": "12Gi" + }, + "requests": { + "cpu": "0.5", + "memory": "1Gi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "privileged": false, + "runAsGroup": 999, + "runAsNonRoot": true, + "runAsUser": 999 + }, + "volumeMounts": [ + { + "mountPath": "/var/lib/postgresql/data", + "name": "pg-data-hd" + } + ] + } + ], + "restartPolicy": "Always", + "securityContext": { + "fsGroup": 999, + "fsGroupChangePolicy": "OnRootMismatch", + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] } }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "db": { - "pvcTemplateName": "pg-data-hd", - "volumeSize": "200Gi", - "volumeStorageClass": "hyperdisk-standard-rwo" - }, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "persistence": { - "secretName": "postgres-secrets" - }, - "tolerations": [ + "volumeClaimTemplates": [ { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" + "metadata": { + "name": "pg-data-hd" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "200Gi" + } + }, + "storageClassName": "hyperdisk-standard-rwo", + "volumeMode": "Filesystem" + } } ] - }, - "version": "0.3.20" + } }, - "name": "validator-postgres", + "name": "validator-postgres-helmless", "provider": "", - "type": "kubernetes:helm.sh/v3:Release" + "type": "kubernetes:apps/v1:StatefulSet" + }, + { + "custom": true, + "id": "", + "inputs": { + "length": 16, + "overrideSpecial": "_%@", + "special": true + }, + "name": "validator-postgres-passwd", + "provider": "", + "type": "random:index/randomPassword:RandomPassword" }, { "custom": true, @@ -737,7 +885,7 @@ }, "migrateValidatorParty": false, "nodeIdentifier": "validator-runbook", - "participantAddress": "participant-2", + "participantAddress": "participant", "participantIdentitiesDumpPeriodicBackup": { "backupInterval": "10m", "location": { @@ -756,8 +904,8 @@ "retention": "30d" }, "persistence": { - "host": "postgres", - "postgresName": "postgres", + "host": "postgres-helmless.validator.svc.cluster.local", + "postgresName": "postgres-helmless", "secretName": "postgres-secrets" }, "pvc": { diff --git a/cluster/expected/validator1/expected.json b/cluster/expected/validator1/expected.json index ed65f7cc81..a3d4175858 100644 --- a/cluster/expected/validator1/expected.json +++ b/cluster/expected/validator1/expected.json @@ -277,16 +277,6 @@ "provider": "", "type": "cn:gcp:ServiceAccount" }, - { - "custom": true, - "id": "organization/infra/infra.mock", - "inputs": { - "name": "organization/infra/infra.mock" - }, - "name": "organization/infra/infra.mock", - "provider": "", - "type": "pulumi:pulumi:StackReference" - }, { "custom": true, "id": "", @@ -479,9 +469,7 @@ }, "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", "ingress": { - "decentralizedSynchronizer": { - "activeMigrationId": "2" - }, + "decentralizedSynchronizer": {}, "splitwell": true }, "spliceDomainNames": { @@ -539,7 +527,7 @@ } }, "name": "validator1-default", - "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-validator1-default::undefined_id", + "provider": "urn:pulumi:test-stack::test-project::pulumi:providers:kubernetes::k8s-imgpull-validator1-default::04da6b54-80e4-46f7-96ec-b56ff0331ba9", "type": "kubernetes:core/v1:ServiceAccountPatch" }, { @@ -555,7 +543,7 @@ "stringData": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "value": { - ".dockerconfigjson": "{\"auths\":{\"digitalasset-canton-enterprise-docker.jfrog.io\":{\"auth\":\"YXJ0X3VzZXI6czNjcjN0\",\"username\":\"art_user\",\"password\":\"s3cr3t\"},\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" + ".dockerconfigjson": "{\"auths\":{\"us-central1-docker.pkg.dev\":{\"auth\":\"X2pzb25fa2V5OnsidHlwZSI6InNlcnZpY2VfYWNjb3VudCIsInByb2plY3RfaWQiOiJmYWtlLXByb2plY3QiLCJwcml2YXRlX2tleV9pZCI6ImZha2VfaWQiLCJwcml2YXRlX2tleSI6Ii0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuZmFrZVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwiY2xpZW50X2VtYWlsIjoiZmFrZUBmYWtlLXByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJjbGllbnRfaWQiOiJmYWtlLWNsaWVudC1pZCIsImF1dGhfdXJpIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLCJ0b2tlbl91cmkiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92MS9jZXJ0cyIsImNsaWVudF94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9mYWtlJTQwZmFrZS1wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwidW5pdmVyc2VfZG9tYWluIjoiZ29vZ2xlYXBpcy5jb20ifQ==\",\"username\":\"_json_key\",\"password\":\"{\\\"type\\\":\\\"service_account\\\",\\\"project_id\\\":\\\"fake-project\\\",\\\"private_key_id\\\":\\\"fake_id\\\",\\\"private_key\\\":\\\"-----BEGIN PRIVATE KEY-----\\\\nfake\\\\n-----END PRIVATE KEY-----\\\\n\\\",\\\"client_email\\\":\\\"fake@fake-project.iam.gserviceaccount.com\\\",\\\"client_id\\\":\\\"fake-client-id\\\",\\\"auth_uri\\\":\\\"https://accounts.google.com/o/oauth2/auth\\\",\\\"token_uri\\\":\\\"https://oauth2.googleapis.com/token\\\",\\\"auth_provider_x509_cert_url\\\":\\\"https://www.googleapis.com/oauth2/v1/certs\\\",\\\"client_x509_cert_url\\\":\\\"https://www.googleapis.com/robot/v1/metadata/x509/fake%40fake-project.iam.gserviceaccount.com\\\",\\\"universe_domain\\\":\\\"googleapis.com\\\"}\"}}}" } }, "type": "kubernetes.io/dockerconfigjson" @@ -564,125 +552,6 @@ "provider": "", "type": "kubernetes:core/v1:Secret" }, - { - "custom": true, - "id": "", - "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-participant", - "compat": "true", - "maxHistory": 10, - "name": "participant-2", - "namespace": "validator1", - "timeout": 600, - "values": { - "additionalEnvVars": [ - { - "name": "GOOGLE_APPLICATION_CREDENTIALS", - "value": "/app/gcp-credentials.json" - }, - { - "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", - "value": "# Ignore missing ACS commitment and commitment mismatches\ncanton.participants.participant.parameters.stores.safe-to-prune-commitment-state = \"all\"\n" - } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1", - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] - } - ] - } - ] - } - } - }, - "auth": { - "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json", - "targetAudience": "https://canton.network.global" - }, - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "disableAuth": false, - "enableHealthProbes": true, - "enablePostgresMetrics": true, - "extraVolumeMounts": [ - { - "mountPath": "/app/gcp-credentials.json", - "name": "gcp-credentials", - "subPath": "googleCredentials" - } - ], - "extraVolumes": [ - { - "name": "gcp-credentials", - "secret": { - "secretName": "gke-credentials" - } - } - ], - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "kms": { - "keyRingId": "validator1_mock", - "locationId": "us-central1", - "projectId": "da-cn-devnet", - "type": "gcp" - }, - "logLevel": "DEBUG", - "metrics": { - "enable": true - }, - "participantAdminUserNameFrom": { - "secretKeyRef": { - "key": "ledger-api-user", - "name": "splice-app-validator-ledger-api-auth", - "optional": false - } - }, - "persistence": { - "databaseName": "participant_2", - "postgresName": "participant-pg", - "schema": "participant", - "secretName": "participant-pg-secrets" - }, - "resources": { - "limits": { - "memory": "8Gi" - }, - "requests": { - "memory": "4Gi" - } - }, - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" - } - ] - }, - "version": "0.3.20" - }, - "name": "validator1-participant-2", - "provider": "", - "type": "kubernetes:helm.sh/v3:Release" - }, { "custom": true, "id": "", @@ -788,6 +657,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -812,6 +682,129 @@ "provider": "", "type": "gcp:sql/databaseInstance:DatabaseInstance" }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-participant", + "compat": "true", + "maxHistory": 10, + "name": "participant", + "namespace": "validator1", + "timeout": 600, + "values": { + "additionalEnvVars": [ + { + "name": "GOOGLE_APPLICATION_CREDENTIALS", + "value": "/app/gcp-credentials.json" + }, + { + "name": "ADDITIONAL_CONFIG_SESSION_SIGNING_KEYS", + "value": "canton.participants.participant.crypto.session-signing-keys.enabled = true" + }, + { + "name": "ADDITIONAL_CONFIG_PARTICIPANT_PRUNING", + "value": "# Ignore missing ACS commitment and commitment mismatches\ncanton.participants.participant.parameters.stores.safe-to-prune-commitment-state = \"all\"\n" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1", + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "auth": { + "jwksUrl": "https://canton-network-dev.us.auth0.com/.well-known/jwks.json", + "targetAudience": "https://canton.network.global" + }, + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "disableAuth": false, + "enableHealthProbes": true, + "enablePostgresMetrics": true, + "extraVolumeMounts": [ + { + "mountPath": "/app/gcp-credentials.json", + "name": "gcp-credentials", + "subPath": "googleCredentials" + } + ], + "extraVolumes": [ + { + "name": "gcp-credentials", + "secret": { + "secretName": "gke-credentials" + } + } + ], + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "kms": { + "keyRingId": "validator1_mock", + "locationId": "us-central1", + "projectId": "da-cn-devnet", + "type": "gcp" + }, + "logLevel": "DEBUG", + "metrics": { + "enable": true + }, + "participantAdminUserNameFrom": { + "secretKeyRef": { + "key": "ledger-api-user", + "name": "splice-app-validator-ledger-api-auth", + "optional": false + } + }, + "persistence": { + "databaseName": "participant_2", + "postgresName": "participant-pg", + "schema": "participant", + "secretName": "participant-pg-secrets" + }, + "resources": { + "limits": { + "memory": "8Gi" + }, + "requests": { + "memory": "4Gi" + } + }, + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + }, + "version": "0.3.20" + }, + "name": "validator1-participant", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, { "custom": true, "id": "", @@ -984,6 +977,7 @@ "deletionProtectionEnabled": true, "edition": "ENTERPRISE", "insightsConfig": { + "enhancedQueryInsightsEnabled": false, "queryInsightsEnabled": true }, "ipConfiguration": { @@ -1090,7 +1084,7 @@ "optional": false } }, - "participantAddress": "participant-2", + "participantAddress": "participant", "participantIdentitiesDumpPeriodicBackup": { "backupInterval": "10m", "location": { diff --git a/cluster/helm/splice-cluster-ingress-runbook/templates/migration_sequencer.yaml b/cluster/helm/splice-cluster-ingress-runbook/templates/migration_sequencer.yaml index f3e29b5b54..82b3734370 100644 --- a/cluster/helm/splice-cluster-ingress-runbook/templates/migration_sequencer.yaml +++ b/cluster/helm/splice-cluster-ingress-runbook/templates/migration_sequencer.yaml @@ -19,6 +19,7 @@ spec: http: - match: - port: 443 + - port: 80 route: - destination: port: @@ -38,6 +39,7 @@ spec: http: - match: - port: 443 + - port: 80 route: - destination: port: diff --git a/cluster/helm/splice-cluster-ingress-runbook/templates/rateLimit.yaml b/cluster/helm/splice-cluster-ingress-runbook/templates/rateLimit.yaml index 721668531e..9e0a4634de 100644 --- a/cluster/helm/splice-cluster-ingress-runbook/templates/rateLimit.yaml +++ b/cluster/helm/splice-cluster-ingress-runbook/templates/rateLimit.yaml @@ -11,12 +11,6 @@ kind: EnvoyFilter metadata: name: scan-rate-limit namespace: {{ .Release.Namespace }} - annotations: - # enable metrics for rate limit - proxy.istio.io/config: |- - proxyStatsMatcher: - inclusionRegexps: - - ".*http_local_rate_limit.*" spec: workloadSelector: labels: @@ -54,9 +48,12 @@ spec: route: rate_limits: - actions: - - request_headers: - descriptor_key: client_ip - header_name: x-forwarded-for + # Deliberately not keyed on the raw x-forwarded-for header: clients can + # prepend arbitrary entries to it, so per-IP limits keyed on it can be + # evaded. masked_remote_address uses the address envoy trusts instead. + - masked_remote_address: + v4_prefix_mask_len: 32 + v6_prefix_mask_len: 128 - header_value_match: descriptor_value: acs expect_match: true @@ -89,9 +86,10 @@ spec: header: key: x-local-rate-limit value: 'true' + max_dynamic_descriptors: 10000 descriptors: - entries: - - key: "client_ip" + - key: "masked_remote_address" - key: header_match value: acs token_bucket: diff --git a/cluster/helm/splice-cluster-ingress-runbook/templates/required.yaml b/cluster/helm/splice-cluster-ingress-runbook/templates/required.yaml index bf1603450a..14d85ae7fc 100644 --- a/cluster/helm/splice-cluster-ingress-runbook/templates/required.yaml +++ b/cluster/helm/splice-cluster-ingress-runbook/templates/required.yaml @@ -5,7 +5,3 @@ {{ $_ := required ".Values.cluster.hostname is required." (.Values.cluster).hostname }} {{ $_ := required ".Values.cluster.svNamespace is required." (.Values.cluster).svNamespace }} -# if ingress.splitwell is set to true then require the active migration id -{{- if .Values.ingress.splitwell }} -{{ $_ := required ".Values.ingress.decentralizedSynchronizer.activeMigrationId is required." ((.Values.ingress).decentralizedSynchronizer).activeMigrationId }} -{{- end }} diff --git a/cluster/helm/splice-cluster-ingress-runbook/templates/splitwell.yaml b/cluster/helm/splice-cluster-ingress-runbook/templates/splitwell.yaml index 94677abc4f..9918cbf7a1 100644 --- a/cluster/helm/splice-cluster-ingress-runbook/templates/splitwell.yaml +++ b/cluster/helm/splice-cluster-ingress-runbook/templates/splitwell.yaml @@ -18,15 +18,19 @@ spec: - port: 443 uri: prefix: "/api/json-api/" + - port: 80 + uri: + prefix: "/api/json-api/" route: - destination: port: number: 7575 - host: participant-{{ ((.Values.ingress).decentralizedSynchronizer).activeMigrationId }}.{{ (.Values.cluster).svNamespace }}.svc.cluster.local + host: participant.{{ (.Values.cluster).svNamespace }}.svc.cluster.local rewrite: uri: "/" - match: - port: 443 + - port: 80 route: - destination: port: diff --git a/cluster/helm/splice-cluster-ingress-runbook/templates/sv.yaml b/cluster/helm/splice-cluster-ingress-runbook/templates/sv.yaml index 52b884c81a..7505a2f0b6 100644 --- a/cluster/helm/splice-cluster-ingress-runbook/templates/sv.yaml +++ b/cluster/helm/splice-cluster-ingress-runbook/templates/sv.yaml @@ -19,6 +19,9 @@ spec: - port: 443 uri: prefix: "/api/sv/" + - port: 80 + uri: + prefix: "/api/sv/" route: - destination: port: @@ -26,6 +29,7 @@ spec: host: sv-app.{{ (.Values.cluster).svNamespace }}.svc.cluster.local - match: - port: 443 + - port: 80 route: - destination: port: @@ -52,6 +56,12 @@ spec: - port: 443 uri: prefix: "/registry/" + - port: 80 + uri: + prefix: "/api/scan/" + - port: 80 + uri: + prefix: "/registry/" route: - destination: port: @@ -59,6 +69,7 @@ spec: host: scan-app.{{ (.Values.cluster).svNamespace }}.svc.cluster.local - match: - port: 443 + - port: 80 route: - destination: port: diff --git a/cluster/helm/splice-cluster-ingress-runbook/templates/validator.yaml b/cluster/helm/splice-cluster-ingress-runbook/templates/validator.yaml index e2cb8554f2..642a350c7f 100644 --- a/cluster/helm/splice-cluster-ingress-runbook/templates/validator.yaml +++ b/cluster/helm/splice-cluster-ingress-runbook/templates/validator.yaml @@ -18,6 +18,9 @@ spec: - port: 443 uri: prefix: "/api/validator/" + - port: 80 + uri: + prefix: "/api/validator/" route: - destination: port: @@ -25,6 +28,7 @@ spec: host: validator-app.{{ (.Values.cluster).svNamespace }}.svc.cluster.local - match: - port: 443 + - port: 80 route: - destination: port: @@ -48,6 +52,9 @@ spec: - port: 443 uri: prefix: "/api/validator/" + - port: 80 + uri: + prefix: "/api/validator/" route: - destination: port: @@ -55,6 +62,7 @@ spec: host: validator-app.{{ (.Values.cluster).svNamespace }}.svc.cluster.local - match: - port: 443 + - port: 80 route: - destination: port: diff --git a/cluster/helm/splice-cometbft/templates/deployment.yaml b/cluster/helm/splice-cometbft/templates/deployment.yaml index ce1f3f9b9b..0bf5d3a513 100644 --- a/cluster/helm/splice-cometbft/templates/deployment.yaml +++ b/cluster/helm/splice-cometbft/templates/deployment.yaml @@ -60,9 +60,19 @@ spec: containerPort: {{ .rpcPort }} protocol: TCP livenessProbe: + {{- if $.Values.watchdog.enabled }} + # combines the file created by the watchdog and the cometbft health check + exec: + command: + - /bin/sh + - -c + - '! [ -f {{ $.Values.watchdog.markerFile }} ] && curl -sf -o /dev/null --max-time 5 http://127.0.0.1:{{ .rpcPort }}/health' + timeoutSeconds: 10 + {{- else }} httpGet: path: /health port: rpc + {{- end }} initialDelaySeconds: {{ $.Values.livenessProbeInitialDelaySeconds | default 600 }} readinessProbe: httpGet: @@ -79,6 +89,10 @@ spec: name: data - mountPath: /tmp name: state-sync + {{- if $.Values.watchdog.enabled }} + - mountPath: {{ dir $.Values.watchdog.markerFile }} + name: watchdog + {{- end }} env: - name: HOME value: /cometbft @@ -109,11 +123,55 @@ spec: cp -rL "${HOME}"/initial-config/. "${HOME}"/config/ "${HOME}"/configure-state-sync.sh "${HOME}"/config/config.toml cp -rnL "${HOME}"/initial-data/* "${HOME}"/data || : - cometbft-canton-network start \ + {{- if $.Values.watchdog.enabled }} + # Clear the restart request of the run that just ended, otherwise the + # liveness probe fails again as soon as it starts probing. + rm -f {{ $.Values.watchdog.markerFile }} + {{- end }} + exec cometbft-canton-network start \ {{ include "cliArgs" (list $.Values) | indent 14 | trim }} {{- with $.Values.resources }} resources: {{- toYaml . | nindent 12 }} {{- end }} + {{- if $.Values.watchdog.enabled }} + # the watchdog is in a separate container mainly for easier debugging and log separation. + - name: "watchdog" + {{- if $.Values.securityContexts }} + {{- with $.Values.securityContexts.containers.watchdog }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + image: {{ $.Values.imageRepo }}/{{ $.Values.watchdog.imageName }}:{{ $.Chart.AppVersion }}{{ (($.Values.imageDigests).cometbft_watchdog) }} + {{- with $.Values.imagePullPolicy }} + imagePullPolicy: {{ . }} + {{- end }} + volumeMounts: + - mountPath: {{ dir $.Values.watchdog.markerFile }} + name: watchdog + env: + - name: WATCHDOG_SEQUENCER_METRICS_URL + value: {{ $.Values.watchdog.sequencerMetricsUrl | quote }} + - name: WATCHDOG_MEDIATOR_METRICS_URL + value: {{ $.Values.watchdog.mediatorMetricsUrl | quote }} + - name: WATCHDOG_POLL_INTERVAL_SECONDS + value: {{ $.Values.watchdog.pollIntervalSeconds | quote }} + - name: WATCHDOG_THRESHOLD + value: {{ $.Values.watchdog.threshold | quote }} + - name: WATCHDOG_EVALUATION_INTERVAL_SECONDS + value: {{ $.Values.watchdog.evaluationIntervalSeconds | quote }} + - name: WATCHDOG_SCRAPE_TIMEOUT_SECONDS + value: {{ $.Values.watchdog.scrapeTimeoutSeconds | quote }} + - name: WATCHDOG_STARTUP_GRACE_SECONDS + value: {{ $.Values.watchdog.startupGraceSeconds | quote }} + - name: WATCHDOG_COOLDOWN_SECONDS + value: {{ $.Values.watchdog.cooldownSeconds | quote }} + - name: WATCHDOG_MARKER_FILE + value: {{ $.Values.watchdog.markerFile | quote }} + {{- with $.Values.watchdog.resources }} + resources: {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} volumes: - name: initial-config projected: @@ -139,6 +197,11 @@ spec: # since we are not going to scale this deployment. persistentVolumeClaim: claimName: {{ $.Values.db.pvcName | default (include "prefix" (list $.Values "cometbft-data")) }} + {{- if $.Values.watchdog.enabled }} + - name: watchdog + emptyDir: + sizeLimit: 1Mi + {{- end }} {{- with $.Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/cluster/helm/splice-cometbft/templates/partials/_config-toml.tpl b/cluster/helm/splice-cometbft/templates/partials/_config-toml.tpl index 789bb5a1e2..c56d123124 100644 --- a/cluster/helm/splice-cometbft/templates/partials/_config-toml.tpl +++ b/cluster/helm/splice-cometbft/templates/partials/_config-toml.tpl @@ -395,7 +395,7 @@ version = "v0" wal_file = "data/cs.wal/wal" # How long we wait for a proposal block before prevoting nil -timeout_propose = "1s" +timeout_propose = "2s" # How much timeout_propose increases with each round timeout_propose_delta = "500ms" # How long we wait after receiving +2/3 prevotes for “anything” (ie. not a single block or nil) diff --git a/cluster/helm/splice-cometbft/tests/cometbft_deployment_test.yaml b/cluster/helm/splice-cometbft/tests/cometbft_deployment_test.yaml index 7150e4d3f9..2e31c85688 100644 --- a/cluster/helm/splice-cometbft/tests/cometbft_deployment_test.yaml +++ b/cluster/helm/splice-cometbft/tests/cometbft_deployment_test.yaml @@ -21,6 +21,8 @@ set: genesis: chainId: "mock-3" chainIdSuffix: "1" # must be a string value + watchdog: + enabled: false tests: - it: "deploys a deployment" template: deployment.yaml diff --git a/cluster/helm/splice-cometbft/tests/cometbft_pvc_test.yaml b/cluster/helm/splice-cometbft/tests/cometbft_pvc_test.yaml index bbc4c8bf86..7bfbb8cae2 100644 --- a/cluster/helm/splice-cometbft/tests/cometbft_pvc_test.yaml +++ b/cluster/helm/splice-cometbft/tests/cometbft_pvc_test.yaml @@ -10,6 +10,8 @@ release: set: node: identifier: "global-domain-3-cometbft" + watchdog: + enabled: false tests: - it: "deploys a PVC" template: pvc.yaml diff --git a/cluster/helm/splice-cometbft/tests/cometbft_watchdog_test.yaml b/cluster/helm/splice-cometbft/tests/cometbft_watchdog_test.yaml new file mode 100644 index 0000000000..f82d197689 --- /dev/null +++ b/cluster/helm/splice-cometbft/tests/cometbft_watchdog_test.yaml @@ -0,0 +1,98 @@ +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +suite: "cometbft watchdog" +templates: + - deployment.yaml +release: + name: global-domain-3 +set: + sv1: + externalAddress: sv.mock.net:26316 + nodeId: "4c7c99516fb3309b89b7f8ed94690994c8ec0ab0" + publicKey: "H2bcJU2zbzbLmP78YWiwMgtB0QG1MNTSozGl1tP11hI=" + keyAddress: "9473617BBC80C12F68CC25B5A754D1ED9035886C" + node: + id: "1234dedbeef1234dedbeef1234dedbeef1234ded" + identifier: "global-domain-3-cometbft" + externalAddress: "global-domain-3-cometbft.sv.mock.net:26356" + keysSecret: "cometbft-keys" + genesis: + chainId: "mock-3" + chainIdSuffix: "1" +tests: + - it: "rejects the chart defaults, so that operators need to explicitly configure the right URLs" + template: deployment.yaml + asserts: + - failedTemplate: + errorPattern: "missing properties 'sequencerMetricsUrl', 'mediatorMetricsUrl'" + + - it: "leaves the pod untouched when disabled" + template: deployment.yaml + set: + watchdog: + enabled: false + documentSelector: + path: kind + value: Deployment + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 1 + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet + value: + path: /health + port: rpc + - notMatchRegex: + path: spec.template.spec.containers[0].command[4] + pattern: "restart-requested" + + - it: "deploys the watchdog sidecar when enabled" + template: deployment.yaml + set: + watchdog: + enabled: true + sequencerMetricsUrl: http://global-domain-3-sequencer:10013/metrics + mediatorMetricsUrl: http://global-domain-3-mediator:10013/metrics + threshold: 2.5 + evaluationIntervalSeconds: 900 + documentSelector: + path: kind + value: Deployment + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 2 + - equal: + path: spec.template.spec.containers[1].name + value: "watchdog" + - matchRegex: + path: spec.template.spec.containers[1].image + pattern: "/cometbft-watchdog:" + - contains: + path: spec.template.spec.containers[1].env + content: + name: WATCHDOG_MEDIATOR_METRICS_URL + value: "http://global-domain-3-mediator:10013/metrics" + - contains: + path: spec.template.spec.containers[1].env + content: + name: WATCHDOG_THRESHOLD + value: "2.5" + - contains: + path: spec.template.spec.containers[1].env + content: + name: WATCHDOG_EVALUATION_INTERVAL_SECONDS + value: "900" + - contains: + path: spec.template.spec.containers[1].volumeMounts + content: + mountPath: /watchdog + name: watchdog + - contains: + path: spec.template.spec.volumes + content: + name: watchdog + emptyDir: + sizeLimit: 1Mi diff --git a/cluster/helm/splice-cometbft/values-template.yaml b/cluster/helm/splice-cometbft/values-template.yaml index 983254bf66..ce844eb38c 100644 --- a/cluster/helm/splice-cometbft/values-template.yaml +++ b/cluster/helm/splice-cometbft/values-template.yaml @@ -104,13 +104,35 @@ mempool: # max number of transactions kept in the mempool size: 4000 # number of transactions to keep to deduplicate new transactions - deduplicationCacheSize: 200000 + deduplicationCacheSize: 1000000 # max number of seconds that a transaction will be kept in the mempool without being included in a block before being discarded - ttlSeconds: 300 + ttlSeconds: 60 # should stakater reloader annotation be added (note: reloader needs to be installed separately) enableReloader: true +# Restarts CometBFT when cometbft starts replaying messages +watchdog: + # Enabled by default to make sure noone accidentally forgets to enable it. + enabled: true + # Required but we don't know the migration id to set sensible defaults. + # sequencerMetricsUrl: http://global-domain-0-sequencer:10013/metrics + # mediatorMetricsUrl: http://global-domain-0-mediator:10013/metrics + pollIntervalSeconds: 30 + threshold: 5 + evaluationIntervalSeconds: 300 + scrapeTimeoutSeconds: 10 + startupGraceSeconds: 600 + cooldownSeconds: 600 + markerFile: /watchdog/restart-requested + imageName: "cometbft-watchdog" + resources: + limits: + memory: 128Mi + requests: + cpu: 0.05 + memory: 64Mi + securityContexts: pod: security_context_profile: pod_security_context @@ -119,3 +141,6 @@ securityContexts: containers: cometbft: security_context_profile: app_security_context + watchdog: + security_context_profile: app_security_context + readOnlyRootFilesystem: true diff --git a/cluster/helm/splice-cometbft/values.schema.json b/cluster/helm/splice-cometbft/values.schema.json index e28c04aa7d..a3b710be71 100644 --- a/cluster/helm/splice-cometbft/values.schema.json +++ b/cluster/helm/splice-cometbft/values.schema.json @@ -16,12 +16,20 @@ "properties": { "cometbft": { "type": "object" + }, + "watchdog": { + "type": "object" } }, - "required": ["cometbft"] + "required": [ + "cometbft" + ] } }, - "required": ["pod", "containers"] + "required": [ + "pod", + "containers" + ] }, "imageName": { "type": "string" @@ -45,6 +53,76 @@ "type": "string" } } + }, + "watchdog": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "sequencerMetricsUrl": { + "type": "string", + "format": "uri", + "minLength": 1 + }, + "mediatorMetricsUrl": { + "type": "string", + "format": "uri", + "minLength": 1 + }, + "pollIntervalSeconds": { + "type": "number", + "minimum": 1 + }, + "threshold": { + "type": "number" + }, + "evaluationIntervalSeconds": { + "type": "number", + "minimum": 1 + }, + "scrapeTimeoutSeconds": { + "type": "number", + "minimum": 1 + }, + "startupGraceSeconds": { + "type": "number", + "minimum": 0 + }, + "cooldownSeconds": { + "type": "number", + "minimum": 0 + }, + "markerFile": { + "type": "string", + "pattern": "^/[^/]+/[^/]+" + }, + "imageName": { + "type": "string" + }, + "resources": { + "type": "object" + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + }, + "required": [ + "enabled" + ] + }, + "then": { + "required": [ + "sequencerMetricsUrl", + "mediatorMetricsUrl" + ] + } } } } diff --git a/cluster/helm/splice-global-domain/templates/mediator.yaml b/cluster/helm/splice-global-domain/templates/mediator.yaml index 8a238386c6..952bcac9d6 100644 --- a/cluster/helm/splice-global-domain/templates/mediator.yaml +++ b/cluster/helm/splice-global-domain/templates/mediator.yaml @@ -57,7 +57,7 @@ spec: image: {{ .Values.imageRepo }}/{{ .Values.mediator.imageName}}:{{ .Chart.AppVersion }}{{ ((.Values.imageDigests).canton_mediator) }} env: - name: JAVA_TOOL_OPTIONS - value: "{{ .Values.defaultJvmOptions }} {{ .Values.additionalJvmOptions }} -Dlogback.configurationFile=/app/logback/logback.xml" + value: "{{ .Values.defaultJvmOptions }} {{ .Values.mediator.additionalJvmOptions }} -Dlogback.configurationFile=/app/logback/logback.xml" - name: CANTON_DOMAIN_POSTGRES_SERVER value: {{ .Values.mediator.persistence.host }} - name: CANTON_DOMAIN_POSTGRES_PORT diff --git a/cluster/helm/splice-global-domain/templates/sequencer.yaml b/cluster/helm/splice-global-domain/templates/sequencer.yaml index cbc185292b..41a147a590 100644 --- a/cluster/helm/splice-global-domain/templates/sequencer.yaml +++ b/cluster/helm/splice-global-domain/templates/sequencer.yaml @@ -53,7 +53,7 @@ spec: {{- end}} env: - name: JAVA_TOOL_OPTIONS - value: "{{ .Values.defaultJvmOptions }} {{ .Values.additionalJvmOptions }} -Dlogback.configurationFile=/app/logback/logback.xml" + value: "{{ .Values.defaultJvmOptions }} {{ .Values.sequencer.additionalJvmOptions }} -Dlogback.configurationFile=/app/logback/logback.xml" - name: CANTON_DOMAIN_POSTGRES_SERVER value: {{ .Values.sequencer.persistence.host }} - name: CANTON_DOMAIN_POSTGRES_PORT diff --git a/cluster/helm/splice-global-domain/tests/mediator_test.yaml b/cluster/helm/splice-global-domain/tests/mediator_test.yaml index e030323f0c..07691efb2d 100644 --- a/cluster/helm/splice-global-domain/tests/mediator_test.yaml +++ b/cluster/helm/splice-global-domain/tests/mediator_test.yaml @@ -196,6 +196,21 @@ tests: path: spec.template.spec.serviceAccountName value: "custom-domain-sa" + - it: "applies mediator-specific JVM options" + set: + mediator.additionalJvmOptions: "-Dmediator.option=true" + sequencer.additionalJvmOptions: "-Dsequencer.option=true" + documentSelector: + path: kind + value: Deployment + asserts: + - matchRegex: + path: spec.template.spec.containers[?(@.name=='mediator')].env[?(@.name=='JAVA_TOOL_OPTIONS')].value + pattern: '-Dmediator\.option=true' + - notMatchRegex: + path: spec.template.spec.containers[?(@.name=='mediator')].env[?(@.name=='JAVA_TOOL_OPTIONS')].value + pattern: '-Dsequencer\.option=true' + - it: "sets default security contexts for pods and containers" documentSelector: path: kind diff --git a/cluster/helm/splice-global-domain/tests/sequencer_test.yaml b/cluster/helm/splice-global-domain/tests/sequencer_test.yaml index 3f22ccd1d3..1d778015b3 100644 --- a/cluster/helm/splice-global-domain/tests/sequencer_test.yaml +++ b/cluster/helm/splice-global-domain/tests/sequencer_test.yaml @@ -406,6 +406,21 @@ tests: path: spec.template.spec.serviceAccountName value: "custom-domain-sa" + - it: "applies sequencer-specific JVM options" + set: + sequencer.additionalJvmOptions: "-Dsequencer.option=true" + mediator.additionalJvmOptions: "-Dmediator.option=true" + documentSelector: + path: kind + value: Deployment + asserts: + - matchRegex: + path: spec.template.spec.containers[?(@.name=='sequencer')].env[?(@.name=='JAVA_TOOL_OPTIONS')].value + pattern: '-Dsequencer\.option=true' + - notMatchRegex: + path: spec.template.spec.containers[?(@.name=='sequencer')].env[?(@.name=='JAVA_TOOL_OPTIONS')].value + pattern: '-Dmediator\.option=true' + - it: "sets default security contexts for pods and containers" set: sequencer: diff --git a/cluster/helm/splice-global-domain/values-template.yaml b/cluster/helm/splice-global-domain/values-template.yaml index 0d8a2f8eee..d4b033a316 100644 --- a/cluster/helm/splice-global-domain/values-template.yaml +++ b/cluster/helm/splice-global-domain/values-template.yaml @@ -20,6 +20,7 @@ resources: sequencer: imageName: "canton-sequencer" + additionalJvmOptions: "" cometbft: imageName: "canton-cometbft-sequencer" persistence: @@ -39,6 +40,7 @@ sequencer: mediator: imageName: "canton-mediator" + additionalJvmOptions: "" persistence: host: postgres.sv-1.svc.cluster.local secretName: "postgres-secrets" diff --git a/cluster/helm/splice-info/scripts/get-status.sh b/cluster/helm/splice-info/scripts/get-status.sh index 5d38e1d116..107ad158ae 100755 --- a/cluster/helm/splice-info/scripts/get-status.sh +++ b/cluster/helm/splice-info/scripts/get-status.sh @@ -5,25 +5,41 @@ set -euo pipefail +set -E +trap 'echo "ERROR: On line $LINENO in function \"${FUNCNAME[0]}\". Exit code is $?." >&2' ERR + SV_METRICS_URL="${SV_METRICS_URL:-http://sv-app:10013/metrics}" SCAN_URL="${SCAN_URL:-http://scan-app:5012}" SV_THRESHOLD="${SV_THRESHOLD:-600}" MEDIATOR_THRESHOLD="${MEDIATOR_THRESHOLD:-900}" SCAN_THRESHOLD_ROUNDS="${SCAN_THRESHOLD_ROUNDS:-900}" -SCAN_THRESHOLD_EVENT="${SCAN_THRESHOLD_EVENT:-300}" +SCAN_THRESHOLD_EVENTS="${SCAN_THRESHOLD_EVENT:-300}" SEQUENCER_THRESHOLD="${SEQUENCER_THRESHOLD:-2520}" # 42 minutes, Sequencer acknowledgments are irregular, so we use a higher threshold here -CURL_TIMEOUT="${CURL_TIMEOUT:-15}" +PARALLELISM="${PARALLELISM:-8}" +CURL_TIMEOUT="${CURL_TIMEOUT:-15}" TLS_SKIP_VERIFY="${TLS_SKIP_VERIFY:-false}" + CURL_CMD=(curl -fs -m "$CURL_TIMEOUT") -[[ $TLS_SKIP_VERIFY == true ]] && CURL_CMD+=(-k) +GRPC_HEALTH_CMD=(grpc_health_code --max-time "$CURL_TIMEOUT") + +if [[ $TLS_SKIP_VERIFY == true ]]; then + CURL_CMD+=(-k) + GRPC_HEALTH_CMD+=(--insecure) +fi + +GRPC_HEALTH_CONN_CMD=("${GRPC_HEALTH_CMD[@]}" --connect-only) + +CURL_CMD_JSON=$(jq -nc --args '$ARGS.positional' -- "${CURL_CMD[@]}") +GRPC_HEALTH_CMD_JSON=$(jq -nc --args '$ARGS.positional' -- "${GRPC_HEALTH_CMD[@]}") +GRPC_HEALTH_CONN_CMD_JSON=$(jq -nc --args '$ARGS.positional' -- "${GRPC_HEALTH_CONN_CMD[@]}") prom2json() { P2J_VERSION="1.5.0" P2J_ARCH="linux-amd64" - P2J_BIN="$HOME/.prom2json-$P2J_VERSION" + P2J_BIN="/tmp/.prom2json-$P2J_VERSION" P2J_URL="https://github.com/prometheus/prom2json/releases/download/v$P2J_VERSION/prom2json-$P2J_VERSION.$P2J_ARCH.tar.gz" P2J_EXPECTED_SHA="5935363cc8c88360e3aa275ddc5a754ad95f6bab6b6052978e686300baa5a4d6" @@ -40,95 +56,212 @@ prom2json() { rm -rf "$P2J_TMPDIR" "$P2J_DIST" fi - "$P2J_BIN" + "$P2J_BIN" "$@" } -sv_get_status() { - SV_METRIC=splice_sv_status_report_creation_time_us +# Converts a JSON object with string values to a JSON object with parsed JSON +# values. If a value is not valid JSON, it is replaced with null. +json_object_values_fromjson() { + jq -e '.[] |= try(fromjson) catch null | values' +} + +grpc_health() { + local max_time + local insecure=false + local connect_only=false + + local args=() + while [[ $# -gt 0 ]]; do + case "$1" in + -m|--max-time) max_time=$2; shift 2 ;; + -k|--insecure) insecure=true; shift ;; + --connect-only) connect_only=true; shift ;; + *) args+=("$1"); shift ;; + esac + done + + [[ ${#args[@]} -eq 1 ]] || + { echo "Usage: grpc_health [-m|--max-time SECONDS] [-k|--insecure] [--connect-only] [http://|https://]HOST:PORT" >&2; return 1; } + + local url=${args[0]} + + local curl_opts=( + --silent + -X POST + -H 'Content-Type: application/grpc' + -H 'TE: trailers' + ) + + [[ -n ${max_time-} ]] && curl_opts+=(--max-time "$max_time") + "$insecure" && curl_opts+=(-k) + + if [[ $url == "https://"* ]]; then + curl_opts+=(--http2) + else + curl_opts+=(--http2-prior-knowledge) + fi + + if "$connect_only"; then + local http_code; http_code=$( + curl --fail "${curl_opts[@]}" \ + --write-out '%{http_code}' \ + "$url" + ) || { echo "error: connection failed" >&2; return 1; } + + if [[ "$http_code" == 200 ]]; then + echo "Connection successful" + return 0 + else + echo "error: connection failed with HTTP code $http_code" >&2 + return 1 + fi + else + local out; out=$( + set -o pipefail + printf '\0\0\0\0\0' | + curl --fail "${curl_opts[@]}" \ + --data-binary @- "$url/grpc.health.v1.Health/Check" | + xxd -p + ) || { echo "error: request failed" >&2; return 1; } + + if [[ "$out" == 00000000020801 ]]; then + echo SERVING + return 0 + else + echo "error: not serving" >&2 + return 1 + fi + fi +} + +grpc_health_code() { + grpc_health "$@" &> /dev/null && echo 0 || echo 2 +} - local exit_code +sv_get_status() { + local sv_metric=splice_sv_status_report_creation_time_us local response; response=$( - "${CURL_CMD[@]}" "$SV_METRICS_URL?name[]=$SV_METRIC" | - prom2json | + get_metric_data "$SV_METRICS_URL" "$sv_metric" | jq -e \ - --arg threshold "$SV_THRESHOLD" \ - --arg metric "$SV_METRIC" \ + --argjson threshold "$SV_THRESHOLD" \ ' - ($threshold | tonumber) as $threshold - | .[] - | select(.name == $metric).metrics + . | map( + .labels.report_publisher as $name | + now - (.value | tonumber) / pow(10;6) as $delay | + { - (.labels.report_publisher): (if (now - (.value | tonumber)/pow (10;6)) < $threshold then 0 else 1 end) + ($name): (if $delay < $threshold then 0 else 1 end) } ) | add + | values ' - ) && exit_code=$? || exit_code=$? - - [[ $exit_code -eq 0 ]] && echo "$response" || echo '{}' + ) && echo "$response" || echo '{}' } -get_sequencer_metric_data() { - local metric_name=$1 +get_metric_data() { + local metrics_url=$1 + local metric_name=$2 - "${CURL_CMD[@]}" "$SEQUENCER_METRICS_URL?name[]=$metric_name" | - prom2json || - echo '[]' + local response; response=$( + "${CURL_CMD[@]}" "$metrics_url?name[]=$metric_name" | # filtering by name makes the response smaller and much faster + prom2json + ) || response='[]' + + local result; result=$( + echo "$response" | + jq -e \ + --arg metric "$metric_name" \ + '.[] | select(.name == $metric).metrics | values' # we have to filter by name again because prom2json returns all metrics prefixed with the given name + ) && echo "$result" || echo '[]' } +# Extracts status from sequencer metric data. Returns a JSON object with +# svNames as keys and 0 (acknowledgment within threshold) or 1 (otherwise) as +# values. get_status_from_sequencer_metric_data() { local metric_json=$1 - local metric_name=$2 - local category_name=$3 - local threshold=$4 - - local exit_code + local category_name=$2 + local threshold=$3 local result; result=$( echo "$metric_json" | jq -e \ - --arg metric "$metric_name" \ --arg category_name "$category_name" \ - --arg threshold "$threshold" \ + --argjson threshold "$threshold" \ ' - ($threshold | tonumber) as $threshold - | .[] - | select(.name == $metric).metrics + . | map( (.labels.member | split("::")) as [$category, $name, $fingerprint] | select($category == $category_name) | + now - (.value | tonumber) / pow(10;6) as $delay | + { - ($name): (if (now - (.value | tonumber)/pow (10;6)) < $threshold then 0 else 1 end) + ($name): (if $delay < $threshold then 0 else 1 end) } ) | add + | values ' - ) && exit_code=$? || exit_code=$? - - [[ $exit_code -eq 0 ]] && echo "$result" || echo '{}' + ) && echo "$result" || echo '{}' } -scan_get_status() { - local scan_url=$SCAN_URL - local scans_info_url="$scan_url/api/scan/v0/scans" +# Usage: json_array_to_bash_array somearr <<< '["val1", "val2"]' +# +# Takes a JSON array of strings from stdin and populates a bash array with the +# same values. The first argument is the name of the bash array to populate. +json_array_to_bash_array() { + local bash_array_name=$1 + local -n bash_array_ref=$bash_array_name + + { + # shellcheck disable=SC2034 + # https://github.com/koalaman/shellcheck/issues/817 + readarray -td '' bash_array_ref + wait "$!" # catch jq exit code + } < <( + jq -j \ + ' + if (type == "array" and all(.[]; type == "string")) then + .[] | (., "\u0000") + else + error("Input must be a JSON array of strings.") + end + ' + ) +} - local scan_info; scan_info=$("${CURL_CMD[@]}" "$scans_info_url" || echo '{}') +# Usage: run_parallel '{"label1": ["command1", "arg1"], "label2": ["command2", "arg2"]}' +# +# Takes a JSON object with labels as keys and command with args as values, runs +# the commands in parallel and returns a JSON object with the same labels as +# keys and the command outputs as values. +run_parallel() { + local label_command_map_json=$1 + + if ! jq -e ' + type == "object" and all(.[]; type == "array" and all(.[]; type == "string")) + ' <<< "$label_command_map_json" > /dev/null + then + echo "Error: JSON must be an object with arrays of strings." >&2 + return 1 + fi - local scan_svnames_and_urls; IFS=$'\n' read -r -d '' -a scan_svnames_and_urls < <( - echo "$scan_info" | - jq -r '.scans?[].scans[] | [.svName, .publicUrl] | join(" ")' && printf '\0' + local labels; json_array_to_bash_array labels < <( + printf "%s" "$label_command_map_json" | jq 'keys' ) - local scan_data; scan_data=$( + local result; result=$( local -i proc_count=0 - local proc_max=8 + local proc_max=$PARALLELISM local lockfile; lockfile=$(mktemp) - for svname_and_url in "${scan_svnames_and_urls[@]}"; do - local svname url - read -r svname url <<< "$svname_and_url"; + for label in "${labels[@]}"; do + local command_with_args; json_array_to_bash_array command_with_args < <( + printf "%s" "$label_command_map_json" | jq --arg label "$label" '.[$label]' + ) # Limit the number of concurrent processes if (( proc_count >= proc_max )); then @@ -137,84 +270,15 @@ scan_get_status() { fi ( - scan_response_rounds=$( - "${CURL_CMD[@]}" \ - --compressed \ - --json '{"cached_open_mining_round_contract_ids":[],"cached_issuing_round_contract_ids":[]}' \ - "$url/api/scan/v0/open-and-issuing-mining-rounds" | jq -e . - ) && exit_code=$? || exit_code=$? - - [[ $exit_code -ne 0 ]] && scan_response_rounds='{}' - - scan_event_is_fetched_successfully=$( - if - migration_id=$( - "${CURL_CMD[@]}" "$url/api/scan/v0/migrations/last" | - jq -er '.migration_id' - ) && - - after=$(TZ=UTC0 printf '%(%FT%TZ)T' "$((EPOCHSECONDS - SCAN_THRESHOLD_EVENT))") && - - events=$( - "${CURL_CMD[@]}" \ - --compressed \ - --json '{"page_size": 1, "after": {"after_migration_id": '"$migration_id"', "after_record_time": "'"$after"'"}}' \ - "$url/api/scan/v0/events" - ) && - - echo "$events" | jq -e '.events | length > 0' > /dev/null - then - echo true - else - echo false - fi - ) - - scan_status=$( - echo "$scan_response_rounds" | - jq \ - --arg svname "$svname" \ - --argjson threshold_rounds "$SCAN_THRESHOLD_ROUNDS" \ - --argjson threshold_event "$SCAN_THRESHOLD_EVENT" \ - --argjson event_is_fetched "$scan_event_is_fetched_successfully" \ - ' - def get_delay($timestamp; $now): - (try($timestamp[0:19] + "Z" | ($now - fromdate) | round) // null) - ; - - def get_round_delay(field; $now): - [ field[]?.contract.created_at ] - | sort[-1] - | get_delay(.; $now) - ; - - now as $now | - get_round_delay(.open_mining_rounds; $now) as $open_delay | - get_round_delay(.issuing_mining_rounds; $now) as $issuing_delay | - [$open_delay, $issuing_delay] as $round_delays | - - { - ($svname): if - ($round_delays | all | not) - then - 2 # unreachable - elif - ($event_is_fetched | not) or - ($round_delays | max > $threshold_rounds) - then - 1 # lagging - else - 0 - end - } - ' - ) + local output; output=$( + "${command_with_args[@]}" 2>/dev/null + ) || true # Use an exclusive lock to make sure we don't mix up the outputs exec {LOCK_FD}<>"$lockfile" flock "$LOCK_FD" - echo "$scan_status" + printf "%s" "$output" | jq -sR --arg label "$label" '{($label): .}' ) & proc_count=$(( proc_count + 1 )) @@ -225,13 +289,244 @@ scan_get_status() { rm "$lockfile" ) - local exit_code + printf "%s" "$result" | jq -es 'add | values' || echo '{}' +} + +# Fetches the list of scans from the Scan API and returns a JSON object with +# svNames as keys and public URLs as values. +get_scan_urls() { + local scan_info; scan_info=$( + "${CURL_CMD[@]}" "$SCAN_URL/api/scan/v0/scans" || echo '{}' + ) + + local scan_urls; scan_urls=$( + local result; result=$( + echo "$scan_info" | + jq -e '.scans[]?.scans | map({ (.svName): .publicUrl }) | add | values' + ) && echo "$result" || echo '{}' + ) + + echo "$scan_urls" +} + +# Tries to reach the scan and checks the age of the last open and issuing +# rounds. Returns a JSON object with svNames as keys and 0 (reachable and +# rounds within threshold), 1 (reachable but rounds not within threshold) or 2 +# (not reachable) as values. +scan_get_status_rounds() { + local scan_urls; scan_urls=$(get_scan_urls) + + local scan_cmds_rounds; scan_cmds_rounds=$( + local result; result=$( + jq -ne \ + --argjson cmd "$CURL_CMD_JSON" \ + --argjson scan_urls "$scan_urls" \ + ' + $scan_urls + | to_entries + | map( + .key as $svName | + .value as $scanUrl | + { + ($svName): + $cmd + + ["--compressed"] + + ["--json", ({"cached_open_mining_round_contract_ids": [], "cached_issuing_round_contract_ids": []} | tojson)] + + [$scanUrl + "/api/scan/v0/open-and-issuing-mining-rounds"] + } + ) + | add + | values + ' + ) && echo "$result" || echo '{}' + ) + + local scan_data_rounds; scan_data_rounds=$( + run_parallel "$scan_cmds_rounds" | + json_object_values_fromjson || echo '{}' + ) + + local scan_status_rounds; scan_status_rounds=$( + result=$( + printf "%s" "$scan_data_rounds" | + jq -e \ + --argjson threshold "$SCAN_THRESHOLD_ROUNDS" \ + ' + def get_delay(field; $now): + [ field[]?.contract.created_at ] + | sort[-1] + | (try(.[0:19] + "Z" | ($now - fromdate) | round) // null) + ; + + .[] |= ( + now as $now | + get_delay(.open_mining_rounds; $now) as $open_delay | + get_delay(.issuing_mining_rounds; $now) as $issuing_delay | + [$open_delay, $issuing_delay] as $delays | + + if ($delays | all | not) then + 2 + elif ($delays | max > $threshold) then + 1 + else + 0 + end + ) | values + ' + ) && echo "$result" || echo '{}' + ) + + echo "$scan_status_rounds" +} + +# Tries to fetch a recent event from scan. Returns 0 if successful, 1 +# otherwise. +scan_try_fetch_event() { + local url=$1 + + if + local migration_id; migration_id=$( + "${CURL_CMD[@]}" "$url/api/scan/v0/migrations/last" | + jq -er '.migration_id' + ) && + + local after; after=$(TZ=UTC0 printf '%(%FT%TZ)T' "$((EPOCHSECONDS - SCAN_THRESHOLD_EVENTS))") && + + local events; events=$( + "${CURL_CMD[@]}" \ + --compressed \ + --json '{"page_size": 1, "after": {"after_migration_id": '"$migration_id"', "after_record_time": "'"$after"'"}}' \ + "$url/api/scan/v0/events" + ) && + + echo "$events" | jq -e '.events | length > 0' > /dev/null + then + echo 0 + else + echo 1 + fi +} + +scan_get_status_events() { + local scan_urls; scan_urls=$(get_scan_urls) + + local scan_cmds_events; scan_cmds_events=$( + local result; result=$( + jq -ne \ + --argjson scan_urls "$scan_urls" \ + ' + $scan_urls | .[] + |= ["scan_try_fetch_event", .] + | values + ' + ) && echo "$result" || echo '{}' + ) + + run_parallel "$scan_cmds_events" | + json_object_values_fromjson || echo '{}' +} + +# Tries to reach the sequencers and checks their health status. Returns a JSON +# object with svNames as keys and 0 (reachable and serving) or 2 (otherwise) as +# values. +sequencer_get_status_reachability() { + local scan_url=$SCAN_URL + local sequencers_info_url="$scan_url/api/scan/v0/dso-sequencers" - local scan_status; scan_status=$( - echo "$scan_data" | jq -es 'sort | add' - ) && exit_code=$? || exit_code=$? + local sequencers_info; sequencers_info=$( + "${CURL_CMD[@]}" "$sequencers_info_url" || echo '{}' + ) - [[ $exit_code -eq 0 ]] && echo "$scan_status" || echo '{}' + local sequencers_info_for_serial; sequencers_info_for_serial=$( + echo "$sequencers_info" | + jq --argjson serial "$SERIAL_ID" '[.domainSequencers[]?.sequencers[] | select(.synchronizerSerial == $serial)]' + ) + + local sequencers_cmds; sequencers_cmds=$( + local result; result=$( + echo "$sequencers_info_for_serial" | + jq -e \ + --argjson cmd "$GRPC_HEALTH_CMD_JSON" \ + ' + . + | map({ (.svName): $cmd + [.url] }) + | add + | values + ' + ) && echo "$result" || echo '{}' + ) + + local sequencer_status; sequencer_status=$( + run_parallel "$sequencers_cmds" | + json_object_values_fromjson || echo '{}' + ) + + echo "$sequencer_status" +} + +# Tries to reach the cantonbft. Returns a JSON object with svNames as keys and +# 0 (reachable) or 2 (otherwise) as values. +cantonbft_get_status_reachability() { + local scan_urls; scan_urls=$(get_scan_urls) + + local get_cantonbfts_info_cmds; get_cantonbfts_info_cmds=$( + local result; result=$( + jq -ne \ + --argjson cmd "$CURL_CMD_JSON" \ + --argjson scan_urls "$scan_urls" \ + ' + $scan_urls | to_entries + | map( + .key as $svName | + .value as $scanUrl | + { + ($svName): $cmd + [$scanUrl + "/api/scan/v0/sv-bft-sequencers"] + } + ) + | add + | values + ' + ) && echo "$result" || echo '{}' + ) + + local cantonbfts_info; cantonbfts_info=$( + { + run_parallel "$get_cantonbfts_info_cmds" | + json_object_values_fromjson || echo '{}' + } | + jq '{ bftSequencers: map(.bftSequencers[]?) }' + ) + + local cantonbfts_info_for_serial; cantonbfts_info_for_serial=$( + echo "$cantonbfts_info" | + jq --argjson serial "$SERIAL_ID" '[.bftSequencers[]? | select(.serialId == $serial)]' + ) + + local cantonbfts_cmds; cantonbfts_cmds=$( + local result; result=$( + echo "$cantonbfts_info_for_serial" | + jq -e \ + --argjson cmd "$GRPC_HEALTH_CONN_CMD_JSON" \ + ' + . + | map( + (.id | split("::")[1]) as $svName | + { + ($svName): $cmd + [.p2pUrl] + } + ) + | add + | values + ' + ) && echo "$result" || echo '{}' + ) + + local cantonbft_status; cantonbft_status=$( + run_parallel "$cantonbfts_cmds" | + json_object_values_fromjson || echo '{}' + ) + + echo "$cantonbft_status" } update_serial_id() { @@ -246,38 +541,91 @@ generate_sequencer_metrics_url() { echo "http://global-domain-$SERIAL_ID-sequencer:10013/metrics" } +# Bitwise combination of status maps. Each map is a JSON object with string keys and +# integer values. The values are combined using a bitwise OR operation. +# combine_status '{k1: v1, k2: v2}' '{k1: v3, k3: v4}' -> '{k1: v1 | v3, k2: v2, k3: v4}' +# +# Examples: +# echo '{"a": 1, "b": 1}' '{"a": 2, "c": 2}' | combine_status -> '{"a": 3, "b": 1, "c": 2}' +# echo '{"a": null}' '{"a": 1}' | combine_status -> '{"a": 1}' +# echo 'null' '{"a": 1}' | combine_status -> '{"a": 1}' +# echo '{"a": 1}' 'null' | combine_status -> '{"a": 1}' +# echo '{"a": null}' 'null' | combine_status -> '{"a": null}' +# echo 'null' 'null' | combine_status -> 'null' +# echo '{}' '{}' | combine_status -> '{}' +combine_status() { + jq -s \ + ' + def bitor: + map(select(. != null)) | unique | + if length == 0 then null + elif length == 1 then first + elif any(. == -1) then -1 + else (map(. % 2 | abs) | max) + 2 * (map(. / 2 | floor) | bitor) + end + ; + + map(select(. != null)) as $inputs | + reduce $inputs[] as $i (null; + reduce ($i | to_entries[]) as $e (. // {}; + .[$e.key] = ([.[$e.key], $e.value] | bitor) + ) + ) + ' +} + main() { + if ! prom2json --version &>/dev/null; then + echo "ERROR: prom2json is not installed. Exiting." >&2 + return 1 + fi + + update_serial_id + if [[ -z "${SEQUENCER_METRICS_URL:-}" ]]; then - update_serial_id SEQUENCER_METRICS_URL=$(generate_sequencer_metrics_url) fi - sv_status=$(sv_get_status) + # Get SV and Scan status + local sv_status; sv_status=$(sv_get_status) + local scan_status_rounds; scan_status_rounds=$(scan_get_status_rounds) + local scan_status_events; scan_status_events=$(scan_get_status_events) + local scan_status; scan_status=$(echo "$scan_status_rounds" "$scan_status_events" | combine_status) + + # Get acknowledgment metrics from Sequencer + local sequencer_metric_name; sequencer_metric_name=daml_sequencer_block_acknowledgments_micros + local sequencer_metric_data; sequencer_metric_data=$(get_metric_data "$SEQUENCER_METRICS_URL" "$sequencer_metric_name") + + # Get Mediator status + local mediator_status; mediator_status=$(get_status_from_sequencer_metric_data "$sequencer_metric_data" MED "$MEDIATOR_THRESHOLD") - sequencer_metric_name=daml_sequencer_block_acknowledgments_micros - sequencer_metric_data=$(get_sequencer_metric_data "$sequencer_metric_name") + # Get Sequencer status + local sequencer_status_lag; sequencer_status_lag=$(get_status_from_sequencer_metric_data "$sequencer_metric_data" SEQ "$SEQUENCER_THRESHOLD") + local sequencer_status_reachability; sequencer_status_reachability=$(sequencer_get_status_reachability) + local sequencer_status; sequencer_status=$(echo "$sequencer_status_lag" "$sequencer_status_reachability" | combine_status) - mediator_status=$(get_status_from_sequencer_metric_data "$sequencer_metric_data" "$sequencer_metric_name" MED "$MEDIATOR_THRESHOLD") - scan_status=$(scan_get_status) - sequencer_status=$(get_status_from_sequencer_metric_data "$sequencer_metric_data" "$sequencer_metric_name" SEQ "$SEQUENCER_THRESHOLD") + # Get CantonBFT status + local cantonbft_status; cantonbft_status=$(cantonbft_get_status_reachability) - jq -n \ + jq -Sn \ --argjson sv "$sv_status" \ --argjson sv_threshold "$SV_THRESHOLD" \ --argjson mediator "$mediator_status" \ --argjson mediator_threshold "$MEDIATOR_THRESHOLD" \ --argjson scan "$scan_status" \ --argjson scan_threshold_rounds "$SCAN_THRESHOLD_ROUNDS" \ - --argjson scan_threshold_event "$SCAN_THRESHOLD_EVENT" \ + --argjson scan_threshold_events "$SCAN_THRESHOLD_EVENTS" \ --argjson sequencer "$sequencer_status" \ --argjson sequencer_threshold "$SEQUENCER_THRESHOLD" \ + --argjson cantonbft "$cantonbft_status" \ ' { status: { sv: {nodes: $sv, description: "Last status report within \($sv_threshold) seconds"}, mediator: {nodes: $mediator, description: "Last acknowledgment within \($mediator_threshold) seconds"}, - scan: {nodes: $scan, description: "Reachable, last open and issuing rounds are within \($scan_threshold_rounds) seconds and recent event is within \($scan_threshold_event) seconds"}, - sequencer: {nodes: $sequencer, description: "Last acknowledgment within \($sequencer_threshold) seconds"}, + scan: {nodes: $scan, description: "Reachable, last open and issuing rounds are within \($scan_threshold_rounds) seconds and recent events are within \($scan_threshold_events) seconds"}, + sequencer: {nodes: $sequencer, description: "Reachable, last acknowledgment within \($sequencer_threshold) seconds"}, + cantonbft: {nodes: $cantonbft, description: "Reachable"}, }, generatedAt: (now | todate), } diff --git a/cluster/helm/splice-istio-gateway/templates/virtualService.yaml b/cluster/helm/splice-istio-gateway/templates/virtualService.yaml index 8fbc29ebef..3759ad35a7 100644 --- a/cluster/helm/splice-istio-gateway/templates/virtualService.yaml +++ b/cluster/helm/splice-istio-gateway/templates/virtualService.yaml @@ -21,6 +21,9 @@ spec: - port: 443 uri: prefix: "/cn-release-bundles" + - port: 80 + uri: + prefix: "/cn-release-bundles" route: - destination: port: @@ -29,6 +32,7 @@ spec: {{- end }} - match: - port: 443 + - port: 80 route: - destination: port: @@ -52,6 +56,7 @@ spec: http: - match: - port: 443 + - port: 80 route: - destination: port: diff --git a/cluster/helm/splice-postgres/Chart-template.yaml b/cluster/helm/splice-postgres/Chart-template.yaml index b242eb78fb..2095f3381c 100644 --- a/cluster/helm/splice-postgres/Chart-template.yaml +++ b/cluster/helm/splice-postgres/Chart-template.yaml @@ -7,7 +7,13 @@ type: application version: VERSION_NUMBER appVersion: VERSION_NUMBER -description: Splice Self-Hosted PGSQL +description: > + DEPRECATED: This chart is unsupported after 2026-11-12, the PostgreSQL 14 + end-of-life date, and receives no PostgreSQL version upgrades. Run Splice + against a PostgreSQL instance you provision yourself; see the migration guide + at https://docs.canton.network/global-synchronizer/production-operations/validator-postgres-migration + +deprecated: true dependencies: - name: splice-util-lib diff --git a/cluster/helm/splice-postgres/templates/NOTES.txt b/cluster/helm/splice-postgres/templates/NOTES.txt index 0df78ba342..a3bc3ee387 100644 --- a/cluster/helm/splice-postgres/templates/NOTES.txt +++ b/cluster/helm/splice-postgres/templates/NOTES.txt @@ -1 +1,14 @@ -Splice Self-Hosted PGSQL +****************************************************************************** +WARNING: splice-postgres IS DEPRECATED +****************************************************************************** + +This chart is unsupported after 2026-11-12, the PostgreSQL 14 upstream +end-of-life date, and receives no PostgreSQL version upgrades. No new chart +versions are published after 2026-10-12. + +Run Splice against a PostgreSQL instance you provision yourself; a managed +database service such as Amazon RDS or Google Cloud SQL is recommended. + +Migration guide: +https://docs.canton.network/global-synchronizer/production-operations/validator-postgres-migration +****************************************************************************** diff --git a/cluster/helm/splice-postgres/values-template.yaml b/cluster/helm/splice-postgres/values-template.yaml index dc028c6d22..dad72df81d 100644 --- a/cluster/helm/splice-postgres/values-template.yaml +++ b/cluster/helm/splice-postgres/values-template.yaml @@ -1,6 +1,12 @@ # Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# DEPRECATED: This chart is unsupported after 2026-11-12, the PostgreSQL 14 +# end-of-life date. A newer PostgreSQL major version does not start on this +# chart's data directory, so do not bump imageName; migrate to an externally +# provisioned PostgreSQL instead. See +# https://docs.canton.network/global-synchronizer/production-operations/validator-postgres-migration + imageRepo: "ghcr.io/digital-asset/decentralized-canton-sync/docker" imageName: "postgres:14" diff --git a/cluster/helm/splice-scan/templates/scan.yaml b/cluster/helm/splice-scan/templates/scan.yaml index 0aa7d5db18..53b219e3e5 100644 --- a/cluster/helm/splice-scan/templates/scan.yaml +++ b/cluster/helm/splice-scan/templates/scan.yaml @@ -143,30 +143,55 @@ spec: - name: ADDITIONAL_CONFIG_BULK_STORAGE value: | canton.scan-apps.scan-app.bulk-storage { - s-3 = { - endpoint = "{{ .s3.endpoint }}" - region = "{{ .s3.region }}" - bucket-name = "{{ .s3.bucketName }}" - access-key-id = ${SPLICE_APP_BULK_STORAGE_S3_ACCESS_KEY_ID} - secret-access-key = ${SPLICE_APP_BULK_STORAGE_S3_SECRET_ACCESS_KEY} + staging = { + endpoint = "{{ .staging.endpoint }}" + region = "{{ .staging.region }}" + bucket-name = "{{ .staging.bucketName }}" + access-key-id = ${SPLICE_APP_BULK_STORAGE_STAGING_ACCESS_KEY_ID} + secret-access-key = ${SPLICE_APP_BULK_STORAGE_STAGING_SECRET_ACCESS_KEY} + } + committed = { + endpoint = "{{ .committed.endpoint }}" + region = "{{ .committed.region }}" + bucket-name = "{{ .committed.bucketName }}" + access-key-id = ${SPLICE_APP_BULK_STORAGE_COMMITTED_ACCESS_KEY_ID} + secret-access-key = ${SPLICE_APP_BULK_STORAGE_COMMITTED_SECRET_ACCESS_KEY} } } - - name: SPLICE_APP_BULK_STORAGE_S3_ACCESS_KEY_ID - {{- if (($.Values.bulkStorage.s3).secretOverrides).accessKey }} - value: {{ $.Values.bulkStorage.s3.secretOverrides.accessKey | quote }} + - name: SPLICE_APP_BULK_STORAGE_STAGING_ACCESS_KEY_ID + {{- if (($.Values.bulkStorage.staging).secretOverrides).accessKey }} + value: {{ $.Values.bulkStorage.staging.secretOverrides.accessKey | quote }} + {{- else }} + valueFrom: + secretKeyRef: + name: {{ $.Values.bulkStorage.staging.secretName }} + key: accessKey + {{- end }} + - name: SPLICE_APP_BULK_STORAGE_STAGING_SECRET_ACCESS_KEY + {{- if (($.Values.bulkStorage.staging).secretOverrides).secretAccessKey }} + value: {{ $.Values.bulkStorage.staging.secretOverrides.secretAccessKey | quote }} {{- else }} valueFrom: secretKeyRef: - name: {{ $.Values.bulkStorage.s3.secretName }} + name: {{ $.Values.bulkStorage.staging.secretName }} + key: secretAccessKey + {{- end }} + - name: SPLICE_APP_BULK_STORAGE_COMMITTED_ACCESS_KEY_ID + {{- if (($.Values.bulkStorage.committed).secretOverrides).accessKey }} + value: {{ $.Values.bulkStorage.committed.secretOverrides.accessKey | quote }} + {{- else }} + valueFrom: + secretKeyRef: + name: {{ $.Values.bulkStorage.committed.secretName }} key: accessKey - {{ end }} - - name: SPLICE_APP_BULK_STORAGE_S3_SECRET_ACCESS_KEY - {{- if (($.Values.bulkStorage.s3).secretOverrides).secretAccessKey }} - value: {{ $.Values.bulkStorage.s3.secretOverrides.secretAccessKey | quote }} + {{- end }} + - name: SPLICE_APP_BULK_STORAGE_COMMITTED_SECRET_ACCESS_KEY + {{- if (($.Values.bulkStorage.committed).secretOverrides).secretAccessKey }} + value: {{ $.Values.bulkStorage.committed.secretOverrides.secretAccessKey | quote }} {{- else }} valueFrom: secretKeyRef: - name: {{ $.Values.bulkStorage.s3.secretName }} + name: {{ $.Values.bulkStorage.committed.secretName }} key: secretAccessKey {{- end }} {{- end }} diff --git a/cluster/helm/splice-scan/tests/scan_test.yaml b/cluster/helm/splice-scan/tests/scan_test.yaml index e0fd0105e0..7f1c413a63 100644 --- a/cluster/helm/splice-scan/tests/scan_test.yaml +++ b/cluster/helm/splice-scan/tests/scan_test.yaml @@ -466,14 +466,22 @@ tests: secretOverrides: postgresPassword: "vault:kv/data/scan/postgres-password" bulkStorage: - s3: + staging: endpoint: "mock-endpoint" region: "mock-region" bucketName: "mock-bucket" secretName: "mock-secret" secretOverrides: - accessKey: "vault:kv/data/scan/s3-access-key" - secretAccessKey: "vault:kv/data/scan/s3-secret-key" + accessKey: "vault:kv/data/scan/staging-access-key" + secretAccessKey: "vault:kv/data/scan/staging-secret-key" + committed: + endpoint: "mock-endpoint" + region: "mock-region" + bucketName: "mock-bucket" + secretName: "mock-secret" + secretOverrides: + accessKey: "vault:kv/data/scan/committed-access-key" + secretAccessKey: "vault:kv/data/scan/committed-secret-key" auth: jwksUrl: "https://mock.com/.well-known/jwks.json" audience: "mock_audience" @@ -494,11 +502,17 @@ tests: path: spec.template.spec.initContainers[?(@.name=='scan-init')].env[?(@.name=='PGPASSWORD')].value pattern: "vault:kv/data/scan/postgres-password" - matchRegex: - path: spec.template.spec.containers[?(@.name=='scan-app')].env[?(@.name=='SPLICE_APP_BULK_STORAGE_S3_ACCESS_KEY_ID')].value - pattern: "vault:kv/data/scan/s3-access-key" + path: spec.template.spec.containers[?(@.name=='scan-app')].env[?(@.name=='SPLICE_APP_BULK_STORAGE_STAGING_ACCESS_KEY_ID')].value + pattern: "vault:kv/data/scan/staging-access-key" + - matchRegex: + path: spec.template.spec.containers[?(@.name=='scan-app')].env[?(@.name=='SPLICE_APP_BULK_STORAGE_STAGING_SECRET_ACCESS_KEY')].value + pattern: "vault:kv/data/scan/staging-secret-key" + - matchRegex: + path: spec.template.spec.containers[?(@.name=='scan-app')].env[?(@.name=='SPLICE_APP_BULK_STORAGE_COMMITTED_ACCESS_KEY_ID')].value + pattern: "vault:kv/data/scan/committed-access-key" - matchRegex: - path: spec.template.spec.containers[?(@.name=='scan-app')].env[?(@.name=='SPLICE_APP_BULK_STORAGE_S3_SECRET_ACCESS_KEY')].value - pattern: "vault:kv/data/scan/s3-secret-key" + path: spec.template.spec.containers[?(@.name=='scan-app')].env[?(@.name=='SPLICE_APP_BULK_STORAGE_COMMITTED_SECRET_ACCESS_KEY')].value + pattern: "vault:kv/data/scan/committed-secret-key" - notExists: path: spec.template.spec.containers[?(@.name=='scan-app')].env[?(@.valueFrom.secretKeyRef)] - matchRegex: diff --git a/cluster/helm/splice-splitwell-app/templates/virtualService.yaml b/cluster/helm/splice-splitwell-app/templates/virtualService.yaml index 1993efd00d..3e878bb4cd 100644 --- a/cluster/helm/splice-splitwell-app/templates/virtualService.yaml +++ b/cluster/helm/splice-splitwell-app/templates/virtualService.yaml @@ -14,6 +14,7 @@ spec: http: - match: - port: 443 + - port: 80 route: - destination: port: diff --git a/cluster/helm/splice-validator/templates/validator.yaml b/cluster/helm/splice-validator/templates/validator.yaml index 1d6c2f3072..17f0e6382f 100644 --- a/cluster/helm/splice-validator/templates/validator.yaml +++ b/cluster/helm/splice-validator/templates/validator.yaml @@ -92,12 +92,12 @@ spec: value: {{ .Values.scanAddress | quote }} {{ if .Values.validatorWalletUser }} - name: SPLICE_APP_VALIDATOR_WALLET_USER_NAME - value: {{ .Values.validatorWalletUser }} + value: {{ .Values.validatorWalletUser | quote }} {{ end }} {{- range $ii, $user := .Values.validatorWalletUsers }} - name: ADDITIONAL_CONFIG_VALIDATOR_WALLET_USER_{{ $ii }} value: | - canton.validator-apps.validator_backend.validator-wallet-users.{{ $ii }} = {{ $user }} + canton.validator-apps.validator_backend.validator-wallet-users.{{ $ii }} = {{ $user | quote }} {{- end }} {{ if .Values.validatorPartyHint }} - name: SPLICE_APP_VALIDATOR_PARTY_HINT diff --git a/cluster/helm/splice-validator/tests/validator_test.yaml b/cluster/helm/splice-validator/tests/validator_test.yaml index 9d63f11f97..5db35709c6 100644 --- a/cluster/helm/splice-validator/tests/validator_test.yaml +++ b/cluster/helm/splice-validator/tests/validator_test.yaml @@ -558,3 +558,4 @@ tests: capabilities: drop: - ALL + diff --git a/cluster/helm/splice-validator/tests/wallet-user-ids_test.yaml b/cluster/helm/splice-validator/tests/wallet-user-ids_test.yaml new file mode 100644 index 0000000000..1e569b9c42 --- /dev/null +++ b/cluster/helm/splice-validator/tests/wallet-user-ids_test.yaml @@ -0,0 +1,69 @@ +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +suite: "Validator wallet user ids" +templates: + - validator.yaml +release: + name: mock-validator +chart: + version: 0.1.1 + appVersion: 0.1.0 + +set: + # Things we need just to pass the schema. validatorWalletUser is deliberately + # left unset here so that validatorWalletUsers can be exercised: the schema + # accepts exactly one of the two. + nodeIdentifier: "helm-mock-1-validator" + validatorPartyHint: "helm-mock-1" + spliceInstanceNames: + networkName: MockNet + networkFaviconUrl: https://mock.net/favicon.ico + amuletName: Mocklet + amuletNameAcronym: MCK + nameServiceName: Mock Name Service + nameServiceNameAcronym: MNS + topup: + enabled: false + auth: + jwksUrl: "https://mock.com/.well-known/jwks.json" + audience: "mock_audience" + +tests: + - it: "quotes wallet user ids in the generated HOCON" + # An id containing '@' -- e.g. an email address, which some identity + # providers use as the user id -- is not a valid unquoted HOCON string. + set: + validatorWalletUsers: + - alice@example.com + - "12345678" + documentSelector: + path: kind + value: Deployment + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: ADDITIONAL_CONFIG_VALIDATOR_WALLET_USER_0 + value: | + canton.validator-apps.validator_backend.validator-wallet-users.0 = "alice@example.com" + # An all-digit id must stay a string rather than becoming a HOCON number. + - contains: + path: spec.template.spec.containers[0].env + content: + name: ADDITIONAL_CONFIG_VALIDATOR_WALLET_USER_1 + value: | + canton.validator-apps.validator_backend.validator-wallet-users.1 = "12345678" + + - it: "keeps a single all-digit wallet user id a string in the env var" + set: + validatorWalletUser: "12345678" + documentSelector: + path: kind + value: Deployment + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SPLICE_APP_VALIDATOR_WALLET_USER_NAME + value: "12345678" diff --git a/cluster/images/canton-sequencer/additional-config.conf b/cluster/images/canton-sequencer/additional-config.conf index 07a0dc6c58..2f65136564 100644 --- a/cluster/images/canton-sequencer/additional-config.conf +++ b/cluster/images/canton-sequencer/additional-config.conf @@ -8,8 +8,12 @@ canton.sequencers.sequencer { use-new-processor = true use-new-client = true } - sequencer.block.throughput-cap.messages { - confirmation-request.strict = false - topology.strict = false + sequencer { + config.consensus-empty-block-creation-timeout = 500.milliseconds + config.output-fetch-how-many-recipients = 2 + block.throughput-cap.messages { + confirmation-request.strict = false + topology.strict = false + } } } diff --git a/cluster/images/cometbft-watchdog/Dockerfile b/cluster/images/cometbft-watchdog/Dockerfile new file mode 100644 index 0000000000..9d075d816a --- /dev/null +++ b/cluster/images/cometbft-watchdog/Dockerfile @@ -0,0 +1,16 @@ +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +FROM python:3.12-slim@sha256:2c941e860699f878900b0edc2403613c234d4b32eda3cc9fa7036991a2a63c4a + +LABEL org.opencontainers.image.base.name="python:3.12-slim" + +COPY restart-watchdog.py /restart-watchdog.py + +COPY target/LICENSE . + +# Ensure logs get output unbuffered even when the output is a pipe and don't write byte code in the container. +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +CMD ["python3", "/restart-watchdog.py"] diff --git a/cluster/images/cometbft-watchdog/local.mk b/cluster/images/cometbft-watchdog/local.mk new file mode 100644 index 0000000000..742cc33298 --- /dev/null +++ b/cluster/images/cometbft-watchdog/local.mk @@ -0,0 +1,9 @@ +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +dir := $(call current_dir) + +$(dir)/$(docker-build): $(dir)/restart-watchdog.py $(dir)/target/LICENSE + +$(dir)/target/LICENSE: ${SPLICE_ROOT}/LICENSE | $(dir)/target + cp $< $@ diff --git a/cluster/images/cometbft-watchdog/restart-watchdog.py b/cluster/images/cometbft-watchdog/restart-watchdog.py new file mode 100644 index 0000000000..34f1d09a7b --- /dev/null +++ b/cluster/images/cometbft-watchdog/restart-watchdog.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import math +import os +import random +import re +import signal +import sys +import time +import urllib.error +import urllib.request + +# Rate of confirmation requests accepted by the sequencer. +SEQUENCER_METRIC = "daml_sequencer_block_events_total" +SEQUENCER_LABELS = {"type": "send-confirmation-request"} +# Rate of confirmation requests processed by the mediator (approved and rejected). +MEDIATOR_METRIC = "daml_mediator_requests_total" +MEDIATOR_LABELS = {} + +LABEL_RE = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)="((?:[^"\\]|\\.)*)"') +ESCAPES = {"\\": "\\", '"': '"', "n": "\n"} + + +class ConfigError(Exception): + pass + + +class ScrapeError(Exception): + pass + + +# minimal json log helper to avoid having to pull in dependencies. field names chosen to match canton logging. +def log(severity, message, **fields): + entry = { + "level": severity, + "@timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + "message": message, + } + entry.update(fields) + print(json.dumps(entry), flush=True) + +# unescape helper to deal with escaped labels +def unescape(value): + out = [] + chars = iter(value) + for char in chars: + if char == "\\": + escaped = next(chars, "") + out.append(ESCAPES.get(escaped, escaped)) + else: + out.append(char) + return "".join(out) + + +def parse_samples(text, metric, required_labels, source): + """Parse all entries for `metric` that have `required_labels` and return them as a dict indexed by the labels. + """ + samples = {} + labelled_prefix = metric + "{" + unlabelled_prefix = metric + " " + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if line.startswith(labelled_prefix): + # Label values may contain '}', the value after the labels never does. + labels_end = line.rindex("}") + labels = { + match.group(1): unescape(match.group(2)) + for match in LABEL_RE.finditer(line[len(labelled_prefix) : labels_end]) + } + rest = line[labels_end + 1 :] + elif line.startswith(unlabelled_prefix): + labels = {} + rest = line[len(metric) :] + else: + continue + if any(labels.get(name) != value for name, value in required_labels.items()): + continue + # `rest` is the value, optionally followed by a timestamp. + try: + value = float(rest.split()[0]) + except (IndexError, ValueError): + log("WARNING", "Ignoring unparseable sample", source=source, line=line) + continue + if math.isnan(value): + continue + samples[tuple(sorted(labels.items()))] = value + return samples + + +def scrape(url, metric, required_labels, timeout): + try: + request = urllib.request.Request( + url, headers={"Accept": "text/plain"} + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + charset = response.headers.get_content_charset() or "utf-8" + body = response.read().decode(charset, errors="replace") + except Exception as error: + raise ScrapeError(f"failed to scrape {url}: {error}") from error + samples = parse_samples(body, metric, required_labels, url) + return samples + + +def counter_increase(previous, current): + # we already did filter to relevant labels when scraping so here we just sum up everything. + total_previous = 0.0 + total_current = 0.0 + for key, value in previous.items(): + total_previous += value + for key, value in current.items(): + total_current += value + return total_current - total_previous + + +def env_float(name): + raw = os.environ.get(name) + if not raw: + raise ConfigError(f"{name} must be set") + try: + value = float(raw) + except ValueError as error: + raise ConfigError(f"{name} must be a number, got {raw!r}") from error + return value + + +def env_url(name): + url = os.environ.get(name, "").strip() + if not url: + raise ConfigError(f"{name} must be set") + return url + + +class Config: + def __init__(self): + self.sequencer_url = env_url("WATCHDOG_SEQUENCER_METRICS_URL") + self.mediator_url = env_url("WATCHDOG_MEDIATOR_METRICS_URL") + self.poll_interval = env_float("WATCHDOG_POLL_INTERVAL_SECONDS") + self.threshold = env_float("WATCHDOG_THRESHOLD") + self.evaluation_interval = env_float("WATCHDOG_EVALUATION_INTERVAL_SECONDS") + self.scrape_timeout = env_float("WATCHDOG_SCRAPE_TIMEOUT_SECONDS") + self.startup_grace = env_float("WATCHDOG_STARTUP_GRACE_SECONDS") + self.cooldown = env_float("WATCHDOG_COOLDOWN_SECONDS") + self.marker_file = os.environ.get("WATCHDOG_MARKER_FILE") + if self.evaluation_interval < self.poll_interval: + raise ConfigError( + "WATCHDOG_EVALUATION_INTERVAL_SECONDS must be at least " + "WATCHDOG_POLL_INTERVAL_SECONDS, otherwise a single poll can trigger a " + "restart" + ) + + def as_dict(self): + return { + "sequencerUrl": self.sequencer_url, + "mediatorUrl": self.mediator_url, + "pollIntervalSeconds": self.poll_interval, + "threshold": self.threshold, + "evaluationIntervalSeconds": self.evaluation_interval, + "scrapeTimeoutSeconds": self.scrape_timeout, + "startupGraceSeconds": self.startup_grace, + "cooldownSeconds": self.cooldown, + "markerFile": self.marker_file, + } + + +class Watchdog: + def __init__(self, config): + self.config = config + self.previous = None + self.breach_since = None + + def reset(self): + self.previous = None + self.breach_since = None + + def request_restart(self, reason): + marker = self.config.marker_file + # atomic file write + temporary = marker + ".tmp" + with open(temporary, "w", encoding="utf-8") as handle: + handle.write(reason + "\n") + os.replace(temporary, marker) + + def poll(self, now): + """Scrape and return last time and rate measured by sequencer and mediator""" + config = self.config + sequencer = scrape( + config.sequencer_url, SEQUENCER_METRIC, SEQUENCER_LABELS, config.scrape_timeout + ) + mediator = scrape( + config.mediator_url, MEDIATOR_METRIC, MEDIATOR_LABELS, config.scrape_timeout + ) + if not sequencer: + raise ScrapeError( + f"no {SEQUENCER_METRIC} samples matching {SEQUENCER_LABELS} were returned" + ) + if not mediator: + raise ScrapeError(f"no {MEDIATOR_METRIC} samples were returned") + + previous = self.previous + self.previous = (now, sequencer, mediator) + if previous is None: + return None + previous_time, previous_sequencer, previous_mediator = previous + elapsed = now - previous_time + if elapsed <= 0: + return None + sequencer_rate = counter_increase(previous_sequencer, sequencer) / elapsed + mediator_rate = counter_increase(previous_mediator, mediator) / elapsed + return previous_time, sequencer_rate, mediator_rate + + def evaluate(self, now, observation): + """Given the data from poll check if we exceeded the threshold. Returns the reason to restart, or None.""" + interval_start, sequencer_rate, mediator_rate = observation + # counters reset to 0 after restart so guard against that. + if sequencer_rate < 0: + Log( + "INFO", + "sequencer rate was negative likely because sequencer restarted, resetting state", + sequencerRate=round(sequencer_rate, 4) + ) + self.reset() + return None + if mediator_rate < 0: + Log( + "INFO", + "mediator rate was negative likely because mediator restarted, resetting state", + mediatorRate=round(mediator_rate, 4) + ) + self.reset() + return None + difference = sequencer_rate - mediator_rate + if difference > self.config.threshold: + if self.breach_since is None: + self.breach_since = interval_start + breach_duration = now - self.breach_since + else: + self.breach_since = None + breach_duration = 0.0 + + log( + "INFO", + "Evaluated synchronizer progress", + sequencerRate=round(sequencer_rate, 4), + mediatorRate=round(mediator_rate, 4), + difference=round(difference, 4), + threshold=self.config.threshold, + breachDurationSeconds=round(breach_duration, 1), + evaluationIntervalSeconds=self.config.evaluation_interval, + ) + + if self.breach_since is not None and breach_duration >= self.config.evaluation_interval: + return ( + f"sequencer rate exceeded mediator rate by {difference:.4f}/s " + f"(threshold {self.config.threshold}/s) for {breach_duration:.0f}s" + ) + return None + + +def main(): + signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) + try: + config = Config() + except ConfigError as error: + log("CRITICAL", f"Invalid configuration: {error}") + return 2 + + watchdog = Watchdog(config) + log("INFO", "Starting CometBFT restart watchdog", config=config.as_dict()) + + # don't immediately kill cometbft after startup + quiet_until = time.monotonic() + config.startup_grace + while True: + cycle_start = time.monotonic() + try: + observation = watchdog.poll(cycle_start) + except ScrapeError as error: + # keep cometbft running if we fail to scrape. + log("WARNING", f"Skipping evaluation: {error}") + watchdog.reset() + observation = None + + if observation is not None: + reason = watchdog.evaluate(cycle_start, observation) + if reason is not None: + quiet_remaining = quiet_until - cycle_start + if quiet_remaining > 0: + log( + "INFO", + f"Not requesting a restart yet, CometBFT is being given time to " + f"come up: {reason}", + quietRemainingSeconds=round(quiet_remaining, 1), + ) + else: + log("WARN", f"Requesting a CometBFT restart: {reason}") + try: + watchdog.request_restart(reason) + except OSError as error: + log("ERROR", f"Failed to write {config.marker_file}: {error}") + return 1 + watchdog.reset() + # don't kill cometbft after we just killed it. + quiet_until = time.monotonic() + config.cooldown + + time.sleep(random.uniform(0.5, 1.0) * max(0.0, config.poll_interval - (time.monotonic() - cycle_start))) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cluster/images/cometbft/Dockerfile b/cluster/images/cometbft/Dockerfile index 22b79437ab..635a5301c2 100644 --- a/cluster/images/cometbft/Dockerfile +++ b/cluster/images/cometbft/Dockerfile @@ -3,10 +3,10 @@ ARG cometbft_version ARG cometbft_sha -FROM digitalasset-canton-enterprise-docker.jfrog.io/cometbft-canton-network:$cometbft_version@sha256:$cometbft_sha +FROM europe-docker.pkg.dev/da-images/public/docker/cometbft-canton-network:$cometbft_version@sha256:$cometbft_sha ARG cometbft_version -LABEL org.opencontainers.image.base.name="digitalasset-canton-enterprise-docker.jfrog.io/cometbft-canton-network:$cometbft_version" +LABEL org.opencontainers.image.base.name="europe-docker.pkg.dev/da-images/public/docker/cometbft-canton-network:$cometbft_version" COPY configure-state-sync.sh /cometbft/ RUN chmod +x /cometbft/configure-state-sync.sh diff --git a/cluster/images/local.mk b/cluster/images/local.mk index b0959f2265..dca762212c 100644 --- a/cluster/images/local.mk +++ b/cluster/images/local.mk @@ -9,6 +9,7 @@ images := \ canton-mediator \ canton-cometbft-sequencer \ cometbft \ + cometbft-watchdog \ \ splice-app \ splice-debug \ @@ -43,7 +44,13 @@ sequencer-image := cluster/images/canton-sequencer splice-ui-image := cluster/images/splice-web-ui images_file := cluster/images/.images -ifdef CI +ifdef GITHUB_ACTIONS + # never use the cache in CI on the master branch + cache_opt := --no-cache + platform_opt := --platform=linux/amd64,linux/arm64 + repo = $(GITHUB_SERVER_URL)/$(GITHUB_REPOSITORY) + commit_sha = $(GITHUB_SHA) +else ifdef CI # CCI # never use the cache in CI on the master branch cache_opt := --no-cache platform_opt := --platform=linux/amd64,linux/arm64 diff --git a/cluster/images/scan-app/app.conf b/cluster/images/scan-app/app.conf index 802305d344..7b5ec38a3a 100644 --- a/cluster/images/scan-app/app.conf +++ b/cluster/images/scan-app/app.conf @@ -60,12 +60,23 @@ canton { parameters { rate-limiting { default { - rate-per-second = 200 + rate-per-second = 100 + sustained-rate-per-second = 50 + } + global { + rate-per-second = 400 + sustained-rate-per-second = 200 + per-client-ip { + enabled = true + limit { + rate-per-second = 100 + sustained-rate-per-second = 50 + } + } } rate-limiters { getAcsSnapshot.rate-per-second = 20 getAcsSnapshotAt.rate-per-second = 10 - getDateOfMostRecentSnapshotBefore.rate-per-second = 10 } } # TODO(DACH-NY/canton-network-internal#2125) Revisit timeouts on 3.4 diff --git a/cluster/images/splice-app/Dockerfile b/cluster/images/splice-app/Dockerfile index 0a4ae86072..b78c59bc99 100644 --- a/cluster/images/splice-app/Dockerfile +++ b/cluster/images/splice-app/Dockerfile @@ -1,7 +1,7 @@ # Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -FROM europe-docker.pkg.dev/da-images/public/docker/da-base-image:full-1.0.9@sha256:127e022c310f9cf19399d34f85ab1f7ca5f248330de12c8a56e3cd12037dbcb7 +FROM europe-docker.pkg.dev/da-images/public/docker/da-base-image:full-1.0.13@sha256:a00f8aadb2c317ebe952d8c96bda5032f85bb29e30acdb257bd848b63b35e447 WORKDIR /app diff --git a/cluster/images/splice-test-cometbft/Dockerfile b/cluster/images/splice-test-cometbft/Dockerfile index 5e6fcbf8cd..b3a8c1e4c2 100644 --- a/cluster/images/splice-test-cometbft/Dockerfile +++ b/cluster/images/splice-test-cometbft/Dockerfile @@ -1,10 +1,10 @@ # tag should match the version in nix/cometbft-driver-sources.json ARG cometbft_version ARG cometbft_sha -FROM digitalasset-canton-enterprise-docker.jfrog.io/cometbft-canton-network:$cometbft_version@sha256:$cometbft_sha +FROM europe-docker.pkg.dev/da-images/public/docker/cometbft-canton-network:$cometbft_version@sha256:$cometbft_sha ARG cometbft_version -LABEL org.opencontainers.image.base.name="digitalasset-canton-enterprise-docker.jfrog.io/cometbft-canton-network:$cometbft_version" +LABEL org.opencontainers.image.base.name="europe-docker.pkg.dev/da-images/public/docker/cometbft-canton-network:$cometbft_version" # Copy the configuration files COPY configs / diff --git a/cluster/images/splice-test-docker-runner/Dockerfile b/cluster/images/splice-test-docker-runner/Dockerfile index 6c5c8bce9f..587bfec4f2 100644 --- a/cluster/images/splice-test-docker-runner/Dockerfile +++ b/cluster/images/splice-test-docker-runner/Dockerfile @@ -1,5 +1,5 @@ -ARG RUNNER_VERSION=2.335.1 -ARG RUNNER_DIGEST=sha256:08c30b0a7105f64bddfc485d2487a22aa03932a791402393352fdf674bda2c29 +ARG RUNNER_VERSION=2.336.0 +ARG RUNNER_DIGEST=sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda # Note that we don't currently support arm64 runners, so we build this only for amd64 FROM --platform=$BUILDPLATFORM ghcr.io/actions/actions-runner:${RUNNER_VERSION}@${RUNNER_DIGEST} diff --git a/cluster/images/splice-test-runner-hook/Dockerfile b/cluster/images/splice-test-runner-hook/Dockerfile index 5c9c8802a2..7104427fb3 100644 --- a/cluster/images/splice-test-runner-hook/Dockerfile +++ b/cluster/images/splice-test-runner-hook/Dockerfile @@ -1,5 +1,5 @@ -ARG RUNNER_VERSION=2.335.1 -ARG RUNNER_DIGEST=sha256:08c30b0a7105f64bddfc485d2487a22aa03932a791402393352fdf674bda2c29 +ARG RUNNER_VERSION=2.336.0 +ARG RUNNER_DIGEST=sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda # Note that we don't currently support arm64 runners, so we build this only for amd64 FROM --platform=$BUILDPLATFORM ghcr.io/actions/actions-runner:${RUNNER_VERSION}@${RUNNER_DIGEST} diff --git a/cluster/images/sv-app/app.conf b/cluster/images/sv-app/app.conf index a359a4b039..8b407493d4 100644 --- a/cluster/images/sv-app/app.conf +++ b/cluster/images/sv-app/app.conf @@ -103,11 +103,23 @@ canton { # TODO(DACH-NY/canton-network-internal#2125) Revisit timeouts on 3.4 custom-timeouts { onboardSvPartyMigrationAuthorize = 20 minutes - onboardSvSequencer = 5 minutes + onboardSvSequencer = 10 minutes } rate-limiting { default { - rate-per-second = 200 + rate-per-second = 20 + sustained-rate-per-second = 10 + } + global { + rate-per-second = 100 + sustained-rate-per-second = 50 + per-client-ip { + enabled = true + limit { + rate-per-second = 20 + sustained-rate-per-second = 10 + } + } } rate-limiters { prepareValidatorOnboarding.rate-per-second = 1 diff --git a/cluster/pulumi/canton-network/bigquery-cloudsql.sh b/cluster/pulumi/canton-network/bigquery-cloudsql.sh index 5603a654c2..3d655ff464 100755 --- a/cluster/pulumi/canton-network/bigquery-cloudsql.sh +++ b/cluster/pulumi/canton-network/bigquery-cloudsql.sh @@ -200,9 +200,11 @@ case "$SUBCOMMAND" in IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = '$PUBLICATION_NAME') THEN ALTER PUBLICATION $PUBLICATION_NAME SET TABLE $TABLES_TO_REPLICATE_JOINED; + ELSE CREATE PUBLICATION $PUBLICATION_NAME - FOR TABLE $TABLES_TO_REPLICATE_JOINED; + FOR TABLE $TABLES_TO_REPLICATE_JOINED + WITH (publish_via_partition_root = true); END IF; END \$\$; COMMIT; -- otherwise fails with "cannot create logical replication slot diff --git a/cluster/pulumi/canton-network/src/chaosMesh.ts b/cluster/pulumi/canton-network/src/chaosMesh.ts index 9442791948..558b78ee1f 100644 --- a/cluster/pulumi/canton-network/src/chaosMesh.ts +++ b/cluster/pulumi/canton-network/src/chaosMesh.ts @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import * as k8s from '@pulumi/kubernetes'; import { - appsAffinityAndTolerations, + appsKubernetesScheduling, clusterYamlConfig, DecentralizedSynchronizerUpgradeConfig, GCP_PROJECT, HELM_MAX_HISTORY_SIZE, - infraAffinityAndTolerations, + infraAndAppsKubernetesSchedulingForDaemonSets, + infraKubernetesScheduling, } from '@canton-network/splice-pulumi-common'; import { Resource } from '@pulumi/pulumi'; import { z } from 'zod'; @@ -16,6 +17,7 @@ const chaosMeshSchema = z.object({ chaosMesh: z .object({ dabftLatency: z.string().optional(), + podKillSchedule: z.string().optional(), }) .optional(), }); @@ -30,6 +32,7 @@ export const podKillSchedule = ( chaosMeshNs: k8s.core.v1.Namespace, appName: string, appNs: string, + schedule: string | undefined, dependsOn: Resource[] ): k8s.apiextensions.CustomResource => new k8s.apiextensions.CustomResource( @@ -42,8 +45,7 @@ export const podKillSchedule = ( namespace: chaosMeshNs.metadata.name, }, spec: { - // TODO(DACH-NY/canton-network-node#10689) Reduce this back to 5min once Canton sequencers stop being so slow - schedule: '@every 60m', + schedule: schedule ?? '@every 60m', historyLimit: 2, concurrencyPolicy: 'Forbid', type: 'PodChaos', @@ -210,35 +212,18 @@ export const installChaosMesh = ({ dependsOn }: ChaosMeshArguments): k8s.helm.v3 }, values: { controllerManager: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, chaosDaemon: { - ...infraAffinityAndTolerations, - // the chaos-daemon needs to run on the apps nodes to be able to inject latency - tolerations: [ - ...infraAffinityAndTolerations.tolerations, - ...appsAffinityAndTolerations.tolerations, - ], - affinity: { - nodeAffinity: { - requiredDuringSchedulingIgnoredDuringExecution: { - nodeSelectorTerms: [ - ...infraAffinityAndTolerations.affinity.nodeAffinity - .requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, - ...appsAffinityAndTolerations.affinity.nodeAffinity - .requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, - ], - }, - }, - }, + ...infraAndAppsKubernetesSchedulingForDaemonSets, runtime: 'containerd', socketPath: '/run/containerd/containerd.sock', }, dashboard: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, dnsServer: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, maxHistory: HELM_MAX_HISTORY_SIZE, }, @@ -251,11 +236,16 @@ export const installChaosMesh = ({ dependsOn }: ChaosMeshArguments): k8s.helm.v3 `global-domain-${DecentralizedSynchronizerUpgradeConfig.active.id}-cometbft`, `global-domain-${DecentralizedSynchronizerUpgradeConfig.active.id}-mediator`, `global-domain-${DecentralizedSynchronizerUpgradeConfig.active.id}-sequencer`, - `participant-${DecentralizedSynchronizerUpgradeConfig.active.id}`, + 'participant', 'scan-app', 'sv-app', 'validator-app', - ].forEach(name => podKillSchedule(ns, name, 'sv-4', [roleBinding, ...dependsOn])); + ].forEach(name => + podKillSchedule(ns, name, 'sv-4', config.chaosMesh?.podKillSchedule, [ + roleBinding, + ...dependsOn, + ]) + ); if (config.chaosMesh?.dabftLatency) { dabftLatency(ns, config.chaosMesh.dabftLatency, [roleBinding, ...dependsOn]); } diff --git a/cluster/pulumi/canton-network/src/clusterVersion.ts b/cluster/pulumi/canton-network/src/clusterVersion.ts index 1a59bea4e4..000a28c1a5 100644 --- a/cluster/pulumi/canton-network/src/clusterVersion.ts +++ b/cluster/pulumi/canton-network/src/clusterVersion.ts @@ -40,6 +40,10 @@ export function installClusterVersion(): k8s.apiextensions.CustomResource { port: 443, uri: { exact: '/version' }, }, + { + port: 80, + uri: { exact: '/version' }, + }, ], directResponse: { status: 200, diff --git a/cluster/pulumi/canton-network/src/dso.ts b/cluster/pulumi/canton-network/src/dso.ts index 457e928b73..971708ef23 100644 --- a/cluster/pulumi/canton-network/src/dso.ts +++ b/cluster/pulumi/canton-network/src/dso.ts @@ -2,44 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 import * as pulumi from '@pulumi/pulumi'; import { - activeVersion, Auth0Client, - BucketConfig, - BucketLocation, - BootstrappingDumpConfig, CnInput, - config, DecentralizedSynchronizerMigrationConfig, - ExpectedValidatorOnboarding, - SvCometBftGovernanceKey, - svCometBftGovernanceKeyFromSecret, - SvIdKey, - svKeyFromSecret, - ValidatorTopupConfig, + exactNamespace, } from '@canton-network/splice-pulumi-common'; import { - approvedSvIdentities, configForSv, coreSvsToDeploy, - initialRound, - StaticCometBftConfigWithNodeName, StaticSvConfig, - SvOnboarding, } from '@canton-network/splice-pulumi-common-sv'; -import { InstalledSv, installSvNode } from '@canton-network/splice-pulumi-common-sv/src/sv'; +import { + InstalledSv, + installSvNodeStandalone, +} from '@canton-network/splice-pulumi-common-sv/src/sv'; interface DsoArgs { auth0Client: Auth0Client; - expectedValidatorOnboardings: ExpectedValidatorOnboarding[]; // Only used by the sv1 - isDevNet: boolean; - periodicBackupConfig?: BucketConfig; - identitiesBackupLocation: BucketLocation; - bootstrappingDumpConfig?: BootstrappingDumpConfig; - topupConfig?: ValidatorTopupConfig; - splitPostgresInstances: boolean; decentralizedSynchronizerUpgradeConfig: DecentralizedSynchronizerMigrationConfig; - onboardingPollingInterval?: string; - disableOnboardingParticipantPromotionDelay: boolean; + exportSvResources?: boolean; } export class Dso extends pulumi.ComponentResource { @@ -47,121 +28,30 @@ export class Dso extends pulumi.ComponentResource { sv1: Promise; allSvs: Promise; - private joinViaSv1(sv1: pulumi.Resource, keys: CnInput): SvOnboarding { - return { - type: 'join-with-key', - sponsorApiUrl: `http://sv-app.sv-1:5014`, - sponsorScanUrl: `http://scan-app.sv-1:5012`, - sponsorRelease: sv1, - keys, - }; - } - private async installSvNode( svConf: StaticSvConfig, - onboarding: SvOnboarding, - nodeConfigs: { - sv1: StaticCometBftConfigWithNodeName; - peers: StaticCometBftConfigWithNodeName[]; - }, - expectedValidatorOnboardings: ExpectedValidatorOnboarding[], - isFirstSv = false, - cometBftGovernanceKey: CnInput | undefined = undefined, extraDependsOn: CnInput[] = [] - ) { + ): Promise { + const xns = exactNamespace(svConf.nodeName, true, this.args.exportSvResources); const dynamicConfig = configForSv(svConf.nodeName); - return installSvNode( - { - isFirstSv, - nodeName: svConf.nodeName, - ingressName: svConf.ingressName, - onboardingName: svConf.onboardingName, - nodeConfigs, - cometBft: svConf.cometBft, - validatorWalletUser: svConf.validatorWalletUser, - auth0ValidatorAppName: svConf.auth0ValidatorAppName, - auth0SvAppName: svConf.auth0SvAppName, - onboarding, - auth0Client: this.args.auth0Client, - expectedValidatorOnboardings, - isDevNet: this.args.isDevNet, - periodicBackupConfig: this.args.periodicBackupConfig, - identitiesBackupLocation: this.args.identitiesBackupLocation, - bootstrappingDumpConfig: this.args.bootstrappingDumpConfig, - topupConfig: this.args.topupConfig, - splitPostgresInstances: this.args.splitPostgresInstances, - disableOnboardingParticipantPromotionDelay: - this.args.disableOnboardingParticipantPromotionDelay, - onboardingPollingInterval: this.args.onboardingPollingInterval, - sweep: svConf.sweep, - cometBftGovernanceKey, - initialRound: initialRound?.toString(), - version: dynamicConfig.versionOverride ?? activeVersion, - ...dynamicConfig, - }, - this.args.decentralizedSynchronizerUpgradeConfig, - extraDependsOn + const sv = await installSvNodeStandalone( + xns, + svConf, + dynamicConfig, + this.args.auth0Client, + extraDependsOn, + (this.args.exportSvResources ?? false) ? { action: 'export' } : undefined ); + // we never run import from here so the following is ok + // migration code is temporary so it doesn't have to be beautiful + return sv!; } private async installDso() { const relevantSvConfs = coreSvsToDeploy; const [sv1Conf, ...restSvConfs] = relevantSvConfs; - const svIdKeys = restSvConfs.reduce>>((acc, conf) => { - const secretName = conf.svIdKeySecretName ?? conf.nodeName.replaceAll('-', '') + '-id'; - return { - ...acc, - [conf.onboardingName]: svKeyFromSecret(secretName), - }; - }, {}); - - const cometBftGovernanceKeys = relevantSvConfs - .filter(conf => configForSv(conf.nodeName)?.participant?.kms) - .reduce>>((acc, conf) => { - const secretName = - conf.cometBftGovernanceKeySecretName ?? - conf.nodeName.replaceAll('-', '') + '-cometbft-governance-key'; - return { - ...acc, - [conf.onboardingName]: svCometBftGovernanceKeyFromSecret(secretName), - }; - }, {}); - - const sv1CometBftConf = { - ...sv1Conf.cometBft, - nodeName: sv1Conf.nodeName, - ingressName: sv1Conf.ingressName, - }; - const peerCometBftConfs = restSvConfs.map(conf => ({ - ...conf.cometBft, - nodeName: conf.nodeName, - ingressName: conf.ingressName, - })); - - const sv1SvRewardWeightBps = (() => { - const found = approvedSvIdentities().find( - identity => identity.name == sv1Conf.onboardingName - ); - return found ? found.rewardWeightBps : 10000; - })(); - - const sv1 = await this.installSvNode( - sv1Conf, - { - type: 'found-dso', - sv1SvRewardWeightBps, - roundZeroDuration: config.optionalEnv('ROUND_ZERO_DURATION'), - initialRound: initialRound?.toString(), - }, - { - sv1: sv1CometBftConf, - peers: peerCometBftConfs, - }, - this.args.expectedValidatorOnboardings, - true, - cometBftGovernanceKeys[sv1Conf.onboardingName] - ); + const sv1 = await this.installSvNode(sv1Conf); // TODO(#893): long-term CantonBFT deployments should be robust enough to onboard in parallel again? const incrementalOnboarding = @@ -177,19 +67,8 @@ export class Dso extends pulumi.ComponentResource { } const [conf, ...remainingConfigs] = configs; - const onboarding: SvOnboarding = this.joinViaSv1(sv1.svApp, svIdKeys[conf.onboardingName]); - const cometBft = { - sv1: sv1CometBftConf, - peers: peerCometBftConfs.filter(c => c.id !== conf.cometBft.id), // remove self from peer list - }; - const newSv = await this.installSvNode( conf, - onboarding, - cometBft, - [], - false, - cometBftGovernanceKeys[conf.onboardingName], incrementalOnboarding ? previousSvs.map(sv => sv.svApp) : [] ); return installSvNodes(remainingConfigs, [...previousSvs, newSv]); diff --git a/cluster/pulumi/canton-network/src/index.ts b/cluster/pulumi/canton-network/src/index.ts index 69b6466f48..f87e5151b2 100644 --- a/cluster/pulumi/canton-network/src/index.ts +++ b/cluster/pulumi/canton-network/src/index.ts @@ -1,5 +1,6 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import * as pulumi from '@pulumi/pulumi'; import { Auth0ClientType, getAuth0Config, Auth0Fetch } from '@canton-network/splice-pulumi-common'; import { installClusterVersion } from './clusterVersion'; @@ -11,22 +12,29 @@ async function auth0CacheAndInstallCluster(auth0Fetch: Auth0Fetch) { installClusterVersion(); - const cluster = await installCluster(auth0Fetch); + const dso = await installCluster(auth0Fetch); await auth0Fetch.saveAuth0Cache(); - return cluster; + return (await dso?.allSvs)?.map(sv => ({ + nodeName: sv.nodeName, + databaseInstanceName: sv.appsPostgres.databaseId, + databaseSecretName: sv.appsPostgres.secretName, + })); } async function main() { const auth0FetchOutput = getAuth0Config(Auth0ClientType.MAINSTACK); - auth0FetchOutput.apply(async auth0Fetch => { - await auth0CacheAndInstallCluster(auth0Fetch); + const svs = auth0FetchOutput.apply(async auth0Fetch => { + const svs = await auth0CacheAndInstallCluster(auth0Fetch); scheduleLoadGenerator(auth0Fetch, []); + + return svs; }); + + return svs; } -// eslint-disable-next-line @typescript-eslint/no-floating-promises -main(); +export const svs = pulumi.output(main()); diff --git a/cluster/pulumi/canton-network/src/installCluster.ts b/cluster/pulumi/canton-network/src/installCluster.ts index ae2cd3fdf2..a856f1fd7f 100644 --- a/cluster/pulumi/canton-network/src/installCluster.ts +++ b/cluster/pulumi/canton-network/src/installCluster.ts @@ -4,21 +4,17 @@ import { Auth0Client, config, DecentralizedSynchronizerUpgradeConfig, - ExpectedValidatorOnboarding, + exactNamespace, isDevNet, - svOnboardingPollingInterval, - svValidatorTopupConfig, + spliceConfig, } from '@canton-network/splice-pulumi-common'; -import { readBackupConfig } from '@canton-network/splice-pulumi-common-validator/src/backup'; +import { configForSv, coreSvsToDeploy } from '@canton-network/splice-pulumi-common-sv'; import { - mustInstallSplitwell, - mustInstallValidator1, - splitwellOnboarding, - standaloneValidatorOnboarding, - validator1Onboarding, -} from '@canton-network/splice-pulumi-common-validator/src/validators'; -import { SplitPostgresInstances } from '@canton-network/splice-pulumi-common/src/config/configs'; -import { Resource } from '@pulumi/pulumi'; + configureScanBigQuery, + ScanBigQueryArgs, +} from '@canton-network/splice-pulumi-common-sv/src/bigQuery'; +import { InstalledSv } from '@canton-network/splice-pulumi-common-sv/src/sv'; +import { CloudPostgres } from '@canton-network/splice-pulumi-common/src/postgres'; import { activeVersion } from '../../common'; import { installChaosMesh } from './chaosMesh'; @@ -31,47 +27,40 @@ console.error(`Launching with isDevNet: ${isDevNet}`); const enableChaosMesh = config.envFlag('ENABLE_CHAOS_MESH'); -const disableOnboardingParticipantPromotionDelay = config.envFlag( - 'DISABLE_ONBOARDING_PARTICIPANT_PROMOTION_DELAY', - false -); - -export async function installCluster( - auth0Client: Auth0Client -): Promise<{ dso: Dso; validator1?: Resource }> { +export async function installCluster(auth0Client: Auth0Client): Promise { console.error( activeVersion.type === 'local' ? 'Using locally built charts by default' : `Using charts from the container registry by default, version ${activeVersion.version}` ); - const backupConfig = await readBackupConfig(); - const expectedValidatorOnboardings: ExpectedValidatorOnboarding[] = []; - if (mustInstallSplitwell) { - expectedValidatorOnboardings.push(splitwellOnboarding); - } - if (mustInstallValidator1) { - expectedValidatorOnboardings.push(validator1Onboarding); + // TODO(#6719) once all clusters have been migrated this can be removed. + const dso = spliceConfig.configuration.synchronizerMigration.splitSvDeploymentEnabled + ? undefined + : new Dso('dso', { + auth0Client, + decentralizedSynchronizerUpgradeConfig: DecentralizedSynchronizerUpgradeConfig, + exportSvResources: + spliceConfig.configuration.synchronizerMigration.migrateToSplitSvDeployment, + }); + + const locallyInstalledSvs = await dso?.allSvs; + const bigQueryArgs = [...iterateBigQueryArgs(locallyInstalledSvs)]; + if (bigQueryArgs.length > 1) { + throw new Error( + `Multiple SVs with BigQuery configuration found: ${bigQueryArgs.map(arg => arg.namespace.logicalName).join(', ')}` + ); } - if (standaloneValidatorOnboarding) { - expectedValidatorOnboardings.push(standaloneValidatorOnboarding); + for (const args of bigQueryArgs) { + await configureScanBigQuery(args); } - const dso = new Dso('dso', { - auth0Client, - expectedValidatorOnboardings, - isDevNet, - ...backupConfig, - topupConfig: svValidatorTopupConfig, - splitPostgresInstances: SplitPostgresInstances, - decentralizedSynchronizerUpgradeConfig: DecentralizedSynchronizerUpgradeConfig, - onboardingPollingInterval: svOnboardingPollingInterval, - disableOnboardingParticipantPromotionDelay, - }); - - const allSvs = await dso.allSvs; - - const svDependencies = allSvs.flatMap(sv => [sv.scan, sv.svApp, sv.validatorApp, sv.ingress]); + const svDependencies = (locallyInstalledSvs ?? []).flatMap(sv => [ + sv.scan, + sv.svApp, + sv.validatorApp, + sv.ingress, + ]); installDocs(); @@ -79,7 +68,46 @@ export async function installCluster( installChaosMesh({ dependsOn: svDependencies }); } - return { - dso, - }; + return dso; +} + +function* iterateBigQueryArgs( + locallyInstalledSvs?: Array +): Generator { + if (locallyInstalledSvs === undefined) { + for (const sv of coreSvsToDeploy) { + const config = configForSv(sv.nodeName); + const bigQueryConfig = config?.scanApp?.bigQuery; + const cloudSqlEnabled = (config.appsPg?.cloudSql ?? spliceConfig.pulumiProjectConfig.cloudSql) + .enabled; + if (bigQueryConfig !== undefined && cloudSqlEnabled) { + const namespace = exactNamespace(sv.nodeName, true, true); + yield { + namespace, + bigQueryConfig: bigQueryConfig, + scanReference: { + type: 'external', + databaseInstanceNamePrefix: `${namespace.logicalName}-cn-apps-pg`, + }, + }; + } + } + } else { + // TODO(#6719) once all clusters have been migrated this can be removed. + for (const sv of locallyInstalledSvs) { + const config = configForSv(sv.nodeName); + const bigQueryConfig = config?.scanApp?.bigQuery; + if (bigQueryConfig !== undefined && sv.appsPostgres instanceof CloudPostgres) { + yield { + namespace: sv.namespace, + bigQueryConfig: bigQueryConfig, + scanReference: { + type: 'local', + databaseInstance: sv.appsPostgres.databaseInstance, + chart: sv.scan, + }, + }; + } + } + } } diff --git a/cluster/pulumi/canton-network/tsconfig.eslint.json b/cluster/pulumi/canton-network/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/canton-network/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/circleci/src/index.ts b/cluster/pulumi/circleci/src/index.ts index 238e845e92..e3b6a5365e 100644 --- a/cluster/pulumi/circleci/src/index.ts +++ b/cluster/pulumi/circleci/src/index.ts @@ -4,10 +4,10 @@ import * as gcp from '@pulumi/gcp'; import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; import { - appsAffinityAndTolerations, + appsKubernetesScheduling, ChartValues, HELM_MAX_HISTORY_SIZE, - infraAffinityAndTolerations, + infraKubernetesScheduling, } from '@canton-network/splice-pulumi-common'; import { spliceEnvConfig } from '@canton-network/splice-pulumi-common/src/config/envConfig'; import { Namespace } from '@pulumi/kubernetes/core/v1'; @@ -166,7 +166,7 @@ function resourceClass( }, }, ], - ...appsAffinityAndTolerations, + ...appsKubernetesScheduling, }, }; } @@ -221,7 +221,7 @@ new k8s.helm.v3.Release('container-agent', { memory: '512Mi', }, }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, maxHistory: HELM_MAX_HISTORY_SIZE, }, }, diff --git a/cluster/pulumi/circleci/tsconfig.eslint.json b/cluster/pulumi/circleci/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/circleci/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/cluster/src/config.ts b/cluster/pulumi/cluster/src/config.ts index 428dd9e2e1..62b33311c4 100644 --- a/cluster/pulumi/cluster/src/config.ts +++ b/cluster/pulumi/cluster/src/config.ts @@ -9,10 +9,14 @@ const GkeNodePoolConfigSchema = z.object({ nodeType: z.string(), bootDiskSizeGb: z.number().optional(), zones: z.literal('*').or(z.array(z.string())).optional(), + labels: z.record(z.string(), z.string()).optional(), + priority: z.number().optional(), }); + const GkeClusterConfigSchema = z.object({ nodePools: z.object({ infra: GkeNodePoolConfigSchema, + additionalInfra: z.array(GkeNodePoolConfigSchema).default([]), apps: GkeNodePoolConfigSchema, additionalApps: z.array(GkeNodePoolConfigSchema).default([]), }), diff --git a/cluster/pulumi/cluster/src/fluentBit.ts b/cluster/pulumi/cluster/src/fluentBit.ts index 016839e162..3ee2e108bb 100644 --- a/cluster/pulumi/cluster/src/fluentBit.ts +++ b/cluster/pulumi/cluster/src/fluentBit.ts @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import * as k8s from '@pulumi/kubernetes'; import { - appsAffinityAndTolerations, + appsKubernetesScheduling, CLUSTER_NAME, exactNamespace, GCP_REGION, - infraAffinityAndTolerations, + infraAndAppsKubernetesSchedulingForDaemonSets, + infraKubernetesScheduling, } from '@canton-network/splice-pulumi-common'; export function installFluentBit(): void { @@ -14,9 +15,7 @@ export function installFluentBit(): void { const fluentBit = exactNamespace('fluent-bit'); const values = { - tolerations: infraAffinityAndTolerations.tolerations.concat( - appsAffinityAndTolerations.tolerations - ), + ...infraAndAppsKubernetesSchedulingForDaemonSets, config: { inputs: [ // Input config is roughly copied from the default GCP config available from `kubectl get configmap -n kube-system fluentbit-gke-config-v1.4.0 -o yaml` diff --git a/cluster/pulumi/cluster/src/nodePools.ts b/cluster/pulumi/cluster/src/nodePools.ts index 276bccadcc..a1bc77fae1 100644 --- a/cluster/pulumi/cluster/src/nodePools.ts +++ b/cluster/pulumi/cluster/src/nodePools.ts @@ -1,9 +1,16 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 import * as gcp from '@pulumi/gcp'; -import { config, GCP_PROJECT } from '@canton-network/splice-pulumi-common'; +import * as k8s from '@pulumi/kubernetes'; +import * as pulumi from '@pulumi/pulumi'; +import { + config, + GCP_PROJECT, + appsComputeClassName, + infraComputeClassName, + useComputeClasses, +} from '@canton-network/splice-pulumi-common'; -import { hyperdiskSupportConfig } from '../../common/src/config/hyperdiskSupportConfig'; import { gkeClusterConfig, GkeNodePoolConfig } from './config'; export async function installNodePools(): Promise { @@ -14,36 +21,37 @@ export async function installNodePools(): Promise { const zones = await gcp.compute.getZones({ region: config.requireEnv('CLOUDSDK_COMPUTE_REGION'), }); + const nodePoolComputeZone = config.optionalEnv('CLOUDSDK_NODEPOOL_COMPUTE_ZONE'); - installAppsNodePools(cluster, zones.names, [ + const appPools = installAppsNodePools(cluster, zones.names, [ gkeClusterConfig.nodePools.apps, ...gkeClusterConfig.nodePools.additionalApps, ]); + const infraPools = installInfraNodePools(cluster, zones.names, nodePoolComputeZone, [ + gkeClusterConfig.nodePools.infra, + ...gkeClusterConfig.nodePools.additionalInfra, + ]); - const nodePoolComputeZone = config.optionalEnv('CLOUDSDK_NODEPOOL_COMPUTE_ZONE'); new gcp.container.NodePool( - 'cn-infra-node-pool', + 'gke-node-pool', { cluster, nodeConfig: { - machineType: gkeClusterConfig.nodePools.infra.nodeType, + machineType: 'e2-standard-4', taints: [ { effect: 'NO_SCHEDULE', - key: 'cn_infra', + key: 'components.gke.io/gke-managed-components', value: 'true', }, ], - labels: { - cn_infra: 'true', - }, loggingVariant: 'DEFAULT', }, nodeLocations: nodePoolComputeZone ? [nodePoolComputeZone] : undefined, initialNodeCount: 1, autoscaling: { - minNodeCount: gkeClusterConfig.nodePools.infra.minNodes, - maxNodeCount: gkeClusterConfig.nodePools.infra.maxNodes, + minNodeCount: 1, + maxNodeCount: 3, }, }, { @@ -51,117 +59,202 @@ export async function installNodePools(): Promise { } ); - new gcp.container.NodePool('gke-node-pool', { - cluster, - nodeConfig: { - machineType: 'e2-standard-4', - taints: [ - { - effect: 'NO_SCHEDULE', - key: 'components.gke.io/gke-managed-components', - value: 'true', - }, - ], - loggingVariant: 'DEFAULT', - }, - nodeLocations: nodePoolComputeZone ? [nodePoolComputeZone] : undefined, - initialNodeCount: 1, - autoscaling: { - minNodeCount: 1, - maxNodeCount: 3, - }, - }); + if (useComputeClasses) { + installComputeClass(appsComputeClassName, appPools); + installComputeClass(infraComputeClassName, infraPools); + } +} + +type NodeConfigLabelsAndTaints = Pick< + gcp.types.input.container.NodePoolNodeConfig, + 'labels' | 'taints' +>; + +interface NodePoolWithConfig { + pool: gcp.container.NodePool; + config: GkeNodePoolConfig; } function installAppsNodePools( cluster: string, allZones: string[], configs: Array -): Array { - const nodepoolLocation = config.optionalEnv('CLOUDSDK_HYPERDISK_NODEPOOL_COMPUTE_ZONE'); +): Array { + const defaultZone = config.optionalEnv('CLOUDSDK_HYPERDISK_NODEPOOL_COMPUTE_ZONE'); return configs.map((config, index) => { - const zones = - config.zones === '*' - ? allZones - : (config.zones ?? (nodepoolLocation !== undefined ? [nodepoolLocation] : undefined)); - if (hyperdiskSupportConfig.hyperdiskSupport.enabled) { - return hyperdiskNodePool(index, cluster, zones, config); - } else { - return appsNodePool(index, cluster, zones, config); - } + const name = + index === 0 + ? 'cn-apps-node-pool-hd' // for backwards compat + : `cn-apps-node-pool-${index}-hd`; + // With ComputeClasses, we rely on the `cloud.google.com/compute-class` label only. + // That label *must* be present for the ComputeClass use the node pool, even + // if it's explicitly mentioned in the ComputeClass priorities. + const labelsAndTaints: NodeConfigLabelsAndTaints = useComputeClasses + ? { + taints: [ + { + effect: 'NO_SCHEDULE', + key: 'cloud.google.com/compute-class', + value: appsComputeClassName, + }, + ], + labels: { + 'cloud.google.com/compute-class': appsComputeClassName, + ...config.labels, + }, + } + : { + taints: [ + { + effect: 'NO_SCHEDULE', + key: 'cn_apps', + value: 'true', + }, + ], + labels: { + cn_apps: 'hyperdisk', + ...config.labels, + }, + }; + const pool = new gcp.container.NodePool( + name, + { + cluster, + nodeConfig: { + machineType: config.nodeType, + bootDisk: { + diskType: 'hyperdisk-balanced', + sizeGb: config.bootDiskSizeGb || 100, + }, + ...labelsAndTaints, + loggingVariant: 'DEFAULT', + }, + nodeLocations: + config.zones === '*' + ? allZones + : (config.zones ?? (defaultZone !== undefined ? [defaultZone] : undefined)), + initialNodeCount: 0, + autoscaling: autoscalingConfigOf(config), + }, + { + replaceOnChanges: ['nodeConfig.machineType'], + } + ); + return { pool, config }; }); } -function hyperdiskNodePool( - index: number, +function installInfraNodePools( cluster: string, - zones: string[] | undefined, - config: GkeNodePoolConfig -): gcp.container.NodePool { - const name = - index === 0 - ? 'cn-apps-node-pool-hd' // for backwards compat - : `cn-apps-node-pool-${index}-hd`; - return new gcp.container.NodePool(name, { - cluster, - nodeConfig: { - machineType: config.nodeType, - bootDisk: { - diskType: 'hyperdisk-balanced', - sizeGb: config.bootDiskSizeGb || 100, - }, - taints: [ - { - effect: 'NO_SCHEDULE', - key: 'cn_apps', - value: 'true', + allZones: string[], + defaultZone: string | undefined, + configs: Array +): Array { + return configs.map((config, index) => { + const name = + index === 0 + ? 'cn-infra-node-pool' // for backwards compat + : `cn-infra-node-pool-${index}`; + + // With ComputeClasses, we rely on the `cloud.google.com/compute-class` label only. + // That label *must* be present for the ComputeClass use the node pool, even + // if it's explicitly mentioned in the ComputeClass priorities. + const labelsAndTaints: NodeConfigLabelsAndTaints = useComputeClasses + ? { + taints: [ + { + effect: 'NO_SCHEDULE', + key: 'cloud.google.com/compute-class', + value: infraComputeClassName, + }, + ], + labels: { + 'cloud.google.com/compute-class': infraComputeClassName, + ...config.labels, + }, + } + : { + taints: [ + { + effect: 'NO_SCHEDULE', + key: 'cn_infra', + value: 'true', + }, + ], + labels: { + cn_infra: 'true', + }, + }; + + const pool = new gcp.container.NodePool( + name, + { + cluster, + nodeConfig: { + machineType: config.nodeType, + ...labelsAndTaints, + loggingVariant: 'DEFAULT', }, - ], - labels: { - cn_apps: 'hyperdisk', + nodeLocations: + config.zones === '*' + ? allZones + : (config.zones ?? (defaultZone !== undefined ? [defaultZone] : undefined)), + initialNodeCount: 1, + autoscaling: autoscalingConfigOf(config), }, - loggingVariant: 'DEFAULT', - }, - nodeLocations: zones, - initialNodeCount: 0, - autoscaling: { - locationPolicy: 'ANY', - minNodeCount: config.minNodes, - maxNodeCount: config.maxNodes, - }, + { + replaceOnChanges: ['nodeConfig.machineType'], + } + ); + return { pool, config }; }); } -function appsNodePool( - index: number, - cluster: string, - zones: string[] | undefined, - appsNodePoolConfig: GkeNodePoolConfig -): gcp.container.NodePool { - const name = - index === 0 - ? 'cn-apps-node-pool' // for backwards compat - : `cn-apps-node-pool-${index}`; - return new gcp.container.NodePool(name, { - cluster, - nodeConfig: { - machineType: appsNodePoolConfig.nodeType, - taints: [ - { - effect: 'NO_SCHEDULE', - key: 'cn_apps', - value: 'true', - }, - ], - labels: { - cn_apps: 'standard', + +function installComputeClass( + name: string, + pools: NodePoolWithConfig[] +): k8s.apiextensions.CustomResource { + // Group node pools by their configured priority. + // Priority defaults to -index (so that the first pool is highest priority, second is next, etc), + // and any explicitly set positive priority will be sorted above the defaulted ones. + const byPriority = new Map[]>(); + pools.forEach(({ pool, config: poolConfig }, index) => { + const priority = poolConfig.priority ?? -index; + const group = byPriority.get(priority) ?? []; + group.push(pool.name); + byPriority.set(priority, group); + }); + + // Sort by descending priority (highest first) and emit one entry per group. + const priorities = [...byPriority.entries()] + .sort(([a], [b]) => b - a) + .map(([, nodepools]) => ({ nodepools })); + + return new k8s.apiextensions.CustomResource( + `compute-class-${name}`, + { + apiVersion: 'cloud.google.com/v1', + kind: 'ComputeClass', + metadata: { name }, + spec: { + priorities, + nodePoolAutoCreation: { enabled: false }, }, - loggingVariant: 'DEFAULT', }, - initialNodeCount: 0, - autoscaling: { - locationPolicy: 'ANY', - minNodeCount: appsNodePoolConfig.minNodes, - maxNodeCount: appsNodePoolConfig.maxNodes, - }, - }); + { + dependsOn: pools.map(({ pool }) => pool), + } + ); +} + +function autoscalingConfigOf(config: GkeNodePoolConfig): gcp.container.NodePoolArgs['autoscaling'] { + return { + // Location policy decides how nodes are allocated across zones when more then one zone is configured. + // By default it is set to BALANCED, which is useful in HA scenarios. We configure multiple zones on + // scratchnets and in CI to get better availability of compute resources so ANY is more suitable. + // For single-zone clusters, which includes prod clusters, this doesn't matter. + locationPolicy: 'ANY', + minNodeCount: config.minNodes, + maxNodeCount: config.maxNodes, + }; } diff --git a/cluster/pulumi/cluster/tsconfig.eslint.json b/cluster/pulumi/cluster/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/cluster/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/common-sv/src/approvedIdentities.ts b/cluster/pulumi/common-sv/src/approvedIdentities.ts index 61944dd782..178baa6169 100644 --- a/cluster/pulumi/common-sv/src/approvedIdentities.ts +++ b/cluster/pulumi/common-sv/src/approvedIdentities.ts @@ -19,11 +19,21 @@ export type ApprovedSvIdentity = { rewardWeightBps: number; }; +export type ApprovedSvIdentityFromYamlFile = { + name: string; + publicKey: string | pulumi.Output; + // js-yaml after 4.2 doesn't support specifying numbers with separating underscores (e.g. 100_000), + // which means that it will parse that as a string and break the on Helm schema validation. + // This is compliant with the yaml spec, which doesn't define numbers with underscores as possible. + // For normal numbers (e.g 100000), it still gets parsed as a number. + rewardWeightBps: string | number; +}; + export function approvedSvIdentitiesFile(): string | undefined { return getPathToPublicConfigFile('approved-sv-id-values.yaml'); } -function approvedSvIdentitiesFromFile(): ApprovedSvIdentity[] { +function approvedSvIdentitiesFromFile(): ApprovedSvIdentityFromYamlFile[] { const file = approvedSvIdentitiesFile(); return file ? loadYamlFromFile(file).approvedSvIdentities : []; } @@ -48,7 +58,12 @@ function approvedSvIdentitiesFromConfig(): ApprovedSvIdentity[] { } export function approvedSvIdentities(): ApprovedSvIdentity[] { - const fromFile = approvedSvIdentitiesFromFile(); + const rawFromFile = approvedSvIdentitiesFromFile(); + const fromFile: ApprovedSvIdentity[] = rawFromFile.map(identity => ({ + name: identity.name, + rewardWeightBps: parseInt(String(identity.rewardWeightBps).replaceAll('_', ''), 10), + publicKey: identity.publicKey, + })); const fromConfig = approvedSvIdentitiesFromConfig(); // We override public keys to the locally configured one, @@ -60,7 +75,8 @@ export function approvedSvIdentities(): ApprovedSvIdentity[] { ); return _.uniqBy([...fromFile, ...fromConfig], 'name').map(identity => ({ - ...identity, + name: identity.name, + rewardWeightBps: identity.rewardWeightBps, publicKey: configuredPublicKeys[identity.name] ?? identity.publicKey, })); } diff --git a/cluster/pulumi/common-sv/src/bigQuery.ts b/cluster/pulumi/common-sv/src/bigQuery.ts index a17cd6c273..2f513f6eaf 100644 --- a/cluster/pulumi/common-sv/src/bigQuery.ts +++ b/cluster/pulumi/common-sv/src/bigQuery.ts @@ -4,7 +4,9 @@ import * as command from '@pulumi/command'; import * as gcp from '@pulumi/gcp'; import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; +import * as fs from 'fs'; import * as ip from 'ip'; +import * as path from 'path'; import { InstalledHelmChart, installPostgresPasswordSecret, @@ -12,21 +14,26 @@ import { import { clusterProdLike, config } from '@canton-network/splice-pulumi-common/src/config'; import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; import { - Postgres, - CloudPostgres, + defaultUserName, generatePassword, + getCloudSdkZone, privateNetworkId, } from '@canton-network/splice-pulumi-common/src/postgres'; import { ExactNamespace, CLUSTER_BASENAME, + GCP_PROJECT, + GCP_REGION, commandScriptPath, } from '@canton-network/splice-pulumi-common/src/utils'; -interface ScanBigQueryConfig { - dataset: string; - prefix: string; -} +import { ScanBigQueryConfig } from './singleSvConfig'; + +// ============================================================================ +// PIPELINE CONFIGURATION & TYPES +// ============================================================================ + +const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000; interface PostgresPassword { contents: pulumi.Output; @@ -35,18 +42,64 @@ interface PostgresPassword { const dbPort = 5432; const replicatorUserName = 'bqdatastream'; + +// Remove legacy datastream configuration once migration to stag-prod pipeline is verified. +// issue: https://github.com/canton-network/splice/issues/6656 +// TODO (#6656) Remove legacy datastream configuration once migration to stag-prod pipeline is verified + const replicationSlotName = 'update_history_datastream_r_slot'; const publicationName = 'update_history_datastream_pub'; -// what tables from Scan to replicate to BigQuery -const tablesToReplicate = [ - 'update_history_creates', - 'update_history_exercises', - 'scan_verdict_store', - 'scan_verdict_transaction_view_store', - 'app_activity_record_store', -]; + +// Stream 2 (Stag-Prod) CDC Replication Configuration +const replicationSlotNameStagProd = 'update_history_datastream_stag_prod_r_slot'; +const publicationNameStagProd = 'update_history_datastream_stag_prod_pub'; + const flywayMigrationToWaitFor = 'V068__app_activity_record_meta.sql'; +// ============================================================================ +// SINGLE SOURCE OF TRUTH: REPLICATED TABLE CONFIGURATION +// ============================================================================ +// what tables from Scan to replicate to BigQuery +interface ReplicatedTableConfig { + primaryKey: string; + datePartitionColumn: string; + timeType: 'micros' | 'datastream_metadata'; +} + +const replicatedTables: Record = { + update_history_creates: { + primaryKey: 'row_id', + datePartitionColumn: 'record_time', + timeType: 'micros', + }, + update_history_exercises: { + primaryKey: 'row_id', + datePartitionColumn: 'record_time', + timeType: 'micros', + }, + scan_verdict_store: { + primaryKey: 'row_id', + datePartitionColumn: 'record_time', + timeType: 'micros', + }, + scan_verdict_transaction_view_store: { + primaryKey: 'verdict_row_id, view_id', + datePartitionColumn: 'source_timestamp', + timeType: 'datastream_metadata', + }, + app_activity_record_store: { + primaryKey: 'verdict_row_id', + datePartitionColumn: 'source_timestamp', + timeType: 'datastream_metadata', + }, +}; + +const tablesToReplicate = Object.keys(replicatedTables); + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + function cloudsdkComputeRegion() { return config.requireEnv('CLOUDSDK_COMPUTE_REGION'); } @@ -54,7 +107,6 @@ function cloudsdkComputeRegion() { function pickDatastreamPeeringCidr(): string { const baseCidr = config.requireEnv('GCP_MASTER_IPV4_CIDR'); const baseSubnet = ip.cidrSubnet(baseCidr); - // assert GCP_MASTER_IPV4_CIDR is a /28 CIDR if (baseSubnet.subnetMaskLength !== 28) { throw new Error(`Expected a /28 CIDR, but got ${baseCidr}`); @@ -63,12 +115,16 @@ function pickDatastreamPeeringCidr(): string { return ip.fromLong(ip.toLong(baseSubnet.networkAddress) + baseSubnet.length) + '/29'; } -function installNatVm(postgres: CloudPostgres): gcp.compute.Instance { - const vmName = `${postgres.namespace.logicalName}-nat-vm`; +function installNatVm( + namespace: ExactNamespace, + zone: string, + databaseInstance: gcp.sql.DatabaseInstance +): gcp.compute.Instance { + const vmName = `${namespace.logicalName}-nat-vm`; // from https://cloud.google.com/datastream/docs/private-connectivity#set-up-reverse-proxy const startupScript = pulumi.interpolate`#! /bin/bash -export DB_ADDR=${postgres.address} +export DB_ADDR=${databaseInstance.privateIpAddress} export DB_PORT=${dbPort} # Enable the VM to receive packets whose destinations do @@ -105,7 +161,7 @@ iptables-save return new gcp.compute.Instance(vmName, { machineType: 'e2-micro', - zone: postgres.zone, + zone, bootDisk: { initializeParams: { image: 'debian-cloud/debian-12', @@ -128,22 +184,28 @@ iptables-save }); } +// ============================================================================ +// DATASTREAM PIPELINE DEFINITIONS +// ============================================================================ + function installDatastream( - postgres: CloudPostgres, + namespace: ExactNamespace, + databaseInstance: gcp.sql.DatabaseInstance, source: gcp.datastream.ConnectionProfile, destination: gcp.datastream.ConnectionProfile, bigQueryDataset: gcp.bigquery.Dataset, - pubRepSlots: pulumi.Resource + pubRepSlots: pulumi.Resource, + desiredState: 'RUNNING' | 'PAUSED' ): gcp.datastream.Stream { - const streamName = `${postgres.namespace.logicalName}-scan-update-history`; - const schemaName = scanAppDatabaseName(postgres); + const streamName = `${namespace.logicalName}-scan-update-history`; + const schemaName = scanAppDatabaseName(namespace); return new gcp.datastream.Stream( streamName, { location: cloudsdkComputeRegion(), streamId: streamName, displayName: streamName, - desiredState: 'RUNNING', + desiredState: desiredState, sourceConfig: { postgresqlSourceConfig: { includeObjects: { @@ -173,26 +235,105 @@ function installDatastream( backfillAll: {}, labels: { cluster: CLUSTER_BASENAME, + datastream_id: 'legacy', }, }, - { dependsOn: [postgres, source, destination, bigQueryDataset, pubRepSlots] } + { dependsOn: [databaseInstance, source, destination, bigQueryDataset, pubRepSlots] } ); } +function installDatastream_stag_prod( + namespace: ExactNamespace, + databaseInstance: gcp.sql.DatabaseInstance, + source: gcp.datastream.ConnectionProfile, + destination: gcp.datastream.ConnectionProfile, + bigQueryDataset: gcp.bigquery.Dataset, + pubRepSlots: pulumi.Resource, + desiredState: 'RUNNING' | 'PAUSED' +): gcp.datastream.Stream { + const streamName = `${CLUSTER_BASENAME}-${namespace.logicalName}-stag-production-datastream`; + const schemaName = scanAppDatabaseName(namespace); + return new gcp.datastream.Stream( + streamName, + { + location: cloudsdkComputeRegion(), + streamId: streamName, + displayName: streamName, + desiredState: desiredState, + sourceConfig: { + postgresqlSourceConfig: { + includeObjects: { + postgresqlSchemas: [ + { + schema: schemaName, + postgresqlTables: tablesToReplicate.map(table => ({ table })), + }, + ], + }, + publication: publicationNameStagProd, + replicationSlot: replicationSlotNameStagProd, + }, + sourceConnectionProfile: source.name, + }, + destinationConfig: { + bigqueryDestinationConfig: { + singleTargetDataset: { + datasetId: pulumi.interpolate`projects/${bigQueryDataset.project}/datasets/${bigQueryDataset.datasetId}`, + }, + dataFreshness: '0s', + appendOnly: {}, + }, + destinationConnectionProfile: destination.name, + }, + backfillNone: {}, // Addressing issue #6919 - partition overflow problem with backfillAll, so using backfillNone for stag-prod datastream + ruleSets: tablesToReplicate.map(tableName => ({ + objectFilter: { + sourceObjectIdentifier: { + postgresqlIdentifier: { + schema: schemaName, + table: tableName, + }, + }, + }, + customizationRules: [ + { + bigqueryPartitioning: { + ingestionTimePartition: { + partitioningTimeGranularity: 'PARTITIONING_TIME_GRANULARITY_HOUR', + }, + }, + }, + ], + })), + labels: { + cluster: CLUSTER_BASENAME, + datastream_id: 'stag_prod', + }, + }, + { + dependsOn: [databaseInstance, source, destination, bigQueryDataset, pubRepSlots], + } + ); +} + +// ============================================================================ +// BIGQUERY DATASET CREATION +// ============================================================================ + function installBigqueryDataset(scanBigQuery: ScanBigQueryConfig): gcp.bigquery.Dataset { return new gcp.bigquery.Dataset(scanBigQuery.dataset, { datasetId: scanBigQuery.dataset, friendlyName: `${scanBigQuery.dataset} Dataset`, location: cloudsdkComputeRegion(), - deleteContentsOnDestroy: true, + deleteContentsOnDestroy: true, //retaining old value // TODO (DACH-NY/canton-network-internal#343) reduce time travel window from 7-day default to 2 days if // it makes a cost difference labels: { cluster: CLUSTER_BASENAME, + datastream_id: 'legacy', }, }); } - /* TODO (DACH-NY/canton-network-internal#341) remove this comment when enabled on all relevant clusters If you see an error like this gcp:datastream:ConnectionProfile (sv-4-scan-bq-cxn): @@ -208,12 +349,211 @@ you have to manually enable the API as described for that cluster. - done for da-cn-ci-2 */ +function installBigqueryStagingDataset(scanBigQuery: ScanBigQueryConfig): gcp.bigquery.Dataset { + return new gcp.bigquery.Dataset(`${scanBigQuery.dataset}-staging`, { + datasetId: `${scanBigQuery.dataset}_staging`, + friendlyName: `${scanBigQuery.dataset} Staging Dataset`, + location: cloudsdkComputeRegion(), + deleteContentsOnDestroy: true, + // ISSUE#6814: Do not rely on ingestion timestamps for retention in staging. + // GCP calculates expiration from the table creation date, which will delete + // staging tables at the 3-day mark even if production sync is incomplete. + labels: { + cluster: CLUSTER_BASENAME, + datastream_id: 'stag_prod', + }, + }); +} + +function installBigqueryProdDataset(scanBigQuery: ScanBigQueryConfig): gcp.bigquery.Dataset { + return new gcp.bigquery.Dataset(`${scanBigQuery.dataset}-prod`, { + datasetId: `${scanBigQuery.dataset}_prod`, + friendlyName: `${scanBigQuery.dataset} Production Dataset`, + location: cloudsdkComputeRegion(), + deleteContentsOnDestroy: false, + labels: { + cluster: CLUSTER_BASENAME, + }, + }); +} +// ============================================================================ +// IAM PERMISSIONS for SCHEDULED QUERIES +// ============================================================================ +interface ScheduledQueryContext { + projectId: pulumi.Output; + transferServiceAgentPermission: gcp.projects.IAMMember; +} + +function installBqScheduledQueryContext(): ScheduledQueryContext { + const currentProject = gcp.organizations.getProjectOutput({}); + const projectId = currentProject.apply(p => { + if (!p.projectId) { + throw new Error('Current GCP project output is missing a projectId.'); + } + return p.projectId; + }); + + const transferServiceAgentPermission = new gcp.projects.IAMMember('bq-transfer-token-creator', { + project: projectId, + role: 'roles/iam.serviceAccountTokenCreator', + member: currentProject.apply( + p => `serviceAccount:service-${p.number}@gcp-sa-bigquerydatatransfer.iam.gserviceaccount.com` + ), + }); + + return { projectId, transferServiceAgentPermission }; +} + +// ============================================================================ +// HOURLY DEDUPLICATION & SCHEDULED QUERIES +// ============================================================================ + +const rawSqlTemplate = fs.readFileSync(path.join(__dirname, 'hourly_append.sql'), 'utf8'); + +function installHourlyScheduledQueries( + namespace: ExactNamespace, + stagingDataset: gcp.bigquery.Dataset, + prodDataset: gcp.bigquery.Dataset, + context: ScheduledQueryContext +) { + const { projectId, transferServiceAgentPermission } = context; + const schemaName = scanAppDatabaseName(namespace); + Object.entries(replicatedTables).forEach(([tableName, tableConfig]) => { + const primaryKeyExpr = tableConfig.primaryKey; + const colName = tableConfig.datePartitionColumn; + + let recordTimestampExpr: string; + if (tableConfig.timeType === 'micros') { + recordTimestampExpr = `TIMESTAMP_MICROS(staging.${colName})`; + } else if (tableConfig.timeType === 'datastream_metadata') { + recordTimestampExpr = `TIMESTAMP_MILLIS(staging.datastream_metadata.source_timestamp)`; + } else { + const unreachable: never = tableConfig.timeType; + throw new Error(`impossible time config: ${unreachable}`); + } + + const recordDateExpr = `CAST(${recordTimestampExpr} AS DATE)`; + + const procedureBody = pulumi + .all([projectId, prodDataset.datasetId, stagingDataset.datasetId]) + .apply(([proj, prodDs, stagingDs]) => { + const prodTable = `\`${proj}.${prodDs}.${tableName}\``; + const stagingTable = `\`${proj}.${stagingDs}.${schemaName}_${tableName}\``; + const watermarksTable = `\`${proj}.${prodDs}.pipeline_watermarks\``; + const prodInfoSchema = `\`${proj}.${prodDs}.INFORMATION_SCHEMA.TABLES\``; + + return rawSqlTemplate + .replaceAll('{{tableName}}', tableName) + .replaceAll('{{schemaName}}', schemaName) + .replaceAll('{{primaryKeyExpr}}', primaryKeyExpr) + .replaceAll('{{recordTimestampExpr}}', recordTimestampExpr) + .replaceAll('{{recordDateExpr}}', recordDateExpr) + .replaceAll('{{prodTable}}', prodTable) + .replaceAll('{{stagingTable}}', stagingTable) + .replaceAll('{{watermarksTable}}', watermarksTable) + .replaceAll('{{prodInfoSchema}}', prodInfoSchema); + }); + + const routineId = `sp_append_${tableName}`; + + const appendRoutine = new gcp.bigquery.Routine(`${tableName}-append-routine`, { + datasetId: prodDataset.datasetId, + routineId: routineId, + routineType: 'PROCEDURE', + language: 'SQL', + definitionBody: procedureBody, + }); + + new gcp.bigquery.DataTransferConfig( + `${CLUSTER_BASENAME}_${tableName}-hourly-append`, + { + displayName: `${CLUSTER_BASENAME}_${tableName} Hourly Append Pipeline`, + location: cloudsdkComputeRegion(), + serviceAccountName: pulumi.interpolate`bigquery@${projectId}.iam.gserviceaccount.com`, + dataSourceId: 'scheduled_query', + schedule: 'every 1 hours from 00:07 to 23:07', + + params: { + query: pulumi.interpolate`CALL \`${projectId}.${prodDataset.datasetId}.${routineId}\`();`, + }, + }, + { + dependsOn: [transferServiceAgentPermission, appendRoutine], + } + ); + }); +} +// ============================================================================ +// Purging data older than 7 days from the staging table +// ============================================================================ +function installDailyPurgeScheduledQueries( + namespace: ExactNamespace, + stagingDataset: gcp.bigquery.Dataset, + context: ScheduledQueryContext, + retentionPeriodSeconds: number +) { + const { projectId, transferServiceAgentPermission } = context; + const schemaName = scanAppDatabaseName(namespace); + const retentionDays = retentionPeriodSeconds / 86400; + + Object.entries(replicatedTables).forEach(([tableName, tableConfig]) => { + const timeExpression = + tableConfig.timeType === 'datastream_metadata' + ? `TIMESTAMP_MILLIS(datastream_metadata.${tableConfig.datePartitionColumn})` + : `TIMESTAMP_MICROS(${tableConfig.datePartitionColumn})`; + + const procedureBody = pulumi + .all([projectId, stagingDataset.datasetId]) + .apply(([proj, stagingDs]) => { + const stagingTable = `\`${proj}.${stagingDs}.${schemaName}_${tableName}\``; + + return ` + DELETE FROM ${stagingTable} + WHERE ${timeExpression} < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL ${retentionPeriodSeconds} SECOND); + `; + }); + + const routineId = `sp_purge_old_records_${tableName}`; + + // Create the cleanup Stored Procedure + const purgeRoutine = new gcp.bigquery.Routine(`${tableName}-purge-routine`, { + datasetId: stagingDataset.datasetId, + routineId: routineId, + routineType: 'PROCEDURE', + language: 'SQL', + definitionBody: procedureBody, + }); + + // Schedule the Stored Procedure to run daily + new gcp.bigquery.DataTransferConfig( + `${CLUSTER_BASENAME}_${tableName}-daily-purge`, + { + displayName: `${CLUSTER_BASENAME}_${tableName} Daily Retention Purge`, + location: cloudsdkComputeRegion(), + serviceAccountName: pulumi.interpolate`bigquery@${projectId}.iam.gserviceaccount.com`, + dataSourceId: 'scheduled_query', + schedule: 'every day 05:21', // Runs daily at 05:21 AM + + params: { + query: pulumi.interpolate`CALL \`${projectId}.${stagingDataset.datasetId}.${routineId}\`();`, + }, + }, + { + dependsOn: [transferServiceAgentPermission, purgeRoutine], + } + ); + }); +} +// ============================================================================ +// CONNECTION PROFILES & NETWORKING +// ============================================================================ + function installBigqueryConnectionProfile( - postgres: CloudPostgres, + namespace: ExactNamespace, bigQuery: gcp.bigquery.Dataset, pcc: gcp.datastream.PrivateConnection ): gcp.datastream.ConnectionProfile { - const profileName = `${postgres.namespace.logicalName}-scan-bq-cxn`; + const profileName = `${namespace.logicalName}-scan-bq-cxn`; return new gcp.datastream.ConnectionProfile( profileName, { @@ -229,18 +569,39 @@ function installBigqueryConnectionProfile( ); } -function scanAppDatabaseName(postgres: Postgres) { - return `scan_${postgres.namespace.logicalName.replace(/-/g, '_')}`; +function installBigqueryStagingConnectionProfile( + namespace: ExactNamespace, + bigQuery: gcp.bigquery.Dataset, + pcc: gcp.datastream.PrivateConnection +): gcp.datastream.ConnectionProfile { + const profileName = `${namespace.logicalName}-scan-bq-staging-cxn`; + return new gcp.datastream.ConnectionProfile( + profileName, + { + connectionProfileId: profileName, + displayName: profileName, + location: cloudsdkComputeRegion(), + bigqueryProfile: {}, + labels: { + cluster: CLUSTER_BASENAME, + }, + }, + { dependsOn: [bigQuery, pcc] } + ); } +function scanAppDatabaseName(namespace: ExactNamespace): string { + return `scan_${namespace.logicalName.replace(/-/g, '_')}`; +} function installPostgresConnectionProfile( - postgres: CloudPostgres, - scan: InstalledHelmChart, + namespace: ExactNamespace, + databaseInstance: gcp.sql.DatabaseInstance, + scan: InstalledHelmChart | undefined, natVm: gcp.compute.Instance, connection: gcp.datastream.PrivateConnection, replicatorPassword: PostgresPassword ): gcp.datastream.ConnectionProfile { - const profileName = `${postgres.namespace.logicalName}-scan-update-history-cxn`; + const profileName = `${namespace.logicalName}-scan-update-history-cxn`; // TODO (#454) may have to await scan migration or pub/rep slots command return new gcp.datastream.ConnectionProfile( @@ -254,7 +615,7 @@ function installPostgresConnectionProfile( port: dbPort, username: replicatorUserName, password: replicatorPassword.contents, - database: scanAppDatabaseName(postgres), + database: scanAppDatabaseName(namespace), }, privateConnectivity: { privateConnection: connection.name, @@ -263,14 +624,14 @@ function installPostgresConnectionProfile( cluster: CLUSTER_BASENAME, }, }, - { dependsOn: [natVm, connection, postgres.databaseInstance, scan] } + { dependsOn: [natVm, connection, databaseInstance, ...(scan !== undefined ? [scan] : [])] } ); } function installPrivateConnectivityConfiguration( - postgres: CloudPostgres + namespace: ExactNamespace ): gcp.datastream.PrivateConnection { - const privateConnectionName = `${postgres.namespace.logicalName}-scan-update-history-datastream-vpc`; + const privateConnectionName = `${namespace.logicalName}-scan-update-history-datastream-vpc`; return new gcp.datastream.PrivateConnection( privateConnectionName, { @@ -311,39 +672,48 @@ function installDatastreamToNatVmFirewallRule( }); } +// ============================================================================ +// POSTGRESQL AUTHENTICATION & REPLICATION SLOT PROVISIONING +// ============================================================================ // TODO (DACH-NY/canton-network-internal#342) if we disable default egress rule, we need another firewall // rule for Nat VM -> Postgres - -function installReplicatorPassword(postgres: CloudPostgres): PostgresPassword { - const secretName = `${postgres.namespace.logicalName}-${replicatorUserName}-passwd`; - const password = generatePassword(`${postgres.instanceName}-${replicatorUserName}-passwd`, { - parent: postgres, +function installReplicatorPassword(namespace: ExactNamespace): PostgresPassword { + const secretName = `${namespace.logicalName}-${replicatorUserName}-passwd`; + const password = generatePassword(`cn-apps-pg-${replicatorUserName}-passwd`, { + aliases: [ + { + parent: getLegacyParentUrn(namespace), + }, + ], protect: spliceConfig.pulumiProjectConfig.cloudSql.protected, }).result; return { contents: password, - secret: installPostgresPasswordSecret(postgres.namespace, password, secretName), + secret: installPostgresPasswordSecret(namespace, password, secretName), }; } function createPostgresReplicatorUser( - postgres: CloudPostgres, + namespace: ExactNamespace, + databaseInstance: gcp.sql.DatabaseInstance, password: PostgresPassword ): gcp.sql.User { - const name = `${postgres.namespace.logicalName}-user-${replicatorUserName}`; + const name = `${namespace.logicalName}-user-${replicatorUserName}`; return new gcp.sql.User( name, { - instance: postgres.databaseInstance.name, + instance: databaseInstance.name, name: replicatorUserName, password: password.contents, }, { - parent: postgres, - deletedWith: postgres.databaseInstance, - retainOnDelete: true, + aliases: [ + { + parent: getLegacyParentUrn(namespace), + }, + ], protect: spliceConfig.pulumiProjectConfig.cloudSql.protected, - dependsOn: [postgres.databaseInstance, password.secret], + dependsOn: [password.secret], } ); } @@ -354,66 +724,284 @@ needs the 'Cloud SQL Editor' IAM role in the relevant GCP project */ function createPublicationAndReplicationSlots( - postgres: CloudPostgres, + namespace: ExactNamespace, + databaseInstance: gcp.sql.DatabaseInstance, replicatorUser: gcp.sql.User, - scan: InstalledHelmChart -) { - const dbName = scanAppDatabaseName(postgres); + scan: InstalledHelmChart | undefined, + enableLegacy: boolean, + enableStagProd: boolean +): { + slot1?: command.local.Command; + slot2?: command.local.Command; +} { + // --------------------------------------------------------------------------- + // 1. Shared Environment & Project Setup + // --------------------------------------------------------------------------- + + const dbName = scanAppDatabaseName(namespace); const schemaName = dbName; - const path = commandScriptPath('cluster/pulumi/canton-network/bigquery-cloudsql.sh'); - const scriptArgs = pulumi.interpolate`\\ - --private-network-project="${gcp.organizations.getProjectOutput({}).apply(proj => proj.name)}" \\ - --compute-region="${cloudsdkComputeRegion()}" \\ - --service-account-email="${postgres.databaseInstance.serviceAccountEmailAddress}" \\ - --schema-name="${schemaName}" \\ - --tables-to-replicate-joined="${tablesToReplicate.join(', ')}" \\ - --postgres-user-name="${postgres.user.name}" \\ - --publication-name="${publicationName}" \\ - --replication-slot-name="${replicationSlotName}" \\ - --replicator-user-name="${replicatorUserName}" \\ - --postgres-instance-name="${postgres.databaseInstance.name}" \\ - --scan-app-database-name="${scanAppDatabaseName(postgres)}" \\ - --flyway-migration-to-wait-for="${flywayMigrationToWaitFor}" \\ - `; - return new command.local.Command( - `${postgres.namespace.logicalName}-${replicatorUserName}-pub-replicate-slots`, - { - create: pulumi.interpolate`'${path}' create-pub-rep-slot ${scriptArgs}`, - delete: pulumi.interpolate`'${path}' delete-pub-rep-slot ${scriptArgs}`, - }, - { - deletedWith: postgres.databaseInstance, - dependsOn: [scan, postgres.databaseInstance, replicatorUser], - deleteBeforeReplace: true, + const scriptPath = commandScriptPath('cluster/pulumi/canton-network/bigquery-cloudsql.sh'); + + const projectId = gcp.organizations.getProjectOutput({}).apply(proj => proj.projectId); + + const commonDependencies = [scan, databaseInstance, replicatorUser]; + + // --------------------------------------------------------------------------- + // 2. Base Arguments Split (Matches Stored Deployment Ordering & Formatting) + // --------------------------------------------------------------------------- + + // Prefix arguments (Arguments 1–6) + const baseArgsPrefix: pulumi.Input[] = [ + pulumi.interpolate`--private-network-project="${projectId}"`, + pulumi.interpolate`--compute-region="${cloudsdkComputeRegion()}"`, + pulumi.interpolate`--service-account-email="${databaseInstance.serviceAccountEmailAddress}"`, + pulumi.interpolate`--schema-name="${schemaName}"`, + pulumi.interpolate`--tables-to-replicate-joined="${tablesToReplicate.join(', ')}"`, + pulumi.interpolate`--postgres-user-name="${defaultUserName}"`, + ]; + + // Suffix arguments (Arguments 9–12) + const baseArgsSuffix: pulumi.Input[] = [ + pulumi.interpolate`--replicator-user-name="${replicatorUserName}"`, + pulumi.interpolate`--postgres-instance-name="${databaseInstance.name}"`, + pulumi.interpolate`--scan-app-database-name="${dbName}"`, + pulumi.interpolate`--flyway-migration-to-wait-for="${flywayMigrationToWaitFor}"`, + ]; + + const buildScriptCommand = ( + action: string, + slotArgs: pulumi.Input[] + ): pulumi.Output => { + const allArgs = [...baseArgsPrefix, ...slotArgs, ...baseArgsSuffix]; + + return pulumi.all(allArgs).apply(args => { + const formattedArgs = args.join(' \\\n '); + return `'${scriptPath}' ${action} \\\n ${formattedArgs} \\\n `; + }); + }; + + // --------------------------------------------------------------------------- + // 3. Legacy Datastream Slot (Slot 1) + // --------------------------------------------------------------------------- + + let slot1: command.local.Command | undefined; + + if (enableLegacy) { + const slot1Args: pulumi.Input[] = [ + pulumi.interpolate`--publication-name="${publicationName}"`, + pulumi.interpolate`--replication-slot-name="${replicationSlotName}"`, + ]; + + slot1 = new command.local.Command( + `${namespace.logicalName}-${replicatorUserName}-pub-replicate-slots`, + { + create: buildScriptCommand('create-pub-rep-slot', slot1Args), + delete: buildScriptCommand('delete-pub-rep-slot', slot1Args), + }, + { + dependsOn: [databaseInstance, replicatorUser, ...(scan !== undefined ? [scan] : [])], + deleteBeforeReplace: true, + } + ); + } + + // --------------------------------------------------------------------------- + // 4. Stag-Prod Datastream Slot (Slot 2) + // --------------------------------------------------------------------------- + + let slot2: command.local.Command | undefined; + + if (enableStagProd) { + const slot2Args: pulumi.Input[] = [ + pulumi.interpolate`--publication-name="${publicationNameStagProd}"`, + pulumi.interpolate`--replication-slot-name="${replicationSlotNameStagProd}"`, + ]; + + slot2 = new command.local.Command( + `${namespace.logicalName}-${replicatorUserName}-pub-replicate-slot-2`, + { + create: buildScriptCommand('create-pub-rep-slot', slot2Args), + delete: buildScriptCommand('delete-pub-rep-slot', slot2Args), + }, + { + dependsOn: [databaseInstance, replicatorUser, ...(scan !== undefined ? [scan] : [])], + deleteBeforeReplace: true, + } + ); + } + + // --------------------------------------------------------------------------- + // 5. Return Created Slots + // --------------------------------------------------------------------------- + + return { + slot1, + slot2, + }; +} +// ============================================================================ +// MAIN ENTRYPOINT +// ============================================================================ + +export async function configureScanBigQuery({ + namespace, + bigQueryConfig, + scanReference, +}: ScanBigQueryArgs): Promise { + // Use config file to determine which datastreams to enable and their desired states + + const { + enableLegacyDatastream, + enableStagProdDatastream, + legacyDesiredState, + stagProdDesiredState, + retentionPeriodSeconds, + } = bigQueryConfig; + + if (!enableLegacyDatastream && !enableStagProdDatastream) { + throw new Error( + 'configureScanBigQuery was called, but both legacy and stag-prod Datastreams are disabled.' + ); + } + const zone = getCloudSdkZone(); + const [databaseInstance, scanChart] = await (async () => { + switch (scanReference.type) { + case 'local': + return [scanReference.databaseInstance, scanReference.chart]; + case 'external': + return [await getScanDb(scanReference.databaseInstanceNamePrefix, zone), undefined]; } + })(); + + const passwordSecret = installReplicatorPassword(namespace); + const slots = createPublicationAndReplicationSlots( + namespace, + databaseInstance, + createPostgresReplicatorUser(namespace, databaseInstance, passwordSecret), + scanChart, + enableLegacyDatastream, + enableStagProdDatastream ); -} -export function configureScanBigQuery( - postgres: CloudPostgres, - scanBigQuery: ScanBigQueryConfig, - scan: InstalledHelmChart -): void { - const passwordSecret = installReplicatorPassword(postgres); - const pubRepSlots = createPublicationAndReplicationSlots( - postgres, - createPostgresReplicatorUser(postgres, passwordSecret), - scan - ); + const natVm = installNatVm(namespace, zone, databaseInstance); + const pcc = installPrivateConnectivityConfiguration(namespace); + installDatastreamToNatVmFirewallRule(namespace, pcc, natVm); - const natVm = installNatVm(postgres); - const dataset = installBigqueryDataset(scanBigQuery); - const pcc = installPrivateConnectivityConfiguration(postgres); - const destinationProfile = installBigqueryConnectionProfile(postgres, dataset, pcc); const sourceProfile = installPostgresConnectionProfile( - postgres, - scan, + namespace, + databaseInstance, + scanChart, natVm, pcc, passwordSecret ); - installDatastreamToNatVmFirewallRule(postgres.namespace, pcc, natVm); - installDatastream(postgres, sourceProfile, destinationProfile, dataset, pubRepSlots); - return; + let legacyDataset: gcp.bigquery.Dataset | undefined; + let stagingDataset: gcp.bigquery.Dataset | undefined; + let prodDataset: gcp.bigquery.Dataset | undefined; + + if (enableLegacyDatastream && slots.slot1) { + legacyDataset = installBigqueryDataset(bigQueryConfig); + const legacyDestinationProfile = installBigqueryConnectionProfile( + namespace, + legacyDataset, + pcc + ); + + installDatastream( + namespace, + databaseInstance, + sourceProfile, + legacyDestinationProfile, + legacyDataset, + slots.slot1, + legacyDesiredState + ); + } + + if (enableStagProdDatastream && slots.slot2) { + stagingDataset = installBigqueryStagingDataset(bigQueryConfig); + prodDataset = installBigqueryProdDataset(bigQueryConfig); + const stagingDestinationProfile = installBigqueryStagingConnectionProfile( + namespace, + stagingDataset, + pcc + ); + + installDatastream_stag_prod( + namespace, + databaseInstance, + sourceProfile, + stagingDestinationProfile, + stagingDataset, + slots.slot2, + stagProdDesiredState + ); + const scheduledQueryContext = installBqScheduledQueryContext(); + installHourlyScheduledQueries(namespace, stagingDataset, prodDataset, scheduledQueryContext); + installDailyPurgeScheduledQueries( + namespace, + stagingDataset, + scheduledQueryContext, + retentionPeriodSeconds + ); + } + // TODO (DACH-NY/canton-network-internal#6451) not sure if this function needs to return anything, + // but we need to return something to satisfy the ScanBigQuery type. + // For now, we return the primary dataset's ID, which is either legacy, staging, or prod, whichever is defined first. + // we should consider removing the return datasetId if it's not needed + + const primaryDataset = legacyDataset ?? stagingDataset ?? prodDataset; + + return { + datasetId: primaryDataset!.id, + }; +} + +export type ScanBigQueryArgs = { + namespace: ExactNamespace; + bigQueryConfig: ScanBigQueryConfig; + scanReference: ScanReference; +}; + +type ScanReference = + | { + type: 'local'; + databaseInstance: gcp.sql.DatabaseInstance; + chart: InstalledHelmChart; + } + | { + type: 'external'; + databaseInstanceNamePrefix: string; + }; + +export type ScanBigQuery = { + datasetId: pulumi.Output; +}; + +async function getScanDb( + instanceNamePrefix: string, + zone: string +): Promise { + const result = await gcp.sql.getDatabaseInstances({ + project: GCP_PROJECT, + region: GCP_REGION, + zone, + }); + const instanceName = + result.instances.find( + instance => + instance.name.startsWith(instanceNamePrefix) && + instance.settings?.[0]?.userLabels?.cluster === CLUSTER_BASENAME + )?.name ?? + (() => { + throw new Error( + `Could not find SV apps database instance with prefix [${instanceNamePrefix}] and user label [cluster=${CLUSTER_BASENAME}].` + ); + })(); + return gcp.sql.DatabaseInstance.get(instanceNamePrefix, instanceName); +} + +function getLegacyParentUrn(namespace: ExactNamespace): pulumi.URN { + return `urn:pulumi:canton-network.${CLUSTER_BASENAME}::canton-network::canton:cloud:postgres::${namespace.logicalName}-cn-apps-pg`; } diff --git a/cluster/pulumi/common-sv/src/bulkStorage.ts b/cluster/pulumi/common-sv/src/bulkStorage.ts index 2596410bfb..1efd3d6eff 100644 --- a/cluster/pulumi/common-sv/src/bulkStorage.ts +++ b/cluster/pulumi/common-sv/src/bulkStorage.ts @@ -11,42 +11,76 @@ import { import { BulkStorageConfig } from './singleSvConfig'; -export type BulkStorageBucket = { +type BulkStorageBucket = { bucket: gcp.storage.Bucket; region: string; secret: k8s.core.v1.Secret; }; +export type BulkStorageBuckets = { + staging: BulkStorageBucket; + committed: BulkStorageBucket; +}; + export function installScanBulkStorage( xns: ExactNamespace, bulkStorageConfig: BulkStorageConfig -): BulkStorageBucket | undefined { +): BulkStorageBuckets | undefined { if (!bulkStorageConfig.enabled) { return; } - const bucketName = `${ClusterBasename}-${xns.logicalName}-bulk`; + const stagingBucketName = `${ClusterBasename}-${xns.logicalName}-bulk-staging`; + const committedBucketName = `${ClusterBasename}-${xns.logicalName}-bulk-committed`; + const saName = `${ClusterBasename}-${xns.logicalName}-bulk-sa`; + // TODO(#3429): review other bucket configs - const bucket = new gcp.storage.Bucket(bucketName, { name: bucketName, location: GcpRegion }); - const bucketServiceAccount = new gcp.serviceaccount.Account(`${bucketName}-sa`, { - accountId: `${bucketName}-sa`, + const bucketServiceAccount = new gcp.serviceaccount.Account(saName, { + accountId: saName, displayName: 'Service Account for Bulk-Storage Bucket Read/Write Access', }); + const hmacKey = new gcp.storage.HmacKey( + `${saName}-hmac`, + { + serviceAccountEmail: bucketServiceAccount.email, + }, + { dependsOn: [bucketServiceAccount] } + ); + + const staging = new gcp.storage.Bucket(stagingBucketName, { + name: stagingBucketName, + location: GcpRegion, + }); new gcp.storage.BucketIAMMember( - `${bucketName}-sa-role`, + `${stagingBucketName}-sa-role`, { - bucket: bucket.name, + bucket: staging.name, role: 'roles/storage.objectUser', member: pulumi.interpolate`serviceAccount:${bucketServiceAccount.email}`, }, - { dependsOn: [bucket, bucketServiceAccount] } + { dependsOn: [staging, bucketServiceAccount] } ); - const hmacKey = new gcp.storage.HmacKey( - `${bucketName}-hmac`, + const committed = new gcp.storage.Bucket(committedBucketName, { + name: committedBucketName, + location: GcpRegion, + }); + new gcp.storage.BucketIAMMember( + `${committedBucketName}-sa-role-creator`, { - serviceAccountEmail: bucketServiceAccount.email, + bucket: committed.name, + role: 'roles/storage.objectCreator', + member: pulumi.interpolate`serviceAccount:${bucketServiceAccount.email}`, }, - { dependsOn: [bucketServiceAccount] } + { dependsOn: [committed, bucketServiceAccount] } + ); + new gcp.storage.BucketIAMMember( + `${committedBucketName}-sa-role-reader`, + { + bucket: committed.name, + role: 'roles/storage.objectViewer', + member: pulumi.interpolate`serviceAccount:${bucketServiceAccount.email}`, + }, + { dependsOn: [committed, bucketServiceAccount] } ); const accessKey = hmacKey.accessId; @@ -72,8 +106,15 @@ export function installScanBulkStorage( ); return { - region: GcpRegion, - bucket, - secret, - } as BulkStorageBucket; + staging: { + bucket: staging, + region: GcpRegion, + secret, + }, + committed: { + bucket: committed, + region: GcpRegion, + secret, + }, + }; } diff --git a/cluster/pulumi/common-sv/src/config.ts b/cluster/pulumi/common-sv/src/config.ts index e7dc0e549f..38a083bc76 100644 --- a/cluster/pulumi/common-sv/src/config.ts +++ b/cluster/pulumi/common-sv/src/config.ts @@ -18,7 +18,7 @@ import { SweepConfig } from '@canton-network/splice-pulumi-common-validator'; import { clusterYamlConfig } from '@canton-network/splice-pulumi-common/src/config/config'; import { z } from 'zod'; -import { BulkStorageBucket } from './bulkStorage'; +import { BulkStorageBuckets } from './bulkStorage'; import { SingleSvConfiguration } from './singleSvConfig'; import { StaticCometBftConfig, @@ -36,16 +36,11 @@ export type SvOnboarding = | { type: 'join-with-key'; keys: CnInput; - sponsorRelease: pulumi.Resource; + sponsorRelease?: pulumi.Resource; sponsorApiUrl: string; sponsorScanUrl: string; }; -export interface ScanBigQueryConfig { - dataset: string; - prefix: string; -} - export interface StaticSvConfigBasic { nodeName: string; ingressName: string; @@ -84,7 +79,7 @@ export interface SvConfig extends StaticSvConfig, SingleSvConfiguration { initialRound?: string; periodicTopologySnapshotConfig?: CnInput; version: CnChartVersion; - bulkStorageBucket?: BulkStorageBucket; + bulkStorageBuckets?: BulkStorageBuckets; } export const TopologySnapshotSchema = z.object({ @@ -102,6 +97,18 @@ export const SvConfigSchema = z.object({ .object({ volumeSize: z.string().optional(), protected: z.boolean().optional(), + watchdog: z + .object({ + disabled: z.boolean().default(false), + threshold: z.number().optional(), + evaluationIntervalSeconds: z.number().optional(), + pollIntervalSeconds: z.number().optional(), + scrapeTimeoutSeconds: z.number().optional(), + startupGraceSeconds: z.number().optional(), + cooldownSeconds: z.number().optional(), + }) + .strict() + .optional(), }) .optional(), scan: z diff --git a/cluster/pulumi/common-sv/src/hourly_append.sql b/cluster/pulumi/common-sv/src/hourly_append.sql new file mode 100644 index 0000000000..c1c0bab71f --- /dev/null +++ b/cluster/pulumi/common-sv/src/hourly_append.sql @@ -0,0 +1,99 @@ +BEGIN + DECLARE current_watermark_micros INT64; + DECLARE current_watermark_ts TIMESTAMP; + DECLARE max_available_timestamp TIMESTAMP; + DECLARE max_closed_timestamp TIMESTAMP; + + -- 1. Check if production table exists; if not, create shell table + IF NOT EXISTS ( + SELECT 1 + FROM {{prodInfoSchema}} + WHERE table_name = '{{tableName}}' + ) THEN + CREATE TABLE {{prodTable}} + PARTITION BY record_date AS + SELECT + *, + {{recordDateExpr}} AS record_date + FROM {{stagingTable}} AS staging + WHERE 1 = 0; + END IF; + + -- 2. Create central watermark tracking table using INT64 (microseconds) + CREATE TABLE IF NOT EXISTS {{watermarksTable}} ( + table_name STRING, + last_watermark_time INT64 + ); + + -- 3. Initialize watermark to epoch zero (0 micros) if missing + IF NOT EXISTS ( + SELECT 1 + FROM {{watermarksTable}} + WHERE table_name = '{{tableName}}' + ) THEN + INSERT INTO {{watermarksTable}} + VALUES ('{{tableName}}', 0); + END IF; + + -- 4. Get current scalar watermark in INT64 micros and convert to TIMESTAMP + SET current_watermark_micros = ( + SELECT MAX(last_watermark_time) + FROM {{watermarksTable}} + WHERE table_name = '{{tableName}}' + ); + + SET current_watermark_ts = TIMESTAMP_MICROS(current_watermark_micros); + + -- 5. Fetch max available timestamp from staging WITH PARTITION PRUNING + SET max_available_timestamp = COALESCE( + ( + SELECT MAX({{recordTimestampExpr}}) + FROM {{stagingTable}} AS staging + WHERE ( + staging._PARTITIONTIME >= TIMESTAMP_SUB(current_watermark_ts, INTERVAL 3 HOUR) + OR staging._PARTITIONTIME IS NULL + ) + ), + -- Fall back to current_watermark_ts if no new data arrived in the last 3 hours + current_watermark_ts + ); + + -- Subtract 1 hour to safely close the ingestion window + SET max_closed_timestamp = TIMESTAMP_SUB(max_available_timestamp, INTERVAL 1 HOUR); + + -- 6. Filter incremental data with Primary Key Deduplication & Partition Pruning + CREATE TEMP TABLE temp_incremental AS ( + SELECT + staging.*, + {{recordDateExpr}} AS record_date + FROM {{stagingTable}} AS staging + WHERE ( + -- Cost control: Scan 3-hour partition window + staging._PARTITIONTIME >= TIMESTAMP_SUB(current_watermark_ts, INTERVAL 3 HOUR) + -- Streaming buffer protection for recent arrivals + OR staging._PARTITIONTIME IS NULL + ) + AND {{recordTimestampExpr}} > current_watermark_ts + AND {{recordTimestampExpr}} <= max_closed_timestamp + -- Primary Key Deduplication + QUALIFY ROW_NUMBER() OVER ( + PARTITION BY {{primaryKeyExpr}} + ORDER BY + staging.datastream_metadata.source_timestamp DESC, + staging.datastream_metadata.change_sequence_number DESC + ) = 1 + ); + + -- 7. Perform append and update watermark if valid new records exist + IF EXISTS (SELECT 1 FROM temp_incremental) THEN + + INSERT INTO {{prodTable}} + SELECT * FROM temp_incremental; + + UPDATE {{watermarksTable}} + SET last_watermark_time = UNIX_MICROS(max_closed_timestamp) + WHERE table_name = '{{tableName}}' + AND last_watermark_time < UNIX_MICROS(max_closed_timestamp); + + END IF; +END; \ No newline at end of file diff --git a/cluster/pulumi/common-sv/src/physicalSynchronizerConfig.ts b/cluster/pulumi/common-sv/src/physicalSynchronizerConfig.ts index 99cee3b64b..e885d75dd6 100644 --- a/cluster/pulumi/common-sv/src/physicalSynchronizerConfig.ts +++ b/cluster/pulumi/common-sv/src/physicalSynchronizerConfig.ts @@ -7,6 +7,7 @@ import { EnvVarConfigSchema, K8sResourceSchema, spliceConfig, + SplicePostgresSchema, } from '@canton-network/splice-pulumi-common'; import { z } from 'zod'; @@ -18,6 +19,11 @@ export const SvMediatorConfigSchema = z additionalEnvVars: z.array(EnvVarConfigSchema).default([]), additionalJvmOptions: z.string().optional(), cloudSql: CloudSqlWithOverrideConfigSchema, + // Mediator is either deployed on cloudSQL or is reset frequently, so we can skip migration + splicePostgres: SplicePostgresSchema.default({ + postgresImage: 'postgres:18', + deployment: 'docker-image', + }), resources: K8sResourceSchema, }) .strict(); @@ -27,7 +33,13 @@ export const SvSequencerConfigSchema = z additionalEnvVars: z.array(EnvVarConfigSchema).default([]), additionalJvmOptions: z.string().optional(), cloudSql: CloudSqlWithOverrideConfigSchema, + // Sequencer is either deployed on cloudSQL or is reset frequently, so we can skip migration + splicePostgres: SplicePostgresSchema.default({ + postgresImage: 'postgres:18', + deployment: 'docker-image', + }), resources: K8sResourceSchema, + enableAntiAffinity: z.boolean().default(true), }) .strict(); export type SvSequencerConfig = z.infer; diff --git a/cluster/pulumi/common-sv/src/singleSvConfig.ts b/cluster/pulumi/common-sv/src/singleSvConfig.ts index cc0b0d649e..bbc8f07cd6 100644 --- a/cluster/pulumi/common-sv/src/singleSvConfig.ts +++ b/cluster/pulumi/common-sv/src/singleSvConfig.ts @@ -93,22 +93,46 @@ const SvAppConfigSchema = z const BulkStorageConfigSchema = z.object({ enabled: z.boolean(), }); + export type BulkStorageConfig = z.infer; -const ScanAppConfigSchema = z + +// 1. Extract ScanBigQueryConfigSchema to validate all Datastream settings. +// All new fields are optional to ensure existing deployments do not fail parsing. +const SECONDS_PER_DAY = 24 * 3600; +export const ScanBigQueryConfigSchema = z .object({ - bigQuery: z - .object({ - dataset: z.string(), - prefix: z.string(), - functionsDataset: z.string().optional(), + dataset: z.string(), + prefix: z.string(), + functionsDataset: z.string().optional(), + enableLegacyDatastream: z.boolean().default(true), + enableStagProdDatastream: z.boolean().default(false), + legacyDesiredState: z.enum(['RUNNING', 'PAUSED']).default('RUNNING'), + stagProdDesiredState: z.enum(['RUNNING', 'PAUSED']).default('RUNNING'), + retentionPeriodSeconds: z + .number() + .min(3 * SECONDS_PER_DAY, { + message: 'Value must be at least 3 days (259,200 seconds)', }) - .optional(), + .refine(v => v % SECONDS_PER_DAY === 0, { + message: 'Value must be an exact number of days, expressed in seconds', + }) + .default(7 * SECONDS_PER_DAY), + }) + .strict(); // Keeps strict mode safe now that all known fields are explicitly defined + +// 2. Single source of truth: infer the TypeScript type directly from the Zod schema +export type ScanBigQueryConfig = z.infer; +// 3. Update ScanAppConfigSchema to reference the extracted sub-schema +const ScanAppConfigSchema = z + .object({ + bigQuery: ScanBigQueryConfigSchema.optional(), bulkStorage: BulkStorageConfigSchema.optional(), additionalEnvVars: z.array(EnvVarConfigSchema).default([]), additionalJvmOptions: z.string().optional(), resources: K8sResourceSchema, }) .strict(); + const SvValidatorAppConfigSchema = z .object({ walletUser: z.string().optional(), @@ -167,7 +191,11 @@ const SingleSvConfigSchema = z appsAsync: z.boolean().default(false), cantonLogLevel: LogLevelSchema, cantonStdoutLogLevel: LogLevelSchema.optional(), + // Log level for the Splice apps' HTTP request logging (org.lfdecentralizedtrust.splice.admin.api) apiRequestLogLevel: LogLevelSchema.optional(), + // Log level for the Canton nodes' Ledger-API audit logging (com.digitalasset.canton.logging.audit) + // Falls back to `apiRequestLogLevel` when not specified + cantonApiRequestLogLevel: LogLevelSchema.optional(), cantonAsync: z.boolean().default(false), cometbftLogLevel: CometbftLogLevelSchema.optional(), cometbftExtraLogLevelFlags: z.string().optional(), diff --git a/cluster/pulumi/common-sv/src/sv.ts b/cluster/pulumi/common-sv/src/sv.ts index 61422d7de9..11cce2ab35 100644 --- a/cluster/pulumi/common-sv/src/sv.ts +++ b/cluster/pulumi/common-sv/src/sv.ts @@ -4,17 +4,21 @@ import * as postgres from '@canton-network/splice-pulumi-common/src/postgres'; import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; import { + activeVersion, ansDomainPrefix, - appsAffinityAndTolerations, + appsKubernetesScheduling, + Auth0Client, btoa, ChartValues, CLUSTER_BASENAME, CLUSTER_HOSTNAME, CnInput, + config as envConfig, daContactPoint, DecentralizedSynchronizerMigrationConfig, + DecentralizedSynchronizerUpgradeConfig, ExactNamespace, - exactNamespace, + envoyClientIpHeaderEnvVar, failOnAppVersionMismatch, fetchAndInstallParticipantBootstrapDump, getAdditionalJvmOptions, @@ -26,14 +30,20 @@ import { installSpliceHelmChart, installSvAppSecrets, installValidatorOnboardingSecret, + isDevNet, networkWideConfig, participantBootstrapDumpSecretName, PersistenceConfig, persistentHeapDumpsPvc, sanitizedForPostgres, spliceInstanceNames, + SplicePostgresConfig, + svCometBftGovernanceKeyFromSecret, svCometBftGovernanceKeySecret, SvIdKey, + svKeyFromSecret, + svOnboardingPollingInterval, + svValidatorTopupConfig, svUserIds, validatorOnboardingSecretName, } from '@canton-network/splice-pulumi-common'; @@ -41,15 +51,27 @@ import { approvedSvIdentities, CantonBftSynchronizerNode, configForSv, + coreSvsToDeploy, DecentralizedSynchronizerNode, + initialRound, installScanBulkStorage, installSvLoopback, + SingleSvConfiguration, + StaticSvConfig, SynchronizerNodes, valuesForSvApp, valuesForSvValidatorApp, } from '@canton-network/splice-pulumi-common-sv'; import { SvConfig, svsConfig } from '@canton-network/splice-pulumi-common-sv/src/config'; +import { readBackupConfig } from '@canton-network/splice-pulumi-common-validator/src/backup'; import { installValidatorApp } from '@canton-network/splice-pulumi-common-validator/src/validator'; +import { + mustInstallSplitwell, + mustInstallValidator1, + splitwellOnboarding, + standaloneValidatorOnboarding, + validator1Onboarding, +} from '@canton-network/splice-pulumi-common-validator/src/validators'; import { delegatelessAutomationExpectedTaskDuration, delegatelessAutomationExpiredRewardCouponBatchSize, @@ -60,15 +82,116 @@ import { installBucketSecret, } from '@canton-network/splice-pulumi-common/src/buckets'; import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; +import { SplitPostgresInstances } from '@canton-network/splice-pulumi-common/src/config/configs'; import { initialAmuletPrice } from '@canton-network/splice-pulumi-common/src/initialAmuletPrice'; import { Postgres } from '@canton-network/splice-pulumi-common/src/postgres'; import { installRateLimits } from '@canton-network/splice-pulumi-common/src/ratelimit/rateLimit'; import { topologySnapshotConfig } from '@canton-network/splice-pulumi-common/src/topology-snapshot'; import { Resource } from '@pulumi/pulumi'; +import pick from 'lodash/pick'; -import { configureScanBigQuery } from './bigQuery'; import { installInfo } from './info'; +// TODO(#6719) once all clusters have been migrated move the whole module to the sv project +export async function installSvNodeStandalone( + xns: ExactNamespace, + staticConfig: StaticSvConfig, + config: SingleSvConfiguration, + auth0Client: Auth0Client, + extraDependsOn: CnInput[] = [], + // TODO(#6719) once all clusters have been migrated remove this + migrationArgs?: MigrationArgs +): Promise { + const nodeName = staticConfig.nodeName; + const [sv1StaticConfig, ...otherSvsStaticConfigs] = coreSvsToDeploy; + const isFoundingSv = nodeName === sv1StaticConfig.nodeName; + const disableOnboardingParticipantPromotionDelay = envConfig.envFlag( + 'DISABLE_ONBOARDING_PARTICIPANT_PROMOTION_DELAY', + false + ); + return await installSvNode( + xns, + { + isFirstSv: isFoundingSv, + ...pick(staticConfig, [ + 'nodeName', + 'ingressName', + 'onboardingName', + 'cometBft', + 'validatorWalletUser', + 'auth0ValidatorAppName', + 'auth0SvAppName', + 'sweep', + ]), + nodeConfigs: { + sv1: { + ...sv1StaticConfig.cometBft, + ...pick(sv1StaticConfig, ['nodeName', 'ingressName']), + }, + peers: otherSvsStaticConfigs + .filter(config => config.nodeName !== nodeName) + .map(config => ({ + ...config.cometBft, + ...pick(config, ['nodeName', 'ingressName']), + })), + }, + onboarding: isFoundingSv + ? { + type: 'found-dso', + sv1SvRewardWeightBps: + approvedSvIdentities().find( + identity => identity.name == sv1StaticConfig.onboardingName + )?.rewardWeightBps ?? 10_000, + roundZeroDuration: envConfig.optionalEnv('ROUND_ZERO_DURATION'), + initialRound: initialRound?.toString(), + } + : { + type: 'join-with-key', + sponsorApiUrl: `http://sv-app.sv-1:5014`, + sponsorScanUrl: `http://scan-app.sv-1:5012`, + keys: svKeyFromSecret( + staticConfig.svIdKeySecretName ?? `${nodeName.replaceAll('-', '')}-id` + ), + }, + auth0Client, + expectedValidatorOnboardings: isFoundingSv + ? [ + ...(function* () { + if (mustInstallSplitwell) { + yield splitwellOnboarding; + } + if (mustInstallValidator1) { + yield validator1Onboarding; + } + if (standaloneValidatorOnboarding !== undefined) { + yield standaloneValidatorOnboarding; + } + })(), + ] + : [], + isDevNet, + ...(await readBackupConfig()), + topupConfig: svValidatorTopupConfig, + splitPostgresInstances: SplitPostgresInstances, + disableOnboardingParticipantPromotionDelay, + onboardingPollingInterval: svOnboardingPollingInterval, + cometBftGovernanceKey: + config.participant?.kms !== undefined + ? svCometBftGovernanceKeyFromSecret( + staticConfig.cometBftGovernanceKeySecretName ?? + `${nodeName.replaceAll('-', '')}-cometbft-governance-key` + ) + : undefined, + initialRound: initialRound?.toString(), + version: config.versionOverride ?? activeVersion, + ...config, + }, + DecentralizedSynchronizerUpgradeConfig, + extraDependsOn, + migrationArgs + ); +} + export function installSvKeySecret( xns: ExactNamespace, keys: CnInput @@ -116,27 +239,23 @@ export function installSvKeySecret( } export type InstalledSv = { + namespace: ExactNamespace; + nodeName: string; validatorApp: Resource; svApp: InstalledHelmChart; scan: InstalledHelmChart; canton: SynchronizerNodes; ingress: Resource; + appsPostgres: postgres.Postgres; }; export async function installSvNode( + xns: ExactNamespace, baseConfig: SvConfig, decentralizedSynchronizerUpgradeConfig: DecentralizedSynchronizerMigrationConfig, - extraDependsOn: CnInput[] = [] -): Promise { - const xns = exactNamespace(baseConfig.nodeName, true); - const loopback = installSvLoopback(xns, decentralizedSynchronizerUpgradeConfig.usesCometbft()); - const imagePullDeps = imagePullSecret(xns); - - const auth0Secrets: CnInput[] = await installSvAppSecrets( - xns, - baseConfig.auth0Client - ); - + extraDependsOn: CnInput[] = [], + migrationArgs?: MigrationArgs +): Promise { const periodicBackupConfig: BucketConfig | undefined = baseConfig.periodicBackupConfig ? { ...baseConfig.periodicBackupConfig, @@ -162,7 +281,7 @@ export async function installSvNode( prefix: baseConfig.identitiesBackupLocation.prefix || `${CLUSTER_BASENAME}/${xns.logicalName}`, }; - const bulkStorageBucket = svConfig.scanApp?.bulkStorage + const bulkStorageBuckets = svConfig.scanApp?.bulkStorage ? installScanBulkStorage(xns, svConfig.scanApp.bulkStorage) : undefined; @@ -170,59 +289,182 @@ export async function installSvNode( ...baseConfig, periodicBackupConfig, identitiesBackupLocation, - bulkStorageBucket, + bulkStorageBuckets, }; - const identitiesBackupConfigSecret = installBucketSecret( - xns, - config.identitiesBackupLocation.bucket - ); + if (migrationArgs?.action === 'import') { + const appsPostgres = await installAppsPostgres(xns, config, migrationArgs); - const topologySnapshotConfigSecret = periodicTopologySnapshotConfig - ? installBucketSecret(xns, periodicTopologySnapshotConfig.location.bucket) - : undefined; - const backupConfigSecret: pulumi.Resource | undefined = config.periodicBackupConfig - ? config.periodicBackupConfig.location.bucket != config.identitiesBackupLocation.bucket - ? installBucketSecret(xns, config.periodicBackupConfig.location.bucket) - : identitiesBackupConfigSecret - : undefined; + return undefined; + } else { + const loopback = installSvLoopback(xns, decentralizedSynchronizerUpgradeConfig.usesCometbft()); + const imagePullDeps = imagePullSecret(xns); - const participantBootstrapDumpSecret: pulumi.Resource | undefined = config.bootstrappingDumpConfig - ? await fetchAndInstallParticipantBootstrapDump(xns, config.bootstrappingDumpConfig) - : undefined; + const auth0Secrets: CnInput[] = await installSvAppSecrets( + xns, + baseConfig.auth0Client + ); - const dependsOn: CnInput[] = auth0Secrets - .concat( - config.onboarding.type == 'join-with-key' - ? installSvKeySecret(xns, config.onboarding.keys) - : [] - ) - .concat( - config.onboarding.type == 'join-with-key' && - config.onboarding.sponsorRelease && - spliceConfig.pulumiProjectConfig.interAppsDependencies - ? [config.onboarding.sponsorRelease] - : [] - ) - .concat( - config.expectedValidatorOnboardings.map(onboarding => - installValidatorOnboardingSecret(xns, onboarding.name, onboarding.secret) + const identitiesBackupConfigSecret = installBucketSecret( + xns, + config.identitiesBackupLocation.bucket + ); + + const topologySnapshotConfigSecret = periodicTopologySnapshotConfig + ? installBucketSecret(xns, periodicTopologySnapshotConfig.location.bucket) + : undefined; + const backupConfigSecret: pulumi.Resource | undefined = config.periodicBackupConfig + ? config.periodicBackupConfig.location.bucket != config.identitiesBackupLocation.bucket + ? installBucketSecret(xns, config.periodicBackupConfig.location.bucket) + : identitiesBackupConfigSecret + : undefined; + + const participantBootstrapDumpSecret: pulumi.Resource | undefined = + config.bootstrappingDumpConfig + ? await fetchAndInstallParticipantBootstrapDump(xns, config.bootstrappingDumpConfig) + : undefined; + + const dependsOn: CnInput[] = auth0Secrets + .concat( + config.onboarding.type == 'join-with-key' + ? installSvKeySecret(xns, config.onboarding.keys) + : [] + ) + .concat( + config.onboarding.type == 'join-with-key' && + config.onboarding.sponsorRelease !== undefined && + spliceConfig.pulumiProjectConfig.interAppsDependencies + ? [config.onboarding.sponsorRelease] + : [] ) - ) - .concat([identitiesBackupConfigSecret]) - .concat(backupConfigSecret ? [backupConfigSecret] : []) - .concat(topologySnapshotConfigSecret ? [topologySnapshotConfigSecret] : []) - .concat(participantBootstrapDumpSecret ? [participantBootstrapDumpSecret] : []) - .concat(loopback) - .concat(imagePullDeps) - .concat( - config.cometBftGovernanceKey - ? svCometBftGovernanceKeySecret(xns, config.cometBftGovernanceKey) - : [] - ) - .concat(bulkStorageBucket ? [bulkStorageBucket.secret, bulkStorageBucket.bucket] : []) - .concat(extraDependsOn); + .concat( + config.expectedValidatorOnboardings.map(onboarding => + installValidatorOnboardingSecret(xns, onboarding.name, onboarding.secret) + ) + ) + .concat([identitiesBackupConfigSecret]) + .concat(backupConfigSecret ? [backupConfigSecret] : []) + .concat(topologySnapshotConfigSecret ? [topologySnapshotConfigSecret] : []) + .concat(participantBootstrapDumpSecret ? [participantBootstrapDumpSecret] : []) + .concat(loopback) + .concat(imagePullDeps) + .concat( + config.cometBftGovernanceKey + ? svCometBftGovernanceKeySecret(xns, config.cometBftGovernanceKey) + : [] + ) + .concat( + bulkStorageBuckets + ? [ + bulkStorageBuckets.staging.secret, + bulkStorageBuckets.staging.bucket, + bulkStorageBuckets.committed.secret, + bulkStorageBuckets.committed.bucket, + ] + : [] + ) + .concat(extraDependsOn); + const appsPostgres = await installAppsPostgres(xns, config, migrationArgs); + + const canton = new SynchronizerNodes( + decentralizedSynchronizerUpgradeConfig, + { + ...config.nodeConfigs, + self: { ...config.cometBft, nodeName: config.nodeName }, + }, + config.ingressName + ); + + const svApp = installSvApp( + decentralizedSynchronizerUpgradeConfig, + { ...config, periodicTopologySnapshotConfig }, + xns, + dependsOn, + appsPostgres, + canton + ); + + const scan = installScan( + xns, + config, + decentralizedSynchronizerUpgradeConfig, + dependsOn, + canton, + svApp, + appsPostgres + ); + + installInfo( + xns, + `info.${config.ingressName}.${CLUSTER_HOSTNAME}`, + 'cluster-ingress/cn-http-gateway', + decentralizedSynchronizerUpgradeConfig, + `http://scan-app.${config.nodeName}:5012`, + scan, + config.version + ); + + const validatorApp = await installValidator( + appsPostgres, + xns, + decentralizedSynchronizerUpgradeConfig, + baseConfig, + backupConfigSecret, + canton, + svApp, + scan + ); + + const ingress = installSpliceHelmChart( + xns, + 'ingress-sv', + 'splice-cluster-ingress-runbook', + { + withSvIngress: true, + ingress: { + decentralizedSynchronizer: { + migrationIds: decentralizedSynchronizerUpgradeConfig + .runningMigrations() + .map(x => x.id.toString()), + }, + }, + spliceDomainNames: { + nameServiceDomain: ansDomainPrefix, + }, + cluster: { + hostname: CLUSTER_HOSTNAME, + svNamespace: xns.logicalName, + svIngressName: config.ingressName, + }, + rateLimit: { + scan: { + enable: false, + }, + }, + }, + config.version, + { dependsOn: [xns.ns] } + ); + + return { + namespace: xns, + nodeName: config.nodeName, + canton, + validatorApp, + svApp, + scan, + ingress, + appsPostgres, + }; + } +} + +async function installAppsPostgres( + xns: ExactNamespace, + config: SvConfig, + migrationArgs?: MigrationArgs +): Promise { const defaultPostgres = config.splitPostgresInstances ? undefined : await postgres.installPostgres( @@ -231,9 +473,10 @@ export async function installSvNode( 'postgres', config.version, spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, false, { - logicalDecoding: !!baseConfig.scanApp?.bigQuery, + logicalDecoding: !!config.scanApp?.bigQuery, } ); @@ -244,98 +487,30 @@ export async function installSvNode( `cn-apps-pg`, `cn-apps-pg`, config.version, - svConfig.appsPg?.cloudSql ?? spliceConfig.pulumiProjectConfig.cloudSql, + config.appsPg?.cloudSql ?? spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, true, { - logicalDecoding: !!baseConfig.scanApp?.bigQuery, + logicalDecoding: !!config.scanApp?.bigQuery, + ...(() => { + switch (migrationArgs?.action) { + case 'import': + return { + existingInstanceName: migrationArgs.databaseInstanceName, + existingSecretName: migrationArgs.databaseSecretName, + }; + case 'export': + return { + retainDbResourcesOnDelete: true, + }; + case undefined: + return {}; + } + })(), } )); - const canton = new SynchronizerNodes( - decentralizedSynchronizerUpgradeConfig, - { - ...config.nodeConfigs, - self: { ...config.cometBft, nodeName: config.nodeName }, - }, - config.ingressName - ); - - const svApp = installSvApp( - decentralizedSynchronizerUpgradeConfig, - { ...config, periodicTopologySnapshotConfig }, - xns, - dependsOn, - appsPostgres, - canton - ); - - const scan = installScan( - xns, - config, - decentralizedSynchronizerUpgradeConfig, - dependsOn, - canton, - svApp, - appsPostgres - ); - - installInfo( - xns, - `info.${config.ingressName}.${CLUSTER_HOSTNAME}`, - 'cluster-ingress/cn-http-gateway', - decentralizedSynchronizerUpgradeConfig, - `http://scan-app.${config.nodeName}:5012`, - scan, - config.version - ); - - if (baseConfig.scanApp?.bigQuery && appsPostgres instanceof postgres.CloudPostgres) { - configureScanBigQuery(appsPostgres, baseConfig.scanApp!.bigQuery, scan); - } - - const validatorApp = await installValidator( - appsPostgres, - xns, - decentralizedSynchronizerUpgradeConfig, - baseConfig, - backupConfigSecret, - canton, - svApp, - scan - ); - - const ingress = installSpliceHelmChart( - xns, - 'ingress-sv', - 'splice-cluster-ingress-runbook', - { - withSvIngress: true, - ingress: { - decentralizedSynchronizer: { - migrationIds: decentralizedSynchronizerUpgradeConfig - .runningMigrations() - .map(x => x.id.toString()), - }, - }, - spliceDomainNames: { - nameServiceDomain: ansDomainPrefix, - }, - cluster: { - hostname: CLUSTER_HOSTNAME, - svNamespace: xns.logicalName, - svIngressName: config.ingressName, - }, - rateLimit: { - scan: { - enable: false, - }, - }, - }, - config.version, - { dependsOn: [xns.ns] } - ); - - return { canton, validatorApp, svApp, scan, ingress }; + return appsPostgres; } function persistenceConfig(postgresDb: postgres.Postgres, dbName: string): PersistenceConfig { @@ -523,7 +698,7 @@ function installSvApp( dependsOn: dependsOn.concat([postgres]).concat(allSynchronizerDependencies), }, undefined, - appsAffinityAndTolerations + appsKubernetesScheduling ); } @@ -598,16 +773,24 @@ function installScan( logLevel: config.logging?.appsLogLevel, apiRequestLogLevel: config.logging?.apiRequestLogLevel, logAsyncFlush: config.logging?.appsAsync, - additionalEnvVars: config.scanApp?.additionalEnvVars || [], + additionalEnvVars: (config.scanApp?.additionalEnvVars || []).concat([ + envoyClientIpHeaderEnvVar('canton.scan-apps.scan-app'), + ]), resources: config.scanApp?.resources, - ...(config.bulkStorageBucket + ...(config.bulkStorageBuckets ? { bulkStorage: { - s3: { - region: config.bulkStorageBucket.region, - bucketName: config.bulkStorageBucket.bucket.name, + staging: { + region: config.bulkStorageBuckets.staging.region, + bucketName: config.bulkStorageBuckets.staging.bucket.name, + endpoint: 'https://storage.googleapis.com', // gcs endpoint for s3 + secretName: config.bulkStorageBuckets.staging.secret.metadata.name, + }, + committed: { + region: config.bulkStorageBuckets.committed.region, + bucketName: config.bulkStorageBuckets.committed.bucket.name, endpoint: 'https://storage.googleapis.com', // gcs endpoint for s3 - secretName: config.bulkStorageBucket.secret.metadata.name, + secretName: config.bulkStorageBuckets.committed.secret.metadata.name, }, }, } @@ -631,3 +814,19 @@ function installScan( ), }); } + +export type MigrationArgs = + | { + action: 'import'; + databaseInstanceName: string; + databaseSecretName: string; + } + | { + action: 'export'; + }; + +export type SvsMigrationOutput = Array<{ + nodeName: string; + databaseInstanceName: string; + databaseSecretName: string; +}>; diff --git a/cluster/pulumi/common-sv/src/svApp.ts b/cluster/pulumi/common-sv/src/svApp.ts index 04847f32e2..c1ac0dcf43 100644 --- a/cluster/pulumi/common-sv/src/svApp.ts +++ b/cluster/pulumi/common-sv/src/svApp.ts @@ -5,6 +5,7 @@ import { CLUSTER_HOSTNAME, DecentralizedSynchronizerMigrationConfig, EnvVarConfig, + envoyClientIpHeaderEnvVar, MigrationInfo, pvcSuffix, standardStorageClassName, @@ -109,7 +110,8 @@ export function valuesForSvApp( .concat(bftSequencerConnectionEnvVars) .concat(mediatorPruningConfig) .concat(cantonBftPruningConfig) - .concat(additionalPackagesToUnvetConfig); + .concat(additionalPackagesToUnvetConfig) + .concat([envoyClientIpHeaderEnvVar('canton.sv-apps.sv')]); const synchronizerValues: { synchronizers: object } = { synchronizers: { diff --git a/cluster/pulumi/common-sv/src/synchronizer/cometbft.ts b/cluster/pulumi/common-sv/src/synchronizer/cometbft.ts index bcf6ada3d1..67792bfd1c 100644 --- a/cluster/pulumi/common-sv/src/synchronizer/cometbft.ts +++ b/cluster/pulumi/common-sv/src/synchronizer/cometbft.ts @@ -4,7 +4,8 @@ import * as k8s from '@pulumi/kubernetes'; import * as _ from 'lodash'; import { activeVersion, - appsAffinityAndTolerations, + appsKubernetesScheduling, + ChartValues, CLUSTER_BASENAME, CLUSTER_HOSTNAME, clusterSmallDisk, @@ -23,7 +24,6 @@ import { withAddedDependencies, } from '@canton-network/splice-pulumi-common'; import { CnChartVersion } from '@canton-network/splice-pulumi-common/src/artifacts'; -import { hyperdiskSupportConfig } from '@canton-network/splice-pulumi-common/src/config/hyperdiskSupportConfig'; import { jsonStringify, Output } from '@pulumi/pulumi'; import { svsConfig } from '../config'; @@ -103,14 +103,6 @@ export function installCometBftNode( ? undefined : installCometBftKeysSecret(xns, nodeConfig.validator.keyAddress, migrationId); - let hyperdiskDbValues = {}; - if (hyperdiskSupportConfig.hyperdiskSupport.enabled) { - hyperdiskDbValues = { - pvcName: `cometbft-migration-${migrationId}-hd-pvc`, - volumeStorageClass: standardStorageClassName, - }; - } - const cometbftChartValues = _.mergeWith(cometBftValues, { sv1: nodeConfigs.sv1, istioVirtualService: { @@ -146,11 +138,13 @@ export function installCometBftNode( }, db: { volumeSize: clusterSmallDisk ? '240Gi' : pvcSize || svsConfig?.cometbft?.volumeSize, - ...hyperdiskDbValues, + pvcName: `cometbft-migration-${migrationId}-hd-pvc`, + volumeStorageClass: standardStorageClassName, }, extraLogLevelFlags: svConfiguration.logging?.cometbftExtraLogLevelFlags, serviceAccountName: imagePullServiceAccountName, resources: svConfiguration.cometbft?.resources, + watchdog: watchdogValues(migrationId), }); if (svConfiguration.cometbft?.additionalHelmValues) { _.merge(cometbftChartValues, svConfiguration.cometbft.additionalHelmValues); @@ -170,11 +164,32 @@ export function installCometBftNode( protect: disableProtection ? false : protectCometBft, }, true, - appsAffinityAndTolerations + appsKubernetesScheduling ); return { rpcServiceName: `${nodeConfig.identifier}-cometbft-rpc`, release }; } +const CANTON_METRICS_PORT = 10013; + +function watchdogValues(migrationId: DomainMigrationIndex): ChartValues { + const watchdog = svsConfig?.cometbft?.watchdog; + if (watchdog?.disabled) { + return { enabled: false }; + } + const synchronizer = `global-domain-${migrationId}`; + return { + enabled: true, + sequencerMetricsUrl: `http://${synchronizer}-sequencer:${CANTON_METRICS_PORT}/metrics`, + mediatorMetricsUrl: `http://${synchronizer}-mediator:${CANTON_METRICS_PORT}/metrics`, + threshold: watchdog?.threshold, + evaluationIntervalSeconds: watchdog?.evaluationIntervalSeconds, + pollIntervalSeconds: watchdog?.pollIntervalSeconds, + scrapeTimeoutSeconds: watchdog?.scrapeTimeoutSeconds, + startupGraceSeconds: watchdog?.startupGraceSeconds, + cooldownSeconds: watchdog?.cooldownSeconds, + }; +} + function installCometBftKeysSecret( xns: ExactNamespace, keyAddress: Output | string, diff --git a/cluster/pulumi/common-sv/tsconfig.eslint.json b/cluster/pulumi/common-sv/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/common-sv/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/common-validator/src/config.ts b/cluster/pulumi/common-validator/src/config.ts index b68e71417a..3147de4175 100644 --- a/cluster/pulumi/common-validator/src/config.ts +++ b/cluster/pulumi/common-validator/src/config.ts @@ -90,7 +90,11 @@ export const ValidatorNodeConfigSchema = z.object({ logging: z .object({ level: LogLevelSchema.optional(), + // Log level for the Splice apps' HTTP request logging (org.lfdecentralizedtrust.splice.admin.api) apiRequestLogLevel: LogLevelSchema.optional(), + // Log level for the Canton nodes' Ledger-API audit logging (com.digitalasset.canton.logging.audit) + // Falls back to `apiRequestLogLevel` when not specified + cantonApiRequestLogLevel: LogLevelSchema.optional(), async: z.boolean().optional(), }) .default({}), diff --git a/cluster/pulumi/common-validator/src/participant.ts b/cluster/pulumi/common-validator/src/participant.ts index a65cc7971f..f41485b826 100644 --- a/cluster/pulumi/common-validator/src/participant.ts +++ b/cluster/pulumi/common-validator/src/participant.ts @@ -6,17 +6,16 @@ import { Auth0Config, auth0UserNameEnvVarSource, ChartValues, - DomainMigrationIndex, ExactNamespace, getAdditionalJvmOptions, getParticipantKmsHelmResources, installSpliceHelmChart, loadYamlFromFile, - sanitizedForPostgres, SPLICE_ROOT, SpliceCustomResourceOptions, spliceConfig, getLedgerApiAudience, + DecentralizedSynchronizerUpgradeConfig, } from '@canton-network/splice-pulumi-common'; import { ValidatorNodeConfig } from '@canton-network/splice-pulumi-common-validator'; import { CnChartVersion } from '@canton-network/splice-pulumi-common/src/artifacts'; @@ -24,7 +23,6 @@ import { Output } from '@pulumi/pulumi'; export async function installParticipant( validatorConfig: ValidatorNodeConfig, - migrationId: DomainMigrationIndex, xns: ExactNamespace, auth0Config: Auth0Config, disableAuth?: boolean, @@ -45,6 +43,7 @@ export async function installParticipant( `participant-pg`, activeVersion, spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, true )); const participantValues: ChartValues = { @@ -55,8 +54,7 @@ export async function installParticipant( } ), ...loadYamlFromFile( - `${SPLICE_ROOT}/apps/app/src/pack/examples/sv-helm/standalone-participant-values.yaml`, - { MIGRATION_ID: migrationId.toString() } + `${SPLICE_ROOT}/apps/app/src/pack/examples/sv-helm/standalone-participant-values.yaml` ), ...kmsValues, metrics: { @@ -72,8 +70,8 @@ export async function installParticipant( }, }; - const name = `participant-${migrationId}`; - const pgName = sanitizedForPostgres(name); + const name = 'participant'; + const pgName = `participant_${DecentralizedSynchronizerUpgradeConfig.frozenMigrationId}`; const release = installSpliceHelmChart( xns, name, @@ -81,7 +79,9 @@ export async function installParticipant( { ...participantValuesWithSpecifiedAud, logLevel: validatorConfig.logging?.level, - apiRequestLogLevel: validatorConfig.logging?.apiRequestLogLevel, + apiRequestLogLevel: + validatorConfig.logging?.cantonApiRequestLogLevel ?? + validatorConfig.logging?.apiRequestLogLevel, logAsyncFlush: validatorConfig.logging?.async, persistence: { databaseName: pgName, @@ -110,6 +110,12 @@ export async function installParticipant( dependsOn: (customOptions?.dependsOn || []) .concat([participantPostgres]) .concat(kmsDependencies), + deleteBeforeReplace: true, + aliases: [ + { + name: `${xns.logicalName}-participant-${DecentralizedSynchronizerUpgradeConfig.frozenMigrationId}`, + }, + ], } ); return { diff --git a/cluster/pulumi/common-validator/tsconfig.eslint.json b/cluster/pulumi/common-validator/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/common-validator/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/common/package.json b/cluster/pulumi/common/package.json index e86c2325d8..3caf421b9f 100644 --- a/cluster/pulumi/common/package.json +++ b/cluster/pulumi/common/package.json @@ -9,7 +9,7 @@ "@pulumi/command": "1.2.1", "@pulumi/gcp": "^9.18.0", "@pulumi/kubernetes": "4.28.0", - "@pulumi/pulumi": "^3.230.0", + "@pulumi/pulumi": "^3.243.0", "@pulumi/random": "4.19.2", "@pulumi/std": "2.3.2", "@pulumi/github": "6.13.1", @@ -35,7 +35,7 @@ "devDependencies": { "@jest/globals": "^30.4.1", "@types/js-yaml": "^4.0.5", - "@types/lodash": "^4.17.24", + "@types/lodash": "^4.17.25", "@types/ws": "^8.18.1", "dedent": "^1.7.2" } diff --git a/cluster/pulumi/common/src/auth0/auth0.ts b/cluster/pulumi/common/src/auth0/auth0.ts index ebd8809ab2..3c89d54028 100644 --- a/cluster/pulumi/common/src/auth0/auth0.ts +++ b/cluster/pulumi/common/src/auth0/auth0.ts @@ -8,7 +8,7 @@ import { Output } from '@pulumi/pulumi'; import { AuthenticationClient, ManagementClient, TokenSet, withRetries } from 'auth0'; import { config, isMainNet } from '../config'; -import { infraStack } from '../stackReferences'; +import { StackReferences } from '../stackReferences'; import { fixedTokens } from '../utils'; import { DEFAULT_AUDIENCE } from './audiences'; import type { @@ -314,7 +314,7 @@ export enum Auth0ClientType { export function getAuth0ClusterConfig(): Output { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const infraOutput: pulumi.Output = infraStack.requireOutput('auth0'); + const infraOutput: pulumi.Output = StackReferences.infra.requireOutput('auth0'); return infraOutput.apply(output => { if ( (output['cantonNetwork'] && output['cantonNetwork']['appToClientId'] === undefined) || diff --git a/cluster/pulumi/common/src/config/cloudSql.ts b/cluster/pulumi/common/src/config/cloudSql.ts deleted file mode 100644 index f9fc0c2ca7..0000000000 --- a/cluster/pulumi/common/src/config/cloudSql.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -import { merge } from 'lodash'; -import { z } from 'zod'; - -import { spliceConfig } from './config'; - -export const CloudSqlConfigSchema = z.object({ - enabled: z.boolean(), - // Docs on cloudsql maintenance windows: https://cloud.google.com/sql/docs/postgres/set-maintenance-window - maintenanceWindow: z - .object({ - day: z.number().min(1).max(7).default(2), // 1 (Monday) to 7 (Sunday) - hour: z.number().min(0).max(23).default(8), // 24-hour format UTC - }) - .default({ day: 2, hour: 8 }), - protected: z.boolean(), - tier: z.string(), - enterprisePlus: z.boolean(), - flags: z.record(z.string(), z.string()).default({}), - // https://cloud.google.com/sql/docs/mysql/backup-recovery/backups#retained-backups - // controls the number of automated gcp sql backups to retain - backupsToRetain: z.number().optional(), - databaseVersion: z.string().default('POSTGRES_14'), -}); -export type CloudSqlConfig = z.infer; diff --git a/cluster/pulumi/common/src/config/configSchema.ts b/cluster/pulumi/common/src/config/configSchema.ts index a170e817bd..3e6fec7028 100644 --- a/cluster/pulumi/common/src/config/configSchema.ts +++ b/cluster/pulumi/common/src/config/configSchema.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { z } from 'zod'; -import { CloudSqlConfigSchema } from './cloudSql'; +import { CloudSqlConfigSchema, SplicePostgresSchema } from './database'; import { defaultActiveMigration, SynchronizerMigrationSchema } from './migrationSchema'; // This is a config that's relevant for all (most) pulumi projects. For project-specific configuration, @@ -13,6 +13,10 @@ const PulumiProjectConfigSchema = z.object({ hasPublicInfo: z.boolean(), interAppsDependencies: z.boolean(), cloudSql: CloudSqlConfigSchema, + defaultSplicePostgresConfig: SplicePostgresSchema.default({ + deployment: 'docker-image', + postgresImage: 'postgres:18', + }), allowDowngrade: z.boolean(), replacePostgresStatefulSetOnChanges: z.boolean().default(false), }); @@ -33,6 +37,18 @@ export const ConfigSchema = z.object({ PulumiProjectConfigSchema.extend({ cloudSql: CloudSqlConfigSchema.partial() }).partial() ) ), + // Settings that affect both how node pools are created and how pods are deployed + // (e.g, how we set labels/taints). Since these are implemented in different pulumi projects, + // we need to have a common config schema for them. + kubernetesScheduling: z + .object({ + computeClasses: z + .object({ + enabled: z.boolean().default(false), + }) + .prefault({}), + }) + .prefault({}), }); export type Config = z.infer; diff --git a/cluster/pulumi/common/src/config/database.ts b/cluster/pulumi/common/src/config/database.ts new file mode 100644 index 0000000000..9ff8bc8ac5 --- /dev/null +++ b/cluster/pulumi/common/src/config/database.ts @@ -0,0 +1,52 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { merge } from 'lodash'; +import { z } from 'zod'; + +import { spliceConfig } from './config'; + +export const CloudSqlConfigSchema = z.object({ + enabled: z.boolean(), + // Docs on cloudsql maintenance windows: https://cloud.google.com/sql/docs/postgres/set-maintenance-window + maintenanceWindow: z + .object({ + day: z.number().min(1).max(7).default(2), // 1 (Monday) to 7 (Sunday) + hour: z.number().min(0).max(23).default(8), // 24-hour format UTC + }) + .default({ day: 2, hour: 8 }), + protected: z.boolean(), + tier: z.string(), + enterprisePlus: z.boolean(), + flags: z.record(z.string(), z.string()).default({}), + // https://cloud.google.com/sql/docs/mysql/backup-recovery/backups#retained-backups + // controls the number of automated gcp sql backups to retain + backupsToRetain: z.number().optional(), + databaseVersion: z.string().default('POSTGRES_14'), +}); +export type CloudSqlConfig = z.infer; + +// Deployment strategy: +// - If no migration is necessary, just default docker-image will deploy the latest version +// - If you want to migrate data, you need to, in this order: +// 1) deployment = 'legacy-helm-chart' (this is the original state, which uses pg14, unconfigurable) +// 2) When the time to migrate comes, scale down all pods that use the database and set deployment = 'migrate' +// 3) Once the migration is complete (i.e., the DB pod is up and running and apps can connect to it), set deployment = 'docker-image' +// Once everything has been migrated we can drop this, as everything will be using docker-image. +export const SplicePostgresMigrateSchema = z.object({ + deployment: z.literal('migrate'), + migrationVolumeSize: z.string(), + postgresImage: z.string(), +}); +export type SplicePostgresMigrateConfig = z.infer; +export const SplicePostgresDockerImageSchema = z.object({ + deployment: z.literal('docker-image'), + postgresImage: z.string(), +}); +export type SplicePostgresDockerImageConfig = z.infer; +export const SplicePostgresSchema = z.union([ + z.object({ deployment: z.literal('legacy-helm-chart') }), + SplicePostgresMigrateSchema, + SplicePostgresDockerImageSchema, +]); + +export type SplicePostgresConfig = z.infer; diff --git a/cluster/pulumi/common/src/config/hyperdiskSupportConfig.ts b/cluster/pulumi/common/src/config/hyperdiskSupportConfig.ts deleted file mode 100644 index 8e83855c8e..0000000000 --- a/cluster/pulumi/common/src/config/hyperdiskSupportConfig.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -import { z } from 'zod'; - -import { clusterSubConfig } from './config'; - -const HyperdiskSupportConfigSchema = z.object({ - hyperdiskSupport: z - .object({ - enabled: z.boolean(), - enabledForInfra: z.boolean(), - }) - .strict(), -}); - -export type HyperdiskSupportConfig = z.infer; - -export const hyperdiskSupportConfig: HyperdiskSupportConfig = HyperdiskSupportConfigSchema.parse( - clusterSubConfig('cluster') -); diff --git a/cluster/pulumi/common/src/config/index.ts b/cluster/pulumi/common/src/config/index.ts index 33faa48511..cd82f1c099 100644 --- a/cluster/pulumi/common/src/config/index.ts +++ b/cluster/pulumi/common/src/config/index.ts @@ -6,7 +6,7 @@ import { spliceEnvConfig } from './envConfig'; export * from './configSchema'; export * from './kms'; -export * from './cloudSql'; +export * from './database'; export { spliceEnvConfig as config } from './envConfig'; export const DeploySvRunbook = spliceEnvConfig.envFlag('SPLICE_DEPLOY_SV_RUNBOOK', false); diff --git a/cluster/pulumi/common/src/config/migrationSchema.ts b/cluster/pulumi/common/src/config/migrationSchema.ts index 879475b52f..e159021dd1 100644 --- a/cluster/pulumi/common/src/config/migrationSchema.ts +++ b/cluster/pulumi/common/src/config/migrationSchema.ts @@ -60,5 +60,8 @@ export const SynchronizerMigrationSchema = z activeDatabaseId: z.number().optional(), attachPvc: z.boolean().default(true), frozenMigrationId: z.number(), + // TODO(#6719) once all clusters have been migrated the following two flags can be removed, hardcoding splitSvDeploymentEnabled to true. + splitSvDeploymentEnabled: z.boolean().default(false), + migrateToSplitSvDeployment: z.boolean().default(false), }) .strict(); diff --git a/cluster/pulumi/common/src/config/scanEndpoints.ts b/cluster/pulumi/common/src/config/scanEndpoints.ts index 1cd62eb796..79e63b4e56 100644 --- a/cluster/pulumi/common/src/config/scanEndpoints.ts +++ b/cluster/pulumi/common/src/config/scanEndpoints.ts @@ -6,6 +6,39 @@ import { z } from 'zod'; import { readAndParseYaml } from './configLoader'; const scanYamlPath = path.join(__dirname, '../../../../../apps/scan/src/main/openapi/scan.yaml'); +const tokenRegistryYamlPaths = [ + // V1 Specs + path.join( + __dirname, + '../../../../../token-standard/splice-api-token-metadata-v1/openapi/token-metadata-v1.yaml' + ), + path.join( + __dirname, + '../../../../../token-standard/splice-api-token-allocation-v1/openapi/allocation-v1.yaml' + ), + path.join( + __dirname, + '../../../../../token-standard/splice-api-token-allocation-instruction-v1/openapi/allocation-instruction-v1.yaml' + ), + path.join( + __dirname, + '../../../../../token-standard/splice-api-token-transfer-instruction-v1/openapi/transfer-instruction-v1.yaml' + ), + + // V2 Specs + path.join( + __dirname, + '../../../../../token-standard/splice-api-token-allocation-v2/openapi/allocation-v2.yaml' + ), + path.join( + __dirname, + '../../../../../token-standard/splice-api-token-allocation-instruction-v2/openapi/allocation-instruction-v2.yaml' + ), + path.join( + __dirname, + '../../../../../token-standard/splice-api-token-transfer-instruction-v2/openapi/transfer-instruction-v2.yaml' + ), +]; const MinimalOpenApiSchema = z.object({ paths: z.object({}).catchall(z.unknown()).default({}) }); @@ -37,3 +70,27 @@ export function parseScanYamlEndpoints(): string[] { return Array.from(endpoints).sort(); } + +/** + * Read all token registry standard OpenAPI paths into normalized `/registry/...` endpoint prefixes. + */ +export function parseTokenRegistrySpecEndpoints(): string[] { + const endpoints = new Set(); + + for (const yamlPath of tokenRegistryYamlPaths) { + const yaml = MinimalOpenApiSchema.parse(readAndParseYaml(yamlPath)); + const paths = yaml.paths; + + for (let fullPath of Object.keys(paths)) { + const paramIndex = fullPath.indexOf('{'); + if (paramIndex !== -1) { + const lastSlash = fullPath.lastIndexOf('/', paramIndex); + fullPath = fullPath.substring(0, lastSlash); + } + + endpoints.add(fullPath); + } + } + + return Array.from(endpoints).sort(); +} diff --git a/cluster/pulumi/common/src/dockerConfig.ts b/cluster/pulumi/common/src/dockerConfig.ts index bd17145059..92e5128782 100644 --- a/cluster/pulumi/common/src/dockerConfig.ts +++ b/cluster/pulumi/common/src/dockerConfig.ts @@ -17,21 +17,14 @@ export class DockerConfig { private jsonConfig: pulumi.Output; private constructor() { - const jfrogCreds = DockerConfig.fetchCredentialsFromSecret('artifactory-keys'); const googleCreds = DockerConfig.fetchGoogleCredentialsFromSecret( 'us-central1-artifact-reader-key' ); - this.jsonConfig = pulumi.all([jfrogCreds, googleCreds]).apply(([jfrog, google]) => { - const artifactoryAuth = DockerConfig.toAuthField(jfrog); + this.jsonConfig = googleCreds.apply(google => { const googleAuth = DockerConfig.toAuthField(google); const conf = Buffer.from( JSON.stringify({ auths: { - 'digitalasset-canton-enterprise-docker.jfrog.io': { - auth: artifactoryAuth, - username: jfrog.username, - password: jfrog.password, - }, 'us-central1-docker.pkg.dev': { auth: googleAuth, username: google.username, diff --git a/cluster/pulumi/common/src/dump-config-common.ts b/cluster/pulumi/common/src/dump-config-common.ts index 2cd9acb2a4..d54ec9a5a0 100644 --- a/cluster/pulumi/common/src/dump-config-common.ts +++ b/cluster/pulumi/common/src/dump-config-common.ts @@ -12,6 +12,7 @@ import { NamespacedAuth0Configs, } from './auth0/auth0types'; import { isMainNet } from './config'; +import { ClusterBasename } from './config/gcpConfig'; // Importing DEFAULT_AUDIENCE from auth0/audiences.ts creates a nightmare of things getting initialized too early, so we just redefine it here const DEFAULT_AUDIENCE = 'https://canton.network.global'; @@ -24,6 +25,7 @@ export enum PulumiFunction { GCP_GET_SECRET_VERSION = 'gcp:secretmanager/getSecretVersion:getSecretVersion', GCP_GET_CLUSTER = 'gcp:container/getCluster:getCluster', STD_BASE64_DECODE = 'std:index:base64decode', + GCP_GET_DATABASE_INSTANCES = 'gcp:sql/getDatabaseInstances:getDatabaseInstances', } export class SecretsFixtureMap extends Map { @@ -227,20 +229,22 @@ export async function initDumpConfig({ process.stdout.write(buffer); process.stdout.write('\n'); - if (args.type === 'pulumi:pulumi:StackReference') { - const [organization, project, stack] = args.name.split('/'); - return { - id: args.name + '_id', - state: { - ...args.inputs, - outputs: pulumi.output(stackOutputsProvider(project, stack) ?? {}), - }, - }; - } else { - return { - id: args.inputs.name + '_id', - state: args.inputs, - }; + switch (args.type) { + case 'pulumi:pulumi:StackReference': { + const [organization, project, stack] = args.name.split('/'); + return { + id: args.name + '_id', + state: { + ...args.inputs, + outputs: pulumi.output(stackOutputsProvider(project, stack) ?? {}), + }, + }; + } + default: + return { + id: args.id ?? args.inputs.name + '_id', + state: args.inputs, + }; } }, call: function (args: pulumi.runtime.MockCallArgs) { @@ -250,7 +254,7 @@ export async function initDumpConfig({ result: `base64-decoded-mock`, }; case PulumiFunction.GCP_GET_PROJECT: - return { ...args.inputs, name: projectName }; + return { ...args.inputs, name: projectName, projectId: projectName }; case PulumiFunction.GCP_GET_SUB_NETWORK: if (args.inputs.name === `cn-${stackName}net-subnet`) { return { ...args.inputs, id: 'subnet-id' }; @@ -317,15 +321,6 @@ export async function initDumpConfig({ ...args.inputs, secretData, }; - } else if (args.inputs.secret == 'artifactory-keys') { - const secretData = JSON.stringify({ - username: 'art_user', - password: 's3cr3t', - }); - return { - ...args.inputs, - secretData, - }; } else if (args.inputs.secret == 'us-central1-artifact-reader-key') { const secretData = JSON.stringify({ type: 'service_account', @@ -372,6 +367,15 @@ export async function initDumpConfig({ ); break; } + case PulumiFunction.GCP_GET_DATABASE_INSTANCES: + return { + instances: [ + { + name: 'sv-1-cn-apps-pg-7ca4614', + settings: [{ userLabels: { cluster: ClusterBasename } }], + }, + ], + }; default: console.error('WARN unhandled call in setMockOptions: ', args); } @@ -393,14 +397,27 @@ export type StackOutputsProvider = ( ) => Partial> | undefined; export const infraStackOutputsProvider: StackOutputsProvider = (project: string) => { - return project === 'infra' - ? { + switch (project) { + case 'canton-network': + return { + svs: [...Array.from({ length: 16 }, (_, index) => `sv-${index + 1}`), 'sv-da-1'].map( + nodeName => ({ + nodeName, + databaseInstanceName: `${nodeName}-cn-apps-pg`, + databaseSecretName: `${nodeName}-cn-apps-pg-secret`, + }) + ), + }; + case 'infra': + return { istioDashboardVersions: '1234', auth0: { svRunbook: svRunbookAuth0Config, cantonNetwork: cantonNetworkAuth0Config, mainnet: cantonNetworkAuth0Config, } as Auth0ClusterConfig, - } - : undefined; + }; + default: + return undefined; + } }; diff --git a/cluster/pulumi/common/src/helm.ts b/cluster/pulumi/common/src/helm.ts index 30840b541e..1c1bb0bdcd 100644 --- a/cluster/pulumi/common/src/helm.ts +++ b/cluster/pulumi/common/src/helm.ts @@ -9,7 +9,6 @@ import path from 'path'; import { CnChartVersion } from './artifacts'; import { config, imagePullPolicy } from './config'; import { spliceConfig } from './config/config'; -import { hyperdiskSupportConfig } from './config/hyperdiskSupportConfig'; import { activeVersion } from './domainMigration'; import { SplicePlaceholderResource } from './pulumiUtilResources'; import { @@ -74,7 +73,7 @@ function installSpliceHelmChartByNamespaceName( version: CnChartVersion = activeVersion, opts?: SpliceCustomResourceOptions, includeNamespaceInName = true, - affinityAndTolerations: object = appsAffinityAndTolerations, + affinityAndTolerations: object = appsKubernetesScheduling, timeout: number = HELM_CHART_TIMEOUT_SEC ): InstalledHelmChart { if (spliceConfig.pulumiProjectConfig.installDataOnly) { @@ -108,7 +107,7 @@ export function installSpliceHelmChart( version: CnChartVersion = activeVersion, opts?: SpliceCustomResourceOptions, includeNamespaceInName = true, - affinityAndTolerations: object = appsAffinityAndTolerations, + affinityAndTolerations: object = appsKubernetesScheduling, timeout: number = HELM_CHART_TIMEOUT_SEC ): InstalledHelmChart { return installSpliceHelmChartByNamespaceName( @@ -172,7 +171,7 @@ export function installSpliceRunbookHelmChartByNamespaceName( chart: chartPath(chartName, version), version: versionStringWithPossibleOverride(version, nsLogicalName, chartName), values: { - ...appsAffinityAndTolerations, + ...appsKubernetesScheduling, ...values, imageRepo: DOCKER_REPO, ...imagePullPolicy, @@ -225,46 +224,80 @@ function versionStringWithPossibleOverride( } } -export const appsAffinityAndTolerations = getAppsAffinityAndTolerations( - hyperdiskSupportConfig.hyperdiskSupport.enabled -); +export const appsComputeClassName = 'cn-apps'; +export const infraComputeClassName = 'cn-infra'; -export const nonHyperdiskAppsAffinityAndTolerations = getAppsAffinityAndTolerations(false); +const appsKubernetesSchedulingComputeClass = { + nodeSelector: { 'cloud.google.com/compute-class': appsComputeClassName }, +}; -function getAppsAffinityAndTolerations(hyperdiskSupport: boolean) { - return { - affinity: { - nodeAffinity: { - requiredDuringSchedulingIgnoredDuringExecution: { - nodeSelectorTerms: [ - { - matchExpressions: [ - { - key: 'cn_apps', - operator: 'Exists', - }, - { - key: 'cn_apps', - operator: hyperdiskSupport ? 'In' : 'NotIn', - values: ['hyperdisk'], - }, - ], - }, - ], - }, +const appsKubernetesSchedulingAffinityTolerations = { + affinity: { + nodeAffinity: { + requiredDuringSchedulingIgnoredDuringExecution: { + nodeSelectorTerms: [ + { + matchExpressions: [ + { + key: 'cn_apps', + operator: 'Exists', + }, + { + key: 'cn_apps', + operator: 'In', + values: ['hyperdisk'], + }, + ], + }, + ], }, }, - tolerations: [ - { - key: 'cn_apps', - operator: 'Exists', - effect: 'NoSchedule', + }, + tolerations: [ + { + key: 'cn_apps', + operator: 'Exists', + effect: 'NoSchedule', + }, + ], +}; + +export const useComputeClasses = + spliceConfig.configuration.kubernetesScheduling.computeClasses.enabled; + +// Values that determine how apps pods are scheduled. +export const appsKubernetesScheduling = useComputeClasses + ? appsKubernetesSchedulingComputeClass + : appsKubernetesSchedulingAffinityTolerations; + +export const infraKubernetesSchedulingComputeClass = { + nodeSelector: { 'cloud.google.com/compute-class': infraComputeClassName }, +}; + +// This should have the same effect as infraKubernetesSchedulingComputeClass, +// but uses affinity instead of nodeSelector. It's not the documented way to use +// compute classes, but can be used for helm charts that do not support node selectors. +export const infraKubernetesSchedulingComputeClassViaAffinity = { + affinity: { + nodeAffinity: { + requiredDuringSchedulingIgnoredDuringExecution: { + nodeSelectorTerms: [ + { + matchExpressions: [ + { + key: 'cloud.google.com/compute-class', + operator: 'In', + values: [infraComputeClassName], + }, + ], + }, + ], }, - ], - }; -} + }, + }, +}; -export const infraAffinityAndTolerations = { +export const infraKubernetesSchedulingAffinityTolerations = { affinity: { nodeAffinity: { requiredDuringSchedulingIgnoredDuringExecution: { @@ -289,3 +322,47 @@ export const infraAffinityAndTolerations = { }, ], }; + +// Values that determine how infra pods are scheduled. +export const infraKubernetesScheduling = useComputeClasses + ? infraKubernetesSchedulingComputeClass + : infraKubernetesSchedulingAffinityTolerations; + +// Values that determine how daemons are scheduled that need to run on BOTH apps and infra nodes. +export const infraAndAppsKubernetesSchedulingForDaemonSets = useComputeClasses + ? { + // DaemonSets do not trigger autoscaling and are instead scheduled once + // per existing, eligible node. + tolerations: [ + { + key: 'cloud.google.com/compute-class', + operator: 'Equal', + value: infraComputeClassName, + effect: 'NoSchedule', + }, + { + key: 'cloud.google.com/compute-class', + operator: 'Equal', + value: appsComputeClassName, + effect: 'NoSchedule', + }, + ], + } + : { + affinity: { + nodeAffinity: { + requiredDuringSchedulingIgnoredDuringExecution: { + nodeSelectorTerms: [ + ...appsKubernetesSchedulingAffinityTolerations.affinity.nodeAffinity + .requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, + ...infraKubernetesSchedulingAffinityTolerations.affinity.nodeAffinity + .requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, + ], + }, + }, + }, + tolerations: [ + ...appsKubernetesSchedulingAffinityTolerations.tolerations, + ...infraKubernetesSchedulingAffinityTolerations.tolerations, + ], + }; diff --git a/cluster/pulumi/common/src/operator/config.ts b/cluster/pulumi/common/src/operator/config.ts index 28c3af446b..ad36bebb71 100644 --- a/cluster/pulumi/common/src/operator/config.ts +++ b/cluster/pulumi/common/src/operator/config.ts @@ -1,11 +1,6 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { - config, - DeploySvRunbook, - GitReferenceSchema, - K8sResourceSchema, -} from '@canton-network/splice-pulumi-common'; +import { config, DeploySvRunbook, K8sResourceSchema } from '@canton-network/splice-pulumi-common'; import { mustInstallSplitwell, mustInstallValidator1, @@ -63,10 +58,7 @@ function* iterateDefaultProjectFilters(): Generator { } } -export const OperatorDeploymentConfigSchema = z.object({ - operatorDeployment: z.object({ - reference: GitReferenceSchema, - }), +export const DeploymentConfigSchema = z.object({ pulumiStacks: z.record(z.string(), StackConfigSchema).and( z.object({ default: StackConfigSchema, @@ -81,13 +73,10 @@ export const OperatorDeploymentConfigSchema = z.object({ }), }); -export type Config = z.infer; +export type Config = z.infer; export type StackConfig = z.infer; -// eslint-disable-next-line -// @ts-ignore -const fullConfig = OperatorDeploymentConfigSchema.parse(clusterYamlConfig); -export const operatorDeploymentConfig = fullConfig.operatorDeployment; +const fullConfig = DeploymentConfigSchema.parse(clusterYamlConfig); export const deploymentConf = fullConfig.deployment; export const PulumiOperatorGracePeriod = 1800; diff --git a/cluster/pulumi/common/src/operator/flux-source.ts b/cluster/pulumi/common/src/operator/flux-source.ts index e4c2ce2ec1..bccce31d96 100644 --- a/cluster/pulumi/common/src/operator/flux-source.ts +++ b/cluster/pulumi/common/src/operator/flux-source.ts @@ -21,7 +21,7 @@ export type StackFromRef = { project: string; stack: string }; // Trim some files to avoid blowing the hardcoded operator size limit of 100mb const repoIgnore = - '**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard'; + '**/splice/canton\n!**/splice/canton/community/app/src/pack/examples/13-observability/grafana/dashboards\n**/daml/dars\n!**/daml/dars/splitwell*\n**/token-standard\n!**/token-standard/*/openapi/**'; function expandGitReference(gitReference: string): { name: string } | { commit: string } { if (gitReference.startsWith('refs/')) { diff --git a/cluster/pulumi/common/src/operator/stack.ts b/cluster/pulumi/common/src/operator/stack.ts index c32a386746..b6bd022e87 100644 --- a/cluster/pulumi/common/src/operator/stack.ts +++ b/cluster/pulumi/common/src/operator/stack.ts @@ -5,7 +5,7 @@ import * as pulumi from '@pulumi/pulumi'; import { CLUSTER_BASENAME, config, - infraAffinityAndTolerations, + infraKubernetesScheduling, isMainNet, } from '@canton-network/splice-pulumi-common'; import { CustomResource } from '@pulumi/kubernetes/apiextensions'; @@ -265,7 +265,7 @@ export function createStackCR( resources: stackConfig.resources, podTemplate: { spec: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, terminationGracePeriodSeconds: PulumiOperatorGracePeriod, volumes: [ { diff --git a/cluster/pulumi/common/src/postgres.ts b/cluster/pulumi/common/src/postgres.ts index 8e0b7ecd18..71df71bc62 100644 --- a/cluster/pulumi/common/src/postgres.ts +++ b/cluster/pulumi/common/src/postgres.ts @@ -1,25 +1,32 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 import * as gcp from '@pulumi/gcp'; +import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; import * as random from '@pulumi/random'; import * as _ from 'lodash'; import { Resource } from '@pulumi/pulumi'; import { CnChartVersion } from './artifacts'; -import { clusterSmallDisk, CloudSqlConfig, config } from './config'; +import { + clusterSmallDisk, + CloudSqlConfig, + config, + SplicePostgresConfig, + SplicePostgresMigrateConfig, + SplicePostgresDockerImageConfig, +} from './config'; import { spliceConfig } from './config/config'; import { GcpProject } from './config/gcpConfig'; -import { hyperdiskSupportConfig } from './config/hyperdiskSupportConfig'; import { - appsAffinityAndTolerations, - infraAffinityAndTolerations, + appsKubernetesScheduling, + infraKubernetesScheduling, installSpliceHelmChart, + SpliceCustomResourceOptions, } from './helm'; import { installPostgresPasswordSecret } from './secrets'; import { standardStorageClassName } from './storage/storageClass'; -import { createVolumeSnapshot } from './storage/volumeSnapshot'; -import { ChartValues, CLUSTER_BASENAME, ExactNamespace, GCP_ZONE } from './utils'; +import { CLUSTER_BASENAME, ExactNamespace, GCP_ZONE } from './utils'; const project = gcp.organizations.getProjectOutput({}); @@ -55,6 +62,7 @@ export interface Postgres extends pulumi.Resource { readonly databaseId?: pulumi.Output; readonly userName: string; + readonly database: Resource; addUser(userName: string): PostgresUser; } @@ -72,6 +80,7 @@ export class CloudPostgres user!: gcp.sql.User; userName!: string; zone!: string; + database!: Resource; private name!: string; private args!: CloudPostgresResolvedArgs; @@ -99,13 +108,7 @@ export class CloudPostgres defaultUserName, retainDbResourcesOnDelete = false, } = args; - const zoneFromEnv = config.optionalEnv('DB_CLOUDSDK_COMPUTE_ZONE') || GCP_ZONE; - if (!zoneFromEnv) { - throw new Error( - 'GCP_ZONE is not set in the environment, and DB_CLOUDSDK_COMPUTE_ZONE is also not set. One of these must be set to specify the zone for the Cloud SQL instance.' - ); - } - const zone = zoneFromEnv; + const zone = getCloudSdkZone(); const databaseInstanceImportOpts = existingInstanceName !== undefined @@ -145,6 +148,7 @@ export class CloudPostgres }, insightsConfig: { queryInsightsEnabled: true, + enhancedQueryInsightsEnabled: cloudSqlConfig.enterprisePlus, }, tier: cloudSqlConfig.tier, edition: cloudSqlConfig.enterprisePlus ? 'ENTERPRISE_PLUS' : 'ENTERPRISE', @@ -190,7 +194,7 @@ export class CloudPostgres ? await gcp.sql.getDatabase({ instance: existingInstanceName, name: 'cantonnet' }) : undefined; - new gcp.sql.Database( + const database = new gcp.sql.Database( `${namespace.logicalName}-db-${instanceName}-cantonnet`, { instance: databaseInstance.name, @@ -217,6 +221,7 @@ export class CloudPostgres this.user = defaultUser.sqlUser; this.userName = defaultUser.userName; this.zone = zone; + this.database = database; return { address: this.address, @@ -314,7 +319,7 @@ export class CloudPostgres active: args.active ?? true, deletionProtection: (args.disableProtection ?? false) ? false : args.cloudSqlConfig.protected, logicalDecoding: args.logicalDecoding ?? false, - defaultUserName: args.userName ?? 'cnadmin', + defaultUserName: args.userName ?? defaultUserName, retainDbResourcesOnDelete: args.retainDbResourcesOnDelete ?? false, }; super('canton:cloud:postgres', name, resolvedArgs, opts); @@ -369,30 +374,35 @@ type CloudPostgresOutput = { secretName: pulumi.Output; }; -export class SplicePostgres extends pulumi.ComponentResource implements Postgres { +/** + * Legacy Helm-backed postgres declaration kept for migration windows where the + * old splice-postgres release must stay declared to avoid Pulumi deleting it. + */ +export class LegacyHelmSplicePostgres extends pulumi.ComponentResource implements Postgres { instanceName: string; namespace: ExactNamespace; address: pulumi.Output; pg: Resource; secretName: pulumi.Output; userName: string; + database: Resource; constructor( xns: ExactNamespace, instanceName: string, - alias: string, - secretName: string, - values?: ChartValues, + getOrInstallPassword: (parent: Resource) => k8s.core.v1.Secret, + values?: LegacyChartValues, overrideDbSizeFromValues?: boolean, disableProtection?: boolean, version?: CnChartVersion, - useInfraAffinityAndTolerations: boolean = false + useinfraKubernetesScheduling: boolean = false, + resourceOpts?: SpliceCustomResourceOptions ) { const logicalName = xns.logicalName + '-' + instanceName; - const logicalNameAlias = xns.logicalName + '-' + alias; // pulumi name before #12391 super('canton:network:postgres', logicalName, [], { + ...resourceOpts, protect: disableProtection ? false : spliceConfig.pulumiProjectConfig.cloudSql.protected, - aliases: [{ name: logicalNameAlias, type: 'canton:network:postgres' }], + aliases: [], }); this.instanceName = instanceName; @@ -401,18 +411,11 @@ export class SplicePostgres extends pulumi.ComponentResource implements Postgres this.address = pulumi.output( `${this.instanceName}.${this.namespace.logicalName}.svc.cluster.local` ); - const password = generatePassword(`${logicalName}-passwd`, { - parent: this, - aliases: [{ name: `${logicalNameAlias}-passwd` }], - }).result; - const passwordSecret = installPostgresPasswordSecret(xns, password, secretName); + const passwordSecret = getOrInstallPassword(this); this.secretName = passwordSecret.metadata.name; // an initial database named cantonnet is created automatically (configured in the Helm chart). const smallDiskSize = clusterSmallDisk ? '240Gi' : undefined; - const supportsHyperdisk = useInfraAffinityAndTolerations - ? hyperdiskSupportConfig.hyperdiskSupport.enabledForInfra - : hyperdiskSupportConfig.hyperdiskSupport.enabled; const pg = installSpliceHelmChart( xns, @@ -423,12 +426,8 @@ export class SplicePostgres extends pulumi.ComponentResource implements Postgres volumeSize: overrideDbSizeFromValues ? values?.db?.volumeSize || smallDiskSize : smallDiskSize, - ...(supportsHyperdisk - ? { - volumeStorageClass: standardStorageClassName, - pvcTemplateName: 'pg-data-hd', - } - : {}), + volumeStorageClass: standardStorageClassName, + pvcTemplateName: 'pg-data-hd', }, persistence: { secretName: this.secretName, @@ -436,7 +435,7 @@ export class SplicePostgres extends pulumi.ComponentResource implements Postgres }), version, { - aliases: [{ name: logicalNameAlias, type: 'kubernetes:helm.sh/v3:Release' }], + aliases: [{ name: instanceName, type: 'kubernetes:helm.sh/v3:Release' }], dependsOn: [passwordSecret], ...(spliceConfig.pulumiProjectConfig.replacePostgresStatefulSetOnChanges ? { @@ -446,9 +445,10 @@ export class SplicePostgres extends pulumi.ComponentResource implements Postgres : {}), }, true, - useInfraAffinityAndTolerations ? infraAffinityAndTolerations : appsAffinityAndTolerations + useinfraKubernetesScheduling ? infraKubernetesScheduling : appsKubernetesScheduling ); this.pg = pg; + this.database = pg; this.registerOutputs({ address: pg.id.apply(() => `${instanceName}.${xns.logicalName}.svc.cluster.local`), @@ -464,59 +464,500 @@ export class SplicePostgres extends pulumi.ComponentResource implements Postgres } } +/** + * Configuration for migrating data from a pre-existing PostgreSQL instance + * (one previously deployed via the splice-postgres Helm chart) into a + * freshly-created StatefulSet volume. + * + * The migration runs once, inside an init container, only when PGDATA is + * empty (i.e. on the very first pod start against a blank PVC). It dumps + * all databases into a dedicated migration PVC that is mounted into + * `/docker-entrypoint-initdb.d` for one-time restore during postgres init. + */ +export interface PostgresMigrationSource { + host: string; + port?: number; + userName?: string; + pvcSize: string; + pvcName: string; +} + +type LegacyChartValues = Partial<{ + resources: k8s.types.input.core.v1.ResourceRequirements; + db: Partial<{ + volumeSize: string; + maxConnections: number; + volumeStorageClass: string; + pvcTemplateName: string; + maxWalSize: string; + dataSource: pulumi.Input; + }>; + appsAffinityAndTolerations: unknown; +}>; + +export class SplicePostgres extends pulumi.ComponentResource implements Postgres { + instanceName: string; + namespace: ExactNamespace; + address: pulumi.Output; + pg: Resource; + secretName: pulumi.Output; + userName: string; + database: Resource; + + constructor( + xns: ExactNamespace, + instanceName: string, + installPassword: (parent: Resource) => k8s.core.v1.Secret, + splicePostgresHelmMigrationConfig: + SplicePostgresMigrateConfig | SplicePostgresDockerImageConfig, + values?: LegacyChartValues, + overrideDbSizeFromValues?: boolean, + disableProtection?: boolean, + version?: CnChartVersion, + useinfraKubernetesScheduling: boolean = false, + resourceOpts?: SpliceCustomResourceOptions + ) { + // Avoiding collisions with the name in LegacyHelmSplicePostgres + const deployedInstanceName = `${instanceName}-helmless`; + const logicalName = xns.logicalName + '-' + deployedInstanceName; + super('canton:network:postgres', logicalName, [], { + ...resourceOpts, + protect: disableProtection ? false : spliceConfig.pulumiProjectConfig.cloudSql.protected, + aliases: [], + }); + + const passwordSecret = installPassword(this); + this.secretName = passwordSecret.metadata.name; + + let migrationSource: PostgresMigrationSource | undefined = undefined; + if (splicePostgresHelmMigrationConfig.deployment == 'migrate') { + new LegacyHelmSplicePostgres( + xns, + instanceName, + () => passwordSecret, // reuse the same secret + values, + overrideDbSizeFromValues, + disableProtection, + version, + useinfraKubernetesScheduling + ); + + migrationSource = { + host: `${instanceName}.${xns.logicalName}.svc.cluster.local`, + port: 5432, + userName: 'cnadmin', + pvcSize: splicePostgresHelmMigrationConfig.migrationVolumeSize, + pvcName: 'migration-data', + }; + } + + this.instanceName = deployedInstanceName; + this.namespace = xns; + const postgresUser: string = 'cnadmin'; + const postgresDb: string = 'cantonnet'; + this.userName = postgresUser; + this.address = pulumi.output( + `${this.instanceName}.${this.namespace.logicalName}.svc.cluster.local` + ); + + const smallDiskSize = clusterSmallDisk ? '240Gi' : undefined; + + const volumeSize = overrideDbSizeFromValues + ? values?.db?.volumeSize || smallDiskSize || '2800Gi' + : smallDiskSize || '2800Gi'; + const pvcTemplateName = 'pg-data-hd'; + const volumeStorageClass = standardStorageClassName; + const maxConnections: number = values?.db?.maxConnections ?? 300; + const maxWalSize: string = values?.db?.maxWalSize ?? '2GB'; + const imageName: string = splicePostgresHelmMigrationConfig.postgresImage; + const resources = _.merge( + { limits: { memory: '12Gi' }, requests: { cpu: '0.5', memory: '1Gi' } }, + values?.resources || {} + ); + const kubernetesScheduling = useinfraKubernetesScheduling + ? infraKubernetesScheduling + : appsKubernetesScheduling; + + // Optional init container that migrates data from a pre-existing postgres instance. + // It runs pg_dumpall against the source and writes migration.dump into a dedicated + // migration PVC that the main container mounts at /docker-entrypoint-initdb.d. + const initContainers: k8s.types.input.core.v1.Container[] = []; + // Extra volumeMounts added to the main postgres container + const migrationVolumeMounts: k8s.types.input.core.v1.VolumeMount[] = []; + const migrationVolumes: k8s.types.input.core.v1.Volume[] = []; + + if (migrationSource) { + const srcPort = String(migrationSource.port ?? 5432); + const srcUser = migrationSource.userName ?? postgresUser; + const migrationPvc = new k8s.core.v1.PersistentVolumeClaim( + `${logicalName}-migration-pvc`, + { + metadata: { + name: `${deployedInstanceName}-migration-pvc`, + namespace: xns.logicalName, + }, + spec: { + accessModes: ['ReadWriteOnce'], + resources: { requests: { storage: migrationSource.pvcSize ?? volumeSize } }, + storageClassName: volumeStorageClass, + volumeMode: 'Filesystem', + }, + }, + { parent: this, dependsOn: [xns.ns] } + ); + + // Shell script executed by the init container. + // Only runs when PGDATA is empty (first-ever pod start). The generated SQL + // file is persisted on a dedicated migration PVC and then consumed by the + // postgres entrypoint from /docker-entrypoint-initdb.d. + const migrationScript = [ + 'set -eou pipefail', + 'if [ -n "$(ls -A "$PGDATA" 2>/dev/null)" ]; then', + ' echo "PGDATA already contains data, skipping migration."', + ' exit 0', + 'fi', + 'echo "PGDATA is empty. Dumping all databases from $SOURCE_HOST:$SOURCE_PORT ..."', + 'pg_dumpall \\', + ' -h "$SOURCE_HOST" \\', + ' -p "$SOURCE_PORT" \\', + ' -U "$SOURCE_USER" \\', + ' --no-role-passwords \\', + ' -f /migration/migration.dump', + "cat > /migration/00-restore.sh << 'RESTORE_EOF'", + '#!/bin/bash', + 'set -euo pipefail', + '[ -f /docker-entrypoint-initdb.d/migration.dump ] || exit 0', + 'echo "Restoring all databases from migration.dump ..."', + 'psql --username "$POSTGRES_USER" --dbname postgres -f /docker-entrypoint-initdb.d/migration.dump', + 'echo "Restore complete."', + 'RESTORE_EOF', + 'chmod +x /migration/00-restore.sh', + 'echo "Migration dump ready at /migration/migration.dump"', + ].join('\n'); + + // Mount migration PVC into /docker-entrypoint-initdb.d so postgres restores it on first init. + migrationVolumes.push({ + name: migrationSource.pvcName, + persistentVolumeClaim: { claimName: migrationPvc.metadata.name }, + }); + migrationVolumeMounts.push({ + name: migrationSource.pvcName, + mountPath: '/docker-entrypoint-initdb.d', + }); + + initContainers.push({ + name: 'pg-migrate', + image: imageName, + imagePullPolicy: 'IfNotPresent', + securityContext: { + runAsNonRoot: true, + runAsUser: 999, + runAsGroup: 999, + allowPrivilegeEscalation: false, + privileged: false, + capabilities: { drop: ['ALL'] }, + }, + command: ['bash', '-c'], + args: [migrationScript], + env: [ + { name: 'PGDATA', value: '/var/lib/postgresql/data/pgdata' }, + { name: 'SOURCE_HOST', value: migrationSource.host }, + { name: 'SOURCE_PORT', value: srcPort }, + { name: 'SOURCE_USER', value: srcUser }, + { + name: 'PGPASSWORD', + valueFrom: { + secretKeyRef: { + name: passwordSecret.metadata.name, + key: 'postgresPassword', + }, + }, + }, + ], + volumeMounts: [ + // Mount PGDATA read-only – we only inspect it to decide whether to dump + { name: pvcTemplateName, mountPath: '/var/lib/postgresql/data', readOnly: true }, + { name: migrationSource.pvcName, mountPath: '/migration' }, + ], + }); + } + + // ConfigMap for non-secret environment variables + const configMap = new k8s.core.v1.ConfigMap( + `${logicalName}-configuration`, + { + metadata: { + name: `${deployedInstanceName}-configuration`, + namespace: xns.logicalName, + }, + data: { + PGDATA: '/var/lib/postgresql/data/pgdata', + POSTGRES_DB: postgresDb, + POSTGRES_USER: postgresUser, + POSTGRES_INITDB_ARGS: '--data-checksums', + }, + }, + { parent: this, dependsOn: [xns.ns] } + ); + + // StatefulSet using the official postgres image + const statefulSet = new k8s.apps.v1.StatefulSet( + logicalName, + { + metadata: { + name: deployedInstanceName, + namespace: xns.logicalName, + }, + spec: { + serviceName: deployedInstanceName, + replicas: 1, + selector: { matchLabels: { app: deployedInstanceName } }, + template: { + metadata: { + labels: { app: deployedInstanceName, namespace: xns.logicalName }, + }, + spec: { + securityContext: { + seccompProfile: { type: 'RuntimeDefault' }, + fsGroup: 999, + fsGroupChangePolicy: 'OnRootMismatch', + }, + ...(initContainers.length > 0 ? { initContainers } : {}), + containers: [ + { + name: deployedInstanceName, + image: imageName, + imagePullPolicy: 'IfNotPresent', + securityContext: { + runAsNonRoot: true, + runAsUser: 999, + runAsGroup: 999, + allowPrivilegeEscalation: false, + privileged: false, + capabilities: { drop: ['ALL'] }, + }, + args: [ + '-c', + `max_connections=${maxConnections}`, + '-c', + `max_wal_size=${maxWalSize}`, + ], + env: [ + { + name: 'POSTGRES_PASSWORD', + valueFrom: { + secretKeyRef: { + name: this.secretName, + key: 'postgresPassword', + }, + }, + }, + ], + envFrom: [{ configMapRef: { name: `${deployedInstanceName}-configuration` } }], + livenessProbe: { + exec: { + command: ['psql', '-U', postgresUser, '-d', 'template1', '-c', 'SELECT 1'], + }, + failureThreshold: 3, + periodSeconds: 10, + successThreshold: 1, + timeoutSeconds: 1, + }, + ports: [{ containerPort: 5432, name: 'postgresdb', protocol: 'TCP' }], + resources, + volumeMounts: [ + { mountPath: '/var/lib/postgresql/data', name: pvcTemplateName }, + ...migrationVolumeMounts, + ], + }, + ], + restartPolicy: 'Always', + ...kubernetesScheduling, + ...(migrationVolumes.length > 0 ? { volumes: migrationVolumes } : {}), + }, + }, + volumeClaimTemplates: [ + { + metadata: { name: pvcTemplateName }, + spec: { + accessModes: ['ReadWriteOnce'], + resources: { requests: { storage: volumeSize } }, + storageClassName: volumeStorageClass, + volumeMode: 'Filesystem', + ...(values?.db?.dataSource ? { dataSource: values.db.dataSource } : {}), + }, + }, + ], + }, + }, + { + parent: this, + dependsOn: [passwordSecret, configMap], + ...(spliceConfig.pulumiProjectConfig.replacePostgresStatefulSetOnChanges + ? { replaceOnChanges: ['*'], deleteBeforeReplace: true } + : {}), + } + ); + + // Headless service for the StatefulSet + new k8s.core.v1.Service( + `${logicalName}-svc`, + { + metadata: { + name: deployedInstanceName, + namespace: xns.logicalName, + }, + spec: { + ports: [{ name: 'postgresdb', port: 5432, protocol: 'TCP' }], + selector: { app: deployedInstanceName }, + }, + }, + { parent: this, dependsOn: [xns.ns] } + ); + + this.pg = statefulSet; + this.database = statefulSet; + + this.registerOutputs({ + address: statefulSet.id.apply( + () => `${deployedInstanceName}.${xns.logicalName}.svc.cluster.local` + ), + secretName: this.secretName, + }); + } + + addUser(_userName: string): PostgresUser { + return { + userName: 'cnadmin', + secretName: this.secretName, + }; + } +} + // toplevel +type SplicePostgresInstallOptions = { + isActive?: boolean; + migrationId?: number; + disableProtection?: boolean; + logicalDecoding?: boolean; + userName?: string; + existingInstanceName?: string; + existingSecretName?: string; + retainDbResourcesOnDelete?: boolean; +}; + export async function installPostgres( xns: ExactNamespace, instanceName: string, alias: string, version: CnChartVersion, cloudSqlConfig: CloudSqlConfig, + splicePostgresHelmMigrationConfig: SplicePostgresConfig, uniqueSecretName = false, - opts: { - isActive?: boolean; - migrationId?: number; - disableProtection?: boolean; - logicalDecoding?: boolean; - userName?: string; - existingInstanceName?: string; - existingSecretName?: string; - retainDbResourcesOnDelete?: boolean; - databaseVersion?: string; - } = {} + opts: SplicePostgresInstallOptions = {} ): Promise { const o = { isActive: true, ...opts }; const secretName = uniqueSecretName ? instanceName + '-secrets' : 'postgres-secrets'; - return cloudSqlConfig.enabled - ? await CloudPostgres.install( - `${xns.logicalName}-${instanceName}`, - { - active: o.isActive, - alias, - cloudSqlConfig, - disableProtection: o.disableProtection, - existingInstanceName: o.existingInstanceName, - existingSecretName: o.existingSecretName, - instanceName, - logicalDecoding: o.logicalDecoding, - migrationId: o.migrationId, - namespace: xns, - secretName, - userName: o.userName, - retainDbResourcesOnDelete: o.retainDbResourcesOnDelete, - }, - { - aliases: [{ name: `${xns.logicalName}-${alias}` }], - } - ) - : new SplicePostgres( - xns, - instanceName, + if (cloudSqlConfig.enabled) { + return await CloudPostgres.install( + `${xns.logicalName}-${instanceName}`, + { + active: o.isActive, alias, + cloudSqlConfig, + disableProtection: o.disableProtection, + existingInstanceName: o.existingInstanceName, + existingSecretName: o.existingSecretName, + instanceName, + logicalDecoding: o.logicalDecoding, + migrationId: o.migrationId, + namespace: xns, secretName, - undefined, - undefined, - undefined, - version - ); + userName: o.userName, + retainDbResourcesOnDelete: o.retainDbResourcesOnDelete, + }, + { + aliases: [{ name: `${xns.logicalName}-${alias}` }], + } + ); + } else { + return installSplicePostgres( + xns, + instanceName, + secretName, + splicePostgresHelmMigrationConfig, + version, + opts + ); + } } + +export function installSplicePostgres( + xns: ExactNamespace, + instanceName: string, + secretName: string, + splicePostgresHelmMigrationConfig: SplicePostgresConfig, + version?: CnChartVersion, + opts: SplicePostgresInstallOptions = {}, + chartValues?: LegacyChartValues, + overrideDbSizeFromValues?: boolean, + useinfraKubernetesScheduling: boolean = false, + resourceOpts?: SpliceCustomResourceOptions +): Postgres { + if (splicePostgresHelmMigrationConfig.deployment == 'legacy-helm-chart') { + return new LegacyHelmSplicePostgres( + xns, + instanceName, + parent => installPasswordWithParent(parent, xns, instanceName, secretName), + chartValues, + overrideDbSizeFromValues, + opts.disableProtection, + version, + useinfraKubernetesScheduling, + resourceOpts + ); + } else { + // If deployment == 'migrate', it will also create the LegacyHelmSplicePostgres + return new SplicePostgres( + xns, + instanceName, + parent => installPasswordWithParent(parent, xns, instanceName, secretName), + splicePostgresHelmMigrationConfig as + SplicePostgresMigrateConfig | SplicePostgresDockerImageConfig, + chartValues, + overrideDbSizeFromValues, + opts.disableProtection, + version, + useinfraKubernetesScheduling, + resourceOpts + ); + } +} + +export function installPasswordWithParent( + parent: Resource, + xns: ExactNamespace, + instanceName: string, + secretName: string +): k8s.core.v1.Secret { + // Password keeps the same historical name to avoid re-creating it unnecessarily + const password = generatePassword(`${xns.logicalName}-${instanceName}-passwd`, { + parent, + // same name, no parent, because multi-validators were creating their own without a parent + aliases: [{ parent: undefined, name: `${xns.logicalName}-${instanceName}-passwd` }], + }).result; + return installPostgresPasswordSecret(xns, password, secretName); +} + +export function getCloudSdkZone(): string { + const zoneFromEnv = config.optionalEnv('DB_CLOUDSDK_COMPUTE_ZONE') || GCP_ZONE; + if (!zoneFromEnv) { + throw new Error( + 'CLOUDSDK_COMPUTE_ZONE is not set in the environment, and DB_CLOUDSDK_COMPUTE_ZONE is also not set. One of these must be set to specify the zone for the Cloud SQL instance.' + ); + } + return zoneFromEnv; +} + +export const defaultUserName = 'cnadmin'; diff --git a/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.test.ts b/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.test.ts new file mode 100644 index 0000000000..c4d69f9f56 --- /dev/null +++ b/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.test.ts @@ -0,0 +1,342 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { expect, jest, test } from '@jest/globals'; + +import { + buildRateLimitActions, + buildRateLimitDescriptors, + parseFillIntervalMs, + validateIpLimits, + validateTokenBuckets, +} from './envoyRateLimiter'; + +jest.mock('@canton-network/splice-pulumi-common/src/config/envConfig', () => ({ + __esModule: true, + spliceEnvConfig: { + requireEnv() { + return 'dummy'; + }, + }, +})); + +const baseLimits = { + maxTokens: 720, + tokensPerFill: 720, + fillInterval: '60s', +}; + +const perIpLimits = { + maxTokens: 120, + tokensPerFill: 120, + fillInterval: '60s', +}; + +test('buildRateLimitDescriptors generates per-endpoint and generic per-IP descriptors', () => { + const descriptors = buildRateLimitDescriptors({ + '/registry/metadata/v1/info': { + name: 'registry-metadata-info', + type: 'limited', + ...baseLimits, + perIpLimits, + }, + }); + + expect(descriptors).toHaveLength(2); + expect(descriptors[0]).toEqual({ + entries: [{ key: 'header_match', value: 'registry-metadata-info' }], + token_bucket: { + max_tokens: 720, + tokens_per_fill: 720, + fill_interval: '60s', + }, + }); + expect(descriptors[1]).toEqual({ + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address' }, + ], + token_bucket: { + max_tokens: 120, + tokens_per_fill: 120, + fill_interval: '60s', + }, + }); +}); + +test('buildRateLimitDescriptors emits named IP overrides before generic per-IP descriptor', () => { + const descriptors = buildRateLimitDescriptors({ + '/registry/metadata/v1/info': { + name: 'registry-metadata-info', + type: 'limited', + ...baseLimits, + perIpLimits: { + ...perIpLimits, + overrides: { + 'single-validator': { + ips: ['192.68.78.50'], + maxTokens: 220, + tokensPerFill: 220, + fillInterval: '60s', + }, + }, + }, + }, + }); + + expect(descriptors).toHaveLength(3); + expect(descriptors[0]).toEqual( + expect.objectContaining({ + entries: [{ key: 'header_match', value: 'registry-metadata-info' }], + }) + ); + expect(descriptors[1]).toEqual({ + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address', value: '192.68.78.50/32' }, + ], + token_bucket: { + max_tokens: 220, + tokens_per_fill: 220, + fill_interval: '60s', + }, + }); + expect(descriptors[2]).toEqual( + expect.objectContaining({ + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address' }, + ], + }) + ); +}); + +test('buildRateLimitDescriptors emits descriptors for named overrides with multiple ips', () => { + const descriptors = buildRateLimitDescriptors({ + '/registry/metadata/v1/info': { + name: 'registry-metadata-info', + type: 'limited', + ...baseLimits, + perIpLimits: { + ...perIpLimits, + overrides: { + 'multi-validators': { + ips: ['192.68.78.51', '192.68.78.52'], + maxTokens: 250, + tokensPerFill: 250, + fillInterval: '60s', + }, + }, + }, + }, + }); + + expect(descriptors).toHaveLength(4); + expect(descriptors[1]).toEqual({ + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address', value: '192.68.78.51/32' }, + ], + token_bucket: { + max_tokens: 250, + tokens_per_fill: 250, + fill_interval: '60s', + }, + }); + expect(descriptors[2]).toEqual({ + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address', value: '192.68.78.52/32' }, + ], + token_bucket: { + max_tokens: 250, + tokens_per_fill: 250, + fill_interval: '60s', + }, + }); +}); + +test('buildRateLimitActions emits per-endpoint and per-IP actions', () => { + const actions = buildRateLimitActions({ + '/registry/metadata/v1/info': { + name: 'registry-metadata-info', + type: 'limited', + ...baseLimits, + perIpLimits, + }, + }); + + expect(actions).toHaveLength(2); + expect(actions[0]).toEqual({ + actions: [ + { + header_value_match: { + descriptor_value: 'registry-metadata-info', + expect_match: true, + headers: [ + { + name: ':path', + string_match: { + prefix: '/registry/metadata/v1/info', + ignore_case: true, + }, + }, + ], + }, + }, + ], + }); + expect(actions[1]).toEqual({ + actions: [ + { + header_value_match: { + descriptor_value: 'registry-metadata-info', + expect_match: true, + headers: [ + { + name: ':path', + string_match: { + prefix: '/registry/metadata/v1/info', + ignore_case: true, + }, + }, + ], + }, + }, + { + // the raw x-forwarded-for header must not be used, it is attacker controlled + masked_remote_address: { + v4_prefix_mask_len: 32, + v6_prefix_mask_len: 128, + }, + }, + ], + }); +}); + +test('validateIpLimits throws on duplicate IP between two named overrides', () => { + expect(() => + validateIpLimits('/registry/metadata/v1/info', { + name: 'registry-metadata-info', + type: 'limited', + ...baseLimits, + perIpLimits: { + ...perIpLimits, + overrides: { + 'group-a': { + ips: ['192.68.78.50', '192.68.78.51'], + maxTokens: 250, + tokensPerFill: 250, + fillInterval: '60s', + }, + 'group-b': { + ips: ['192.68.78.51'], + maxTokens: 250, + tokensPerFill: 250, + fillInterval: '60s', + }, + }, + }, + }) + ).toThrow("192.68.78.51 (in override 'group-b')"); +}); + +test('validateIpLimits accepts unique IPs across named overrides', () => { + expect(() => + validateIpLimits('/registry/metadata/v1/info', { + name: 'registry-metadata-info', + type: 'limited', + ...baseLimits, + perIpLimits: { + ...perIpLimits, + overrides: { + 'single-validator': { + ips: ['192.68.78.50'], + maxTokens: 220, + tokensPerFill: 220, + fillInterval: '60s', + }, + 'multi-validators': { + ips: ['192.68.78.51', '192.68.78.52'], + maxTokens: 250, + tokensPerFill: 250, + fillInterval: '60s', + }, + }, + }, + }) + ).not.toThrow(); +}); + +test('parseFillIntervalMs parses protobuf durations and rejects other formats', () => { + expect(parseFillIntervalMs('60s', 'ctx')).toEqual(60000); + expect(parseFillIntervalMs('0.5s', 'ctx')).toEqual(500); + expect(() => parseFillIntervalMs('500ms', 'ctx')).toThrow('invalid fillInterval'); + expect(() => parseFillIntervalMs('1m', 'ctx')).toThrow('invalid fillInterval'); +}); + +test('validateTokenBuckets accepts intervals that are multiples of the global interval', () => { + expect(() => + validateTokenBuckets(baseLimits, { + '/api/scan/v0/acs': { + name: 'acs', + type: 'limited', + maxTokens: 500, + tokensPerFill: 500, + fillInterval: '120s', + perIpLimits, + }, + }) + ).not.toThrow(); +}); + +test('validateTokenBuckets rejects intervals that envoy would NACK', () => { + expect(() => + validateTokenBuckets(baseLimits, { + '/api/scan/v0/acs': { + name: 'acs', + type: 'limited', + maxTokens: 500, + tokensPerFill: 500, + fillInterval: '90s', + }, + }) + ).toThrow('must be a multiple of the globalLimits fillInterval'); + + // below envoy's 50ms minimum + expect(() => + validateTokenBuckets( + { maxTokens: 1, tokensPerFill: 1, fillInterval: '0.01s' }, + { + '/api/scan/v0/acs': { + name: 'acs', + type: 'limited', + maxTokens: 500, + tokensPerFill: 500, + fillInterval: '60s', + }, + } + ) + ).toThrow('below the 50ms minimum'); + + // per-IP overrides are validated as well + expect(() => + validateTokenBuckets(baseLimits, { + '/api/scan/v0/acs': { + name: 'acs', + type: 'limited', + ...baseLimits, + perIpLimits: { + ...perIpLimits, + overrides: { + 'single-validator': { + ips: ['192.68.78.50'], + maxTokens: 220, + tokensPerFill: 220, + fillInterval: '90s', + }, + }, + }, + }, + }) + ).toThrow("perIpLimits override 'single-validator'"); +}); diff --git a/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.ts b/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.ts index 7a47f69d59..8844d0305b 100644 --- a/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.ts +++ b/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.ts @@ -3,7 +3,8 @@ import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; -import { parseScanYamlEndpoints } from '../config/scanEndpoints'; +import { parseScanYamlEndpoints, parseTokenRegistrySpecEndpoints } from '../config/scanEndpoints'; +import { localRateLimitedHeader } from './rateLimitHeaders'; interface Limits { maxTokens: number; @@ -11,9 +12,13 @@ interface Limits { fillInterval: string; } +interface PerIpLimits extends Limits { + overrides?: Record; +} + interface MatchedLimits extends Limits { type: 'limited'; - clientIp: boolean; + perIpLimits?: PerIpLimits; } interface Banned { @@ -39,10 +44,23 @@ type LocalLimit = { name: string; } & L; -// This is arbitrary, but must not match any limit `name` used for an EnvoyFilter -// above. All existing manual YAML entries use 'client_ip' so this is the nicest -// migration away from always specifying that. -const clientIpEntryKey = 'client_ip'; +// The descriptor entry key emitted by envoy's `masked_remote_address` rate limit action. +// This is hardcoded in envoy (see MaskedRemoteAddressAction::populateDescriptor) and +// cannot be configured, so the descriptors we generate must use exactly this key. +const clientIpEntryKey = 'masked_remote_address'; +const reservedEntryKeys = [clientIpEntryKey, 'client_ip']; + +// The per-IP descriptors are wildcard descriptors: envoy allocates a token bucket per +// observed client IP and keeps them in an LRU cache whose size defaults to a mere 20 +// entries. +export const maxDynamicDescriptorsPerLimit = 10000; + +// Envoy rejects token buckets refilling faster than this, and requires every descriptor +// fill interval to be a multiple of the global (default) bucket's fill interval. +const minFillIntervalMs = 50; + +// uint32 in envoy's TokenBucket proto +const maxTokenValue = 4294967295; interface RateLimitEnvoyFilterArgs extends PerEndpointLimits { namespace: string; @@ -74,7 +92,9 @@ export function extractPathPrefixes( const isBanned = rl.type === 'banned'; return { pathPrefix, isBanned }; }) - .filter(info => info.pathPrefix.startsWith('/api/scan')); + .filter( + info => info.pathPrefix.startsWith('/api/scan') || info.pathPrefix.startsWith('/registry') + ); } function validateEndpointCoverage( @@ -94,15 +114,99 @@ function validateEndpointCoverage( return { missing, orphaned }; } +export function validateIpLimits(pathPrefix: string, rateLimit: LocalLimit): void { + if (!rateLimit.perIpLimits) { + return; + } + + const seenIps = new Set(); + const duplicates: string[] = []; + + Object.entries(rateLimit.perIpLimits.overrides || {}).forEach(([overrideKey, override]) => { + override.ips.forEach(ip => { + if (seenIps.has(ip)) { + duplicates.push(`${ip} (in override '${overrideKey}')`); + } else { + seenIps.add(ip); + } + }); + }); + + if (duplicates.length > 0) { + throw new Error(`${pathPrefix}: duplicate IPs in per-IP rate limits: ${duplicates.join(', ')}`); + } +} + +/** + * Parses a protobuf duration (as accepted by envoy's `fill_interval`) into milliseconds. + */ +export function parseFillIntervalMs(fillInterval: string, context: string): number { + const match = /^(\d+(?:\.\d+)?)s$/.exec(fillInterval); + if (!match) { + throw new Error( + `${context}: invalid fillInterval '${fillInterval}', expected a duration in seconds such as '60s'` + ); + } + return Math.round(parseFloat(match[1]) * 1000); +} + +function validateLimits(context: string, limits: Limits, globalFillIntervalMs?: number): void { + const fillIntervalMs = parseFillIntervalMs(limits.fillInterval, context); + // envoy rejects fill intervals below 50ms, see the local rate limit filter docs. + // https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto#envoy-v3-api-field-extensions-filters-http-local-ratelimit-v3-localratelimit-token-bucket + if (fillIntervalMs < minFillIntervalMs) { + throw new Error( + `${context}: fillInterval '${limits.fillInterval}' is below the ${minFillIntervalMs}ms minimum enforced by envoy` + ); + } + // envoy requires descriptor fill intervals to be a multiple of the default bucket's + // fill interval; violating this makes istiod push a config that envoy NACKs, which + // silently leaves the sidecar running without any rate limits at all. + // https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto#envoy-v3-api-field-extensions-filters-http-local-ratelimit-v3-localratelimit-descriptors + if (globalFillIntervalMs !== undefined && fillIntervalMs % globalFillIntervalMs !== 0) { + throw new Error( + `${context}: fillInterval '${limits.fillInterval}' must be a multiple of the globalLimits fillInterval (${globalFillIntervalMs}ms)` + ); + } + [['maxTokens', limits.maxTokens] as const, ['tokensPerFill', limits.tokensPerFill] as const] + .filter(([, value]) => !Number.isInteger(value) || value < 0 || value > maxTokenValue) + .forEach(([field, value]) => { + throw new Error( + `${context}: ${field} must be an integer in [0, ${maxTokenValue}], got ${value}` + ); + }); +} + +export function validateTokenBuckets( + globalLimits: Limits, + effectiveRateLimits: LocalLimits +): void { + validateLimits('globalLimits', globalLimits); + const globalFillIntervalMs = parseFillIntervalMs(globalLimits.fillInterval, 'globalLimits'); + Object.entries(effectiveRateLimits).forEach(([pathPrefix, rateLimit]) => { + validateLimits(pathPrefix, rateLimit, globalFillIntervalMs); + if (rateLimit.perIpLimits) { + validateLimits(`${pathPrefix} perIpLimits`, rateLimit.perIpLimits, globalFillIntervalMs); + Object.entries(rateLimit.perIpLimits.overrides || {}).forEach(([name, override]) => + validateLimits( + `${pathPrefix} perIpLimits override '${name}'`, + override, + globalFillIntervalMs + ) + ); + } + }); +} + function validateEffectiveRateLimits( args: RateLimitEnvoyFilterArgs ): LocalLimits | undefined { const collidingPathNames = Object.entries(args.rateLimits || {}) - .filter(([, rl]) => rl.name === clientIpEntryKey) + .filter(([, rl]) => reservedEntryKeys.includes(rl.name)) .map(([path]) => path); if (collidingPathNames.length > 0) { throw new Error( - `${collidingPathNames.join(', ')} use reserved name ${clientIpEntryKey}; choose a different name` + `${collidingPathNames.join(', ')} use reserved name ${reservedEntryKeys.join('/')}; choose a different name` ); } @@ -115,23 +219,38 @@ function validateEffectiveRateLimits( const { missing, orphaned } = validateEndpointCoverage(scanEndpoints, configuredScanPrefixes); - if (missing.length > 0 || orphaned.length > 0) { + const tokenRegistryEndpoints = parseTokenRegistrySpecEndpoints(); + + const configuredRegistryPrefixes = Object.keys(args.rateLimits || {}).filter(pathPrefix => + pathPrefix.startsWith('/registry') + ); + + const registryValidation = validateEndpointCoverage( + tokenRegistryEndpoints, + configuredRegistryPrefixes + ); + + const totalMissing = missing.concat(registryValidation.missing); + const totalOrphaned = orphaned.concat(registryValidation.orphaned); + + if (totalMissing.length > 0 || totalOrphaned.length > 0) { const errorParts: string[] = ['Rate limit configuration errors:']; - if (missing.length > 0) { + if (totalMissing.length > 0) { + errorParts.push(`- Missing rate limit prefixes for endpoints: ${totalMissing.join(', ')}`); errorParts.push( - `- Missing rate limit prefixes for scan.yaml endpoints: ${missing.join(', ')}` + "If you're adding new endpoints in a Splice PR, add them to cluster/configs/shared/rate-limits." ); } - if (orphaned.length > 0) { + if (totalOrphaned.length > 0) { errorParts.push( - `- Orphaned rate limit prefixes not matching any scan.yaml endpoint: ${orphaned.join(', ')}` + `- Orphaned rate limit prefixes not matching any schema route: ${totalOrphaned.join(', ')}` ); } throw new Error(errorParts.join('\n')); } // Filter out banned and unlimited entries - return Object.fromEntries( + const effectiveRateLimits = Object.fromEntries( Object.entries(args.rateLimits || {}).filter( (ent): ent is [string, LocalLimit] => { // TODO (#4201): in banned case, implement actual banning with special short-circuit for whitelisted IPs @@ -142,6 +261,119 @@ function validateEffectiveRateLimits( } ) ); + + Object.entries(effectiveRateLimits).forEach(([pathPrefix, rateLimit]) => { + validateIpLimits(pathPrefix, rateLimit); + }); + + validateTokenBuckets(args.globalLimits, effectiveRateLimits); + + return effectiveRateLimits; +} + +export function clientIpDescriptorValue(ip: string): string { + return `${ip}/32`; +} + +export function buildRateLimitActions(effectiveRateLimits: LocalLimits): unknown[] { + return Object.entries(effectiveRateLimits).flatMap(([pathPrefix, rateLimit]) => { + const actions = []; + + // Action 1: generate the per-endpoint action + const baseAction = { + header_value_match: { + descriptor_value: rateLimit.name, + expect_match: true, + headers: [ + { + name: ':path', + string_match: { + prefix: pathPrefix, + ignore_case: true, + }, + }, + ], + }, + }; + + actions.push({ actions: [baseAction] }); + + // Action 2: generate the per-IP action if perIpLimits exists + if (rateLimit.perIpLimits) { + actions.push({ + actions: [ + baseAction, + { + // We deliberately do not key on the raw x-forwarded-for header: clients can + // prepend arbitrary entries to it, and our gateway only appends to it, so + // the header value is attacker controlled and per-IP limits could be evaded + // by simply varying the header on every request. + // masked_remote_address instead uses the address envoy trusts, which for the + // sidecar is the last x-forwarded-for hop, i.e. the one appended by our own + // ingress gateway. + masked_remote_address: { + // one bucket per client address + v4_prefix_mask_len: 32, + // the default of 0 would put all IPv6 clients into a single bucket + v6_prefix_mask_len: 128, + }, + }, + ], + }); + } + + return actions; + }); +} + +export function buildRateLimitDescriptors( + effectiveRateLimits: LocalLimits +): unknown[] { + return Object.values(effectiveRateLimits).flatMap(rateLimit => { + const descs = []; + + // per-endpoint bucket + descs.push({ + entries: [{ key: 'header_match', value: rateLimit.name }], + token_bucket: { + max_tokens: rateLimit.maxTokens, + tokens_per_fill: rateLimit.tokensPerFill, + fill_interval: rateLimit.fillInterval, + }, + }); + + // generate the per-IP buckets if configured + if (rateLimit.perIpLimits) { + // IP-specific overrides first, so they take precedence over the generic per-IP bucket + Object.entries(rateLimit.perIpLimits.overrides || {}).forEach(([, override]) => { + override.ips.forEach(ip => { + descs.push({ + entries: [ + { key: 'header_match', value: rateLimit.name }, + { key: clientIpEntryKey, value: clientIpDescriptorValue(ip) }, + ], + token_bucket: { + max_tokens: override.maxTokens, + tokens_per_fill: override.tokensPerFill, + fill_interval: override.fillInterval, + }, + }); + }); + }); + + // Generic per-IP fallback last + descs.push({ + entries: [{ key: 'header_match', value: rateLimit.name }, { key: clientIpEntryKey }], + token_bucket: { + max_tokens: rateLimit.perIpLimits.maxTokens, + tokens_per_fill: rateLimit.perIpLimits.tokensPerFill, + fill_interval: rateLimit.perIpLimits.fillInterval, + }, + }); + } + + return descs; + }); } export class RateLimitEnvoyFilter extends pulumi.ComponentResource { @@ -155,44 +387,7 @@ export class RateLimitEnvoyFilter extends pulumi.ComponentResource { super('splice:RateLimit', `splice-${args.namespace}-${name}`, args, opts); const effectiveRateLimits = validateEffectiveRateLimits(args); - const rateLimitActions: unknown[] = - Object.entries(effectiveRateLimits || {}).map(([pathPrefix, rateLimit]) => { - return { - actions: [ - { - header_value_match: { - descriptor_value: rateLimit.name, - expect_match: true, - headers: [ - { - name: ':path', - string_match: { - prefix: pathPrefix, - ignore_case: true, - }, - }, - ], - }, - }, - ...(rateLimit.clientIp - ? [ - { - request_headers: { - descriptor_key: 'client_ip', - header_name: 'x-forwarded-for', - }, - }, - ] - : []), - ], - }; - }) || []; - - const enableEnvoyRateLimitMetricsAnnotation = ` -proxyStatsMatcher: - inclusionRegexps: - - ".*http_local_rate_limit.*" -`.trim(); + const rateLimitActions = buildRateLimitActions(effectiveRateLimits || {}); this.envoyFilter = new k8s.apiextensions.CustomResource( `${args.namespace}-${name}`, @@ -202,9 +397,6 @@ proxyStatsMatcher: metadata: { name: name, namespace: args.namespace, - annotations: { - 'proxy.istio.io/config': enableEnvoyRateLimitMetricsAnnotation, - }, }, spec: { workloadSelector: { @@ -287,30 +479,20 @@ proxyStatsMatcher: { append_action: 'OVERWRITE_IF_EXISTS_OR_ADD', header: { - key: 'x-local-rate-limit', + key: localRateLimitedHeader, value: 'true', }, }, ], + // Emit X-RateLimit-Limit/Remaining/Reset. + // used in sidecar access logs + // not sent to the client, because we strip them on the ingress gateway + enable_x_ratelimit_headers: 'DRAFT_VERSION_03', + max_dynamic_descriptors: maxDynamicDescriptorsPerLimit, // simplified descriptors by combining with actions and requiring all the tokens of an action to be set // a descriptor in practice is a subset of tags from a rate limit // but important to note that for each rate limit only one descriptor can match, if multiple descriptors match, the first one is used - descriptors: Object.values(effectiveRateLimits || {}).map(rateLimit => { - return { - entries: [ - { - key: 'header_match', - value: rateLimit.name, - }, - ...(rateLimit.clientIp ? [{ key: clientIpEntryKey }] : []), - ], - token_bucket: { - max_tokens: rateLimit.maxTokens, - tokens_per_fill: rateLimit.tokensPerFill, - fill_interval: rateLimit.fillInterval, - }, - }; - }), + descriptors: buildRateLimitDescriptors(effectiveRateLimits || {}), }, }, }, diff --git a/cluster/pulumi/common/src/ratelimit/index.ts b/cluster/pulumi/common/src/ratelimit/index.ts index f5fa1d1cb3..fe638079e0 100644 --- a/cluster/pulumi/common/src/ratelimit/index.ts +++ b/cluster/pulumi/common/src/ratelimit/index.ts @@ -1,4 +1,5 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 export * from './rateLimitSchema'; +export * from './rateLimitHeaders'; export * from './envoyRateLimiter'; diff --git a/cluster/pulumi/common/src/ratelimit/rateLimit.ts b/cluster/pulumi/common/src/ratelimit/rateLimit.ts index dc14115f71..fe1e9f2f57 100644 --- a/cluster/pulumi/common/src/ratelimit/rateLimit.ts +++ b/cluster/pulumi/common/src/ratelimit/rateLimit.ts @@ -1,8 +1,41 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import * as k8s from '@pulumi/kubernetes'; + import { RateLimitEnvoyFilter } from './envoyRateLimiter'; import { ExternalRateLimit } from './rateLimitSchema'; +/** + * Makes the sidecar log every rate limited request, independently of whether access + * logging is enabled cluster wide. + */ +function logRateLimitedRequests(namespace: string, app: string): k8s.apiextensions.CustomResource { + return new k8s.apiextensions.CustomResource(`${namespace}-${app}-rate-limit-access-log`, { + apiVersion: 'telemetry.istio.io/v1', + kind: 'Telemetry', + metadata: { + name: `${app}-rate-limit-access-log`, + namespace, + }, + spec: { + selector: { + matchLabels: { + app, + }, + }, + accessLogging: [ + { + // the default envoy provider, which uses the mesh-wide accessLogFormat + providers: [{ name: 'envoy' }], + // Rate limited requests are recognizable in the log by + // response_code_details=local_rate_limited and response_flags containing RL. + filter: { expression: 'response.code == 429' }, + }, + ], + }, + }); +} + export function installRateLimits( namespace: string, app: string, @@ -16,4 +49,5 @@ export function installRateLimits( globalLimits: rateLimit.globalLimits, rateLimits: rateLimit.rateLimits, }); + logRateLimitedRequests(namespace, app); } diff --git a/cluster/pulumi/common/src/ratelimit/rateLimitHeaders.ts b/cluster/pulumi/common/src/ratelimit/rateLimitHeaders.ts new file mode 100644 index 0000000000..1c4135982c --- /dev/null +++ b/cluster/pulumi/common/src/ratelimit/rateLimitHeaders.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const localRateLimitedHeader = 'x-local-rate-limit'; + +export const rateLimitResponseHeaders = [ + localRateLimitedHeader, + // draft RFC headers enabled via enable_x_ratelimit_headers + 'x-ratelimit-limit', + 'x-ratelimit-remaining', + 'x-ratelimit-reset', + // added by envoy itself on the local reply it generates when rate limiting + 'x-envoy-ratelimited', +]; + +export const envoyExternalAddressHeader = 'x-envoy-external-address'; + +/** + * Overrides the (client-controlled) headers the apps extract the client IP for their per-client-IP + * rate limiting from, so that only the non-spoofable header set by the Envoy sidecar is used. + */ +export function envoyClientIpHeaderEnvVar(appConfigPath: string): { + name: string; + value: string; +} { + return { + name: 'ADDITIONAL_CONFIG_CLIENT_IP_HEADERS', + value: `${appConfigPath}.parameters.rate-limiting.client-ip-headers = ["${envoyExternalAddressHeader}"]\n`, + }; +} diff --git a/cluster/pulumi/common/src/ratelimit/rateLimitSchema.test.ts b/cluster/pulumi/common/src/ratelimit/rateLimitSchema.test.ts new file mode 100644 index 0000000000..2ca5ae6fdb --- /dev/null +++ b/cluster/pulumi/common/src/ratelimit/rateLimitSchema.test.ts @@ -0,0 +1,128 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { expect, test } from '@jest/globals'; + +import { RateLimitSchema } from './rateLimitSchema'; + +const validConfig = { + globalLimits: { + maxTokens: 1000, + tokensPerFill: 1000, + fillInterval: '60s', + }, + rateLimits: { + '/registry/metadata/v1/info': { + name: 'registry-metadata-info', + type: 'limited', + maxTokens: 720, + tokensPerFill: 720, + fillInterval: '60s', + perIpLimits: { + maxTokens: 120, + tokensPerFill: 120, + fillInterval: '60s', + }, + }, + }, +}; + +test('RateLimitSchema accepts config without overrides', () => { + expect(() => RateLimitSchema.parse(validConfig)).not.toThrow(); +}); + +test('RateLimitSchema accepts named overrides with ips', () => { + const config = { + ...validConfig, + rateLimits: { + '/registry/metadata/v1/info': { + ...validConfig.rateLimits['/registry/metadata/v1/info'], + perIpLimits: { + ...validConfig.rateLimits['/registry/metadata/v1/info'].perIpLimits, + overrides: { + 'single-validator': { + ips: ['192.68.78.50'], + maxTokens: 220, + tokensPerFill: 220, + fillInterval: '60s', + }, + 'multi-validators': { + ips: ['192.68.78.51', '192.68.78.52'], + maxTokens: 250, + tokensPerFill: 250, + fillInterval: '60s', + }, + }, + }, + }, + }, + }; + expect(() => RateLimitSchema.parse(config)).not.toThrow(); +}); + +test('RateLimitSchema rejects override without ips', () => { + const config = { + ...validConfig, + rateLimits: { + '/registry/metadata/v1/info': { + ...validConfig.rateLimits['/registry/metadata/v1/info'], + perIpLimits: { + ...validConfig.rateLimits['/registry/metadata/v1/info'].perIpLimits, + overrides: { + '192.68.78.50': { + maxTokens: 220, + tokensPerFill: 220, + fillInterval: '60s', + }, + }, + }, + }, + }, + }; + expect(() => RateLimitSchema.parse(config)).toThrow(); +}); + +test('RateLimitSchema rejects non-IPv4 addresses in ips', () => { + const config = { + ...validConfig, + rateLimits: { + '/registry/metadata/v1/info': { + ...validConfig.rateLimits['/registry/metadata/v1/info'], + perIpLimits: { + ...validConfig.rateLimits['/registry/metadata/v1/info'].perIpLimits, + overrides: { + 'multi-validators': { + ips: ['2001:db8::1'], + maxTokens: 250, + tokensPerFill: 250, + fillInterval: '60s', + }, + }, + }, + }, + }, + }; + expect(() => RateLimitSchema.parse(config)).toThrow(); +}); + +test('RateLimitSchema rejects empty override ips array', () => { + const config = { + ...validConfig, + rateLimits: { + '/registry/metadata/v1/info': { + ...validConfig.rateLimits['/registry/metadata/v1/info'], + perIpLimits: { + ...validConfig.rateLimits['/registry/metadata/v1/info'].perIpLimits, + overrides: { + 'empty-group': { + ips: [], + maxTokens: 250, + tokensPerFill: 250, + fillInterval: '60s', + }, + }, + }, + }, + }, + }; + expect(() => RateLimitSchema.parse(config)).toThrow(); +}); diff --git a/cluster/pulumi/common/src/ratelimit/rateLimitSchema.ts b/cluster/pulumi/common/src/ratelimit/rateLimitSchema.ts index 8fe8ea3514..a08c3e78d3 100644 --- a/cluster/pulumi/common/src/ratelimit/rateLimitSchema.ts +++ b/cluster/pulumi/common/src/ratelimit/rateLimitSchema.ts @@ -1,5 +1,6 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { isIP } from 'net'; import { z } from 'zod'; export const BucketRateLimitSchema = z.object({ @@ -8,9 +9,21 @@ export const BucketRateLimitSchema = z.object({ fillInterval: z.string(), }); +const Ipv4AddressSchema = z.string().refine(ip => isIP(ip) === 4, { + message: 'Expected IPv4 address', +}); + +const OverrideSchema = BucketRateLimitSchema.extend({ + ips: z.array(Ipv4AddressSchema).min(1), +}); + +export const PerIpLimitsSchema = BucketRateLimitSchema.extend({ + overrides: z.record(z.string().min(1), OverrideSchema).optional(), +}); + const BucketMatchedRateLimitSchema = BucketRateLimitSchema.extend({ type: z.literal('limited'), - clientIp: z.boolean(), + perIpLimits: PerIpLimitsSchema.optional(), }); export const BannedSchema = z.object({ diff --git a/cluster/pulumi/common/src/serviceAccount.ts b/cluster/pulumi/common/src/serviceAccount.ts index 62803263c8..d23c386728 100644 --- a/cluster/pulumi/common/src/serviceAccount.ts +++ b/cluster/pulumi/common/src/serviceAccount.ts @@ -4,8 +4,7 @@ import * as gcp from '@pulumi/gcp'; import * as pulumi from '@pulumi/pulumi'; type Role = - | string - | { id: string; condition: { title: string; description: string; expression: string } }; + string | { id: string; condition: { title: string; description: string; expression: string } }; const roleToPulumiName = (role: Role): string => { if (typeof role === 'string') { diff --git a/cluster/pulumi/common/src/stackReferences.ts b/cluster/pulumi/common/src/stackReferences.ts index ca1d52d77b..ec8bf767d1 100644 --- a/cluster/pulumi/common/src/stackReferences.ts +++ b/cluster/pulumi/common/src/stackReferences.ts @@ -4,12 +4,26 @@ import * as pulumi from '@pulumi/pulumi'; import { CLUSTER_BASENAME } from './utils'; -// Reference to upstream infrastructure stack. -export const infraStack = new pulumi.StackReference(`organization/infra/infra.${CLUSTER_BASENAME}`); - export class StackReferences { private static refCache: Partial> = {}; + // Reference to upstream infrastructure stack. + public static get infra(): pulumi.StackReference { + const projectName = 'infra'; + const stackName = `${projectName}.${CLUSTER_BASENAME}`; + return (StackReferences.refCache[stackName] ??= new pulumi.StackReference( + `organization/${projectName}/${stackName}` + )); + } + + public static get cantonNetwork(): pulumi.StackReference { + const projectName = 'canton-network'; + const stackName = `${projectName}.${CLUSTER_BASENAME}`; + return (StackReferences.refCache[stackName] ??= new pulumi.StackReference( + `organization/${projectName}/${stackName}` + )); + } + public static svCanton(sv: string, migrationId: number): pulumi.StackReference { const projectName = 'sv-canton'; const stackName = `${projectName}.${sv}-migration-${migrationId}.${CLUSTER_BASENAME}`; diff --git a/cluster/pulumi/common/src/storage/storageClass.ts b/cluster/pulumi/common/src/storage/storageClass.ts index 86d55d46d4..28107308f0 100644 --- a/cluster/pulumi/common/src/storage/storageClass.ts +++ b/cluster/pulumi/common/src/storage/storageClass.ts @@ -1,11 +1,8 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 import { spliceConfig } from '../config/config'; -import { hyperdiskSupportConfig } from '../config/hyperdiskSupportConfig'; -export const standardStorageClassName = hyperdiskSupportConfig.hyperdiskSupport.enabled - ? 'hyperdisk-standard-rwo' - : 'standard-rwo'; +export const standardStorageClassName = 'hyperdisk-standard-rwo'; export function persistentHeapDumpsPvc(): { size: string; volumeStorageClass: string } | undefined { return spliceConfig.configuration.persistentHeapDumps @@ -13,11 +10,7 @@ export function persistentHeapDumpsPvc(): { size: string; volumeStorageClass: st : undefined; } -export const infraStandardStorageClassName = hyperdiskSupportConfig.hyperdiskSupport.enabledForInfra - ? 'hyperdisk-standard-rwo' - : 'standard-rwo'; +export const infraStandardStorageClassName = 'hyperdisk-standard-rwo'; -export const infraPremiumStorageClassName = hyperdiskSupportConfig.hyperdiskSupport.enabledForInfra - ? 'hyperdisk-balanced-rwo' - : 'premium-rwo'; -export const pvcSuffix = hyperdiskSupportConfig.hyperdiskSupport.enabled ? 'hd-pvc' : 'pvc'; +export const infraPremiumStorageClassName = 'hyperdisk-balanced-rwo'; +export const pvcSuffix = 'hd-pvc'; diff --git a/cluster/pulumi/common/tsconfig.eslint.json b/cluster/pulumi/common/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/common/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/deployment/tsconfig.eslint.json b/cluster/pulumi/deployment/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/deployment/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/eslint.config.mjs b/cluster/pulumi/eslint.config.mjs index d5a0756535..8717187764 100644 --- a/cluster/pulumi/eslint.config.mjs +++ b/cluster/pulumi/eslint.config.mjs @@ -32,7 +32,7 @@ export default defineConfig([{ sourceType: "script", parserOptions: { - project: ["./tsconfig.json"], + project: ["./tsconfig.eslint.json"], }, }, diff --git a/cluster/pulumi/gcp/tsconfig.eslint.json b/cluster/pulumi/gcp/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/gcp/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/gha/src/controller.ts b/cluster/pulumi/gha/src/controller.ts index 8f50500da5..31a466fa7d 100644 --- a/cluster/pulumi/gha/src/controller.ts +++ b/cluster/pulumi/gha/src/controller.ts @@ -3,7 +3,7 @@ import * as k8s from '@pulumi/kubernetes'; import { HELM_MAX_HISTORY_SIZE, - infraAffinityAndTolerations, + infraKubernetesScheduling, } from '@canton-network/splice-pulumi-common'; import { Namespace } from '@pulumi/kubernetes/core/v1'; @@ -22,7 +22,7 @@ export function installController(repo: string, runnersNamespaceName: string): k version: ghaConfig.runnerScaleSetVersion, namespace: controllerNamespace.metadata.name, values: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, maxHistory: HELM_MAX_HISTORY_SIZE, flags: { logFormat: 'json', diff --git a/cluster/pulumi/gha/src/dockerMirror.ts b/cluster/pulumi/gha/src/dockerMirror.ts index 050707317a..2dd5b36f8d 100644 --- a/cluster/pulumi/gha/src/dockerMirror.ts +++ b/cluster/pulumi/gha/src/dockerMirror.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import * as k8s from '@pulumi/kubernetes'; import { - infraAffinityAndTolerations, + infraKubernetesScheduling, standardStorageClassName, } from '@canton-network/splice-pulumi-common'; import { Namespace } from '@pulumi/kubernetes/core/v1'; @@ -56,7 +56,7 @@ export function installDockerRegistryMirror(): k8s.helm.v3.Release { }, }, }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, }, { diff --git a/cluster/pulumi/gha/src/github.ts b/cluster/pulumi/gha/src/github.ts index ea7d33d673..a8fb74956f 100644 --- a/cluster/pulumi/gha/src/github.ts +++ b/cluster/pulumi/gha/src/github.ts @@ -51,28 +51,6 @@ export function installGithubRepo(repo: string): void { owner: ghaConfig.githubOrg, }); - // A bit ugly that we reuse this straight from DockerConfig, but we plan to - // retire artifactory altogether soon, so we don't bother cleaning this up. - const creds = DockerConfig.fetchCredentialsFromSecret('artifactory-keys'); - new github.ActionsVariable( - `artifactory-user-${repo}`, - { - repository: repo, - variableName: 'ARTIFACTORY_USER', - value: creds.apply(creds => creds.username), - }, - { provider: orgProvider } - ); - new github.ActionsSecret( - `artifactory-password-${repo}`, - { - repository: repo, - secretName: 'ARTIFACTORY_PASSWORD', - value: creds.apply(creds => creds.password), - }, - { provider: orgProvider } - ); - const auth0TestsManagementApi = getSecretVersionOutput({ secret: 'auth0-tests-management-api' }); new github.ActionsSecret( `auth0-tests-management-api-client-id-${repo}`, diff --git a/cluster/pulumi/gha/src/runners.test.ts b/cluster/pulumi/gha/src/runners.test.ts index d8053ad2cd..bcf609f0a1 100644 --- a/cluster/pulumi/gha/src/runners.test.ts +++ b/cluster/pulumi/gha/src/runners.test.ts @@ -43,7 +43,7 @@ jest.mock('@canton-network/splice-pulumi-common', () => ({ GCP_REGION: 'us-central123', GCP_ZONE: 'some-wonderful-place', imagePullSecretByNamespaceNameForServiceAccount: () => [], - infraAffinityAndTolerations: {}, + infraKubernetesScheduling: {}, CloudSqlConfigSchema: z.object({ flags: z.record(z.string(), z.string()).default({}) }), installPostgresPasswordSecret: () => { return { metadata: { name: 'secret' } }; diff --git a/cluster/pulumi/gha/src/runners.ts b/cluster/pulumi/gha/src/runners.ts index c5083df662..cc39db7b08 100644 --- a/cluster/pulumi/gha/src/runners.ts +++ b/cluster/pulumi/gha/src/runners.ts @@ -3,12 +3,12 @@ import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; import { - appsAffinityAndTolerations, + appsKubernetesScheduling, DOCKER_REPO, ExactNamespace, HELM_MAX_HISTORY_SIZE, imagePullSecretByNamespaceNameForServiceAccount, - infraAffinityAndTolerations, + infraKubernetesScheduling, K8sResourceSchema, SingleK8sResourceSchema, } from '@canton-network/splice-pulumi-common'; @@ -86,7 +86,7 @@ function installDockerRunnerScaleSet( listenerTemplate: { spec: { containers: [{ name: 'listener' }], - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, }, template: { @@ -231,7 +231,7 @@ function installDockerRunnerScaleSet( }, ], serviceAccountName: serviceAccountName, - ...appsAffinityAndTolerations, + ...appsKubernetesScheduling, }, metadata: { // prevent eviction by the gke autoscaler @@ -244,7 +244,7 @@ function installDockerRunnerScaleSet( }, }, }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, maxHistory: HELM_MAX_HISTORY_SIZE, }, }, @@ -407,7 +407,7 @@ function installK8sRunnerScaleSet( }, ], serviceAccountName: serviceAccountName, - ...appsAffinityAndTolerations, + ...appsKubernetesScheduling, }, metadata: { // prevent eviction by the gke autoscaler @@ -439,7 +439,7 @@ function installK8sRunnerScaleSet( listenerTemplate: { spec: { containers: [{ name: 'listener' }], - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, }, template: { @@ -524,7 +524,7 @@ function installK8sRunnerScaleSet( // Mount the volumes as owned by the runner user fsGroup: 1001, }, - ...appsAffinityAndTolerations, + ...appsKubernetesScheduling, volumes: [ { name: 'work', @@ -559,7 +559,7 @@ function installK8sRunnerScaleSet( }, }, }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, maxHistory: HELM_MAX_HISTORY_SIZE, }, }, diff --git a/cluster/pulumi/gha/tsconfig.eslint.json b/cluster/pulumi/gha/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/gha/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/infra/src/cloudArmor.ts b/cluster/pulumi/infra/src/cloudArmor.ts index 9ef7407a92..fdccf6fe3d 100644 --- a/cluster/pulumi/infra/src/cloudArmor.ts +++ b/cluster/pulumi/infra/src/cloudArmor.ts @@ -76,7 +76,8 @@ export function configureCloudArmorPolicy( // Step 2: Add predefined WAF rules if (cac.predefinedWafRules && cac.predefinedWafRules.length > 0) { - addPredefinedWafRules(/*securityPolicy, args.predefinedWafRules, cac.allRulesPreviewOnly, ruleOpts*/); + addPredefinedWafRules(); + /*securityPolicy, args.predefinedWafRules, cac.allRulesPreviewOnly, ruleOpts*/ } // Step 3: Add IP whitelisting rules @@ -147,9 +148,11 @@ function addThrottleAndBanRules( // this makes the pulumi update cleaner if toggling just one service if (throttleAcrossAllEndpointsAllIps.maxRequestsBeforeHttp429 > 0) { const ruleName = `throttle-all-endpoints-all-ips-${confEntryHead}`; - + const hostnameRegex = hostname + ? _.escapeRegExp(hostname) + : `scan\\.[\\w-]+\\.${_.escapeRegExp(config.clusterHostname)}`; const pathExpr = allowedPathsCondition(scanExternalRateLimits, pathPrefix); - const hostExpr = `request.headers['host'].matches(R"^${_.escapeRegExp(hostname)}(?::[0-9]+)?$")`; + const hostExpr = `request.headers['host'].matches(R"^${hostnameRegex}(?::[0-9]+)?$")`; const matchExpr = `${pathExpr} && ${hostExpr}`; new PolicyRule( @@ -227,15 +230,14 @@ function allowedPathsCondition(scanExternalRateLimits: PerEndpointLimits, pathPr .filter(p => !p.isBanned) .map(p => p.pathPrefix); - // Factor out /api/scan/ prefix (with trailing slash) - const scanPrefix = '/api/scan/'; - const scanPathRxs = pathPrefixes - .filter(p => p.startsWith(scanPrefix)) - .map(p => _.escapeRegExp(p.substring(scanPrefix.length))); // Remove prefix for factoring + const basePrefix = pathPrefix.endsWith('/') ? pathPrefix : `${pathPrefix}/`; + const dynamicPathRxs = pathPrefixes + .filter(p => p.startsWith(basePrefix)) + .map(p => _.escapeRegExp(p.substring(basePrefix.length))); // Build regex pattern - if (scanPathRxs.length > 0) { - const regexPattern = `${scanPrefix}(${scanPathRxs.join('|')})`; + if (dynamicPathRxs.length > 0) { + const regexPattern = `${basePrefix}(?:${dynamicPathRxs.join('|')})`; const pathExpr = `request.path.matches(R"^${regexPattern}")`; // limit from https://docs.cloud.google.com/armor/quotas#limits @@ -249,7 +251,7 @@ function allowedPathsCondition(scanExternalRateLimits: PerEndpointLimits, pathPr return pathExpr; } else { - // Fallback to simple prefix if no scan paths + // Fallback to simple prefix matching if no paths were resolved return `request.path.startsWith(R"${pathPrefix}")`; } } else { diff --git a/cluster/pulumi/infra/src/config.ts b/cluster/pulumi/infra/src/config.ts index e8fb8b7e37..a82d449669 100644 --- a/cluster/pulumi/infra/src/config.ts +++ b/cluster/pulumi/infra/src/config.ts @@ -1,13 +1,8 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 import * as pulumi from '@pulumi/pulumi'; -import { - config, - loadJsonFromFile, - externalIpRangesFile, -} from '@canton-network/splice-pulumi-common'; +import { config } from '@canton-network/splice-pulumi-common'; import { clusterYamlConfig } from '@canton-network/splice-pulumi-common/src/config/config'; -import { getSecretVersionOutput } from '@pulumi/gcp/secretmanager'; import util from 'node:util'; import { z } from 'zod'; @@ -27,7 +22,10 @@ const CloudArmorConfigSchema = z.object({ .catchall( z.object({ rulePreviewOnly: z.boolean().default(false), - hostname: z.string().regex(/^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/, 'valid DNS hostname'), + hostname: z + .string() + .regex(/^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/, 'valid DNS hostname') + .optional(), pathPrefix: z.string().regex(/^\/[^"]*$/, 'HTTP request path starting with /'), throttleAcrossAllEndpointsAllIps: z.object({ withinIntervalSeconds: z.number().positive(), @@ -54,12 +52,15 @@ export const InfraConfigSchema = z.object({ istio: z.object({ enableIngressAccessLogging: z.boolean(), enableClusterAccessLogging: z.boolean().default(false), + enablePublicTokenRegistry: z.boolean().default(false), + enableGeneralIpWhitelist: z.boolean().default(false), istiodValues: z.object({}).catchall(z.any()).default({}), sequencerFlowControl: z.object({ initialStreamWindowSize: z.int(), initialConnectionWindowSize: z.int(), }), }), + enableSweetSecurity: z.boolean().default(false), extraCustomResources: z.object({}).catchall(z.any()).default({}), }), cloudArmor: CloudArmorConfigSchema, @@ -82,46 +83,3 @@ console.error( export const infraConfig = fullConfig.infra; export const cloudArmorConfig: CloudArmorConfig = fullConfig.cloudArmor; - -type IpRangesDict = { [key: string]: IpRangesDict } | string[]; - -function extractIpRanges(x: IpRangesDict, svsOnly: boolean = false): string[] { - if (svsOnly) { - if (Array.isArray(x)) { - throw new Error('Cannot distinguish SV IP ranges from non-SV IP ranges in an array'); - } - return extractIpRanges(x['svs'], false); - } else { - return Array.isArray(x) - ? x - : Object.keys(x).reduce((acc: string[], k: string) => acc.concat(extractIpRanges(x[k])), []); - } -} - -export function loadIPRanges(svsOnly: boolean = false): pulumi.Output { - const file = externalIpRangesFile(); - const externalIpRanges = file ? extractIpRanges(loadJsonFromFile(file), svsOnly) : []; - - const internalWhitelistedIps = getSecretVersionOutput({ - secret: 'pulumi-internal-whitelists', - }).apply(whitelists => { - const secretData = whitelists.secretData; - const json = JSON.parse(secretData); - const ret: string[] = []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - json.forEach((ip: any) => { - ret.push(ip); - }); - return ret; - }); - - const configWhitelistedIps = infraConfig.ipWhitelisting?.extraWhitelistedIngress || []; - const excludedIps = infraConfig.ipWhitelisting?.excludedIps || []; - - return internalWhitelistedIps.apply(whitelists => - whitelists - .concat(externalIpRanges) - .concat(configWhitelistedIps) - .filter(ip => excludedIps.indexOf(ip) < 0) - ); -} diff --git a/cluster/pulumi/infra/src/index.ts b/cluster/pulumi/infra/src/index.ts index 10b83d3b17..5230bd1dd8 100644 --- a/cluster/pulumi/infra/src/index.ts +++ b/cluster/pulumi/infra/src/index.ts @@ -5,6 +5,7 @@ import * as k8s from '@pulumi/kubernetes'; import { config } from '@canton-network/splice-pulumi-common'; import { svsConfig } from '@canton-network/splice-pulumi-common-sv/src/config'; +import { configureSweet } from '../sweet'; import { configureAuth0 } from './auth0'; import { configureCloudArmorPolicy } from './cloudArmor'; import { @@ -53,7 +54,7 @@ if (useGKEL7Gateway) { ingressAddress: network.ingressIp, gatewayName: 'cn-gke-l7-gateway', backendServiceName: istio.httpServiceName, - serviceTarget: { port: 443 }, // see configureGateway for why 443 + serviceTarget: { port: 80 }, tlsSecretName: `cn-${clusterBasename}net-tls`, securityPolicy: cloudArmorSecurityPolicy, istioResource: istio.istioResource, @@ -64,6 +65,10 @@ configureStorage(); configureReloader(); +if (infraConfig.enableSweetSecurity) { + configureSweet(); +} + installExtraCustomResources(); if (enableGCReaperJob) { diff --git a/cluster/pulumi/infra/src/istio.ts b/cluster/pulumi/infra/src/istio.ts index 4e773d9df0..5f46cdb2ed 100644 --- a/cluster/pulumi/infra/src/istio.ts +++ b/cluster/pulumi/infra/src/istio.ts @@ -3,13 +3,12 @@ import * as gcp from '@pulumi/gcp'; import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; -import * as assert from 'assert/strict'; import { allSvsToDeployBasic, coreSvsToDeployBasic, } from '@canton-network/splice-pulumi-common-sv/src/svConfigsBasic'; import { cometBFTExternalPort } from '@canton-network/splice-pulumi-common-sv/src/synchronizer/cometbftConfig'; -import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; +import { rateLimitResponseHeaders } from '@canton-network/splice-pulumi-common/src/ratelimit/rateLimitHeaders'; import { mergeWith } from 'lodash'; import { @@ -21,11 +20,15 @@ import { GCP_ZONE, getDnsNames, HELM_MAX_HISTORY_SIZE, - infraAffinityAndTolerations, + infraKubernetesScheduling, isDevNet, isMainNet, } from '../../common'; -import { clusterBasename, infraConfig, loadIPRanges } from './config'; +import { clusterBasename, infraConfig } from './config'; +import { configureIstioGatewayPolicies, installAppWhitelisting } from './whitelisting'; +import { loadInternalWhitelistedIps, loadIPRanges } from './whitelisting/ipRanges'; +import { configurePublicInfo } from './whitelisting/publicInfo'; +import { configurePublicTokenRegistry } from './whitelisting/publicTokenRegistry'; interface ConfiguredIstio { allResources: pulumi.Resource[]; @@ -80,7 +83,7 @@ function configureIstiod( const defaultValues = { autoscaleMin: 2, autoscaleMax: 30, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, global: { istioNamespace: ingressNs.metadata.name, logAsJson: true, @@ -126,9 +129,18 @@ function configureIstiod( upstream_service_time: '%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%', user_agent: '%REQ(USER-AGENT)%', x_forwarded_for: '%REQ(X-FORWARDED-FOR)%', + // rate limiting fields, will show up in sidecar access logging + local_rate_limited: '%RESP(x-local-rate-limit)%', + rate_limit_limit: '%RESP(x-ratelimit-limit)%', + rate_limit_remaining: '%RESP(x-ratelimit-remaining)%', + rate_limit_reset: '%RESP(x-ratelimit-reset)%', }), // https://istio.io/latest/docs/ops/integrations/prometheus/#option-1-metrics-merging disable as we don't use annotations enablePrometheusMerge: false, + // https://istio.io/latest/docs/ops/best-practices/security/#path-normalization + pathNormalization: { + normalization: 'MERGE_SLASHES', + }, defaultConfig: { // The GCP NLB with externalTrafficPolicy: Local preserves the client's // source IP without adding X-Forwarded-For hops, so there are no trusted @@ -143,6 +155,13 @@ function configureIstiod( }, // wait for the istio container to start before starting apps to avoid network errors holdApplicationUntilProxyStarts: true, + // Export the local rate limit filter counters (enabled/ok/rate_limited/enforced). + // Deliberately narrow: inclusionRegexps is *additive* on top of Istio's + // defaults, so a broad regex here would blow up Prometheus cardinality. + // docs: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/local_rate_limit_filter#statistics + proxyStatsMatcher: { + inclusionRegexps: ['.*http_local_rate_limit.*'], + }, }, // We have clients retry so we disable istio’s automatic retries. defaultHttpRetryPolicy: { @@ -189,14 +208,16 @@ type IngressPort = { port: number; targetPort: number; protocol: string; + appProtocol?: string; }; -function ingressPort(name: string, port: number): IngressPort { +function ingressPort(name: string, port: number, appProtocol?: string): IngressPort { return { name: name, port: port, targetPort: port, protocol: 'TCP', + ...(appProtocol ? { appProtocol } : {}), }; } @@ -230,13 +251,17 @@ function configureInternalGatewayService( // The loopback traffic would be prevented by our policy. To still allow it, we // add the node pool ip ranges to the list. // eslint-disable-next-line promise/prefer-await-to-then - const internalIPRanges = cluster.then(c => + const gcpInternalIPRanges = cluster.then(c => c.nodePools.map(p => p.networkConfigs.map(c => c.podIpv4CidrBlock)).flat() ); - const externalIPRanges = loadIPRanges(); + const gatewayIPRanges = infraConfig.istio.enableGeneralIpWhitelist + ? pulumi.all([loadIPRanges(), gcpInternalIPRanges]).apply(([a, b]) => a.concat(b)) + : pulumi + .all([loadInternalWhitelistedIps(), gcpInternalIPRanges]) + .apply(([a, b]) => a.concat(b)); return configureGatewayService( ingressNs, - pulumi.all([externalIPRanges, internalIPRanges]).apply(([a, b]) => a.concat(b)), + gatewayIPRanges, ingress.viaGKEL7 ? { type: 'ClusterIP' } : { @@ -301,82 +326,6 @@ function configureCometBFTGatewayService( ); } -/** - * There doesn't seem to be an istio-level limit on number of IP lists but at - * some point we probably hit some k8s limits on the size of a definition so we - * split it into 100-500 IP ranges per policy. - * - * For 100k IPs, the difference between a chunk size of 100 vs 500 from scratch - * is 20min in pulumi vs 130min in pulumi. But we're still concerned about k8s - * limits on definition size. So if we break 10000 we'll gradually increase - * the chunk size, 20 IPs at a time, until reaching 500 chunk size for 50k IPs, - * which at least is tested for up to 100k IPs. - * - * Why 20? Too small jumps makes much noisier Pulumi previews. Too large, and we - * might jump right into a limit only revealed after extensive testing without - * really knowing where that limit is. 20 is a compromise: only jumps every 200 - * IPs so realignment updates are rare. - */ -function istioAccessPolicyChunkSize(ipRangesLength: number) { - assert.ok(ipRangesLength >= 0, 'nonsense'); - assert.ok( - ipRangesLength < 250000, - `${ipRangesLength} IPs untested, consider testing & increasing maximum chunk size` - ); - const stepSize = 20; - return Math.max(100, Math.min(500, Math.ceil(ipRangesLength / (stepSize * 100)) * stepSize)); -} - -const istioApiVersion = 'security.istio.io/v1beta1'; - -function istioAccessPolicies( - ingressNs: k8s.core.v1.Namespace, - externalIPRanges: pulumi.Output, - suffix: string -) { - const selector = { - matchLabels: { - app: `istio-ingress${suffix}`, - }, - }; - const defaultDenyAll = new k8s.apiextensions.CustomResource( - `istio-access-policy-deny-all${suffix}`, - { - apiVersion: istioApiVersion, - kind: 'AuthorizationPolicy', - metadata: { - name: `istio-access-policy-deny-all${suffix}`, - namespace: ingressNs.metadata.name, - }, - // empty spec is deny all - spec: { selector }, - } - ); - return externalIPRanges.apply(ipRanges => { - const chunkSize = istioAccessPolicyChunkSize(ipRanges.length); - const chunks = Array.from({ length: Math.ceil(ipRanges.length / chunkSize) }, (_, i) => - ipRanges.slice(i * chunkSize, i * chunkSize + chunkSize) - ); - const policies = chunks.map( - (chunk, i) => - new k8s.apiextensions.CustomResource(`istio-access-policy-allow${suffix}-${i}`, { - apiVersion: istioApiVersion, - kind: 'AuthorizationPolicy', - metadata: { - name: `istio-access-policy-allow${suffix}-${i}`, - namespace: ingressNs.metadata.name, - }, - spec: { - selector, - action: 'ALLOW', - rules: [{ from: [{ source: { remoteIpBlocks: chunk } }] }], - }, - }) - ); - return [defaultDenyAll].concat(policies); - }); -} - // how gateway is configured: https://github.com/istio/istio/blob/master/manifests/charts/gateway/templates/service.yaml type IstioGatewayVariant = | { @@ -404,10 +353,9 @@ function configureGatewayService( // - For cometbft traffic, which is tcp traffic, we failed to use istio policies, so we route it through a dedicated // LoadBalancer service that uses loadBalancerSourceRanges. The size limit is not an issue as we need only SV IPs. // These IPs should be provided in externalIPRangesInLB. + const istioPolicies = configureIstioGatewayPolicies(ingressNs, externalIPRangesInIstio, suffix); - const istioPolicies = istioAccessPolicies(ingressNs, externalIPRangesInIstio, suffix); - - const { serviceValues, deploymentValues } = + const { serviceValues, deploymentValues, port80Protocol } = gatewayVariant.type === 'LoadBalancer' ? { serviceValues: { @@ -421,6 +369,7 @@ function configureGatewayService( externalTrafficPolicy: 'Local', }, deploymentValues: {}, + port80Protocol: undefined, } : { // Create a ClusterIP Service for the istio ingress so the GKE L7 Gateway can @@ -437,6 +386,9 @@ function configureGatewayService( }), }, }, + // force HTTP/2 (h2c) between GKE L7 Gateway and istio-ingress for + // gRPC routes + port80Protocol: 'kubernetes.io/h2c', }; const gateway = new k8s.helm.v3.Release( @@ -471,11 +423,11 @@ function configureGatewayService( ...serviceValues, ports: [ ingressPort('status-port', 15021), // istio default - ingressPort('http2', 80), + ingressPort('http2', 80, port80Protocol), ingressPort('https', 443), ].concat(ingressPorts), }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, // The httpLoadBalancing addon needs to be enabled to use backend service-based network load balancers. annotations: { 'cloud.google.com/l4-rbs': 'enabled', @@ -560,29 +512,18 @@ function configureGateway( }, ...(withSeparateGcpGateway ? {} : { tls: { httpsRedirect: true } }), }, - withSeparateGcpGateway - ? { - hosts, - // our VirtualServices charts hardcode 443 as port match on http; - // without this you get 403 route_not_found in istio - port: { - name: 'http-on-443', - number: 443, - protocol: 'HTTP', - }, - } - : { - hosts, - port: { - name: 'https', - number: 443, - protocol: 'HTTPS', - }, - tls: { - mode: 'SIMPLE', - credentialName: `cn-${clusterBasename}net-tls`, - }, - }, + { + hosts, + port: { + name: 'https', + number: 443, + protocol: 'HTTPS', + }, + tls: { + mode: 'SIMPLE', + credentialName: `cn-${clusterBasename}net-tls`, + }, + }, ], }, }, @@ -660,6 +601,12 @@ function configureDocsAndReleases( prefix: '/cn-release-bundles', }, }, + { + port: 80, + uri: { + prefix: '/cn-release-bundles', + }, + }, ], route: [ { @@ -695,6 +642,9 @@ function configureDocsAndReleases( { port: 443, }, + { + port: 80, + }, ], route: [ { @@ -715,50 +665,6 @@ function configureDocsAndReleases( ]; } -function configurePublicInfo(ingressNs: k8s.core.v1.Namespace): k8s.apiextensions.CustomResource[] { - return spliceConfig.pulumiProjectConfig.hasPublicInfo - ? [ - new k8s.apiextensions.CustomResource('allow-sv-info', { - apiVersion: 'security.istio.io/v1beta1', - kind: 'AuthorizationPolicy', - metadata: { - name: 'allow-sv-info', - namespace: ingressNs.metadata.name, - }, - spec: { - selector: { - matchLabels: { - istio: 'ingress', - }, - }, - action: 'ALLOW', - rules: [ - { - to: [ - { - operation: { - hosts: [ - // We could also have done `info.sv*.whatever` here but enumerating what we expect seems slightly more secure - ...new Set( - allSvsToDeployBasic - .map(sv => [ - `info.${sv.ingressName}.${getDnsNames().cantonDnsName}`, - `info.${sv.ingressName}.${getDnsNames().daDnsName}`, - ]) - .flat() - ), - ], - }, - }, - ], - }, - ], - }, - }), - ] - : []; -} - function configureSequencerHighPerformanceGrpcDestinationRules( ingressNs: k8s.core.v1.Namespace ): Array { @@ -799,13 +705,13 @@ function configureSequencerHighPerformanceGrpcDestinationRule( }, connectionPool: { http: { - http1MaxPendingRequests: 10000, - http2MaxRequests: 10000, - maxConcurrentStreams: 10000, + http1MaxPendingRequests: 20000, + http2MaxRequests: 20000, + maxConcurrentStreams: 20000, maxRequestsPerConnection: 0, }, tcp: { - maxConnections: 10000, + maxConnections: 20000, }, }, }, @@ -813,6 +719,10 @@ function configureSequencerHighPerformanceGrpcDestinationRule( }); } +// Ports of the http2 servers that we apply the upstream flow control config to: +// the sequencer public API and the sequencer BFT P2P API. +const sequencerFlowControlUpstreamPorts = [5008, 5010]; + // Istio proxies lots of client connections over relatively few connections. If one of the client connections gets stuck // (e.g. because the client died) buffers will fill up and eventually istio will stop sending connection-level window updates // to the sequencer and trigger netty flow control. This surfaces as requests that send back response headers but then nothing else until the client times out. @@ -825,6 +735,40 @@ function configureSequencerHighPerformanceGrpcDestinationRule( function configureSequencerFlowControl( ingressNs: k8s.core.v1.Namespace ): k8s.apiextensions.CustomResource { + const http2ProtocolOptions = { + initial_stream_window_size: infraConfig.istio.sequencerFlowControl.initialStreamWindowSize, + initial_connection_window_size: + infraConfig.istio.sequencerFlowControl.initialConnectionWindowSize, + connection_keepalive: { + interval: '30s', + timeout: '5s', + }, + }; + // istio -> upstream (aka sequencer) + const upstreamPatch = (portNumber: number) => ({ + applyTo: 'CLUSTER', + match: { + cluster: { + portNumber, + // Ideally we would just apply it everywhere. But doing it without this portNumber breaks http1 configs. In theory there is `auto_config` which should do the right thing but then it doesn't apply it at all anymore. + // So for now we just apply it to the sequencer ports (public API and BFT P2P) which are the only externally exposed http2 servers so the only things where this really should matter in practice. + }, + }, + patch: { + operation: 'MERGE', + value: { + typed_extension_protocol_options: { + 'envoy.extensions.upstreams.http.v3.HttpProtocolOptions': { + '@type': 'type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions', + use_downstream_protocol_config: { + http_protocol_options: {}, + http2_protocol_options: http2ProtocolOptions, + }, + }, + }, + }, + }, + }); return new k8s.apiextensions.CustomResource('sequencer-flow-control', { apiVersion: 'networking.istio.io/v1alpha3', kind: 'EnvoyFilter', @@ -853,58 +797,57 @@ function configureSequencerFlowControl( typed_config: { '@type': 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager', - http2_protocol_options: { - initial_stream_window_size: - infraConfig.istio.sequencerFlowControl.initialStreamWindowSize, - initial_connection_window_size: - infraConfig.istio.sequencerFlowControl.initialConnectionWindowSize, - connection_keepalive: { - interval: '30s', - timeout: '5s', - }, - }, + http2_protocol_options: http2ProtocolOptions, }, }, }, }, - { - // istio -> upstream (aka sequencer) - applyTo: 'CLUSTER', - match: { - cluster: { - portNumber: 5008, - // Ideally we would just apply it everywhere. But doing it without this portNumber breaks http1 configs. In theory there is `auto_config` which should do the right thing but then it doesn't apply it at all anymore. - // So for now we just apply it to the sequencer which is the only externally exposed http2 server so the only thing where this really should matter in practice. - }, + ...sequencerFlowControlUpstreamPorts.map(upstreamPatch), + ], + }, + }); +} + +function stripRateLimitHeaders( + ingressNs: k8s.core.v1.Namespace, + gwSvc: k8s.helm.v3.Release +): k8s.apiextensions.CustomResource { + return new k8s.apiextensions.CustomResource( + 'strip-rate-limit-headers', + { + apiVersion: 'networking.istio.io/v1alpha3', + kind: 'EnvoyFilter', + metadata: { + name: 'strip-rate-limit-headers', + namespace: ingressNs.metadata.name, + }, + spec: { + workloadSelector: { + labels: { + istio: 'ingress', }, - patch: { - operation: 'MERGE', - value: { - typed_extension_protocol_options: { - 'envoy.extensions.upstreams.http.v3.HttpProtocolOptions': { - '@type': - 'type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions', - use_downstream_protocol_config: { - http_protocol_options: {}, - http2_protocol_options: { - initial_stream_window_size: - infraConfig.istio.sequencerFlowControl.initialStreamWindowSize, - initial_connection_window_size: - infraConfig.istio.sequencerFlowControl.initialConnectionWindowSize, - connection_keepalive: { - interval: '30s', - timeout: '5s', - }, - }, - }, - }, + }, + configPatches: [ + { + applyTo: 'ROUTE_CONFIGURATION', + match: { + context: 'GATEWAY', + }, + patch: { + // repeated fields are appended, so this does not clobber anything istio sets + operation: 'MERGE', + value: { + response_headers_to_remove: rateLimitResponseHeaders, }, }, }, - }, - ], + ], + }, }, - }); + { + dependsOn: [gwSvc], + } + ); } export function configureIstio( @@ -932,17 +875,26 @@ export function configureIstio( const gateways = configureGateway(ingressNs, gwSvc, cometBftSvc, expectGKEL7Gateway); const docsAndReleases = configureDocsAndReleases(true, gateways); const publicInfo = configurePublicInfo(ingressNs.ns); + + const publicTokenRegistry = infraConfig.istio.enablePublicTokenRegistry + ? configurePublicTokenRegistry(ingressNs.ns) + : []; + const sequencerHighPerformanceGrpcRules = configureSequencerHighPerformanceGrpcDestinationRules( ingressNs.ns ); const sequencerFlowControl = configureSequencerFlowControl(ingressNs.ns); + installAppWhitelisting(ingressNs.ns); + const rateLimitHeaderStripping = stripRateLimitHeaders(ingressNs.ns, gwSvc); return { allResources: [ ...gateways, ...docsAndReleases, ...publicInfo, + ...publicTokenRegistry, ...sequencerHighPerformanceGrpcRules, ...[sequencerFlowControl], + ...[rateLimitHeaderStripping], ], httpServiceName: 'istio-ingress', istioResource: gwSvc, diff --git a/cluster/pulumi/infra/src/maintenance.ts b/cluster/pulumi/infra/src/maintenance.ts index 47753d355c..a96adac040 100644 --- a/cluster/pulumi/infra/src/maintenance.ts +++ b/cluster/pulumi/infra/src/maintenance.ts @@ -4,7 +4,7 @@ import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; import { versionFromDefault } from '@canton-network/splice-pulumi-common/src/version'; -import { DOCKER_REPO, infraAffinityAndTolerations } from '../../common'; +import { DOCKER_REPO, infraKubernetesScheduling } from '../../common'; const cronJobName = 'gc-pod-reaper-job'; const reaperNamespace = 'gc-pod-reaper'; @@ -139,7 +139,7 @@ export function deployGCPodReaper( }, spec: { schedule: schedule, - concurrencyPolicy: 'Forbid', + concurrencyPolicy: 'Replace', // Replace stuck jobs instead of freezing successfulJobsHistoryLimit: 2, failedJobsHistoryLimit: 2, jobTemplate: { @@ -149,16 +149,17 @@ export function deployGCPodReaper( }, }, spec: { + activeDeadlineSeconds: 1200, // Kill the job if it hangs for 20 minutes template: { spec: { serviceAccountName: serviceAccountName, restartPolicy: 'OnFailure', - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, containers: [ { name: cronJobName, image: `${DOCKER_REPO}/splice-debug:${versionFromDefault()}`, - imagePullPolicy: 'Always', + imagePullPolicy: 'IfNotPresent', // Stop forcing pulls if image is cached command: deleteBadPodsCommand, env: [ { diff --git a/cluster/pulumi/infra/src/network.ts b/cluster/pulumi/infra/src/network.ts index 3345b8f408..b67e25cbfc 100644 --- a/cluster/pulumi/infra/src/network.ts +++ b/cluster/pulumi/infra/src/network.ts @@ -11,9 +11,12 @@ import { ExactNamespace, GCP_PROJECT, getDnsNames, + infraKubernetesSchedulingAffinityTolerations, + infraKubernetesSchedulingComputeClassViaAffinity, isDevNet, + useComputeClasses, } from '@canton-network/splice-pulumi-common'; -import { infraAffinityAndTolerations } from '@canton-network/splice-pulumi-common'; +import { infraKubernetesScheduling } from '@canton-network/splice-pulumi-common'; import { svConfigsBasic } from '@canton-network/splice-pulumi-common-sv/src/svConfigsBasic'; import { gcpDnsProject } from './config'; @@ -84,15 +87,21 @@ function certManager(certManagerNamespaceName: string): certmanager.CertManager namespace: ns.metadata.name, version: '1.18.2', }, - ...infraAffinityAndTolerations, + // Ideally, we would just use `...infraKubernetesScheduling` here. + // Unfortunately, the cert-manager helm chart does not support the `nodeSelector` field. + // It uses the wrong typing for it and then enforces it in Go, + // so no amount of TypeScript hacking will help. + ...(useComputeClasses + ? infraKubernetesSchedulingComputeClassViaAffinity + : infraKubernetesSchedulingAffinityTolerations), webhook: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, cainjector: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, startupapicheck: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, }); } diff --git a/cluster/pulumi/infra/src/reloader.ts b/cluster/pulumi/infra/src/reloader.ts index 13de0c4734..688bf54233 100644 --- a/cluster/pulumi/infra/src/reloader.ts +++ b/cluster/pulumi/infra/src/reloader.ts @@ -4,7 +4,7 @@ import * as k8s from '@pulumi/kubernetes'; import { HELM_MAX_HISTORY_SIZE, exactNamespace, - infraAffinityAndTolerations, + infraKubernetesScheduling, } from '@canton-network/splice-pulumi-common'; export function configureReloader(): k8s.helm.v3.Release { @@ -29,7 +29,7 @@ export function configureReloader(): k8s.helm.v3.Release { readOnlyRootFileSystem: true, enableMetricsByNamespace: true, deployment: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, containerSecurityContext: { capabilities: { diff --git a/cluster/pulumi/infra/src/whitelisting/gateway.ts b/cluster/pulumi/infra/src/whitelisting/gateway.ts new file mode 100644 index 0000000000..868c7bc1ad --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/gateway.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as k8s from '@pulumi/kubernetes'; +import * as pulumi from '@pulumi/pulumi'; + +import { createIstioIpAllowPolicies, istioApiVersion } from './policies'; + +export function configureGatewayAccessPolicies( + ingressNs: k8s.core.v1.Namespace, + ipRanges: pulumi.Output, + suffix: string +): pulumi.Output { + const selector = { + matchLabels: { + app: `istio-ingress${suffix}`, + }, + }; + const defaultDenyAll = new k8s.apiextensions.CustomResource( + `istio-access-policy-deny-all${suffix}`, + { + apiVersion: istioApiVersion, + kind: 'AuthorizationPolicy', + metadata: { + name: `istio-access-policy-deny-all${suffix}`, + namespace: ingressNs.metadata.name, + }, + // empty spec is deny all + spec: { selector }, + } + ); + return createIstioIpAllowPolicies({ + namePrefix: `istio-access-policy-allow${suffix}`, + namespace: ingressNs.metadata.name, + selector, + ipRanges, + }).apply(policies => [defaultDenyAll, ...policies]); +} diff --git a/cluster/pulumi/infra/src/whitelisting/index.ts b/cluster/pulumi/infra/src/whitelisting/index.ts new file mode 100644 index 0000000000..9f0a88db5e --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/index.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as k8s from '@pulumi/kubernetes'; +import * as pulumi from '@pulumi/pulumi'; + +import { infraConfig } from '../config'; +import { configureGatewayAccessPolicies } from './gateway'; +import { configureScanAndSvAppWhitelist } from './scanAndSvApp'; +import { configureSequencerWhitelist } from './sequencer'; + +export function installAppWhitelisting( + namespace: k8s.core.v1.Namespace +): pulumi.Output[] { + if (infraConfig.istio.enableGeneralIpWhitelist) { + return []; + } else { + return [ + ...configureScanAndSvAppWhitelist(namespace), + ...configureSequencerWhitelist(namespace), + ]; + } +} + +export function configureIstioGatewayPolicies( + ingressNs: k8s.core.v1.Namespace, + externalIPRangesInIstio: pulumi.Output, + suffix: string +): pulumi.Output { + return configureGatewayAccessPolicies(ingressNs, externalIPRangesInIstio, suffix); +} diff --git a/cluster/pulumi/infra/src/whitelisting/ipRanges.ts b/cluster/pulumi/infra/src/whitelisting/ipRanges.ts new file mode 100644 index 0000000000..4265119cf9 --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/ipRanges.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as pulumi from '@pulumi/pulumi'; +import { externalIpRangesFile, loadJsonFromFile } from '@canton-network/splice-pulumi-common'; +import { getSecretVersionOutput } from '@pulumi/gcp/secretmanager'; + +import { infraConfig } from '../config'; + +type IpRangesDict = { [key: string]: IpRangesDict } | string[]; + +function extractIpRanges(x: IpRangesDict, svsOnly: boolean = false): string[] { + if (svsOnly) { + if (Array.isArray(x)) { + throw new Error('Cannot distinguish SV IP ranges from non-SV IP ranges in an array'); + } + return extractIpRanges(x['svs'], false); + } else { + return Array.isArray(x) + ? x + : Object.keys(x).reduce((acc: string[], k: string) => acc.concat(extractIpRanges(x[k])), []); + } +} + +export function loadInternalWhitelistedIps(): pulumi.Output { + const excludedIps = infraConfig.ipWhitelisting?.excludedIps || []; + + return getSecretVersionOutput({ + secret: 'pulumi-internal-whitelists', + }).apply(whitelists => { + const secretData = whitelists.secretData; + const json = JSON.parse(secretData); + const ret: string[] = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json.forEach((ip: any) => { + ret.push(ip); + }); + const ips = ret.filter(ip => excludedIps.indexOf(ip) < 0); + return [...new Set(ips)]; + }); +} + +export function loadIPRanges(svsOnly: boolean = false): pulumi.Output { + const file = externalIpRangesFile(); + const externalIpRanges = file ? extractIpRanges(loadJsonFromFile(file), svsOnly) : []; + + const configWhitelistedIps = infraConfig.ipWhitelisting?.extraWhitelistedIngress || []; + const excludedIps = infraConfig.ipWhitelisting?.excludedIps || []; + + return loadInternalWhitelistedIps().apply(whitelists => { + const ips = whitelists + .concat(externalIpRanges) + .concat(configWhitelistedIps) + .filter(ip => excludedIps.indexOf(ip) < 0); + return [...new Set(ips)]; + }); +} diff --git a/cluster/pulumi/infra/src/whitelisting/policies.ts b/cluster/pulumi/infra/src/whitelisting/policies.ts new file mode 100644 index 0000000000..34ddae3ed8 --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/policies.ts @@ -0,0 +1,83 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as k8s from '@pulumi/kubernetes'; +import * as pulumi from '@pulumi/pulumi'; +import * as assert from 'assert/strict'; + +export const istioApiVersion = 'security.istio.io/v1beta1'; +export const istioIngressSelector = { matchLabels: { app: 'istio-ingress' } }; + +/** + * There doesn't seem to be an istio-level limit on number of IP lists but at + * some point we probably hit some k8s limits on the size of a definition so we + * split it into 100-500 IP ranges per policy. + * + * For 100k IPs, the difference between a chunk size of 100 vs 500 from scratch + * is 20min in pulumi vs 130min in pulumi. But we're still concerned about k8s + * limits on definition size. So if we break 10000 we'll gradually increase + * the chunk size, 20 IPs at a time, until reaching 500 chunk size for 50k IPs, + * which at least is tested for up to 100k IPs. + * + * Why 20? Too small jumps makes much noisier Pulumi previews. Too large, and we + * might jump right into a limit only revealed after extensive testing without + * really knowing where that limit is. 20 is a compromise: only jumps every 200 + * IPs so realignment updates are rare. + */ +export function istioAccessPolicyChunkSize(ipRangesLength: number): number { + assert.ok(ipRangesLength >= 0, 'nonsense'); + assert.ok( + ipRangesLength < 250000, + `${ipRangesLength} IPs untested, consider testing & increasing maximum chunk size` + ); + const stepSize = 20; + return Math.max(100, Math.min(500, Math.ceil(ipRangesLength / (stepSize * 100)) * stepSize)); +} + +function chunkIpRanges(ipRanges: string[]): string[][] { + const chunkSize = istioAccessPolicyChunkSize(ipRanges.length); + return Array.from({ length: Math.ceil(ipRanges.length / chunkSize) }, (_, i) => + ipRanges.slice(i * chunkSize, i * chunkSize + chunkSize) + ); +} + +export interface IstioIpAllowPolicyArgs { + namePrefix: string; + namespace: pulumi.Input; + selector: object; + ipRanges: pulumi.Output; + to?: object[]; + opts?: pulumi.CustomResourceOptions; +} + +export function createIstioIpAllowPolicies( + args: IstioIpAllowPolicyArgs +): pulumi.Output { + const { namePrefix, namespace, selector, ipRanges, to, opts } = args; + return ipRanges.apply(ranges => + chunkIpRanges(ranges).map( + (chunk, i) => + new k8s.apiextensions.CustomResource( + `${namePrefix}-${i}`, + { + apiVersion: istioApiVersion, + kind: 'AuthorizationPolicy', + metadata: { + name: `${namePrefix}-${i}`, + namespace, + }, + spec: { + selector, + action: 'ALLOW', + rules: [ + { + from: [{ source: { remoteIpBlocks: chunk } }], + ...(to ? { to } : {}), + }, + ], + }, + }, + opts + ) + ) + ); +} diff --git a/cluster/pulumi/infra/src/whitelisting/publicInfo.ts b/cluster/pulumi/infra/src/whitelisting/publicInfo.ts new file mode 100644 index 0000000000..83e669d089 --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/publicInfo.ts @@ -0,0 +1,54 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as k8s from '@pulumi/kubernetes'; +import { getDnsNames } from '@canton-network/splice-pulumi-common'; +import { allSvsToDeployBasic } from '@canton-network/splice-pulumi-common-sv/src/svConfigsBasic'; +import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; + +import { istioApiVersion } from './policies'; + +export function configurePublicInfo( + ingressNs: k8s.core.v1.Namespace +): k8s.apiextensions.CustomResource[] { + return spliceConfig.pulumiProjectConfig.hasPublicInfo + ? [ + new k8s.apiextensions.CustomResource('allow-sv-info', { + apiVersion: istioApiVersion, + kind: 'AuthorizationPolicy', + metadata: { + name: 'allow-sv-info', + namespace: ingressNs.metadata.name, + }, + spec: { + selector: { + matchLabels: { + istio: 'ingress', + }, + }, + action: 'ALLOW', + rules: [ + { + to: [ + { + operation: { + hosts: [ + // We could also have done `info.sv*.whatever` here but enumerating what we expect seems slightly more secure + ...new Set( + allSvsToDeployBasic + .map(sv => [ + `info.${sv.ingressName}.${getDnsNames().cantonDnsName}`, + `info.${sv.ingressName}.${getDnsNames().daDnsName}`, + ]) + .flat() + ), + ], + }, + }, + ], + }, + ], + }, + }), + ] + : []; +} diff --git a/cluster/pulumi/infra/src/whitelisting/publicTokenRegistry.ts b/cluster/pulumi/infra/src/whitelisting/publicTokenRegistry.ts new file mode 100644 index 0000000000..bee80586a5 --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/publicTokenRegistry.ts @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as k8s from '@pulumi/kubernetes'; + +import { istioApiVersion } from './policies'; + +export function configurePublicTokenRegistry( + ingressNs: k8s.core.v1.Namespace +): k8s.apiextensions.CustomResource[] { + return [ + new k8s.apiextensions.CustomResource('allow-public-token-registry', { + apiVersion: istioApiVersion, + kind: 'AuthorizationPolicy', + metadata: { + name: 'allow-public-token-registry', + namespace: ingressNs.metadata.name, + }, + spec: { + selector: { + matchLabels: { + app: 'istio-ingress', + }, + }, + action: 'ALLOW', + rules: [ + { + to: [ + { + operation: { + paths: ['/registry/*'], + }, + }, + ], + }, + ], + }, + }), + ]; +} diff --git a/cluster/pulumi/infra/src/whitelisting/scanAndSvApp.ts b/cluster/pulumi/infra/src/whitelisting/scanAndSvApp.ts new file mode 100644 index 0000000000..9194393e58 --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/scanAndSvApp.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as k8s from '@pulumi/kubernetes'; +import * as pulumi from '@pulumi/pulumi'; +import { getDnsNames, SPLICE_ROOT } from '@canton-network/splice-pulumi-common'; +import { allSvsToDeployBasic } from '@canton-network/splice-pulumi-common-sv/src/svConfigsBasic'; + +import { loadIPRanges } from './ipRanges'; +import { createIstioIpAllowPolicies, istioIngressSelector } from './policies'; +import { readSvPublicIngressPathsByAudience } from './svPublicEndpoints'; + +export const svOpenApiFile = `${SPLICE_ROOT}/apps/sv/src/main/openapi/sv-internal.yaml`; + +function hostsFor(prefix: string): string[] { + const dnsNames = [getDnsNames().cantonDnsName, getDnsNames().daDnsName]; + return allSvsToDeployBasic.flatMap(sv => + dnsNames.map(dns => `${prefix}.${sv.ingressName}.${dns}`) + ); +} + +export function configureScanAndSvAppWhitelist( + namespace: k8s.core.v1.Namespace +): pulumi.Output[] { + const scanHosts = hostsFor('scan'); + const svHosts = hostsFor('sv'); + const publicPaths = readSvPublicIngressPathsByAudience(svOpenApiFile); + + return [ + createIstioIpAllowPolicies({ + namePrefix: 'scan-app-ip-whitelist', + namespace: namespace.metadata.name, + selector: istioIngressSelector, + ipRanges: loadIPRanges(), + to: [{ operation: { hosts: scanHosts } }], + }), + createIstioIpAllowPolicies({ + namePrefix: 'sv-app-validators-ip-whitelist', + namespace: namespace.metadata.name, + selector: istioIngressSelector, + ipRanges: loadIPRanges(), + to: [{ operation: { hosts: svHosts, paths: publicPaths['validators'] } }], + }), + createIstioIpAllowPolicies({ + namePrefix: 'sv-app-svs-ip-whitelist', + namespace: namespace.metadata.name, + selector: istioIngressSelector, + ipRanges: loadIPRanges(true), + to: [{ operation: { hosts: svHosts, paths: publicPaths['svs'] } }], + }), + ]; +} diff --git a/cluster/pulumi/infra/src/whitelisting/sequencer.ts b/cluster/pulumi/infra/src/whitelisting/sequencer.ts new file mode 100644 index 0000000000..5aa706c2d9 --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/sequencer.ts @@ -0,0 +1,60 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as k8s from '@pulumi/kubernetes'; +import * as pulumi from '@pulumi/pulumi'; +import { + DecentralizedSynchronizerUpgradeConfig, + getDnsNames, +} from '@canton-network/splice-pulumi-common'; +import { allSvsToDeployBasic } from '@canton-network/splice-pulumi-common-sv/src/svConfigsBasic'; + +import { loadIPRanges } from './ipRanges'; +import { createIstioIpAllowPolicies, istioIngressSelector } from './policies'; + +export function configureSequencerWhitelist( + namespace: k8s.core.v1.Namespace +): pulumi.Output[] { + const dnsNames = [getDnsNames().cantonDnsName, getDnsNames().daDnsName]; + const migrations = DecentralizedSynchronizerUpgradeConfig.runningMigrations(); + + const publicApiHosts = allSvsToDeployBasic.flatMap(sv => + migrations.flatMap(migration => + dnsNames.flatMap(dns => [ + `sequencer-${migration.id}.${sv.ingressName}.${dns}`, + `sequencer-${migration.id}.${sv.ingressName}.${dns}:*`, + ]) + ) + ); + const p2pHosts = allSvsToDeployBasic.flatMap(sv => + migrations + .filter(migration => migration.sequencer.enableBftSequencer) + .flatMap(migration => + dnsNames.flatMap(dns => [ + `sequencer-p2p-${migration.id}.${sv.ingressName}.${dns}`, + `sequencer-p2p-${migration.id}.${sv.ingressName}.${dns}:*`, + ]) + ) + ); + + const policies = [ + createIstioIpAllowPolicies({ + namePrefix: 'sequencer-pub-ip-whitelist', + namespace: namespace.metadata.name, + selector: istioIngressSelector, + ipRanges: loadIPRanges(), + to: [{ operation: { hosts: publicApiHosts } }], + }), + ]; + if (p2pHosts.length > 0) { + policies.push( + createIstioIpAllowPolicies({ + namePrefix: 'sequencer-p2p-ip-whitelist', + namespace: namespace.metadata.name, + selector: istioIngressSelector, + ipRanges: loadIPRanges(true), + to: [{ operation: { hosts: p2pHosts } }], + }) + ); + } + return policies; +} diff --git a/cluster/pulumi/infra/src/whitelisting/svPublicEndpoints.test.ts b/cluster/pulumi/infra/src/whitelisting/svPublicEndpoints.test.ts new file mode 100644 index 0000000000..d505a1d842 --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/svPublicEndpoints.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as fs from 'fs'; +import * as path from 'path'; +import { describe, expect, test } from '@jest/globals'; + +import { + exposedAudiences, + parseSvPublicEndpoints, + publicAudiences, + svPublicIngressPathsByAudience, + toIngressPath, +} from './svPublicEndpoints'; + +const svOpenApiFile = path.join( + __dirname, + '../../../../../apps/sv/src/main/openapi/sv-internal.yaml' +); + +describe('the SV OpenAPI spec', () => { + const content = fs.readFileSync(svOpenApiFile, 'utf-8'); + + test('declares an x-external-audience for every sv_public endpoint', () => { + const endpoints = parseSvPublicEndpoints(content); + expect(endpoints.length).toBeGreaterThan(0); + endpoints.forEach(endpoint => { + expect(publicAudiences).toContain(endpoint.audience); + }); + }); + + test('exposes the expected paths per audience', () => { + const paths = svPublicIngressPathsByAudience(content); + expect(paths['validators']).toContain('/api/sv/v0/onboard/validator'); + expect(paths['validators']).toContain('/api/sv/v0/dso'); + expect(paths['svs']).toContain('/api/sv/v0/migration-id'); + expect(paths['svs']).toContain('/api/sv/v0/onboard/sv/status/*'); + const allPaths = exposedAudiences.flatMap(audience => paths[audience]); + // endpoints with an audience of none must not be whitelisted + expect(allPaths).not.toContain('/api/sv/v0/admin/domain/cometbft/status'); + expect(allPaths).not.toContain('/api/sv/v0/admin/domain/cometbft/json-rpc'); + // non-public endpoints must not be whitelisted + expect(allPaths).not.toContain('/api/sv/v0/admin/sv/votes'); + expect(allPaths).not.toContain('/api/sv/readyz'); + }); +}); + +describe('parseSvPublicEndpoints', () => { + const spec = (extra: string) => ` +openapi: 3.0.0 +paths: + /v0/foo: + get: + x-jvm-package: sv_public +${extra} + operationId: getFoo + responses: + "200": + description: ok +`; + + test('fails if an sv_public endpoint has no x-external-audience', () => { + expect(() => parseSvPublicEndpoints(spec(''))).toThrow(/must declare x-external-audience/); + }); + + test('fails on an unknown x-external-audience', () => { + expect(() => parseSvPublicEndpoints(spec(' x-external-audience: everyone'))).toThrow( + /must declare x-external-audience/ + ); + }); + + test('accepts a valid x-external-audience', () => { + expect(parseSvPublicEndpoints(spec(' x-external-audience: validators'))).toEqual([ + { path: '/v0/foo', method: 'get', operationId: 'getFoo', audience: 'validators' }, + ]); + }); + + test('accepts an audience of none but does not whitelist the endpoint', () => { + const content = spec(' x-external-audience: none'); + expect(parseSvPublicEndpoints(content)).toEqual([ + { path: '/v0/foo', method: 'get', operationId: 'getFoo', audience: 'none' }, + ]); + const paths = svPublicIngressPathsByAudience(content); + expect(exposedAudiences.flatMap(audience => paths[audience])).toEqual([]); + }); + + test('rejects x-external-audience on non-public endpoints', () => { + const nonPublic = ` +openapi: 3.0.0 +paths: + /v0/foo: + get: + x-jvm-package: sv_operator + x-external-audience: validators + operationId: getFoo +`; + expect(() => parseSvPublicEndpoints(nonPublic)).toThrow(/only allowed on endpoints/); + }); +}); + +test('toIngressPath replaces path parameters with a wildcard', () => { + expect(toIngressPath('/v0/onboard/sv/status/{candidate_party_id_or_name}')).toBe( + '/api/sv/v0/onboard/sv/status/*' + ); + expect(toIngressPath('/v0/dso')).toBe('/api/sv/v0/dso'); +}); diff --git a/cluster/pulumi/infra/src/whitelisting/svPublicEndpoints.ts b/cluster/pulumi/infra/src/whitelisting/svPublicEndpoints.ts new file mode 100644 index 0000000000..267e059b0c --- /dev/null +++ b/cluster/pulumi/infra/src/whitelisting/svPublicEndpoints.ts @@ -0,0 +1,96 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as fs from 'fs'; +import { load } from 'js-yaml'; + +export const svApiPathPrefix = '/api/sv'; + +export const publicAudiences = ['validators', 'svs', 'none'] as const; +export type PublicAudience = (typeof publicAudiences)[number]; + +export const exposedAudiences = ['validators', 'svs'] as const; +export type ExposedAudience = (typeof exposedAudiences)[number]; + +const httpMethods = ['get', 'put', 'post', 'delete', 'patch', 'head', 'options']; + +export type SvPublicEndpoint = { + path: string; + method: string; + operationId?: string; + audience: PublicAudience; +}; + +function isPublicAudience(value: unknown): value is PublicAudience { + return publicAudiences.includes(value as PublicAudience); +} + +/** + * Parses the SV OpenAPI spec and returns all endpoints that are exposed without + * authentication (`x-jvm-package: sv_public`). + * + * Throws if an `sv_public` endpoint does not declare a valid `x-external-audience`, + * or if a non-public endpoint declares one. + */ +export function parseSvPublicEndpoints(openApiContent: string): SvPublicEndpoint[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spec = load(openApiContent) as any; + const paths = spec?.paths || {}; + const endpoints: SvPublicEndpoint[] = []; + const errors: string[] = []; + for (const path of Object.keys(paths)) { + for (const method of httpMethods) { + const operation = paths[path]?.[method]; + if (!operation) { + continue; + } + const audience = operation['x-external-audience']; + const isPublic = operation['x-jvm-package'] === 'sv_public'; + if (!isPublic) { + if (audience !== undefined) { + errors.push( + `${method.toUpperCase()} ${path}: x-external-audience is only allowed on endpoints with x-jvm-package: sv_public` + ); + } + continue; + } + if (!isPublicAudience(audience)) { + errors.push( + `${method.toUpperCase()} ${path}: sv_public endpoints must declare x-external-audience as one of ${publicAudiences.join( + ', ' + )} but got ${JSON.stringify(audience)}` + ); + continue; + } + endpoints.push({ path, method, operationId: operation.operationId, audience }); + } + } + if (errors.length > 0) { + throw new Error(`Invalid SV OpenAPI public endpoint definitions:\n${errors.join('\n')}`); + } + return endpoints; +} + +export function toIngressPath(openApiPath: string): string { + const withWildcards = openApiPath.replace(/\{[^}]+\}/g, '*'); + return `${svApiPathPrefix}${withWildcards}`; +} + +export function svPublicIngressPathsByAudience( + openApiContent: string +): Record { + const endpoints = parseSvPublicEndpoints(openApiContent); + return Object.fromEntries( + exposedAudiences.map(audience => [ + audience, + [ + ...new Set(endpoints.filter(e => e.audience === audience).map(e => toIngressPath(e.path))), + ].sort(), + ]) + ) as Record; +} + +export function readSvPublicIngressPathsByAudience( + openApiFile: string +): Record { + return svPublicIngressPathsByAudience(fs.readFileSync(openApiFile, 'utf-8')); +} diff --git a/cluster/pulumi/infra/sweet.ts b/cluster/pulumi/infra/sweet.ts new file mode 100644 index 0000000000..67cf473f88 --- /dev/null +++ b/cluster/pulumi/infra/sweet.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as gcp from '@pulumi/gcp'; +import * as k8s from '@pulumi/kubernetes'; +import { + HELM_MAX_HISTORY_SIZE, + exactNamespace, + infraKubernetesScheduling, +} from '@canton-network/splice-pulumi-common'; + +export function configureSweet(): k8s.helm.v3.Release { + const operatorNs = exactNamespace('sweet-operator', false, false); + const sweetNs = exactNamespace('sweet', false, false); + + const apiKey = gcp.secretmanager.getSecretVersionOutput({ + secret: 'sweet-api-key', + }).secretData; + const secret = gcp.secretmanager.getSecretVersionOutput({ + secret: 'sweet-secret', + }).secretData; + + return new k8s.helm.v3.Release( + 'sweet-operator', + { + name: 'sweet-operator', + chart: 'oci://registry.sweet.security/helm/operatorchart', + version: '1.0.265090+06a1b12d61fc35ceb20b350388f7e812d382e4b2', + namespace: sweetNs.ns.metadata.name, + values: { + sweet: { + apiKey, + secret, + }, + operator: { + ...infraKubernetesScheduling, + }, + frontier: { + extraValues: { + informer: { + ...infraKubernetesScheduling, + }, + } + }, + admiral: { + extraValues: { + admirald: { + ...infraKubernetesScheduling, + }, + }, + } + }, + maxHistory: HELM_MAX_HISTORY_SIZE, + }, + { + dependsOn: [operatorNs.ns, sweetNs.ns], + } + + ); + +} diff --git a/cluster/pulumi/infra/tsconfig.eslint.json b/cluster/pulumi/infra/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/infra/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/multi-validator/src/config.ts b/cluster/pulumi/multi-validator/src/config.ts index 0362d1ae20..1df541717e 100644 --- a/cluster/pulumi/multi-validator/src/config.ts +++ b/cluster/pulumi/multi-validator/src/config.ts @@ -3,7 +3,7 @@ import { clusterYamlConfig } from '@canton-network/splice-pulumi-common/src/config/config'; import { z } from 'zod'; -import { LogLevelSchema } from '../../common'; +import { LogLevelSchema, SplicePostgresSchema } from '../../common'; import { K8sResourceSchema } from '../../common/src/config/configSchema'; export const EnvironmentVariableSchema = z.object({ @@ -17,6 +17,10 @@ export const MultiValidatorConfigSchema = z.object({ multiValidator: z .object({ postgresPvcSize: z.string().optional(), + // Multi-validator needs to be migrated + postgres: SplicePostgresSchema.default({ + deployment: 'legacy-helm-chart', + }), requiresOnboardingSecret: z.boolean().default(false), extraValidatorEnvVars: z.array(EnvironmentVariableSchema).default([]), extraParticipantEnvVars: z.array(EnvironmentVariableSchema).default([]), diff --git a/cluster/pulumi/multi-validator/src/installNode.ts b/cluster/pulumi/multi-validator/src/installNode.ts index 760bf7d76b..df4eeba47c 100644 --- a/cluster/pulumi/multi-validator/src/installNode.ts +++ b/cluster/pulumi/multi-validator/src/installNode.ts @@ -21,11 +21,13 @@ export async function installNode(): Promise { for (let i = 0; i < numInstances; i++) { const postgres = installPostgres(namespace, `postgres-${i}`, imagePullDeps); const postgresConf = { - host: `postgres-${i}`, + // being just the init container it doesn't matter too much so as long as it has psql + initImageName: 'postgres:18', + host: postgres.address, port: '5432', schema: 'cantonnet', secret: { - name: `postgres-${i}-secret`, + name: postgres.secretName, key: 'postgresPassword', }, }; diff --git a/cluster/pulumi/multi-validator/src/multiNodeDeployment.ts b/cluster/pulumi/multi-validator/src/multiNodeDeployment.ts index 25f5e72e99..b9134b3202 100644 --- a/cluster/pulumi/multi-validator/src/multiNodeDeployment.ts +++ b/cluster/pulumi/multi-validator/src/multiNodeDeployment.ts @@ -3,7 +3,7 @@ import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; import { - appsAffinityAndTolerations, + appsKubernetesScheduling, DOCKER_REPO, imagePullPolicy, jmxOptions, @@ -18,11 +18,12 @@ import { EnvironmentVariable, multiValidatorConfig } from './config'; export interface BaseMultiNodeArgs { namespace: k8s.core.v1.Namespace; postgres: { - host: string; + initImageName: string; + host: pulumi.Output; schema: string; port: string; db: string; - secret: { name: string; key: string }; + secret: { name: pulumi.Output; key: string }; }; } @@ -140,7 +141,7 @@ export class MultiNodeDeployment extends pulumi.ComponentResource { initContainers: [ { name: 'pg-init', - image: 'postgres:14', + image: args.postgres.initImageName, env: [ { name: 'PGPASSWORD', @@ -152,13 +153,12 @@ export class MultiNodeDeployment extends pulumi.ComponentResource { command: [ 'bash', '-c', - ` + args.postgres.host.apply( + host => ` function createDb() { local dbname="$1" - until errmsg=$(psql -h ${ - args.postgres.host - } --username=cnadmin --dbname=cantonnet -c "create database $dbname" 2>&1); do + until errmsg=$(psql -h ${host} --username=cnadmin --dbname=cantonnet -c "create database $dbname" 2>&1); do if [[ $errmsg == *"already exists"* ]]; then echo "Database $dbname already exists. Done." break @@ -173,11 +173,12 @@ export class MultiNodeDeployment extends pulumi.ComponentResource { { length: numNodesPerInstance }, (_, i) => `createDb ${args.postgres.db}_${zeroPad(i, 2)}` ).join('\n')} - `, + ` + ), ], }, ], - ...appsAffinityAndTolerations, + ...appsKubernetesScheduling, }, }, }, diff --git a/cluster/pulumi/multi-validator/src/postgres.ts b/cluster/pulumi/multi-validator/src/postgres.ts index 622416d3c1..c510afc7a9 100644 --- a/cluster/pulumi/multi-validator/src/postgres.ts +++ b/cluster/pulumi/multi-validator/src/postgres.ts @@ -1,63 +1,51 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 import * as pulumi from '@pulumi/pulumi'; -import * as random from '@pulumi/random'; import { activeVersion, - appsAffinityAndTolerations, + appsKubernetesScheduling, CnInput, ExactNamespace, - InstalledHelmChart, - installPostgresPasswordSecret, - installSpliceRunbookHelmChart, spliceConfig, standardStorageClassName, - createVolumeSnapshot, } from '@canton-network/splice-pulumi-common'; +import { installSplicePostgres, Postgres } from '@canton-network/splice-pulumi-common/src/postgres'; -import { hyperdiskSupportConfig } from '../../common/src/config/hyperdiskSupportConfig'; import { multiValidatorConfig } from './config'; export function installPostgres( xns: ExactNamespace, name: string, dependsOn: CnInput[] -): InstalledHelmChart { - const password = new random.RandomPassword(`${xns.logicalName}-${name}-passwd`, { - length: 16, - overrideSpecial: '_%@', - special: true, - }).result; +): Postgres { const secretName = `${name}-secret`; - const passwordSecret = installPostgresPasswordSecret(xns, password, secretName); if (!multiValidatorConfig) { throw new Error('multiValidator config must be set when they are enabled'); } const config = multiValidatorConfig!; - return installSpliceRunbookHelmChart( + return installSplicePostgres( xns, name, - 'splice-postgres', + secretName, + config.postgres, + activeVersion, + {}, { - persistence: { secretName }, db: { volumeSize: config.postgresPvcSize, maxConnections: 1000, - ...(hyperdiskSupportConfig.hyperdiskSupport.enabled - ? { - volumeStorageClass: standardStorageClassName, - pvcTemplateName: 'pg-data-hd', - } - : {}), + volumeStorageClass: standardStorageClassName, + pvcTemplateName: 'pg-data-hd', }, resources: config.resources?.postgres, - appsAffinityAndTolerations, + appsAffinityAndTolerations: appsKubernetesScheduling, }, - activeVersion, + true, // overrideDbSizeFromValues + false, // useinfraKubernetesScheduling { - dependsOn: [passwordSecret, ...dependsOn], + dependsOn, ...(spliceConfig.pulumiProjectConfig.replacePostgresStatefulSetOnChanges ? { replaceOnChanges: ['*'], diff --git a/cluster/pulumi/multi-validator/tsconfig.eslint.json b/cluster/pulumi/multi-validator/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/multi-validator/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/observability/grafana-alerting/acs-stores_alerts.yaml b/cluster/pulumi/observability/grafana-alerting/acs-stores_alerts.yaml index 7a2de5e0c9..c52bc27fd9 100644 --- a/cluster/pulumi/observability/grafana-alerting/acs-stores_alerts.yaml +++ b/cluster/pulumi/observability/grafana-alerting/acs-stores_alerts.yaml @@ -16,7 +16,7 @@ groups: datasourceUid: prometheus model: editorMode: code - expr: avg_over_time(sum by (node_type) (splice_store_acs_size{namespace="sv-1"})[1d:1h]) + expr: avg_over_time(sum by (node_type) (splice_history_acs_snapshots_snapshot_size{namespace="sv-1"})[1d:1h]) instant: true intervalMs: 1000 legendFormat: __auto @@ -33,7 +33,7 @@ groups: type: prometheus uid: prometheus editorMode: code - expr: avg_over_time(sum by (node_type) (splice_store_acs_size{namespace="sv-1"})[7d:1h] offset 1d) or avg_over_time(sum by (node_type) (splice_store_acs_size{namespace="sv-1"})[1d:1h]) + expr: avg_over_time(sum by (node_type) (splice_history_acs_snapshots_snapshot_size{namespace="sv-1"})[7d:1h] offset 1d) or avg_over_time(sum by (node_type) (splice_history_acs_snapshots_snapshot_size{namespace="sv-1"})[1d:1h]) instant: true intervalMs: 1000 legendFormat: __auto diff --git a/cluster/pulumi/observability/grafana-alerting/dso_missed_confirmations_alerts.yaml b/cluster/pulumi/observability/grafana-alerting/dso_missed_confirmations_alerts.yaml index 9ebde7b920..969e928332 100644 --- a/cluster/pulumi/observability/grafana-alerting/dso_missed_confirmations_alerts.yaml +++ b/cluster/pulumi/observability/grafana-alerting/dso_missed_confirmations_alerts.yaml @@ -9,20 +9,20 @@ groups: title: DSO Party Missed Confirmations condition: missed confirmations threshold exceeded data: - - refId: missed confirmation rate + - refId: missed_confirmations relativeTimeRange: from: $DSO_MISSED_CONFIRMATIONS_WINDOW_SECONDS to: 0 datasourceUid: prometheus model: editorMode: code - expr: sum by (namespace) (increase(daml_mediator_timeout_non_responsive_participants_total{party=~"DSO::.*"}[$DSO_MISSED_CONFIRMATIONS_WINDOW_MINUTESm])) / sum by (namespace) (increase(daml_mediator_requests_total[$DSO_MISSED_CONFIRMATIONS_WINDOW_MINUTESm])) + expr: max by (party) (increase(daml_mediator_timeout_non_responsive_participants_total{party=~"DSO::.*"}[$DSO_MISSED_CONFIRMATIONS_WINDOW_MINUTESm])) instant: true intervalMs: 1000 legendFormat: __auto maxDataPoints: 43200 range: false - refId: missed confirmation rate + refId: missed_confirmations - refId: missed confirmations threshold exceeded datasourceUid: __expr__ model: @@ -43,7 +43,7 @@ groups: datasource: type: __expr__ uid: __expr__ - expression: missed confirmation rate + expression: missed_confirmations intervalMs: 1000 maxDataPoints: 43200 refId: missed confirmations threshold exceeded @@ -57,6 +57,6 @@ groups: __dashboardUid__: cnck4jp __panelId__: "1" severity: critical - summary: The DSO party in namespace {{ $labels.namespace }} is missing more than $DSO_MISSED_CONFIRMATIONS_THRESHOLD_PERCENT% of its confirmations over the last $DSO_MISSED_CONFIRMATIONS_WINDOW_MINUTES minutes. + summary: The DSO party missed {{ index $values "missed_confirmations" }}, more than $DSO_MISSED_CONFIRMATIONS_THRESHOLD of its confirmations over the last $DSO_MISSED_CONFIRMATIONS_WINDOW_MINUTES minutes. labels: {} isPaused: false diff --git a/cluster/pulumi/observability/grafana-alerting/global-sync-health_alerts.yaml b/cluster/pulumi/observability/grafana-alerting/global-sync-health_alerts.yaml new file mode 100644 index 0000000000..b856482081 --- /dev/null +++ b/cluster/pulumi/observability/grafana-alerting/global-sync-health_alerts.yaml @@ -0,0 +1,215 @@ +apiVersion: 1 +groups: + - orgId: 1 + name: global-synchronizer-health + folder: canton-network + interval: 1m + rules: + # Confirmation requests that were sequenced but never processed by the mediator + # (e.g. dropped due to CometBFT replays). There is no dedicated Canton metric for + # discarded events yet, so we approximate it as the difference between sequenced + # send-confirmation-request events and requests received by the mediator. + - uid: gsh0discarded0cr + title: High Rate of Discarded Confirmation Requests + condition: discarded fraction threshold exceeded + data: + - refId: discarded_fraction + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: prometheus + model: + editorMode: code + expr: (sum by (namespace) (rate(daml_sequencer_block_events_total{type="send-confirmation-request"}[10m])) - sum by (namespace) (rate(daml_mediator_requests_total[10m]))) / sum by (namespace) (rate(daml_sequencer_block_events_total{type="send-confirmation-request"}[10m])) + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: discarded_fraction + - refId: discarded fraction threshold exceeded + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - $DISCARDED_CONFIRMATION_REQUESTS_THRESHOLD + type: gt + operator: + type: and + query: + params: + - C + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: discarded_fraction + intervalMs: 1000 + maxDataPoints: 43200 + refId: discarded fraction threshold exceeded + type: threshold + dashboardUid: fe8wt04z620aof + panelId: 10 + noDataState: OK + execErrState: OK + for: 10m + annotations: + __dashboardUid__: fe8wt04z620aof + __panelId__: "10" + runbook_url: "" + severity: warning + summary: "{{ $labels.namespace }} discarded more than $DISCARDED_CONFIRMATION_REQUESTS_THRESHOLD of sequenced confirmation requests over the last 10 minutes" + description: 'A significant fraction of sequenced confirmation requests was never processed by the mediator. This typically matches CometBFT replays.' + labels: {} + isPaused: false + # Failure rate of confirmation requests, as observed by the mediator. + # Can increase for non-network reasons, but is relatively unlikely to. + - uid: gsh0failedcr0rel + title: High Failed Confirmation Request Rate + condition: failure rate threshold exceeded + data: + - refId: failure_rate + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: prometheus + model: + editorMode: code + expr: 1 - sum by (namespace) (rate(daml_mediator_approved_requests_total[30m])) / sum by (namespace) (rate(daml_mediator_requests_total{duplicate_reject="false"}[30m])) + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: failure_rate + - refId: failure rate threshold exceeded + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - $FAILED_CONFIRMATION_REQUESTS_THRESHOLD + type: gt + operator: + type: and + query: + params: + - C + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: failure_rate + intervalMs: 1000 + maxDataPoints: 43200 + refId: failure rate threshold exceeded + type: threshold + dashboardUid: fe8wt04z620aof + panelId: 8 + noDataState: OK + execErrState: OK + for: 5m + annotations: + __dashboardUid__: fe8wt04z620aof + __panelId__: "8" + runbook_url: "" + severity: warning + summary: "{{ $labels.namespace }} saw more than $FAILED_CONFIRMATION_REQUESTS_THRESHOLD of confirmation requests fail over the last 30 minutes" + description: 'Confirmation requests are failing at a high rate. Can happen for non-network reasons, but is relatively unlikely to.' + labels: {} + isPaused: false + # Overall TPS (approved confirmation requests per second), compared to the + # previous 30m window. Can drop for non-network reasons, but a large relative + # drop is a strong signal for network issues. + - uid: gsh0tps0drop0rel + title: Significant TPS Drop + condition: tps dropped + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: prometheus + model: + editorMode: code + expr: sum by (namespace) (rate(daml_mediator_approved_requests_total[30m])) + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + - refId: B + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: prometheus + model: + editorMode: code + expr: sum by (namespace) (rate(daml_mediator_approved_requests_total[30m] offset 30m)) + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: B + - refId: C + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: __expr__ + model: + datasource: + name: Expression + type: __expr__ + uid: __expr__ + expression: $A < $B * $TPS_DROP_THRESHOLD + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: math + - refId: tps dropped + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 0 + type: gt + operator: + type: and + query: + params: [] + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: C + intervalMs: 1000 + maxDataPoints: 43200 + refId: tps dropped + type: threshold + dashboardUid: fe8wt04z620aof + panelId: 4 + noDataState: OK + execErrState: OK + for: 5m + annotations: + __dashboardUid__: fe8wt04z620aof + __panelId__: "4" + runbook_url: "" + severity: warning + summary: "{{ $labels.namespace }} saw TPS drop below $TPS_DROP_THRESHOLD of the previous 30 minute window" + description: 'The rate of approved confirmation requests over the last 30m dropped significantly compared to the previous 30m window. Can happen for non-network reasons, but is relatively unlikely to.' + labels: {} + isPaused: false diff --git a/cluster/pulumi/observability/grafana-alerting/istio-rate-limiting_alerts.yaml b/cluster/pulumi/observability/grafana-alerting/istio-rate-limiting_alerts.yaml new file mode 100644 index 0000000000..774ba9e502 --- /dev/null +++ b/cluster/pulumi/observability/grafana-alerting/istio-rate-limiting_alerts.yaml @@ -0,0 +1,67 @@ +apiVersion: 1 +groups: + - orgId: 1 + name: istio-rate-limit + folder: platform + interval: 5m + rules: + - uid: cfurlpifrmvi8w + title: Enforced requests + condition: C + data: + - refId: A + relativeTimeRange: + from: 21600 + to: 0 + datasourceUid: prometheus + model: + datasource: + type: prometheus + uid: prometheus + editorMode: code + expr: sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~"sv.*"}[$__rate_interval])) by (namespace, pod) + instant: true + interval: "" + intervalMs: 30000 + legendFormat: '{{namespace}} - {{pod}}' + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 0 + type: gt + operator: + type: and + query: + params: [] + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: cnr56dj + panelId: 9 + noDataState: OK + execErrState: Alerting + for: 5m + annotations: + runbook_url: "" + severity: warning + description: Envoy local rate limiting is active, requests are being rejected. Check the "Istio Rate Limiting" dashboard. + summary: Envoy is rate limiting requests in {{ $labels.namespace }} on pod {{ $labels.pod }}. + labels: + "": "" + gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0AjsonPayload.response_code=429 + isPaused: false diff --git a/cluster/pulumi/observability/grafana-alerting/scan_bft_sequencers_alerts.yaml b/cluster/pulumi/observability/grafana-alerting/scan_bft_sequencers_alerts.yaml new file mode 100644 index 0000000000..618e452e39 --- /dev/null +++ b/cluster/pulumi/observability/grafana-alerting/scan_bft_sequencers_alerts.yaml @@ -0,0 +1,79 @@ +apiVersion: 1 +groups: + - orgId: 1 + name: scan bft sequencer reads + folder: canton-network + interval: 10m + rules: + - uid: aesvbftscanreadfail1 + title: SV failing to read BFT sequencer list from a scan + condition: C + data: + - refId: A + relativeTimeRange: + from: 1800 + to: 0 + datasourceUid: prometheus + model: + datasource: + type: prometheus + uid: prometheus + editorMode: code + expr: |- + sum by (namespace, job, target_host) ( + histogram_count(increase(daml_http_client_requests_duration_seconds{http_client="HttpScanAppClient", operation="ListBftSequencers", status_code!~"2.."}[30m])) + ) + / + sum by (namespace, job, target_host) ( + histogram_count(increase(daml_http_client_requests_duration_seconds{http_client="HttpScanAppClient", operation="ListBftSequencers"}[30m])) + ) + instant: true + intervalMs: 1000 + legendFormat: '{{namespace}}:{{job}}:{{target_host}}' + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 0.5 + - 0 + type: gt + operator: + type: and + query: + params: [] + reducer: + params: [] + type: last + type: query + unloadEvaluator: + params: + - 0.5 + - 0 + type: lte + datasource: + name: Expression + type: __expr__ + uid: __expr__ + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: a8a3113a-bccf-4728-b752-dd7a5d6f9bda + panelId: 2 + noDataState: OK + execErrState: OK + for: 0m + annotations: + __dashboardUid__: a8a3113a-bccf-4728-b752-dd7a5d6f9bda + __panelId__: "2" + description: More than 50% of this SV's reads of the BFT sequencer list (/v0/sv-bft-sequencers) from this scan failed (non-2xx response, timeout, or connection error) over the last 30 minutes. The BFT peer reconciler tolerates individual failures safely, but while this scan is unreachable the SV cannot pick up BFT sequencer endpoint changes published by that scan's SV. + severity: warning + summary: SV {{ $labels.job }} in namespace {{ $labels.namespace }} failed {{ index $values "A" }} of its BFT sequencer list reads from scan {{ $labels.target_host }} in the last 30m. + labels: {} + isPaused: false diff --git a/cluster/pulumi/observability/grafana-alerting/scan_connection_disagreement_alerts.yaml b/cluster/pulumi/observability/grafana-alerting/scan_connection_disagreement_alerts.yaml index 30c1b3ce14..d5012f7f21 100644 --- a/cluster/pulumi/observability/grafana-alerting/scan_connection_disagreement_alerts.yaml +++ b/cluster/pulumi/observability/grafana-alerting/scan_connection_disagreement_alerts.yaml @@ -20,12 +20,14 @@ groups: uid: prometheus editorMode: code expr: |- - sum by (namespace, job, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus="disagree", success="true"$SCAN_DISAGREEMENT_FILTER}[30m])) - / - sum by (namespace, job, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{$SCAN_DISAGREEMENT_FILTER_BARE}[30m])) + max by (scan_connection, request) ( + sum by (namespace, job, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus="disagree", success="true"$SCAN_DISAGREEMENT_FILTER}[30m])) + / + sum by (namespace, job, node_name, scan_connection, request) (increase(splice_validator_scan_bft_per_connection_consensus_total{$SCAN_DISAGREEMENT_FILTER_BARE}[30m])) + ) > 0 instant: true intervalMs: 1000 - legendFormat: '{{namespace}}:{{job}}:{{node_name}}:{{scan_connection}}:{{request}}' + legendFormat: '{{scan_connection}}:{{request}}' maxDataPoints: 43200 range: false refId: A @@ -70,7 +72,7 @@ groups: __panelId__: "6" description: More than $SCAN_DISAGREEMENT_SUCCESS_THRESHOLD_PERCENT% of the BFT consensus comparisons for this request on this scan connection returned a successful (2xx) response that disagreed with the BFT consensus result over the last 30 minutes. severity: critical - summary: Scan connection {{ $labels.scan_connection }} of job {{ $labels.job }} (node {{ $labels.node_name }}) in namespace {{ $labels.namespace }} returned successful responses disagreeing with BFT consensus on {{ index $values "A" }} of comparisons for request {{ $labels.request }} in the last 30m (threshold $SCAN_DISAGREEMENT_SUCCESS_THRESHOLD_PERCENT%). + summary: Scan connection {{ $labels.scan_connection }} returned successful responses disagreeing with BFT consensus on {{ index $values "A" }} of comparisons for request {{ $labels.request }} in the last 30m (threshold $SCAN_DISAGREEMENT_SUCCESS_THRESHOLD_PERCENT%). labels: {} isPaused: false - uid: aescandisagreefail1 @@ -88,12 +90,14 @@ groups: uid: prometheus editorMode: code expr: |- - sum by (namespace, job, node_name, scan_connection) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus="disagree", success="false"$SCAN_DISAGREEMENT_CONNECTION_FILTER}[30m])) - / - sum by (namespace, job, node_name, scan_connection) (increase(splice_validator_scan_bft_per_connection_consensus_total{$SCAN_DISAGREEMENT_CONNECTION_FILTER_BARE}[30m])) + max by (scan_connection) ( + sum by (namespace, job, node_name, scan_connection) (increase(splice_validator_scan_bft_per_connection_consensus_total{consensus="disagree", success="false"$SCAN_DISAGREEMENT_CONNECTION_FILTER}[30m])) + / + sum by (namespace, job, node_name, scan_connection) (increase(splice_validator_scan_bft_per_connection_consensus_total{$SCAN_DISAGREEMENT_CONNECTION_FILTER_BARE}[30m])) + ) > 0 instant: true intervalMs: 1000 - legendFormat: '{{namespace}}:{{job}}:{{node_name}}:{{scan_connection}}' + legendFormat: '{{scan_connection}}' maxDataPoints: 43200 range: false refId: A @@ -138,6 +142,6 @@ groups: __panelId__: "6" description: More than $SCAN_DISAGREEMENT_SUCCESS_THRESHOLD_PERCENT% of the BFT consensus comparisons on this scan connection returned a failed (non-2xx) response that disagreed with the BFT consensus result over the last 30 minutes. This can indicate the scan node is down. severity: warning - summary: Scan connection {{ $labels.scan_connection }} of job {{ $labels.job }} (node {{ $labels.node_name }}) in namespace {{ $labels.namespace }} returned failed responses disagreeing with BFT consensus on {{ index $values "A" }} of comparisons in the last 30m (threshold $SCAN_DISAGREEMENT_SUCCESS_THRESHOLD_PERCENT%). + summary: Scan connection {{ $labels.scan_connection }} returned failed responses disagreeing with BFT consensus on {{ index $values "A" }} of comparisons in the last 30m (threshold $SCAN_DISAGREEMENT_SUCCESS_THRESHOLD_PERCENT%). labels: {} isPaused: false diff --git a/cluster/pulumi/observability/grafana-alerting/splice-rate-limiting_alerts.yaml b/cluster/pulumi/observability/grafana-alerting/splice-rate-limiting_alerts.yaml new file mode 100644 index 0000000000..309aa5e23e --- /dev/null +++ b/cluster/pulumi/observability/grafana-alerting/splice-rate-limiting_alerts.yaml @@ -0,0 +1,123 @@ +apiVersion: 1 +groups: + - orgId: 1 + name: splice-rate-limiting + folder: platform + interval: 5m + rules: + - uid: cfvmu2hs596v4d + title: Splice Rate Limiting Usage + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: prometheus + model: + editorMode: code + expr: |- + (max by (namespace, node_name, http_service, limiter, limiter_type) ( + splice_rate_limiting_max_limit_per_second{limiter_type!="per-attribute"} + ) * $SPLICE_RATE_LIMITS_USAGE_THRESHOLD) + - + sum by (namespace, node_name, http_service, limiter, limiter_type) ( + rate(splice_rate_limiting_total{limiter_type!="per-attribute"}[$__rate_interval]) + ) + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 0 + type: lt + operator: + type: and + query: + params: [] + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: splice-rate-limit-db + panelId: 6 + noDataState: OK + execErrState: Alerting + for: 5m + annotations: + description: Incoming request rate for this limiter has exceeded $SPLICE_RATE_LIMITS_USAGE_THRESHOLD of its configured splice_rate_limiting_max_limit_per_second for 5 minutes. + summary: Rate limiter "{{ $labels.limiter }}" on {{ $labels.node_name }} ({{ $labels.http_service }}) in {{ $labels.namespace }} is receiving more requests than $SPLICE_RATE_LIMITS_USAGE_THRESHOLD of its configured limit allows. + labels: + gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0AjsonPayload.response_code=429 + isPaused: false + - uid: chvmb2hs494v1y + title: Splice Rate Limiting Rejections + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: prometheus + model: + editorMode: code + expr: |- + sum by (namespace, node_name, http_service, limiter, limiter_type) ( + increase(splice_rate_limiting_total{result!="accepted"$SPLICE_RATE_LIMITS_FILTER}[$__rate_interval]) + ) + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - $SPLICE_RATE_LIMITS_REJECTION_COUNT_THRESHOLD + type: gt + operator: + type: and + query: + params: [] + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: splice-rate-limit-db + panelId: 6 + noDataState: OK + execErrState: Alerting + for: 5m + annotations: + description: This limiter has been rejecting more than $SPLICE_RATE_LIMITS_REJECTION_COUNT_THRESHOLD requests with HTTP 429 for 5 minutes. + summary: Rate limiter "{{ $labels.limiter }}" ({{ $labels.limiter_type }}) on {{ $labels.node_name }} ({{ $labels.http_service }}) in {{ $labels.namespace }} is rejecting requests. + labels: + gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0AjsonPayload.response_code=429 + isPaused: false diff --git a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering-performance.json b/cluster/pulumi/observability/grafana-dashboards/canton-bft/bft-ordering-performance.json similarity index 92% rename from canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering-performance.json rename to cluster/pulumi/observability/grafana-dashboards/canton-bft/bft-ordering-performance.json index 576d0b3589..dbd8068774 100644 --- a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering-performance.json +++ b/cluster/pulumi/observability/grafana-dashboards/canton-bft/bft-ordering-performance.json @@ -17,8 +17,8 @@ }, "editable": true, "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 8, + "graphTooltip": 1, + "id": 10, "links": [], "liveNow": false, "panels": [ @@ -40,6 +40,7 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "Wall-clock duration between the instant a request is received by the CantonBFT orderer and the instant the fully assembled ordered block metadata is stored, just before the request is pushed to the post-ordering stages. Its meaningfulness requires wall-clock synchronization, as not all ordered requests are received and timestamped on this node.", "fieldConfig": { "defaults": { "color": { @@ -571,8 +572,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -664,8 +664,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" } ] } @@ -753,8 +752,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -806,6 +804,7 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "The rate of gRPC message send retries", "fieldConfig": { "defaults": { "color": { @@ -846,8 +845,99 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by (namespace, job, node, target_sequencer) (irate(daml_sequencer_bftordering_p2p_send_sends_retried{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval]))", + "legendFormat": "{{namespace}}.{{node}}.{{target_sequencer}}", + "range": true, + "refId": "A" + } + ], + "title": "gRPC send retry rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" }, { "color": "red", @@ -939,8 +1029,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -958,7 +1047,7 @@ "x": 12, "y": 47 }, - "id": 15, + "id": 119, "options": { "legend": { "calcs": [], @@ -2743,26 +2832,13 @@ ], "title": "Availability Store Cache Statistics", "type": "timeseries" - } - ], - "title": "Mempool+Availability", - "type": "row" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 32 - }, - "id": 45, - "panels": [ + }, { "datasource": { "type": "prometheus", "uid": "prometheus" }, + "description": "Batches that have been ordered, but aren't available locally so need to be fetched from other peers.", "fieldConfig": { "defaults": { "color": { @@ -2775,7 +2851,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, @@ -2811,18 +2887,17 @@ "value": 80 } ] - }, - "unit": "s" + } }, "overrides": [] }, "gridPos": { - "h": 7, + "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 86 }, - "id": 80, + "id": 116, "options": { "legend": { "calcs": [], @@ -2831,7 +2906,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2842,13 +2917,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-consensus\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "sum by (namespace, job, node, Leader) (irate(daml_sequencer_bftordering_availability_missing_batches_need_output_fetch_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[1m]))", + "legendFormat": "(reporter {{namespace}}.{{node}}): originator {{Leader}}", "range": true, "refId": "A" } ], - "title": "Consensus module queue latency", + "title": "Number of batches that need to be fetched", "type": "timeseries" }, { @@ -2856,6 +2931,7 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "The time from first output fetch request until we get a response.", "fieldConfig": { "defaults": { "color": { @@ -2904,17 +2980,18 @@ "value": 80 } ] - } + }, + "unit": "s" }, "overrides": [] }, "gridPos": { - "h": 7, + "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 86 }, - "id": 104, + "id": 118, "options": { "legend": { "calcs": [], @@ -2923,7 +3000,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2934,13 +3011,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_consensus{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node,From) (rate(daml_sequencer_bftordering_availability_output_fetch_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", + "legendFormat": "(reporter {{namespace}}.{{node}}) from {{From}}", "range": true, "refId": "A" } ], - "title": "Consensus module queue size", + "title": "Output Fetch Latency", "type": "timeseries" }, { @@ -2948,6 +3025,7 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "The rate of batch fetch timeouts per fetch request recipient", "fieldConfig": { "defaults": { "color": { @@ -2960,7 +3038,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, @@ -3002,12 +3080,12 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 8, "w": 12, - "x": 0, - "y": 40 + "x": 12, + "y": 94 }, - "id": 81, + "id": 120, "options": { "legend": { "calcs": [], @@ -3016,7 +3094,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3026,16 +3104,40 @@ "type": "prometheus", "uid": "prometheus" }, + "disableTextWrap": false, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-segment-module\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "exemplar": false, + "expr": "increase(daml_sequencer_bftordering_availability_output_fetch_timeouts{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[1m])", + "format": "time_series", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "(reporter {{namespace}}.{{node}}) from {{From}}", "range": true, - "refId": "A" + "refId": "A", + "useBackend": false } ], - "title": "Segment module queue latency", + "title": "Output Fetch Timeouts", "type": "timeseries" - }, + } + ], + "title": "Mempool+Availability", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 45, + "panels": [ { "datasource": { "type": "prometheus", @@ -3053,7 +3155,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 0, + "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, @@ -3081,25 +3183,25 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", "value": 80 } ] - } + }, + "unit": "s" }, "overrides": [] }, "gridPos": { "h": 7, "w": 12, - "x": 12, - "y": 40 + "x": 0, + "y": 33 }, - "id": 105, + "id": 80, "options": { "legend": { "calcs": [], @@ -3119,13 +3221,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_segment_module{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-consensus\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Segment module queue size", + "title": "Consensus module queue latency", "type": "timeseries" }, { @@ -3133,7 +3235,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Block commit time at the consensus (PBFT) level.", "fieldConfig": { "defaults": { "color": { @@ -3146,7 +3247,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, @@ -3169,43 +3270,508 @@ "mode": "off" } }, - "decimals": 2, - "mappings": [ - { - "options": { - "match": "null", - "result": { - "text": "N/A" - } - }, - "type": "special" - } - ], + "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", - "value": 2 + "value": 80 } ] - }, - "unit": "s" + } }, "overrides": [] }, "gridPos": { "h": 7, "w": 12, - "x": 0, - "y": 47 + "x": 12, + "y": 33 }, - "hideTimeOverride": true, - "id": 39, + "id": 104, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_consensus{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "legendFormat": "{{namespace}}.{{node}}", + "range": true, + "refId": "A" + } + ], + "title": "Consensus module queue size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 81, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-segment-module\"}[$__rate_interval])))", + "legendFormat": "{{namespace}}.{{node}}", + "range": true, + "refId": "A" + } + ], + "title": "Segment module queue latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 105, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_segment_module{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "legendFormat": "{{namespace}}.{{node}}", + "range": true, + "refId": "A" + } + ], + "title": "Segment module queue size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Block commit time at the consensus (PBFT) level.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 2 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 47 + }, + "hideTimeOverride": true, + "id": 39, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_consensus_commit_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", + "format": "time_series", + "instant": false, + "interval": "30s", + "intervalFactor": 1, + "legendFormat": "{{namespace}}.{{node}}", + "range": true, + "refId": "A" + } + ], + "title": "Consensus Block Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Time elapsed between sending a PrePrepare and seeing that the block has been ordered", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 47 + }, + "id": 46, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-segment-proposal-to-commit-latency\"}[$__rate_interval])))", + "legendFormat": "{{namespace}}.{{node}}", + "range": true, + "refId": "A" + } + ], + "title": "Consensus segment block proposal to commit latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Time elapsed between ordered blocks proposed by a segment", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 54 + }, + "id": 88, "options": { "legend": { "calcs": [], @@ -3218,25 +3784,20 @@ "sort": "none" } }, - "pluginVersion": "10.4.0", "targets": [ { "datasource": { + "type": "prometheus", "uid": "prometheus" }, "editorMode": "code", - "exemplar": false, - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_consensus_commit_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", - "format": "time_series", - "instant": false, - "interval": "30s", - "intervalFactor": 1, + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-segment-block-commit-latency\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Consensus Block Latency", + "title": "Consensus segment block ordering latency", "type": "timeseries" }, { @@ -3244,7 +3805,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time elapsed between sending a PrePrepare and seeing that the block has been ordered", + "description": "Time between a proposal request to the availability module needed to continue a segment and a PrePrepare being built", "fieldConfig": { "defaults": { "color": { @@ -3285,8 +3846,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -3299,12 +3859,12 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 6, "w": 12, "x": 12, - "y": 47 + "y": 54 }, - "id": 46, + "id": 87, "options": { "legend": { "calcs": [], @@ -3324,13 +3884,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-segment-proposal-to-commit-latency\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-block-proposal-wait\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Consensus segment block proposal to commit latency", + "title": "Wait for proposal", "type": "timeseries" }, { @@ -3338,7 +3898,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time elapsed between ordered blocks proposed by a segment", + "description": "Time between the epoch completion and the start of the next epoch", "fieldConfig": { "defaults": { "color": { @@ -3392,12 +3952,12 @@ "overrides": [] }, "gridPos": { - "h": 6, + "h": 7, "w": 12, "x": 0, - "y": 54 + "y": 60 }, - "id": 88, + "id": 86, "options": { "legend": { "calcs": [], @@ -3417,13 +3977,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-segment-block-commit-latency\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-epoch-start-wait\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Consensus segment block ordering latency", + "title": "Wait for epoch start", "type": "timeseries" }, { @@ -3431,7 +3991,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time between a proposal request to the availability module needed to continue a segment and a PrePrepare being built", + "description": "Time between the last led segment's completion and the epoch completion", "fieldConfig": { "defaults": { "color": { @@ -3485,12 +4045,12 @@ "overrides": [] }, "gridPos": { - "h": 6, + "h": 7, "w": 12, "x": 12, - "y": 54 + "y": 60 }, - "id": 87, + "id": 83, "options": { "legend": { "calcs": [], @@ -3510,13 +4070,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-block-proposal-wait\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-epoch-completion-wait\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Wait for proposal", + "title": "Wait for epoch completion", "type": "timeseries" }, { @@ -3524,7 +4084,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time between the epoch completion and the start of the next epoch", "fieldConfig": { "defaults": { "color": { @@ -3578,12 +4137,12 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 6, "w": 12, "x": 0, - "y": 60 + "y": 67 }, - "id": 86, + "id": 63, "options": { "legend": { "calcs": [], @@ -3603,13 +4162,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-epoch-start-wait\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.startEpoch\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Wait for epoch start", + "title": "Start epoch (DB)", "type": "timeseries" }, { @@ -3617,7 +4176,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time between the last led segment's completion and the epoch completion", "fieldConfig": { "defaults": { "color": { @@ -3671,12 +4229,12 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 6, "w": 12, "x": 12, - "y": 60 + "y": 67 }, - "id": 83, + "id": 47, "options": { "legend": { "calcs": [], @@ -3696,13 +4254,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-epoch-completion-wait\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.completeEpoch\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Wait for epoch completion", + "title": "Complete epoch (DB)", "type": "timeseries" }, { @@ -3763,12 +4321,12 @@ "overrides": [] }, "gridPos": { - "h": 6, + "h": 7, "w": 12, "x": 0, - "y": 67 + "y": 73 }, - "id": 63, + "id": 64, "options": { "legend": { "calcs": [], @@ -3788,13 +4346,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.startEpoch\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPrePrepare\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Start epoch (DB)", + "title": "Insert pre-prepare", "type": "timeseries" }, { @@ -3855,12 +4413,12 @@ "overrides": [] }, "gridPos": { - "h": 6, + "h": 7, "w": 12, "x": 12, - "y": 67 + "y": 73 }, - "id": 47, + "id": 50, "options": { "legend": { "calcs": [], @@ -3880,13 +4438,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.completeEpoch\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPreparesAtomically\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Complete epoch (DB)", + "title": "Insert prepares", "type": "timeseries" }, { @@ -3947,12 +4505,12 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 6, "w": 12, "x": 0, - "y": 73 + "y": 80 }, - "id": 64, + "id": 52, "options": { "legend": { "calcs": [], @@ -3972,13 +4530,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPrePrepare\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addOrderedBlockAtomically\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Insert pre-prepare", + "title": "Insert ordered block", "type": "timeseries" }, { @@ -4039,12 +4597,12 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 6, "w": 12, "x": 12, - "y": 73 + "y": 80 }, - "id": 50, + "id": 66, "options": { "legend": { "calcs": [], @@ -4064,13 +4622,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPreparesAtomically\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-signature-verify-poa-ack\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Insert prepares", + "title": "Check PoA ack signature", "type": "timeseries" }, { @@ -4131,12 +4689,12 @@ "overrides": [] }, "gridPos": { - "h": 6, + "h": 7, "w": 12, "x": 0, - "y": 80 + "y": 86 }, - "id": 52, + "id": 68, "options": { "legend": { "calcs": [], @@ -4156,13 +4714,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addOrderedBlockAtomically\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-validate-signed-message\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Insert ordered block", + "title": "Validate message", "type": "timeseries" }, { @@ -4223,12 +4781,12 @@ "overrides": [] }, "gridPos": { - "h": 6, + "h": 7, "w": 12, "x": 12, - "y": 80 + "y": 86 }, - "id": 66, + "id": 76, "options": { "legend": { "calcs": [], @@ -4248,13 +4806,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-signature-verify-poa-ack\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"sign-BftSignedConsensusMessage\"}[$__rate_interval])))", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Check PoA ack signature", + "title": "Sign message", "type": "timeseries" }, { @@ -4274,7 +4832,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, @@ -4318,13 +4876,17 @@ "h": 7, "w": 12, "x": 0, - "y": 86 + "y": 92 }, - "id": 68, + "id": 114, "options": { "legend": { - "calcs": [], - "displayMode": "list", + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", "placement": "bottom", "showLegend": true }, @@ -4340,13 +4902,14 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-validate-signed-message\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum by(namespace, job, node, Leader) (rate(daml_sequencer_bftordering_consensus_relative_segment_latency_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", node=~\"$node\"}[$__rate_interval])))", + "interval": "", + "legendFormat": "(reporter: {{namespace}}.{{node}}) {{Leader}}", "range": true, "refId": "A" } ], - "title": "Validate message", + "title": "Relative peer segment completion delay", "type": "timeseries" }, { @@ -4354,6 +4917,7 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "Rate of empty blocks created due to segment flushing, in which a node detects that a strong quorum of peers have already finished their segment, and thus the local segment should be rushed (flushed) to completion to reduce ordering and delivery delays.", "fieldConfig": { "defaults": { "color": { @@ -4366,7 +4930,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, @@ -4394,7 +4958,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4402,7 +4967,7 @@ } ] }, - "unit": "s" + "unit": "cps" }, "overrides": [] }, @@ -4410,12 +4975,16 @@ "h": 7, "w": 12, "x": 12, - "y": 86 + "y": 92 }, - "id": 76, + "id": 115, "options": { "legend": { - "calcs": [], + "calcs": [ + "min", + "max", + "mean" + ], "displayMode": "list", "placement": "bottom", "showLegend": true @@ -4432,13 +5001,14 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"sign-BftSignedConsensusMessage\"}[$__rate_interval])))", + "expr": "irate(daml_sequencer_bftordering_consensus_flushed_blocks_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", + "interval": "", "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Sign message", + "title": "Segment Flush Block Rate", "type": "timeseries" } ], @@ -4500,8 +5070,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -4593,8 +5162,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -4686,8 +5254,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5143,8 +5710,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5186,7 +5752,7 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, daml_sequencer_bftordering_consensus_view_change_progress_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"})", + "expr": "histogram_quantile($percentile, sum by(namespace, job, node, Leader) (rate(daml_sequencer_bftordering_consensus_view_change_progress_latency_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", node=~\"$node\"}[$__rate_interval])))", "interval": "", "legendFormat": "(reporter: {{namespace}}.{{node}}) {{Leader}}", "range": true, @@ -5255,8 +5821,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5349,8 +5914,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5374,7 +5938,7 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "single", @@ -5388,7 +5952,7 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"state-transfer-total-epoch-transfer-latency\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\", ordering_stage=~\"state-transfer-total-epoch-transfer-latency\"}[$__rate_interval])))", "legendFormat": "Data transfer", "range": true, "refId": "A" @@ -5399,9 +5963,9 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"state-transfer-store-epochs\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\", ordering_stage=~\"state-transfer-store-epochs\"}[$__rate_interval])))", "hide": false, - "legendFormat": "Complete + start DB query", + "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "B" } @@ -5471,8 +6035,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5564,8 +6127,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5852,8 +6414,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5944,8 +6505,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6036,8 +6596,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6128,8 +6687,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6600,8 +7158,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6693,8 +7250,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6786,8 +7342,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6893,8 +7448,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6986,8 +7540,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7079,8 +7632,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7172,8 +7724,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7633,8 +8184,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7740,8 +8290,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7833,8 +8382,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7941,8 +8489,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8035,8 +8582,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8129,8 +8675,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8223,8 +8768,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8317,8 +8861,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8390,9 +8933,9 @@ "definition": "label_values(daml_health_status,namespace}", "hide": 0, "includeAll": true, + "label": "Namespace", "multi": true, "name": "namespace", - "label": "Namespace", "options": [], "query": { "qryType": 1, @@ -8423,9 +8966,9 @@ "definition": "label_values(daml_health_status{namespace=~\"$namespace\"},job)", "hide": 0, "includeAll": true, + "label": "Job", "multi": true, "name": "job", - "label": "Job", "options": [], "query": { "qryType": 1, @@ -8456,9 +8999,9 @@ "definition": "label_values(daml_health_status{namespace=~\"$namespace\", job=~\"$job\"},node)", "hide": 0, "includeAll": true, + "label": "Node", "multi": true, "name": "node", - "label": "Node", "options": [], "query": { "qryType": 1, diff --git a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering.json b/cluster/pulumi/observability/grafana-dashboards/canton-bft/bft-ordering.json similarity index 93% rename from canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering.json rename to cluster/pulumi/observability/grafana-dashboards/canton-bft/bft-ordering.json index 847a1c66cc..be6cd9fe46 100644 --- a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering.json +++ b/cluster/pulumi/observability/grafana-dashboards/canton-bft/bft-ordering.json @@ -18,7 +18,7 @@ "description": "", "editable": true, "fiscalYearStartMonth": 0, - "graphTooltip": 0, + "graphTooltip": 1, "id": 8, "links": [], "liveNow": false, @@ -58,7 +58,37 @@ "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } }, "decimals": 0, "mappings": [], @@ -85,20 +115,16 @@ "id": 72, "maxDataPoints": 100, "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + "tooltip": { + "mode": "multi", + "sort": "none" + } }, "pluginVersion": "9.5.12", "targets": [ @@ -117,7 +143,7 @@ } ], "title": "Current Epoch", - "type": "stat" + "type": "timeseries" }, { "datasource": { @@ -1225,6 +1251,7 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "Wall-clock duration between the instant a request is received by the CantonBFT orderer and the instant the fully assembled ordered block metadata is stored, just before the request is pushed to the post-ordering stages. Its meaningfulness requires wall-clock synchronization, as not all ordered requests are received and timestamped on this node.", "fieldConfig": { "defaults": { "color": { @@ -1572,7 +1599,7 @@ "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", + "legendFormat": "{{namespace}}.{{node}} ({{mode}})", "refId": "A" } ], @@ -1812,7 +1839,7 @@ "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", + "legendFormat": "{{namespace}}.{{node}} ({{mode}})", "refId": "A" } ], @@ -2164,10 +2191,6 @@ "title": "Sequencer core buffer size (blocks #)", "type": "timeseries" }, - - - - { "datasource": { "type": "prometheus", @@ -2266,7 +2289,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Size of the stream buffer at various stages", + "description": "", "fieldConfig": { "defaults": { "color": { @@ -2309,7 +2332,7 @@ "steps": [ { "color": "green", - "value": 0 + "value": null } ] }, @@ -2323,6 +2346,220 @@ "x": 12, "y": 94 }, + "hideTimeOverride": true, + "id": 105, + "options": { + "legend": { + "calcs": [ + "max", + "min", + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "increase(daml_sequencer_bftordering_topology_blacklisted_epochs{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[5m])", + "format": "time_series", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "(reporter: {{namespace}}.{{node}}): {{sequencer_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Total number of epochs a sequencer was blacklisted in the last 5m", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 94 + }, + "hideTimeOverride": true, + "id": 105, + "options": { + "legend": { + "calcs": [ + "max", + "min", + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "daml_sequencer_bftordering_topology_blacklisted_epochs{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}\n", + "format": "time_series", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "(reporter: {{namespace}}.{{node}}): {{sequencer_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Total number of epochs a sequencer is blacklisted", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Size of the stream buffer at various stages", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 101 + }, "id": 104, "options": { "legend": { @@ -2436,7 +2673,7 @@ "h": 7, "w": 6, "x": 12, - "y": 94 + "y": 101 }, "hideTimeOverride": true, "id": 85, @@ -2570,7 +2807,7 @@ "h": 7, "w": 6, "x": 18, - "y": 94 + "y": 101 }, "hideTimeOverride": false, "id": 50, @@ -2658,7 +2895,7 @@ "h": 28, "w": 24, "x": 0, - "y": 101 + "y": 108 }, "hideTimeOverride": false, "id": 73, @@ -2820,7 +3057,7 @@ "h": 7, "w": 12, "x": 0, - "y": 129 + "y": 136 }, "hideTimeOverride": false, "id": 74, @@ -2917,8 +3154,7 @@ "value": 20 } ] - }, - "unit": "short" + } }, "overrides": [] }, @@ -2926,7 +3162,7 @@ "h": 7, "w": 12, "x": 12, - "y": 129 + "y": 136 }, "hideTimeOverride": true, "id": 78, @@ -3031,7 +3267,7 @@ "h": 7, "w": 12, "x": 0, - "y": 136 + "y": 143 }, "hideTimeOverride": true, "id": 77, @@ -3136,7 +3372,7 @@ "h": 7, "w": 12, "x": 12, - "y": 136 + "y": 143 }, "hideTimeOverride": true, "id": 79, @@ -3241,7 +3477,7 @@ "h": 7, "w": 12, "x": 0, - "y": 143 + "y": 150 }, "hideTimeOverride": true, "id": 82, @@ -3346,7 +3582,7 @@ "h": 7, "w": 12, "x": 12, - "y": 143 + "y": 150 }, "hideTimeOverride": true, "id": 76, @@ -3393,7 +3629,7 @@ "h": 1, "w": 24, "x": 0, - "y": 150 + "y": 157 }, "id": 55, "panels": [], @@ -3479,7 +3715,7 @@ "h": 7, "w": 12, "x": 0, - "y": 151 + "y": 158 }, "hideTimeOverride": true, "id": 83, @@ -3580,7 +3816,7 @@ "h": 14, "w": 6, "x": 12, - "y": 151 + "y": 158 }, "id": 59, "options": { @@ -3673,7 +3909,7 @@ "h": 14, "w": 6, "x": 18, - "y": 151 + "y": 158 }, "id": 58, "options": { @@ -3774,7 +4010,7 @@ "h": 7, "w": 12, "x": 0, - "y": 158 + "y": 165 }, "hideTimeOverride": true, "id": 84, @@ -3812,7 +4048,7 @@ "type": "timeseries" } ], - "refresh": "5s", + "refresh": "30s", "schemaVersion": 38, "style": "dark", "tags": [ diff --git a/cluster/pulumi/observability/grafana-dashboards/canton-network/app-rewards.json b/cluster/pulumi/observability/grafana-dashboards/canton-network/app-rewards.json index 8ea7846361..d648b9e431 100644 --- a/cluster/pulumi/observability/grafana-dashboards/canton-network/app-rewards.json +++ b/cluster/pulumi/observability/grafana-dashboards/canton-network/app-rewards.json @@ -360,7 +360,32 @@ ] } }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 200 + } + ] + } + ] }, "gridPos": { "h": 8, @@ -371,6 +396,7 @@ "id": 32, "options": { "cellHeight": "sm", + "enablePagination": true, "showHeader": true }, "pluginVersion": "12.4.0", @@ -378,7 +404,8 @@ { "editorMode": "code", "exemplar": false, - "expr": "max by (namespace)(splice_scan_reward_computation_reward_coupons_v2_hidden_coupons{namespace=~\"$namespace\"})", + "expr": "max by (party)(splice_reward_coupons_v2_hidden_coupons{namespace=~\"$namespace\"})", + "format": "table", "instant": true, "legendFormat": "__auto", "range": false, @@ -386,6 +413,31 @@ } ], "title": "Parties with hidden coupons", + "transformations": [ + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Value" + } + ] + } + }, + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "party", + "Value" + ] + } + } + } + ], "type": "table" }, { @@ -1149,9 +1201,7 @@ "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { - "calcs": [ - "median" - ], + "calcs": ["median"], "fields": "", "values": false }, @@ -1210,9 +1260,7 @@ "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { - "calcs": [ - "lastNotNull" - ], + "calcs": ["lastNotNull"], "fields": "", "values": false }, @@ -1271,9 +1319,7 @@ "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { - "calcs": [ - "lastNotNull" - ], + "calcs": ["lastNotNull"], "fields": "", "values": false }, @@ -1332,9 +1378,7 @@ "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { - "calcs": [ - "lastNotNull" - ], + "calcs": ["lastNotNull"], "fields": "", "values": false }, diff --git a/cluster/pulumi/observability/grafana-dashboards/canton-network/global-sync-utilization.json b/cluster/pulumi/observability/grafana-dashboards/canton-network/global-sync-utilization.json index b9167cb768..7765eaecac 100644 --- a/cluster/pulumi/observability/grafana-dashboards/canton-network/global-sync-utilization.json +++ b/cluster/pulumi/observability/grafana-dashboards/canton-network/global-sync-utilization.json @@ -636,13 +636,297 @@ "targets": [ { "editorMode": "code", - "expr": "1 - sum by (namespace, migration_id) (rate(daml_mediator_approved_requests_total{namespace=~\"sv-1\"}[30m])) / sum by (namespace, migration_id)(rate(daml_mediator_requests_total{namespace=~\"sv-1\"}[30m]))", + "expr": "1 - sum by (namespace, migration_id) (rate(daml_mediator_approved_requests_total{namespace=~\"sv-1\"}[30m])) / sum by (namespace, migration_id)(rate(daml_mediator_requests_total{namespace=~\"sv-1\", duplicate_reject=\"false\"}[30m]))", "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Failed Confirmation Request Rate", + "title": "Failed Confirmation Request Rate (excluding duplicate requests)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Confirmation requests with duplicate confirmation request UUID. These get rejected by the mediator.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 28 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "sum by (namespace, migration_id)(rate(daml_mediator_requests_total{namespace=~\"sv-1\", duplicate_reject=\"true\"}[30m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Duplicate Confirmation Requests/s", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Confirmation requests that were sequenced but never processed by the mediator (e.g. dropped due to CometBFT replays): sequenced send-confirmation-request events minus requests received by the mediator.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 36 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "editorMode": "code", + "expr": "sum by (namespace) (rate(daml_sequencer_block_events_total{namespace=~\"$namespace\",type=\"send-confirmation-request\"}[$__rate_interval])) - sum by (namespace) (rate(daml_mediator_requests_total{namespace=~\"$namespace\"}[$__rate_interval]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Discarded confirmation requests/s", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Fraction of sequenced confirmation requests that were never processed by the mediator (e.g. dropped due to CometBFT replays).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 36 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "editorMode": "code", + "expr": "(sum by (namespace) (rate(daml_sequencer_block_events_total{namespace=~\"$namespace\",type=\"send-confirmation-request\"}[10m])) - sum by (namespace) (rate(daml_mediator_requests_total{namespace=~\"$namespace\"}[10m]))) / sum by (namespace) (rate(daml_sequencer_block_events_total{namespace=~\"$namespace\",type=\"send-confirmation-request\"}[10m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Discarded confirmation requests (fraction)", "type": "timeseries" } ], diff --git a/cluster/pulumi/observability/grafana-dashboards/canton-network/treasury-service.json b/cluster/pulumi/observability/grafana-dashboards/canton-network/treasury-service.json new file mode 100644 index 0000000000..5b3626dd13 --- /dev/null +++ b/cluster/pulumi/observability/grafana-dashboards/canton-network/treasury-service.json @@ -0,0 +1,323 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Time items spent in the treasury service queue before being processed.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 27, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "histogram_quantile($quantile, sum by (namespace, job, owner) (rate(splice_wallet_treasury_queue_latency_duration_seconds[$__rate_interval])))", + "legendFormat": "{{namespace}}-{{job}}-{{owner}}", + "range": true, + "refId": "A" + } + ], + "title": "Treasury service queue latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Size of the queue used by the treasury service automation", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "max by (namespace, job, owner) (splice_wallet_treasury_queue_size{namespace=~\"$namespace\",job=~\"$job\",owner=~\"$owner\"})", + "legendFormat": "{{namespace}}-{{job}}-{{owner}}", + "range": true, + "refId": "A" + } + ], + "title": "Treasury Service Queue Size", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "30s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(splice_wallet_treasury_queue_size,namespace)", + "includeAll": true, + "label": "Namespace", + "name": "namespace", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(splice_wallet_treasury_queue_size,namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "regexApplyTo": "value", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(splice_wallet_treasury_queue_size,job)", + "includeAll": true, + "multi": true, + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(splice_wallet_treasury_queue_size,job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "regexApplyTo": "value", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(splice_wallet_treasury_queue_size,owner)", + "includeAll": true, + "multi": true, + "name": "owner", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(splice_wallet_treasury_queue_size,owner)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "regexApplyTo": "value", + "type": "query" + }, + { + "allowCustomValue": false, + "current": { + "text": "0.5", + "value": "0.5" + }, + "name": "quantile", + "options": [], + "query": "0.5,0.9,0.99,0.999", + "type": "custom", + "valuesFormat": "csv" + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Treasury Service", + "uid": "cn255kr", + "version": 12, + "weekStart": "" +} diff --git a/cluster/pulumi/observability/grafana-dashboards/canton-network/validator-scan-connections.json b/cluster/pulumi/observability/grafana-dashboards/canton-network/validator-scan-connections.json index ea5f32d192..66a5b90bbf 100644 --- a/cluster/pulumi/observability/grafana-dashboards/canton-network/validator-scan-connections.json +++ b/cluster/pulumi/observability/grafana-dashboards/canton-network/validator-scan-connections.json @@ -824,6 +824,128 @@ ], "title": "Successful responses disagreeing with BFT consensus ", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 58 + }, + "id": 101, + "panels": [], + "title": "Request outcomes per scan connection", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Total number of requests to this scan connection over the selected time range, split by outcome (ok, or the failure category with its HTTP status code when available).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "success" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 0, + "y": 59 + }, + "id": 9, + "options": { + "displayLabels": [ + "percent", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "values": [ + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "repeat": "scan_connection", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (outcome, http_status) (increase(splice_validator_scan_per_connection_calls_total{namespace=~\"$namespace\", job=~\"$job\", node_name=~\"$node_name\", scan_connection=~\"$scan_connection\", request=~\"${per_connection_request}\"}[$__range]))", + "instant": true, + "legendFormat": "{{outcome}} - {{http_status}}", + "range": false, + "refId": "A" + } + ], + "title": "$scan_connection", + "transformations": [ + { + "id": "renameByRegex", + "options": { + "regex": "^ok.*$", + "renamePattern": "success" + } + }, + { + "id": "renameByRegex", + "options": { + "regex": "^(.*) - (none)?$", + "renamePattern": "$1" + } + } + ], + "type": "piechart" } ], "preload": false, diff --git a/cluster/pulumi/observability/grafana-dashboards/canton/acknowledgements.json b/cluster/pulumi/observability/grafana-dashboards/canton/acknowledgements.json index f50eba0f85..b01e06a61a 100644 --- a/cluster/pulumi/observability/grafana-dashboards/canton/acknowledgements.json +++ b/cluster/pulumi/observability/grafana-dashboards/canton/acknowledgements.json @@ -448,7 +448,7 @@ "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 2, - "regex": "/global-domain-(?\\d)-sequencer/g", + "regex": "/global-domain-(?\\d+)-sequencer/g", "sort": 1, "type": "query" }, diff --git a/cluster/pulumi/observability/grafana-dashboards/canton/lsu-status.json b/cluster/pulumi/observability/grafana-dashboards/canton/lsu-status.json index 4c2ac3e811..76d09f4923 100644 --- a/cluster/pulumi/observability/grafana-dashboards/canton/lsu-status.json +++ b/cluster/pulumi/observability/grafana-dashboards/canton/lsu-status.json @@ -458,7 +458,56 @@ ] } }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "successor_psid" + }, + "properties": [ + { + "id": "custom.width", + "value": 749 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "namespace" + }, + "properties": [ + { + "id": "custom.width", + "value": 144 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "job" + }, + "properties": [ + { + "id": "custom.width", + "value": 147 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "custom.width", + "value": 544 + } + ] + } + ] }, "gridPos": { "h": 10, @@ -488,7 +537,7 @@ }, "disableTextWrap": false, "editorMode": "code", - "expr": "max by (job, node) (daml_participant_lsu_status{namespace=\"$namespace\"})", + "expr": "daml_participant_lsu_status{namespace=\"$namespace\"}", "format": "table", "fullMetaSearch": false, "includeNullMetadata": true, @@ -505,8 +554,19 @@ "id": "organize", "options": { "excludeByName": { - "Time": true + "Time": true, + "__name__": true, + "component": true, + "container": true, + "endpoint": true, + "instance": true, + "job": false, + "node": true, + "otel_scope_name": true, + "pod": true, + "service": true }, + "includeByName": {}, "indexByName": {}, "renameByName": {} } @@ -548,10 +608,10 @@ { "current": { "text": [ - "global-domain-8-sequencer" + "All" ], "value": [ - "global-domain-8-sequencer" + "$__all" ] }, "datasource": { diff --git a/cluster/pulumi/observability/grafana-dashboards/canton/unresponsive_parties.json b/cluster/pulumi/observability/grafana-dashboards/canton/unresponsive_parties.json index 4cc0a1b83c..2d56e45941 100644 --- a/cluster/pulumi/observability/grafana-dashboards/canton/unresponsive_parties.json +++ b/cluster/pulumi/observability/grafana-dashboards/canton/unresponsive_parties.json @@ -297,8 +297,8 @@ }, "tooltip": { "hideZeros": false, - "mode": "single", - "sort": "none" + "mode": "multi", + "sort": "desc" } }, "pluginVersion": "12.4.0", diff --git a/cluster/pulumi/observability/grafana-dashboards/jvm/jvm.json b/cluster/pulumi/observability/grafana-dashboards/jvm/jvm.json index d1ebc8fc65..d9927705ad 100644 --- a/cluster/pulumi/observability/grafana-dashboards/jvm/jvm.json +++ b/cluster/pulumi/observability/grafana-dashboards/jvm/jvm.json @@ -24,7 +24,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, - "id": 9858, "links": [], "panels": [ { @@ -89,7 +88,7 @@ "showThresholdMarkers": true, "sizing": "auto" }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -146,6 +145,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -193,7 +193,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -250,6 +250,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -297,7 +298,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -361,6 +362,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -407,7 +409,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -476,6 +478,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -522,7 +525,7 @@ "sort": "none" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -573,6 +576,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -620,7 +624,7 @@ "sort": "none" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -640,7 +644,6 @@ }, "editorMode": "code", "expr": "sum by(jvm_gc_action, jvm_gc_name, container, pod, namespace) (histogram_avg(rate(jvm_gc_duration_seconds{namespace=~\"$namespace\", pod=~\"$pod\", container=~\"$container\", service=~\"$app\"}[$__rate_interval])))", - "hide": false, "legendFormat": "avg {{namespace}} {{jvm_gc_name}} {{jvm_gc_action}} {{pod}} {{container}}", "range": true, "refId": "B" @@ -696,6 +699,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -743,7 +747,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -794,6 +798,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -841,7 +846,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -892,6 +897,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -939,7 +945,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -990,6 +996,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1037,7 +1044,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -1088,6 +1095,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1135,7 +1143,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -1143,7 +1151,7 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "jvm_buffer_memory_usage_bytes{namespace=~\"$namespace\", pod=~\"$pod\", container=~\"$container\", service=~\"$app\"}", + "expr": "jvm_buffer_memory_used_bytes{namespace=~\"$namespace\", pod=~\"$pod\", container=~\"$container\", service=~\"$app\"}", "legendFormat": "{{pod}} {{container}} - {{jvm_buffer_pool_name}}", "range": true, "refId": "A" @@ -1186,6 +1194,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1233,7 +1242,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -1253,7 +1262,7 @@ ], "preload": false, "refresh": "", - "schemaVersion": 41, + "schemaVersion": 42, "tags": [ "jvm" ], @@ -1299,14 +1308,13 @@ }, "refresh": 2, "regex": "", + "regexApplyTo": "value", "type": "query" }, { "allValue": ".*", "current": { - "text": [ - "All" - ], + "text": "All", "value": [ "$__all" ] @@ -1329,6 +1337,7 @@ }, "refresh": 2, "regex": "", + "regexApplyTo": "value", "sort": 1, "type": "query" }, @@ -1358,6 +1367,7 @@ }, "refresh": 2, "regex": "", + "regexApplyTo": "value", "sort": 1, "type": "query" }, @@ -1365,9 +1375,7 @@ "allValue": ".*", "current": { "text": "All", - "value": [ - "$__all" - ] + "value": "$__all" }, "datasource": { "type": "prometheus", @@ -1387,6 +1395,7 @@ }, "refresh": 2, "regex": "", + "regexApplyTo": "value", "sort": 1, "type": "query" } @@ -1400,5 +1409,6 @@ "timezone": "", "title": "JVM Metrics", "uid": "rgt7PhA4z", - "version": 1 + "version": 1, + "weekStart": "" } diff --git a/cluster/pulumi/observability/grafana-dashboards/platform/http_client.json b/cluster/pulumi/observability/grafana-dashboards/platform/http_client.json index dd6d4accec..dcfb5ae269 100644 --- a/cluster/pulumi/observability/grafana-dashboards/platform/http_client.json +++ b/cluster/pulumi/observability/grafana-dashboards/platform/http_client.json @@ -18,7 +18,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, - "id": 15005, "links": [], "panels": [ { @@ -55,6 +54,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -83,8 +83,8 @@ "overrides": [] }, "gridPos": { - "h": 11, - "w": 15, + "h": 9, + "w": 24, "x": 0, "y": 0 }, @@ -96,7 +96,9 @@ ], "displayMode": "table", "placement": "right", - "showLegend": true + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true }, "tooltip": { "hideZeros": false, @@ -104,7 +106,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -116,7 +118,7 @@ "expr": "histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", operation=~\"$operation\", status_code=~\"2.*\"}[$__rate_interval]))", "fullMetaSearch": false, "includeNullMetadata": false, - "legendFormat": "{{namespace}} {{job}} {{http_client}} {{operation}} {{status_code}}", + "legendFormat": "{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}", "range": true, "refId": "A", "useBackend": false @@ -159,6 +161,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -181,41 +184,44 @@ "value": 80 } ] - } + }, + "unit": "s" }, "overrides": [] }, "gridPos": { - "h": 11, - "w": 9, - "x": 15, - "y": 0 + "h": 8, + "w": 24, + "x": 0, + "y": 9 }, - "id": 2, + "id": 4, "options": { "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", "showLegend": true }, "tooltip": { - "hideZeros": false, + "hideZeros": true, "mode": "multi", "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "editorMode": "code", - "expr": "histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", operation=~\"$operation\", status_code!~\"2.*\"}[$__rate_interval]))", - "legendFormat": "{{namespace}} {{job}} {{http_client}} {{operation}} {{status_code}}", + "expr": "histogram_quantile($percentile, rate(daml_http_client_requests_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", http_client=~\"$http_client\", operation=~\"$operation\"}[$__rate_interval]))", + "legendFormat": "{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}", "range": true, "refId": "A" } ], - "title": "Errors", + "title": "Client Requests Timing quantile 0.95 ", "type": "timeseries" }, { @@ -252,6 +258,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -274,46 +281,170 @@ "value": 80 } ] - }, - "unit": "s" + } }, "overrides": [] }, "gridPos": { - "h": 8, - "w": 24, + "h": 11, + "w": 15, "x": 0, - "y": 11 + "y": 17 }, - "id": 4, + "id": 2, "options": { "legend": { - "calcs": [ - "lastNotNull" - ], + "calcs": [], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { - "hideZeros": true, + "hideZeros": false, "mode": "multi", "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "editorMode": "code", - "expr": "histogram_quantile($percentile, rate(daml_http_client_requests_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", http_client=~\"$http_client\", operation=~\"$operation\"}[$__rate_interval]))", - "legendFormat": "{{namespace}} {{job}} {{http_client}} {{operation}} {{status_code}}", + "expr": "histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", operation=~\"$operation\", status_code!~\"2.*\"}[$__rate_interval]))", + "legendFormat": "{{namespace}} {{job}} {{http_client}} {{target_host}} {{operation}} {{status}} {{status_code}}", "range": true, "refId": "A" } ], - "title": "Client Requests Timing quantile 0.95 ", + "title": "Errors", "type": "timeseries" }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status_code" + }, + "properties": [ + { + "id": "custom.width", + "value": 125 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "target_host" + }, + "properties": [ + { + "id": "custom.width", + "value": 375 + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 9, + "x": 15, + "y": 17 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Value" + } + ] + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "builder", + "exemplar": false, + "expr": "sum by(namespace, job, http_client, operation, status_code, target_host) (histogram_count(increase(daml_http_client_requests_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", operation=~\"$operation\", status_code!~\"2.*\"}[$__range])))", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Errors", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "endpoint": true, + "instance": true + }, + "includeByName": {}, + "indexByName": { + "Time": 2, + "Value": 15, + "endpoint": 3, + "http_client": 4, + "instance": 5, + "job": 1, + "namespace": 0, + "node_name": 6, + "node_type": 7, + "operation": 8, + "otel_scope_name": 9, + "pod": 10, + "service": 11, + "status": 12, + "status_code": 13, + "target_host": 14 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, { "datasource": { "type": "prometheus", @@ -348,6 +479,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -378,7 +510,7 @@ "h": 8, "w": 24, "x": 0, - "y": 19 + "y": 28 }, "id": 3, "options": { @@ -396,12 +528,12 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "editorMode": "code", "expr": "daml_http_client_requests_inflight{namespace=~\"$namespace\", job=~\"$job\", http_client=~\"$http_client\", operation=~\"$operation\"}\n", - "legendFormat": "{{namespace}} {{job}} {{http_client}} {{operation}} {{status_code}}", + "legendFormat": "{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}", "range": true, "refId": "A" } @@ -411,7 +543,7 @@ } ], "preload": false, - "schemaVersion": 41, + "schemaVersion": 42, "tags": [], "templating": { "list": [ @@ -437,13 +569,18 @@ }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query" }, { "allowCustomValue": false, "current": { - "text": "All", - "value": "$__all" + "text": [ + "All" + ], + "value": [ + "$__all" + ] }, "definition": "label_values(daml_http_client_requests_duration_seconds{namespace=~\"$namespace\"},job)", "description": "", @@ -458,13 +595,18 @@ }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query" }, { "allowCustomValue": false, "current": { - "text": "All", - "value": "$__all" + "text": [ + "All" + ], + "value": [ + "$__all" + ] }, "definition": "label_values(daml_http_client_requests_duration_seconds{namespace=~\"$namespace\", job=~\"$job\", http_client=~\"$http_client\"},operation)", "description": "", @@ -479,6 +621,7 @@ }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query" }, { @@ -500,6 +643,7 @@ }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query" }, { @@ -508,35 +652,31 @@ "value": "0.9" }, "name": "percentile", - "options": [ - { - "selected": true, - "text": "0.9", - "value": "0.9" - }, - { - "selected": false, - "text": "0.95", - "value": "0.95" - }, - { - "selected": false, - "text": "0.99", - "value": "0.99" - } - ], + "options": [], "query": "0.9,0.95,0.99", - "type": "custom" + "type": "custom", + "valuesFormat": "csv" + }, + { + "baseFilters": [], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "filters": [], + "name": "filter", + "type": "adhoc" } ] }, "time": { - "from": "now-12h", + "from": "now-15m", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Http Client", "uid": "a8a3113a-bccf-4728-b752-dd7a5d6f9bda", - "version": 3 + "version": 3, + "weekStart": "" } diff --git a/cluster/pulumi/observability/grafana-dashboards/platform/istio.json b/cluster/pulumi/observability/grafana-dashboards/platform/istio.json new file mode 100644 index 0000000000..eaee05bd49 --- /dev/null +++ b/cluster/pulumi/observability/grafana-dashboards/platform/istio.json @@ -0,0 +1,651 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Rate of HTTP 429 (Too Many Requests) responses per second, broken down by the namespace reporting the request and the destination service receiving it.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "sum by(namespace, destination_service) (rate(istio_requests_total{response_code=\"429\", namespace=~\"$namespace\"}[$__rate_interval]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Istio total rejected requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Fraction of requests evaluated by the Envoy local rate limit filter that were rejected with HTTP 429, per namespace and pod.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)\n/\nsum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enabled{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "{{namespace}} - {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Rejection ratio", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Rate of number of requests for which rate limiting was applied (e.g.: 429 returned)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)", + "legendFormat": "{{namespace}} - {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Enforced requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Rate for responses without an available token (but not necessarily enforced)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_rate_limited{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace,job)", + "legendFormat": "{{namespace}} - {{job}}", + "range": true, + "refId": "A" + } + ], + "title": "Rejected requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Rate for number of requests for which the rate limiter was consulted", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enabled{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)", + "legendFormat": "{{namespace}} - {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Enabled requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Rate for number of requests under limit responses from the token bucket", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_ok{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)", + "legendFormat": "{{namespace}} - {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Allowed requests", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "allowCustomValue": false, + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(namespace)", + "includeAll": true, + "multi": true, + "name": "namespace", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "regexApplyTo": "value", + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Istio Rate Limiting", + "uid": "cnr56dj", + "version": 3, + "weekStart": "" +} diff --git a/cluster/pulumi/observability/grafana-dashboards/platform/rate_limiters.json b/cluster/pulumi/observability/grafana-dashboards/platform/rate_limiters.json index 521957dc9f..38af12e07f 100644 --- a/cluster/pulumi/observability/grafana-dashboards/platform/rate_limiters.json +++ b/cluster/pulumi/observability/grafana-dashboards/platform/rate_limiters.json @@ -24,7 +24,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 510, "links": [], "panels": [ { @@ -61,6 +60,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -89,7 +89,7 @@ "overrides": [] }, "gridPos": { - "h": 9, + "h": 8, "w": 12, "x": 0, "y": 0 @@ -108,7 +108,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -116,14 +116,14 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "sum(rate(splice_rate_limiting_total{namespace=~\"$namespace\", http_service=~\"$http_service\", limiter=~\"$limiter\", node_name=~\"$node_name\", result!=\"accepted\"}[$__rate_interval])) by (result, limiter)", + "expr": "sum(rate(splice_rate_limiting_total{namespace=~\"$namespace\", http_service=~\"$http_service\", limiter=~\"$limiter\", node_name=~\"$node_name\", result!=\"accepted\"}[$__rate_interval])) by (result)", "instant": false, - "legendFormat": "{{result}}", + "legendFormat": "{{ result }}", "range": true, "refId": "A" } ], - "title": "Rejections", + "title": "Total Rejections", "type": "timeseries" }, { @@ -149,7 +149,7 @@ "overrides": [] }, "gridPos": { - "h": 9, + "h": 8, "w": 12, "x": 12, "y": 0 @@ -177,13 +177,14 @@ "fields": "", "values": false }, + "sort": "desc", "tooltip": { "hideZeros": false, "mode": "multi", "sort": "none" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -235,6 +236,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -263,12 +265,116 @@ "overrides": [] }, "gridPos": { - "h": 12, + "h": 9, "w": 24, "x": 0, - "y": 9 + "y": 8 }, "id": 5, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(splice_rate_limiting_total{namespace=~\"$namespace\", http_service=~\"$http_service\", limiter=~\"$limiter\", node_name=~\"$node_name\"}[$__rate_interval])) by (limiter, limiter_type, limiter_attribute)", + "instant": false, + "legendFormat": "{{ limiter }} {{limiter_type}} {{limiter_attribute}}", + "range": true, + "refId": "A" + } + ], + "title": "Total Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 6, "options": { "legend": { "calcs": [], @@ -282,7 +388,7 @@ "sort": "desc" } }, - "pluginVersion": "12.1.1", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -290,33 +396,229 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "sum(rate(splice_rate_limiting_total{namespace=~\"$namespace\", http_service=~\"$http_service\", limiter=~\"$limiter\", node_name=~\"$node_name\"}[$__rate_interval])) by (limiter)", + "expr": "sum(rate(splice_rate_limiting_total{namespace=~\"$namespace\", http_service=~\"$http_service\", limiter=~\"$limiter\", node_name=~\"$node_name\", result!=\"accepted\"}[$__rate_interval])) by (result, limiter, limiter_type, limiter_attribute)", "instant": false, - "legendFormat": "{{limiter}}", + "legendFormat": "{{ limiter }} {{limiter_type}} {{limiter_attribute}}", "range": true, "refId": "A" + } + ], + "title": "Rejections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.4.0", + "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, "editorMode": "code", - "expr": "splice_rate_limiting_max_limit_per_second", - "hide": false, + "expr": "splice_rate_limiting_max_limit_per_second{namespace=~\"$namespace\", node_name=~\"$node_name\", http_service=~\"$http_service\", limiter=~\"$limiter\"}", "instant": false, - "legendFormat": "{{limiter}}", + "legendFormat": "{{ limiter }} {{limiter_type}}{{limiter_attribute}}", "range": true, - "refId": "B" + "refId": "A" } ], - "title": "Total Requests", + "title": "Limits", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Cannot extract the attribute value", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 33 + }, + "id": 8, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(rate_limiting_unknown_attribute_not_limited{namespace=~\"$namespace\", http_service=~\"$http_service\", limiter=~\"$limiter\", node_name=~\"$node_name\"}[$__rate_interval])) by (limiter, limiter_type, limiter_attribute)", + "instant": false, + "legendFormat": "{{ limiter }} {{limiter_type}} {{limiter_attribute}}", + "range": true, + "refId": "A" + } + ], + "title": "Attribute Limits not enforced", "type": "timeseries" } ], "preload": false, "refresh": "1m", - "schemaVersion": 41, + "schemaVersion": 42, "tags": [ "prometheus", "rate-limiting" @@ -326,12 +628,8 @@ { "allowCustomValue": false, "current": { - "text": [ - "sv-1" - ], - "value": [ - "sv-1" - ] + "text": "sv-1", + "value": "sv-1" }, "datasource": { "type": "prometheus", @@ -348,6 +646,7 @@ }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query" }, { @@ -377,6 +676,7 @@ }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "sort": 3, "type": "query" }, @@ -384,7 +684,9 @@ "allowCustomValue": false, "current": { "text": "All", - "value": "$__all" + "value": [ + "$__all" + ] }, "datasource": { "type": "prometheus", @@ -403,6 +705,7 @@ }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "sort": 1, "type": "query" }, @@ -410,7 +713,9 @@ "allowCustomValue": false, "current": { "text": "All", - "value": "$__all" + "value": [ + "$__all" + ] }, "datasource": { "type": "prometheus", @@ -429,18 +734,20 @@ }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "sort": 2, "type": "query" } ] }, "time": { - "from": "now-6h", + "from": "now-12h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Splice Rate Limiting", "uid": "splice-rate-limit-db", - "version": 3 + "version": 2, + "weekStart": "" } diff --git a/cluster/pulumi/observability/grafana-dashboards/splice-stores/acs-size.json b/cluster/pulumi/observability/grafana-dashboards/splice-stores/acs-size.json index 247d158258..f537aebbf3 100644 --- a/cluster/pulumi/observability/grafana-dashboards/splice-stores/acs-size.json +++ b/cluster/pulumi/observability/grafana-dashboards/splice-stores/acs-size.json @@ -18,7 +18,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 3573, "links": [], "panels": [ { @@ -26,73 +25,85 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "Number of rows in the latest saved ACS snapshot, as reported by the SV Scan app. This is the metric behind the 'ACS growth' alert. Note that it counts rows, not contracts: contracts with multiple stakeholders are counted multiple times. A value of -1 means the size of the last snapshot save is unknown.", "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" }, "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" }, - "inspect": false + "thresholdsStyle": { + "mode": "off" + } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", "value": 80 } ] - } + }, + "unit": "short" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Trend #A" - }, - "properties": [ - { - "id": "displayName", - "value": "ACS size" - } - ] - } - ] + "overrides": [] }, "gridPos": { - "h": 26, + "h": 10, "w": 24, "x": 0, "y": 0 }, "id": 2, "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "ACS size" - } - ] + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -100,25 +111,15 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "sum by (namespace, store_name, store_party) (splice_store_acs_size{namespace=~\"$namespace\",store_name=~\"$store_name\"})", + "expr": "splice_history_acs_snapshots_snapshot_size{namespace=~\"$namespace\"}", "instant": false, - "legendFormat": "__auto", + "legendFormat": "{{namespace}}", "range": true, "refId": "A" } ], - "title": "ACS Size", - "transformations": [ - { - "id": "timeSeriesTable", - "options": { - "A": { - "timeField": "Time" - } - } - } - ], - "type": "table" + "title": "ACS size (rows in latest saved snapshot)", + "type": "timeseries" }, { "datasource": { @@ -154,6 +155,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -168,7 +170,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -184,7 +187,7 @@ "h": 20, "w": 24, "x": 0, - "y": 26 + "y": 10 }, "id": 1, "options": { @@ -200,7 +203,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.4.0", "targets": [ { "datasource": { @@ -208,20 +211,20 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "sum without (__name__,endpoint,instance,job,migration,otel_scope) (splice_store_acs_size{namespace=~\"$namespace\",store_name=~\"$store_name\"})", + "expr": "(increase(splice_store_acs_size_increase_total{namespace=~\"$namespace\",store_name=~\"$store_name\"}[24h]))-(increase(splice_store_acs_size_decrease_total{namespace=~\"$namespace\",store_name=~\"$store_name\"}[24h]))", "instant": false, - "legendFormat": "__auto", + "legendFormat": "{{namespace}} {{store_name}}", "range": true, "refId": "A" } ], - "title": "Splice Store ACS Size", + "title": "Splice Store ACS Change in last 24h", "type": "timeseries" } ], "preload": false, "refresh": "", - "schemaVersion": 41, + "schemaVersion": 42, "tags": [], "templating": { "list": [ @@ -235,18 +238,19 @@ "type": "prometheus", "uid": "prometheus" }, - "definition": "label_values(splice_store_acs_size,namespace)", + "definition": "label_values(splice_store_acs_size_increase_total,namespace)", "includeAll": true, "multi": true, "name": "namespace", "options": [], "query": { "qryType": 1, - "query": "label_values(splice_store_acs_size,namespace)", + "query": "label_values(splice_store_acs_size_increase_total,namespace)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query" }, { @@ -259,18 +263,19 @@ "type": "prometheus", "uid": "prometheus" }, - "definition": "label_values(splice_store_acs_size,store_name)", + "definition": "label_values(splice_store_acs_size_increase_total,store_name)", "includeAll": true, "multi": true, "name": "store_name", "options": [], "query": { "qryType": 1, - "query": "label_values(splice_store_acs_size,store_name)", + "query": "label_values(splice_store_acs_size_increase_total,store_name)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query" } ] @@ -281,7 +286,8 @@ }, "timepicker": {}, "timezone": "", - "title": "Splice Store ACS Size", + "title": "ACS Size", "uid": "dduss3xr5or28c", - "version": 1 + "version": 2, + "weekStart": "" } diff --git a/cluster/pulumi/observability/grafana-dashboards/splice-stores/mediator-verdicts-ingestion.json b/cluster/pulumi/observability/grafana-dashboards/splice-stores/mediator-verdicts-ingestion.json index 680b6e6877..0c0f3f4668 100644 --- a/cluster/pulumi/observability/grafana-dashboards/splice-stores/mediator-verdicts-ingestion.json +++ b/cluster/pulumi/observability/grafana-dashboards/splice-stores/mediator-verdicts-ingestion.json @@ -311,7 +311,7 @@ { "disableTextWrap": false, "editorMode": "code", - "expr": "splice_scan_verdict_ingestion_last_record_time_us{namespace=~\"$namespace\",node_name=~\"$node_name\", pod_name=~\"$pod_name\"} / 1000 unless (splice_scan_verdict_ingestion_last_record_time_us{namespace=~\"$namespace\",node_name=~\"$node_name\", pod_name=~\"$pod_name\"} / 1000 == 0)", + "expr": "(splice_scan_verdict_ingestion_last_record_time_us{namespace=~\"$namespace\",node_name=~\"$node_name\", pod_name=~\"$pod_name\"} > 0) / 1e3", "fullMetaSearch": false, "includeNullMetadata": true, "legendFormat": "{{namespace}} {{job}} {{pod}}", @@ -631,6 +631,6 @@ "timezone": "", "title": "Mediator Verdicts Ingestion", "uid": "86f70b02-f283-469e-bf90-033dc9f55888", - "version": 10, + "version": 1, "weekStart": "" } diff --git a/cluster/pulumi/observability/src/config.ts b/cluster/pulumi/observability/src/config.ts index 56a05cecd3..c71ea3da67 100644 --- a/cluster/pulumi/observability/src/config.ts +++ b/cluster/pulumi/observability/src/config.ts @@ -1,6 +1,10 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { clusterSubConfig } from '@canton-network/splice-pulumi-common'; +import { + clusterSubConfig, + SplicePostgresConfig, + SplicePostgresSchema, +} from '@canton-network/splice-pulumi-common'; import { z } from 'zod'; const quotaMetricNameSchema = z @@ -26,9 +30,41 @@ const GcpQuotasConfigSchema = z.object({ }), }); +const NatPortUsageConfigSchema = z.object({ + thresholdPercent: z.number().min(0).max(100), + droppedSentPacketsThreshold: z.number().min(0), +}); + +export type NatPortUsageConfig = z.infer; + +const MuteTimeWindowSchema = z.object({ + times: z.array( + z.object({ + startTime: z.string(), // UTC + endTime: z.string(), // UTC + }) + ), + weekdays: z.array(z.string()).optional(), // e.g. ['monday', 'tuesday:friday'] +}); +export type MuteTimeWindow = z.infer; + +const MuteTimeIntervalSchema = z.array( + z.object({ + name: z.string(), + objectMatchers: z.array(z.tuple([z.string(), z.string(), z.string()])), + timeWindows: z.array(MuteTimeWindowSchema), + }) +); +export type MuteTimeInterval = z.infer[number]; + +// Observability needs to be migrated +const defaultObservabilityPostgresConfig: SplicePostgresConfig = { + deployment: 'legacy-helm-chart', +}; const MonitoringConfigSchema = z .object({ enableGrafanaServiceAccountToken: z.boolean(), + grafanaPostgres: SplicePostgresSchema.default({ deployment: 'legacy-helm-chart' }), alerting: z.object({ enableNoDataAlerts: z.boolean(), alerts: z.object({ @@ -66,6 +102,13 @@ const MonitoringConfigSchema = z // rate is computed. windowMinutes: z.number(), }), + spliceRateLimits: z.object({ + // Fraction (0-1) of a rate limiter's configured maximum rate above which the alert fires + usageThreshold: z.number(), + // Rejected requests per second, above which the rejection alert fires + rejectionCountThreshold: z.number(), + excludedLimiters: z.array(z.string()).default([]), + }), cloudSql: z.object({ maintenance: z.boolean(), }), @@ -129,11 +172,24 @@ const MonitoringConfigSchema = z tolerance: z.number(), }), gcpQuotas: GcpQuotasConfigSchema, - natPortUsage: z - .object({ - thresholdPercent: z.number().min(0).max(100), - }) - .default({ thresholdPercent: 80 }), + natPortUsage: NatPortUsageConfigSchema.default({ + thresholdPercent: 80, + // `default 30` because every once in a while (likely due to dynamic port allocation), + // a few packets (less than 1/s) get dropped and getting alerted on it every time can be very noisy. + droppedSentPacketsThreshold: 30, + }), + globalSynchronizerHealth: z.object({ + // Fraction (0-1) of sequenced confirmation requests that were discarded + // (i.e., never processed by the mediator, e.g. due to CometBFT replays) + // above which the alert fires. + discardedConfirmationRequestsThreshold: z.number(), + // Fraction (0-1) of confirmation requests that failed (as observed by the + // mediator, over the last 30m) above which the alert fires. + failedConfirmationRequestsThreshold: z.number(), + // Fire when TPS (approved confirmation requests per second) over the last + // 30m drops below this fraction of the previous 30m. + tpsDropThreshold: z.number(), + }), trafficBasedRewards: z.object({ featuredAppRightsLimit: z.number(), verdictIngestionBatchSizeThreshold: z.number(), @@ -142,17 +198,7 @@ const MonitoringConfigSchema = z }), logAlerts: z.object({}).catchall(z.string()).default({}), loggedSecretsFilter: z.string().optional(), - muteTimeIntervals: z - .array( - z.object({ - name: z.string(), - objectMatchers: z.array(z.tuple([z.string(), z.string(), z.string()])), - startTime: z.string(), // UTC - endTime: z.string(), // UTC - weekdays: z.array(z.string()).optional(), // e.g. ['monday', 'tuesday:friday'] - }) - ) - .default([]), + muteTimeIntervals: MuteTimeIntervalSchema.default([]), }), }) .strict(); @@ -161,12 +207,6 @@ export const monitoringConfig = MonitoringConfigSchema.parse(clusterSubConfig('m export type GcpQuotaAlertsConfig = z.infer; -const NatPortUsageConfigSchema = z.object({ - thresholdPercent: z.number().min(0).max(100), -}); - -export type NatPortUsageConfig = z.infer; - const PrometheusConfigSchema = z.object({ prometheus: z.object({ storageSize: z.string(), diff --git a/cluster/pulumi/observability/src/gcpAlerts.ts b/cluster/pulumi/observability/src/gcpAlerts.ts index 9610355026..0c7ab73a8a 100644 --- a/cluster/pulumi/observability/src/gcpAlerts.ts +++ b/cluster/pulumi/observability/src/gcpAlerts.ts @@ -210,7 +210,7 @@ export function installClusterMaintenanceUpdateAlerts( filter: ` resource.labels.cluster_name="${CLUSTER_NAME}" resource.type=~"(gke_cluster|gke_nodepool)" -jsonPayload.state=~"STARTED"`, +jsonPayload.@type=~"UpgradeEvent"`, labelExtractors: { cluster: 'EXTRACT(resource.labels.cluster_name)', }, @@ -471,8 +471,7 @@ export function installNatAlerts( { displayName: `NAT allocation failed in ${CLUSTER_BASENAME}`, conditionPrometheusQueryLanguage: { - query: - 'sum by (nat_gateway_name) (router_googleapis_com:nat_nat_allocation_failed{monitored_resource="nat_gateway"}) > 0', + query: `sum by (gateway_name) (router_googleapis_com:nat_nat_allocation_failed{monitored_resource="nat_gateway", gateway_name=~"nat-${CLUSTER_BASENAME}-gw.*"}) > 0`, ...prometheusDefaults, }, }, @@ -491,9 +490,12 @@ export function installNatAlerts( { displayName: `NAT dropped sent packets in ${CLUSTER_BASENAME}`, conditionPrometheusQueryLanguage: { - query: - 'sum by (nat_gateway_name, reason) (router_googleapis_com:nat_dropped_sent_packets_count{monitored_resource="nat_gateway"}) > 0', + query: `sum by (gateway_name, reason) (router_googleapis_com:nat_dropped_sent_packets_count{monitored_resource="nat_gateway", gateway_name=~"nat-${CLUSTER_BASENAME}-gw.*"}) > ${natConfig.droppedSentPacketsThreshold}`, ...prometheusDefaults, + // Ignore temporary spikes (likely caused by dynamic port allocation). + // We mostly care about sustained dropped sent packets, + // which most often indicates that we don't have enough ports available to the VMs. + duration: '1200s', }, }, ], @@ -511,7 +513,7 @@ export function installNatAlerts( { displayName: `NAT port usage high in ${CLUSTER_BASENAME}`, conditionPrometheusQueryLanguage: { - query: `sum by (nat_gateway_name) ((router_googleapis_com:nat_port_usage{monitored_resource="nat_gateway"} / 64512) * 100) > ${natConfig.thresholdPercent}`, + query: `sum by (gateway_name) ((router_googleapis_com:nat_port_usage{monitored_resource="nat_gateway", gateway_name=~"nat-${CLUSTER_BASENAME}-gw.*"} / 64512) * 100) > ${natConfig.thresholdPercent}`, // 64512 is the maximum number of ports per IP for Cloud NAT, as documented here: https://docs.cloud.google.com/nat/docs/ports-and-addresses#ports ...prometheusDefaults, }, diff --git a/cluster/pulumi/observability/src/grafana-dashboards.ts b/cluster/pulumi/observability/src/grafana-dashboards.ts index 57005cf906..03ef9195ec 100644 --- a/cluster/pulumi/observability/src/grafana-dashboards.ts +++ b/cluster/pulumi/observability/src/grafana-dashboards.ts @@ -11,11 +11,6 @@ export function createGrafanaDashboards(namespace: Input): void { namespace, `${SPLICE_ROOT}/cluster/pulumi/observability/grafana-dashboards/` ); - createConfigMapForFolder( - namespace, - `${SPLICE_ROOT}/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/`, - 'canton-bft' - ); } function createdNestedConfigMapForFolder(namespace: Input, folderPath: string) { diff --git a/cluster/pulumi/observability/src/istio.ts b/cluster/pulumi/observability/src/istio.ts index 4872133468..eeee71f00d 100644 --- a/cluster/pulumi/observability/src/istio.ts +++ b/cluster/pulumi/observability/src/istio.ts @@ -36,11 +36,10 @@ export function istioMonitoring( { port: 'http-envoy-prom', path: '/stats/prometheus', - // keep only istio metrics, drop envoy metrics metricRelabelings: [ { sourceLabels: ['__name__'], - regex: 'istio_.*', + regex: '(istio_.*' + '|envoy_.*http_local_rate_limit_.*)', action: 'keep', }, // drop instance label, we have the pod name diff --git a/cluster/pulumi/observability/src/observability.ts b/cluster/pulumi/observability/src/observability.ts index 09edb3b92c..729deb6de4 100644 --- a/cluster/pulumi/observability/src/observability.ts +++ b/cluster/pulumi/observability/src/observability.ts @@ -11,13 +11,12 @@ import { CLUSTER_NAME, clusterProdLike, commandScriptPath, - createVolumeSnapshot, DecentralizedSynchronizerUpgradeConfig, ExactNamespace, GCP_PROJECT, GrafanaKeys, HELM_MAX_HISTORY_SIZE, - infraAffinityAndTolerations, + infraKubernetesScheduling, infraPremiumStorageClassName, infraStandardStorageClassName, loadTesterConfig, @@ -30,13 +29,12 @@ import { standardSvConfigsBasic, } from '@canton-network/splice-pulumi-common-sv/src/svConfigsBasic'; import { SweepConfig } from '@canton-network/splice-pulumi-common-validator'; -import { SplicePostgres } from '@canton-network/splice-pulumi-common/src/postgres'; -import { infraStack } from '@canton-network/splice-pulumi-common/src/stackReferences'; +import { installSplicePostgres, Postgres } from '@canton-network/splice-pulumi-common/src/postgres'; +import { StackReferences } from '@canton-network/splice-pulumi-common/src/stackReferences'; import { local } from '@pulumi/command'; import { getSecretVersionOutput } from '@pulumi/gcp/secretmanager/getSecretVersion'; import { Input } from '@pulumi/pulumi'; -import { hyperdiskSupportConfig } from '../../common/src/config/hyperdiskSupportConfig'; import { clusterIsResetPeriodically, enableAlertEmailToSupportTeam, @@ -98,7 +96,7 @@ const shouldIgnoreNoDataOrDataSourceError = clusterIsResetPeriodically; // eslint-disable-next-line @typescript-eslint/no-explicit-any const istioDashboardVersions: pulumi.Output = - infraStack.requireOutput('istioDashboardVersions'); + StackReferences.infra.requireOutput('istioDashboardVersions'); export function configureObservability(namespace: ExactNamespace): pulumi.Resource { // If the stack version is updated the crd version might need to be upgraded as well, check the release notes https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack @@ -195,9 +193,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour logFormat: 'json', storage: { volumeClaimTemplate: { - ...(hyperdiskSupportConfig.hyperdiskSupport.enabledForInfra - ? { metadata: { name: 'alertmanager-hd-pvc' } } - : {}), + metadata: { name: 'alertmanager-hd-pvc' }, spec: { storageClassName: infraStandardStorageClassName, accessModes: ['ReadWriteOnce'], @@ -209,7 +205,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour }, }, }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, templateFiles: { 'template.tmpl': substituteSlackNotificationTemplate( @@ -228,7 +224,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour tls: { enabled: false, // because `admissionWebhooks` are disabled, see: https://github.com/prometheus-community/helm-charts/issues/418 }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, prometheus: { prometheusSpec: { @@ -256,9 +252,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour scrapeNativeHistograms: true, storageSpec: { volumeClaimTemplate: { - ...(hyperdiskSupportConfig.hyperdiskSupport.enabledForInfra - ? { metadata: { name: 'prometheus-hd-pvc' } } - : {}), + metadata: { name: 'prometheus-hd-pvc' }, spec: { storageClassName: infraPremiumStorageClassName, accessModes: ['ReadWriteOnce'], @@ -271,7 +265,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour }, }, externalUrl: prometheusExternalUrl, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, }, grafana: { @@ -400,7 +394,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour }, adminUser: 'cn-admin', adminPassword: adminPassword, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, 'kube-state-metrics': { fullnameOverride: 'ksm', @@ -505,7 +499,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour }, ], }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, 'prometheus-node-exporter': { fullnameOverride: 'node-exporter', @@ -553,7 +547,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour ); createGrafanaAlerting(namespaceName); if (monitoringConfig.enableGrafanaServiceAccountToken) { - createGrafanaServiceAccount(namespaceName, adminPassword, [prometheusStack, postgres.pg]); + createGrafanaServiceAccount(namespaceName, adminPassword, [prometheusStack, postgres.database]); } createGrafanaEnvoyFilter(namespaceName, [prometheusStack]); @@ -572,7 +566,7 @@ export function configureObservability(namespace: ExactNamespace): pulumi.Resour namespace: namespaceName, additionalLabels: { release: 'prometheus-grafana-monitoring' }, }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, maxHistory: HELM_MAX_HISTORY_SIZE, }); @@ -743,15 +737,16 @@ function substituteScanConnectionDisagreementAlerts(alert: string): string { if (config.excludedConnections.length > 0) { matchers.push(`scan_connection!~"${config.excludedConnections.join('|')}"`); } - if (config.excludedHttpStatusCodes.length > 0) { - matchers.push(`http_status!~"${config.excludedHttpStatusCodes.join('|')}"`); - } const bareFilter = matchers.join(', '); const filter = bareFilter ? `, ${bareFilter}` : ''; const connectionMatchers: string[] = []; if (config.excludedConnections.length > 0) { connectionMatchers.push(`scan_connection!~"${config.excludedConnections.join('|')}"`); } + if (config.excludedHttpStatusCodes.length > 0) { + matchers.push(`http_status!~"${config.excludedHttpStatusCodes.join('|')}"`); + connectionMatchers.push(`http_status!~"${config.excludedHttpStatusCodes.join('|')}"`); + } const connectionBareFilter = connectionMatchers.join(', '); const connectionFilter = connectionBareFilter ? `, ${connectionBareFilter}` : ''; return alert @@ -769,12 +764,25 @@ function substituteScanConnectionDisagreementAlerts(alert: string): string { function substituteDsoMissedConfirmationsAlerts(alert: string): string { const config = monitoringConfig.alerting.alerts.dsoMissedConfirmations; return alert - .replaceAll('$DSO_MISSED_CONFIRMATIONS_THRESHOLD_PERCENT', (config.threshold * 100).toString()) .replaceAll('$DSO_MISSED_CONFIRMATIONS_THRESHOLD', config.threshold.toString()) .replaceAll('$DSO_MISSED_CONFIRMATIONS_WINDOW_SECONDS', (config.windowMinutes * 60).toString()) .replaceAll('$DSO_MISSED_CONFIRMATIONS_WINDOW_MINUTES', config.windowMinutes.toString()); } +function substituteSpliceRateLimitsAlerts(alert: string): string { + const config = monitoringConfig.alerting.alerts.spliceRateLimits; + const bareFilter = + config.excludedLimiters.length > 0 ? `limiter!~"${config.excludedLimiters.join('|')}"` : ''; + const filter = bareFilter ? `, ${bareFilter}` : ''; + return alert + .replaceAll('$SPLICE_RATE_LIMITS_USAGE_THRESHOLD', config.usageThreshold.toString()) + .replaceAll( + '$SPLICE_RATE_LIMITS_REJECTION_COUNT_THRESHOLD', + config.rejectionCountThreshold.toString() + ) + .replaceAll('$SPLICE_RATE_LIMITS_FILTER', filter); +} + // AmuletMetrics was previously using owner.toString instead of owner.toProtoPrimitive // This function makes it compatible for both. function partyIdTransform(partyId: string) { @@ -822,13 +830,14 @@ function createGrafanaAlerting(namespace: Input) { muteTimes: monitoringConfig.alerting.muteTimeIntervals.map(interval => ({ orgId: 1, name: interval.name, - time_intervals: [ - { - times: [{ start_time: interval.startTime, end_time: interval.endTime }], - ...(interval.weekdays ? { weekdays: interval.weekdays } : {}), - location: 'UTC', - }, - ], + time_intervals: interval.timeWindows.map(window => ({ + times: window.times.map(t => ({ + start_time: t.startTime, + end_time: t.endTime, + })), + ...(window.weekdays ? { weekdays: window.weekdays } : {}), + location: 'UTC', + })), })), }), } @@ -869,7 +878,7 @@ function createGrafanaAlerting(namespace: Input) { monitoringConfig.alerting.alerts.deployment.pendingPeriodMinutes.toString() ), 'load-tester_alerts.yaml': readGrafanaAlertingFile('load-tester_alerts.yaml') - .replace( + .replaceAll( '$LOAD_TESTER_MIN_RATE', loadTesterConfig?.minRate ? loadTesterConfig?.minRate.toString() : '1.0' ) @@ -948,26 +957,26 @@ function createGrafanaAlerting(namespace: Input) { : {}), 'acknowledgement_alerts.yaml': readGrafanaAlertingFile( 'acknowledgement_alerts.yaml' - ).replace( + ).replaceAll( '$MEDIATOR_ACKNOWLEDGEMENT_LAG_SECONDS', monitoringConfig.alerting.alerts.mediators.acknowledgementLagSeconds.toString() ), 'sequencer_client_delay_alerts.yaml': readGrafanaAlertingFile( 'sequencer_client_delay_alerts.yaml' - ).replace( + ).replaceAll( '$SEQUENCER_CLIENT_DELAY_THRESHOLD_SECONDS', monitoringConfig.alerting.alerts.sequencerClientDelay.seconds.toString() ), 'acs_commitment_alerts.yaml': readGrafanaAlertingFile('acs_commitment_alerts.yaml') - .replace( + .replaceAll( '$ACS_COMMITMENT_CHECKPOINT_DELAY_THRESHOLD_SECONDS', monitoringConfig.alerting.alerts.acsCommitments.checkpointDelay.seconds.toString() ) - .replace( + .replaceAll( '$ACS_COMMITMENT_DELAY_THRESHOLD_SECONDS', monitoringConfig.alerting.alerts.acsCommitments.completedDelay.seconds.toString() ) - .replace( + .replaceAll( '$ACS_COMMITMENT_COMPUTE_DURATION_THRESHOLD_SECONDS', monitoringConfig.alerting.alerts.acsCommitments.computeDuration.seconds.toString() ), @@ -980,15 +989,33 @@ function createGrafanaAlerting(namespace: Input) { 'scan_connection_disagreement_alerts.yaml': substituteScanConnectionDisagreementAlerts( readGrafanaAlertingFile('scan_connection_disagreement_alerts.yaml') ), + 'scan_bft_sequencers_alerts.yaml': readGrafanaAlertingFile( + 'scan_bft_sequencers_alerts.yaml' + ), + 'global-sync-health_alerts.yaml': readGrafanaAlertingFile( + 'global-sync-health_alerts.yaml' + ) + .replaceAll( + '$DISCARDED_CONFIRMATION_REQUESTS_THRESHOLD', + monitoringConfig.alerting.alerts.globalSynchronizerHealth.discardedConfirmationRequestsThreshold.toString() + ) + .replaceAll( + '$FAILED_CONFIRMATION_REQUESTS_THRESHOLD', + monitoringConfig.alerting.alerts.globalSynchronizerHealth.failedConfirmationRequestsThreshold.toString() + ) + .replaceAll( + '$TPS_DROP_THRESHOLD', + monitoringConfig.alerting.alerts.globalSynchronizerHealth.tpsDropThreshold.toString() + ), 'extra_k8s_alerts.yaml': readGrafanaAlertingFile('extra_k8s_alerts.yaml'), 'sequencer_rate_limit_alerts.yaml': readGrafanaAlertingFile( 'sequencer_rate_limit_alerts.yaml' ) - .replace( + .replaceAll( '$SEQUENCER_RATE_LIMIT_REJECTION_RATE_THRESHOLD', monitoringConfig.alerting.alerts.sequencerRateLimits.rejectionRateThreshold.toString() ) - .replace( + .replaceAll( '$SEQUENCER_RATE_LIMIT_CIRCUIT_BREAKER_STATE_THRESHOLD', monitoringConfig.alerting.alerts.sequencerRateLimits.circuitBreakerStateThreshold.toString() ), @@ -997,7 +1024,7 @@ function createGrafanaAlerting(namespace: Input) { 'cantonbft_alerts.yaml': readGrafanaAlertingFile( 'cantonbft_alerts.yaml' ).replaceAll( - '$BFT_ORDERING_INGRESS_REQUESTS_QUEUED_THRESHOLD', + '$CANTON_BFT_MEMPOOL_SIZE_THRESHOLD', monitoringConfig.alerting.alerts.cantonBft.mempoolMaxSizeThreshold.toString() ), } @@ -1023,18 +1050,24 @@ function createGrafanaAlerting(namespace: Input) { 'traffic_based_rewards_alerts.yaml': readGrafanaAlertingFile( 'traffic_based_rewards_alerts.yaml' ) - .replace( + .replaceAll( '$FEATURED_APP_RIGHTS_LIVE_ROW_LIMIT', monitoringConfig.alerting.alerts.trafficBasedRewards.featuredAppRightsLimit.toString() ) - .replace( + .replaceAll( '$VERDICT_INGESTION_BATCH_SIZE_THRESHOLD', monitoringConfig.alerting.alerts.trafficBasedRewards.verdictIngestionBatchSizeThreshold.toString() ) - .replace( + .replaceAll( '$VERDICT_INGESTION_BATCH_SIZE_PENDING_PERIOD_MINUTES', monitoringConfig.alerting.alerts.trafficBasedRewards.verdictIngestionBatchSizePendingPeriodMinutes.toString() ), + 'istio-rate-limiting_alerts.yaml': readGrafanaAlertingFile( + 'istio-rate-limiting_alerts.yaml' + ), + 'splice-rate-limiting_alerts.yaml': substituteSpliceRateLimitsAlerts( + readGrafanaAlertingFile('splice-rate-limiting_alerts.yaml') + ), }, }).map(([k, v]) => [k, defaultAlertSubstitutions(v)]) ), @@ -1153,13 +1186,13 @@ function readAndSetAlertRulesGrafanaAlertingFile(file: string, rules: AlertRules content.groups[0].rules = rules.map(rule => { const newRuleString = genericAlertRule - .replace('$REPORT_PUBLISHER_FORMULA', rule.reportPublisherFormula ?? 'NOT_REPLACED') - .replace('$NOTIFICATION_DELAY', rule.notificationDelay ?? 'NOT_REPLACED') - .replace('$TEAM_LABEL', rule.teamLabel ?? 'NOT_REPLACED') - .replace('$SUB_TITLE', rule.subtitle ?? 'NOT_REPLACED') - .replace('$RULE_UID', rule.uid ?? 'NOT_REPLACED') - .replace('$OWNER_PREFIX_REGEX', rule.ownerPrefixRegex ?? 'NOT_REPLACED') - .replace('$MAX_BALANCE_THRESHOLD', rule.maxBalanceThreshold ?? 'NOT_REPLACED'); + .replaceAll('$REPORT_PUBLISHER_FORMULA', rule.reportPublisherFormula ?? 'NOT_REPLACED') + .replaceAll('$NOTIFICATION_DELAY', rule.notificationDelay ?? 'NOT_REPLACED') + .replaceAll('$TEAM_LABEL', rule.teamLabel ?? 'NOT_REPLACED') + .replaceAll('$SUB_TITLE', rule.subtitle ?? 'NOT_REPLACED') + .replaceAll('$RULE_UID', rule.uid ?? 'NOT_REPLACED') + .replaceAll('$OWNER_PREFIX_REGEX', rule.ownerPrefixRegex ?? 'NOT_REPLACED') + .replaceAll('$MAX_BALANCE_THRESHOLD', rule.maxBalanceThreshold ?? 'NOT_REPLACED'); return yaml.load(newRuleString) as GrafanaRule; }); const newFileContent = yaml.dump(content); @@ -1189,16 +1222,17 @@ function grafanaKeysFromSecret(): pulumi.Output { }); } -function installPostgres(namespace: ExactNamespace): SplicePostgres { - return new SplicePostgres( +function installPostgres(namespace: ExactNamespace): Postgres { + const instanceName = 'grafana-postgres'; + return installSplicePostgres( namespace, - 'grafana-postgres', - 'grafana-postgres', + instanceName, 'grafana-postgres-secret', + monitoringConfig.grafanaPostgres, + undefined, // chart version + { disableProtection: true }, { db: { volumeSize: '20Gi' } }, // A tiny pvc should be enough for grafana true, // overrideDbSizeFromValues - true, // disableProtection - undefined, // chart version - true // useInfraAffinityAndTolerations + true // useinfraKubernetesScheduling ); } diff --git a/cluster/pulumi/observability/tsconfig.eslint.json b/cluster/pulumi/observability/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/observability/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/operator/src/config.ts b/cluster/pulumi/operator/src/config.ts new file mode 100644 index 0000000000..2f3f7d495f --- /dev/null +++ b/cluster/pulumi/operator/src/config.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { config, GitReferenceSchema } from '@canton-network/splice-pulumi-common'; +import { clusterSubConfig } from '@canton-network/splice-pulumi-common/src/config/config'; +import { z } from 'zod'; + +export const OperatorDeploymentConfigSchema = z.object({ + reference: GitReferenceSchema, + flux: z + .object({ + alertSlackChannel: z + .string() + .optional() + .prefault(() => config.optionalEnv('SLACK_ALERT_NOTIFICATION_CHANNEL_FULL_NAME')), + }) + .prefault({}), +}); + +export type Config = z.infer; + +export const operatorDeploymentConfig = OperatorDeploymentConfigSchema.parse( + clusterSubConfig('operatorDeployment') +); +export const fluxConfig = operatorDeploymentConfig.flux; diff --git a/cluster/pulumi/operator/src/flux/flux-alerts.ts b/cluster/pulumi/operator/src/flux/flux-alerts.ts index 901fad9bac..9ad0d74cd9 100644 --- a/cluster/pulumi/operator/src/flux/flux-alerts.ts +++ b/cluster/pulumi/operator/src/flux/flux-alerts.ts @@ -3,10 +3,11 @@ import * as k8s from '@pulumi/kubernetes'; import { CLUSTER_BASENAME, clusterProdLike, config } from '@canton-network/splice-pulumi-common'; +import { fluxConfig } from '../config'; import { namespace } from '../namespace'; import { flux } from './flux'; -if (clusterProdLike) { +if (clusterProdLike && fluxConfig.alertSlackChannel) { const slackToken = new k8s.core.v1.Secret('slack', { metadata: { name: 'slack', @@ -29,7 +30,7 @@ if (clusterProdLike) { }, spec: { type: 'slack', - channel: config.requireEnv('SLACK_ALERT_NOTIFICATION_CHANNEL_FULL_NAME'), + channel: fluxConfig.alertSlackChannel, address: 'https://slack.com/api/chat.postMessage', secretRef: { name: slackToken.metadata.name }, }, diff --git a/cluster/pulumi/operator/src/flux/flux.ts b/cluster/pulumi/operator/src/flux/flux.ts index ec9b48d5bf..2ed1310009 100644 --- a/cluster/pulumi/operator/src/flux/flux.ts +++ b/cluster/pulumi/operator/src/flux/flux.ts @@ -3,7 +3,7 @@ import * as k8s from '@pulumi/kubernetes'; import { HELM_MAX_HISTORY_SIZE, - infraAffinityAndTolerations, + infraKubernetesScheduling, } from '@canton-network/splice-pulumi-common'; import { namespace } from '../namespace'; @@ -21,13 +21,13 @@ export const flux = new k8s.helm.v3.Release('flux', { }, values: { cli: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, notificationController: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, sourceController: { - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, }, helmController: { create: false, diff --git a/cluster/pulumi/operator/src/index.ts b/cluster/pulumi/operator/src/index.ts index 402316361d..881d272fe3 100644 --- a/cluster/pulumi/operator/src/index.ts +++ b/cluster/pulumi/operator/src/index.ts @@ -4,7 +4,7 @@ import { CLUSTER_BASENAME } from '@canton-network/splice-pulumi-common'; import { gitRepoForRef } from '@canton-network/splice-pulumi-common/src/operator/flux-source'; import { createEnvRefs } from '@canton-network/splice-pulumi-common/src/operator/stack'; -import { operatorDeploymentConfig } from '../../common/src/operator/config'; +import { operatorDeploymentConfig } from './config'; import { flux } from './flux'; import { namespace } from './namespace'; import { installDeploymentStack } from './stacks/deployment'; diff --git a/cluster/pulumi/operator/src/operator.ts b/cluster/pulumi/operator/src/operator.ts index fa60fdd0a9..1fef6fa044 100644 --- a/cluster/pulumi/operator/src/operator.ts +++ b/cluster/pulumi/operator/src/operator.ts @@ -7,7 +7,7 @@ import { config, HELM_MAX_HISTORY_SIZE, imagePullSecret, - infraAffinityAndTolerations, + infraKubernetesScheduling, } from '@canton-network/splice-pulumi-common'; import { local } from '@pulumi/command'; @@ -56,7 +56,7 @@ export const operator = new k8s.helm.v3.Release( serviceMonitor: { enabled: true, }, - ...infraAffinityAndTolerations, + ...infraKubernetesScheduling, maxHistory: HELM_MAX_HISTORY_SIZE, }, }, diff --git a/cluster/pulumi/operator/tsconfig.eslint.json b/cluster/pulumi/operator/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/operator/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/package-lock.json b/cluster/pulumi/package-lock.json index a79a914f8a..8f668a304e 100644 --- a/cluster/pulumi/package-lock.json +++ b/cluster/pulumi/package-lock.json @@ -35,20 +35,20 @@ "commander": "^14.0.3" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@jest/globals": "^30.4.1", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/request": "^2.48.13", - "@typescript-eslint/eslint-plugin": "^8.61.1", + "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.57.2", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-promise": "^7.3.0", "jest": "^30.4.2", - "minimatch": "10.2.5", - "prettier": "^3.8.4", - "ts-jest": "^29.4.11", + "minimatch": "10.2.6", + "prettier": "^3.9.6", + "ts-jest": "^29.4.12", "typescript": "^5.9.3" } }, @@ -93,7 +93,7 @@ "@pulumi/gcp": "^9.18.0", "@pulumi/github": "6.13.1", "@pulumi/kubernetes": "4.28.0", - "@pulumi/pulumi": "^3.230.0", + "@pulumi/pulumi": "^3.243.0", "@pulumi/random": "4.19.2", "@pulumi/std": "2.3.2", "@types/auth0": "^3.3.11", @@ -108,7 +108,7 @@ "devDependencies": { "@jest/globals": "^30.4.1", "@types/js-yaml": "^4.0.5", - "@types/lodash": "^4.17.24", + "@types/lodash": "^4.17.25", "@types/ws": "^8.18.1", "dedent": "^1.7.2" } @@ -959,9 +959,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "peer": true, @@ -1013,9 +1013,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, "license": "MIT", "dependencies": { @@ -1025,7 +1025,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -1037,9 +1037,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1061,9 +1061,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { @@ -3078,9 +3078,9 @@ } }, "node_modules/@pulumi/pulumi": { - "version": "3.230.0", - "resolved": "https://registry.npmjs.org/@pulumi/pulumi/-/pulumi-3.230.0.tgz", - "integrity": "sha512-wWHP65RKj2zz8R8N4sImPie4bQM/walrCOeNUl46TkdHOOihFdg6cMO2l1IPufh/EBBLtiUBrzpP0wi8OWqhsQ==", + "version": "3.247.0", + "resolved": "https://registry.npmjs.org/@pulumi/pulumi/-/pulumi-3.247.0.tgz", + "integrity": "sha512-JaXgWfRaT8XLQaqy6MriSxnphuyE56EjD8enZcNSEZ9nWthuTLV6i+w7q1KdcDJKGuMEn6yBeZuBoNafQtx4XA==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.10.1", @@ -3100,12 +3100,10 @@ "execa": "^5.1.0", "fdir": "^6.5.0", "google-protobuf": "^3.21.4", - "got": "^11.8.6", "ini": "^2.0.0", "js-yaml": "^3.14.2", "minimist": "^1.2.6", "normalize-package-data": "^6.0.0", - "package-directory": "^8.1.0", "picomatch": "^4.0.0", "require-from-string": "^2.0.1", "semver": "^7.5.2", @@ -3272,17 +3270,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -3303,17 +3290,6 @@ "@sinonjs/commons": "^3.0.1" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@tootallnate/once": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", @@ -3493,17 +3469,6 @@ "@types/node": "*" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", @@ -3553,11 +3518,6 @@ "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==" }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" - }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -3620,18 +3580,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", "dev": true, "license": "MIT" }, @@ -3698,14 +3650,6 @@ "node": ">= 0.12" } }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -3792,17 +3736,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", - "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/type-utils": "8.61.1", - "@typescript-eslint/utils": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3815,7 +3759,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.61.1", + "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3831,16 +3775,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", - "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "engines": { @@ -3855,15 +3799,64 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/project-service": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", - "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.61.1", - "@typescript-eslint/types": "^8.61.1", + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "engines": { @@ -3878,14 +3871,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", - "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3895,10 +3888,24 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", - "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", "engines": { @@ -3913,15 +3920,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", - "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3937,10 +3944,91 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/@typescript-eslint/types": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", - "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", "engines": { @@ -3952,16 +4040,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", - "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.61.1", - "@typescript-eslint/tsconfig-utils": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3979,17 +4067,48 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", - "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4003,14 +4122,95 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", - "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4021,6 +4221,20 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", @@ -5030,9 +5244,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5170,45 +5384,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5370,17 +5545,6 @@ "node": ">=12" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cmd-shim": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz", @@ -5598,31 +5762,6 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -5655,14 +5794,6 @@ "node": ">=0.10.0" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -6193,9 +6324,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6285,10 +6416,24 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "peer": true, @@ -6651,18 +6796,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-up-simple": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -7262,30 +7395,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -7487,18 +7596,6 @@ "node": ">= 14" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", @@ -8803,9 +8900,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8847,7 +8954,9 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "peer": true }, "node_modules/json-parse-even-better-errors": { "version": "5.0.0", @@ -8958,6 +9067,8 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -9049,14 +9160,6 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "engines": { - "node": ">=8" - } - }, "node_modules/lru-cache": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", @@ -9159,21 +9262,13 @@ "node": ">=6" } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -9192,15 +9287,15 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/minimist": { @@ -9510,17 +9605,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npm-bundled": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", @@ -9831,14 +9915,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9891,21 +9967,6 @@ "node": ">=6" } }, - "node_modules/package-directory": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/package-directory/-/package-directory-8.2.0.tgz", - "integrity": "sha512-qJSu5Mo6tHmRxCy2KCYYKYgcfBdUpy9dwReaZD/xwf608AUk/MoRtIOWzgDtUeGeC7n/55yC3MI1Q+MbSoektw==", - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -10207,9 +10268,9 @@ } }, "node_modules/prettier": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", - "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -10379,17 +10440,6 @@ ], "license": "MIT" }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-is-18": { "name": "react-is", "version": "18.3.1", @@ -10519,11 +10569,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" - }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -10557,17 +10602,6 @@ "node": ">=4" } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -10685,9 +10719,9 @@ } }, "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11376,9 +11410,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -11490,9 +11524,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.11", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", - "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", "dev": true, "license": "MIT", "dependencies": { @@ -11502,7 +11536,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.8.0", + "semver": "^7.8.5", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -12393,7 +12427,7 @@ "dependencies": { "@canton-network/splice-pulumi-common": "1.0.0", "@canton-network/splice-pulumi-common-sv": "1.0.0", - "@pulumi/pulumi": "^3.230.0" + "@pulumi/pulumi": "^3.243.0" } }, "sv-runbook": { diff --git a/cluster/pulumi/package.json b/cluster/pulumi/package.json index f6e4c34262..1ac82f9ff8 100644 --- a/cluster/pulumi/package.json +++ b/cluster/pulumi/package.json @@ -3,20 +3,20 @@ "version": "1.0.0", "main": "src/index.ts", "devDependencies": { - "@eslint/eslintrc": "^3.3.5", + "@eslint/eslintrc": "^3.3.6", "@jest/globals": "^30.4.1", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/request": "^2.48.13", - "@typescript-eslint/eslint-plugin": "^8.61.1", + "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.57.2", - "@eslint/js": "9.39.4", + "@eslint/js": "9.39.5", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-promise": "^7.3.0", "jest": "^30.4.2", - "minimatch": "10.2.5", - "prettier": "^3.8.4", - "ts-jest": "^29.4.11", + "minimatch": "10.2.6", + "prettier": "^3.9.6", + "ts-jest": "^29.4.12", "typescript": "^5.9.3" }, "scripts": { diff --git a/cluster/pulumi/policies/tsconfig.eslint.json b/cluster/pulumi/policies/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/policies/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/pulumiUp.ts b/cluster/pulumi/pulumiUp.ts index a61152971d..ac536b515e 100644 --- a/cluster/pulumi/pulumiUp.ts +++ b/cluster/pulumi/pulumiUp.ts @@ -6,9 +6,7 @@ import { mustInstallValidator1, } from '@canton-network/splice-pulumi-common-validator/src/validators'; import { runSvCantonForAllMigrations } from '@canton-network/splice-pulumi-sv-canton/pulumi'; -import { - runSvProjectForAllSvs, -} from '@canton-network/splice-pulumi-sv/pulumi'; +import { runSvProjectForAllSvs } from '@canton-network/splice-pulumi-sv/pulumi'; import { awaitAllOrThrowAllExceptions, Operation, PulumiAbortController, stack } from './pulumi'; import { upOperation, upStack } from './pulumiOperations'; @@ -60,7 +58,7 @@ async function runAllStacksUp() { runAllStacksUp().catch((err: unknown) => { console.error( `\nPulumi up finished with errors. See the summary above for details.\n` + - (err instanceof Error ? err.message : String(err)) + (err instanceof Error ? err.message : String(err)) ); process.exit(1); }); diff --git a/cluster/pulumi/splitwell/src/splitwell.ts b/cluster/pulumi/splitwell/src/splitwell.ts index d3a20de854..233608935a 100644 --- a/cluster/pulumi/splitwell/src/splitwell.ts +++ b/cluster/pulumi/splitwell/src/splitwell.ts @@ -51,6 +51,7 @@ export async function installSplitwell( 'splitwell-pg', activeVersion, spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, splitPostgresInstances ); @@ -62,7 +63,6 @@ export async function installSplitwell( const participant = await installParticipant( splitwellConfig, - decentralizedSynchronizerMigrationConfig.activeMigrationId, xns, auth0Client.getCfg(), false, @@ -81,6 +81,7 @@ export async function installSplitwell( 'sw-pg', activeVersion, spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, true )); const splitwellDbName = 'app_splitwell'; @@ -127,6 +128,7 @@ export async function installSplitwell( 'validator-pg', activeVersion, spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, true )); const validatorDbName = 'val_splitwell'; diff --git a/cluster/pulumi/splitwell/tsconfig.eslint.json b/cluster/pulumi/splitwell/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/splitwell/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/sv-canton/package.json b/cluster/pulumi/sv-canton/package.json index a988b2f3a7..a7d5482b24 100644 --- a/cluster/pulumi/sv-canton/package.json +++ b/cluster/pulumi/sv-canton/package.json @@ -5,7 +5,7 @@ "dependencies": { "@canton-network/splice-pulumi-common": "1.0.0", "@canton-network/splice-pulumi-common-sv": "1.0.0", - "@pulumi/pulumi": "^3.230.0" + "@pulumi/pulumi": "^3.243.0" }, "scripts": { "fix": "npm run format:fix && npm run lint:fix", diff --git a/cluster/pulumi/sv-canton/src/canton.ts b/cluster/pulumi/sv-canton/src/canton.ts index 245b086ffe..56852a5230 100644 --- a/cluster/pulumi/sv-canton/src/canton.ts +++ b/cluster/pulumi/sv-canton/src/canton.ts @@ -83,6 +83,7 @@ export async function installCantonComponents( `mediator-pg`, version, physicalSynchronizerConfig.mediator.cloudSql, + physicalSynchronizerConfig.mediator.splicePostgres, true, { isActive: migrationStillRunning, @@ -98,6 +99,7 @@ export async function installCantonComponents( `sequencer-pg`, version, physicalSynchronizerConfig.sequencer.cloudSql, + physicalSynchronizerConfig.sequencer.splicePostgres, true, { isActive: migrationStillRunning, migrationId, disableProtection } )); @@ -109,6 +111,7 @@ export async function installCantonComponents( `sequencer-bft-pg`, version, physicalSynchronizerConfig.sequencer.cloudSql, + physicalSynchronizerConfig.sequencer.splicePostgres, true, { isActive: migrationStillRunning, migrationId, disableProtection } ) diff --git a/cluster/pulumi/sv-canton/src/decentralizedSynchronizerNode.ts b/cluster/pulumi/sv-canton/src/decentralizedSynchronizerNode.ts index f537fb307b..39b3fbf348 100644 --- a/cluster/pulumi/sv-canton/src/decentralizedSynchronizerNode.ts +++ b/cluster/pulumi/sv-canton/src/decentralizedSynchronizerNode.ts @@ -150,6 +150,9 @@ abstract class InStackDecentralizedSynchronizerNode : [] ).concat(physicalSynchronizerConfig.sequencer.additionalEnvVars), resources: physicalSynchronizerConfig.sequencer.resources, + additionalJvmOptions: getAdditionalJvmOptions( + physicalSynchronizerConfig.sequencer.additionalJvmOptions + ), }, mediator: { ...decentralizedSynchronizerValues.mediator, @@ -162,6 +165,9 @@ abstract class InStackDecentralizedSynchronizerNode }, additionalEnvVars: physicalSynchronizerConfig.mediator.additionalEnvVars, resources: physicalSynchronizerConfig.mediator.resources, + additionalJvmOptions: getAdditionalJvmOptions( + physicalSynchronizerConfig.mediator.additionalJvmOptions + ), }, enablePostgresMetrics: true, metrics: { @@ -171,12 +177,9 @@ abstract class InStackDecentralizedSynchronizerNode }, }, livenessProbeInitialDelaySeconds: domainLivenessProbeInitialDelaySeconds, - // TODO(#5805): These are used both for sequencer and mediator while the mediator config is ignored. - additionalJvmOptions: getAdditionalJvmOptions( - physicalSynchronizerConfig.sequencer.additionalJvmOptions - ), pvc: persistentHeapDumpsPvc(), serviceAccountName: imagePullServiceAccountName, + enableAntiAffinity: physicalSynchronizerConfig.sequencer.enableAntiAffinity, }, }, this.version, @@ -275,7 +278,7 @@ export class InStackCometBftDecentralizedSynchronizerNode version, svConfig.logging?.cantonLogLevel, svConfig.logging?.cantonStdoutLogLevel, - svConfig.logging?.apiRequestLogLevel, + svConfig.logging?.cantonApiRequestLogLevel ?? svConfig.logging?.apiRequestLogLevel, svConfig.logging?.cantonAsync, imagePullServiceAccountName, opts @@ -323,7 +326,7 @@ export class InStackCantonBftDecentralizedSynchronizerNode extends InStackDecent version, svConfig.logging?.cantonLogLevel, svConfig.logging?.cantonStdoutLogLevel, - svConfig.logging?.apiRequestLogLevel, + svConfig.logging?.cantonApiRequestLogLevel ?? svConfig.logging?.apiRequestLogLevel, svConfig.logging?.cantonAsync, imagePullServiceAccountName, opts diff --git a/cluster/pulumi/sv-canton/tsconfig.eslint.json b/cluster/pulumi/sv-canton/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/sv-canton/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/sv-runbook/dump-config.ts b/cluster/pulumi/sv-runbook/dump-config.ts index eabc59091e..5cf462114e 100644 --- a/cluster/pulumi/sv-runbook/dump-config.ts +++ b/cluster/pulumi/sv-runbook/dump-config.ts @@ -9,11 +9,6 @@ import { async function main() { await initDumpConfig(); - /* eslint-disable no-process-env */ - process.env.ARTIFACTORY_USER = 'artie'; - /* eslint-disable no-process-env */ - process.env.ARTIFACTORY_PASSWORD = 's3cr3t'; - const installNode = await import('./src/installNode'); const secrets = new SecretsFixtureMap(); // Need to import this directly to avoid initializing any configs before the mocks are initialized diff --git a/cluster/pulumi/sv-runbook/src/installNode.ts b/cluster/pulumi/sv-runbook/src/installNode.ts index 4540e0c7aa..e8c4141e01 100644 --- a/cluster/pulumi/sv-runbook/src/installNode.ts +++ b/cluster/pulumi/sv-runbook/src/installNode.ts @@ -50,6 +50,7 @@ import { externalIpRangesFile, clusterNetwork, CnChartVersion, + envoyClientIpHeaderEnvVar, } from '@canton-network/splice-pulumi-common'; import { approvedSvIdentities, @@ -102,7 +103,7 @@ export async function installNode( console.error( activeVersion.type === 'local' ? 'Using locally built charts by default' - : `Using charts from the artifactory by default, version ${activeVersion.version}` + : `Using charts from the ghcr by default, version ${activeVersion.version}` ); console.error(`CLUSTER_BASENAME: ${CLUSTER_BASENAME}`); console.error(`Installing SV node in namespace: ${svNamespaceStr}`); @@ -419,6 +420,9 @@ async function installSvAndValidator( enable: true, }, ...synchronizerValues, + additionalEnvVars: (defaultScanValues.additionalEnvVars || []).concat([ + envoyClientIpHeaderEnvVar('canton.scan-apps.scan-app'), + ]), resources: svConfig.scanApp?.resources, pvc: persistentHeapDumpsPvc(), }; diff --git a/cluster/pulumi/sv-runbook/src/postgres.ts b/cluster/pulumi/sv-runbook/src/postgres.ts index 83952ccd9e..9336973aa4 100644 --- a/cluster/pulumi/sv-runbook/src/postgres.ts +++ b/cluster/pulumi/sv-runbook/src/postgres.ts @@ -10,7 +10,11 @@ import { supportsSvRunbookReset, } from '@canton-network/splice-pulumi-common'; import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; -import { CloudPostgres, SplicePostgres } from '@canton-network/splice-pulumi-common/src/postgres'; +import { + CloudPostgres, + installPasswordWithParent, + SplicePostgres, +} from '@canton-network/splice-pulumi-common/src/postgres'; export async function installPostgres( xns: ExactNamespace, @@ -40,11 +44,15 @@ export async function installPostgres( return new SplicePostgres( xns, name, - name, - secretName, + parent => installPasswordWithParent(parent, xns, name, secretName), + // No need to support legacy chart + { + deployment: 'docker-image', + postgresImage: valuesFromFile.db.postgresImage || 'postgres:18', + }, values, - undefined, - supportsSvRunbookReset + undefined, // overrideDbSizeFromValues + supportsSvRunbookReset // disableProtection ); } } diff --git a/cluster/pulumi/sv-runbook/tsconfig.eslint.json b/cluster/pulumi/sv-runbook/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/sv-runbook/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/sv/src/installNode.ts b/cluster/pulumi/sv/src/installNode.ts index a5fea169f3..5b6ba4ead3 100644 --- a/cluster/pulumi/sv/src/installNode.ts +++ b/cluster/pulumi/sv/src/installNode.ts @@ -7,6 +7,7 @@ import { exactNamespace, imagePullSecretWithNonDefaultServiceAccount, installLedgerApiUserSecret, + spliceConfig, } from '@canton-network/splice-pulumi-common'; import { configForSv, @@ -14,18 +15,39 @@ import { svConfigs, svRunbookConfig, } from '@canton-network/splice-pulumi-common-sv'; +import { + installSvNodeStandalone, + MigrationArgs, + SvsMigrationOutput, +} from '@canton-network/splice-pulumi-common-sv/src/sv'; +import { StackReferences } from '@canton-network/splice-pulumi-common/src/stackReferences'; import { installParticipant } from './participant'; export async function installNode(sv: string, auth0Client: Auth0Client): Promise { + // TODO(#6719) once all clusters have been migrated hardcode splitSvDeploymentEnabled to true + const splitSvDeploymentEnabled = + spliceConfig.configuration.synchronizerMigration.splitSvDeploymentEnabled; const staticConfig = findStaticConfigOrFail(sv); const config = configForSv(staticConfig.nodeName); - const xns = exactNamespace(staticConfig.nodeName, true, true); + const xns = exactNamespace(staticConfig.nodeName, true, !splitSvDeploymentEnabled); const serviceAccountName = 'sv'; const imagePullDeps = imagePullSecretWithNonDefaultServiceAccount(xns, serviceAccountName); const auth0Config = auth0Client.getCfg(); const ledgerApiUserSecret = installLedgerApiUserSecret(auth0Client, xns, 'sv', 'sv'); const ledgerApiUserSecretSource = auth0UserNameEnvVarSource('sv', true); + // TODO(#6719) once all clusters have been migrated remove this + const migrateToSplitSvDeployment = + spliceConfig.configuration.synchronizerMigration.migrateToSplitSvDeployment; + if ( + (splitSvDeploymentEnabled || migrateToSplitSvDeployment) && + staticConfig.nodeName !== svRunbookConfig.nodeName + ) { + const migrationArgs = migrateToSplitSvDeployment + ? await getMigrationArgsForSv(staticConfig.nodeName) + : undefined; + await installSvNodeStandalone(xns, staticConfig, config, auth0Client, [], migrationArgs); + } await installParticipant( { xns, @@ -51,3 +73,18 @@ function findStaticConfigOrFail(sv: string): StaticSvConfig { return svConfig; } } + +// TODO(#6719) once all clusters have been migrated remove this +async function getMigrationArgsForSv(nodeName: string): Promise { + const svs = (await StackReferences.cantonNetwork.requireOutputValue('svs')) as SvsMigrationOutput; + const sv = + svs.find(sv => sv.nodeName === nodeName) ?? + (() => { + throw new Error(`No migration output found for SV: ${nodeName}`); + })(); + return { + action: 'import', + databaseInstanceName: sv.databaseInstanceName, + databaseSecretName: sv.databaseSecretName, + }; +} diff --git a/cluster/pulumi/sv/src/participant.ts b/cluster/pulumi/sv/src/participant.ts index 83117eb9c2..ac372bd715 100644 --- a/cluster/pulumi/sv/src/participant.ts +++ b/cluster/pulumi/sv/src/participant.ts @@ -61,6 +61,7 @@ async function installParticipantPostgres({ 'participant-pg', version, participant?.cloudSql ?? spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, true, { disableProtection, diff --git a/cluster/pulumi/sv/tsconfig.eslint.json b/cluster/pulumi/sv/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/sv/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/tsconfig.eslint.json b/cluster/pulumi/tsconfig.eslint.json new file mode 100644 index 0000000000..843ee98bc1 --- /dev/null +++ b/cluster/pulumi/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["*.ts"], + "exclude": [] +} diff --git a/cluster/pulumi/validator-runbook/src/installNode.ts b/cluster/pulumi/validator-runbook/src/installNode.ts index ad83a65b39..7ac1faedd5 100644 --- a/cluster/pulumi/validator-runbook/src/installNode.ts +++ b/cluster/pulumi/validator-runbook/src/installNode.ts @@ -45,7 +45,10 @@ import { } from '@canton-network/splice-pulumi-common'; import { installLoopback } from '@canton-network/splice-pulumi-common-sv'; import { installParticipant } from '@canton-network/splice-pulumi-common-validator'; -import { SplicePostgres } from '@canton-network/splice-pulumi-common/src/postgres'; +import { + installPasswordWithParent, + SplicePostgres, +} from '@canton-network/splice-pulumi-common/src/postgres'; import { installPartyAllocator } from './partyAllocator'; import { validatorConfig, validatorName } from './validatorConfig'; @@ -69,7 +72,7 @@ export async function installNode(auth0Client: Auth0Client): Promise { console.error( validatorVersion.type === 'local' ? 'Using locally built charts by default' - : `Using charts from the artifactory by default, version ${validatorVersion.version}` + : `Using charts from the ghcr by default, version ${validatorVersion.version}` ); const xns = exactNamespace(validatorConfig.namespace, true); @@ -168,12 +171,16 @@ async function installValidator( db: { ...postgresValuesFromFile.db, volumeSize: validatorConfig.postgresPvcSize }, } : postgresValuesFromFile; + const postgresInstanceName = 'postgres'; const postgres = new SplicePostgres( xns, - 'postgres', - // can be removed once base version > 0.2.1 - `postgres`, - 'postgres-secrets', + postgresInstanceName, + parent => installPasswordWithParent(parent, xns, postgresInstanceName, 'postgres-secrets'), + // No need to support legacy chart + { + deployment: 'docker-image', + postgresImage: postgresValuesFromFile.db.postgresImage || 'postgres:18', + }, postgresValues, true, supportsValidatorRunbookReset, @@ -182,7 +189,6 @@ async function installValidator( const participantAddress = ( await installParticipant( validatorConfig, - DecentralizedSynchronizerUpgradeConfig.activeMigrationId, xns, auth0Client.getCfg(), false, // We don't currently support non-auth for validator-runbook @@ -247,7 +253,8 @@ async function installValidator( ...(participantBootstrapDumpSecret ? { nodeIdentifier: newParticipantIdentifier } : {}), persistence: { ...validatorValuesFromYamlFiles.persistence, - postgresName: 'postgres', + postgresName: postgres.instanceName, + host: postgres.address, }, pvc: { volumeStorageClass: standardStorageClassName, diff --git a/cluster/pulumi/validator-runbook/src/partyAllocator.ts b/cluster/pulumi/validator-runbook/src/partyAllocator.ts index 797c709f84..60006d771e 100644 --- a/cluster/pulumi/validator-runbook/src/partyAllocator.ts +++ b/cluster/pulumi/validator-runbook/src/partyAllocator.ts @@ -5,7 +5,6 @@ import { activeVersion, CnInput, createVolumeSnapshot, - DecentralizedSynchronizerUpgradeConfig, ExactNamespace, InstalledHelmChart, installSpliceHelmChart, @@ -13,8 +12,6 @@ import { } from '@canton-network/splice-pulumi-common'; import { PartyAllocatorConfig } from '@canton-network/splice-pulumi-common-validator'; -import { hyperdiskSupportConfig } from '../../common/src/config/hyperdiskSupportConfig'; - export function installPartyAllocator( xns: ExactNamespace, config: PartyAllocatorConfig, @@ -28,7 +25,7 @@ export function installPartyAllocator( config: { token: '${SPLICE_APP_VALIDATOR_LEDGER_API_AUTH_TOKEN}', userId: '${SPLICE_APP_VALIDATOR_LEDGER_API_AUTH_USER_NAME}', - jsonLedgerApiUrl: `http://participant-${DecentralizedSynchronizerUpgradeConfig.activeMigrationId}:7575`, + jsonLedgerApiUrl: `http://participant:7575`, scanApiUrl: 'http://scan-app.sv-1:5012', validatorApiUrl: 'http://validator-app:5003', maxParties: config.maxParties, @@ -40,9 +37,7 @@ export function installPartyAllocator( pvc: { ...(config.pvcSize ? { size: config.pvcSize } : {}), volumeStorageClass: standardStorageClassName, - name: hyperdiskSupportConfig.hyperdiskSupport.enabled - ? 'party-allocator-keys-hd-pvc' - : 'party-allocator-keys', + name: 'party-allocator-keys-hd-pvc', }, }, activeVersion, diff --git a/cluster/pulumi/validator-runbook/tsconfig.eslint.json b/cluster/pulumi/validator-runbook/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/validator-runbook/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/pulumi/validator1/src/validator1.ts b/cluster/pulumi/validator1/src/validator1.ts index 39007b0cd3..4f84de9529 100644 --- a/cluster/pulumi/validator1/src/validator1.ts +++ b/cluster/pulumi/validator1/src/validator1.ts @@ -60,6 +60,7 @@ export async function installValidator1( 'postgres', activeVersion, spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, false ) : undefined; @@ -72,6 +73,7 @@ export async function installValidator1( `validator-pg`, activeVersion, spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, true )); const validatorDbName = `validator1`; @@ -80,7 +82,6 @@ export async function installValidator1( const participant = await installParticipant( validator1Config, - decentralizedSynchronizerMigrationConfig.activeMigrationId, xns, auth0Client.getCfg(), validator1Config?.disableAuth, @@ -130,7 +131,7 @@ export async function installValidator1( version: activeVersion, additionalEnvVars: validator1Config?.validatorApp?.additionalEnvVars, }); - installIngress(xns, installSplitwell, decentralizedSynchronizerMigrationConfig); + installIngress(xns, installSplitwell); if (installSplitwell) { installSpliceHelmChart( @@ -156,11 +157,7 @@ export async function installValidator1( return validator; } -function installIngress( - xns: ExactNamespace, - splitwell: boolean, - decentralizedSynchronizerMigrationConfig: DecentralizedSynchronizerMigrationConfig -) { +function installIngress(xns: ExactNamespace, splitwell: boolean) { installSpliceHelmChart( xns, `cluster-ingress-${xns.logicalName}`, @@ -176,9 +173,7 @@ function installIngress( }, ingress: { splitwell: splitwell, - decentralizedSynchronizer: { - activeMigrationId: decentralizedSynchronizerMigrationConfig.activeMigrationId.toString(), - }, + decentralizedSynchronizer: {}, }, } ); diff --git a/cluster/pulumi/validator1/tsconfig.eslint.json b/cluster/pulumi/validator1/tsconfig.eslint.json new file mode 100644 index 0000000000..f88dce88fd --- /dev/null +++ b/cluster/pulumi/validator1/tsconfig.eslint.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.json"} diff --git a/cluster/scripts/find-recent-backup.sh b/cluster/scripts/find-recent-backup.sh index 4ed3d89caa..f646ffd1f2 100755 --- a/cluster/scripts/find-recent-backup.sh +++ b/cluster/scripts/find-recent-backup.sh @@ -17,11 +17,11 @@ function usage() { function is_full_backup_kube() { local component_backup_names=$1 - local expected_components=$2 + local expected_patterns=$2 # Check if all expected components can be found in the component_backup_names - for component in $expected_components; do - count=$(echo "$component_backup_names" | grep -c "$component") + for pattern in $expected_patterns; do + count=$(echo "$component_backup_names" | grep -c -F -- "$pattern") if [ "$count" -ne 1 ]; then return 1 fi @@ -43,8 +43,17 @@ function latest_full_backup_run_id_kube() { local is_sv=$3 local expected_components=$4 local before_timestamp=$5 - if [ "$is_sv" == "true" ]; then - expected_components="$expected_components cometbft" + local include_cometbft=$6 + + local expected_patterns="" + for component in $expected_components; do + local instance + instance="$(create_component_instance "$component" "$migration_id" "$namespace")" + expected_patterns="${expected_patterns:+$expected_patterns }-${instance}-pg-" + done + + if [ "$is_sv" == "true" ] && [ "$include_cometbft" == "true" ]; then + expected_patterns="$expected_patterns cometbft" fi local all_run_ids @@ -53,7 +62,7 @@ function latest_full_backup_run_id_kube() { while read -r run_id; do component_backup_names=$(get_component_backup_names_kube "$migration_id" "$run_id") - if is_full_backup_kube "$component_backup_names" "$expected_components"; then + if is_full_backup_kube "$component_backup_names" "$expected_patterns"; then echo "$run_id" return 0 fi @@ -70,6 +79,7 @@ function latest_full_backup_run_id_gcloud() { local stack declare -A backup_id_dict + local sequencer_end_time="" # participant backup must be newer than cn-apps backup stack=$(get_stack_for_namespace_component "$namespace" "participant") @@ -89,6 +99,7 @@ function latest_full_backup_run_id_gcloud() { for component in $expected_components; do [ "$component" == "participant" ] && continue + [ "$component" == "cantonBft" ] && continue stack=$(get_stack_for_namespace_component "$namespace" "$component") instance="$(create_component_instance "$component" "$migration_id" "$namespace")" @@ -97,21 +108,43 @@ function latest_full_backup_run_id_gcloud() { local cloudsql_id cloudsql_id=$(get_cloudsql_id "$full_component_instance" "$stack") - local backup_id + local entry if [ "$component" == "cn-apps" ]; then # cn-apps backup must be older than participant backup - backup_id=$(gcloud sql backups list --instance "$cloudsql_id" --format=json | jq -r --arg pt "$participant_end_time" '[.[] | select(.endTime <= $pt)] | first | .id') + entry=$(gcloud sql backups list --instance "$cloudsql_id" --format=json | jq -r --arg pt "$participant_end_time" '[.[] | select(.endTime <= $pt)] | first | "\(.id) \(.endTime)"') else - backup_id=$(gcloud sql backups list --instance "$cloudsql_id" --format=json | jq -r --argjson ts "$before_timestamp" '[.[] | select(.endTime <= ($ts | todate))] | first | .id') + entry=$(gcloud sql backups list --instance "$cloudsql_id" --format=json | jq -r --argjson ts "$before_timestamp" '[.[] | select(.endTime <= ($ts | todate))] | first | "\(.id) \(.endTime)"') fi + local backup_id + backup_id=$(echo "$entry" | awk '{print $1}') + local backup_end_time + backup_end_time=$(echo "$entry" | awk '{print $2}') if [ -z "$backup_id" ] || [ "$backup_id" == "null" ]; then _error "No backup found for component $component (instance $cloudsql_id) before timestamp $before_timestamp" fi backup_id_dict[$component]="$backup_id" + + if [ "$component" == "sequencer" ]; then + sequencer_end_time="$backup_end_time" + fi done + # cantonBft backup must be older than the sequencer backup + if [[ " $expected_components " == *" cantonBft "* ]]; then + stack=$(get_stack_for_namespace_component "$namespace" "cantonBft") + instance="$(create_component_instance "cantonBft" "$migration_id" "$namespace")" + local bft_cloudsql_id + bft_cloudsql_id=$(get_cloudsql_id "$namespace-$instance-pg" "$stack") + local bft_backup_id + bft_backup_id=$(gcloud sql backups list --instance "$bft_cloudsql_id" --format=json | jq -r --arg st "$sequencer_end_time" '[.[] | select(.endTime <= $st)] | first | .id') + if [ -z "$bft_backup_id" ] || [ "$bft_backup_id" == "null" ]; then + _error "No backup found for component cantonBft (instance $bft_cloudsql_id) before sequencer backup time $sequencer_end_time" + fi + backup_id_dict["cantonBft"]="$bft_backup_id" + fi + local result="" for component in "${!backup_id_dict[@]}"; do result="${result:+$result,}$component:${backup_id_dict[$component]}" @@ -138,11 +171,30 @@ function main() { before_timestamp=$(date +%s) fi + local config + config=$(get_resolved_config) + local bft_sequencer_enabled + bft_sequencer_enabled=$(echo "$config" | yq " + ([.synchronizerMigration.active, .synchronizerMigration.upgrade, .synchronizerMigration.legacy] + + (.synchronizerMigration.archived // []) + + (.synchronizerMigration.additionalLegacy // [])) + | map(select(.id == $migration_id)) + | .[0].sequencer.enableBftSequencer // false") + local include_cometbft="true" + if [ "$bft_sequencer_enabled" == "true" ]; then + include_cometbft="false" + fi + case "$namespace" in sv|sv-[0-9]|sv-[0-9][0-9]|sv-da-*) is_sv=true full_instance="$namespace-cn-apps-pg" expected_components="cn-apps sequencer participant mediator" + local bft_db_enabled + bft_db_enabled=$(canton_bft_db_enabled "$migration_id" "$config") + if [ "$bft_db_enabled" == "true" ]; then + expected_components="$expected_components cantonBft" + fi stack=$(get_stack_for_namespace_component "$namespace" "cn-apps") ;; *) @@ -156,7 +208,7 @@ function main() { type=$(get_postgres_type "$full_instance" "$stack") # We only check the postgres type of one component and assume other components have the same type. if [ "$type" == "canton:network:postgres" ]; then - backup_run_id=$(latest_full_backup_run_id_kube "$namespace" "$migration_id" "$is_sv" "$expected_components" "$before_timestamp") + backup_run_id=$(latest_full_backup_run_id_kube "$namespace" "$migration_id" "$is_sv" "$expected_components" "$before_timestamp" "$include_cometbft") echo "$backup_run_id" elif [ "$type" == "canton:cloud:postgres" ]; then backup_map_id=$(latest_full_backup_run_id_gcloud "$namespace" "$migration_id" "$is_sv" "$expected_components" "$before_timestamp") diff --git a/cluster/scripts/monitor-sv-catchup.sh b/cluster/scripts/monitor-sv-catchup.sh deleted file mode 100755 index 550b1bcfa3..0000000000 --- a/cluster/scripts/monitor-sv-catchup.sh +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -set -eou pipefail - -# Monitors the catchup progress of an SV after a restore from an old backup. -# Reads thresholds from the resolved config (testing.catchup.thresholds). -# Posts a Slack message with per-component rates and pass/fail outcome. -# -# Usage: monitor-sv-catchup.sh - -# shellcheck disable=SC1091 -source "${TOOLS_LIB}/libcli.source" -# shellcheck disable=SC1091 -source "${SPLICE_ROOT}/cluster/scripts/utils.source" - -namespace=${1:?namespace must be provided} -slack_channel=${2:?slack_channel must be provided} - -# Read thresholds from config, with defaults if not set -config=$(get_resolved_config) -seq_min_eps=$(echo "$config" | yq ".svs.${namespace}.testing.catchup.thresholds.sequencerMinEventsPerSecond // 1000") -part_min_eps=$(echo "$config" | yq ".svs.${namespace}.testing.catchup.thresholds.participantMinEventsPerSecond // 1000") -med_min_eps=$(echo "$config"| yq ".svs.${namespace}.testing.catchup.thresholds.mediatorMinEventsPerSecond // 1000") -seq_delay_ok=$(echo "$config" | yq ".svs.${namespace}.testing.catchup.thresholds.sequencerBlockDelaySeconds // 30") -part_delay_ok=$(echo "$config"| yq ".svs.${namespace}.testing.catchup.thresholds.participantDelaySeconds // 30") -med_delay_ok=$(echo "$config" | yq ".svs.${namespace}.testing.catchup.thresholds.mediatorDelaySeconds // 30") - -if [[ "$GCP_CLUSTER_BASENAME" == *scratch* ]]; then - PROM="https://prometheus.${GCP_CLUSTER_BASENAME}.network.canton.global" -else - PROM="https://prometheus.${GCP_CLUSTER_BASENAME}.global.canton.network.digitalasset.com" -fi - -outcome="timed_out" -poll_interval=60 -start=$(date +%s) -start_time=$(date -u -d @"$start" '+%Y-%m-%dT%H:%M:%S.%3NZ') -timeout_hours="8" -timeout_secs=$(( timeout_hours * 3600 )) - -# Helper to query Prometheus for a single value -# Fetch data from 2 minutes ago -# Default to value higher than threshold but less than expected max -function query_prom() { - local default=${2:-"180"} - local ts - ts=$(date -d '2 minutes ago' +%s) - curl -ksf "${PROM}/api/v1/query" \ - --data-urlencode "query=${1}" \ - --data-urlencode "time=${ts}" \ - | jq -r ".data.result[0].value[1] // \"${default}\"" -} - -function query_seq_delay() { - query_prom "min by (namespace, job) (daml_sequencer_block_delay{namespace=\"${namespace}\", component=\"sequencer\", job=~\"global-domain-.*-sequencer\"}) / 1000" "${seq_delay_ok}" -} -function query_part_delay() { - query_prom "max by (namespace) (timestamp(daml_sequencer_client_handler_last_sequencing_time_micros{namespace=\"${namespace}\",component=\"participant\"}) - (daml_sequencer_client_handler_last_sequencing_time_micros{namespace=\"${namespace}\",component=\"participant\"} / 1e6))" "${part_delay_ok}" -} -function query_med_delay() { - query_prom "max by (namespace, job) (timestamp(daml_sequencer_client_handler_last_sequencing_time_micros{namespace=\"${namespace}\",component=\"mediator\",job=~\"global-domain-.*-mediator\"}) - (daml_sequencer_client_handler_last_sequencing_time_micros{namespace=\"${namespace}\",component=\"mediator\",job=~\"global-domain-.*-mediator\"} / 1e6))" "${med_delay_ok}" -} - -function query_seq_rate() { - query_prom "sum by(namespace, job) (rate(daml_sequencer_block_events_total{namespace=\"${namespace}\", job=~\"global-domain-.*-sequencer\"}[1m]))" "0" -} -function query_part_rate() { - query_prom "sum by (namespace) (rate(daml_sequencer_client_handler_sequencer_events{namespace=\"${namespace}\",component=\"participant\"}[1m]))" "0" -} -function query_med_rate() { - query_prom "sum by (namespace) (rate(daml_sequencer_client_handler_sequencer_events{namespace=\"${namespace}\",component=\"mediator\",job=~\"global-domain-.*-mediator\"}[1m]))" "0" -} - -# Accumulators inside the loop -seq_rate_sum=0 -part_rate_sum=0 -med_rate_sum=0 -rate_sample_count=0 - -_info "Monitoring catchup for ${namespace}" -_info "Thresholds: seq>=${seq_min_eps} eps, participant>=${part_min_eps} eps, mediator>=${med_min_eps} eps" -_info "Caught-up when: seq<=${seq_delay_ok}s, participant<=${part_delay_ok}s, mediator<=${med_delay_ok}s" -_info "Test timeout: ${timeout_hours}h" - -while true; do - elapsed=$(( $(date +%s) - start )) - if [ "$elapsed" -ge "$timeout_secs" ]; then - _error_msg "Catchup timed out after ${timeout_hours}h" - break - fi - - # Track delays to determine if caught up - seq_delay=$(query_seq_delay) - part_delay=$(query_part_delay) - med_delay=$(query_med_delay) - - seq_caught=$(echo "$seq_delay < $seq_delay_ok" | bc -l) - part_caught=$(echo "$part_delay < $part_delay_ok" | bc -l) - med_caught=$(echo "$med_delay < $med_delay_ok" | bc -l) - - _info "Delays — seq: ${seq_delay}s, participant: ${part_delay}s, mediator: ${med_delay}s (elapsed: ${elapsed}s)" - - if [ "$seq_caught" = "1" ] && [ "$part_caught" = "1" ] && [ "$med_caught" = "1" ]; then - _info "All components caught up" - outcome="success" - break - fi - - # Track peak rates during catchup - seq_rate=$(query_seq_rate) - part_rate=$(query_part_rate) - med_rate=$(query_med_rate) - - seq_rate_sum=$(echo "$seq_rate_sum + $seq_rate" | bc -l) - part_rate_sum=$(echo "$part_rate_sum + $part_rate" | bc -l) - med_rate_sum=$(echo "$med_rate_sum + $med_rate" | bc -l) - rate_sample_count=$((rate_sample_count + 1)) - - _info "Mean rates so far> seq: $(echo "scale=1; $seq_rate_sum / $rate_sample_count" | bc) eps, participant: $(echo "scale=1; $part_rate_sum / $rate_sample_count" | bc) eps, mediator: $(echo "scale=1; $med_rate_sum / $rate_sample_count" | bc) eps" - sleep "$poll_interval" -done - -elapsed=$(( $(date +%s) - start )) -elapsed_mins=$(( elapsed / 60 )) -end_time=$(date -u '+%Y-%m-%dT%H:%M:%S.%3NZ') - -if [ "$rate_sample_count" -gt 0 ]; then - seq_rate_mean=$(echo "$seq_rate_sum / $rate_sample_count" | bc -l) - part_rate_mean=$(echo "$part_rate_sum / $rate_sample_count" | bc -l) - med_rate_mean=$(echo "$med_rate_sum / $rate_sample_count" | bc -l) -else - seq_rate_mean=0 - part_rate_mean=0 - med_rate_mean=0 -fi - -seq_ok=$(echo "$seq_rate_mean >= $seq_min_eps"| bc -l) -part_ok=$(echo "$part_rate_mean >= $part_min_eps" | bc -l) -med_ok=$(echo "$med_rate_mean >= $med_min_eps" | bc -l) - -if [ "$outcome" = "success" ]; then - icon="✅" - exit_code=0 -else - icon="❌" - exit_code=1 -fi - -grafana_base="https://grafana.${GCP_CLUSTER_BASENAME}.global.canton.network.digitalasset.com" -grafana_domain_link="${grafana_base}/d/ca9df344-c699-4efe-83c2-5fb2639d96d9/global-domain-catchup?orgId=1&timezone=UTC&var-DS=prometheus&var-namespace=${namespace}&var-migration=All&viewPanel=panel-11&from=${start_time}&to=${end_time}" -grafana_participant_link="${grafana_base}/d/edkzo5ukgeqyoc/participant?orgId=1&timezone=UTC&var-namespace=${namespace}&var-job=All&var-participant=All&viewPanel=panel-13&from=${start_time}&to=${end_time}" - -message="${icon} *SV Catchup Test — \`${namespace}\` on \`${GCP_CLUSTER_BASENAME}\`* -Outcome: ${outcome} | Duration: ${elapsed_mins}m | Started: ${start_time} | Ended: ${end_time} - -*Per-component mean rates over catchup window (${rate_sample_count} samples):* -• Sequencer: \`$(printf "%.1f" "$seq_rate_mean")\` events/s (expected ≥ ${seq_min_eps}) $([ "$seq_ok" = "1" ] && echo "✅" || echo "❌") -• Participant: \`$(printf "%.1f" "$part_rate_mean")\` events/s (expected ≥ ${part_min_eps}) $([ "$part_ok" = "1" ] && echo "✅" || echo "❌") -• Mediator: \`$(printf "%.1f" "$med_rate_mean")\` events/s (expected ≥ ${med_min_eps}) $([ "$med_ok" = "1" ] && echo "✅" || echo "❌") - -*Dashboards:* -• <${grafana_domain_link}|Sequencer> -• <${grafana_participant_link}|Participant and Mediator>" - -_info "$message" - -"${DA_REPO_ROOT}/.circleci/scripts/slack/post-slack-message.sh" \ - "$message" "$slack_channel" - -exit $exit_code diff --git a/cluster/scripts/node-backup.sh b/cluster/scripts/node-backup.sh index e0acd20ec9..ebc0e869fe 100755 --- a/cluster/scripts/node-backup.sh +++ b/cluster/scripts/node-backup.sh @@ -71,18 +71,11 @@ function backup_pvc_postgres() { local namespace=$2 local instance=$3 local migration_id=$4 - local hyperdisk_enabled=$5 _info "** Backup up pvc-based postgres $description **" - # Since we only have one replica, it's always 0. - replica_index="0" local pvc_name - if [ "$hyperdisk_enabled" = "true" ]; then - pvc_name="pg-data-hd-$instance-$replica_index" - else - pvc_name="pg-data-$instance-$replica_index" - fi + pvc_name=$(get_postgres_pvc_name "$namespace" "$instance") backup_pvc "$description" "$namespace" "$pvc_name" "$migration_id" } @@ -174,14 +167,13 @@ function backup_postgres() { local instance=$3 local migration_id=$4 local stack=$5 - local hyperdisk_enabled=$6 local full_instance="$namespace-$instance" type=$(get_postgres_type "$full_instance" "$stack") if [ "$type" == "canton:network:postgres" ]; then - backup_pvc_postgres "$description" "$namespace" "$instance" "$migration_id" "$hyperdisk_enabled" + backup_pvc_postgres "$description" "$namespace" "$instance" "$migration_id" elif [ "$type" == "canton:cloud:postgres" ]; then backup_cloudsql "$description" "$full_instance" "$stack" elif [ -z "$type" ]; then @@ -197,21 +189,14 @@ function wait_for_postgres_backup() { local instance=$3 local migration_id=$4 local stack=$5 - local hyperdisk_enabled=$6 local full_instance="$namespace-$instance" type=$(get_postgres_type "$full_instance" "$stack") if [ "$type" == "canton:network:postgres" ]; then - # Since we only have one replica, it's always 0. - replica_index="0" local pvc_name - if [ "$hyperdisk_enabled" = "true" ]; then - pvc_name="pg-data-hd-$instance-$replica_index" - else - pvc_name="pg-data-$instance-$replica_index" - fi + pvc_name=$(get_postgres_pvc_name "$namespace" "$instance") wait_for_pvc_backup "$description" "$namespace" "$pvc_name" elif [ "$type" == "canton:cloud:postgres" ]; then wait_for_cloudsql_backup "$description" "$full_instance" "$stack" @@ -226,7 +211,6 @@ function backup_component() { local component=$2 local requested_component=$3 local migration_id=$4 - local hyperdisk_enabled=$5 local stack stack=$(get_stack_for_namespace_component "$namespace" "$component") @@ -236,16 +220,12 @@ function backup_component() { if [ "$component" == "$requested_component" ] || [ -z "$requested_component" ]; then if [ "$component" == "cometbft-$migration_id" ]; then local cometbft_pvc_name - if [ "$hyperdisk_enabled" = "true" ]; then - cometbft_pvc_name="cometbft-migration-${migration_id}-hd-pvc" - else - cometbft_pvc_name="global-domain-${migration_id}-cometbft-cometbft-data" - fi + cometbft_pvc_name="cometbft-migration-${migration_id}-hd-pvc" backup_pvc "cometBFT" "$namespace" "$cometbft_pvc_name" "$migration_id" else local db_name db_name=$(create_component_instance "$component" "$migration_id" "$namespace") - SPLICE_SV=$namespace SPLICE_MIGRATION_ID=$migration_id backup_postgres "$component" "$namespace" "$db_name-pg" "$migration_id" "$stack" "$hyperdisk_enabled" + SPLICE_SV=$namespace SPLICE_MIGRATION_ID=$migration_id backup_postgres "$component" "$namespace" "$db_name-pg" "$migration_id" "$stack" fi else _info "Skipping backup of $component, not requested" @@ -257,7 +237,6 @@ function wait_for_backup() { local component=$2 local requested_component=$3 local migration_id=$4 - local hyperdisk_enabled=$5 local stack stack=$(get_stack_for_namespace_component "$namespace" "$component") @@ -265,16 +244,12 @@ function wait_for_backup() { if [ "$component" == "$requested_component" ] || [ -z "$requested_component" ]; then if [ "$component" == "cometbft-$migration_id" ]; then local cometbft_pvc_name - if [ "$hyperdisk_enabled" = "true" ]; then - cometbft_pvc_name="cometbft-migration-${migration_id}-hd-pvc" - else - cometbft_pvc_name="global-domain-${migration_id}-cometbft-cometbft-data" - fi + cometbft_pvc_name="cometbft-migration-${migration_id}-hd-pvc" wait_for_pvc_backup "cometBFT" "$namespace" "$cometbft_pvc_name" else local instance instance=$(create_component_instance "$component" "$migration_id" "$namespace") - wait_for_postgres_backup "$component" "$namespace" "$instance-pg" "$migration_id" "$stack" "$hyperdisk_enabled" + wait_for_postgres_backup "$component" "$namespace" "$instance-pg" "$migration_id" "$stack" fi else _info "Skipping waiting for backup of $component, not requested" @@ -295,37 +270,56 @@ function main() { local migration_id=$3 local requested_component="${4:-}" - # Get resolved config and extract hyperdisk support flag local config config=$(get_resolved_config) - local hyperdisk_enabled - hyperdisk_enabled=$(echo "$config" | yq '.cluster.hyperdiskSupport.enabled // false') + + # Determine whether the BFT sequencer is enabled for the migration being backed up. + local bft_sequencer_enabled + bft_sequencer_enabled=$(echo "$config" | yq " + ([.synchronizerMigration.active, .synchronizerMigration.upgrade, .synchronizerMigration.legacy] + + (.synchronizerMigration.archived // []) + + (.synchronizerMigration.additionalLegacy // [])) + | map(select(.id == $migration_id)) + | .[0].sequencer.enableBftSequencer // false") # TODO(#9361): support multiple domains / non-default-ID'd ones if [ "$1" == "validator" ]; then _info "Backing up validator $namespace" - backup_component "$namespace" "validator" "$requested_component" "$migration_id" "$hyperdisk_enabled" - wait_for_backup "$namespace" "validator" "$requested_component" "$migration_id" "$hyperdisk_enabled" + backup_component "$namespace" "validator" "$requested_component" "$migration_id" + wait_for_backup "$namespace" "validator" "$requested_component" "$migration_id" # CN apps must be strictly before participant, so we sync on apps before starting the participant backup - backup_component "$namespace" "participant" "$requested_component" "$migration_id" "$hyperdisk_enabled" - wait_for_backup "$namespace" "participant" "$requested_component" "$migration_id" "$hyperdisk_enabled" + backup_component "$namespace" "participant" "$requested_component" "$migration_id" + wait_for_backup "$namespace" "participant" "$requested_component" "$migration_id" elif [ "$1" == "sv" ]; then _info "Backing up SV node $namespace" - backup_component "$namespace" "cn-apps" "$requested_component" "$migration_id" "$hyperdisk_enabled" - backup_component "$namespace" "mediator" "$requested_component" "$migration_id" "$hyperdisk_enabled" - backup_component "$namespace" "sequencer" "$requested_component" "$migration_id" "$hyperdisk_enabled" - backup_component "$namespace" "cometbft-$migration_id" "$requested_component" "$migration_id" "$hyperdisk_enabled" + local bft_enabled + bft_enabled=$(canton_bft_db_enabled "$migration_id" "$config") + if [ "$bft_enabled" == "true" ]; then + backup_component "$namespace" "cantonBft" "$requested_component" "$migration_id" + wait_for_backup "$namespace" "cantonBft" "$requested_component" "$migration_id" + fi - wait_for_backup "$namespace" "cn-apps" "$requested_component" "$migration_id" "$hyperdisk_enabled" + backup_component "$namespace" "cn-apps" "$requested_component" "$migration_id" + backup_component "$namespace" "mediator" "$requested_component" "$migration_id" + backup_component "$namespace" "sequencer" "$requested_component" "$migration_id" + if [ "$bft_sequencer_enabled" == "true" ]; then + _info "BFT sequencer is enabled for migration $migration_id, skipping CometBFT backup" + else + backup_component "$namespace" "cometbft-$migration_id" "$requested_component" "$migration_id" + fi + + wait_for_backup "$namespace" "cn-apps" "$requested_component" "$migration_id" # CN apps must be strictly before participant, so we sync on apps before starting the participant backup - backup_component "$namespace" "participant" "$requested_component" "$migration_id" "$hyperdisk_enabled" + backup_component "$namespace" "participant" "$requested_component" "$migration_id" - wait_for_backup "$namespace" "participant" "$requested_component" "$migration_id" "$hyperdisk_enabled" - wait_for_backup "$namespace" "mediator" "$requested_component" "$migration_id" "$hyperdisk_enabled" - wait_for_backup "$namespace" "sequencer" "$requested_component" "$migration_id" "$hyperdisk_enabled" - wait_for_backup "$namespace" "cometbft-$migration_id" "$requested_component" "$migration_id" "$hyperdisk_enabled" + wait_for_backup "$namespace" "participant" "$requested_component" "$migration_id" + wait_for_backup "$namespace" "mediator" "$requested_component" "$migration_id" + wait_for_backup "$namespace" "sequencer" "$requested_component" "$migration_id" + if [ "$bft_sequencer_enabled" != "true" ]; then + wait_for_backup "$namespace" "cometbft-$migration_id" "$requested_component" "$migration_id" + fi else usage exit 1 diff --git a/cluster/scripts/node-restore.sh b/cluster/scripts/node-restore.sh index 616234e5d7..8417504cac 100755 --- a/cluster/scripts/node-restore.sh +++ b/cluster/scripts/node-restore.sh @@ -16,6 +16,8 @@ function component_to_deployments() { local -r namespace=$3 if [[ "$component" == "sequencer" ]]; then echo "global-domain-$migration_id-sequencer" + elif [[ "$component" == "cantonBft" ]]; then + echo "global-domain-$migration_id-sequencer" elif [[ "$component" == "mediator" ]]; then echo "global-domain-$migration_id-mediator" elif [[ "$component" == "participant" && "$namespace" == sv* ]]; then @@ -156,21 +158,12 @@ function restore_pvc_postgres() { local -r namespace=$1 local -r component=$2 local -r run_id=$3 - local -r hyperdisk_enabled=$4 - local template_name local storage_class - if [ "$hyperdisk_enabled" = "true" ]; then - template_name="pg-data-hd" - storage_class="hyperdisk-standard-rwo" - else - template_name="pg-data" - storage_class="standard-rwo" - fi + storage_class="hyperdisk-standard-rwo" - local -r ss_name="$component-pg" - local -r pg_pod_name="$ss_name-0" - local -r pvc_name="$template_name-$pg_pod_name" + local -r ss_name=$(get_postgres_statefulset_name "$namespace" "$component-pg") + local -r pvc_name=$(get_postgres_pvc_name "$namespace" "$component-pg") local -r snapshot_name="$pvc_name-$run_id" _info "Scaling down postgres StatefulSet" @@ -263,7 +256,6 @@ function restore_component() { local -r migration_id=$3 local -r run_id=$4 local -r restore_cluster=$5 # cluster to restore into (if different from current) - local -r hyperdisk_enabled=$6 local -r deployment_names=$(component_to_deployments "$component" "$migration_id" "$namespace") local stack @@ -275,20 +267,11 @@ function restore_component() { local cometbft_pvc_name local cometbft_snapshot_name - if [ "$hyperdisk_enabled" = "true" ]; then - cometbft_pvc_name="cometbft-migration-${migration_id}-hd-pvc" - cometbft_snapshot_name="${cometbft_pvc_name}-$run_id" - else - cometbft_pvc_name="global-domain-$migration_id-cometbft-cometbft-data" - cometbft_snapshot_name="${cometbft_pvc_name}-$run_id" - fi + cometbft_pvc_name="cometbft-migration-${migration_id}-hd-pvc" + cometbft_snapshot_name="${cometbft_pvc_name}-$run_id" local cometbft_storage_class - if [ "$hyperdisk_enabled" = "true" ]; then - cometbft_storage_class="hyperdisk-standard-rwo" - else - cometbft_storage_class="premium-rwo" - fi + cometbft_storage_class="hyperdisk-standard-rwo" restore_pvc_from_snapshot "$namespace" "$cometbft_snapshot_name" "$cometbft_pvc_name" "$cometbft_storage_class" kubectl scale deployment -n "$namespace" "${deployment_names}" --replicas=1 @@ -298,7 +281,7 @@ function restore_component() { type=$(get_postgres_type "$namespace-$instance-pg" "$stack") case "$type" in "canton:network:postgres") - restore_pvc_postgres "$namespace" "$instance" "$run_id" "$hyperdisk_enabled" + restore_pvc_postgres "$namespace" "$instance" "$run_id" ;; "canton:cloud:postgres") restore_cloudsql_postgres "$namespace" "$component" "$run_id" "$migration_id" "$restore_cluster" @@ -439,48 +422,72 @@ function main() { local config config=$(get_resolved_config) - local hyperdisk_enabled - hyperdisk_enabled=$(echo "$config" | yq '.cluster.hyperdiskSupport.enabled // false') + + # Determine whether the BFT sequencer is enabled for the migration being restored. + # When it is, CometBFT is not used and there is nothing to restore for it. + local bft_sequencer_enabled + bft_sequencer_enabled=$(echo "$config" | yq " + ([.synchronizerMigration.active, .synchronizerMigration.upgrade, .synchronizerMigration.legacy] + + (.synchronizerMigration.archived // []) + + (.synchronizerMigration.additionalLegacy // [])) + | map(select(.id == $migration_id)) + | .[0].sequencer.enableBftSequencer // false") + + local bft_db_enabled + bft_db_enabled=$(canton_bft_db_enabled "$migration_id" "$config") + + local -a components=() + for component in "${@:4}"; do + if [ "$component" == "cometbft" ] && [ "$bft_sequencer_enabled" == "true" ]; then + _info "BFT sequencer is enabled for migration $migration_id, skipping CometBFT restore" + continue + fi + if [ "$component" == "cantonBft" ] && [ "$bft_db_enabled" != "true" ]; then + _info "Dedicated CantonBFT sequencer DB is not enabled for migration $migration_id, skipping CantonBFT restore" + continue + fi + components+=("$component") + done if [[ "$run_id" == *","* ]]; then _info " ** Validate backup ids ** " local map_keys map_keys=$(echo "$run_id" | tr ',' '\n' | cut -d: -f1 | sort) local req_components - req_components=$(printf '%s\n' "${@:4}" | sort) + req_components=$(printf '%s\n' "${components[@]}" | sort) if [ "$map_keys" != "$req_components" ]; then - _error "Backup map keys ($map_keys) do not match requested components (${*:4})" + _error "Backup map keys ($map_keys) do not match requested components (${components[*]})" fi fi - for component in "${@:4}"; do + for component in "${components[@]}"; do component_to_deployments "$component" "$migration_id" "$namespace" done _info " ** Scaling down ** " - for component in "${@:4}"; do + for component in "${components[@]}"; do down "$namespace" "$component" "$migration_id" done - for component in "${@:4}"; do + for component in "${components[@]}"; do wait_down "$namespace" "$component" "$migration_id" done _info " ** Restoring ** " - for component in "${@:4}"; do + for component in "${components[@]}"; do local component_run_id component_run_id=$(get_component_run_id "$run_id" "$component") - restore_component "$namespace" "$component" "$migration_id" "$component_run_id" "$restore_cluster" "$hyperdisk_enabled" + restore_component "$namespace" "$component" "$migration_id" "$component_run_id" "$restore_cluster" done _info " ** Waiting for all restore operations to finish ** " - for component in "${@:4}"; do + for component in "${components[@]}"; do wait_restore_component "$namespace" "$component" done _info " ** Scaling up ** " - for component in "${@:4}"; do + for component in "${components[@]}"; do up "$namespace" "$component" "$migration_id" done diff --git a/cluster/scripts/utils.source b/cluster/scripts/utils.source index b870b56410..da6f79a103 100755 --- a/cluster/scripts/utils.source +++ b/cluster/scripts/utils.source @@ -30,18 +30,57 @@ function get_cloudsql_id() { fi } +function get_postgres_statefulset_name() { + local namespace=$1 + local instance=$2 + + local candidate + for candidate in "${instance}-helmless" "$instance"; do + if kubectl get statefulset -n "$namespace" "$candidate" >/dev/null 2>&1; then + echo "$candidate" + return 0 + fi + done + + _error "No postgres StatefulSet found for base name $instance in namespace $namespace" +} + +function get_postgres_pvc_name() { + local namespace=$1 + local instance=$2 + + local statefulset_name + statefulset_name=$(get_postgres_statefulset_name "$namespace" "$instance") + + local pvc_template_name + pvc_template_name=$(kubectl get statefulset -n "$namespace" "$statefulset_name" -o jsonpath='{.spec.volumeClaimTemplates[0].metadata.name}') + + if [ -z "$pvc_template_name" ]; then + _error "Unable to determine PVC template name for StatefulSet $statefulset_name in namespace $namespace" + fi + + echo "${pvc_template_name}-${statefulset_name}-0" +} + function get_stack_for_namespace_component() { local namespace=$1 local component=$2 + local split_sv_deployment + split_sv_deployment=$(get_resolved_config | yq '.synchronizerMigration.splitSvDeploymentEnabled // false') + local stack="" if [[ "${namespace}" =~ sv.* ]]; then if [[ "${component}" == "participant" ]]; then stack="sv" elif [[ "${component}" == "sequencer" ]]; then stack="sv-canton" + elif [[ "${component}" == "cantonBft" ]]; then + stack="sv-canton" elif [[ "${component}" == "mediator" ]]; then stack="sv-canton" + elif [[ "${split_sv_deployment}" == "true" ]]; then + stack="sv" else stack="canton-network" fi @@ -57,16 +96,35 @@ create_component_instance() { local migration_id="$2" local namespace="$3" - if [[ ("$component" == "sequencer" || "$component" == "mediator") + local instance_base="$component" + if [[ "$component" == "cantonBft" ]]; then + instance_base="sequencer-bft" + fi + + if [[ ("$component" == "sequencer" || "$component" == "cantonBft" || "$component" == "mediator") && ("$namespace" != "splitwell" && "$namespace" != "validator1" && "$namespace" != "sv") ]]; then - component_instance="${component}-${migration_id}" + component_instance="${instance_base}-${migration_id}" else - component_instance="${component}" + component_instance="${instance_base}" fi echo "$component_instance" } +function canton_bft_db_enabled() { + local migration_id=$1 + local config=$2 + + echo "$config" | yq " + ([.synchronizerMigration.active, .synchronizerMigration.upgrade, .synchronizerMigration.legacy] + + (.synchronizerMigration.additionalLegacy // []) + + (.synchronizerMigration.archived // [])) + | map(select(. != null and .id == ${migration_id})) + | .[0] + | ((.sequencer.enableBftSequencer // false) and (.sequencer.dedicatedBftSequencerDb // true)) + " +} + function get_resolved_config() { "${SPLICE_ROOT}/cluster/scripts/get-resolved-config.sh" } diff --git a/daml/daml-ide-mono/README.md b/daml/daml-ide-mono/README.md new file mode 100644 index 0000000000..4a24340200 --- /dev/null +++ b/daml/daml-ide-mono/README.md @@ -0,0 +1,15 @@ +# What is this? + +This directory, along with its fake `daml.yaml` can be populated with symlinks by +`./scripts/setup-mono-package.sh` to serve as a fake single-dar package containing all the daml in +Splice such that cross-package changes can be worked on with a tighter feedback loop using the +Daml Language Server (LSP) in VS Code or other editors, e.g. by running `dpm studio` in this directory +after having run the script. + +You'll also have to remember to re-run the script if you add new `.daml` files to any package. + +You should still verify that everything builds/tests for real once you're done of course, but this mode +of interaction can be very useful to eliminate 12-20 second build cycles and instead get immediate +feedback from the VS Code extension when working on tests and making changes that would otherwise be in +their upstream DAR dependencies, for example. + diff --git a/daml/daml-ide-mono/daml.yaml b/daml/daml-ide-mono/daml.yaml new file mode 100644 index 0000000000..e998fdd9a9 --- /dev/null +++ b/daml/daml-ide-mono/daml.yaml @@ -0,0 +1,17 @@ +sdk-version: 3.5.2 +name: splice-mono-ide +source: daml +version: 0.0.1 +dependencies: + - daml-prim + - daml-stdlib + - daml-script +build-options: + - --ghc-option=-Wunused-binds + - --ghc-option=-Wunused-matches + - --target=2.1 + - -Wno-upgrade-exceptions + - -Wno-deprecated-exceptions + - -Wno-template-interface-depends-on-daml-script + - -Wno-upgrade-interfaces + - --force-utility-package=no diff --git a/docs/gen-daml-docs.sh b/docs/gen-daml-docs.sh index ac5e208f7f..1fd1c1818e 100755 --- a/docs/gen-daml-docs.sh +++ b/docs/gen-daml-docs.sh @@ -47,6 +47,7 @@ DAML_PROJECT_FILES="\ -not -ipath '*splitwell*' \ -not -ipath '*app-manager*' \ -not -ipath '*dummy-holding*' \ + -not -ipath '*daml-ide-mono*' \ -print)" DAML_PROJECT_FILES=$(printf "%s\n" "$DAML_PROJECT_FILES" | grep -vf <(printf "%s\n" "${NON_COMPILED_DAML_PROJECTS[@]}" | xargs -n1 basename)) diff --git a/docs/src/release_notes_upcoming.rst b/docs/src/release_notes_upcoming.rst index f48afd779b..6986ce68a3 100644 --- a/docs/src/release_notes_upcoming.rst +++ b/docs/src/release_notes_upcoming.rst @@ -5,48 +5,40 @@ .. NOTE: add your upcoming release notes below this line. They are included in the `release_notes.rst`. -.. release-notes:: Upcoming - - .. note:: - - Next-release notes - - - Deployment - - - Helm - - - Added security contexts for all Helm-based deployments intended for production. - This improves the security of Kubernetes based deployments. - - - Scan - - - Add a metric for the size of the most recent ACS snapshot - - - Token Standard APIs - - - Add the ``accountInputFieldsToShow`` property in the token metadata - API (``token-metadata-v1.yaml``). - This property allows instruments that support only account-ids or only - account providers to inform wallets of this fact. - This change is backwards compatible. - - - Daml - - - Adds support for specifying weight on the ``FeaturedAppRight`` contract as described in - `CIP-0104 amendment `__. - - Fix a bug in ``AmuletAllocation``, which prohibited settling V1 amulet allocations when - using them with the Token Standard V2 feature of setting multiple - executors via metadata. - - - These changes require a Daml upgrade to the following versions: - - ================== ======= - name version - ================== ======= - amulet 0.1.22 - amuletNameService 0.1.23 - dsoGovernance 0.1.28 - validatorLifecycle 0.1.8 - wallet 0.1.23 - walletPayments 0.1.22 - ================== ======= +release-notes:: Upcoming + + - Scan & SV App + + - The client IP used for per-client-IP HTTP rate limiting is now extracted based on a + configurable, ordered list of headers, ``rate-limiting.client-ip-headers``, which defaults + to ``["x-forwarded-for", "x-real-ip"]``. The first configured header that is present and + whose value parses as an IP literal is used; for comma separated values (as in + ``X-Forwarded-For``) the first entry is taken. Configuring an empty list disables the + extraction, in which case no per-client-IP rate limit is enforced. + + This replaces the ``rate-limiting.trusted-client-ip-header`` and + ``rate-limiting.enable-client-provided-ip-headers`` options, which have been removed. + + - The endpoints ``/v1/state/acs`` and ``/v1/holdings/state`` are now deprecated + with the goal of them being replaced with their V2 counterparts. + The only change is the type of the pagination token (``after`` in request, ``next_page_token`` in response), + which is now a String instead of a number. + + - Docker + + - Updated Docker base image to 1.0.13, which updates gRPC health probe to v0.4.55. + + - SV app + + - The SV app OpenAPI specification now annotates endpoints + (``x-jvm-package: sv_public``) with an ``x-external-audience`` extension, which is one of + ``validators`` (endpoints that validator operators need to reach), ``svs`` + (endpoints that only other SVs need to reach) or ``none`` (endpoints that do not + need to be reachable from outside of the SV node's own deployment, e.g. the CometBFT + endpoints). SV operators can use this + annotation to restrict the external exposure of their SV app: only the endpoints of a + given audience need to be reachable from the corresponding networks, and endpoints with + an audience of ``none``, as well as endpoints without an ``x-external-audience``, do not + need to be exposed to external traffic at all. + Note that endpoints currently marked for exposure to validators will be phased out in the foreseeable future, + and replaced by a new limited number of endpoints which should be available only on DevNet. diff --git a/gha-scripts/package-lock.json b/gha-scripts/package-lock.json index 2c43862448..49f6f0c078 100644 --- a/gha-scripts/package-lock.json +++ b/gha-scripts/package-lock.json @@ -12,7 +12,7 @@ "@actions/core": "^3.0.1", "@actions/github": "^9.1.1", "@types/js-yaml": "^4.0.5", - "@types/node": "^25.9.4", + "@types/node": "^26.1.0", "esbuild": "^0.28.1", "typescript": "^6.0.3" } @@ -671,13 +671,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", - "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/argparse": { @@ -816,9 +816,9 @@ } }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, diff --git a/gha-scripts/package.json b/gha-scripts/package.json index 3a3c32efa5..7ef5e81922 100644 --- a/gha-scripts/package.json +++ b/gha-scripts/package.json @@ -13,7 +13,7 @@ "@actions/core": "^3.0.1", "@actions/github": "^9.1.1", "@types/js-yaml": "^4.0.5", - "@types/node": "^25.9.4", + "@types/node": "^26.1.0", "esbuild": "^0.28.1", "typescript": "^6.0.3" } diff --git a/load-tester/package-lock.json b/load-tester/package-lock.json index 47716d11ed..d552d057c4 100644 --- a/load-tester/package-lock.json +++ b/load-tester/package-lock.json @@ -719,9 +719,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, "license": "MIT", "dependencies": { @@ -731,7 +731,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -988,9 +988,9 @@ "peer": true }, "node_modules/@types/k6": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/k6/-/k6-2.0.0.tgz", - "integrity": "sha512-ztO2fVOAQxtCpF6VWTn8O6FW6Z4DpjhY+XvD2GpCe/+fAfekD45bQ+vrM0B0C0c5ajLpVgWTMlLu9dKZ1nI+wg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/k6/-/k6-2.0.1.tgz", + "integrity": "sha512-3lAXtNx1qMwnn+fbE65yP1h9ADJyG6xQaIfGrhasZI2uck8S+gLuCbQl38YkbR5lGeE/NyCiF0FznmElpUgBsA==", "dev": true, "license": "MIT" }, @@ -1005,17 +1005,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", - "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/type-utils": "8.61.1", - "@typescript-eslint/utils": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1028,7 +1028,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.61.1", + "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1044,16 +1044,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", - "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "engines": { @@ -1068,15 +1068,64 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/project-service": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", - "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.61.1", - "@typescript-eslint/types": "^8.61.1", + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "engines": { @@ -1091,14 +1140,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", - "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1108,10 +1157,24 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", - "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", "engines": { @@ -1126,15 +1189,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", - "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1150,10 +1213,130 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/types": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", - "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", "engines": { @@ -1165,16 +1348,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", - "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.61.1", - "@typescript-eslint/tsconfig-utils": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1192,6 +1375,24 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1203,26 +1404,39 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -1232,16 +1446,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", - "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1255,14 +1469,134 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", - "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1273,6 +1607,20 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", @@ -1341,9 +1689,9 @@ "license": "MIT" }, "node_modules/bignumber.js": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.4.tgz", - "integrity": "sha512-AJ9dSeaUGj2xu7tEwmdqb51dqdb633xo4njI9K8ZFfcLrNr0XN8/EPkkZUNaF9fkCblGt2zVwZymesUdGynEkQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.5.tgz", + "integrity": "sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -1901,9 +2249,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -2140,9 +2488,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -2163,9 +2511,9 @@ } }, "node_modules/prettier": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", - "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { diff --git a/nix/canton-sources.json b/nix/canton-sources.json index 9c294f153b..9782da4d7a 100644 --- a/nix/canton-sources.json +++ b/nix/canton-sources.json @@ -1,8 +1,8 @@ { - "version": "3.5.8-snapshot.20260707.19067.0.vc42c3eb4", - "oss_sha256": "sha256:0qgzpgwaaiq7jw7674b9lnrpr7sn8qcr24xbs263iwm8w7v2819i", - "canton_base_image_sha256": "sha256:fd7ec98ace2af1927e52c3db899c0568a3bf6a5e450393e649b7837cac985b2d", - "canton_participant_image_sha256": "sha256:14a1642ee63d4739c64dabbe617f1f5e819a5af2a4f860df055313933564bb4d", - "canton_mediator_image_sha256": "sha256:439d4b2ae0d8b6365f03cddc290c8e2d3c37e898b11f7f6bfac45edd546655c2", - "canton_sequencer_image_sha256": "sha256:53c2b32b3709f9baf057b3a87a812afa162ee5bbe8e38ae415052ede397e1c37" + "version": "3.5.15", + "oss_sha256": "sha256:1f366zywrd2rgdzr0xgzxicywrsv1y03zaifrcv63ghpi7cm6551", + "canton_base_image_sha256": "sha256:30fe61fe70056abfd11492992b25709af992c290c3fadce22aaecf6d237fe4e7", + "canton_participant_image_sha256": "sha256:743c9894b8cdad3698fb615789b8a10397b0398a3879291c52d7bcec3ad49640", + "canton_mediator_image_sha256": "sha256:5664f65a1abd9f806bc8ed780e5d289afa764781dac689e9f847895f3f2f8310", + "canton_sequencer_image_sha256": "sha256:0721e9c0544d48beca0f9b00cbc1f01b9d79c54c13df11e09ce24f0e3eee1501" } diff --git a/nix/cometbft-driver.nix b/nix/cometbft-driver.nix index 6ad4dfeec8..19fb5587ec 100644 --- a/nix/cometbft-driver.nix +++ b/nix/cometbft-driver.nix @@ -6,7 +6,7 @@ stdenv.mkDerivation rec { name = "cometbft-driver"; version = sources.version; src = builtins.fetchurl { - url = "https://digitalasset.jfrog.io/artifactory/canton-drivers/com/digitalasset/canton/drivers/canton-drivers/${sources.version}/canton-drivers-${sources.version}.tar.gz"; + url = "https://storage.googleapis.com/da-images-public/canton-drivers/canton-drivers-${version}.tar.gz"; sha256 = sources.sha256; }; dontUnpack = true; diff --git a/nix/flake.lock b/nix/flake.lock index af32a560cc..8b82e3f9cb 100644 --- a/nix/flake.lock +++ b/nix/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1774273680, - "narHash": "sha256-a++tZ1RQsDb1I0NHrFwdGuRlR5TORvCEUksM459wKUA=", + "lastModified": 1782118813, + "narHash": "sha256-BnbXO5s5EhV89lLXMAGCzPdEN5a6vNqvMk71obeTEUw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fdc7b8f7b30fdbedec91b71ed82f36e1637483ed", + "rev": "b3c092d3c36d91e2f61f3dfb39a159f180a56659", "type": "github" }, "original": { diff --git a/nix/flake.nix b/nix/flake.nix index e9c18e68dc..5d3b04fd92 100644 --- a/nix/flake.nix +++ b/nix/flake.nix @@ -9,37 +9,24 @@ outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: - let enterprise_pkgs = import nixpkgs { inherit system; overlays = import ./overlays.nix { use_enterprise = true; }; }; - oss_pkgs = import nixpkgs { inherit system; overlays = import ./overlays.nix { use_enterprise = false; }; }; - enterprise_x86Pkgs = + let pkgs = import nixpkgs { inherit system; overlays = import ./overlays.nix; }; + x86Pkgs = if system == "aarch64-darwin" - then import nixpkgs { system = "x86_64-darwin"; overlays = import ./overlays.nix { use_enterprise = true; }; } - else enterprise_pkgs; - oss_x86Pkgs = - if system == "aarch64-darwin" - then import nixpkgs { system = "x86_64-darwin"; overlays = import ./overlays.nix { use_enterprise = false; }; } - else oss_pkgs; + then import nixpkgs { system = "x86_64-darwin"; overlays = import ./overlays.nix; } + else pkgs; in { packages = { # Forwarded so we can get the path from sbt. - reredirects = oss_pkgs.python3.pkgs.sphinx-reredirects; + reredirects = pkgs.python3.pkgs.sphinx-reredirects; }; - # For now, the default is enterprise. Use `nix develop path:nix#oss` to use the OSS version. devShells.default = import ./shell.nix { - x86Pkgs = enterprise_x86Pkgs; - pkgs = enterprise_pkgs; - variant = "enterprise"; - }; - devShells.oss = import ./shell.nix { - x86Pkgs = oss_x86Pkgs; - pkgs = oss_pkgs; - variant = "oss"; + inherit pkgs x86Pkgs; + variant = "full"; }; devShells.static_tests = import ./shell.nix { - x86Pkgs = oss_x86Pkgs; - pkgs = oss_pkgs; + inherit pkgs x86Pkgs; variant = "static_tests"; }; } diff --git a/nix/overlays.nix b/nix/overlays.nix index 4edd72d58a..23e4803197 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -1,4 +1,3 @@ -{ use_enterprise }: [(self: super: { # We need the old version as our code is not compatible with the new one. # Just overwriting the version does not work as they changed the build code to be @@ -28,6 +27,22 @@ }); }); git-search-replace = super.callPackage ./git-search-replace.nix {}; + # helm-unittest v1.1.0 switched plugin.yaml to use platformCommand with platform-specific + # binary names (e.g. untt-linux-amd64) instead of a generic `command: untt`. + # The nix derivation only installs a single `untt` binary, so we create symlinks + # for all platform variants referenced in plugin.yaml. + kubernetes-helmPlugins = super.kubernetes-helmPlugins // { + helm-unittest = super.kubernetes-helmPlugins.helm-unittest.overrideAttrs (old: { + postInstall = old.postInstall + '' + ln -s $out/helm-unittest/untt $out/helm-unittest/untt-linux-amd64 + ln -s $out/helm-unittest/untt $out/helm-unittest/untt-linux-arm64 + ln -s $out/helm-unittest/untt $out/helm-unittest/untt-linux-ppc64le + ln -s $out/helm-unittest/untt $out/helm-unittest/untt-linux-s390x + ln -s $out/helm-unittest/untt $out/helm-unittest/untt-macos-amd64 + ln -s $out/helm-unittest/untt $out/helm-unittest/untt-macos-arm64 + ''; + }); + }; sphinx-lint = super.callPackage ./sphinx-lint.nix {}; pulumi-bin = super.pulumi-bin.overrideAttrs (_: previousAttrs: let diff --git a/nix/shell.nix b/nix/shell.nix index 7fc98ff6aa..6328d312e5 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,6 +1,5 @@ { pkgs, x86Pkgs, variant }: let - use_enterprise = if variant == "enterprise" then true else false; inherit (pkgs) stdenv fetchzip; sources = builtins.fromJSON (builtins.readFile ./canton-sources.json); cometbftDriverSources = builtins.fromJSON (builtins.readFile ./cometbft-driver-sources.json); @@ -95,6 +94,7 @@ let redocly ripgrep rsync + ruff sbt scala_2_13 selenium-server-standalone @@ -152,9 +152,8 @@ in pkgs.mkShell { DAML_COMPILER_VERSION = "${dpmSdkSources.version}"; COMETBFT_RELEASE_VERSION = "${cometbftDriverSources.version}"; COMETBFT_IMAGE_SHA256 = "${cometbftDriverSources.image_sha256}"; - COMETBFT_DRIVER = if use_enterprise then "${pkgs.cometbft_driver}" else ""; + COMETBFT_DRIVER = "${pkgs.cometbft_driver}"; PULUMI_HOME = "${pkgs.pulumi-bin}"; - IS_ENTERPRISE = if use_enterprise then "true" else "false"; # Avoid sbt-assembly falling over. See https://github.com/sbt/sbt-assembly/issues/496 LC_ALL = if stdenv.isDarwin then "" else "C.UTF-8"; # Avoid "warning: setlocale: LC_ALL: cannot change locale (C.UTF-8)" diff --git a/project/BuildCommon.scala b/project/BuildCommon.scala index 4aff0be830..4e04762d8c 100644 --- a/project/BuildCommon.scala +++ b/project/BuildCommon.scala @@ -206,14 +206,12 @@ object BuildCommon { Global / concurrentRestrictions += Tags.limit(damlTestTag, 4), // copied from the Canton OSS repo Global / excludeLintKeys += Compile / damlBuildOrder, - Global / excludeLintKeys += `canton-blake2b` / autoAPIMappings, Global / excludeLintKeys += `canton-community-app` / autoAPIMappings, Global / excludeLintKeys += `canton-community-app` / Compile / damlDarLfVersion, Global / excludeLintKeys += `canton-community-common` / autoAPIMappings, Global / excludeLintKeys += `canton-community-synchronizer` / autoAPIMappings, Global / excludeLintKeys += `canton-community-participant` / autoAPIMappings, // Global / excludeLintKeys += `demo` / autoAPIMappings, - Global / excludeLintKeys += `canton-slick-fork` / autoAPIMappings, Global / excludeLintKeys += Global / damlCodeGeneration, Global / googleCredentialsDisable := true, Global / resolvers += ("Canton snapshots" at "artifactregistry://europe-maven.pkg.dev/da-images/public-maven-unstable"), @@ -367,6 +365,7 @@ object BuildCommon { // """ // ), scalacOptions ++= Seq( + "-Xsource-features:leading-infix", "-Wconf:src=src_managed/.*:silent", // disable scala 3 migration warnings for canton as we're not gonna fix those "-Wconf:cat=scala3-migration:silent", @@ -377,135 +376,12 @@ object BuildCommon { headerResources / excludeFilter := "*", ) ++ sharedProtocSettings ++ Headers.NoHeaderSettings - // Project for utilities that are also used outside of the Canton repo - lazy val `canton-util-external` = { - import CantonDependencies._ - sbt.Project - .apply("canton-util-external", file("canton/base/util-external")) - .dependsOn( - `canton-pekko-fork`, - `canton-magnolify-addon`, - `canton-wartremover-extension` % "compile->compile;test->test", - // Canton depends on the Daml code via a git submodule and the two - // projects below. We instead depend on the artifacts released - // from the Daml repo listed in libraryDependencies below. - // `daml-copy-common`, - // `daml-copy-testing` % "test->test", - ) - .settings( - sharedCantonSettings, - libraryDependencies ++= Seq( - aws_kms, - aws_sts, - better_files, - gcp_kms, - canton_observability_metrics, - daml_tracing, - daml_executors, - daml_lf_data, - daml_nonempty_cats, - logback_classic, - logback_core, - scala_logging, - scala_collection_contrib, - scalatest % Test, - mockito_scala % Test, - scalatestMockito % Test, - cats, - jul_to_slf4j % Test, - log4j_core, - log4j_api, - monocle_macro, // Include it here, even if unused, so that it can be used everywhere - pureconfig, // Only dependencies may be needed, but it is simplest to include it like this - opentelemetry_api, - opentelemetry_sdk, - opentelemetry_sdk_autoconfigure, - opentelemetry_instrumentation_grpc, - opentelemetry_zipkin, - ), - dependencyOverrides ++= Seq(log4j_core, log4j_api), - // commented out from Canton OS repo as settings don't apply to us (yet) - // JvmRulesPlugin.damlRepoHeaderSettings, - ) - } - - lazy val `canton-base-errors` = { - import CantonDependencies._ - sbt.Project - .apply("canton-base-errors", file("canton/base/errors")) - .dependsOn( - `canton-google-common-protos-scala`, - `canton-wartremover-extension` % "compile->compile;test->test", - ) - .settings( - sharedCantonSettings, - libraryDependencies ++= Seq( - slf4j_api, - grpc_api, - reflections, - scalatest % Test, - scalacheck % Test, - scalatestScalacheck % Test, - ), - ) - } - - lazy val `canton-daml-tls` = { - import CantonDependencies._ - sbt.Project - .apply("canton-daml-tls", file("canton/base/daml-tls")) - .dependsOn( - `canton-wartremover-extension` % "compile->compile;test->test", - `canton-util-observability`, - `canton-util-external`, - ) - .settings( - sharedCantonSettings, - libraryDependencies ++= Seq( - scopt, - grpc_netty_shaded, - apache_commons_io % "test", - ), - ) - } - - lazy val `canton-daml-adjustable-clock` = { - import CantonDependencies._ - sbt.Project - .apply("canton-daml-adjustable-clock", file("canton/base/adjustable-clock")) - .settings( - sharedCantonSettings - ) - } - - lazy val `canton-daml-jwt` = { - import CantonDependencies._ - sbt.Project - .apply("canton-daml-jwt", file("canton/base/daml-jwt")) - .disablePlugins(WartRemover) - .settings( - sharedSettings, - libraryDependencies ++= Seq( - auth0_java, - auth0_jwks, - scalatest % Test, - scalaz_core, - slf4j_api, - circe_core, - circe_generic, - circe_parser, - ), - ) - } - lazy val `canton-util-observability` = { import CantonDependencies._ sbt.Project .apply("canton-util-observability", file("canton/community/util-observability")) .dependsOn( - `canton-base-errors` % "compile->compile;test->test", - `canton-util-external`, - `canton-wartremover-extension` % "compile->compile;test->test", + `canton-wartremover-extension` % "compile->compile;test->test" ) .settings( sharedCantonSettings, @@ -514,8 +390,10 @@ object BuildCommon { libraryDependencies ++= Seq( daml_grpc_utils, better_files, + canton_base_errors, canton_observability_metrics, canton_contextualized_logging, + canton_util_external, daml_lf_data, daml_nonempty_cats, daml_tracing, @@ -545,7 +423,6 @@ object BuildCommon { `canton-community-synchronizer`, `canton-community-participant`, `canton-community-integration-testing` % "test", - `canton-ledger-api-core` % "test->test", ) .enablePlugins(DamlPlugin) .settings( @@ -556,6 +433,7 @@ object BuildCommon { disableTests, removeTestSources, libraryDependencies ++= Seq( + CantonDependencies.canton_ledger_api_core, scala_logging, jul_to_slf4j, janino, // not used at compile time, but required for conditionals in logback configuration @@ -632,14 +510,7 @@ object BuildCommon { .apply("canton-community-base", file("canton/community/base")) .enablePlugins(BuildInfoPlugin) .dependsOn( - `canton-slick-fork`, - `canton-util-external`, - `canton-daml-jwt`, - `canton-daml-tls`, - `canton-ledger-common`, - `canton-community-admin-api`, - `canton-kms-driver-api`, - `canton-scalatest-addon` % "compile->test", + `canton-community-admin-api` // Canton depends on the Daml code via a git submodule and the two // projects below. We instead depend on the artifacts released // from the Daml repo listed in libraryDependencies below. @@ -653,16 +524,23 @@ object BuildCommon { // JvmRulesPlugin.damlRepoHeaderSettings, libraryDependencies ++= Seq( apache_commons_compress, + aws_kms, better_files, bouncycastle_bcpkix_jdk15on, bouncycastle_bcprov_jdk15on, + canton_kms_driver_api, + canton_slick_fork, + canton_util_external, cats, chimney, circe_core, circe_generic, daml_executors, + daml_jwt, + daml_tls, flyway.excludeAll(ExclusionRule("org.apache.logging.log4j")), flyway_postgresql, + gcp_kms, grpc_services, postgres, pprint, @@ -678,6 +556,7 @@ object BuildCommon { CantonDependencies.opentelemetry_instrumentation_hikari, CantonDependencies.canton_java_bindings, ), + libraryDependencies ++= canton_ledger_common_deps, Compile / PB.targets := Seq( scalapb.gen(flatPackage = true) -> (Compile / sourceManaged).value / "protobuf" ), @@ -789,6 +668,8 @@ object BuildCommon { `canton-observability-metrics-testing`, ) .settings( + Compile / unmanagedSources / excludeFilter := + (Compile / unmanagedSources / excludeFilter).value || "UseLedgerApiTestTool.scala", excludeTranscodeConflictingDependencies, sharedCantonSettings, @@ -816,14 +697,9 @@ object BuildCommon { .apply("canton-community-common", file("canton/community/common")) .enablePlugins(DamlPlugin) .dependsOn( - `canton-blake2b`, - `canton-pekko-fork` % "compile->compile;test->test", - `canton-magnolify-addon`, `canton-community-base`, `canton-wartremover-extension` % "compile->compile;test->test", - `canton-util-external` % "compile->compile;test->test", `canton-community-testing` % "test", - `canton-ledger-common` % "compile->compile;test->test", ) .settings( removeTestSources, @@ -843,6 +719,9 @@ object BuildCommon { daml_lf_engine, daml_lf_transaction, // needed for importing java classes daml_nonempty_cats, + canton_blake2b, + canton_util_external, + canton_magnolify_addon, logback_classic, logback_core, scala_logging, @@ -944,11 +823,13 @@ object BuildCommon { import CantonDependencies._ sbt.Project .apply("canton-community-admin-api", file("canton/community/admin-api")) - .dependsOn(`canton-util-external`) .settings( sharedCantonSettings, libraryDependencies ++= Seq( - scalapb_runtime // not sufficient to include only through the `common` dependency - race conditions ensue + canton_util_external, + grpc_api, + scalapb_runtime, // not sufficient to include only through the `common` dependency - race conditions ensue + scalapb_runtime_grpc, ), Compile / PB.targets := Seq( scalapb.gen(flatPackage = true) -> (Compile / sourceManaged).value / "protobuf" @@ -971,9 +852,9 @@ object BuildCommon { .apply("canton-community-participant", file("canton/community/participant")) .dependsOn( `canton-community-common` % "compile->compile;test->test", - `canton-ledger-api-core` % "compile->compile;test->test", `canton-ledger-json-api`, `canton-community-admin-api`, + `canton-traffic-enforcement-component`, ) .enablePlugins(DamlPlugin) .settings( @@ -981,6 +862,7 @@ object BuildCommon { sharedCantonSettings, excludeTranscodeConflictingDependencies, libraryDependencies ++= Seq( + canton_ledger_api_core, scala_logging, scalatest % Test, scalatestScalacheck % Test, @@ -1037,43 +919,10 @@ object BuildCommon { ) } - lazy val `canton-blake2b` = { - import CantonDependencies._ - sbt.Project - .apply("canton-blake2b", file("canton/community/lib/Blake2b")) - .disablePlugins(ScalafmtPlugin, WartRemover) - .settings( - sharedCantonSettings, - removeTestSources, - sharedSettings, - libraryDependencies ++= Seq( - bouncycastle_bcprov_jdk15on, - bouncycastle_bcpkix_jdk15on, - ), - ) - } - - lazy val `canton-slick-fork` = { - import CantonDependencies._ - sbt.Project - .apply("canton-slick-fork", file("canton/community/lib/slick")) - .disablePlugins(ScalafmtPlugin, WartRemover) - .settings( - sharedCantonSettings, - removeTestSources, - sharedSettings, - libraryDependencies ++= Seq( - scala_reflect, - slick, - ), - ) - } - lazy val `canton-wartremover-extension` = { import CantonDependencies._ sbt.Project .apply("canton-wartremover-extension", file("canton/community/lib/wartremover")) - .dependsOn(`canton-wartremover-annotations`, `canton-slick-fork`) .settings( Test / scalacOptions ++= Seq( "-Wconf:msg=synchronized not selected from this instance:silent" @@ -1081,6 +930,8 @@ object BuildCommon { disableTests, sharedSettings, libraryDependencies ++= Seq( + canton_slick_fork, + canton_wartremover_annotations, cats, grpc_stub, mockito_scala % Test, @@ -1097,233 +948,35 @@ object BuildCommon { ) } - lazy val `canton-wartremover-annotations` = - sbt.Project - .apply("canton-wartremover-annotations", file("canton/community/lib/wartremover-annotations")) - .settings(sharedSettings) - - // https://github.com/DACH-NY/canton/issues/10617: remove when no longer needed - lazy val `canton-pekko-fork` = { - import CantonDependencies._ - sbt.Project - .apply("canton-pekko-fork", file("canton/community/lib/pekko")) - .disablePlugins(ScalafixPlugin, ScalafmtPlugin, WartRemover) - .settings( - sharedCantonSettings, - sharedSettings, - libraryDependencies ++= Seq( - pekko_stream, - pekko_stream_testkit % Test, - pekko_slf4j, - scalatest % Test, - ), - // commented out from Canton OS repo as settings don't apply to us (yet) - // // Exclude to apply our license header to any Scala files - // headerSources / excludeFilter := "*.scala", - // coverageEnabled := false, - ) - } - - lazy val `canton-magnolify-addon` = { - import CantonDependencies._ - sbt.Project - .apply("canton-magnloify-addon", file("canton/community/lib/magnolify")) - .settings( - sharedSettings, - libraryDependencies ++= Seq( - cats, - daml_nonempty, - magnolia, - magnolify_scalacheck, - magnolify_shared % Test, - scala_reflect, - scalacheck, - scalatest % Test, - ), - ) - - } - - lazy val `canton-scalatest-addon` = { - import CantonDependencies._ - sbt.Project - .apply("canton-scalatest-addon", file("canton/community/lib/scalatest")) - .settings( - sharedSettings, - libraryDependencies += scalatest, - // Exclude to apply our license header to any Scala files - headerSources / excludeFilter := "*.scala", - ) - } - - lazy val `canton-ledger-common` = { - import CantonDependencies._ - sbt.Project - .apply("canton-ledger-common", file("canton/community/ledger/ledger-common")) - .disablePlugins(WartRemover, ScalafmtPlugin) - .dependsOn( - `canton-util-external`, - `canton-daml-jwt`, - `canton-util-observability`, - ) - .settings( - removeTestSources, - sharedCantonSettings, - disableTests, - sharedSettings, - scalacOptions += "-Wconf:src=src_managed/.*:silent", - Compile / PB.targets := Seq( - PB.gens.java -> (Compile / sourceManaged).value / "protobuf", - scalapb.gen(flatPackage = false) -> (Compile / sourceManaged).value / "protobuf", - ), - // commented out from Canton OS repo as settings don't apply to us (yet) - // addProtobufFilesToHeaderCheck(Compile), - libraryDependencies ++= Seq( - canton_contextualized_logging, - daml_lf_engine, - daml_lf_archive_reader, - CantonDependencies.canton_java_bindings, - CantonDependencies.canton_ledger_api_scala, - daml_tracing, - apache_commons_codec, - apache_commons_io, - daml_ledger_resources, - daml_timer_utils, - daml_rs_grpc_pekko, - opentelemetry_api, - pekko_stream, - slf4j_api, - grpc_api, - reflections, - grpc_netty_shaded, - caffeine, - scalapb_runtime, - scalapb_runtime_grpc, - scopt, - awaitility % Test, - logback_classic % Test, - scalatest % Test, - mockito_scala % Test, - scalatestMockito % Test, - pekko_stream_testkit % Test, - scalacheck % Test, - opentelemetry_sdk_testing % Test, - scalatestScalacheck % Test, - daml_lf_data, - daml_lf_transaction, - daml_ports % Test, - ), - Test / fork := true, - Test / testForkedParallel := true, - // commented out from Canton OS repo as settings don't apply to us (yet) - // coverageEnabled := false, - // JvmRulesPlugin.damlRepoHeaderSettings, - ) - } - - lazy val `canton-ledger-api-core` = { - import CantonDependencies._ - sbt.Project - .apply("canton-ledger-api-core", file("canton/community/ledger/ledger-api-core")) - .dependsOn( - `canton-base-errors` % "test->test", - `canton-ledger-common` % "compile->compile;test->test", - `canton-community-common` % "compile->compile;test->test", - `canton-daml-adjustable-clock` % "test->test", - `canton-daml-tls` % "test->test", - ) - .disablePlugins( - WartRemover, - ScalafmtPlugin, - ) // to accommodate different daml repo coding style - .settings( - removeTestSources, - sharedCantonSettings, - sharedSettings, - scalacOptions += "-Wconf:src=src_managed/.*:silent", - Compile / PB.targets := Seq( - scalapb.gen(flatPackage = false) -> (Compile / sourceManaged).value / "protobuf" - ), - libraryDependencies ++= Seq( - CantonDependencies.canton_ledger_api_scala, - auth0_java, - auth0_jwks, - circe_core, - daml_ports, - hikaricp, - guava, - bouncycastle_bcprov_jdk15on % Test, - bouncycastle_bcpkix_jdk15on % Test, - scalaz_scalacheck % Test, - grpc_netty_shaded, - grpc_services, - grpc_protobuf, - postgres, - h2, - flyway, - oracle, - anorm, - scalapb_runtime_grpc, - scalapb_json4s % Test, - scalapb_runtime, - scalaz_scalacheck % Test, - testcontainers % Test, - testcontainers_postgresql % Test, - ), - Test / parallelExecution := true, - Test / fork := false, - ) - } - - // this project builds scala protobuf versions that include - // java conversions of a few google standard items - // the google protobuf files are extracted from the provided jar files - lazy val `canton-google-common-protos-scala` = { + private[this] lazy val canton_ledger_common_deps = { import CantonDependencies._ - sbt.Project - .apply( - "canton-google-common-protos-scala", - file("canton/community/lib/google-common-protos-scala"), - ) - .disablePlugins( - ScalafixPlugin, - ScalafmtPlugin, - WartRemover, - ) - .settings( - sharedCantonSettings, - scalacOptions --= removeCompileFlagsForDaml, - sharedSettings, - // we restrict the compilation to a few files that we actually need, skipping the large majority ... - excludeFilter := HiddenFileFilter || "scalapb.proto", - PB.generate / includeFilter := "status.proto" || "code.proto" || "error_details.proto" || "health.proto", - dependencyOverrides ++= Seq(), - // compile proto files that we've extracted here - Compile / PB.protoSources += (target.value / "protobuf_external"), - Compile / PB.targets := Seq( - // with java conversions but no java classes! - scalapb.gen( - javaConversions = true, - flatPackage = false, // consistent with upstream daml - ) -> (Compile / sourceManaged).value - ), - libraryDependencies ++= Seq( - scalapb_runtime, - scalapb_runtime_grpc, - // the grpc services is necessary so we can build the - // scala version of the health services, without - // building the java protoc (to avoid duplicate symbols - // during assembly) - grpc_services, - // extract the protobuf to target/protobuf_external - // however, we'll only be including the ones in the includeFilter - grpc_services % "protobuf", - google_common_protos % "protobuf", - google_common_protos, - google_protobuf_java, - google_protobuf_java_util, - ), - ) + Seq( + canton_contextualized_logging, + canton_util_external, + daml_lf_engine, + daml_lf_archive_reader, + CantonDependencies.canton_java_bindings, + CantonDependencies.canton_ledger_api_scala, + daml_jwt, + daml_tracing, + apache_commons_codec, + apache_commons_io, + daml_ledger_resources, + daml_timer_utils, + daml_rs_grpc_pekko, + opentelemetry_api, + pekko_stream, + slf4j_api, + grpc_api, + reflections, + grpc_netty_shaded, + caffeine, + scalapb_runtime, + scalapb_runtime_grpc, + scopt, + daml_lf_data, + daml_lf_transaction, + ) } // this project exists solely for the purpose of extracting value.proto @@ -1353,8 +1006,7 @@ object BuildCommon { sbt.Project .apply("canton-ledger-json-api", file("canton/community/ledger/ledger-json-api")) .dependsOn( - `canton-ledger-api-core`, - `canton-ledger-common` % "test->test", + `canton-util-observability`, `canton-community-testing` % Test, ) .disablePlugins( @@ -1376,9 +1028,11 @@ object BuildCommon { .map(cat => s"cat=$cat:silent") .mkString(",", ",", ""), libraryDependencies ++= Seq( + CantonDependencies.canton_ledger_api_core, CantonDependencies.canton_transcode_json, CantonDependencies.canton_transcode_proto_scala, CantonDependencies.canton_transcode_daml_lf, + circe_generic_extras, pekko_http, pekko_http_core, daml_lf_api_type_signature, @@ -1416,17 +1070,64 @@ object BuildCommon { ) } + lazy val `daml-lf-transaction-test-lib` = project + .in(file("canton/community/daml-lf/transaction-test-lib")) + .disablePlugins( + WartRemover + ) + .settings( + sharedCantonSettings, + Compile / unmanagedSources / includeFilter := + "*ValueGenerators.scala" || "TransactionBuilder.scala" || "NodeIdTransactionBuilder.scala" || "TestIdFactory.scala", + libraryDependencies ++= { + import CantonDependencies._ + Seq( + daml_lf_api_type_signature, + daml_lf_data, + daml_lf_language, + daml_lf_transaction, + scalacheck, + scala_logging, + scalatestScalacheck, + scalatest, + scalaz_core, + scalaz_scalacheck, + shapeless, + ) + }, + ) + .dependsOn( + `daml-lf-data-scalacheck` + ) + + lazy val `daml-lf-data-scalacheck` = project + .in(file("canton/community/daml-lf/data-scalacheck")) + .disablePlugins(WartRemover) + .settings( + sharedCantonSettings, + libraryDependencies ++= { + import CantonDependencies._ + Seq( + daml_lf_data, + scalacheck, + ) + }, + Test / scalacOptions ++= Seq( + "-Wconf:msg=match may not be exhaustive:s" + ), + ) + lazy val `canton-sequencer-driver-api` = { import CantonDependencies._ sbt.Project .apply("canton-sequencer-driver-api", file("canton/community/sequencer-driver")) .dependsOn( - `canton-util-external`, - `canton-util-observability`, + `canton-util-observability` ) .settings( sharedCantonSettings, libraryDependencies ++= Seq( + canton_util_external, logback_classic, logback_core, scala_logging, @@ -1448,6 +1149,52 @@ object BuildCommon { ) } + lazy val `canton-traffic-enforcement-component` = + sbt + .Project( + "canton-traffic-enforcement-component", + file("canton/community/traffic-enforcement/component"), + ) + .dependsOn( + `canton-util-observability`, + `canton-community-testing` % Test, + `canton-community-common` % "compile->compile;test->test", + ) + .enablePlugins(DamlPlugin) + .settings( + sharedCantonSettings, + Compile / PB.targets := Seq( + scalapb.gen(flatPackage = false) -> (Compile / sourceManaged).value / "protobuf" + ), + libraryDependencies ++= { + import CantonDependencies._ + Seq( + apache_commons_io, + canton_traffic_enforcement_api, + canton_ledger_api_core, + canton_ledger_api_scala, + pekko_actor_typed, + pekko_stream, + pekko_projection_core, + pekko_projection_jdbc, + pekko_projection_slick, + pekko_persistence, + pekko_persistence_query, + // Scope not only to test on purpose as we use the in-memory implementation + // in prod code as well + pekko_projection_testkit, + pekko_actor_testkit_typed, + pekko_slf4j % "compile->compile;test->test", + pureconfig, + pureconfig_generic, + scalapb_runtime, + scalapb_runtime_grpc, + logback_classic % Runtime, + scalatest % Test, + ) + }, + ) + lazy val `canton-community-reference-driver` = { import CantonDependencies._ sbt.Project @@ -1456,35 +1203,20 @@ object BuildCommon { file("canton/community/reference-sequencer-driver/"), ) .dependsOn( - `canton-util-external`, `canton-community-common` % "compile->compile;test->test", `canton-sequencer-driver-api` % "compile->compile;test->test", `canton-community-testing` % Test, ) - .dependsOn(`canton-util-external`) .settings( sharedCantonSettings, dependencyOverrides ++= Seq(log4j_core, log4j_api), + libraryDependencies ++= Seq(canton_util_external), Compile / PB.targets := Seq( scalapb.gen(flatPackage = true) -> (Compile / sourceManaged).value / "protobuf" ), ) } - lazy val `canton-kms-driver-api` = project - .in(file("canton/community/kms-driver-api")) - .settings( - sharedCantonSettings, - libraryDependencies ++= { - import CantonDependencies.* - Seq( - pureconfig, - slf4j_api, - opentelemetry_api, - ) - }, - ) - import defs._ /** Typescript code generation from daml models. diff --git a/project/CantonDependencies.scala b/project/CantonDependencies.scala index 4de226f8ce..93e5c65bd7 100644 --- a/project/CantonDependencies.scala +++ b/project/CantonDependencies.scala @@ -7,7 +7,7 @@ import sbt.* object CantonDependencies { // Slightly changed compared to Canton OSS repo to avoid the need for a meta sbt project val version: String = "3.5.0-snapshot.20260401.14638.0.v9a1531c5" - val canton_library_version = "3.5.3" + val canton_library_version = "3.5.7-snapshot.20260630.19042.0.vc85c6a30" val daml_language_versions = Seq("2.1") val daml_libraries_version = version // Defined in `../nix/dpm-sdk-sources.json`, as the compiler version is also used by @@ -26,6 +26,8 @@ object CantonDependencies { lazy val anorm = "org.playframework.anorm" %% "anorm" % "2.7.0" lazy val apispec_version = "0.11.7" lazy val pekko_version = "1.2.1" + lazy val pekko_projection_version = "1.1.0" + lazy val pekko_persistence_version = "1.2.1" lazy val pekko_http_version = "1.2.0" lazy val auth0_java = "com.auth0" % "java-jwt" % "4.2.1" lazy val auth0_jwks = "com.auth0" % "jwks-rsa" % "0.21.2" @@ -52,6 +54,8 @@ object CantonDependencies { lazy val reflections = "org.reflections" % "reflections" % "0.10.2" lazy val pureconfig = "com.github.pureconfig" %% "pureconfig" % pureconfig_version lazy val pureconfig_cats = "com.github.pureconfig" %% "pureconfig-cats" % pureconfig_version + lazy val pureconfig_generic = + "com.github.pureconfig" %% "pureconfig-generic" % pureconfig_version // TODO(SC) exclude ("com.chuusai", s"shapeless_$scala_version_short") lazy val scala_collection_contrib = "org.scala-lang.modules" %% "scala-collection-contrib" % "0.2.2" @@ -93,12 +97,23 @@ object CantonDependencies { "com.daml" %% "daml-grpc-utils" % canton_library_version lazy val canton_java_bindings = "com.daml" % "bindings-java" % canton_library_version + lazy val canton_kms_driver_api = "com.daml" %% "kms-driver-api" % canton_library_version + lazy val canton_ledger_api_core = "com.daml" %% "ledger-api-core" % canton_library_version lazy val canton_ledger_api_scala = "com.daml" %% "ledger-api-scala" % canton_library_version + lazy val canton_base_errors = "com.daml" %% "base-errors" % canton_library_version lazy val canton_observability_metrics = "com.daml" %% "observability-metrics" % canton_library_version lazy val canton_contextualized_logging = "com.daml" %% "contextualized-logging" % canton_library_version - + lazy val canton_slick_fork = "com.daml" %% "slick-fork" % canton_library_version + lazy val canton_traffic_enforcement_api = + "com.daml" %% "traffic-enforcement-api" % canton_library_version + lazy val canton_util_external = "com.daml" %% "util-external" % canton_library_version + lazy val canton_wartremover_annotations = + "com.daml" %% "wartremover-annotations" % canton_library_version + + lazy val canton_blake2b = "com.daml" %% "blake2b" % canton_library_version + lazy val canton_magnolify_addon = "com.daml" %% "magnolify-addon" % canton_library_version lazy val canton_transcode_json = "com.daml" % "transcode-codec-json_3" % canton_library_version lazy val canton_transcode_proto_scala = "com.daml" % "transcode-codec-proto-scala_3" % canton_library_version @@ -121,6 +136,8 @@ object CantonDependencies { lazy val daml_nonempty_cats = "com.daml" %% "nonempty-cats" % canton_library_version lazy val daml_tracing = "com.daml" %% "observability-tracing" % canton_library_version lazy val daml_executors = "com.daml" %% "executors" % canton_library_version + lazy val daml_jwt = "com.daml" %% "daml-jwt" % canton_library_version + lazy val daml_tls = "com.daml" %% "daml-tls" % canton_library_version lazy val daml_ports = "com.daml" %% "ports" % canton_library_version lazy val daml_ledger_resources = "com.daml" %% "ledger-resources" % canton_library_version lazy val daml_ledger_api_value_scalapb = @@ -148,6 +165,8 @@ object CantonDependencies { lazy val scopt = "com.github.scopt" %% "scopt" % "4.0.0" lazy val pekko_actor_typed = "org.apache.pekko" %% "pekko-actor-typed" % pekko_version + lazy val pekko_actor_testkit_typed = + "org.apache.pekko" %% "pekko-actor-testkit-typed" % pekko_version lazy val pekko_stream = "org.apache.pekko" %% "pekko-stream" % pekko_version lazy val pekko_stream_testkit = "org.apache.pekko" %% "pekko-stream-testkit" % pekko_version lazy val pekko_slf4j = "org.apache.pekko" %% "pekko-slf4j" % pekko_version @@ -155,9 +174,24 @@ object CantonDependencies { lazy val pekko_http_core = "org.apache.pekko" %% "pekko-http-core" % pekko_http_version lazy val pekko_http_testkit = "org.apache.pekko" %% "pekko-http-testkit" % pekko_http_version + lazy val pekko_projection_core = + "org.apache.pekko" %% "pekko-projection-core" % pekko_projection_version + lazy val pekko_projection_jdbc = + "org.apache.pekko" %% "pekko-projection-jdbc" % pekko_projection_version + lazy val pekko_projection_slick = + "org.apache.pekko" %% "pekko-projection-slick" % pekko_projection_version + lazy val pekko_projection_testkit = + "org.apache.pekko" %% "pekko-projection-testkit" % pekko_projection_version + lazy val pekko_persistence = + "org.apache.pekko" %% "pekko-persistence" % pekko_persistence_version + lazy val pekko_persistence_query = + "org.apache.pekko" %% "pekko-persistence-query" % pekko_persistence_version + lazy val scala_logging = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" lazy val scalacheck = "org.scalacheck" %% "scalacheck" % scalacheck_version lazy val scalatest = "org.scalatest" %% "scalatest" % scalatest_version + lazy val scalatest_shouldmatchers = + "org.scalatest" %% "scalatest-shouldmatchers" % scalatest_version lazy val scalaz_core = "org.scalaz" %% "scalaz-core" % scalaz_version lazy val scalatestScalacheck = "org.scalatestplus" %% "scalacheck-1-18" % (scalatest_version + ".0") @@ -298,7 +332,7 @@ object CantonDependencies { "com.google.protobuf" % "protobuf-java-util" % protobuf_version // AWS SDK for Java API to encrypt/decrypt keys using AWS KMS - lazy val aws_version = "2.29.5" + lazy val aws_version = "2.49.4" lazy val aws_kms = "software.amazon.awssdk" % "kms" % aws_version lazy val aws_sts = "software.amazon.awssdk" % "sts" % aws_version diff --git a/project/ProtocNixPlugin.scala b/project/ProtocNixPlugin.scala new file mode 100644 index 0000000000..ad6d5e5542 --- /dev/null +++ b/project/ProtocNixPlugin.scala @@ -0,0 +1,71 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import sbt.* +import sbt.Keys.* + +/** Points `protocGenerate` at a stable launcher for every native protoc plugin. + * + * When `NIX_CC` is set (it always is, since sbt runs inside the nix shell) sbt-protoc wraps each + * native plugin binary in a fresh `/tmp/nix` script per task run and passes that path as + * `--plugin=`. The path is part of the task's cache key, so codegen and the doc rendering rerun on + * every build even when no proto changed, and the scripts pile up in `/tmp` for the life of the + * sbt server. Writing the launcher ourselves under a digest-named path keeps the key stable, and + * the `.sh` suffix stops sbt-protoc from wrapping it again. + */ +object ProtocNixPlugin extends AutoPlugin { + override def trigger: PluginTrigger = allRequirements + override def requires: Plugins = sbtprotoc.ProtocPlugin + + import sbtprotoc.ProtocPlugin.ProtobufConfig + import sbtprotoc.ProtocPlugin.autoImport.PB + + // Per user, so a shared /tmp does not hand one user a directory the next cannot write to. + private val launcherDir = + IO.temporaryDirectory / s"canton-protoc-plugins-${sys.props("user.name")}" + + /** The artifact of a plugin binary that needs a launcher, if this entry is one. */ + private def nativePluginArtifact(entry: Attributed[File]): Option[Artifact] = + entry + .get(artifact.key) + .filter(a => a.`type` == PB.ProtocPlugin && !entry.data.getName.endsWith(".sh")) + + private val launcherLock = new Object + + private def launcherFor(name: String, binary: File, linker: String): File = { + val script = + s"""#!/bin/sh + |exec $linker ${binary.getAbsolutePath} "$$@" + |""".stripMargin + // Digest in the name, so a new linker or binary is a new path and thus a new cache key. + val launcher = launcherDir / s"$name-${Hash.toHex(Hash(script)).take(12)}.sh" + // Two projects can share a plugin, so they race on one path; IO.write truncates. + launcherLock.synchronized { + // Recreate if a tmp sweeper removed it. The path stays the same, so the cache still holds. + if (!launcher.isFile) { + IO.createDirectory(launcherDir) + IO.write(launcher, script) + } + launcher.setExecutable(true) + binary.setExecutable(true) + } + launcher + } + + override def projectSettings: Seq[Def.Setting[_]] = Seq( + // Scoped exactly as sbt-protoc reads it, so PB.unpackDependencies keeps seeing the real files. + ProtobufConfig / PB.generate / managedClasspath := { + val classpath = (ProtobufConfig / managedClasspath).value + protocbridge.ProtocRunner.maybeNixDynamicLinker() match { + case None => classpath + case Some(linker) => + classpath.map { entry => + nativePluginArtifact(entry).fold(entry) { pluginArtifact => + // Keep the metadata: sbt-protoc names the --plugin= flag after the artifact. + Attributed(launcherFor(pluginArtifact.name, entry.data, linker))(entry.metadata) + } + } + } + } + ) +} diff --git a/project/build.properties b/project/build.properties index cc68b53f1a..e544c4d115 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.11 +sbt.version=1.12.14 diff --git a/project/ignore-patterns/canton-standalone-cancel-global-synchronizer-upgrade.ignore.txt b/project/ignore-patterns/canton-standalone-cancel-global-synchronizer-upgrade.ignore.txt new file mode 100644 index 0000000000..644cf52929 --- /dev/null +++ b/project/ignore-patterns/canton-standalone-cancel-global-synchronizer-upgrade.ignore.txt @@ -0,0 +1,4 @@ +mediator=sv.*StandaloneMediator.*Cannot submit before or at the lower bound for sequencing time +Cannot submit before or at the lower bound for sequencing time +The operation 'request current time' was not successful.*Cannot submit before or at the lower bound for sequencing time +Now retrying operation 'request current time' diff --git a/project/ignore-patterns/canton-standalone-mediator-offboarding.ignore.txt b/project/ignore-patterns/canton-standalone-mediator-offboarding.ignore.txt index b3fbf3e067..e587679936 100644 --- a/project/ignore-patterns/canton-standalone-mediator-offboarding.ignore.txt +++ b/project/ignore-patterns/canton-standalone-mediator-offboarding.ignore.txt @@ -16,3 +16,5 @@ Failed to send result to sequencer for request.*Unregistered recipients.*unregis Member MED::sv4SvOffboarding.* access is disabled Could not send a time-advancing message + +Request failed for server.*Sequencer.* Is the server running? diff --git a/project/ignore-patterns/canton-standalone-mediator-offboarding_before_shutdown.ignore.txt b/project/ignore-patterns/canton-standalone-mediator-offboarding_before_shutdown.ignore.txt deleted file mode 100644 index 6295947706..0000000000 --- a/project/ignore-patterns/canton-standalone-mediator-offboarding_before_shutdown.ignore.txt +++ /dev/null @@ -1 +0,0 @@ -Request failed for server.*Sequencer.* Is the server running? diff --git a/project/ignore-patterns/canton-standalone-sv4-reonboarding.ignore.txt b/project/ignore-patterns/canton-standalone-sv4-reonboarding.ignore.txt index 7fb638c674..cb8a0afddc 100644 --- a/project/ignore-patterns/canton-standalone-sv4-reonboarding.ignore.txt +++ b/project/ignore-patterns/canton-standalone-sv4-reonboarding.ignore.txt @@ -26,3 +26,5 @@ Member MED::sv4SvReonboarding.* access is disabled Request failed for server-DefaultSequencer-0.*sv4 SEQUENCER_SUBMISSION_REQUEST_MALFORMED.*MED::sv4SvReonboarding.* is not part of the mediator group + +Request failed for server.*Sequencer.* Is the server running? diff --git a/project/ignore-patterns/canton-standalone-sv4-reonboarding_before_shutdown.ignore.txt b/project/ignore-patterns/canton-standalone-sv4-reonboarding_before_shutdown.ignore.txt deleted file mode 100644 index 6295947706..0000000000 --- a/project/ignore-patterns/canton-standalone-sv4-reonboarding_before_shutdown.ignore.txt +++ /dev/null @@ -1 +0,0 @@ -Request failed for server.*Sequencer.* Is the server running? diff --git a/project/ignore-patterns/canton_log.ignore.txt b/project/ignore-patterns/canton_log.ignore.txt index 706926c960..a2eb79ef4f 100644 --- a/project/ignore-patterns/canton_log.ignore.txt +++ b/project/ignore-patterns/canton_log.ignore.txt @@ -94,6 +94,12 @@ ACS_COMMITMENT_MISMATCH.*aliceValidatorLocalNewForValidatorReonboardingIT # In principle the confirmations from three of the mediators and counter-participants should be sufficient, but who knows what we do in all tests; ignoring unless it causes problems Response message for request.*timed out.*c.d.c.p.p.TransactionProcessor:participant= +# Transient WARN triggered by a shutdown of the sync connection while the participant is sending confirmation responses. +Failed to send responses: RequestFailed\(No connection available\).*c.d.c.p.p.TransactionProcessor:participant= + +# Transient WARN triggered by a shutdown of the sync connection while the participant is submitting a transaction; the submitter retries. +Failed to submit transaction due to Error\(RequestFailed\(No connection available\)\).*c.d.c.p.p.TransactionProcessor:participant= + # TODO(#936): remove these ignores if possible The operation 'insert block' has failed with an exception Now retrying operation 'insert block' @@ -148,9 +154,6 @@ Sequencing result message timed out.*mediator= # TODO (DACH-NY/canton-network-internal#966) - remove if not necessary anymore ACS_COMMITMENT_DEGRADATION -# TODO(#2706) Investigate and remove once fixed -Waiting for allocation of.*on synchronizer splitwell.*timed out - #Todo(DACH-NY/cn-test-failures/7808) The synchronizer Synchronizer 'global' failed the following topology transactions @@ -177,7 +180,7 @@ Missing successor information for the following sequencers.*sv4 Skipping insertion of pending onboarding flag clearance operation as no party ID was provided # LSU: submissions on the old physical synchronizer after the upgrade time has passed are rejected -Request failed for server-DefaultSequencer-0.\\n GrpcRequestRefusedByServer: FAILED_PRECONDITION/SEQUENCER_SUBMISSION_AFTER_UPGRADE_TIME +GrpcRequestRefusedByServer: FAILED_PRECONDITION/SEQUENCER_SUBMISSION_AFTER_UPGRADE_TIME # LSU SV4 migrates late SEQ::sv4.*Sequencer can't take requests because it is behind on processing events Sequencer can't take requests because it is behind on processing events.*sequencer=>sv4StandaloneSequencer @@ -198,7 +201,13 @@ Mempool received client request but this node is currently blacklisted, rejectin # This should go away with latest Canton bump Flyway upgrade recommended: PostgreSQL 18.4 is newer than this version of Flyway and support has not been tested. The latest supported version of PostgreSQL is 17 +# TODO(DACH-NY/cn-test-failures#9055) can be removed when it's no longer a warn. +LOCAL_VERDICT_FAILED_MODEL_CONFORMANCE_CHECK.*Rejected transaction due to a failed model conformance check: UnvettedPackages.* + # Make sure to have a trailing newline # TODO(DACH-NY/cn-test-failures#9136) remove once Canton fixes this race condition from not storing packages in dependency order INTERNAL/Missing package-id.*in package metadata view + +# We can get temporarily blacklisted during onboarding triggering a retry for this. +Now retrying operation 'request current time' diff --git a/project/ignore-patterns/canton_log_bft.ignore.txt b/project/ignore-patterns/canton_log_bft.ignore.txt index d7d1bd3c17..591650e44a 100644 --- a/project/ignore-patterns/canton_log_bft.ignore.txt +++ b/project/ignore-patterns/canton_log_bft.ignore.txt @@ -1,2 +1,4 @@ # Expected warning until https://github.com/DACH-NY/canton/pull/31166 lands discarding expired batches +# Silence warning for stuck ordering layer in tests. Likely comes from onboarding new SVs while we don't have enough SVs to reach thresholds without the new one. +Waiting for new topology after epoch completion for.*seconds without receiving it from the output module diff --git a/project/ignore-patterns/canton_log_simtime_extra.ignore.txt b/project/ignore-patterns/canton_log_simtime_extra.ignore.txt index dc91549eca..d87108c8f1 100644 --- a/project/ignore-patterns/canton_log_simtime_extra.ignore.txt +++ b/project/ignore-patterns/canton_log_simtime_extra.ignore.txt @@ -23,7 +23,7 @@ Failed to update unsequenced submission.*NOT_SEQUENCED_TIMEOUT SEQUENCER_SUBSCRIPTION_LOST.*: Lost subscription to domain .* Will try to recover automatically. # It seems this happens when a transaction is in-flight and we advance time. We should just retry. -INTERPRETATION_TIME_EXCEEDED.*: Interpretation time exceeds limit of Ledger Effective Time +INTERPRETATION_TIME_EXCEEDED.*: (Interpretation )?[Tt]ime exceeds limit of Ledger Effective Time Could not send a time-advancing message diff --git a/project/ignore-patterns/canton_network_test_log.ignore.txt b/project/ignore-patterns/canton_network_test_log.ignore.txt index dbd7354be7..63edef0dbe 100644 --- a/project/ignore-patterns/canton_network_test_log.ignore.txt +++ b/project/ignore-patterns/canton_network_test_log.ignore.txt @@ -61,9 +61,6 @@ Trying to re-register template.* # TODO(#975): investigate and remove once fixed failed with UNKNOWN/channel closed.*SvAppLedgerApiConnectivityIntegrationTest -# This might be logged between the test finishing and shutdown of the nodes (see #8991) -Noticed an DsoRules epoch change.*DecentralizedSynchronizerMigrationIntegrationTest - # TODO (#825): this only applies to simtime. # In simtime tests where the rounds are advanced too quickly, this trigger might not have enough time to receive the coupon. # Nevertheless, it is still useful to have the warning in production environments, where this should never happen. @@ -107,10 +104,6 @@ Splice unsafe shutdown future # SV UI polls cometbft status endpoint even when there's no cometBFT (we have this in the full network docker compose test) .*CometBFT is not configured for this app.* -# Expected during SV onboarding as we start scan and the sv app concurrently -# TODO(#893) maybe remove this again -Failed to read bft sequencers list from scan - Failed to connect to scan of Digital-Asset-Eng-3.*AppUpgradeIntegrationTest # Ignore scan issues in disaster recovery tests as we don't start scan there @@ -133,15 +126,15 @@ Circuit breaker .* tripped after .* failures.*(Command|Splice)CircuitBreakerTest # This is more likely in test code where we run multiple apps against the same database, all trying to create the same index. Index .* should be created and is invalid, dropping it -# TODO(#4738) - remove this -processTaskWithRetry failed with an unknown exception, not retrying.*SqlIndexInitializationTrigger.*PSQLException: ERROR: tuple concurrently updated -Skipping processing of.*due to unexpected failure.*SqlIndexInitializationTrigger.*PSQLException: ERROR: tuple concurrently updated - # Injected errors in s3Mock Simulated S3 error # Shutdown issues Previous channel ManagedChannelImpl.* was garbage collected without being shut down! +# This is logged by pekko's RestartSource. Ideally we'd prevent it from logging that when shutting down, +# but it doesn't offer a clean way of doing that. Lowering log level also doesn't seem sensible, +# as we don't want to miss non-shutdown warnings in production. +Restarting stream due to failure.*Channel shutdown invoked # TODO(#564) - check what's happening here during shutdown Timeout 10 seconds expired, but readers are still active. Shutting down forcibly.*LsuIntegrationTest @@ -169,3 +162,10 @@ Ryuk has been disabled # Calls to this have retries; if the retries were not enough, we'll get a louder error. api/sv/v0/onboard/validator \(POST\) resulted in a timeout + +# In BaseStorePerformanceTest, we run migrations in an unforked sbt JVM, so Flyway +# finds the sbt test jar (apps-app_*-tests.jar). But, Flyway cannot open it, +# so it skips it with this WARN. It contains no migrations, so this is harmless and unrelated to DB migrations. +# The same warning is emitted by both the FlywayExecutor and the ClassPathScanner loggers. +# a single pattern matching the message (the unloadable sbt tests jar) covers both. +Skipping unloadable jar file: file:.*-tests\.jar diff --git a/project/ignore-patterns/sbt-output.ignore.txt b/project/ignore-patterns/sbt-output.ignore.txt index c1d17cbe10..07e8e8c1be 100644 --- a/project/ignore-patterns/sbt-output.ignore.txt +++ b/project/ignore-patterns/sbt-output.ignore.txt @@ -43,7 +43,7 @@ protoc-jar: caught exception, retrying: java\.io\.IOException: Cannot run progra .*no longer exists at.* # sbt 1.4 onwards logs GC warnings -.*\[.*warn.*\].* of the last .* were spent in garbage collection\. +.*\[.*warn.*\].* In the last .* were spent in GC\. # During release bundling, the copy of the Canton OS repo emits a bunch of warnings .*\[.*warn.*\].*Negative time @@ -131,9 +131,13 @@ Cannot use file /tmp/hsperfdata_ci WARN: InvalidDefaultArgInFrom # the script does not try to be secure -.*api_jwt\.py:153: InsecureKeyLengthWarning:.* +.*api_jwt\.py:.*: InsecureKeyLengthWarning:.* # com.daml.testing-utils seems to bring its own logback-test.xml, but it doesn't seem to break anything so we just ignore it Resource.*logback-test.xml.*occurs Setting level of logger.*to WARN Propagating WARN level on Logger + +# rolldown/vite emits a PLUGIN_TIMINGS performance advisory (e.g. for the built-in +# `rolldown:vite-resolve` resolver) on otherwise-successful builds. Not an error. +.*\[PLUGIN_TIMINGS\] Warning:.* diff --git a/scripts/copy-canton.sh b/scripts/copy-canton.sh index 576c9ba6f7..76327a3198 100755 --- a/scripts/copy-canton.sh +++ b/scripts/copy-canton.sh @@ -16,10 +16,21 @@ rsync -av --delete --exclude version.sbt --exclude community-build.sbt --exclude --exclude '*/wartremove/test/*' --exclude "*/ledger-api-bench-tool" \ --exclude '.ci' --exclude '.circleci' --exclude '.hooks' --exclude 'contributing' --exclude 'docker' \ --exclude 'docs-open' --exclude 'nix' --exclude 'performance' --exclude 'dashboards' --exclude 'release' \ + --exclude 'base/adjustable-clock' \ --exclude 'base/contextualized-logging' --exclude 'base/crypto' \ + --exclude 'base/daml-jwt' --exclude 'base/daml-tls' \ + --exclude 'base/errors' --exclude 'base/util-external' \ + --exclude '*/community/lib/Blake2b' \ + --exclude '*/community/lib/google-common-protos-scala' \ + --exclude '*/community/lib/magnolify' \ + --exclude '*/community/lib/scalatest' \ + --exclude '*/community/lib/slick' \ + --exclude '*/community/lib/wartremover-annotations' \ --exclude 'community/bindings-java' --exclude "*/community/transcode" \ + --exclude '*/community/kms-driver-api' \ --exclude '*/community/ledger-api-scala' --exclude "*/ledger-api-proto" \ --exclude '*/canton-community-app/test/scala/*/integration/tests' \ + --exclude '*/community/ledger/ledger-api-core' \ --exclude '*/canton/community/model-based-testing-drivers' \ --exclude '*/canton/community/model-based-testing-generators' \ --exclude '*/canton/community/model-based-testing-integration-tests' \ @@ -27,3 +38,9 @@ rsync -av --delete --exclude version.sbt --exclude community-build.sbt --exclude canton/ # remove any broken symlinks after the copy find -L canton/ -type l -exec rm {} + + +canton_bft_src="$1/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton" +canton_bft_dest="cluster/pulumi/observability/grafana-dashboards/canton-bft" +rm -rf "$canton_bft_dest" +mkdir -p "$canton_bft_dest" +cp "$canton_bft_src"/*.json "$canton_bft_dest/" diff --git a/scripts/monthly-schedule.py b/scripts/monthly-schedule.py new file mode 100755 index 0000000000..1860d940c8 --- /dev/null +++ b/scripts/monthly-schedule.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import calendar +import datetime +import json +import os +import re +import urllib.error +import urllib.request +from dataclasses import dataclass +from typing import Optional + +DEFAULT_GROUP_NAME = "Upcoming" + +DATE_COLUMN_NAME = "Date/Time US EST" +STATUS_COLUMN_NAME = "Submission Status" +NETWORK_COLUMN_NAME = "Network" +ACTIVITY_COLUMN_NAME = "Type of Activity" +VERSION_COLUMN_NAME = "Minor Versions" +DEPENDENCY_COLUMN_NAME = "Dependent On" + +INITIAL_STATUS = "To Be Confirmed" + +NETWORK_DEVNET = "DevNet" +NETWORK_TESTNET = "TestNet" +NETWORK_MAINNET = "MainNet" + +ACTIVITY_WEEKLY = "Weekly Upgrades" +ACTIVITY_DAML = "Splice Daml Model Effectivity" +ACTIVITY_LSU = "Protocol Upgrades (LSU)" +ACTIVITY_CONFIG = "Configuration Change" + +_BOARD_CACHE: dict[int, dict] = {} +_ITEMS_CACHE: dict[int, dict[str, list[str]]] = {} + + +@dataclass(frozen=True) +class ScheduledEvent: + title: str + date: datetime.date + network: str + activity: str + minor_version: str + time_utc: Optional[str] = None + depends_on: Optional[str] = None + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create/update the monthly Splice release schedule in monday.com." + ) + + parser.add_argument("version", help="Minor version, e.g. 0.8") + + parser.add_argument("month", help="Month in YYYY-MM format, e.g. 2026-08") + + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and show changes without modifying monday.com.", + ) + + args = parser.parse_args() + + if re.fullmatch(r"\d+\.\d+", args.version) is None: + parser.error("version must be in MAJOR.MINOR form, for example 0.8") + + if re.fullmatch(r"\d{4}-(0[1-9]|1[0-2])", args.month) is None: + parser.error("month must be in YYYY-MM format") + + return args + + +def first_monday_in_month(month: str) -> datetime.date: + year, month_num = map(int, month.split("-")) + + first_day = datetime.date(year, month_num, 1) + + return first_day + datetime.timedelta(days=(0 - first_day.weekday()) % 7) + + +def mondays_in_month(month: str) -> int: + year, month_num = map(int, month.split("-")) + + _, days_in_month = calendar.monthrange(year, month_num) + + first_day = datetime.date(year, month_num, 1) + + first_monday_offset = (0 - first_day.weekday()) % 7 + + return (days_in_month - first_monday_offset + 6) // 7 + + +def schedule_date(month: str, weekday: str, week_number: int) -> datetime.date: + weekdays = { + "monday": 0, + "tuesday": 1, + "wednesday": 2, + "thursday": 3, + "friday": 4, + "saturday": 5, + "sunday": 6, + } + + key = weekday.strip().lower() + + if key not in weekdays: + raise ValueError(f"Unknown weekday: {weekday}") + + if week_number < 0: + raise ValueError("week_number must be >= 0") + + return first_monday_in_month(month) + datetime.timedelta(weeks=week_number, days=weekdays[key]) + + +def required_env(name: str) -> str: + value = os.getenv(name) + + if value is None or value.strip() == "": + raise RuntimeError(f"Missing required environment variable: {name}") + + return value.strip() + + +def board_id_from_env() -> int: + value = required_env("MONDAY_BOARD_ID") + + try: + return int(value) + except ValueError as exc: + raise RuntimeError(f"MONDAY_BOARD_ID must be numeric; got {value!r}") from exc + + +def monday_request(token: str, query: str, variables: dict) -> dict: + headers = {"Authorization": token, "Content-Type": "application/json", + "Accept": "application/graphql-response+json, application/json"} + + if os.getenv("MONDAY_API_VERSION"): + headers["API-Version"] = os.environ["MONDAY_API_VERSION"] + + request = urllib.request.Request( + "https://api.monday.com/v2", + data=json.dumps({"query": query, "variables": variables}).encode("utf-8"), + headers=headers, + method="POST", + ) + + try: + with urllib.request.urlopen(request, timeout=30) as response: + body = response.read().decode("utf-8") + + except urllib.error.HTTPError as exc: + response_body = exc.read().decode("utf-8", errors="replace") + + raise RuntimeError(f"Monday API request failed ({exc.code}): {response_body}") from exc + + except urllib.error.URLError as exc: + raise RuntimeError(f"Could not reach Monday API: {exc}") from exc + + parsed = json.loads(body) + + if parsed.get("errors"): + raise RuntimeError( + "Monday API returned errors: " + json.dumps(parsed["errors"], ensure_ascii=False) + ) + + return parsed + + +def get_board(token: str, board_id: int) -> dict: + if board_id in _BOARD_CACHE: + return _BOARD_CACHE[board_id] + + query = """ + query BoardInfo($boardId: [ID!]!) { + boards(ids: $boardId) { + id + name + columns { + id + title + type + settings + } + groups { + id + title + archived + deleted + } + } + } + """ + + response = monday_request(token, query, {"boardId": [board_id]}) + + boards = response.get("data", {}).get("boards", []) + + if not boards: + raise RuntimeError(f"Board not found or not accessible: {board_id}") + + _BOARD_CACHE[board_id] = boards[0] + + return boards[0] + + +def get_column(board: dict, title: str) -> dict: + target = title.strip().casefold() + + matches = [ + column + for column in board.get("columns", []) + if (str(column.get("title", "")).strip().casefold() == target) + ] + + if not matches: + existing = ", ".join( + sorted(str(column.get("title", "")) for column in board.get("columns", [])) + ) + + raise RuntimeError(f"Column {title!r} not found. Board columns are: {existing}") + + if len(matches) > 1: + raise RuntimeError(f"More than one column is named {title!r}.") + + return matches[0] + + +def get_group_id(board: dict, group_name: str) -> str: + target = group_name.strip().casefold() + + matches = [ + group + for group in board.get("groups", []) + if ( + not group.get("archived") + and not group.get("deleted") + and (str(group.get("title", "")).strip().casefold() == target) + ) + ] + + if not matches: + existing = ", ".join( + str(group.get("title", "")) + for group in board.get("groups", []) + if (not group.get("archived") and not group.get("deleted")) + ) + + raise RuntimeError(f"Group {group_name!r} not found. Active groups are: {existing}") + + if len(matches) > 1: + raise RuntimeError(f"More than one active group is named {group_name!r}.") + + return str(matches[0]["id"]) + + +def column_labels(column: dict) -> set[str]: + settings = column.get("settings") or {} + + if isinstance(settings, str): + try: + settings = json.loads(settings) + except json.JSONDecodeError: + return set() + + if not isinstance(settings, dict): + return set() + + labels: set[str] = set() + + raw_labels = settings.get("labels", []) + + if isinstance(raw_labels, list): + for entry in raw_labels: + if isinstance(entry, dict): + label = entry.get("label") or entry.get("name") + + if isinstance(label, str) and label: + labels.add(label) + + elif isinstance(entry, str) and entry: + labels.add(entry) + + elif isinstance(raw_labels, dict): + for entry in raw_labels.values(): + if isinstance(entry, str) and entry: + labels.add(entry) + + elif isinstance(entry, dict): + label = entry.get("label") or entry.get("name") + + if isinstance(label, str) and label: + labels.add(label) + + return labels + + +def ensure_column_type(column: dict, allowed: set[str]) -> None: + actual = str(column.get("type", "")).lower() + + if actual not in allowed: + raise RuntimeError( + f"Column {column['title']!r} has type {actual!r}; expected one of {sorted(allowed)}" + ) + + +def require_label(column: dict, label: str) -> None: + labels = column_labels(column) + + if labels and label not in labels: + raise RuntimeError( + f"Column {column['title']!r} does not contain label {label!r}. Available labels: {sorted(labels)}" + ) + + +def preflight(token: str, board_id: int, group_name: str) -> tuple[dict, str]: + board = get_board(token, board_id) + + date_col = get_column(board, DATE_COLUMN_NAME) + + status_col = get_column(board, STATUS_COLUMN_NAME) + + network_col = get_column(board, NETWORK_COLUMN_NAME) + + activity_col = get_column(board, ACTIVITY_COLUMN_NAME) + + version_col = get_column(board, VERSION_COLUMN_NAME) + + dependency_col = get_column(board, DEPENDENCY_COLUMN_NAME) + + ensure_column_type(date_col, {"date"}) + + ensure_column_type(status_col, {"status", "color"}) + + ensure_column_type(network_col, {"status", "color", "dropdown", "text"}) + + ensure_column_type(activity_col, {"status", "color", "dropdown", "text"}) + + ensure_column_type(version_col, {"dropdown", "status", "color", "text"}) + + ensure_column_type(dependency_col, {"dependency"}) + + require_label(status_col, INITIAL_STATUS) + + for label in (NETWORK_DEVNET, NETWORK_TESTNET, NETWORK_MAINNET): + require_label(network_col, label) + + for label in (ACTIVITY_WEEKLY, ACTIVITY_DAML, ACTIVITY_LSU, ACTIVITY_CONFIG): + require_label(activity_col, label) + + return (board, get_group_id(board, group_name)) + + +def choice_value(column: dict, label: str): + column_type = str(column.get("type", "")).lower() + + if column_type in {"status", "color"}: + return {"label": label} + + if column_type == "dropdown": + return {"labels": [label]} + + if column_type in {"text", "long_text"}: + return label + + raise RuntimeError(f"Unsupported choice column type {column_type!r} for {column['title']!r}") + + +def date_value(event_date: datetime.date, time_utc: Optional[str]) -> dict: + value = {"date": event_date.isoformat()} + + if time_utc is not None: + if re.fullmatch(r"(?:[01]\d|2[0-3]):[0-5]\d", time_utc) is None: + raise ValueError(f"Invalid UTC time {time_utc!r}; expected HH:MM") + + value["time"] = f"{time_utc}:00" + + return value + + +def build_column_values( + board: dict, event: ScheduledEvent, include_submission_status: bool +) -> dict: + date_col = get_column(board, DATE_COLUMN_NAME) + + network_col = get_column(board, NETWORK_COLUMN_NAME) + + activity_col = get_column(board, ACTIVITY_COLUMN_NAME) + + version_col = get_column(board, VERSION_COLUMN_NAME) + + values = { + str(date_col["id"]): date_value(event.date, event.time_utc), + str(network_col["id"]): choice_value(network_col, event.network), + str(activity_col["id"]): choice_value(activity_col, event.activity), + str(version_col["id"]): choice_value(version_col, event.minor_version), + } + + if include_submission_status: + status_col = get_column(board, STATUS_COLUMN_NAME) + + values[str(status_col["id"])] = choice_value(status_col, INITIAL_STATUS) + + return values + + +def load_existing_items(token: str, board_id: int) -> dict[str, list[str]]: + if board_id in _ITEMS_CACHE: + return _ITEMS_CACHE[board_id] + + items_by_name: dict[str, list[str]] = {} + + query = """ + query BoardItems($boardId: [ID!]!) { + boards(ids: $boardId) { + items_page(limit: 500) { + cursor + items { + id + name + } + } + } + } + """ + + response = monday_request(token, query, {"boardId": [board_id]}) + + boards = response.get("data", {}).get("boards", []) + + if not boards: + raise RuntimeError(f"Board not found: {board_id}") + + page = boards[0].get("items_page") or {} + + for item in page.get("items", []): + items_by_name.setdefault(str(item["name"]), []).append(str(item["id"])) + + cursor = page.get("cursor") + + next_query = """ + query MoreItems($cursor: String!) { + next_items_page(cursor: $cursor) { + cursor + items { + id + name + } + } + } + """ + + while cursor: + response = monday_request(token, next_query, {"cursor": cursor}) + + page = response.get("data", {}).get("next_items_page") or {} + + for item in page.get("items", []): + items_by_name.setdefault(str(item["name"]), []).append(str(item["id"])) + + cursor = page.get("cursor") + + _ITEMS_CACHE[board_id] = items_by_name + + return items_by_name + + +def create_item( + token: str, board_id: int, group_id: str, board: dict, event: ScheduledEvent +) -> str: + mutation = """ + mutation CreateItem( + $boardId: ID!, + $groupId: String!, + $itemName: String!, + $columnValues: JSON! + ) { + create_item( + board_id: $boardId, + group_id: $groupId, + item_name: $itemName, + column_values: $columnValues, + create_labels_if_missing: true + ) { + id + } + } + """ + + response = monday_request( + token, + mutation, + { + "boardId": board_id, + "groupId": group_id, + "itemName": event.title, + "columnValues": json.dumps( + build_column_values(board, event, include_submission_status=True) + ), + }, + ) + + return str(response["data"]["create_item"]["id"]) + + +def update_item( + token: str, board_id: int, board: dict, item_id: str, event: ScheduledEvent +) -> None: + mutation = """ + mutation UpdateItem( + $boardId: ID!, + $itemId: ID!, + $columnValues: JSON! + ) { + change_multiple_column_values( + board_id: $boardId, + item_id: $itemId, + column_values: $columnValues, + create_labels_if_missing: true + ) { + id + } + } + """ + + monday_request( + token, + mutation, + { + "boardId": board_id, + "itemId": item_id, + "columnValues": json.dumps( + build_column_values(board, event, include_submission_status=False) + ), + }, + ) + + +def describe_event(event: ScheduledEvent) -> str: + when = event.date.isoformat() + + if event.time_utc: + when += f" {event.time_utc} UTC" + + return f"{when} | {event.network} | {event.activity} | {event.minor_version}" + + +def upsert_event( + token: str, board_id: int, group_id: str, board: dict, event: ScheduledEvent, dry_run: bool +) -> Optional[str]: + items = load_existing_items(token, board_id) + + matches = items.get(event.title, []) + + if len(matches) > 1: + raise RuntimeError( + f"Cannot safely update {event.title!r}: multiple exact-name items exist: {matches}" + ) + + details = describe_event(event) + + if matches: + item_id = matches[0] + + if dry_run: + print(f"WOULD UPDATE {item_id}: {event.title} -> {details}") + else: + update_item(token, board_id, board, item_id, event) + + print(f"UPDATED {item_id}: {event.title} -> {details}") + + return item_id + + if dry_run: + print(f"WOULD CREATE: {event.title} -> {details}") + + return None + + item_id = create_item(token, board_id, group_id, board, event) + + items.setdefault(event.title, []).append(item_id) + + print(f"CREATED {item_id}: {event.title} -> {details}") + + return item_id + + +def set_dependency( + token: str, board_id: int, board: dict, item_id: str, dependency_item_id: str +) -> None: + dependency_col = get_column(board, DEPENDENCY_COLUMN_NAME) + + mutation = """ + mutation SetDependency( + $boardId: ID!, + $itemId: ID!, + $columnValues: JSON! + ) { + change_multiple_column_values( + board_id: $boardId, + item_id: $itemId, + column_values: $columnValues + ) { + id + } + } + """ + + monday_request( + token, + mutation, + { + "boardId": board_id, + "itemId": item_id, + "columnValues": json.dumps( + {str(dependency_col["id"]): {"item_ids": [str(dependency_item_id)]}} + ), + }, + ) + + +def make_schedule(version: str, month: str) -> list[ScheduledEvent]: + events: list[ScheduledEvent] = [] + + patch_count = mondays_in_month(month) + + specs = [ + { + "network": NETWORK_DEVNET, + "weekly_offset": 0, + "daml_week": 2, + "freeze_week": 2, + "freeze_day": "tuesday", + "lsu_week": 2, + "lsu_day": "wednesday", + "config_week": 3, + "daml_title": (f"DevNet New Daml models introduced by Splice {version}.x take effect"), + "freeze_title": (f"DevNet Topology Freeze ({version} Required) (MONTH YEAR)"), + "lsu_title": (f"DevNet LSU ({version} Required) (MONTH YEAR)"), + "config_title": (f"DevNet Breaking Config Changes ({version} Required)"), + }, + { + "network": NETWORK_TESTNET, + "weekly_offset": 1, + "daml_week": 3, + "freeze_week": 3, + "freeze_day": "tuesday", + "lsu_week": 3, + "lsu_day": "wednesday", + "config_week": 4, + "daml_title": (f"TestNet New Daml models introduced by Splice {version}.x take effect"), + "freeze_title": (f"TestNet Topology Freeze ({version} Required) (MONTH YEAR)"), + "lsu_title": (f"TestNet LSU ({version} Required) (MONTH YEAR)"), + "config_title": (f"TestNet Breaking Config Changes ({version} Required)"), + }, + { + "network": NETWORK_MAINNET, + "weekly_offset": 2, + "daml_week": 4, + "freeze_week": 4, + "freeze_day": "friday", + "lsu_week": 4, + "lsu_day": "saturday", + "config_week": 5, + "daml_title": (f"MainNet New Daml models introduced by Splice {version}.x take effect"), + "freeze_title": (f"MainNet Topology Freeze ({version})"), + "lsu_title": (f"MainNet LSU ({version} Required) (MONTH YEAR)"), + "config_title": (f"MainNet Breaking Config Changes ({version} Required)"), + }, + ] + + for spec in specs: + network = spec["network"] + + lsu_date = schedule_date(month, str(spec["lsu_day"]), int(spec["lsu_week"])) + + lsu_month_year = f"{calendar.month_name[lsu_date.month]} {lsu_date.year:04d}" + + freeze_title = str(spec["freeze_title"]).replace("MONTH YEAR", lsu_month_year) + + lsu_title = str(spec["lsu_title"]).replace("MONTH YEAR", lsu_month_year) + + for patch in range(patch_count): + events.append( + ScheduledEvent( + title=(f"{network} upgrades to Splice {version}.{patch}"), + date=schedule_date(month, "monday", patch + int(spec["weekly_offset"])), + network=str(network), + activity=(ACTIVITY_WEEKLY), + minor_version=(version), + ) + ) + + events.extend( + [ + ScheduledEvent( + title=str(spec["daml_title"]), + date=schedule_date(month, "tuesday", int(spec["daml_week"])), + time_utc="12:00", + network=str(network), + activity=(ACTIVITY_DAML), + minor_version=(version), + ), + ScheduledEvent( + title=freeze_title, + date=schedule_date(month, str(spec["freeze_day"]), int(spec["freeze_week"])), + time_utc="13:00", + network=str(network), + activity=(ACTIVITY_LSU), + minor_version=(version), + ), + ScheduledEvent( + title=lsu_title, + date=lsu_date, + time_utc="13:00", + network=str(network), + activity=(ACTIVITY_LSU), + minor_version=(version), + depends_on=freeze_title, + ), + ScheduledEvent( + title=str(spec["config_title"]), + date=schedule_date(month, "tuesday", int(spec["config_week"])), + time_utc="12:00", + network=str(network), + activity=(ACTIVITY_CONFIG), + minor_version=(version), + ), + ] + ) + + return events + + +def main() -> None: + args = parse_args() + + token = required_env("MONDAY_API_TOKEN") + + board_id = board_id_from_env() + + group_name = os.getenv("MONDAY_GROUP_NAME", DEFAULT_GROUP_NAME).strip() + + first_monday = first_monday_in_month(args.month) + + print() + + print(f"Splice {args.version}.x") + + print(f"Monday board: {board_id}") + + print(f"Target group: {group_name}") + + print(f"First DevNet Monday: {first_monday.isoformat()}") + + if args.dry_run: + print("DRY RUN — validating the board and showing changes only.") + + board, group_id = preflight(token, board_id, group_name) + + print(f"Board name: {board.get('name', '')}") + + print("Preflight validation: OK") + + events = make_schedule(args.version, args.month) + + print(f"Schedule events: {len(events)}") + + print() + + item_ids_by_title: dict[str, str] = {} + + current_network: Optional[str] = None + + for event in events: + if event.network != current_network: + current_network = event.network + + print(current_network) + + item_id = upsert_event(token, board_id, group_id, board, event, args.dry_run) + + if item_id: + item_ids_by_title[event.title] = item_id + + print() + + print("Dependencies") + + for event in events: + if not event.depends_on: + continue + + if args.dry_run: + print(f"WOULD LINK: {event.title} depends on {event.depends_on}") + + continue + + item_id = item_ids_by_title.get(event.title) + + dependency_item_id = item_ids_by_title.get(event.depends_on) + + if not item_id: + raise RuntimeError(f"Could not find Monday item for {event.title!r}") + + if not dependency_item_id: + raise RuntimeError(f"Could not find dependency item {event.depends_on!r}") + + set_dependency(token, board_id, board, item_id, dependency_item_id) + + print(f"LINKED: {event.title} depends on {event.depends_on}") + + print() + + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/scripts/rename.sh b/scripts/rename.sh index 92ff142232..8fc9397d5e 100755 --- a/scripts/rename.sh +++ b/scripts/rename.sh @@ -1216,7 +1216,7 @@ function subcmd_no_illegal_daml_references() { ) for pattern in "${illegal_patterns[@]}"; do echo "Checking for occurences of '$pattern' (case sensitive, in code other than splitwell)" - if rg -P "$pattern" daml/ token-standard/ -g '!*/splitwell/*' -g '!*/splitwell-test/*' -g '!daml/dars.lock' -g '!token-standard/README.md' -g '!token-standard/V2_VALIDATION.md' -g '!token-standard/TOKEN_STANDARD_V2_DEVNET.md' -g '!*.json' -g '!token-standard/dependencies/*' -g '!**/target/'; then + if rg -P "$pattern" daml/ token-standard/ -g '!*/splitwell/*' -g '!*/splitwell-test/*' -g '!daml/dars.lock' -g '!token-standard/README.md' -g '!token-standard/V2_VALIDATION.md' -g '!token-standard/TOKEN_STANDARD_V2_DEVNET.md' -g'!daml/daml-ide-mono/README.md' -g '!*.json' -g '!token-standard/dependencies/*' -g '!**/target/'; then echo "$pattern occurs in Daml code (other than splitwell), remove all references" exit 1 fi diff --git a/scripts/setup-mono-package.sh b/scripts/setup-mono-package.sh new file mode 100755 index 0000000000..d7e8cc34f8 --- /dev/null +++ b/scripts/setup-mono-package.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Populate daml/daml-ide-mono/daml/ with per-file symlinks into every +# workspace Daml package's source tree. daml/daml-ide-mono/daml.yaml is +# checked in and static - this script only regenerates the source tree +# so that VS Code can work on the union as a single package. +# +# Re-run this any time a .daml file is added or removed anywhere in the +# workspace. + +set -euo pipefail + +repo=$(cd "$(dirname "$0")/.." && pwd) +dest="$repo/daml/daml-ide-mono/daml" + +rm -rf "$dest" +mkdir -p "$dest" + +for pkg in "$repo"/daml/*/daml.yaml \ + "$repo"/token-standard/*/daml.yaml \ + "$repo"/token-standard/examples/*/daml.yaml; do + src=$(dirname "$pkg")/daml + [ -d "$src" ] || continue + ( cd "$src" && find . -name '*.daml' -printf '%P\n' ) | + while IFS= read -r rel; do + link="$dest/$rel" + mkdir -p "$(dirname "$link")" + target=$(realpath --relative-to="$(dirname "$link")" "$src/$rel") + ln -sfn "$target" "$link" + done +done diff --git a/scripts/test-postgres-migration-k8s.py b/scripts/test-postgres-migration-k8s.py new file mode 100755 index 0000000000..b4892ccdb1 --- /dev/null +++ b/scripts/test-postgres-migration-k8s.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Replay the Kubernetes section of the PostgreSQL 14 migration guide +(https://docs.canton.network/global-synchronizer/production-operations/validator-postgres-migration) +against a scratch cluster, verifying that the documented procedure works end-to-end. + +Adds the augmentations a scratch cluster needs on top of the documented commands: +tolerations copied from a running pod, service-mesh sidecar injection disabled on +client pods, the source password read from the secret, and values recovered from +the live releases. + +Prerequisites: kubectl context on the cluster, helm; a provisioned target +PostgreSQL (cnadmin user with CREATEDB, cantonnet database, reachable from pods). +The source instance is left untouched (decommissioning stays a manual step), and +wallet-level verification (balances, a fresh transfer) stays manual. + +usage: NAMESPACE=validator1 TARGET_HOST=10.0.0.5 TARGET_PASSWORD=... \ + scripts/test-postgres-migration-k8s.py +""" + +import base64 +import json +import os +import re +import subprocess +import sys +import tempfile +import time + + +def fail(msg: str) -> None: + print(f"FAIL: {msg}") + sys.exit(1) + + +def require_env(name: str, hint: str) -> str: + value = os.environ.get(name) + if not value: + fail(f"set {name} to {hint}") + return value + + +NAMESPACE = require_env("NAMESPACE", "the validator namespace") +TARGET_HOST = require_env("TARGET_HOST", "the target PostgreSQL host or IP") +TARGET_PASSWORD = require_env("TARGET_PASSWORD", "the cnadmin password on the target") +SOURCE_HOST = os.environ.get("SOURCE_HOST", "postgres") # splice-postgres release/service name +SECRET_NAME = os.environ.get("SECRET_NAME", "postgres-secrets") +HELM_REPO = os.environ.get("HELM_REPO", "oci://ghcr.io/digital-asset/decentralized-canton-sync/helm") +PG_CLIENT_IMAGE = os.environ.get("PG_CLIENT_IMAGE", "postgres:17") + + +def run(args: list[str], capture: bool = False, check: bool = True, + stdin_data: str | None = None) -> subprocess.CompletedProcess: + # stdin defaults to /dev/null so attached pods can never consume anything + return subprocess.run( + args, + text=True, + input=stdin_data, + stdin=None if stdin_data is not None else subprocess.DEVNULL, + stdout=subprocess.PIPE if capture else None, + stderr=subprocess.PIPE if capture else None, + check=check, + ) + + +def kubectl_json(*args: str): + out = run(["kubectl", *args, "-o", "json"], capture=True).stdout + return json.loads(out) + + +for tool in ("kubectl", "helm"): + from shutil import which + if which(tool) is None: + fail(f"{tool} not found") + +WORK = tempfile.mkdtemp(prefix="pgmig-k8s-") +print(f"work dir: {WORK}") + +secret = kubectl_json("get", "secret", SECRET_NAME, "-n", NAMESPACE) +POSTGRES_PASSWORD = base64.b64decode(secret["data"]["postgresPassword"]).decode() + +# Tolerations from the running postgres pod; sidecar injection off for client pods. +source_pod = kubectl_json("get", "pod", f"{SOURCE_HOST}-0", "-n", NAMESPACE) +OVERRIDES = json.dumps({ + "metadata": {"annotations": {"sidecar.istio.io/inject": "false"}}, + "spec": {"tolerations": source_pod["spec"].get("tolerations", [])}, +}) + + +def pgpod(name: str, password: str, *cmd: str, env: dict[str, str] | None = None, + check: bool = True) -> subprocess.CompletedProcess: + """Run a one-off client pod; returns the completed process with output captured.""" + env_args = [f"--env=PGPASSWORD={password}"] + for key, value in (env or {}).items(): + env_args.append(f"--env={key}={value}") + result = run( + ["kubectl", "run", name, "--rm", "-i", "--restart=Never", "-n", NAMESPACE, + f"--image={PG_CLIENT_IMAGE}", *env_args, f"--overrides={OVERRIDES}", "--", *cmd], + capture=True, check=False, + ) + if check and result.returncode != 0: + print(result.stdout) + print(result.stderr, file=sys.stderr) + fail(f"pod {name} failed running: {' '.join(cmd[:3])} ...") + return result + + +def pod_lines(result: subprocess.CompletedProcess) -> list[str]: + """Pod stdout minus kubectl chatter and blanks.""" + return [ + line for line in result.stdout.splitlines() + if line.strip() and not line.startswith("pod ") + ] + + +print("### 0. Probe the target (reachability, CREATEDB, connection limit)") +probe = pgpod("pg-client", TARGET_PASSWORD, + "psql", "-h", TARGET_HOST, "-U", "cnadmin", "-d", "cantonnet", + "-c", "CREATE DATABASE probe", "-c", "DROP DATABASE probe", + "-c", "SHOW max_connections") +print(probe.stdout, end="") + +print("### 1. Enumerate the databases") +enum = pgpod("pg-client", POSTGRES_PASSWORD, + "psql", "-h", SOURCE_HOST, "-U", "cnadmin", "-d", "cantonnet", "-tA", + "-c", "SELECT datname FROM pg_database WHERE NOT datistemplate AND datname <> 'postgres'") +databases = pod_lines(enum) +print("\n".join(databases)) +if not databases: + fail("no databases enumerated") + +print("### 2. Stop the applications") +downtime_start = time.monotonic() +run(["kubectl", "scale", "deployment", "--all", "--replicas=0", "-n", NAMESPACE]) +for _ in range(60): + pods = kubectl_json("get", "pods", "-n", NAMESPACE)["items"] + still_running = [ + p["metadata"]["name"] for p in pods + if p["status"].get("phase") == "Running" + and not p["metadata"]["name"].startswith(f"{SOURCE_HOST}-") + ] + if not still_running: + break + time.sleep(5) +else: + fail(f"pods still running after quiesce: {still_running}") + +print("### 3. Create the databases on the target") +for db in databases: + if db == "cantonnet": + continue + result = pgpod("pg-client", TARGET_PASSWORD, + "psql", "-h", TARGET_HOST, "-U", "cnadmin", "-d", "cantonnet", + "-c", f'DROP DATABASE IF EXISTS "{db}" WITH (FORCE)', + "-c", f'CREATE DATABASE "{db}"') + print(result.stdout, end="") + +print("### 4. Copy each database") +for db in databases: + copy_cmd = ( + f"PGPASSWORD=\"$SOURCE_PGPASSWORD\" pg_dump -h {SOURCE_HOST} -U cnadmin -Fc '{db}'" + f" | PGPASSWORD=\"$TARGET_PGPASSWORD\" pg_restore -h {TARGET_HOST} -U cnadmin" + f" --no-owner --no-privileges --exit-on-error -d '{db}'" + ) + result = pgpod("pg-migrate", TARGET_PASSWORD, "bash", "-c", copy_cmd, + env={"SOURCE_PGPASSWORD": POSTGRES_PASSWORD, + "TARGET_PGPASSWORD": TARGET_PASSWORD}, + check=False) + if result.returncode != 0: + print(result.stdout) + print(result.stderr, file=sys.stderr) + fail(f"copy of {db}") + print(f"copied: {db}") + +print("### 4b. Compare table counts per database (source vs target)") +COUNT_SQL = ("SELECT count(*) FROM pg_tables" + " WHERE schemaname NOT IN ('pg_catalog','information_schema')") + + +def count_tables(host: str, password: str, db: str) -> int: + result = pgpod("pg-client", password, + "psql", "-h", host, "-U", "cnadmin", "-d", db, "-tA", "-c", COUNT_SQL) + return int(pod_lines(result)[0]) + + +for db in databases: + src = count_tables(SOURCE_HOST, POSTGRES_PASSWORD, db) + tgt = count_tables(TARGET_HOST, TARGET_PASSWORD, db) + print(f"tables in {db}: source={src} target={tgt}") + if src != tgt: + fail(f"table count mismatch in {db}") + +print("### 5. Point the applications at the target") +# Decide which releases to repoint BEFORE touching the secret: a release +# qualifies when its persistence.host is the source instance, as a bare +# service name or a cluster FQDN (e.g. ..svc.cluster.local). +releases = json.loads(run(["helm", "list", "-n", NAMESPACE, "-o", "json"], capture=True).stdout) +to_repoint = [] +for release in releases: + name, chart = release["name"], release["chart"] + if chart.startswith("splice-postgres-"): + continue + values_raw = run(["helm", "get", "values", name, "-n", NAMESPACE, "-o", "json"], + capture=True).stdout + values = json.loads(values_raw) or {} + host = (values.get("persistence") or {}).get("host", "") + if host == SOURCE_HOST or host.startswith(f"{SOURCE_HOST}."): + to_repoint.append((name, chart, values)) +if not to_repoint: + fail(f"no release has persistence.host pointing at {SOURCE_HOST}") +print("releases to repoint:") +for name, chart, _ in to_repoint: + print(f"{name} {chart}") + +secret_yaml = run(["kubectl", "create", "secret", "generic", SECRET_NAME, + f"--from-literal=postgresPassword={TARGET_PASSWORD}", + "-n", NAMESPACE, "--dry-run=client", "-o", "yaml"], capture=True).stdout +run(["kubectl", "apply", "-f", "-"], stdin_data=secret_yaml) + + +def repoint(name: str, chart: str, values: dict) -> None: + match = re.match(r"^(?P.+)-(?P\d+\.\d+\..+)$", chart) + if not match: + fail(f"cannot parse chart/version from {chart}") + values.setdefault("persistence", {}) + values["persistence"]["host"] = TARGET_HOST + values["persistence"]["port"] = 5432 + values_file = os.path.join(WORK, f"{name}-values.json") # helm accepts JSON values + with open(values_file, "w") as f: + json.dump(values, f, indent=2) + print(f"upgrading {name} ({match['chart']} {match['version']})") + run(["helm", "upgrade", name, f"{HELM_REPO}/{match['chart']}", + "--version", match["version"], "-f", values_file, + "-n", NAMESPACE, "--wait", "--timeout", "10m"]) + + +# participants first, mirroring install order +for name, chart, values in to_repoint: + if chart.startswith("splice-participant-"): + repoint(name, chart, values) +for name, chart, values in to_repoint: + if not chart.startswith("splice-participant-"): + repoint(name, chart, values) + +print("### 6. Verify") +run(["kubectl", "wait", "deployment", "--all", "--for=condition=Available", + "-n", NAMESPACE, "--timeout=600s"]) +print(f"quiesce -> available: {int(time.monotonic() - downtime_start)}s") + +activity = pgpod("pg-client", TARGET_PASSWORD, + "psql", "-h", TARGET_HOST, "-U", "cnadmin", "-d", "cantonnet", + "-c", "SHOW server_version", + "-c", "SELECT datname, count(*) FROM pg_stat_activity" + " WHERE datname <> 'cantonnet' GROUP BY 1") +print(activity.stdout, end="") +if "participant" not in activity.stdout: + fail("no application connections on the target") + +idle = pgpod("pg-client", POSTGRES_PASSWORD, + "psql", "-h", SOURCE_HOST, "-U", "cnadmin", "-d", "cantonnet", "-tA", + "-c", "SELECT count(*) FROM pg_stat_activity" + " WHERE datname NOT IN ('cantonnet','postgres') AND datname IS NOT NULL") +if pod_lines(idle)[0] != "0": + fail(f"{pod_lines(idle)[0]} connection(s) still on the source instance") + +print(f"PASS: applications migrated to {TARGET_HOST} (source instance idle)") +print("manual follow-ups: wallet balance + fresh transfer; scale non-helm deployments") +print("back up; decommission the source per step 7 of the guide once verified.") diff --git a/scripts/test-postgres-migration.py b/scripts/test-postgres-migration.py new file mode 100755 index 0000000000..8cff95baf7 --- /dev/null +++ b/scripts/test-postgres-migration.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""End-to-end test of the PostgreSQL major-version migration documented at +https://docs.canton.network/global-synchronizer/production-operations/validator-postgres-migration, +run against LocalNet. + +Flow: LocalNet on postgres:SRC_PG -> seed wallet data (tap + cross-participant +transfer) -> stop applications -> pg_dump every database with the target-version +client -> fresh postgres:TGT_PG -> pg_restore -> restart -> assert that balances +are preserved and a new transfer succeeds. + +WARNING: tears down any running LocalNet compose project (down -v) at start. + +usage: [SRC_PG=14] [TGT_PG=17] [IMAGE_TAG=0.6.11] scripts/test-postgres-migration.py +""" + +import base64 +import hashlib +import hmac +import json +import os +import subprocess +import sys +import tempfile +import time +import urllib.error +import urllib.request + +SRC_PG = os.environ.get("SRC_PG", "14") +TGT_PG = os.environ.get("TGT_PG", "17") +IMAGE_TAG = os.environ.get("IMAGE_TAG", "0.6.11") +DB_PASSWORD = "supersafe" + +REPO_ROOT = subprocess.run(["git", "rev-parse", "--show-toplevel"], + capture_output=True, text=True, check=True).stdout.strip() +LOCALNET_DIR = os.environ.get("LOCALNET_DIR", f"{REPO_ROOT}/cluster/compose/localnet") +DUMPS = tempfile.mkdtemp(prefix="pgmig-dumps-") + + +def fail(msg: str) -> None: + print(f"FAIL: {msg}") + sys.exit(1) + + +def run(args: list[str], capture: bool = False, check: bool = True, + env: dict[str, str] | None = None) -> subprocess.CompletedProcess: + return subprocess.run( + args, text=True, check=check, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE if capture else None, + stderr=subprocess.STDOUT if capture else None, + env={**os.environ, **(env or {})}, + ) + + +def compose(*args: str, postgres_version: str, check: bool = True) -> None: + run(["docker", "compose", + "--env-file", f"{LOCALNET_DIR}/compose.env", + "--env-file", f"{LOCALNET_DIR}/env/common.env", + "-f", f"{LOCALNET_DIR}/compose.yaml", + "-f", f"{LOCALNET_DIR}/resource-constraints.yaml", + "--profile", "sv", "--profile", "app-provider", "--profile", "app-user", + *args], + check=check, + env={"IMAGE_TAG": IMAGE_TAG, "LOCALNET_DIR": LOCALNET_DIR, + "POSTGRES_VERSION": postgres_version}) + + +def b64url(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode() + + +def wallet(port: int, user: str, method: str, path: str, body: dict | None = None): + """Call a LocalNet wallet API through nginx with an unsafe-auth HS256 JWT.""" + header = b64url(b'{"alg":"HS256","typ":"JWT"}') + payload = b64url(json.dumps( + {"sub": user, "aud": "https://canton.network.global", "exp": 4102444800}).encode()) + sig = b64url(hmac.new(b"unsafe", f"{header}.{payload}".encode(), hashlib.sha256).digest()) + request = urllib.request.Request( + f"http://127.0.0.1:{port}{path}", + method=method, + data=json.dumps(body).encode() if body is not None else None, + headers={"Host": "wallet.localhost", + "Authorization": f"Bearer {header}.{payload}.{sig}", + **({"Content-Type": "application/json"} if body is not None else {})}, + ) + with urllib.request.urlopen(request, timeout=30) as response: + return json.loads(response.read()) + + +def retry(what: str, attempts: int, delay: int, fn): + for _ in range(attempts): + try: + result = fn() + if result is not None: + return result + except (urllib.error.URLError, OSError, json.JSONDecodeError, KeyError): + pass + time.sleep(delay) + fail(f"{what} did not succeed after {attempts} attempts") + + +def balance(port: int, user: str) -> str: + return wallet(port, user, "GET", "/api/validator/v0/wallet/balance")["effective_unlocked_qty"] + + +def wait_healthy() -> None: + def all_healthy(): + out = run(["docker", "ps", "--format", "{{.Names}} {{.Status}}"], capture=True).stdout + lines = [ln for ln in out.splitlines() if ln.strip()] + unhealthy = [ln for ln in lines if "(healthy)" not in ln] + return True if len(unhealthy) <= 1 else None # nginx has no healthcheck + retry("containers becoming healthy", 60, 5, all_healthy) + + +def wait_wallet(port: int, user: str) -> None: + # healthy containers do not imply onboarded wallets; wait for the wallet API too + retry(f"wallet for {user}", 60, 5, + lambda: True if wallet(port, user, "GET", "/api/validator/v0/wallet/user-status") + .get("user_wallet_installed") else None) + + +def wait_pg_healthy() -> None: + def healthy(): + out = run(["docker", "inspect", "-f", "{{.State.Health.Status}}", "postgres"], + capture=True, check=False).stdout.strip() + return True if out == "healthy" else None + retry("postgres becoming healthy", 30, 2, healthy) + + +def tap() -> None: + # right after bootstrap taps fail until the first open mining round exists; retry + retry("tap", 30, 10, + lambda: True if "contract_id" in wallet( + 2000, "app-user", "POST", "/api/validator/v0/wallet/tap", {"amount": "1000.0"}) + else None) + + +def transfer(amount: str, tracking_id: str) -> None: + provider = wallet(3000, "app-provider", "GET", + "/api/validator/v0/wallet/user-status")["party_id"] + offer = wallet(2000, "app-user", "POST", "/api/validator/v0/wallet/transfer-offers", + {"receiver_party_id": provider, "amount": amount, + "description": tracking_id, "expires_at": 4102444800000000, + "tracking_id": tracking_id}) + if "offer_contract_id" not in offer: + fail(f"transfer offer {tracking_id} not created: {offer}") + + def find_offer(): # receiver sees the offer only after ingestion + offers = wallet(3000, "app-provider", "GET", + "/api/validator/v0/wallet/transfer-offers")["offers"] + return offers[0]["contract_id"] if offers else None + cid = retry(f"offer visibility for {tracking_id}", 30, 2, find_offer) + accepted = wallet(3000, "app-provider", "POST", + f"/api/validator/v0/wallet/transfer-offers/{cid}/accept", {}) + if "accepted_offer_contract_id" not in accepted: + fail(f"transfer {tracking_id} not accepted: {accepted}") + + +print(f"migrating postgres:{SRC_PG} -> postgres:{TGT_PG} (splice {IMAGE_TAG}); dumps in {DUMPS}") + +print(f"### 0. Clean slate, start LocalNet on postgres:{SRC_PG}") +compose("down", "-v", "--remove-orphans", postgres_version=SRC_PG, check=False) +run(["docker", "volume", "rm", "-f", f"localnet_postgres_pg{SRC_PG}_backup"], check=False) +compose("up", "-d", postgres_version=SRC_PG) +wait_healthy() +run(["docker", "exec", "postgres", "postgres", "--version"]) + +print("### 1. Seed data: tap 1000 USD on app-user, transfer 12345 CC to app-provider") +wait_wallet(2000, "app-user") +wait_wallet(3000, "app-provider") +tap() +time.sleep(3) +transfer("12345.0", "pgmig-pre") +time.sleep(8) +user_bal_before = balance(2000, "app-user") +prov_bal_before = balance(3000, "app-provider") +print(f"pre-migration balances: app-user={user_bal_before} app-provider={prov_bal_before}") + +print("### 2. Quiesce: stop everything except postgres") +compose("stop", postgres_version=SRC_PG) +compose("start", "postgres", postgres_version=SRC_PG) +wait_pg_healthy() +running = run(["docker", "ps", "--format", "{{.Names}}"], capture=True).stdout.split() +if running != ["postgres"]: + fail(f"quiesce incomplete, still running: {running}") + +print(f"### 3. Dump all databases with the postgres:{TGT_PG} client") +databases = run( + ["docker", "exec", "postgres", "psql", "-U", "cnadmin", "-d", "postgres", "-tA", + "-c", "SELECT datname FROM pg_database WHERE NOT datistemplate AND datname <> 'postgres'"], + capture=True).stdout.split() +for db in databases: + run(["docker", "run", "--rm", "--network", "localnet", "-e", f"PGPASSWORD={DB_PASSWORD}", + "-v", f"{DUMPS}:/dumps", f"postgres:{TGT_PG}", + "pg_dump", "-h", "postgres", "-U", "cnadmin", "-Fc", + "-f", f"/dumps/{db}.dump", db]) +run(["ls", "-lh", DUMPS]) + +print(f"### 4. Keep the pg{SRC_PG} volume as rollback point, remove original") +compose("stop", "postgres", postgres_version=SRC_PG) +run(["docker", "rm", "-f", "postgres"]) +run(["docker", "volume", "create", f"localnet_postgres_pg{SRC_PG}_backup"], capture=True) +run(["docker", "run", "--rm", "-v", "localnet_postgres:/from:ro", + "-v", f"localnet_postgres_pg{SRC_PG}_backup:/to", + "alpine", "sh", "-c", "cp -a /from/. /to/"]) +run(["docker", "volume", "rm", "localnet_postgres"]) + +print(f"### 5. Fresh postgres:{TGT_PG} (entrypoint pre-creates empty databases)") +compose("up", "-d", "postgres", postgres_version=TGT_PG) +wait_pg_healthy() +run(["docker", "exec", "postgres", "postgres", "--version"]) +# create databases the entrypoint did not pre-create (all exist on LocalNet; +# mirrors the migration guide, where start.sh-injected names are missing) +for db in databases: + run(["docker", "exec", "postgres", "psql", "-U", "cnadmin", "-d", "postgres", + "-c", f'CREATE DATABASE "{db}"'], capture=True, check=False) + +print("### 6. Restore every dump") +for db in databases: + run(["docker", "run", "--rm", "--network", "localnet", "-e", f"PGPASSWORD={DB_PASSWORD}", + "-v", f"{DUMPS}:/dumps:ro", f"postgres:{TGT_PG}", + "pg_restore", "-h", "postgres", "-U", "cnadmin", + "--no-owner", "--no-privileges", "--exit-on-error", "-d", db, f"/dumps/{db}.dump"]) + print(f"restored: {db}") + +print(f"### 7. Restart the full stack on postgres:{TGT_PG}") +compose("up", "-d", postgres_version=TGT_PG) +wait_healthy() +wait_wallet(2000, "app-user") +wait_wallet(3000, "app-provider") + +print("### 8. Assertions") +user_bal_after = balance(2000, "app-user") +prov_bal_after = balance(3000, "app-provider") +print(f"post-migration balances: app-user={user_bal_after} app-provider={prov_bal_after}") +# balances only grow between the two checks (devnet reward issuance) +if float(user_bal_after) < float(user_bal_before): + fail("app-user balance shrank across migration") +if float(prov_bal_after) < float(prov_bal_before): + fail("app-provider balance shrank across migration") + +transfer("55.0", "pgmig-post") + +errors = run(["docker", "logs", "splice", "--since", "10m"], capture=True).stdout +error_count = errors.count('"level":"ERROR"') +if error_count: + fail(f"{error_count} ERROR lines in splice logs") + +print(f"PASS: migration postgres:{SRC_PG} -> postgres:{TGT_PG} verified") diff --git a/start-frontends.sh b/start-frontends.sh index 94156a9825..f49eb260a4 100755 --- a/start-frontends.sh +++ b/start-frontends.sh @@ -15,7 +15,9 @@ function tmux_cmd() { else tmux new-window -t "$t" -n "$title" fi - tmux send-keys -t "$t" "cd $wd" C-m + local direnv_ready_event="${tmux_session}-cd-done-${tmux_window}" + tmux send-keys -t "$t" "cd $wd && tmux wait-for -S $direnv_ready_event" C-m + tmux wait-for "$direnv_ready_event" tmux send-keys -t "$t" "$cmd" C-m tmux_window=$((tmux_window + 1)) } @@ -75,11 +77,9 @@ function start_frontend() { local log_file="${LOG_DIR}/npm-${app}-${user}.out" tmux_cmd "${app}-${user}" "${frontend_dir}" \ - "trap \"rm -f ${config_file}\" EXIT" - - tmux send-keys -t "${tmux_session}:$((tmux_window - 1))" \ - "BROWSER=none PORT=$port JSON_API_URL=$JSON_API_URL VITE_SPLICE_CONFIG=\"\$(cat $config_file)\" \ - npm start 2>&1 | tee -a $log_file" C-m + "trap \"rm -f ${config_file}\" EXIT && \ + BROWSER=none PORT=$port JSON_API_URL=$JSON_API_URL VITE_SPLICE_CONFIG=\"\$(cat $config_file)\" \ + npm start 2>&1 | tee -a $log_file" } function start_test() { diff --git a/test-full-class-names-canton-enterprise.log b/test-full-class-names-canton-enterprise.log deleted file mode 100644 index 29ac689ce0..0000000000 --- a/test-full-class-names-canton-enterprise.log +++ /dev/null @@ -1 +0,0 @@ -org.lfdecentralizedtrust.splice.integration.tests.ParticipantKmsIdentitiesEnterpriseIntegrationTest diff --git a/test-full-class-names-docker-no-canton.log b/test-full-class-names-docker-no-canton.log index 4e8668c1bd..68b85d6602 100644 --- a/test-full-class-names-docker-no-canton.log +++ b/test-full-class-names-docker-no-canton.log @@ -1,4 +1,5 @@ org.lfdecentralizedtrust.splice.integration.tests.LocalNetFrontendIntegrationTest +org.lfdecentralizedtrust.splice.integration.tests.LocalNetReassignIntegrationTest org.lfdecentralizedtrust.splice.scan.store.bulk.AcsSnapshotBulkStorageCommitFromStagingTest org.lfdecentralizedtrust.splice.scan.store.bulk.AcsSnapshotBulkStorageWriterFromDbTest org.lfdecentralizedtrust.splice.scan.store.bulk.BulkStorageCommitFromStagingTest diff --git a/test-full-class-names-non-integration.log b/test-full-class-names-non-integration.log index 6e5afedcd2..a52b083e47 100644 --- a/test-full-class-names-non-integration.log +++ b/test-full-class-names-non-integration.log @@ -11,9 +11,12 @@ org.lfdecentralizedtrust.splice.environment.ActiveContractsRestartTest org.lfdecentralizedtrust.splice.environment.CommandCircuitBreakerTest org.lfdecentralizedtrust.splice.environment.CommandIdDedupTest org.lfdecentralizedtrust.splice.environment.TopologyAwarePackageVersionSupportTest +org.lfdecentralizedtrust.splice.http.HttpRateLimiterTest +org.lfdecentralizedtrust.splice.http.InvalidResponseContentTest org.lfdecentralizedtrust.splice.http.NonProxyHostsTest org.lfdecentralizedtrust.splice.http.UrlValidatorTest org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnectionTest +org.lfdecentralizedtrust.splice.scan.admin.api.client.SingleScanConnectionTest org.lfdecentralizedtrust.splice.scan.admin.http.GeneratedScanRouteDropNullsTest org.lfdecentralizedtrust.splice.scan.admin.http.ScanHttpEncodingsTest org.lfdecentralizedtrust.splice.scan.admin.http.UpdateHistoryOmitNullStringComplianceTest @@ -25,9 +28,11 @@ org.lfdecentralizedtrust.splice.scan.rewards.AppActivityComputationTest org.lfdecentralizedtrust.splice.scan.rewards.RewardComputationInputsTest org.lfdecentralizedtrust.splice.scan.store.DbAppActivityRecordStoreTest org.lfdecentralizedtrust.splice.scan.store.DbScanAppRewardsStoreTest +org.lfdecentralizedtrust.splice.scan.store.QueryAcsSnapshotPaginationTokenTest org.lfdecentralizedtrust.splice.scan.store.ScanEventStoreTest org.lfdecentralizedtrust.splice.scan.store.bulk.ZstdTest org.lfdecentralizedtrust.splice.scan.store.db.CheckMetaVersionsTest +org.lfdecentralizedtrust.splice.store.DbUnavailablePartiesStoreTest org.lfdecentralizedtrust.splice.store.DomainTimeStoreTest org.lfdecentralizedtrust.splice.store.InMemorySynchronizerStoreTest org.lfdecentralizedtrust.splice.store.KeyValueStoreTest @@ -36,6 +41,7 @@ org.lfdecentralizedtrust.splice.store.TxLogBackfillingStoreTest org.lfdecentralizedtrust.splice.store.UpdateHistoryBackfillingTest org.lfdecentralizedtrust.splice.store.UpdateHistoryTest org.lfdecentralizedtrust.splice.store.db.AcsSnapshotStoreTest +org.lfdecentralizedtrust.splice.store.db.AdvisoryLocksTest org.lfdecentralizedtrust.splice.store.db.DbExternalPartyWalletStoreTest org.lfdecentralizedtrust.splice.store.db.DbMultiDomainAcsStoreTest org.lfdecentralizedtrust.splice.store.db.DbScanRewardsReferenceStoreTest @@ -45,6 +51,7 @@ org.lfdecentralizedtrust.splice.store.db.DbSvSvStoreTest org.lfdecentralizedtrust.splice.store.db.DbTcsStoreTest org.lfdecentralizedtrust.splice.store.db.DbUserWalletStoreTest org.lfdecentralizedtrust.splice.store.db.SpliceStorageMultiLockTest +org.lfdecentralizedtrust.splice.sv.automation.VoteRequestMetricsTriggerTest org.lfdecentralizedtrust.splice.sv.cometbft.CometBftNodeTest org.lfdecentralizedtrust.splice.sv.cometbft.CometBftRequestSignerTest org.lfdecentralizedtrust.splice.sv.onboarding.SequencerBftPeerReconcilerSpec diff --git a/test-full-class-names.log b/test-full-class-names.log index 759f3556ed..38f35e559a 100644 --- a/test-full-class-names.log +++ b/test-full-class-names.log @@ -1,7 +1,6 @@ org.lfdecentralizedtrust.splice.integration.tests.AmuletAllocationsIntegrationTest -org.lfdecentralizedtrust.splice.integration.tests.AmuletBasedExpiryWithIgnoredPackageIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.AmuletExpiryIntegrationTest -org.lfdecentralizedtrust.splice.integration.tests.AmuletExpiryWithMinimalPackageIntegrationTest +org.lfdecentralizedtrust.splice.integration.tests.AmuletExpiryV1FallbackIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.Ans4SvsIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.AnsIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.AutoIgnoreUnresponsivePartiesIntegrationTest @@ -20,6 +19,8 @@ org.lfdecentralizedtrust.splice.integration.tests.DevelopmentFundCouponIntegrati org.lfdecentralizedtrust.splice.integration.tests.DirectoryPeriodicBackupIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.DistributedDomainIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.DowngradeSvPackagesIntegrationTest +org.lfdecentralizedtrust.splice.integration.tests.ExpiryWithIgnoredAmuletVersionIntegrationTest +org.lfdecentralizedtrust.splice.integration.tests.ExpiryWithNoVettedAmuletVersionIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.ExternalPartySetupProposalIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.ExternallySignedPartyOnboardingTest org.lfdecentralizedtrust.splice.integration.tests.FeaturedAppActivityMarkerIntegrationTest @@ -27,6 +28,7 @@ org.lfdecentralizedtrust.splice.integration.tests.GcpBucketPeriodicBackupIntegra org.lfdecentralizedtrust.splice.integration.tests.MemberTrafficIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.MultiHostValidatorOperatorIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.PackageWithDependencyIntegrationTest +org.lfdecentralizedtrust.splice.integration.tests.ParticipantKmsIdentitiesIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.ParticipantPlaintextIdentitiesIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.PeriodicTopologySnapshotIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.RecoverExternalPartyIntegrationTest diff --git a/token-standard/cli/src/txparse/parserv2.ts b/token-standard/cli/src/txparse/parserv2.ts index e05539ac83..04820bf3f8 100644 --- a/token-standard/cli/src/txparse/parserv2.ts +++ b/token-standard/cli/src/txparse/parserv2.ts @@ -166,10 +166,16 @@ export class V2TransactionParser { return null; } - const result = holdingViewToResult( - createdEvent.contractId, - Holding.decoder.runWithException(holdingView.viewValue), - ); + let decodedPayload: Holding; + try { + decodedPayload = Holding.decoder.runWithException(holdingView.viewValue); + } catch (err) { + console.error( + `Failed to decode Holding. View: ${JSON.stringify(holdingView)}. Error: ${JSON.stringify(err)}`, + ); + throw err; + } + const result = holdingViewToResult(createdEvent.contractId, decodedPayload); return { holding: result,